1+ from datetime import datetime , time , timedelta , timezone
2+ import re
3+ from typing import Any
4+ import os
5+ from utils .mdx import MDX
6+ from utils .ames import open_ames_1001
7+ from pprint import pprint
8+
9+ short_name = "dc82dstc4"
10+ provider_path = "dc82dstc4/"
11+ nav_lookup_local_path = ""
12+
13+ # dc82dstc4 (TC4 2DS DC-8) contains no nav data;
14+ # nav data needs to be looked up from TC4 NAV DC-8 (dc8navtc4)
15+ # which is already out of ghrc-private.
16+ # nav_lookup is hard-coded here because it's easier, not because it's elegant.
17+ nav_lookup = {
18+ '20070628' : {'max_lat' : 40.138952 ,
19+ 'max_lon' : - 119.374708 ,
20+ 'min_lat' : 38.44974 ,
21+ 'min_lon' : - 126.368228 },
22+ '20070630' : {'max_lat' : 44.426224 ,
23+ 'max_lon' : - 121.488932 ,
24+ 'min_lat' : 38.643756 ,
25+ 'min_lon' : - 127.532152 },
26+ '20070702' : {'max_lat' : 38.794256 ,
27+ 'max_lon' : - 117.093816 ,
28+ 'min_lat' : 31.694268 ,
29+ 'min_lon' : - 125.133096 },
30+ '20070713' : {'max_lat' : 38.679943 ,
31+ 'max_lon' : - 84.20351 ,
32+ 'min_lat' : 9.726677 ,
33+ 'min_lon' : - 121.400814 },
34+ '20070717' : {'max_lat' : 10.00803 ,
35+ 'max_lon' : - 80.030766 ,
36+ 'min_lat' : - 3.376579 ,
37+ 'min_lon' : - 84.826298 },
38+ '20070721' : {'max_lat' : 12.294216 ,
39+ 'max_lon' : - 72.433205 ,
40+ 'min_lat' : 2.468834 ,
41+ 'min_lon' : - 85.591049 },
42+ '20070722' : {'max_lat' : 15.941677 ,
43+ 'max_lon' : - 78.158283 ,
44+ 'min_lat' : 5.919914 ,
45+ 'min_lon' : - 85.819702 },
46+ '20070724' : {'max_lat' : 10.007858 ,
47+ 'max_lon' : - 84.144459 ,
48+ 'min_lat' : 5.012512 ,
49+ 'min_lon' : - 86.060028 },
50+ '20070728' : {'max_lat' : 16.579227 ,
51+ 'max_lon' : - 81.294193 ,
52+ 'min_lat' : 8.705292 ,
53+ 'min_lon' : - 88.796482 },
54+ '20070729' : {'max_lat' : 10.585155 ,
55+ 'max_lon' : - 78.218193 ,
56+ 'min_lat' : - 6.471634 ,
57+ 'min_lon' : - 85.170479 },
58+ '20070731' : {'max_lat' : 10.619144 ,
59+ 'max_lon' : - 82.360039 ,
60+ 'min_lat' : 7.928867 ,
61+ 'min_lon' : - 89.509563 },
62+ '20070803' : {'max_lat' : 13.876762 ,
63+ 'max_lon' : - 80.040035 ,
64+ 'min_lat' : 5.130787 ,
65+ 'min_lon' : - 86.32061 },
66+ '20070805' : {'max_lat' : 10.00906 ,
67+ 'max_lon' : - 76.675987 ,
68+ 'min_lat' : 5.282021 ,
69+ 'min_lon' : - 84.434395 },
70+ '20070806' : {'max_lat' : 10.01215 ,
71+ 'max_lon' : - 84.131927 ,
72+ 'min_lat' : - 3.001328 ,
73+ 'min_lon' : - 92.314682 },
74+ '20070808' : {'max_lat' : 10.008202 ,
75+ 'max_lon' : - 70.252419 ,
76+ 'min_lat' : 1.433201 ,
77+ 'min_lon' : - 85.174427 },
78+ '20070810' : {'max_lat' : 39.2169 ,
79+ 'max_lon' : - 84.136219 ,
80+ 'min_lat' : 9.953098 ,
81+ 'min_lon' : - 121.400471 }
82+ }
83+
84+
85+ class MDXProcessing (MDX ):
86+
87+ def __init__ (self ):
88+ super ().__init__ ()
89+ self .nav_lookup = nav_lookup
90+
91+ def main (self ):
92+ # Nav lookup is already done and no longer needed here, but left here
93+ # for reference and reuse
94+ #self.nav_lookup = self.build_navigation_lookup(provider_path)
95+ print (f"Nav data available for { list (nav_lookup .keys ())} " )
96+
97+ self .process_collection (short_name , provider_path )
98+ #self.shutdown_ec2()
99+
100+ def process (self , filename : str , stream ) -> dict [str , Any ]:
101+ date_key = self .get_date_key (filename )
102+
103+ if date_key not in self .nav_lookup :
104+ raise ValueError (
105+ f"No navigation data found for { filename } "
106+ f"(date { date_key } )"
107+ )
108+ spatial = self .nav_lookup [date_key ]
109+ temporal = self .read_temporal_bounds (stream )
110+
111+ if date_key != temporal ["date_key" ]:
112+ raise ValueError (
113+ f"Filename date { date_key } does not match"
114+ f"NASA Ames DATE { temporal ['date_key' ]} for { filename } "
115+ )
116+
117+ return {
118+ "start" : temporal ["start_time" ],
119+ "end" : temporal ["end_time" ],
120+ "north" : spatial ["max_lat" ],
121+ "south" : spatial ["min_lat" ],
122+ "east" : spatial ["max_lon" ],
123+ "west" : spatial ["min_lon" ],
124+ "format" : "ASCII"
125+ }
126+
127+ @staticmethod
128+ def get_date_key (filename ):
129+ """Parse date key from filename."""
130+ match = re .search (r"(\d{8})(?:_\d{6})?\.txt$" , filename )
131+
132+ if not match :
133+ raise ValueError (f"Could not extract date from { filename } " )
134+
135+ return match .group (1 )
136+
137+ def build_navigation_lookup (self , provider_path : str , bucket : str = 'ghrcw-private' ):
138+ """Pre-read spatial-aware NP files in dataset and store GPS bounds by date."""
139+
140+ nav_lookup = {}
141+
142+ for entry in os .scandir (nav_lookup_local_path ):
143+ if not entry .path .endswith (".txt" ):
144+ continue
145+
146+ date_key = self .get_date_key (entry .name )
147+ nav_dict = self .read_spatial_bounds (entry .path )
148+ nav_lookup [date_key ] = nav_dict
149+
150+ print ("Found navigation data for these dates: " )
151+ pprint (nav_lookup )
152+
153+ return nav_lookup
154+
155+ @staticmethod
156+ def read_spatial_bounds (stream ) -> dict [str , float ]:
157+ """Read data file and compute spatial bounds."""
158+
159+ min_lat = float ("inf" )
160+ max_lat = float ("-inf" )
161+ min_lon = float ("inf" )
162+ max_lon = float ("-inf" )
163+
164+ with open_ames_1001 (stream ) as (header , records ):
165+
166+ # Headers per TC4_Nav_DC8_TN20070628.txt
167+ lat_idx = header .variable_names .index (
168+ "Lat (dec degree_N)"
169+ )
170+ lon_idx = header .variable_names .index (
171+ "Lon (dec degree_E)"
172+ )
173+
174+ for record in records :
175+ latitude = record .values [lat_idx ]
176+ longitude = record .values [lon_idx ]
177+
178+ min_lat = min (min_lat , latitude )
179+ max_lat = max (max_lat , latitude )
180+ min_lon = min (min_lon , longitude )
181+ max_lon = max (max_lon , longitude )
182+
183+ if min_lat == float ("inf" ):
184+ raise ValueError ("No valid latitude/longitude values found" )
185+
186+ nav_dict = {
187+ "min_lat" : min_lat ,
188+ "max_lat" : max_lat ,
189+ "min_lon" : min_lon ,
190+ "max_lon" : max_lon ,
191+ }
192+ return nav_dict
193+
194+ @staticmethod
195+ def read_temporal_bounds (stream ) -> dict [str , Any ]:
196+ """Read data file and compute temporal bounds (start and end date)."""
197+
198+ start_time = datetime .max .replace (tzinfo = timezone .utc )
199+ end_time = datetime .min .replace (tzinfo = timezone .utc )
200+
201+ with open_ames_1001 (stream ) as (header , records ):
202+
203+ beginning_of_day = datetime .combine (
204+ header .data_date ,
205+ time .min ,
206+ tzinfo = timezone .utc ,
207+ )
208+ date_key = f"{ header .data_date .year :04d} { header .data_date .month :02d} { header .data_date .day :02d} "
209+
210+ for record in records :
211+ timestamp = beginning_of_day + timedelta (
212+ seconds = record .independent
213+ )
214+
215+ start_time = min (start_time , timestamp )
216+ end_time = max (end_time , timestamp )
217+
218+ temp_dict = {
219+ "start_time" : start_time ,
220+ "end_time" : end_time ,
221+ "base_time" : header .data_date ,
222+ "date_key" : date_key ,
223+ }
224+ return temp_dict
225+
226+ if __name__ == '__main__' :
227+ MDXProcessing ().main ()
228+ # The below can be use to run a profiler and see which functions are
229+ # taking the most time to process
230+ # cProfile.run('MDXProcessing().main()', sort='tottime')
0 commit comments