Repository: martinfleis/momepy
Pull request #156 (2020-03-30 16:00:25)

ENH: Orientation for LineString geometry
`Orientation` now supports LineString geometry as well.

codecov[bot]: # [Codecov](https://codecov.io/gh/martinfleis/momepy/pull/156?src=pr&el=h1) Report
> Merging [#156](https://codecov.io/gh/martinfleis/momepy/pull/156?src=pr&el=desc) into [master](https://codecov.io/gh/martinfleis/momepy/commit/12d023c302e971967b0b3b51df6059ed0ac4505a&el=desc) will **decrease** coverage by `0.10%`.
> The diff coverage is `96.66%`.

[![Impacted file tree graph](https://codecov.io/gh/martinfleis/momepy/pull/156/graphs/tree.svg?width=650&height=150&src=pr&token=i04999UxkK)](https://codecov.io/gh/martinfleis/momepy/pull/156?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #156      +/-   ##
==========================================
- Coverage   93.67%   93.57%   -0.11%     
==========================================
  Files           9        9              
  Lines        2499     2460      -39     
==========================================
- Hits         2341     2302      -39     
  Misses        158      158              
```


| [Impacted Files](https://codecov.io/gh/martinfleis/momepy/pull/156?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [momepy/distribution.py](https://codecov.io/gh/martinfleis/momepy/pull/156/diff?src=pr&el=tree#diff-bW9tZXB5L2Rpc3RyaWJ1dGlvbi5weQ==) | `98.17% <96.66%> (-0.23%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/martinfleis/momepy/pull/156?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/martinfleis/momepy/pull/156?src=pr&el=footer). Last update [12d023c...95a6107](https://codecov.io/gh/martinfleis/momepy/pull/156?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/momepy/distribution.py b/momepy/distribution.py
index eb77feb..bcba2f5 100644
--- a/momepy/distribution.py
+++ b/momepy/distribution.py
@@ -9,7 +9,6 @@ import statistics
 
 import numpy as np
 import pandas as pd
-from shapely.geometry import Point
 from tqdm import tqdm  # progress bar
 
 from .utils import _azimuth
@@ -33,8 +32,10 @@ class Orientation:
     """
     Calculate the orientation of object
 
+    Captures the deviation of orientation from cardinal directions.
     Defined as an orientation of the longext axis of bounding rectangle in range 0 - 45.
-    It captures the deviation of orientation from cardinal directions.
+    Orientation of LineStrings is represented by the orientation of line
+    connecting first and last point of the segment.
 
     Parameters
     ----------
@@ -70,48 +71,38 @@ class Orientation:
             return math.hypot(b[0] - a[0], b[1] - a[1])
 
         for geom in tqdm(gdf.geometry, total=gdf.shape[0]):
-            # TODO: vectorize once minimum_rotated_rectangle is in geopandas from pygeos
-            bbox = list(geom.minimum_rotated_rectangle.exterior.coords)
-            axis1 = _dist(bbox[0], bbox[3])
-            axis2 = _dist(bbox[0], bbox[1])
-
-            if axis1 <= axis2:
-                az = _azimuth(bbox[0], bbox[1])
-                if 90 > az >= 45:
-                    diff = az - 45
-                    az = az - 2 * diff
-                elif 135 > az >= 90:
-                    diff = az - 90
-                    az = az - 2 * diff
-                    diff = az - 45
-                    az = az - 2 * diff
-                elif 181 > az >= 135:
-                    diff = az - 135
-                    az = az - 2 * diff
-                    diff = az - 90
-                    az = az - 2 * diff
-                    diff = az - 45
-                    az = az - 2 * diff
-                results_list.append(az)
+            if geom.type in ["Polygon", "MultiPolygon", "LinearRing"]:
+                # TODO: vectorize once minimum_rotated_rectangle is in geopandas from pygeos
+                bbox = list(geom.minimum_rotated_rectangle.exterior.coords)
+                axis1 = _dist(bbox[0], bbox[3])
+                axis2 = _dist(bbox[0], bbox[1])
+
+                if axis1 <= axis2:
+                    az = _azimuth(bbox[0], bbox[1])
+                else:
+                    az = _azimuth(bbox[0], bbox[3])
+            elif geom.type in ["LineString", "MultiLineString"]:
+                coords = geom.coords
+                az = _azimuth(coords[0], coords[-1])
             else:
-                az = 170
-                az = _azimuth(bbox[0], bbox[3])
-                if 90 > az >= 45:
-                    diff = az - 45
-                    az = az - 2 * diff
-                elif 135 > az >= 90:
-                    diff = az - 90
-                    az = az - 2 * diff
-                    diff = az - 45
-                    az = az - 2 * diff
-                elif 181 > az >= 135:
-                    diff = az - 135
-                    az = az - 2 * diff
-                    diff = az - 90
-                    az = az - 2 * diff
-                    diff = az - 45
-                    az = az - 2 * diff
-                results_list.append(az)
+                results_list.append(np.nan)
+
+            if 90 > az >= 45:
+                diff = az - 45
+                az = az - 2 * diff
+            elif 135 > az >= 90:
+                diff = az - 90
+                az = az - 2 * diff
+                diff = az - 45
+                az = az - 2 * diff
+            elif 181 > az >= 135:
+                diff = az - 135
+                az = az - 2 * diff
+                diff = az - 90
+                az = az - 2 * diff
+                diff = az - 45
+                az = az - 2 * diff
+            results_list.append(az)
 
         self.series = pd.Series(results_list, index=gdf.index)
 
@@ -227,7 +218,7 @@ class StreetAlignment:
         GeoDataFrame containing street network
     orientations : str, list, np.array, pd.Series
         the name of the dataframe column, np.array, or pd.Series where is stored object orientation value
-        (can be calculated using :py:func:`momepy.orientation`)
+        (can be calculated using :py:func:`momepy.Orientation`)
     network_id : str (default None)
         the name of the column storing network ID in both left and right
     left_network_id : str, list, np.array, pd.Series (default None)
@@ -272,9 +263,6 @@ class StreetAlignment:
         self.right = right
         self.network_id = network_id
 
-        # define empty list for results
-        results_list = []
-
         left = left.copy()
         right = right.copy()
 
@@ -305,40 +293,16 @@ class StreetAlignment:
             right_network_id = "mm_nis"
         self.right_network_id = right[right_network_id]
 
-        index_keep = left.index
-        right = right.set_index(right_network_id)
-        left = left.set_index(left_network_id)
+        right["_orientation"] = Orientation(right).series
 
-        geomcol = right._geometry_column_name
+        merged = left[[left_network_id, orientations]].merge(
+            right[[right_network_id, "_orientation"]],
+            left_on=left_network_id,
+            right_on=right_network_id,
+            how="left",
+        )
 
-        # iterating over rows one by one
-        for nid, orientation in tqdm(
-            left[orientations].iteritems(), total=left.shape[0]
-        ):
-            if pd.isnull(nid):
-                results_list.append(0)
-            else:
-                streetssub = right.at[nid, geomcol].coords
-                start = streetssub[0]
-                end = streetssub[-1]
-                az = _azimuth(start, end)
-                if 90 > az >= 45:
-                    diff = az - 45
-                    az = az - 2 * diff
-                elif 135 > az >= 90:
-                    diff = az - 90
-                    az = az - 2 * diff
-                    diff = az - 45
-                    az = az - 2 * diff
-                elif 181 > az >= 135:
-                    diff = az - 135
-                    az = az - 2 * diff
-                    diff = az - 90
-                    az = az - 2 * diff
-                    diff = az - 45
-                    az = az - 2 * diff
-                results_list.append(abs(orientation - az))
-        self.series = pd.Series(results_list, index=index_keep)
+        self.series = np.abs(merged[orientations] - merged["_orientation"])
 
 
 class CellAlignment:


Test patch:
diff --git a/tests/test_distribution.py b/tests/test_distribution.py
index d9282bc..7ac1993 100644
--- a/tests/test_distribution.py
+++ b/tests/test_distribution.py
@@ -24,6 +24,10 @@ class TestDistribution:
         check = 41.05146788287027
         assert self.df_buildings["orient"][0] == pytest.approx(check)
 
+        self.df_streets["orient"] = mm.Orientation(self.df_streets).series
+        check = 40.7607
+        assert self.df_streets["orient"][0] == pytest.approx(check)
+
     def test_SharedWallsRatio(self):
         self.df_buildings["swr"] = mm.SharedWallsRatio(self.df_buildings, "uID").series
         self.df_buildings["swr_uid"] = mm.SharedWallsRatio(