From e2f08404815f7f5831d11ce1612d12e1dba103d4 Mon Sep 17 00:00:00 2001 From: Rkleisley <63410265+Rkleisley@users.noreply.github.com> Date: Thu, 9 Jan 2025 20:42:29 -0500 Subject: [PATCH 01/11] Split Function to split a geolinestring into multiple segments by a provided distance --- geostructures/structures.py | 90 +++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/geostructures/structures.py b/geostructures/structures.py index 5c2d678..286d65f 100644 --- a/geostructures/structures.py +++ b/geostructures/structures.py @@ -1479,6 +1479,96 @@ def intersects_shape(self, shape: 'GeoShape', **kwargs) -> bool: # because the centroid may fall in a hole return o_edges[0][0][0] in self or s_edges[0][0][0] in shape + @staticmethod + def split(self, distance_meters: float) -> List[GeoLineString]: + """ + Splits a GeoLineString into smaller GeoLineStrings of equal length while also dividing + the associated time interval proportionally if it spans a range. If the time interval + is a single timestamp, time is not split. + + Args: + distance_meters (float): The desired length of each segment. + + Returns: + List[GeoLineString]: A list of GeoLineStrings, each with proportional time intervals if applicable. + """ + out = [] + cumulative_length = 0 + segments: List[Tuple[Coordinate, Coordinate]] = self.segments.copy() + vertices = [segments[0][0]] + remaining_distance_meters = distance_meters + + # Total line length + total_length_meters = sum(haversine_distance_meters(*segment) for segment in segments) + + start_time = end_time = total_duration_seconds = None + if self.dt and self.dt.start != self.dt.end: # Check for a valid time range + start_time, end_time = self.dt.start, self.dt.end + total_duration_seconds = (end_time - start_time).total_seconds() + + while segments: + remaining_segment_length = haversine_distance(*segments[0]) + + while remaining_distance_meters < remaining_segment_length: + end_point = inverse_haversine_degrees( + vertices[-1], + bearing_degrees(vertices[-1], segments[0][1]), + remaining_distance_meters + ) + vertices.append(end_point) + cumulative_length += remaining_distance_meters + + # Calculate proportional time interval + dt = None + if total_duration_seconds is not None: + segment_start_time = start_time + timedelta(seconds= + (cumulative_length - remaining_distance_meters) + / total_length_meters * total_duration_seconds + ) + segment_end_time = start_time + timedelta(seconds= + cumulative_length + / total_length_meters * total_duration_seconds + ) + dt = TimeInterval(segment_start_time, segment_end_time) + + elif self.dt: + dt = self.dt + + out.append(GeoLineString(vertices), dt=dt) + vertices = [end_point] + remaining_segment_length = haversine_distance_meters(vertices[-1], segments[0][1]) + + remaining_distance_meters = remaining_distance_meters - remaining_segment_length + cumulative_length += remaining_segment_length + vertices.append(segments[0][1]) + if len(segments) == 1: + break + + segments.pop(0) + + if remaining_distance_meters and total_duration_seconds is not None: + segment_start_time = start_time + timedelta( + seconds=(cumulative_length - remaining_distance_meters) + / total_length_meters * total_duration_seconds + ) + segment_end_time = start_time + timedelta( + seconds=cumulative_length + / total_length_meters * total_duration_seconds + ) + dt = TimeInterval(segment_start_time, segment_end_time) + vertices.append(segments[0][1]) + out.append(GeoLineString(vertices, dt=dt)) + + elif remaining_distance_meters and self.dt: + vertices.append(segments[0][1]) + out.append(GeoLineString(vertices), dt = self.dt) + + if remaining_distance_meters: + vertices.append(segments[0][1]) + out.append(GeoLineString(vertices)) + + return out + def to_geo_interface(self, **kwargs): return { **self.__geo_interface__, From 5fd48c666934e15aea16924d3090f216f402efb5 Mon Sep 17 00:00:00 2001 From: Rkleisley <63410265+Rkleisley@users.noreply.github.com> Date: Thu, 9 Jan 2025 20:45:39 -0500 Subject: [PATCH 02/11] Update structures.py --- geostructures/structures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geostructures/structures.py b/geostructures/structures.py index 286d65f..b9fe2c9 100644 --- a/geostructures/structures.py +++ b/geostructures/structures.py @@ -1479,7 +1479,7 @@ def intersects_shape(self, shape: 'GeoShape', **kwargs) -> bool: # because the centroid may fall in a hole return o_edges[0][0][0] in self or s_edges[0][0][0] in shape - @staticmethod + @staticmethod def split(self, distance_meters: float) -> List[GeoLineString]: """ Splits a GeoLineString into smaller GeoLineStrings of equal length while also dividing From f3f06c463cd6844048a987778f0c8bc7a6a2a116 Mon Sep 17 00:00:00 2001 From: Robert Kleisley Date: Thu, 9 Jan 2025 20:58:40 -0500 Subject: [PATCH 03/11] Added split function to geolinestring which creates more segments based on distance --- geostructures/structures.py | 93 ++++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/geostructures/structures.py b/geostructures/structures.py index 5c2d678..77e28bd 100644 --- a/geostructures/structures.py +++ b/geostructures/structures.py @@ -10,6 +10,7 @@ from abc import ABC import copy +from datetime import timedelta from functools import cached_property import math import statistics @@ -23,7 +24,7 @@ _RE_LINESTRING_WKT, LineLikeMixin, PointLikeMixin, PolygonLikeMixin, SingleShapeBase, SimpleShapeMixin ) -from geostructures.time import GEOTIME_TYPE +from geostructures.time import GEOTIME_TYPE, TimeInterval from geostructures.coordinates import Coordinate from geostructures.calc import ( inverse_haversine_radians, @@ -1451,6 +1452,96 @@ def from_wkt( dt=dt, properties=properties, ) + + @staticmethod + def split(self, distance_meters: float) -> List['GeoLineString']: + """ + Splits a GeoLineString into smaller GeoLineStrings of equal length while also dividing + the associated time interval proportionally if it spans a range. If the time interval + is a single timestamp, time is not split. + + Args: + distance_meters (float): The desired length of each segment. + + Returns: + List[GeoLineString]: A list of GeoLineStrings, each with proportional time intervals if applicable. + """ + out = [] + cumulative_length = 0 + segments: List[Tuple[Coordinate, Coordinate]] = self.segments.copy() + vertices = [segments[0][0]] + remaining_distance_meters = distance_meters + + # Total line length + total_length_meters = sum(haversine_distance_meters(*segment) for segment in segments) + + start_time = end_time = total_duration_seconds = None + if self.dt and self.dt.start != self.dt.end: # Check for a valid time range + start_time, end_time = self.dt.start, self.dt.end + total_duration_seconds = (end_time - start_time).total_seconds() + + while segments: + remaining_segment_length = haversine_distance_meters(*segments[0]) + + while remaining_distance_meters < remaining_segment_length: + end_point = inverse_haversine_degrees( + vertices[-1], + bearing_degrees(vertices[-1], segments[0][1]), + remaining_distance_meters + ) + vertices.append(end_point) + cumulative_length += remaining_distance_meters + + # Calculate proportional time interval + dt = None + if total_duration_seconds is not None: + segment_start_time = start_time + timedelta(seconds= + (cumulative_length - remaining_distance_meters) + / total_length_meters * total_duration_seconds + ) + segment_end_time = start_time + timedelta(seconds= + cumulative_length + / total_length_meters * total_duration_seconds + ) + dt = TimeInterval(segment_start_time, segment_end_time) + + elif self.dt: + dt = self.dt + + out.append(GeoLineString(vertices), dt=dt) + vertices = [end_point] + remaining_segment_length = haversine_distance_meters(vertices[-1], segments[0][1]) + + remaining_distance_meters = remaining_distance_meters - remaining_segment_length + cumulative_length += remaining_segment_length + vertices.append(segments[0][1]) + if len(segments) == 1: + break + + segments.pop(0) + + if remaining_distance_meters and total_duration_seconds is not None: + segment_start_time = start_time + timedelta( + seconds=(cumulative_length - remaining_distance_meters) + / total_length_meters * total_duration_seconds + ) + segment_end_time = start_time + timedelta( + seconds=cumulative_length + / total_length_meters * total_duration_seconds + ) + dt = TimeInterval(segment_start_time, segment_end_time) + vertices.append(segments[0][1]) + out.append(GeoLineString(vertices, dt=dt)) + + elif remaining_distance_meters and self.dt: + vertices.append(segments[0][1]) + out.append(GeoLineString(vertices), dt = self.dt) + + if remaining_distance_meters: + vertices.append(segments[0][1]) + out.append(GeoLineString(vertices)) + + return out def intersects_shape(self, shape: 'GeoShape', **kwargs) -> bool: from geostructures.typing import MultiShape, PolygonLike, PointLike, LineLike From fc609c778166b06288749551e35fd5904199deca Mon Sep 17 00:00:00 2001 From: Robert Kleisley Date: Thu, 9 Jan 2025 21:20:05 -0500 Subject: [PATCH 04/11] Added split function to geolinestring which creates more segments based on distance --- geostructures/structures.py | 112 ++---------------------------------- 1 file changed, 5 insertions(+), 107 deletions(-) diff --git a/geostructures/structures.py b/geostructures/structures.py index ad59adc..8d379e3 100644 --- a/geostructures/structures.py +++ b/geostructures/structures.py @@ -1452,7 +1452,7 @@ def from_wkt( dt=dt, properties=properties, ) - + @staticmethod def split(self, distance_meters: float) -> List['GeoLineString']: """ @@ -1495,14 +1495,8 @@ def split(self, distance_meters: float) -> List['GeoLineString']: # Calculate proportional time interval dt = None if total_duration_seconds is not None: - segment_start_time = start_time + timedelta(seconds= - (cumulative_length - remaining_distance_meters) - / total_length_meters * total_duration_seconds - ) - segment_end_time = start_time + timedelta(seconds= - cumulative_length - / total_length_meters * total_duration_seconds - ) + segment_start_time = start_time + timedelta(seconds=(cumulative_length - remaining_distance_meters) / total_length_meters * total_duration_seconds) + segment_end_time = start_time + timedelta(seconds=cumulative_length / total_length_meters * total_duration_seconds) dt = TimeInterval(segment_start_time, segment_end_time) elif self.dt: @@ -1521,14 +1515,8 @@ def split(self, distance_meters: float) -> List['GeoLineString']: segments.pop(0) if remaining_distance_meters and total_duration_seconds is not None: - segment_start_time = start_time + timedelta( - seconds=(cumulative_length - remaining_distance_meters) - / total_length_meters * total_duration_seconds - ) - segment_end_time = start_time + timedelta( - seconds=cumulative_length - / total_length_meters * total_duration_seconds - ) + segment_start_time = start_time + timedelta(seconds=(cumulative_length - remaining_distance_meters) / total_length_meters * total_duration_seconds) + segment_end_time = start_time + timedelta(seconds=cumulative_length / total_length_meters * total_duration_seconds) dt = TimeInterval(segment_start_time, segment_end_time) vertices.append(segments[0][1]) out.append(GeoLineString(vertices, dt=dt)) @@ -1570,96 +1558,6 @@ def intersects_shape(self, shape: 'GeoShape', **kwargs) -> bool: # because the centroid may fall in a hole return o_edges[0][0][0] in self or s_edges[0][0][0] in shape - @staticmethod - def split(self, distance_meters: float) -> List[GeoLineString]: - """ - Splits a GeoLineString into smaller GeoLineStrings of equal length while also dividing - the associated time interval proportionally if it spans a range. If the time interval - is a single timestamp, time is not split. - - Args: - distance_meters (float): The desired length of each segment. - - Returns: - List[GeoLineString]: A list of GeoLineStrings, each with proportional time intervals if applicable. - """ - out = [] - cumulative_length = 0 - segments: List[Tuple[Coordinate, Coordinate]] = self.segments.copy() - vertices = [segments[0][0]] - remaining_distance_meters = distance_meters - - # Total line length - total_length_meters = sum(haversine_distance_meters(*segment) for segment in segments) - - start_time = end_time = total_duration_seconds = None - if self.dt and self.dt.start != self.dt.end: # Check for a valid time range - start_time, end_time = self.dt.start, self.dt.end - total_duration_seconds = (end_time - start_time).total_seconds() - - while segments: - remaining_segment_length = haversine_distance(*segments[0]) - - while remaining_distance_meters < remaining_segment_length: - end_point = inverse_haversine_degrees( - vertices[-1], - bearing_degrees(vertices[-1], segments[0][1]), - remaining_distance_meters - ) - vertices.append(end_point) - cumulative_length += remaining_distance_meters - - # Calculate proportional time interval - dt = None - if total_duration_seconds is not None: - segment_start_time = start_time + timedelta(seconds= - (cumulative_length - remaining_distance_meters) - / total_length_meters * total_duration_seconds - ) - segment_end_time = start_time + timedelta(seconds= - cumulative_length - / total_length_meters * total_duration_seconds - ) - dt = TimeInterval(segment_start_time, segment_end_time) - - elif self.dt: - dt = self.dt - - out.append(GeoLineString(vertices), dt=dt) - vertices = [end_point] - remaining_segment_length = haversine_distance_meters(vertices[-1], segments[0][1]) - - remaining_distance_meters = remaining_distance_meters - remaining_segment_length - cumulative_length += remaining_segment_length - vertices.append(segments[0][1]) - if len(segments) == 1: - break - - segments.pop(0) - - if remaining_distance_meters and total_duration_seconds is not None: - segment_start_time = start_time + timedelta( - seconds=(cumulative_length - remaining_distance_meters) - / total_length_meters * total_duration_seconds - ) - segment_end_time = start_time + timedelta( - seconds=cumulative_length - / total_length_meters * total_duration_seconds - ) - dt = TimeInterval(segment_start_time, segment_end_time) - vertices.append(segments[0][1]) - out.append(GeoLineString(vertices, dt=dt)) - - elif remaining_distance_meters and self.dt: - vertices.append(segments[0][1]) - out.append(GeoLineString(vertices), dt = self.dt) - - if remaining_distance_meters: - vertices.append(segments[0][1]) - out.append(GeoLineString(vertices)) - - return out - def to_geo_interface(self, **kwargs): return { **self.__geo_interface__, From 675e54215a2de34ac2d24a20cd12b536019387a5 Mon Sep 17 00:00:00 2001 From: Robert Kleisley Date: Thu, 9 Jan 2025 21:27:36 -0500 Subject: [PATCH 05/11] Updates to appease Flake8 --- geostructures/structures.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/geostructures/structures.py b/geostructures/structures.py index 8d379e3..841649b 100644 --- a/geostructures/structures.py +++ b/geostructures/structures.py @@ -1495,8 +1495,14 @@ def split(self, distance_meters: float) -> List['GeoLineString']: # Calculate proportional time interval dt = None if total_duration_seconds is not None: - segment_start_time = start_time + timedelta(seconds=(cumulative_length - remaining_distance_meters) / total_length_meters * total_duration_seconds) - segment_end_time = start_time + timedelta(seconds=cumulative_length / total_length_meters * total_duration_seconds) + segment_start = (cumulative_length - remaining_distance_meters) + segment_end = cumulative_length + segment_start_time = start_time + timedelta( + seconds=(segment_start / total_length_meters * total_duration_seconds) + ) + segment_end_time = start_time + timedelta( + seconds=(segment_end / total_length_meters * total_duration_seconds) + ) dt = TimeInterval(segment_start_time, segment_end_time) elif self.dt: @@ -1515,8 +1521,14 @@ def split(self, distance_meters: float) -> List['GeoLineString']: segments.pop(0) if remaining_distance_meters and total_duration_seconds is not None: - segment_start_time = start_time + timedelta(seconds=(cumulative_length - remaining_distance_meters) / total_length_meters * total_duration_seconds) - segment_end_time = start_time + timedelta(seconds=cumulative_length / total_length_meters * total_duration_seconds) + segment_start = (cumulative_length - remaining_distance_meters) + segment_end = cumulative_length + segment_start_time = start_time + timedelta( + seconds=(segment_start / total_length_meters * total_duration_seconds) + ) + segment_end_time = start_time + timedelta( + seconds=(segment_end / total_length_meters * total_duration_seconds) + ) dt = TimeInterval(segment_start_time, segment_end_time) vertices.append(segments[0][1]) out.append(GeoLineString(vertices, dt=dt)) From 05cbdb2ee0b24c878f7dd50042a4bb6edf2aed31 Mon Sep 17 00:00:00 2001 From: Robert Kleisley Date: Thu, 9 Jan 2025 21:31:40 -0500 Subject: [PATCH 06/11] corrected typo --- geostructures/structures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geostructures/structures.py b/geostructures/structures.py index 841649b..7e448a8 100644 --- a/geostructures/structures.py +++ b/geostructures/structures.py @@ -1535,7 +1535,7 @@ def split(self, distance_meters: float) -> List['GeoLineString']: elif remaining_distance_meters and self.dt: vertices.append(segments[0][1]) - out.append(GeoLineString(vertices), dt = self.dt) + out.append(GeoLineString(vertices, dt=self.dt)) if remaining_distance_meters: vertices.append(segments[0][1]) From eb38b21b6132da3ddfb47586876d10eced0ad562 Mon Sep 17 00:00:00 2001 From: Robert Kleisley Date: Fri, 17 Jan 2025 11:32:26 -0500 Subject: [PATCH 07/11] Updates to split with additional error handleing --- geostructures/structures.py | 183 +++++++++++++++++++++--------------- 1 file changed, 107 insertions(+), 76 deletions(-) diff --git a/geostructures/structures.py b/geostructures/structures.py index 7e448a8..bd98bb2 100644 --- a/geostructures/structures.py +++ b/geostructures/structures.py @@ -1453,122 +1453,153 @@ def from_wkt( properties=properties, ) - @staticmethod + def intersects_shape(self, shape: 'GeoShape', **kwargs) -> bool: + from geostructures.typing import MultiShape, PolygonLike, PointLike, LineLike + + if isinstance(shape, MultiShape): + for subshape in shape.geoshapes: + if self.intersects_shape(subshape, **kwargs): + return True + + return False + + if isinstance(shape, PointLike): + return shape in self + + s_edges = [self.segments] + o_edges = shape.edges(**kwargs) if isinstance(shape, PolygonLike) else [cast(LineLike, shape).segments] + if do_edges_intersect( + [x for edge_ring in s_edges for x in edge_ring], + [x for edge_ring in o_edges for x in edge_ring] + ): + # At least one edge pair intersects + return True + + # If no edges intersect, one shape could still contain the other + # which counts as intersection. Have to use a point from the boundary + # because the centroid may fall in a hole + return o_edges[0][0][0] in self or s_edges[0][0][0] in shape + def split(self, distance_meters: float) -> List['GeoLineString']: """ - Splits a GeoLineString into smaller GeoLineStrings of equal length while also dividing - the associated time interval proportionally if it spans a range. If the time interval - is a single timestamp, time is not split. + Splits a GeoLineString into smaller segments based on a specified distance. Args: - distance_meters (float): The desired length of each segment. + distance_meters (float): The maximum distance for each segment in meters. Returns: - List[GeoLineString]: A list of GeoLineStrings, each with proportional time intervals if applicable. + List[GeoLineString]: A list of GeoLineString objects, each of which is no longer + than the specified distance. If the total length of the + line is less than the specified distance, the original line + is returned as a single segment. + + Notes: + - If the GeoLineString is time-bounded (has a datetime interval), the resulting + segments will have proportional datetime intervals based on the segment's length. + - If the specified distance is greater than the total length of the line, + a warning is issued, and the original line is returned. """ - out = [] - cumulative_length = 0 - segments: List[Tuple[Coordinate, Coordinate]] = self.segments.copy() - vertices = [segments[0][0]] - remaining_distance_meters = distance_meters - # Total line length - total_length_meters = sum(haversine_distance_meters(*segment) for segment in segments) + out = [] # List to store resulting GeoLineString segments + segments: List[Tuple[Coordinate, Coordinate]] = self.segments.copy() # Copy of all line segments + vertices = [segments[0][0]] # Initialize the first vertex from the starting point of the first segment + remaining_distance_meters = None # Remaining distance from a previous iteration + total_length_meters = sum(haversine_distance_meters(*segment) for segment in segments) # Total line length + + # Handle case where the total line length is less than the specified distance + if total_length_meters <= distance_meters: + warnings.warn( + f'Total length ({total_length_meters}) is less than the distance ({distance_meters}); returning line.' + ) + return [self] + + properties = self._properties.copy() # Copy of properties for the GeoLineString + cumulative_length = 0 # Tracks cumulative length traversed + dt = None # Placeholder for datetime interval for each segment start_time = end_time = total_duration_seconds = None - if self.dt and self.dt.start != self.dt.end: # Check for a valid time range + # If time interval is provided, calculate total duration in seconds + if self.dt and self.dt.start != self.dt.end: start_time, end_time = self.dt.start, self.dt.end total_duration_seconds = (end_time - start_time).total_seconds() + # Iterate through segments and split accordingly while segments: - remaining_segment_length = haversine_distance_meters(*segments[0]) + remaining_segment_length = haversine_distance(*segments[0]) # Length of the current segment - while remaining_distance_meters < remaining_segment_length: + if remaining_distance_meters is not None: + # If there's a remaining distance from the previous iteration, process it + cumulative_length += distance_meters - remaining_distance_meters end_point = inverse_haversine_degrees( vertices[-1], bearing_degrees(vertices[-1], segments[0][1]), remaining_distance_meters ) - vertices.append(end_point) - cumulative_length += remaining_distance_meters + # Calculate the time interval proportionally if applicable + if total_duration_seconds is not None: + segment_start_time = start_time + timedelta( + seconds=cumulative_length / total_length_meters * total_duration_seconds + ) + dt = TimeInterval(dt.end, segment_start_time) + elif self.dt: + dt = self.dt - # Calculate proportional time interval - dt = None + vertices.append(end_point) # Add the calculated endpoint to vertices + out.append(GeoLineString(vertices), properties=properties.copy(), dt=dt) # Store the segment + vertices = [end_point] # Reset vertices for the next segment + remaining_segment_length = haversine_distance_meters(vertices[-1], segments[0][1]) + remaining_distance_meters = None + + while distance_meters < remaining_segment_length: + # Handle cases where the current segment is longer than the specified distance + cumulative_length += distance_meters + end_point = inverse_haversine_degrees( + vertices[-1], + bearing_degrees(vertices[-1], segments[0][1]), + distance_meters + ) + # Calculate the time interval proportionally if applicable if total_duration_seconds is not None: - segment_start = (cumulative_length - remaining_distance_meters) - segment_end = cumulative_length segment_start_time = start_time + timedelta( - seconds=(segment_start / total_length_meters * total_duration_seconds) + seconds=(cumulative_length - remaining_distance_meters) + / total_length_meters * total_duration_seconds ) segment_end_time = start_time + timedelta( - seconds=(segment_end / total_length_meters * total_duration_seconds) + seconds=cumulative_length / total_length_meters * total_duration_seconds ) dt = TimeInterval(segment_start_time, segment_end_time) - elif self.dt: dt = self.dt - out.append(GeoLineString(vertices), dt=dt) - vertices = [end_point] + vertices.append(end_point) # Add the calculated endpoint to vertices + out.append(GeoLineString(vertices), properties=properties.copy(), dt=dt) # Store the segment + vertices = [end_point] # Reset vertices for the next segment remaining_segment_length = haversine_distance_meters(vertices[-1], segments[0][1]) + # Calculate the remaining distance after processing the current segment remaining_distance_meters = remaining_distance_meters - remaining_segment_length - cumulative_length += remaining_segment_length - vertices.append(segments[0][1]) + cumulative_length += remaining_distance_meters + vertices.append(segments[0][1]) # Add the endpoint of the current segment + if len(segments) == 1: break - segments.pop(0) - - if remaining_distance_meters and total_duration_seconds is not None: - segment_start = (cumulative_length - remaining_distance_meters) - segment_end = cumulative_length - segment_start_time = start_time + timedelta( - seconds=(segment_start / total_length_meters * total_duration_seconds) - ) - segment_end_time = start_time + timedelta( - seconds=(segment_end / total_length_meters * total_duration_seconds) - ) - dt = TimeInterval(segment_start_time, segment_end_time) - vertices.append(segments[0][1]) - out.append(GeoLineString(vertices, dt=dt)) - - elif remaining_distance_meters and self.dt: - vertices.append(segments[0][1]) - out.append(GeoLineString(vertices, dt=self.dt)) + segments.pop(0) # Remove the processed segment + # Handle the final segment if there are remaining distances if remaining_distance_meters: - vertices.append(segments[0][1]) - out.append(GeoLineString(vertices)) - - return out - - def intersects_shape(self, shape: 'GeoShape', **kwargs) -> bool: - from geostructures.typing import MultiShape, PolygonLike, PointLike, LineLike - - if isinstance(shape, MultiShape): - for subshape in shape.geoshapes: - if self.intersects_shape(subshape, **kwargs): - return True - - return False - - if isinstance(shape, PointLike): - return shape in self + last_segment = out.pop() if out else None + if last_segment: + vertices = last_segment.vertices[:-1] + [vertices[-1]] + # Assign the time interval for the final segment if applicable + if total_duration_seconds is not None: + dt = TimeInterval(last_segment.dt.start, end_time) + elif self.dt: + dt = self.dt - s_edges = [self.segments] - o_edges = shape.edges(**kwargs) if isinstance(shape, PolygonLike) else [cast(LineLike, shape).segments] - if do_edges_intersect( - [x for edge_ring in s_edges for x in edge_ring], - [x for edge_ring in o_edges for x in edge_ring] - ): - # At least one edge pair intersects - return True + out.append(GeoLineString(vertices, properties=properties.copy(), dt=dt)) - # If no edges intersect, one shape could still contain the other - # which counts as intersection. Have to use a point from the boundary - # because the centroid may fall in a hole - return o_edges[0][0][0] in self or s_edges[0][0][0] in shape + return out def to_geo_interface(self, **kwargs): return { From 14bdc0e2d4f204ebc2cdec3951206c6fba364703 Mon Sep 17 00:00:00 2001 From: Robert Kleisley Date: Fri, 17 Jan 2025 11:58:34 -0500 Subject: [PATCH 08/11] Added tests for .split() --- tests/test_structures.py | 68 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/test_structures.py b/tests/test_structures.py index 1983e08..4dcf077 100644 --- a/tests/test_structures.py +++ b/tests/test_structures.py @@ -1593,3 +1593,71 @@ def test_geopoint_from_wkt(): def test_geopoint_to_wkt(geopoint): assert geopoint.to_wkt() == 'POINT(0.0 0.0)' + +@pytest.fixture +def basic_line(): + # A basic GeoLineString without time + return GeoLineString([ + Coordinate(0, 0), + Coordinate(1, 1), + Coordinate(2, 2) + ]) + + +@pytest.fixture +def timed_line(): + # A GeoLineString with a time interval + return GeoLineString( + [ + Coordinate(0, 0), + Coordinate(1, 1), + Coordinate(2, 2) + ], + dt=TimeInterval( + start=datetime(2025, 1, 1, 0, 0, 0), + end=datetime(2025, 1, 1, 2, 0, 0) + ) + ) + + +def test_split_no_split_needed(basic_line): + # Distance is greater than the total length of the line + result = basic_line.split(5000) + assert len(result) == 1 + assert result[0] == basic_line + + +def test_split_even_segments(basic_line): + # Splitting into two equal segments + result = basic_line.split(157249) # Approx distance between (0,0) and (1,1) + assert len(result) == 2 + assert result[0].vertices[-1] == Coordinate(1, 1) + + +def test_split_with_remainder(basic_line): + # Distance doesn't evenly divide the total length + result = basic_line.split(200000) + assert len(result) > 1 + assert all(len(segment.vertices) > 1 for segment in result) + + +def test_split_with_time_intervals(timed_line): + # Ensure time intervals are proportional + result = timed_line.split(157249) + assert len(result) == 2 + assert result[0].dt.start == timed_line.dt.start + assert result[1].dt.end == timed_line.dt.end + assert result[0].dt.end < result[1].dt.start + + +def test_warning_on_large_distance(basic_line): + # Warning if the split distance exceeds the total length + with pytest.warns(UserWarning): + result = basic_line.split(5000) + assert len(result) == 1 + + +def test_split_exact_division(basic_line): + # Distance perfectly divides the line + result = basic_line.split(157249 * 2) + assert len(result) == 1 \ No newline at end of file From 84655417ea5e3fb0c5ad9e5d3c04a3d2f9711c9a Mon Sep 17 00:00:00 2001 From: Robert Kleisley Date: Fri, 17 Jan 2025 11:59:12 -0500 Subject: [PATCH 09/11] Updated imports --- geostructures/structures.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/geostructures/structures.py b/geostructures/structures.py index bd98bb2..7afba9c 100644 --- a/geostructures/structures.py +++ b/geostructures/structures.py @@ -15,6 +15,7 @@ import math import statistics from typing import cast, Any, Dict, List, Optional, Tuple, Sequence, TYPE_CHECKING +import warnings import numpy as np @@ -1525,7 +1526,7 @@ def split(self, distance_meters: float) -> List['GeoLineString']: # Iterate through segments and split accordingly while segments: - remaining_segment_length = haversine_distance(*segments[0]) # Length of the current segment + remaining_segment_length = haversine_distance_meters(*segments[0]) # Length of the current segment if remaining_distance_meters is not None: # If there's a remaining distance from the previous iteration, process it From fffcc6803ca45ad385a46fed8a66ce8acc570b1e Mon Sep 17 00:00:00 2001 From: Robert Kleisley Date: Fri, 17 Jan 2025 13:36:52 -0500 Subject: [PATCH 10/11] Added tests for split --- tests/test_structures.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/tests/test_structures.py b/tests/test_structures.py index 4dcf077..137edfa 100644 --- a/tests/test_structures.py +++ b/tests/test_structures.py @@ -1622,21 +1622,31 @@ def timed_line(): def test_split_no_split_needed(basic_line): # Distance is greater than the total length of the line - result = basic_line.split(5000) + result = basic_line.split(200000) assert len(result) == 1 assert result[0] == basic_line def test_split_even_segments(basic_line): - # Splitting into two equal segments - result = basic_line.split(157249) # Approx distance between (0,0) and (1,1) + # Splitting into segments close to half the total length + half_length = 157237.40665500844 # Approximate 1/2 total length of line + result = basic_line.split(half_length) + + # Check if the segments are created assert len(result) == 2 - assert result[0].vertices[-1] == Coordinate(1, 1) + + # Validate the first segment + assert result[0].vertices[0] == basic_line.vertices[0] + assert result[0].vertices[-1].longitude != basic_line.vertices[-1].longitude # Shouldn't reach the end + + # Validate the second segment starts where the first ended + assert result[1].vertices[0] == result[0].vertices[-1] + assert result[1].vertices[-1] == basic_line.vertices[-1] def test_split_with_remainder(basic_line): # Distance doesn't evenly divide the total length - result = basic_line.split(200000) + result = basic_line.split(5000) assert len(result) > 1 assert all(len(segment.vertices) > 1 for segment in result) @@ -1647,13 +1657,15 @@ def test_split_with_time_intervals(timed_line): assert len(result) == 2 assert result[0].dt.start == timed_line.dt.start assert result[1].dt.end == timed_line.dt.end - assert result[0].dt.end < result[1].dt.start + assert result[0].dt.end == result[1].dt.start + assert result[0].dt.end < result[1].dt.end + assert result[0].dt.start < result[1].dt.start def test_warning_on_large_distance(basic_line): # Warning if the split distance exceeds the total length with pytest.warns(UserWarning): - result = basic_line.split(5000) + result = basic_line.split(400000) assert len(result) == 1 From 545bbc4fc030ec0c77a727a29fffe25d5d3c9fa1 Mon Sep 17 00:00:00 2001 From: Robert Kleisley Date: Fri, 17 Jan 2025 13:37:39 -0500 Subject: [PATCH 11/11] Tests are useful corrected lines where distance_meters had been replaced with remaining_distance_meters --- geostructures/structures.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/geostructures/structures.py b/geostructures/structures.py index 7afba9c..152aa91 100644 --- a/geostructures/structures.py +++ b/geostructures/structures.py @@ -1546,7 +1546,7 @@ def split(self, distance_meters: float) -> List['GeoLineString']: dt = self.dt vertices.append(end_point) # Add the calculated endpoint to vertices - out.append(GeoLineString(vertices), properties=properties.copy(), dt=dt) # Store the segment + out.append(GeoLineString(vertices, properties=properties.copy(), dt=dt)) # Store the segment vertices = [end_point] # Reset vertices for the next segment remaining_segment_length = haversine_distance_meters(vertices[-1], segments[0][1]) remaining_distance_meters = None @@ -1562,7 +1562,7 @@ def split(self, distance_meters: float) -> List['GeoLineString']: # Calculate the time interval proportionally if applicable if total_duration_seconds is not None: segment_start_time = start_time + timedelta( - seconds=(cumulative_length - remaining_distance_meters) + seconds=(cumulative_length - distance_meters) / total_length_meters * total_duration_seconds ) segment_end_time = start_time + timedelta( @@ -1573,12 +1573,12 @@ def split(self, distance_meters: float) -> List['GeoLineString']: dt = self.dt vertices.append(end_point) # Add the calculated endpoint to vertices - out.append(GeoLineString(vertices), properties=properties.copy(), dt=dt) # Store the segment + out.append(GeoLineString(vertices, properties=properties.copy(), dt=dt)) # Store the segment vertices = [end_point] # Reset vertices for the next segment remaining_segment_length = haversine_distance_meters(vertices[-1], segments[0][1]) # Calculate the remaining distance after processing the current segment - remaining_distance_meters = remaining_distance_meters - remaining_segment_length + remaining_distance_meters = distance_meters - remaining_segment_length cumulative_length += remaining_distance_meters vertices.append(segments[0][1]) # Add the endpoint of the current segment