From 9260592469b2ce1cb242342c19c51445ee98f714 Mon Sep 17 00:00:00 2001 From: yanjunbo <13008250033@163.com> Date: Sun, 27 Oct 2024 15:54:40 +0800 Subject: [PATCH] Support loading from cache for `mosstool.map.osm` --- data/cad/cad.xlsx | Bin 0 -> 79612 bytes examples/map_cad2geojson.py | 187 ++++++++++++++++++++++ mosstool/map/_map_util/format_checker.py | 40 ++++- mosstool/map/builder/builder.py | 7 +- mosstool/map/osm/building.py | 188 ++++++++++++++--------- mosstool/map/osm/point_of_interest.py | 132 +++++++++------- mosstool/map/osm/roadnet.py | 162 +++++++++++-------- pyproject.toml | 2 +- 8 files changed, 522 insertions(+), 196 deletions(-) create mode 100644 data/cad/cad.xlsx create mode 100644 examples/map_cad2geojson.py diff --git a/data/cad/cad.xlsx b/data/cad/cad.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..dc2788f58b4a585b55911ba9140e6c3a9fde1fae GIT binary patch literal 79612 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�g8RHPw!T}{ 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 qUu List[Dict]: + df = pd.read_excel(cad_path) + df = df.fillna(0) + if x_transform_func is None: + x_transform_func = lambda x: x + if y_transform_func is None: + y_transform_func = lambda y: y + node_id, way_id = node_start_id, way_start_id + osm_data: List[Dict] = [] + ANGLE_STEP = 1 + _edge_node_ids:Set[int] = set() + for i, now_row in df.iterrows(): + if now_row["名称"] == "直线": + _edge_node_ids.add(node_id) + n = { + "type": "node", + "id": node_id, + "x": float(now_row["起点 X"]), + "y": float(now_row["起点 Y"]), + } + osm_data.append(n) + node_id += 1 + + _edge_node_ids.add(node_id) + n = { + "type": "node", + "id": node_id, + "x": float(now_row["端点 X"]), + "y": float(now_row["端点 Y"]), + } + osm_data.append(n) + node_id += 1 + + w = { + "type": "way", + "id": way_id, + "nodes": [node_id - 2, node_id - 1], + "tags": {"highway": "tertiary"}, # here we set the same tag for all ways + "others": now_row, + } + way_id += 1 + + osm_data.append(w) + elif now_row["名称"] == "圆弧": + c_x = float(now_row["中心 X"]) + c_y = float(now_row["中心 Y"]) + start_angle = float(now_row["起点角度"]) + r = float(now_row["半径"]) + agl = 0 + nodes_indexes = [] + total_angle = float(now_row["总角度"]) + for agl in np.linspace(0, total_angle, max(int(total_angle / ANGLE_STEP), 3)): + n = { + "type": "node", + "id": node_id, + "x": c_x + r * math.cos(math.pi * (agl + start_angle) / 180), + "y": c_y + r * math.sin(math.pi * (agl + start_angle) / 180), + } + osm_data.append(n) + nodes_indexes.append(n["id"]) + node_id += 1 + _edge_node_ids.add(nodes_indexes[0]) + _edge_node_ids.add(nodes_indexes[-1]) + w = { + "type": "way", + "id": way_id, + "nodes": nodes_indexes, + "tags": {"highway": "tertiary"}, # here we set the same tag for all ways + "others": now_row, + } + osm_data.append(w) + way_id += 1 + for i in osm_data: + for _key, _transform_func in zip( + ["x", "y"], [x_transform_func, y_transform_func] + ): + if _key in i: + i[_key] = _transform_func(i[_key]) + to_merge_xys_dict = {i["id"]:(i["x"], i["y"]) for i in osm_data if "x" in i and "y" in i} + tree_id_to_node_id = {idx:i for idx,i in enumerate(to_merge_xys_dict.keys())} + tree = KDTree([v for v in to_merge_xys_dict.values()]) # type:ignore + merged = set() + father_id_dict = { + i: i for i in range(node_id) + } # Initialize the parent node id of each node + for _node_idx, xy in to_merge_xys_dict.items(): + if _node_idx in merged: + continue + a = [tree_id_to_node_id[i] for i in tree.query_ball_point(xy, merge_gate)] # type:ignore + if len(a) == 1: + unique_node_idx = a.pop() + father_id_dict[unique_node_idx] = _node_idx + merged.add(unique_node_idx) + else: + visited_nids = {_node_idx} + while len(a) > 0: + b = [] + for i in a: + if i in visited_nids: + continue + _xy = to_merge_xys_dict[i] + b.extend( + [tree_id_to_node_id[j] for j in tree.query_ball_point(_xy, merge_gate)] # type:ignore + ) + visited_nids.add(i) + father_id_dict[i] = _node_idx + a, b = b, [] + merged |= visited_nids + for i in range(node_id): + while father_id_dict[i] != father_id_dict[father_id_dict[i]]: + father_id_dict[i] = father_id_dict[father_id_dict[i]] + to_delete_ids = set() + for i in osm_data: + if i["type"] == "way": + i["nodes"] = [father_id_dict[n] for n in i["nodes"]] + if not len(set(i["nodes"]))>=2: + to_delete_ids.add(("way",i["id"])) + elif i["type"] == "node" and father_id_dict[i["id"]] != i["id"]: + to_delete_ids.add(("node",i["id"])) + return [i for i in osm_data if (i["type"],i["id"]) not in to_delete_ids] + + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s %(message)s", +) +PROJ_STR = "+proj=tmerc +lat_0=33.9 +lon_0=116.4" +os.makedirs("cache", exist_ok=True) +rn = RoadNet( + proj_str=PROJ_STR, +) +CAD_PATH = "./data/cad/cad.xlsx" +osm_data = cad2osm( + CAD_PATH, + x_transform_func=lambda x: x - 229036.4782002, + y_transform_func=lambda y: y - 214014.32078879, +) +import pickle + +pickle.dump(osm_data, open("./cache/osm_data.pkl", "wb")) +print(Counter(i["type"] for i in osm_data)) +path = "cache/topo_from_cad.geojson" +net = rn.create_road_net(path, osm_data_cache=osm_data) + +builder = Builder( + net=net, + gen_sidewalk_speed_limit=50 / 3.6, + road_expand_mode="M", + proj_str=PROJ_STR, +) +m = builder.build("test") +pb = dict2pb(m, Map()) +with open("data/temp/cad_map.pb", "wb") as f: + f.write(pb.SerializeToString()) diff --git a/mosstool/map/_map_util/format_checker.py b/mosstool/map/_map_util/format_checker.py index 5439792..65e16d3 100644 --- a/mosstool/map/_map_util/format_checker.py +++ b/mosstool/map/_map_util/format_checker.py @@ -1,15 +1,15 @@ """Check the field type and value of map format""" import logging -import sys from collections import defaultdict +from typing import Dict, List, Optional from geojson import FeatureCollection from ...type import LaneType from .._map_util.const import * -__all__ = ["geojson_format_check", "output_format_check"] +__all__ = ["geojson_format_check", "output_format_check", "osm_format_checker"] class _FormatCheckHandler(logging.Handler): @@ -302,7 +302,7 @@ def geojson_format_check( handler.trigger_warnings() -def output_format_check(output_map: dict, output_lane_length_check: bool): +def output_format_check(output_map: dict, output_lane_length_check: bool) -> None: logging.basicConfig(level=logging.INFO) handler = _FormatCheckHandler() logger = logging.getLogger() @@ -411,3 +411,37 @@ def output_format_check(output_map: dict, output_lane_length_check: bool): f"Junction {lane_type} lane {lane_id} is too long ({length} m), please check input GeoJSON file!" ) handler.trigger_warnings() + + +def osm_format_checker( + osm_cache_check: bool, + osm_data: Optional[List[Dict]] = None, + required_keys_dict: Optional[Dict[str, List[str]]] = None, +) -> None: + if not osm_cache_check: + return + if osm_data is None: + return + logging.basicConfig(level=logging.INFO) + handler = _FormatCheckHandler() + logger = logging.getLogger() + logger.addHandler(handler) + if not hasattr(osm_data, "__iter__"): + logging.warning(f"input OSM data is not iterable!") + else: + if not all(isinstance(d, dict) for d in osm_data): + logging.warning(f"Not all items in OSM data is a `Dict`!") + else: + for _key in ["type", "id"]: + if not all(_key in d for d in osm_data): + logging.warning(f"insufficient key {_key} provided in OSM data!") + if required_keys_dict is None: + required_keys_dict = {} + for d in osm_data: + _keys = required_keys_dict.get(d["type"], []) + if any(_key not in d for _key in _keys): + logging.warning( + f"insufficient keys {_keys} provided in OSM data {d}!" + ) + + handler.trigger_warnings() diff --git a/mosstool/map/builder/builder.py b/mosstool/map/builder/builder.py index f3f0ab9..0695d9e 100644 --- a/mosstool/map/builder/builder.py +++ b/mosstool/map/builder/builder.py @@ -185,11 +185,16 @@ def __init__( ) # (N, 2) xyz_coords = np.column_stack([xy_coords, z_coords]) # (N, 3) else: + z_coords = ( + coords[:, 2] + if coords.shape[1] > 2 + else np.zeros((coords.shape[0], 1), dtype=np.float64) + ) xy_coords = np.array( [c[:2] for c in feature["geometry"]["coordinates"]], dtype=np.float64, ) - xyz_coords = coords + xyz_coords = np.column_stack([xy_coords, z_coords]) # (N, 3) feature["geometry"]["coordinates_xy"] = xy_coords feature["geometry"]["coordinates_xyz"] = xyz_coords if feature["geometry"]["type"] == "LineString": diff --git a/mosstool/map/osm/building.py b/mosstool/map/osm/building.py index 0875026..ee1f131 100644 --- a/mosstool/map/osm/building.py +++ b/mosstool/map/osm/building.py @@ -1,5 +1,5 @@ import logging -from typing import Dict, Optional +from typing import Dict, List, Optional, Tuple import pyproj import requests @@ -8,6 +8,8 @@ from shapely.geometry import MultiPolygon as sMultiPolygon from shapely.geometry import Polygon as sPolygon +from .._map_util.format_checker import osm_format_checker + __all__ = ["Building"] @@ -18,23 +20,23 @@ class Building: def __init__( self, - proj_str: str, - max_longitude: float, - min_longitude: float, - max_latitude: float, - min_latitude: float, + proj_str: Optional[str] = None, + max_longitude: Optional[float] = None, + min_longitude: Optional[float] = None, + max_latitude: Optional[float] = None, + min_latitude: Optional[float] = None, wikipedia_name: Optional[str] = None, proxies: Optional[Dict[str, str]] = None, ): """ Args: - - proj_str (str): projection string, e.g. 'epsg:3857' - - max_longitude (float): max longitude - - min_longitude (float): min longitude - - max_latitude (float): max latitude - - min_latitude (float): min latitude - - wikipedia_name (str): wikipedia name of the area in OSM. - - proxies (dict): proxies for requests, e.g. {'http': 'http://localhost:1080', 'https': 'http://localhost:1080'} + - proj_str (Optional[str]): projection string, e.g. 'epsg:3857' + - max_longitude (Optional[float]): max longitude + - min_longitude (Optional[float]): min longitude + - max_latitude (Optional[float]): max latitude + - min_latitude (Optional[float]): min latitude + - wikipedia_name (Optional[str]): wikipedia name of the area in OSM. + - proxies (Optional[Dict[str, str]]): proxies for requests, e.g. {'http': 'http://localhost:1080', 'https': 'http://localhost:1080'} """ self.bbox = ( min_latitude, @@ -42,7 +44,12 @@ def __init__( max_latitude, max_longitude, ) - self.projector = pyproj.Proj(proj_str) + self.proj_parameter = proj_str + self.projector = ( + pyproj.Proj(self.proj_parameter) if self.proj_parameter else None + ) + if self.projector is None and any(i is None for i in self.bbox): + logging.warning("Using osm cache data for input and xy coords for output!") self.wikipedia_name = wikipedia_name self.proxies = proxies # OSM raw data @@ -55,62 +62,76 @@ def __init__( # generate AOIs self.aois: list = [] - def _query_raw_data(self): + def _query_raw_data(self, osm_data_cache: Optional[List[Dict]] = None): """ Get raw data from OSM API OSM query language: https://wiki.openstreetmap.org/wiki/Overpass_API/Language_Guide Can be run and visualized in real time at https://overpass-turbo.eu/ """ - logging.info("Querying osm raw data") - bbox_str = ",".join(str(i) for i in self.bbox) - query_header = f"[out:json][timeout:120][bbox:{bbox_str}];" - area_wikipedia_name = self.wikipedia_name - if area_wikipedia_name is not None: - query_header += f'area[wikipedia="{area_wikipedia_name}"]->.searchArea;' - osm_data = None - for _ in range(3): # retry 3 times - try: - query_body_raw = [ - ( - "way", - '[!highway][!tunnel][!boundary][!railway][!natural][!barrier][!junction][!waterway][!public_transport][landuse!~"grass|forest"][place!~"suburb|neighbourhood"][amenity!=fountain][historic!=city_gate][artwork_type!=sculpture][man_made!~"bridge|water_well"][building!~"wall|train_station|roof"]', - ), - ( - "rel", - '[landuse][landuse!~"grass|forest"][type=multipolygon]', - ), - ( - "rel", - "[amenity][amenity!=fountain][type=multipolygon]", - ), - ( - "rel", - '[building][building!~"wall|train_station|roof"][type=multipolygon]', - ), - ( - "rel", - "[leisure][leisure!=nature_reserve][type=multipolygon]", - ), - ] - query_body = "" - for obj, args in query_body_raw: - area = ( - "(area.searchArea)" if area_wikipedia_name is not None else "" - ) - query_body += obj + area + args + ";" - query_body = "(" + query_body + ");" - query = query_header + query_body + "(._;>;);" + "out body;" - logging.info(f"{query}") - osm_data = requests.get( - "http://overpass-api.de/api/interpreter?data=" + query, - proxies=self.proxies, - ).json()["elements"] - break - except Exception as e: - logging.warning(f"Exception when querying OSM data {e}") - logging.warning("No response from OSM, Please try again later!") - if osm_data is None: - raise Exception("No AOI response from OSM!") + if osm_data_cache is None: + logging.info("Querying osm raw data") + assert all( + i is not None for i in self.bbox + ), f"longitude and latitude are required without cache file!" + (min_lat, min_lon, max_lat, max_lon) = self.bbox + if self.proj_parameter is None and self.projector is None: + proj_str = f"+proj=tmerc +lat_0={(max_lat+min_lat)/2} +lon_0={(max_lon+min_lon)/2}" # type: ignore + self.proj_parameter = proj_str + self.projector = pyproj.Proj(self.proj_parameter) + bbox_str = ",".join(str(i) for i in self.bbox) + query_header = f"[out:json][timeout:120][bbox:{bbox_str}];" + area_wikipedia_name = self.wikipedia_name + if area_wikipedia_name is not None: + query_header += f'area[wikipedia="{area_wikipedia_name}"]->.searchArea;' + osm_data = None + for _ in range(3): # retry 3 times + try: + query_body_raw = [ + ( + "way", + '[!highway][!tunnel][!boundary][!railway][!natural][!barrier][!junction][!waterway][!public_transport][landuse!~"grass|forest"][place!~"suburb|neighbourhood"][amenity!=fountain][historic!=city_gate][artwork_type!=sculpture][man_made!~"bridge|water_well"][building!~"wall|train_station|roof"]', + ), + ( + "rel", + '[landuse][landuse!~"grass|forest"][type=multipolygon]', + ), + ( + "rel", + "[amenity][amenity!=fountain][type=multipolygon]", + ), + ( + "rel", + '[building][building!~"wall|train_station|roof"][type=multipolygon]', + ), + ( + "rel", + "[leisure][leisure!=nature_reserve][type=multipolygon]", + ), + ] + query_body = "" + for obj, args in query_body_raw: + area = ( + "(area.searchArea)" + if area_wikipedia_name is not None + else "" + ) + query_body += obj + area + args + ";" + query_body = "(" + query_body + ");" + query = query_header + query_body + "(._;>;);" + "out body;" + logging.info(f"{query}") + osm_data = requests.get( + "http://overpass-api.de/api/interpreter?data=" + query, + proxies=self.proxies, + ).json()["elements"] + break + except Exception as e: + logging.warning(f"Exception when querying OSM data {e}") + logging.warning("No response from OSM, Please try again later!") + if osm_data is None: + raise Exception("No AOI response from OSM!") + else: + logging.info("Loading osm raw data from cache") + osm_data = osm_data_cache nodes = [d for d in osm_data if d["type"] == "node"] ways = [d for d in osm_data if d["type"] == "way"] rels = [ @@ -145,10 +166,19 @@ def _make_raw_aoi(self): ways_aoi = self._ways_aoi ways_rel = self._ways_rel logging.info("Making raw aoi") - nodes_dict = { - n["id"]: [a for a in self.projector(n["lon"], n["lat"], inverse=False)] - for n in nodes - } + nodes_dict = {} + for n in nodes: + if "x" in n and "y" in n: + nodes_dict[n["id"]] = [a for a in (n["x"], n["y"])] + elif "lon" in n and "lat" in n: + assert ( + self.projector is not None + ), f"proj_str are required when downloading from OSM!" + nodes_dict[n["id"]] = [ + a for a in self.projector(n["lon"], n["lat"], inverse=False) + ] + else: + raise ValueError(f"Neither lon and lat or x and y in node {n}!") ways_aoi_dict = {w["id"]: w for w in ways_aoi} # nodes, tags ways_rel_dict = {w["id"]: w for w in ways_rel} rels_dict = {r["id"]: r for r in rels} # members: [(ref, role)], tags @@ -311,17 +341,31 @@ def _make_raw_aoi(self): logging.info(f"invalid_relation_cnt: {invalid_relation_cnt}") logging.info(f"raw aoi: {len(self.aois)}") - def create_building(self, output_path: Optional[str] = None): + def _transform_coordinate(self, c: Tuple[float, float]) -> List[float]: + if self.projector is None: + return [c[0], c[1]] + else: + return list(self.projector(c[0], c[1], inverse=True)) + + def create_building( + self, + output_path: Optional[str] = None, + osm_data_cache: Optional[List[Dict]] = None, + osm_cache_check: bool = False, + ): """ Create AOIs from OpenStreetMap. Args: + - osm_data_cache (Optional[List[Dict]]): OSM data cache. - output_path (str): GeoJSON file output path. + - osm_cache_check (bool): check the format of input OSM data cache. Returns: - AOIs in GeoJSON format. """ - self._query_raw_data() + osm_format_checker(osm_cache_check, osm_data_cache) + self._query_raw_data(osm_data_cache) self._make_raw_aoi() geos = [] for aoi in self.aois: @@ -330,7 +374,7 @@ def create_building(self, output_path: Optional[str] = None): geometry=Polygon( [ [ - list(self.projector(c[0], c[1], inverse=True)) + self._transform_coordinate(c) for c in aoi["geo"].exterior.coords ] ] diff --git a/mosstool/map/osm/point_of_interest.py b/mosstool/map/osm/point_of_interest.py index c7aa108..221e15e 100644 --- a/mosstool/map/osm/point_of_interest.py +++ b/mosstool/map/osm/point_of_interest.py @@ -1,9 +1,11 @@ import logging -from typing import Dict, Optional +from typing import Dict, List, Optional import requests from geojson import Feature, FeatureCollection, Point, dump +from .._map_util.format_checker import osm_format_checker + __all__ = ["PointOfInterest"] @@ -14,21 +16,21 @@ class PointOfInterest: def __init__( self, - max_longitude: float, - min_longitude: float, - max_latitude: float, - min_latitude: float, + max_longitude: Optional[float] = None, + min_longitude: Optional[float] = None, + max_latitude: Optional[float] = None, + min_latitude: Optional[float] = None, wikipedia_name: Optional[str] = None, proxies: Optional[Dict[str, str]] = None, ): """ Args: - - max_longitude (float): max longitude - - min_longitude (float): min longitude - - max_latitude (float): max latitude - - min_latitude (float): min latitude - - wikipedia_name (str): wikipedia name of the area in OSM. - - proxies (dict): proxies for requests, e.g. {'http': 'http://localhost:1080', 'https': 'http://localhost:1080'} + - max_longitude (Optional[float]): max longitude + - min_longitude (Optional[float]): min longitude + - max_latitude (Optional[float]): max latitude + - min_latitude (Optional[float]): min latitude + - wikipedia_name (Optional[str]): wikipedia name of the area in OSM. + - proxies (Optional[Dict[str, str]]): proxies for requests, e.g. {'http': 'http://localhost:1080', 'https': 'http://localhost:1080'} """ self.bbox = ( min_latitude, @@ -44,57 +46,66 @@ def __init__( # generate POIs self.pois: list = [] - def _query_raw_data(self): + def _query_raw_data(self, osm_data_cache: Optional[List[Dict]] = None): """ Get raw data from OSM API OSM query language: https://wiki.openstreetmap.org/wiki/Overpass_API/Language_Guide Can be run and visualized in real time at https://overpass-turbo.eu/ """ - logging.info("Querying osm raw data") - bbox_str = ",".join(str(i) for i in self.bbox) - query_header = f"[out:json][timeout:120][bbox:{bbox_str}];" - area_wikipedia_name = self.wikipedia_name - if area_wikipedia_name is not None: - query_header += f'area[wikipedia="{area_wikipedia_name}"]->.searchArea;' - osm_data = None - for _ in range(3): # retry 3 times - try: - query_body_raw = [ - ( - "node", - "", - ), - ] - query_body = "" - for obj, args in query_body_raw: - area = ( - "(area.searchArea)" if area_wikipedia_name is not None else "" - ) - query_body += obj + area + args + ";" - query_body = "(" + query_body + ");" - query = query_header + query_body + "(._;>;);" + "out body;" - logging.info(f"{query}") - osm_data = requests.get( - "http://overpass-api.de/api/interpreter?data=" + query, - proxies=self.proxies, - ).json()["elements"] - break - except Exception as e: - logging.warning(f"Exception when querying OSM data {e}") - logging.warning("No response from OSM, Please try again later!") - if osm_data is None: - raise Exception("No POI response from OSM!") + if osm_data_cache is None: + logging.info("Querying osm raw data") + assert all( + i is not None for i in self.bbox + ), f"longitude and latitude are required without cache file!" + bbox_str = ",".join(str(i) for i in self.bbox) + query_header = f"[out:json][timeout:120][bbox:{bbox_str}];" + area_wikipedia_name = self.wikipedia_name + if area_wikipedia_name is not None: + query_header += f'area[wikipedia="{area_wikipedia_name}"]->.searchArea;' + osm_data = None + for _ in range(3): # retry 3 times + try: + query_body_raw = [ + ( + "node", + "", + ), + ] + query_body = "" + for obj, args in query_body_raw: + area = ( + "(area.searchArea)" + if area_wikipedia_name is not None + else "" + ) + query_body += obj + area + args + ";" + query_body = "(" + query_body + ");" + query = query_header + query_body + "(._;>;);" + "out body;" + logging.info(f"{query}") + osm_data = requests.get( + "http://overpass-api.de/api/interpreter?data=" + query, + proxies=self.proxies, + ).json()["elements"] + break + except Exception as e: + logging.warning(f"Exception when querying OSM data {e}") + logging.warning("No response from OSM, Please try again later!") + if osm_data is None: + raise Exception("No POI response from OSM!") + else: + osm_data = osm_data_cache nodes = [d for d in osm_data if d["type"] == "node"] logging.info(f"node: {len(nodes)}") self._nodes = nodes + def _make_raw_poi(self): """ Construct POI from original OSM data. """ - _raw_pois = [] + _raw_pois = [] for d in self._nodes: - d_tags = d.get("tags",{}) - p_name = d_tags.get("name","") + d_tags = d.get("tags", {}) + p_name = d_tags.get("name", "") # name d["name"] = p_name # catg @@ -129,30 +140,37 @@ def _make_raw_poi(self): continue logging.info(f"raw poi: {len(_raw_pois)}") self.pois = _raw_pois - def create_pois(self, output_path: Optional[str] = None): + + def create_pois( + self, + output_path: Optional[str] = None, + osm_data_cache: Optional[List[Dict]] = None, + osm_cache_check: bool = False, + ): """ Create POIs from OpenStreetMap. Args: + - osm_data_cache (Optional[List[Dict]]): OSM data cache. - output_path (str): GeoJSON file output path. + - osm_cache_check (bool): check the format of input OSM data cache. Returns: - POIs in GeoJSON format. """ - self._query_raw_data() + osm_format_checker(osm_cache_check, osm_data_cache, {"node": ["lon", "lat"]}) + self._query_raw_data(osm_data_cache) self._make_raw_poi() geos = [] - for poi_id,poi in enumerate(self.pois): + for poi_id, poi in enumerate(self.pois): geos.append( Feature( - geometry=Point( - [poi["lon"],poi["lat"]] - ), + geometry=Point([poi["lon"], poi["lat"]]), properties={ "id": poi_id, "osm_tags": poi["tags"], - "name":poi["name"], - "catg":poi["catg"], + "name": poi["name"], + "catg": poi["catg"], }, ) ) diff --git a/mosstool/map/osm/roadnet.py b/mosstool/map/osm/roadnet.py index 5d13eb9..7929b5e 100644 --- a/mosstool/map/osm/roadnet.py +++ b/mosstool/map/osm/roadnet.py @@ -10,8 +10,8 @@ import requests from geojson import Feature, FeatureCollection, LineString, MultiPoint, dump from shapely.geometry import LineString as sLineString -from tqdm import tqdm +from .._map_util.format_checker import osm_format_checker from .._map_util.osm_const import * from ._motif import close_nodes, motif_H, suc_is_close_by_other_way from ._wayutil import merge_way_nodes, parse_osm_way_tags @@ -26,33 +26,37 @@ class RoadNet: def __init__( self, - proj_str: str, - max_longitude: float, - min_longitude: float, - max_latitude: float, - min_latitude: float, + proj_str: Optional[str] = None, + max_longitude: Optional[float] = None, + min_longitude: Optional[float] = None, + max_latitude: Optional[float] = None, + min_latitude: Optional[float] = None, wikipedia_name: Optional[str] = None, proxies: Optional[Dict[str, str]] = None, ): """ Args: - - proj_str (str): projection string, e.g. 'epsg:3857' - - max_longitude (float): max longitude - - min_longitude (float): min longitude - - max_latitude (float): max latitude - - min_latitude (float): min latitude - - wikipedia_name (str): wikipedia name of the area in OSM. - - proxies (dict): proxies for requests, e.g. {'http': 'http://localhost:1080', 'https': 'http://localhost:1080'} + - proj_str (Optional[str]): projection string, e.g. 'epsg:3857' + - max_longitude (Optional[float]): max longitude + - min_longitude (Optional[float]): min longitude + - max_latitude (Optional[float]): max latitude + - min_latitude (Optional[float]): min latitude + - wikipedia_name (Optional[str]): wikipedia name of the area in OSM. + - proxies (Optional[Dict[str, str]]): proxies for requests, e.g. {'http': 'http://localhost:1080', 'https': 'http://localhost:1080'} """ # configs self.proj_parameter = proj_str - self.projector = pyproj.Proj(self.proj_parameter) + self.projector = ( + pyproj.Proj(self.proj_parameter) if proj_str is not None else None + ) self.bbox = ( min_latitude, min_longitude, max_latitude, max_longitude, ) + if self.projector is None and any(i is None for i in self.bbox): + logging.warning("Using osm cache data for input and xy coords for output!") self.proxies = proxies self.wikipedia_name = wikipedia_name self.way_filter = WAY_FILTER @@ -76,6 +80,14 @@ def default_way_settings(self): def _download_osm(self): """Fetch raw data from OpenStreetMap""" + assert all( + i is not None for i in self.bbox + ), f"longitude and latitude are required without cache file!" + (min_lat, min_lon, max_lat, max_lon) = self.bbox + if self.proj_parameter is None and self.projector is None: + proj_str = f"+proj=tmerc +lat_0={(max_lat+min_lat)/2} +lon_0={(max_lon+min_lon)/2}" # type: ignore + self.proj_parameter = proj_str + self.projector = pyproj.Proj(self.proj_parameter) bbox_str = ",".join(str(i) for i in self.bbox) query = f"[out:json][timeout:180][bbox:{bbox_str}];" wikipedia_name = self.wikipedia_name @@ -252,21 +264,36 @@ def to_topo(self): return FeatureCollection(geos) - def _get_osm(self): - osm_data = self._download_osm() + def _get_osm(self, osm_data_cache: Optional[List[Dict]] = None): + if osm_data_cache is not None: + osm_data = osm_data_cache + else: + osm_data = self._download_osm() # find every valuable nodes self.nodes = {} - for doc in tqdm(osm_data): + for doc in osm_data: if doc["type"] != "node": continue - if "lon" in doc and "lat" in doc: + if "x" in doc and "y" in doc: + # doc["x"], doc["y"] = round(doc["x"], 6), round(doc["y"], 6) + if self.projector is not None: + doc["lon"], doc["lat"] = self.projector( + doc["x"], doc["y"], inverse=True + ) + else: + doc["lon"], doc["lat"] = doc["x"], doc["y"] + self.nodes[doc["id"]] = doc + elif "lon" in doc and "lat" in doc: # Latitude and longitude are specified with a precision of six decimal places. doc["lon"], doc["lat"] = round(doc["lon"], 6), round(doc["lat"], 6) + assert ( + self.projector is not None + ), f"proj_str are required when downloading from OSM!" doc["x"], doc["y"] = self.projector(doc["lon"], doc["lat"]) self.nodes[doc["id"]] = doc # all ways self.ways = {} - for way in tqdm(osm_data): + for way in osm_data: # Filter non-way elements if way["type"] != "way": continue @@ -297,7 +324,7 @@ def _get_osm(self): way_id = max(self.ways) # New road collection new_ways = {} - for way in tqdm(self.ways.values()): + for way in self.ways.values(): last_pos = 0 for pos, node in list(enumerate(way["nodes"]))[1:-1]: if node in joints: @@ -442,45 +469,48 @@ def _remove_simple_joints(self): # Two-way processing, find all connected components, confirm that there are 2 points with degree 1, and the rest are points with degree 2 # Select any point with degree 1, calculate the path to another point, and then assemble the new way # Find all Unicom components - for component in list(nx.connected_components(g)): - # Use assert to confirm that there are 2 nodes with degree 1, and the rest are nodes with degree 2 - degree_one_count = 0 - degree_two_count = 0 - for node in component: - if g.degree(node) == 1: # type: ignore - degree_one_count += 1 - elif g.degree(node) == 2: # type: ignore - degree_two_count += 1 - else: - raise AssertionError( - "The degree of the node does not meet the requirements" - ) - assert ( - degree_one_count == 2 - ), "The number of nodes with degree 1 is incorrect" - assert ( - degree_two_count == len(component) - 2 - ), "The number of nodes with degree 2 is incorrect" - # Select a node with degree 1 and calculate the path to another node with degree 1 - degree_one_nodes = [ - node for node in component if g.degree(node) == 1 # type: ignore - ] - start_node = degree_one_nodes[0] - end_node = degree_one_nodes[1] - way_ids = nx.shortest_path(g, start_node, end_node) - # Assemble new ways and delete redundant ways - main_way_id = way_ids[0] - main_way = self.ways[main_way_id] - main_way["remove_simple_joints"] = "main" - for way_id in way_ids[1:]: - way = self.ways[way_id] - try: - main_way["nodes"] = merge_way_nodes(main_way["nodes"], way["nodes"]) - except ValueError as e: - # with open("cache/graph.pkl", "wb") as f: - # pickle.dump([g, component], f) - raise e - removed_ids.add(way_id) + try: + for component in list(nx.connected_components(g)): + # Use assert to confirm that there are 2 nodes with degree 1, and the rest are nodes with degree 2 + degree_one_count = 0 + degree_two_count = 0 + for node in component: + if g.degree(node) == 1: # type: ignore + degree_one_count += 1 + elif g.degree(node) == 2: # type: ignore + degree_two_count += 1 + else: + raise AssertionError( + "The degree of the node does not meet the requirements" + ) + assert ( + degree_one_count == 2 + ), "The number of nodes with degree 1 is incorrect" + assert ( + degree_two_count == len(component) - 2 + ), "The number of nodes with degree 2 is incorrect" + # Select a node with degree 1 and calculate the path to another node with degree 1 + degree_one_nodes = [ + node for node in component if g.degree(node) == 1 # type: ignore + ] + start_node = degree_one_nodes[0] + end_node = degree_one_nodes[1] + way_ids = nx.shortest_path(g, start_node, end_node) + # Assemble new ways and delete redundant ways + main_way_id = way_ids[0] + main_way = self.ways[main_way_id] + main_way["remove_simple_joints"] = "main" + for way_id in way_ids[1:]: + way = self.ways[way_id] + try: + main_way["nodes"] = merge_way_nodes(main_way["nodes"], way["nodes"]) + except ValueError as e: + # with open("cache/graph.pkl", "wb") as f: + # pickle.dump([g, component], f) + raise e + removed_ids.add(way_id) + except Exception as e: + logging.warning(f"Error when trying to simplify roadnet {e}!") for wid in removed_ids: del self.ways[wid] self._assert_ways() @@ -668,18 +698,26 @@ def _clean_topo(self): del self.node2junction[node] del self.junction2nodes[c] - def create_road_net(self, output_path: Optional[str] = None): + def create_road_net( + self, + output_path: Optional[str] = None, + osm_data_cache: Optional[List[Dict]] = None, + osm_cache_check: bool = False, + ): """ Create Road net from OpenStreetMap. Args: - - output_path (str): GeoJSON file output path. + - osm_data_cache (Optional[List[Dict]]): OSM data cache. + - output_path (Optional[str]): GeoJSON file output path. + - osm_cache_check (bool): check the format of input OSM data cache. Returns: - roads and junctions in GeoJSON format. """ + osm_format_checker(osm_cache_check, osm_data_cache) # Get OSM data - self._get_osm() + self._get_osm(osm_data_cache) # self.dump_as_geojson("cache/1.geojson") # Process diff --git a/pyproject.toml b/pyproject.toml index fb6b325..e09d32e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "mosstool" -version = "1.0.12" +version = "1.0.13" description = "MObility Simulation System toolbox " authors = ["Jun Zhang "] license = "MIT"