ecoenv 0.1.0__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.
- ecoenv-0.1.0/LICENSE +21 -0
- ecoenv-0.1.0/PKG-INFO +52 -0
- ecoenv-0.1.0/README.md +48 -0
- ecoenv-0.1.0/ecoenv/__init__.py +21 -0
- ecoenv-0.1.0/ecoenv/env_request.py +51 -0
- ecoenv-0.1.0/ecoenv/globi.py +0 -0
- ecoenv-0.1.0/ecoenv/sentinel.py +197 -0
- ecoenv-0.1.0/ecoenv/worldclim.py +154 -0
- ecoenv-0.1.0/ecoenv.egg-info/PKG-INFO +52 -0
- ecoenv-0.1.0/ecoenv.egg-info/SOURCES.txt +14 -0
- ecoenv-0.1.0/ecoenv.egg-info/dependency_links.txt +1 -0
- ecoenv-0.1.0/ecoenv.egg-info/requires.txt +4 -0
- ecoenv-0.1.0/ecoenv.egg-info/top_level.txt +1 -0
- ecoenv-0.1.0/setup.cfg +4 -0
- ecoenv-0.1.0/setup.py +20 -0
- ecoenv-0.1.0/tests/test_ecoenv.py +387 -0
ecoenv-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 EcoToolBox
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ecoenv-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ecoenv
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python library for ecological data collection and environmental analysis
|
|
5
|
+
Author: Ane Simões
|
|
6
|
+
Author-email: anes.2017@alunos.utfpr.edu.br
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Python: >=3.7
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: ee
|
|
12
|
+
Requires-Dist: pandas
|
|
13
|
+
Requires-Dist: requests
|
|
14
|
+
Requires-Dist: python-dotenv
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: author-email
|
|
17
|
+
Dynamic: description
|
|
18
|
+
Dynamic: description-content-type
|
|
19
|
+
Dynamic: license
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
Dynamic: requires-dist
|
|
22
|
+
Dynamic: requires-python
|
|
23
|
+
Dynamic: summary
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# USAGE.md
|
|
27
|
+
|
|
28
|
+
## Overview
|
|
29
|
+
|
|
30
|
+
**ecoenv** is a library designed to facilitate data management and processing for scientific and research applications.
|
|
31
|
+
|
|
32
|
+
## Getting Started
|
|
33
|
+
|
|
34
|
+
### Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install ecoenv
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Basic Usage
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import ecoenv
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
ecoenv.autenticateEE('project_name')
|
|
47
|
+
ecoenv.get_environment_data(gdf, ndvi=True, ndwi=True, temperature=True, precipitation=True)
|
|
48
|
+
```
|
|
49
|
+
## Functions
|
|
50
|
+
- `autenticateEE`: Authenticates the user with the Earth Engine API using the specified project name.
|
|
51
|
+
- `get_environment_data`: Retrieves environmental data based on the provided geospatial data frame (gdf) and specified parameters (ndvi, ndwi, temperature, precipitation).
|
|
52
|
+
|
ecoenv-0.1.0/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
|
|
2
|
+
# EcoEnv
|
|
3
|
+
|
|
4
|
+
## Overview
|
|
5
|
+
|
|
6
|
+
EcoEnv is a project focused on environmental monitoring and ecological analysis. This repository contains tools and utilities for tracking environmental metrics and managing ecosystem data.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- Environmental data collection
|
|
11
|
+
- Ecological metrics analysis
|
|
12
|
+
- Data visualization
|
|
13
|
+
- Real-time monitoring
|
|
14
|
+
|
|
15
|
+
## Getting Started
|
|
16
|
+
|
|
17
|
+
### Prerequisites
|
|
18
|
+
|
|
19
|
+
- Python 3.8+
|
|
20
|
+
- pip or conda
|
|
21
|
+
|
|
22
|
+
### Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
git clone https://github.com/EcoToolBox/EcoEnv.git
|
|
26
|
+
cd EcoEnv
|
|
27
|
+
pip install -r requirements.txt
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Usage
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import ecoenk
|
|
34
|
+
|
|
35
|
+
# Your code here
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Contributing
|
|
39
|
+
|
|
40
|
+
Contributions are welcome! Please fork the repository and submit a pull request.
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
45
|
+
|
|
46
|
+
## Support
|
|
47
|
+
|
|
48
|
+
For issues and questions, please open an issue on GitHub.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .env_request import get_environment_data, autenticateEE
|
|
2
|
+
# from .gbif import save_gbif_credentials, delete_gbif_credentials, get_species_autocomplete
|
|
3
|
+
# from .speciesLink import save_specieslink_apikey, delete_specieslink_apikey
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"get_environment_data",
|
|
7
|
+
"autenticateEE"
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
def version():
|
|
11
|
+
return "0.0.1"
|
|
12
|
+
def describe ():
|
|
13
|
+
description = (
|
|
14
|
+
"Eco Env Library\n"
|
|
15
|
+
"Version: {}\n"
|
|
16
|
+
"Implement functions to get environment data from GEE and (soon) GlobalBioticInteractions as:\n"
|
|
17
|
+
" - get_environment_data\n"
|
|
18
|
+
" - autenticateEE\n"
|
|
19
|
+
). format (version())
|
|
20
|
+
print (description)
|
|
21
|
+
return description
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
|
|
2
|
+
import hashlib
|
|
3
|
+
import ee
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
8
|
+
|
|
9
|
+
from requests_cache import logger
|
|
10
|
+
|
|
11
|
+
from .sentinel import get_sentinel_properties
|
|
12
|
+
from .worldclim import get_average_temperature_and_precipation
|
|
13
|
+
|
|
14
|
+
path_root = Path(__file__).resolve().parent
|
|
15
|
+
|
|
16
|
+
def autenticateEE(project):
|
|
17
|
+
ee.Authenticate()
|
|
18
|
+
ee.Initialize(project=project)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_environment_data(gdf, ndvi=True, ndwi=True, temperature=True, precipitation=True):
|
|
22
|
+
if not (ndvi or ndwi or temperature or precipitation):
|
|
23
|
+
raise ValueError("At least one of the following parameters must be True: ndvi, ndwi, temperature, precipitation.")
|
|
24
|
+
|
|
25
|
+
index_list = []
|
|
26
|
+
if ndvi:
|
|
27
|
+
index_list.append('NDVI')
|
|
28
|
+
if ndwi:
|
|
29
|
+
index_list.append('NDWI')
|
|
30
|
+
if temperature:
|
|
31
|
+
index_list.append('Temperature')
|
|
32
|
+
if precipitation:
|
|
33
|
+
index_list.append('Precipitation')
|
|
34
|
+
try:
|
|
35
|
+
env_data = None
|
|
36
|
+
logger.info("Fetching fresh data from sources...")
|
|
37
|
+
|
|
38
|
+
if ndvi or ndwi:
|
|
39
|
+
env_data = get_sentinel_properties(gdf, 250, ndvi, ndwi)
|
|
40
|
+
|
|
41
|
+
if temperature or precipitation:
|
|
42
|
+
if(env_data is None):
|
|
43
|
+
env_data = get_average_temperature_and_precipation(gdf, batch_size=5000, temperature=temperature, precipitation=precipitation)
|
|
44
|
+
else:
|
|
45
|
+
env_data = get_average_temperature_and_precipation(env_data, batch_size=5000, temperature=temperature, precipitation=precipitation)
|
|
46
|
+
|
|
47
|
+
return env_data
|
|
48
|
+
|
|
49
|
+
except Exception as e:
|
|
50
|
+
logger.error(f"Critical error retrieving environment data: {e}", exc_info=True)
|
|
51
|
+
return pd.DataFrame()
|
|
File without changes
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
2
|
+
import hashlib
|
|
3
|
+
import ee
|
|
4
|
+
import geopandas as gpd
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from datetime import date, timedelta
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from diskcache import Cache
|
|
10
|
+
|
|
11
|
+
NDVI = True
|
|
12
|
+
NDWI = True
|
|
13
|
+
|
|
14
|
+
path_root = Path(__file__).resolve().parent
|
|
15
|
+
|
|
16
|
+
cache = Cache(path_root / ".sentinel_cache")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def chunk_gdf(gdf, size=500):
|
|
20
|
+
n = len(gdf)
|
|
21
|
+
for i in range(0, n, size):
|
|
22
|
+
yield gdf.iloc[i:i+size]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def add_index(img):
|
|
26
|
+
index = []
|
|
27
|
+
|
|
28
|
+
if NDVI:
|
|
29
|
+
index.append(img.normalizedDifference(['B8', 'B4']).rename('NDVI'))
|
|
30
|
+
|
|
31
|
+
if NDWI:
|
|
32
|
+
index.append(img.normalizedDifference(['B3', 'B8']).rename('NDWI'))
|
|
33
|
+
|
|
34
|
+
return img.addBands(index)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def generate_cache_key(gdf, ndvi, ndwi):
|
|
38
|
+
lat_min = gdf.geometry.bounds['miny'].min()
|
|
39
|
+
lat_max = gdf.geometry.bounds['maxy'].max()
|
|
40
|
+
lon_min = gdf.geometry.bounds['minx'].min()
|
|
41
|
+
lon_max = gdf.geometry.bounds['maxx'].max()
|
|
42
|
+
|
|
43
|
+
date_min = gdf['eventDate'].min()
|
|
44
|
+
date_max = gdf['eventDate'].max()
|
|
45
|
+
|
|
46
|
+
normalized = {
|
|
47
|
+
"lat_min": float(lat_min),
|
|
48
|
+
"lat_max": float(lat_max),
|
|
49
|
+
"lon_min": float(lon_min),
|
|
50
|
+
"lon_max": float(lon_max),
|
|
51
|
+
"date_min": str(date_min),
|
|
52
|
+
"date_max": str(date_max),
|
|
53
|
+
"ndvi": ndvi,
|
|
54
|
+
"ndwi": ndwi,
|
|
55
|
+
"size": len(gdf)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return hashlib.md5(
|
|
59
|
+
json.dumps(normalized, sort_keys=True).encode()
|
|
60
|
+
).hexdigest()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_index_from_sentinel_batch(batch, ndvi, ndwi):
|
|
64
|
+
|
|
65
|
+
print(f"Processing batch: {len(batch)}")
|
|
66
|
+
|
|
67
|
+
if pd.Timestamp(batch['eventDate'].max()) < pd.Timestamp('2019-01-01'):
|
|
68
|
+
startDate = date.strftime(date.today() - timedelta(30), '%Y-%m-%d')
|
|
69
|
+
endDate = date.strftime(date.today(), '%Y-%m-%d')
|
|
70
|
+
else:
|
|
71
|
+
startDate = batch['eventDate'].min()
|
|
72
|
+
endDate = batch['eventDate'].max()
|
|
73
|
+
|
|
74
|
+
index = []
|
|
75
|
+
if ndvi:
|
|
76
|
+
index.append('NDVI')
|
|
77
|
+
if ndwi:
|
|
78
|
+
index.append('NDWI')
|
|
79
|
+
|
|
80
|
+
img = (
|
|
81
|
+
ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
|
|
82
|
+
.filterDate(startDate, endDate)
|
|
83
|
+
.map(add_index)
|
|
84
|
+
.select(index)
|
|
85
|
+
.median()
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
batch['eventDate'] = batch['eventDate'].astype(str)
|
|
89
|
+
geojson = json.loads(batch.to_json())
|
|
90
|
+
fc = ee.FeatureCollection(geojson)
|
|
91
|
+
|
|
92
|
+
sampled = img.reduceRegions(
|
|
93
|
+
collection=fc,
|
|
94
|
+
reducer=ee.Reducer.mean(),
|
|
95
|
+
scale=30
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
result = sampled.getInfo()
|
|
99
|
+
|
|
100
|
+
gdf_out = gpd.GeoDataFrame.from_features(
|
|
101
|
+
result["features"],
|
|
102
|
+
crs="EPSG:4326"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if not ndvi or not ndwi:
|
|
106
|
+
gdf_out = gdf_out.rename(columns={'mean': index[0]})
|
|
107
|
+
|
|
108
|
+
return gdf_out
|
|
109
|
+
|
|
110
|
+
def get_sentinel_properties(
|
|
111
|
+
gdf,
|
|
112
|
+
batch_size=150,
|
|
113
|
+
ndvi=True,
|
|
114
|
+
ndwi=True
|
|
115
|
+
):
|
|
116
|
+
|
|
117
|
+
if not ndvi and not ndwi:
|
|
118
|
+
raise ValueError("At least one of NDVI or NDWI must be True.")
|
|
119
|
+
|
|
120
|
+
gdf = gdf.sort_values("eventDate", ascending=True)
|
|
121
|
+
|
|
122
|
+
cached_rows = []
|
|
123
|
+
missing_rows = []
|
|
124
|
+
|
|
125
|
+
# 🔍 Separa registros já existentes dos que faltam
|
|
126
|
+
for _, row in gdf.iterrows():
|
|
127
|
+
key = generate_point_key(row, ndvi, ndwi)
|
|
128
|
+
|
|
129
|
+
if key in cache:
|
|
130
|
+
cached_rows.append(cache[key])
|
|
131
|
+
else:
|
|
132
|
+
missing_rows.append(row)
|
|
133
|
+
|
|
134
|
+
print(f"cached: {len(cached_rows)}")
|
|
135
|
+
print(f"missing: {len(missing_rows)}")
|
|
136
|
+
|
|
137
|
+
new_results = []
|
|
138
|
+
|
|
139
|
+
# 🔥 Busca só os faltantes
|
|
140
|
+
if missing_rows:
|
|
141
|
+
missing_gdf = gpd.GeoDataFrame(
|
|
142
|
+
missing_rows,
|
|
143
|
+
geometry=gdf.geometry.name,
|
|
144
|
+
crs=gdf.crs
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
batches = list(chunk_gdf(missing_gdf, batch_size))
|
|
148
|
+
|
|
149
|
+
with ThreadPoolExecutor(max_workers=4) as executor:
|
|
150
|
+
futures = [
|
|
151
|
+
executor.submit(
|
|
152
|
+
get_index_from_sentinel_batch,
|
|
153
|
+
batch,
|
|
154
|
+
ndvi,
|
|
155
|
+
ndwi
|
|
156
|
+
)
|
|
157
|
+
for batch in batches
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
for future in futures:
|
|
161
|
+
result = future.result()
|
|
162
|
+
new_results.append(result)
|
|
163
|
+
|
|
164
|
+
if new_results:
|
|
165
|
+
new_df = pd.concat(new_results, ignore_index=True)
|
|
166
|
+
|
|
167
|
+
for _, row in new_df.iterrows():
|
|
168
|
+
key = generate_point_key(row, ndvi, ndwi)
|
|
169
|
+
cache.set(key, row)
|
|
170
|
+
|
|
171
|
+
else:
|
|
172
|
+
new_df = pd.DataFrame()
|
|
173
|
+
|
|
174
|
+
else:
|
|
175
|
+
new_df = pd.DataFrame()
|
|
176
|
+
|
|
177
|
+
if cached_rows:
|
|
178
|
+
cached_df = pd.DataFrame(cached_rows)
|
|
179
|
+
else:
|
|
180
|
+
cached_df = pd.DataFrame()
|
|
181
|
+
|
|
182
|
+
final_df = pd.concat([cached_df, new_df], ignore_index=True)
|
|
183
|
+
|
|
184
|
+
return final_df
|
|
185
|
+
|
|
186
|
+
def generate_point_key(row, ndvi, ndwi):
|
|
187
|
+
normalized = {
|
|
188
|
+
"lat": float(row["latitude"]),
|
|
189
|
+
"lon": float(row["longitude"]),
|
|
190
|
+
"date": str(row["eventDate"]),
|
|
191
|
+
"ndvi": ndvi,
|
|
192
|
+
"ndwi": ndwi
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return hashlib.md5(
|
|
196
|
+
json.dumps(normalized, sort_keys=True).encode()
|
|
197
|
+
).hexdigest()
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
2
|
+
import hashlib
|
|
3
|
+
import ee
|
|
4
|
+
import geopandas as gpd
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from diskcache import Cache
|
|
9
|
+
|
|
10
|
+
path_root = Path(__file__).resolve().parent
|
|
11
|
+
|
|
12
|
+
# 🔥 Cache persistente
|
|
13
|
+
cache = Cache(path_root / ".worldclim_cache")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def chunk_gdf(gdf, size=500):
|
|
17
|
+
n = len(gdf)
|
|
18
|
+
for i in range(0, n, size):
|
|
19
|
+
yield gdf.iloc[i:i+size]
|
|
20
|
+
|
|
21
|
+
def generate_cache_key(gdf, index, scale):
|
|
22
|
+
if isinstance(gdf, pd.DataFrame):
|
|
23
|
+
gdf = gpd.GeoDataFrame(gdf, geometry=gpd.points_from_xy(gdf.longitude, gdf.latitude), crs='EPSG:4326')
|
|
24
|
+
|
|
25
|
+
if not isinstance(gdf, gpd.GeoDataFrame):
|
|
26
|
+
raise TypeError(
|
|
27
|
+
f"Expected GeoDataFrame, got {type(gdf)}"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if gdf.geometry is None:
|
|
31
|
+
raise ValueError("GeoDataFrame has no geometry column.")
|
|
32
|
+
|
|
33
|
+
if gdf.empty:
|
|
34
|
+
raise ValueError("GeoDataFrame is empty.")
|
|
35
|
+
|
|
36
|
+
bounds = gdf.total_bounds
|
|
37
|
+
|
|
38
|
+
lat_min, lon_min, lat_max, lon_max = (
|
|
39
|
+
bounds[1], # miny
|
|
40
|
+
bounds[0], # minx
|
|
41
|
+
bounds[3], # maxy
|
|
42
|
+
bounds[2], # maxx
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
date_min = gdf['eventDate'].min()
|
|
46
|
+
date_max = gdf['eventDate'].max()
|
|
47
|
+
|
|
48
|
+
normalized = {
|
|
49
|
+
"lat_min": float(lat_min),
|
|
50
|
+
"lat_max": float(lat_max),
|
|
51
|
+
"lon_min": float(lon_min),
|
|
52
|
+
"lon_max": float(lon_max),
|
|
53
|
+
"date_min": str(date_min),
|
|
54
|
+
"date_max": str(date_max),
|
|
55
|
+
"index": index,
|
|
56
|
+
"scale": scale,
|
|
57
|
+
"size": len(gdf)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return hashlib.md5(
|
|
61
|
+
json.dumps(normalized, sort_keys=True).encode()
|
|
62
|
+
).hexdigest()
|
|
63
|
+
|
|
64
|
+
def add_worldclim_properties(gdf, img, scale):
|
|
65
|
+
|
|
66
|
+
print(f"Processing {len(gdf)} records...")
|
|
67
|
+
|
|
68
|
+
gdf['eventDate'] = gdf['eventDate'].astype(str)
|
|
69
|
+
geojson = json.loads(gdf.to_json())
|
|
70
|
+
fc = ee.FeatureCollection(geojson)
|
|
71
|
+
|
|
72
|
+
sampled = img.reduceRegions(
|
|
73
|
+
collection=fc,
|
|
74
|
+
reducer=ee.Reducer.mean(),
|
|
75
|
+
scale=scale
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
result = sampled.getInfo()
|
|
79
|
+
|
|
80
|
+
gdf_out = gpd.GeoDataFrame.from_features(
|
|
81
|
+
result["features"],
|
|
82
|
+
crs="EPSG:4326"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return gdf_out
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_average_temperature_and_precipation(
|
|
89
|
+
gdf,
|
|
90
|
+
scale=1000,
|
|
91
|
+
batch_size=5000,
|
|
92
|
+
temperature=True,
|
|
93
|
+
precipitation=True
|
|
94
|
+
):
|
|
95
|
+
|
|
96
|
+
index = []
|
|
97
|
+
|
|
98
|
+
if temperature:
|
|
99
|
+
index.append("tavg")
|
|
100
|
+
|
|
101
|
+
if precipitation:
|
|
102
|
+
index.append("prec")
|
|
103
|
+
|
|
104
|
+
if not index:
|
|
105
|
+
raise ValueError("At least temperature or precipitation must be True.")
|
|
106
|
+
|
|
107
|
+
cache_key = generate_cache_key(gdf, index, scale)
|
|
108
|
+
|
|
109
|
+
if cache_key in cache:
|
|
110
|
+
print("DiskCache full hit")
|
|
111
|
+
return cache[cache_key]
|
|
112
|
+
|
|
113
|
+
print("Downloading full dataset from GEE...")
|
|
114
|
+
|
|
115
|
+
img = (
|
|
116
|
+
ee.ImageCollection("WORLDCLIM/V1/MONTHLY")
|
|
117
|
+
.select(index)
|
|
118
|
+
.mean()
|
|
119
|
+
.multiply(0.1)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if len(gdf) > batch_size:
|
|
123
|
+
|
|
124
|
+
all_batches = []
|
|
125
|
+
|
|
126
|
+
with ThreadPoolExecutor(max_workers=4) as executor:
|
|
127
|
+
|
|
128
|
+
futures = [
|
|
129
|
+
executor.submit(
|
|
130
|
+
add_worldclim_properties,
|
|
131
|
+
batch,
|
|
132
|
+
img,
|
|
133
|
+
scale
|
|
134
|
+
)
|
|
135
|
+
for batch in chunk_gdf(gdf, size=batch_size)
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
for future in as_completed(futures):
|
|
139
|
+
all_batches.append(future.result())
|
|
140
|
+
|
|
141
|
+
final_gdf = gpd.GeoDataFrame(
|
|
142
|
+
pd.concat(all_batches, ignore_index=True),
|
|
143
|
+
crs="EPSG:4326"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
else:
|
|
147
|
+
final_gdf = add_worldclim_properties(gdf, img, scale)
|
|
148
|
+
|
|
149
|
+
if not temperature or not precipitation:
|
|
150
|
+
final_gdf = final_gdf.rename(columns={"mean": index[0]})
|
|
151
|
+
|
|
152
|
+
cache.set(cache_key, final_gdf)
|
|
153
|
+
|
|
154
|
+
return final_gdf
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ecoenv
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python library for ecological data collection and environmental analysis
|
|
5
|
+
Author: Ane Simões
|
|
6
|
+
Author-email: anes.2017@alunos.utfpr.edu.br
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Python: >=3.7
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: ee
|
|
12
|
+
Requires-Dist: pandas
|
|
13
|
+
Requires-Dist: requests
|
|
14
|
+
Requires-Dist: python-dotenv
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: author-email
|
|
17
|
+
Dynamic: description
|
|
18
|
+
Dynamic: description-content-type
|
|
19
|
+
Dynamic: license
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
Dynamic: requires-dist
|
|
22
|
+
Dynamic: requires-python
|
|
23
|
+
Dynamic: summary
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# USAGE.md
|
|
27
|
+
|
|
28
|
+
## Overview
|
|
29
|
+
|
|
30
|
+
**ecoenv** is a library designed to facilitate data management and processing for scientific and research applications.
|
|
31
|
+
|
|
32
|
+
## Getting Started
|
|
33
|
+
|
|
34
|
+
### Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install ecoenv
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Basic Usage
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import ecoenv
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
ecoenv.autenticateEE('project_name')
|
|
47
|
+
ecoenv.get_environment_data(gdf, ndvi=True, ndwi=True, temperature=True, precipitation=True)
|
|
48
|
+
```
|
|
49
|
+
## Functions
|
|
50
|
+
- `autenticateEE`: Authenticates the user with the Earth Engine API using the specified project name.
|
|
51
|
+
- `get_environment_data`: Retrieves environmental data based on the provided geospatial data frame (gdf) and specified parameters (ndvi, ndwi, temperature, precipitation).
|
|
52
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
ecoenv/__init__.py
|
|
5
|
+
ecoenv/env_request.py
|
|
6
|
+
ecoenv/globi.py
|
|
7
|
+
ecoenv/sentinel.py
|
|
8
|
+
ecoenv/worldclim.py
|
|
9
|
+
ecoenv.egg-info/PKG-INFO
|
|
10
|
+
ecoenv.egg-info/SOURCES.txt
|
|
11
|
+
ecoenv.egg-info/dependency_links.txt
|
|
12
|
+
ecoenv.egg-info/requires.txt
|
|
13
|
+
ecoenv.egg-info/top_level.txt
|
|
14
|
+
tests/test_ecoenv.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ecoenv
|
ecoenv-0.1.0/setup.cfg
ADDED
ecoenv-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name='ecoenv',
|
|
5
|
+
version='0.1.0',
|
|
6
|
+
description='A Python library for ecological data collection and environmental analysis',
|
|
7
|
+
long_description=open('USAGE.md').read(),
|
|
8
|
+
long_description_content_type='text/markdown',
|
|
9
|
+
author='Ane Simões',
|
|
10
|
+
author_email='anes.2017@alunos.utfpr.edu.br',
|
|
11
|
+
packages=find_packages(),
|
|
12
|
+
install_requires=[
|
|
13
|
+
"ee",
|
|
14
|
+
"pandas",
|
|
15
|
+
"requests",
|
|
16
|
+
"python-dotenv",
|
|
17
|
+
],
|
|
18
|
+
license='MIT',
|
|
19
|
+
python_requires='>=3.7',
|
|
20
|
+
)
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import geopandas as gpd
|
|
4
|
+
from shapely.geometry import Point
|
|
5
|
+
from unittest.mock import patch, MagicMock
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ecoenv.env_request import autenticateEE, get_environment_data
|
|
9
|
+
from ecoenv.sentinel import (
|
|
10
|
+
chunk_gdf,
|
|
11
|
+
generate_cache_key as sentinel_cache_key,
|
|
12
|
+
generate_point_key,
|
|
13
|
+
get_index_from_sentinel_batch,
|
|
14
|
+
get_sentinel_properties,
|
|
15
|
+
add_index,
|
|
16
|
+
)
|
|
17
|
+
from ecoenv.worldclim import (
|
|
18
|
+
chunk_gdf as worldclim_chunk_gdf,
|
|
19
|
+
generate_cache_key as worldclim_cache_key,
|
|
20
|
+
add_worldclim_properties,
|
|
21
|
+
get_average_temperature_and_precipation,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def sample_gdf():
|
|
27
|
+
"""Create a sample GeoDataFrame for testing."""
|
|
28
|
+
data = {
|
|
29
|
+
'latitude': [10.0, 20.0, 30.0],
|
|
30
|
+
'longitude': [50.0, 60.0, 70.0],
|
|
31
|
+
'eventDate': ['2020-01-01', '2020-06-15', '2021-01-01'],
|
|
32
|
+
}
|
|
33
|
+
gdf = gpd.GeoDataFrame(
|
|
34
|
+
data,
|
|
35
|
+
geometry=[Point(50, 10), Point(60, 20), Point(70, 30)],
|
|
36
|
+
crs='EPSG:4326'
|
|
37
|
+
)
|
|
38
|
+
return gdf
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.fixture
|
|
42
|
+
def sample_gdf_with_dates():
|
|
43
|
+
"""Create a GeoDataFrame with proper date formatting."""
|
|
44
|
+
data = {
|
|
45
|
+
'latitude': [10.0, 20.0],
|
|
46
|
+
'longitude': [50.0, 60.0],
|
|
47
|
+
'eventDate': pd.to_datetime(['2020-01-01', '2020-06-15']),
|
|
48
|
+
}
|
|
49
|
+
gdf = gpd.GeoDataFrame(
|
|
50
|
+
data,
|
|
51
|
+
geometry=[Point(50, 10), Point(60, 20)],
|
|
52
|
+
crs='EPSG:4326'
|
|
53
|
+
)
|
|
54
|
+
return gdf
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestEnvRequest:
|
|
58
|
+
"""Tests for env_request.py"""
|
|
59
|
+
|
|
60
|
+
@patch('ecoenv.env_request.ee')
|
|
61
|
+
def test_autenticateEE(self, mock_ee):
|
|
62
|
+
"""Test authentication with Earth Engine."""
|
|
63
|
+
autenticateEE("test-project")
|
|
64
|
+
mock_ee.Authenticate.assert_called_once()
|
|
65
|
+
mock_ee.Initialize.assert_called_once_with(project="test-project")
|
|
66
|
+
|
|
67
|
+
@patch('ecoenv.env_request.get_sentinel_properties')
|
|
68
|
+
@patch('ecoenv.env_request.get_average_temperature_and_precipation')
|
|
69
|
+
def test_get_environment_data_all_true(self, mock_temp, mock_sentinel, sample_gdf):
|
|
70
|
+
"""Test get_environment_data with all parameters True."""
|
|
71
|
+
mock_sentinel.return_value = sample_gdf
|
|
72
|
+
mock_temp.return_value = sample_gdf
|
|
73
|
+
|
|
74
|
+
result = get_environment_data(
|
|
75
|
+
sample_gdf,
|
|
76
|
+
ndvi=True,
|
|
77
|
+
ndwi=True,
|
|
78
|
+
temperature=True,
|
|
79
|
+
precipitation=True
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
assert mock_sentinel.called
|
|
83
|
+
assert mock_temp.called
|
|
84
|
+
|
|
85
|
+
@patch('ecoenv.env_request.get_sentinel_properties')
|
|
86
|
+
def test_get_environment_data_ndvi_only(self, mock_sentinel, sample_gdf):
|
|
87
|
+
"""Test get_environment_data with only NDVI."""
|
|
88
|
+
mock_sentinel.return_value = sample_gdf
|
|
89
|
+
|
|
90
|
+
result = get_environment_data(
|
|
91
|
+
sample_gdf,
|
|
92
|
+
ndvi=True,
|
|
93
|
+
ndwi=False,
|
|
94
|
+
temperature=False,
|
|
95
|
+
precipitation=False
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
mock_sentinel.assert_called_once()
|
|
99
|
+
|
|
100
|
+
def test_get_environment_data_all_false(self, sample_gdf):
|
|
101
|
+
"""Test that ValueError is raised when all parameters are False."""
|
|
102
|
+
with pytest.raises(ValueError):
|
|
103
|
+
get_environment_data(
|
|
104
|
+
sample_gdf,
|
|
105
|
+
ndvi=False,
|
|
106
|
+
ndwi=False,
|
|
107
|
+
temperature=False,
|
|
108
|
+
precipitation=False
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@patch('ecoenv.env_request.get_sentinel_properties')
|
|
112
|
+
@patch('ecoenv.env_request.get_average_temperature_and_precipation')
|
|
113
|
+
def test_get_environment_data_exception(self, mock_temp, mock_sentinel, sample_gdf):
|
|
114
|
+
"""Test exception handling in get_environment_data."""
|
|
115
|
+
mock_sentinel.side_effect = Exception("API Error")
|
|
116
|
+
|
|
117
|
+
result = get_environment_data(sample_gdf)
|
|
118
|
+
|
|
119
|
+
assert isinstance(result, pd.DataFrame)
|
|
120
|
+
assert result.empty
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TestSentinel:
|
|
124
|
+
"""Tests for sentinel.py"""
|
|
125
|
+
|
|
126
|
+
def test_chunk_gdf(self, sample_gdf):
|
|
127
|
+
"""Test chunking of GeoDataFrame."""
|
|
128
|
+
chunks = list(chunk_gdf(sample_gdf, size=1))
|
|
129
|
+
assert len(chunks) == 3
|
|
130
|
+
assert len(chunks[0]) == 1
|
|
131
|
+
|
|
132
|
+
def test_chunk_gdf_size_larger_than_gdf(self, sample_gdf):
|
|
133
|
+
"""Test chunking when chunk size is larger than GeoDataFrame."""
|
|
134
|
+
chunks = list(chunk_gdf(sample_gdf, size=10))
|
|
135
|
+
assert len(chunks) == 1
|
|
136
|
+
assert len(chunks[0]) == 3
|
|
137
|
+
|
|
138
|
+
def test_generate_point_key(self, sample_gdf_with_dates):
|
|
139
|
+
"""Test generation of cache key for a point."""
|
|
140
|
+
row = sample_gdf_with_dates.iloc[0]
|
|
141
|
+
key = generate_point_key(row, ndvi=True, ndwi=True)
|
|
142
|
+
|
|
143
|
+
assert isinstance(key, str)
|
|
144
|
+
assert len(key) == 32 # MD5 hash length
|
|
145
|
+
|
|
146
|
+
def test_generate_point_key_consistency(self, sample_gdf_with_dates):
|
|
147
|
+
"""Test that same input generates same key."""
|
|
148
|
+
row = sample_gdf_with_dates.iloc[0]
|
|
149
|
+
key1 = generate_point_key(row, ndvi=True, ndwi=False)
|
|
150
|
+
key2 = generate_point_key(row, ndvi=True, ndwi=False)
|
|
151
|
+
|
|
152
|
+
assert key1 == key2
|
|
153
|
+
|
|
154
|
+
def test_generate_point_key_different_params(self, sample_gdf_with_dates):
|
|
155
|
+
"""Test that different parameters generate different keys."""
|
|
156
|
+
row = sample_gdf_with_dates.iloc[0]
|
|
157
|
+
key1 = generate_point_key(row, ndvi=True, ndwi=False)
|
|
158
|
+
key2 = generate_point_key(row, ndvi=False, ndwi=True)
|
|
159
|
+
|
|
160
|
+
assert key1 != key2
|
|
161
|
+
|
|
162
|
+
@patch('ecoenv.sentinel.ee')
|
|
163
|
+
def test_add_index(self, mock_ee):
|
|
164
|
+
"""Test add_index function."""
|
|
165
|
+
mock_img = MagicMock()
|
|
166
|
+
mock_ndvi = MagicMock()
|
|
167
|
+
mock_ndwi = MagicMock()
|
|
168
|
+
|
|
169
|
+
mock_img.normalizedDifference.side_effect = [mock_ndvi, mock_ndwi]
|
|
170
|
+
mock_ndvi.rename.return_value = mock_ndvi
|
|
171
|
+
mock_ndwi.rename.return_value = mock_ndwi
|
|
172
|
+
mock_img.addBands.return_value = mock_img
|
|
173
|
+
|
|
174
|
+
result = add_index(mock_img)
|
|
175
|
+
|
|
176
|
+
assert mock_img.normalizedDifference.called
|
|
177
|
+
assert mock_img.addBands.called
|
|
178
|
+
|
|
179
|
+
def test_sentinel_cache_key_generation(self, sample_gdf_with_dates):
|
|
180
|
+
"""Test cache key generation for sentinel data."""
|
|
181
|
+
key = sentinel_cache_key(sample_gdf_with_dates, ndvi=True, ndwi=True)
|
|
182
|
+
|
|
183
|
+
assert isinstance(key, str)
|
|
184
|
+
assert len(key) == 32
|
|
185
|
+
|
|
186
|
+
def test_sentinel_cache_key_different_dates(self, sample_gdf_with_dates):
|
|
187
|
+
"""Test that different dates generate different cache keys."""
|
|
188
|
+
gdf2 = sample_gdf_with_dates.copy()
|
|
189
|
+
gdf2['eventDate'] = pd.to_datetime(['2019-01-01', '2019-06-15'])
|
|
190
|
+
|
|
191
|
+
key1 = sentinel_cache_key(sample_gdf_with_dates, ndvi=True, ndwi=True)
|
|
192
|
+
key2 = sentinel_cache_key(gdf2, ndvi=True, ndwi=True)
|
|
193
|
+
|
|
194
|
+
assert key1 != key2
|
|
195
|
+
|
|
196
|
+
def test_get_sentinel_properties_ndvi_ndwi_both_false(self, sample_gdf_with_dates):
|
|
197
|
+
"""Test ValueError when both NDVI and NDWI are False."""
|
|
198
|
+
with pytest.raises(ValueError):
|
|
199
|
+
get_sentinel_properties(sample_gdf_with_dates, ndvi=False, ndwi=False)
|
|
200
|
+
|
|
201
|
+
@patch('ecoenv.sentinel.get_index_from_sentinel_batch')
|
|
202
|
+
def test_get_sentinel_properties_single_batch(self, mock_batch, sample_gdf_with_dates):
|
|
203
|
+
"""Test get_sentinel_properties with single batch."""
|
|
204
|
+
mock_batch.return_value = sample_gdf_with_dates
|
|
205
|
+
|
|
206
|
+
result = get_sentinel_properties(
|
|
207
|
+
sample_gdf_with_dates,
|
|
208
|
+
batch_size=150,
|
|
209
|
+
ndvi=True,
|
|
210
|
+
ndwi=True
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
assert isinstance(result, pd.DataFrame)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class TestWorldclim:
|
|
217
|
+
"""Tests for worldclim.py"""
|
|
218
|
+
|
|
219
|
+
def test_worldclim_chunk_gdf(self, sample_gdf):
|
|
220
|
+
"""Test chunking of worldclim GeoDataFrame."""
|
|
221
|
+
chunks = list(worldclim_chunk_gdf(sample_gdf, size=2))
|
|
222
|
+
assert len(chunks) == 2
|
|
223
|
+
|
|
224
|
+
def test_worldclim_generate_cache_key_with_geodataframe(self, sample_gdf_with_dates):
|
|
225
|
+
"""Test cache key generation for worldclim with GeoDataFrame."""
|
|
226
|
+
key = worldclim_cache_key(sample_gdf_with_dates, ["tavg"], 1000)
|
|
227
|
+
|
|
228
|
+
assert isinstance(key, str)
|
|
229
|
+
assert len(key) == 32
|
|
230
|
+
|
|
231
|
+
def test_worldclim_generate_cache_key_with_dataframe(self):
|
|
232
|
+
"""Test cache key generation with regular DataFrame."""
|
|
233
|
+
data = {
|
|
234
|
+
'latitude': [10.0, 20.0],
|
|
235
|
+
'longitude': [50.0, 60.0],
|
|
236
|
+
'eventDate': pd.to_datetime(['2020-01-01', '2020-06-15']),
|
|
237
|
+
}
|
|
238
|
+
df = pd.DataFrame(data)
|
|
239
|
+
|
|
240
|
+
key = worldclim_cache_key(df, ["tavg"], 1000)
|
|
241
|
+
|
|
242
|
+
assert isinstance(key, str)
|
|
243
|
+
assert len(key) == 32
|
|
244
|
+
|
|
245
|
+
def test_worldclim_generate_cache_key_invalid_type(self):
|
|
246
|
+
"""Test that invalid type raises TypeError."""
|
|
247
|
+
with pytest.raises(TypeError):
|
|
248
|
+
worldclim_cache_key([1, 2, 3], ["tavg"], 1000)
|
|
249
|
+
|
|
250
|
+
def test_worldclim_generate_cache_key_empty_gdf(self, sample_gdf_with_dates):
|
|
251
|
+
"""Test that empty GeoDataFrame raises ValueError."""
|
|
252
|
+
empty_gdf = sample_gdf_with_dates.iloc[0:0]
|
|
253
|
+
|
|
254
|
+
with pytest.raises(ValueError):
|
|
255
|
+
worldclim_cache_key(empty_gdf, ["tavg"], 1000)
|
|
256
|
+
|
|
257
|
+
@patch('ecoenv.worldclim.ee')
|
|
258
|
+
def test_add_worldclim_properties(self, mock_ee, sample_gdf_with_dates):
|
|
259
|
+
"""Test adding worldclim properties to GeoDataFrame."""
|
|
260
|
+
mock_img = MagicMock()
|
|
261
|
+
mock_fc = MagicMock()
|
|
262
|
+
mock_ee.FeatureCollection.return_value = mock_fc
|
|
263
|
+
|
|
264
|
+
mock_sampled = MagicMock()
|
|
265
|
+
mock_img.reduceRegions.return_value = mock_sampled
|
|
266
|
+
mock_sampled.getInfo.return_value = {
|
|
267
|
+
"features": [
|
|
268
|
+
{
|
|
269
|
+
"type": "Feature",
|
|
270
|
+
"geometry": {"type": "Point", "coordinates": [50, 10]},
|
|
271
|
+
"properties": {"tavg": 25.0}
|
|
272
|
+
}
|
|
273
|
+
]
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
result = add_worldclim_properties(sample_gdf_with_dates, mock_img, 1000)
|
|
277
|
+
|
|
278
|
+
assert isinstance(result, gpd.GeoDataFrame)
|
|
279
|
+
|
|
280
|
+
def test_get_average_temperature_and_precipation_no_index(self, sample_gdf_with_dates):
|
|
281
|
+
"""Test ValueError when both temperature and precipitation are False."""
|
|
282
|
+
with pytest.raises(ValueError):
|
|
283
|
+
get_average_temperature_and_precipation(
|
|
284
|
+
sample_gdf_with_dates,
|
|
285
|
+
temperature=False,
|
|
286
|
+
precipitation=False
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
@patch('ecoenv.worldclim.add_worldclim_properties')
|
|
290
|
+
@patch('ecoenv.worldclim.ee')
|
|
291
|
+
def test_get_average_temperature_small_batch(
|
|
292
|
+
self,
|
|
293
|
+
mock_ee,
|
|
294
|
+
mock_add,
|
|
295
|
+
sample_gdf_with_dates
|
|
296
|
+
):
|
|
297
|
+
"""Test temperature and precipitation retrieval for small batch."""
|
|
298
|
+
mock_add.return_value = sample_gdf_with_dates
|
|
299
|
+
mock_collection = MagicMock()
|
|
300
|
+
mock_ee.ImageCollection.return_value = mock_collection
|
|
301
|
+
mock_collection.select.return_value = mock_collection
|
|
302
|
+
mock_collection.mean.return_value = mock_collection
|
|
303
|
+
mock_collection.multiply.return_value = MagicMock()
|
|
304
|
+
|
|
305
|
+
result = get_average_temperature_and_precipation(
|
|
306
|
+
sample_gdf_with_dates,
|
|
307
|
+
batch_size=5000,
|
|
308
|
+
temperature=True,
|
|
309
|
+
precipitation=True
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
assert isinstance(result, gpd.GeoDataFrame)
|
|
313
|
+
|
|
314
|
+
@patch('ecoenv.worldclim.add_worldclim_properties')
|
|
315
|
+
@patch('ecoenv.worldclim.ee')
|
|
316
|
+
def test_get_average_temperature_large_batch(
|
|
317
|
+
self,
|
|
318
|
+
mock_ee,
|
|
319
|
+
mock_add,
|
|
320
|
+
sample_gdf_with_dates
|
|
321
|
+
):
|
|
322
|
+
"""Test temperature retrieval with multiple batches."""
|
|
323
|
+
large_gdf = pd.concat(
|
|
324
|
+
[sample_gdf_with_dates] * 3000,
|
|
325
|
+
ignore_index=True
|
|
326
|
+
)
|
|
327
|
+
large_gdf = gpd.GeoDataFrame(
|
|
328
|
+
large_gdf,
|
|
329
|
+
geometry=sample_gdf_with_dates.geometry,
|
|
330
|
+
crs='EPSG:4326'
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
mock_add.return_value = large_gdf.iloc[:1000]
|
|
334
|
+
mock_collection = MagicMock()
|
|
335
|
+
mock_ee.ImageCollection.return_value = mock_collection
|
|
336
|
+
mock_collection.select.return_value = mock_collection
|
|
337
|
+
mock_collection.mean.return_value = mock_collection
|
|
338
|
+
mock_collection.multiply.return_value = MagicMock()
|
|
339
|
+
|
|
340
|
+
result = get_average_temperature_and_precipation(
|
|
341
|
+
large_gdf,
|
|
342
|
+
batch_size=1000,
|
|
343
|
+
temperature=True,
|
|
344
|
+
precipitation=False
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
assert isinstance(result, gpd.GeoDataFrame)
|
|
348
|
+
|
|
349
|
+
@patch('ecoenv.worldclim.ee')
|
|
350
|
+
def test_get_average_temperature_only(self, mock_ee, sample_gdf_with_dates):
|
|
351
|
+
"""Test retrieving only temperature data."""
|
|
352
|
+
mock_collection = MagicMock()
|
|
353
|
+
mock_ee.ImageCollection.return_value = mock_collection
|
|
354
|
+
mock_collection.select.return_value = mock_collection
|
|
355
|
+
mock_collection.mean.return_value = mock_collection
|
|
356
|
+
mock_collection.multiply.return_value = MagicMock()
|
|
357
|
+
|
|
358
|
+
with patch('ecoenv.worldclim.add_worldclim_properties') as mock_add:
|
|
359
|
+
mock_add.return_value = sample_gdf_with_dates
|
|
360
|
+
|
|
361
|
+
result = get_average_temperature_and_precipation(
|
|
362
|
+
sample_gdf_with_dates,
|
|
363
|
+
temperature=True,
|
|
364
|
+
precipitation=False
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
assert isinstance(result, gpd.GeoDataFrame)
|
|
368
|
+
|
|
369
|
+
@patch('ecoenv.worldclim.ee')
|
|
370
|
+
def test_get_average_precipitation_only(self, mock_ee, sample_gdf_with_dates):
|
|
371
|
+
"""Test retrieving only precipitation data."""
|
|
372
|
+
mock_collection = MagicMock()
|
|
373
|
+
mock_ee.ImageCollection.return_value = mock_collection
|
|
374
|
+
mock_collection.select.return_value = mock_collection
|
|
375
|
+
mock_collection.mean.return_value = mock_collection
|
|
376
|
+
mock_collection.multiply.return_value = MagicMock()
|
|
377
|
+
|
|
378
|
+
with patch('ecoenv.worldclim.add_worldclim_properties') as mock_add:
|
|
379
|
+
mock_add.return_value = sample_gdf_with_dates
|
|
380
|
+
|
|
381
|
+
result = get_average_temperature_and_precipation(
|
|
382
|
+
sample_gdf_with_dates,
|
|
383
|
+
temperature=False,
|
|
384
|
+
precipitation=True
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
assert isinstance(result, gpd.GeoDataFrame)
|