Coverage for gws-app/gws/lib/gml/writer.py: 98%
102 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""GML geometry writer."""
3from typing import Optional
5import shapely.geometry
7import gws
8import gws.lib.uom
9import gws.lib.xmlx as xmlx
10from gws.lib.xmlx import tag
12# @TODO PostGis options 2 and 4 (https://postgis.net/docs/ST_AsGML.html)
15DEFAULT_VERSION = 3
18def shape_to_element(
19 shape: gws.Shape,
20 version: int = DEFAULT_VERSION,
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 In GML 3, line strings are written as ``Curve`` elements and multi line strings
31 and multi polygons as ``MultiCurve`` and ``MultiSurface``.
33 Args:
34 shape: A Shape object.
35 version: GML version (2 or 3).
36 coordinate_precision: Number of decimal places. The default depends on the units of the shape CRS.
37 always_xy: If ``True``, coordinates are always written in the XY (lon/lat) order,
38 otherwise in the axis order of the CRS.
39 with_xmlns: If ``True``, put the elements in the GML namespace.
40 with_inline_xmlns: If ``True``, declare the namespace on the geometry element.
41 namespace: Namespace to use (default ``GML_2`` for version 2 and ``GML``, that is GML 3.2, for version 3).
42 crs_format: CRS format for ``srsName`` (default ``url`` for version 2 and ``urn`` for version 3).
44 Returns:
45 A GML element.
47 Raises:
48 ``gws.Error``: If the version or the geometry type is not supported.
49 """
51 v = int(version or DEFAULT_VERSION)
52 if v == 2:
53 cls = _Writer2
54 elif v == 3:
55 cls = _Writer3
56 else:
57 raise gws.Error(f'unsupported GML version {version!r}')
59 wr = cls(shape, coordinate_precision, always_xy, with_xmlns, namespace, crs_format)
61 geom: shapely.geometry.base.BaseGeometry = getattr(shape, 'geom')
63 # OGC 07-036r1 10.1.4.1
64 # If no srsName attribute is given, the CRS shall be specified as part of the larger context this geometry element is part of...
65 # NOTE It is expected that the attribute will be specified at the direct position level only in rare cases.
67 el = wr.element(geom)
68 if wr.ns and with_inline_xmlns:
69 el.declare(wr.ns)
71 return el
74_METHODS = {
75 'Point': 'point',
76 'LineString': 'linestring',
77 'Polygon': 'polygon',
78 'MultiPoint': 'multipoint',
79 'MultiLineString': 'multilinestring',
80 'MultiPolygon': 'multipolygon',
81 'GeometryCollection': 'geometrycollection',
82}
85class _Writer:
86 """Base GML writer, holds the output options and the version-independent geometry types."""
88 version: int
89 """GML version."""
90 defaultCrsFormat: gws.CrsFormat
91 """CRS format used when none is given."""
92 defaultNamespace: gws.XmlNamespace
93 """Namespace used when none is given."""
95 precision: int
96 """Number of decimal places for coordinates."""
97 swap_xy: bool
98 """If ``True``, coordinates are written in the YX order."""
99 crsName: dict
100 """The ``srsName`` attribute for geometry elements."""
101 pfx: str
102 """Not used."""
103 ns: Optional[gws.XmlNamespace]
104 """Namespace of the elements, or ``None`` for no namespace."""
106 def __init__(self, shape, coordinate_precision, always_xy, with_xmlns, namespace, crs_format):
107 """Create a writer.
109 Args:
110 shape: The shape to write, provides the CRS.
111 coordinate_precision: Number of decimal places, or ``None`` for the default of the CRS units.
112 always_xy: If ``True``, write coordinates in the XY order.
113 with_xmlns: If ``True``, put the elements in a namespace.
114 namespace: Namespace to use, or ``None`` for the default.
115 crs_format: CRS format to use, or ``None`` for the default.
116 """
118 crs_format = crs_format or self.defaultCrsFormat
119 self.crsName = {'srsName': shape.crs.to_string(crs_format)}
121 self.swap_xy = (shape.crs.axis_for_format(crs_format) == gws.Axis.yx) and not always_xy
123 self.precision = gws.lib.uom.DEFAULT_PRECISION[shape.crs.uom]
124 if coordinate_precision is not None:
125 self.precision = coordinate_precision
127 self.ns = None
128 self.nsu = ''
129 if with_xmlns:
130 self.ns = namespace or self.defaultNamespace
131 self.nsu = '{' + self.ns.uri + '}'
133 def t(self, name, *args):
134 """Create an element in the writer namespace.
136 Args:
137 name: Element name without a namespace.
138 *args: Attributes, text and child elements, as accepted by ``gws.lib.xmlx.tag``.
140 Returns:
141 An XML element.
142 """
144 return tag(self.nsu + name, *args)
146 def element(self, geom):
147 """Convert a geometry to a GML element.
149 Args:
150 geom: A shapely geometry.
152 Returns:
153 A GML element.
155 Raises:
156 ``gws.Error``: If the geometry type is not supported.
157 """
159 typ = geom.geom_type
160 name = _METHODS.get(typ)
161 if name:
162 return getattr(self, name)(geom)
163 raise gws.Error(f'cannot convert geometry type {typ!r} to GML')
165 def multipoint(self, geom):
166 """Create a ``MultiPoint`` element.
168 Args:
169 geom: A shapely geometry.
171 Returns:
172 A GML element.
173 """
175 return self.t(
176 'MultiPoint',
177 self.crsName,
178 [self.t('pointMember', self.element(p)) for p in geom.geoms],
179 )
181 def geometrycollection(self, geom):
182 """Create a ``MultiGeometry`` element.
184 Args:
185 geom: A shapely geometry.
187 Returns:
188 A GML element.
189 """
191 return self.t(
192 'MultiGeometry',
193 self.crsName,
194 [self.t('geometryMember', self.element(p)) for p in geom.geoms],
195 )
197 def round_coords(self, geom):
198 """Round the coordinates of a geometry and swap them if needed.
200 Args:
201 geom: A shapely geometry with a ``coords`` sequence.
203 Yields:
204 Coordinate pairs in the output axis order.
205 """
207 for x, y in geom.coords:
208 x = int(x) if self.precision == 0 else round(x, self.precision)
209 y = int(y) if self.precision == 0 else round(y, self.precision)
210 if self.swap_xy:
211 x, y = y, x
212 yield x, y
215class _Writer2(_Writer):
216 """GML 2 writer."""
218 version = 2
219 defaultCrsFormat = gws.CrsFormat.url
220 defaultNamespace = xmlx.namespace.c.GML_2
222 def point(self, geom):
223 """Create a ``Point`` element.
225 Args:
226 geom: A shapely geometry.
228 Returns:
229 A GML element.
230 """
232 return self.t('Point', self.crsName, self.coordinates(geom))
234 def linestring(self, geom):
235 """Create a ``LineString`` element.
237 Args:
238 geom: A shapely geometry.
240 Returns:
241 A GML element.
242 """
244 return self.t('LineString', self.crsName, self.coordinates(geom))
246 def polygon(self, geom):
247 """Create a ``Polygon`` element with ``outerBoundaryIs`` and ``innerBoundaryIs`` rings.
249 Args:
250 geom: A shapely geometry.
252 Returns:
253 A GML element.
254 """
256 return self.t(
257 'Polygon',
258 self.crsName,
259 self.t('outerBoundaryIs', self.t('LinearRing', self.coordinates(geom.exterior))),
260 [self.t('innerBoundaryIs', self.t('LinearRing', self.coordinates(interior))) for interior in geom.interiors],
261 )
263 def multilinestring(self, geom):
264 """Create a ``MultiLineString`` element.
266 Args:
267 geom: A shapely geometry.
269 Returns:
270 A GML element.
271 """
273 return self.t(
274 'MultiLineString',
275 self.crsName,
276 [self.t('lineStringMember', self.element(p)) for p in geom.geoms],
277 )
279 def multipolygon(self, geom):
280 """Create a ``MultiPolygon`` element.
282 Args:
283 geom: A shapely geometry.
285 Returns:
286 A GML element.
287 """
289 return self.t(
290 'MultiPolygon',
291 self.crsName,
292 [self.t('polygonMember', self.element(p)) for p in geom.geoms],
293 )
295 def coordinates(self, geom):
296 """Create a ``coordinates`` element.
298 Args:
299 geom: A shapely geometry.
301 Returns:
302 A GML element.
303 """
305 cs = [str(x) + ',' + str(y) for x, y in self.round_coords(geom)]
306 return self.t('coordinates', {'decimal': '.', 'cs': ',', 'ts': ' '}, ' '.join(cs))
309class _Writer3(_Writer):
310 """GML 3 writer."""
312 version = 3
313 defaultCrsFormat = gws.CrsFormat.urn
314 defaultNamespace = xmlx.namespace.c.GML
316 def point(self, geom):
317 """Create a ``Point`` element.
319 Args:
320 geom: A shapely geometry.
322 Returns:
323 A GML element.
324 """
326 return self.t('Point', self.crsName, self.pos(geom))
328 def linestring(self, geom):
329 """Create a ``Curve`` element with a single ``LineStringSegment``.
331 Args:
332 geom: A shapely geometry.
334 Returns:
335 A GML element.
336 """
338 return self.t(
339 'Curve',
340 self.crsName,
341 self.t('segments', self.t('LineStringSegment', self.pos_list(geom))),
342 )
344 def polygon(self, geom):
345 """Create a ``Polygon`` element with ``exterior`` and ``interior`` rings.
347 Args:
348 geom: A shapely geometry.
350 Returns:
351 A GML element.
352 """
354 return self.t(
355 'Polygon',
356 self.crsName,
357 self.t('exterior', self.t('LinearRing', self.pos_list(geom.exterior))),
358 [self.t('interior', self.t('LinearRing', self.pos_list(interior))) for interior in geom.interiors],
359 )
361 def multilinestring(self, geom):
362 """Create a ``MultiCurve`` element.
364 Args:
365 geom: A shapely geometry.
367 Returns:
368 A GML element.
369 """
371 return self.t(
372 'MultiCurve',
373 self.crsName,
374 [self.t('curveMember', self.element(p)) for p in geom.geoms],
375 )
377 def multipolygon(self, geom):
378 """Create a ``MultiSurface`` element.
380 Args:
381 geom: A shapely geometry.
383 Returns:
384 A GML element.
385 """
387 return self.t(
388 'MultiSurface',
389 self.crsName,
390 [self.t('surfaceMember', self.element(p)) for p in geom.geoms],
391 )
393 def pos(self, geom):
394 """Create a ``pos`` element.
396 Args:
397 geom: A shapely geometry.
399 Returns:
400 A GML element.
401 """
403 return self.t('pos', {'srsDimension': 2}, self.pos_list_content(geom))
405 def pos_list(self, geom):
406 """Create a ``posList`` element.
408 Args:
409 geom: A shapely geometry.
411 Returns:
412 A GML element.
413 """
415 return self.t('posList', {'srsDimension': 2}, self.pos_list_content(geom))
417 def pos_list_content(self, geom):
418 """Format the coordinates of a geometry as a space-separated list.
420 Args:
421 geom: A shapely geometry with a ``coords`` sequence.
423 Returns:
424 Coordinates as a string.
425 """
427 cs = []
428 for x, y in self.round_coords(geom):
429 cs.append(str(x))
430 cs.append(str(y))
431 return ' '.join(cs)