1
1
"""pydantic models for GeoJSON Geometry objects."""
2
-
3
2
import abc
4
- from typing import Any , Iterator , List , Literal , Optional , Union
3
+ from typing import Any , Iterator , List , Literal , Optional , Protocol , Union
5
4
6
5
from pydantic import BaseModel , Field , ValidationError , validator
7
6
from pydantic .error_wrappers import ErrorWrapper
20
19
)
21
20
22
21
23
- def _position_wkt_coordinates (position : Position ) -> str :
22
+ def _position_wkt_coordinates (coordinates : Position , force_z : bool = False ) -> str :
24
23
"""Converts a Position to WKT Coordinates."""
25
- return " " .join (str (number ) for number in position )
24
+ wkt_coordinates = " " .join (str (number ) for number in coordinates )
25
+ if force_z and len (coordinates ) < 3 :
26
+ wkt_coordinates += " 0.0"
27
+ return wkt_coordinates
26
28
27
29
28
30
def _position_has_z (position : Position ) -> bool :
29
31
return len (position ) == 3
30
32
31
33
32
- def _position_list_wkt_coordinates (positions : List [Position ]) -> str :
34
+ def _position_list_wkt_coordinates (
35
+ coordinates : List [Position ], force_z : bool = False
36
+ ) -> str :
33
37
"""Converts a list of Positions to WKT Coordinates."""
34
- return ", " .join (_position_wkt_coordinates (position ) for position in positions )
38
+ return ", " .join (
39
+ _position_wkt_coordinates (position , force_z ) for position in coordinates
40
+ )
35
41
36
42
37
43
def _position_list_has_z (positions : List [Position ]) -> bool :
38
44
"""Checks if any position in a list has a Z."""
39
45
return any (_position_has_z (position ) for position in positions )
40
46
41
47
42
- def _lines_wtk_coordinates (lines : List [List [Position ]]) -> str :
48
+ def _lines_wtk_coordinates (
49
+ coordinates : List [LineStringCoords ], force_z : bool = False
50
+ ) -> str :
43
51
"""Converts lines to WKT Coordinates."""
44
- return ", " .join (f"({ _position_list_wkt_coordinates (line )} )" for line in lines )
52
+ return ", " .join (
53
+ f"({ _position_list_wkt_coordinates (line , force_z )} )" for line in coordinates
54
+ )
45
55
46
56
47
- def _lines_has_z (lines : List [List [ Position ] ]) -> bool :
57
+ def _lines_has_z (lines : List [LineStringCoords ]) -> bool :
48
58
"""Checks if any position in a list has a Z."""
49
59
return any (
50
60
_position_has_z (position ) for positions in lines for position in positions
51
61
)
52
62
53
63
64
+ def _polygons_wkt_coordinates (
65
+ coordinates : List [PolygonCoords ], force_z : bool = False
66
+ ) -> str :
67
+ return "," .join (
68
+ f"({ _lines_wtk_coordinates (polygon , force_z )} )" for polygon in coordinates
69
+ )
70
+
71
+
72
+ class _WktCallable (Protocol ):
73
+ def __call__ (self , coordinates : Any , force_z : bool ) -> str :
74
+ ...
75
+
76
+
54
77
class _GeometryBase (BaseModel , abc .ABC , GeoInterfaceMixin ):
55
78
"""Base class for geometry models"""
56
79
57
80
type : str
58
81
coordinates : Any
59
82
bbox : Optional [BBox ] = None
60
83
84
+ __wkt_coordinates__ : _WktCallable
85
+
61
86
@property
62
87
@abc .abstractmethod
63
88
def has_z (self ) -> bool :
64
89
"""Checks if any coordinate has a Z value."""
65
90
...
66
91
67
- @property
68
- @abc .abstractmethod
69
- def _wkt_coordinates (self ) -> str :
70
- ...
71
-
72
- @property
73
- def _wkt_type (self ) -> str :
74
- """Return the WKT name of the geometry."""
75
- return self .type .upper ()
76
-
77
92
@property
78
93
def wkt (self ) -> str :
79
94
"""Return the Well Known Text representation."""
80
95
# Start with the WKT Type
81
- wkt = self ._wkt_type
96
+ wkt = self .type .upper ()
97
+ has_z = self .has_z
82
98
if self .coordinates :
83
99
# If any of the coordinates have a Z add a "Z" to the WKT
84
- wkt += " Z " if self . has_z else " "
100
+ wkt += " Z " if has_z else " "
85
101
# Add the rest of the WKT inside parentheses
86
- wkt += f"({ self ._wkt_coordinates } )"
102
+ wkt += f"({ self .__wkt_coordinates__ ( self . coordinates , force_z = has_z ) } )"
87
103
else :
88
104
# Otherwise it will be "EMPTY"
89
105
wkt += " EMPTY"
@@ -97,70 +113,64 @@ class Point(_GeometryBase):
97
113
type : Literal ["Point" ]
98
114
coordinates : Position
99
115
116
+ __wkt_coordinates__ = staticmethod (_position_wkt_coordinates )
117
+
100
118
@property
101
119
def has_z (self ) -> bool :
102
120
"""Checks if any coordinate has a Z value."""
103
121
return _position_has_z (self .coordinates )
104
122
105
- @property
106
- def _wkt_coordinates (self ) -> str :
107
- return _position_wkt_coordinates (self .coordinates )
108
-
109
123
110
124
class MultiPoint (_GeometryBase ):
111
125
"""MultiPoint Model"""
112
126
113
127
type : Literal ["MultiPoint" ]
114
128
coordinates : MultiPointCoords
115
129
130
+ __wkt_coordinates__ = staticmethod (_position_list_wkt_coordinates )
131
+
116
132
@property
117
133
def has_z (self ) -> bool :
118
134
"""Checks if any coordinate has a Z value."""
119
135
return _position_list_has_z (self .coordinates )
120
136
121
- @property
122
- def _wkt_coordinates (self ) -> str :
123
- return _position_list_wkt_coordinates (self .coordinates )
124
-
125
137
126
138
class LineString (_GeometryBase ):
127
139
"""LineString Model"""
128
140
129
141
type : Literal ["LineString" ]
130
142
coordinates : LineStringCoords
131
143
144
+ __wkt_coordinates__ = staticmethod (_position_list_wkt_coordinates )
145
+
132
146
@property
133
147
def has_z (self ) -> bool :
134
148
"""Checks if any coordinate has a Z value."""
135
149
return _position_list_has_z (self .coordinates )
136
150
137
- @property
138
- def _wkt_coordinates (self ) -> str :
139
- return _position_list_wkt_coordinates (self .coordinates )
140
-
141
151
142
152
class MultiLineString (_GeometryBase ):
143
153
"""MultiLineString Model"""
144
154
145
155
type : Literal ["MultiLineString" ]
146
156
coordinates : MultiLineStringCoords
147
157
158
+ __wkt_coordinates__ = staticmethod (_lines_wtk_coordinates )
159
+
148
160
@property
149
161
def has_z (self ) -> bool :
150
162
"""Checks if any coordinate has a Z value."""
151
163
return _lines_has_z (self .coordinates )
152
164
153
- @property
154
- def _wkt_coordinates (self ) -> str :
155
- return _lines_wtk_coordinates (self .coordinates )
156
-
157
165
158
166
class Polygon (_GeometryBase ):
159
167
"""Polygon Model"""
160
168
161
169
type : Literal ["Polygon" ]
162
170
coordinates : PolygonCoords
163
171
172
+ __wkt_coordinates__ = staticmethod (_lines_wtk_coordinates )
173
+
164
174
@validator ("coordinates" )
165
175
def check_closure (cls , coordinates : List ) -> List :
166
176
"""Validate that Polygon is closed (first and last coordinate are the same)."""
@@ -186,10 +196,6 @@ def has_z(self) -> bool:
186
196
"""Checks if any coordinates have a Z value."""
187
197
return _lines_has_z (self .coordinates )
188
198
189
- @property
190
- def _wkt_coordinates (self ) -> str :
191
- return _lines_wtk_coordinates (self .coordinates )
192
-
193
199
@classmethod
194
200
def from_bounds (
195
201
cls , xmin : float , ymin : float , xmax : float , ymax : float
@@ -209,17 +215,13 @@ class MultiPolygon(_GeometryBase):
209
215
type : Literal ["MultiPolygon" ]
210
216
coordinates : MultiPolygonCoords
211
217
218
+ __wkt_coordinates__ = staticmethod (_polygons_wkt_coordinates )
219
+
212
220
@property
213
221
def has_z (self ) -> bool :
214
222
"""Checks if any coordinates have a Z value."""
215
223
return any (_lines_has_z (polygon ) for polygon in self .coordinates )
216
224
217
- @property
218
- def _wkt_coordinates (self ) -> str :
219
- return "," .join (
220
- f"({ _lines_wtk_coordinates (polygon )} )" for polygon in self .coordinates
221
- )
222
-
223
225
@validator ("coordinates" )
224
226
def check_closure (cls , coordinates : List ) -> List :
225
227
"""Validate that Polygon is closed (first and last coordinate are the same)."""
@@ -254,24 +256,23 @@ def __getitem__(self, index: int) -> Geometry:
254
256
"""get geometry at a given index"""
255
257
return self .geometries [index ]
256
258
257
- @property
258
- def _wkt_type (self ) -> str :
259
- """Return the WKT name of the geometry."""
260
- return self .type .upper ()
261
-
262
- @property
263
- def _wkt_coordinates (self ) -> str :
264
- """Encode coordinates as WKT."""
265
- return ", " .join (geom .wkt for geom in self .geometries )
266
-
267
259
@property
268
260
def wkt (self ) -> str :
269
261
"""Return the Well Known Text representation."""
270
- return (
271
- self ._wkt_type
272
- + " "
273
- + (f"({ self ._wkt_coordinates } )" if self ._wkt_coordinates else "EMPTY" )
262
+ # Each geometry will check its own coordinates for Z and include "Z" in the wkt
263
+ # if necessary. Rather than looking at the coordinates for each of the geometries
264
+ # again, we can just get the wkt from each of them and check if there is a Z
265
+ # anywhere in the text.
266
+
267
+ # Get the wkt from each of the geometries in the collection
268
+ geometries = (
269
+ f'({ ", " .join (geom .wkt for geom in self .geometries )} )'
270
+ if self .geometries
271
+ else "EMPTY"
274
272
)
273
+ # If any of them contain `Z` add Z to the output wkt
274
+ z = " Z " if "Z" in geometries else " "
275
+ return f"{ self .type .upper ()} { z } { geometries } "
275
276
276
277
277
278
def parse_geometry_obj (obj : Any ) -> Geometry :
0 commit comments