84a15f1c创建于 2025年7月17日历史提交
"""Tests for ``$ rio shapes``."""


import json
import re

import numpy as np
import pytest

import rasterio
from rasterio.rio.main import main_group


DEFAULT_SHAPE = (10, 10)


def bbox(*args):
    return ' '.join([str(x) for x in args])


def test_shapes(runner, pixelated_image_file):
    result = runner.invoke(main_group, ["shapes", "--collection", pixelated_image_file])
    assert result.exit_code == 0
    assert result.output.count('"FeatureCollection"') == 1
    assert result.output.count('"Feature"') == 4
    assert np.allclose(
        json.loads(result.output)["features"][0]["geometry"]["coordinates"],
        [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]],
    )


def test_shapes_invalid_bidx(runner, pixelated_image_file):
    result = runner.invoke(
        main_group, ['shapes', '--collection', pixelated_image_file, '--bidx', 4])

    assert result.exit_code == 1
    # Underlying exception message trapped by shapes


def test_shapes_sequence(runner, pixelated_image_file):
    """
    --sequence option should produce 4 features in series rather than
    inside a feature collection.
    """
    result = runner.invoke(
        main_group, ["shapes", "--collection", pixelated_image_file, "--sequence"]
    )

    assert result.exit_code == 0
    assert result.output.count('"FeatureCollection"') == 0
    assert result.output.count('"Feature"') == 4
    assert result.output.count("\n") == 4


def test_shapes_sequence_rs(runner, pixelated_image_file):
    """ --rs option should use the feature separator character. """

    result = runner.invoke(
        main_group, ['shapes', '--collection', pixelated_image_file, '--sequence', '--rs'])

    assert result.exit_code == 0
    assert result.output.count('"FeatureCollection"') == 0
    assert result.output.count('"Feature"') == 4
    assert result.output.count("\u001e") == 4


def test_shapes_with_nodata(runner, pixelated_image, pixelated_image_file):
    """
    An area of nodata should also be represented with a shape when using
    --with-nodata option
    """
    pixelated_image[0:2, 8:10] = 255

    with rasterio.open(pixelated_image_file, 'r+') as out:
        out.write(pixelated_image, indexes=1)

    result = runner.invoke(
        main_group, ['shapes', '--collection', pixelated_image_file, '--with-nodata'])
    assert result.exit_code == 0
    assert result.output.count('"FeatureCollection"') == 1
    assert result.output.count('"Feature"') == 5


def test_shapes_indent(runner, pixelated_image_file):
    """
    --indent option should produce lots of newlines and contiguous spaces
    """
    result = runner.invoke(
        main_group, ["shapes", "--collection", pixelated_image_file, "--indent", 2]
    )

    assert result.exit_code == 0
    assert result.output.count('"FeatureCollection"') == 1
    assert result.output.count('"Feature"') == 4
    assert result.output.count("\n") > 100
    assert result.output.count("        ") > 100


def test_shapes_compact(runner, pixelated_image_file):
    result = runner.invoke(
        main_group, ["shapes", "--collection", pixelated_image_file, "--compact"]
    )

    assert result.exit_code == 0
    assert result.output.count('"FeatureCollection"') == 1
    assert result.output.count('"Feature"') == 4
    assert result.output.count(", ") == 0
    assert result.output.count(": ") == 0


def test_shapes_sampling(runner, pixelated_image_file):
    """ --sampling option should remove the single pixel features """
    result = runner.invoke(
        main_group, ['shapes', '--collection', pixelated_image_file, '--sampling', 2])

    assert result.exit_code == 0
    assert result.output.count('"FeatureCollection"') == 1
    assert result.output.count('"Feature"') == 2


def test_shapes_precision(runner, pixelated_image_file):
    """ Output numbers should have no more than 1 decimal place """

    result = runner.invoke(
        main_group, ['shapes', '--collection', pixelated_image_file, '--precision', 1])

    assert result.exit_code == 0
    assert result.output.count('"FeatureCollection"') == 1
    assert result.output.count('"Feature"') == 4
    assert re.search(r'\s\d*\.\d{2,}', result.output) is None


def test_shapes_mask(runner, pixelated_image, pixelated_image_file):
    """ --mask should extract the nodata area of the image """
    shapely = pytest.importorskip("shapely", reason="Test requires shapely.")
    from shapely.geometry import shape

    pixelated_image[0:5, 0:10] = 255
    pixelated_image[0:10, 0:3] = 255
    pixelated_image[8:10, 8:10] = 255

    with rasterio.open(pixelated_image_file, 'r+') as out:
        out.write(pixelated_image, indexes=1)

    result = runner.invoke(
        main_group, ["shapes", "--collection", pixelated_image_file, "--mask"]
    )
    assert result.exit_code == 0
    assert result.output.count('"FeatureCollection"') == 1
    assert result.output.count('"Feature"') == 1
    assert shape(json.loads(result.output)["features"][0]["geometry"]).area == 31.0


def test_shapes_mask_sampling(runner, pixelated_image, pixelated_image_file):
    """using --sampling with the mask should snap coordinates to the nearest
    factor of 5
    """
    shapely = pytest.importorskip("shapely", reason="Test requires shapely.")
    from shapely.geometry import shape

    pixelated_image[0:5, 0:10] = 255
    pixelated_image[0:10, 0:3] = 255
    pixelated_image[8:10, 8:10] = 255

    with rasterio.open(pixelated_image_file, 'r+') as out:
        out.write(pixelated_image, indexes=1)

    result = runner.invoke(
        main_group,
        ["shapes", "--collection", pixelated_image_file, "--mask", "--sampling", 5],
    )

    assert result.exit_code == 0
    assert result.output.count('"FeatureCollection"') == 1
    assert result.output.count('"Feature"') == 1
    assert shape(json.loads(result.output)["features"][0]["geometry"]).area == 25.0


def test_shapes_band1_as_mask(runner, pixelated_image, pixelated_image_file):
    """
    When using --as-mask option, pixel value should not matter, only depends
    on pixels being contiguous.
    """
    shapely = pytest.importorskip("shapely", reason="Test requires shapely.")
    from shapely.geometry import shape

    pixelated_image[2:3, 2:3] = 4

    with rasterio.open(pixelated_image_file, 'r+') as out:
        out.write(pixelated_image, indexes=1)

    result = runner.invoke(
        main_group,
        [
            "shapes",
            "--collection",
            pixelated_image_file,
            "--band",
            "--bidx",
            "1",
            "--as-mask",
        ],
    )

    assert result.exit_code == 0
    assert result.output.count('"FeatureCollection"') == 1
    assert result.output.count('"Feature"') == 3
    assert shape(json.loads(result.output)["features"][0]["geometry"]).area == 1.0