Coverage for src/rok4/Layer.py: 85%
132 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-01-29 10:29 +0100
« prev ^ index » next coverage.py v7.4.1, created at 2024-01-29 10:29 +0100
1"""Provide classes to use a layer.
3The module contains the following classe:
5- `Layer` - Descriptor to broadcast pyramids' data
6"""
8from typing import Dict, List, Tuple, Union
9import json
10from json.decoder import JSONDecodeError
11import os
12import re
14from rok4.Exceptions import *
15from rok4.Pyramid import Pyramid, PyramidType
16from rok4.TileMatrixSet import TileMatrixSet
17from rok4.Storage import *
18from rok4.Utils import *
21class Layer:
22 """A data layer, raster or vector
24 Attributes:
25 __name (str): layer's technical name
26 __pyramids (Dict[str, Union[rok4.Pyramid.Pyramid,str,str]]): used pyramids
27 __format (str): pyramid's list path
28 __tms (rok4.TileMatrixSet.TileMatrixSet): Used grid
29 __keywords (List[str]): Keywords
30 __levels (Dict[str, rok4.Pyramid.Level]): Used pyramids' levels
31 __best_level (rok4.Pyramid.Level): Used pyramids best level
32 __resampling (str): Interpolation to use fot resampling
33 __bbox (Tuple[float, float, float, float]): data bounding box, TMS coordinates system
34 __geobbox (Tuple[float, float, float, float]): data bounding box, EPSG:4326
35 """
37 @classmethod
38 def from_descriptor(cls, descriptor: str) -> "Layer":
39 """Create a layer from its descriptor
41 Args:
42 descriptor (str): layer's descriptor path
44 Raises:
45 FormatError: Provided path is not a well formed JSON
46 MissingAttributeError: Attribute is missing in the content
47 StorageError: Storage read issue (layer descriptor)
48 MissingEnvironmentError: Missing object storage informations
50 Returns:
51 Layer: a Layer instance
52 """
53 try:
54 data = json.loads(get_data_str(descriptor))
56 except JSONDecodeError as e:
57 raise FormatError("JSON", descriptor, e)
59 layer = cls()
61 storage_type, path, root, base_name = get_infos_from_path(descriptor)
62 layer.__name = base_name[:-5] # on supprime l'extension.json
64 try:
65 # Attributs communs
66 layer.__title = data["title"]
67 layer.__abstract = data["abstract"]
68 layer.__load_pyramids(data["pyramids"])
70 # Paramètres optionnels
71 if "keywords" in data:
72 for k in data["keywords"]:
73 layer.__keywords.append(k)
75 if layer.type == PyramidType.RASTER:
76 if "resampling" in data:
77 layer.__resampling = data["resampling"]
79 if "styles" in data:
80 layer.__styles = data["styles"]
81 else:
82 layer.__styles = ["normal"]
84 # Les bbox, native et géographique
85 if "bbox" in data:
86 layer.__geobbox = (
87 data["bbox"]["south"],
88 data["bbox"]["west"],
89 data["bbox"]["north"],
90 data["bbox"]["east"],
91 )
92 layer.__bbox = reproject_bbox(layer.__geobbox, "EPSG:4326", layer.__tms.srs, 5)
93 # On force l'emprise de la couche, on recalcule donc les tuiles limites correspondantes pour chaque niveau
94 for l in layer.__levels.values():
95 l.set_limits_from_bbox(layer.__bbox)
96 else:
97 layer.__bbox = layer.__best_level.bbox
98 layer.__geobbox = reproject_bbox(layer.__bbox, layer.__tms.srs, "EPSG:4326", 5)
100 except KeyError as e:
101 raise MissingAttributeError(descriptor, e)
103 return layer
105 @classmethod
106 def from_parameters(cls, pyramids: List[Dict[str, str]], name: str, **kwargs) -> "Layer":
107 """Create a default layer from parameters
109 Args:
110 pyramids (List[Dict[str, str]]): pyramids to use and extrem levels, bottom and top
111 name (str): layer's technical name
112 **title (str): Layer's title (will be equal to name if not provided)
113 **abstract (str): Layer's abstract (will be equal to name if not provided)
114 **styles (List[str]): Styles identifier to authorized for the layer
115 **resampling (str): Interpolation to use for resampling
117 Raises:
118 Exception: name contains forbidden characters or used pyramids do not shared same parameters (format, tms...)
120 Returns:
121 Layer: a Layer instance
122 """
124 layer = cls()
126 # Informations obligatoires
127 if not re.match("^[A-Za-z0-9_-]*$", name):
128 raise Exception(
129 f"Layer's name have to contain only letters, number, hyphen and underscore, to be URL and storage compliant ({name})"
130 )
132 layer.__name = name
133 layer.__load_pyramids(pyramids)
135 # Les bbox, native et géographique
136 layer.__bbox = layer.__best_level.bbox
137 layer.__geobbox = reproject_bbox(layer.__bbox, layer.__tms.srs, "EPSG:4326", 5)
139 # Informations calculées
140 layer.__keywords.append(layer.type.name)
141 layer.__keywords.append(layer.__name)
143 # Informations optionnelles
144 if "title" in kwargs and kwargs["title"] is not None:
145 layer.__title = kwargs["title"]
146 else:
147 layer.__title = name
149 if "abstract" in kwargs and kwargs["abstract"] is not None:
150 layer.__abstract = kwargs["abstract"]
151 else:
152 layer.__abstract = name
154 if layer.type == PyramidType.RASTER:
155 if "styles" in kwargs and kwargs["styles"] is not None and len(kwargs["styles"]) > 0:
156 layer.__styles = kwargs["styles"]
157 else:
158 layer.__styles = ["normal"]
160 if "resampling" in kwargs and kwargs["resampling"] is not None:
161 layer.__resampling = kwargs["resampling"]
163 return layer
165 def __init__(self) -> None:
166 self.__format = None
167 self.__tms = None
168 self.__best_level = None
169 self.__levels = {}
170 self.__keywords = []
171 self.__pyramids = []
173 def __load_pyramids(self, pyramids: List[Dict[str, str]]) -> None:
174 """Load and check pyramids
176 Args:
177 pyramids (List[Dict[str, str]]): List of descriptors' paths and optionnaly top and bottom levels
179 Raises:
180 Exception: Pyramids' do not all own the same format
181 Exception: Pyramids' do not all own the same TMS
182 Exception: Pyramids' do not all own the same channels number
183 Exception: Overlapping in usage pyramids' levels
184 """
186 ## Toutes les pyramides doivent avoir les même caractéristiques
187 channels = None
188 for p in pyramids:
189 pyramid = Pyramid.from_descriptor(p["path"])
190 bottom_level = p.get("bottom_level", None)
191 top_level = p.get("top_level", None)
193 if bottom_level is None:
194 bottom_level = pyramid.bottom_level.id
196 if top_level is None:
197 top_level = pyramid.top_level.id
199 if self.__format is not None and self.__format != pyramid.format:
200 raise Exception(
201 f"Used pyramids have to own the same format : {self.__format} != {pyramid.format}"
202 )
203 else:
204 self.__format = pyramid.format
206 if self.__tms is not None and self.__tms.id != pyramid.tms.id:
207 raise Exception(
208 f"Used pyramids have to use the same TMS : {self.__tms.id} != {pyramid.tms.id}"
209 )
210 else:
211 self.__tms = pyramid.tms
213 if self.type == PyramidType.RASTER:
214 if channels is not None and channels != pyramid.raster_specifications["channels"]:
215 raise Exception(
216 f"Used RASTER pyramids have to own the same number of channels : {channels} != {pyramid.raster_specifications['channels']}"
217 )
218 else:
219 channels = pyramid.raster_specifications["channels"]
220 self.__resampling = pyramid.raster_specifications["interpolation"]
222 levels = pyramid.get_levels(bottom_level, top_level)
223 for l in levels:
224 if l.id in self.__levels:
225 raise Exception(f"Level {l.id} is present in two used pyramids")
226 self.__levels[l.id] = l
228 self.__pyramids.append(
229 {"pyramid": pyramid, "bottom_level": bottom_level, "top_level": top_level}
230 )
232 self.__best_level = sorted(self.__levels.values(), key=lambda l: l.resolution)[0]
234 def __str__(self) -> str:
235 return f"{self.type.name} layer '{self.__name}'"
237 @property
238 def serializable(self) -> Dict:
239 """Get the dict version of the layer object, descriptor compliant
241 Returns:
242 Dict: descriptor structured object description
243 """
244 serialization = {
245 "title": self.__title,
246 "abstract": self.__abstract,
247 "keywords": self.__keywords,
248 "wmts": {"authorized": True},
249 "tms": {"authorized": True},
250 "bbox": {
251 "south": self.__geobbox[0],
252 "west": self.__geobbox[1],
253 "north": self.__geobbox[2],
254 "east": self.__geobbox[3],
255 },
256 "pyramids": [],
257 }
259 for p in self.__pyramids:
260 serialization["pyramids"].append(
261 {
262 "bottom_level": p["bottom_level"],
263 "top_level": p["top_level"],
264 "path": p["pyramid"].descriptor,
265 }
266 )
268 if self.type == PyramidType.RASTER:
269 serialization["wms"] = {
270 "authorized": True,
271 "crs": ["CRS:84", "IGNF:WGS84G", "EPSG:3857", "EPSG:4258", "EPSG:4326"],
272 }
274 if self.__tms.srs.upper() not in serialization["wms"]["crs"]:
275 serialization["wms"]["crs"].append(self.__tms.srs.upper())
277 serialization["styles"] = self.__styles
278 serialization["resampling"] = self.__resampling
280 return serialization
282 def write_descriptor(self, directory: str = None) -> None:
283 """Print layer's descriptor as JSON
285 Args:
286 directory (str, optional): Directory (file or object) where to print the layer's descriptor, called <layer's name>.json. Defaults to None, JSON is printed to standard output.
287 """
288 content = json.dumps(self.serializable)
290 if directory is None:
291 print(content)
292 else:
293 put_data_str(content, os.path.join(directory, f"{self.__name}.json"))
295 @property
296 def type(self) -> PyramidType:
297 if self.__format == "TIFF_PBF_MVT":
298 return PyramidType.VECTOR
299 else:
300 return PyramidType.RASTER
302 @property
303 def bbox(self) -> Tuple[float, float, float, float]:
304 return self.__bbox
306 @property
307 def geobbox(self) -> Tuple[float, float, float, float]:
308 return self.__geobbox