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

100 statements  

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

1"""GML geometry writer.""" 

2 

3from typing import Optional 

4 

5import shapely.geometry 

6 

7import gws 

8import gws.gis.crs 

9import gws.gis.extent 

10import gws.base.feature 

11import gws.base.shape 

12import gws.lib.xmlx as xmlx 

13import gws.lib.uom 

14 

15 

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

17 

18def shape_to_element( 

19 shape: gws.Shape, 

20 version: int = 3, 

21 coordinate_precision: Optional[int] = None, 

22 always_xy: bool = False, 

23 with_xmlns: bool = True, 

24 with_inline_xmlns: bool = False, 

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

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

27) -> gws.XmlElement: 

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

29 

30 Args: 

31 shape: A Shape object. 

32 version: GML version (2 or 3). 

33 coordinate_precision: The amount of decimal places. 

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

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

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

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

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

39 

40 Returns: 

41 A GML element. 

42 """ 

43 

44 opts = gws.Data() 

45 opts.version = int(version) 

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

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

48 

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

50 crs = shape.crs.to_string(crs_format) 

51 

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

53 

54 opts.precision = coordinate_precision 

55 if opts.precision is None: 

56 opts.precision = gws.lib.uom.DEFAULT_PRECISION[shape.crs.uom] 

57 

58 opts.ns = '' 

59 ns = None 

60 

61 if with_xmlns: 

62 ns = namespace or xmlx.namespace.get('gml2' if opts.version == 2 else 'gml3') 

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

64 

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

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

67 

68 # OGC 07-036r1 10.1.4.1 

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

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

71 

72 el = xmlx.tag(*fn(geom, opts, {'srsName': crs})) 

73 if ns and with_inline_xmlns: 

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

75 

76 return el 

77 

78 

79def _point2(geom, opts, crs): 

80 return f'{opts.ns}Point', crs, _coordinates(geom, opts) 

81 

82 

83def _point3(geom, opts, crs): 

84 return f'{opts.ns}Point', crs, _pos(geom, opts) 

85 

86 

87def _linestring2(geom, opts, crs): 

88 return f'{opts.ns}LineString', crs, _coordinates(geom, opts) 

89 

90 

91def _linestring3(geom, opts, crs): 

92 return [ 

93 f'{opts.ns}Curve', crs, 

94 [ 

95 f'{opts.ns}segments', 

96 [ 

97 f'{opts.ns}LineStringSegment', _pos_list(geom, opts) 

98 ] 

99 ] 

100 ] 

101 

102 

103def _polygon2(geom, opts, crs): 

104 return [ 

105 f'{opts.ns}Polygon', crs, 

106 [ 

107 f'{opts.ns}outerBoundaryIs', 

108 [ 

109 f'{opts.ns}LinearRing', _coordinates(geom.exterior, opts) 

110 ] 

111 ], 

112 [ 

113 [ 

114 f'{opts.ns}innerBoundaryIs', 

115 [ 

116 f'{opts.ns}LinearRing', _coordinates(interior, opts) 

117 ] 

118 ] 

119 for interior in geom.interiors 

120 ] 

121 ] 

122 

123 

124def _polygon3(geom, opts, crs): 

125 return [ 

126 f'{opts.ns}Polygon', crs, 

127 [ 

128 f'{opts.ns}exterior', 

129 [ 

130 f'{opts.ns}LinearRing', _pos_list(geom.exterior, opts) 

131 ] 

132 ], 

133 [ 

134 [ 

135 f'{opts.ns}interior', 

136 [ 

137 f'{opts.ns}LinearRing', _pos_list(interior, opts) 

138 ] 

139 ] 

140 for interior in geom.interiors 

141 ] 

142 ] 

143 

144 

145def _multipoint2(geom, opts, crs): 

146 return f'{opts.ns}MultiPoint', crs, [[f'{opts.ns}pointMember', _tag2(p, opts)] for p in geom.geoms] 

147 

148 

149def _multipoint3(geom, opts, crs): 

150 return f'{opts.ns}MultiPoint', crs, [[f'{opts.ns}pointMember', _tag3(p, opts)] for p in geom.geoms] 

151 

152 

153def _multilinestring2(geom, opts, crs): 

154 return f'{opts.ns}MultiLineString', crs, [[f'{opts.ns}lineStringMember', _tag2(p, opts)] for p in geom.geoms] 

155 

156 

157def _multilinestring3(geom, opts, crs): 

158 return f'{opts.ns}MultiCurve', crs, [[f'{opts.ns}curveMember', _tag3(p, opts)] for p in geom.geoms] 

159 

160 

161def _multipolygon2(geom, opts, crs): 

162 return f'{opts.ns}MultiPolygon', crs, [[f'{opts.ns}polygonMember', _tag2(p, opts)] for p in geom.geoms] 

163 

164 

165def _multipolygon3(geom, opts, crs): 

166 return f'{opts.ns}MultiSurface', crs, [[f'{opts.ns}surfaceMember', _tag3(p, opts)] for p in geom.geoms] 

167 

168 

169def _geometrycollection2(geom, opts, crs): 

170 return f'{opts.ns}MultiGeometry', crs, [[f'{opts.ns}geometryMember', _tag2(p, opts)] for p in geom.geoms] 

171 

172 

173def _geometrycollection3(geom, opts, crs): 

174 return f'{opts.ns}MultiGeometry', crs, [[f'{opts.ns}geometryMember', _tag3(p, opts)] for p in geom.geoms] 

175 

176 

177def _pos(geom, opts): 

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

179 

180 

181def _pos_list(geom, opts): 

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

183 

184 

185def _pos_list_content(geom, opts): 

186 cs = [] 

187 

188 for x, y in geom.coords: 

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

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

191 if opts.swapxy: 

192 x, y = y, x 

193 cs.append(str(x)) 

194 cs.append(str(y)) 

195 

196 return ' '.join(cs) 

197 

198 

199def _coordinates(geom, opts): 

200 cs = [] 

201 

202 for x, y in geom.coords: 

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

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

205 if opts.swapxy: 

206 x, y = y, x 

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

208 

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

210 

211 

212_FNS_2 = { 

213 'Point': _point2, 

214 'LineString': _linestring2, 

215 'Polygon': _polygon2, 

216 'MultiPoint': _multipoint2, 

217 'MultiLineString': _multilinestring2, 

218 'MultiPolygon': _multipolygon2, 

219 'GeometryCollection': _geometrycollection2, 

220} 

221 

222_FNS_3 = { 

223 'Point': _point3, 

224 'LineString': _linestring3, 

225 'Polygon': _polygon3, 

226 'MultiPoint': _multipoint3, 

227 'MultiLineString': _multilinestring3, 

228 'MultiPolygon': _multipolygon3, 

229 'GeometryCollection': _geometrycollection3, 

230} 

231 

232 

233def _tag2(geom, opts, crs=None): 

234 typ = geom.geom_type 

235 fn = _FNS_2.get(typ) 

236 if fn: 

237 return fn(geom, opts, crs) 

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

239 

240 

241def _tag3(geom, opts, crs=None): 

242 typ = geom.geom_type 

243 fn = _FNS_3.get(typ) 

244 if fn: 

245 return fn(geom, opts, crs) 

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

247 

248 

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

250 el.set(key, val) 

251 for c in el: 

252 _att_attr(c, key, val)