22
33import com .bc .fiduceo .core .Dimension ;
44import com .bc .fiduceo .core .Interval ;
5+ import com .bc .fiduceo .core .NodeType ;
56import com .bc .fiduceo .geometry .Polygon ;
67import com .bc .fiduceo .location .PixelLocator ;
78import com .bc .fiduceo .reader .AcquisitionInfo ;
89import com .bc .fiduceo .reader .time .TimeLocator ;
10+ import com .bc .fiduceo .reader .time .TimeLocator_MillisSince1970 ;
11+ import com .bc .fiduceo .util .NetCDFUtils ;
12+ import com .bc .fiduceo .util .TimeUtils ;
13+ import com .bc .fiduceo .util .VariableProxy ;
914import org .esa .snap .core .util .StringUtils ;
15+ import org .esa .snap .core .util .io .FileUtils ;
1016import ucar .ma2 .Array ;
1117import ucar .ma2 .ArrayInt ;
18+ import ucar .ma2 .DataType ;
1219import ucar .ma2 .InvalidRangeException ;
20+ import ucar .nc2 .Attribute ;
1321import ucar .nc2 .Variable ;
1422
23+ import java .io .BufferedReader ;
1524import java .io .File ;
25+ import java .io .FileReader ;
1626import java .io .IOException ;
27+ import java .util .ArrayList ;
1728import java .util .Calendar ;
29+ import java .util .Date ;
1830import java .util .List ;
1931
32+ import static com .bc .fiduceo .util .NetCDFUtils .*;
33+
2034class NdbcCWReader extends NdbcReader {
2135
36+ public static final String GST = "GST" ;
37+ public static final String MEASUREMENT_TYPE = "measurement_type" ;
38+ public static final String ANEMOMETER_HEIGHT = "anemometer_height" ;
39+ public static final String SST_DEPTH = "sst_depth" ;
40+ public static final String WSPD = "WSPD" ;
41+ public static final String GTIME = "GTIME" ;
2242 private static final String REG_EX_CW = "\\ w{5}c\\ d{4}.txt" ;
23-
43+ private static final String STATION_ID = "station_id" ;
44+ private static final String STATION_TYPE = "station_type" ;
45+ private static final String LATITUDE = "latitude" ;
46+ private static final String BAROMETER_HEIGHT = "barometer_height" ;
47+ private static final String WDIR = "WDIR" ;
2448 private static StationDatabase stationDatabase ;
2549
50+ private ArrayList <CwRecord > records ;
51+ private TimeLocator timeLocator ;
52+ private Station station ;
2653
2754 NdbcCWReader () {
2855 }
2956
3057 @ Override
3158 public void open (File file ) throws IOException {
3259 ensureStationDatabase ();
60+ loadStation (file );
61+ parseFile (file );
62+ }
63+
64+ private void loadStation (File file ) {
65+ final String fileName = FileUtils .getFilenameWithoutExtension (file );
66+ final String stationId = fileName .substring (0 , 5 );
67+ station = stationDatabase .get (stationId );
68+ if (station == null ) {
69+ throw new IllegalArgumentException ("unsupported station, id = " + stationId );
70+ }
71+ }
72+
73+ private void parseFile (File file ) throws IOException {
74+ records = new ArrayList <>();
75+ try (final FileReader fileReader = new FileReader (file )) {
76+ final Calendar calendar = TimeUtils .getUTCCalendar ();
77+ final BufferedReader bufferedReader = new BufferedReader (fileReader );
78+ String line ;
79+ while ((line = bufferedReader .readLine ()) != null ) {
80+ if (line .startsWith ("#" )) {
81+ // skip comment lines tb 2023-02-27
82+ continue ;
83+ }
84+
85+ final CwRecord cwRecord = parseLine (line , calendar );
86+ records .add (cwRecord );
87+ }
88+ }
3389 }
3490
3591 @ Override
3692 public void close () throws IOException {
93+ if (records != null ) {
94+ records .clear ();
95+ records = null ;
96+ }
97+
98+ timeLocator = null ;
3799 }
38100
39101 @ Override
40102 public AcquisitionInfo read () throws IOException {
41103 final AcquisitionInfo acquisitionInfo = new AcquisitionInfo ();
42104
105+ int minTime = Integer .MAX_VALUE ;
106+ int maxTime = Integer .MIN_VALUE ;
107+ for (final CwRecord record : records ) {
108+ if (record .utc < minTime ) {
109+ minTime = record .utc ;
110+ }
111+ if (record .utc > maxTime ) {
112+ maxTime = record .utc ;
113+ }
114+ }
115+
116+ acquisitionInfo .setSensingStart (new Date (minTime * 1000L ));
117+ acquisitionInfo .setSensingStop (new Date (maxTime * 1000L ));
118+
119+ acquisitionInfo .setNodeType (NodeType .UNDEFINED );
120+
43121 return acquisitionInfo ;
44122 }
45123
@@ -50,17 +128,33 @@ public String getRegEx() {
50128
51129 @ Override
52130 public PixelLocator getPixelLocator () throws IOException {
53- throw new RuntimeException ("not implemented" );
131+ throw new RuntimeException ("not implemented" ); // intentional tb 2023-02-27
54132 }
55133
56134 @ Override
57135 public PixelLocator getSubScenePixelLocator (Polygon sceneGeometry ) throws IOException {
58- throw new RuntimeException ("not implemented" );
136+ throw new RuntimeException ("not implemented" ); // intentional tb 2023-02-27
59137 }
60138
61139 @ Override
62140 public TimeLocator getTimeLocator () throws IOException {
63- throw new RuntimeException ("not implemented" );
141+ if (timeLocator == null ) {
142+ createTimeLocator ();
143+ }
144+
145+ return timeLocator ;
146+ }
147+
148+ private void createTimeLocator () {
149+ long [] timeArray = new long [records .size ()];
150+
151+ int i = 0 ;
152+ for (final CwRecord record : records ) {
153+ timeArray [i ] = record .utc * 1000L ;
154+ i ++;
155+ }
156+
157+ timeLocator = new TimeLocator_MillisSince1970 (timeArray );
64158 }
65159
66160 @ Override
@@ -70,7 +164,37 @@ public int[] extractYearMonthDayFromFilename(String fileName) {
70164
71165 @ Override
72166 public Array readRaw (int centerX , int centerY , Interval interval , String variableName ) throws IOException , InvalidRangeException {
73- throw new RuntimeException ("not implemented" );
167+ if (variableName .equals (STATION_ID )) {
168+
169+ } else if (variableName .equals (STATION_TYPE )) {
170+ final StationType type = station .getType ();
171+ return createResultArray (toByte (type ), -1 , DataType .BYTE , interval );
172+ } else if (variableName .equals (MEASUREMENT_TYPE )) {
173+ final MeasurementType measurementType = station .getMeasurementType ();
174+ return createResultArray (toByte (measurementType ), -1 , DataType .BYTE , interval );
175+ } else if (variableName .equals (LATITUDE )) {
176+ return createResultArray (station .getLat (), Float .NaN , DataType .FLOAT , interval );
177+ } else if (variableName .equals (ANEMOMETER_HEIGHT )) {
178+ return createResultArray (station .getAnemometerHeight (), Float .NaN , DataType .FLOAT , interval );
179+ } else if (variableName .equals (BAROMETER_HEIGHT )) {
180+ return createResultArray (station .getBarometerHeight (), Float .NaN , DataType .FLOAT , interval );
181+ } else if (variableName .equals (SST_DEPTH )) {
182+ return createResultArray (station .getSSTDepth (), Float .NaN , DataType .FLOAT , interval );
183+ } else if (variableName .equals (WDIR )) {
184+ final CwRecord record = records .get (centerY );
185+ return createResultArray (record .windDir , 999 , DataType .SHORT , interval );
186+ } else if (variableName .equals (WSPD )) {
187+ final CwRecord record = records .get (centerY );
188+ return createResultArray (record .windSpeed , 99.f , DataType .FLOAT , interval );
189+ } else if (variableName .equals (GST )) {
190+ final CwRecord record = records .get (centerY );
191+ return createResultArray (record .gustSpeed , 99.f , DataType .FLOAT , interval );
192+ } else if (variableName .equals (GTIME )) {
193+ final CwRecord record = records .get (centerY );
194+ return createResultArray (record .gustTime , 9999 , DataType .SHORT , interval );
195+ }
196+
197+ return null ;
74198 }
75199
76200 @ Override
@@ -85,12 +209,99 @@ public ArrayInt.D2 readAcquisitionTime(int x, int y, Interval interval) throws I
85209
86210 @ Override
87211 public List <Variable > getVariables () throws InvalidRangeException , IOException {
88- throw new RuntimeException ("not implemented" );
212+ final ArrayList <Variable > variables = new ArrayList <>();
213+
214+ List <Attribute > attributes = new ArrayList <>();
215+ attributes .add (new Attribute (CF_LONG_NAME , "Station identifier" ));
216+ variables .add (new VariableProxy (STATION_ID , DataType .STRING , attributes ));
217+
218+ attributes = new ArrayList <>();
219+ attributes .add (new Attribute (CF_LONG_NAME , "Station type. 0: OCEAN_BUOY, 1: COAST_BUOY, 2: LAKE_BUOY, 3: OCEAN_STATION, 4: COAST_STATION, 5: LAKE_STATION" ));
220+ variables .add (new VariableProxy (STATION_TYPE , DataType .BYTE , attributes ));
221+
222+ attributes = new ArrayList <>();
223+ attributes .add (new Attribute (CF_LONG_NAME , "Measurement type. 0: CONSTANT_WIND, 1: STANDARD_METEOROLOGICAL" ));
224+ variables .add (new VariableProxy (MEASUREMENT_TYPE , DataType .BYTE , attributes ));
225+
226+ attributes = new ArrayList <>();
227+ attributes .add (new Attribute (CF_UNITS_NAME , "degree_east" ));
228+ attributes .add (new Attribute (CF_FILL_VALUE_NAME , NetCDFUtils .getDefaultFillValue (float .class )));
229+ attributes .add (new Attribute (CF_STANDARD_NAME , "longitude" ));
230+ variables .add (new VariableProxy ("longitude" , DataType .FLOAT , attributes ));
231+
232+ attributes = new ArrayList <>();
233+ attributes .add (new Attribute (CF_UNITS_NAME , "degree_north" ));
234+ attributes .add (new Attribute (CF_FILL_VALUE_NAME , NetCDFUtils .getDefaultFillValue (float .class )));
235+ attributes .add (new Attribute (CF_STANDARD_NAME , "latitude" ));
236+ variables .add (new VariableProxy (LATITUDE , DataType .FLOAT , attributes ));
237+
238+ attributes = new ArrayList <>();
239+ attributes .add (new Attribute (CF_UNITS_NAME , "m" ));
240+ attributes .add (new Attribute (CF_FILL_VALUE_NAME , Float .NaN ));
241+ attributes .add (new Attribute (CF_LONG_NAME , "Height of instrument above site elevation" ));
242+ variables .add (new VariableProxy (ANEMOMETER_HEIGHT , DataType .FLOAT , attributes ));
243+
244+ attributes = new ArrayList <>();
245+ attributes .add (new Attribute (CF_UNITS_NAME , "m" ));
246+ attributes .add (new Attribute (CF_FILL_VALUE_NAME , Float .NaN ));
247+ attributes .add (new Attribute (CF_LONG_NAME , "Height of instrument above site elevation" ));
248+ variables .add (new VariableProxy ("air_temp_height" , DataType .FLOAT , attributes ));
249+
250+ attributes = new ArrayList <>();
251+ attributes .add (new Attribute (CF_UNITS_NAME , "m" ));
252+ attributes .add (new Attribute (CF_FILL_VALUE_NAME , Float .NaN ));
253+ attributes .add (new Attribute (CF_LONG_NAME , "Height of instrument above above mean sea level" ));
254+ variables .add (new VariableProxy (BAROMETER_HEIGHT , DataType .FLOAT , attributes ));
255+
256+ attributes = new ArrayList <>();
257+ attributes .add (new Attribute (CF_UNITS_NAME , "m" ));
258+ attributes .add (new Attribute (CF_FILL_VALUE_NAME , Float .NaN ));
259+ attributes .add (new Attribute (CF_LONG_NAME , "Depth of instrument below water line" ));
260+ variables .add (new VariableProxy (SST_DEPTH , DataType .FLOAT , attributes ));
261+
262+ attributes = new ArrayList <>();
263+ attributes .add (new Attribute (CF_UNITS_NAME , "seconds since 1970-01-01" ));
264+ attributes .add (new Attribute (CF_FILL_VALUE_NAME , NetCDFUtils .getDefaultFillValue (int .class )));
265+ attributes .add (new Attribute (CF_STANDARD_NAME , "time" ));
266+ variables .add (new VariableProxy ("time" , DataType .INT , attributes ));
267+
268+ // @todo 1 tb/tb check CF standard names for the measurement data 2023-02027
269+ attributes = new ArrayList <>();
270+ attributes .add (new Attribute (CF_UNITS_NAME , "degT" ));
271+ attributes .add (new Attribute (CF_FILL_VALUE_NAME , 999 ));
272+ attributes .add (new Attribute (CF_LONG_NAME , "Ten-minute average wind direction measurements in degrees clockwise from true North." ));
273+ variables .add (new VariableProxy (WDIR , DataType .SHORT , attributes ));
274+
275+ attributes = new ArrayList <>();
276+ attributes .add (new Attribute (CF_UNITS_NAME , "m/s" ));
277+ attributes .add (new Attribute (CF_FILL_VALUE_NAME , 99.f ));
278+ attributes .add (new Attribute (CF_LONG_NAME , "Ten-minute average wind speed values in m/s." ));
279+ variables .add (new VariableProxy (WSPD , DataType .FLOAT , attributes ));
280+
281+ attributes = new ArrayList <>();
282+ attributes .add (new Attribute (CF_UNITS_NAME , "degT" ));
283+ attributes .add (new Attribute (CF_FILL_VALUE_NAME , 999 ));
284+ attributes .add (new Attribute (CF_LONG_NAME , "Direction, in degrees clockwise from true North, of the GST, reported at the last hourly 10-minute segment." ));
285+ variables .add (new VariableProxy ("GDR" , DataType .SHORT , attributes ));
286+
287+ attributes = new ArrayList <>();
288+ attributes .add (new Attribute (CF_UNITS_NAME , "m/s" ));
289+ attributes .add (new Attribute (CF_FILL_VALUE_NAME , 99.f ));
290+ attributes .add (new Attribute (CF_LONG_NAME , "Maximum 5-second peak gust during the measurement hour, reported at the last hourly 10-minute segment." ));
291+ variables .add (new VariableProxy (GST , DataType .FLOAT , attributes ));
292+
293+ attributes = new ArrayList <>();
294+ attributes .add (new Attribute (CF_UNITS_NAME , "hhmm" ));
295+ attributes .add (new Attribute (CF_FILL_VALUE_NAME , 9999 ));
296+ attributes .add (new Attribute (CF_LONG_NAME , "The minute of the hour that the GSP occurred, reported at the last hourly 10-minute segment." ));
297+ variables .add (new VariableProxy (GTIME , DataType .SHORT , attributes ));
298+
299+ return variables ;
89300 }
90301
91302 @ Override
92303 public Dimension getProductSize () throws IOException {
93- throw new RuntimeException ( "not implemented" );
304+ return new Dimension ( "product_size" , 1 , records . size () );
94305 }
95306
96307 @ Override
@@ -109,15 +320,15 @@ private void ensureStationDatabase() throws IOException {
109320 }
110321 }
111322
112- public CwRecord parseLine (String line , Calendar calendar ) {
323+ CwRecord parseLine (String line , Calendar calendar ) {
113324 final CwRecord cwRecord = new CwRecord ();
114325
115- line = line .replace (" " , " " ); // some fields are separated by two blanks (sigh) tb 2023-02-24
326+ line = line .replaceAll (" + " , " " ); // some fields are separated by two or more blanks (sigh) tb 2023-02-27
116327 final String [] tokens = StringUtils .split (line , new char []{' ' }, true );
117328
118329 calendar .setTimeInMillis (0 );
119330 calendar .set (Calendar .YEAR , Integer .parseInt (tokens [0 ]));
120- calendar .set (Calendar .MONTH , Integer .parseInt (tokens [1 ]) - 1 );
331+ calendar .set (Calendar .MONTH , Integer .parseInt (tokens [1 ]) - 1 ); // calendar wants month zero-based tb 2023-02-27
121332 calendar .set (Calendar .DAY_OF_MONTH , Integer .parseInt (tokens [2 ]));
122333 calendar .set (Calendar .HOUR_OF_DAY , Integer .parseInt (tokens [3 ]));
123334 calendar .set (Calendar .MINUTE , Integer .parseInt (tokens [4 ]));
0 commit comments