import logging from functools import partial import fiona import pyproj import shapely.ops as ops from fiona.transform import transform_geom from shapely.geometry import mapping from shapely.geometry import MultiPolygon from shapely.geometry import Polygon from shapely.geometry import shape from shapely.geometry.polygon import orient import geoutils import utils from merge import merge_features from property_transformation import get_transformed_properties from property_transformation import PropertyMappingFailedException def _force_geometry_2d(geometry): """ Convert a geometry to 2d """ if geometry["type"] in ["Polygon", "MultiLineString"]: geometry["coordinates"] = [ _force_linestring_2d(l) for l in geometry["coordinates"] ] elif geometry["type"] in ["LineString", "MultiPoint"]: geometry["coordinates"] = _force_linestring_2d(geometry["coordinates"]) elif geometry["type"] == "Point": geometry["coordinates"] = geometry["coordinates"][:2] elif geometry["type"] == "MultiPolygon": geometry["coordinates"] = [ [_force_linestring_2d(l) for l in g] for g in geometry["coordinates"] ] return geometry def _force_linestring_2d(linestring): """ Convert a list of coordinates to 2d """ return [c[:2] for c in linestring] def _force_geometry_ccw(geometry): if geometry["type"] == "Polygon": return _force_polygon_ccw(geometry) elif geometry["type"] == "MultiPolygon": oriented_polygons = [ _force_polygon_ccw({"type": "Polygon", "coordinates": g}) for g in geometry["coordinates"] ] geometry["coordinates"] = [g["coordinates"] for g in oriented_polygons] return geometry else: return geometry def _force_polygon_ccw(geometry): polygon = shape(geometry) return mapping(orient(polygon)) def _fix_geometry(geometry): shapely_geometry = shape(geometry) if not shapely_geometry.is_valid: buffered = shapely_geometry.buffer(0.0) # this will fix some invalid geometries, including bow-tie geometries, but for others it will return an empty geometry if buffered and ( (type(buffered) == Polygon and buffered.exterior) or (type(buffered) == MultiPolygon and len(buffered.geoms) > 0) ): return mapping(buffered) return geometry def read_fiona(source, prop_map, filterer=None, merge_on=None): """Process a fiona collection """ collection = { "type": "FeatureCollection", "features": [], "bbox": [float("inf"), float("inf"), float("-inf"), float("-inf")], } skipped_count = 0 failed_count = 0 transformer = partial( transform_geom, source.crs, "EPSG:4326", antimeridian_cutting=True, precision=6 ) for feature in source: if filterer is not None and not filterer.keep(feature): skipped_count += 1 continue if feature["geometry"] is None: logging.error("empty geometry") failed_count += 1 continue try: transformed_geometry = transformer(_force_geometry_2d(feature["geometry"])) fixed_geometry = _fix_geometry(transformed_geometry) feature["geometry"] = _force_geometry_ccw(fixed_geometry) if merge_on: feature["original_properties"] = feature["properties"] feature["properties"] = get_transformed_properties( feature["properties"], prop_map ) collection["features"].append(feature) except PropertyMappingFailedException as e: logging.error(str(e) + ": " + str(feature["properties"])) failed_count += 1 except Exception as e: logging.exception("Error processing feature: " + str(feature)) failed_count += 1 pre_merge_count = len(collection["features"]) if merge_on: collection = merge_features( collection, merge_on, properties_key="original_properties" ) for feature in collection["features"]: if "original_properties" in feature: del feature["original_properties"] feature["properties"]["acres"] = geoutils.get_area_acres(feature["geometry"]) feature["bbox"] = geoutils.get_bbox_from_geojson_feature(feature) if "id" in feature["properties"]: feature["id"] = feature["properties"]["id"] if len(collection["features"]) > 0: collection["bbox"] = geoutils.get_bbox_from_geojson(collection) logging.info( "skipped %i features, kept %i features, merged %i features, errored %i features" % ( skipped_count, pre_merge_count, pre_merge_count - len(collection["features"]), failed_count, ) ) return collection