def __new__(self, coordinates=None): if coordinates is None: # empty geometry # TODO better way? return shapely.from_wkt("LINEARRING EMPTY") elif isinstance(coordinates, LineString): if type(coordinates) == LinearRing: # return original objects since geometries are immutable return coordinates elif not coordinates.is_valid: raise TopologicalError("An input LineString must be valid.") else: # LineString # TODO convert LineString to LinearRing more directly? coordinates = coordinates.coords else: # check coordinates on points def _coords(o): if isinstance(o, Point): return o.coords[0] else: return o coordinates = [_coords(o) for o in coordinates] if len(coordinates) == 0: # empty geometry # TODO better constructor + should shapely.linearrings handle this? return shapely.from_wkt("LINEARRING EMPTY") geom = shapely.linearrings(coordinates) if not isinstance(geom, LinearRing): raise ValueError("Invalid values passed to LinearRing constructor") return geom
def _repair(geom): repaired = geom.buffer(0) if geom.geom_type in ["Polygon", "MultiPolygon"] else geom if repaired.is_valid: return repaired else: raise TopologicalError( "geometry is invalid (%s) and cannot be repaired" % explain_validity(repaired) )
def _reproject_geom(geometry, src_crs, dst_crs, validity_check=True): if geometry.is_empty or src_crs == dst_crs: return geometry.buffer(0) out_geom = to_shape( transform_geom(src_crs.to_dict(), dst_crs.to_dict(), mapping(geometry))).buffer(0) if validity_check and not out_geom.is_valid or out_geom.is_empty: raise TopologicalError("invalid geometry after reprojection") return out_geom
def width_height(bounds): try: l, b, r, t = reproject_geometry(box(*bounds), src_crs=tile.crs, dst_crs=dst_pyramid.crs).bounds except ValueError: raise TopologicalError( "bounds cannot be translated into target CRS") return r - l, t - b
def _reproject_geom(geometry, src_crs, dst_crs): if geometry.is_empty or src_crs == dst_crs: return _repair(geometry) out_geom = _repair( to_shape( transform_geom(src_crs.to_dict(), dst_crs.to_dict(), mapping(geometry)))) if validity_check and (not out_geom.is_valid or out_geom.is_empty): raise TopologicalError("invalid geometry after reprojection") return out_geom
def __new__(self, coordinates=None): """ Parameters ---------- coordinates : sequence A sequence of (x, y [,z]) numeric coordinate pairs or triples. Also can be a sequence of Point objects. Rings are implicitly closed. There is no need to specific a final coordinate pair identical to the first. Example ------- Construct a square ring. >>> ring = LinearRing( ((0, 0), (0, 1), (1 ,1 ), (1 , 0)) ) >>> ring.is_closed True >>> ring.length 4.0 """ if coordinates is None: # empty geometry # TODO better way? return shapely.from_wkt("LINEARRING EMPTY") elif isinstance(coordinates, LineString): if type(coordinates) == LinearRing: # return original objects since geometries are immutable return coordinates elif not coordinates.is_valid: raise TopologicalError("An input LineString must be valid.") else: # LineString # TODO convert LineString to LinearRing more directly? coordinates = coordinates.coords else: # check coordinates on points def _coords(o): if isinstance(o, Point): return o.coords[0] else: return o coordinates = [_coords(o) for o in coordinates] if len(coordinates) == 0: # empty geometry # TODO better constructor + should shapely.linearrings handle this? return shapely.from_wkt("LINEARRING EMPTY") geom = shapely.linearrings(coordinates) if not isinstance(geom, LinearRing): raise ValueError("Invalid values passed to LinearRing constructor") return geom
def _get_reprojected_features(input_file=None, dst_bounds=None, dst_crs=None, validity_check=False): with fiona.open(input_file, 'r') as vector: vector_crs = CRS(vector.crs) # Reproject tile bounding box to source file CRS for filter: if vector_crs == dst_crs: dst_bbox = box(*dst_bounds) else: dst_bbox = reproject_geometry(box(*dst_bounds), src_crs=dst_crs, dst_crs=vector_crs, validity_check=True) for feature in vector.filter(bbox=dst_bbox.bounds): feature_geom = to_shape(feature['geometry']) if not feature_geom.is_valid: feature_geom = feature_geom.buffer(0) # skip feature if geometry cannot be repaired if not feature_geom.is_valid: logger.exception("feature omitted: %s", explain_validity(feature_geom)) continue # only return feature if geometry type stayed the same after # reprojecction geom = clean_geometry_type(feature_geom.intersection(dst_bbox), feature_geom.geom_type) if geom: # Reproject each feature to tile CRS try: geom = reproject_geometry(geom, src_crs=vector_crs, dst_crs=dst_crs, validity_check=validity_check) if validity_check and not geom.is_valid: raise TopologicalError( "reprojected geometry invalid: %s" % (explain_validity(geom))) except TopologicalError: logger.exception("feature omitted: reprojection failed") yield { 'properties': feature['properties'], 'geometry': mapping(geom) } else: logger.exception( "feature omitted: geometry type changed after reprojection" )
def polylabel(polygon, tolerance=1.0): """Finds pole of inaccessibility for a given polygon. Based on Vladimir Agafonkin's https://github.com/mapbox/polylabel Parameters ---------- polygon : shapely.geometry.Polygon tolerance : int or float, optional `tolerance` represents the highest resolution in units of the input geometry that will be considered for a solution. (default value is 1.0). Returns ------- shapely.geometry.Point A point representing the pole of inaccessibility for the given input polygon. Raises ------ shapely.geos.TopologicalError If the input polygon is not a valid geometry. Example ------- >>> polygon = LineString([(0, 0), (50, 200), (100, 100), (20, 50), ... (-100, -20), (-150, -200)]).buffer(100) >>> label = polylabel(polygon, tolerance=10) >>> label.wkt 'POINT (59.35615556364569 121.8391962974644)' """ if not polygon.is_valid: raise TopologicalError('Invalid polygon') minx, miny, maxx, maxy = polygon.bounds width = maxx - minx height = maxy - miny cell_size = min(width, height) h = cell_size / 2.0 cell_queue = [] # First best cell approximation is one constructed from the centroid # of the polygon x, y = polygon.centroid.coords[0] best_cell = Cell(x, y, 0, polygon) # Special case for rectangular polygons avoiding floating point error bbox_cell = Cell(minx + width / 2.0, miny + height / 2, 0, polygon) if bbox_cell.distance > best_cell.distance: best_cell = bbox_cell # build a regular square grid covering the polygon x = minx while x < maxx: y = miny while y < maxy: heappush(cell_queue, Cell(x + h, y + h, h, polygon)) y += cell_size x += cell_size # minimum priority queue while cell_queue: cell = heappop(cell_queue) # update the best cell if we find a better one if cell.distance > best_cell.distance: best_cell = cell # continue to the next iteration if we cant find a better solution # based on tolerance if cell.max_distance - best_cell.distance <= tolerance: continue # split the cell into quadrants h = cell.h / 2.0 heappush(cell_queue, Cell(cell.x - h, cell.y - h, h, polygon)) heappush(cell_queue, Cell(cell.x + h, cell.y - h, h, polygon)) heappush(cell_queue, Cell(cell.x - h, cell.y + h, h, polygon)) heappush(cell_queue, Cell(cell.x + h, cell.y + h, h, polygon)) return best_cell.centroid
def geos_linearring_from_py(ob, update_geom=None, update_ndim=0): # If a LinearRing is passed in, clone it and return # If a valid LineString is passed in, clone the coord seq and return a # LinearRing. # # NB: access to coordinates using the array protocol has been moved # entirely to the speedups module. if isinstance(ob, LineString): if type(ob) == LinearRing: return geos_geom_from_py(ob) elif not ob.is_valid: raise TopologicalError("An input LineString must be valid.") elif ob.is_closed and len(ob.coords) >= 4: return geos_geom_from_py(ob, lgeos.GEOSGeom_createLinearRing) else: ob = list(ob.coords) try: m = len(ob) except TypeError: # generators ob = list(ob) m = len(ob) if m == 0: return None def _coords(o): if isinstance(o, Point): return o.coords[0] else: return o n = len(_coords(ob[0])) if m < 3: raise ValueError("A LinearRing must have at least 3 coordinate tuples") assert (n == 2 or n == 3) # Add closing coordinates if not provided if (m == 3 or _coords(ob[0])[0] != _coords(ob[-1])[0] or _coords(ob[0])[1] != _coords(ob[-1])[1]): M = m + 1 else: M = m # Create a coordinate sequence if update_geom is not None: if n != update_ndim: raise ValueError( "Coordinate dimensions mismatch: target geom has {} dims, " "update geom has {} dims".format(n, update_ndim)) cs = lgeos.GEOSGeom_getCoordSeq(update_geom) else: cs = lgeos.GEOSCoordSeq_create(M, n) # add to coordinate sequence for i in range(m): coords = _coords(ob[i]) # Because of a bug in the GEOS C API, # always set X before Y lgeos.GEOSCoordSeq_setX(cs, i, coords[0]) lgeos.GEOSCoordSeq_setY(cs, i, coords[1]) if n == 3: try: lgeos.GEOSCoordSeq_setZ(cs, i, coords[2]) except IndexError: raise ValueError("Inconsistent coordinate dimensionality") # Add closing coordinates to sequence? if M > m: coords = _coords(ob[0]) # Because of a bug in the GEOS C API, # always set X before Y lgeos.GEOSCoordSeq_setX(cs, M - 1, coords[0]) lgeos.GEOSCoordSeq_setY(cs, M - 1, coords[1]) if n == 3: lgeos.GEOSCoordSeq_setZ(cs, M - 1, coords[2]) if update_geom is not None: return None else: return lgeos.GEOSGeom_createLinearRing(cs), n
def tile_to_zoom_level(tile, dst_pyramid=None, matching_method="gdal", precision=8): """ Determine the best zoom level in target TilePyramid from given Tile. Parameters ---------- tile : BufferedTile dst_pyramid : BufferedTilePyramid matching_method : str ('gdal' or 'min') gdal: Uses GDAL's standard method. Here, the target resolution is calculated by averaging the extent's pixel sizes over both x and y axes. This approach returns a zoom level which may not have the best quality but will speed up reading significantly. min: Returns the zoom level which matches the minimum resolution of the extent's four corner pixels. This approach returns the zoom level with the best possible quality but with low performance. If the tile extent is outside of the destination pyramid, a TopologicalError will be raised. precision : int Round resolutions to n digits before comparing. Returns ------- zoom : int """ def width_height(bounds): try: l, b, r, t = reproject_geometry(box(*bounds), src_crs=tile.crs, dst_crs=dst_pyramid.crs).bounds except ValueError: raise TopologicalError( "bounds cannot be translated into target CRS") return r - l, t - b if tile.tp.crs == dst_pyramid.crs: return tile.zoom else: if matching_method == "gdal": # use rasterio/GDAL method to calculate default warp target properties # enabling CHECK_WITH_INVERT_PROJ fixes #269, otherwise this function would # return a non-optimal zooom level for reprojection with rasterio.Env(CHECK_WITH_INVERT_PROJ=True): transform, width, height = calculate_default_transform( tile.tp.crs, dst_pyramid.crs, tile.width, tile.height, *tile.bounds) # this is the resolution the tile would have in destination CRS tile_resolution = round(transform[0], precision) elif matching_method == "min": # calculate the minimum pixel size from the four tile corner pixels l, b, r, t = tile.bounds x = tile.pixel_x_size y = tile.pixel_y_size res = [] for bounds in [ (l, t - y, l + x, t), # left top (l, b, l + x, b + y), # left bottom (r - x, b, r, b + y), # right bottom (r - x, t - y, r, t) # right top ]: try: w, h = width_height(bounds) res.extend([w, h]) except TopologicalError: logger.debug("pixel outside of destination pyramid") if res: tile_resolution = round(min(res), precision) else: raise TopologicalError("tile outside of destination pyramid") else: raise ValueError("invalid method given: %s", matching_method) logger.debug( "we are looking for a zoom level interpolating to %s resolution", tile_resolution) zoom = 0 while True: td_resolution = round(dst_pyramid.pixel_x_size(zoom), precision) if td_resolution <= tile_resolution: break zoom += 1 logger.debug("target zoom for %s: %s (%s)", tile_resolution, zoom, td_resolution) return zoom