Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 8 additions & 14 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
## Summary


Fonction de lecture d'une tuile vecteur décodée.

## Changelog

### [Added]

* Level
* Fonction de test d'une tuile `is_in_limits` : ses indices sont ils dans les limites du niveau ?
* Pyramid
* La lecture d'une tuile vérifie avant que les indices sont bien dans les limites du niveau
* Les exceptions levées lors du décodage de la tuile raster emettent une exception `FormatError`
* `get_tile_indices` accepte en entrée un système de coordonnées : c'est celui des coordonnées fournies et permet de faire une reprojection si celui ci n'est pas le même que celui des données dans la pyramide
* Utils
* Meilleure gestion de reprojection par `reproject_bbox` : on détecte des systèmes identiques en entrée ou quand seul l'ordre des axes changent, pour éviter le calcul
* Ajout de la fonction de reprojection d'un point `reproject_point` : on détecte des systèmes identiques en entrée ou quand seul l'ordre des axes changent, pour éviter le calcul

* Décodage d'une tuile vecteur avec `get_tile_data_vector` (le pendant vecteur de `get_tile_data_raster`) : le résultat est un "dictionnaire GeoJSON", et les coordonnées sont en relatif à la tuile (souvent entre 0 et 4096)

### [Changed]

* Utils :
* `bbox_to_geometry` : on ne fournit plus de système de coordonnées, la fonction se content de créer la géométrie OGR à partir de la bbox, avec éventuellement une densification en points des bords
* Pyramid :
* Renommage de fonction : `update_limits` -> `set_limits_from_bbox`. Le but est d'être plus explicite sur le fonctionnement de la fonction (on écrase les limites, on ne les met pas juste à jour par union avec la bbox fournie)
* Storage
* La lecture d'un fichier ou objet qui n'existe pas émet toujours une exception `FileNotFoundError`
* Pyramid
* Si la tuile que l'on veut lire est dans une dalle qui n'existe pas, on retourne `None`

<!--
### [Added]

Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ classifiers = [
dependencies = [
"boto3 >= 1.26.54",
"pillow >= 9.4.0",
"numpy >= 1.24.2"
"numpy >= 1.24.2",
"mapbox-vector-tile >= 2.0.1"
]

[project.optional-dependencies]
Expand Down
49 changes: 48 additions & 1 deletion src/rok4/Pyramid.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import numpy
import zlib
import io
import mapbox_vector_tile
from PIL import Image

from rok4.Exceptions import *
Expand Down Expand Up @@ -815,7 +816,12 @@ def get_tile_data_binary(self, level: str, column: int, row: int) -> str:
# Une dalle ROK4 a une en-tête fixe de 2048 octets,
# puis sont stockés les offsets (chacun sur 4 octets)
# puis les tailles (chacune sur 4 octets)
binary_index = get_data_binary(slab_path, (2048, 2 * 4 * level_object.slab_width * level_object.slab_height))
try:
binary_index = get_data_binary(slab_path, (2048, 2 * 4 * level_object.slab_width * level_object.slab_height))
except FileNotFoundError as e:
# L'absence de la dalle est gérée comme simplement une absence de données
return None

offsets = numpy.frombuffer(
binary_index,
dtype = numpy.dtype('uint32'),
Expand Down Expand Up @@ -928,6 +934,47 @@ def get_tile_data_raster(self, level: str, column: int, row: int) -> numpy.ndarr

return data

def get_tile_data_vector(self, level: str, column: int, row: int) -> Dict:
"""Get a vector pyramid's tile as GeoJSON dictionnary

Args:
level (str): Tile's level
column (int): Tile's column
row (int): Tile's row

Raises:
Exception: Cannot get vector data for a raster pyramid
Exception: Level not found in the pyramid
NotImplementedError: Pyramid owns one-tile slabs
NotImplementedError: Vector pyramid format not handled
MissingEnvironmentError: Missing object storage informations
StorageError: Storage read issue
FormatError: Cannot decode tile

Returns:
str: data, as GeoJSON dictionnary. None if no data
"""

if self.type == PyramidType.RASTER:
raise Exception("Cannot get tile as vector data : it's a raster pyramid")

binary_tile = self.get_tile_data_binary(level, column, row)

if binary_tile is None:
return None

level_object = self.get_level(level)

if self.__format == "TIFF_PBF_MVT":
try:
data = mapbox_vector_tile.decode(binary_tile)
except Exception as e:
raise FormatError("PBF (MVT)", "binary tile", e)
else:
raise NotImplementedError(f"Cannot get tile as vector data for format {self.__format}")

return data

def get_tile_indices(self, x: float, y: float, level: str = None, **kwargs) -> Tuple[str, int, int, int, int]:
"""Get pyramid's tile and pixel indices from point's coordinates

Expand Down
15 changes: 15 additions & 0 deletions src/rok4/Storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ def get_data_str(path: str) -> str:
Raises:
MissingEnvironmentError: Missing object storage informations
StorageError: Storage read issue
FileNotFoundError: File or object does not exist

Returns:
str: Data content
Expand All @@ -257,6 +258,7 @@ def get_data_binary(path: str, range: Tuple[int, int] = None) -> str:
Raises:
MissingEnvironmentError: Missing object storage informations
StorageError: Storage read issue
FileNotFoundError: File or object does not exist

Returns:
str: Data binary content
Expand All @@ -281,6 +283,12 @@ def get_data_binary(path: str, range: Tuple[int, int] = None) -> str:
Range=f"bytes=${range[0]}-${range[1] - range[0] - 1}"
)['Body'].read()

except botocore.exceptions.ClientError as e:
if e.response['Error']['Code'] == "404":
raise FileNotFoundError(f"{storage_type.value}{path}")
else:
raise StorageError("S3", e)

except Exception as e:
raise StorageError("S3", e)

Expand All @@ -295,6 +303,9 @@ def get_data_binary(path: str, range: Tuple[int, int] = None) -> str:
else:
data = ioctx.read(base_name, range[1], range[0])

except rados.ObjectNotFound as e:
raise FileNotFoundError(f"{storage_type.value}{path}")

except Exception as e:
raise StorageError("CEPH", e)

Expand All @@ -309,6 +320,10 @@ def get_data_binary(path: str, range: Tuple[int, int] = None) -> str:
data = f.read(range[1])

f.close()

except FileNotFoundError as e:
raise FileNotFoundError(f"{storage_type.value}{path}")

except Exception as e:
raise StorageError("FILE", e)

Expand Down
23 changes: 23 additions & 0 deletions tests/fixtures/TIFF_PBF_MVT.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"levels": [
{
"storage": {
"image_directory": "TIFF_PBF_MVT/DATA/4",
"type": "FILE",
"path_depth": 2
},
"tables": [],
"id": "4",
"tiles_per_height": 4,
"tile_limits": {
"max_col": 15,
"min_col": 0,
"max_row": 25,
"min_row": 0
},
"tiles_per_width": 4
}
],
"format": "TIFF_PBF_MVT",
"tile_matrix_set": "PM"
}
Binary file added tests/fixtures/TIFF_PBF_MVT/DATA/4/00/00/21.tif
Binary file not shown.
35 changes: 31 additions & 4 deletions tests/test_Pyramid.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,16 +158,14 @@ def test_vector_ok(mocked_tms_class, mocked_get_data_str):

@mock.patch.dict(os.environ, {}, clear=True)
@mock.patch('rok4.Pyramid.TileMatrixSet')
def test_tile_read(mocked_tms_class):
def test_tile_read_raster(mocked_tms_class):

tms_instance = MagicMock()
tms_instance.name = "UTM20W84MART_1M_MNT"
tms_instance.srs = "IGNF:UTM20W84MART"

tm_instance = MagicMock()
tm_instance.id = "8"
tm_instance.resolution = 1
tm_instance.point_to_indices.return_value = (0,0,128,157)
tm_instance.tile_size = (256,256)

tms_instance.get_level.return_value = tm_instance
Expand All @@ -181,8 +179,37 @@ def test_tile_read(mocked_tms_class):
assert data.shape == (256,256,1)
assert data[128][128][0] == 447.25
except Exception as exc:
assert False, f"Pyramid creation raises an exception: {exc}"
assert False, f"Pyramid raster tile read raises an exception: {exc}"



@mock.patch.dict(os.environ, {}, clear=True)
@mock.patch('rok4.Pyramid.TileMatrixSet')
def test_tile_read_vector(mocked_tms_class):

tms_instance = MagicMock()
tms_instance.name = "PM"
tms_instance.srs = "EPSG:3857"

tm_instance = MagicMock()
tm_instance.id = "4"
tm_instance.tile_size = (256,256)

tms_instance.get_level.return_value = tm_instance

mocked_tms_class.return_value = tms_instance

try:
pyramid = Pyramid.from_descriptor("file://tests/fixtures/TIFF_PBF_MVT.json")

data = pyramid.get_tile_data_vector("4", 5, 5)
assert data is None

data = pyramid.get_tile_data_vector("4", 8, 5)
assert type(data) is dict
assert "ecoregions_3857" in data
except Exception as exc:
assert False, f"Pyramid vector tile read raises an exception: {exc}"

def test_b36_path_decode():
assert b36_path_decode("3E/42/01.tif") == (4032, 18217,)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_Storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def test_s3_invalid_endpoint(mocked_s3_client):
@mock.patch.dict(os.environ, {}, clear=True)
@mock.patch("builtins.open", side_effect=FileNotFoundError("not_found"))
def test_file_read_error(mock_file):
with pytest.raises(StorageError):
with pytest.raises(FileNotFoundError):
data = get_data_str("file:///path/to/file.ext")

mock_file.assert_called_with("/path/to/file.ext", "rb")
Expand Down