Coverage for gws-app/gws/lib/gml/writer.py: 96%

101 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-16 23:09 +0200

1"""GML geometry writer.""" 

2 

3from typing import Optional 

4 

5import shapely.geometry 

6 

7import gws 

8import gws.lib.uom 

9import gws.lib.xmlx as xmlx 

10 

11# @TODO PostGis options 2 and 4 (https://postgis.net/docs/ST_AsGML.html) 

12 

13 

14class _Options(gws.Data): 

15 version: int 

16 precision: float 

17 swapxy: bool 

18 xmlns: str 

19 crsName: dict 

20 

21 

22DEFAULT_VERSION = 3 

23 

24 

25def shape_to_element( 

26 shape: gws.Shape, 

27 version: int = DEFAULT_VERSION, 

28 coordinate_precision: Optional[int] = None, 

29 always_xy: bool = False, 

30 with_xmlns: bool = True, 

31 with_inline_xmlns: bool = False, 

32 namespace: Optional[gws.XmlNamespace] = None, 

33 crs_format: Optional[gws.CrsFormat] = None, 

34) -> gws.XmlElement: 

35 """Convert a Shape to a GML geometry element. 

36 

37 Args: 

38 shape: A Shape object. 

39 version: GML version (2 or 3). 

40 coordinate_precision: The amount of decimal places. 

41 always_xy: If ``True``, coordinates are assumed to be always in the XY (lon/lat) order. 

42 with_xmlns: If ``True`` add the "gml" namespace prefix. 

43 with_inline_xmlns: If ``True`` add inline "xmlns" attributes. 

44 namespace: Use this namespace (default "gml"). 

45 crs_format: Crs format to use (default "url" for version 2 and "urn" for version 3). 

46 

47 Returns: 

48 A GML element. 

49 """ 

50 

51 opts = _Options() 

52 opts.version = int(version or DEFAULT_VERSION) 

53 if opts.version not in {2, 3}: 

54 raise gws.Error(f'unsupported GML version {version!r}') 

55 

56 crs_format = crs_format or (gws.CrsFormat.url if opts.version == 2 else gws.CrsFormat.urn) 

57 opts.crsName = {'srsName': shape.crs.to_string(crs_format)} 

58 

59 opts.swapxy = (shape.crs.axis_for_format(crs_format) == gws.Axis.yx) and not always_xy 

60 opts.precision = coordinate_precision if coordinate_precision is not None else gws.lib.uom.DEFAULT_PRECISION[shape.crs.uom] 

61 

62 opts.xmlns = '' 

63 ns = None 

64 if with_xmlns: 

65 ns = namespace or xmlx.namespace.require('gml2' if opts.version == 2 else 'gml') 

66 opts.xmlns = ns.xmlns + ':' 

67 

68 geom: shapely.geometry.base.BaseGeometry = getattr(shape, 'geom') 

69 fn = _tag2 if opts.version == 2 else _tag3 

70 

71 # OGC 07-036r1 10.1.4.1 

72 # If no srsName attribute is given, the CRS shall be specified as part of the larger context this geometry element is part of... 

73 # NOTE It is expected that the attribute will be specified at the direct position level only in rare cases. 

74 

75 el = xmlx.tag(*fn(geom, opts)) 

76 if ns and with_inline_xmlns: 

77 _att_attr(el, 'xmlns:' + ns.xmlns, ns.uri) 

78 

79 return el 

80 

81 

82def _point2(geom, opts): 

83 return f'{opts.xmlns}Point', opts.crsName, _coordinates(geom, opts) 

84 

85 

86def _point3(geom, opts): 

87 return f'{opts.xmlns}Point', opts.crsName, _pos(geom, opts) 

88 

89 

90def _linestring2(geom, opts): 

91 return f'{opts.xmlns}LineString', opts.crsName, _coordinates(geom, opts) 

92 

93 

94def _linestring3(geom, opts): 

95 return [ 

96 f'{opts.xmlns}Curve', 

97 opts.crsName, 

98 [ 

99 f'{opts.xmlns}segments', 

100 [ 

101 f'{opts.xmlns}LineStringSegment', 

102 _pos_list(geom, opts), 

103 ], 

104 ], 

105 ] 

106 

107 

108def _polygon2(geom, opts): 

109 return [ 

110 f'{opts.xmlns}Polygon', 

111 opts.crsName, 

112 [ 

113 f'{opts.xmlns}outerBoundaryIs', 

114 [ 

115 f'{opts.xmlns}LinearRing', 

116 _coordinates(geom.exterior, opts), 

117 ], 

118 ], 

119 [ 

120 [ 

121 f'{opts.xmlns}innerBoundaryIs', 

122 [ 

123 f'{opts.xmlns}LinearRing', 

124 _coordinates(interior, opts), 

125 ], 

126 ] 

127 for interior in geom.interiors 

128 ], 

129 ] 

130 

131 

132def _polygon3(geom, opts): 

133 return [ 

134 f'{opts.xmlns}Polygon', 

135 opts.crsName, 

136 [ 

137 f'{opts.xmlns}exterior', 

138 [ 

139 f'{opts.xmlns}LinearRing', 

140 _pos_list(geom.exterior, opts), 

141 ], 

142 ], 

143 [ 

144 [ 

145 f'{opts.xmlns}interior', 

146 [ 

147 f'{opts.xmlns}LinearRing', 

148 _pos_list(interior, opts), 

149 ], 

150 ] 

151 for interior in geom.interiors 

152 ], 

153 ] 

154 

155 

156def _multipoint2(geom, opts): 

157 return f'{opts.xmlns}MultiPoint', opts.crsName, [[f'{opts.xmlns}pointMember', _tag2(p, opts)] for p in geom.geoms] 

158 

159 

160def _multipoint3(geom, opts): 

161 return f'{opts.xmlns}MultiPoint', opts.crsName, [[f'{opts.xmlns}pointMember', _tag3(p, opts)] for p in geom.geoms] 

162 

163 

164def _multilinestring2(geom, opts): 

165 return f'{opts.xmlns}MultiLineString', opts.crsName, [[f'{opts.xmlns}lineStringMember', _tag2(p, opts)] for p in geom.geoms] 

166 

167 

168def _multilinestring3(geom, opts): 

169 return f'{opts.xmlns}MultiCurve', opts.crsName, [[f'{opts.xmlns}curveMember', _tag3(p, opts)] for p in geom.geoms] 

170 

171 

172def _multipolygon2(geom, opts): 

173 return f'{opts.xmlns}MultiPolygon', opts.crsName, [[f'{opts.xmlns}polygonMember', _tag2(p, opts)] for p in geom.geoms] 

174 

175 

176def _multipolygon3(geom, opts): 

177 return f'{opts.xmlns}MultiSurface', opts.crsName, [[f'{opts.xmlns}surfaceMember', _tag3(p, opts)] for p in geom.geoms] 

178 

179 

180def _geometrycollection2(geom, opts): 

181 return f'{opts.xmlns}MultiGeometry', opts.crsName, [[f'{opts.xmlns}geometryMember', _tag2(p, opts)] for p in geom.geoms] 

182 

183 

184def _geometrycollection3(geom, opts): 

185 return f'{opts.xmlns}MultiGeometry', opts.crsName, [[f'{opts.xmlns}geometryMember', _tag3(p, opts)] for p in geom.geoms] 

186 

187 

188def _pos(geom, opts): 

189 return f'{opts.xmlns}pos', {'srsDimension': 2}, _pos_list_content(geom, opts) 

190 

191 

192def _pos_list(geom, opts): 

193 return f'{opts.xmlns}posList', {'srsDimension': 2}, _pos_list_content(geom, opts) 

194 

195 

196def _pos_list_content(geom, opts): 

197 cs = [] 

198 

199 for x, y in geom.coords: 

200 x = int(x) if opts.precision == 0 else round(x, opts.precision) 

201 y = int(y) if opts.precision == 0 else round(y, opts.precision) 

202 if opts.swapxy: 

203 x, y = y, x 

204 cs.append(str(x)) 

205 cs.append(str(y)) 

206 

207 return ' '.join(cs) 

208 

209 

210def _coordinates(geom, opts): 

211 cs = [] 

212 

213 for x, y in geom.coords: 

214 x = int(x) if opts.precision == 0 else round(x, opts.precision) 

215 y = int(y) if opts.precision == 0 else round(y, opts.precision) 

216 if opts.swapxy: 

217 x, y = y, x 

218 cs.append(str(x) + ',' + str(y)) 

219 

220 return f'{opts.xmlns}coordinates', {'decimal': '.', 'cs': ',', 'ts': ' '}, ' '.join(cs) 

221 

222 

223_FNS_2 = { 

224 'Point': _point2, 

225 'LineString': _linestring2, 

226 'Polygon': _polygon2, 

227 'MultiPoint': _multipoint2, 

228 'MultiLineString': _multilinestring2, 

229 'MultiPolygon': _multipolygon2, 

230 'GeometryCollection': _geometrycollection2, 

231} 

232 

233_FNS_3 = { 

234 'Point': _point3, 

235 'LineString': _linestring3, 

236 'Polygon': _polygon3, 

237 'MultiPoint': _multipoint3, 

238 'MultiLineString': _multilinestring3, 

239 'MultiPolygon': _multipolygon3, 

240 'GeometryCollection': _geometrycollection3, 

241} 

242 

243 

244def _tag2(geom, opts): 

245 typ = geom.geom_type 

246 fn = _FNS_2.get(typ) 

247 if fn: 

248 return fn(geom, opts) 

249 raise gws.Error(f'cannot convert geometry type {typ!r} to GML') 

250 

251 

252def _tag3(geom, opts): 

253 typ = geom.geom_type 

254 fn = _FNS_3.get(typ) 

255 if fn: 

256 return fn(geom, opts) 

257 raise gws.Error(f'cannot convert geometry type {typ!r} to GML') 

258 

259 

260def _att_attr(el: gws.XmlElement, key, val): 

261 el.set(key, val) 

262 for c in el: 

263 _att_attr(c, key, val)