Coverage for gws-app/gws/base/ows/client/_test/featureinfo_test.py: 100%

162 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-16 23:09 +0200

1import gws 

2import gws.test.util as u 

3import gws.lib.crs 

4import gws.base.ows.client.featureinfo as featureinfo 

5 

6 

7def test_parse_msgmloutput(): 

8 """Test MapServer msGMLOutput format parsing.""" 

9 xml = """ 

10 <msGMLOutput xmlns:gml="http://www.opengis.net/gml"> 

11 <roads_layer> 

12 <gml:name>Road Network</gml:name> 

13 <road_1 fid="roads.1"> 

14 <gml:boundedBy> 

15 <gml:Box srsName="EPSG:4326"> 

16 <gml:coordinates>-71.123,42.234 -71.122,42.235</gml:coordinates> 

17 </gml:Box> 

18 </gml:boundedBy> 

19 <GEOMETRY> 

20 <gml:LineString srsName="EPSG:4326"> 

21 <gml:coordinates>-71.123,42.234 -71.122,42.235</gml:coordinates> 

22 </gml:LineString> 

23 </GEOMETRY> 

24 <name>Main Street</name> 

25 <type>residential</type> 

26 <speed_limit>25</speed_limit> 

27 </road_1> 

28 <road_2 fid="roads.2"> 

29 <name>Highway 101</name> 

30 <type>highway</type> 

31 <speed_limit>65</speed_limit> 

32 </road_2> 

33 </roads_layer> 

34 <buildings_layer> 

35 <gml:name>Buildings</gml:name> 

36 <building_1 id="bldg.100"> 

37 <name>City Hall</name> 

38 <height>45</height> 

39 <use>government</use> 

40 </building_1> 

41 </buildings_layer> 

42 </msGMLOutput> 

43 """ 

44 

45 rs = featureinfo.parse(xml) 

46 

47 assert len(rs) == 3 

48 

49 # Road features 

50 road1 = next(r for r in rs if r.uid == 'roads.1') 

51 assert road1.meta['layerName'] == 'Road Network' 

52 assert road1.attributes['name'] == 'Main Street' 

53 assert road1.attributes['type'] == 'residential' 

54 assert road1.shape is not None 

55 

56 road2 = next(r for r in rs if r.uid == 'roads.2') 

57 assert road2.meta['layerName'] == 'Road Network' 

58 assert road2.attributes['name'] == 'Highway 101' 

59 assert road2.attributes['speed_limit'] == '65' 

60 

61 # Building feature 

62 bldg = next(r for r in rs if r.uid == 'bldg.100') 

63 assert bldg.meta['layerName'] == 'Buildings' 

64 assert bldg.attributes['name'] == 'City Hall' 

65 

66 

67def test_parse_featurecollection(): 

68 """Test OGC FeatureCollection format parsing.""" 

69 xml = """ 

70 <wfs:FeatureCollection xmlns:wfs="http://www.opengis.net/wfs" 

71 xmlns:gml="http://www.opengis.net/gml" 

72 xmlns:app="http://example.com/app"> 

73 <wfs:member> 

74 <app:buildings gml:id="buildings.123"> 

75 <app:name>City Hall</app:name> 

76 <app:height>45.5</app:height> 

77 <app:use>government</app:use> 

78 <app:geometry> 

79 <gml:Polygon srsName="EPSG:4326"> 

80 <gml:outerBoundaryIs> 

81 <gml:LinearRing> 

82 <gml:coordinates>-71.1,42.2 -71.09,42.2 -71.09,42.21 -71.1,42.21 -71.1,42.2</gml:coordinates> 

83 </gml:LinearRing> 

84 </gml:outerBoundaryIs> 

85 </gml:Polygon> 

86 </app:geometry> 

87 </app:buildings> 

88 </wfs:member> 

89 <wfs:member> 

90 <app:roads gml:id="roads.456"> 

91 <app:name>Main Street</app:name> 

92 <app:type>residential</app:type> 

93 <app:lanes>2</app:lanes> 

94 </app:roads> 

95 </wfs:member> 

96 <wfs:member> 

97 <app:parks gml:id="parks.789"> 

98 <app:name>Central Park</app:name> 

99 <app:area>125.5</app:area> 

100 <app:facilities> 

101 <app:playground>yes</app:playground> 

102 <app:parking>no</app:parking> 

103 </app:facilities> 

104 </app:parks> 

105 </wfs:member> 

106 </wfs:FeatureCollection> 

107 """ 

108 

109 rs = featureinfo.parse(xml) 

110 

111 assert len(rs) == 3 

112 

113 building = next(r for r in rs if r.uid == 'buildings.123') 

114 assert building.meta['layerName'] == 'buildings' 

115 assert building.attributes['name'] == 'City Hall' 

116 assert building.attributes['height'] == '45.5' 

117 assert building.shape is not None 

118 

119 road = next(r for r in rs if r.uid == 'roads.456') 

120 assert road.meta['layerName'] == 'roads' 

121 assert road.attributes['name'] == 'Main Street' 

122 assert road.attributes['lanes'] == '2' 

123 

124 park = next(r for r in rs if r.uid == 'parks.789') 

125 assert park.meta['layerName'] == 'parks' 

126 assert park.attributes['name'] == 'Central Park' 

127 assert park.attributes['facilities.playground'] == 'yes' 

128 assert park.attributes['facilities.parking'] == 'no' 

129 

130 

131def test_parse_getfeatureinforesponse(): 

132 """Test GeoServer GetFeatureInfoResponse format parsing.""" 

133 xml = """ 

134 <GetFeatureInfoResponse> 

135 <Layer name="ne:populated_places"> 

136 <Feature id="populated_places.1"> 

137 <Attribute name="name" value="Boston"/> 

138 <Attribute name="pop_max" value="4628910"/> 

139 <Attribute name="adm0name" value="United States of America"/> 

140 <Attribute name="geometry" value="POINT(-71.0275 42.3584)"/> 

141 </Feature> 

142 <Feature id="populated_places.2"> 

143 <Attribute name="name" value="Cambridge"/> 

144 <Attribute name="pop_max" value="105162"/> 

145 <Attribute name="adm0name" value="United States of America"/> 

146 <Attribute name="geometry" value="POINT(-71.1056 42.3736)"/> 

147 </Feature> 

148 </Layer> 

149 <Layer name="ne:roads"> 

150 <Feature id="roads.500"> 

151 <Attribute name="name" value="Interstate 95"/> 

152 <Attribute name="type" value="highway"/> 

153 <Attribute name="lanes" value="6"/> 

154 <Attribute name="geometry" value="LINESTRING(-71.0 42.3, -71.1 42.4)"/> 

155 </Feature> 

156 <Feature id="roads.501"> 

157 <Attribute name="name" value="Commonwealth Ave"/> 

158 <Attribute name="type" value="arterial"/> 

159 <Attribute name="lanes" value="4"/> 

160 </Feature> 

161 </Layer> 

162 </GetFeatureInfoResponse> 

163 """ 

164 

165 rs = featureinfo.parse(xml, default_crs=gws.lib.crs.WGS84) 

166 

167 assert len(rs) == 4 

168 

169 boston = next(r for r in rs if r.uid == 'populated_places.1') 

170 assert boston.meta['layerName'] == 'ne:populated_places' 

171 assert boston.attributes['name'] == 'Boston' 

172 assert boston.attributes['pop_max'] == '4628910' 

173 assert boston.shape is not None 

174 

175 cambridge = next(r for r in rs if r.uid == 'populated_places.2') 

176 assert cambridge.attributes['name'] == 'Cambridge' 

177 assert cambridge.attributes['pop_max'] == '105162' 

178 

179 highway = next(r for r in rs if r.uid == 'roads.500') 

180 assert highway.meta['layerName'] == 'ne:roads' 

181 assert highway.attributes['name'] == 'Interstate 95' 

182 assert highway.attributes['lanes'] == '6' 

183 assert highway.shape is not None 

184 

185 avenue = next(r for r in rs if r.uid == 'roads.501') 

186 assert avenue.attributes['name'] == 'Commonwealth Ave' 

187 assert avenue.attributes['type'] == 'arterial' 

188 

189 

190def test_parse_getfeatureinforesponse_raster(): 

191 """Test GeoServer GetFeatureInfoResponse (raster) format parsing.""" 

192 xml = """ 

193 <GetFeatureInfoResponse> 

194 <Layer name="ne:populated_places"> 

195 <Attribute name="name" value="Boston"/> 

196 <Attribute name="pop_max" value="4628910"/> 

197 <Attribute name="adm0name" value="United States of America"/> 

198 <Attribute name="geometry" value="POINT(-71.0275 42.3584)"/> 

199 </Layer> 

200 <Layer name="ne:roads"> 

201 <Attribute name="name" value="Interstate 95"/> 

202 <Attribute name="type" value="highway"/> 

203 <Attribute name="lanes" value="6"/> 

204 <Attribute name="geometry" value="LINESTRING(-71.0 42.3, -71.1 42.4)"/> 

205 <Feature id="roads.501"> 

206 <Attribute name="name" value="Commonwealth Ave"/> 

207 <Attribute name="type" value="arterial"/> 

208 <Attribute name="lanes" value="4"/> 

209 </Feature> 

210 </Layer> 

211 </GetFeatureInfoResponse> 

212 """ 

213 

214 rs = featureinfo.parse(xml, default_crs=gws.lib.crs.WGS84) 

215 

216 assert len(rs) == 3 

217 

218 boston = rs[0] 

219 assert boston.meta['layerName'] == 'ne:populated_places' 

220 assert boston.attributes['name'] == 'Boston' 

221 assert boston.attributes['pop_max'] == '4628910' 

222 assert boston.shape is not None 

223 

224 avenue = rs[1] 

225 assert avenue.attributes['name'] == 'Commonwealth Ave' 

226 assert avenue.attributes['type'] == 'arterial' 

227 

228 highway = rs[2] 

229 assert highway.meta['layerName'] == 'ne:roads' 

230 assert highway.attributes['name'] == 'Interstate 95' 

231 assert highway.attributes['lanes'] == '6' 

232 assert highway.shape is not None 

233 

234 

235def test_parse_featureinforesponse(): 

236 """Test ArcGIS FeatureInfoResponse format parsing.""" 

237 xml = """ 

238 <FeatureInfoResponse xmlns:esri_wms="http://www.esri.com/wms"> 

239 <FIELDS objectid="1001" shape="polygon" area="15632.45" landuse="residential" zone="R1"/> 

240 <FIELDS objectid="1002" shape="point" population="25000" city="Springfield" state="MA"/> 

241 <FIELDS fid="1003" shape="linestring" name="Route 66" highway_type="interstate" lanes="4"/> 

242 <FIELDS id="1004" area="8500.25" landuse="commercial" zone="C2"/> 

243 </FeatureInfoResponse> 

244 """ 

245 

246 rs = featureinfo.parse(xml) 

247 

248 assert len(rs) == 4 

249 

250 residential = next(r for r in rs if r.uid == '1001') 

251 assert residential.attributes['area'] == '15632.45' 

252 assert residential.attributes['landuse'] == 'residential' 

253 assert residential.attributes['zone'] == 'R1' 

254 assert 'shape' not in residential.attributes 

255 

256 city = next(r for r in rs if r.uid == '1002') 

257 assert city.attributes['population'] == '25000' 

258 assert city.attributes['city'] == 'Springfield' 

259 assert city.attributes['state'] == 'MA' 

260 

261 highway = next(r for r in rs if r.uid == '1003') 

262 assert highway.attributes['name'] == 'Route 66' 

263 assert highway.attributes['lanes'] == '4' 

264 

265 commercial = next(r for r in rs if r.uid == '1004') 

266 assert commercial.attributes['landuse'] == 'commercial' 

267 assert commercial.attributes['zone'] == 'C2' 

268 

269 

270def test_parse_geobak(): 

271 """Test GeoBAK format parsing.""" 

272 xml = """ 

273 <geobak_20:Sachdatenabfrage xmlns:geobak_20="http://www.geobak.sachsen.de/20"> 

274 <geobak_20:Kartenebene>Flurstücke</geobak_20:Kartenebene> 

275 <geobak_20:Inhalt> 

276 <geobak_20:Datensatz> 

277 <geobak_20:Attribut> 

278 <geobak_20:Name>Flurstücksnummer</geobak_20:Name> 

279 <geobak_20:Wert>123/45</geobak_20:Wert> 

280 </geobak_20:Attribut> 

281 <geobak_20:Attribut> 

282 <geobak_20:Name>Gemarkung</geobak_20:Name> 

283 <geobak_20:Wert>Dresden</geobak_20:Wert> 

284 </geobak_20:Attribut> 

285 <geobak_20:Attribut> 

286 <geobak_20:Name>Fläche</geobak_20:Name> 

287 <geobak_20:Wert>1250.5</geobak_20:Wert> 

288 </geobak_20:Attribut> 

289 </geobak_20:Datensatz> 

290 </geobak_20:Inhalt> 

291 <geobak_20:Inhalt> 

292 <geobak_20:Datensatz> 

293 <geobak_20:Attribut> 

294 <geobak_20:Name>Flurstücksnummer</geobak_20:Name> 

295 <geobak_20:Wert>678/90</geobak_20:Wert> 

296 </geobak_20:Attribut> 

297 <geobak_20:Attribut> 

298 <geobak_20:Name>Gemarkung</geobak_20:Name> 

299 <geobak_20:Wert>Leipzig</geobak_20:Wert> 

300 </geobak_20:Attribut> 

301 <geobak_20:Attribut> 

302 <geobak_20:Name>Fläche</geobak_20:Name> 

303 <geobak_20:Wert>875.0</geobak_20:Wert> 

304 </geobak_20:Attribut> 

305 </geobak_20:Datensatz> 

306 </geobak_20:Inhalt> 

307 </geobak_20:Sachdatenabfrage> 

308 """ 

309 

310 rs = featureinfo.parse(xml) 

311 

312 assert len(rs) == 2 

313 

314 dresden_rec = next(r for r in rs if r.attributes.get('gemarkung') == 'Dresden') 

315 assert dresden_rec.attributes['flurstücksnummer'] == '123/45' 

316 assert dresden_rec.attributes['fläche'] == '1250.5' 

317 

318 leipzig_rec = next(r for r in rs if r.attributes.get('gemarkung') == 'Leipzig') 

319 assert leipzig_rec.attributes['flurstücksnummer'] == '678/90' 

320 assert leipzig_rec.attributes['fläche'] == '875.0' 

321 

322 

323def test_parse_error(): 

324 """Test parsing empty or invalid responses.""" 

325 # Empty string 

326 rs = featureinfo.parse('') 

327 assert rs == [] 

328 

329 # Non-XML content 

330 with u.raises(featureinfo.Error): 

331 rs = featureinfo.parse('No features found') 

332 

333 # Invalid XML 

334 with u.raises(featureinfo.Error): 

335 rs = featureinfo.parse('<invalid><xml') 

336 

337 xml = """ 

338 <UnknownFormat> 

339 <SomeElement> 

340 <attribute>value</attribute> 

341 </SomeElement> 

342 </UnknownFormat> 

343 """ 

344 

345 with u.raises(featureinfo.Error): 

346 rs = featureinfo.parse(xml) 

347 

348 

349def test_parse_nested_attributes(): 

350 """Test parsing nested attributes in GML features.""" 

351 xml = """ 

352 <wfs:FeatureCollection xmlns:wfs="http://www.opengis.net/wfs" 

353 xmlns:gml="http://www.opengis.net/gml" 

354 xmlns:app="http://example.com/app"> 

355 <wfs:member> 

356 <app:complex_feature gml:id="complex.1"> 

357 <app:simple_attr>Simple Value</app:simple_attr> 

358 <app:nested_element> 

359 <app:sub_attr1>Sub Value 1</app:sub_attr1> 

360 <app:sub_attr2>Sub Value 2</app:sub_attr2> 

361 </app:nested_element> 

362 </app:complex_feature> 

363 </wfs:member> 

364 <wfs:member> 

365 <app:building gml:id="building.1"> 

366 <app:name>Office Building</app:name> 

367 <app:address> 

368 <app:street>123 Main St</app:street> 

369 <app:city>Boston</app:city> 

370 <app:contact> 

371 <app:phone>555-1234</app:phone> 

372 <app:email>info@example.com</app:email> 

373 </app:contact> 

374 </app:address> 

375 </app:building> 

376 </wfs:member> 

377 </wfs:FeatureCollection> 

378 """ 

379 

380 rs = featureinfo.parse(xml) 

381 

382 assert len(rs) == 2 

383 

384 complex_rec = next(r for r in rs if r.uid == 'complex.1') 

385 assert complex_rec.attributes['simple_attr'] == 'Simple Value' 

386 assert complex_rec.attributes['nested_element.sub_attr1'] == 'Sub Value 1' 

387 assert complex_rec.attributes['nested_element.sub_attr2'] == 'Sub Value 2' 

388 

389 building_rec = next(r for r in rs if r.uid == 'building.1') 

390 assert building_rec.attributes['name'] == 'Office Building' 

391 assert building_rec.attributes['address.street'] == '123 Main St' 

392 assert building_rec.attributes['address.city'] == 'Boston' 

393 assert building_rec.attributes['address.contact.phone'] == '555-1234' 

394 

395 

396def test_parse_with_boundedby_only(): 

397 """Test parsing feature with only boundedBy geometry.""" 

398 xml = """ 

399 <msGMLOutput xmlns:gml="http://www.opengis.net/gml"> 

400 <test_layer> 

401 <test_feature> 

402 <gml:boundedBy> 

403 <gml:Box srsName="EPSG:4326"> 

404 <gml:coordinates>-71.123,42.234 -71.122,42.235</gml:coordinates> 

405 </gml:Box> 

406 </gml:boundedBy> 

407 <name>Test Feature</name> 

408 </test_feature> 

409 <test_feature2> 

410 <gml:boundedBy> 

411 <gml:Box srsName="EPSG:4326"> 

412 <gml:coordinates>-70.123,41.234 -70.122,41.235</gml:coordinates> 

413 </gml:Box> 

414 </gml:boundedBy> 

415 <name>Another Feature</name> 

416 <type>test</type> 

417 </test_feature2> 

418 </test_layer> 

419 </msGMLOutput> 

420 """ 

421 

422 rs = featureinfo.parse(xml) 

423 

424 assert len(rs) == 2 

425 

426 feat1 = next(r for r in rs if r.attributes.get('name') == 'Test Feature') 

427 assert feat1.shape is not None 

428 

429 feat2 = next(r for r in rs if r.attributes.get('name') == 'Another Feature') 

430 assert feat2.attributes['type'] == 'test' 

431 assert feat2.shape is not None 

432 

433 

434def test_parse_case_insensitive(): 

435 """Test that XML parsing is case insensitive.""" 

436 xml = """ 

437 <FEATURECOLLECTION> 

438 <MEMBER> 

439 <BUILDING ID="1"> 

440 <NAME>Test Building</NAME> 

441 <HEIGHT>50</HEIGHT> 

442 </BUILDING> 

443 </MEMBER> 

444 <MEMBER> 

445 <ROAD ID="2"> 

446 <NAME>Main Street</NAME> 

447 <TYPE>residential</TYPE> 

448 </ROAD> 

449 </MEMBER> 

450 </FEATURECOLLECTION> 

451 """ 

452 

453 rs = featureinfo.parse(xml) 

454 

455 assert len(rs) == 2 

456 

457 building = next(r for r in rs if r.uid == '1') 

458 assert building.attributes['name'] == 'Test Building' 

459 assert building.attributes['height'] == '50' 

460 

461 road = next(r for r in rs if r.uid == '2') 

462 assert road.attributes['name'] == 'Main Street' 

463 assert road.attributes['type'] == 'residential' 

464 

465 

466def test_parse_multiple_geometries_last_wins(): 

467 """Test that when multiple geometries exist, the last one is used.""" 

468 xml = """ 

469 <wfs:FeatureCollection xmlns:wfs="http://www.opengis.net/wfs" 

470 xmlns:gml="http://www.opengis.net/gml" 

471 xmlns:app="http://example.com/app"> 

472 <wfs:member> 

473 <app:feature gml:id="test.1"> 

474 <app:name>Multi Geometry Feature</app:name> 

475 <gml:Point srsName="EPSG:4326"> 

476 <gml:coordinates>-71.1,42.2</gml:coordinates> 

477 </gml:Point> 

478 <gml:LineString srsName="EPSG:4326"> 

479 <gml:coordinates>-71.1,42.2 -71.09,42.21</gml:coordinates> 

480 </gml:LineString> 

481 </app:feature> 

482 </wfs:member> 

483 <wfs:member> 

484 <app:feature gml:id="test.2"> 

485 <app:name>Single Geometry Feature</app:name> 

486 <gml:Point srsName="EPSG:4326"> 

487 <gml:coordinates>-70.1,41.2</gml:coordinates> 

488 </gml:Point> 

489 </app:feature> 

490 </wfs:member> 

491 </wfs:FeatureCollection> 

492 """ 

493 

494 rs = featureinfo.parse(xml) 

495 

496 assert len(rs) == 2 

497 

498 multi_geom = next(r for r in rs if r.uid == 'test.1') 

499 assert multi_geom.attributes['name'] == 'Multi Geometry Feature' 

500 assert multi_geom.shape is not None 

501 

502 single_geom = next(r for r in rs if r.uid == 'test.2') 

503 assert single_geom.attributes['name'] == 'Single Geometry Feature' 

504 assert single_geom.shape is not None 

505 

506 

507def test_real_life_examples(): 

508 import os 

509 for de in os.scandir(os.path.dirname(__file__) + '/featureinfo'): 

510 xml = gws.u.read_file(de.path) 

511 rs = featureinfo.parse(xml, default_crs=gws.lib.crs.WGS84) 

512 assert isinstance(rs, list) 

513 # for r in rs: 

514 # print(f'\n{de.name} {r.uid=} {r.attributes=}')