Coverage for src/rok4/tile_matrix_set.py: 96%
77 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-01 15:35 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-01 15:35 +0000
1"""Provide classes to use a tile matrix set.
3The module contains the following classes:
5- `TileMatrixSet` - Multi level grid
6- `TileMatrix` - A tile matrix set level
8Loading a tile matrix set requires environment variables :
10- ROK4_TMS_DIRECTORY
11"""
13# -- IMPORTS --
15# standard library
16import json
17import os
18from json.decoder import JSONDecodeError
19from typing import Dict, List, Tuple
21# package
22from rok4.exceptions import FormatError, MissingAttributeError, MissingEnvironmentError
23from rok4.storage import get_data_str
24from rok4.utils import srs_to_spatialreference
26# -- GLOBALS --
29class TileMatrix:
30 """A tile matrix is a tile matrix set's level.
32 Attributes:
33 id (str): TM identifiant (no underscore).
34 tms (TileMatrixSet): TMS to whom it belong
35 resolution (float): Ground size of a pixel, using unity of the TMS's coordinate system.
36 origin (Tuple[float, float]): X,Y coordinates of the upper left corner for the level, the grid's origin.
37 tile_size (Tuple[int, int]): Pixel width and height of a tile.
38 matrix_size (Tuple[int, int]): Number of tile in the level, widthwise and heightwise.
39 """
41 def __init__(self, level: Dict, tms: "TileMatrixSet") -> None:
42 """Constructor method
44 Args:
45 level: Level attributes, according to JSON structure
46 tms: TMS object containing the level to create
48 Raises:
49 MissingAttributeError: Attribute is missing in the content
50 """
52 self.tms = tms
53 try:
54 self.id = level["id"]
55 if self.id.find("_") != -1:
56 raise Exception(
57 f"TMS {tms.path} owns a level whom id contains an underscore ({self.id})"
58 )
59 self.resolution = level["cellSize"]
60 self.origin = (
61 level["pointOfOrigin"][0],
62 level["pointOfOrigin"][1],
63 )
64 self.tile_size = (
65 level["tileWidth"],
66 level["tileHeight"],
67 )
68 self.matrix_size = (
69 level["matrixWidth"],
70 level["matrixHeight"],
71 )
72 self.__latlon = (
73 self.tms.sr.EPSGTreatsAsLatLong() or self.tms.sr.EPSGTreatsAsNorthingEasting()
74 )
75 except KeyError as e:
76 raise MissingAttributeError(tms.path, f"tileMatrices[].{e}")
78 def x_to_column(self, x: float) -> int:
79 """Convert west-east coordinate to tile's column
81 Args:
82 x (float): west-east coordinate (TMS coordinates system)
84 Returns:
85 int: tile's column
86 """
87 return int((x - self.origin[0]) / (self.resolution * self.tile_size[0]))
89 def y_to_row(self, y: float) -> int:
90 """Convert north-south coordinate to tile's row
92 Args:
93 y (float): north-south coordinate (TMS coordinates system)
95 Returns:
96 int: tile's row
97 """
98 return int((self.origin[1] - y) / (self.resolution * self.tile_size[1]))
100 def tile_to_bbox(self, tile_col: int, tile_row: int) -> Tuple[float, float, float, float]:
101 """Get tile terrain extent (xmin, ymin, xmax, ymax), in TMS coordinates system
103 TMS spatial reference is Lat / Lon case is handled.
105 Args:
106 tile_col (int): column indice
107 tile_row (int): row indice
109 Returns:
110 Tuple[float, float, float, float]: terrain extent (xmin, ymin, xmax, ymax)
111 """
112 if self.__latlon:
113 return (
114 self.origin[1] - self.resolution * (tile_row + 1) * self.tile_size[1],
115 self.origin[0] + self.resolution * tile_col * self.tile_size[0],
116 self.origin[1] - self.resolution * tile_row * self.tile_size[1],
117 self.origin[0] + self.resolution * (tile_col + 1) * self.tile_size[0],
118 )
119 else:
120 return (
121 self.origin[0] + self.resolution * tile_col * self.tile_size[0],
122 self.origin[1] - self.resolution * (tile_row + 1) * self.tile_size[1],
123 self.origin[0] + self.resolution * (tile_col + 1) * self.tile_size[0],
124 self.origin[1] - self.resolution * tile_row * self.tile_size[1],
125 )
127 def bbox_to_tiles(self, bbox: Tuple[float, float, float, float]) -> Tuple[int, int, int, int]:
128 """Get extrems tile columns and rows corresponding to provided bounding box
130 TMS spatial reference is Lat / Lon case is handled.
132 Args:
133 bbox (Tuple[float, float, float, float]): bounding box (xmin, ymin, xmax, ymax), in TMS coordinates system
135 Returns:
136 Tuple[int, int, int, int]: extrem tiles (col_min, row_min, col_max, row_max)
137 """
139 if self.__latlon:
140 return (
141 self.x_to_column(bbox[1]),
142 self.y_to_row(bbox[2]),
143 self.x_to_column(bbox[3]),
144 self.y_to_row(bbox[0]),
145 )
146 else:
147 return (
148 self.x_to_column(bbox[0]),
149 self.y_to_row(bbox[3]),
150 self.x_to_column(bbox[2]),
151 self.y_to_row(bbox[1]),
152 )
154 def point_to_indices(self, x: float, y: float) -> Tuple[int, int, int, int]:
155 """Get pyramid's tile and pixel indices from point's coordinates
157 TMS spatial reference with Lat / Lon order is handled.
159 Args:
160 x (float): point's x
161 y (float): point's y
163 Returns:
164 Tuple[int, int, int, int]: tile's column, tile's row, pixel's (in the tile) column, pixel's row
165 """
167 if self.__latlon:
168 absolute_pixel_column = int((y - self.origin[0]) / self.resolution)
169 absolute_pixel_row = int((self.origin[1] - x) / self.resolution)
170 else:
171 absolute_pixel_column = int((x - self.origin[0]) / self.resolution)
172 absolute_pixel_row = int((self.origin[1] - y) / self.resolution)
174 return (
175 absolute_pixel_column // self.tile_size[0],
176 absolute_pixel_row // self.tile_size[1],
177 absolute_pixel_column % self.tile_size[0],
178 absolute_pixel_row % self.tile_size[1],
179 )
181 @property
182 def tile_width(self) -> int:
183 return self.tile_size[0]
185 @property
186 def tile_heigth(self) -> int:
187 return self.tile_size[1]
190class TileMatrixSet:
191 """A tile matrix set is multi levels grid definition
193 Attributes:
194 name (str): TMS's name
195 path (str): TMS origin path (JSON)
196 id (str): TMS identifier
197 srs (str): TMS coordinates system
198 sr (osgeo.osr.SpatialReference): TMS OSR spatial reference
199 levels (Dict[str, TileMatrix]): TMS levels
200 """
202 def __init__(self, name: str) -> None:
203 """Constructor method
205 Args:
206 name: TMS's name
208 Raises:
209 MissingEnvironmentError: Missing object storage informations
210 Exception: No level in the TMS, CRS not recognized by OSR
211 StorageError: Storage read issue
212 FileNotFoundError: TMS file or object does not exist
213 FormatError: Provided path is not a well formed JSON
214 MissingAttributeError: Attribute is missing in the content
215 """
217 self.name = name
219 try:
220 self.path = os.path.join(os.environ["ROK4_TMS_DIRECTORY"], f"{self.name}.json")
221 except KeyError as e:
222 raise MissingEnvironmentError(e)
224 try:
225 data = json.loads(get_data_str(self.path))
227 self.id = data["id"]
228 self.srs = data["crs"]
229 self.sr = srs_to_spatialreference(self.srs)
230 self.levels = {}
231 for level in data["tileMatrices"]:
232 lev = TileMatrix(level, self)
233 self.levels[lev.id] = lev
235 if len(self.levels.keys()) == 0:
236 raise Exception(f"TMS '{self.path}' has no level")
238 if data["orderedAxes"] != ["X", "Y"] and data["orderedAxes"] != ["Lon", "Lat"]:
239 raise Exception(
240 f"TMS '{self.path}' own invalid axes order : only X/Y or Lon/Lat are handled"
241 )
243 except JSONDecodeError as e:
244 raise FormatError("JSON", self.path, e)
246 except KeyError as e:
247 raise MissingAttributeError(self.path, e)
249 except RuntimeError as e:
250 raise Exception(
251 f"Wrong attribute 'crs' ('{self.srs}') in '{self.path}', not recognize by OSR. Trace : {e}"
252 )
254 def get_level(self, level_id: str) -> "TileMatrix":
255 """Get one level according to its identifier
257 Args:
258 level_id: Level identifier
260 Returns:
261 The corresponding tile matrix, None if not present
262 """
264 return self.levels.get(level_id, None)
266 @property
267 def sorted_levels(self) -> List[TileMatrix]:
268 return sorted(self.levels.values(), key=lambda level: level.resolution)