geo-explorer 0.9.7__py3-none-any.whl → 0.9.9__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.
- geo_explorer/file_browser.py +134 -112
- geo_explorer/fs.py +1 -1
- geo_explorer/geo_explorer.py +4299 -3947
- geo_explorer/nc.py +199 -0
- geo_explorer/utils.py +82 -33
- {geo_explorer-0.9.7.dist-info → geo_explorer-0.9.9.dist-info}/METADATA +2 -2
- geo_explorer-0.9.9.dist-info/RECORD +14 -0
- geo_explorer-0.9.7.dist-info/RECORD +0 -13
- {geo_explorer-0.9.7.dist-info → geo_explorer-0.9.9.dist-info}/LICENSE +0 -0
- {geo_explorer-0.9.7.dist-info → geo_explorer-0.9.9.dist-info}/LICENSE.md +0 -0
- {geo_explorer-0.9.7.dist-info → geo_explorer-0.9.9.dist-info}/WHEEL +0 -0
geo_explorer/nc.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from typing import ClassVar
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import pyproj
|
|
7
|
+
import rasterio
|
|
8
|
+
import shapely
|
|
9
|
+
from geopandas import GeoDataFrame
|
|
10
|
+
from geopandas import GeoSeries
|
|
11
|
+
from shapely.geometry import Polygon
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from xarray import DataArray
|
|
15
|
+
from xarray import Dataset
|
|
16
|
+
except ImportError:
|
|
17
|
+
|
|
18
|
+
class DataArray:
|
|
19
|
+
"""Placeholder."""
|
|
20
|
+
|
|
21
|
+
class Dataset:
|
|
22
|
+
"""Placeholder."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
from .utils import _PROFILE_DICT
|
|
26
|
+
from .utils import get_xarray_bounds
|
|
27
|
+
from .utils import time_method_call
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AbstractImageConfig(abc.ABC):
|
|
31
|
+
rgb_bands: ClassVar[list[str] | None] = None
|
|
32
|
+
reducer: ClassVar[str | None] = None
|
|
33
|
+
|
|
34
|
+
def __init__(self, code_block: str | None = None) -> None:
|
|
35
|
+
self._code_block = code_block
|
|
36
|
+
self.code_block = code_block # trigger setter
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def code_block(self) -> str | None:
|
|
40
|
+
return self._code_block
|
|
41
|
+
|
|
42
|
+
@code_block.setter
|
|
43
|
+
def code_block(self, value: str | None):
|
|
44
|
+
if value and (
|
|
45
|
+
"xarr=" not in value.replace(" ", "")
|
|
46
|
+
or not any(txt in value.replace(" ", "") for txt in ("=ds", "(ds"))
|
|
47
|
+
):
|
|
48
|
+
raise ValueError(
|
|
49
|
+
"'code_block' must be a piece of code that takes the xarray object 'ds' and defines the object 'xarr'. "
|
|
50
|
+
f"Got '{value}'"
|
|
51
|
+
)
|
|
52
|
+
self._code_block = value
|
|
53
|
+
|
|
54
|
+
@abc.abstractmethod
|
|
55
|
+
def get_crs(self, ds: Dataset, path: str) -> pyproj.CRS:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
@abc.abstractmethod
|
|
59
|
+
def get_bounds(self, ds: Dataset, path: str) -> tuple[float, float, float, float]:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
@time_method_call(_PROFILE_DICT)
|
|
63
|
+
def filter_ds(
|
|
64
|
+
self,
|
|
65
|
+
ds: Dataset,
|
|
66
|
+
bounds: tuple[float, float, float, float],
|
|
67
|
+
code_block: str | None,
|
|
68
|
+
) -> GeoDataFrame | None:
|
|
69
|
+
crs = self.get_crs(ds, None)
|
|
70
|
+
ds_bounds = get_xarray_bounds(ds)
|
|
71
|
+
|
|
72
|
+
bbox_correct_crs = (
|
|
73
|
+
GeoSeries([shapely.box(*bounds)], crs=4326).to_crs(crs).union_all()
|
|
74
|
+
)
|
|
75
|
+
clipped_bbox = bbox_correct_crs.intersection(shapely.box(*ds_bounds))
|
|
76
|
+
minx, miny, maxx, maxy = clipped_bbox.bounds
|
|
77
|
+
|
|
78
|
+
ds = ds.sel(
|
|
79
|
+
x=slice(minx, maxx),
|
|
80
|
+
y=slice(maxy, miny),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return _run_code_block(ds, code_block)
|
|
84
|
+
|
|
85
|
+
@time_method_call(_PROFILE_DICT)
|
|
86
|
+
def to_numpy(self, xarr: Dataset | DataArray) -> GeoDataFrame | None:
|
|
87
|
+
if isinstance(xarr, Dataset) and len(xarr.data_vars) == 1:
|
|
88
|
+
xarr = xarr[next(iter(xarr.data_vars))]
|
|
89
|
+
elif isinstance(xarr, Dataset):
|
|
90
|
+
try:
|
|
91
|
+
xarr = xarr[self.rgb_bands]
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
if "time" in set(xarr.dims) and (
|
|
96
|
+
not hasattr(xarr["time"].values, "__len__") or len(xarr["time"].values) > 1
|
|
97
|
+
):
|
|
98
|
+
if self.reducer is None:
|
|
99
|
+
xarr = xarr.isel(time=0)
|
|
100
|
+
else:
|
|
101
|
+
xarr = getattr(xarr, self.reducer)(dim="time")
|
|
102
|
+
|
|
103
|
+
if isinstance(xarr, Dataset) and self.rgb_bands:
|
|
104
|
+
return np.array([xarr[band].values for band in self.rgb_bands])
|
|
105
|
+
elif isinstance(xarr, Dataset):
|
|
106
|
+
return np.array([xarr[var].values for var in xarr.data_vars])
|
|
107
|
+
|
|
108
|
+
if isinstance(xarr, np.ndarray):
|
|
109
|
+
return xarr
|
|
110
|
+
|
|
111
|
+
return xarr.values
|
|
112
|
+
|
|
113
|
+
def __str__(self) -> str:
|
|
114
|
+
code_block = f"'{self.code_block}'" if self.code_block else None
|
|
115
|
+
return f"{self.__class__.__name__}({code_block})"
|
|
116
|
+
|
|
117
|
+
def __repr__(self) -> str:
|
|
118
|
+
return str(self)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _run_code_block(
|
|
122
|
+
ds: DataArray | Dataset, code_block: str | None
|
|
123
|
+
) -> Dataset | DataArray:
|
|
124
|
+
if not code_block:
|
|
125
|
+
return ds
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
xarr = eval(code_block)
|
|
129
|
+
if callable(xarr):
|
|
130
|
+
xarr = xarr(ds)
|
|
131
|
+
except SyntaxError:
|
|
132
|
+
loc = {}
|
|
133
|
+
exec(code_block, globals=globals() | {"ds": ds}, locals=loc)
|
|
134
|
+
xarr = loc["xarr"]
|
|
135
|
+
|
|
136
|
+
if isinstance(xarr, np.ndarray) and isinstance(ds, DataArray):
|
|
137
|
+
ds.values = xarr
|
|
138
|
+
return ds
|
|
139
|
+
elif isinstance(xarr, np.ndarray) and isinstance(ds, Dataset):
|
|
140
|
+
raise ValueError(
|
|
141
|
+
"Cannot return np.ndarray from 'code_block' if ds is xarray.Dataset."
|
|
142
|
+
)
|
|
143
|
+
return xarr
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class GeoTIFFConfig(AbstractImageConfig):
|
|
147
|
+
def get_crs(self, ds, path):
|
|
148
|
+
with rasterio.open(path) as src:
|
|
149
|
+
return src.crs
|
|
150
|
+
|
|
151
|
+
def get_bounds(self, ds, path) -> tuple[float, float, float, float]:
|
|
152
|
+
with rasterio.open(path) as src:
|
|
153
|
+
return tuple(
|
|
154
|
+
GeoSeries([shapely.box(*src.bounds)], crs=src.crs)
|
|
155
|
+
.to_crs(4326)
|
|
156
|
+
.total_bounds
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class NetCDFConfig(AbstractImageConfig):
|
|
161
|
+
"""Sets the configuration for reading NetCDF files and getting crs and bounds.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
code_block: String of Python code that takes an xarray.Dataset (ds) and returns an xarray.DataArray (xarr).
|
|
165
|
+
Note that the input Dataset must be references as 'ds' and the output must be assigned to 'xarr'.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
rgb_bands: ClassVar[list[str]] = ["B4", "B3", "B2"]
|
|
169
|
+
|
|
170
|
+
def get_bounds(self, ds, path) -> tuple[float, float, float, float]:
|
|
171
|
+
return get_xarray_bounds(ds)
|
|
172
|
+
|
|
173
|
+
def get_crs(self, ds: Dataset, path: str) -> pyproj.CRS:
|
|
174
|
+
attrs = [x for x in ds.attrs if "projection" in x.lower() or "crs" in x.lower()]
|
|
175
|
+
if not attrs:
|
|
176
|
+
raise ValueError(f"Could not find CRS attribute in dataset: {ds}")
|
|
177
|
+
for i, attr in enumerate(attrs):
|
|
178
|
+
try:
|
|
179
|
+
return pyproj.CRS(ds.attrs[attr])
|
|
180
|
+
except Exception as e:
|
|
181
|
+
if i == len(attrs) - 1:
|
|
182
|
+
attrs_dict = {attr: ds.attrs[attr] for attr in attrs}
|
|
183
|
+
raise ValueError(
|
|
184
|
+
f"No valid CRS attribute found among {attrs_dict}"
|
|
185
|
+
) from e
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class NBSNetCDFConfig(NetCDFConfig):
|
|
189
|
+
def get_crs(self, ds: Dataset, path: str) -> pyproj.CRS:
|
|
190
|
+
return pyproj.CRS(ds.UTM_projection.epsg_code)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class Sentinel2NBSNetCDFConfig(NBSNetCDFConfig):
|
|
194
|
+
rgb_bands: ClassVar[list[str]] = ["B4", "B3", "B2"]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _pd():
|
|
198
|
+
"""Function that makes sure 'pd' is not removed by 'ruff' fixes. Because pd is useful in code_block."""
|
|
199
|
+
pd
|
geo_explorer/utils.py
CHANGED
|
@@ -1,47 +1,24 @@
|
|
|
1
1
|
import time
|
|
2
|
+
from collections.abc import Callable
|
|
2
3
|
from functools import wraps
|
|
3
4
|
from pathlib import Path
|
|
4
|
-
from collections.abc import Callable
|
|
5
5
|
|
|
6
6
|
import dash_bootstrap_components as dbc
|
|
7
7
|
from dash import html
|
|
8
8
|
|
|
9
|
+
_PROFILE_DICT = {}
|
|
9
10
|
|
|
10
|
-
def _standardize_path(path: str | Path) -> str:
|
|
11
|
-
"""Make sure delimiter is '/' and path ends without '/'."""
|
|
12
|
-
return str(path).replace("\\", "/").replace(r"\"", "/")
|
|
13
11
|
|
|
12
|
+
DEBUG: bool = 1
|
|
14
13
|
|
|
15
|
-
def _clicked_button_style():
|
|
16
|
-
return {
|
|
17
|
-
"color": "#e4e4e4",
|
|
18
|
-
"background": "#2F3034",
|
|
19
|
-
}
|
|
20
14
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def get_button_with_tooltip(
|
|
30
|
-
button_text, id, tooltip_text: str, n_clicks=0, **button_kwargs
|
|
31
|
-
) -> list[html.Button, dbc.Tooltip]:
|
|
32
|
-
return [
|
|
33
|
-
html.Button(
|
|
34
|
-
button_text,
|
|
35
|
-
id=id,
|
|
36
|
-
n_clicks=n_clicks,
|
|
37
|
-
**button_kwargs,
|
|
38
|
-
),
|
|
39
|
-
dbc.Tooltip(
|
|
40
|
-
tooltip_text,
|
|
41
|
-
target=id,
|
|
42
|
-
delay={"show": 500, "hide": 100},
|
|
43
|
-
),
|
|
44
|
-
]
|
|
15
|
+
def debug_print(*args):
|
|
16
|
+
print(
|
|
17
|
+
*(
|
|
18
|
+
f"{type(arg).__name__}: {arg}" if isinstance(arg, Exception) else arg
|
|
19
|
+
for arg in args
|
|
20
|
+
)
|
|
21
|
+
)
|
|
45
22
|
|
|
46
23
|
|
|
47
24
|
def time_method_call(method_dict) -> Callable:
|
|
@@ -82,3 +59,75 @@ def time_function_call(method_dict):
|
|
|
82
59
|
return wrapper
|
|
83
60
|
|
|
84
61
|
return decorator
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if not DEBUG:
|
|
65
|
+
|
|
66
|
+
def debug_print(*args):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
def time_method_call(_) -> Callable:
|
|
70
|
+
def decorator(method):
|
|
71
|
+
@wraps(method)
|
|
72
|
+
def wrapper(self, *args, **kwargs):
|
|
73
|
+
return method(self, *args, **kwargs)
|
|
74
|
+
|
|
75
|
+
return wrapper
|
|
76
|
+
|
|
77
|
+
return decorator
|
|
78
|
+
|
|
79
|
+
def time_function_call(_):
|
|
80
|
+
def decorator(func):
|
|
81
|
+
@wraps(func)
|
|
82
|
+
def wrapper(*args, **kwargs):
|
|
83
|
+
return func(*args, **kwargs)
|
|
84
|
+
|
|
85
|
+
return wrapper
|
|
86
|
+
|
|
87
|
+
return decorator
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_xarray_bounds(ds) -> tuple[float, float, float, float]:
|
|
91
|
+
return (
|
|
92
|
+
float(ds["x"].min().values),
|
|
93
|
+
float(ds["y"].min().values),
|
|
94
|
+
float(ds["x"].max().values),
|
|
95
|
+
float(ds["y"].max().values),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _standardize_path(path: str | Path) -> str:
|
|
100
|
+
"""Make sure delimiter is '/' and path ends without '/'."""
|
|
101
|
+
return str(path).replace("\\", "/").replace(r"\"", "/")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _clicked_button_style():
|
|
105
|
+
return {
|
|
106
|
+
"color": "#e4e4e4",
|
|
107
|
+
"background": "#2F3034",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _unclicked_button_style():
|
|
112
|
+
return {
|
|
113
|
+
"background": "#e4e4e4",
|
|
114
|
+
"color": "black",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_button_with_tooltip(
|
|
119
|
+
button_text, id, tooltip_text: str, n_clicks=0, **button_kwargs
|
|
120
|
+
) -> list[html.Button, dbc.Tooltip]:
|
|
121
|
+
return [
|
|
122
|
+
html.Button(
|
|
123
|
+
button_text,
|
|
124
|
+
id=id,
|
|
125
|
+
n_clicks=n_clicks,
|
|
126
|
+
**button_kwargs,
|
|
127
|
+
),
|
|
128
|
+
dbc.Tooltip(
|
|
129
|
+
tooltip_text,
|
|
130
|
+
target=id,
|
|
131
|
+
delay={"show": 500, "hide": 100},
|
|
132
|
+
),
|
|
133
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: geo-explorer
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.9
|
|
4
4
|
Summary: Explore geodata interactively.
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Morten Letnes
|
|
@@ -20,7 +20,7 @@ Requires-Dist: fsspec (>=2024.10.1)
|
|
|
20
20
|
Requires-Dist: geopandas (>=0.14.0)
|
|
21
21
|
Requires-Dist: jenkspy (>=0.3.2)
|
|
22
22
|
Requires-Dist: matplotlib (>=3.7.0)
|
|
23
|
-
Requires-Dist: msgspec (>=0.19.0
|
|
23
|
+
Requires-Dist: msgspec (>=0.19.0)
|
|
24
24
|
Requires-Dist: numpy (>=1.26.4)
|
|
25
25
|
Requires-Dist: pandas (>=2.2.1)
|
|
26
26
|
Requires-Dist: polars (>=1.32.0)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
geo_explorer/__init__.py,sha256=LAnELq1tD-bpPHVM_bPa8DqAD4kY6dE9ZalZSp3R0Ug,108
|
|
2
|
+
geo_explorer/assets/chroma.min.js,sha256=cNBWTSvPBSwnJYaqi4SM3IFXRjpWCLsa4VVfJ8yYWNw,46173
|
|
3
|
+
geo_explorer/assets/on_each_feature.js,sha256=pjpr3MGlaTZf8pa191hRB5Iui8un7jBG5mFw_GNGqxE,2630
|
|
4
|
+
geo_explorer/assets/stylesheet.css,sha256=1Dow3oh532wDZMEFr-VlkCdfekuf_20K97AVKatJun0,1957
|
|
5
|
+
geo_explorer/file_browser.py,sha256=NmMAKHznJ1TOJ8TH-axwfvKaZ7r0gH9QSCmVCXHnfZw,23270
|
|
6
|
+
geo_explorer/fs.py,sha256=N_WtpAjohO6OKAzoWzNxko0Rv_Y4oH0I8ackcoPu4f8,1693
|
|
7
|
+
geo_explorer/geo_explorer.py,sha256=PzOfYFtiMwiO6YadXIsNwpZFi5fAQoRW9q43VQ_dD4s,192229
|
|
8
|
+
geo_explorer/nc.py,sha256=zjW0X_prH1M-KHgJ2qspTlI5pgyBntWRD3GxGB0eO4A,6203
|
|
9
|
+
geo_explorer/utils.py,sha256=qRLhthS881rKOUdItJCyo__3a4I2Xv3l50eeumO-8Ek,3234
|
|
10
|
+
geo_explorer-0.9.9.dist-info/LICENSE,sha256=lL2h0dNKGTKAE0CjTy62SDbRennVD1xPgM5LzGqhKeo,1074
|
|
11
|
+
geo_explorer-0.9.9.dist-info/LICENSE.md,sha256=hxspefYgWP3U6OZFhCifqWMI5ksnKzgFxNKgQnG7Ozc,1074
|
|
12
|
+
geo_explorer-0.9.9.dist-info/METADATA,sha256=9vaB4hjPmzQF94l57v09FnCeAuu3rbhuIszV2wAavpc,8487
|
|
13
|
+
geo_explorer-0.9.9.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
14
|
+
geo_explorer-0.9.9.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
geo_explorer/__init__.py,sha256=LAnELq1tD-bpPHVM_bPa8DqAD4kY6dE9ZalZSp3R0Ug,108
|
|
2
|
-
geo_explorer/assets/chroma.min.js,sha256=cNBWTSvPBSwnJYaqi4SM3IFXRjpWCLsa4VVfJ8yYWNw,46173
|
|
3
|
-
geo_explorer/assets/on_each_feature.js,sha256=pjpr3MGlaTZf8pa191hRB5Iui8un7jBG5mFw_GNGqxE,2630
|
|
4
|
-
geo_explorer/assets/stylesheet.css,sha256=1Dow3oh532wDZMEFr-VlkCdfekuf_20K97AVKatJun0,1957
|
|
5
|
-
geo_explorer/file_browser.py,sha256=A7BZyioiq1lJS5Eo1d292G4al9EVrUhzxjh_5shhQP8,22386
|
|
6
|
-
geo_explorer/fs.py,sha256=P2tSdMvB0aJtsfXmqDEIzJN7bGJyWuHiRPXWeyrmtv8,1675
|
|
7
|
-
geo_explorer/geo_explorer.py,sha256=9iJdR1b5LG-ruv1XD-QllAJVZkqKbhZlereMH3pSNG0,179193
|
|
8
|
-
geo_explorer/utils.py,sha256=Mg1NqUm8hq17z681YQExCve49eoJZjhoG8xWKwYfExE,2249
|
|
9
|
-
geo_explorer-0.9.7.dist-info/LICENSE,sha256=lL2h0dNKGTKAE0CjTy62SDbRennVD1xPgM5LzGqhKeo,1074
|
|
10
|
-
geo_explorer-0.9.7.dist-info/LICENSE.md,sha256=hxspefYgWP3U6OZFhCifqWMI5ksnKzgFxNKgQnG7Ozc,1074
|
|
11
|
-
geo_explorer-0.9.7.dist-info/METADATA,sha256=7ejjHGScFdZmKczefiksWsFfLeNxsju75nEVuPo8bao,8495
|
|
12
|
-
geo_explorer-0.9.7.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
13
|
-
geo_explorer-0.9.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|