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

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

2 

3from typing import Optional 

4 

5import re 

6 

7import gws 

8import gws.gis.crs 

9import gws.gis.extent 

10import gws.lib.net 

11 

12 

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

14 # <ows:OperationsMetadata> 

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

16 

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

18 if els: 

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

20 

21 # <Capability> 

22 # <Request> 

23 # <GetCapabilities>... 

24 

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

26 if el: 

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

28 

29 return [] 

30 

31 

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

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

34 

35 # @TODO Range 

36 # @TODO Constraint 

37 

38 # <Parameter name="Format"> 

39 # <AllowedValues> 

40 # <Value>image/gif</Value> 

41 # ... 

42 

43 # <Parameter name="AcceptVersions"> 

44 # <Value>1.0.0</Value> 

45 

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 

51 

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> 

65 

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

67 

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

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

70 

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

72 if 'OUTPUTFORMAT' in op.allowedParameters: 

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

74 

75 return op 

76 

77 

78## 

79 

80 

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

98 

99 md = gws.Metadata() 

100 

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

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

103 

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

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

106 

107 # <Capabilities 

108 # <ServiceMetadataURL 

109 

110 link = _parse_link(caps_el.find('ServiceMetadataURL')) 

111 if link: 

112 md.serviceMetaLink = link 

113 

114 return gws.u.strip(md) 

115 

116 

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

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

119 # <Name... 

120 # <Title... 

121 

122 md = gws.Metadata() 

123 _element_metadata(el, md) 

124 return gws.u.strip(md) 

125 

126 

127def _element_metadata(el: gws.XmlElement, md: gws.Metadata): 

128 if not el: 

129 return 

130 

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

139 

140 e = el.find('AuthorityURL') 

141 if e: 

142 md.authorityUrl = _parse_url(e) 

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

144 

145 e = el.find('Identifier') 

146 if e: 

147 md.authorityIdentifier = e.text 

148 

149 

150_contact_mapping = [ 

151 # wms 

152 

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

163 

164 # ows 

165 

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] 

177 

178 

179def _contact_metadata(el: gws.XmlElement, md: gws.Metadata): 

180 if not el: 

181 return 

182 

183 src = el.textdict(deep=True) 

184 

185 for dkey, skey in _contact_mapping: 

186 if skey in src: 

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

188 

189 

190## 

191 

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

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

194 

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. 

198 

199 Args: 

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

201 """ 

202 

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

204 if el: 

205 return gws.gis.extent.from_list(_parse_bbox(el)) 

206 

207 

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. 

210 

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

212 for OWS, DefaultCRS and OtherCRS. 

213 

214 Args: 

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

216 extra_crs_ids: additional CRS ids. 

217 

218 Returns: 

219 A list of ``Crs`` objects. 

220 """ 

221 

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

229 

230 crsids = set() 

231 

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

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

234 

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) 

239 

240 crsids.update(extra_crs_ids or []) 

241 

242 return gws.u.compact(gws.gis.crs.get(s) for s in crsids) 

243 

244 

245## 

246 

247 

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

249 # <Style> 

250 # <Name>default... 

251 # <Title>... 

252 # <LegendURL 

253 # <Format>... 

254 # <OnlineResource... 

255 

256 st = gws.SourceStyle() 

257 

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 

266 

267 

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 

273 

274 

275## 

276 

277 

278def to_float(s, default=0.0): 

279 return float(s or default) 

280 

281 

282def to_int(s, default=0): 

283 # accept floats as well, but convert to int 

284 return int(float(s or default)) 

285 

286 

287def to_float_pair(s): 

288 s = s.split() 

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

290 

291 

292## 

293 

294 

295def _parse_bbox(el: gws.XmlElement): 

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

297 

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

299 

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 ] 

307 

308 # <ows:BoundingBox/WGS84BoundingBox 

309 # <ows:LowerCorner> 0 1 

310 # <ows:UpperCorner> 2 3 

311 

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 ] 

321 

322 # <EX_GeographicBoundingBox> 

323 # <westBoundLongitude> 0 

324 # <eastBoundLongitude> 2 

325 # <southBoundLatitude> 1 

326 # <northBoundLatitude> 3 

327 

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 ] 

339 

340 

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

342 def cleanup(s): 

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

344 

345 if not el: 

346 return '' 

347 

348 # <ows:DCP> 

349 # <ows:HTTP> 

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

351 

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

353 if s: 

354 return cleanup(s) 

355 

356 # <whatever <-- 

357 # <OnlineResource xlink:href=... 

358 

359 e = el.findfirst('OnlineResource') 

360 if e: 

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

362 

363 return '' 

364 

365 

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

367 if not el: 

368 return None 

369 

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

371 # regarding different MetadataURL formats 

372 

373 # simple 

374 if el.get('href'): 

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

376 

377 # nested 

378 d = gws.u.strip({ 

379 'url': _parse_url(el), 

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

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

382 }) 

383 

384 if d: 

385 return gws.MetadataLink(d)