Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix to gdf use to geojson #108

Merged
merged 2 commits into from
Aug 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
"program": "${workspaceFolder}/topojson/__init__.py",
"console": "integratedTerminal",
"justMyCode": false
},
{
"name": "Debug Tests",
"type": "python",
"request": "test",
"console": "integratedTerminal"
}
// {
// "name": "Debug Tests",
// "type": "python",
// "request": "test",
// "console": "integratedTerminal",
// "justMyCode": false
// }
]
}
476 changes: 189 additions & 287 deletions Untitled.ipynb

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions tests/test_topology.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,8 +414,16 @@ def test_topology_geojson_winding_order():
],
}
)

topo = topojson.Topology(data, prequantize=False)
gj = topo.to_geojson()

assert gj
assert gj


def test_topology_geodataframe_valid():
data = geopandas.read_file(geopandas.datasets.get_path("naturalearth_lowres"))
topo = topojson.Topology(data)
gdf = topo.toposimplify(10, prevent_oversimplify=False).to_gdf()

assert gdf.shape[0] == 177

34 changes: 28 additions & 6 deletions topojson/core/topology.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,21 +230,43 @@ def to_geojson(
fc, fp, pretty=pretty, indent=indent, maxlinelength=maxlinelength
)

def to_gdf(self):
def to_gdf(
self, crs=None, validate=False, winding_order="CCW_CW", objectname="data"
):
"""
Convert the Topology to a GeoDataFrame. Remember that this will destroy the
computed Topology.

Note: This function use the TopoJSON driver within Fiona to parse the Topology
to a GeoDataFrame. If data is missing (eg. Fiona cannot parse nested
geometrycollections) you can trying using the `.to_geojson()` function prior
creating the GeoDataFrame.
Note: This function use not the TopoJSON driver within Fiona, but a custom
implemented more robust variant. See for info the `to_geojson()` function.

Parameters
----------
crs : str, dict
coordinate reference system to set on the resulting frame.
Default is `None`.
validate : boolean
Set to `True` to validate each feature before inclusion in the GeoJSON. Only
features that are valid geometries objects will be included.
Default is `False`.
winding_order : str
Determines the winding order of the features in the output geometry. Choose
between `CW_CCW` for clockwise orientation for outer rings and counter-
clockwise for interior rings. Or `CCW_CW` for counter-clockwise for outer
rings and clockwise for interior rings.
Default is `CCW_CW` for GeoJSON.
objectname : str
The name of the object within the Topology to convert to GeoJSON.
Default is `data`.
"""
from ..utils import serialize_as_geodataframe

topo_object = copy.deepcopy(self.output)
topo_object = self._resolve_coords(topo_object)
return serialize_as_geodataframe(topo_object)
fc = serialize_as_geojson(
topo_object, validate=validate, objectname=objectname, order=winding_order
)
return serialize_as_geodataframe(fc, crs=crs)

def to_alt(
self, color=None, tooltip=True, projection="identity", objectname="data"
Expand Down
48 changes: 23 additions & 25 deletions topojson/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ def wrapper(*args, **kw):


# --------- supportive functions for serialization -----------
def coordinates(arcs, tp_arcs):
def coordinates(arcs, tp_arcs, geom_type):
"""
Return GeoJSON coordinates for the sequence(s) of arcs.

Expand All @@ -159,9 +159,17 @@ def coordinates(arcs, tp_arcs):
]
)
coords = coords[~np.isnan(coords).any(axis=1)].tolist()
if geom_type in ["Polygon", "MultiPolygon"]:
if len(coords) < 3:
# This may happen if an arc has only two points.
coords.extend([coords[0]])
elif geom_type in ["LineString", "MultiLineString"]:
if len(coords) < 2:
# This should never happen per the specification.
coords.extend([coords[0]])
return coords
elif isinstance(arcs[0], (list, tuple)):
return list(coordinates(arc, tp_arcs) for arc in arcs)
return list(coordinates(arc, tp_arcs, geom_type) for arc in arcs)
else:
raise ValueError("Invalid input %s", arcs)

Expand Down Expand Up @@ -197,7 +205,10 @@ def geometry(obj, tp_arcs, transform=None):
return {"type": obj["type"], "coordinates": point_coord[0]}

else:
return {"type": obj["type"], "coordinates": coordinates(obj["arcs"], tp_arcs)}
return {
"type": obj["type"],
"coordinates": coordinates(obj["arcs"], tp_arcs, obj["type"]),
}


def prettyjson(obj, indent=2, maxlinelength=80):
Expand Down Expand Up @@ -372,41 +383,26 @@ def indentitems(items, indent, level):


# ----------------- serialization functions ------------------
def serialize_as_geodataframe(topo_object, url=False):
def serialize_as_geodataframe(fc, crs=None):
"""
Convert a topology dictionary or string into a GeoDataFrame.

Parameters
----------
topo_object : dict, str
a complete object representing an topojson encoded file as
dict, str-object or str-url
fc : dict, str
a complete object representing a GeoJSON file
crs : str, dict
coordinate reference system to set on the resulting frame.
Default is `None`.

Returns
-------
geopandas.GeoDataFrame
topojson object parsed as GeoDataFrame
"""
import fiona
import geopandas
import json

# parse the object as byte string
if isinstance(topo_object, dict):
bytes_topo = str.encode(json.dumps(topo_object))
elif url is True:
import requests

request = requests.get(topo_object)
bytes_topo = bytes(request.content)
else:
bytes_topo = str.encode(topo_object)
# into an in-memory file
vsimem = fiona.ogrext.buffer_to_virtual_file(bytes_topo)

# read the features from a fiona collection into a GeoDataFrame
with fiona.Collection(vsimem, driver="TopoJSON") as f:
gdf = geopandas.GeoDataFrame.from_features(f, crs=f.crs)
gdf = geopandas.GeoDataFrame().from_features(features=fc["features"], crs=crs)
return gdf


Expand Down Expand Up @@ -518,6 +514,8 @@ def serialize_as_geojson(
f = {"id": index, "type": "Feature"}
if "properties" in feature.keys():
f["properties"] = feature["properties"].copy()
else:
f["properties"] = {}

# the transform is only used in cases of points or multipoints
geommap = geometry(feature, np_arcs, transform)
Expand Down