Coverage for src/rok4/TileMatrixSet.py: 99%

71 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-01-29 10:29 +0100

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

10""" 

11 

12from rok4.Exceptions import * 

13from rok4.Storage import get_data_str 

14from rok4.Utils import * 

15 

16from typing import Dict, List, Tuple 

17from json.decoder import JSONDecodeError 

18import json 

19import os 

20 

21 

22class TileMatrix: 

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

24 

25 Attributes: 

26 id (str): TM identifiant (no underscore). 

27 tms (TileMatrixSet): TMS to whom it belong 

28 resolution (float): Ground size of a pixel, using unity of the TMS's coordinate system. 

29 origin (Tuple[float, float]): X,Y coordinates of the upper left corner for the level, the grid's origin. 

30 tile_size (Tuple[int, int]): Pixel width and height of a tile. 

31 matrix_size (Tuple[int, int]): Number of tile in the level, widthwise and heightwise. 

32 """ 

33 

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

35 """Constructor method 

36 

37 Args: 

38 level: Level attributes, according to JSON structure 

39 tms: TMS object containing the level to create 

40 

41 Raises: 

42 MissingAttributeError: Attribute is missing in the content 

43 """ 

44 

45 self.tms = tms 

46 try: 

47 self.id = level["id"] 

48 if self.id.find("_") != -1: 

49 raise Exception( 

50 f"TMS {tms.path} owns a level whom id contains an underscore ({self.id})" 

51 ) 

52 self.resolution = level["cellSize"] 

53 self.origin = ( 

54 level["pointOfOrigin"][0], 

55 level["pointOfOrigin"][1], 

56 ) 

57 self.tile_size = ( 

58 level["tileWidth"], 

59 level["tileHeight"], 

60 ) 

61 self.matrix_size = ( 

62 level["matrixWidth"], 

63 level["matrixHeight"], 

64 ) 

65 self.__latlon = ( 

66 self.tms.sr.EPSGTreatsAsLatLong() or self.tms.sr.EPSGTreatsAsNorthingEasting() 

67 ) 

68 except KeyError as e: 

69 raise MissingAttributeError(tms.path, f"tileMatrices[].{e}") 

70 

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

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

73 

74 Args: 

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

76 

77 Returns: 

78 int: tile's column 

79 """ 

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

81 

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

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

84 

85 Args: 

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

87 

88 Returns: 

89 int: tile's row 

90 """ 

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

92 

93 def tile_to_bbox(self, tile_col: int, tile_row: int) -> Tuple[float, float, float, float]: 

94 """Get tile terrain extent (xmin, ymin, xmax, ymax), in TMS coordinates system 

95 

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

97 

98 Args: 

99 tile_col (int): column indice 

100 tile_row (int): row indice 

101 

102 Returns: 

103 Tuple[float, float, float, float]: terrain extent (xmin, ymin, xmax, ymax) 

104 """ 

105 if self.__latlon: 

106 return ( 

107 self.origin[1] - self.resolution * (tile_row + 1) * self.tile_size[1], 

108 self.origin[0] + self.resolution * tile_col * self.tile_size[0], 

109 self.origin[1] - self.resolution * tile_row * self.tile_size[1], 

110 self.origin[0] + self.resolution * (tile_col + 1) * self.tile_size[0], 

111 ) 

112 else: 

113 return ( 

114 self.origin[0] + self.resolution * tile_col * self.tile_size[0], 

115 self.origin[1] - self.resolution * (tile_row + 1) * self.tile_size[1], 

116 self.origin[0] + self.resolution * (tile_col + 1) * self.tile_size[0], 

117 self.origin[1] - self.resolution * tile_row * self.tile_size[1], 

118 ) 

119 

120 def bbox_to_tiles(self, bbox: Tuple[float, float, float, float]) -> Tuple[int, int, int, int]: 

121 """Get extrems tile columns and rows corresponding to provided bounding box 

122 

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

124 

125 Args: 

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

127 

128 Returns: 

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

130 """ 

131 

132 if self.__latlon: 

133 return ( 

134 self.x_to_column(bbox[1]), 

135 self.y_to_row(bbox[2]), 

136 self.x_to_column(bbox[3]), 

137 self.y_to_row(bbox[0]), 

138 ) 

139 else: 

140 return ( 

141 self.x_to_column(bbox[0]), 

142 self.y_to_row(bbox[3]), 

143 self.x_to_column(bbox[2]), 

144 self.y_to_row(bbox[1]), 

145 ) 

146 

147 def point_to_indices(self, x: float, y: float) -> Tuple[int, int, int, int]: 

148 """Get pyramid's tile and pixel indices from point's coordinates 

149 

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

151 

152 Args: 

153 x (float): point's x 

154 y (float): point's y 

155 

156 Returns: 

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

158 """ 

159 

160 if self.__latlon: 

161 absolute_pixel_column = int((y - self.origin[0]) / self.resolution) 

162 absolute_pixel_row = int((self.origin[1] - x) / self.resolution) 

163 else: 

164 absolute_pixel_column = int((x - self.origin[0]) / self.resolution) 

165 absolute_pixel_row = int((self.origin[1] - y) / self.resolution) 

166 

167 return ( 

168 absolute_pixel_column // self.tile_size[0], 

169 absolute_pixel_row // self.tile_size[1], 

170 absolute_pixel_column % self.tile_size[0], 

171 absolute_pixel_row % self.tile_size[1], 

172 ) 

173 

174 

175class TileMatrixSet: 

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

177 

178 Attributes: 

179 name (str): TMS's name 

180 path (str): TMS origin path (JSON) 

181 id (str): TMS identifier 

182 srs (str): TMS coordinates system 

183 sr (osgeo.osr.SpatialReference): TMS OSR spatial reference 

184 levels (Dict[str, TileMatrix]): TMS levels 

185 """ 

186 

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

188 """Constructor method 

189 

190 Args: 

191 name: TMS's name 

192 

193 Raises: 

194 MissingEnvironmentError: Missing object storage informations 

195 Exception: No level in the TMS, CRS not recognized by OSR 

196 StorageError: Storage read issue 

197 FormatError: Provided path is not a well formed JSON 

198 MissingAttributeError: Attribute is missing in the content 

199 """ 

200 

201 self.name = name 

202 

203 try: 

204 self.path = os.path.join(os.environ["ROK4_TMS_DIRECTORY"], f"{self.name}.json") 

205 except KeyError as e: 

206 raise MissingEnvironmentError(e) 

207 

208 try: 

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

210 

211 self.id = data["id"] 

212 self.srs = data["crs"] 

213 self.sr = srs_to_spatialreference(self.srs) 

214 self.levels = {} 

215 for l in data["tileMatrices"]: 

216 lev = TileMatrix(l, self) 

217 self.levels[lev.id] = lev 

218 

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

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

221 

222 if data["orderedAxes"] != ["X", "Y"] and data["orderedAxes"] != ["Lon", "Lat"]: 

223 raise Exception( 

224 f"TMS '{self.path}' own invalid axes order : only X/Y or Lon/Lat are handled" 

225 ) 

226 

227 except JSONDecodeError as e: 

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

229 

230 except KeyError as e: 

231 raise MissingAttributeError(self.path, e) 

232 

233 except RuntimeError as e: 

234 raise Exception( 

235 f"Wrong attribute 'crs' ('{self.srs}') in '{self.path}', not recognize by OSR" 

236 ) 

237 

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

239 """Get one level according to its identifier 

240 

241 Args: 

242 level_id: Level identifier 

243 

244 Returns: 

245 The corresponding tile matrix, None if not present 

246 """ 

247 

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

249 

250 @property 

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

252 return sorted(self.levels.values(), key=lambda l: l.resolution)