diff --git a/examples/data/damages_analysis_OSdamage/network.ini b/examples/data/damages_analysis_OSdamage/network.ini
index 3fed67ff6..cf19bbe17 100644
--- a/examples/data/damages_analysis_OSdamage/network.ini
+++ b/examples/data/damages_analysis_OSdamage/network.ini
@@ -31,7 +31,7 @@ hazard_crs = EPSG:32736
[cleanup]
snapping_threshold = None
-segmentation_length = None
+segmentation_length = 100
merge_lines = True
merge_on_id = False
cut_at_intersections = False
\ No newline at end of file
diff --git a/examples/data/damages_analysis_huizinga/network.ini b/examples/data/damages_analysis_huizinga/network.ini
index 3fed67ff6..cf19bbe17 100644
--- a/examples/data/damages_analysis_huizinga/network.ini
+++ b/examples/data/damages_analysis_huizinga/network.ini
@@ -31,7 +31,7 @@ hazard_crs = EPSG:32736
[cleanup]
snapping_threshold = None
-segmentation_length = None
+segmentation_length = 100
merge_lines = True
merge_on_id = False
cut_at_intersections = False
\ No newline at end of file
diff --git a/examples/data/damages_analysis_manual/network.ini b/examples/data/damages_analysis_manual/network.ini
index 3fed67ff6..cf19bbe17 100644
--- a/examples/data/damages_analysis_manual/network.ini
+++ b/examples/data/damages_analysis_manual/network.ini
@@ -31,7 +31,7 @@ hazard_crs = EPSG:32736
[cleanup]
snapping_threshold = None
-segmentation_length = None
+segmentation_length = 100
merge_lines = True
merge_on_id = False
cut_at_intersections = False
\ No newline at end of file
diff --git a/examples/example_damages_OSdamage.ipynb b/examples/example_damages_OSdamage.ipynb
index dc730b854..39d626b53 100644
--- a/examples/example_damages_OSdamage.ipynb
+++ b/examples/example_damages_OSdamage.ipynb
@@ -112,7 +112,7 @@
"name = beira
\n",
"
\n",
"[network]
\n",
- "**directed = False
\n",
+ "directed = False
\n",
"source = OSM download
\n",
"primary_file = None
\n",
"diversion_file = None
\n",
@@ -123,28 +123,28 @@
"save_gpkg = True\n",
"
\n",
"[origins_destinations]
\n",
- "**origins = None
\n",
+ "origins = None
\n",
"destinations = None
\n",
"origins_names = None
\n",
"destinations_names = None
\n",
"id_name_origin_destination = None
\n",
"origin_count = None
\n",
"origin_out_fraction = None
\n",
- "category = category**
\n",
+ "category = categorybr>\n",
"
\n",
"[hazard]
\n",
- "**hazard_map = max_flood_depth.tif
\n",
+ "hazard_map = max_flood_depth.tif
\n",
"hazard_id = None
\n",
"hazard_field_name = waterdepth
\n",
"aggregate_wl = max
\n",
- "hazard_crs = EPSG:32736**
\n",
+ "hazard_crs = EPSG:32736br>\n",
"
\n",
- "*[cleanup]
\n",
+ "[cleanup]
\n",
"snapping_threshold = None
\n",
- "segmentation_length = None
\n",
+ "segmentation_length = 100
\n",
"merge_lines = True
\n",
"merge_on_id = False
\n",
- "cut_at_intersections = False
*"
+ "cut_at_intersections = False
"
]
},
{
diff --git a/examples/example_damages_huizinga.ipynb b/examples/example_damages_huizinga.ipynb
index b7a754ac9..e811d3580 100644
--- a/examples/example_damages_huizinga.ipynb
+++ b/examples/example_damages_huizinga.ipynb
@@ -132,7 +132,7 @@
"name = beira
\n",
"
\n",
"[network]
\n",
- "**directed = False
\n",
+ "directed = False
\n",
"source = OSM download
\n",
"primary_file = None
\n",
"diversion_file = None
\n",
@@ -143,28 +143,28 @@
"save_gpkg = True\n",
"
\n",
"[origins_destinations]
\n",
- "**origins = None
\n",
+ "origins = None
\n",
"destinations = None
\n",
"origins_names = None
\n",
"destinations_names = None
\n",
"id_name_origin_destination = None
\n",
"origin_count = None
\n",
"origin_out_fraction = None
\n",
- "category = category**
\n",
+ "category = categorybr>\n",
"
\n",
"[hazard]
\n",
- "**hazard_map = max_flood_depth.tif
\n",
+ "hazard_map = max_flood_depth.tif
\n",
"hazard_id = None
\n",
"hazard_field_name = waterdepth
\n",
"aggregate_wl = max
\n",
- "hazard_crs = EPSG:32736**
\n",
+ "hazard_crs = EPSG:32736br>\n",
"
\n",
- "*[cleanup]
\n",
+ "[cleanup]
\n",
"snapping_threshold = None
\n",
- "segmentation_length = None
\n",
+ "segmentation_length = 100
\n",
"merge_lines = True
\n",
"merge_on_id = False
\n",
- "cut_at_intersections = False
*"
+ "cut_at_intersections = False
"
]
},
{
diff --git a/examples/example_damages_manual.ipynb b/examples/example_damages_manual.ipynb
index d047f011f..d743464d1 100644
--- a/examples/example_damages_manual.ipynb
+++ b/examples/example_damages_manual.ipynb
@@ -112,7 +112,7 @@
"name = beira
\n",
"
\n",
"[network]
\n",
- "**directed = False
\n",
+ "directed = False
\n",
"source = OSM download
\n",
"primary_file = None
\n",
"diversion_file = None
\n",
@@ -123,25 +123,25 @@
"save_gpkg = True\n",
"
\n",
"[origins_destinations]
\n",
- "**origins = None
\n",
+ "origins = None
\n",
"destinations = None
\n",
"origins_names = None
\n",
"destinations_names = None
\n",
"id_name_origin_destination = None
\n",
"origin_count = None
\n",
"origin_out_fraction = None
\n",
- "category = category**
\n",
+ "category = category
\n",
"
\n",
"[hazard]
\n",
- "**hazard_map = max_flood_depth.tif
\n",
+ "hazard_map = max_flood_depth.tif
\n",
"hazard_id = None
\n",
"hazard_field_name = waterdepth
\n",
"aggregate_wl = max
\n",
- "hazard_crs = EPSG:32736**
\n",
+ "hazard_crs = EPSG:32736
\n",
"
\n",
"*[cleanup]
\n",
"snapping_threshold = None
\n",
- "segmentation_length = None
\n",
+ "segmentation_length = 100
\n",
"merge_lines = True
\n",
"merge_on_id = False
\n",
"cut_at_intersections = False
*"
diff --git a/ra2ce/network/network_wrappers/network_wrapper_protocol.py b/ra2ce/network/network_wrappers/network_wrapper_protocol.py
index b6159869d..1018cc9bd 100644
--- a/ra2ce/network/network_wrappers/network_wrapper_protocol.py
+++ b/ra2ce/network/network_wrappers/network_wrapper_protocol.py
@@ -18,14 +18,55 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-from typing import Protocol, runtime_checkable
+import logging
+import math
+from typing import Protocol, runtime_checkable, Union, Optional
from geopandas import GeoDataFrame
from networkx import MultiGraph
+from ra2ce.network.segmentation import Segmentation
+
@runtime_checkable
class NetworkWrapperProtocol(Protocol):
+ def segment_graph(
+ self,
+ edges: GeoDataFrame,
+ export_link_table: bool,
+ link_tables: Optional[tuple] = None,
+ ) -> Union[GeoDataFrame, tuple[GeoDataFrame, tuple]]:
+ """
+ Segments a complex graph based on the given segmentation length.
+
+ Args:
+ - segmentation_length (Optional[float]): The length to segment the graph edges. If None, no segmentation is applied.
+ - edges_complex (gpd.GeoDataFrame): The GeoDataFrame containing the complex graph edges.
+ - crs (str): The coordinate reference system to apply if the CRS is missing after segmentation.
+
+ Returns:
+ - gpd.GeoDataFrame: The segmented edges_complex GeoDataFrame.
+ """
+ if not math.isnan(self.segmentation_length):
+ segmentation = Segmentation(edges, self.segmentation_length)
+ segmented_edges = segmentation.apply_segmentation()
+ if segmented_edges.crs is None: # The CRS might have disappeared.
+ segmented_edges.crs = self.crs # set the right CRS
+
+ if export_link_table:
+ updated_link_tables = segmentation.generate_link_tables()
+ segmented_edges.drop(columns=["rfid_c"], inplace=True)
+ segmented_edges.rename(columns={"splt_id": "rfid_c"}, inplace=True)
+ return segmented_edges, updated_link_tables
+ return segmented_edges
+ elif export_link_table and link_tables:
+ return edges, link_tables
+ elif export_link_table and not link_tables:
+ logging.warning("empty link_tables is passed")
+ return edges, tuple()
+ else:
+ return edges
+
def get_network(self) -> tuple[MultiGraph, GeoDataFrame]:
"""
Gets a network built within this wrapper instance. No arguments are accepted, the `__init__` method is meant to assign all required attributes for a wrapper.
diff --git a/ra2ce/network/network_wrappers/osm_network_wrapper/osm_network_wrapper.py b/ra2ce/network/network_wrappers/osm_network_wrapper/osm_network_wrapper.py
index 432e4002d..bf6d8a5c2 100644
--- a/ra2ce/network/network_wrappers/osm_network_wrapper/osm_network_wrapper.py
+++ b/ra2ce/network/network_wrappers/osm_network_wrapper/osm_network_wrapper.py
@@ -59,7 +59,7 @@ def __init__(self, config_data: NetworkConfigData) -> None:
config_data.network.attributes_to_exclude_in_simplification
)
self.output_graph_dir = config_data.output_graph_dir
- self.graph_crs = config_data.crs
+ self.crs = config_data.crs
# Network
self.network_type = config_data.network.network_type
@@ -67,6 +67,9 @@ def __init__(self, config_data: NetworkConfigData) -> None:
self.polygon_graph = self._get_clean_graph_from_osm(config_data.network.polygon)
self.is_directed = config_data.network.directed
+ # Cleanup
+ self.segmentation_length = config_data.cleanup.segmentation_length
+
@classmethod
def with_polygon(
cls, config_data: NetworkConfigData, polygon: BaseGeometry
@@ -153,6 +156,11 @@ def get_network(self) -> tuple[MultiGraph, GeoDataFrame]:
edges_complex, _ = nut.graph_to_gdf(graph_complex)
logging.info("Finished converting the graph to a geodataframe")
+ # Segment the complex graph
+ edges_complex, link_tables = self.segment_graph(
+ edges_complex, export_link_table=True, link_tables=link_tables
+ )
+
# Save the link tables linking complex and simple IDs
self._export_linking_tables(link_tables)
@@ -250,7 +258,7 @@ def _download_clean_graph_from_osm(
)
)
if "crs" not in _complex_graph.graph.keys():
- _complex_graph.graph["crs"] = self.graph_crs
+ _complex_graph.graph["crs"] = self.crs
self.get_clean_graph(_complex_graph)
return _complex_graph
diff --git a/ra2ce/network/network_wrappers/shp_network_wrapper.py b/ra2ce/network/network_wrappers/shp_network_wrapper.py
index 9e1b1f5af..3d05d201e 100644
--- a/ra2ce/network/network_wrappers/shp_network_wrapper.py
+++ b/ra2ce/network/network_wrappers/shp_network_wrapper.py
@@ -20,7 +20,6 @@
"""
import logging
-import math
import geopandas as gpd
import networkx as nx
@@ -32,7 +31,6 @@
from ra2ce.network.network_wrappers.network_wrapper_protocol import (
NetworkWrapperProtocol,
)
-from ra2ce.network.segmentation import Segmentation
class ShpNetworkWrapper(NetworkWrapperProtocol):
@@ -190,9 +188,6 @@ def get_network(
graph_complex, edges_complex = self._get_complex_graph_and_edges(edges, id_name)
- if not math.isnan(self.segmentation_length):
- edges_complex = Segmentation(edges_complex, self.segmentation_length)
- edges_complex = edges_complex.apply_segmentation()
- if edges_complex.crs is None: # The CRS might have dissapeared.
- edges_complex.crs = self.crs # set the right CRS
+ edges_complex = self.segment_graph(edges_complex, export_link_table=False)
+
return graph_complex, edges_complex
diff --git a/ra2ce/network/network_wrappers/trails_network_wrapper.py b/ra2ce/network/network_wrappers/trails_network_wrapper.py
index 16aefe1e2..bb3245421 100644
--- a/ra2ce/network/network_wrappers/trails_network_wrapper.py
+++ b/ra2ce/network/network_wrappers/trails_network_wrapper.py
@@ -30,7 +30,6 @@
NetworkWrapperProtocol,
)
from ra2ce.network.networks_utils import graph_from_gdf
-from ra2ce.network.segmentation import Segmentation
class TrailsNetworkWrapper(NetworkWrapperProtocol):
@@ -90,14 +89,8 @@ def get_network(self) -> tuple[MultiGraph, GeoDataFrame]:
"RA2CE will not clean-up your graph, assuming that it is already done in TRAILS"
)
- edges_complex = edges
- if self.segmentation_length:
- logging.info("TRAILS importer: start segmentating graph")
- to_segment = Segmentation(edges, self.segmentation_length)
- edges_simple_segmented = to_segment.apply_segmentation()
- if edges_simple_segmented.crs is None: # The CRS might have disappeared.
- edges_simple_segmented.crs = edges.crs # set the right CRS
- edges_complex = edges_simple_segmented
+ # Segment the complex graph
+ edges_complex = self.segment_graph(edges, export_link_table=False)
graph_complex = graph_simple # NOTE THAT DIFFERENCE
# BETWEEN SIMPLE AND COMPLEX DOES NOT EXIST WHEN IMPORTING WITH TRAILS
diff --git a/ra2ce/network/network_wrappers/vector_network_wrapper.py b/ra2ce/network/network_wrappers/vector_network_wrapper.py
index e4ecb0d4f..c8b7fb5b7 100644
--- a/ra2ce/network/network_wrappers/vector_network_wrapper.py
+++ b/ra2ce/network/network_wrappers/vector_network_wrapper.py
@@ -68,6 +68,7 @@ def __init__(
self.output_graph_dir = config_data.output_graph_dir
# Cleanup
+ self.segmentation_length = config_data.cleanup.segmentation_length
self.delete_duplicate_nodes = config_data.cleanup.delete_duplicate_nodes
def get_network(
@@ -111,6 +112,11 @@ def get_network(
edges_complex, _ = nut.graph_to_gdf(graph_complex)
logging.info("Finished converting the graph to a geodataframe")
+ # Segment the complex graph
+ edges_complex, link_tables = self.segment_graph(
+ edges_complex, export_link_table=True, link_tables=link_tables
+ )
+
# Save the link tables linking complex and simple IDs
self._export_linking_tables(link_tables)
diff --git a/ra2ce/network/segmentation.py b/ra2ce/network/segmentation.py
index b10500e07..9c146bde0 100644
--- a/ra2ce/network/segmentation.py
+++ b/ra2ce/network/segmentation.py
@@ -21,11 +21,13 @@
import logging
from decimal import Decimal
+from typing import Union
import geopandas as gpd
+from geopy import distance
from shapely.geometry import LineString, MultiLineString, Point
-from ra2ce.network.networks_utils import cut as network_cut
+from ra2ce.network.networks_utils import cut as network_cut, line_length
class Segmentation: # Todo: more naturally, this would be METHOD of the network class.
@@ -34,7 +36,7 @@ class Segmentation: # Todo: more naturally, this would be METHOD of the network
Variables:
*self.edges_input* (Geopandas DataFrame) : the edges that are to be segmented
- *self.segmentation_length* (float) : segmentation lenght in degrees #Todo also in meters?
+ *self.segmentation_length* (float) : segmentation length in metres
*self.save_files* (Boolean) : save segmented graph?
Result:
@@ -43,16 +45,53 @@ class Segmentation: # Todo: more naturally, this would be METHOD of the network
"""
- def __init__(self, edges_input, segmentation_length, save_files: bool = False):
+ def __init__(
+ self,
+ edges_input: gpd.GeoDataFrame,
+ segmentation_length: float,
+ save_files: bool = False,
+ ):
# General
self.edges_input = edges_input # Edges GeoDataFrame
self.edges_segmented = (
None # This is where the result will be saved Edges GeoDataframe
)
- self.segmentation_length = segmentation_length
+ self.link_tables = (
+ None # will include dictionaries simple_ids to complex and vice-versa
+ )
+ self._get_segmentation_length_from_metre(segmentation_length)
self.save_files = save_files # Todo not implemented yet
- def apply_segmentation(self):
+ def _get_segmentation_length_from_metre(self, segmentation_length_in_metre: float):
+ """
+ Convert a length in meters to degrees at a given latitude.
+
+ Args:
+ - segmentation_length_in_metre: Length in meters to be converted.
+ """
+
+ origin = (
+ 0,
+ 0,
+ ) # reference at the equator. The conversion is more accurate at the equator and changes as you move towards the poles.
+
+ # Calculate the destination point segmentation_length_in_metre north (latitude direction)
+ try:
+ distance.geodesic.ELLIPSOID = self.edges_input.crs.ellipsoid.name.replace(
+ " ", "-"
+ )
+ except:
+ distance.geodesic.ELLIPSOID = "WGS-84"
+
+ destination_point_lat = distance.geodesic(
+ meters=segmentation_length_in_metre
+ ).destination(origin, bearing=0)
+
+ self.segmentation_length = (
+ destination_point_lat.latitude - origin[0]
+ ) # length in degrees
+
+ def apply_segmentation(self) -> gpd.GeoDataFrame:
self.cut_gdf()
logging.info(
"Finished segmenting the geodataframe with split length: {} degree".format(
@@ -127,7 +166,7 @@ def split_linestring(self, linestring: LineString, split_length: float):
split_length (float): Length by which to split the linestring into equal segments.
Returns:
- result_list (list): List of LineString objects that all have the same length split_lenght.
+ result_list (list): list of LineString objects that all have the same length split_lenght.
"""
n_segments = self.number_of_segments(linestring, split_length)
@@ -173,7 +212,7 @@ def cut_gdf(self):
for column in columns:
data[column] = []
- count = 0
+ count = 1
for _, row in gdf.iterrows():
geom = row["geometry"]
assert type(geom) == LineString or type(geom) == MultiLineString
@@ -183,8 +222,61 @@ def cut_gdf(self):
for key, value in row.items():
if key == "geometry":
data[key].append(linestring)
+ elif key == "length":
+ data[key].append(line_length(linestring, self.edges_input.crs))
+ elif key == "time":
+ data[key].append(
+ round((row["length"] / row["avgspeed"]) / 1000, 5)
+ )
else:
data[key].append(value)
data["splt_id"].append(count)
count += 1
self.edges_segmented = gpd.GeoDataFrame(data)
+
+ def generate_link_tables(
+ self,
+ ) -> tuple[dict[int, Union[int, list[int]]], dict[int, int]]:
+ """
+ Generate mappings from a GeoDataFrame and two dictionaries.
+
+ Args:
+ - dicts: Tuple of two dictionaries.
+ - First dictionary maps rfid to rfid_c (integer or list of integers).
+ - Second dictionary maps rfid_c to rfid (integer).
+
+ Returns:
+ - A tuple containing two dictionaries:
+ - The first dictionary maps rfid to splt_id or list of splt_id.
+ - The second dictionary maps splt_id to rfid.
+ """
+ # Initialize result dictionaries
+ splt_id_to_rfid = {}
+ rfid_to_splt_id = {}
+
+ # Create a mapping from splt_id to rfid
+ for _, row in self.edges_segmented.iterrows():
+ splt_id = row["splt_id"]
+ rfid = row["rfid"]
+ splt_id_to_rfid[splt_id] = rfid
+
+ # Create a mapping from rfid to splt_id
+ for _, row in self.edges_segmented.iterrows():
+ splt_id = row["splt_id"]
+ rfid = row["rfid"]
+ rfid_c = row["rfid_c"]
+
+ # Determine the expected rfid from rfid_c
+ if rfid in rfid_to_splt_id:
+ if isinstance(rfid_to_splt_id[rfid], list):
+ rfid_to_splt_id[rfid].append(splt_id)
+ else:
+ rfid_to_splt_id[rfid] = [rfid_to_splt_id[rfid], splt_id]
+ else:
+ rfid_to_splt_id[rfid] = splt_id
+
+ # Sort the dictionaries by their keys
+ splt_id_to_rfid_sorted = dict(sorted(splt_id_to_rfid.items()))
+ rfid_to_splt_id_sorted = dict(sorted(rfid_to_splt_id.items()))
+ self.link_tables = (rfid_to_splt_id_sorted, splt_id_to_rfid_sorted)
+ return self.link_tables
diff --git a/tests/network/network_wrappers/test_osm_network_wrapper.py b/tests/network/network_wrappers/test_osm_network_wrapper.py
index ec8b7a388..cba6aac93 100644
--- a/tests/network/network_wrappers/test_osm_network_wrapper.py
+++ b/tests/network/network_wrappers/test_osm_network_wrapper.py
@@ -40,7 +40,7 @@ def test_initialize_without_graph_crs(self):
# 3. Verify final expectations.
assert isinstance(_wrapper, OsmNetworkWrapper)
assert isinstance(_wrapper, NetworkWrapperProtocol)
- assert _wrapper.graph_crs.to_epsg() == 4326
+ assert _wrapper.crs.to_epsg() == 4326
@staticmethod
def _get_dummy_network_config_data() -> NetworkConfigData: