import numpy as np import pandas as pd import requests from sklearn.cluster import KMeans from dashboard_website import db host = "http://acetyl.net:5000" # queries acetyl.net:5000, the OSRM engine # external facing functions # gets single leg route between bike and clue # should be HD and GeoJSON def getRouteFullJSON(bike, clue): url = f"{host}/route/v1/bike/{bike['longitude']},{bike['latitude']};{clue['longitude']},{clue['latitude']}?steps=true&overview=full&geometries=geojson" r = response.get(url) return r.json() def getRouteHDPolyline(bike, clue): 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'] return p def getRouteFastPolyline(bike, clue): 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'] return p def getZSP(bike, home, clue_cluster): pass # determines clusters based on current bikes and clues def getClusters(bikes, clues, endpoint): clusters = [[] for bike in bikes] active_bikes = [bike for bike in bikes if bike.status == "ACTIVE"] active_clues = [clue for clue in clues if clue.status == "UNVISITED"] # select only active bikes # select only unvisited clues clusters_t, route_geo = cluster_and_optimize(active_clues, active_bikes, endpoint) for cluster in clusters_t: clusters[i] = cluster # return list of clue clusters corresponding to bikes pass # utility functions (internal) def cluster_and_optimize(clues: [db.Clue], bikes: [db.Bike], end: db.Point, time_diff=0.25, max_time=24, n=2): """ Takes a dataframe of gps coordinates, a list of centroids, and an end point and returns a dataframe with a cluster :param clues: a list of clues :param bikes: a list of centroids :param end: the end point of the trip :param time_diff: the maximum time difference between the longest trip and the average trip :param max_time: the maximum time of the trip :param n: the number of routes to create :return: a dataframe with a cluster column """ regular_points = [] for clue in clues: regular_points.append(db.Point(clue.latitude, clue.longitude)) # Create a new column with normalized gps coordinates and centroids normalized_points, norm_centroids = __normalize_points([db.Point(clue.latitude, clue.longitude) for clue in clues], [db.Point(bike.latitude, bike.longitude) for bike in bikes]) # Cluster the coordinates kmeans = KMeans(n_clusters=len(norm_centroids), init=norm_centroids) kmeans.fit(normalized_points) # Create a dataframe with the gps coordinates, the cluster, and the list of coordinates df = pd.DataFrame({'gps': regular_points, 'cluster': kmeans.labels_}) # Create the coordinate list from the bikes (the centroids) and the routes (the clues) routes = [] for i in range(len(bikes)): routes.append(df[df['cluster'] == i]['gps'].tolist()) 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) df.loc[df['gps'].astype(str).isin(map(str, routes[i])), 'cluster'] = i def __points_to_string(point: [db.Point]): """ Takes a list of points and returns a string of the list of points :param point: a list of points :return: a string of the list of points """ string = '' for i in point: string += str(i.longitude) + ',' + str(i.latitude) + ';' return string def __get_trip_time(coordinate_string, num_waypoints, start, end, time_per_waypoint=90): """ Takes a string of coordinates and returns the trip time in hours :param coordinate_string: a string of coordinates :param num_waypoints: the number of waypoints :param start: the start point of the trip :param end: the end point of the trip :param time_per_waypoint: the time per waypoint in seconds :return: the trip time in hours """ coordinates = requests.get( 'http://acetyl.net:5000/trip/v1/bike/' + start + coordinate_string + end + '?roundtrip=false&source=first&destination=last') coordinates = coordinates.json() travel_time_seconds = int(coordinates['trips'][0]['duration']) waypoint_time_seconds = num_waypoints * time_per_waypoint total_time_hours = (travel_time_seconds + waypoint_time_seconds) / 3600 return total_time_hours def __minimize_route_time_diff(routes: [db.Point], starts: [db.Point], end: db.Point, time_diff, n): """ Takes a list of lists of coordinates, a list of start points, an end point, a time difference, and a number of routes :param routes: the list of lists of coordinates :param starts: the list of start points :param end: the end point :param time_diff: the time difference :param n: the number of routes :return: a list of lists of coordinates """ times = [] for i, route in enumerate(routes): times.append(__get_trip_time(__points_to_string(route), len(route), __points_to_string([starts[i]]), __points_to_string([end]))) # Find the average trip time average_time = np.mean(times) # Sort the trip times sorted_indices = np.argsort(times) # Find the difference between the longest trip time and the average trip time time_difference = times[sorted_indices[-1]] - average_time # If the difference is greater than the time difference, move a coordinate from the longest route to the shortest route if time_difference > time_diff: # Move a coordinate from the longest route to the shortest route 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) # Recursively minimize the time difference between the routes return __minimize_route_time_diff(routes, starts, end, time_diff, n) # 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: [db.Point], start: db.Point, end: db.Point, max_time): """ Takes a list of coordinates, a start point, an end point, and a maximum time and returns a list of coordinates :param route_coordinates: the list of coordinates :param start: the start point :param end: the end point :param max_time: the maximum time :return: a list of coordinates """ # Find the trip time for the route route_time = __get_trip_time(__points_to_string(route_coordinates), len(route_coordinates), __points_to_string([start]), __points_to_string([end])) # 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) return __remove_longest_waypoints(route_coordinates, start, end, max_time) return route_coordinates def __normalize_points(coordinates: [db.Point], centroids: [db.Point]): """ Takes a list of coordinates and a list of centroids and returns a list of normalized coordinates and a list of normalized centroids :param coordinates: the list of coordinates :param centroids: the list of centroids :return: the list of normalized coordinates and the list of normalized centroids """ # Create a list of latitudes and longitudes latitudes = [i.latitude for i in coordinates] longitudes = [i.longitude for i in coordinates] # Find the minimum and maximum latitudes and longitudes min_lat = min(latitudes) max_lat = max(latitudes) min_lon = min(longitudes) max_lon = max(longitudes) # Normalize the coordinates and centroids using min-max normalization normalized_coordinates = [] normalized_centroids = [] for i in coordinates: normalized_coordinates.append( [__min_max_normalize(i[0], min_lat, max_lat), __min_max_normalize(i[1], min_lon, max_lon)]) for i in centroids: normalized_centroids.append( [__min_max_normalize(i[0], min_lat, max_lat), __min_max_normalize(i[1], min_lon, max_lon)]) return normalized_coordinates, normalized_centroids def __min_max_normalize(value, min_value, max_value): """ Takes a value, a minimum value, and a maximum value and returns the normalized value :param value: the value :param min_value: the minimum value :param max_value: the maximum value :return: the normalized value """ return (value - min_value) / (max_value - min_value) def __find_closest_coordinate(coordinates: [db.Point], centroid: db.Point): """ Takes a list of coordinates and a centroid and returns the coordinate in the list that is closest to the centroid :param coordinates: the list of coordinates :param centroid: the centroid :return: the coordinate in the list that is closest to the centroid """ closest_coordinate = coordinates[0] closest_coordinate_distance = __distance(closest_coordinate, centroid) for coordinate in coordinates: if __distance(coordinate, centroid) < closest_coordinate_distance: closest_coordinate = coordinate closest_coordinate_distance = __distance(coordinate, centroid) return closest_coordinate def __find_farthest_coordinate(coordinates: [db.Point], centroid: db.Point): """ Takes a list of coordinates and a centroid and returns the coordinate in the list that is farthest from the centroid :param coordinates: the list of coordinates :param centroid: the centroid :return: the coordinate in the list that is farthest from the centroid """ farthest_coordinate = coordinates[0] farthest_coordinate_distance = __distance(farthest_coordinate, centroid) for coordinate in coordinates: if __distance(coordinate, centroid) > farthest_coordinate_distance: farthest_coordinate = coordinate farthest_coordinate_distance = __distance(coordinate, centroid) return farthest_coordinate def __mean_center(coordinates: [db.Point]): """ Takes a list of coordinates and returns the mean center of the coordinates :param coordinates: the list of coordinates :return: the mean center of the coordinates """ return db.Point(np.mean([i.latitude for i in coordinates]), np.mean([i.longitude for i in coordinates])) def __distance(coordinate1: db.Point, coordinate2: db.Point): """ Takes two coordinates and returns the distance between them :param coordinate1: the first coordinate :param coordinate2: the second coordinate :return: the distance between the two coordinates """ return ((coordinate1.latitude - coordinate2.latitude) ** 2 + ( coordinate1.longitude - coordinate2.longitude) ** 2) ** 0.5