Repository: mattijn/topojson

Issue:
Creating a topology for data without junctions and shared_coords=False, prequantize=False gives error
Creating a topology for data that has no junctions with shared_coords=False, prequantize=False gives the following error:
`AttributeError: 'LineString' object has no attribute 'tolist'`

![image](https://user-images.githubusercontent.com/1565789/187900689-9cd28809-74d1-4c47-bf47-73789d30d9e8.png)

I already had a look and will post a PR with a fix soon...

Discussion:
mattijn: Thanks for the PR! I had to manually start the GitHub Actions workflow, but it seems some tests did not pass with this change. I do not think that that is intended?

Patch:
diff --git a/topojson/core/cut.py b/topojson/core/cut.py
index f4b1826..66b9b0a 100644
--- a/topojson/core/cut.py
+++ b/topojson/core/cut.py
@@ -115,25 +115,35 @@ class Cut(Join):
                 warnings.filterwarnings("ignore", category=ShapelyDeprecationWarning)
                 tree_splitter = STRtree(mp)
             slist = []
+
+            # create dict with original geometry type per linestring
+            linestring_object_types = self._get_linestring_types(data)
+
             # junctions are only existing in coordinates of linestring
             if self.options.shared_coords:
-                for ls in data["linestrings"]:
+                for index, ls in enumerate(data["linestrings"]):
                     line, splitter = np_array_bbox_points_line(ls, tree_splitter)
-                    # prev function returns None for splitter if there is nothing to split
+                    # prev function returns None for splitter if nothing to split
                     if splitter is not None:
-                        slines = fast_split(line, splitter)
+                        is_ring = False
+                        if linestring_object_types[index] in ["Polygon", "MultiPolygon"]:
+                            is_ring = True
+                        slines = fast_split(line, splitter, is_ring)
                         slist.append(slines)
                     else:
                         slist.append(np.array([ls.coords]))
 
             # junctions can exist between existing coords of linestring
             else:
-                for ls in data["linestrings"]:
+                for index, ls in enumerate(data["linestrings"]):
                     # slines = split(ls, mp)
                     line, splitter = insert_coords_in_line(ls, tree_splitter)
-                    # prev function returns None for splitter if there is nothing to split
+                    # prev function returns None for splitter if nothing to split
                     if splitter is not None:
-                        slines = fast_split(line, splitter)
+                        is_ring = False
+                        if linestring_object_types[index] in ["Polygon", "MultiPolygon"]:
+                            is_ring = True
+                        slines = fast_split(line, splitter, is_ring)
                         slist.append(slines)
                     else:
                         slist.append(np.array([ls.coords]))
@@ -149,12 +159,12 @@ class Cut(Join):
             bk_array = np.expand_dims(
                 bk_array[~np.isnan(bk_array)].astype(np.int64), axis=1
             )
-            self._segments_list = data["linestrings"]
+            self._segments_list = [np.array(ls.coords) for ls in data["linestrings"]]
             self._duplicates = find_duplicates(data["linestrings"], type="linestring")
             self._bookkeeping_linestrings = bk_array
 
         else:
-            self._segments_list = data["linestrings"]
+            self._segments_list = [np.array(ls.coords) for ls in data["linestrings"]]
 
         # prepare to return object
         data["linestrings"] = self._segments_list
@@ -163,6 +173,34 @@ class Cut(Join):
 
         return data
 
+    def _get_linestring_types(self, data) -> dict:
+        # create dict with original geometry type per linestring
+        def recurse_geometries(object):
+            # If object is not a list, make it a list to be able to loop
+            if not isinstance(object, list):
+                object = [object]
+
+            # Loop over children of object
+            for object_child in object:
+                # Depending on input format there is one or more geometry in an object
+                if "geometries" in object_child:
+                    geometries = object_child["geometries"]
+                    recurse_geometries(geometries)
+                elif object_child['type'] != "Point":
+                    # For non-Point geometries, loop over arcs
+                    for arc_id in object_child["arcs"]:
+                        # Find the linestrings for the arc via bookkeeping_geoms
+                        for linestring_id in data["bookkeeping_geoms"][arc_id]:
+                            linestring_object_types[linestring_id] = (
+                                object_child["type"]
+                            )
+
+        linestring_object_types = {}
+        for object_key in data["objects"]:
+            recurse_geometries(data["objects"][object_key])
+
+        return linestring_object_types
+
     def _flatten_and_index(self, slist):
         """
         Function to create a flattened list of splitted linestrings and create a
diff --git a/topojson/core/dedup.py b/topojson/core/dedup.py
index 620011e..26d8c84 100644
--- a/topojson/core/dedup.py
+++ b/topojson/core/dedup.py
@@ -67,9 +67,6 @@ class Dedup(Cut):
 
         # deduplicate equal geometries
         # create numpy array from bookkeeping_geoms variable for numerical computation
-        if not self.options.topology and data["linestrings"]:
-            data["linestrings"] = [np.array(d.coords) for d in data["linestrings"]]
-
         if len(data["bookkeeping_linestrings"]):
             array_bk = np.vstack(data["bookkeeping_linestrings"])
         else:
diff --git a/topojson/ops.py b/topojson/ops.py
index 5472b6d..77d613e 100644
--- a/topojson/ops.py
+++ b/topojson/ops.py
@@ -219,7 +219,7 @@ def insert_coords_in_line(line, tree_splitter):
     return new_ls_xy, pts_xy_on_line
 
 
-def fast_split(line, splitter):
+def fast_split(line, splitter, is_ring):
     """
     Split a LineString (numpy.array) with a Point or MultiPoint.
     This function is a replacement for the shapely.ops.split function, but faster.
@@ -230,6 +230,10 @@ def fast_split(line, splitter):
         numpy array with coordinates that you like to be split
     splitter : numpy.array
         numpy array with coordinates on which the line should be tried splitting
+    is_ring : bool
+        True if the line represents a ring. In this case, for the first point found, the
+        linestring will be rotated rather than split. For consecutive points it will be
+        split.
 
     Returns
     -------
@@ -252,6 +256,16 @@ def fast_split(line, splitter):
         )
     )
 
+    # For a ring, rotate rather than split for the first splitter_index
+    # Remark: the start and end coordinate of a ring are the same, so keep it that way
+    if is_ring and len(splitter_indices) > 0 and splitter_indices[0] != 0:
+        first_index = splitter_indices[0]
+        line = line[:-1]
+        line = np.roll(line, -first_index, axis=0)
+        line = np.append(line, [line[0]], axis=0)
+        splitter_indices = splitter_indices[1:]
+        splitter_indices = splitter_indices - first_index
+
     # compute the indices on which to split the line
     # cannot split on first or last index of linestring
     splitter_indices = splitter_indices[


Test patch:
diff --git a/tests/test_topology.py b/tests/test_topology.py
index bf84699..44992c1 100644
--- a/tests/test_topology.py
+++ b/tests/test_topology.py
@@ -1,9 +1,12 @@
 import os
 import json
-from shapely import geometry, wkt
-import geopandas
-import geojson
+import pytest
+
 import fiona
+import geojson
+import geopandas
+from shapely import geometry, wkt
+
 import topojson
 
 
@@ -633,3 +636,28 @@ def test_topology_write_multiple_object_json_dict():
     topo_dict = topo.to_dict()
 
     assert len(topo_dict["objects"]) == 2
+
+
+@pytest.mark.parametrize(
+    "shared_coords, prequantize", 
+    [(True, True), (True, False), (False, True), (False, False)]
+)
+def test_topology_polygon_filled_island_no_junctions(shared_coords, prequantize):
+    data = geopandas.GeoDataFrame(
+        {
+            "name": ["abcde_fghij", "jihgf"],
+            "geometry": [
+                geometry.Polygon(
+                    shell=[[0, 0], [3, 0], [3, 3], [0, 3], [0, 0]], 
+                    holes=[[[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]],
+                ),
+                geometry.Polygon([[1, 1], [2, 1], [2, 2], [1, 2], [1, 1]]),
+            ]
+        }
+    )
+    topo = topojson.Topology(data, prequantize=prequantize, shared_coords=shared_coords)
+    topo_gdf = topo.to_gdf()
+
+    assert len(topo.output["arcs"]) == 2
+    assert topo_gdf.geometry[0] == data["geometry"][0]
+    assert topo_gdf.geometry[1] == data["geometry"][1]