def _describe_file(filepath): """ Helper function to describe a geospatial data First checks if a sidecar mcf file is available, if so uses that if not, script will parse the file to retrieve some info from the file :param filepath: path to file :returns: `dict` of GeoJSON item """ content = {'bbox': None, 'geometry': None, 'properties': {}} mcf_file = '{}.yml'.format(os.path.splitext(filepath)[0]) if os.path.isfile(mcf_file): try: from pygeometa.core import read_mcf, MCFReadError from pygeometa.schemas.stac import STACItemOutputSchema md = read_mcf(mcf_file) stacjson = STACItemOutputSchema.write(STACItemOutputSchema, md) stacdata = loads(stacjson) for k, v in stacdata.items(): content[k] = v except ImportError: LOGGER.debug('pygeometa not found') except MCFReadError as err: LOGGER.warning('MCF error: {}'.format(err)) else: LOGGER.debug('No mcf found at: {}'.format(mcf_file)) if content['geometry'] is None and content['bbox'] is None: try: import rasterio from rasterio.crs import CRS from rasterio.warp import transform_bounds except ImportError as err: LOGGER.warning('rasterio not found') LOGGER.warning(err) return content try: import fiona except ImportError as err: LOGGER.warning('fiona not found') LOGGER.warning(err) return content try: # raster LOGGER.debug('Testing raster data detection') d = rasterio.open(filepath) content['bbox'] = [ d.bounds.left, d.bounds.bottom, d.bounds.right, d.bounds.top ] content['geometry'] = { 'type': 'Polygon', 'coordinates': [[[d.bounds.left, d.bounds.bottom], [d.bounds.left, d.bounds.top], [d.bounds.right, d.bounds.top], [d.bounds.right, d.bounds.bottom], [d.bounds.left, d.bounds.bottom]]] } for k, v in d.tags(d.count).items(): content['properties'][k] = v if k in ['GRIB_REF_TIME']: value = int(v.split()[0]) datetime_ = datetime.fromtimestamp(value) content['properties']['datetime'] = datetime_.isoformat( ) + 'Z' # noqa except rasterio.errors.RasterioIOError: try: LOGGER.debug('Testing vector data detection') d = fiona.open(filepath) scrs = CRS(d.crs) if scrs.to_epsg() is not None and scrs.to_epsg() != 4326: tcrs = CRS.from_epsg(4326) bnds = transform_bounds(scrs, tcrs, d.bounds[0], d.bounds[1], d.bounds[2], d.bounds[3]) content['properties']['projection'] = scrs.to_epsg() else: bnds = d.bounds if d.schema['geometry'] not in [None, 'None']: content['bbox'] = [bnds[0], bnds[1], bnds[2], bnds[3]] content['geometry'] = { 'type': 'Polygon', 'coordinates': [[[bnds[0], bnds[1]], [bnds[0], bnds[3]], [bnds[2], bnds[3]], [bnds[2], bnds[1]], [bnds[0], bnds[1]]]] } for k, v in d.schema['properties'].items(): content['properties'][k] = v if d.driver == 'ESRI Shapefile': id_ = os.path.splitext(os.path.basename(filepath))[0] content['assets'] = {} for suffix in ['shx', 'dbf', 'prj']: fullpath = '{}.{}'.format( os.path.splitext(filepath)[0], suffix) if os.path.exists(fullpath): filectime = file_modified_iso8601(fullpath) filesize = os.path.getsize(fullpath) content['assets'][suffix] = { 'href': './{}.{}'.format(id_, suffix), 'created': filectime, 'file:size': filesize } except fiona.errors.DriverError: LOGGER.debug('Could not detect raster or vector data') return content
def CRS_to_uri(crs: CRS) -> str: """Convert CRS to URI.""" epsg_code = crs.to_epsg() return f"http://www.opengis.net/def/crs/EPSG/0/{epsg_code}"
def _describe_file(filepath): """ Helper function to describe a geospatial data :param filepath: path to file :returns: `dict` of GeoJSON item """ content = {'bbox': None, 'geometry': None, 'properties': {}} try: import rasterio from rasterio.crs import CRS from rasterio.warp import transform_bounds LOGGER.warning( 'rasterio not found. Cannot derive geospatial properties') # noqa except ImportError as err: LOGGER.warning(err) return content try: import fiona except ImportError as err: LOGGER.warning('fiona not found. Cannot derive geospatial properties') LOGGER.warning(err) return content try: # raster LOGGER.debug('Testing raster data detection') d = rasterio.open(filepath) content['bbox'] = [ d.bounds.left, d.bounds.bottom, d.bounds.right, d.bounds.top ] content['geometry'] = { 'type': 'Polygon', 'coordinates': [[[d.bounds.left, d.bounds.bottom], [d.bounds.left, d.bounds.top], [d.bounds.right, d.bounds.top], [d.bounds.right, d.bounds.bottom], [d.bounds.left, d.bounds.bottom]]] } for k, v in d.tags(1).items(): content['properties'][k] = v except rasterio.errors.RasterioIOError: LOGGER.debug('Testing vector data detection') d = fiona.open(filepath) scrs = CRS(d.crs) if scrs.to_epsg() is not None and scrs.to_epsg() != 4326: tcrs = CRS.from_epsg(4326) bnds = transform_bounds(scrs, tcrs, d.bounds[0], d.bounds[1], d.bounds[2], d.bounds[3]) content['properties']['projection'] = scrs.to_epsg() else: bnds = d.bounds if d.schema['geometry'] not in [None, 'None']: content['bbox'] = [bnds[0], bnds[1], bnds[2], bnds[3]] content['geometry'] = { 'type': 'Polygon', 'coordinates': [[[bnds[0], bnds[1]], [bnds[0], bnds[3]], [bnds[2], bnds[3]], [bnds[2], bnds[1]], [bnds[0], bnds[1]]]] } for k, v in d.schema['properties'].items(): content['properties'][k] = v if d.driver == 'ESRI Shapefile': id_ = os.path.splitext(os.path.basename(filepath))[0] content['assets'] = {} for suffix in ['shx', 'dbf', 'prj', 'shp.xml']: content['assets'][suffix] = { 'href': './{}.{}'.format(id_, suffix) } return content
def custom( cls, extent: List[float], crs: CRS, tile_width: int = 256, tile_height: int = 256, matrix_scale: List = [1, 1], extent_crs: Optional[CRS] = None, minzoom: int = 0, maxzoom: int = 24, title: str = "Custom TileMatrixSet", identifier: str = "Custom", ): """ Construct a custom TileMatrixSet. Attributes ---------- crs: rasterio.crs.CRS Tile Matrix Set coordinate reference system extent: list Bounding box of the Tile Matrix Set, (left, bottom, right, top). tile_width: int Width of each tile of this tile matrix in pixels (default is 256). tile_height: int Height of each tile of this tile matrix in pixels (default is 256). matrix_scale: list Tiling schema coalescence coefficient (default: [1, 1] for EPSG:3857). Should be set to [2, 1] for EPSG:4326. see: http://docs.opengeospatial.org/is/17-083r2/17-083r2.html#14 extent_crs: rasterio.crs.CRS Extent's coordinate reference system, as a rasterio CRS object. (default: same as input crs) minzoom: int Tile Matrix Set minimum zoom level (default is 0). maxzoom: int Tile Matrix Set maximum zoom level (default is 24). title: str Tile Matrix Set title (default is 'Custom TileMatrixSet') identifier: str Tile Matrix Set identifier (default is 'Custom') Returns: -------- TileMatrixSet """ tms: Dict[str, Any] = { "title": title, "identifier": identifier, "supportedCRS": crs, "tileMatrix": [], } is_inverted = False if crs.to_epsg(): # We use URI because with EPSG code it doesn't work in GDAL 2 crs_uri = CRS_to_uri(crs) is_inverted = crs_axis_inverted(CRS.from_user_input(crs_uri)) if is_inverted: tms["boundingBox"] = TMSBoundingBox( crs=extent_crs or crs, lowerCorner=[extent[1], extent[0]], upperCorner=[extent[2], extent[3]], ) else: tms["boundingBox"] = TMSBoundingBox( crs=extent_crs or crs, lowerCorner=[extent[0], extent[1]], upperCorner=[extent[2], extent[3]], ) if extent_crs: bbox = BoundingBox( *transform_bounds(extent_crs, crs, *extent, densify_pts=21)) else: bbox = BoundingBox(*extent) x_origin = bbox.left if not is_inverted else bbox.top y_origin = bbox.top if not is_inverted else bbox.left width = abs(bbox.right - bbox.left) height = abs(bbox.top - bbox.bottom) mpu = meters_per_unit(crs) for zoom in range(minzoom, maxzoom + 1): res = max( width / (tile_width * matrix_scale[0]) / 2.0**zoom, height / (tile_height * matrix_scale[1]) / 2.0**zoom, ) tms["tileMatrix"].append( TileMatrix(**dict( identifier=str(zoom), scaleDenominator=res * mpu / 0.00028, topLeftCorner=[x_origin, y_origin], tileWidth=tile_width, tileHeight=tile_height, matrixWidth=matrix_scale[0] * 2**zoom, matrixHeight=matrix_scale[1] * 2**zoom, ))) return cls(**tms)