#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on 17 June 2025
:Authors:
radanovics
largely copied from epygram epy_cartoplot.py
"""
import sys
import argparse
import textwrap
from bronx.syntax.parsing import str2dict
from bronx.stdtypes.date import Date
import epygram
from epygram import epylog, epygramError
from epygram.cli.args_catalog import (add_arg_to_parser,
files_args, fields_args,
misc_args, output_args,
runtime_args, graphical_args)
import matplotlib.pyplot as plt
import cartopy.feature as cf
from snowtools.plots.maps.quicklookmap import read_and_preprocess, wind_map, difference_map, scalar_map
CFEATURES = [f for f in dir(cf) if all([c.isupper() for c in f])]
"""Cartopy features"""
[docs]def main_cli(filename,
fid=None,
Ufid=None,
Vfid=None,
refname=None,
diffmode=False,
date=None,
# pre-processing
operation=None,
global_shift_center=None,
zoom=None,
# figure
title=None,
# graphical settings
plot_method='pcolormesh',
minmax=None,
mask_threshold=None,
colorsnumber=50,
colormap='plasma',
center_cmap_on_0=False,
scatter_kw=None,
# cartography
parallels='auto',
meridians='auto',
french_depts=False,
cartopy_features=[],
# wind/vectors
vectors_subsampling=20,
vector_plot_method='quiver',
wind_components_are_projected_on=None,
quiverkey=None,
map_factor_correction=False,
# output
savefig=False,
outputfilename=None,
figures_dpi=epygram.config.default_figures_dpi):
"""
Plot fields.
:param filename: name of the file to be processed.
:param fid: field identifier.
:param Ufid: U-component of wind field identifier.
:param Vfid: V-component of wind field identifier.
:param refname: name of the reference file to be compared to.
:param diffmode: if True, plots the difference field between the field in filename and refname.
:param date: date and time to choose from the file
Pre-processing:
:param operation: makes the requested operation
(e.g. {'operation':'-','operand':273.15} or
{'operation':'exp'}) on the field before plot.
:param global_shift_center: for global lon/lat grids, shift the center by the
requested angle (in degrees). Enables a [0,360] grid
to be shifted to a [-180,180] grid, for instance (with -180 argument).
:param zoom: a dict(lonmin, lonmax, latmin, latmax) on which to build the plot.
Figure:
:param title: title to be written over plot.
Graphical settings:
:param plot_method: matplotlib plotting method to be used, among
('pcolormesh', 'contourf', 'contour', 'scatter').
:param minmax: tuple giving (or not) min and max fields values to be plotted.
:param colorsnumber: number of color discretization/isolines for fields plots.
:param colormap: name of the colormap for fields plots.
:param center_cmap_on_0: to center the colormap on 0.
:param mask_threshold: dict with min and/or max value(s) to mask outside.
:param scatter_kw: kwargs to be passed to matplotlib's ax.scatter().
Only for plot_method = 'scatter'.
Cartography:
:param meridians and parallels: enable to fine-tune the choice of lines to
plot, with either:
- 'auto': automatic scaling to the map extents
- 'default': range(0,360,10) and range(-90,90,10)
- a list of values
- a grid step, e.g. 5 to plot each 5 degree.
- None: no one is plot
:param french_depts: draws french departments instead of countries boundaries.
:param cartopy_features: list of cartopy.feature.??? features.
Vector plots:
:param vectors_subsampling: subsampling ratio of vectors plots.
:param vector_plot_method: among ('quiver', 'barbs', 'streamplot') for vector plots.
:param wind_components_are_projected_on: inform the plot on which axes the
vector components are projected on ('grid' or 'lonlat').
If None (default), look for information in the field, or raise error.
:param quiverkey: options to be passed to plotfield to activate a quiver key
(cf. pyplot.quiverkey).
:param map_factor_correction: if True, applies a correction of magnitude
to vector due to map factor.
Output:
:param savefig: save figures to file, instead of interactive plot
:param outputfilename: specify an output filename for the plot,
including format as extension.
:param figures_dpi: quality of saved figures.
"""
# 0/ checks, determine mode, initializations
# checks
assert not all([f is None for f in (fid, Ufid, Vfid)]), "Mandatory arguments: *fid* OR *Ufid/Vfid*."
if fid is not None:
assert Ufid is Vfid is None, "Exclusive arguments: *fid* OR *Ufid/Vfid*."
if Ufid is not None:
assert Vfid is not None, "Arguments Ufid/Vfid got by pair."
# mode
windmode = fid is None
assert not (diffmode and windmode), "Exclusive options: diff to reference file and wind plot."
# plot options
plot_kwargs = dict(
# graphical settings
plot_method=plot_method,
minmax=minmax,
mask_threshold=mask_threshold,
scatter_kw=scatter_kw,
# cartography
parallels=parallels,
meridians=meridians,
epygram_departments=french_depts,
cartopy_features=cartopy_features,
# colormapping
colormap=colormap,
colorsnumber=colorsnumber,
center_cmap_on_0=center_cmap_on_0)
# pre-processing options
preprocess_options = dict(
operation=operation,
global_shift_center=global_shift_center,
zoom=zoom)
# 1/ resource(s)
resource = epygram.formats.resource(filename, openmode='r')
if resource.format == 'DDHLFA':
raise epygramError('use ddhlfa_plot.py tool for DDHLFA files.')
if diffmode:
reference = epygram.formats.resource(refname, openmode='r')
if windmode:
# 2.1/ wind
u = read_and_preprocess(resource, Ufid, date,
**preprocess_options)
v = read_and_preprocess(resource, Vfid, date,
**preprocess_options)
field = epygram.fields.make_vector_field(u, v)
if title is None:
title = "\n".join([str(fid), str(field.validity.get())])
takeover = wind_map(field, title, map_factor_correction, vectors_subsampling, wind_components_are_projected_on,
vector_plot_method, quiverkey, plot_kwargs)
fig = takeover['fig']
else:
# 2.2/ scalar field
if diffmode:
# 2.2.1/ diff of scalar fields
# read field and ref, compute diff
field = read_and_preprocess(resource, fid, date,
**preprocess_options)
ref_field = read_and_preprocess(reference, fid, date,
**preprocess_options)
if title is None:
print(resource.container.basename)
title = "\n".join([resource.container.basename + ' - ' +
reference.container.basename,
str(fid), str(field.validity.get())])
takeover = difference_map(field, ref_field, title, plot_kwargs)
fig = takeover['fig']
else:
# 2.2.2/ plot single scalar fields
field = read_and_preprocess(resource, fid, date,
**preprocess_options)
if title is None:
title = "\n".join([str(fid), str(field.validity.get())])
takeover = scalar_map(field, title, plot_kwargs)
fig = takeover['fig']
# 3/ output
if savefig:
epylog.info("save plot...")
save_kwargs = dict(bbox_inches='tight', dpi=figures_dpi)
fig.savefig(outputfilename, **save_kwargs)
else:
plt.show()
# end of main() ###############################################################
def main():
# 1. Parse arguments
####################
parser = argparse.ArgumentParser(
description=textwrap.dedent("""
Tool to plot maps of meteorological or snow cover related 2D
fields. Provide either a graphical user interface (GUI) or
do plots based on command line arguments.
To use the Graphical interface, just run this script without
argument or by providing only filename, and optionnally
field name and date.
To do directly a plot without graphical interaction, you can
use all arguments below. Make sure to use --directplot or
--outputfilename flag.
"""),
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='(Use EPyGrAM v' + epygram.__version__ + ')')
add_arg_to_parser(
parser,
['filename',
dict(type=str, nargs='?',
help='name of the file to be processed.')])
add_arg_to_parser(parser, fields_args['field'])
add_arg_to_parser(parser, fields_args['windfieldU'])
add_arg_to_parser(parser, fields_args['windfieldV'])
add_arg_to_parser(parser, ['--date',
dict(type=str, dest='date',
help="Selected date in a bronx.Date compatible string format", )])
diffmodes = parser.add_mutually_exclusive_group()
add_arg_to_parser(diffmodes, files_args['file_to_refer_in_diff'])
add_arg_to_parser(parser, misc_args['operation_on_field'])
add_arg_to_parser(parser, misc_args['mask_threshold'])
add_arg_to_parser(parser, misc_args['wind_components_are_projected_on'])
add_arg_to_parser(parser, misc_args['map_factor_correction'])
# graphics
add_arg_to_parser(parser, [
"--directplot",
dict(
action="store_true", default=False,
help="Do a non-interactive plot taking into account all options rather than using the GUI.")])
add_arg_to_parser(parser, graphical_args['plot_method'])
add_arg_to_parser(parser, graphical_args['minmax'])
add_arg_to_parser(parser, graphical_args['levels_number'])
add_arg_to_parser(parser, graphical_args['colormap'], default='YlGnBu')
add_arg_to_parser(parser, graphical_args['center_cmap_on_0'])
add_arg_to_parser(parser, graphical_args['title'])
add_arg_to_parser(parser, graphical_args['cartopy_features'],
help="cartopy features (cartopy.feature.*), separated by comma " +
str(CFEATURES))
add_arg_to_parser(parser, graphical_args['french_departments'])
add_arg_to_parser(parser, graphical_args['parallels'])
add_arg_to_parser(parser, graphical_args['meridians'])
add_arg_to_parser(parser, graphical_args['vectors_subsampling'])
add_arg_to_parser(parser, graphical_args['scatter_kw'])
add_arg_to_parser(parser, graphical_args['lonlat_zoom'])
add_arg_to_parser(parser, graphical_args['vector_plot_method'])
add_arg_to_parser(parser, graphical_args['quiverkey'])
add_arg_to_parser(parser, graphical_args['figures_dpi'])
add_arg_to_parser(parser, graphical_args['global_shift_center'])
# diff
add_arg_to_parser(parser, graphical_args['diffcolormap'], default='RdBu_r')
add_arg_to_parser(parser, graphical_args['diff_center_cmap_on_0'])
# output
add_arg_to_parser(parser, output_args['outputfilename'], default=None)
add_arg_to_parser(parser, runtime_args['verbose'])
args = parser.parse_args()
# 2. Initializations
####################
epygram.init_env()
# 2.0 logs
epylog.setLevel('WARNING')
if args.verbose:
epylog.setLevel('INFO')
# 2.0.1 GUI
if not args.directplot and args.outputfilename is None:
from snowtools.plots.maps.mapplotter_gui import main as main_gui
if args.filename is not None:
from snowtools.plots.maps.mapplotter_fileobj import read_file
fileobj = read_file(args.filename)
else:
fileobj = None
arguments = {}
if args.field is not None:
variable = fileobj.variable_desc(args.field)
if variable is not None:
arguments['variable'] = variable['full_name']
main_gui(fileobj=fileobj, arguments=arguments)
sys.exit(0)
# 2.1 options
refname = args.refname
if args.refname is not None:
diffmode = True
center_cmap_on_0 = args.diffcenter_cmap_on_0
colormap = args.diffcolormap
else:
diffmode = False
center_cmap_on_0 = args.center_cmap_on_0
colormap = args.colormap
if args.minmax is not None:
minmax = args.minmax.split(',')
else:
minmax = None
if args.zoom is not None:
zoom = str2dict(args.zoom, float)
else:
zoom = None
if args.operation is not None:
_operation = args.operation.split(',')
operation = {'operation':_operation.pop(0).strip()}
if len(_operation) > 0:
operation['operand'] = float(_operation.pop(0).strip())
else:
operation = None
if args.parallels == 'None':
parallels = None
elif ',' in args.parallels:
parallels = [float(p.strip()) for p in args.parallels.split(',')]
else:
try:
parallels = float(args.parallels)
except ValueError:
parallels = args.parallels
if args.meridians == 'None':
meridians = None
elif ',' in args.meridians:
meridians = [float(m.strip()) for m in args.meridians.split(',')]
else:
try:
meridians = float(args.meridians)
except ValueError:
meridians = args.meridians
if args.mask_threshold is not None:
mask_threshold = str2dict(args.mask_threshold, float)
else:
mask_threshold = None
if args.quiverkey is None or args.quiverkey == '':
quiverkey = None
else:
quiverkey = str2dict(args.quiverkey, float)
if args.scatter_kw is not None:
scatter_kw = str2dict(args.scatter_kw, int)
else:
scatter_kw = None
if args.cartopy_features is not None:
cartopy_features = args.cartopy_features.split(',')
else:
cartopy_features = []
if args.outputfilename:
savefig = True
else:
savefig = False
# 2.2 field to be processed
if args.Ucomponentofwind is not None or args.Vcomponentofwind is not None:
if None in (args.Ucomponentofwind, args.Vcomponentofwind):
raise epygramError("wind mode: both U & V components of wind must be supplied")
if diffmode:
raise NotImplementedError("diffmode (-d/D) AND wind mode (--wU/wV) options together.")
elif args.field is None:
raise epygramError("Need to specify a field (-f) or two wind fields (--wU/--wV).")
if args.date is None:
raise ValueError("Need to specify a date (--date).")
else:
date = Date(args.date)
# 3. Main
#########
main_cli(args.filename,
fid=args.field,
Ufid=args.Ucomponentofwind,
Vfid=args.Vcomponentofwind,
refname=refname,
diffmode=diffmode,
date=date,
# pre-processing
operation=operation,
global_shift_center=args.global_shift_center,
zoom=zoom,
# figure
title=args.title,
# graphical settings
plot_method=args.plot_method,
minmax=minmax,
mask_threshold=mask_threshold,
colorsnumber=args.levelsnumber,
colormap=colormap,
center_cmap_on_0=center_cmap_on_0,
scatter_kw=scatter_kw,
# cartography
parallels=parallels,
meridians=meridians,
french_depts=args.depts,
cartopy_features=cartopy_features,
# wind/vectors
vectors_subsampling=args.vectors_subsampling,
vector_plot_method=args.vector_plot_method,
wind_components_are_projected_on=args.wind_components_are_projected_on,
quiverkey=quiverkey,
map_factor_correction=args.map_factor_correction,
# output
savefig=savefig,
outputfilename=args.outputfilename)
if __name__ == '__main__':
main()