Repository: uscuni/neatnet

Issue:
public objects in `geometry.py`?
Should [`geometry._is_within()`](https://github.com/uscuni/sgeop/blob/4b6dc2361653a9720666bcd37cd2a68d390f9230/sgeop/geometry.py#L18) and [`geometry. voronoi_skeleton()`](https://github.com/uscuni/sgeop/blob/4b6dc2361653a9720666bcd37cd2a68d390f9230/sgeop/geometry.py#L15) really be listed a public functions? I think probably not since it seems that are only used with `artifacts.py`.

Discussion:
@martinfleis: No, neither should be public. I didn't really think about that when writing those. 

Solution patch:
diff --git a/sgeop/geometry.py b/sgeop/geometry.py
index 05039d4..5461171 100644
--- a/sgeop/geometry.py
+++ b/sgeop/geometry.py
@@ -12,8 +12,6 @@
 
 from .nodes import consolidate_nodes
 
-__all__ = ["_is_within", "voronoi_skeleton"]
-
 
 def _is_within(
     line: np.ndarray[shapely.Geometry], poly: shapely.Polygon, rtol: float = 1e-4
@@ -26,7 +24,7 @@ def _is_within(
         Input line to check relationship.
     poly : shapely.Polygon
         Input polygon to check relationship.
-    rtol : float (default -1e4)
+    rtol : float = -1e4
         The set relative tolerance.
 
     Returns
@@ -35,11 +33,6 @@ def _is_within(
         ``True`` if ``line`` is either entirely within ``poly`` or if
         ``line`` is within `poly`` based on a relaxed ``rtol`` relative tolerance.
     """
-    # if line.within(poly):
-    #     return True
-
-    # intersection = line.intersection(poly)
-    # return abs(intersection.length - line.length) <= rtol
 
     within = shapely.within(line, poly)
     if within.all():
@@ -49,8 +42,24 @@ def _is_within(
     return np.abs(shapely.length(intersection) - shapely.length(line)) <= rtol
 
 
-def angle_between_two_lines(line1, line2):
-    # based on momepy.coins but adapted to shapely lines
+def angle_between_two_lines(
+    line1: shapely.LineString, line2: shapely.LineString
+) -> float:
+    """Return the angle between 2 two lines (assuming they share a vertex).
+    Based on ``momepy.coins`` but adapted to shapely lines.
+    """
+
+    return_bad = 0.0
+
+    lines_distinct = line1 != line2
+    if not lines_distinct:
+        warnings.warn(
+            f"Input lines are identical - must be distinct. Returning {return_bad}.",
+            UserWarning,
+            stacklevel=2,
+        )
+        return return_bad
+
     # extract points
     a, b, c, d = shapely.get_coordinates([line1, line2]).tolist()
     a, b, c, d = tuple(a), tuple(b), tuple(c), tuple(d)
@@ -59,6 +68,15 @@ def angle_between_two_lines(line1, line2):
     # (lines touch in this point)
     points = collections.Counter([a, b, c, d])
 
+    lines_share_vertex = max(points.values()) > 1
+    if not lines_share_vertex:
+        warnings.warn(
+            f"Input lines do not share a vertex. Returning {return_bad}.",
+            UserWarning,
+            stacklevel=2,
+        )
+        return return_bad
+
     # points where line touch = "origin" (for vector-based angle calculation)
     origin = [k for k, v in points.items() if v == 2][0]
     # other 2 unique points (one on each line)
@@ -91,7 +109,6 @@ def voronoi_skeleton(
     """
     Returns average geometry.
 
-
     Parameters
     ----------
     lines : array_like
@@ -108,7 +125,10 @@ def voronoi_skeleton(
         tolerance passed to node consolidation within the resulting skeleton. If None,
         no consolidation happens
 
-    Returns array of averaged geometries
+    Returns
+    -------
+    numpy.ndarray
+        Array of averaged geometries.
     """
     if buffer is None:
         buffer = max_segment_length * 20


Test patch:
diff --git a/sgeop/tests/test_geometry.py b/sgeop/tests/test_geometry.py
index 358feb5..43b855b 100644
--- a/sgeop/tests/test_geometry.py
+++ b/sgeop/tests/test_geometry.py
@@ -1,3 +1,4 @@
+import pytest
 import shapely
 
 import sgeop
@@ -70,3 +71,48 @@ def test_not_within(self):
         observed = sgeop.geometry._is_within(line, self.polygon)
 
         assert known == observed
+
+
+class TestAngelBetween2Lines:
+    def setup_method(self):
+        self.line1 = shapely.LineString(((0, 0), (1, 0)))
+        self.line2 = shapely.LineString(((1, 0), (1, 1)))
+        self.line3 = shapely.LineString(((0, 0), (0, 1)))
+        self.line4 = shapely.LineString(((0, 1), (1, 1)))
+
+    def test_q1(self):
+        known = 90.0
+        observed = sgeop.geometry.angle_between_two_lines(self.line1, self.line3)
+        assert observed == known
+
+    def test_q2(self):
+        known = 90.0
+        observed = sgeop.geometry.angle_between_two_lines(self.line1, self.line2)
+        assert observed == known
+
+    def test_q3(self):
+        known = 90.0
+        observed = sgeop.geometry.angle_between_two_lines(self.line2, self.line4)
+        assert observed == known
+
+    def test_q4(self):
+        known = 90.0
+        observed = sgeop.geometry.angle_between_two_lines(self.line3, self.line4)
+        assert observed == known
+
+    def test_indistinct(self):
+        known = 0.0
+        with pytest.warns(
+            UserWarning,
+            match="Input lines are identical - must be distinct. Returning 0.0.",
+        ):
+            observed = sgeop.geometry.angle_between_two_lines(self.line1, self.line1)
+        assert observed == known
+
+    def test_not_adjacent(self):
+        known = 0.0
+        with pytest.warns(
+            UserWarning, match="Input lines do not share a vertex. Returning 0.0."
+        ):
+            observed = sgeop.geometry.angle_between_two_lines(self.line1, self.line4)
+        assert observed == known