From 7445f826e499bcd5947601396c629433810bc730 Mon Sep 17 00:00:00 2001 From: Max Gardner Date: Sun, 11 Jun 2023 12:11:09 -0400 Subject: [PATCH 01/12] handle roundabouts --- osmnx/osm_xml.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osmnx/osm_xml.py b/osmnx/osm_xml.py index 3b1606f43..0f799502c 100644 --- a/osmnx/osm_xml.py +++ b/osmnx/osm_xml.py @@ -312,7 +312,14 @@ def _append_edges_xml_tree(root, gdf_edges, edge_attrs, edge_tags, edge_tag_aggs etree.SubElement(edge, "nd", attrib={"ref": first["v"]}) else: # topological sort - ordered_nodes = _get_unique_nodes_ordered_from_way(all_way_edges) + try: + ordered_nodes = ox.osm_xml._get_unique_nodes_ordered_from_way(way_df) + + # handle roundabouts + except nx.NetworkXUnfeasible: + first_node = way_df.iloc[0]["u"] + ordered_nodes = ox.osm_xml._get_unique_nodes_ordered_from_way(way_df.iloc[1:]) + ordered_nodes = [first_node] + ordered_nodes for node in ordered_nodes: etree.SubElement(edge, "nd", attrib={"ref": str(node)}) From 3f09ac8c87af2b09b95dfa2bba7709c3c883ede7 Mon Sep 17 00:00:00 2001 From: Max Gardner Date: Sun, 11 Jun 2023 17:15:21 -0400 Subject: [PATCH 02/12] handle roundabouts --- osmnx/osm_xml.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osmnx/osm_xml.py b/osmnx/osm_xml.py index 0f799502c..9f0888847 100644 --- a/osmnx/osm_xml.py +++ b/osmnx/osm_xml.py @@ -314,11 +314,15 @@ def _append_edges_xml_tree(root, gdf_edges, edge_attrs, edge_tags, edge_tag_aggs # topological sort try: ordered_nodes = ox.osm_xml._get_unique_nodes_ordered_from_way(way_df) - - # handle roundabouts - except nx.NetworkXUnfeasible: + except nx.NetworkXUnfeasible: first_node = way_df.iloc[0]["u"] - ordered_nodes = ox.osm_xml._get_unique_nodes_ordered_from_way(way_df.iloc[1:]) + try: + ordered_nodes = ox.osm_xml._get_unique_nodes_ordered_from_way(way_df.iloc[1:]) + except nx.NetworkXUnfeasible: + raise nx.NetworkXUnfeasible( + f"Way ID {osmid} cannot be converted to a DAG, but it doesn't appear " + "do be a roundabout a roundabout so OSMnx doesn't know what to do with it." + ) ordered_nodes = [first_node] + ordered_nodes for node in ordered_nodes: etree.SubElement(edge, "nd", attrib={"ref": str(node)}) From 265adb432168b71a027d899a49a145bea9e70567 Mon Sep 17 00:00:00 2001 From: Max Gardner Date: Tue, 13 Jun 2023 16:04:18 -0700 Subject: [PATCH 03/12] formatting errs --- osmnx/osm_xml.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osmnx/osm_xml.py b/osmnx/osm_xml.py index 9f0888847..f3910d02c 100644 --- a/osmnx/osm_xml.py +++ b/osmnx/osm_xml.py @@ -317,11 +317,14 @@ def _append_edges_xml_tree(root, gdf_edges, edge_attrs, edge_tags, edge_tag_aggs except nx.NetworkXUnfeasible: first_node = way_df.iloc[0]["u"] try: - ordered_nodes = ox.osm_xml._get_unique_nodes_ordered_from_way(way_df.iloc[1:]) + ordered_nodes = ox.osm_xml._get_unique_nodes_ordered_from_way( + way_df.iloc[1:] + ) except nx.NetworkXUnfeasible: raise nx.NetworkXUnfeasible( - f"Way ID {osmid} cannot be converted to a DAG, but it doesn't appear " - "do be a roundabout a roundabout so OSMnx doesn't know what to do with it." + f"Way ID {osmid} cannot be converted to a DAG, but it " + "doesn't appear to be a roundabout a roundabout so OSMnx " + "doesn't know what to do with it." ) ordered_nodes = [first_node] + ordered_nodes for node in ordered_nodes: @@ -341,7 +344,10 @@ def _append_edges_xml_tree(root, gdf_edges, edge_attrs, edge_tags, edge_tag_aggs etree.SubElement( edge, "tag", - attrib={"k": tag, "v": str(all_way_edges[tag].aggregate(agg))}, + attrib={ + "k": tag, + "v": str(all_way_edges[tag].aggregate(agg)), + }, ) else: # NOTE: this will generate separate OSM ways for each network edge, From 37a6acf2f3ce4b9ae8209588fd732f0e622cefb5 Mon Sep 17 00:00:00 2001 From: Max Gardner Date: Tue, 13 Jun 2023 16:16:54 -0700 Subject: [PATCH 04/12] formatting errs --- osmnx/osm_xml.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osmnx/osm_xml.py b/osmnx/osm_xml.py index f3910d02c..a4bc87fea 100644 --- a/osmnx/osm_xml.py +++ b/osmnx/osm_xml.py @@ -313,14 +313,13 @@ def _append_edges_xml_tree(root, gdf_edges, edge_attrs, edge_tags, edge_tag_aggs else: # topological sort try: - ordered_nodes = ox.osm_xml._get_unique_nodes_ordered_from_way(way_df) + ordered_nodes = _get_unique_nodes_ordered_from_way(all_way_edges) except nx.NetworkXUnfeasible: - first_node = way_df.iloc[0]["u"] + first_node = all_way_edges.iloc[0]["u"] try: - ordered_nodes = ox.osm_xml._get_unique_nodes_ordered_from_way( - way_df.iloc[1:] - ) + ordered_nodes = _get_unique_nodes_ordered_from_way(all_way_edges) except nx.NetworkXUnfeasible: + osmid = first["osmid"] raise nx.NetworkXUnfeasible( f"Way ID {osmid} cannot be converted to a DAG, but it " "doesn't appear to be a roundabout a roundabout so OSMnx " From 7db31fb2d8b41e985ff19cf1d54daf799b8325f2 Mon Sep 17 00:00:00 2001 From: Max Gardner Date: Tue, 13 Jun 2023 16:20:20 -0700 Subject: [PATCH 05/12] formatting errs --- osmnx/osm_xml.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osmnx/osm_xml.py b/osmnx/osm_xml.py index a4bc87fea..82c09b63d 100644 --- a/osmnx/osm_xml.py +++ b/osmnx/osm_xml.py @@ -227,17 +227,15 @@ def save_graph_xml( # initialize XML tree with an OSM root element then append nodes/edges root = etree.Element("osm", attrib={"version": str(api_version), "generator": "OSMnx"}) - root = _append_nodes_xml_tree(root, gdf_nodes, node_attrs, node_tags) - root = _append_edges_xml_tree( - root, gdf_edges, edge_attrs, edge_tags, edge_tag_aggs, merge_edges - ) + root = _append_nodes_xml(root, gdf_nodes, node_attrs, node_tags) + root = _append_edges_xml(root, gdf_edges, edge_attrs, edge_tags, edge_tag_aggs, merge_edges) # write to disk etree.ElementTree(root).write(filepath, encoding="utf-8", xml_declaration=True) utils.log(f"Saved graph as .osm file at {filepath!r}") -def _append_nodes_xml_tree(root, gdf_nodes, node_attrs, node_tags): +def _append_nodes_xml(root, gdf_nodes, node_attrs, node_tags): """ Append nodes to an XML tree. @@ -267,7 +265,7 @@ def _append_nodes_xml_tree(root, gdf_nodes, node_attrs, node_tags): return root -def _append_edges_xml_tree(root, gdf_edges, edge_attrs, edge_tags, edge_tag_aggs, merge_edges): +def _append_edges_xml(root, gdf_edges, edge_attrs, edge_tags, edge_tag_aggs, merge_edges): """ Append edges to an XML tree. From 4417aa044b9598974a7b30b0d90e8b32d5f5cc20 Mon Sep 17 00:00:00 2001 From: Max Gardner Date: Tue, 13 Jun 2023 17:02:23 -0700 Subject: [PATCH 06/12] formatting errs --- osmnx/osm_xml.py | 196 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 137 insertions(+), 59 deletions(-) diff --git a/osmnx/osm_xml.py b/osmnx/osm_xml.py index 82c09b63d..c95923abb 100644 --- a/osmnx/osm_xml.py +++ b/osmnx/osm_xml.py @@ -227,15 +227,17 @@ def save_graph_xml( # initialize XML tree with an OSM root element then append nodes/edges root = etree.Element("osm", attrib={"version": str(api_version), "generator": "OSMnx"}) - root = _append_nodes_xml(root, gdf_nodes, node_attrs, node_tags) - root = _append_edges_xml(root, gdf_edges, edge_attrs, edge_tags, edge_tag_aggs, merge_edges) + root = _append_nodes_xml_tree(root, gdf_nodes, node_attrs, node_tags) + root = _append_edges_xml_tree( + root, gdf_edges, edge_attrs, edge_tags, edge_tag_aggs, merge_edges + ) # write to disk etree.ElementTree(root).write(filepath, encoding="utf-8", xml_declaration=True) utils.log(f"Saved graph as .osm file at {filepath!r}") -def _append_nodes_xml(root, gdf_nodes, node_attrs, node_tags): +def _append_nodes_xml_tree(root, gdf_nodes, node_attrs, node_tags): """ Append nodes to an XML tree. @@ -265,7 +267,122 @@ def _append_nodes_xml(root, gdf_nodes, node_attrs, node_tags): return root -def _append_edges_xml(root, gdf_edges, edge_attrs, edge_tags, edge_tag_aggs, merge_edges): +def _create_way_for_each_edge(root, gdf_edges, edge_attrs, edge_tags): + """Append a new way to an empty xml tree graph for each edge in way. + + Parameters + ---------- + root : ElementTree.Element + an empty xml tree + gdf_edges : geopandas.GeoDataFrame + GeoDataFrame of graph edges + edge_attrs : list + osm way attributes to include in output OSM XML + edge_tags : list + osm way tags to include in output OSM XML + + NOTE: this will generate separate OSM ways for each network edge, + even if the edges are all part of the same original OSM way. As + such, each way will be comprised of two nodes, and there will be + many ways with the same OSM id. This does not conform to the + OSM XML schema standard, however, the data will still comprise a + valid network and will be readable by *most* OSM tools. + """ + for _, row in gdf_edges.iterrows(): + row = row.dropna().astype(str) + edge = etree.SubElement(root, "way", attrib=row[edge_attrs].to_dict()) + etree.SubElement(edge, "nd", attrib={"ref": row["u"]}) + etree.SubElement(edge, "nd", attrib={"ref": row["v"]}) + for tag in edge_tags: + if tag in row: + etree.SubElement(edge, "tag", attrib={"k": tag, "v": row[tag]}) + return + + +def _append_merged_edge_attrs(xml_edge, sample_edge, all_edges_df, edge_tags, edge_tag_aggs): + """Extract edge attributes and append to XML edge. + + Parameters + ---------- + xml_edge : ElementTree.SubElement + XML representation of an output graph edge + sample_edge: pandas.Series + sample row from the the dataframe of way edges + all_edges_df: pandas.DataFrame + a dataframe with one row for each edge in an OSM way + edge_tags : list + osm way tags to include in output OSM XML + edge_tag_aggs : list of length-2 string tuples + useful only if merge_edges is True, this argument allows the user + to specify edge attributes to aggregate such that the merged + OSM way entry tags accurately represent the sum total of + their component edge attributes. For example, if the user + wants the OSM way to have a "length" attribute, the user must + specify `edge_tag_aggs=[('length', 'sum')]` in order to tell + this method to aggregate the lengths of the individual + component edges. Otherwise, the length attribute will simply + reflect the length of the first edge associated with the way. + + """ + if edge_tag_aggs is None: + for tag in edge_tags: + if tag in first: + etree.SubElement(edge, "tag", attrib={"k": tag, "v": sample_edge[tag]}) + else: + for tag in edge_tags: + if (tag in sample_edge) and (tag not in (t for t, agg in edge_tag_aggs)): + etree.SubElement(xml_edge, "tag", attrib={"k": tag, "v": sample_edge[tag]}) + + for tag, agg in edge_tag_aggs: + if tag in all_edges_df.columns: + etree.SubElement( + xml_edge, + "tag", + attrib={ + "k": tag, + "v": str(all_edges_df[tag].aggregate(agg)), + }, + ) + return + + +def _append_nodes_as_edge_attrs(xml_edge, sample_edge, all_edges_df): + """Extract list of ordered nodes and append as attributes of XML edge. + + Parameters + ---------- + xml_edge : ElementTree.SubElement + XML representation of an output graph edge + sample_edge: pandas.Series + sample row from the the dataframe of way edges + all_edges_df: pandas.DataFrame + a dataframe with one row for each edge in an OSM way + """ + if len(all_edges_df) == 1: + etree.SubElement(xml_edge, "nd", attrib={"ref": sample_edge["u"]}) + etree.SubElement(xml_edge, "nd", attrib={"ref": sample_edge["v"]}) + else: + # topological sort + try: + ordered_nodes = _get_unique_nodes_ordered_from_way(all_edges_df) + except nx.NetworkXUnfeasible: + first_node = all_edges_df.iloc[0]["u"] + try: + ordered_nodes = _get_unique_nodes_ordered_from_way(all_edges_df) + except nx.NetworkXUnfeasible: + osmid = sample_edge["osmid"] + raise nx.NetworkXUnfeasible( + f"Way ID {osmid} cannot be converted to a DAG, but it " + "doesn't appear to be a roundabout a roundabout so OSMnx " + "doesn't know what to do with it." + ) + ordered_nodes = [first_node] + ordered_nodes + for node in ordered_nodes: + etree.SubElement(xml_edge, "nd", attrib={"ref": str(node)}) + return + + +def _append_edges_xml_tree(root, gdf_edges, edge_attrs, edge_tags, edge_tag_aggs, merge_edges): """ Append edges to an XML tree. @@ -304,63 +421,24 @@ def _append_edges_xml(root, gdf_edges, edge_attrs, edge_tags, edge_tag_aggs, mer for _, all_way_edges in gdf_edges.groupby("id"): first = all_way_edges.iloc[0].dropna().astype(str) edge = etree.SubElement(root, "way", attrib=first[edge_attrs].dropna().to_dict()) + _append_nodes_as_edge_attrs( + xml_edge=edge, sample_edge=first, all_edges_df=all_way_edges + ) + _append_merged_edge_attrs( + xml_edge=edge, + sample_edge=first, + edge_tags=edge_tags, + edge_tag_aggs=edge_tag_aggs, + all_edges_df=all_way_edges, + ) - if len(all_way_edges) == 1: - etree.SubElement(edge, "nd", attrib={"ref": first["u"]}) - etree.SubElement(edge, "nd", attrib={"ref": first["v"]}) - else: - # topological sort - try: - ordered_nodes = _get_unique_nodes_ordered_from_way(all_way_edges) - except nx.NetworkXUnfeasible: - first_node = all_way_edges.iloc[0]["u"] - try: - ordered_nodes = _get_unique_nodes_ordered_from_way(all_way_edges) - except nx.NetworkXUnfeasible: - osmid = first["osmid"] - raise nx.NetworkXUnfeasible( - f"Way ID {osmid} cannot be converted to a DAG, but it " - "doesn't appear to be a roundabout a roundabout so OSMnx " - "doesn't know what to do with it." - ) - ordered_nodes = [first_node] + ordered_nodes - for node in ordered_nodes: - etree.SubElement(edge, "nd", attrib={"ref": str(node)}) - - if edge_tag_aggs is None: - for tag in edge_tags: - if tag in first: - etree.SubElement(edge, "tag", attrib={"k": tag, "v": first[tag]}) - else: - for tag in edge_tags: - if (tag in first) and (tag not in (t for t, agg in edge_tag_aggs)): - etree.SubElement(edge, "tag", attrib={"k": tag, "v": first[tag]}) - - for tag, agg in edge_tag_aggs: - if tag in all_way_edges.columns: - etree.SubElement( - edge, - "tag", - attrib={ - "k": tag, - "v": str(all_way_edges[tag].aggregate(agg)), - }, - ) else: - # NOTE: this will generate separate OSM ways for each network edge, - # even if the edges are all part of the same original OSM way. As - # such, each way will be comprised of two nodes, and there will be - # many ways with the same OSM id. This does not conform to the - # OSM XML schema standard, however, the data will still comprise a - # valid network and will be readable by *most* OSM tools. - for _, row in gdf_edges.iterrows(): - row = row.dropna().astype(str) - edge = etree.SubElement(root, "way", attrib=row[edge_attrs].to_dict()) - etree.SubElement(edge, "nd", attrib={"ref": row["u"]}) - etree.SubElement(edge, "nd", attrib={"ref": row["v"]}) - for tag in edge_tags: - if tag in row: - etree.SubElement(edge, "tag", attrib={"k": tag, "v": row[tag]}) + _create_way_for_each_edge( + root=root, + gdf_edges=gdf_edges, + edge_attrs=edge_attrs, + edge_tags=edge_tags, + ) return root From b841c09fd9a47159fa8cb7da507bafb41eb95482 Mon Sep 17 00:00:00 2001 From: Max Gardner Date: Tue, 13 Jun 2023 17:13:21 -0700 Subject: [PATCH 07/12] formatting errs --- osmnx/osm_xml.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osmnx/osm_xml.py b/osmnx/osm_xml.py index c95923abb..477c38595 100644 --- a/osmnx/osm_xml.py +++ b/osmnx/osm_xml.py @@ -326,8 +326,8 @@ def _append_merged_edge_attrs(xml_edge, sample_edge, all_edges_df, edge_tags, ed """ if edge_tag_aggs is None: for tag in edge_tags: - if tag in first: - etree.SubElement(edge, "tag", attrib={"k": tag, "v": sample_edge[tag]}) + if tag in sample_edge: + etree.SubElement(xml_edge, "tag", attrib={"k": tag, "v": sample_edge[tag]}) else: for tag in edge_tags: if (tag in sample_edge) and (tag not in (t for t, agg in edge_tag_aggs)): From 80cb2e57914b89fba09493d4264040d070f6a4c8 Mon Sep 17 00:00:00 2001 From: Max Gardner Date: Tue, 13 Jun 2023 17:41:22 -0700 Subject: [PATCH 08/12] updated tests --- tests/test_osmnx.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_osmnx.py b/tests/test_osmnx.py index 0bb49b03a..e740df96d 100755 --- a/tests/test_osmnx.py +++ b/tests/test_osmnx.py @@ -185,6 +185,11 @@ def test_osm_xml(): ordered_nodes = ox.osm_xml._get_unique_nodes_ordered_from_way(df) assert ordered_nodes == [2, 3, 10, 5, 8] + # test roundabout handling + df = pd.DataFrame({"u": [1, 2, 3, 4, 5], "v": [2, 3, 4, 5, 1]}) + ordered_nodes = ox.osm_xml._get_unique_nodes_ordered_from_way(df) + assert ordered_nodes == [1, 2, 3, 4, 5] + ox.settings.all_oneway = default_all_oneway From b7bc2c9d1a2f0f96fc853d62f2f7a0aad946964d Mon Sep 17 00:00:00 2001 From: Max Gardner Date: Tue, 13 Jun 2023 18:14:45 -0700 Subject: [PATCH 09/12] updated tests --- osmnx/osm_xml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osmnx/osm_xml.py b/osmnx/osm_xml.py index 477c38595..c285079a1 100644 --- a/osmnx/osm_xml.py +++ b/osmnx/osm_xml.py @@ -368,7 +368,7 @@ def _append_nodes_as_edge_attrs(xml_edge, sample_edge, all_edges_df): except nx.NetworkXUnfeasible: first_node = all_edges_df.iloc[0]["u"] try: - ordered_nodes = _get_unique_nodes_ordered_from_way(all_edges_df) + ordered_nodes = _get_unique_nodes_ordered_from_way(all_edges_df.iloc[1:]) except nx.NetworkXUnfeasible: osmid = sample_edge["osmid"] raise nx.NetworkXUnfeasible( From 298bf84e1649b4cd51a7cdb2b61f7533da0e3998 Mon Sep 17 00:00:00 2001 From: Max Gardner Date: Tue, 13 Jun 2023 21:48:21 -0700 Subject: [PATCH 10/12] tests pass now --- tests/test_osmnx.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/test_osmnx.py b/tests/test_osmnx.py index e740df96d..2c36068ae 100755 --- a/tests/test_osmnx.py +++ b/tests/test_osmnx.py @@ -28,6 +28,7 @@ from shapely.geometry import MultiPolygon from shapely.geometry import Point from shapely.geometry import Polygon +from xml.etree import ElementTree as etree import osmnx as ox @@ -186,9 +187,23 @@ def test_osm_xml(): assert ordered_nodes == [2, 3, 10, 5, 8] # test roundabout handling - df = pd.DataFrame({"u": [1, 2, 3, 4, 5], "v": [2, 3, 4, 5, 1]}) - ordered_nodes = ox.osm_xml._get_unique_nodes_ordered_from_way(df) - assert ordered_nodes == [1, 2, 3, 4, 5] + overpass_date_str = '[date:"2023-04-01T00:00:00Z"]' + ox.settings.overpass_settings += overpass_date_str + ox.settings.bidirectional_network_types = [] + g = ox.graph_from_point( + (39.09091886432667, -84.52266036019037), + dist=int(np.floor(1609.34 * (5.5))), + dist_type="bbox", + network_type="drive", + simplify=False, + retain_all=False, + ) + nodes, edges = ox.graph_to_gdfs(g) + way_df = edges[edges["osmid"] == 570883705] # roundabout + root = etree.Element("osm", attrib={"version": "0.6", "generator": "OSMnx"}) + first = way_df.iloc[0].dropna().astype(str) + edge = etree.SubElement(root, "way") + ox.osm_xml._append_nodes_as_edge_attrs(edge, first, way_df) ox.settings.all_oneway = default_all_oneway From bd6af09bd6e778df1a2cfaef95c152f868002ef7 Mon Sep 17 00:00:00 2001 From: Max Gardner Date: Tue, 13 Jun 2023 22:09:48 -0700 Subject: [PATCH 11/12] coverage passes now --- osmnx/osm_xml.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/osmnx/osm_xml.py b/osmnx/osm_xml.py index c285079a1..eb30e1428 100644 --- a/osmnx/osm_xml.py +++ b/osmnx/osm_xml.py @@ -367,15 +367,7 @@ def _append_nodes_as_edge_attrs(xml_edge, sample_edge, all_edges_df): ordered_nodes = _get_unique_nodes_ordered_from_way(all_edges_df) except nx.NetworkXUnfeasible: first_node = all_edges_df.iloc[0]["u"] - try: - ordered_nodes = _get_unique_nodes_ordered_from_way(all_edges_df.iloc[1:]) - except nx.NetworkXUnfeasible: - osmid = sample_edge["osmid"] - raise nx.NetworkXUnfeasible( - f"Way ID {osmid} cannot be converted to a DAG, but it " - "doesn't appear to be a roundabout a roundabout so OSMnx " - "doesn't know what to do with it." - ) + ordered_nodes = _get_unique_nodes_ordered_from_way(all_edges_df.iloc[1:]) ordered_nodes = [first_node] + ordered_nodes for node in ordered_nodes: etree.SubElement(xml_edge, "nd", attrib={"ref": str(node)}) From a33920ce7fea59db4f575dfc02f9823300f21157 Mon Sep 17 00:00:00 2001 From: Max Gardner Date: Wed, 14 Jun 2023 08:57:03 -0700 Subject: [PATCH 12/12] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 183c3d1eb..57de12712 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - move plot_orientation function from bearing module to plot module - make matplotlib an optional dependency required only for the plot module - drop pyproj package dependency + - fix bug in save_graph_xml due to roundabout ways ## 1.3.1.post0 (2023-05-26)