geobox 1.4.2__py3-none-any.whl → 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- geobox/__init__.py +2 -2
- geobox/aio/__init__.py +63 -0
- geobox/aio/api.py +2640 -0
- geobox/aio/apikey.py +263 -0
- geobox/aio/attachment.py +339 -0
- geobox/aio/base.py +262 -0
- geobox/aio/basemap.py +196 -0
- geobox/aio/dashboard.py +342 -0
- geobox/aio/feature.py +527 -0
- geobox/aio/field.py +321 -0
- geobox/aio/file.py +522 -0
- geobox/aio/layout.py +341 -0
- geobox/aio/log.py +145 -0
- geobox/aio/map.py +1034 -0
- geobox/aio/model3d.py +415 -0
- geobox/aio/mosaic.py +696 -0
- geobox/aio/plan.py +315 -0
- geobox/aio/query.py +693 -0
- geobox/aio/raster.py +869 -0
- geobox/aio/route.py +63 -0
- geobox/aio/scene.py +342 -0
- geobox/aio/settings.py +194 -0
- geobox/aio/task.py +402 -0
- geobox/aio/tile3d.py +339 -0
- geobox/aio/tileset.py +672 -0
- geobox/aio/usage.py +243 -0
- geobox/aio/user.py +507 -0
- geobox/aio/vectorlayer.py +1363 -0
- geobox/aio/version.py +273 -0
- geobox/aio/view.py +983 -0
- geobox/aio/workflow.py +341 -0
- geobox/api.py +14 -15
- geobox/apikey.py +28 -1
- geobox/attachment.py +27 -1
- geobox/base.py +4 -4
- geobox/basemap.py +30 -1
- geobox/dashboard.py +27 -0
- geobox/feature.py +33 -13
- geobox/field.py +33 -21
- geobox/file.py +40 -46
- geobox/layout.py +28 -1
- geobox/log.py +31 -7
- geobox/map.py +34 -2
- geobox/model3d.py +31 -37
- geobox/mosaic.py +28 -7
- geobox/plan.py +29 -3
- geobox/query.py +39 -14
- geobox/raster.py +26 -13
- geobox/scene.py +26 -0
- geobox/settings.py +30 -1
- geobox/task.py +28 -6
- geobox/tile3d.py +27 -1
- geobox/tileset.py +26 -5
- geobox/usage.py +32 -1
- geobox/user.py +62 -6
- geobox/utils.py +34 -0
- geobox/vectorlayer.py +40 -4
- geobox/version.py +25 -1
- geobox/view.py +37 -17
- geobox/workflow.py +27 -1
- {geobox-1.4.2.dist-info → geobox-2.0.1.dist-info}/METADATA +4 -1
- geobox-2.0.1.dist-info/RECORD +68 -0
- geobox-1.4.2.dist-info/RECORD +0 -38
- {geobox-1.4.2.dist-info → geobox-2.0.1.dist-info}/WHEEL +0 -0
- {geobox-1.4.2.dist-info → geobox-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {geobox-1.4.2.dist-info → geobox-2.0.1.dist-info}/top_level.txt +0 -0
geobox/aio/feature.py
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
from urllib.parse import urljoin
|
|
2
|
+
from typing import Optional, List, Dict, Any, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from .base import AsyncBase
|
|
5
|
+
from ..enums import FeatureType
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .vectorlayer import VectorLayer
|
|
9
|
+
from ..api import GeoboxClient as SyncGeoboxClient
|
|
10
|
+
from ..feature import Feature as SyncFeature
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Feature(AsyncBase):
|
|
14
|
+
|
|
15
|
+
BASE_SRID = 3857
|
|
16
|
+
|
|
17
|
+
def __init__(self,
|
|
18
|
+
layer: 'VectorLayer',
|
|
19
|
+
srid: Optional[int] = 3857,
|
|
20
|
+
data: Optional[Dict] = {}):
|
|
21
|
+
"""
|
|
22
|
+
Constructs all the necessary attributes for the Feature object.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
layer (VectorLayer): The vector layer this feature belongs to
|
|
26
|
+
srid (int, optional): The Spatial Reference System Identifier (default is 3857)
|
|
27
|
+
data (Dict, optional): The feature data contains the feature geometry and properties
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
>>> from geobox import GeoboxClient, Feature
|
|
31
|
+
>>> client = GeoboxClient()
|
|
32
|
+
>>> layer = client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
|
|
33
|
+
>>> feature = Feature(layer=layer, srid=4326) # example srid set to 4326
|
|
34
|
+
>>> feature.save()
|
|
35
|
+
"""
|
|
36
|
+
super().__init__(api=layer.api)
|
|
37
|
+
self.layer = layer
|
|
38
|
+
self._srid = srid
|
|
39
|
+
self.data = data or {}
|
|
40
|
+
self.original_geometry = self.data.get('geometry')
|
|
41
|
+
self.endpoint = urljoin(layer.endpoint, f'features/{self.data.get("id")}/') if self.data.get('id') else None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def __dir__(self) -> List[str]:
|
|
45
|
+
"""
|
|
46
|
+
Return a list of available attributes for the Feature object.
|
|
47
|
+
|
|
48
|
+
This method extends the default dir() behavior to include:
|
|
49
|
+
- All keys from the feature data dictionary
|
|
50
|
+
- All keys from the geometry dictionary
|
|
51
|
+
- All keys from the properties dictionary
|
|
52
|
+
|
|
53
|
+
This allows for better IDE autocompletion and introspection of feature attributes.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
list: A list of attribute names available on this Feature object.
|
|
57
|
+
"""
|
|
58
|
+
return super().__dir__() + list(self.data.keys()) + list(self.data.get('geometry').keys()) + list(self.data.get('properties').keys())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def __repr__(self) -> str:
|
|
62
|
+
"""
|
|
63
|
+
Return a string representation of the Feature object.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
str: A string representation of the Feature object.
|
|
67
|
+
"""
|
|
68
|
+
return f"Feature(id={self.id}, type={self.feature_type})"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def __getattr__(self, name: str) -> Any:
|
|
72
|
+
"""
|
|
73
|
+
Get an attribute from the resource.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
name (str): The name of the attribute
|
|
77
|
+
"""
|
|
78
|
+
if name in self.data:
|
|
79
|
+
return self.data.get(name)
|
|
80
|
+
# elif name in self.data['geometry']:
|
|
81
|
+
# return self.data['geometry'].get(name)
|
|
82
|
+
elif name in self.data['properties']:
|
|
83
|
+
return self.data['properties'].get(name)
|
|
84
|
+
|
|
85
|
+
raise AttributeError(f"Feature has no attribute {name}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def srid(self) -> int:
|
|
90
|
+
"""
|
|
91
|
+
Get the Spatial Reference System Identifier (SRID) of the feature.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
int: The SRID of the feature.
|
|
95
|
+
|
|
96
|
+
Example:
|
|
97
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
98
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
99
|
+
>>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
|
|
100
|
+
>>> feature = await layer.get_feature(id=1)
|
|
101
|
+
>>> feature.srid # 3857
|
|
102
|
+
"""
|
|
103
|
+
return self._srid
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def feature_type(self) -> 'FeatureType':
|
|
108
|
+
"""
|
|
109
|
+
Get the type of the feature.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
FeatureType: The type of the feature.
|
|
113
|
+
|
|
114
|
+
Example:
|
|
115
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
116
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
117
|
+
>>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
|
|
118
|
+
>>> feature = await layer.get_feature(id=1)
|
|
119
|
+
>>> feature.feature_type
|
|
120
|
+
"""
|
|
121
|
+
return FeatureType(self.data.get('geometry').get('type')) if self.data.get('geometry') else None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def coordinates(self) -> List[float]:
|
|
126
|
+
"""
|
|
127
|
+
Get the coordinates of the ferepoature.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
list: The coordinates of the feature.
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
134
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
135
|
+
>>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
|
|
136
|
+
>>> feature = await layer.get_feature(id=1)
|
|
137
|
+
>>> feature.coordinates
|
|
138
|
+
"""
|
|
139
|
+
return self.data.get('geometry').get('coordinates') if self.data.get('geometry') else None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@coordinates.setter
|
|
143
|
+
def coordinates(self, value: List[float]) -> None:
|
|
144
|
+
"""
|
|
145
|
+
Set the coordinates of the feature.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
value (list): The coordinates to set.
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
152
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
153
|
+
>>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
|
|
154
|
+
>>> feature = await layer.get_feature(id=1)
|
|
155
|
+
>>> feature.coordinates = [10, 20]
|
|
156
|
+
"""
|
|
157
|
+
self.data['geometry']['coordinates'] = value
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
async def length(self) -> float:
|
|
162
|
+
"""
|
|
163
|
+
[async] Returns the length of thefeature geometry (geometry package extra is required!)
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
float: the length of thefeature geometry
|
|
167
|
+
|
|
168
|
+
Example:
|
|
169
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
170
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
171
|
+
>>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
|
|
172
|
+
>>> feature = await layer.get_feature(id=1)
|
|
173
|
+
>>> await feature.length
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
return self.geom_length
|
|
177
|
+
except AttributeError:
|
|
178
|
+
endpoint = f'{self.endpoint}length'
|
|
179
|
+
return await self.api.get(endpoint)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
async def area(self) -> float:
|
|
184
|
+
"""
|
|
185
|
+
[async] Returns the area of thefeature geometry (geometry package extra is required!)
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
float: the area of thefeature geometry
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
192
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
193
|
+
>>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
|
|
194
|
+
>>> feature = await layer.get_feature(id=1)
|
|
195
|
+
>>> await feature.area
|
|
196
|
+
"""
|
|
197
|
+
try:
|
|
198
|
+
return self.geom_area
|
|
199
|
+
except AttributeError:
|
|
200
|
+
endpoint = f'{self.endpoint}area'
|
|
201
|
+
return await self.api.get(endpoint)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async def save(self) -> None:
|
|
205
|
+
"""
|
|
206
|
+
[async] Save the feature. Creates a new feature if feature_id is None, updates existing feature otherwise.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
None
|
|
210
|
+
|
|
211
|
+
Example:
|
|
212
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
213
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
214
|
+
>>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
|
|
215
|
+
>>> feature = await layer.get_feature(id=1)
|
|
216
|
+
>>> feature.properties['name'] = 'New Name'
|
|
217
|
+
>>> await feature.save()
|
|
218
|
+
"""
|
|
219
|
+
data = self.data.copy()
|
|
220
|
+
try:
|
|
221
|
+
if self.id:
|
|
222
|
+
if self.srid != self.BASE_SRID:
|
|
223
|
+
self.data['geometry'] = self.original_geometry
|
|
224
|
+
await self.update(self.data)
|
|
225
|
+
self.coordinates = data['geometry']['coordinates']
|
|
226
|
+
except AttributeError:
|
|
227
|
+
endpoint = urljoin(self.layer.endpoint, 'features/')
|
|
228
|
+
response = await self.layer.api.post(endpoint, data)
|
|
229
|
+
self.endpoint = urljoin(self.layer.endpoint, f'features/{response["id"]}/')
|
|
230
|
+
self.data.update(response)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
async def delete(self) -> None:
|
|
234
|
+
"""
|
|
235
|
+
[async] Delete the feature.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
None
|
|
239
|
+
|
|
240
|
+
Example:
|
|
241
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
242
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
243
|
+
>>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
|
|
244
|
+
>>> feature = await layer.get_feature(id=1)
|
|
245
|
+
>>> await feature.delete()
|
|
246
|
+
"""
|
|
247
|
+
await super().delete(self.endpoint)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
async def update(self, geojson: Dict) -> Dict:
|
|
251
|
+
"""
|
|
252
|
+
[async] Update the feature data property.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
geojson (Dict): The GeoJSON data for the feature
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Dict: The response from the API.
|
|
259
|
+
|
|
260
|
+
Example:
|
|
261
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
262
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
263
|
+
>>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
|
|
264
|
+
>>> feature = await layer.get_feature(id=1)
|
|
265
|
+
>>> geojson = {
|
|
266
|
+
... "geometry": {
|
|
267
|
+
... "type": "Point",
|
|
268
|
+
... "coordinates": [10, 20]
|
|
269
|
+
... }
|
|
270
|
+
... }
|
|
271
|
+
>>> await feature.update(geojson)
|
|
272
|
+
"""
|
|
273
|
+
return await super()._update(self.endpoint, geojson, clean=False)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@classmethod
|
|
277
|
+
async def create_feature(cls, layer: 'VectorLayer', geojson: Dict) -> 'Feature':
|
|
278
|
+
"""
|
|
279
|
+
[async] Create a new feature in the vector layer.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
layer (VectorLayer): The vector layer to create the feature in
|
|
283
|
+
geojson (Dict): The GeoJSON data for the feature
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Feature: The created feature instance
|
|
287
|
+
|
|
288
|
+
Example:
|
|
289
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
290
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
291
|
+
>>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
|
|
292
|
+
>>> geojson = {
|
|
293
|
+
... "type": "Feature",
|
|
294
|
+
... "geometry": {"type": "Point", "coordinates": [10, 20]},
|
|
295
|
+
... "properties": {"name": "My Point"}
|
|
296
|
+
... }
|
|
297
|
+
>>> feature = await Feature.create_feature(layer, geojson)
|
|
298
|
+
"""
|
|
299
|
+
endpoint = urljoin(layer.endpoint, 'features/')
|
|
300
|
+
return await cls._create(layer.api, endpoint, geojson, factory_func=lambda api, item: Feature(layer, data=item))
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@classmethod
|
|
304
|
+
async def get_feature(cls, layer: 'VectorLayer', feature_id: int, user_id: int = None) -> 'Feature':
|
|
305
|
+
"""
|
|
306
|
+
[async] Get a feature by its ID.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
layer (VectorLayer): The vector layer the feature belongs to
|
|
310
|
+
feature_id (int): The ID of the feature
|
|
311
|
+
user_id (int): specific user. privileges required.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Feature: The retrieved feature instance
|
|
315
|
+
|
|
316
|
+
Example:
|
|
317
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
318
|
+
>>> from geobox.aio.feature import Feature
|
|
319
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
320
|
+
>>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
|
|
321
|
+
>>> feature = await Feature.get_feature(layer, feature_id=1)
|
|
322
|
+
"""
|
|
323
|
+
param = {
|
|
324
|
+
'f': 'json',
|
|
325
|
+
'user_id': user_id
|
|
326
|
+
}
|
|
327
|
+
endpoint = urljoin(layer.endpoint, f'features/')
|
|
328
|
+
return await cls._get_detail(layer.api, endpoint, uuid=feature_id, params=param, factory_func=lambda api, item: Feature(layer, data=item))
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@property
|
|
332
|
+
def geometry(self) -> 'BaseGeometry':
|
|
333
|
+
"""
|
|
334
|
+
Get the feature geometry as a Shapely geometry object.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
shapely.geometry.BaseGeometry: The Shapely geometry object representing the feature's geometry
|
|
338
|
+
|
|
339
|
+
Raises:
|
|
340
|
+
ValueError: If the geometry is not a dictionary
|
|
341
|
+
ValueError: If the geometry type is not present in the feature data
|
|
342
|
+
ValueError: If the geometry coordinates are not present in the feature data
|
|
343
|
+
ImportError: If shapely is not installed
|
|
344
|
+
|
|
345
|
+
Example:
|
|
346
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
347
|
+
>>> from geobox.aio.feature import Feature
|
|
348
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
349
|
+
>>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
|
|
350
|
+
>>> feature = await layer.get_feature(id=1)
|
|
351
|
+
>>> feature.geometry
|
|
352
|
+
"""
|
|
353
|
+
try:
|
|
354
|
+
from shapely.geometry import shape
|
|
355
|
+
except ImportError:
|
|
356
|
+
raise ImportError(
|
|
357
|
+
"The 'geometry' extra is required for this function. "
|
|
358
|
+
"Install it with: pip install geobox[geometry]"
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
if not self.data:
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
elif not self.data.get('geometry'):
|
|
365
|
+
raise ValueError("Geometry is not present in the feature data")
|
|
366
|
+
|
|
367
|
+
elif not isinstance(self.data['geometry'], dict):
|
|
368
|
+
raise ValueError("Geometry is not a dictionary")
|
|
369
|
+
|
|
370
|
+
elif not self.data['geometry'].get('type'):
|
|
371
|
+
raise ValueError("Geometry type is not present in the feature data")
|
|
372
|
+
|
|
373
|
+
elif not self.data['geometry'].get('coordinates'):
|
|
374
|
+
raise ValueError("Geometry coordinates are not present in the feature data")
|
|
375
|
+
|
|
376
|
+
else:
|
|
377
|
+
return shape(self.data['geometry'])
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@geometry.setter
|
|
381
|
+
def geometry(self, value: object) -> None:
|
|
382
|
+
"""
|
|
383
|
+
Set the feature geometry.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
value (object): The geometry to set.
|
|
387
|
+
|
|
388
|
+
Raises:
|
|
389
|
+
ValueError: If geometry type is not supported
|
|
390
|
+
ValueError: If the geometry has a different type than the layer type
|
|
391
|
+
ImportError: If shapely is not installed
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
None
|
|
395
|
+
|
|
396
|
+
Example:
|
|
397
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
398
|
+
>>> from geobox.aio.feature import Feature
|
|
399
|
+
>>> from shapely.affinity import translate
|
|
400
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
401
|
+
>>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
|
|
402
|
+
>>> feature = await layer.get_feature(id=1)
|
|
403
|
+
>>> geom = feature.geometry
|
|
404
|
+
>>> geom = translate(geom, 3.0, 0.5) # example change applied to the feature's geometry
|
|
405
|
+
>>> feature.geometry = geom
|
|
406
|
+
>>> await feature.save()
|
|
407
|
+
"""
|
|
408
|
+
try:
|
|
409
|
+
from shapely.geometry import mapping, Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon
|
|
410
|
+
except ImportError:
|
|
411
|
+
raise ImportError(
|
|
412
|
+
"The 'geometry' extra is required for this function. "
|
|
413
|
+
"Install it with: pip install geobox[geometry]"
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
if not isinstance(value, (Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon)):
|
|
417
|
+
raise ValueError("Geometry must be a Shapely geometry object")
|
|
418
|
+
|
|
419
|
+
elif self.feature_type and value.geom_type != self.feature_type.value:
|
|
420
|
+
raise ValueError("Geometry must have the same type as the layer type")
|
|
421
|
+
|
|
422
|
+
else:
|
|
423
|
+
self.data['geometry'] = mapping(value)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def transform(self, out_srid: int) -> 'Feature':
|
|
427
|
+
"""
|
|
428
|
+
Transform the feature geometry to a new SRID.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
out_srid (int): The target SRID to transform the geometry to (e.g., 4326 for WGS84, 3857 for Web Mercator)
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Feature: A new Feature instance with transformed geometry.
|
|
435
|
+
|
|
436
|
+
Raises:
|
|
437
|
+
ValueError: If the feature has no geometry or if the transformation fails.
|
|
438
|
+
ImportError: If pyproj is not installed.
|
|
439
|
+
|
|
440
|
+
Example:
|
|
441
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
442
|
+
>>> from geobox.aio.feature import Feature
|
|
443
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
444
|
+
>>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
|
|
445
|
+
>>> feature = await layer.get_feature(id=1, srid=3857)
|
|
446
|
+
>>> # Transform from Web Mercator (3857) to WGS84 (4326)
|
|
447
|
+
>>> transformed = feature.transform(out_srid=4326)
|
|
448
|
+
>>> transformed.srid # 4326
|
|
449
|
+
"""
|
|
450
|
+
try:
|
|
451
|
+
from pyproj import Transformer
|
|
452
|
+
from shapely.geometry import Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, mapping
|
|
453
|
+
except ImportError:
|
|
454
|
+
raise ImportError(
|
|
455
|
+
"The 'geometry' extra is required for this function. "
|
|
456
|
+
"Install it with: pip install geobox[geometry]"
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
if not self.data or not self.data.get('geometry'):
|
|
460
|
+
raise ValueError("Feature geometry is required for transformation")
|
|
461
|
+
|
|
462
|
+
# Get the current SRID from the feature or default to 3857 (Web Mercator)
|
|
463
|
+
current_srid = self.srid or 3857
|
|
464
|
+
|
|
465
|
+
# Create transformer
|
|
466
|
+
transformer = Transformer.from_crs(current_srid, out_srid, always_xy=True)
|
|
467
|
+
|
|
468
|
+
# Get the geometry
|
|
469
|
+
geom = self.geometry
|
|
470
|
+
|
|
471
|
+
# Transform coordinates based on geometry type
|
|
472
|
+
if geom.geom_type == 'Point':
|
|
473
|
+
x, y = geom.x, geom.y
|
|
474
|
+
new_x, new_y = transformer.transform(x, y)
|
|
475
|
+
new_geom = Point(new_x, new_y)
|
|
476
|
+
elif geom.geom_type == 'LineString':
|
|
477
|
+
coords = list(geom.coords)
|
|
478
|
+
new_coords = [transformer.transform(x, y) for x, y in coords]
|
|
479
|
+
new_geom = LineString(new_coords)
|
|
480
|
+
elif geom.geom_type == 'Polygon':
|
|
481
|
+
exterior = [transformer.transform(x, y) for x, y in geom.exterior.coords]
|
|
482
|
+
interiors = [[transformer.transform(x, y) for x, y in interior.coords] for interior in geom.interiors]
|
|
483
|
+
new_geom = Polygon(exterior, holes=interiors)
|
|
484
|
+
elif geom.geom_type == 'MultiPoint':
|
|
485
|
+
new_geoms = [Point(transformer.transform(point.x, point.y)) for point in geom.geoms]
|
|
486
|
+
new_geom = MultiPoint(new_geoms)
|
|
487
|
+
elif geom.geom_type == 'MultiLineString':
|
|
488
|
+
new_geoms = [LineString([transformer.transform(x, y) for x, y in line.coords]) for line in geom.geoms]
|
|
489
|
+
new_geom = MultiLineString(new_geoms)
|
|
490
|
+
elif geom.geom_type == 'MultiPolygon':
|
|
491
|
+
new_geoms = []
|
|
492
|
+
for poly in geom.geoms:
|
|
493
|
+
exterior = [transformer.transform(x, y) for x, y in poly.exterior.coords]
|
|
494
|
+
interiors = [[transformer.transform(x, y) for x, y in interior.coords] for interior in poly.interiors]
|
|
495
|
+
new_geoms.append(Polygon(exterior, holes=interiors))
|
|
496
|
+
new_geom = MultiPolygon(new_geoms)
|
|
497
|
+
|
|
498
|
+
# update the feature data
|
|
499
|
+
self.data['geometry'] = mapping(new_geom)
|
|
500
|
+
self._srid = out_srid
|
|
501
|
+
|
|
502
|
+
return self
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def to_sync(self, sync_client: 'SyncGeoboxClient') -> 'SyncFeature':
|
|
506
|
+
"""
|
|
507
|
+
[async] Switch to sync version of the feature instance to have access to the sync methods
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
sync_client (SyncGeoboxClient): The sync version of the GeoboxClient instance for making requests.
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
geobox.feature.Feature: the sync instance of the feature.
|
|
514
|
+
|
|
515
|
+
Example:
|
|
516
|
+
>>> from geobox import Geoboxclient
|
|
517
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
518
|
+
>>> client = GeoboxClient()
|
|
519
|
+
>>> async with AsyncGeoboxClient() as async_client:
|
|
520
|
+
>>> layer = await async_client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
|
|
521
|
+
>>> feature = await layer.get_feature(id=1, srid=3857)
|
|
522
|
+
>>> sync_feature = feature.to_sync(client)
|
|
523
|
+
"""
|
|
524
|
+
from ..feature import Feature as SyncFeature
|
|
525
|
+
|
|
526
|
+
sync_layer = self.layer.to_sync(sync_client=sync_client)
|
|
527
|
+
return SyncFeature(layer=sync_layer, srid=self.srid, data=self.data)
|