diff --git a/mosstool/map/_map_util/aoi_matcher.py b/mosstool/map/_map_util/aoi_matcher.py index 6c7e59c..d1808bd 100644 --- a/mosstool/map/_map_util/aoi_matcher.py +++ b/mosstool/map/_map_util/aoi_matcher.py @@ -11,14 +11,25 @@ import shapely.ops as ops from scipy.spatial import KDTree from shapely.affinity import scale -from shapely.geometry import (LineString, MultiLineString, MultiPoint, - MultiPolygon, Point, Polygon) +from shapely.geometry import ( + LineString, + MultiLineString, + MultiPoint, + MultiPolygon, + Point, + Polygon, +) from shapely.strtree import STRtree from ...type import AoiType from .._util.angle import abs_delta_angle, delta_angle -from .._util.line import (connect_line_string, get_line_angle, - get_start_vector, line_extend, offset_lane) +from .._util.line import ( + connect_line_string, + get_line_angle, + get_start_vector, + line_extend, + offset_lane, +) from .aoiutils import geo_coords # ATTENTION: In order to achieve longer distance POI merging, the maximum recursion depth needs to be modified. @@ -830,7 +841,8 @@ def _add_aoi_stop_unit(arg): w_matched = _str_tree_matcher_unit( geo, w_matcher, w_tree, W_DIS_GATE, W_HUGE_GATE ) - if d_matched and w_matched: # The station must connect both roadways and sidewalks + # if d_matched and w_matched: # The station must connect both roadways and sidewalks + if d_matched: # The station must connect roadways base_aoi = { "id": 0, # It is difficult to deal with the problem of uid += 1 during parallelization, so assign the id after parallelization is completed. "type": aoi_type, diff --git a/mosstool/map/_map_util/const.py b/mosstool/map/_map_util/const.py index 2a5238a..b95f4c7 100644 --- a/mosstool/map/_map_util/const.py +++ b/mosstool/map/_map_util/const.py @@ -81,6 +81,7 @@ STATION_CAPACITY = { "BUS": 35, "SUBWAY": 850, + "UNSPECIFIED": 100, } DEFAULT_SCHEDULES = { "BUS": [t for t in range(int(5.5 * 3600), int(22.5 * 3600), int(5 * 60))], diff --git a/mosstool/map/_map_util/junctionutils.py b/mosstool/map/_map_util/junctionutils.py new file mode 100644 index 0000000..b1bb614 --- /dev/null +++ b/mosstool/map/_map_util/junctionutils.py @@ -0,0 +1,208 @@ +import logging +from collections import Counter, defaultdict +from copy import deepcopy +from math import atan2 +from typing import (Callable, Dict, List, Literal, Optional, Set, Tuple, Union, + cast) + +import numpy as np +import pycityproto.city.map.v2.map_pb2 as mapv2 +from shapely.geometry import LineString, MultiPoint, Point + +from .._map_util.const import * +from .._util.angle import abs_delta_angle, delta_angle +from .._util.line import get_line_angle, get_start_vector + + +def add_overlaps(output_junctions: dict, output_lanes: dict) -> None: + for _, junc in output_junctions.items(): + junc_poly_lanes = [ + (l, LineString([[n["x"], n["y"]] for n in l["center_line"]["nodes"]])) + for l in (output_lanes[lid] for lid in junc["lane_ids"]) + ] + for ii, (lane_i, poly_i) in enumerate(junc_poly_lanes): + angle_i = get_line_angle(poly_i) + start_vec_i = get_start_vector(poly_i) + for lane_j, poly_j in junc_poly_lanes[ii + 1 :]: + if lane_i["predecessors"][0] == lane_j["predecessors"][0]: + continue + intersections = poly_i.intersection(poly_j) + if isinstance(intersections, MultiPoint): + intersections = [p for p in intersections.geoms] + elif isinstance(intersections, Point): + intersections = [intersections] + else: + continue + # Priority rules: + # 0. yield to crosswalks + # 1. yield to road on the right + # 2. turning roads yield to non-turning road + # 3. right turns yields to left turns on opposite direction + self_first = True + both_false = False + angle_j = get_line_angle(poly_j) + start_vec_j = get_start_vector(poly_j) + abs_delta_angle_i_j = abs_delta_angle(angle_i, angle_j) + while True: + if lane_i["turn"] == mapv2.LANE_TURN_STRAIGHT: + if lane_j["turn"] == mapv2.LANE_TURN_STRAIGHT: + if ( + abs_delta_angle(abs_delta_angle_i_j, np.pi / 2) + < np.pi / 4 # Close to 90 degrees + ): + if ( + np.cross(start_vec_i, start_vec_j) < 0 + ): # i is about 90 degrees to the right of j + # ⬆ + # ⬆ j + # ⬆ + # ➡➡➡➡i + # ⬆ + self_first = False + else: + both_false = True + break + if lane_j["turn"] == mapv2.LANE_TURN_STRAIGHT: + self_first = False + break + if abs_delta_angle_i_j > np.pi * 5 / 6: + if ( + lane_i["turn"] == mapv2.LANE_TURN_RIGHT + and lane_j["turn"] == mapv2.LANE_TURN_LEFT + ): + self_first = False + break + if ( + lane_j["turn"] == mapv2.LANE_TURN_RIGHT + and lane_i["turn"] == mapv2.LANE_TURN_LEFT + ): + break + # default + both_false = True + break + if both_false: + self_first = other_first = False + else: + other_first = not self_first + lane_i["overlaps"].extend( + { + "self": {"lane_id": lane_i["id"], "s": poly_i.project(p)}, + "other": {"lane_id": lane_j["id"], "s": poly_j.project(p)}, + "self_first": self_first, + } + for p in intersections + ) + lane_j["overlaps"].extend( + { + "other": {"lane_id": lane_i["id"], "s": poly_i.project(p)}, + "self": {"lane_id": lane_j["id"], "s": poly_j.project(p)}, + "self_first": other_first, + } + for p in intersections + ) + + +def add_driving_groups( + output_junctions: dict, output_lanes: dict, output_roads: dict +) -> None: + # Shallow copy + lanes = {lid: d for lid, d in output_lanes.items()} + roads = {rid: d for rid, d in output_roads.items()} + juncs = {jid: d for jid, d in output_junctions.items()} + + # Mapping from road id to angle + road_start_angle = {} + road_end_angle = {} + for road in roads.values(): + l = lanes[road["lane_ids"][len(road["lane_ids"]) // 2]] + nodes = l["center_line"]["nodes"] + start_angle = atan2( + nodes[1]["y"] - nodes[0]["y"], nodes[1]["x"] - nodes[0]["x"] + ) + end_angle = atan2( + nodes[-1]["y"] - nodes[-2]["y"], nodes[-1]["x"] - nodes[-2]["x"] + ) + road_start_angle[road["id"]] = start_angle + road_end_angle[road["id"]] = end_angle + # Main logic + for jid, j in juncs.items(): + # (in road id, out road id) -> [lane id] + lane_groups = defaultdict(list) + for lid in j["lane_ids"]: + l = lanes[lid] + if l["type"] != mapv2.LANE_TYPE_DRIVING: # driving + # Only process traffic lanes + continue + pres = l["predecessors"] + assert ( + len(pres) == 1 + ), f"Lane {lid} at junction {jid} has multiple predecessors!" + pre_lane_id = pres[0]["id"] + pre_lane = lanes[pre_lane_id] + sucs = l["successors"] + assert ( + len(sucs) == 1 + ), f"Lane {lid} at junction {jid} has multiple successors!" + suc_lane_id = sucs[0]["id"] + suc_lane = lanes[suc_lane_id] + pre_parent_id, suc_parent_id = ( + pre_lane["parent_id"], + suc_lane["parent_id"], + ) + if pre_parent_id >= JUNC_START_ID: + logging.warning( + f"Junction lane {lid} has predecessor lane {pre_lane_id} in junction {pre_parent_id}, all predecessors and successors of a junction lane should be road lanes." + ) + continue + if suc_parent_id >= JUNC_START_ID: + logging.warning( + f"Junction lane {lid} has successor lane {suc_lane_id} in junction {suc_parent_id}, all predecessors and successors of a junction lane should be road lanes." + ) + continue + key = (pre_parent_id, suc_parent_id) + lane_groups[key].append(lid) + # Write into junction + # message JunctionLaneGroup { + # # The entrance road to this lane group + # int32 in_road_id = 1; + # # Entrance angle of this lane group (radians) + # double in_angle = 2; + # # The exit road of this lane group + # int32 out_road_id = 3; + # # The exit angle of this lane group (radians) + # double out_angle = 4; + # # Lanes included in this lane group + # repeated int32 lane_ids = 5; + # # The steering attributes of this lane group + # LaneTurn turn = 6; + # } + j["driving_lane_groups"] = [ + { + "in_road_id": k[0], + "in_angle": road_end_angle[k[0]], + "out_road_id": k[1], + "out_angle": road_start_angle[k[1]], + "lane_ids": v, + } + for k, v in lane_groups.items() + ] + # Check whether the turns of all lanes in each group are the same, and set the turn of the lane group + for group in j["driving_lane_groups"]: + lane_ids = group["lane_ids"] + turns = [lanes[lid]["turn"] for lid in lane_ids] + # Turn the Around into a left turn + turns = [ + mapv2.LANE_TURN_LEFT if t == mapv2.LANE_TURN_AROUND else t + for t in turns + ] + # Check if all turns are the same + group_turn = turns[0] + if not all(t == group_turn for t in turns): + # Reset group turn as the most common turn + group_turn = Counter(turns).most_common(1)[0][0] + logging.warning( + f"Not all lane turns are the same at junction {jid} driving group, reset all lane turn to {group_turn}" + ) + for lid in lane_ids: + lanes[lid]["turn"] = group_turn + group["turn"] = group_turn diff --git a/mosstool/map/builder/builder.py b/mosstool/map/builder/builder.py index 9c16e08..91a390e 100644 --- a/mosstool/map/builder/builder.py +++ b/mosstool/map/builder/builder.py @@ -27,6 +27,7 @@ from .._map_util.convert_aoi import convert_aoi, convert_poi from .._map_util.format_checker import geojson_format_check, output_format_check from .._map_util.gen_traffic_light import generate_traffic_light +from .._map_util.junctionutils import add_driving_groups, add_overlaps from .._map_util.map_aois_matchers import match_map_aois from .._util.angle import abs_delta_angle, delta_angle from .._util.line import ( @@ -3592,189 +3593,11 @@ def _add_junc_lane_overlaps(self): """ junc lanesadd overlap """ - for _, junc in self.output_junctions.items(): - junc_poly_lanes = [ - (l, LineString([[n["x"], n["y"]] for n in l["center_line"]["nodes"]])) - for l in (self.output_lanes[lid] for lid in junc["lane_ids"]) - ] - for ii, (lane_i, poly_i) in enumerate(junc_poly_lanes): - angle_i = get_line_angle(poly_i) - start_vec_i = get_start_vector(poly_i) - for lane_j, poly_j in junc_poly_lanes[ii + 1 :]: - if lane_i["predecessors"][0] == lane_j["predecessors"][0]: - continue - intersections = poly_i.intersection(poly_j) - if isinstance(intersections, MultiPoint): - intersections = [p for p in intersections.geoms] - elif isinstance(intersections, Point): - intersections = [intersections] - else: - continue - # Priority rules: - # 0. yield to crosswalks - # 1. yield to road on the right - # 2. turning roads yield to non-turning road - # 3. right turns yields to left turns on opposite direction - self_first = True - both_false = False - angle_j = get_line_angle(poly_j) - start_vec_j = get_start_vector(poly_j) - abs_delta_angle_i_j = abs_delta_angle(angle_i, angle_j) - while True: - if lane_i["turn"] == mapv2.LANE_TURN_STRAIGHT: - if lane_j["turn"] == mapv2.LANE_TURN_STRAIGHT: - if ( - abs_delta_angle(abs_delta_angle_i_j, np.pi / 2) - < np.pi / 4 # Close to 90 degrees - ): - if ( - np.cross(start_vec_i, start_vec_j) < 0 - ): # i is about 90 degrees to the right of j - # ⬆ - # ⬆ j - # ⬆ - # ➡➡➡➡i - # ⬆ - self_first = False - else: - both_false = True - break - if lane_j["turn"] == mapv2.LANE_TURN_STRAIGHT: - self_first = False - break - if abs_delta_angle_i_j > np.pi * 5 / 6: - if ( - lane_i["turn"] == mapv2.LANE_TURN_RIGHT - and lane_j["turn"] == mapv2.LANE_TURN_LEFT - ): - self_first = False - break - if ( - lane_j["turn"] == mapv2.LANE_TURN_RIGHT - and lane_i["turn"] == mapv2.LANE_TURN_LEFT - ): - break - # default - both_false = True - break - if both_false: - self_first = other_first = False - else: - other_first = not self_first - lane_i["overlaps"].extend( - { - "self": {"lane_id": lane_i["id"], "s": poly_i.project(p)}, - "other": {"lane_id": lane_j["id"], "s": poly_j.project(p)}, - "self_first": self_first, - } - for p in intersections - ) - lane_j["overlaps"].extend( - { - "other": {"lane_id": lane_i["id"], "s": poly_i.project(p)}, - "self": {"lane_id": lane_j["id"], "s": poly_j.project(p)}, - "self_first": other_first, - } - for p in intersections - ) + add_overlaps(self.output_junctions, self.output_lanes) def _add_driving_lane_group(self): """Clustering lane groups from the lanes at the junction""" - # Shallow copy - lanes = {lid: d for lid, d in self.output_lanes.items()} - roads = {rid: d for rid, d in self.output_roads.items()} - juncs = {jid: d for jid, d in self.output_junctions.items()} - - # Mapping from road id to angle - road_start_angle = {} - road_end_angle = {} - for road in roads.values(): - l = lanes[road["lane_ids"][len(road["lane_ids"]) // 2]] - nodes = l["center_line"]["nodes"] - start_angle = atan2( - nodes[1]["y"] - nodes[0]["y"], nodes[1]["x"] - nodes[0]["x"] - ) - end_angle = atan2( - nodes[-1]["y"] - nodes[-2]["y"], nodes[-1]["x"] - nodes[-2]["x"] - ) - road_start_angle[road["id"]] = start_angle - road_end_angle[road["id"]] = end_angle - # Main logic - for jid, j in juncs.items(): - # (in road id, out road id) -> [lane id] - lane_groups = defaultdict(list) - for lid in j["lane_ids"]: - l = lanes[lid] - if l["type"] != mapv2.LANE_TYPE_DRIVING: # driving - # Only process traffic lanes - continue - pres = l["predecessors"] - assert ( - len(pres) == 1 - ), f"Lane {lid} at junction {jid} has multiple predecessors!" - pre_lane_id = pres[0]["id"] - pre_lane = lanes[pre_lane_id] - sucs = l["successors"] - assert ( - len(sucs) == 1 - ), f"Lane {lid} at junction {jid} has multiple successors!" - suc_lane_id = sucs[0]["id"] - suc_lane = lanes[suc_lane_id] - pre_parent_id, suc_parent_id = ( - pre_lane["parent_id"], - suc_lane["parent_id"], - ) - if pre_parent_id >= JUNC_START_ID: - logging.warning( - f"Junction lane {lid} has predecessor lane {pre_lane_id} in junction {pre_parent_id}, all predecessors and successors of a junction lane should be road lanes." - ) - continue - if suc_parent_id >= JUNC_START_ID: - logging.warning( - f"Junction lane {lid} has successor lane {suc_lane_id} in junction {suc_parent_id}, all predecessors and successors of a junction lane should be road lanes." - ) - continue - key = (pre_parent_id, suc_parent_id) - lane_groups[key].append(lid) - # Write into junction - # message JunctionLaneGroup { - # # The entrance road to this lane group - # int32 in_road_id = 1; - # # Entrance angle of this lane group (radians) - # double in_angle = 2; - # # The exit road of this lane group - # int32 out_road_id = 3; - # # The exit angle of this lane group (radians) - # double out_angle = 4; - # # Lanes included in this lane group - # repeated int32 lane_ids = 5; - # # The steering attributes of this lane group - # LaneTurn turn = 6; - # } - j["driving_lane_groups"] = [ - { - "in_road_id": k[0], - "in_angle": road_end_angle[k[0]], - "out_road_id": k[1], - "out_angle": road_start_angle[k[1]], - "lane_ids": v, - } - for k, v in lane_groups.items() - ] - # Check whether the turns of all lanes in each group are the same, and set the turn of the lane group - for group in j["driving_lane_groups"]: - lane_ids = group["lane_ids"] - turns = [lanes[lid]["turn"] for lid in lane_ids] - # Turn the Around into a left turn - turns = [ - mapv2.LANE_TURN_LEFT if t == mapv2.LANE_TURN_AROUND else t - for t in turns - ] - # Check if all turns are the same - assert all( - t == turns[0] for t in turns - ), f"Not all lane turns are the same at junction {jid} driving group" - group["turn"] = turns[0] + add_driving_groups(self.output_junctions, self.output_lanes, self.output_roads) def _add_traffic_light(self): lanes = {lid: d for lid, d in self.output_lanes.items()} diff --git a/mosstool/map/public_transport/public_transport_post.py b/mosstool/map/public_transport/public_transport_post.py index 8b2b3ab..ed45067 100644 --- a/mosstool/map/public_transport/public_transport_post.py +++ b/mosstool/map/public_transport/public_transport_post.py @@ -5,9 +5,8 @@ from typing import Optional import numpy as np -from pycityproto.city.routing.v2.routing_service_pb2 import GetRouteRequest import pycityproto.city.map.v2.map_pb2 as mapv2 - +from pycityproto.city.routing.v2.routing_service_pb2 import GetRouteRequest from tqdm import tqdm from mosstool.trip.route import RoutingClient @@ -33,7 +32,7 @@ async def _fill_public_lines(m: dict, server_address: str): x, y = np.mean(coords, axis=0)[:2] aoi_center_point[aoi_id] = (x, y) - def get_subline_type(pub_type: str): + def _get_subline_type(pub_type: str): if pub_type == "SUBWAY": return mapv2.SUBLINE_TYPE_SUBWAY elif pub_type == "BUS": @@ -41,7 +40,7 @@ def get_subline_type(pub_type: str): else: return mapv2.SUBLINE_TYPE_UNSPECIFIED - def get_aoi_dis(aoi_id1, aoi_id2): + def _get_aoi_dis(aoi_id1, aoi_id2): x1, y1 = aoi_center_point[aoi_id1] x2, y2 = aoi_center_point[aoi_id2] return ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5 @@ -120,7 +119,7 @@ def route_length( ) if len( road_ids - ) >= 1 and route_len < 1.6 * get_aoi_dis( + ) >= 1 and route_len < 1.6 * _get_aoi_dis( aoi_start, aoi_end ): eta = res.journeys[0].driving.eta @@ -183,12 +182,13 @@ def route_length( "station_connection_road_ids": subline[ "station_connection_road_ids" ], - "type": get_subline_type(pub["type"]), + "type": _get_subline_type(pub["type"]), "parent_name": subline["parent_name"], "schedules": { "departure_times": departure_times, "offset_times": offset_times, }, + "capacity":STATION_CAPACITY[pub["type"]], "taz_costs": [], } ) @@ -242,7 +242,7 @@ def route_length( external[ex_key] = [ d for i, d in enumerate(external[ex_key]) if i in d_idxs ] - # clear lane的aoi ids + # clear lane.aoi_ids for _, lane in lanes.items(): lane["aoi_ids"] = [] # add aoi id @@ -327,7 +327,7 @@ def _post_compute(m: dict, workers: int): sublines_data = m["sublines"] taz_cost_args = [] - def station_distance(road_ids, s_start: float, s_end: float) -> float: + def _station_distance(road_ids, s_start: float, s_end: float) -> float: res = 0 for road_id in road_ids[:-1]: road = roads[road_id] @@ -359,7 +359,7 @@ def station_distance(road_ids, s_start: float, s_end: float) -> float: if lane["parent_id"] == road_ids[-1]: s_end = d["s"] break - route_lengths.append(station_distance(road_ids, s_start, s_end)) + route_lengths.append(_station_distance(road_ids, s_start, s_end)) arg = (subline, station_aois, (x_min, x_step, y_min, y_step), route_lengths) taz_cost_args.append(arg) @@ -373,14 +373,14 @@ def station_distance(road_ids, s_start: float, s_end: float) -> float: for subline in sublines_data: subline_id = subline["id"] subline["taz_costs"] = subline_id2taz_costs[subline_id] - for sl in sublines_data: - if not sl["type"] == mapv2.SUBLINE_TYPE_SUBWAY: - continue - transfer_stations = [] - for aid in sl["aoi_ids"]: - station = aois[aid] - if len(station["subline_ids"]) > 1: - transfer_stations.append(station) + # for sl in sublines_data: + # if not sl["type"] == mapv2.SUBLINE_TYPE_SUBWAY: + # continue + # transfer_stations = [] + # for aid in sl["aoi_ids"]: + # station = aois[aid] + # if len(station["subline_ids"]) > 1: + # transfer_stations.append(station) return m diff --git a/mosstool/map/sumo/map.py b/mosstool/map/sumo/map.py index a8fdb32..3ea45c5 100644 --- a/mosstool/map/sumo/map.py +++ b/mosstool/map/sumo/map.py @@ -1,8 +1,8 @@ import logging -from multiprocessing import cpu_count import time from collections import defaultdict from math import atan2 +from multiprocessing import cpu_count from typing import Optional from xml.dom.minidom import parse @@ -16,12 +16,9 @@ from .._map_util.const import * from .._map_util.convert_aoi import convert_sumo_aoi_poi, convert_sumo_stops from .._map_util.gen_traffic_light import convert_traffic_light +from .._map_util.junctionutils import add_driving_groups, add_overlaps from .._util.angle import abs_delta_angle -from .._util.line import ( - connect_line_string, - get_line_angle, - get_start_vector, -) +from .._util.line import connect_line_string, get_line_angle, get_start_vector __all__ = ["MapConverter"] @@ -434,7 +431,7 @@ def _add_lane_conn(self): else: from_uid = self._id2uid[from_lid] from_lane = self._map_lanes[from_uid] - if from_lane["parent_id"] >= 3_0000_0000: + if from_lane["parent_id"] >= JUNC_START_ID: continue for to_lid in to_lids: if not to_lid in self._id2uid.keys(): @@ -443,7 +440,7 @@ def _add_lane_conn(self): from_lane = self._map_lanes[from_uid] to_uid = self._id2uid[to_lid] to_lane = self._map_lanes[to_uid] - if to_lane["parent_id"] >= 3_0000_0000: + if to_lane["parent_id"] >= JUNC_START_ID: continue from_lane["out"].append( {"id": to_uid, "type": mapv2.LANE_CONNECTION_TYPE_HEAD} @@ -456,167 +453,13 @@ def _add_junc_lane_overlaps(self): """ add overlap for junction lanes """ - for _, junc in self._output_junctions.items(): - junc_poly_lanes = [ - (l, LineString([[n["x"], n["y"]] for n in l["center_line"]["nodes"]])) - for l in (self._output_lanes[lid] for lid in junc["lane_ids"]) - ] - for ii, (lane_i, poly_i) in enumerate(junc_poly_lanes): - angle_i = get_line_angle(poly_i) - start_vec_i = get_start_vector(poly_i) - for lane_j, poly_j in junc_poly_lanes[ii + 1 :]: - if lane_i["predecessors"][0] == lane_j["predecessors"][0]: - continue - intersections = poly_i.intersection(poly_j) - if isinstance(intersections, MultiPoint): - intersections = [p for p in intersections.geoms] - elif isinstance(intersections, Point): - intersections = [intersections] - else: - continue - # Priority rules: - # 0. yield to crosswalks - # 1. yield to road on the right - # 2. turning roads yield to non-turning road - # 3. right turns yields to left turns on opposite direction - self_first = True - both_false = False - angle_j = get_line_angle(poly_j) - start_vec_j = get_start_vector(poly_j) - abs_delta_angle_i_j = abs_delta_angle(angle_i, angle_j) - while True: - if lane_i["turn"] == mapv2.LANE_TURN_STRAIGHT: - if lane_j["turn"] == mapv2.LANE_TURN_STRAIGHT: - if ( - abs_delta_angle(abs_delta_angle_i_j, np.pi / 2) - < np.pi / 4 # Close to 90 degree - ): - if ( - np.cross(start_vec_i, start_vec_j) < 0 - ): # i is about 90 degrees to the right of j - # ⬆ - # ⬆ j - # ⬆ - # ➡➡➡➡ i - # ⬆ - self_first = False - else: - both_false = True - break - if lane_j["turn"] == mapv2.LANE_TURN_STRAIGHT: - self_first = False - break - if abs_delta_angle_i_j > np.pi * 5 / 6: - if ( - lane_i["turn"] == mapv2.LANE_TURN_RIGHT - and lane_j["turn"] == mapv2.LANE_TURN_LEFT - ): - self_first = False - break - if ( - lane_j["turn"] == mapv2.LANE_TURN_RIGHT - and lane_i["turn"] == mapv2.LANE_TURN_LEFT - ): - break - # default - both_false = True - break - if both_false: - self_first = other_first = False - else: - other_first = not self_first - lane_i["overlaps"].extend( - { - "self": {"lane_id": lane_i["id"], "s": poly_i.project(p)}, - "other": {"lane_id": lane_j["id"], "s": poly_j.project(p)}, - "self_first": self_first, - } - for p in intersections - ) - lane_j["overlaps"].extend( - { - "other": {"lane_id": lane_i["id"], "s": poly_i.project(p)}, - "self": {"lane_id": lane_j["id"], "s": poly_j.project(p)}, - "self_first": other_first, - } - for p in intersections - ) + add_overlaps(self._output_junctions, self._output_lanes) def _add_driving_lane_group(self): """Clustering lane groups from the lanes at the junction""" - # Shallow copy - lanes = {lid: d for lid, d in self._output_lanes.items()} - roads = {rid: d for rid, d in self._output_roads.items()} - juncs = {jid: d for jid, d in self._output_junctions.items()} - - # Mapping from road id to angle - road_start_angle = {} - road_end_angle = {} - for road in roads.values(): - l = lanes[road["lane_ids"][len(road["lane_ids"]) // 2]] - nodes = l["center_line"]["nodes"] - start_angle = atan2( - nodes[1]["y"] - nodes[0]["y"], nodes[1]["x"] - nodes[0]["x"] - ) - end_angle = atan2( - nodes[-1]["y"] - nodes[-2]["y"], nodes[-1]["x"] - nodes[-2]["x"] - ) - road_start_angle[road["id"]] = start_angle - road_end_angle[road["id"]] = end_angle - # Main logic - for j in juncs.values(): - # (in road id, out road id) -> [lane id] - lane_groups = defaultdict(list) - for lid in j["lane_ids"]: - l = lanes[lid] - if l["type"] != mapv2.LANE_TYPE_DRIVING: # driving - # Only process traffic lanes - continue - pres = l["predecessors"] - assert len(pres) == 1 - pre_lane = lanes[pres[0]["id"]] - sucs = l["successors"] - assert len(sucs) == 1 - suc_lane = lanes[sucs[0]["id"]] - key = (pre_lane["parent_id"], suc_lane["parent_id"]) - lane_groups[key].append(lid) - # Write into junction - # message JunctionLaneGroup { - # # The entrance road to this lane group - # int32 in_road_id = 1; - # # Entrance angle of this lane group (radians) - # double in_angle = 2; - # # The exit road of this lane group - # int32 out_road_id = 3; - # # The exit angle of this lane group (radians) - # double out_angle = 4; - # # Lanes included in this lane group - # repeated int32 lane_ids = 5; - # # The steering attributes of this lane group - # LaneTurn turn = 6; - # } - j["driving_lane_groups"] = [ - { - "in_road_id": k[0], - "in_angle": road_end_angle[k[0]], - "out_road_id": k[1], - "out_angle": road_start_angle[k[1]], - "lane_ids": v, - } - for k, v in lane_groups.items() - ] - # Check whether the turns of all lanes in each group are the same, and set the turn of the lane group - for group in j["driving_lane_groups"]: - lane_ids = group["lane_ids"] - turns = [lanes[lid]["turn"] for lid in lane_ids] - # Turn the U-turn into a left turn - turns = [ - mapv2.LANE_TURN_LEFT if t == mapv2.LANE_TURN_AROUND else t - for t in turns - ] - # Check if all turns are the same - assert all(t == turns[0] for t in turns) - group["turn"] = turns[0] + add_driving_groups( + self._output_junctions, self._output_lanes, self._output_roads + ) def _add_traffic_light(self): lanes = {lid: d for lid, d in self._output_lanes.items()} diff --git a/mosstool/trip/generator/_util/const.py b/mosstool/trip/generator/_util/const.py index 41b540f..e63b736 100644 --- a/mosstool/trip/generator/_util/const.py +++ b/mosstool/trip/generator/_util/const.py @@ -2,6 +2,7 @@ OD generate constants """ +import pycityproto.city.geo.v2.geo_pb2 as geov2 import pycityproto.city.map.v2.map_pb2 as mapv2 import pycityproto.city.person.v1.person_pb2 as personv1 import pycityproto.city.trip.v2.trip_pb2 as tripv2 @@ -20,7 +21,7 @@ "HOH": 10.91, "HOH+": 11.45, "HWHWH+": 1.43, - "HSOSH":0.00, + "HSOSH": 0.00, } BUS = tripv2.TRIP_MODE_BUS_WALK CAR = tripv2.TRIP_MODE_DRIVE_ONLY @@ -34,6 +35,7 @@ CAR, WALK, ] +PT_START_ID = 1_0000_0000 PRIMARY_SCHOOL, JUNIOR_HIGH_SCHOOL, HIGH_SCHOOL, COLLEGE, BACHELOR, MASTER, DOCTOR = ( personv1.EDUCATION_PRIMARY_SCHOOL, personv1.EDUCATION_JUNIOR_HIGH_SCHOOL, @@ -80,3 +82,32 @@ EDUCATION_CATGS = {"education"} # home catg HOME_CATGS = {"residential"} +PT_DRIVER_ATTRIBUTES = { + "BUS": { + "length": 15, + "width": 2, + "max_speed": 41.666666666666664, + "max_acceleration": 3, + "max_braking_acceleration": -10, + "usual_acceleration": 2, + "usual_braking_acceleration": -4.5, + }, + "SUBWAY": { + "length": 25, + "width": 2, + "max_speed": 41.666666666666664, + "max_acceleration": 3, + "max_braking_acceleration": -10, + "usual_acceleration": 2, + "usual_braking_acceleration": -4.5, + }, + "UNSPECIFIED": { + "length": 5, + "width": 2, + "max_speed": 41.666666666666664, + "max_acceleration": 3, + "max_braking_acceleration": -10, + "usual_acceleration": 2, + "usual_braking_acceleration": -4.5, + }, +} diff --git a/mosstool/trip/generator/generate_from_od.py b/mosstool/trip/generator/generate_from_od.py index 29136eb..df1e92b 100644 --- a/mosstool/trip/generator/generate_from_od.py +++ b/mosstool/trip/generator/generate_from_od.py @@ -12,13 +12,11 @@ from geopandas.geodataframe import GeoDataFrame from pycityproto.city.geo.v2.geo_pb2 import AoiPosition, Position from pycityproto.city.map.v2.map_pb2 import Map -from pycityproto.city.person.v1.person_pb2 import ( - Consumption, - Education, - Gender, - Person, - PersonProfile, -) +from pycityproto.city.person.v1.person_pb2 import (BusAttribute, Person, + PersonAttribute, + PersonProfile, TripStop) +from pycityproto.city.routing.v2.routing_pb2 import (DrivingJourneyBody, + Journey, JourneyType) from pycityproto.city.trip.v2.trip_pb2 import Schedule, Trip, TripMode from ...map._map_util.aoiutils import geo_coords @@ -702,6 +700,7 @@ def generate_persons( Returns: - List[Person]: The generated person objects. """ + self.persons = [] # user input time curve if departure_time_curve is not None: assert len(departure_time_curve) >= 24 @@ -724,3 +723,147 @@ def generate_persons( return [] self._generate_mobi(agent_num, area_pops, person_profiles, seed) return self.persons + + def _get_driving_pos_dict(self) -> Dict[Tuple[int, int], geov2.LanePosition]: + road_aoi_id2d_pos = {} + road_id2d_lane_ids = {} + lane_id2parent_road_id = {} + m_lanes = {l.id: l for l in self.m.lanes} + m_roads = {r.id: r for r in self.m.roads} + m_aois = {a.id: a for a in self.m.aois} + for road_id, road in m_roads.items(): + d_lane_ids = [ + lid + for lid in road.lane_ids + if m_lanes[lid].type == mapv2.LANE_TYPE_DRIVING + ] + road_id2d_lane_ids[road_id] = d_lane_ids + for lane_id, lane in m_lanes.items(): + parent_id = lane.parent_id + # road lane + if parent_id in m_roads: + lane_id2parent_road_id[lane_id] = parent_id + for aoi_id, aoi in m_aois.items(): + for d_pos in aoi.driving_positions: + pos_lane_id, _ = d_pos.lane_id, d_pos.s + # junction lane + assert ( + pos_lane_id in lane_id2parent_road_id + ), f"Bad lane position {d_pos} at AOI {aoi_id}" + parent_road_id = lane_id2parent_road_id[pos_lane_id] + road_aoi_key = (parent_road_id, aoi_id) + road_aoi_id2d_pos[road_aoi_key] = d_pos + return road_aoi_id2d_pos + + def generate_public_transport_drivers( + self, stop_duration_time: float = 30.0 + ) -> List[Person]: + """ + Args: + - stop_duration_time (float): The duration time (in second) for bus at each stop. + + Returns: + - List[Person]: The generated driver objects. + """ + self.persons = [] + road_aoi_id2d_pos = self._get_driving_pos_dict() + + def _transfer_conn_road_ids( + station_connection_road_ids: List[List[int]], + ) -> List[int]: + assert ( + len(station_connection_road_ids) > 0 + and len(station_connection_road_ids[0]) > 0 + ), f"Bad conn_road_ids {station_connection_road_ids}" + route_road_ids = [] + for next_road_ids in station_connection_road_ids: + if len(route_road_ids) > 0 and route_road_ids[-1] == next_road_ids[0]: + route_road_ids += next_road_ids[1:] + else: + route_road_ids += next_road_ids + return route_road_ids + + agent_id = PT_START_ID + for sl in self.m.sublines: + sl_id = sl.id + aoi_ids = list(sl.aoi_ids) + departure_times = list(sl.schedules.departure_times) + offset_times = list(sl.schedules.offset_times) + station_connection_road_ids = [ + [rid for rid in rids.road_ids] + for rids in sl.station_connection_road_ids + ] + sl_type = sl.type + if sl_type == mapv2.SUBLINE_TYPE_BUS: + sl_capacity = STATION_CAPACITY["BUS"] + sl_attributes = PT_DRIVER_ATTRIBUTES["BUS"] + elif sl_type == mapv2.SUBLINE_TYPE_SUBWAY: + sl_capacity = STATION_CAPACITY["SUBWAY"] + sl_attributes = PT_DRIVER_ATTRIBUTES["SUBWAY"] + elif sl_type == mapv2.SUBLINE_TYPE_UNSPECIFIED: + sl_capacity = STATION_CAPACITY["UNSPECIFIED"] + sl_attributes = PT_DRIVER_ATTRIBUTES["UNSPECIFIED"] + else: + raise ValueError(f"Bad Subline Type {sl_type}") + if not sl_type in {mapv2.SUBLINE_TYPE_BUS}: + continue + sl_capacity = sl.capacity if sl.capacity > 0 else sl_capacity + route_road_ids = _transfer_conn_road_ids(station_connection_road_ids) + for tm in departure_times: + p = Person() + p.CopyFrom(self.template) + p.id = agent_id + home_aoi_id, end_aoi_id = aoi_ids[0], aoi_ids[-1] + trip_stop_aoi_ids = aoi_ids[1:-1] # stop aoi ids during the trip + trip_stop_lane_id_s = [] + for cur_road_ids, cur_aoi_id in zip( + station_connection_road_ids[:-1], trip_stop_aoi_ids + ): + road_aoi_key = (cur_road_ids[-1], cur_aoi_id) + if road_aoi_key not in road_aoi_id2d_pos: + raise ValueError(f"bad road and AOI pair {road_aoi_key}") + d_pos = road_aoi_id2d_pos[road_aoi_key] + d_lane_id, d_s = d_pos.lane_id, d_pos.s + trip_stop_lane_id_s.append((d_lane_id, d_s)) + assert len(trip_stop_lane_id_s) == len( + trip_stop_aoi_ids + ), f"Bad PublicTransport Route at {aoi_ids}" + for (d_lane_id, d_s), aoi_id in zip( + trip_stop_lane_id_s, trip_stop_aoi_ids + ): + trip_stop = cast(TripStop, p.trip_stops.add()) + trip_stop.lane_id = d_lane_id + trip_stop.s = d_s + trip_stop.aoi_id = aoi_id + trip_stop.duration = stop_duration_time + if sl_attributes: + p.attribute.CopyFrom(dict2pb(sl_attributes, PersonAttribute())) + # PT subline id + p.bus_attribute.CopyFrom( + BusAttribute(subline_id=sl_id, capacity=sl_capacity, model="") + ) + p.home.CopyFrom(Position(aoi_position=AoiPosition(aoi_id=home_aoi_id))) + schedule = cast(Schedule, p.schedules.add()) + schedule.departure_time = tm + schedule.loop_count = 1 + trip = Trip( + mode=cast( + TripMode, + CAR, + ), + end=Position(aoi_position=AoiPosition(aoi_id=end_aoi_id)), + activity="", + model="", + routes=[ + Journey( + driving=DrivingJourneyBody( + road_ids=route_road_ids, eta=sum(offset_times) + ), + type=JourneyType.JOURNEY_TYPE_DRIVING, + ) + ], + ) + schedule.trips.append(trip) + self.persons.append(p) + agent_id += 1 + return self.persons diff --git a/pyproject.toml b/pyproject.toml index f693aba..247d320 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "mosstool" -version = "0.2.20" +version = "0.2.21" description = "MObility Simulation System toolbox " authors = ["Jun Zhang "] license = "MIT"