geo-explorer 0.9.8__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/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
- def _unclicked_button_style():
23
- return {
24
- "background": "#e4e4e4",
25
- "color": "black",
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.8
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,<0.20.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=N_WtpAjohO6OKAzoWzNxko0Rv_Y4oH0I8ackcoPu4f8,1693
7
- geo_explorer/geo_explorer.py,sha256=Dj3T64Jg7RW4LxIFOHGSm4CAZPMQXLDNgqdivlatjcs,179810
8
- geo_explorer/utils.py,sha256=Mg1NqUm8hq17z681YQExCve49eoJZjhoG8xWKwYfExE,2249
9
- geo_explorer-0.9.8.dist-info/LICENSE,sha256=lL2h0dNKGTKAE0CjTy62SDbRennVD1xPgM5LzGqhKeo,1074
10
- geo_explorer-0.9.8.dist-info/LICENSE.md,sha256=hxspefYgWP3U6OZFhCifqWMI5ksnKzgFxNKgQnG7Ozc,1074
11
- geo_explorer-0.9.8.dist-info/METADATA,sha256=dGNYMcISz5MOmN4DqpOBuvaPNf81efUYbhiyEBEzP7w,8495
12
- geo_explorer-0.9.8.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
13
- geo_explorer-0.9.8.dist-info/RECORD,,