Coverage for src/rok4_tools/tmsizer_utils/processors/reduce.py: 62%

86 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-11-06 17:15 +0000

1"""Provide processor combining data. Input data is transformed and accumulated and the result is yielded juste once, at the end. 

2 

3The module contains the following classes: 

4 

5- `CountProcessor` - Count the number of item read from the input processor 

6- `HeatmapProcessor` - Generate an heat map with all point coordinate read from the input processor 

7""" 

8import sys 

9import numpy as np 

10from typing import Dict, List, Tuple, Union, Iterator 

11from math import floor 

12 

13import rasterio 

14from rasterio.transform import from_origin 

15from rasterio.io import MemoryFile 

16from rasterio import logging 

17logging.getLogger().setLevel(logging.ERROR) 

18 

19from rok4_tools.tmsizer_utils.processors.processor import Processor 

20 

21class CountProcessor(Processor): 

22 """Processor counting the number of item read from the input processor 

23 

24 All input formats are allowed and output format is "COUNT" 

25 

26 Attributes: 

27 __input (Processor): Processor from which data is read 

28 """ 

29 

30 def __init__(self, input: Processor): 

31 """Constructor method 

32 

33 Args: 

34 input (Processor): Processor from which data is read 

35 """ 

36 

37 super().__init__("COUNT") 

38 

39 self.__input = input 

40 

41 def process(self) -> Iterator[int]: 

42 """Count number of input item 

43 

44 Yield count only once at the end 

45 

46 Examples: 

47 

48 Get input items count 

49 

50 from rok4_tools.tmsizer_utils.processors.reduce import CountProcessor 

51 

52 try: 

53 # Creation of Processor source_processor 

54 processor = CountProcessor(source_processor, level="15", format="GeoJSON" ) 

55 count = processor.process().__next__() 

56 print(f"{count} items in source_processor") 

57 

58 except Exception as e: 

59 print("{e}") 

60 

61 Yields: 

62 Iterator[int]: the count of input items 

63 """ 

64 

65 for item in self.__input.process(): 

66 self._processed += 1 

67 

68 yield self._processed 

69 

70 def __str__(self) -> str: 

71 return f"CountProcessor : {self._processed} {self.__input.format} items counted" 

72 

73 

74class HeatmapProcessor(Processor): 

75 """Processor counting the number of item read from the input processor 

76 

77 Accepted input format is "POINT" and output format is "FILELIKE". Output file-like object is an in-memory GeoTIFF 

78 

79 Attributes: 

80 __input (Processor): Processor from which data is read 

81 __bbox (Tuple[float, float, float, float]): Bounding box of the heat map (xmin,ymin,xmax,ymax) 

82 __dimensions (Tuple[int, int]): Pixel dimensions of the heat map (width, height) 

83 __resolutions (Tuple[float, float]): Pixel resolution (x resolution, y resolution) 

84 """ 

85 

86 input_formats_allowed = ["POINT"] 

87 

88 areas = { 

89 "EPSG:3857": { 

90 "FXX": [-649498, 5048729, 1173394, 6661417] 

91 } 

92 } 

93 

94 def __init__(self, input: Processor, **options): 

95 """Constructor method 

96 

97 Args: 

98 input (Processor): Processor from which data is read 

99 **bbox (str): Bounding box of the heat map. Format "<xmin>,<ymin>,<xmax>,<ymax>". Coordinates system have to be the pivot TMS' one 

100 **dimensions (str): Pixel dimensions of the heat map.Format "<width>x<height>" 

101 

102 Raises: 

103 ValueError: Input format is not allowed 

104 KeyError: A mandatory option is missing 

105 ValueError: A mandatory option is not valid 

106 ValueError: Provided level is not in the pivot TMS 

107 """ 

108 

109 if input.format not in self.input_formats_allowed: 

110 raise Exception(f"Input format {input.format} is not handled for HeatmapProcessor : allowed formats are {self.input_formats_allowed}") 

111 

112 super().__init__("FILELIKE") 

113 

114 self.__input = input 

115 

116 if "bbox" in options: 

117 try: 

118 self.__bbox = [float(c) for c in options["bbox"].split(",")] 

119 self.__bbox = tuple(self.__bbox) 

120 except ValueError as e: 

121 raise ValueError(f"Option 'bbox' contains non float values : {e}") 

122 

123 if len(self.__bbox) != 4 or self.__bbox[0] >= self.__bbox[2] or self.__bbox[1] >= self.__bbox[3]: 

124 raise ValueError(f"Option 'bbox' have to be provided with format <xmin>,<ymin>,<xmax>,<ymax> (floats, min < max)") 

125 

126 elif "area" in options: 

127 try: 

128 self.__bbox = self.areas[self.tms.srs][options["area"]] 

129 except KeyError as e: 

130 if self.tms.srs in self.areas: 

131 raise ValueError(f"Area '{options['area']}' is not available for the TMS coordinates system ({self.tms.srs}): available areas are {', '.join(self.areas[self.tms.srs].keys())}") 

132 else : 

133 raise ValueError(f"No defined areas for the TMS coordinates system ({self.tms.srs})") 

134 else: 

135 raise KeyError(f"Option 'bbox' or 'area' is required for a heatmap processing") 

136 

137 if "dimensions" in options: 

138 try: 

139 self.__dimensions = [int(d) for d in options["dimensions"].split("x")] 

140 self.__dimensions = tuple(self.__dimensions) 

141 except ValueError as e: 

142 raise ValueError(f"Option 'dimensions' contains non integer values : {e}") 

143 

144 if len(self.__dimensions) != 2 or self.__dimensions[0] <= 0 or self.__dimensions[1] <= 0: 

145 raise ValueError(f"Option 'dimensions' have to be provided with format <width>x<height> (positive integers)") 

146 

147 self.__resolutions = ( 

148 (self.__bbox[2] - self.__bbox[0]) / self.__dimensions[0], 

149 (self.__bbox[3] - self.__bbox[1]) / self.__dimensions[1] 

150 ) 

151 elif "level" in options: 

152 level = self.tms.get_level(options["level"]) 

153 if level is None: 

154 raise ValueError(f"The provided level '{options['dimensions']}' (to have one pixel per tile) is not in the TMS") 

155 

156 # On va caler la bbox pour qu'elle coïncide avec les limites de tuiles du niveau demandé 

157 (col_min, row_min, col_max, row_max) = level.bbox_to_tiles(self.__bbox) 

158 

159 # Calage du coin en bas à gauche 

160 (xmin, ymin, xmax, ymax) = level.tile_to_bbox(col_min, row_max) 

161 self.__bbox[0] = xmin 

162 self.__bbox[1] = ymin 

163 

164 # Calage du coin en haut à droite 

165 (xmin, ymin, xmax, ymax) = level.tile_to_bbox(col_max, row_min) 

166 self.__bbox[2] = xmax 

167 self.__bbox[3] = ymax 

168 

169 self.__resolutions = ( 

170 xmax - xmin, 

171 ymax - ymin 

172 ) 

173 

174 self.__dimensions = ( 

175 int((self.__bbox[2] - self.__bbox[0]) / self.__resolutions[0]), 

176 int((self.__bbox[3] - self.__bbox[1]) / self.__resolutions[1]) 

177 ) 

178 

179 else: 

180 raise KeyError(f"Option 'dimensions' or 'level' is required for a heatmap processing") 

181 

182 if self.__dimensions[0] > 10000 or self.__dimensions[1] > 10000: 

183 raise ValueError(f"Heatmap dimensions have to be less than 10 000 x 10 000: here it's {self.__dimensions}") 

184 

185 def process(self) -> Iterator[MemoryFile]: 

186 """Read point coordinates from the input processor and accumule them as a heat map 

187 

188 Points outsides the provided bounding box are ignored. 

189 

190 Examples: 

191 

192 Get intersecting tiles' indices 

193 

194 from rok4_tools.tmsizer_utils.processors.reduce import HeatmapProcessor 

195 

196 try: 

197 # Creation of Processor source_processor with format POINT 

198  

199 processor = HeatmapProcessor(source_processor, bbox="65000,6100000,665000,6500000", dimensions="600x400" ) 

200 f = processor.process().__next__() 

201 

202 with open("hello.txt", "w") as my_file: 

203 my_file.write(f.read()) 

204  

205 except Exception as e: 

206 print("{e}") 

207 

208 Yields: 

209 Iterator[rasterio.io.MemoryFile]: In-memory GeoTIFF 

210 """ 

211 

212 data = np.zeros((self.__dimensions[1], self.__dimensions[0]), dtype=np.uint32) 

213 

214 if self.__input.format == "POINT": 

215 

216 for item in self.__input.process(): 

217 self._processed += 1 

218 

219 (x_center, y_center) = item 

220 

221 if x_center > self.__bbox[2] or y_center > self.__bbox[3] or x_center < self.__bbox[0] or y_center < self.__bbox[1]: 

222 continue 

223 

224 pcol = floor((x_center - self.__bbox[0]) / self.__resolutions[0]) 

225 prow = floor((self.__bbox[3] - y_center) / self.__resolutions[1]) 

226 

227 data[prow][pcol] += 1 

228 

229 memfile = MemoryFile() 

230 with memfile.open( 

231 driver='GTiff', 

232 height=data.shape[0], 

233 width=data.shape[1], 

234 count=1, 

235 dtype=data.dtype, 

236 crs=rasterio.CRS.from_string(self.tms.srs), 

237 nodata=0, 

238 transform=from_origin(self.__bbox[0], self.__bbox[3], self.__resolutions[0], self.__resolutions[1]), 

239 ) as dataset: 

240 dataset.write(data, indexes=1) 

241 

242 yield memfile 

243 

244 def __str__(self) -> str: 

245 return f"HeatmapProcessor : {self._processed} hits on image with dimensions {self.__dimensions} and bbox {self.__bbox} (resolutions {self.__resolutions})"