Coverage for gws-app/gws/base/ows/client/parseutil.py: 18%
135 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 23:09 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 23:09 +0200
1"""Parse utilities for OWS XML files."""
3from typing import Optional
5import re
7import gws
8import gws.base.metadata
9import gws.lib.crs
10import gws.lib.extent
11import gws.lib.net
14def service_operations(caps_el: gws.XmlElement) -> list[gws.OwsOperation]:
15 # <ows:OperationsMetadata>
16 # <ows:Operation name="GetCapabilities">...
18 els = caps_el.findall('OperationsMetadata/Operation')
19 if els:
20 return [_parse_operation(e) for e in els]
22 # <Capability>
23 # <Request>
24 # <GetCapabilities>...
26 el = caps_el.find('Capability/Request')
27 if el:
28 return [_parse_operation(e) for e in el]
30 return []
33def _parse_operation(el: gws.XmlElement) -> gws.OwsOperation:
34 op = gws.OwsOperation(verb=el.get('name') or el.tag)
36 # @TODO Range
37 # @TODO Constraint
39 # <Parameter name="Format">
40 # <AllowedValues>
41 # <Value>image/gif</Value>
42 # ...
44 # <Parameter name="AcceptVersions">
45 # <Value>1.0.0</Value>
47 op.allowedParameters = {}
48 for param_el in el.findall('Parameter'):
49 values = param_el.textlist('Value') + param_el.textlist('AllowedValues/Value')
50 if values:
51 op.allowedParameters[param_el.get('name').upper()] = values
53 # <Operation name="GetMap">
54 # <DCP> <HTTP>
55 # <Get xlink:href="...."/>
56 # <Post xlink:href="..."/>
57 # </HTTP> </DCP>
58 #
59 #
60 # <GetMap>
61 # <Format>image/png</Format>
62 # <DCPType> <HTTP> <Get>
63 # <OnlineResource xlink:type="simple" xlink:href="..."/>
64 # </Get> </HTTP> </DCPType>
65 # </GetMap>
67 op.postUrl = _parse_url(el.findfirst('DCP/HTTP/Post', 'DCPType/HTTP/Post'))
69 u = _parse_url(el.findfirst('DCP/HTTP/Get', 'DCPType/HTTP/Get'))
70 op.url, op.params = gws.lib.net.extract_params(u)
72 op.formats = el.textlist('Format')
73 if 'OUTPUTFORMAT' in op.allowedParameters:
74 op.formats.extend(op.allowedParameters['OUTPUTFORMAT'])
76 return op
79##
82def service_metadata(caps_el: gws.XmlElement) -> gws.Metadata:
83 # wms
84 #
85 # <Capabilities
86 # <Service...
87 # <Name>...
88 # <Title>...
89 # <ContactInformation>...
90 #
91 # ows
92 #
93 # <Capabilities
94 # <ows:ServiceIdentification>
95 # <ows:Title>....
96 # <ows:ServiceProvider>
97 # <ows:ProviderName>...
98 # <ows:ServiceContact>...
100 md = gws.base.metadata.new()
102 _element_metadata(caps_el.findfirst('Service', 'ServiceIdentification'), md)
103 _contact_metadata(caps_el.findfirst('Service/ContactInformation', 'ServiceProvider/ServiceContact'), md)
105 md.contactProviderName = caps_el.textof('ServiceProvider/ProviderName')
106 md.contactProviderSite = caps_el.textof('ServiceProvider/ProviderSite')
108 # <Capabilities
109 # <ServiceMetadataURL
111 service_link = _parse_link(caps_el.find('ServiceMetadataURL'))
112 if service_link:
113 md.serviceMetadataURL = service_link.url
115 return gws.base.metadata.normalize(md)
118def element_metadata(el: gws.XmlElement) -> gws.Metadata:
119 # <whatever, e.g. Layer or FeatureType
120 # <Name...
121 # <Title...
123 md = gws.base.metadata.new()
124 _element_metadata(el, md)
125 return gws.base.metadata.normalize(md)
128def _element_metadata(el: Optional[gws.XmlElement], md: gws.Metadata):
129 if not el:
130 return
132 md.abstract = el.textof('Abstract')
133 md.accessConstraints = el.textof('AccessConstraints')
134 md.attribution = el.textof('Attribution/Title')
135 md.fees = el.textof('Fees')
136 md.keywords = el.textlist('Keywords', 'KeywordList', deep=True)
137 md.name = el.textof('Name', 'Identifier')
138 md.title = el.textof('Title')
139 md.metaLinks = gws.u.compact(_parse_link(e) for e in el.findall('MetadataURL'))
141 e = el.find('AuthorityURL')
142 if e:
143 md.authorityUrl = _parse_url(e)
144 md.authorityName = e.get('name')
146 e = el.find('Identifier')
147 if e:
148 md.authorityIdentifier = e.text
151_contact_mapping = [
152 # wms
154 ('contactArea', 'StateOrProvince'),
155 ('contactCity', 'City'),
156 ('contactCountry', 'Country'),
157 ('contactEmail', 'ContactElectronicMailAddress'),
158 ('contactFax', 'ContactFacsimileTelephone'),
159 ('contactOrganization', 'ContactOrganization'),
160 ('contactPerson', 'ContactPerson'),
161 ('contactPhone', 'ContactVoiceTelephone'),
162 ('contactPosition', 'ContactPosition'),
163 ('contactZip', 'PostCode'),
165 # ows
167 ('contactArea', 'AdministrativeArea'),
168 ('contactCity', 'City'),
169 ('contactCountry', 'Country'),
170 ('contactEmail', 'ElectronicMailAddress'),
171 ('contactFax', 'Facsimile'),
172 ('contactOrganization', 'ProviderName'),
173 ('contactPerson', 'IndividualName'),
174 ('contactPhone', 'Voice'),
175 ('contactPosition', 'PositionName'),
176 ('contactZip', 'PostalCode'),
177]
180def _contact_metadata(el: Optional[gws.XmlElement], md: gws.Metadata):
181 if not el:
182 return
184 src = el.textdict(deep=True)
186 for dkey, skey in _contact_mapping:
187 if skey in src:
188 setattr(md, dkey, src[skey])
191##
193def wgs_extent(layer_el: gws.XmlElement) -> Optional[gws.Extent]:
194 """Read WGS bounding box from a Layer/FeatureType element.
196 Extracts coordinates from ``EX_GeographicBoundingBox`` (WMS), ``WGS84BoundingBox`` (OWS)
197 or ``LatLonBoundingBox``. For the latter, assume x=longitude, y=latitude,
198 as per OGC 01-068r3, 6.5.6.
200 Args:
201 layer_el: 'Layer' or 'FeatureType' element.
202 """
204 el = layer_el.findfirst('EX_GeographicBoundingBox', 'WGS84BoundingBox', 'LatLonBoundingBox')
205 if el:
206 return gws.lib.extent.from_list(_parse_bbox(el))
209def supported_crs(layer_el: gws.XmlElement, extra_crs_ids: list[str] = None) -> list[gws.Crs]:
210 """Enumerate supported CRS for a Layer/FeatureType element.
212 For WMS, enumerates CRS/SRS and BoundingBox tags,
213 for OWS, DefaultCRS and OtherCRS.
215 Args:
216 layer_el: 'Layer' or 'FeatureType' element.
217 extra_crs_ids: additional CRS ids.
219 Returns:
220 A list of ``Crs`` objects.
221 """
223 # <Layer...
224 # <CRS>EPSG....
225 # <BoundingBox CRS="EPSG:" minx=....
226 #
227 # <FeatureType...
228 # <DefaultCRS>urn:ogc:def:crs:EPSG...
229 # <OtherCRS>urn:ogc:def:crs:EPSG...
231 crsids = set()
233 for el in layer_el.findall('BoundingBox'):
234 crsids.add(el.get('SRS') or el.get('CRS'))
236 for tag in 'DefaultSRS', 'DefaultCRS', 'OtherSRS', 'OtherCRS', 'SRS', 'CRS':
237 for el in layer_el.findall(tag):
238 if el.text:
239 crsids.add(el.text)
241 crsids.update(extra_crs_ids or [])
243 return gws.u.compact(gws.lib.crs.get(s) for s in crsids)
246##
249def parse_style(el: gws.XmlElement) -> gws.SourceStyle:
250 # <Style>
251 # <Name>default...
252 # <Title>...
253 # <LegendURL
254 # <Format>...
255 # <OnlineResource...
257 st = gws.SourceStyle()
259 st.metadata = element_metadata(el)
260 st.name = st.metadata.get('name', '').lower()
261 st.legendUrl = _parse_url(el.findfirst('LegendURL'))
262 st.isDefault = (
263 el.get('IsDefault') == 'true'
264 or st.name == 'default'
265 or st.name.endswith(':default'))
266 return st
269def default_style(styles: list[gws.SourceStyle]) -> Optional[gws.SourceStyle]:
270 for s in styles:
271 if s.isDefault:
272 return s
273 return styles[0] if styles else None
276##
279def to_float(s, default=0.0):
280 return float(s or default)
283def to_int(s, default=0):
284 # accept floats as well, but convert to int
285 return int(float(s or default))
288def to_float_pair(s):
289 s = s.split()
290 return float(s[0]), float(s[1])
293##
296def _parse_bbox(el: gws.XmlElement):
297 # note: bboxes are always converted to (x1, y1, x2, y2) with x1 < x2, y1 < y2
299 # <BoundingBox/LatLonBoundingBox CRS="..." minx="0" miny="1" maxx="2" maxy="3"/>
301 if el.get('minx'):
302 return [
303 to_float(el.get('minx')),
304 to_float(el.get('miny')),
305 to_float(el.get('maxx')),
306 to_float(el.get('maxy')),
307 ]
309 # <ows:BoundingBox/WGS84BoundingBox
310 # <ows:LowerCorner> 0 1
311 # <ows:UpperCorner> 2 3
313 if el.findfirst('LowerCorner'):
314 x1, y1 = to_float_pair(el.textof('LowerCorner'))
315 x2, y2 = to_float_pair(el.textof('UpperCorner'))
316 return [
317 min(x1, x2),
318 min(y1, y2),
319 max(x1, x2),
320 max(y1, y2),
321 ]
323 # <EX_GeographicBoundingBox>
324 # <westBoundLongitude> 0
325 # <eastBoundLongitude> 2
326 # <southBoundLatitude> 1
327 # <northBoundLatitude> 3
329 if el.findfirst('westBoundLongitude'):
330 x1 = to_float(el.textof('eastBoundLongitude'))
331 y1 = to_float(el.textof('southBoundLatitude'))
332 x2 = to_float(el.textof('westBoundLongitude'))
333 y2 = to_float(el.textof('northBoundLatitude'))
334 return [
335 min(x1, x2),
336 min(y1, y2),
337 max(x1, x2),
338 max(y1, y2),
339 ]
342def _parse_url(el: gws.XmlElement) -> str:
343 def cleanup(s):
344 return (s or '').strip(' ?&')
346 if not el:
347 return ''
349 # <ows:DCP>
350 # <ows:HTTP>
351 # <ows:Get xlink:href=... <-- we are here
353 s = el.get('href') or el.get('onlineResource')
354 if s:
355 return cleanup(s)
357 # <whatever <--
358 # <OnlineResource xlink:href=...
360 e = el.findfirst('OnlineResource')
361 if e:
362 return cleanup(e.get('href', default=e.text))
364 return ''
367def _parse_link(el: gws.XmlElement) -> Optional[gws.MetadataLink]:
368 if not el:
369 return None
371 # see base/ows/server/templatelib.py
372 # regarding different MetadataURL formats
374 # simple
375 if el.get('href'):
376 return gws.MetadataLink(url=el.get('href'))
378 # nested
379 d = gws.u.strip({
380 'url': _parse_url(el),
381 'type': el.get('type'),
382 'format': el.textof('Format'),
383 })
385 if d:
386 return gws.MetadataLink(d)