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