Due to the lapse in federal government funding, NASA is not updating this website. We sincerely regret this inconvenience.
NASA Logo
Ocean Color Science Software

ocssw V2022
mapgen.py
Go to the documentation of this file.
1 #!/usr/bin/env python3
2 import argparse
3 from datetime import datetime
4 import logging
5 from math import ceil, floor
6 import os
7 import re
8 from shlex import quote
9 import subprocess
10 import sys
11 
12 import seadasutils.MetaUtils as MetaUtils
13 from seadasutils.ParamUtils import ParamProcessing
14 from seadasutils.setupenv import build_executable_path
15 from seadasutils.SensorUtils import by_sensor
17 
18 def getBinRes(resolution):
19  resvalue = 2000
20  if "km" in resolution:
21  resvalue = float(resolution.strip('km')) * 1000.
22  elif "m" in resolution:
23  resvalue = float(resolution.strip('m'))
24  elif "deg" in resolution:
25  resvalue = float(resolution.strip('deg')) * 111312.
26 
27  if resvalue <= 99.0 :
28  return 'HH'
29  elif resvalue < 250.0:
30  return 'HQ'
31  elif resvalue < 500.0:
32  return 'Q'
33  elif resvalue < 1150.0:
34  return 'H'
35  elif resvalue < 2300.0:
36  return '1'
37  elif resvalue < 4600.0:
38  return '2'
39  elif resvalue < 9200. :
40  return '4'
41  else:
42  return '9'
43 
44 def get_ftype(filepath):
45  level = 1
46  cmd = ['ncdump', '-h', filepath]
47  result = subprocess.run(cmd, shell=False, capture_output=True, text=True)
48  levelstr = None
49  if not result.returncode:
50  for line in result.stdout.splitlines():
51  if 'processing_level' in line:
52  levelstr = line
53  break
54  if levelstr:
55  if re.search(r'"L2',levelstr):
56  level = 2
57  elif re.search(r'"L3',levelstr):
58  level = 3
59 
60  return level
61 
62 def whoops(errormsg, status=1):
63  logging.error(errormsg)
64  sys.exit(status)
65 
66 def execute_command(command):
67  """
68  Execute a process on the system
69  """
70  logging.info("Running: " + ' '.join(command))
71 
72  result = subprocess.run(command, shell=False, capture_output=True, text=True)
73  std_out = result.stdout
74  err_out = result.stderr
75 
76  if result.returncode:
77  logging.debug(std_out)
78  if err_out:
79  logging.error(err_out)
80  whoops("Exiting: {command} returned a status {status}".format(command=' '.join(command),status=result.returncode), result.returncode)
81  else:
82  if std_out:
83  logging.debug(std_out)
84  if err_out:
85  logging.debug(err_out)
86 
87 def run_clo_program(program, inputs):
88  """
89  Set up and run a program that accepts keyword=value command line arguemnts ala CLO
90  """
91  logging.info('\n###### {program} ######\n'.format(program=program))
92  cmd = [str(build_executable_path(program))]
93 
94  params = ["%s=%s" % (k,v) for k, v in inputs.items()]
95 
96  cmd.extend(params)
97  execute_command(cmd)
98 
99 if __name__ == "__main__":
100 
101  parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter,description='''\
102 generate mapped output from a SeaDAS supported satellite data files
103 arguments can be specified on the commandline or in a parameter file
104 the two methods can be used together, with commandline over-riding the parfile''',add_help=True)
105  parser.add_argument('--parfile', '-p', type=str, help='input parameter file')
106  parser.add_argument('--ifile', '-i', type=str, help='input file or text file list of files')
107  parser.add_argument('--geofile', '-g', type=str,help='geolocation file or text file list of files')
108  parser.add_argument('--ofile', '-o', type=str, help='output file name; default: <ifile>.MAP.<oformat ext>')
109  parser.add_argument('--logfile','-l', type=str, default='mapgen_<timestamp>.log', help='''\
110 log file
111 default: mapgen_<timestamp>.log
112 <timestamp> is in seconds since Jan 1, 1970 00:00:00
113 this file is deleted if verbose is not set and no errors
114 occur during processing''')
115 
116  parser.add_argument('--use_rgb', action='store_true', default=False, help='''\
117 generate an RGB image output
118 default: a pseudo-true color image with bands to use
119  controlled by --product_rgb option''')
120  prodGroup = parser.add_mutually_exclusive_group()
121  prodGroup.add_argument('--product', type=str, help='product(s) to map; comma separated')
122  prodGroup.add_argument('--product_rgb', type=str,help='''\
123 comma separated string of RGB products
124 e.g., product_rgb=rhos_645,rhos_555,rhos_469
125 default: sensor specific, see
126 $OCDATAROOT/<sensor>/l1mapgen_defaults.par''')
127 
128  parser.add_argument('--resolution', '-r', type=str, default='2.0km', help='''\
129  #.#: width of a pixel in meters
130  #.#km: width of a pixel in kilometers
131  #.#deg: width of a pixel in degrees''')
132  parser.add_argument('--oformat', choices=['netcdf4','png','ppm','tiff'], type=str, default="png", help='''\
133 netcdf4: Network Common Data Form v4 file
134  can contain more than one product
135 png: Portable Network Graphics format image
136 ppm: Portable PixMap format image
137 tiff: Tagged Image File Format with georeference tags''')
138  parser.add_argument('--use_transparency','-t',action='store_true', default=False, help='make missing data transparent\nonly valid for color PNG and TIFF output')
139  parser.add_argument('--north', '-n', type=float, help=('northern-most latitude; default: input file max lßatitude'))
140  parser.add_argument('--south', '-s', type=float, help=('southern-most latitude; default: input file min latitude'))
141  parser.add_argument('--east', '-e', type=float, help=('eastern-most latitude; default: input file max longitude'))
142  parser.add_argument('--west', '-w', type=float, help=('western-most latitude; default: input file min longitude'))
143  parser.add_argument('--projection', type=str,default='platecarree', help='''\
144  "proj" projection string or one of the following:
145  platecarree: Plate Carree (cylindrical) projection
146  projection="+proj=eqc +lat_0=<central_meridian>"
147  mollweide: Mollweide projection
148  projection="+proj=moll +lat_0=<central_meridian>"
149  lambert: Lambert conformal conic projection
150  projection="+proj=lcc +lat_0=<central_meridian>"
151  albersconic: Albers equal-area conic projection
152  projection="+proj=aea +lat_0=<central_meridian>"
153  mercator: Mercator cylindrical map projection
154  projection="+proj=merc +lat_0=<central_meridian>"
155  ease2: Ease Grid 2 projection
156  projection="+proj=cea +lon_0=0 +lat_ts=30 +ellps=WGS84
157  +datum=WGS84 +units=m +lat_0=<central_meridian>"''')
158  parser.add_argument('--central_meridian', type=float, help='central meridian to use for projection in degrees east')
159  parser.add_argument('--palfile', type=str, help='palette filename\ndefault: see $OCDATAROOT/common/product.xml')
160  parser.add_argument('--fudge', type=float,default=1.0, help='factor used to modify pixel search radius for mapping')
161  parser.add_argument('--datamin', type=float, help='minimum value for scaling (default from product.xml)')
162  parser.add_argument('--datamax', type=float, help='maximum value for scaling (default from product.xml)')
163  parser.add_argument('--scale_type', choices=['linear','log','arctan'],type=str, help='data scaling method (default from product.xml)')
164  parser.add_argument('--threshold', type=float, help='minimum percentage of filled pixels for image generation\ndefault: 0')
165  parser.add_argument('--trimNSEW', action='store_false',default=True, help='do not trim output to match input NSEW range')
166  parser.add_argument('--write_projtext', action='store_true',default=False, help='write projection information to a text file (for mapgen_overlay script)')
167  parser.add_argument('--keep-intermediates', action='store_true',default=False, help='do not delete the intermediate L2/L3B files produced')
168  parser.add_argument('--verbose', '-v',action='count',default=0, help="let's get chatty; each occurrence increases verbosity\ndefault: error\n-v info -vv debug'")
169 
170  args=parser.parse_args()
171 
172  sanitize = ['ifile','ofile','logfile','geofile','parfile','product','resolution']
173  for arg in vars(args):
174  if arg in sanitize:
175  value = getattr(args,arg)
176  if value:
177  setattr(args,arg,quote(value))
178 
179  if 'timestamp' in args.logfile:
180  setattr(args,'logfile',''.join(['mapgen_',datetime.now().strftime('%s'),'.log']))
181  # Set up the logging levels
182  levels = [logging.ERROR, logging.INFO, logging.DEBUG]
183  # limit verbosity to the number of levels, in case someone thinks if 2 v's are good 20 is better
184  verbosity = max(0, min(args.verbose, len(levels)-1))
185 
186  logging.basicConfig(filename=args.logfile,
187  format='-%(levelname)s- %(asctime)s - %(message)s',
188  level=levels[verbosity])
189  logging.getLogger().addHandler(logging.StreamHandler(sys.stdout))
190 
191  if not os.getenv("OCSSWROOT"):
192  whoops("You must define the OCSSW enviroment for me to help you...")
193 
194  if args.parfile:
195  param_proc = ParamProcessing(parfile=args.parfile)
196  param_proc.parseParFile()
197  for param in (param_proc.params['main'].keys()):
198  value = param_proc.params['main'][param]
199  if param in sanitize:
200  value = quote(value)
201  if hasattr(args,param):
202  setattr(args,param,value)
203  elif param == 'file':
204  args.ifile = value
205 
206  if not args.ifile:
207  parser.print_help()
208  whoops("\nYou must specify an input file either on the command line or in a parameter file")
209 
210  if not os.path.isfile(args.ifile):
211  whoops("\nYou must specify a valid input file either on the command line or in a parameter file")
212 
213  if not args.ofile:
214  extension = args.oformat
215  if extension == 'netcdf4':
216  extension = 'nc'
217  setattr(args,'ofile',args.ifile + '.MAP.' + extension)
218 
219  geo_opts = ["north", "south", "east", "west"]
220  l2bin_opts = ["resolution"]
221  l3map_opts = ["product", "use_rgb","product_rgb", "oformat", "resolution",
222  "projection", "central_meridian", "palfile", "fudge", "threshold",
223  "datamin", "datamax", "scale_type", "palfile", "use_transparency",
224  "trimNSEW","write_projtext"]
225  tmpfile_l2gen = "mapgen_l2gen_list.txt"
226  tmpfile_l2bin = "mapgen_tmp.l2bin.nc"
227 
228  if args.verbose > 1:
229  msg = "Parameters passed to or assumed by mapgen:\n"
230  for key,value in (vars(args)).items():
231  msg += "\t{k}={v}\n".format(k=key,v=value)
232  logging.debug(msg)
233 
234  ftype = None
235  ifiles = []
236  geofiles = []
237  if 'text' in MetaUtils.get_mime_data(args.ifile) and (not re.search('MTL.txt', args.ifile)) and (not re.search('xfdumanifest.xml', args.ifile)):
238  with open(args.ifile,'r') as lstfile:
239  ifile_list = lstfile.readlines()
240  for ifile in ifile_list:
241  if not os.path.isfile(ifile.strip()):
242  whoops("Hmmm...seems I don't recognize this input: {ifile}".format(ifile=ifile))
243 
244  ifiles.append(ifile.strip())
245  if ftype:
246  if get_ftype(ifile.strip()) != ftype:
247  whoops("Seems you are mixing input types. While I might be able to make sense of your request, it'd be nicer to pass me just on type of file at a time...")
248  else:
249  ftype = get_ftype(ifile.strip())
250  else:
251  ifiles.append(args.ifile)
252  ftype = get_ftype(args.ifile)
253 
254  if not args.use_rgb and not args.product:
255  whoops("My cousin has the artificial intelligence in the family...I'll need you to tell me what product(s) you wish to map...")
256  if ftype == 3 and len(ifiles) > 1:
257  whoops("I can only map one Level 3 file at a time...")
258 
259  # initialize the command-line option dictionary
260  clo = {}
261  # For L1 inputs we need to process to L2 for the rhos/t
262  if ftype == 1:
263  if not args.use_rgb:
264  whoops("You gave me Level 1 data, but didn't tell me you wanted RGB output, and I don't want to assume... If you want another product, I'll need Level 2 inputs")
265 
266  if args.oformat == 'netcdf4':
267  whoops("netCDF output is not supported for RGB images")
268 
269  # we only care about geolocation files if we need to run l2gen...
270  if args.geofile:
271  if 'text' in MetaUtils.get_mime_data(args.geofile):
272  with open(args.geofile,'r') as geolistfile:
273  geofiles = geolistfile.readlines()
274  else:
275  geofiles.append(args.geofile)
276  if len(geofiles) != len(ifiles):
277  whoops("Number of provided geofiles does not match number of provided ifiles...")
278 
279  l2filelst = open(tmpfile_l2gen,'w')
280  for i, l1file in enumerate(ifiles):
281  if args.verbose > 1:
282  logging.info("{cnt}: {ifile}".format(cnt=i, ifile=l1file))
283 
284  # Build the l2gen command
285  l1file = l1file.strip()
286  clo.clear()
287  clo['ifile']=l1file
288  if args.geofile:
289  clo['geofile'] = geofiles[i].strip()
290 
291  ofile = "{filebase}_{cnt:02}.nc".format(filebase=tmpfile_l2gen.replace('_list.txt',''),cnt=i+1)
292  clo['ofile'] = ofile
293  if args.product_rgb:
294  clo['l2prod'] = args.product_rgb
295  else:
297  # ftype_string, sensor = file_typer.get_file_type()
298  sensor = file_typer.get_sensor()
299 
300  dirs_by_sensor = by_sensor(sensor)
301  sensor_dir = dirs_by_sensor['dir']
302  if 'subdir' in dirs_by_sensor:
303  sensor_sub_dir = dirs_by_sensor['subdir']
304  if sensor_sub_dir:
305  filename_l3mapgen_defaults = os.path.join(os.getenv('OCDATAROOT'),
306  sensor_dir, sensor_sub_dir, 'l3mapgen_defaults.par')
307  if not os.path.exists(filename_l3mapgen_defaults):
308  filename_l3mapgen_defaults = os.path.join(os.getenv('OCDATAROOT'),
309  sensor_dir, 'l3mapgen_defaults.par')
310  else:
311  filename_l3mapgen_defaults = os.path.join(os.getenv('OCDATAROOT'),
312  sensor_dir, 'l3mapgen_defaults.par')
313  param_proc = ParamProcessing(parfile=filename_l3mapgen_defaults)
314  param_proc.parseParFile()
315  for param in (param_proc.params['main'].keys()):
316  if param == 'product_rgb':
317  clo['l2prod'] = args.product_rgb = param_proc.params['main']['product_rgb']
318  clo['atmocor'] = '0'
319  clo['proc_sst'] = '0'
320 
321  for geo_opt in geo_opts:
322  if getattr(args,geo_opt) is not None:
323  clo[geo_opt] = str(getattr(args,geo_opt))
324 
325  if args.verbose > 1:
326  msg = "l2gen parameters for {ofile}\n".format(ofile=ofile)
327  for key,value in (vars(args)).items():
328  msg += "\t{k}={v}\n".format(k=key,v=value)
329  logging.debug(msg)
330 
331  run_clo_program('l2gen', clo)
332  logging.info("Generated L2 file for input: {ifile}".format(ifile=l1file))
333 
334  l2filelst.write(ofile+'\n')
335  l2filelst.close()
336 
337  # Bin the inputs
338  if ftype != 3:
339  # Build the l2bin command line
340  clo.clear()
341  if args.use_rgb:
342  clo['flaguse'] = 'ATMFAIL,BOWTIEDEL'
343  else:
344  if args.product and re.search(r'sst.?',args.product):
345  clo['suite'] = 'SST'
346  clo['flaguse'] = 'LAND,BOWTIEDEL'
347  else:
348  clo['flaguse'] = 'ATMFAIL,CLDICE,BOWTIEDEL'
349  clo['l3bprod'] = args.product
350  if ftype == 1:
351  clo['ifile'] = tmpfile_l2gen
352  else:
353  clo['ifile'] = args.ifile
354 
355  clo['ofile'] = tmpfile_l2bin
356  clo['area_weighting'] = '1'
357  clo['prodtype'] = "regional"
358 
359  if args.north is not None:
360  clo['latnorth'] = ceil(float(args.north))
361  if args.south is not None:
362  clo['latsouth'] = floor(float(args.south))
363  if args.west is not None:
364  clo['lonwest'] = floor(float(args.west))
365  if args.east is not None:
366  clo['loneast'] = ceil(float(args.east))
367  for l2bin_opt in l2bin_opts:
368  if getattr(args,l2bin_opt) is not None:
369  if l2bin_opt == 'resolution':
370  clo[l2bin_opt] = getBinRes(str(getattr(args,l2bin_opt)))
371  else:
372  clo[l2bin_opt] = str(getattr(args,l2bin_opt))
373 
374  if args.verbose > 1:
375  msg = "l2bin parameters:\n"
376  for key,value in (vars(args)).items():
377  msg += "\t{k}={v}\n".format(k=key,v=value)
378  logging.debug(msg)
379 
380  run_clo_program('l2bin', clo)
381  logging.info("Generated bin file {binfile}".format(binfile=tmpfile_l2bin))
382 
383  # Build the l3mapgen command line
384  clo.clear()
385  if not args.use_rgb:
386  clo['mask_land'] = 1
387  if ftype != 3:
388  clo['ifile'] = tmpfile_l2bin
389  else:
390  clo['ifile'] = args.ifile
391 
392  clo['interp'] = 'nearest'
393 
394  for opt in l3map_opts + geo_opts:
395  if getattr(args,opt) is not None:
396  # set boolean options if defined
397  if type(getattr(args,opt)) is bool:
398  if getattr(args,opt) is True:
399  clo[opt] = '1'
400  else:
401  clo[opt] = str(getattr(args,opt))
402 
403  if args.product:
404  clo['product'] = args.product
405 
406  if args.oformat == 'netcdf4':
407  clo['ofile'] = args.ofile
408  else:
409  if args.product:
410  if len(args.product.split(',')) > 1:
411  ofile = args.ofile.replace(args.oformat,'') + 'PRODUCT' + '.' + args.oformat
412  clo['ofile'] = ofile
413  else:
414  clo['product'] = args.product
415  clo['ofile'] = args.ofile
416  else:
417  clo['ofile'] = args.ofile
418 
419  # Generate the maps!
420  if args.verbose > 1:
421  msg = "l3mapgen parameters:\n"
422  for key,value in (vars(args)).items():
423  msg += "\t{k}={v}\n".format(k=key,v=value)
424  logging.debug(msg)
425 
426  run_clo_program('l3mapgen', clo)
427  logging.info("Generated map file(s)")
428 
429  # Clean up our crumbs...
430  if not args.keep_intermediates:
431  if os.path.exists(tmpfile_l2gen):
432  l2filelst = open(tmpfile_l2gen,'r')
433  for l2f in l2filelst.readlines():
434  l2f = l2f.strip()
435  if os.path.exists(l2f):
436  if args.verbose:
437  logging.info("removing: {rmfile}".format(rmfile=l2f))
438  os.remove(l2f)
439  l2filelst.close()
440  os.remove(tmpfile_l2gen)
441  if os.path.exists(tmpfile_l2bin):
442  if args.verbose:
443  logging.info("removing: {rmfile}".format(rmfile=tmpfile_l2bin))
444  os.remove(tmpfile_l2bin)
445 
446  # Remove the logfile if empty (because verbosity was zero and no errors were thrown...)
447  if os.stat(args.logfile).st_size == 0:
448  os.remove(args.logfile)
type
Definition: mapgen.py:105
float
Definition: mapgen.py:139
def execute_command(command)
Definition: mapgen.py:66
def build_executable_path(prog_name)
def run_clo_program(program, inputs)
Definition: mapgen.py:87
def getBinRes(resolution)
Definition: mapgen.py:18
format
Definition: mapgen.py:187
def whoops(errormsg, status=1)
Definition: mapgen.py:62
Definition: aerosol.c:136
def get_ftype(filepath)
Definition: mapgen.py:44