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
« 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
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 """
45 rs = featureinfo.parse(xml)
47 assert len(rs) == 3
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
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'
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'
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 """
109 rs = featureinfo.parse(xml)
111 assert len(rs) == 3
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
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'
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'
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 """
165 rs = featureinfo.parse(xml, default_crs=gws.lib.crs.WGS84)
167 assert len(rs) == 4
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
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'
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
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'
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 """
214 rs = featureinfo.parse(xml, default_crs=gws.lib.crs.WGS84)
216 assert len(rs) == 3
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
224 avenue = rs[1]
225 assert avenue.attributes['name'] == 'Commonwealth Ave'
226 assert avenue.attributes['type'] == 'arterial'
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
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 """
246 rs = featureinfo.parse(xml)
248 assert len(rs) == 4
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
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'
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'
265 commercial = next(r for r in rs if r.uid == '1004')
266 assert commercial.attributes['landuse'] == 'commercial'
267 assert commercial.attributes['zone'] == 'C2'
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 """
310 rs = featureinfo.parse(xml)
312 assert len(rs) == 2
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'
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'
323def test_parse_error():
324 """Test parsing empty or invalid responses."""
325 # Empty string
326 rs = featureinfo.parse('')
327 assert rs == []
329 # Non-XML content
330 with u.raises(featureinfo.Error):
331 rs = featureinfo.parse('No features found')
333 # Invalid XML
334 with u.raises(featureinfo.Error):
335 rs = featureinfo.parse('<invalid><xml')
337 xml = """
338 <UnknownFormat>
339 <SomeElement>
340 <attribute>value</attribute>
341 </SomeElement>
342 </UnknownFormat>
343 """
345 with u.raises(featureinfo.Error):
346 rs = featureinfo.parse(xml)
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 """
380 rs = featureinfo.parse(xml)
382 assert len(rs) == 2
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'
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'
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 """
422 rs = featureinfo.parse(xml)
424 assert len(rs) == 2
426 feat1 = next(r for r in rs if r.attributes.get('name') == 'Test Feature')
427 assert feat1.shape is not None
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
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 """
453 rs = featureinfo.parse(xml)
455 assert len(rs) == 2
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'
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'
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 """
494 rs = featureinfo.parse(xml)
496 assert len(rs) == 2
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
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
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=}')