Skip to content

Commit 8ef50de

Browse files
Merge pull request #3 from geopython/encode-v32
Encoding for GML v3.2 and GeoRSS
2 parents a083255 + 06e1988 commit 8ef50de

File tree

10 files changed

+1381
-12
lines changed

10 files changed

+1381
-12
lines changed

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# pygml
22

3-
A pure python parser for OGC GML Geometries.
3+
A pure python parser and encoder for OGC GML Geometries.
44

55
[![PyPI version](https://badge.fury.io/py/pygml.svg)](https://badge.fury.io/py/pygml)
66
[![CI](https://github.com/geopython/pygml/actions/workflows/test.yaml/badge.svg)](https://github.com/geopython/pygml/actions/workflows/test.yaml)
@@ -29,3 +29,18 @@ Geometry(geometry={'type': 'Point', 'coordinates': (1.0, 1.0)})
2929
>>> print(geom.__geo_interface__)
3030
{'type': 'Point', 'coordinates': (1.0, 1.0)}
3131
```
32+
33+
Conversely, it is possible to encode GeoJSON or Geo Interfaces to GML
34+
35+
36+
```python
37+
>>> from pygml.v32 import encode_v32
38+
>>> from lxml import etree
39+
>>> tree = encode_v32({'type': 'Point', 'coordinates': (1.0, 1.0)}, 'ID')
40+
>>> print(etree.tostring(tree, pretty_print=True).decode())
41+
<gml:Point xmlns:gml="http://www.opengis.net/gml/3.2" srsName="urn:ogc:def:crs:OGC::CRS84" gml:id="ID">
42+
<gml:pos>1.0 1.0</gml:pos>
43+
</gml:Point>
44+
45+
>>>
46+
```

pygml/axisorder.py

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,36 @@
1+
# ------------------------------------------------------------------------------
2+
#
3+
# Project: pygml <https://github.com/geopython/pygml>
4+
# Authors: Fabian Schindler <[email protected]>
5+
#
6+
# ------------------------------------------------------------------------------
7+
# Copyright (C) 2021 EOX IT Services GmbH
8+
#
9+
# Permission is hereby granted, free of charge, to any person obtaining a copy
10+
# of this software and associated documentation files (the "Software"), to deal
11+
# in the Software without restriction, including without limitation the rights
12+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
# copies of the Software, and to permit persons to whom the Software is
14+
# furnished to do so, subject to the following conditions:
15+
#
16+
# The above copyright notice and this permission notice shall be included in
17+
# all copies of this Software or works derived from this Software.
18+
#
19+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
# THE SOFTWARE.
26+
# ------------------------------------------------------------------------------
27+
28+
129
import re
30+
from typing import Union
231

3-
# copied from https://github.com/geopython/OWSLib/blob/a9c1be25676ab530fd0327c2450922f288ca25f4/owslib/crs.py
32+
# copied from:
33+
# https://github.com/geopython/OWSLib/blob/a9c1be25676ab530fd0327c2450922f288ca25f4/owslib/crs.py
434
AXISORDER_YX = {
535
4326, 4258, 31466, 31467, 31468, 31469, 2166, 2167, 2168, 2036, 2044, 2045,
636
2065, 2081, 2082, 2083, 2085, 2086, 2091, 2092, 2093, 2096, 2097, 2098,
@@ -158,22 +188,40 @@
158188
r'http://www\.opengis\.net/gml/srs/epsg\.xml\#|'
159189
r'urn:EPSG:geographicCRS:|'
160190
r'urn:ogc:def:crs:EPSG::|'
161-
r'urn:ogc:def:crs:EPSG:)([0-9]+)'
191+
r'urn:ogc:def:crs:OGC::|'
192+
r'urn:ogc:def:crs:EPSG:)([0-9]+|CRS84)'
162193
)
163194

164195

165-
def is_crs_yx(crs: str) -> bool:
166-
"""
196+
def get_crs_code(crs: str) -> Union[int, str]:
197+
""" Extract the CRS code from the given CRS identifier string,
198+
which can be one of:
167199
* EPSG:<EPSG code>
168200
* http://www.opengis.net/def/crs/EPSG/0/<EPSG code> (URI Style 1)
169201
* http://www.opengis.net/gml/srs/epsg.xml#<EPSG code> (URI Style 2)
170202
* urn:EPSG:geographicCRS:<epsg code>
171203
* urn:ogc:def:crs:EPSG::<EPSG code>
204+
* urn:ogc:def:crs:OGC::<EPSG code>
172205
* urn:ogc:def:crs:EPSG:<EPSG code>
206+
207+
Returns the code as an integer in case of EPSG code or as the
208+
string 'CRS84'
173209
"""
174210
match = RE_CRS_CODE.match(crs)
175211
if not match:
176212
raise ValueError('Failed to retrieve CRS code')
177213

178-
code = int(match.groups()[1])
214+
value_group = match.groups()[1]
215+
216+
try:
217+
return int(value_group)
218+
except ValueError:
219+
return value_group
220+
221+
222+
def is_crs_yx(crs: str) -> bool:
223+
""" Determines whether the given CRS uses Y/X (or latitude/longitude)
224+
axis order.
225+
"""
226+
code = get_crs_code(crs)
179227
return code in AXISORDER_YX

pygml/dimensionality.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# ------------------------------------------------------------------------------
2+
#
3+
# Project: pygml <https://github.com/geopython/pygml>
4+
# Authors: Fabian Schindler <[email protected]>
5+
#
6+
# ------------------------------------------------------------------------------
7+
# Copyright (C) 2021 EOX IT Services GmbH
8+
#
9+
# Permission is hereby granted, free of charge, to any person obtaining a copy
10+
# of this software and associated documentation files (the "Software"), to deal
11+
# in the Software without restriction, including without limitation the rights
12+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
# copies of the Software, and to permit persons to whom the Software is
14+
# furnished to do so, subject to the following conditions:
15+
#
16+
# The above copyright notice and this permission notice shall be included in
17+
# all copies of this Software or works derived from this Software.
18+
#
19+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
# THE SOFTWARE.
26+
# ------------------------------------------------------------------------------
27+
28+
from typing import Optional
29+
from collections.abc import Sequence
30+
31+
from .types import GeomDict
32+
33+
34+
def get_dimensionality(geometry: GeomDict) -> Optional[int]:
35+
""" Returns the dimensionality of a given GeoJSON geometry.
36+
This is obtained by descending into the first coordinate
37+
and using its length.
38+
When no coordinates can be retrieved (e.g: in case of
39+
GeometryCollections) None is returned.
40+
"""
41+
42+
coordinates = geometry.get('coordinates')
43+
44+
if coordinates:
45+
# drill down into nested coordinates
46+
while isinstance(coordinates[0], Sequence):
47+
coordinates = coordinates[0]
48+
return len(coordinates)
49+
return None

pygml/georss.py

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,19 @@
2525
# THE SOFTWARE.
2626
# ------------------------------------------------------------------------------
2727

28-
from typing import List
28+
from pygml.axisorder import get_crs_code
29+
from typing import Callable, List
2930

3031
from lxml import etree
32+
from lxml.builder import ElementMaker
3133

3234
from .basics import (
3335
parse_pos, parse_poslist, swap_coordinate_xy, swap_coordinates_xy
3436
)
37+
from .dimensionality import get_dimensionality
3538
from .types import GeomDict
3639
from .pre_v32 import NAMESPACE as NAMESPACE_PRE32, parse_pre_v32
37-
from .v32 import NAMESPACE as NAMESPACE_32, parse_v32
40+
from .v32 import NAMESPACE as NAMESPACE_32, encode_v32, parse_v32
3841
from .v33 import NAMESPACE as NAMESPACE_33_CE, parse_v33_ce
3942

4043

@@ -120,3 +123,73 @@ def parse_georss(element: Element) -> GeomDict:
120123
result['bbox'] = bbox
121124

122125
return result
126+
127+
128+
GEORSS = ElementMaker(namespace=NAMESPACE, nsmap=NSMAP)
129+
130+
GmlEncoder = Callable[[GeomDict, str], Element]
131+
132+
133+
def encode_georss(geometry: GeomDict,
134+
gml_encoder: GmlEncoder = encode_v32) -> Element:
135+
""" Encodes a GeoJSON geometry as a GeoRSS ``lxml.etree.Element``.
136+
Tries to use the native GeoRSS elements ``point``, ``line``,
137+
or ``polygon`` when possible. Falls back to ``georss:where``
138+
with using the ``gml_encoder`` function (defaulting to GML 3.2):
139+
- MultiPoint, MultiLineString, MultiPolygon geometries
140+
- Polygons with interiors
141+
- GeometryCollections
142+
- any geometry with CRS other than CRS84 or EPSG:4326
143+
- when dealing with >2D geometries
144+
"""
145+
type_ = geometry['type']
146+
coordinates = geometry.get('coordinates')
147+
crs = geometry.get('crs')
148+
dims = get_dimensionality(geometry)
149+
150+
code = None
151+
if crs:
152+
crs_name = crs.get('properties', {}).get('name')
153+
code = get_crs_code(crs_name)
154+
155+
if code in (None, 4326, 'CRS84') and dims == 2:
156+
if type_ == 'Point':
157+
return GEORSS(
158+
'point',
159+
' '.join(
160+
str(v) for v in swap_coordinate_xy(coordinates)
161+
)
162+
)
163+
164+
elif type_ == 'LineString':
165+
return GEORSS(
166+
'line',
167+
' '.join(
168+
' '.join(
169+
str(v) for v in coordinate
170+
) for coordinate in swap_coordinates_xy(coordinates)
171+
)
172+
)
173+
174+
elif type_ == 'Polygon':
175+
# only exterior
176+
if len(coordinates) == 1:
177+
return GEORSS(
178+
'polygon',
179+
' '.join(
180+
' '.join(
181+
str(v) for v in coordinate
182+
) for coordinate in swap_coordinates_xy(coordinates[0])
183+
)
184+
)
185+
186+
# fall back to GML encoding when we have:
187+
# - MultiPoint, MultiLineString, MultiPolygon geometries
188+
# - Polygons with interiors
189+
# - GeometryCollections
190+
# - any geometry with CRS other than CRS84 or EPSG4326
191+
# - when dealing with >2D geometries
192+
return GEORSS(
193+
'where',
194+
gml_encoder(geometry, 'ID')
195+
)

0 commit comments

Comments
 (0)