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

1"""Provide classes to use a tile matrix set. 

2 

3The module contains the following classes: 

4 

5- `TileMatrixSet` - Multi level grid 

6- `TileMatrix` - A tile matrix set level 

7 

8Loading a tile matrix set requires environment variables : 

9 

10- ROK4_TMS_DIRECTORY 

11""" 

12 

13# -- IMPORTS -- 

14 

15# standard library 

16import json 

17import os 

18from json.decoder import JSONDecodeError 

19from typing import Dict, List, Tuple 

20 

21# package 

22from rok4.exceptions import FormatError, MissingAttributeError, MissingEnvironmentError 

23from rok4.storage import get_data_str 

24from rok4.utils import srs_to_spatialreference 

25 

26# -- GLOBALS -- 

27 

28 

29class TileMatrix: 

30 """A tile matrix is a tile matrix set's level. 

31 

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 """ 

40 

41 def __init__(self, level: Dict, tms: "TileMatrixSet") -> None: 

42 """Constructor method 

43 

44 Args: 

45 level: Level attributes, according to JSON structure 

46 tms: TMS object containing the level to create 

47 

48 Raises: 

49 MissingAttributeError: Attribute is missing in the content 

50 """ 

51 

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}") 

77 

78 def x_to_column(self, x: float) -> int: 

79 """Convert west-east coordinate to tile's column 

80 

81 Args: 

82 x (float): west-east coordinate (TMS coordinates system) 

83 

84 Returns: 

85 int: tile's column 

86 """ 

87 return int((x - self.origin[0]) / (self.resolution * self.tile_size[0])) 

88 

89 def y_to_row(self, y: float) -> int: 

90 """Convert north-south coordinate to tile's row 

91 

92 Args: 

93 y (float): north-south coordinate (TMS coordinates system) 

94 

95 Returns: 

96 int: tile's row 

97 """ 

98 return int((self.origin[1] - y) / (self.resolution * self.tile_size[1])) 

99 

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 

102 

103 TMS spatial reference is Lat / Lon case is handled. 

104 

105 Args: 

106 tile_col (int): column indice 

107 tile_row (int): row indice 

108 

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 ) 

126 

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 

129 

130 TMS spatial reference is Lat / Lon case is handled. 

131 

132 Args: 

133 bbox (Tuple[float, float, float, float]): bounding box (xmin, ymin, xmax, ymax), in TMS coordinates system 

134 

135 Returns: 

136 Tuple[int, int, int, int]: extrem tiles (col_min, row_min, col_max, row_max) 

137 """ 

138 

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 ) 

153 

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 

156 

157 TMS spatial reference with Lat / Lon order is handled. 

158 

159 Args: 

160 x (float): point's x 

161 y (float): point's y 

162 

163 Returns: 

164 Tuple[int, int, int, int]: tile's column, tile's row, pixel's (in the tile) column, pixel's row 

165 """ 

166 

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) 

173 

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 ) 

180 

181 @property 

182 def tile_width(self) -> int: 

183 return self.tile_size[0] 

184 

185 @property 

186 def tile_heigth(self) -> int: 

187 return self.tile_size[1] 

188 

189 

190class TileMatrixSet: 

191 """A tile matrix set is multi levels grid definition 

192 

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 """ 

201 

202 def __init__(self, name: str) -> None: 

203 """Constructor method 

204 

205 Args: 

206 name: TMS's name 

207 

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 """ 

216 

217 self.name = name 

218 

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) 

223 

224 try: 

225 data = json.loads(get_data_str(self.path)) 

226 

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 

234 

235 if len(self.levels.keys()) == 0: 

236 raise Exception(f"TMS '{self.path}' has no level") 

237 

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 ) 

242 

243 except JSONDecodeError as e: 

244 raise FormatError("JSON", self.path, e) 

245 

246 except KeyError as e: 

247 raise MissingAttributeError(self.path, e) 

248 

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 ) 

253 

254 def get_level(self, level_id: str) -> "TileMatrix": 

255 """Get one level according to its identifier 

256 

257 Args: 

258 level_id: Level identifier 

259 

260 Returns: 

261 The corresponding tile matrix, None if not present 

262 """ 

263 

264 return self.levels.get(level_id, None) 

265 

266 @property 

267 def sorted_levels(self) -> List[TileMatrix]: 

268 return sorted(self.levels.values(), key=lambda level: level.resolution)