def create_from_features( features, output, minzoom, maxzoom, property, quadkey_zoom, min_tile_cover, tile_cover_sort, quiet, ): """Create mosaic definition file.""" mosaicjson = MosaicJSON.from_features( list(features), minzoom, maxzoom, quadkey_zoom=quadkey_zoom, accessor=lambda feature: feature["properties"][property], minimum_tile_cover=min_tile_cover, tile_cover_sort=tile_cover_sort, quiet=quiet, ) if output: with MosaicBackend(output, mosaic_def=mosaicjson) as mosaic: mosaic.write() else: click.echo(json.dumps(mosaicjson.dict(exclude_none=True)))
def update( self, features: Sequence[Dict], add_first: bool = True, quiet: bool = False, **kwargs, ): """Update existing MosaicJSON on backend.""" logger.debug(f"Updating {self.mosaic_name}...") new_mosaic = MosaicJSON.from_features( features, self.mosaic_def.minzoom, self.mosaic_def.maxzoom, quadkey_zoom=self.quadkey_zoom, quiet=quiet, **kwargs, ) bounds = bbox_union(new_mosaic.bounds, self.mosaic_def.bounds) self.mosaic_def._increase_version() self.mosaic_def.bounds = bounds self.mosaic_def.center = ( (bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2, self.mosaic_def.minzoom, ) self.bounds = bounds items: List[Dict[str, Any]] = [] # Create Metadata item # Note: `parse_float=Decimal` is required because DynamoDB requires all numbers to be # in Decimal type (ref: https://blog.ruanbekker.com/blog/2019/02/05/convert-float-to-decimal-data-types-for-boto3-dynamodb-using-python/) meta = json.loads(self.mosaic_def.json(exclude={"tiles"}), parse_float=Decimal) items.append({ "quadkey": self._metadata_quadkey, "mosaicId": self.mosaic_name, **meta }) # Create Tile items for quadkey, new_assets in new_mosaic.tiles.items(): tile = mercantile.quadkey_to_tile(quadkey) assets = self.assets_for_tile(*tile) assets = [*new_assets, *assets ] if add_first else [*assets, *new_assets] items.append({ "mosaicId": self.mosaic_name, "quadkey": quadkey, "assets": assets }) self._write_items(items)
def mosaic(preference, check_exists, file): features = load_features(file) mosaic = MosaicJSON.from_features(features, minzoom=11, maxzoom=16, asset_filter=asset_filter, accessor=path_accessor, check_exists=check_exists, preference=preference) print(json.dumps(mosaic.dict(), separators=(',', ':')))
def _read( # type: ignore self, query: Dict, minzoom: int, maxzoom: int, accessor: Callable = default_stac_accessor, max_items: Optional[int] = None, stac_query_limit: int = 500, stac_next_link_key: Optional[str] = None, **kwargs: Any, ) -> MosaicJSON: """ Fetch STAC API and construct the mosaicjson. Attributes ---------- query : Dict, required STAC API POST request query. minzoom: int, required mosaic min-zoom. maxzoom: int, required mosaic max-zoom. accessor: callable, required Function called on each feature to get its identifier. max_items: int, optional Limit the maximum of items returned by the API stac_query_limit: int, optional Add "limit" option to the POST Query, default is set to 500. stac_next_link_key: str, optional link's 'next' key. kwargs: any Options forwarded to `MosaicJSON.from_features` Returns ------- mosaic_definition : MosaicJSON Mosaic definition. """ logger.debug(f"Using STAC backend: {self.path}") features = _fetch( self.path, query, max_items=max_items, limit=stac_query_limit, next_link_key=stac_next_link_key, ) logger.debug(f"Creating mosaic from {len(features)} features") return MosaicJSON.from_features( features, minzoom, maxzoom, accessor=accessor, **kwargs )
def create_mosaic(urls=None, res=None): allowed_res = ['1', '13', '1m', '2'] if res and res not in allowed_res: raise ValueError(f'res must be in {allowed_res}') urls = find_urls_for_res(res) features = [] for url in urls: key = url.split('/')[4] geom = box(*parse_grid(key)) full_s3_path = f's3://{BUCKET}/{url}' feature = Feature(geometry=geom, properties={'path': full_s3_path}) features.append(dict(feature)) minzoom, maxzoom = DEFAULT_ZOOMS[res] mosaic = MosaicJSON.from_features( features, minzoom, maxzoom, accessor=lambda x: x['properties']['path'])
def update( self, features: Sequence[Dict], add_first: bool = True, quiet: bool = False, **kwargs, ): """Update existing MosaicJSON on backend.""" new_mosaic = MosaicJSON.from_features( features, self.mosaic_def.minzoom, self.mosaic_def.maxzoom, quadkey_zoom=self.quadkey_zoom, quiet=quiet, **kwargs, ) for quadkey, new_assets in new_mosaic.tiles.items(): tile = mercantile.quadkey_to_tile(quadkey) assets = self.assets_for_tile(*tile) assets = [*new_assets, *assets ] if add_first else [*assets, *new_assets] # add custom sorting algorithm (e.g based on path name) self.mosaic_def.tiles[quadkey] = assets bounds = bbox_union(new_mosaic.bounds, self.mosaic_def.bounds) self.mosaic_def._increase_version() self.mosaic_def.bounds = bounds self.mosaic_def.center = ( (bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2, self.mosaic_def.minzoom, ) # We only write if path is set if self.path: self.write(overwrite=True) return
def create_from_features( features, output, minzoom, maxzoom, property, quadkey_zoom, min_tile_cover, tile_cover_sort, name, description, attribution, quiet, ): """Create mosaic definition file.""" mosaicjson = MosaicJSON.from_features( list(features), minzoom, maxzoom, quadkey_zoom=quadkey_zoom, accessor=lambda feature: feature["properties"][property], minimum_tile_cover=min_tile_cover, tile_cover_sort=tile_cover_sort, quiet=quiet, ) if name: mosaicjson.name = name if description: mosaicjson.description = description if attribution: mosaicjson.attribution = attribution if output: with MosaicBackend(output, mosaic_def=mosaicjson) as mosaic: mosaic.write(overwrite=True) else: click.echo(mosaicjson.json(exclude_none=True))
def _read(self) -> MosaicJSON: """ Fetch STAC API and construct the mosaicjson. Returns: MosaicJSON: Mosaic definition. """ logger.debug(f"Using STAC backend: {self.path}") features = _fetch( self.path, self.query, **self.stac_api_options, ) logger.debug(f"Creating mosaic from {len(features)} features") # We need a specific accessor for STAC options = self.mosaic_options.copy() if "accessor" not in options: options["accessor"] = default_stac_accessor return MosaicJSON.from_features(features, self.minzoom, self.maxzoom, **options)
class BaseBackend(BaseReader): """Base Class for cogeo-mosaic backend storage.""" path: str = attr.ib() mosaic_def: MosaicJSON = attr.ib(default=None) reader: Type[BaseReader] = attr.ib(default=COGReader) reader_options: Dict = attr.ib(factory=dict) backend_options: Dict = attr.ib(factory=dict) # TMS is outside the init because mosaicJSON and cogeo-mosaic only # works with WebMercator (mercantile) for now. tms: TileMatrixSet = attr.ib(init=False, default=WEB_MERCATOR_TMS) _backend_name: str _file_byte_size: Optional[int] = 0 @mosaic_def.validator def _check_mosaic_def(self, attribute, value): if value is not None: self.mosaic_def = MosaicJSON(**dict(value)) def __attrs_post_init__(self): """Post Init: if not passed in init, try to read from self.path.""" self.mosaic_def = self.mosaic_def or self._read(**self.backend_options) self.minzoom = self.mosaic_def.minzoom self.maxzoom = self.mosaic_def.maxzoom self.bounds = self.mosaic_def.bounds @property def center(self): """Return center from the mosaic definition.""" return self.mosaic_def.center def info(self, quadkeys: bool = False) -> Info: # type: ignore """Mosaic info.""" return Info( bounds=self.mosaic_def.bounds, center=self.mosaic_def.center, maxzoom=self.mosaic_def.maxzoom, minzoom=self.mosaic_def.minzoom, name=self.mosaic_def.name if self.mosaic_def.name else "mosaic", quadkeys=[] if not quadkeys else self._quadkeys, ) @property def _quadkeys(self) -> List[str]: """Return the list of quadkey tiles.""" return list(self.mosaic_def.tiles) def stats(self): """PlaceHolder for BaseReader.stats.""" raise NotImplementedError @property def metadata(self) -> Metadata: # type: ignore """Retrieve Mosaic metadata Returns ------- MosaicJSON as dict without `tiles` key. """ return Metadata(**self.mosaic_def.dict()) def assets_for_tile(self, x: int, y: int, z: int) -> List[str]: """Retrieve assets for tile.""" return self.get_assets(x, y, z) def assets_for_point(self, lng: float, lat: float) -> List[str]: """Retrieve assets for point.""" tile = mercantile.tile(lng, lat, self.quadkey_zoom) return self.get_assets(tile.x, tile.y, tile.z) @cached( TTLCache(maxsize=cache_config.maxsize, ttl=cache_config.ttl), key=lambda self, x, y, z: hashkey(self.path, x, y, z), ) def get_assets(self, x: int, y: int, z: int) -> List[str]: """Find assets.""" mercator_tile = mercantile.Tile(x=x, y=y, z=z) quadkeys = find_quadkeys(mercator_tile, self.quadkey_zoom) return list( itertools.chain.from_iterable( [self.mosaic_def.tiles.get(qk, []) for qk in quadkeys])) def tile( # type: ignore self, x: int, y: int, z: int, reverse: bool = False, **kwargs: Any, ) -> Tuple[ImageData, List[str]]: """Get Tile from multiple observation.""" assets = self.assets_for_tile(x, y, z) if not assets: raise NoAssetFoundError(f"No assets found for tile {z}-{x}-{y}") if reverse: assets = list(reversed(assets)) def _reader(asset: str, x: int, y: int, z: int, **kwargs: Any) -> ImageData: with self.reader(asset, **self.reader_options) as src_dst: return src_dst.tile(x, y, z, **kwargs) return mosaic_reader(assets, _reader, x, y, z, **kwargs) def point( self, lon: float, lat: float, threads=MAX_THREADS, reverse: bool = False, **kwargs: Any, ) -> List[Dict]: """Get Point value from multiple observation.""" assets = self.assets_for_point(lon, lat) if not assets: raise NoAssetFoundError(f"No assets found for point ({lon},{lat})") if reverse: assets = list(reversed(assets)) def _reader(asset: str, lon: float, lat: float, **kwargs) -> Dict: with self.reader(asset, **self.reader_options) as src_dst: return src_dst.point(lon, lat, **kwargs) tasks = create_tasks(_reader, assets, threads, lon, lat, **kwargs) return [{ "asset": asset, "values": pt } for pt, asset in filter_tasks( tasks, allowed_exceptions=(PointOutsideBounds, ))] def preview(self): """PlaceHolder for BaseReader.preview.""" raise NotImplementedError def part(self): """PlaceHolder for BaseReader.part.""" raise NotImplementedError def feature(self): """PlaceHolder for BaseReader.feature.""" raise NotImplementedError @abc.abstractmethod def _read(self) -> MosaicJSON: """Fetch mosaic definition""" @property def mosaicid(self) -> str: """Return sha224 id of the mosaicjson document.""" return get_hash(**self.mosaic_def.dict(exclude_none=True)) @property def quadkey_zoom(self) -> int: """Return Quadkey zoom property.""" return self.mosaic_def.quadkey_zoom or self.mosaic_def.minzoom @abc.abstractmethod def write(self, overwrite: bool = True): """Upload new MosaicJSON to backend.""" def update( self, features: Sequence[Dict], add_first: bool = True, quiet: bool = False, **kwargs, ): """Update existing MosaicJSON on backend.""" new_mosaic = self.mosaic_def.from_features( features, self.mosaic_def.minzoom, self.mosaic_def.maxzoom, quadkey_zoom=self.quadkey_zoom, quiet=quiet, **kwargs, ) for quadkey, new_assets in new_mosaic.tiles.items(): tile = mercantile.quadkey_to_tile(quadkey) assets = self.assets_for_tile(*tile) assets = [*new_assets, *assets ] if add_first else [*assets, *new_assets] # add custom sorting algorithm (e.g based on path name) self.mosaic_def.tiles[quadkey] = assets bounds = bbox_union(new_mosaic.bounds, self.mosaic_def.bounds) self.mosaic_def._increase_version() self.mosaic_def.bounds = bounds self.mosaic_def.center = ( (bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2, self.mosaic_def.minzoom, ) # We only write if path is set if self.path: self.write(overwrite=True) return
class DynamoDBBackend(BaseBackend): """DynamoDB Backend Adapter.""" def __init__( self, table_name: str, mosaic_def: Optional[Union[MosaicJSON, Dict]] = None, region: str = os.getenv("AWS_REGION", "us-east-1"), client: Optional[Any] = None, ): """Initialize DynamoDBBackend.""" self.client = client or boto3.resource("dynamodb", region_name=region) self.table = self.client.Table(table_name) self.path = f"dynamodb://{region}/{table_name}" if mosaic_def is not None: self.mosaic_def = MosaicJSON(**dict(mosaic_def)) else: self.mosaic_def = self._read() def tile(self, x: int, y: int, z: int) -> List[str]: """Retrieve assets for tile.""" return self.get_assets(x, y, z) def point(self, lng: float, lat: float) -> List[str]: """Retrieve assets for point.""" tile = mercantile.tile(lng, lat, self.quadkey_zoom) return self.get_assets(tile.x, tile.y, tile.z) @property def _quadkeys(self) -> List[str]: """Return the list of quadkey tiles.""" warnings.warn( "Performing full scan operation might be slow and expensive on large database." ) resp = self.table.scan( ProjectionExpression="quadkey") # TODO: Add pagination return [qk["quadkey"] for qk in resp["Items"] if qk["quadkey"] != "-1"] def write(self): """Write mosaicjson document to AWS DynamoDB.""" self._create_table() items = self._create_items() self._write_items(items) def _update_quadkey(self, quadkey: str, dataset: List[str]): """Update quadkey list.""" self.table.put_item(Item={"quadkey": quadkey, "assets": dataset}) def _update_metadata(self): """Update bounds and center.""" meta = json.loads(json.dumps(self.metadata), parse_float=Decimal) meta["quadkey"] = "-1" self.table.put_item(Item=meta) def update( self, features: Sequence[Dict], add_first: bool = True, quiet: bool = False, **kwargs, ): """Update existing MosaicJSON on backend.""" new_mosaic = self.mosaic_def.from_features( features, self.mosaic_def.minzoom, self.mosaic_def.maxzoom, quadkey_zoom=self.quadkey_zoom, quiet=quiet, **kwargs, ) fout = os.devnull if quiet else sys.stderr with click.progressbar( # type: ignore new_mosaic.tiles.items(), file=fout, show_percent=True) as items: for quadkey, new_assets in items: tile = mercantile.quadkey_to_tile(quadkey) assets = self.tile(*tile) assets = [*new_assets, *assets ] if add_first else [*assets, *new_assets] # add custom sorting algorithm (e.g based on path name) self._update_quadkey(quadkey, assets) bounds = bbox_union(new_mosaic.bounds, self.mosaic_def.bounds) self.mosaic_def._increase_version() self.mosaic_def.bounds = bounds self.mosaic_def.center = ( (bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2, self.mosaic_def.minzoom, ) self._update_metadata() return def _create_table(self, billing_mode: str = "PAY_PER_REQUEST"): # Define schema for primary key # Non-keys don't need a schema attr_defs = [{"AttributeName": "quadkey", "AttributeType": "S"}] key_schema = [{"AttributeName": "quadkey", "KeyType": "HASH"}] # Note: errors if table already exists try: self.client.create_table( AttributeDefinitions=attr_defs, TableName=self.table.table_name, KeySchema=key_schema, BillingMode=billing_mode, ) # If outside try/except block, could wait forever if unable to # create table self.table.wait_until_exists() except self.client.exceptions.ResourceInUseException: warnings.warn("Unable to create table, may already exist") return def _create_items(self) -> List[Dict]: items = [] # Create one metadata item with quadkey=-1 # Convert float to decimal # https://blog.ruanbekker.com/blog/2019/02/05/convert-float-to-decimal-data-types-for-boto3-dynamodb-using-python/ meta = json.loads(json.dumps(self.metadata), parse_float=Decimal) # NOTE: quadkey is a string type meta["quadkey"] = "-1" items.append(meta) for quadkey, assets in self.mosaic_def.tiles.items(): item = {"quadkey": quadkey, "assets": assets} items.append(item) return items def _write_items(self, items: List[Dict]): with self.table.batch_writer() as batch: with click.progressbar(items, length=len(items), show_percent=True) as progitems: for item in progitems: batch.put_item(item) @functools.lru_cache(maxsize=512) def _read(self) -> MosaicJSON: # type: ignore """Get Mosaic definition info.""" meta = self._fetch_dynamodb("-1") # Numeric values are loaded from DynamoDB as Decimal types # Convert maxzoom, minzoom, quadkey_zoom to float/int for key in ["minzoom", "maxzoom", "quadkey_zoom"]: if meta.get(key): meta[key] = int(meta[key]) # Convert bounds, center to float/int for key in ["bounds", "center"]: if meta.get(key): meta[key] = list(map(float, meta[key])) # Create pydantic class # For now, a tiles key must exist meta["tiles"] = {} return MosaicJSON(**meta) @functools.lru_cache(maxsize=512) def get_assets(self, x: int, y: int, z: int) -> List[str]: """Find assets.""" mercator_tile = mercantile.Tile(x=x, y=y, z=z) quadkeys = find_quadkeys(mercator_tile, self.quadkey_zoom) assets = list( itertools.chain.from_iterable([ self._fetch_dynamodb(qk).get("assets", []) for qk in quadkeys ])) # Find mosaics recursively? return assets def _fetch_dynamodb(self, quadkey: str) -> Dict: return self.table.get_item(Key={"quadkey": quadkey}).get("Item", {})
def update( self, features: Sequence[Dict], add_first: bool = True, quiet: bool = False, **kwargs, ): """Update existing MosaicJSON on backend.""" logger.debug(f"Updating {self.mosaic_name}...") new_mosaic = MosaicJSON.from_features( features, self.mosaic_def.minzoom, self.mosaic_def.maxzoom, quadkey_zoom=self.quadkey_zoom, quiet=quiet, **kwargs, ) bounds = bbox_union(new_mosaic.bounds, self.mosaic_def.bounds) self.mosaic_def._increase_version() self.mosaic_def.bounds = bounds self.mosaic_def.center = ( (bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2, self.mosaic_def.minzoom, ) self.bounds = bounds with self.db: self.db.execute( f""" UPDATE {self._metadata_table} SET mosaicjson = :mosaicjson, name = :name, description = :description, version = :version, attribution = :attribution, minzoom = :minzoom, maxzoom = :maxzoom, quadkey_zoom = :quadkey_zoom, bounds = :bounds, center = :center WHERE name=:name """, self.mosaic_def.dict(), ) if add_first: self.db.executemany( f""" UPDATE "{self.mosaic_name}" SET assets = ( SELECT json_group_array(value) FROM ( SELECT value FROM json_each(?) UNION ALL SELECT value FROM json_each(assets) ) ) WHERE quadkey=?; """, [(assets, qk) for qk, assets in new_mosaic.tiles.items()], ) else: self.db.executemany( f""" UPDATE "{self.mosaic_name}" SET assets = ( SELECT json_group_array(value) FROM ( SELECT value FROM json_each(assets) UNION ALL SELECT value FROM json_each(?) ) ) WHERE quadkey=?; """, [(assets, qk) for qk, assets in new_mosaic.tiles.items()], )
def features_to_mosaicJSON(features: List[Dict], quadkey_zoom: int = None, minzoom: int = 7, maxzoom: int = 12, index: Union[bool, Dict] = True, sort='min-cloud') -> Dict: """ Create a mosaicJSON from stac features. Attributes ---------- features : list sat-api features. minzoom : int, optional, (default: 7) Mosaic Min Zoom. maxzoom : int, optional (default: 12) Mosaic Max Zoom. Returns ------- out : dict MosaicJSON definition. """ if not index: mosaic = MosaicJSON.from_features(features=features, minzoom=minzoom, maxzoom=maxzoom, quadkey_zoom=quadkey_zoom, accessor=landsat_accessor) return mosaic.dict(exclude_none=True) if not isinstance(index, dict): path = index_data_path() with gzip.open(path, 'rt') as f: index = json.load(f) # Define quadkey zoom from index quadkey_zoom = len(list(index.values())[0][0]) pr_keys = set(index.keys()) sorted_features = {} for feature in features: pathrow = feature['properties']['eo:column'].zfill( 3) + feature['properties']['eo:row'].zfill(3) if pathrow not in pr_keys: continue sorted_features[pathrow] = sorted_features.get(pathrow, []) sorted_features[pathrow].append(feature) tiles = {} for pathrow, feats in sorted_features.items(): if sort == 'min-cloud': selected = min(feats, key=lambda x: x['properties']['eo:cloud_cover']) elif sort == 'max-cloud': selected = max(feats, key=lambda x: x['properties']['eo:cloud_cover']) else: selected = feats[0] product_id = landsat_accessor(selected) quadkeys = index[pathrow] for qk in quadkeys: tiles[qk] = tiles.get(qk, set()) tiles[qk].add(product_id) bounds = quadkeys_to_bounds(tiles.keys()) mosaic = MosaicJSON(mosaicjson="0.0.2", minzoom=minzoom, maxzoom=maxzoom, quadkey_zoom=quadkey_zoom, bounds=bounds, tiles=tiles) return mosaic.dict(exclude_none=True)
def mosaic_bulk(meta_path, s3_list_path, min_scale, max_scale, min_year, max_year, woodland_tint, allow_orthophoto, bounds, minzoom, maxzoom, quadkey_zoom, sort_preference, closest_to_year, filter_only): """Create MosaicJSON from CSV of bulk metadata """ if (sort_preference == 'closest-to-year') and (not closest_to_year): msg = 'closest-to-year parameter required when sort-preference is closest-to-year' raise ValueError(msg) df = pd.read_csv(meta_path, low_memory=False) # Rename column names to lower case and snake case df = df.rename(columns=lambda col: col.lower().replace(' ', '_')) # Keep only historical maps # Newer maps are only in GeoPDF, and not in GeoTIFF, let alone COG df = df[df['series'] == 'HTMC'] # Create year column as Imprint Year if it exists, otherwise Date On Map df['year'] = df['imprint_year'].fillna(df['date_on_map']) # Apply filters if min_scale: df = df[df['scale'] >= min_scale] if max_scale: df = df[df['scale'] <= max_scale] if min_year: df = df[df['year'] >= min_year] if max_year: df = df[df['year'] <= max_year] if woodland_tint is not None: if woodland_tint: df = df[df['woodland_tint'] == 'Y'] else: df = df[df['woodland_tint'] == 'N'] if not allow_orthophoto: df = df[df['orthophoto'].isna()] # Create s3 GeoTIFF paths from metadata df['s3_tif'] = construct_s3_tif_url(df['download_product_s3']) if s3_list_path: # Load list of GeoTIFF files s3_files_df = load_s3_list(s3_list_path) # Keep only files that exist as GeoTIFF df = filter_cog_exists(df, s3_files_df) df['geometry'] = df.apply(construct_geometry, axis=1) gdf = gpd.GeoDataFrame(df) # Filter within provided bounding box if bounds: bounds = box(*map(float, bounds.split(','))) gdf = gdf[gdf.geometry.intersects(bounds)] if not maxzoom: maxzoom = gdf.apply( lambda row: get_maxzoom(row['scale'], row['scanner_resolution']), axis=1) # Take 75th percentile of maxzoom series maxzoom = int(round(maxzoom.describe()['75%'])) if not minzoom: minzoom = maxzoom - 5 # Columns to keep for creating MosaicJSON cols = ['scale', 'year', 's3_tif', 'geometry', 'cell_id'] if sort_preference == 'newest': sort_by = ['year', 'scale'] sort_ascending = [False, True] elif sort_preference == 'oldest': sort_by = ['year', 'scale'] sort_ascending = [True, True] elif sort_preference == 'closest-to-year': gdf['reference_year'] = (closest_to_year - gdf['year']).abs() sort_by = ['reference_year', 'scale'] sort_ascending = [True, True] cols.remove('year') cols.append('reference_year') if filter_only: for row in gdf[cols].iterfeatures(): print(json.dumps(row, separators=(',', ':'))) return # Convert to features features = gdf[cols].__geo_interface__['features'] mosaic = MosaicJSON.from_features(features, minzoom=minzoom, maxzoom=maxzoom, quadkey_zoom=quadkey_zoom, asset_filter=asset_filter, accessor=path_accessor, sort_by=sort_by, sort_ascending=sort_ascending) print(json.dumps(mosaic.dict(), separators=(',', ':')))