cf-xarray 0.9.3__tar.gz → 0.9.4__tar.gz
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.
- cf_xarray-0.9.4/.github/release.yml +5 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/PKG-INFO +1 -1
- cf_xarray-0.9.4/cf_xarray/_version.py +1 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/geometry.py +354 -205
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/tests/test_geometry.py +7 -1
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray.egg-info/PKG-INFO +1 -1
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray.egg-info/SOURCES.txt +1 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/api.rst +1 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/geometry.md +2 -3
- cf_xarray-0.9.3/cf_xarray/_version.py +0 -1
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.binder/environment.yml +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.deepsource.toml +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.github/dependabot.yml +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.github/workflows/ci.yaml +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.github/workflows/parse_logs.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.github/workflows/pypi.yaml +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.github/workflows/testpypi-release.yaml +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.github/workflows/upstream-dev-ci.yaml +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.gitignore +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.pre-commit-config.yaml +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.readthedocs.yml +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.tributors +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/CITATION.cff +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/LICENSE +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/README.rst +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/__init__.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/accessor.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/coding.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/criteria.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/datasets.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/formatting.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/helpers.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/options.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/py.typed +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/scripts/make_doc.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/scripts/print_versions.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/sgrid.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/tests/__init__.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/tests/conftest.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/tests/test_accessor.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/tests/test_coding.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/tests/test_helpers.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/tests/test_options.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/tests/test_scripts.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/tests/test_units.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/units.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/utils.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray.egg-info/dependency_links.txt +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray.egg-info/requires.txt +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray.egg-info/top_level.txt +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/ci/doc.yml +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/ci/environment-no-optional-deps.yml +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/ci/environment.yml +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/ci/upstream-dev-env.yml +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/codecov.yml +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/2D_bounds_averaged.png +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/2D_bounds_error.png +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/2D_bounds_nonunique.png +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/Makefile +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/_static/dataset-diagram-logo.tex +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/_static/full-logo.png +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/_static/logo.png +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/_static/logo.svg +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/_static/rich-repr-example.png +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/_static/style.css +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/bounds.md +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/cartopy_rotated_pole.png +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/coding.md +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/conf.py +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/contributing.rst +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/coord_axes.md +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/custom-criteria.md +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/dsg.md +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/examples/introduction.ipynb +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/faq.md +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/flags.md +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/grid_mappings.md +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/howtouse.md +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/index.rst +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/make.bat +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/parametricz.md +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/plotting.md +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/provenance.md +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/quickstart.md +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/roadmap.rst +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/selecting.md +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/sgrid_ugrid.md +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/units.md +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/whats-new.rst +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/pyproject.toml +0 -0
- {cf_xarray-0.9.3 → cf_xarray-0.9.4}/setup.cfg +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.9.4"
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import copy
|
|
4
|
-
from collections
|
|
4
|
+
from collections import ChainMap
|
|
5
|
+
from collections.abc import Hashable, Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
5
7
|
|
|
6
8
|
import numpy as np
|
|
7
9
|
import pandas as pd
|
|
8
10
|
import xarray as xr
|
|
11
|
+
from numpy.typing import ArrayLike
|
|
9
12
|
|
|
10
13
|
GEOMETRY_CONTAINER_NAME = "geometry_container"
|
|
14
|
+
FEATURES_DIM_NAME = "features"
|
|
11
15
|
|
|
12
16
|
__all__ = [
|
|
13
17
|
"decode_geometries",
|
|
@@ -30,9 +34,149 @@ __all__ = [
|
|
|
30
34
|
# 1. node coordinates are exact; the 'normal' coordinates are a reasonable value to use, if you do not know how to interpret the nodes.
|
|
31
35
|
|
|
32
36
|
|
|
37
|
+
@dataclass
|
|
38
|
+
class GeometryNames:
|
|
39
|
+
"""Helper class to ease handling of all the variable names needed for CF geometries."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
suffix: str = "",
|
|
44
|
+
grid_mapping_name: str | None = None,
|
|
45
|
+
grid_mapping: str | None = None,
|
|
46
|
+
):
|
|
47
|
+
self.container_name: str = GEOMETRY_CONTAINER_NAME + suffix
|
|
48
|
+
self.node_dim: str = "node" + suffix
|
|
49
|
+
self.node_count: str = "node_count" + suffix
|
|
50
|
+
self.node_coordinates_x: str = "x" + suffix
|
|
51
|
+
self.node_coordinates_y: str = "y" + suffix
|
|
52
|
+
self.coordinates_x: str = "crd_x" + suffix
|
|
53
|
+
self.coordinates_y: str = "crd_y" + suffix
|
|
54
|
+
self.part_node_count: str = "part_node_count" + suffix
|
|
55
|
+
self.part_dim: str = "part" + suffix
|
|
56
|
+
self.interior_ring: str = "interior_ring" + suffix
|
|
57
|
+
self.attrs_x: dict[str, str] = {}
|
|
58
|
+
self.attrs_y: dict[str, str] = {}
|
|
59
|
+
self.grid_mapping_attr = {"grid_mapping": grid_mapping} if grid_mapping else {}
|
|
60
|
+
|
|
61
|
+
# Special treatment of selected grid mappings
|
|
62
|
+
if grid_mapping_name in ["latitude_longitude", "rotated_latitude_longitude"]:
|
|
63
|
+
# Special case for longitude_latitude type grid mappings
|
|
64
|
+
self.coordinates_x = "lon"
|
|
65
|
+
self.coordinates_y = "lat"
|
|
66
|
+
if grid_mapping_name == "latitude_longitude":
|
|
67
|
+
self.attrs_x = dict(units="degrees_east", standard_name="longitude")
|
|
68
|
+
self.attrs_y = dict(units="degrees_north", standard_name="latitude")
|
|
69
|
+
elif grid_mapping_name == "rotated_latitude_longitude":
|
|
70
|
+
self.attrs_x = dict(
|
|
71
|
+
units="degrees_east", standard_name="grid_longitude"
|
|
72
|
+
)
|
|
73
|
+
self.attrs_y = dict(
|
|
74
|
+
units="degrees_north", standard_name="grid_latitude"
|
|
75
|
+
)
|
|
76
|
+
elif grid_mapping_name is not None:
|
|
77
|
+
self.attrs_x = dict(standard_name="projection_x_coordinate")
|
|
78
|
+
self.attrs_y = dict(standard_name="projection_y_coordinate")
|
|
79
|
+
self.attrs_x.update(self.grid_mapping_attr)
|
|
80
|
+
self.attrs_y.update(self.grid_mapping_attr)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def geometry_container_attrs(self) -> dict[str, str]:
|
|
84
|
+
return {
|
|
85
|
+
"node_count": self.node_count,
|
|
86
|
+
"node_coordinates": f"{self.node_coordinates_x} {self.node_coordinates_y}",
|
|
87
|
+
"coordinates": f"{self.coordinates_x} {self.coordinates_y}",
|
|
88
|
+
**self.grid_mapping_attr,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
def coords(
|
|
92
|
+
self,
|
|
93
|
+
*,
|
|
94
|
+
dim: Hashable,
|
|
95
|
+
x: ArrayLike,
|
|
96
|
+
y: ArrayLike,
|
|
97
|
+
crdX: ArrayLike | None = None,
|
|
98
|
+
crdY: ArrayLike | None = None,
|
|
99
|
+
) -> dict[str, xr.DataArray]:
|
|
100
|
+
"""
|
|
101
|
+
Construct coordinate DataArrays for the numpy data (x, y, crdX, crdY)
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
x: array
|
|
106
|
+
Node coordinates for X coordinate
|
|
107
|
+
y: array
|
|
108
|
+
Node coordinates for Y coordinate
|
|
109
|
+
crdX: array, optional
|
|
110
|
+
Nominal X coordinate
|
|
111
|
+
crdY: array, optional
|
|
112
|
+
Nominal X coordinate
|
|
113
|
+
"""
|
|
114
|
+
mapping = {
|
|
115
|
+
self.node_coordinates_x: xr.DataArray(
|
|
116
|
+
x, dims=self.node_dim, attrs={"axis": "X", **self.attrs_x}
|
|
117
|
+
),
|
|
118
|
+
self.node_coordinates_y: xr.DataArray(
|
|
119
|
+
y, dims=self.node_dim, attrs={"axis": "Y", **self.attrs_y}
|
|
120
|
+
),
|
|
121
|
+
}
|
|
122
|
+
if crdX is not None:
|
|
123
|
+
mapping[self.coordinates_x] = xr.DataArray(
|
|
124
|
+
crdX,
|
|
125
|
+
dims=(dim,),
|
|
126
|
+
attrs={"nodes": self.node_coordinates_x, **self.attrs_x},
|
|
127
|
+
)
|
|
128
|
+
if crdY is not None:
|
|
129
|
+
mapping[self.coordinates_y] = xr.DataArray(
|
|
130
|
+
crdY,
|
|
131
|
+
dims=(dim,),
|
|
132
|
+
attrs={"nodes": self.node_coordinates_y, **self.attrs_y},
|
|
133
|
+
)
|
|
134
|
+
return mapping
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _assert_single_geometry_container(ds: xr.Dataset) -> Hashable:
|
|
138
|
+
container_names = _get_geometry_containers(ds)
|
|
139
|
+
if len(container_names) > 1:
|
|
140
|
+
raise ValueError(
|
|
141
|
+
"Only one geometry container is supported by cf_to_points. "
|
|
142
|
+
"To handle multiple geometries use `decode_geometries` instead."
|
|
143
|
+
)
|
|
144
|
+
(container_name,) = container_names
|
|
145
|
+
return container_name
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _get_geometry_containers(obj: xr.DataArray | xr.Dataset) -> list[Hashable]:
|
|
149
|
+
"""
|
|
150
|
+
Translate from key (either CF key or variable name) to its bounds' variable names.
|
|
151
|
+
|
|
152
|
+
This function interprets the ``geometry`` attribute on DataArrays.
|
|
153
|
+
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
obj : DataArray, Dataset
|
|
157
|
+
DataArray belonging to the coordinate to be checked
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
List[str]
|
|
162
|
+
Variable name(s) in parent xarray object that are bounds of `key`
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
if isinstance(obj, xr.DataArray):
|
|
166
|
+
obj = obj._to_temp_dataset()
|
|
167
|
+
variables = obj._variables
|
|
168
|
+
|
|
169
|
+
results = set()
|
|
170
|
+
for name, var in variables.items():
|
|
171
|
+
attrs_or_encoding = ChainMap(var.attrs, var.encoding)
|
|
172
|
+
if "geometry_type" in attrs_or_encoding:
|
|
173
|
+
results.update([name])
|
|
174
|
+
return list(results)
|
|
175
|
+
|
|
176
|
+
|
|
33
177
|
def decode_geometries(encoded: xr.Dataset) -> xr.Dataset:
|
|
34
178
|
"""
|
|
35
|
-
Decode CF encoded geometries to
|
|
179
|
+
Decode CF encoded geometries to numpy object arrays containing shapely geometries.
|
|
36
180
|
|
|
37
181
|
Parameters
|
|
38
182
|
----------
|
|
@@ -50,46 +194,57 @@ def decode_geometries(encoded: xr.Dataset) -> xr.Dataset:
|
|
|
50
194
|
cf_to_shapely
|
|
51
195
|
encode_geometries
|
|
52
196
|
"""
|
|
53
|
-
|
|
197
|
+
|
|
198
|
+
containers = _get_geometry_containers(encoded)
|
|
199
|
+
if not containers:
|
|
54
200
|
raise NotImplementedError(
|
|
55
|
-
|
|
56
|
-
"
|
|
201
|
+
"No geometry container variables detected, none of the provided variables "
|
|
202
|
+
"have a `geometry_type` attribute."
|
|
57
203
|
)
|
|
58
204
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
205
|
+
todrop: list[Hashable] = []
|
|
206
|
+
decoded = xr.Dataset()
|
|
207
|
+
for container_name in containers:
|
|
208
|
+
enc_geom_var = encoded[container_name]
|
|
209
|
+
geom_attrs = enc_geom_var.attrs
|
|
210
|
+
|
|
211
|
+
# Grab the coordinates attribute
|
|
212
|
+
geom_attrs.update(enc_geom_var.encoding)
|
|
213
|
+
|
|
214
|
+
geom_var = cf_to_shapely(encoded, container=container_name).variable
|
|
215
|
+
|
|
216
|
+
todrop.extend(
|
|
217
|
+
(container_name,)
|
|
218
|
+
+ tuple(
|
|
219
|
+
s
|
|
220
|
+
for s in " ".join(
|
|
221
|
+
geom_attrs.get(attr, "")
|
|
222
|
+
for attr in [
|
|
223
|
+
"interior_ring",
|
|
224
|
+
"node_coordinates",
|
|
225
|
+
"node_count",
|
|
226
|
+
"part_node_count",
|
|
227
|
+
"coordinates",
|
|
228
|
+
]
|
|
229
|
+
).split(" ")
|
|
230
|
+
if s
|
|
231
|
+
)
|
|
86
232
|
)
|
|
87
|
-
|
|
88
|
-
|
|
233
|
+
|
|
234
|
+
name = geom_attrs.get("variable_name", None)
|
|
235
|
+
if name in encoded.dims:
|
|
236
|
+
decoded = decoded.assign_coords(
|
|
237
|
+
xr.Coordinates(coords={name: geom_var}, indexes={})
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
decoded[name] = geom_var
|
|
241
|
+
|
|
242
|
+
decoded.update(encoded.drop_vars(todrop))
|
|
89
243
|
|
|
90
244
|
# Is this a good idea? We are deleting information.
|
|
245
|
+
# OTOH we have decoded it to a useful in-memory representation
|
|
91
246
|
for var in decoded._variables.values():
|
|
92
|
-
if var.attrs.get("geometry")
|
|
247
|
+
if var.attrs.get("geometry") in containers:
|
|
93
248
|
var.attrs.pop("geometry")
|
|
94
249
|
return decoded
|
|
95
250
|
|
|
@@ -101,11 +256,6 @@ def encode_geometries(ds: xr.Dataset):
|
|
|
101
256
|
Practically speaking, geometry variables are numpy object arrays where the first
|
|
102
257
|
element is a shapely geometry.
|
|
103
258
|
|
|
104
|
-
.. warning::
|
|
105
|
-
|
|
106
|
-
Only a single geometry variable is supported at present. Contributions to fix this
|
|
107
|
-
are welcome.
|
|
108
|
-
|
|
109
259
|
Parameters
|
|
110
260
|
----------
|
|
111
261
|
ds : Dataset
|
|
@@ -114,7 +264,9 @@ def encode_geometries(ds: xr.Dataset):
|
|
|
114
264
|
Returns
|
|
115
265
|
-------
|
|
116
266
|
Dataset
|
|
117
|
-
Where all geometry variables are encoded.
|
|
267
|
+
Where all geometry variables are encoded. The information in a single geometry
|
|
268
|
+
variable in the input is split across multiple variables in the returned Dataset
|
|
269
|
+
following the CF conventions.
|
|
118
270
|
|
|
119
271
|
See Also
|
|
120
272
|
--------
|
|
@@ -151,58 +303,57 @@ def encode_geometries(ds: xr.Dataset):
|
|
|
151
303
|
# e.g. xvec GeometryIndex
|
|
152
304
|
ds = ds.drop_indexes(to_drop)
|
|
153
305
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
306
|
+
variables = {}
|
|
307
|
+
for name in geom_var_names:
|
|
308
|
+
# TODO: do we prefer this choice be invariant to number of geometry variables
|
|
309
|
+
suffix = "_" + str(name) if len(geom_var_names) > 1 else ""
|
|
310
|
+
container_name = GEOMETRY_CONTAINER_NAME + suffix
|
|
311
|
+
# If `name` is a dimension name, then we need to drop it. Otherwise we don't
|
|
312
|
+
# So set errors="ignore"
|
|
313
|
+
variables.update(
|
|
314
|
+
shapely_to_cf(ds[name], suffix=suffix)
|
|
315
|
+
.drop_vars(name, errors="ignore")
|
|
316
|
+
._variables
|
|
159
317
|
)
|
|
160
318
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
319
|
+
geom_var = ds[name]
|
|
320
|
+
more_updates = {}
|
|
321
|
+
for varname, var in ds._variables.items():
|
|
322
|
+
if varname == name:
|
|
323
|
+
continue
|
|
324
|
+
# TODO: this is incomplete. It works for vector data cubes where one of the geometry vars
|
|
325
|
+
# is a dimension coordinate.
|
|
326
|
+
if name in var.dims:
|
|
327
|
+
var = var.copy()
|
|
328
|
+
var._attrs = copy.deepcopy(var._attrs)
|
|
329
|
+
var.attrs["geometry"] = container_name
|
|
330
|
+
# The grid_mapping and coordinates attributes can be carried by the geometry container
|
|
331
|
+
# variable provided they are also carried by the data variables associated with the container.
|
|
332
|
+
if to_add := geom_var.attrs.get("coordinates", ""):
|
|
333
|
+
var.attrs["coordinates"] = var.attrs.get("coordinates", "") + to_add
|
|
334
|
+
more_updates[varname] = var
|
|
335
|
+
variables.update(more_updates)
|
|
336
|
+
|
|
337
|
+
# WARNING: cf-xarray specific convention.
|
|
338
|
+
# For vector data cubes, `name` is a dimension name.
|
|
339
|
+
# By encoding to CF, we have
|
|
340
|
+
# encoded the information in that variable across many different
|
|
341
|
+
# variables (e.g. node_count) with `name` as a dimension.
|
|
342
|
+
# We have to record `name` somewhere so that we reconstruct
|
|
343
|
+
# a geometry variable of the right name at decode-time.
|
|
344
|
+
variables[container_name].attrs["variable_name"] = name
|
|
345
|
+
|
|
346
|
+
encoded = xr.Dataset(variables).set_coords(
|
|
347
|
+
set(ds._coord_names) - set(geom_var_names)
|
|
167
348
|
)
|
|
168
349
|
|
|
169
|
-
geom_var = ds[name]
|
|
170
|
-
|
|
171
|
-
more_updates = {}
|
|
172
|
-
for varname, var in ds._variables.items():
|
|
173
|
-
if varname == name:
|
|
174
|
-
continue
|
|
175
|
-
# TODO: this is incomplete. It works for vector data cubes where one of the geometry vars
|
|
176
|
-
# is a dimension coordinate.
|
|
177
|
-
if name in var.dims:
|
|
178
|
-
var = var.copy()
|
|
179
|
-
var._attrs = copy.deepcopy(var._attrs)
|
|
180
|
-
var.attrs["geometry"] = GEOMETRY_CONTAINER_NAME
|
|
181
|
-
# The grid_mapping and coordinates attributes can be carried by the geometry container
|
|
182
|
-
# variable provided they are also carried by the data variables associated with the container.
|
|
183
|
-
if to_add := geom_var.attrs.get("coordinates", ""):
|
|
184
|
-
var.attrs["coordinates"] = var.attrs.get("coordinates", "") + to_add
|
|
185
|
-
more_updates[varname] = var
|
|
186
|
-
variables.update(more_updates)
|
|
187
|
-
|
|
188
|
-
# WARNING: cf-xarray specific convention.
|
|
189
|
-
# For vector data cubes, `name` is a dimension name.
|
|
190
|
-
# By encoding to CF, we have
|
|
191
|
-
# encoded the information in that variable across many different
|
|
192
|
-
# variables (e.g. node_count) with `name` as a dimension.
|
|
193
|
-
# We have to record `name` somewhere so that we reconstruct
|
|
194
|
-
# a geometry variable of the right name at decode-time.
|
|
195
|
-
variables[GEOMETRY_CONTAINER_NAME].attrs["variable_name"] = name
|
|
196
|
-
|
|
197
|
-
encoded = xr.Dataset(variables)
|
|
198
|
-
|
|
199
350
|
return encoded
|
|
200
351
|
|
|
201
352
|
|
|
202
353
|
def reshape_unique_geometries(
|
|
203
354
|
ds: xr.Dataset,
|
|
204
355
|
geom_var: str = "geometry",
|
|
205
|
-
new_dim: str =
|
|
356
|
+
new_dim: str = FEATURES_DIM_NAME,
|
|
206
357
|
) -> xr.Dataset:
|
|
207
358
|
"""Reshape a dataset containing a geometry variable so that all unique features are
|
|
208
359
|
identified along a new dimension.
|
|
@@ -263,7 +414,12 @@ def reshape_unique_geometries(
|
|
|
263
414
|
return out
|
|
264
415
|
|
|
265
416
|
|
|
266
|
-
def shapely_to_cf(
|
|
417
|
+
def shapely_to_cf(
|
|
418
|
+
geometries: xr.DataArray | Sequence,
|
|
419
|
+
grid_mapping: str | None = None,
|
|
420
|
+
*,
|
|
421
|
+
suffix: str = "",
|
|
422
|
+
):
|
|
267
423
|
"""
|
|
268
424
|
Convert a DataArray with shapely geometry objects into a CF-compliant dataset.
|
|
269
425
|
|
|
@@ -277,8 +433,8 @@ def shapely_to_cf(geometries: xr.DataArray | Sequence, grid_mapping: str | None
|
|
|
277
433
|
A CF grid mapping name. When given, coordinates and attributes are named and set accordingly.
|
|
278
434
|
Defaults to None, in which case the coordinates are simply names "crd_x" and "crd_y".
|
|
279
435
|
|
|
280
|
-
|
|
281
|
-
|
|
436
|
+
container_name: str, optional
|
|
437
|
+
Name for the "geometry container" scalar variable in the encoded Dataset
|
|
282
438
|
|
|
283
439
|
Returns
|
|
284
440
|
-------
|
|
@@ -289,7 +445,7 @@ def shapely_to_cf(geometries: xr.DataArray | Sequence, grid_mapping: str | None
|
|
|
289
445
|
- 'node_count' : The number of nodes per feature. Always present for Lines and Polygons. For Points: only present if there are multipart geometries.
|
|
290
446
|
- 'part_node_count' : The number of nodes per individual geometry. Only for Lines with multipart geometries and for Polygons with multipart geometries or holes.
|
|
291
447
|
- 'interior_ring' : Integer boolean indicating whether rings are interior or exterior. Only for Polygons with holes.
|
|
292
|
-
-
|
|
448
|
+
- container_name : Empty variable with attributes describing the geometry type.
|
|
293
449
|
|
|
294
450
|
References
|
|
295
451
|
----------
|
|
@@ -308,56 +464,39 @@ def shapely_to_cf(geometries: xr.DataArray | Sequence, grid_mapping: str | None
|
|
|
308
464
|
geom.item().geom_type if isinstance(geom, xr.DataArray) else geom.geom_type
|
|
309
465
|
for geom in geometries
|
|
310
466
|
}
|
|
311
|
-
if types.issubset({"Point", "MultiPoint"}):
|
|
312
|
-
ds = points_to_cf(geometries)
|
|
313
|
-
elif types.issubset({"LineString", "MultiLineString"}):
|
|
314
|
-
ds = lines_to_cf(geometries)
|
|
315
|
-
elif types.issubset({"Polygon", "MultiPolygon"}):
|
|
316
|
-
ds = polygons_to_cf(geometries)
|
|
317
|
-
else:
|
|
318
|
-
raise ValueError(
|
|
319
|
-
f"Mixed geometry types are not supported in CF-compliant datasets. Got {types}"
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
ds[GEOMETRY_CONTAINER_NAME].attrs.update(coordinates="crd_x crd_y")
|
|
323
467
|
|
|
468
|
+
grid_mapping_varname = None
|
|
324
469
|
if (
|
|
325
470
|
grid_mapping is None
|
|
326
471
|
and isinstance(geometries, xr.DataArray)
|
|
327
472
|
and (grid_mapping_varname := geometries.attrs.get("grid_mapping"))
|
|
328
473
|
):
|
|
329
474
|
if grid_mapping_varname in geometries.coords:
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
ds[GEOMETRY_CONTAINER_NAME].attrs.update(coordinates="lon lat")
|
|
351
|
-
elif grid_mapping is not None:
|
|
352
|
-
ds.crd_x.attrs.update(standard_name="projection_x_coordinate")
|
|
353
|
-
ds.x.attrs.update(standard_name="projection_x_coordinate")
|
|
354
|
-
ds.crd_y.attrs.update(standard_name="projection_y_coordinate")
|
|
355
|
-
ds.y.attrs.update(standard_name="projection_y_coordinate")
|
|
475
|
+
# Not all CRS can be encoded in CF
|
|
476
|
+
grid_mapping = geometries.coords[grid_mapping_varname].attrs.get(
|
|
477
|
+
"grid_mapping_name", None
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
# TODO: consider accepting a GeometryNames instance from the user instead
|
|
481
|
+
names = GeometryNames(
|
|
482
|
+
suffix=suffix, grid_mapping_name=grid_mapping, grid_mapping=grid_mapping_varname
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
if types.issubset({"Point", "MultiPoint"}):
|
|
486
|
+
ds = points_to_cf(geometries, names=names)
|
|
487
|
+
elif types.issubset({"LineString", "MultiLineString"}):
|
|
488
|
+
ds = lines_to_cf(geometries, names=names)
|
|
489
|
+
elif types.issubset({"Polygon", "MultiPolygon"}):
|
|
490
|
+
ds = polygons_to_cf(geometries, names=names)
|
|
491
|
+
else:
|
|
492
|
+
raise ValueError(
|
|
493
|
+
f"Mixed geometry types are not supported in CF-compliant datasets. Got {types}"
|
|
494
|
+
)
|
|
356
495
|
|
|
357
496
|
return ds
|
|
358
497
|
|
|
359
498
|
|
|
360
|
-
def cf_to_shapely(ds: xr.Dataset):
|
|
499
|
+
def cf_to_shapely(ds: xr.Dataset, *, container: Hashable = GEOMETRY_CONTAINER_NAME):
|
|
361
500
|
"""
|
|
362
501
|
Convert geometries stored in a CF-compliant way to shapely objects stored in a single variable.
|
|
363
502
|
|
|
@@ -378,22 +517,35 @@ def cf_to_shapely(ds: xr.Dataset):
|
|
|
378
517
|
----------
|
|
379
518
|
Please refer to the CF conventions document: http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#geometries
|
|
380
519
|
"""
|
|
381
|
-
|
|
520
|
+
if container not in ds._variables:
|
|
521
|
+
raise ValueError(
|
|
522
|
+
f"{container!r} is not the name of a variable in the provided Dataset."
|
|
523
|
+
)
|
|
524
|
+
if not (geom_type := ds[container].attrs.get("geometry_type", None)):
|
|
525
|
+
raise ValueError(
|
|
526
|
+
f"{container!r} is not the name of a valid geometry variable. "
|
|
527
|
+
"It does not have a `geometry_type` attribute."
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
# Extract all necessary geometry variables
|
|
531
|
+
subds = ds.cf[[container]]
|
|
382
532
|
if geom_type == "point":
|
|
383
|
-
geometries = cf_to_points(
|
|
533
|
+
geometries = cf_to_points(subds)
|
|
384
534
|
elif geom_type == "line":
|
|
385
|
-
geometries = cf_to_lines(
|
|
535
|
+
geometries = cf_to_lines(subds)
|
|
386
536
|
elif geom_type == "polygon":
|
|
387
|
-
geometries = cf_to_polygons(
|
|
537
|
+
geometries = cf_to_polygons(subds)
|
|
388
538
|
else:
|
|
389
539
|
raise ValueError(
|
|
390
540
|
f"Valid CF geometry types are 'point', 'line' and 'polygon'. Got {geom_type}"
|
|
391
541
|
)
|
|
542
|
+
if gm := ds[container].attrs.get("grid_mapping"):
|
|
543
|
+
geometries.attrs["grid_mapping"] = gm
|
|
392
544
|
|
|
393
545
|
return geometries.rename("geometry")
|
|
394
546
|
|
|
395
547
|
|
|
396
|
-
def points_to_cf(pts: xr.DataArray | Sequence):
|
|
548
|
+
def points_to_cf(pts: xr.DataArray | Sequence, *, names: GeometryNames | None = None):
|
|
397
549
|
"""Get a list of points (shapely.geometry.[Multi]Point) and return a CF-compliant geometry dataset.
|
|
398
550
|
|
|
399
551
|
Parameters
|
|
@@ -410,11 +562,14 @@ def points_to_cf(pts: xr.DataArray | Sequence):
|
|
|
410
562
|
from shapely.geometry import MultiPoint
|
|
411
563
|
|
|
412
564
|
if isinstance(pts, xr.DataArray):
|
|
565
|
+
# TODO: Fix this hardcoding
|
|
566
|
+
if pts.ndim != 1:
|
|
567
|
+
raise ValueError("Only 1D DataArrays are supported.")
|
|
413
568
|
dim = pts.dims[0]
|
|
414
569
|
coord = pts[dim] if dim in pts.coords else None
|
|
415
570
|
pts_ = pts.values.tolist()
|
|
416
571
|
else:
|
|
417
|
-
dim =
|
|
572
|
+
dim = FEATURES_DIM_NAME
|
|
418
573
|
coord = None
|
|
419
574
|
pts_ = pts
|
|
420
575
|
|
|
@@ -430,33 +585,27 @@ def points_to_cf(pts: xr.DataArray | Sequence):
|
|
|
430
585
|
crdX.append(xy[0, 0])
|
|
431
586
|
crdY.append(xy[0, 1])
|
|
432
587
|
|
|
588
|
+
if names is None:
|
|
589
|
+
names = GeometryNames()
|
|
590
|
+
|
|
433
591
|
ds = xr.Dataset(
|
|
434
592
|
data_vars={
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
"node_count": "node_count",
|
|
440
|
-
"node_coordinates": "x y",
|
|
441
|
-
"coordinates": "crd_x crd_y",
|
|
442
|
-
}
|
|
593
|
+
names.node_count: xr.DataArray(node_count, dims=(dim,)),
|
|
594
|
+
names.container_name: xr.DataArray(
|
|
595
|
+
data=np.nan,
|
|
596
|
+
attrs={"geometry_type": "point", **names.geometry_container_attrs},
|
|
443
597
|
),
|
|
444
598
|
},
|
|
445
|
-
coords=
|
|
446
|
-
"x": xr.DataArray(x, dims=("node",), attrs={"axis": "X"}),
|
|
447
|
-
"y": xr.DataArray(y, dims=("node",), attrs={"axis": "Y"}),
|
|
448
|
-
"crd_x": xr.DataArray(crdX, dims=(dim,), attrs={"nodes": "x"}),
|
|
449
|
-
"crd_y": xr.DataArray(crdY, dims=(dim,), attrs={"nodes": "y"}),
|
|
450
|
-
},
|
|
599
|
+
coords=names.coords(x=x, y=y, crdX=crdX, crdY=crdY, dim=dim),
|
|
451
600
|
)
|
|
452
601
|
|
|
453
602
|
if coord is not None:
|
|
454
603
|
ds = ds.assign_coords({dim: coord})
|
|
455
604
|
|
|
456
605
|
# Special case when we have no MultiPoints
|
|
457
|
-
if (ds.node_count == 1).all():
|
|
458
|
-
ds = ds.drop_vars(
|
|
459
|
-
del ds[
|
|
606
|
+
if (ds[names.node_count] == 1).data.all():
|
|
607
|
+
ds = ds.drop_vars(names.node_count)
|
|
608
|
+
del ds[names.container_name].attrs["node_count"]
|
|
460
609
|
return ds
|
|
461
610
|
|
|
462
611
|
|
|
@@ -467,7 +616,7 @@ def cf_to_points(ds: xr.Dataset):
|
|
|
467
616
|
----------
|
|
468
617
|
ds : xr.Dataset
|
|
469
618
|
A dataset with CF-compliant point geometries.
|
|
470
|
-
Must have a "
|
|
619
|
+
Must have a *single* "geometry container" variable with at least a 'node_coordinates' attribute.
|
|
471
620
|
Must also have the two 1D variables listed by this attribute.
|
|
472
621
|
|
|
473
622
|
Returns
|
|
@@ -479,8 +628,9 @@ def cf_to_points(ds: xr.Dataset):
|
|
|
479
628
|
"""
|
|
480
629
|
from shapely.geometry import MultiPoint, Point
|
|
481
630
|
|
|
631
|
+
container_name = _assert_single_geometry_container(ds)
|
|
482
632
|
# Shorthand for convenience
|
|
483
|
-
geo = ds[
|
|
633
|
+
geo = ds[container_name].attrs
|
|
484
634
|
|
|
485
635
|
# The features dimension name, defaults to the one of 'node_count' or the dimension of the coordinates, if present.
|
|
486
636
|
feat_dim = None
|
|
@@ -488,14 +638,14 @@ def cf_to_points(ds: xr.Dataset):
|
|
|
488
638
|
xcoord_name, _ = geo["coordinates"].split(" ")
|
|
489
639
|
(feat_dim,) = ds[xcoord_name].dims
|
|
490
640
|
|
|
491
|
-
x_name, y_name = ds[
|
|
641
|
+
x_name, y_name = ds[container_name].attrs["node_coordinates"].split(" ")
|
|
492
642
|
xy = np.stack([ds[x_name].values, ds[y_name].values], axis=-1)
|
|
493
643
|
|
|
494
|
-
node_count_name = ds[
|
|
644
|
+
node_count_name = ds[container_name].attrs.get("node_count")
|
|
495
645
|
if node_count_name is None:
|
|
496
646
|
# No node_count means all geometries are single points (node_count = 1)
|
|
497
|
-
# And if we had no coordinates, then the dimension defaults to
|
|
498
|
-
feat_dim = feat_dim or
|
|
647
|
+
# And if we had no coordinates, then the dimension defaults to FEATURES_DIM_NAME
|
|
648
|
+
feat_dim = feat_dim or FEATURES_DIM_NAME
|
|
499
649
|
node_count = xr.DataArray([1] * xy.shape[0], dims=(feat_dim,))
|
|
500
650
|
if feat_dim in ds.coords:
|
|
501
651
|
node_count = node_count.assign_coords({feat_dim: ds[feat_dim]})
|
|
@@ -512,10 +662,13 @@ def cf_to_points(ds: xr.Dataset):
|
|
|
512
662
|
geoms[i] = MultiPoint(xy[j : j + n, :])
|
|
513
663
|
j += n
|
|
514
664
|
|
|
515
|
-
|
|
665
|
+
da = xr.DataArray(geoms, dims=node_count.dims, coords=node_count.coords)
|
|
666
|
+
if node_count_name:
|
|
667
|
+
del da[node_count_name]
|
|
668
|
+
return da
|
|
516
669
|
|
|
517
670
|
|
|
518
|
-
def lines_to_cf(lines: xr.DataArray | Sequence):
|
|
671
|
+
def lines_to_cf(lines: xr.DataArray | Sequence, *, names: GeometryNames | None = None):
|
|
519
672
|
"""Convert an iterable of lines (shapely.geometry.[Multi]Line) into a CF-compliant geometry dataset.
|
|
520
673
|
|
|
521
674
|
Parameters
|
|
@@ -540,6 +693,9 @@ def lines_to_cf(lines: xr.DataArray | Sequence):
|
|
|
540
693
|
coord = None
|
|
541
694
|
lines_ = np.array(lines)
|
|
542
695
|
|
|
696
|
+
if names is None:
|
|
697
|
+
names = GeometryNames()
|
|
698
|
+
|
|
543
699
|
_, arr, offsets = to_ragged_array(lines_)
|
|
544
700
|
x = arr[:, 0]
|
|
545
701
|
y = arr[:, 1]
|
|
@@ -558,33 +714,23 @@ def lines_to_cf(lines: xr.DataArray | Sequence):
|
|
|
558
714
|
|
|
559
715
|
ds = xr.Dataset(
|
|
560
716
|
data_vars={
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
attrs={
|
|
565
|
-
"geometry_type": "line",
|
|
566
|
-
"node_count": "node_count",
|
|
567
|
-
"part_node_count": "part_node_count",
|
|
568
|
-
"node_coordinates": "x y",
|
|
569
|
-
"coordinates": "crd_x crd_y",
|
|
570
|
-
}
|
|
717
|
+
names.node_count: xr.DataArray(node_count, dims=(dim,)),
|
|
718
|
+
names.container_name: xr.DataArray(
|
|
719
|
+
data=np.nan,
|
|
720
|
+
attrs={"geometry_type": "line", **names.geometry_container_attrs},
|
|
571
721
|
),
|
|
572
722
|
},
|
|
573
|
-
coords=
|
|
574
|
-
"x": xr.DataArray(x, dims=("node",), attrs={"axis": "X"}),
|
|
575
|
-
"y": xr.DataArray(y, dims=("node",), attrs={"axis": "Y"}),
|
|
576
|
-
"crd_x": xr.DataArray(crdX, dims=(dim,), attrs={"nodes": "x"}),
|
|
577
|
-
"crd_y": xr.DataArray(crdY, dims=(dim,), attrs={"nodes": "y"}),
|
|
578
|
-
},
|
|
723
|
+
coords=names.coords(x=x, y=y, crdX=crdX, crdY=crdY, dim=dim),
|
|
579
724
|
)
|
|
580
725
|
|
|
581
726
|
if coord is not None:
|
|
582
727
|
ds = ds.assign_coords({dim: coord})
|
|
583
728
|
|
|
584
729
|
# Special case when we have no MultiLines
|
|
585
|
-
if len(
|
|
586
|
-
ds =
|
|
587
|
-
|
|
730
|
+
if len(part_node_count) != len(node_count):
|
|
731
|
+
ds[names.part_node_count] = xr.DataArray(part_node_count, dims=names.part_dim)
|
|
732
|
+
ds[names.container_name].attrs["part_node_count"] = names.part_node_count
|
|
733
|
+
|
|
588
734
|
return ds
|
|
589
735
|
|
|
590
736
|
|
|
@@ -607,8 +753,10 @@ def cf_to_lines(ds: xr.Dataset):
|
|
|
607
753
|
"""
|
|
608
754
|
from shapely import GeometryType, from_ragged_array
|
|
609
755
|
|
|
756
|
+
container_name = _assert_single_geometry_container(ds)
|
|
757
|
+
|
|
610
758
|
# Shorthand for convenience
|
|
611
|
-
geo = ds[
|
|
759
|
+
geo = ds[container_name].attrs
|
|
612
760
|
|
|
613
761
|
# The features dimension name, defaults to the one of 'node_count'
|
|
614
762
|
# or the dimension of the coordinates, if present.
|
|
@@ -645,10 +793,14 @@ def cf_to_lines(ds: xr.Dataset):
|
|
|
645
793
|
# get items from lines or multilines depending on number of parts
|
|
646
794
|
geoms = np.where(np.diff(offset2) == 1, lines[offset2[:-1]], multilines)
|
|
647
795
|
|
|
648
|
-
return xr.DataArray(
|
|
796
|
+
return xr.DataArray(
|
|
797
|
+
geoms, dims=node_count.dims, coords=node_count.coords
|
|
798
|
+
).drop_vars(node_count_name)
|
|
649
799
|
|
|
650
800
|
|
|
651
|
-
def polygons_to_cf(
|
|
801
|
+
def polygons_to_cf(
|
|
802
|
+
polygons: xr.DataArray | Sequence, *, names: GeometryNames | None = None
|
|
803
|
+
):
|
|
652
804
|
"""Convert an iterable of polygons (shapely.geometry.[Multi]Polygon) into a CF-compliant geometry dataset.
|
|
653
805
|
|
|
654
806
|
Parameters
|
|
@@ -656,6 +808,9 @@ def polygons_to_cf(polygons: xr.DataArray | Sequence):
|
|
|
656
808
|
polygons : sequence of shapely.geometry.Polygon or MultiPolygon
|
|
657
809
|
The sequence of [multi]polygons to translate to a CF dataset.
|
|
658
810
|
|
|
811
|
+
names: GeometryNames, optional
|
|
812
|
+
Structure that helps manipulate geometry attrs.
|
|
813
|
+
|
|
659
814
|
Returns
|
|
660
815
|
-------
|
|
661
816
|
xr.Dataset
|
|
@@ -673,6 +828,9 @@ def polygons_to_cf(polygons: xr.DataArray | Sequence):
|
|
|
673
828
|
coord = None
|
|
674
829
|
polygons_ = np.array(polygons)
|
|
675
830
|
|
|
831
|
+
if names is None:
|
|
832
|
+
names = GeometryNames()
|
|
833
|
+
|
|
676
834
|
_, arr, offsets = to_ragged_array(polygons_)
|
|
677
835
|
x = arr[:, 0]
|
|
678
836
|
y = arr[:, 1]
|
|
@@ -696,40 +854,27 @@ def polygons_to_cf(polygons: xr.DataArray | Sequence):
|
|
|
696
854
|
|
|
697
855
|
ds = xr.Dataset(
|
|
698
856
|
data_vars={
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
attrs={
|
|
704
|
-
"geometry_type": "polygon",
|
|
705
|
-
"node_count": "node_count",
|
|
706
|
-
"part_node_count": "part_node_count",
|
|
707
|
-
"interior_ring": "interior_ring",
|
|
708
|
-
"node_coordinates": "x y",
|
|
709
|
-
"coordinates": "crd_x crd_y",
|
|
710
|
-
}
|
|
857
|
+
names.node_count: xr.DataArray(node_count, dims=(dim,)),
|
|
858
|
+
names.container_name: xr.DataArray(
|
|
859
|
+
data=np.nan,
|
|
860
|
+
attrs={"geometry_type": "polygon", **names.geometry_container_attrs},
|
|
711
861
|
),
|
|
712
862
|
},
|
|
713
|
-
coords=
|
|
714
|
-
"x": xr.DataArray(x, dims=("node",), attrs={"axis": "X"}),
|
|
715
|
-
"y": xr.DataArray(y, dims=("node",), attrs={"axis": "Y"}),
|
|
716
|
-
"crd_x": xr.DataArray(crdX, dims=(dim,), attrs={"nodes": "x"}),
|
|
717
|
-
"crd_y": xr.DataArray(crdY, dims=(dim,), attrs={"nodes": "y"}),
|
|
718
|
-
},
|
|
863
|
+
coords=names.coords(x=x, y=y, crdX=crdX, crdY=crdY, dim=dim),
|
|
719
864
|
)
|
|
720
865
|
|
|
721
866
|
if coord is not None:
|
|
722
867
|
ds = ds.assign_coords({dim: coord})
|
|
723
868
|
|
|
724
869
|
# Special case when we have no MultiPolygons and no holes
|
|
725
|
-
if len(
|
|
726
|
-
ds =
|
|
727
|
-
|
|
870
|
+
if len(part_node_count) != len(node_count):
|
|
871
|
+
ds[names.part_node_count] = xr.DataArray(part_node_count, dims=names.part_dim)
|
|
872
|
+
ds[names.container_name].attrs["part_node_count"] = names.part_node_count
|
|
728
873
|
|
|
729
874
|
# Special case when we have no holes
|
|
730
|
-
if (
|
|
731
|
-
ds =
|
|
732
|
-
|
|
875
|
+
if (interior_ring != 0).any():
|
|
876
|
+
ds[names.interior_ring] = xr.DataArray(interior_ring, dims=names.part_dim)
|
|
877
|
+
ds[names.container_name].attrs["interior_ring"] = names.interior_ring
|
|
733
878
|
return ds
|
|
734
879
|
|
|
735
880
|
|
|
@@ -752,8 +897,10 @@ def cf_to_polygons(ds: xr.Dataset):
|
|
|
752
897
|
"""
|
|
753
898
|
from shapely import GeometryType, from_ragged_array
|
|
754
899
|
|
|
900
|
+
container_name = _assert_single_geometry_container(ds)
|
|
901
|
+
|
|
755
902
|
# Shorthand for convenience
|
|
756
|
-
geo = ds[
|
|
903
|
+
geo = ds[container_name].attrs
|
|
757
904
|
|
|
758
905
|
# The features dimension name, defaults to the one of 'part_node_count'
|
|
759
906
|
# or the dimension of the coordinates, if present.
|
|
@@ -805,4 +952,6 @@ def cf_to_polygons(ds: xr.Dataset):
|
|
|
805
952
|
# get items from polygons or multipolygons depending on number of parts
|
|
806
953
|
geoms = np.where(np.diff(offset3) == 1, polygons[offset3[:-1]], multipolygons)
|
|
807
954
|
|
|
808
|
-
return xr.DataArray(
|
|
955
|
+
return xr.DataArray(
|
|
956
|
+
geoms, dims=node_count.dims, coords=node_count.coords
|
|
957
|
+
).drop_vars(node_count_name)
|
|
@@ -360,6 +360,8 @@ def test_shapely_to_cf_errors():
|
|
|
360
360
|
)
|
|
361
361
|
assert encoded["x"].attrs["standard_name"] == "projection_x_coordinate"
|
|
362
362
|
assert encoded["y"].attrs["standard_name"] == "projection_y_coordinate"
|
|
363
|
+
for name in ["x", "y", "crd_x", "crd_y"]:
|
|
364
|
+
assert "grid_mapping" not in encoded[name].attrs
|
|
363
365
|
|
|
364
366
|
|
|
365
367
|
@requires_shapely
|
|
@@ -500,6 +502,10 @@ def test_encode_decode(geometry_ds, polygon_geometry):
|
|
|
500
502
|
)
|
|
501
503
|
).assign({"foo": ("geoms", [1, 2])})
|
|
502
504
|
|
|
503
|
-
|
|
505
|
+
polyds = (
|
|
506
|
+
polygon_geometry.rename("polygons").rename({"index": "index2"}).to_dataset()
|
|
507
|
+
)
|
|
508
|
+
multi_ds = xr.merge([polyds, geometry_ds[1]])
|
|
509
|
+
for ds in (geometry_ds[1], polygon_geometry.to_dataset(), geom_dim_ds, multi_ds):
|
|
504
510
|
roundtripped = decode_geometries(encode_geometries(ds))
|
|
505
511
|
xr.testing.assert_identical(ds, roundtripped)
|
|
@@ -97,9 +97,8 @@ ds.identical(decoded)
|
|
|
97
97
|
|
|
98
98
|
The following limitations can be relaxed in the future. PRs welcome!
|
|
99
99
|
|
|
100
|
-
1. cf-xarray uses `"geometry_container"` as the name for the geometry variable always
|
|
101
|
-
1.
|
|
102
|
-
1. CF xarray will not set the `"geometry"` attribute that links a variable to a geometry by default unless the geometry variable is a dimension coordiante for that variable. This heuristic works OK for vector data cubes (e.g. [xvec](https://xvec.readthedocs.io/en/stable/)). You should set the `"geometry"` attribute manually otherwise. Suggestions for better behaviour here are very welcome.
|
|
100
|
+
1. cf-xarray uses `"geometry_container"` as the name for the geometry variable always. If there are multiple geometry variables then `"geometry_N"`is used where N is an integer >= 0. cf-xarray behaves similarly for all associated geometry variables names: i.e. `"node"`, `"node_count"`, `"part_node_count"`, `"part"`, `"interior_ring"`. `"x"`, `"y"` (with suffixes if needed) are always the node coordinate variable names, and `"crd_x"`, `"crd_y"` are the nominal X, Y coordinate locations. None of this is configurable at the moment.
|
|
101
|
+
1. CF xarray will not set the `"geometry"` attribute that links a variable to a geometry by default unless the geometry variable is a dimension coordinate for that variable. This heuristic works OK for vector data cubes (e.g. [xvec](https://xvec.readthedocs.io/en/stable/)). You should set the `"geometry"` attribute manually otherwise. Suggestions for better behaviour here are very welcome.
|
|
103
102
|
|
|
104
103
|
## Lower-level conversions
|
|
105
104
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.9.3"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|