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

1"""Parse utilities for OWS XML files.""" 

2 

3from typing import Optional 

4 

5import re 

6 

7import gws 

8import gws.base.metadata 

9import gws.lib.crs 

10import gws.lib.extent 

11import gws.lib.net 

12 

13 

14def service_operations(caps_el: gws.XmlElement) -> list[gws.OwsOperation]: 

15 # <ows:OperationsMetadata> 

16 # <ows:Operation name="GetCapabilities">... 

17 

18 els = caps_el.findall('OperationsMetadata/Operation') 

19 if els: 

20 return [_parse_operation(e) for e in els] 

21 

22 # <Capability> 

23 # <Request> 

24 # <GetCapabilities>... 

25 

26 el = caps_el.find('Capability/Request') 

27 if el: 

28 return [_parse_operation(e) for e in el] 

29 

30 return [] 

31 

32 

33def _parse_operation(el: gws.XmlElement) -> gws.OwsOperation: 

34 op = gws.OwsOperation(verb=el.get('name') or el.tag) 

35 

36 # @TODO Range 

37 # @TODO Constraint 

38 

39 # <Parameter name="Format"> 

40 # <AllowedValues> 

41 # <Value>image/gif</Value> 

42 # ... 

43 

44 # <Parameter name="AcceptVersions"> 

45 # <Value>1.0.0</Value> 

46 

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 

52 

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> 

66 

67 op.postUrl = _parse_url(el.findfirst('DCP/HTTP/Post', 'DCPType/HTTP/Post')) 

68 

69 u = _parse_url(el.findfirst('DCP/HTTP/Get', 'DCPType/HTTP/Get')) 

70 op.url, op.params = gws.lib.net.extract_params(u) 

71 

72 op.formats = el.textlist('Format') 

73 if 'OUTPUTFORMAT' in op.allowedParameters: 

74 op.formats.extend(op.allowedParameters['OUTPUTFORMAT']) 

75 

76 return op 

77 

78 

79## 

80 

81 

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

99 

100 md = gws.base.metadata.new() 

101 

102 _element_metadata(caps_el.findfirst('Service', 'ServiceIdentification'), md) 

103 _contact_metadata(caps_el.findfirst('Service/ContactInformation', 'ServiceProvider/ServiceContact'), md) 

104 

105 md.contactProviderName = caps_el.textof('ServiceProvider/ProviderName') 

106 md.contactProviderSite = caps_el.textof('ServiceProvider/ProviderSite') 

107 

108 # <Capabilities 

109 # <ServiceMetadataURL 

110 

111 service_link = _parse_link(caps_el.find('ServiceMetadataURL')) 

112 if service_link: 

113 md.serviceMetadataURL = service_link.url 

114 

115 return gws.base.metadata.normalize(md) 

116 

117 

118def element_metadata(el: gws.XmlElement) -> gws.Metadata: 

119 # <whatever, e.g. Layer or FeatureType 

120 # <Name... 

121 # <Title... 

122 

123 md = gws.base.metadata.new() 

124 _element_metadata(el, md) 

125 return gws.base.metadata.normalize(md) 

126 

127 

128def _element_metadata(el: Optional[gws.XmlElement], md: gws.Metadata): 

129 if not el: 

130 return 

131 

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')) 

140 

141 e = el.find('AuthorityURL') 

142 if e: 

143 md.authorityUrl = _parse_url(e) 

144 md.authorityName = e.get('name') 

145 

146 e = el.find('Identifier') 

147 if e: 

148 md.authorityIdentifier = e.text 

149 

150 

151_contact_mapping = [ 

152 # wms 

153 

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'), 

164 

165 # ows 

166 

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] 

178 

179 

180def _contact_metadata(el: Optional[gws.XmlElement], md: gws.Metadata): 

181 if not el: 

182 return 

183 

184 src = el.textdict(deep=True) 

185 

186 for dkey, skey in _contact_mapping: 

187 if skey in src: 

188 setattr(md, dkey, src[skey]) 

189 

190 

191## 

192 

193def wgs_extent(layer_el: gws.XmlElement) -> Optional[gws.Extent]: 

194 """Read WGS bounding box from a Layer/FeatureType element. 

195 

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. 

199 

200 Args: 

201 layer_el: 'Layer' or 'FeatureType' element. 

202 """ 

203 

204 el = layer_el.findfirst('EX_GeographicBoundingBox', 'WGS84BoundingBox', 'LatLonBoundingBox') 

205 if el: 

206 return gws.lib.extent.from_list(_parse_bbox(el)) 

207 

208 

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. 

211 

212 For WMS, enumerates CRS/SRS and BoundingBox tags, 

213 for OWS, DefaultCRS and OtherCRS. 

214 

215 Args: 

216 layer_el: 'Layer' or 'FeatureType' element. 

217 extra_crs_ids: additional CRS ids. 

218 

219 Returns: 

220 A list of ``Crs`` objects. 

221 """ 

222 

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

230 

231 crsids = set() 

232 

233 for el in layer_el.findall('BoundingBox'): 

234 crsids.add(el.get('SRS') or el.get('CRS')) 

235 

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) 

240 

241 crsids.update(extra_crs_ids or []) 

242 

243 return gws.u.compact(gws.lib.crs.get(s) for s in crsids) 

244 

245 

246## 

247 

248 

249def parse_style(el: gws.XmlElement) -> gws.SourceStyle: 

250 # <Style> 

251 # <Name>default... 

252 # <Title>... 

253 # <LegendURL 

254 # <Format>... 

255 # <OnlineResource... 

256 

257 st = gws.SourceStyle() 

258 

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 

267 

268 

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 

274 

275 

276## 

277 

278 

279def to_float(s, default=0.0): 

280 return float(s or default) 

281 

282 

283def to_int(s, default=0): 

284 # accept floats as well, but convert to int 

285 return int(float(s or default)) 

286 

287 

288def to_float_pair(s): 

289 s = s.split() 

290 return float(s[0]), float(s[1]) 

291 

292 

293## 

294 

295 

296def _parse_bbox(el: gws.XmlElement): 

297 # note: bboxes are always converted to (x1, y1, x2, y2) with x1 < x2, y1 < y2 

298 

299 # <BoundingBox/LatLonBoundingBox CRS="..." minx="0" miny="1" maxx="2" maxy="3"/> 

300 

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 ] 

308 

309 # <ows:BoundingBox/WGS84BoundingBox 

310 # <ows:LowerCorner> 0 1 

311 # <ows:UpperCorner> 2 3 

312 

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 ] 

322 

323 # <EX_GeographicBoundingBox> 

324 # <westBoundLongitude> 0 

325 # <eastBoundLongitude> 2 

326 # <southBoundLatitude> 1 

327 # <northBoundLatitude> 3 

328 

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 ] 

340 

341 

342def _parse_url(el: gws.XmlElement) -> str: 

343 def cleanup(s): 

344 return (s or '').strip(' ?&') 

345 

346 if not el: 

347 return '' 

348 

349 # <ows:DCP> 

350 # <ows:HTTP> 

351 # <ows:Get xlink:href=... <-- we are here 

352 

353 s = el.get('href') or el.get('onlineResource') 

354 if s: 

355 return cleanup(s) 

356 

357 # <whatever <-- 

358 # <OnlineResource xlink:href=... 

359 

360 e = el.findfirst('OnlineResource') 

361 if e: 

362 return cleanup(e.get('href', default=e.text)) 

363 

364 return '' 

365 

366 

367def _parse_link(el: gws.XmlElement) -> Optional[gws.MetadataLink]: 

368 if not el: 

369 return None 

370 

371 # see base/ows/server/templatelib.py 

372 # regarding different MetadataURL formats 

373 

374 # simple 

375 if el.get('href'): 

376 return gws.MetadataLink(url=el.get('href')) 

377 

378 # nested 

379 d = gws.u.strip({ 

380 'url': _parse_url(el), 

381 'type': el.get('type'), 

382 'format': el.textof('Format'), 

383 }) 

384 

385 if d: 

386 return gws.MetadataLink(d)