Example #1
0
    def test_expand_bbox_to_ratio(self):
        bbox = [
            623869.6556559108, 2458358.334500141, 4291621.974352865,
            5270015.93640312, "EPSG:3857"
        ]
        center = [(bbox[1] + bbox[0]) / 2, (bbox[3] + bbox[2]) / 2]

        width = 250
        height = 200

        new_bbox = utils.expand_bbox_to_ratio(bbox,
                                              target_height=height,
                                              target_width=width)

        # round to 4 decimal places in order to get rid of float rounding errors
        ratio = round(
            abs(new_bbox[3] - new_bbox[2]) / abs(new_bbox[1] - new_bbox[0]), 4)

        new_center = [(new_bbox[1] + new_bbox[0]) / 2,
                      (new_bbox[3] + new_bbox[2]) / 2]

        self.assertEqual(
            height / width, ratio,
            "Expected ratio to be equal target ratio after transformation")
        self.assertEqual(
            center, new_center,
            "Expected center to be preserved after transformation")
Example #2
0
def create_thumbnail(
    instance: Union[Layer, Map],
    wms_version: str = settings.OGC_SERVER["default"].get("WMS_VERSION", "1.1.1"),
    bbox: Optional[Union[List, Tuple]] = None,
    forced_crs: Optional[str] = None,
    styles: Optional[List] = None,
    overwrite: bool = False,
    background_zoom: Optional[int] = None,
) -> None:
    """
    Function generating and saving a thumbnail of the given instance (Layer or Map), which is composed of
    outcomes of WMS GetMap queries to the instance's layers providers, and an outcome of querying background
    provider for thumbnail's background (by default Slippy Map provider).

    :param instance: instance of Layer or Map models
    :param wms_version: WMS version of the query
    :param bbox: bounding box of the thumbnail in format: (west, east, south, north, CRS), where CRS is in format
                 "EPSG:XXXX"
    :param forced_crs: CRS which should be used to fetch data from WMS services in format "EPSG:XXXX". By default
                       all data is translated and retrieved in EPSG:3857, since this enables background fetching from
                       Slippy Maps providers. Forcing another CRS can cause skipping background generation in
                       the thumbnail
    :param styles: styles, which OGC server should use for rendering an image
    :param overwrite: overwrite existing thumbnail
    :param background_zoom: zoom of the XYZ Slippy Map used to retrieve background image,
                            if Slippy Map is used as background
    """

    instance.refresh_from_db()

    default_thumbnail_name = _generate_thumbnail_name(instance)
    mime_type = "image/png"
    width = settings.THUMBNAIL_SIZE["width"]
    height = settings.THUMBNAIL_SIZE["height"]

    if default_thumbnail_name is None:
        # instance is Map and has no layers defined
        utils.assign_missing_thumbnail(instance)
        return

    # handle custom, uploaded thumbnails, which may have different extensions from the default thumbnail
    thumbnail_exists = False
    if instance.thumbnail_url and instance.thumbnail_url != settings.MISSING_THUMBNAIL:
        thumbnail_exists = thumb_exists(instance.thumbnail_url.rsplit('/')[-1])

    if (thumbnail_exists or thumb_exists(default_thumbnail_name)) and not overwrite:
        logger.debug(f"Thumbnail for {instance.name} already exists. Skipping thumbnail generation.")
        return

    # --- determine target CRS and bbox ---
    target_crs = forced_crs.upper() if forced_crs is not None else "EPSG:3857"

    compute_bbox_from_layers = False
    if bbox:
        # make sure BBOX is provided with the CRS in a correct format
        source_crs = bbox[-1]

        srid_regex = re.match(r"EPSG:\d+", source_crs)
        if not srid_regex:
            logger.error(f"Thumbnail bbox is in a wrong format: {bbox}")
            raise ThumbnailError("Wrong BBOX format")

        # for the EPSG:3857 (default thumb's CRS) - make sure received BBOX can be transformed to the target CRS;
        # if it can't be (original coords are outside of the area of use of EPSG:3857), thumbnail generation with
        # the provided bbox is impossible.
        if target_crs == 'EPSG:3857' and bbox[-1].upper() != 'EPSG:3857':
            bbox = utils.crop_to_3857_area_of_use(bbox)

        bbox = utils.transform_bbox(bbox, target_crs=target_crs)
    else:
        compute_bbox_from_layers = True

    # --- define layer locations ---
    locations, layers_bbox = _layers_locations(instance, compute_bbox=compute_bbox_from_layers, target_crs=target_crs)

    if compute_bbox_from_layers:
        if not layers_bbox:
            raise ThumbnailError(f"Thumbnail generation couldn't determine a BBOX for: {instance}.")
        else:
            bbox = layers_bbox

    # --- expand the BBOX to match the set thumbnail's ratio (prevent thumbnail's distortions) ---
    bbox = utils.expand_bbox_to_ratio(bbox)

    # --- add default style ---
    if not styles and hasattr(instance, "default_style"):
        if instance.default_style:
            styles = [instance.default_style.name]

    # --- fetch WMS layers ---
    partial_thumbs = []

    for ogc_server, layers in locations:
        try:
            partial_thumbs.append(utils.get_map(
                ogc_server,
                layers,
                wms_version=wms_version,
                bbox=bbox,
                mime_type=mime_type,
                styles=styles,
                width=width,
                height=height,
            ))
        except Exception as e:
            logger.error(f"Exception occurred while fetching partial thumbnail for {instance.name}.")
            logger.exception(e)

    if not partial_thumbs:
        utils.assign_missing_thumbnail(instance)
        raise ThumbnailError("Thumbnail generation failed - no image retrieved from WMS services.")

    # --- merge retrieved WMS images ---
    merged_partial_thumbs = Image.new("RGBA", (width, height), (255, 255, 255, 0))

    for image in partial_thumbs:
        if image:
            content = BytesIO(image)
            try:
                img = Image.open(content)
                img.verify()  # verify that it is, in fact an image
                img = Image.open(BytesIO(image))  # "re-open" the file (required after running verify method)
                merged_partial_thumbs.paste(img, mask=img.convert('RGBA'))
            except UnidentifiedImageError as e:
                logger.error(f"Thumbnail generation. Error occurred while fetching layer image: {image}")
                logger.exception(e)

    # --- fetch background image ---
    try:
        BackgroundGenerator = import_string(settings.THUMBNAIL_BACKGROUND["class"])
        background = BackgroundGenerator(width, height).fetch(bbox, background_zoom)
    except Exception as e:
        logger.error(f"Thumbnail generation. Error occurred while fetching background image: {e}")
        logger.exception(e)
        background = None

    # --- overlay image with background ---
    thumbnail = Image.new("RGB", (width, height), (250, 250, 250))

    if background is not None:
        thumbnail.paste(background, (0, 0))

    thumbnail.paste(merged_partial_thumbs, (0, 0), merged_partial_thumbs)

    # convert image to the format required by save_thumbnail
    with BytesIO() as output:
        thumbnail.save(output, format="PNG")
        content = output.getvalue()

    # save thumbnail
    instance.save_thumbnail(default_thumbnail_name, image=content)
Example #3
0
def create_thumbnail(
    instance: Union[Dataset, Map],
    wms_version: str = settings.OGC_SERVER["default"].get("WMS_VERSION", "1.1.1"),
    bbox: Optional[Union[List, Tuple]] = None,
    forced_crs: Optional[str] = None,
    styles: Optional[List] = None,
    overwrite: bool = False,
    background_zoom: Optional[int] = None,
) -> None:
    """
    Function generating and saving a thumbnail of the given instance (Dataset or Map), which is composed of
    outcomes of WMS GetMap queries to the instance's datasets providers, and an outcome of querying background
    provider for thumbnail's background (by default Slippy Map provider).

    :param instance: instance of Dataset or Map models
    :param wms_version: WMS version of the query
    :param bbox: bounding box of the thumbnail in format: (west, east, south, north, CRS), where CRS is in format
                 "EPSG:XXXX"
    :param forced_crs: CRS which should be used to fetch data from WMS services in format "EPSG:XXXX". By default
                       all data is translated and retrieved in EPSG:3857, since this enables background fetching from
                       Slippy Maps providers. Forcing another CRS can cause skipping background generation in
                       the thumbnail
    :param styles: styles, which OGC server should use for rendering an image
    :param overwrite: overwrite existing thumbnail
    :param background_zoom: zoom of the XYZ Slippy Map used to retrieve background image,
                            if Slippy Map is used as background
    """

    instance.refresh_from_db()

    default_thumbnail_name = _generate_thumbnail_name(instance)
    mime_type = "image/png"
    width = settings.THUMBNAIL_SIZE["width"]
    height = settings.THUMBNAIL_SIZE["height"]

    if default_thumbnail_name is None:
        # instance is Map and has no datasets defined
        utils.assign_missing_thumbnail(instance)
        return

    # handle custom, uploaded thumbnails, which may have different extensions from the default thumbnail
    thumbnail_exists = False
    if instance.thumbnail_url and instance.thumbnail_url != static(utils.MISSING_THUMB):
        thumbnail_exists = utils.thumb_exists(instance.thumbnail_url.rsplit('/')[-1])

    if (thumbnail_exists or utils.thumb_exists(default_thumbnail_name)) and not overwrite:
        logger.debug(f"Thumbnail for {instance.name} already exists. Skipping thumbnail generation.")
        return

    # --- determine target CRS and bbox ---
    target_crs = forced_crs.upper() if forced_crs is not None else "EPSG:3857"

    compute_bbox_from_datasets = False
    is_map_with_datasets = True

    if isinstance(instance, Map):
        is_map_with_datasets = MapLayer.objects.filter(map=instance, local=True).exclude(dataset=None).count() > 0
    if bbox:
        bbox = utils.clean_bbox(bbox, target_crs)
    elif instance.ll_bbox_polygon:
        _bbox = BBOXHelper(instance.ll_bbox_polygon.extent)
        srid = instance.ll_bbox_polygon.srid
        bbox = [_bbox.xmin, _bbox.xmax, _bbox.ymin, _bbox.ymax, f"EPSG:{srid}"]
        bbox = utils.clean_bbox(bbox, target_crs)
    else:
        compute_bbox_from_datasets = True
    # --- define dataset locations ---
    locations, datasets_bbox = _datasets_locations(instance, compute_bbox=compute_bbox_from_datasets, target_crs=target_crs)

    if compute_bbox_from_datasets and is_map_with_datasets:
        if not datasets_bbox:
            raise ThumbnailError(f"Thumbnail generation couldn't determine a BBOX for: {instance}.")
        else:
            bbox = datasets_bbox

    # --- expand the BBOX to match the set thumbnail's ratio (prevent thumbnail's distortions) ---
    bbox = utils.expand_bbox_to_ratio(bbox) if bbox else None

    # --- add default style ---
    if not styles and hasattr(instance, "default_style"):
        if instance.default_style:
            styles = [instance.default_style.name]

    # --- fetch WMS datasets ---
    partial_thumbs = []

    for ogc_server, datasets, _styles in locations:
        if isinstance(instance, Map) and len(datasets) == len(_styles):
            styles = _styles
        try:
            partial_thumbs.append(
                utils.get_map(
                    ogc_server,
                    datasets,
                    wms_version=wms_version,
                    bbox=bbox,
                    mime_type=mime_type,
                    styles=styles,
                    width=width,
                    height=height,
                )
            )
        except Exception as e:
            logger.error(f"Exception occurred while fetching partial thumbnail for {instance.title}.")
            logger.exception(e)

    if not partial_thumbs and is_map_with_datasets:
        utils.assign_missing_thumbnail(instance)
        raise ThumbnailError("Thumbnail generation failed - no image retrieved from WMS services.")

    # --- merge retrieved WMS images ---
    merged_partial_thumbs = Image.new("RGBA", (width, height), (255, 255, 255, 0))

    for image in partial_thumbs:
        if image:
            content = BytesIO(image)
            try:
                img = Image.open(content)
                img.verify()  # verify that it is, in fact an image
                img = Image.open(BytesIO(image))  # "re-open" the file (required after running verify method)
                merged_partial_thumbs.paste(img, mask=img.convert('RGBA'))
            except UnidentifiedImageError as e:
                logger.error(f"Thumbnail generation. Error occurred while fetching dataset image: {image}")
                logger.exception(e)

    # --- fetch background image ---
    try:
        BackgroundGenerator = import_string(settings.THUMBNAIL_BACKGROUND["class"])
        background = BackgroundGenerator(width, height).fetch(bbox, background_zoom) if bbox else None
    except Exception as e:
        logger.error(f"Thumbnail generation. Error occurred while fetching background image: {e}")
        logger.exception(e)
        background = None

    # --- overlay image with background ---
    thumbnail = Image.new("RGB", (width, height), (250, 250, 250))

    if background is not None:
        thumbnail.paste(background, (0, 0))

    thumbnail.paste(merged_partial_thumbs, (0, 0), merged_partial_thumbs)

    # convert image to the format required by save_thumbnail
    with BytesIO() as output:
        thumbnail.save(output, format="PNG")
        content = output.getvalue()

    # save thumbnail
    instance.save_thumbnail(default_thumbnail_name, image=content)
    return instance.thumbnail_url