deoverlap 3.2.1__tar.gz → 3.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deoverlap
3
- Version: 3.2.1
3
+ Version: 3.2.2
4
4
  Summary: De-overlap Shapely geometries that sit within a tolerance of each other.
5
5
  Author-email: Pietro Leoni <pietro.leoni@gmail.com>
6
6
  License: MIT License
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "deoverlap"
9
- version = "3.2.1"
9
+ version = "3.2.2"
10
10
  authors = [
11
11
  { name="Pietro Leoni", email="pietro.leoni@gmail.com" },
12
12
  ]
@@ -1,6 +1,6 @@
1
1
  """De-overlap Shapely geometries that sit within a tolerance of each other."""
2
2
 
3
- __version__ = "3.2.1"
3
+ __version__ = "3.2.2"
4
4
 
5
5
  from .deoverlap import (
6
6
  ClipMode,
@@ -69,6 +69,7 @@ class _SegId:
69
69
  index: int
70
70
  count: int
71
71
  closed: bool
72
+ heading: float = 0.0 # direction of travel, radians
72
73
 
73
74
 
74
75
  @dataclass
@@ -189,6 +190,17 @@ def _seg_adjacent(a: _SegId, b: _SegId, window: int) -> bool:
189
190
  return False
190
191
 
191
192
 
193
+ def _heading(a: tuple[float, float], b: tuple[float, float]) -> float:
194
+ return math.atan2(b[1] - a[1], b[0] - a[0])
195
+
196
+
197
+ def _folds_back(a: _SegId, b: _SegId, angle_tol_rad: float) -> bool:
198
+ """True if the chain doubles back: headings nearly opposite (a hairpin)."""
199
+ d = abs(a.heading - b.heading) % (2 * math.pi)
200
+ d = min(d, 2 * math.pi - d)
201
+ return d > math.pi - angle_tol_rad
202
+
203
+
192
204
  def _reassemble(parts: Sequence[BaseGeometry], original: BaseGeometry) -> BaseGeometry:
193
205
  clean = [p for p in parts if p is not None and not p.is_empty]
194
206
  if not clean:
@@ -200,6 +212,10 @@ def _reassemble(parts: Sequence[BaseGeometry], original: BaseGeometry) -> BaseGe
200
212
  and merged.equals(original.boundary)
201
213
  ):
202
214
  return original
215
+ if merged.geom_type == "MultiLineString":
216
+ # Rejoin pieces that meet end to end: the arc through a ring's start
217
+ # vertex, or consecutive edges in segments mode.
218
+ merged = line_merge(merged)
203
219
  return merged
204
220
 
205
221
 
@@ -285,7 +301,7 @@ def _explode_segments(
285
301
  continue
286
302
  segments.append(LineString([a, b]))
287
303
  origins.append(origin)
288
- seg_ids.append(_SegId(chain, i, count, True))
304
+ seg_ids.append(_SegId(chain, i, count, True, _heading(a, b)))
289
305
  parent_lengths.append(parent_len)
290
306
  else:
291
307
  count = len(coords) - 1
@@ -295,7 +311,7 @@ def _explode_segments(
295
311
  continue
296
312
  segments.append(LineString([a, b]))
297
313
  origins.append(origin)
298
- seg_ids.append(_SegId(chain, i, count, False))
314
+ seg_ids.append(_SegId(chain, i, count, False, _heading(a, b)))
299
315
  parent_lengths.append(parent_len)
300
316
  chain += 1
301
317
 
@@ -407,6 +423,7 @@ class _MaskIndex:
407
423
  seg_id is not None
408
424
  and other_seg is not None
409
425
  and _seg_adjacent(seg_id, other_seg, self.segment_adjacency)
426
+ and not _folds_back(seg_id, other_seg, angle_tol_rad)
410
427
  ):
411
428
  continue
412
429
  if self.parallel_only and angle is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deoverlap
3
- Version: 3.2.1
3
+ Version: 3.2.2
4
4
  Summary: De-overlap Shapely geometries that sit within a tolerance of each other.
5
5
  Author-email: Pietro Leoni <pietro.leoni@gmail.com>
6
6
  License: MIT License
@@ -108,6 +108,35 @@ def test_group_keeps_split_ring_as_one_multipart():
108
108
  assert len(flat_result.kept) >= 2
109
109
 
110
110
 
111
+ def test_cut_ring_joins_across_its_start_point():
112
+ """The arc through the ring's first/last vertex is one piece, not two."""
113
+ ring = LineString([(0, 0), (2, 0), (2, 2), (0, 2), (0, 0)])
114
+ cutter = LineString([(1, -1), (1, 3)])
115
+ grouped = deoverlap([cutter, ring], 0.15, group=True, keep=KeepPolicy.FIRST)
116
+ assert len(flatten_geometries(grouped.kept_parts[1])) == 2
117
+ flat = deoverlap([cutter, ring], 0.15, group=False, keep=KeepPolicy.FIRST)
118
+ assert len(flat.kept) == 3 # cutter + two arcs
119
+
120
+
121
+ def test_segments_removes_fold_back_between_neighbours():
122
+ """A hairpin folds onto itself; neighbouring edges must still suppress."""
123
+ spike = LineString([(0, 0), (6, 0), (6.5, 0.4), (6, 0.08), (0, 0.08), (0, 0)])
124
+ result = deoverlap(
125
+ [spike], 0.1, segments=True, parallel_only=True, keep="first",
126
+ min_length=0.05, keep_duplicates=True,
127
+ )
128
+ kept = result.kept_parts[0]
129
+ back = LineString([(6.5, 0.4), (6, 0.08)])
130
+ assert kept.intersection(back.buffer(0.01)).length < 0.1
131
+
132
+
133
+ def test_segments_keeps_ordinary_corners():
134
+ """A 90 degree corner between neighbours is not a fold-back."""
135
+ square = LineString([(0, 0), (2, 0), (2, 2), (0, 2), (0, 0)])
136
+ result = deoverlap([square], 0.3, segments=True, parallel_only=True)
137
+ assert result.kept_parts[0].length == pytest.approx(square.length, abs=0.05)
138
+
139
+
111
140
  def test_parallel_only_preserves_crossing():
112
141
  horizontal = LineString([(0, 0), (4, 0)])
113
142
  vertical = LineString([(2, -2), (2, 2)])
File without changes
File without changes
File without changes