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
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
1"""GML geometry writer."""
3from typing import Optional
5import shapely.geometry
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
16# @TODO PostGis options 2 and 4 (https://postgis.net/docs/ST_AsGML.html)
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.
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).
40 Returns:
41 A GML element.
42 """
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}')
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)
52 opts.swapxy = (shape.crs.axis_for_format(crs_format) == gws.Axis.yx) and not always_xy
54 opts.precision = coordinate_precision
55 if opts.precision is None:
56 opts.precision = gws.lib.uom.DEFAULT_PRECISION[shape.crs.uom]
58 opts.ns = ''
59 ns = None
61 if with_xmlns:
62 ns = namespace or xmlx.namespace.get('gml2' if opts.version == 2 else 'gml3')
63 opts.ns = ns.xmlns + ':'
65 geom: shapely.geometry.base.BaseGeometry = getattr(shape, 'geom')
66 fn = _tag2 if opts.version == 2 else _tag3
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.
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)
76 return el
79def _point2(geom, opts, crs):
80 return f'{opts.ns}Point', crs, _coordinates(geom, opts)
83def _point3(geom, opts, crs):
84 return f'{opts.ns}Point', crs, _pos(geom, opts)
87def _linestring2(geom, opts, crs):
88 return f'{opts.ns}LineString', crs, _coordinates(geom, opts)
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 ]
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 ]
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 ]
145def _multipoint2(geom, opts, crs):
146 return f'{opts.ns}MultiPoint', crs, [[f'{opts.ns}pointMember', _tag2(p, opts)] for p in geom.geoms]
149def _multipoint3(geom, opts, crs):
150 return f'{opts.ns}MultiPoint', crs, [[f'{opts.ns}pointMember', _tag3(p, opts)] for p in geom.geoms]
153def _multilinestring2(geom, opts, crs):
154 return f'{opts.ns}MultiLineString', crs, [[f'{opts.ns}lineStringMember', _tag2(p, opts)] for p in geom.geoms]
157def _multilinestring3(geom, opts, crs):
158 return f'{opts.ns}MultiCurve', crs, [[f'{opts.ns}curveMember', _tag3(p, opts)] for p in geom.geoms]
161def _multipolygon2(geom, opts, crs):
162 return f'{opts.ns}MultiPolygon', crs, [[f'{opts.ns}polygonMember', _tag2(p, opts)] for p in geom.geoms]
165def _multipolygon3(geom, opts, crs):
166 return f'{opts.ns}MultiSurface', crs, [[f'{opts.ns}surfaceMember', _tag3(p, opts)] for p in geom.geoms]
169def _geometrycollection2(geom, opts, crs):
170 return f'{opts.ns}MultiGeometry', crs, [[f'{opts.ns}geometryMember', _tag2(p, opts)] for p in geom.geoms]
173def _geometrycollection3(geom, opts, crs):
174 return f'{opts.ns}MultiGeometry', crs, [[f'{opts.ns}geometryMember', _tag3(p, opts)] for p in geom.geoms]
177def _pos(geom, opts):
178 return f'{opts.ns}pos', {'srsDimension': 2}, _pos_list_content(geom, opts)
181def _pos_list(geom, opts):
182 return f'{opts.ns}posList', {'srsDimension': 2}, _pos_list_content(geom, opts)
185def _pos_list_content(geom, opts):
186 cs = []
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))
196 return ' '.join(cs)
199def _coordinates(geom, opts):
200 cs = []
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))
209 return f'{opts.ns}coordinates', {'decimal': '.', 'cs': ',', 'ts': ' '}, ' '.join(cs)
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}
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}
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')
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')
249def _att_attr(el: gws.XmlElement, key, val):
250 el.set(key, val)
251 for c in el:
252 _att_attr(c, key, val)