eotdl 2025.2.10__py3-none-any.whl → 2025.4.2__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.
Files changed (61) hide show
  1. eotdl/__init__.py +1 -1
  2. eotdl/access/__init__.py +13 -3
  3. eotdl/access/download.py +47 -14
  4. eotdl/access/search.py +33 -5
  5. eotdl/access/sentinelhub/__init__.py +6 -2
  6. eotdl/access/sentinelhub/client.py +7 -6
  7. eotdl/access/sentinelhub/evalscripts.py +266 -0
  8. eotdl/access/sentinelhub/parameters.py +101 -23
  9. eotdl/access/sentinelhub/utils.py +54 -15
  10. eotdl/cli.py +2 -2
  11. eotdl/commands/datasets.py +28 -31
  12. eotdl/commands/models.py +27 -30
  13. eotdl/commands/stac.py +57 -0
  14. eotdl/curation/__init__.py +0 -8
  15. eotdl/curation/stac/__init__.py +1 -8
  16. eotdl/curation/stac/api.py +58 -0
  17. eotdl/curation/stac/stac.py +31 -341
  18. eotdl/datasets/__init__.py +2 -2
  19. eotdl/datasets/ingest.py +36 -161
  20. eotdl/datasets/retrieve.py +0 -9
  21. eotdl/datasets/stage.py +64 -0
  22. eotdl/files/__init__.py +0 -2
  23. eotdl/files/ingest.bck +178 -0
  24. eotdl/files/ingest.py +237 -166
  25. eotdl/{datasets → files}/metadata.py +16 -17
  26. eotdl/models/__init__.py +1 -1
  27. eotdl/models/ingest.py +35 -158
  28. eotdl/models/stage.py +63 -0
  29. eotdl/repos/APIRepo.py +1 -1
  30. eotdl/repos/DatasetsAPIRepo.py +56 -43
  31. eotdl/repos/FilesAPIRepo.py +260 -167
  32. eotdl/repos/ModelsAPIRepo.py +50 -42
  33. eotdl/repos/STACAPIRepo.py +40 -0
  34. eotdl/repos/__init__.py +1 -0
  35. eotdl/tools/time_utils.py +3 -3
  36. {eotdl-2025.2.10.dist-info → eotdl-2025.4.2.dist-info}/METADATA +1 -1
  37. eotdl-2025.4.2.dist-info/RECORD +66 -0
  38. eotdl/curation/stac/assets.py +0 -110
  39. eotdl/curation/stac/dataframe.py +0 -172
  40. eotdl/curation/stac/dataframe_bck.py +0 -253
  41. eotdl/curation/stac/dataframe_labeling.py +0 -63
  42. eotdl/curation/stac/extensions/__init__.py +0 -23
  43. eotdl/curation/stac/extensions/base.py +0 -30
  44. eotdl/curation/stac/extensions/dem.py +0 -18
  45. eotdl/curation/stac/extensions/eo.py +0 -117
  46. eotdl/curation/stac/extensions/label/__init__.py +0 -7
  47. eotdl/curation/stac/extensions/label/base.py +0 -136
  48. eotdl/curation/stac/extensions/label/image_name_labeler.py +0 -203
  49. eotdl/curation/stac/extensions/label/scaneo.py +0 -219
  50. eotdl/curation/stac/extensions/ml_dataset.py +0 -648
  51. eotdl/curation/stac/extensions/projection.py +0 -44
  52. eotdl/curation/stac/extensions/raster.py +0 -53
  53. eotdl/curation/stac/extensions/sar.py +0 -55
  54. eotdl/curation/stac/extent.py +0 -158
  55. eotdl/curation/stac/parsers.py +0 -61
  56. eotdl/datasets/download.py +0 -104
  57. eotdl/files/list_files.py +0 -13
  58. eotdl/models/metadata.py +0 -43
  59. eotdl-2025.2.10.dist-info/RECORD +0 -81
  60. {eotdl-2025.2.10.dist-info → eotdl-2025.4.2.dist-info}/WHEEL +0 -0
  61. {eotdl-2025.2.10.dist-info → eotdl-2025.4.2.dist-info}/entry_points.txt +0 -0
eotdl/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2025.02.10"
1
+ __version__ = "2025.04.02"
eotdl/access/__init__.py CHANGED
@@ -2,6 +2,16 @@
2
2
  Data access module for eotdl package.
3
3
  """
4
4
 
5
- from .download import download_sentinel_imagery, search_and_download_sentinel_imagery
6
- from .search import search_sentinel_imagery
7
- from .sentinelhub.parameters import SUPPORTED_SENSORS
5
+ from .download import (
6
+ download_sentinel_imagery,
7
+ search_and_download_sentinel_imagery,
8
+ advanced_imagery_download,
9
+ )
10
+ from .search import search_sentinel_imagery, advanced_imagery_search
11
+ from .sentinelhub.parameters import (
12
+ SUPPORTED_COLLECTION_IDS,
13
+ DATA_COLLECTION_ID,
14
+ get_default_parameters,
15
+ OUTPUT_FORMAT,
16
+ )
17
+ from .sentinelhub.evalscripts import EvalScripts
eotdl/access/download.py CHANGED
@@ -2,49 +2,82 @@
2
2
  Download imagery
3
3
  """
4
4
 
5
- from datetime import datetime
5
+ from datetime import datetime, timedelta
6
6
  from typing import Union, List, Optional
7
7
 
8
8
  from .sentinelhub import (
9
9
  SHClient,
10
- SH_PARAMETERS_DICT,
11
10
  evaluate_sentinel_parameters,
12
11
  imagery_from_tmp_to_dir,
12
+ get_default_parameters,
13
+ SHParameters,
14
+ filter_times,
13
15
  )
14
- from .search import search_sentinel_imagery
16
+ from .search import advanced_imagery_search
15
17
 
16
18
 
17
19
  def download_sentinel_imagery(
18
20
  output: str,
19
21
  time_interval: Union[str, datetime, List[Union[str, datetime]]],
20
22
  bounding_box: List[Union[int, float]],
21
- sensor: str,
23
+ collection_id: str,
22
24
  name: Optional[str] = None,
23
25
  ) -> None:
24
26
  """
25
27
  Download Sentinel imagery
26
28
  """
27
- evaluate_sentinel_parameters(sensor, time_interval, bounding_box, output)
29
+ evaluate_sentinel_parameters(time_interval, bounding_box, collection_id, output)
30
+ parameters = get_default_parameters(collection_id)
31
+ return advanced_imagery_download(
32
+ output, time_interval, bounding_box, parameters, name
33
+ )
34
+
28
35
 
29
- client = SHClient()
30
- parameters = SH_PARAMETERS_DICT[sensor]()
36
+ def advanced_imagery_download(
37
+ output: str,
38
+ time_interval: Union[str, datetime, List[Union[str, datetime]]],
39
+ bounding_box: List[Union[int, float]],
40
+ parameters: SHParameters,
41
+ name: Optional[str] = None,
42
+ ) -> None:
43
+ """
44
+ Advanced imagery download
45
+ """
46
+ evaluate_sentinel_parameters(
47
+ time_interval, bounding_box, output=output, parameters=parameters
48
+ )
49
+ client = SHClient(parameters.BASE_URL)
31
50
 
32
- results = search_sentinel_imagery(time_interval, bounding_box, sensor)
33
- timestamps = [date.strftime("%Y-%m-%d") for date in results.get_timestamps()]
51
+ results = advanced_imagery_search(time_interval, bounding_box, parameters)
52
+ timestamps = results.get_timestamps()
53
+ time_difference = timedelta(hours=1)
54
+ filtered_timestamps = filter_times(timestamps, time_difference)
34
55
 
35
56
  requests_list = []
36
- for date in timestamps:
37
- requests_list.append(client.request_data(date, bounding_box, parameters))
57
+ for date in filtered_timestamps:
58
+ timestamp_interval = (date - time_difference, date + time_difference)
59
+ requests_list.append(
60
+ client.request_data(timestamp_interval, bounding_box, parameters)
61
+ )
38
62
  if len(requests_list) == 0:
39
- print(f"No images found for {sensor} in the specified time: {time_interval}")
63
+ print(
64
+ f"No images found for {parameters.DATA_COLLECTION.api_id} in the specified time: {time_interval}"
65
+ )
40
66
  return
41
67
  elif len(requests_list) <= 2:
42
68
  bulk = False
43
69
  else:
44
70
  bulk = True
45
71
 
46
- data = client.download_data(requests_list)
47
- imagery_from_tmp_to_dir(output, client.tmp_dir, name=name, bulk=bulk)
72
+ client.download_data(requests_list)
73
+ imagery_from_tmp_to_dir(
74
+ output,
75
+ bounding_box,
76
+ client.tmp_dir,
77
+ name=name,
78
+ bulk=bulk,
79
+ output_format=parameters.OUTPUT_FORMAT.value,
80
+ )
48
81
 
49
82
 
50
83
  def search_and_download_sentinel_imagery(
eotdl/access/search.py CHANGED
@@ -5,20 +5,48 @@ Search imagery
5
5
  from typing import Union, List
6
6
  from datetime import datetime
7
7
 
8
- from .sentinelhub import SHClient, SH_PARAMETERS_DICT, evaluate_sentinel_parameters
8
+ from .sentinelhub import (
9
+ SHClient,
10
+ get_default_parameters,
11
+ evaluate_sentinel_parameters,
12
+ SHParameters,
13
+ supports_cloud_coverage,
14
+ )
9
15
 
10
16
 
11
17
  def search_sentinel_imagery(
12
18
  time_interval: Union[str, datetime, List[Union[str, datetime]]],
13
19
  bounding_box: List[Union[int, float]],
14
- sensor: str,
20
+ collection_id: str,
15
21
  ) -> None:
16
22
  """
17
23
  Search Sentinel imagery
18
24
  """
19
25
  evaluate_sentinel_parameters(
20
- sensor, time_interval, bounding_box, output_needed=False
26
+ time_interval, bounding_box, collection_id, output_needed=False
21
27
  )
22
- client = SHClient()
23
- parameters = SH_PARAMETERS_DICT[sensor]()
28
+ parameters = get_default_parameters(collection_id)
29
+ client = SHClient(parameters.BASE_URL)
30
+ return client.search_data(bounding_box, time_interval, parameters)
31
+
32
+
33
+ def advanced_imagery_search(
34
+ time_interval: Union[str, datetime, List[Union[str, datetime]]],
35
+ bounding_box: List[Union[int, float]],
36
+ parameters: SHParameters,
37
+ ) -> None:
38
+ """
39
+ Advanced imagery search
40
+ """
41
+ evaluate_sentinel_parameters(
42
+ time_interval, bounding_box, output_needed=False, parameters=parameters
43
+ )
44
+
45
+ if (
46
+ supports_cloud_coverage(parameters.DATA_COLLECTION.api_id)
47
+ and parameters.MAX_CLOUD_COVERAGE
48
+ ):
49
+ parameters.FILTER = "eo:cloud_cover < " + str(parameters.MAX_CLOUD_COVERAGE)
50
+
51
+ client = SHClient(parameters.BASE_URL)
24
52
  return client.search_data(bounding_box, time_interval, parameters)
@@ -3,6 +3,10 @@ Sentinel-Hub data access module.
3
3
  """
4
4
 
5
5
  from .client import SHClient
6
- from .parameters import SHParameters, SH_PARAMETERS_DICT
6
+ from .parameters import (
7
+ SHParameters,
8
+ supports_cloud_coverage,
9
+ get_default_parameters,
10
+ )
7
11
  from .evalscripts import EvalScripts
8
- from .utils import evaluate_sentinel_parameters, imagery_from_tmp_to_dir
12
+ from .utils import evaluate_sentinel_parameters, imagery_from_tmp_to_dir, filter_times
@@ -3,6 +3,7 @@ Module for managing the Sentinel Hub configuration and data access
3
3
  """
4
4
 
5
5
  import json
6
+ from datetime import datetime
6
7
  from os import getenv
7
8
  from os.path import exists
8
9
  from sentinelhub import (
@@ -13,13 +14,11 @@ from sentinelhub import (
13
14
  CRS,
14
15
  SentinelHubRequest,
15
16
  SentinelHubDownloadClient,
16
- MimeType,
17
17
  )
18
18
  import uuid
19
19
 
20
20
  from ...repos.AuthRepo import AuthRepo
21
21
  from .parameters import SHParameters
22
- from ...tools.time_utils import prepare_time_interval
23
22
  from ...tools.geo_utils import compute_image_size
24
23
 
25
24
 
@@ -28,7 +27,7 @@ class SHClient:
28
27
  Client class to manage the Sentinel Hub Python interface.
29
28
  """
30
29
 
31
- def __init__(self) -> None:
30
+ def __init__(self, base_url) -> None:
32
31
  """ """
33
32
  self.config = SHConfig()
34
33
  if getenv("SH_CLIENT_ID") and getenv("SH_CLIENT_SECRET"):
@@ -57,6 +56,7 @@ class SHClient:
57
56
  else:
58
57
  self.config.sh_client_id = creds["SH_CLIENT_ID"]
59
58
  self.config.sh_client_secret = creds["SH_CLIENT_SECRET"]
59
+ self.config.sh_base_url = base_url
60
60
  self.catalog = SentinelHubCatalog(config=self.config)
61
61
  self.tmp_dir = "/tmp/sentinelhub/" + str(uuid.uuid4())
62
62
 
@@ -77,12 +77,11 @@ class SHClient:
77
77
  return search_iterator
78
78
 
79
79
  def request_data(
80
- self, time_interval, bounding_box: list, parameters: SHParameters
80
+ self, time_interval: datetime, bounding_box: list, parameters: SHParameters
81
81
  ) -> list:
82
82
  """
83
83
  Request data from Sentinel Hub
84
84
  """
85
- time_interval = prepare_time_interval(time_interval)
86
85
  bounding_box, _ = compute_image_size(bounding_box, parameters)
87
86
 
88
87
  return SentinelHubRequest(
@@ -95,7 +94,9 @@ class SHClient:
95
94
  mosaicking_order=parameters.MOSAICKING_ORDER,
96
95
  )
97
96
  ],
98
- responses=[SentinelHubRequest.output_response("default", MimeType.TIFF)],
97
+ responses=[
98
+ SentinelHubRequest.output_response("default", parameters.OUTPUT_FORMAT)
99
+ ],
99
100
  bbox=bounding_box,
100
101
  size=bbox_to_dimensions(bounding_box, parameters.RESOLUTION),
101
102
  config=self.config,
@@ -27,6 +27,28 @@ class EvalScripts:
27
27
  }
28
28
  """
29
29
 
30
+ SENTINEL_1_ENHANCED_VISUALIZATION = """
31
+ //VERSION=3
32
+ function setup() {
33
+ return {
34
+ input: ["VV", "VH", "dataMask"],
35
+ output: { bands: 4 }
36
+ }
37
+ }
38
+
39
+ function evaluatePixel(sample) {
40
+ var water_threshold = 25; //lower means more water
41
+
42
+ if (sample.VV / sample.VH > water_threshold) {
43
+ // watervis
44
+ return [sample.VV, 8 * sample.VV, 0.5 + 3 * sample.VV + 2000 * sample.VH, sample.dataMask];
45
+ } else {
46
+ // landvis
47
+ return [3 * sample.VV, 1.1 * sample.VV + 8.75 * sample.VH, 1.75 * sample.VH, sample.dataMask];
48
+ }
49
+ }
50
+ """
51
+
30
52
  SENTINEL_2_L1C = """
31
53
  //VERSION=3
32
54
  function setup() {
@@ -113,6 +135,26 @@ class EvalScripts:
113
135
  }
114
136
  """
115
137
 
138
+ SENTINEL_2_L2A_TRUE_COLOR = """
139
+ //VERSION=3
140
+ let minVal = 0.0;
141
+ let maxVal = 0.4;
142
+
143
+ let viz = new HighlightCompressVisualizer(minVal, maxVal);
144
+
145
+ function setup() {
146
+ return {
147
+ input: ["B04", "B03", "B02","dataMask"],
148
+ output: { bands: 4 }
149
+ };
150
+ }
151
+
152
+ function evaluatePixel(samples) {
153
+ let val = [samples.B04, samples.B03, samples.B02,samples.dataMask];
154
+ return viz.processList(val);
155
+ }
156
+ """
157
+
116
158
  DEM = """
117
159
  //VERSION=3
118
160
 
@@ -130,3 +172,227 @@ class EvalScripts:
130
172
  return [sample.DEM]
131
173
  }
132
174
  """
175
+
176
+ DEM_TOPOGRAPHIC = """
177
+ //VERSION=3
178
+ // To set custom max and min values, set
179
+ // defaultVis to false and choose your max and
180
+ // min values. The color map will then be scaled
181
+ // to those max and min values
182
+ const defaultVis = true
183
+ const max = 9000
184
+ const min = -9000
185
+
186
+ function setup() {
187
+ return {
188
+ input: ["DEM", "dataMask"],
189
+ output: [
190
+ { id: "default", bands: 4, sampleTYPE: 'AUTO' },
191
+ { id: "index", bands: 1, sampleType: 'FLOAT32' },
192
+ { id: "dataMask", bands: 1 }
193
+ ]
194
+ };
195
+ }
196
+
197
+ function updateMap(max, min) {
198
+ const numIntervals = map.length
199
+ const intervalLength = (max - min) / (numIntervals - 1);
200
+ for (let i = 0; i < numIntervals; i++) {
201
+ map[i][0] = max - intervalLength * i
202
+ }
203
+ }
204
+
205
+ const map = [
206
+ [8000, 0xffffff],
207
+ [7000, 0xf2f2f2],
208
+ [6000, 0xe5e5e5],
209
+ [5500, 0x493811],
210
+ [5000, 0x5e4c26],
211
+ [4500, 0x726038],
212
+ [4000, 0x87724c],
213
+ [3500, 0x998760],
214
+ [3000, 0xad9b75],
215
+ [2500, 0xc1af89],
216
+ [2000, 0xd6c49e],
217
+ [1500, 0xead8af],
218
+ [1000, 0xfcedbf],
219
+ [900, 0xaadda0],
220
+ [800, 0xa5d69b],
221
+ [700, 0x96ce8e],
222
+ [600, 0x84c17a],
223
+ [500, 0x7aba70],
224
+ [400, 0x72b266],
225
+ [300, 0x5ea354],
226
+ [200, 0x4c933f],
227
+ [100, 0x3d873d],
228
+ [75, 0x357c3a],
229
+ [50, 0x2d722d],
230
+ [25, 0x266821],
231
+ [10, 0x1e5e14],
232
+ [0.01, 0x165407]
233
+ ];
234
+
235
+ if (!defaultVis) updateMap(max, min);
236
+ // add ocean color
237
+ map.push([-10000, 0x0f0f8c])
238
+ const visualizer = new ColorMapVisualizer(map);
239
+
240
+ function evaluatePixel(sample) {
241
+ let val = sample.DEM;
242
+ let imgVals = visualizer.process(val)
243
+
244
+ // Return the 4 inputs and define content for each one
245
+ return {
246
+ default: [...imgVals, sample.dataMask],
247
+ index: [val],
248
+ dataMask: [sample.dataMask]
249
+ };
250
+ }
251
+ """
252
+
253
+ HLS_FALSE_COLOR = """
254
+ //VERSION=3
255
+
256
+ function setup() {
257
+ return {
258
+ input: ["NIR_Narrow", "Red", "Green", "dataMask"],
259
+ output: { bands: 3 }
260
+ };
261
+ }
262
+
263
+ function evaluatePixel(sample) {
264
+ return [2.5 * sample.NIR_Narrow, 2.5 * sample.Red, 2.5 * sample.Green, sample.dataMask];
265
+ }
266
+ """
267
+
268
+ HLS_TRUE_COLOR = """
269
+ //VERSION=3
270
+
271
+ function setup() {
272
+ return {
273
+ input: ["Blue","Green","Red", "dataMask"],
274
+ output: { bands: 4 }
275
+ };
276
+ }
277
+
278
+ function evaluatePixel(sample) {
279
+ return [2.5 * sample.Red, 2.5 * sample.Green, 2.5 * sample.Blue, sample.dataMask];
280
+ }
281
+ """
282
+
283
+ HLS_NDVI = """
284
+ //VERSION=3
285
+
286
+ function setup() {
287
+ return {
288
+ input: ["NIR_Narrow", "Red", "dataMask"],
289
+ output: { bands: 4 }
290
+ };
291
+ }
292
+
293
+ function evaluatePixel(sample) {
294
+ var ndvi = (sample.NIR_Narrow - sample.Red) / (sample.NIR_Narrow + sample.Red)
295
+
296
+ if (ndvi<-1.1) return [0,0,0, sample.dataMask];
297
+ else if (ndvi<-0.2) return [0.75,0.75,0.75, sample.dataMask];
298
+ else if (ndvi<-0.1) return [0.86,0.86,0.86, sample.dataMask];
299
+ else if (ndvi<0) return [1,1,0.88, sample.dataMask];
300
+ else if (ndvi<0.025) return [1,0.98,0.8, sample.dataMask];
301
+ else if (ndvi<0.05) return [0.93,0.91,0.71, sample.dataMask];
302
+ else if (ndvi<0.075) return [0.87,0.85,0.61, sample.dataMask];
303
+ else if (ndvi<0.1) return [0.8,0.78,0.51, sample.dataMask];
304
+ else if (ndvi<0.125) return [0.74,0.72,0.42, sample.dataMask];
305
+ else if (ndvi<0.15) return [0.69,0.76,0.38, sample.dataMask];
306
+ else if (ndvi<0.175) return [0.64,0.8,0.35, sample.dataMask];
307
+ else if (ndvi<0.2) return [0.57,0.75,0.32, sample.dataMask];
308
+ else if (ndvi<0.25) return [0.5,0.7,0.28, sample.dataMask];
309
+ else if (ndvi<0.3) return [0.44,0.64,0.25, sample.dataMask];
310
+ else if (ndvi<0.35) return [0.38,0.59,0.21, sample.dataMask];
311
+ else if (ndvi<0.4) return [0.31,0.54,0.18, sample.dataMask];
312
+ else if (ndvi<0.45) return [0.25,0.49,0.14, sample.dataMask];
313
+ else if (ndvi<0.5) return [0.19,0.43,0.11, sample.dataMask];
314
+ else if (ndvi<0.55) return [0.13,0.38,0.07, sample.dataMask];
315
+ else if (ndvi<0.6) return [0.06,0.33,0.04, sample.dataMask];
316
+ else return [0,0.27,0, sample.dataMask];
317
+ }
318
+ """
319
+
320
+ LANDSAT_OT_L2_TRUE_COLOR = """
321
+ //VERSION=3
322
+
323
+ let minVal = 0.0;
324
+ let maxVal = 0.4;
325
+
326
+ let viz = new DefaultVisualizer(minVal, maxVal);
327
+
328
+ function evaluatePixel(samples) {
329
+ let val = [samples.B04, samples.B03, samples.B02, samples.dataMask];
330
+ return viz.processList(val);
331
+ }
332
+
333
+ function setup() {
334
+ return {
335
+ input: [{
336
+ bands: [ "B02", "B03", "B04", "dataMask" ]
337
+ }],
338
+ output: { bands: 4 } }
339
+ }
340
+ """
341
+ LANDSAT_OT_L2_NDVI = """
342
+ //VERSION=3
343
+
344
+ function setup() {
345
+ return {
346
+ input: ["B03", "B04", "B05", "dataMask"],
347
+ output: [
348
+ { id: "default", bands: 4 },
349
+ { id: "index", bands: 1, sampleType: "FLOAT32" },
350
+ { id: "eobrowserStats", bands: 2 },
351
+ { id: "dataMask", bands: 1 },
352
+ ],
353
+ };
354
+ }
355
+
356
+ function evaluatePixel(samples) {
357
+ let val = index(samples.B05, samples.B04);
358
+ let imgVals = null;
359
+ // The library for tiffs works well only if there is only one channel returned.
360
+ // So we encode the "no data" as NaN here and ignore NaNs on frontend.
361
+ const indexVal = samples.dataMask === 1 && val >= -1 && val <= 1 ? val : NaN;
362
+
363
+ if (val < -1.1) imgVals = [0, 0, 0, samples.dataMask];
364
+ else if (val < -0.2) imgVals = [0.75, 0.75, 0.75, samples.dataMask];
365
+ else if (val < -0.1) imgVals = [0.86, 0.86, 0.86, samples.dataMask];
366
+ else if (val < 0) imgVals = [1, 1, 0.88, samples.dataMask];
367
+ else if (val < 0.025) imgVals = [1, 0.98, 0.8, samples.dataMask];
368
+ else if (val < 0.05) imgVals = [0.93, 0.91, 0.71, samples.dataMask];
369
+ else if (val < 0.075) imgVals = [0.87, 0.85, 0.61, samples.dataMask];
370
+ else if (val < 0.1) imgVals = [0.8, 0.78, 0.51, samples.dataMask];
371
+ else if (val < 0.125) imgVals = [0.74, 0.72, 0.42, samples.dataMask];
372
+ else if (val < 0.15) imgVals = [0.69, 0.76, 0.38, samples.dataMask];
373
+ else if (val < 0.175) imgVals = [0.64, 0.8, 0.35, samples.dataMask];
374
+ else if (val < 0.2) imgVals = [0.57, 0.75, 0.32, samples.dataMask];
375
+ else if (val < 0.25) imgVals = [0.5, 0.7, 0.28, samples.dataMask];
376
+ else if (val < 0.3) imgVals = [0.44, 0.64, 0.25, samples.dataMask];
377
+ else if (val < 0.35) imgVals = [0.38, 0.59, 0.21, samples.dataMask];
378
+ else if (val < 0.4) imgVals = [0.31, 0.54, 0.18, samples.dataMask];
379
+ else if (val < 0.45) imgVals = [0.25, 0.49, 0.14, samples.dataMask];
380
+ else if (val < 0.5) imgVals = [0.19, 0.43, 0.11, samples.dataMask];
381
+ else if (val < 0.55) imgVals = [0.13, 0.38, 0.07, samples.dataMask];
382
+ else if (val < 0.6) imgVals = [0.06, 0.33, 0.04, samples.dataMask];
383
+ else imgVals = [0, 0.27, 0, samples.dataMask];
384
+
385
+ return {
386
+ default: imgVals,
387
+ index: [indexVal],
388
+ eobrowserStats: [val, isCloud(samples) ? 1 : 0],
389
+ dataMask: [samples.dataMask],
390
+ };
391
+ }
392
+
393
+ function isCloud(samples) {
394
+ const NGDR = index(samples.B03, samples.B04);
395
+ const bRatio = (samples.B03 - 0.175) / (0.39 - 0.175);
396
+ return bRatio > 1 || (bRatio > 0 && NGDR > 0);
397
+ }
398
+ """