Coverage for gws-app/gws/lib/gml/parser.py: 87%
104 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
1"""GML geometry parsers."""
3import gws
4import gws.base.shape
5import gws.lib.bounds
6import gws.lib.crs
7import gws.lib.extent
10class Error(gws.Error):
11 pass
14_GEOMETRY_TAGS = {
15 'curve',
16 'linearring',
17 'linestring',
18 'linestringsegment',
19 'multicurve',
20 'multilinestring',
21 'multipoint',
22 'multipolygon',
23 'multisurface',
24 'point',
25 'polygon',
26}
29def parse_envelope(el: gws.XmlElement, default_crs: gws.Crs = None, always_xy: bool = False) -> gws.Bounds:
30 """Parse a gml:Box/gml:Envelope element
32 Args:
33 el: A xml-Element.
34 default_crs: A Crs object.
35 always_xy: If ``True``, coordinates are assumed to be in the XY (lon/lat) order.
37 Returns:
38 A Bounds object.
39 """
41 # GML2: <gml:Box><gml:coordinates>1,2 3,4
42 # GML3: <gml:Envelope srsDimension="2"><gml:lowerCorner>1 2 <gml:upperCorner>3 4
44 crs = gws.lib.crs.get(el.get('srsName')) or default_crs
45 if not crs:
46 raise Error('no CRS declared for envelope')
49 try:
50 coords = [None, None]
52 if el.lcName == 'box':
53 coords = _coords(el)
55 elif el.lcName == 'envelope':
56 for coord_el in el:
57 if coord_el.lcName == 'lowercorner':
58 coords[0] = _coords_pos(coord_el)[0]
59 if coord_el.lcName == 'uppercorner':
60 coords[1] = _coords_pos(coord_el)[0]
62 ext = gws.lib.extent.from_points(*coords)
64 except Exception as exc:
65 raise Error('envelope parse error') from exc
67 return gws.lib.bounds.from_extent(ext, crs, always_xy)
70def is_geometry_element(el: gws.XmlElement) -> bool:
71 """Checks if the current element is a valid geometry type.
73 Args:
74 el: A GML element.
76 Returns:
77 ``True`` if the element is a geometry type.
78 """
80 return el.lcName in _GEOMETRY_TAGS
83def parse_shape(el: gws.XmlElement, default_crs: gws.Crs = None, always_xy: bool = False) -> gws.Shape:
84 """Convert a GML geometry element to a Shape.
86 Args:
87 el: A GML element.
88 default_crs: A Crs object.
89 always_xy: If ``True``, coordinates are assumed to be in the XY (lon/lat) order.
91 Returns:
92 A GWS shape object.
93 """
95 crs = gws.lib.crs.get(el.get('srsName')) or default_crs
96 if not crs:
97 raise Error('no CRS declared')
99 dct = parse_geometry(el)
100 return gws.base.shape.from_geojson(dct, crs, always_xy)
103def parse_geometry(el: gws.XmlElement) -> dict:
104 """Convert a GML geometry element to a geometry dict.
106 Args:
107 el: A GML element.
109 Returns:
110 The GML geometry as a geometry dict.
111 """
113 try:
114 return _to_geom(el)
115 except Exception as exc:
116 raise Error('parse error') from exc
119##
121def _to_geom(el: gws.XmlElement):
122 if el.lcName == 'point':
123 # <gml:Point> pos/coordinates
124 return {'type': 'Point', 'coordinates': _coords(el)[0]}
126 if el.lcName in {'linestring', 'linearring', 'linestringsegment'}:
127 # <gml:LineString> posList/coordinates
128 return {'type': 'LineString', 'coordinates': _coords(el)}
130 if el.lcName == 'curve':
131 # GML3: <gml:Curve> <gml:segments> <gml:LineStringSegment>
132 # NB we only take the first segment
133 return _to_geom(el[0][0])
135 if el.lcName == 'polygon':
136 # GML2: <gml:Polygon> <gml:outerBoundaryIs> <gml:LinearRing> <gml:innerBoundaryIs> <gml:LinearRing>...
137 # GML3: <gml:Polygon> <gml:exterior> <gml:LinearRing> <gml:interior> <gml:LinearRing>...
138 return {'type': 'Polygon', 'coordinates': _rings(el)}
140 if el.lcName == 'multipoint':
141 # <gml:MultiPoint> <gml:pointMember> <gml:Point>
142 return {'type': 'MultiPoint', 'coordinates': [m['coordinates'] for m in _members(el)]}
144 if el.lcName in {'multilinestring', 'multicurve'}:
145 # GML2: <gml:MultiLineString> <gml:lineStringMember> <gml:LineString>
146 # GML3: <gml:MultiCurve> <gml:curveMember> <gml:Curve>
147 return {'type': 'MultiLineString', 'coordinates': [m['coordinates'] for m in _members(el)]}
149 if el.lcName in {'multipolygon', 'multisurface'}:
150 # GML2: <gml:MultiPolygon> <gml:polygonMember> <gml:Polygon>
151 # GML3: <gml:MultiSurface> <gml:surfaceMember> <gml:Polygon>
152 return {'type': 'MultiPolygon', 'coordinates': [m['coordinates'] for m in _members(el)]}
154 raise Error(f'unknown GML geometry tag {el.name!r}')
157def _members(multi_el: gws.XmlElement):
158 ms = []
160 for el in multi_el:
161 if el.lcName.endswith('member'):
162 ms.append(_to_geom(el[0]))
164 return ms
167def _rings(poly_el):
168 rings = [None]
170 for el in poly_el:
171 if el.lcName in {'exterior', 'outerboundaryis'}:
172 d = _to_geom(el[0])
173 rings[0] = d['coordinates']
174 continue
176 if el.lcName in {'interior', 'innerboundaryis'}:
177 d = _to_geom(el[0])
178 rings.append(d['coordinates'])
179 continue
181 return rings
184def _coords(any_el):
185 for el in any_el:
186 if el.lcName == 'coordinates':
187 return _coords_coordinates(el)
188 if el.lcName == 'pos':
189 return _coords_pos(el)
190 if el.lcName == 'poslist':
191 return _coords_poslist(el)
192 raise Error(f'expected coordinates list')
195def _coords_coordinates(el):
196 # <gml:coordinates>1,2 3,4...
198 ts = el.get('ts', default=' ')
199 cs = el.get('cs', default=',')
201 clist = []
203 for pair in el.text.split(ts):
204 x, y = pair.split(cs)
205 clist.append([float(x), float(y)])
207 return clist
210def _coords_pos(el):
211 # <gml:pos srsDimension="2">1 2</gml:pos>
213 s = el.text.split()
214 x = s[0]
215 y = s[1]
216 # NB pos returns a list of points too!
217 return [[float(x), float(y)]]
220def _coords_poslist(el):
221 # <gml:posList srsDimension="2">1 2 3...
223 clist = []
224 dim = int(el.get('srsDimension', default='2'))
225 s = el.text.split()
227 for n in range(0, len(s), dim):
228 x = s[n]
229 y = s[n + 1]
230 clist.append([float(x), float(y)])
232 return clist