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
5 changes: 3 additions & 2 deletions .github/workflows/build-and-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -192,14 +192,15 @@ jobs:
sed "s#__version__#${{ github.ref_name }}#" templates/index.template.md >docs/index.md
echo "# Versions" >docs/versions.md
echo "" >>docs/versions.md
for v in `ls -t docs/versions`; do sed "s#__version__#$v#" templates/versions.template.md >>docs/versions.md; done
for v in `ls -t docs/versions | grep -v latest`; do sed "s#__version__#$v#" templates/versions.template.md >>docs/versions.md; done
sed "s#__version__#${{ github.ref_name }}#" templates/latest.template.html >docs/versions/latest/index.html
rm -r artifact

- name: Publish on gh-pages branch
run: |
git config user.name github-actions
git config user.email github-actions@github.com
git add -v docs/versions/${{ github.ref_name }}/ docs/index.md docs/versions.md
git add -v docs/versions/${{ github.ref_name }}/ docs/versions/latest/ docs/index.md docs/versions.md
git commit -m "Add documentation for version ${{ github.ref_name }}"
git push

Expand Down
67 changes: 67 additions & 0 deletions .github/workflows/build-docs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Build doc site with mkdocs and deploy

on:
workflow_dispatch:

jobs:

build:

name: Build doc site with mkdocs

runs-on: ubuntu-latest

steps:
- name: Checkout project
uses: actions/checkout@v3
with:
ref: 'gh-pages'
token: ${{ secrets.GITHUB_TOKEN }}

- name: Setup python
uses: actions/setup-python@v4
with:
python-version: "3.10"
cache: 'pip'

- name: Install Mkdocs
run: pip install -r requirements.txt

- name: Run mkdocs and archive
run : |
mkdocs build
tar \
--dereference --hard-dereference \
--directory "site" \
-cvf "${{ runner.temp }}/site.tar" \
--exclude=.git \
--exclude=.github \
.

- name: Upload pages
uses: actions/upload-artifact@main
with:
name: github-pages
path: ${{ runner.temp }}/site.tar
retention-days: 1

deploy:

name: Deploy site to github pages

needs: build

permissions:
pages: write # to deploy to Pages
id-token: write # to verify the deployment originates from an appropriate source

# Deploy to the github-pages environment
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}

runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
31 changes: 14 additions & 17 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
## Summary

Ajout de fonctionnalités de lecture de donnée d'une pyramide et suivi des recommandations PyPA pour la gestion du projet.


## Changelog

### [Added]

* TileMatrix :
* Fonction de calcul des indices de tuile et de pixel dans la tuile à partir d'un point dans le système de coordonnées du TMS
* Pyramid :
* Fonction de calcul des indices de tuile et de pixel dans la tuile à partir d'un point dans le système de coordonnées du TMS et éventuellement un niveau
* Fonctions de lecture d'une tuile : au format binaire source ou au format tableau à 3 dimensions pour les tuiles raster
* Storage :
* Fonction de lecture binaire, complète ou partielle, d'un fichier ou objet S3 ou CEPH
* Exceptions : NotImplementedError permet de préciser qu'une fonctionnalité n'a pas été implémentée pour tous les cas. Ici, on ne gère pas la décompression des données raster pour les compressions packbit et LZW
* 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

* Ajout de la publication PyPI dans la CI GitHub

### [Changed]

* Storage :
* La lecture sous forme de chaîne s'appuie sur la lecture complète binaire. Aucun changement à l'usage.
* TileMatrixSet : quelque soit le système de coordonnées, on ne gère que un ordre des axes X,Y ou Lon,Lat. Cependant, les fonctions de calcul de ou à partir de bbox respectent l'ordre du système dans ces dernières.

* Passage de la configuration du projet dans le fichier `pyproject.toml`

* 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)
<!--
### [Added]

Expand Down
2 changes: 1 addition & 1 deletion src/rok4/Layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def from_descriptor(cls, descriptor: str) -> 'Layer':
layer.__bbox = reproject_bbox(layer.__geobbox, "EPSG:4326", layer.__tms.srs, 5)
# On force l'emprise de la couche, on recalcule donc les tuiles limites correspondantes pour chaque niveau
for l in layer.__levels.values():
l.update_limits(layer.__bbox)
l.set_limits_from_bbox(layer.__bbox)
else:
layer.__bbox = layer.__best_level.bbox
layer.__geobbox = reproject_bbox(layer.__bbox, layer.__tms.srs, "EPSG:4326", 5)
Expand Down
82 changes: 62 additions & 20 deletions src/rok4/Pyramid.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from rok4.Exceptions import *
from rok4.TileMatrixSet import TileMatrixSet, TileMatrix
from rok4.Storage import *
from rok4.Utils import *

class PyramidType(Enum):
RASTER = "RASTER"
Expand Down Expand Up @@ -299,23 +300,33 @@ def slab_width(self) -> int:
def slab_height(self) -> int:
return self.__slab_size[1]

def update_limits(self, bbox: Tuple[float, float, float, float]) -> None:
"""Update tile limits, based on provided bounding box
def is_in_limits(self, column: int, row: int) -> bool:
"""Is the tile indices in limits ?

Args:
column (int): tile's column
row (int): tile's row

Returns:
bool: True if tiles' limits contain the provided tile's indices
"""
return self.__tile_limits["min_row"] <= row and self.__tile_limits["max_row"] >= row and self.__tile_limits["min_col"] <= column and self.__tile_limits["max_col"] >= column

def set_limits_from_bbox(self, bbox: Tuple[float, float, float, float]) -> None:
"""Set tile limits, based on provided bounding box

Args:
bbox (Tuple[float, float, float, float]): terrain extent (xmin, ymin, xmax, ymax), in TMS coordinates system

"""
print(self.id)
print(self.__tile_limits)

col_min, row_min, col_max, row_max = self.__pyramid.tms.get_level(self.__id).bbox_to_tiles(bbox)
self.__tile_limits = {
"min_row": row_min,
"max_col": col_max,
"max_row": row_max,
"min_col": col_min
}
print(self.__tile_limits)


class Pyramid:
Expand Down Expand Up @@ -772,7 +783,7 @@ def get_tile_data_binary(self, level: str, column: int, row: int) -> str:
StorageError: Storage read issue

Returns:
str: data, as binary string
str: data, as binary string, None if no data
"""

level_object = self.get_level(level)
Expand All @@ -783,6 +794,9 @@ def get_tile_data_binary(self, level: str, column: int, row: int) -> str:
if level_object.slab_width == 1 and level_object.slab_height == 1:
raise NotImplementedError(f"One-tile slab pyramid is not handled")

if not level_object.is_in_limits(column, row):
return None

# Indices de la dalle
slab_column = column // level_object.slab_width
slab_row = row // level_object.slab_height
Expand Down Expand Up @@ -814,6 +828,9 @@ def get_tile_data_binary(self, level: str, column: int, row: int) -> str:
count = level_object.slab_width * level_object.slab_height
)

if sizes[tile_index] == 0:
return None

return get_data_binary(slab_path, (offsets[tile_index], sizes[tile_index]))

def get_tile_data_raster(self, level: str, column: int, row: int) -> numpy.ndarray:
Expand All @@ -836,21 +853,30 @@ def get_tile_data_raster(self, level: str, column: int, row: int) -> numpy.ndarr
NotImplementedError: Raster pyramid format not handled
MissingEnvironmentError: Missing object storage informations
StorageError: Storage read issue
FormatError: Cannot decode tile

Returns:
str: data, as binary string
str: data, as numpy array, None if no data
"""

if self.type == PyramidType.VECTOR:
raise Exception("Cannot get tile as raster data : it's a vector 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_JPG_UINT8" or self.__format == "TIFF_JPG90_UINT8":

img = Image.open(io.BytesIO(binary_tile))
try:
img = Image.open(io.BytesIO(binary_tile))
except Exception as e:
raise FormatError("JPEG", "binary tile", e)

data = numpy.asarray(img)

elif self.__format == "TIFF_RAW_UINT8":
Expand All @@ -861,21 +887,33 @@ def get_tile_data_raster(self, level: str, column: int, row: int) -> numpy.ndarr
data.shape = (level_object.tile_matrix.tile_size[0], level_object.tile_matrix.tile_size[1], self.__raster_specifications["channels"])

elif self.__format == "TIFF_PNG_UINT8":
img = Image.open(io.BytesIO(binary_tile))
try:
img = Image.open(io.BytesIO(binary_tile))
except Exception as e:
raise FormatError("PNG", "binary tile", e)

data = numpy.asarray(img)

elif self.__format == "TIFF_ZIP_UINT8":
data = numpy.frombuffer(
zlib.decompress( binary_tile ),
dtype = numpy.dtype('uint8')
)
try:
data = numpy.frombuffer(
zlib.decompress( binary_tile ),
dtype = numpy.dtype('uint8')
)
except Exception as e:
raise FormatError("ZIP", "binary tile", e)

data.shape = (level_object.tile_matrix.tile_size[0], level_object.tile_matrix.tile_size[1], self.__raster_specifications["channels"])

elif self.__format == "TIFF_ZIP_FLOAT32":
data = numpy.frombuffer(
zlib.decompress( binary_tile ),
dtype = numpy.dtype('float32')
)
try:
data = numpy.frombuffer(
zlib.decompress( binary_tile ),
dtype = numpy.dtype('float32')
)
except Exception as e:
raise FormatError("ZIP", "binary tile", e)

data.shape = (level_object.tile_matrix.tile_size[0], level_object.tile_matrix.tile_size[1], self.__raster_specifications["channels"])

elif self.__format == "TIFF_RAW_FLOAT32":
Expand All @@ -888,10 +926,9 @@ def get_tile_data_raster(self, level: str, column: int, row: int) -> numpy.ndarr
else:
raise NotImplementedError(f"Cannot get tile as raster data for format {self.__format}")


return data

def get_tile_indices(self, x: float, y: float, level: str = None) -> Tuple[str, int, int, int, int]:
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

Used coordinates system have to be the pyramide one. If EPSG:4326, x is latitude and y longitude.
Expand All @@ -900,9 +937,11 @@ def get_tile_indices(self, x: float, y: float, level: str = None) -> Tuple[str,
x (float): point's x
y (float): point's y
level (str, optional): Pyramid's level to take into account, the bottom one if None . Defaults to None.
**srs (string): spatial reference system of provided coordinates, with authority and code (same as the pyramid's one if not provided)

Raises:
Exception: Cannot find level to calculate indices
RuntimeError: Provided SRS is invalid for OSR

Returns:
Tuple[str, int, int, int, int]: Level identifier, tile's column, tile's row, pixel's (in the tile) column, pixel's row
Expand All @@ -915,5 +954,8 @@ def get_tile_indices(self, x: float, y: float, level: str = None) -> Tuple[str,
if level_object is None:
raise Exception(f"Cannot found the level to calculate indices")

return (level_object.id,) + level_object.tile_matrix.point_to_indices(x, y)
if "srs" in kwargs and kwargs["srs"] is not None and kwargs["srs"].upper() != self.__tms.srs.upper():
sr = srs_to_spatialreference(kwargs["srs"])
x, y = reproject_point((x, y), sr, self.__tms.sr )

return (level_object.id,) + level_object.tile_matrix.point_to_indices(x, y)
Loading