Coverage for gws-app/gws/base/ows/client/parseutil.py: 0%
134 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
1"""Parse utilities for OWS XML files."""
3from typing import Optional
5import re
7import gws
8import gws.gis.crs
9import gws.gis.extent
10import gws.lib.net
13def service_operations(caps_el: gws.XmlElement) -> list[gws.OwsOperation]:
14 # <ows:OperationsMetadata>
15 # <ows:Operation name="GetCapabilities">...
17 els = caps_el.findall('OperationsMetadata/Operation')
18 if els:
19 return [_parse_operation(e) for e in els]
21 # <Capability>
22 # <Request>
23 # <GetCapabilities>...
25 el = caps_el.find('Capability/Request')
26 if el:
27 return [_parse_operation(e) for e in el]
29 return []
32def _parse_operation(el: gws.XmlElement) -> gws.OwsOperation:
33 op = gws.OwsOperation(verb=el.get('name') or el.tag)
35 # @TODO Range
36 # @TODO Constraint
38 # <Parameter name="Format">
39 # <AllowedValues>
40 # <Value>image/gif</Value>
41 # ...
43 # <Parameter name="AcceptVersions">
44 # <Value>1.0.0</Value>
46 op.allowedParameters = {}
47 for param_el in el.findall('Parameter'):
48 values = param_el.textlist('Value') + param_el.textlist('AllowedValues/Value')
49 if values:
50 op.allowedParameters[param_el.get('name').upper()] = values
52 # <Operation name="GetMap">
53 # <DCP> <HTTP>
54 # <Get xlink:href="...."/>
55 # <Post xlink:href="..."/>
56 # </HTTP> </DCP>
57 #
58 #
59 # <GetMap>
60 # <Format>image/png</Format>
61 # <DCPType> <HTTP> <Get>
62 # <OnlineResource xlink:type="simple" xlink:href="..."/>
63 # </Get> </HTTP> </DCPType>
64 # </GetMap>
66 op.postUrl = _parse_url(el.findfirst('DCP/HTTP/Post', 'DCPType/HTTP/Post'))
68 u = _parse_url(el.findfirst('DCP/HTTP/Get', 'DCPType/HTTP/Get'))
69 op.url, op.params = gws.lib.net.extract_params(u)
71 op.formats = el.textlist('Format')
72 if 'OUTPUTFORMAT' in op.allowedParameters:
73 op.formats.extend(op.allowedParameters['OUTPUTFORMAT'])
75 return op
78##
81def service_metadata(caps_el: gws.XmlElement) -> gws.Metadata:
82 # wms
83 #
84 # <Capabilities
85 # <Service...
86 # <Name>...
87 # <Title>...
88 # <ContactInformation>...
89 #
90 # ows
91 #
92 # <Capabilities
93 # <ows:ServiceIdentification>
94 # <ows:Title>....
95 # <ows:ServiceProvider>
96 # <ows:ProviderName>...
97 # <ows:ServiceContact>...
99 md = gws.Metadata()
101 _element_metadata(caps_el.findfirst('Service', 'ServiceIdentification'), md)
102 _contact_metadata(caps_el.findfirst('Service/ContactInformation', 'ServiceProvider/ServiceContact'), md)
104 md.contactProviderName = caps_el.textof('ServiceProvider/ProviderName')
105 md.contactProviderSite = caps_el.textof('ServiceProvider/ProviderSite')
107 # <Capabilities
108 # <ServiceMetadataURL
110 link = _parse_link(caps_el.find('ServiceMetadataURL'))
111 if link:
112 md.serviceMetaLink = link
114 return gws.u.strip(md)
117def element_metadata(el: gws.XmlElement) -> gws.Metadata:
118 # <whatever, e.g. Layer or FeatureType
119 # <Name...
120 # <Title...
122 md = gws.Metadata()
123 _element_metadata(el, md)
124 return gws.u.strip(md)
127def _element_metadata(el: gws.XmlElement, md: gws.Metadata):
128 if not el:
129 return
131 md.abstract = el.textof('Abstract')
132 md.accessConstraints = el.textof('AccessConstraints')
133 md.attribution = gws.MetadataAttribution(title=el.textof('Attribution/Title'))
134 md.fees = el.textof('Fees')
135 md.keywords = el.textlist('Keywords', 'KeywordList', deep=True)
136 md.name = el.textof('Name', 'Identifier')
137 md.title = el.textof('Title')
138 md.metaLinks = gws.u.compact(_parse_link(e) for e in el.findall('MetadataURL'))
140 e = el.find('AuthorityURL')
141 if e:
142 md.authorityUrl = _parse_url(e)
143 md.authorityName = e.get('name')
145 e = el.find('Identifier')
146 if e:
147 md.authorityIdentifier = e.text
150_contact_mapping = [
151 # wms
153 ('contactArea', 'StateOrProvince'),
154 ('contactCity', 'City'),
155 ('contactCountry', 'Country'),
156 ('contactEmail', 'ContactElectronicMailAddress'),
157 ('contactFax', 'ContactFacsimileTelephone'),
158 ('contactOrganization', 'ContactOrganization'),
159 ('contactPerson', 'ContactPerson'),
160 ('contactPhone', 'ContactVoiceTelephone'),
161 ('contactPosition', 'ContactPosition'),
162 ('contactZip', 'PostCode'),
164 # ows
166 ('contactArea', 'AdministrativeArea'),
167 ('contactCity', 'City'),
168 ('contactCountry', 'Country'),
169 ('contactEmail', 'ElectronicMailAddress'),
170 ('contactFax', 'Facsimile'),
171 ('contactOrganization', 'ProviderName'),
172 ('contactPerson', 'IndividualName'),
173 ('contactPhone', 'Voice'),
174 ('contactPosition', 'PositionName'),
175 ('contactZip', 'PostalCode'),
176]
179def _contact_metadata(el: gws.XmlElement, md: gws.Metadata):
180 if not el:
181 return
183 src = el.textdict(deep=True)
185 for dkey, skey in _contact_mapping:
186 if skey in src:
187 setattr(md, dkey, src[skey])
190##
192def wgs_extent(layer_el: gws.XmlElement) -> Optional[gws.Extent]:
193 """Read WGS bounding box from a Layer/FeatureType element.
195 Extracts coordinates from ``EX_GeographicBoundingBox`` (WMS), ``WGS84BoundingBox`` (OWS)
196 or ``LatLonBoundingBox``. For the latter, assume x=longitude, y=latitude,
197 as per OGC 01-068r3, 6.5.6.
199 Args:
200 layer_el: 'Layer' or 'FeatureType' element.
201 """
203 el = layer_el.findfirst('EX_GeographicBoundingBox', 'WGS84BoundingBox', 'LatLonBoundingBox')
204 if el:
205 return gws.gis.extent.from_list(_parse_bbox(el))
208def supported_crs(layer_el: gws.XmlElement, extra_crs_ids: list[str] = None) -> list[gws.Crs]:
209 """Enumerate supported CRS for a Layer/FeatureType element.
211 For WMS, enumerates CRS/SRS and BoundingBox tags,
212 for OWS, DefaultCRS and OtherCRS.
214 Args:
215 layer_el: 'Layer' or 'FeatureType' element.
216 extra_crs_ids: additional CRS ids.
218 Returns:
219 A list of ``Crs`` objects.
220 """
222 # <Layer...
223 # <CRS>EPSG....
224 # <BoundingBox CRS="EPSG:" minx=....
225 #
226 # <FeatureType...
227 # <DefaultCRS>urn:ogc:def:crs:EPSG...
228 # <OtherCRS>urn:ogc:def:crs:EPSG...
230 crsids = set()
232 for el in layer_el.findall('BoundingBox'):
233 crsids.add(el.get('SRS') or el.get('CRS'))
235 for tag in 'DefaultSRS', 'DefaultCRS', 'OtherSRS', 'OtherCRS', 'SRS', 'CRS':
236 for el in layer_el.findall(tag):
237 if el.text:
238 crsids.add(el.text)
240 crsids.update(extra_crs_ids or [])
242 return gws.u.compact(gws.gis.crs.get(s) for s in crsids)
245##
248def parse_style(el: gws.XmlElement) -> gws.SourceStyle:
249 # <Style>
250 # <Name>default...
251 # <Title>...
252 # <LegendURL
253 # <Format>...
254 # <OnlineResource...
256 st = gws.SourceStyle()
258 st.metadata = element_metadata(el)
259 st.name = st.metadata.get('name', '').lower()
260 st.legendUrl = _parse_url(el.findfirst('LegendURL'))
261 st.isDefault = (
262 el.get('IsDefault') == 'true'
263 or st.name == 'default'
264 or st.name.endswith(':default'))
265 return st
268def default_style(styles: list[gws.SourceStyle]) -> Optional[gws.SourceStyle]:
269 for s in styles:
270 if s.isDefault:
271 return s
272 return styles[0] if styles else None
275##
278def to_float(s, default=0.0):
279 return float(s or default)
282def to_int(s, default=0):
283 # accept floats as well, but convert to int
284 return int(float(s or default))
287def to_float_pair(s):
288 s = s.split()
289 return float(s[0]), float(s[1])
292##
295def _parse_bbox(el: gws.XmlElement):
296 # note: bboxes are always converted to (x1, y1, x2, y2) with x1 < x2, y1 < y2
298 # <BoundingBox/LatLonBoundingBox CRS="..." minx="0" miny="1" maxx="2" maxy="3"/>
300 if el.get('minx'):
301 return [
302 to_float(el.get('minx')),
303 to_float(el.get('miny')),
304 to_float(el.get('maxx')),
305 to_float(el.get('maxy')),
306 ]
308 # <ows:BoundingBox/WGS84BoundingBox
309 # <ows:LowerCorner> 0 1
310 # <ows:UpperCorner> 2 3
312 if el.findfirst('LowerCorner'):
313 x1, y1 = to_float_pair(el.textof('LowerCorner'))
314 x2, y2 = to_float_pair(el.textof('UpperCorner'))
315 return [
316 min(x1, x2),
317 min(y1, y2),
318 max(x1, x2),
319 max(y1, y2),
320 ]
322 # <EX_GeographicBoundingBox>
323 # <westBoundLongitude> 0
324 # <eastBoundLongitude> 2
325 # <southBoundLatitude> 1
326 # <northBoundLatitude> 3
328 if el.findfirst('westBoundLongitude'):
329 x1 = to_float(el.textof('eastBoundLongitude'))
330 y1 = to_float(el.textof('southBoundLatitude'))
331 x2 = to_float(el.textof('westBoundLongitude'))
332 y2 = to_float(el.textof('northBoundLatitude'))
333 return [
334 min(x1, x2),
335 min(y1, y2),
336 max(x1, x2),
337 max(y1, y2),
338 ]
341def _parse_url(el: gws.XmlElement) -> str:
342 def cleanup(s):
343 return (s or '').strip(' ?&')
345 if not el:
346 return ''
348 # <ows:DCP>
349 # <ows:HTTP>
350 # <ows:Get xlink:href=... <-- we are here
352 s = el.get('href') or el.get('onlineResource')
353 if s:
354 return cleanup(s)
356 # <whatever <--
357 # <OnlineResource xlink:href=...
359 e = el.findfirst('OnlineResource')
360 if e:
361 return cleanup(e.get('href', default=e.text))
363 return ''
366def _parse_link(el: gws.XmlElement) -> Optional[gws.MetadataLink]:
367 if not el:
368 return None
370 # see base/ows/server/templatelib.py
371 # regarding different MetadataURL formats
373 # simple
374 if el.get('href'):
375 return gws.MetadataLink(url=el.get('href'))
377 # nested
378 d = gws.u.strip({
379 'url': _parse_url(el),
380 'type': el.get('type'),
381 'format': el.textof('Format'),
382 })
384 if d:
385 return gws.MetadataLink(d)