Example #1
0
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)))
Example #2
0
    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)
Example #3
0
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=(',', ':')))
Example #4
0
    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
        )
Example #5
0
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'])
Example #6
0
    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
Example #7
0
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))
Example #8
0
    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)
Example #9
0
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
Example #10
0
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", {})
Example #11
0
    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()],
                )
Example #12
0
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)
Example #13
0
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=(',', ':')))