transformnd 0.1.0__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.
- transformnd/__init__.py +24 -0
- transformnd/adapters/__init__.py +34 -0
- transformnd/adapters/base.py +167 -0
- transformnd/adapters/bounding_box.py +16 -0
- transformnd/adapters/pandas.py +46 -0
- transformnd/adapters/polars.py +44 -0
- transformnd/adapters/shapely.py +124 -0
- transformnd/base.py +441 -0
- transformnd/extents/__init__.py +0 -0
- transformnd/extents/base.py +10 -0
- transformnd/extents/bounding_box.py +36 -0
- transformnd/graph.py +192 -0
- transformnd/py.typed +0 -0
- transformnd/transforms/__init__.py +19 -0
- transformnd/transforms/affine.py +512 -0
- transformnd/transforms/bijection.py +68 -0
- transformnd/transforms/by_dimension.py +133 -0
- transformnd/transforms/map_axis.py +67 -0
- transformnd/transforms/moving_least_squares.py +66 -0
- transformnd/transforms/reflection.py +207 -0
- transformnd/transforms/simple.py +172 -0
- transformnd/transforms/thinplate.py +83 -0
- transformnd/util.py +251 -0
- transformnd-0.1.0.dist-info/METADATA +133 -0
- transformnd-0.1.0.dist-info/RECORD +26 -0
- transformnd-0.1.0.dist-info/WHEEL +4 -0
transformnd/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
.. include:: ../../README.md
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .base import Transform, TransformSequence, TransformWrapper
|
|
6
|
+
from .util import SpaceRef, TransformSignature, check_ndim
|
|
7
|
+
from . import transforms
|
|
8
|
+
from . import adapters
|
|
9
|
+
from .graph import TransformGraph
|
|
10
|
+
from importlib.metadata import version as _version
|
|
11
|
+
|
|
12
|
+
__version__ = _version("transformnd")
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"Transform",
|
|
16
|
+
"TransformGraph",
|
|
17
|
+
"TransformSequence",
|
|
18
|
+
"TransformWrapper",
|
|
19
|
+
"TransformSignature",
|
|
20
|
+
"SpaceRef",
|
|
21
|
+
"check_ndim",
|
|
22
|
+
"transforms",
|
|
23
|
+
"adapters",
|
|
24
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Adapters for transforming objects which are not well-behaved numpy arrays.
|
|
2
|
+
|
|
3
|
+
Adapter instances are callables which take the transform to be applied,
|
|
4
|
+
the object to apply it to, and optionally some other arguments.
|
|
5
|
+
The adapter knows how to get coordinates out of the object,
|
|
6
|
+
and then create a new object with those transformed coordinates.
|
|
7
|
+
|
|
8
|
+
Classes which compose over transformable objects can be adapted with the
|
|
9
|
+
`AttrAdapter` class.
|
|
10
|
+
See the `SimpleAdapter` or `FnAdapter` for wrapping simple adapting functions.
|
|
11
|
+
Implement your own adapter by inheriting from `BaseAdapter`.
|
|
12
|
+
|
|
13
|
+
See `.pandas.DataFrameAdapter` for an example of creating an adapter
|
|
14
|
+
for an external type.
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .base import (
|
|
19
|
+
AttrAdapter,
|
|
20
|
+
BaseAdapter,
|
|
21
|
+
FnAdapter,
|
|
22
|
+
NullAdapter,
|
|
23
|
+
ReshapeAdapter,
|
|
24
|
+
SimpleAdapter,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"BaseAdapter",
|
|
29
|
+
"SimpleAdapter",
|
|
30
|
+
"NullAdapter",
|
|
31
|
+
"FnAdapter",
|
|
32
|
+
"AttrAdapter",
|
|
33
|
+
"ReshapeAdapter",
|
|
34
|
+
]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Simple adapter cases."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from functools import partial
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import TypeVar, Any
|
|
8
|
+
|
|
9
|
+
from array_api_compat import array_namespace
|
|
10
|
+
|
|
11
|
+
from ..base import Transform, ArrayT
|
|
12
|
+
|
|
13
|
+
ObjectT = TypeVar("ObjectT")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseAdapter[ObjectT, ArrayT](ABC):
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def apply(self, transform: Transform[ArrayT], obj: ObjectT) -> ObjectT:
|
|
19
|
+
"""Apply the given transformation to a non-array object.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
transform : Transform
|
|
24
|
+
obj : T
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
T
|
|
29
|
+
"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def partial(self, *args, **kwargs) -> Callable[..., ObjectT]:
|
|
33
|
+
"""Create a partial function with frozen arguments.
|
|
34
|
+
|
|
35
|
+
Useful for applying the same transform to many objects,
|
|
36
|
+
or many transforms to the same object,
|
|
37
|
+
or for adapters with additional arguments,
|
|
38
|
+
using the same config repeatedly.
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
Callable
|
|
43
|
+
"""
|
|
44
|
+
return partial(self.apply, *args, **kwargs)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class NullAdapter(BaseAdapter[ArrayT, ArrayT]):
|
|
48
|
+
"""Adapter which simply applies the transform."""
|
|
49
|
+
|
|
50
|
+
def apply(self, transform: Transform[ArrayT], obj: ArrayT) -> ArrayT:
|
|
51
|
+
return transform.apply(obj)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class FnAdapter(BaseAdapter[ObjectT, ArrayT]):
|
|
55
|
+
def __init__(self, fn: Callable[[Transform[ArrayT], ObjectT], ObjectT]):
|
|
56
|
+
"""Adapter which simply wraps a function, for typing purposes.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
fn : Callable[[Transform, T], T]
|
|
61
|
+
Function which takes the object,
|
|
62
|
+
and applies the transformation to it.
|
|
63
|
+
"""
|
|
64
|
+
self.fn = fn
|
|
65
|
+
|
|
66
|
+
def apply(self, transform: Transform[ArrayT], obj: ObjectT) -> ObjectT:
|
|
67
|
+
return self.fn(transform, obj)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class AttrAdapter(BaseAdapter[ObjectT, ArrayT]):
|
|
71
|
+
def __init__(self, **kwargs: BaseAdapter[Any, ArrayT] | None) -> None:
|
|
72
|
+
"""Adapter which transforms an object by applying transforms to its attributes.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
adapters : Dict[str, Optional[BaseAdapter]]
|
|
77
|
+
Keys are attribute names, values are adapters with which
|
|
78
|
+
to apply the transform to those attributes.
|
|
79
|
+
`None` is shorthand for `NullAdapter()`;
|
|
80
|
+
i.e. the attribute is an array and can be transformed
|
|
81
|
+
without being adapted.
|
|
82
|
+
"""
|
|
83
|
+
self.adapters = {
|
|
84
|
+
k: NullAdapter[ArrayT]() if v is None else v for k, v in kwargs.items()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
def apply(
|
|
88
|
+
self, transform: Transform[ArrayT], obj: ObjectT, in_place: bool = False
|
|
89
|
+
) -> ObjectT:
|
|
90
|
+
"""Apply the given transformation to the object, via its attributes.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
transform : Transform
|
|
95
|
+
obj : T
|
|
96
|
+
in_place : bool, optional
|
|
97
|
+
Whether to mutate the given object in place,
|
|
98
|
+
by default False (i.e. make a deep copy of it).
|
|
99
|
+
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
T
|
|
103
|
+
"""
|
|
104
|
+
if not in_place:
|
|
105
|
+
obj = deepcopy(obj)
|
|
106
|
+
|
|
107
|
+
for k, v in self.adapters.items():
|
|
108
|
+
member = getattr(obj, k)
|
|
109
|
+
try:
|
|
110
|
+
transformed = v.apply(transform, member, in_place=True) # type: ignore
|
|
111
|
+
except TypeError as e:
|
|
112
|
+
if "got an unexpected keyword argument 'in_place'" in str(e):
|
|
113
|
+
transformed = v.apply(transform, member)
|
|
114
|
+
else:
|
|
115
|
+
raise e
|
|
116
|
+
setattr(obj, k, transformed)
|
|
117
|
+
|
|
118
|
+
return obj
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class SimpleAdapter(BaseAdapter[ObjectT, ArrayT], ABC):
|
|
122
|
+
"""
|
|
123
|
+
Helper class for cases with simple conversion methods.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
@abstractmethod
|
|
127
|
+
def _to_array(self, obj: ObjectT) -> ArrayT:
|
|
128
|
+
"""Convert the object into an array of coordinates."""
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
@abstractmethod
|
|
132
|
+
def _from_array(self, coords: ArrayT) -> ObjectT:
|
|
133
|
+
"""Convert an array of coordinates into the correct type."""
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
def apply(self, transform: Transform[ArrayT], obj: ObjectT) -> ObjectT:
|
|
137
|
+
coords: ArrayT = self._to_array(obj)
|
|
138
|
+
transformed = transform.apply(coords)
|
|
139
|
+
return self._from_array(transformed)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class ReshapeAdapter(BaseAdapter[ArrayT, ArrayT]):
|
|
143
|
+
"""Adapter which reshapes a numpy.ndarray"""
|
|
144
|
+
|
|
145
|
+
def __init__(self, dim_axis: int = -1) -> None:
|
|
146
|
+
"""Adapt numpy arrays which are not of the correct shape.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
dim_axis : int, optional
|
|
151
|
+
Which axis contains the coordinates' dimensions,
|
|
152
|
+
by default -1 (last)
|
|
153
|
+
"""
|
|
154
|
+
self.dim_axis: int = dim_axis
|
|
155
|
+
|
|
156
|
+
def apply(self, transform: Transform[ArrayT], obj: ArrayT) -> ArrayT:
|
|
157
|
+
xp = array_namespace(obj)
|
|
158
|
+
dim_axis = self.dim_axis
|
|
159
|
+
if self.dim_axis < 0:
|
|
160
|
+
dim_axis += xp.ndim(obj)
|
|
161
|
+
|
|
162
|
+
moved = xp.moveaxis(obj, dim_axis, -1)
|
|
163
|
+
m_shape = moved.shape
|
|
164
|
+
|
|
165
|
+
flattened = xp.reshape(moved, (-1, m_shape[-1]))
|
|
166
|
+
transformed = transform.apply(flattened)
|
|
167
|
+
return xp.moveaxis(xp.reshape(transformed, m_shape), -1, dim_axis)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from array_api_compat import array_namespace
|
|
2
|
+
from transformnd.base import Transform
|
|
3
|
+
from .base import BaseAdapter
|
|
4
|
+
from ..extents.bounding_box import BoundingBox
|
|
5
|
+
from ..base import ArrayT
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BoundingBoxAdapter(BaseAdapter[BoundingBox[ArrayT], ArrayT]):
|
|
9
|
+
def apply(
|
|
10
|
+
self, transform: Transform[ArrayT], obj: BoundingBox[ArrayT]
|
|
11
|
+
) -> BoundingBox[ArrayT]:
|
|
12
|
+
xp = array_namespace(obj.mins)
|
|
13
|
+
stacked = xp.stack([obj.mins, obj.maxes])
|
|
14
|
+
transformed = transform.apply(stacked)
|
|
15
|
+
mins, maxes = xp.unstack(transformed)
|
|
16
|
+
return BoundingBox(mins, maxes)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Adapt pandas DataFrames for transformation."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Hashable
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from ..base import Transform
|
|
9
|
+
from .base import BaseAdapter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PandasAdapter(BaseAdapter[pd.DataFrame, np.ndarray]):
|
|
13
|
+
def __init__(self, columns: list[Hashable]):
|
|
14
|
+
"""Adapt transformation for coordinates stored in a pandas DataFrame.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
columns : list of keys
|
|
19
|
+
Keys for columns containing coordinates, e.g. `["x", "y", "z"]`
|
|
20
|
+
"""
|
|
21
|
+
self.columns = columns
|
|
22
|
+
|
|
23
|
+
def apply(
|
|
24
|
+
self, transform: Transform, df: pd.DataFrame, in_place: bool = False
|
|
25
|
+
) -> pd.DataFrame:
|
|
26
|
+
"""Transform the dataframe, optionally in-place.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
transform : Transform
|
|
31
|
+
df : pd.DataFrame
|
|
32
|
+
|
|
33
|
+
in_place : bool, optional
|
|
34
|
+
Whether to mutate the dataframe in place,
|
|
35
|
+
by default False (i.e. make a copy of it).
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
-------
|
|
39
|
+
pandas.DataFrame
|
|
40
|
+
"""
|
|
41
|
+
coords = df[self.columns].to_numpy()
|
|
42
|
+
transformed = transform.apply(coords)
|
|
43
|
+
if not in_place:
|
|
44
|
+
df = df.copy()
|
|
45
|
+
df[self.columns] = transformed
|
|
46
|
+
return df
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Adapt polars DataFrames for transformation."""
|
|
2
|
+
|
|
3
|
+
import polars as pl
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from ..base import Transform
|
|
7
|
+
from .base import BaseAdapter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PolarsAdapter(BaseAdapter[pl.DataFrame, np.ndarray]):
|
|
11
|
+
def __init__(self, columns: list[str]):
|
|
12
|
+
"""Adapt transformation for coordinates stored in a polars DataFrame.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
columns : list of keys
|
|
17
|
+
Keys for columns containing coordinates, e.g. `["x", "y", "z"]`
|
|
18
|
+
"""
|
|
19
|
+
self.columns = columns
|
|
20
|
+
|
|
21
|
+
def apply(
|
|
22
|
+
self, transform: Transform, df: pl.DataFrame, in_place: bool = False
|
|
23
|
+
) -> pl.DataFrame:
|
|
24
|
+
"""Transform the dataframe, optionally in-place.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
transform : Transform
|
|
29
|
+
df : pl.DataFrame
|
|
30
|
+
|
|
31
|
+
in_place : bool, optional
|
|
32
|
+
Whether to mutate the dataframe in place,
|
|
33
|
+
by default False (i.e. make a copy of it).
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
pandas.DataFrame
|
|
38
|
+
"""
|
|
39
|
+
coords = df[self.columns].to_numpy()
|
|
40
|
+
transformed = transform.apply(coords)
|
|
41
|
+
if not in_place:
|
|
42
|
+
df = df.clone()
|
|
43
|
+
df[self.columns] = transformed
|
|
44
|
+
return df
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Callable
|
|
3
|
+
from shapely.geometry import (
|
|
4
|
+
LinearRing,
|
|
5
|
+
LineString,
|
|
6
|
+
MultiLineString,
|
|
7
|
+
MultiPoint,
|
|
8
|
+
MultiPolygon,
|
|
9
|
+
Point,
|
|
10
|
+
Polygon,
|
|
11
|
+
GeometryCollection,
|
|
12
|
+
)
|
|
13
|
+
from shapely.geometry.base import BaseGeometry, BaseMultipartGeometry
|
|
14
|
+
from shapely.coords import CoordinateSequence
|
|
15
|
+
|
|
16
|
+
from ..base import Transform, ArrayT
|
|
17
|
+
from .base import BaseAdapter
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def as_numpy(coords: CoordinateSequence) -> np.ndarray:
|
|
21
|
+
return np.asarray(coords)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GeometryAdapter(BaseAdapter[BaseGeometry, ArrayT]):
|
|
25
|
+
"""Transform shapely geometries.
|
|
26
|
+
|
|
27
|
+
As well as the generic `apply()`,
|
|
28
|
+
there are `apply_*()` methods for transforming different geometry subclasses.
|
|
29
|
+
|
|
30
|
+
N.B. some transforms may create invalid topologies
|
|
31
|
+
(incorrect winding, self-intersections etc.)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
array_fn: Callable[[CoordinateSequence], ArrayT] = as_numpy, # type:ignore
|
|
37
|
+
) -> None:
|
|
38
|
+
self.array_fn = array_fn
|
|
39
|
+
|
|
40
|
+
def apply_point(self, transform: Transform, point: Point) -> Point:
|
|
41
|
+
return Point(*transform.apply(self.array_fn(point.coords))[0])
|
|
42
|
+
|
|
43
|
+
def apply_linestring(
|
|
44
|
+
self, transform: Transform, linestring: LineString
|
|
45
|
+
) -> LineString:
|
|
46
|
+
return LineString(transform.apply(self.array_fn(linestring.coords)))
|
|
47
|
+
|
|
48
|
+
def apply_linear_ring(
|
|
49
|
+
self, transform: Transform, linear_ring: LinearRing
|
|
50
|
+
) -> LinearRing:
|
|
51
|
+
return LinearRing(transform.apply(self.array_fn(linear_ring.coords)))
|
|
52
|
+
|
|
53
|
+
def apply_polygon(self, transform: Transform, polygon: Polygon) -> Polygon:
|
|
54
|
+
return Polygon(
|
|
55
|
+
self.apply_linear_ring(transform, polygon.exterior),
|
|
56
|
+
[self.apply_linear_ring(transform, i) for i in polygon.interiors],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def apply_multipoint(self, transform: Transform, obj: MultiPoint) -> MultiPoint:
|
|
60
|
+
return MultiPoint([self.apply_point(transform, o) for o in obj.geoms])
|
|
61
|
+
|
|
62
|
+
def apply_multilinestring(
|
|
63
|
+
self, transform: Transform, obj: MultiLineString
|
|
64
|
+
) -> MultiLineString:
|
|
65
|
+
return MultiLineString([self.apply_linestring(transform, o) for o in obj.geoms])
|
|
66
|
+
|
|
67
|
+
def apply_multipolygon(
|
|
68
|
+
self, transform: Transform, obj: MultiPolygon
|
|
69
|
+
) -> MultiPolygon:
|
|
70
|
+
return MultiPolygon([self.apply_polygon(transform, o) for o in obj.geoms])
|
|
71
|
+
|
|
72
|
+
def apply_multipart(
|
|
73
|
+
self, transform: Transform, obj: BaseMultipartGeometry
|
|
74
|
+
) -> BaseMultipartGeometry:
|
|
75
|
+
"""Apply the transform to any shapely multipart geometry."""
|
|
76
|
+
if isinstance(obj, MultiPoint):
|
|
77
|
+
return self.apply_multipoint(transform, obj)
|
|
78
|
+
elif isinstance(obj, MultiLineString):
|
|
79
|
+
return self.apply_multilinestring(transform, obj)
|
|
80
|
+
elif isinstance(obj, MultiPolygon):
|
|
81
|
+
return self.apply_multipolygon(transform, obj)
|
|
82
|
+
elif isinstance(obj, GeometryCollection):
|
|
83
|
+
return self.apply_collection(transform, obj)
|
|
84
|
+
else:
|
|
85
|
+
raise ValueError(f"Unknown multipart geometry type {type(obj)}")
|
|
86
|
+
|
|
87
|
+
def apply_collection(
|
|
88
|
+
self, transform: Transform, obj: GeometryCollection
|
|
89
|
+
) -> GeometryCollection:
|
|
90
|
+
return GeometryCollection([self.apply(transform, o) for o in obj.geoms])
|
|
91
|
+
|
|
92
|
+
def apply(
|
|
93
|
+
self,
|
|
94
|
+
transform: Transform,
|
|
95
|
+
obj: BaseGeometry,
|
|
96
|
+
) -> BaseGeometry:
|
|
97
|
+
"""Transform the shapely geometry.
|
|
98
|
+
|
|
99
|
+
See the other `apply_*` methods if you already know what type of geometry you're working with;
|
|
100
|
+
this may be a bit faster.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
transform : Transform
|
|
105
|
+
obj : BaseGeometry
|
|
106
|
+
Some shapely geometry in 2 or 3D
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
BaseGeometry
|
|
111
|
+
An object of the same type as the input.
|
|
112
|
+
"""
|
|
113
|
+
if isinstance(obj, BaseMultipartGeometry):
|
|
114
|
+
return self.apply_multipart(transform, obj)
|
|
115
|
+
elif isinstance(obj, Point):
|
|
116
|
+
return self.apply_point(transform, obj)
|
|
117
|
+
elif isinstance(obj, LineString):
|
|
118
|
+
return self.apply_linestring(transform, obj)
|
|
119
|
+
elif isinstance(obj, LinearRing):
|
|
120
|
+
return self.apply_linear_ring(transform, obj)
|
|
121
|
+
elif isinstance(obj, Polygon):
|
|
122
|
+
return self.apply_polygon(transform, obj)
|
|
123
|
+
else:
|
|
124
|
+
raise ValueError(f"Unknown geometry type {type(obj)}")
|