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

1"""GML geometry parsers.""" 

2 

3import gws 

4import gws.lib.shape 

5import gws.lib.bounds 

6import gws.lib.crs 

7import gws.lib.extent 

8 

9 

10class Error(gws.Error): 

11 """GML parse error.""" 

12 

13 pass 

14 

15 

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] 

29 

30 

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. 

33 

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. 

38 

39 Returns: 

40 A Bounds object. 

41 

42 Raises: 

43 ``Error``: If no CRS is given or the element cannot be parsed. 

44 """ 

45 

46 # GML2: <gml:Box><gml:coordinates>1,2 3,4 

47 # GML3: <gml:Envelope srsDimension="2"><gml:lowerCorner>1 2 <gml:upperCorner>3 4 

48 

49 crs = gws.lib.crs.get(el.get('srsName')) or default_crs 

50 if not crs: 

51 raise Error('no CRS declared for envelope') 

52 

53 try: 

54 ext = _parse_envelope_extent(el) 

55 except Exception as exc: 

56 raise Error('envelope parse error') from exc 

57 

58 return gws.lib.bounds.from_extent(ext, crs, always_xy) 

59 

60 

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) 

65 

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) 

75 

76 raise ValueError('invalid envelope element') 

77 

78 

79def is_geometry_element(el: gws.XmlElement) -> bool: 

80 """Check if an element is a supported GML geometry element. 

81 

82 Args: 

83 el: An XML element. 

84 

85 Returns: 

86 ``True`` if the element name is one of the supported geometry tags. 

87 """ 

88 

89 return el.isa(*_GEOMETRY_TAGS) 

90 

91 

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. 

94 

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. 

99 

100 Returns: 

101 A Shape object. 

102 

103 Raises: 

104 ``Error``: If no CRS is given or the element cannot be parsed. 

105 """ 

106 

107 crs = gws.lib.crs.get(el.get('srsName')) or default_crs 

108 if not crs: 

109 raise Error('no CRS declared') 

110 

111 dct = parse_geometry(el) 

112 return gws.lib.shape.from_geojson(dct, crs, always_xy) 

113 

114 

115def parse_geometry(el: gws.XmlElement) -> dict: 

116 """Convert a GML geometry element to a GeoJSON-like geometry dict. 

117 

118 A ``Curve`` is converted to a ``LineString`` from its first segment only. 

119 ``MultiCurve`` and ``MultiSurface`` become ``MultiLineString`` and ``MultiPolygon``. 

120 

121 Args: 

122 el: A GML geometry element. 

123 

124 Returns: 

125 A dict with ``type`` and ``coordinates``. 

126 

127 Raises: 

128 ``Error``: If the element cannot be parsed. 

129 """ 

130 

131 try: 

132 return _to_geom(el) 

133 except Exception as exc: 

134 raise Error('parse error') from exc 

135 

136 

137## 

138 

139 

140def _to_geom(el: gws.XmlElement): 

141 if el.isa('Point'): 

142 # <gml:Point> pos/coordinates 

143 return {'type': 'Point', 'coordinates': _coords(el)[0]} 

144 

145 if el.isa('LineString', 'LinearRing', 'LineStringSegment'): 

146 # <gml:LineString> posList/coordinates 

147 return {'type': 'LineString', 'coordinates': _coords(el)} 

148 

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

153 

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

158 

159 if el.isa('MultiPoint'): 

160 # <gml:MultiPoint> <gml:pointMember> <gml:Point> 

161 return {'type': 'MultiPoint', 'coordinates': [m['coordinates'] for m in _members(el)]} 

162 

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)]} 

167 

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)]} 

172 

173 raise Error(f'unknown GML geometry tag {el.name!r}') 

174 

175 

176def _members(multi_el: gws.XmlElement): 

177 ms = [] 

178 

179 for el in multi_el: 

180 if el.name.lower().endswith('member'): 

181 ms.append(_to_geom(el[0])) 

182 

183 return ms 

184 

185 

186def _rings(poly_el): 

187 rings = [None] 

188 

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 

194 

195 if el.isa('interior', 'innerBoundaryIs'): 

196 d = _to_geom(el[0]) 

197 rings.append(d['coordinates']) 

198 continue 

199 

200 return rings 

201 

202 

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

212 

213 

214def _coords_coordinates(el): 

215 # <gml:coordinates>1,2 3,4... 

216 

217 ts = el.get('ts', default=' ') 

218 cs = el.get('cs', default=',') 

219 

220 clist = [] 

221 

222 for pair in el.text.split(ts): 

223 x, y = pair.split(cs) 

224 clist.append([float(x), float(y)]) 

225 

226 return clist 

227 

228 

229def _coords_pos(el): 

230 # <gml:pos srsDimension="2">1 2</gml:pos> 

231 

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

237 

238 

239def _coords_poslist(el): 

240 # <gml:posList srsDimension="2">1 2 3... 

241 

242 clist = [] 

243 dim = int(el.get('srsDimension', default='2')) 

244 s = el.text.split() 

245 

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

250 

251 return clist