Coverage for gws-app/gws/gis/gml/parser.py: 0%

104 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-17 01:37 +0200

1"""GML geometry parsers.""" 

2 

3import gws 

4import gws.base.shape 

5import gws.gis.bounds 

6import gws.gis.crs 

7import gws.gis.extent 

8 

9 

10class Error(gws.Error): 

11 pass 

12 

13 

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} 

27 

28 

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 

31 

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. 

36 

37 Returns: 

38 A Bounds object. 

39 """ 

40 

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

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

43 

44 crs = gws.gis.crs.get(el.get('srsName')) or default_crs 

45 if not crs: 

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

47 

48 coords = None 

49 

50 try: 

51 if el.lcName == 'box': 

52 coords = _coords(el) 

53 

54 elif el.lcName == 'envelope': 

55 coords = [None, None] 

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] 

61 

62 ext = gws.gis.extent.from_points(*coords) 

63 

64 except Exception as exc: 

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

66 

67 return gws.gis.bounds.from_extent(ext, crs, always_xy) 

68 

69 

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

71 """Checks if the current element is a valid geometry type. 

72 

73 Args: 

74 el: A GML element. 

75 

76 Returns: 

77 ``True`` if the element is a geometry type. 

78 """ 

79 

80 return el.lcName in _GEOMETRY_TAGS 

81 

82 

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. 

85 

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. 

90 

91 Returns: 

92 A GWS shape object. 

93 """ 

94 

95 crs = gws.gis.crs.get(el.get('srsName')) or default_crs 

96 if not crs: 

97 raise Error('no CRS declared') 

98 

99 dct = parse_geometry(el) 

100 return gws.base.shape.from_geojson(dct, crs, always_xy) 

101 

102 

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

104 """Convert a GML geometry element to a geometry dict. 

105 

106 Args: 

107 el: A GML element. 

108 

109 Returns: 

110 The GML geometry as a geometry dict. 

111 """ 

112 

113 try: 

114 return _to_geom(el) 

115 except Exception as exc: 

116 raise Error('parse error') from exc 

117 

118 

119## 

120 

121def _to_geom(el: gws.XmlElement): 

122 if el.lcName == 'point': 

123 # <gml:Point> pos/coordinates 

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

125 

126 if el.lcName in {'linestring', 'linearring', 'linestringsegment'}: 

127 # <gml:LineString> posList/coordinates 

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

129 

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

134 

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

139 

140 if el.lcName == 'multipoint': 

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

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

143 

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

148 

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

153 

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

155 

156 

157def _members(multi_el: gws.XmlElement): 

158 ms = [] 

159 

160 for el in multi_el: 

161 if el.lcName.endswith('member'): 

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

163 

164 return ms 

165 

166 

167def _rings(poly_el): 

168 rings = [None] 

169 

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 

175 

176 if el.lcName in {'interior', 'innerboundaryis'}: 

177 d = _to_geom(el[0]) 

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

179 continue 

180 

181 return rings 

182 

183 

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 

193 

194def _coords_coordinates(el): 

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

196 

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

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

199 

200 clist = [] 

201 

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

203 x, y = pair.split(cs) 

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

205 

206 return clist 

207 

208 

209def _coords_pos(el): 

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

211 

212 s = el.text.split() 

213 x = s[0] 

214 y = s[1] 

215 # NB pos returns a list of points too! 

216 return [[float(x), float(y)]] 

217 

218 

219def _coords_poslist(el): 

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

221 

222 clist = [] 

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

224 s = el.text.split() 

225 

226 for n in range(0, len(s), dim): 

227 x = s[n] 

228 y = s[n + 1] 

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

230 

231 return clist