Coverage for gws-app/gws/lib/gml/parser.py: 93%
109 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""GML geometry parsers."""
3import gws
4import gws.lib.shape
5import gws.lib.bounds
6import gws.lib.crs
7import gws.lib.extent
10class Error(gws.Error):
11 """GML parse error."""
13 pass
16_GEOMETRY_TAGS = [
17 'Curve',
18 'LinearRing',
19 'LineString',
20 'LineStringSegment',
21 'MultiCurve',
22 'MultiLineString',
23 'MultiPoint',
24 'MultiPolygon',
25 'MultiSurface',
26 'Point',
27 'Polygon',
28]
31def parse_envelope(el: gws.XmlElement, default_crs: gws.Crs = None, always_xy: bool = False) -> gws.Bounds:
32 """Parse a ``gml:Box`` or ``gml:Envelope`` element into bounds.
34 Args:
35 el: A ``Box`` or ``Envelope`` element.
36 default_crs: CRS to use when the element has no ``srsName`` attribute.
37 always_xy: If ``True``, coordinates are assumed to be in the XY (lon/lat) order.
39 Returns:
40 A Bounds object.
42 Raises:
43 ``Error``: If no CRS is given or the element cannot be parsed.
44 """
46 # GML2: <gml:Box><gml:coordinates>1,2 3,4
47 # GML3: <gml:Envelope srsDimension="2"><gml:lowerCorner>1 2 <gml:upperCorner>3 4
49 crs = gws.lib.crs.get(el.get('srsName')) or default_crs
50 if not crs:
51 raise Error('no CRS declared for envelope')
53 try:
54 ext = _parse_envelope_extent(el)
55 except Exception as exc:
56 raise Error('envelope parse error') from exc
58 return gws.lib.bounds.from_extent(ext, crs, always_xy)
61def _parse_envelope_extent(el: gws.XmlElement) -> gws.Extent:
62 if el.isa('Box'):
63 a, b = _coords(el)
64 return gws.lib.extent.from_points(a, b)
66 if el.isa('Envelope'):
67 a = b = None
68 for coord_el in el:
69 if coord_el.isa('lowerCorner'):
70 a = _coords_pos(coord_el)[0]
71 if coord_el.isa('upperCorner'):
72 b = _coords_pos(coord_el)[0]
73 if a and b:
74 return gws.lib.extent.from_points(a, b)
76 raise ValueError('invalid envelope element')
79def is_geometry_element(el: gws.XmlElement) -> bool:
80 """Check if an element is a supported GML geometry element.
82 Args:
83 el: An XML element.
85 Returns:
86 ``True`` if the element name is one of the supported geometry tags.
87 """
89 return el.isa(*_GEOMETRY_TAGS)
92def parse_shape(el: gws.XmlElement, default_crs: gws.Crs = None, always_xy: bool = False) -> gws.Shape:
93 """Convert a GML geometry element to a Shape.
95 Args:
96 el: A GML geometry element.
97 default_crs: CRS to use when the element has no ``srsName`` attribute.
98 always_xy: If ``True``, coordinates are assumed to be in the XY (lon/lat) order.
100 Returns:
101 A Shape object.
103 Raises:
104 ``Error``: If no CRS is given or the element cannot be parsed.
105 """
107 crs = gws.lib.crs.get(el.get('srsName')) or default_crs
108 if not crs:
109 raise Error('no CRS declared')
111 dct = parse_geometry(el)
112 return gws.lib.shape.from_geojson(dct, crs, always_xy)
115def parse_geometry(el: gws.XmlElement) -> dict:
116 """Convert a GML geometry element to a GeoJSON-like geometry dict.
118 A ``Curve`` is converted to a ``LineString`` from its first segment only.
119 ``MultiCurve`` and ``MultiSurface`` become ``MultiLineString`` and ``MultiPolygon``.
121 Args:
122 el: A GML geometry element.
124 Returns:
125 A dict with ``type`` and ``coordinates``.
127 Raises:
128 ``Error``: If the element cannot be parsed.
129 """
131 try:
132 return _to_geom(el)
133 except Exception as exc:
134 raise Error('parse error') from exc
137##
140def _to_geom(el: gws.XmlElement):
141 if el.isa('Point'):
142 # <gml:Point> pos/coordinates
143 return {'type': 'Point', 'coordinates': _coords(el)[0]}
145 if el.isa('LineString', 'LinearRing', 'LineStringSegment'):
146 # <gml:LineString> posList/coordinates
147 return {'type': 'LineString', 'coordinates': _coords(el)}
149 if el.isa('Curve'):
150 # GML3: <gml:Curve> <gml:segments> <gml:LineStringSegment>
151 # NB we only take the first segment
152 return _to_geom(el[0][0])
154 if el.isa('Polygon'):
155 # GML2: <gml:Polygon> <gml:outerBoundaryIs> <gml:LinearRing> <gml:innerBoundaryIs> <gml:LinearRing>...
156 # GML3: <gml:Polygon> <gml:exterior> <gml:LinearRing> <gml:interior> <gml:LinearRing>...
157 return {'type': 'Polygon', 'coordinates': _rings(el)}
159 if el.isa('MultiPoint'):
160 # <gml:MultiPoint> <gml:pointMember> <gml:Point>
161 return {'type': 'MultiPoint', 'coordinates': [m['coordinates'] for m in _members(el)]}
163 if el.isa('MultiLineString', 'MultiCurve'):
164 # GML2: <gml:MultiLineString> <gml:lineStringMember> <gml:LineString>
165 # GML3: <gml:MultiCurve> <gml:curveMember> <gml:Curve>
166 return {'type': 'MultiLineString', 'coordinates': [m['coordinates'] for m in _members(el)]}
168 if el.isa('MultiPolygon', 'MultiSurface'):
169 # GML2: <gml:MultiPolygon> <gml:polygonMember> <gml:Polygon>
170 # GML3: <gml:MultiSurface> <gml:surfaceMember> <gml:Polygon>
171 return {'type': 'MultiPolygon', 'coordinates': [m['coordinates'] for m in _members(el)]}
173 raise Error(f'unknown GML geometry tag {el.name!r}')
176def _members(multi_el: gws.XmlElement):
177 ms = []
179 for el in multi_el:
180 if el.name.lower().endswith('member'):
181 ms.append(_to_geom(el[0]))
183 return ms
186def _rings(poly_el):
187 rings = [None]
189 for el in poly_el:
190 if el.isa('exterior', 'outerBoundaryIs'):
191 d = _to_geom(el[0])
192 rings[0] = d['coordinates']
193 continue
195 if el.isa('interior', 'innerBoundaryIs'):
196 d = _to_geom(el[0])
197 rings.append(d['coordinates'])
198 continue
200 return rings
203def _coords(any_el):
204 for el in any_el:
205 if el.isa('coordinates'):
206 return _coords_coordinates(el)
207 if el.isa('pos'):
208 return _coords_pos(el)
209 if el.isa('posList'):
210 return _coords_poslist(el)
211 raise Error(f'expected coordinates list')
214def _coords_coordinates(el):
215 # <gml:coordinates>1,2 3,4...
217 ts = el.get('ts', default=' ')
218 cs = el.get('cs', default=',')
220 clist = []
222 for pair in el.text.split(ts):
223 x, y = pair.split(cs)
224 clist.append([float(x), float(y)])
226 return clist
229def _coords_pos(el):
230 # <gml:pos srsDimension="2">1 2</gml:pos>
232 s = el.text.split()
233 x = s[0]
234 y = s[1]
235 # NB pos returns a list of points too!
236 return [(float(x), float(y))]
239def _coords_poslist(el):
240 # <gml:posList srsDimension="2">1 2 3...
242 clist = []
243 dim = int(el.get('srsDimension', default='2'))
244 s = el.text.split()
246 for n in range(0, len(s), dim):
247 x = s[n]
248 y = s[n + 1]
249 clist.append([float(x), float(y)])
251 return clist