water-column-sonar-processing 0.0.1__py3-none-any.whl → 26.1.14__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.

Potentially problematic release.


This version of water-column-sonar-processing might be problematic. Click here for more details.

Files changed (60) hide show
  1. water_column_sonar_processing/__init__.py +13 -0
  2. water_column_sonar_processing/aws/__init__.py +7 -0
  3. water_column_sonar_processing/aws/dynamodb_manager.py +355 -0
  4. water_column_sonar_processing/aws/s3_manager.py +418 -0
  5. water_column_sonar_processing/aws/s3fs_manager.py +64 -0
  6. {model → water_column_sonar_processing}/aws/sns_manager.py +10 -21
  7. {model → water_column_sonar_processing}/aws/sqs_manager.py +11 -19
  8. water_column_sonar_processing/cruise/__init__.py +4 -0
  9. water_column_sonar_processing/cruise/create_empty_zarr_store.py +129 -0
  10. water_column_sonar_processing/cruise/datatree_manager.py +21 -0
  11. water_column_sonar_processing/cruise/resample_regrid.py +323 -0
  12. water_column_sonar_processing/geometry/__init__.py +13 -0
  13. water_column_sonar_processing/geometry/elevation_manager.py +111 -0
  14. water_column_sonar_processing/geometry/geometry_manager.py +241 -0
  15. water_column_sonar_processing/geometry/line_simplification.py +176 -0
  16. water_column_sonar_processing/geometry/pmtile_generation.py +266 -0
  17. water_column_sonar_processing/geometry/spatiotemporal.py +106 -0
  18. water_column_sonar_processing/index/__init__.py +3 -0
  19. water_column_sonar_processing/index/index_manager.py +381 -0
  20. water_column_sonar_processing/model/__init__.py +3 -0
  21. water_column_sonar_processing/model/zarr_manager.py +741 -0
  22. water_column_sonar_processing/processing/__init__.py +4 -0
  23. water_column_sonar_processing/processing/raw_to_netcdf.py +320 -0
  24. water_column_sonar_processing/processing/raw_to_zarr.py +331 -0
  25. water_column_sonar_processing/utility/__init__.py +13 -0
  26. {model → water_column_sonar_processing}/utility/cleaner.py +7 -7
  27. water_column_sonar_processing/utility/constants.py +118 -0
  28. {model → water_column_sonar_processing}/utility/pipeline_status.py +47 -24
  29. water_column_sonar_processing/utility/timestamp.py +12 -0
  30. water_column_sonar_processing-26.1.14.dist-info/METADATA +240 -0
  31. water_column_sonar_processing-26.1.14.dist-info/RECORD +34 -0
  32. {water_column_sonar_processing-0.0.1.dist-info → water_column_sonar_processing-26.1.14.dist-info}/WHEEL +1 -1
  33. {water_column_sonar_processing-0.0.1.dist-info → water_column_sonar_processing-26.1.14.dist-info/licenses}/LICENSE +1 -1
  34. water_column_sonar_processing-26.1.14.dist-info/top_level.txt +1 -0
  35. __init__.py +0 -0
  36. model/__init__.py +0 -0
  37. model/aws/__init__.py +0 -0
  38. model/aws/dynamodb_manager.py +0 -149
  39. model/aws/s3_manager.py +0 -356
  40. model/aws/s3fs_manager.py +0 -74
  41. model/cruise/__init__.py +0 -0
  42. model/cruise/create_empty_zarr_store.py +0 -166
  43. model/cruise/resample_regrid.py +0 -248
  44. model/geospatial/__init__.py +0 -0
  45. model/geospatial/geometry_manager.py +0 -194
  46. model/geospatial/geometry_simplification.py +0 -81
  47. model/geospatial/pmtile_generation.py +0 -74
  48. model/index/__init__.py +0 -0
  49. model/index/index.py +0 -228
  50. model/model.py +0 -138
  51. model/utility/__init__.py +0 -0
  52. model/utility/constants.py +0 -56
  53. model/utility/timestamp.py +0 -12
  54. model/zarr/__init__.py +0 -0
  55. model/zarr/bar.py +0 -28
  56. model/zarr/foo.py +0 -11
  57. model/zarr/zarr_manager.py +0 -298
  58. water_column_sonar_processing-0.0.1.dist-info/METADATA +0 -89
  59. water_column_sonar_processing-0.0.1.dist-info/RECORD +0 -32
  60. water_column_sonar_processing-0.0.1.dist-info/top_level.txt +0 -2
@@ -0,0 +1,129 @@
1
+ import os
2
+ import tempfile
3
+
4
+ import numpy as np
5
+
6
+ from water_column_sonar_processing.aws import DynamoDBManager, S3Manager
7
+ from water_column_sonar_processing.model import ZarrManager
8
+ from water_column_sonar_processing.utility import Cleaner
9
+ from water_column_sonar_processing.utility import Constants
10
+
11
+
12
+ # TODO: change name to "CreateLocalEmptyZarrStore"
13
+ class CreateEmptyZarrStore:
14
+ #######################################################
15
+ def __init__(
16
+ self,
17
+ ):
18
+ self.__overwrite = True
19
+ # self.input_bucket_name = os.environ.get("INPUT_BUCKET_NAME")
20
+ # self.output_bucket_name = os.environ.get("OUTPUT_BUCKET_NAME")
21
+
22
+ #######################################################
23
+ @staticmethod
24
+ def create_cruise_level_zarr_store(
25
+ output_bucket_name: str,
26
+ ship_name: str,
27
+ cruise_name: str,
28
+ sensor_name: str,
29
+ table_name: str,
30
+ ) -> None:
31
+ """
32
+ Initialize zarr store for the entire cruise which aggregates all the raw data.
33
+ All cruises will be resampled at 20 cm depth.
34
+ # tempdir="/tmp", # TODO: create better tmp directory for testing
35
+ """
36
+ tempdir = tempfile.TemporaryDirectory()
37
+ try:
38
+ dynamo_db_manager = DynamoDBManager()
39
+ s3_manager = S3Manager()
40
+
41
+ df = dynamo_db_manager.get_table_as_df(
42
+ table_name=table_name,
43
+ cruise_name=cruise_name,
44
+ )
45
+
46
+ # TODO: filter the dataframe just for enums >= LEVEL_1_PROCESSING
47
+ # df[df['PIPELINE_STATUS'] < PipelineStatus.LEVEL_1_PROCESSING] = np.nan
48
+
49
+ # TODO: VERIFY GEOJSON EXISTS as prerequisite!!! ...no more geojson needed
50
+
51
+ print(f"DataFrame shape: {df.shape}")
52
+ cruise_channels = list(
53
+ set([i for sublist in df["CHANNELS"].dropna() for i in sublist])
54
+ )
55
+ cruise_channels.sort()
56
+
57
+ consolidated_zarr_width = np.sum(
58
+ df["NUM_PING_TIME_DROPNA"].dropna().astype(int)
59
+ )
60
+
61
+ # [4] max measurement resolution for the whole cruise
62
+ # Each max-echo-range is paired with water-level and then find the max of that
63
+ cruise_max_echo_range = np.max(
64
+ (df["MAX_ECHO_RANGE"] + df["WATER_LEVEL"]).dropna().astype(float)
65
+ ) # max_echo_range now includes water_level
66
+
67
+ print(f"cruise_max_echo_range: {cruise_max_echo_range}")
68
+
69
+ # [5] get number of channels
70
+ cruise_frequencies = [
71
+ float(i) for i in df["FREQUENCIES"].dropna().values.flatten()[0]
72
+ ]
73
+
74
+ new_width = int(consolidated_zarr_width)
75
+ ################################################################
76
+ # Delete any existing stores
77
+ zarr_prefix = os.path.join(
78
+ str(Constants.LEVEL_2.value), ship_name, cruise_name, sensor_name
79
+ )
80
+ child_objects = s3_manager.get_child_objects(
81
+ bucket_name=output_bucket_name,
82
+ sub_prefix=zarr_prefix,
83
+ )
84
+
85
+ if len(child_objects) > 0:
86
+ s3_manager.delete_nodd_objects(
87
+ bucket_name=output_bucket_name,
88
+ objects=child_objects,
89
+ )
90
+ ################################################################
91
+ # Create new model store
92
+ zarr_manager = ZarrManager()
93
+ zarr_manager.create_zarr_store(
94
+ path=tempdir.name,
95
+ ship_name=ship_name,
96
+ cruise_name=cruise_name,
97
+ sensor_name=sensor_name,
98
+ frequencies=cruise_frequencies,
99
+ width=new_width,
100
+ max_echo_range=cruise_max_echo_range,
101
+ # cruise_min_epsilon=cruise_min_epsilon,
102
+ calibration_status=True,
103
+ )
104
+ #################################################################
105
+ # TODO: would be more elegant to create directly into s3 bucket
106
+ s3_manager.upload_zarr_store_to_s3(
107
+ output_bucket_name=output_bucket_name,
108
+ local_directory=tempdir.name,
109
+ object_prefix=zarr_prefix,
110
+ cruise_name=cruise_name,
111
+ )
112
+ #################################################################
113
+ # TODO: verify count of the files uploaded
114
+ #################################################################
115
+ # TODO: update enum in dynamodb
116
+ print("Done creating cruise level zarr store.")
117
+ #################################################################
118
+ except Exception as err:
119
+ raise RuntimeError(
120
+ f"Problem trying to create new cruise model store, {err}"
121
+ )
122
+ finally:
123
+ cleaner = Cleaner()
124
+ cleaner.delete_local_files()
125
+ # TODO: should delete zarr store in temp directory too?
126
+ print("Done creating cruise level model store")
127
+
128
+
129
+ ###########################################################
@@ -0,0 +1,21 @@
1
+ # ### https://xarray-datatree.readthedocs.io/en/latest/data-structures.html
2
+ # import xarray as xr
3
+ # from datatree import DataTree
4
+ #
5
+ #
6
+ # class DatatreeManager:
7
+ # #######################################################
8
+ # def __init__(
9
+ # self,
10
+ # ):
11
+ # self.dtype = "float32"
12
+ #
13
+ # #################################################################
14
+ # def create_datatree(
15
+ # self,
16
+ # input_ds,
17
+ # ) -> None:
18
+ # ds1 = xr.Dataset({"foo": "orange"})
19
+ # dt = DataTree(name="root", dataset=ds1) # create root node
20
+ # # ds2 = xr.Dataset({"bar": 0}, coords={"y": ("y", [0, 1, 2])})
21
+ # return dt
@@ -0,0 +1,323 @@
1
+ import gc
2
+ import warnings
3
+ from pathlib import Path
4
+
5
+ import numpy as np
6
+ import xarray as xr
7
+
8
+ from water_column_sonar_processing.aws import DynamoDBManager
9
+ from water_column_sonar_processing.model import ZarrManager
10
+
11
+ warnings.simplefilter("ignore", category=RuntimeWarning)
12
+
13
+
14
+ class ResampleRegrid:
15
+ #######################################################
16
+ def __init__(
17
+ self,
18
+ ):
19
+ self.__overwrite = True
20
+ self.dtype = "float32"
21
+
22
+ #################################################################
23
+ def interpolate_data(
24
+ self,
25
+ input_xr: xr.Dataset,
26
+ ping_times: np.ndarray,
27
+ all_cruise_depth_values: np.ndarray, # includes water_level offset
28
+ water_level: float = 0.0,
29
+ ) -> np.ndarray:
30
+ """
31
+ Input dataset is passed in along with times and depth values to regrid to.
32
+ """
33
+ print("Interpolating dataset.")
34
+ try:
35
+ # add offset for the water level to the whole input xarray
36
+ input_xr.depth.values = input_xr.depth.values + water_level
37
+
38
+ data = np.empty(
39
+ ( # Depth / Time / Frequency
40
+ len(all_cruise_depth_values),
41
+ len(ping_times),
42
+ len(input_xr.frequency_nominal.values),
43
+ ),
44
+ dtype=self.dtype,
45
+ )
46
+
47
+ data[:] = np.nan
48
+
49
+ regrid_resample = xr.DataArray( # where data will be written to
50
+ data=data,
51
+ coords={
52
+ "depth": all_cruise_depth_values,
53
+ "time": ping_times,
54
+ "frequency": input_xr.frequency_nominal.values,
55
+ },
56
+ dims=("depth", "time", "frequency"),
57
+ name="Sv",
58
+ )
59
+
60
+ channels = input_xr.channel.values
61
+ for channel in range(len(channels)):
62
+ gc.collect()
63
+ max_depths = np.nanmax(
64
+ a=input_xr.depth.sel(channel=input_xr.channel[channel]).values,
65
+ # + water_level,
66
+ axis=1,
67
+ )
68
+ superset_of_max_depths = set(max_depths)
69
+ set_of_max_depths = list(
70
+ {x for x in superset_of_max_depths if x == x}
71
+ ) # To speed things up resample in groups denoted by max_depth -- so samples might no longer be adjacent
72
+ for select_max_depth in set_of_max_depths:
73
+ # TODO: for nan just skip and leave all nan's
74
+ select_indices = [
75
+ i
76
+ for i in range(0, len(max_depths))
77
+ if max_depths[i] == select_max_depth
78
+ ]
79
+
80
+ data_select = input_xr.Sv.sel(channel=input_xr.channel[channel])[
81
+ select_indices, :
82
+ ].T.values
83
+
84
+ times_select = input_xr.ping_time.values[select_indices]
85
+ # input_xr.depth[0][0] -> [0., 499.9] before
86
+ # input_xr.depth.values = input_xr.depth.values + water_level # issue here!! overwritting all the data
87
+ # input_xr.depth[0][0] -> [7.5, 507.40] after
88
+ depths_all = input_xr.depth.sel(
89
+ channel=input_xr.channel[channel],
90
+ ping_time=input_xr.ping_time[select_indices[0]],
91
+ ).values
92
+ depths_select = depths_all[~np.isnan(depths_all)]
93
+ #
94
+ da_select = xr.DataArray(
95
+ data=data_select[: len(depths_select), :],
96
+ dims=("depth", "time"),
97
+ coords={
98
+ "depth": depths_select,
99
+ "time": times_select,
100
+ },
101
+ )
102
+ # 'resampled' is now the interpolated superset of new dimensions
103
+ resampled = da_select.interp( # need to define the data with water level (domain)
104
+ depth=all_cruise_depth_values, # and need to interpolate over the (range)
105
+ method="nearest",
106
+ assume_sorted=True,
107
+ ) # good through here, @27 is -3.11 which is 5.4 m depth
108
+
109
+ ### write to outptut ###
110
+ regrid_resample.loc[ # ~150 MB for 5001x7706x4
111
+ dict(
112
+ time=times_select,
113
+ frequency=input_xr.frequency_nominal.values[channel],
114
+ )
115
+ ] = resampled
116
+ # print(f"updated {len(times_select)} ping times")
117
+ gc.collect()
118
+ return regrid_resample.values.copy()
119
+ except Exception as err:
120
+ raise RuntimeError(f"Problem finding the dynamodb table, {err}")
121
+ finally:
122
+ gc.collect()
123
+ print("Done interpolating dataset.")
124
+
125
+ #################################################################
126
+ def resample_regrid(
127
+ self,
128
+ ship_name,
129
+ cruise_name,
130
+ sensor_name,
131
+ table_name,
132
+ bucket_name,
133
+ override_select_files=None,
134
+ endpoint_url=None,
135
+ ) -> None:
136
+ """
137
+ The goal here is to interpolate the dataset against the depth values already populated
138
+ in the existing file level model stores. We open the cruise-level store with model for
139
+ read/write operations. We open the file-level store with Xarray to leverage tools for
140
+ resampling and subsetting the dataset.
141
+ """
142
+ print("Resample Regrid, Interpolating dataset.")
143
+ try:
144
+ zarr_manager = ZarrManager()
145
+
146
+ output_zarr_store = zarr_manager.open_s3_zarr_store_with_zarr(
147
+ ship_name=ship_name,
148
+ cruise_name=cruise_name,
149
+ sensor_name=sensor_name,
150
+ output_bucket_name=bucket_name,
151
+ endpoint_url=endpoint_url,
152
+ )
153
+
154
+ dynamo_db_manager = DynamoDBManager()
155
+ cruise_df = dynamo_db_manager.get_table_as_df(
156
+ cruise_name=cruise_name,
157
+ table_name=table_name,
158
+ )
159
+
160
+ #########################################################
161
+ #########################################################
162
+ all_file_names = cruise_df["FILE_NAME"]
163
+
164
+ if override_select_files is not None:
165
+ all_file_names = override_select_files
166
+
167
+ # Iterate files
168
+ for file_name in all_file_names:
169
+ gc.collect()
170
+ file_name_stem = Path(file_name).stem
171
+ print(f"Processing file: {file_name_stem}.")
172
+
173
+ if f"{file_name_stem}.raw" not in list(cruise_df["FILE_NAME"]):
174
+ print("Raw file file_stem not found in dynamodb.")
175
+ raise Exception("Raw file file_stem not found in dynamodb.")
176
+
177
+ # status = PipelineStatus['LEVEL_1_PROCESSING']
178
+ # TODO: filter rows by enum success, filter the dataframe just for enums >= LEVEL_1_PROCESSING
179
+ # df[df['PIPELINE_STATUS'] < PipelineStatus.LEVEL_1_PROCESSING] = np.nan
180
+
181
+ # Get index from all cruise files. Note: should be based on which are included in cruise.
182
+ index = int(
183
+ cruise_df.index[cruise_df["FILE_NAME"] == f"{file_name_stem}.raw"][
184
+ 0
185
+ ]
186
+ )
187
+
188
+ # Get input store
189
+ input_xr_zarr_store = zarr_manager.open_s3_zarr_store_with_xarray(
190
+ ship_name=ship_name,
191
+ cruise_name=cruise_name,
192
+ sensor_name=sensor_name,
193
+ file_name_stem=file_name_stem,
194
+ bucket_name=bucket_name,
195
+ endpoint_url=endpoint_url,
196
+ )
197
+
198
+ #########################################################################
199
+ # This is the vertical offset of the sensor related to the ocean surface
200
+ # See https://echopype.readthedocs.io/en/stable/data-proc-additional.html
201
+ if "water_level" in input_xr_zarr_store.keys():
202
+ water_level = float(input_xr_zarr_store.water_level.values)
203
+ else:
204
+ water_level = 0.0
205
+ #########################################################################
206
+ # [3] Get needed time indices — along the x-axis
207
+ # Offset from start index to insert new dataset. Note that missing values are excluded.
208
+ ping_time_cumsum = np.insert(
209
+ np.cumsum(
210
+ cruise_df["NUM_PING_TIME_DROPNA"].dropna().to_numpy(dtype=int)
211
+ ),
212
+ obj=0,
213
+ values=0,
214
+ )
215
+ start_ping_time_index = ping_time_cumsum[index]
216
+ end_ping_time_index = ping_time_cumsum[index + 1]
217
+
218
+ max_echo_range = np.max( # Should water level go in here?
219
+ (cruise_df["MAX_ECHO_RANGE"] + cruise_df["WATER_LEVEL"])
220
+ .dropna()
221
+ .astype(np.float32)
222
+ )
223
+ # cruise_min_epsilon = np.min(
224
+ # cruise_df["MIN_ECHO_RANGE"].dropna().astype(float)
225
+ # ) # TODO: currently overwriting to 0.25 m
226
+
227
+ all_cruise_depth_values = zarr_manager.get_depth_values(
228
+ max_echo_range=max_echo_range,
229
+ # cruise_min_epsilon=cruise_min_epsilon,
230
+ )
231
+
232
+ if set(
233
+ input_xr_zarr_store.Sv.dims
234
+ ) != { # Cruise dimensions are: (depth, time, frequency)
235
+ "channel",
236
+ "ping_time",
237
+ "range_sample",
238
+ }:
239
+ raise Exception("Xarray dimensions are not as expected.")
240
+
241
+ # indices, geospatial = geo_manager.read_s3_geo_json( # TODO: remove this!!!!
242
+ # ship_name=ship_name,
243
+ # cruise_name=cruise_name,
244
+ # sensor_name=sensor_name,
245
+ # file_name_stem=file_name_stem,
246
+ # input_xr_zarr_store=input_xr_zarr_store,
247
+ # endpoint_url=endpoint_url,
248
+ # output_bucket_name=bucket_name,
249
+ # )
250
+
251
+ input_xr = input_xr_zarr_store # .isel(ping_time=indices)
252
+
253
+ ping_times = input_xr.ping_time.values
254
+ output_zarr_store["time"][start_ping_time_index:end_ping_time_index] = (
255
+ input_xr.ping_time.data
256
+ )
257
+
258
+ # --- UPDATING --- # # TODO: problem, this returns dimensionless array
259
+ regrid_resample = self.interpolate_data(
260
+ input_xr=input_xr,
261
+ ping_times=ping_times,
262
+ all_cruise_depth_values=all_cruise_depth_values, # should accommodate the water_level already
263
+ water_level=water_level,
264
+ )
265
+
266
+ print(
267
+ f"start_ping_time_index: {start_ping_time_index}, end_ping_time_index: {end_ping_time_index}"
268
+ )
269
+ #########################################################################
270
+ # write Sv values to cruise-level-model-store
271
+
272
+ for fff in range(regrid_resample.shape[-1]):
273
+ output_zarr_store["Sv"][
274
+ : regrid_resample[:, :, fff].shape[0],
275
+ start_ping_time_index:end_ping_time_index,
276
+ fff,
277
+ ] = regrid_resample[:, :, fff]
278
+ #########################################################################
279
+ # in the future. See https://github.com/CI-CMG/water-column-sonar-processing/issues/11
280
+ if "detected_seafloor_depth" in list(input_xr.variables):
281
+ print("Adding detected_seafloor_depth to output")
282
+ detected_seafloor_depth = input_xr.detected_seafloor_depth.values
283
+ detected_seafloor_depth[detected_seafloor_depth == 0.0] = np.nan
284
+
285
+ # As requested, use the lowest frequencies to determine bottom
286
+ detected_seafloor_depths = detected_seafloor_depth[0, :]
287
+
288
+ detected_seafloor_depths[detected_seafloor_depths == 0.0] = np.nan
289
+ print(f"min depth measured: {np.nanmin(detected_seafloor_depths)}")
290
+ print(f"max depth measured: {np.nanmax(detected_seafloor_depths)}")
291
+ output_zarr_store["bottom"][
292
+ start_ping_time_index:end_ping_time_index
293
+ ] = detected_seafloor_depths
294
+ #
295
+ #########################################################################
296
+ # [5] write subset of latitude/longitude
297
+ # output_zarr_store["latitude"][
298
+ # start_ping_time_index:end_ping_time_index
299
+ # ] = geospatial.dropna()[
300
+ # "latitude"
301
+ # ].values # TODO: get from ds_sv directly, dont need geojson anymore
302
+ # output_zarr_store["longitude"][
303
+ # start_ping_time_index:end_ping_time_index
304
+ # ] = geospatial.dropna()["longitude"].values
305
+ #########################################################################
306
+ output_zarr_store["latitude"][
307
+ start_ping_time_index:end_ping_time_index
308
+ ] = input_xr_zarr_store.latitude.dropna(dim="ping_time").values
309
+ output_zarr_store["longitude"][
310
+ start_ping_time_index:end_ping_time_index
311
+ ] = input_xr_zarr_store.longitude.dropna(dim="ping_time").values
312
+ #########################################################################
313
+ except Exception as err:
314
+ raise RuntimeError(f"Problem with resample_regrid, {err}")
315
+ finally:
316
+ print("Exiting resample_regrid.")
317
+ # TODO: read across times and verify dataset was written?
318
+ gc.collect()
319
+
320
+ #######################################################
321
+
322
+
323
+ ###########################################################
@@ -0,0 +1,13 @@
1
+ from .elevation_manager import ElevationManager
2
+ from .geometry_manager import GeometryManager
3
+ from .line_simplification import LineSimplification
4
+ from .pmtile_generation import PMTileGeneration
5
+ from .spatiotemporal import Spatiotemporal
6
+
7
+ __all__ = [
8
+ "ElevationManager",
9
+ "GeometryManager",
10
+ "LineSimplification",
11
+ "PMTileGeneration",
12
+ "Spatiotemporal",
13
+ ]
@@ -0,0 +1,111 @@
1
+ """
2
+ https://gis.ngdc.noaa.gov/arcgis/rest/services/DEM_mosaics/DEM_global_mosaic/ImageServer/identify?geometry=-31.70235%2C13.03332&geometryType=esriGeometryPoint&returnGeometry=false&returnCatalogItems=false&f=json
3
+
4
+ https://gis.ngdc.noaa.gov/arcgis/rest/services/DEM_mosaics/DEM_global_mosaic/ImageServer/
5
+ identify?
6
+ geometry=-31.70235%2C13.03332
7
+ &geometryType=esriGeometryPoint
8
+ &returnGeometry=false
9
+ &returnCatalogItems=false
10
+ &f=json
11
+ {"objectId":0,"name":"Pixel","value":"-5733","location":{"x":-31.702349999999999,"y":13.03332,"spatialReference":{"wkid":4326,"latestWkid":4326}},"properties":null,"catalogItems":null,"catalogItemVisibilities":[]}
12
+ -5733
13
+
14
+ (base) rudy:deleteME rudy$ curl https://api.opentopodata.org/v1/gebco2020?locations=13.03332,-31.70235
15
+ {
16
+ "results": [
17
+ {
18
+ "dataset": "gebco2020",
19
+ "elevation": -5729.0,
20
+ "location": {
21
+ "lat": 13.03332,
22
+ "lng": -31.70235
23
+ }
24
+ }
25
+ ],
26
+ "status": "OK"
27
+ }
28
+ """
29
+
30
+ import json
31
+ import time
32
+ from collections.abc import Generator
33
+
34
+ import requests
35
+
36
+
37
+ def chunked(ll: list, n: int) -> Generator:
38
+ # Yields successively n-sized chunks from ll.
39
+ for i in range(0, len(ll), n):
40
+ yield ll[i : i + n]
41
+
42
+
43
+ class ElevationManager:
44
+ #######################################################
45
+ def __init__(
46
+ self,
47
+ ):
48
+ self.DECIMAL_PRECISION = 5 # precision for GPS coordinates
49
+ self.TIMEOUT_SECONDS = 10
50
+
51
+ #######################################################
52
+ def get_arcgis_elevation(
53
+ self,
54
+ lngs: list,
55
+ lats: list,
56
+ chunk_size: int = 500, # I think this is the api limit
57
+ ) -> int:
58
+ # Reference: https://developers.arcgis.com/rest/services-reference/enterprise/map-to-image/
59
+ # Info: https://www.arcgis.com/home/item.html?id=c876e3c96a8642ab8557646a3b4fa0ff
60
+ ### 'https://gis.ngdc.noaa.gov/arcgis/rest/services/DEM_mosaics/DEM_global_mosaic/ImageServer/identify?geometry={"points":[[-31.70235,13.03332],[-32.70235,14.03332]]}&geometryType=esriGeometryMultipoint&returnGeometry=false&returnCatalogItems=false&f=json'
61
+ if len(lngs) != len(lats):
62
+ raise ValueError("lngs and lats must have same length")
63
+
64
+ geometryType = "esriGeometryMultipoint" # TODO: allow single point?
65
+
66
+ depths = []
67
+
68
+ list_of_points = [list(elem) for elem in list(zip(lngs, lats))]
69
+ for chunk in chunked(list_of_points, chunk_size):
70
+ time.sleep(0.1)
71
+ # order: (lng, lat)
72
+ geometry = f'{{"points":{str(chunk)}}}'
73
+ url = f"https://gis.ngdc.noaa.gov/arcgis/rest/services/DEM_mosaics/DEM_global_mosaic/ImageServer/identify?geometry={geometry}&geometryType={geometryType}&returnGeometry=false&returnCatalogItems=false&f=json"
74
+ result = requests.get(url, timeout=self.TIMEOUT_SECONDS)
75
+ res = json.loads(result.content.decode("utf8"))
76
+ if "results" in res:
77
+ for element in res["results"]:
78
+ depths.append(float(element["value"]))
79
+ elif "value" in res:
80
+ depths.append(float(res["value"]))
81
+
82
+ return depths
83
+
84
+ # def get_gebco_bathymetry_elevation(self) -> int:
85
+ # # Documentation: https://www.opentopodata.org/datasets/gebco2020/
86
+ # latitude = 13.03332
87
+ # longitude = -31.70235
88
+ # dataset = "gebco2020"
89
+ # url = f"https://api.opentopodata.org/v1/{dataset}?locations={latitude},{longitude}"
90
+ # pass
91
+
92
+ # def get_elevation(
93
+ # self,
94
+ # df,
95
+ # lat_column,
96
+ # lon_column,
97
+ # ) -> int:
98
+ # """Query service using lat, lon. add the elevation values as a new column."""
99
+ # url = r'https://epqs.nationalmap.gov/v1/json?'
100
+ # elevations = []
101
+ # for lat, lon in zip(df[lat_column], df[lon_column]):
102
+ # # define rest query params
103
+ # params = {
104
+ # 'output': 'json',
105
+ # 'x': lon,
106
+ # 'y': lat,
107
+ # 'units': 'Meters'
108
+ # }
109
+ # result = requests.get((url + urllib.parse.urlencode(params)))
110
+ # elevations.append(result.json()['value'])
111
+ # return elevations