Skip to content

Commit 7635e62

Browse files
add vector processes (#301)
* add vector processes * pre-commit hook
1 parent 022ebc0 commit 7635e62

File tree

4 files changed

+327
-0
lines changed

4 files changed

+327
-0
lines changed
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import copy
2+
import logging
3+
from typing import Optional
4+
5+
import geopandas as gpd
6+
import numpy as np
7+
import shapely
8+
import xarray as xr
9+
import xvec
10+
11+
from openeo_processes_dask.process_implementations.data_model import VectorCube
12+
from openeo_processes_dask.process_implementations.exceptions import (
13+
DimensionNotAvailable,
14+
UnitMismatch,
15+
)
16+
17+
__all__ = ["load_geojson", "vector_buffer", "vector_reproject"]
18+
19+
logger = logging.getLogger(__name__)
20+
21+
22+
def load_geojson(data: dict, properties: Optional[list[str]] = []) -> VectorCube:
23+
DEFAULT_CRS = "epsg:4326"
24+
25+
if isinstance(data, dict):
26+
# Get crs from geometries
27+
if "features" in data:
28+
for feature in data["features"]:
29+
if "properties" not in feature:
30+
feature["properties"] = {}
31+
elif feature["properties"] is None:
32+
feature["properties"] = {}
33+
if isinstance(data.get("crs", {}), dict):
34+
DEFAULT_CRS = (
35+
data.get("crs", {}).get("properties", {}).get("name", DEFAULT_CRS)
36+
)
37+
else:
38+
DEFAULT_CRS = int(data.get("crs", {}))
39+
logger.info(f"CRS in geometries: {DEFAULT_CRS}.")
40+
41+
if "type" in data and data["type"] == "FeatureCollection":
42+
gdf = gpd.GeoDataFrame.from_features(data, crs=DEFAULT_CRS)
43+
elif "type" in data and data["type"] in ["Polygon"]:
44+
polygon = shapely.geometry.Polygon(data["coordinates"][0])
45+
gdf = gpd.GeoDataFrame(geometry=[polygon])
46+
gdf.crs = DEFAULT_CRS
47+
48+
dimensions = ["geometry"]
49+
coordinates = {"geometry": gdf.geometry}
50+
51+
if len(properties) == 0:
52+
if "features" in data:
53+
feature = data["features"][0]
54+
if "properties" in feature:
55+
property = feature["properties"]
56+
if len(property) == 1:
57+
key = list(property.keys())[0]
58+
value = list(property.values())
59+
dimensions.append("properties")
60+
if isinstance(value, list) and len(value) > 1:
61+
values = np.zeros((len(gdf.geometry), len(value)))
62+
coordinates["properties"] = np.arange(len(value))
63+
elif isinstance(value, list) and len(value) == 1:
64+
values = np.zeros((len(gdf.geometry), 1))
65+
coordinates["properties"] = np.array([key])
66+
else:
67+
values = np.zeros((len(gdf.geometry), 1))
68+
coordinates["properties"] = np.array([key])
69+
70+
for i, feature in enumerate(data["features"]):
71+
value = feature.get("properties", {}).get(key, None)
72+
values[i, :] = value
73+
elif len(property) > 1:
74+
dimensions.append("properties")
75+
keys = list(property.keys())
76+
coordinates["properties"] = keys
77+
values = np.zeros((len(gdf.geometry), len(keys)))
78+
for i, feature in enumerate(data["features"]):
79+
for j, key in enumerate(keys):
80+
value = feature.get("properties", {}).get(key, None)
81+
values[i, j] = value
82+
83+
elif len(properties) == 1:
84+
property = properties[0]
85+
if "features" in data:
86+
feature = data["features"][0]
87+
if "properties" in feature:
88+
if property in feature["properties"]:
89+
value = feature["properties"][property]
90+
dimensions.append("properties")
91+
if isinstance(value, list) and len(value) > 0:
92+
values = np.zeros((len(gdf.geometry), len(value)))
93+
coordinates["properties"] = np.arange(len(value))
94+
elif isinstance(value, list) and len(value) == 1:
95+
values = np.zeros((len(gdf.geometry), 1))
96+
coordinates["properties"] = np.array([property])
97+
else:
98+
values = np.zeros((len(gdf.geometry), 1))
99+
coordinates["properties"] = np.array([property])
100+
101+
for i, feature in enumerate(data["features"]):
102+
value = feature.get("properties", {}).get(property, None)
103+
values[i, :] = value
104+
else:
105+
if "features" in data:
106+
dimensions.append("properties")
107+
coordinates["properties"] = properties
108+
values = np.zeros((len(gdf.geometry), len(properties)))
109+
for i, feature in enumerate(data["features"]):
110+
for j, key in enumerate(properties):
111+
value = feature.get("properties", {}).get(key, None)
112+
values[i, j] = value
113+
114+
output_vector_cube = xr.DataArray(values, coords=coordinates, dims=dimensions)
115+
output_vector_cube = output_vector_cube.xvec.set_geom_indexes(
116+
"geometry", crs=gdf.crs
117+
)
118+
return output_vector_cube
119+
120+
121+
def vector_buffer(geometries: VectorCube, distance: float) -> VectorCube:
122+
from shapely import buffer
123+
124+
geometries_copy = copy.deepcopy(geometries)
125+
126+
if isinstance(geometries_copy, xr.DataArray) and "geometry" in geometries_copy.dims:
127+
if hasattr(geometries_copy, "xvec") and hasattr(
128+
geometries_copy["geometry"], "crs"
129+
):
130+
if geometries_copy["geometry"].crs.is_geographic:
131+
raise UnitMismatch(
132+
"The unit of the spatial reference system is not meters, but the given distance is in meters."
133+
)
134+
135+
geometry = geometries_copy["geometry"].values.tolist()
136+
137+
new_geometry = [buffer(geom, distance) for geom in geometry]
138+
139+
geometries_copy["geometry"] = new_geometry
140+
141+
return geometries_copy
142+
143+
else:
144+
raise DimensionNotAvailable(f"No geometry dimension found in {geometries}")
145+
146+
147+
def vector_reproject(
148+
data: VectorCube, projection, dimension: Optional[str] = None
149+
) -> VectorCube:
150+
DEFAULT_CRS = "epsg:4326"
151+
152+
data_copy = copy.deepcopy(data)
153+
154+
if not dimension:
155+
dimension = "geometry"
156+
157+
if isinstance(data, xr.DataArray) and dimension in data.dims:
158+
if hasattr(data, "xvec") and hasattr(data[dimension], "crs"):
159+
data_copy = data_copy.xvec.to_crs({dimension: projection})
160+
161+
return data_copy
162+
else:
163+
data_copy = data_copy.xvec.set_geom_indexes(dimension, crs=DEFAULT_CRS)
164+
data_copy = data_copy.xvec.to_crs({dimension: projection})
165+
166+
return data_copy
167+
else:
168+
raise DimensionNotAvailable(f"No geometry dimension found in {data}")

openeo_processes_dask/process_implementations/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,7 @@ class KernelDimensionsUneven(OpenEOException):
108108

109109
class MinMaxSwapped(OpenEOException):
110110
pass
111+
112+
113+
class UnitMismatch(OpenEOException):
114+
pass

tests/conftest.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,50 @@ def vector_data_cube() -> VectorCube:
137137

138138
dask_obj = dask_geopandas.from_geopandas(df, npartitions=1)
139139
return dask_obj
140+
141+
142+
@pytest.fixture
143+
def geometry_point(x=10.47, y=46.12, crs="EPSG:4326"):
144+
# Create a small polygon
145+
coordinates = [x, y]
146+
147+
geometry = {
148+
"type": "FeatureCollection",
149+
"features": [
150+
{
151+
"id": "0",
152+
"type": "Feature",
153+
"properties": {"id": "0", "class": 1},
154+
"geometry": {"type": "Point", "coordinates": coordinates},
155+
}
156+
],
157+
}
158+
159+
return geometry
160+
161+
162+
@pytest.fixture
163+
def geometry_dict(west=10.47, east=10.48, south=46.12, north=46.18, crs="EPSG:4326"):
164+
# Create a small polygon
165+
coordinates = [
166+
[[west, south], [west, north], [east, north], [east, south], [west, south]]
167+
]
168+
169+
geometry = {
170+
"type": "FeatureCollection",
171+
"features": [
172+
{
173+
"id": "0",
174+
"type": "Feature",
175+
"properties": {"id": "0", "class": 1},
176+
"geometry": {"type": "Polygon", "coordinates": coordinates},
177+
}
178+
],
179+
}
180+
181+
return geometry
182+
183+
184+
@pytest.fixture
185+
def wkt_string():
186+
return 'PROJCRS["WGS 84 / Pseudo-Mercator",BASEGEOGCRS["WGS 84",ENSEMBLE["World Geodetic System 1984 ensemble",MEMBER["World Geodetic System 1984 (Transit)"],MEMBER["World Geodetic System 1984 (G730)"],MEMBER["World Geodetic System 1984 (G873)"],MEMBER["World Geodetic System 1984 (G1150)"],MEMBER["World Geodetic System 1984 (G1674)"],MEMBER["World Geodetic System 1984 (G1762)"],MEMBER["World Geodetic System 1984 (G2139)"],MEMBER["World Geodetic System 1984 (G2296)"],ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]],ENSEMBLEACCURACY[2.0]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],ID["EPSG",4326]],CONVERSION["Popular Visualisation Pseudo-Mercator",METHOD["Popular Visualisation Pseudo Mercator",ID["EPSG",1024]],PARAMETER["Latitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8801]],PARAMETER["Longitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8802]],PARAMETER["False easting",0,LENGTHUNIT["metre",1],ID["EPSG",8806]],PARAMETER["False northing",0,LENGTHUNIT["metre",1],ID["EPSG",8807]]],CS[Cartesian,2],AXIS["easting (X)",east,ORDER[1],LENGTHUNIT["metre",1]],AXIS["northing (Y)",north,ORDER[2],LENGTHUNIT["metre",1]],USAGE[SCOPE["Web mapping and visualisation."],AREA["World between 85.06°S and 85.06°N."],BBOX[-85.06,-180,85.06,180]],ID["EPSG",3857]]'

tests/test_geometries.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from copy import deepcopy
2+
3+
import pytest
4+
import shapely
5+
import xarray as xr
6+
import xvec
7+
8+
from openeo_processes_dask.process_implementations.cubes.geometries import *
9+
from openeo_processes_dask.process_implementations.exceptions import UnitMismatch
10+
11+
12+
def test_load_geojson_point(geometry_point):
13+
""""""
14+
geometry = load_geojson(geometry_point)
15+
16+
assert isinstance(geometry, xr.DataArray)
17+
assert geometry.dims == ("geometry", "properties")
18+
assert hasattr(geometry["geometry"], "crs")
19+
assert len(geometry["properties"]) == 2
20+
21+
geometry_class = load_geojson(geometry_point, properties=["class"])
22+
23+
assert isinstance(geometry, xr.DataArray)
24+
assert geometry_class.dims == ("geometry", "properties")
25+
assert geometry_class["properties"].values == "class"
26+
27+
28+
def test_load_geojson_polygon(geometry_dict):
29+
""""""
30+
geometry = load_geojson(geometry_dict)
31+
32+
assert isinstance(geometry, xr.DataArray)
33+
assert geometry.dims == ("geometry", "properties")
34+
assert hasattr(geometry["geometry"], "crs")
35+
assert len(geometry["properties"]) == 2
36+
37+
geometry_class = load_geojson(geometry_dict, properties=["class"])
38+
39+
assert isinstance(geometry, xr.DataArray)
40+
assert geometry_class.dims == ("geometry", "properties")
41+
assert geometry_class["properties"].values == "class"
42+
43+
geometry_dict_2 = deepcopy(geometry_dict)
44+
geometry_dict_2["features"][0]["properties"]["class"] = [2, 3, 4, 5, 6]
45+
46+
geometry_2 = load_geojson(geometry_dict_2, properties=["class"])
47+
48+
assert isinstance(geometry_2, xr.DataArray)
49+
assert geometry_class.dims == ("geometry", "properties")
50+
assert geometry_class["properties"].values == "class"
51+
52+
53+
def test_point_reproject(geometry_point, wkt_string):
54+
""""""
55+
geometry = load_geojson(geometry_point)
56+
geometry_crs = geometry["geometry"].crs
57+
58+
epsg_vector = vector_reproject(data=geometry, projection="EPSG:3857")
59+
epsg_crs = epsg_vector["geometry"].crs
60+
61+
assert geometry_crs != epsg_crs
62+
63+
wkt_vector = vector_reproject(data=geometry, projection=wkt_string)
64+
wkt_crs = wkt_vector["geometry"].crs
65+
66+
assert geometry_crs != wkt_crs
67+
68+
69+
def test_polygon_reproject(geometry_dict, wkt_string):
70+
""""""
71+
geometry = load_geojson(geometry_dict)
72+
geometry_crs = geometry["geometry"].crs
73+
74+
epsg_vector = vector_reproject(data=geometry, projection="EPSG:3857")
75+
epsg_crs = epsg_vector["geometry"].crs
76+
77+
assert geometry_crs != epsg_crs
78+
79+
wkt_vector = vector_reproject(data=geometry, projection=wkt_string)
80+
wkt_crs = wkt_vector["geometry"].crs
81+
82+
assert geometry_crs != wkt_crs
83+
84+
85+
def test_point_buffer(geometry_point):
86+
""""""
87+
geometry = load_geojson(geometry_point)
88+
epsg_vector = vector_reproject(data=geometry, projection="EPSG:3857")
89+
90+
buffered_vector = vector_buffer(geometries=epsg_vector, distance=1)
91+
buffered_area = shapely.area(buffered_vector["geometry"].values[0])
92+
93+
assert buffered_area
94+
95+
96+
def test_polygon_buffer(geometry_dict):
97+
""""""
98+
geometry = load_geojson(geometry_dict)
99+
epsg_vector = vector_reproject(data=geometry, projection="EPSG:3857")
100+
geometry_area = shapely.area(epsg_vector["geometry"].values[0])
101+
102+
buffered_vector = vector_buffer(geometries=epsg_vector, distance=0.5)
103+
buffered_area = shapely.area(buffered_vector["geometry"].values[0])
104+
105+
assert buffered_area > geometry_area
106+
107+
with pytest.raises(UnitMismatch):
108+
error = vector_buffer(geometries=geometry, distance=1)

0 commit comments

Comments
 (0)