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 22:59 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 22:59 +0200
1"""GML geometry writer."""
3from typing import Optional
5import shapely.geometry
7import gws
8import gws.lib.uom
9import gws.lib.xmlx as xmlx
11# @TODO PostGis options 2 and 4 (https://postgis.net/docs/ST_AsGML.html)
14class _Options(gws.Data):
15 version: int
16 precision: float
17 swapxy: bool
18 xmlns: str
19 crsName: dict
22DEFAULT_VERSION = 3
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.
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).
47 Returns:
48 A GML element.
49 """
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}')
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)}
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]
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 + ':'
68 geom: shapely.geometry.base.BaseGeometry = getattr(shape, 'geom')
69 fn = _tag2 if opts.version == 2 else _tag3
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.
75 el = xmlx.tag(*fn(geom, opts))
76 if ns and with_inline_xmlns:
77 _att_attr(el, 'xmlns:' + ns.xmlns, ns.uri)
79 return el
82def _point2(geom, opts):
83 return f'{opts.xmlns}Point', opts.crsName, _coordinates(geom, opts)
86def _point3(geom, opts):
87 return f'{opts.xmlns}Point', opts.crsName, _pos(geom, opts)
90def _linestring2(geom, opts):
91 return f'{opts.xmlns}LineString', opts.crsName, _coordinates(geom, opts)
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 ]
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 ]
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 ]
156def _multipoint2(geom, opts):
157 return f'{opts.xmlns}MultiPoint', opts.crsName, [[f'{opts.xmlns}pointMember', _tag2(p, opts)] for p in geom.geoms]
160def _multipoint3(geom, opts):
161 return f'{opts.xmlns}MultiPoint', opts.crsName, [[f'{opts.xmlns}pointMember', _tag3(p, opts)] for p in geom.geoms]
164def _multilinestring2(geom, opts):
165 return f'{opts.xmlns}MultiLineString', opts.crsName, [[f'{opts.xmlns}lineStringMember', _tag2(p, opts)] for p in geom.geoms]
168def _multilinestring3(geom, opts):
169 return f'{opts.xmlns}MultiCurve', opts.crsName, [[f'{opts.xmlns}curveMember', _tag3(p, opts)] for p in geom.geoms]
172def _multipolygon2(geom, opts):
173 return f'{opts.xmlns}MultiPolygon', opts.crsName, [[f'{opts.xmlns}polygonMember', _tag2(p, opts)] for p in geom.geoms]
176def _multipolygon3(geom, opts):
177 return f'{opts.xmlns}MultiSurface', opts.crsName, [[f'{opts.xmlns}surfaceMember', _tag3(p, opts)] for p in geom.geoms]
180def _geometrycollection2(geom, opts):
181 return f'{opts.xmlns}MultiGeometry', opts.crsName, [[f'{opts.xmlns}geometryMember', _tag2(p, opts)] for p in geom.geoms]
184def _geometrycollection3(geom, opts):
185 return f'{opts.xmlns}MultiGeometry', opts.crsName, [[f'{opts.xmlns}geometryMember', _tag3(p, opts)] for p in geom.geoms]
188def _pos(geom, opts):
189 return f'{opts.xmlns}pos', {'srsDimension': 2}, _pos_list_content(geom, opts)
192def _pos_list(geom, opts):
193 return f'{opts.xmlns}posList', {'srsDimension': 2}, _pos_list_content(geom, opts)
196def _pos_list_content(geom, opts):
197 cs = []
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))
207 return ' '.join(cs)
210def _coordinates(geom, opts):
211 cs = []
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))
220 return f'{opts.xmlns}coordinates', {'decimal': '.', 'cs': ',', 'ts': ' '}, ' '.join(cs)
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}
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}
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')
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')
260def _att_attr(el: gws.XmlElement, key, val):
261 el.set(key, val)
262 for c in el:
263 _att_attr(c, key, val)