Repository: mattijn/topojson

Issue:
bounding box not working when using geodata
I'm working on using your library including a warper to process postgres geogis data into TopoJson.
I'm getting a `"ValueError: not enough values to unpack (expected 4, got 0)"` error when calling `tj = topojson.Topology(geoList)`.
geoList content is enclosed as "pretty" and "raw" files
[geoList_pretty.txt](https://github.com/mattijn/topojson/files/4617905/geoList_pretty.txt)
[geoList_raw.txt](https://github.com/mattijn/topojson/files/4617907/geoList_raw.txt)





Discussion:

Patch:
diff --git a/topojson/core/extract.py b/topojson/core/extract.py
index 5d548f2..71e80cb 100644
--- a/topojson/core/extract.py
+++ b/topojson/core/extract.py
@@ -570,50 +570,56 @@ class Extract(object):
             self._key = key
             self._obj = self._data[self._key]
 
-            # detect if object is a shapely geometry
-            if hasattr(self._obj, "geom_type"):
-                geom = self._obj
-                self._obj = {"type": geom.geom_type}
-                self._data[self._key] = self._obj
-
-            # detect if object contains shapely supported geometries:
-            elif "geometry" in self._obj.keys() and hasattr(
-                self._obj["geometry"], "geom_type"
-            ):
-                # extract geometry and collect type and properties
-                geom = self._obj["geometry"]
-                self._obj.pop("geometry", None)
-                self._obj = {"properties": self._obj, "type": geom.geom_type}
-                self._data[self._key] = self._obj
-
-            # no direct shapely geometries avaiable. Try forcing
-            else:
-                # detect if the obect contains a __geo_interface__.
-                if hasattr(self._obj, "__geo_interface__"):
-                    self._obj = self._obj.__geo_interface__
+            try:
+                # detect if object is a shapely geometry
+                if hasattr(self._obj, "geom_type"):
+                    geom = self._obj
+                    self._obj = {"type": geom.geom_type}
+                    self._data[self._key] = self._obj
+
+                # detect if object contains shapely supported geometries:
+                elif "geometry" in self._obj.keys() and hasattr(
+                    self._obj["geometry"], "geom_type"
+                ):
+                    # extract geometry and collect type and properties
+                    geom = self._obj["geometry"]
+                    self._obj.pop("geometry", None)
+                    self._obj = {"properties": self._obj, "type": geom.geom_type}
                     self._data[self._key] = self._obj
 
-                # try parsing the object into a shapely geometry. If this not succeeds
-                # then the object might be a GeoJSON Feature or FeatureCollection. If
-                # this fails as well then the object is not recognized and removed.
-                try:
-                    geom = geometry.shape(self._obj)
-                    # object can be mapped, but may not be valid. remove invalid objects
-                    # and continue
-                    if not geom.is_valid:
+                # no direct shapely geometries avaiable. Try forcing
+                else:
+                    # detect if the obect contains a __geo_interface__.
+                    if hasattr(self._obj, "__geo_interface__"):
+                        self._obj = self._obj.__geo_interface__
+                        self._data[self._key] = self._obj
+
+                    # try parsing the object into a shapely geometry. If this not succeeds
+                    # then the object might be a GeoJSON Feature or FeatureCollection. If
+                    # this fails as well then the object is not recognized and removed.
+                    try:
+                        geom = geometry.shape(self._obj)
+                        # object can be mapped, but may not be valid. remove invalid objects
+                        # and continue
+                        if not geom.is_valid:
+                            self._invalid_geoms += 1
+                            del self._data[self._key]
+                            continue
+                    except ValueError:
+                        # object might be a GeoJSON Feature or FeatureCollection
+                        geom = geojson.loads(geojson.dumps(self._obj))
+                    except AttributeError:
+                        geom = geojson.loads(self._obj)
+                    except (IndexError, TypeError):
+                        # object is not valid
                         self._invalid_geoms += 1
                         del self._data[self._key]
                         continue
-                except ValueError:
-                    # object might be a GeoJSON Feature or FeatureCollection
-                    geom = geojson.loads(geojson.dumps(self._obj))
-                except AttributeError:
-                    geom = geojson.loads(self._obj)
-                except (IndexError, TypeError):
-                    # object is not valid
-                    self._invalid_geoms += 1
-                    del self._data[self._key]
-                    continue
+            except AttributeError:
+                # object is not valid
+                self._invalid_geoms += 1
+                del self._data[self._key]
+                continue
 
             # the geom object is recognized, lets serialize the geometric type.
             # this function redirects the geometric object based on its type to
diff --git a/topojson/core/join.py b/topojson/core/join.py
index 67c7e2e..f7e1d71 100644
--- a/topojson/core/join.py
+++ b/topojson/core/join.py
@@ -144,6 +144,10 @@ class Join(Extract):
         ptbs = geometry.asMultiPoint(data["coordinates"]).bounds
         data["bbox"] = compare_bounds(lsbs, ptbs)
 
+        if not data["linestrings"] and not data["coordinates"]:
+            data["junctions"] = self.junctions
+            return data
+
         # prequantize linestrings if required
         if self.options.prequantize > 0:
             # set default if not specifically given in the options
@@ -165,6 +169,7 @@ class Join(Extract):
             return data
 
         if self.options.shared_paths == "coords":
+
             def _get_verts(geom):
                 # get coords of each LineString
                 return [x for x in geom.coords]
@@ -176,7 +181,9 @@ class Join(Extract):
                 verts = _get_verts(ls)
                 for i, vert in enumerate(verts):
                     ran = geoms.pop(vert, None)
-                    neighs = sorted([verts[i - 1], verts[i + 1 if i < len(verts) - 1 else 0]])
+                    neighs = sorted(
+                        [verts[i - 1], verts[i + 1 if i < len(verts) - 1 else 0]]
+                    )
                     if ran and ran != neighs:
                         junctions.append(vert)
                     geoms[vert] = neighs
diff --git a/topojson/core/topology.py b/topojson/core/topology.py
index 2d60a9b..4adfa94 100644
--- a/topojson/core/topology.py
+++ b/topojson/core/topology.py
@@ -298,6 +298,8 @@ class Topology(Hashmap):
         result = copy.deepcopy(self)
         arcs = result.output["arcs"]
 
+        if not arcs:
+            return result
         # dequantize if quantization is applied
         if "transform" in result.output.keys():
             np_arcs = np_array_from_arcs(arcs)
diff --git a/topojson/utils.py b/topojson/utils.py
index b768d08..e38b7e3 100644
--- a/topojson/utils.py
+++ b/topojson/utils.py
@@ -400,22 +400,23 @@ def serialize_as_svg(topo_object, separate=False, include_junctions=False):
     keys = topo_object.keys()
     if "arcs" in keys:
         arcs = topo_object["arcs"]
-        # dequantize if quantization is applied
-        if "transform" in keys:
+        if arcs:
+            # dequantize if quantization is applied
+            if "transform" in keys:
 
-            np_arcs = np_array_from_arcs(arcs)
+                np_arcs = np_array_from_arcs(arcs)
 
-            transform = topo_object["transform"]
-            scale = transform["scale"]
-            translate = transform["translate"]
+                transform = topo_object["transform"]
+                scale = transform["scale"]
+                translate = transform["translate"]
 
-            np_arcs = dequantize(np_arcs, scale, translate)
-            l_arcs = []
-            for ls in np_arcs:
-                l_arcs.append(ls[~np.isnan(ls)[:, 0]].tolist())
-            arcs = l_arcs
+                np_arcs = dequantize(np_arcs, scale, translate)
+                l_arcs = []
+                for ls in np_arcs:
+                    l_arcs.append(ls[~np.isnan(ls)[:, 0]].tolist())
+                arcs = l_arcs
 
-        arcs = [geometry.LineString(arc) for arc in arcs]
+            arcs = [geometry.LineString(arc) for arc in arcs]
 
     else:
         arcs = topo_object["linestrings"]
@@ -475,18 +476,18 @@ def serialize_as_geojson(
 
     # prepare arcs from topology object
     arcs = topo_object["arcs"]
+    transform = None
+    if "transform" in topo_object.keys():
+        transform = topo_object["transform"]
+        scale = transform["scale"]
+        translate = transform["translate"]
+
     if arcs:
         np_arcs = np_array_from_arcs(arcs)
-
-        transform = None
         # dequantize if quantization is applied
-        if "transform" in topo_object.keys():
-
-            transform = topo_object["transform"]
-            scale = transform["scale"]
-            translate = transform["translate"]
-
-            np_arcs = dequantize(np_arcs, scale, translate)
+        np_arcs = dequantize(np_arcs, scale, translate)
+    else:
+        np_arcs = None
 
     # select object member from topology object
     features = topo_object["objects"][objectname]["geometries"]


Test patch:
diff --git a/tests/test_extract.py b/tests/test_extract.py
index 9aa942a..73ce4e4 100644
--- a/tests/test_extract.py
+++ b/tests/test_extract.py
@@ -334,3 +334,14 @@ def test_extract_geopandas_geodataframe():
     topo = Extract(data).to_dict()
 
     assert len(topo["bookkeeping_geoms"]) == 3
+
+
+# dict shoud have a valued key:geom_object. Otherwise key:value is removed
+def test_extract_invalid_dict_item():
+    data = {
+        "type": "MultiPolygon",
+        "coordinates": [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]],
+    }
+    topo = Extract(data).to_dict()
+
+    assert len(topo["bookkeeping_geoms"]) == 0
diff --git a/tests/test_join.py b/tests/test_join.py
index 1660b38..c316485 100644
--- a/tests/test_join.py
+++ b/tests/test_join.py
@@ -599,3 +599,19 @@ def test_join_linemerge_multilinestring():
         assert geometry.MultiPoint(topo["junctions"]).equals(
             geometry.MultiPoint([(0.0, 0.0), (1.0, 0.0)])
         )
+
+    # test a list of two invalid geometric objects with prequantize True
+    def test_join_invalid_prequantize():
+        data = [
+            {
+                "type": "MultiPolygon",
+                "coordinates": [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]],
+            },
+            {
+                "type": "MultiPolygon",
+                "coordinates": [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]],
+            },
+        ]
+        topo = Join(data, options={"prequantize": True}).to_dict()
+
+        assert len(topo["junctions"]) == 0
diff --git a/tests/test_topology.py b/tests/test_topology.py
index 668e811..6854cf3 100644
--- a/tests/test_topology.py
+++ b/tests/test_topology.py
@@ -26,7 +26,7 @@ def test_topology_winding_order_TopoOptions():
     topo = topojson.Topology(data, winding_order="CW_CCW").to_dict(options=True)
 
     assert len(topo["objects"]) == 1
-    assert len(topo["options"]) == 8
+    assert len(topo["options"]) == 9
 
 
 # test winding order using kwarg variables
@@ -37,7 +37,7 @@ def test_topology_winding_order_kwarg_vars():
     topo = topojson.Topology(data, winding_order="CW_CCW").to_dict(options=True)
 
     assert len(topo["objects"]) == 1
-    assert len(topo["options"]) == 8
+    assert len(topo["options"]) == 9
 
 
 def test_topology_computing_topology():
@@ -191,9 +191,11 @@ def test_topology_polygon_point():
 
 def test_topology_point():
     data = [{"type": "Point", "coordinates": [0.5, 0.5]}]
+    # topo = topojson.Topology(data, topoquantize=True).to_dict()
     with pytest.raises(SystemExit) as topo:
         topojson.Topology(data, topoquantize=True).to_dict()
 
+    # assert len(topo["arcs"]) == 0
     assert topo.type == SystemExit
     assert topo.value.code == "Cannot quantize when xmax-xmin OR ymax-ymin equals 0"
 
@@ -307,3 +309,14 @@ def test_topology_to_geojson_polygon_point():
     assert "]]]}}" in topo  # feat 1
     assert "]}}]}" in topo  # feat 2
 
+
+def test_topology_to_geojson_quantized_points_only():
+    data = [{"type": "MultiPoint", "coordinates": [[0.5, 0.5], [1.0, 1.0]]}]
+    topo = topojson.Topology(data, prequantize=True).to_dict()
+
+    assert len(topo["arcs"]) == 0
+    assert topo["objects"]["data"]["geometries"][0]["coordinates"] == [
+        [0, 0],
+        [999999, 999999],
+    ]
+    assert topo["transform"]["translate"] == [0.5, 0.5]