bes  Updated for version 3.20.13
FONgTransform.cc
1 // FONgTransform.cc
2 
3 // This file is part of BES GDAL File Out Module
4 
5 // Copyright (c) 2012 OPeNDAP, Inc.
6 // Author: James Gallagher <jgallagher@opendap.org>
7 //
8 // This library is free software; you can redistribute it and/or
9 // modify it under the terms of the GNU Lesser General Public
10 // License as published by the Free Software Foundation; either
11 // version 2.1 of the License, or (at your option) any later version.
12 //
13 // This library is distributed in the hope that it will be useful,
14 // but WITHOUT ANY WARRANTY; without even the implied warranty of
15 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 // Lesser General Public License for more details.
17 //
18 // You should have received a copy of the GNU Lesser General Public
19 // License along with this library; if not, write to the Free Software
20 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21 //
22 // You can contact University Corporation for Atmospheric Research at
23 // 3080 Center Green Drive, Boulder, CO 80301
24 
25 #include "config.h"
26 
27 #include <cstdlib>
28 
29 #include <set>
30 
31 #include <gdal.h>
32 #include <gdal_priv.h>
33 #include <gdal_utils.h>
34 
35 #include <libdap/DDS.h>
36 #include <libdap/ConstraintEvaluator.h>
37 
38 #include <libdap/Structure.h>
39 #include <libdap/Array.h>
40 #include <libdap/Grid.h>
41 #include <libdap/util.h>
42 #include <libdap/Error.h>
43 
44 #include <dispatch/BESDebug.h>
45 #include <dispatch/BESInternalError.h>
46 #include <dispatch/TheBESKeys.h>
47 
48 #include "FONgTransform.h"
49 #include "FONgGrid.h"
50 
51 using namespace std;
52 using namespace libdap;
53 
64 FONgTransform::FONgTransform(DDS *dds, ConstraintEvaluator &/*evaluator*/, const string &localfile) :
65  d_dest(0), d_dds(dds), d_localfile(localfile),
66  d_geo_transform_set(false), d_width(0.0), d_height(0.0), d_top(0.0), d_left(0.0),
67  d_bottom(0.0), d_right(0.0), d_no_data(0.0), d_no_data_type(none), d_num_bands(0)
68 {
69  BESDEBUG("fong3", "dds numvars = " << dds->num_var() << endl);
70  if (localfile.empty())
71  throw BESInternalError("Empty local file name passed to constructor", __FILE__, __LINE__);
72 }
73 
79 {
80  vector<FONgGrid *>::iterator i = d_fong_vars.begin();
81  vector<FONgGrid *>::iterator e = d_fong_vars.end();
82  while (i != e) {
83  delete (*i++);
84  }
85 }
86 
90 static bool
91 is_convertable_type(const BaseType *b)
92 {
93  switch (b->type()) {
94  case dods_grid_c:
95  return true;
96 
97  // TODO Add support for Array (?)
98  case dods_array_c:
99  default:
100  return false;
101  }
102 }
103 
110 static FONgGrid *convert(BaseType *v)
111 {
112  switch (v->type()) {
113  case dods_grid_c:
114  return new FONgGrid(static_cast<Grid*>(v));
115 
116  default:
117  throw BESInternalError("file out GeoTiff, unable to write unknown variable type", __FILE__, __LINE__);
118  }
119 }
120 
121 #if 0
140 void FONgTransform::m_scale_data(double *data)
141 {
142  // It is an error to call this if no_data_type() is 'none'
143  set<double> hist;
144  for (int i = 0; i < width() * height(); ++i)
145  hist.insert(data[i]);
146 
147  BESDEBUG("fong3", "Hist count: " << hist.size() << endl);
148 
149  if (no_data_type() == negative && hist.size() > 1) {
150  // Values are sorted by 'weak' <, so this is the smallest value
151  // and ++i is the next smallest value. Assume no_data is the
152  // smallest value in the data set and ++i is the smallest actual
153  // data value. Reset the no_data value to be 1.0 < the smallest
154  // actual value. This makes for a good grayscale photometric
155  // GeoTiff w/o changing the actual data values.
156  set<double>::iterator i = hist.begin();
157  double smallest = *(++i);
158  if (fabs(smallest + no_data()) > 1) {
159  smallest -= 1.0;
160 
161  BESDEBUG("fong3", "New no_data value: " << smallest << endl);
162 
163  for (int i = 0; i < width() * height(); ++i) {
164  if (data[i] <= no_data()) {
165  data[i] = smallest;
166  }
167  }
168  }
169  }
170  else {
171  set<double>::reverse_iterator r = hist.rbegin();
172  double biggest = *(++r);
173  if (fabs(no_data() - biggest) > 1) {
174  biggest += 1.0;
175 
176  BESDEBUG("fong3", "New no_data value: " << biggest << endl);
177 
178  for (int i = 0; i < width() * height(); ++i) {
179  if (data[i] >= no_data()) {
180  data[i] = biggest;
181  }
182  }
183  }
184  }
185 }
186 #endif
187 
200 {
201 
202  BESDEBUG("fong3", "left: " << d_left << ", top: " << d_top << ", right: " << d_right << ", bottom: " << d_bottom << endl);
203  BESDEBUG("fong3", "width: " << d_width << ", height: " << d_height << endl);
204 
205  d_gt[0] = d_left; // The leftmost 'x' value, which is longitude
206  d_gt[3] = d_top; // The topmost 'y' value, which is latitude should be max latitude
207 
208  // We assume data w/o any rotation (a north-up image)
209  d_gt[2] = 0.0;
210  d_gt[4] = 0.0;
211 
212  // Compute the lower left values. Note that wehn GDAL builds the geotiff
213  // output dataset, it correctly inverts the image when the source data has
214  // inverted latitude values.?
215  d_gt[1] = (d_right - d_left) / d_width; // width in pixels; top and bottom in lat
216  d_gt[5] = (d_bottom - d_top) / d_height;
217 
218  BESDEBUG("fong3", "gt[0]: " << d_gt[0] << ", gt[1]: " << d_gt[1] << ", gt[2]: " << d_gt[2] \
219  << ", gt[3]: " << d_gt[3] << ", gt[4]: " << d_gt[4] << ", gt[5]: " << d_gt[5] << endl);
220 
221  return d_gt;
222 }
223 
235 bool FONgTransform::effectively_two_D(FONgGrid *fbtp)
236 {
237  if (fbtp->type() == dods_grid_c) {
238  Grid *g = static_cast<FONgGrid*>(fbtp)->grid();
239 
240  if (g->get_array()->dimensions() == 2)
241  return true;
242 
243  // count the dimensions with sizes other than 1
244  Array *a = g->get_array();
245  int dims = 0;
246  for (Array::Dim_iter d = a->dim_begin(); d != a->dim_end(); ++d) {
247  if (a->dimension_size(d, true) > 1)
248  ++dims;
249  }
250 
251  return dims == 2;
252  }
253 
254  return false;
255 }
256 
257 static void build_delegate(BaseType *btp, FONgTransform &t)
258 {
259  if (btp->send_p() && is_convertable_type(btp)) {
260  BESDEBUG( "fong3", "converting " << btp->name() << endl);
261 
262  // Build the delegate
263  FONgGrid *fb = convert(btp);
264 
265  // Get the information needed for the transform.
266  // Note that FONgGrid::extract_coordinates() also pushes the
267  // new FONgBaseType instance onto the FONgTransform's vector of
268  // delagate variable objects.
269  fb->extract_coordinates(t);
270  }
271 }
272 
273 // Helper function to descend into Structures looking for Grids (and Arrays
274 // someday).
275 static void find_vars_helper(Structure *s, FONgTransform &t)
276 {
277  Structure::Vars_iter vi = s->var_begin();
278  while (vi != s->var_end()) {
279  if ((*vi)->send_p() && is_convertable_type(*vi)) {
280  build_delegate(*vi, t);
281  }
282  else if ((*vi)->type() == dods_structure_c) {
283  find_vars_helper(static_cast<Structure*>(*vi), t);
284  }
285 
286  ++vi;
287  }
288 }
289 
290 // Helper function to scan the DDS top-level for Grids, ...
291 // Note that FONgGrid::extract_coordinates() sets a bunch of
292 // values in the FONgBaseType instance _and_ this instance of
293 // FONgTransform. One of these is 'num_bands()'. For GeoTiff,
294 // num_bands() must be 1. This is tested in transform().
295 static void find_vars(DDS *dds, FONgTransform &t)
296 {
297  DDS::Vars_iter vi = dds->var_begin();
298  while (vi != dds->var_end()) {
299  BESDEBUG( "fong3", "looking at: " << (*vi)->name() << " and it is/isn't selected: " << (*vi)->send_p() << endl);
300  if ((*vi)->send_p() && is_convertable_type(*vi)) {
301  BESDEBUG( "fong3", "converting " << (*vi)->name() << endl);
302  build_delegate(*vi, t);
303  }
304  else if ((*vi)->type() == dods_structure_c) {
305  find_vars_helper(static_cast<Structure*>(*vi), t);
306  }
307 
308  ++vi;
309  }
310 }
311 
319 {
320  // Scan the entire DDS looking for variables that have been 'projected' and
321  // build the delegate objects for them.
322  find_vars(d_dds, *this);
323 
324  for (int i = 0; i < num_bands(); ++i)
325  if (!effectively_two_D(var(i)))
326  throw Error("GeoTiff responses can consist of two-dimensional variables only; use constraints to reduce the size of Grids as needed.");
327 
328  GDALDriver *Driver = GetGDALDriverManager()->GetDriverByName("MEM");
329  if( Driver == NULL )
330  throw Error("Could not get the MEM driver from/for GDAL: " + string(CPLGetLastErrorMsg()));
331 
332  char **Metadata = Driver->GetMetadata();
333  if (!CSLFetchBoolean(Metadata, GDAL_DCAP_CREATE, FALSE))
334  throw Error("Could not make output format.");
335 
336  BESDEBUG("fong3", "num_bands: " << num_bands() << "." << endl);
337 
338  // Create band in the memory using data type GDT_Byte.
339  // Most image viewers reproduce tiff files with Bits/Sample: 8
340 
341  // Make this type depend on the value of a bes.conf parameter.
342  // See FONgRequestHandler.cc and FONgRequestHandler::d_use_byte_for_geotiff_bands.
343  // FIXME This is a hack. But maybe it's good enough?
344  // jhrg 3/20/19
345  if (TheBESKeys::TheKeys()->read_bool_key("FONg.GeoTiff.band.type.byte", true))
346  d_dest = Driver->Create("in_memory_dataset", width(), height(), num_bands(), GDT_Byte, 0/*options*/);
347  else
348  d_dest = Driver->Create("in_memory_dataset", width(), height(), num_bands(), GDT_Float32, 0/*options*/);
349 
350  if (!d_dest)
351  throw Error("Could not create the geotiff dataset: " + string(CPLGetLastErrorMsg()));
352 
353  d_dest->SetGeoTransform(geo_transform());
354 
355  BESDEBUG("fong3", "Made new temp file and set georeferencing (" << num_bands() << " vars)." << endl);
356 
357  bool projection_set = false;
358  string wkt = "";
359  for (int i = 0; i < num_bands(); ++i) {
360  FONgGrid *fbtp = var(i);
361 
362  if (!projection_set) {
363  wkt = fbtp->get_projection(d_dds);
364  if (d_dest->SetProjection(wkt.c_str()) != CPLE_None)
365  throw Error("Could not set the projection: " + string(CPLGetLastErrorMsg()));
366  projection_set = true;
367  }
368  else {
369  string wkt_i = fbtp->get_projection(d_dds);
370  if (wkt_i != wkt)
371  throw Error("In building a multiband response, different bands had different projection information.");
372  }
373 
374  GDALRasterBand *band = d_dest->GetRasterBand(i+1);
375  if (!band)
376  throw Error("Could not get the " + long_to_string(i+1) + "th band: " + string(CPLGetLastErrorMsg()));
377 
378  double *data = 0;
379 
380  try {
381  // TODO We can read any of the basic DAP2 types and let RasterIO convert it to any other type.
382  // That is, we can read these values in their native type, skipping the conversion here. That
383  // would make this code faster. jhrg 3/20/19
384  data = fbtp->get_data();
385 
386  // If the latitude values are inverted, the 0th value will be less than
387  // the last value.
388  vector<double> local_lat;
389  // TODO: Rewrite this to avoid expensive procedure
390  extract_double_array(fbtp->d_lat, local_lat);
391 
392  // NB: Here the 'type' value indicates the type of data in the buffer. The
393  // type of the band is set above when the dataset is created.
394 
395  if (local_lat[0] < local_lat[local_lat.size() - 1]) {
396  BESDEBUG("fong3", "Writing reversed raster. Lat[0] = " << local_lat[0] << endl);
397  //---------- write reverse raster----------
398  for (int row = 0; row <= height()-1; ++row) {
399  int offsety=height()-row-1;
400  CPLErr error_write = band->RasterIO(GF_Write, 0, offsety, width(), 1, data+(row*width()), width(), 1, GDT_Float64, 0, 0);
401  if (error_write != CPLE_None)
402  throw Error("Could not write data for band: " + long_to_string(i + 1) + ": " + string(CPLGetLastErrorMsg()));
403  }
404  }
405  else {
406  BESDEBUG("fong3", "calling band->RasterIO" << endl);
407  CPLErr error = band->RasterIO(GF_Write, 0, 0, width(), height(), data, width(), height(), GDT_Float64, 0, 0);
408  if (error != CPLE_None)
409  throw Error("Could not write data for band: " + long_to_string(i+1) + ": " + string(CPLGetLastErrorMsg()));
410  }
411 
412  delete[] data;
413  }
414  catch (...) {
415  delete[] data;
416  GDALClose(d_dest);
417  throw;
418  }
419  }
420 
421  // Now get the GTiff driver and use CreateCopy() on the d_dest "MEM" dataset
422  GDALDataset *tif_dst = 0;
423  try {
424  Driver = GetGDALDriverManager()->GetDriverByName("GTiff");
425  if (Driver == NULL)
426  throw Error("Could not get driver for GeoTiff: " + string(CPLGetLastErrorMsg()));
427 
428  // The drivers only support CreateCopy()
429  char **Metadata = Driver->GetMetadata();
430  if (!CSLFetchBoolean(Metadata, GDAL_DCAP_CREATECOPY, FALSE))
431  BESDEBUG("fong", "Driver does not support dataset creation via 'CreateCopy()'." << endl);
432 
433  // NB: Changing PHOTOMETIC to MINISWHITE doesn't seem to have any visible affect,
434  // although the resulting files differ. jhrg 11/21/12
435  char **options = NULL;
436  options = CSLSetNameValue(options, "PHOTOMETRIC", "MINISBLACK" ); // The default for GDAL
437  // TODO Adding these options: -co COMPRESS=LZW -co TILED=YES -co INTERLEAVE=BAND
438  // will produce a COG file. The INTERLEAVE=BAND is not strictly needed but would
439  // be nice if there are multiple variables.
440  // See https://geoexamples.com/other/2019/02/08/cog-tutorial.html
441 
442  BESDEBUG("fong3", "Before CreateCopy, number of bands: " << d_dest->GetRasterCount() << endl);
443 
444  // implementation of gdal_translate -scale to adjust color levels
445  char **argv = NULL;
446  argv = CSLAddString(argv, "-scale");
447  GDALTranslateOptions *opts = GDALTranslateOptionsNew(argv, NULL /*binary options*/);
448  int usage_error = CE_None;
449  GDALDatasetH dst_handle = GDALTranslate(d_localfile.c_str(), d_dest, opts, &usage_error);
450  if (!dst_handle || usage_error != CE_None) {
451  GDALClose(dst_handle);
452  GDALTranslateOptionsFree(opts);
453  string msg = string("Error calling GDAL translate: ") + CPLGetLastErrorMsg();
454  BESDEBUG("fong3", "ERROR transform_to_geotiff(): " << msg << endl);
455  throw BESError(msg, BES_INTERNAL_ERROR, __FILE__, __LINE__);
456  }
457 
458  tif_dst = Driver->CreateCopy(d_localfile.c_str(), reinterpret_cast<GDALDataset*>(dst_handle), FALSE/*strict*/,
459  options, NULL/*progress*/, NULL/*progress data*/);
460 
461  GDALClose(dst_handle);
462  GDALTranslateOptionsFree(opts);
463 
464  if (!tif_dst)
465  throw Error("Could not create the GeoTiff dataset: " + string(CPLGetLastErrorMsg()));
466  }
467  catch (...) {
468  GDALClose(d_dest);
469  GDALClose (tif_dst);
470  throw;
471  }
472  GDALClose(d_dest);
473  GDALClose(tif_dst);
474 }
475 
491 {
492  // Scan the entire DDS looking for variables that have been 'projected' and
493  // build the delegate objects for them.
494  find_vars(d_dds, *this);
495 
496  for (int i = 0; i < num_bands(); ++i)
497  if (!effectively_two_D(var(i)))
498  throw Error("GeoTiff responses can consist of two-dimensional variables only; use constraints to reduce the size of Grids as needed.");
499 
500  GDALDriver *Driver = GetGDALDriverManager()->GetDriverByName("MEM");
501  if( Driver == NULL )
502  throw Error("Could not get the MEM driver from/for GDAL: " + string(CPLGetLastErrorMsg()));
503 
504  char **Metadata = Driver->GetMetadata();
505  if (!CSLFetchBoolean(Metadata, GDAL_DCAP_CREATE, FALSE))
506  throw Error("Driver JP2OpenJPEG does not support dataset creation.");
507 
508  // No creation options for a memory dataset
509  // NB: This is where the type of the bands is set. JPEG2000 only supports integer types.
510  d_dest = Driver->Create("in_memory_dataset", width(), height(), num_bands(), GDT_Byte, 0 /*options*/);
511  if (!d_dest)
512  throw Error("Could not create in-memory dataset: " + string(CPLGetLastErrorMsg()));
513 
514  d_dest->SetGeoTransform(geo_transform());
515 
516  BESDEBUG("fong3", "Made new temp file and set georeferencing (" << num_bands() << " vars)." << endl);
517 
518  bool projection_set = false;
519  string wkt = "";
520  for (int i = 0; i < num_bands(); ++i) {
521  FONgGrid *fbtp = var(i);
522 
523  if (!projection_set) {
524  wkt = fbtp->get_projection(d_dds);
525  if (d_dest->SetProjection(wkt.c_str()) != CPLE_None)
526  throw Error("Could not set the projection: " + string(CPLGetLastErrorMsg()));
527  projection_set = true;
528  }
529  else {
530  string wkt_i = fbtp->get_projection(d_dds);
531  if (wkt_i != wkt)
532  throw Error("In building a multiband response, different bands had different projection information.");
533  }
534 
535  GDALRasterBand *band = d_dest->GetRasterBand(i+1);
536  if (!band)
537  throw Error("Could not get the " + long_to_string(i+1) + "th band: " + string(CPLGetLastErrorMsg()));
538 
539  double *data = 0;
540  try {
541  // TODO We can read any of the basic DAP2 types and let RasterIO convert it to any other type.
542  data = fbtp->get_data();
543 
544  // If the latitude values are inverted, the 0th value will be less than
545  // the last value.
546  vector<double> local_lat;
547  // TODO: Rewrite this to avoid expensive procedure
548  extract_double_array(fbtp->d_lat, local_lat);
549 
550  // NB: Here the 'type' value indicates the type of data in the buffer. The
551  // type of the band is set above when the dataset is created.
552 
553  if (local_lat[0] < local_lat[local_lat.size() - 1]) {
554  BESDEBUG("fong3", "Writing reversed raster. Lat[0] = " << local_lat[0] << endl);
555  //---------- write reverse raster----------
556  for (int row = 0; row <= height()-1; ++row) {
557  int offsety=height()-row-1;
558  CPLErr error_write = band->RasterIO(GF_Write, 0, offsety, width(), 1, data+(row*width()), width(), 1, GDT_Float64, 0, 0);
559  if (error_write != CPLE_None)
560  throw Error("Could not write data for band: " + long_to_string(i + 1) + ": " + string(CPLGetLastErrorMsg()));
561  }
562  }
563  else {
564  BESDEBUG("fong3", "calling band->RasterIO" << endl);
565  CPLErr error = band->RasterIO(GF_Write, 0, 0, width(), height(), data, width(), height(), GDT_Float64, 0, 0);
566  if (error != CPLE_None)
567  throw Error("Could not write data for band: " + long_to_string(i+1) + ": " + string(CPLGetLastErrorMsg()));
568  }
569 
570  delete[] data;
571 
572  }
573  catch (...) {
574  delete[] data;
575  GDALClose(d_dest);
576  throw;
577  }
578  }
579 
580  // Now get the OpenJPEG driver and use CreateCopy() on the d_dest "MEM" dataset
581  GDALDataset *jpeg_dst = 0;
582  try {
583  Driver = GetGDALDriverManager()->GetDriverByName("JP2OpenJPEG");
584  if (Driver == NULL)
585  throw Error("Could not get driver for JP2OpenJPEG: " + string(CPLGetLastErrorMsg()));
586 
587  // The JPEG2000 drivers only support CreateCopy()
588  char **Metadata = Driver->GetMetadata();
589  if (!CSLFetchBoolean(Metadata, GDAL_DCAP_CREATECOPY, FALSE))
590  BESDEBUG("fong", "Driver JP2OpenJPEG does not support dataset creation via 'CreateCopy()'." << endl);
591  //throw Error("Driver JP2OpenJPEG does not support dataset creation via 'CreateCopy()'.");
592 
593  char **options = NULL;
594  options = CSLSetNameValue(options, "CODEC", "JP2");
595  options = CSLSetNameValue(options, "GMLJP2", "YES");
596  options = CSLSetNameValue(options, "GeoJP2", "NO");
597  options = CSLSetNameValue(options, "QUALITY", "100"); // 25 is the default;
598  options = CSLSetNameValue(options, "REVERSIBLE", "YES"); // lossy compression
599 
600  BESDEBUG("fong3", "Before JPEG2000 CreateCopy, number of bands: " << d_dest->GetRasterCount() << endl);
601 
602  // implementation of gdal_translate -scale to adjust color levels
603  char **argv = NULL;
604  argv = CSLAddString(argv, "-scale");
605  GDALTranslateOptions *opts = GDALTranslateOptionsNew(argv, NULL /*binary options*/);
606  int usage_error = CE_None;
607  GDALDatasetH dst_h = GDALTranslate(d_localfile.c_str(), d_dest, opts, &usage_error);
608  if (!dst_h || usage_error != CE_None) {
609  GDALClose(dst_h);
610  GDALTranslateOptionsFree(opts);
611  string msg = string("Error calling GDAL translate: ") + CPLGetLastErrorMsg();
612  BESDEBUG("fong3", "ERROR transform_to_jpeg2000(): " << msg << endl);
613  throw BESError(msg, BES_INTERNAL_ERROR, __FILE__, __LINE__);
614  }
615 
616  jpeg_dst = Driver->CreateCopy(d_localfile.c_str(), reinterpret_cast<GDALDataset*>(dst_h), FALSE/*strict*/,
617  options, NULL/*progress*/, NULL/*progress data*/);
618 
619  GDALClose(dst_h);
620  GDALTranslateOptionsFree(opts);
621 
622  if (!jpeg_dst)
623  throw Error("Could not create the JPEG200 dataset: " + string(CPLGetLastErrorMsg()));
624  }
625  catch (...) {
626  GDALClose(d_dest);
627  GDALClose (jpeg_dst);
628  throw;
629  }
630 
631  GDALClose(d_dest);
632  GDALClose(jpeg_dst);
633 }
Base exception class for the BES with basic string message.
Definition: BESError.h:59
exception thrown if internal error encountered
A DAP Grid with file out netcdf information included.
Definition: FONgGrid.h:52
string get_projection(libdap::DDS *dds)
Set the projection information For Grids, look for CF information. If it's not present,...
Definition: FONgGrid.cc:296
virtual double * get_data()
Get the data values for the band(s). Call must delete.
Definition: FONgGrid.cc:337
virtual void extract_coordinates(FONgTransform &t)
Get the GDAL/OGC WKT projection string.
Definition: FONgGrid.cc:203
Transformation object that converts an OPeNDAP DataDDS to a GeoTiff file.
Definition: FONgTransform.h:42
FONgTransform(libdap::DDS *dds, libdap::ConstraintEvaluator &evaluator, const string &localfile)
Constructor that creates transformation object from the specified DataDDS object to the specified fil...
virtual ~FONgTransform()
Destructor.
virtual void transform_to_jpeg2000()
Transforms the variables of the DataDDS to a JPEG2000 file.
virtual double * geo_transform()
Build the geotransform array needed by GDAL.
virtual void transform_to_geotiff()
Transforms the variables of the DataDDS to a GeoTiff file.
static TheBESKeys * TheKeys()
Definition: TheBESKeys.cc:71