Source code for ncvue.ncvmain

#!/usr/bin/env python
"""
Main ncvue window.

This sets up the main notebook window with the plotting panels and
analyses the netcdf file, e.g. determining the unlimited dimensions,
calculating dates, etc.

This module was written by Matthias Cuntz while at Institut National de
Recherche pour l'Agriculture, l'Alimentation et l'Environnement (INRAE), Nancy,
France.

Copyright (c) 2020-2021 Matthias Cuntz - mc (at) macu (dot) de

Released under the MIT License; see LICENSE file for details.

History:

* Written Nov-Dec 2020 by Matthias Cuntz (mc (at) macu (dot) de)

.. moduleauthor:: Matthias Cuntz

The following classes are provided:

.. autosummary::
   ncvMain
"""
from __future__ import absolute_import, division, print_function
import tkinter as tk
try:
    import tkinter.ttk as ttk
except Exception:
    import sys
    print('Using the themed widget set introduced in Tk 8.5.')
    print('Try to use mcview.py, which uses wxpython instead.')
    sys.exit()
import numpy as np
import curses.ascii as ca
from .ncvutils   import SEPCHAR, vardim2var, zip_dim_name_length
from .ncvscatter import ncvScatter
from .ncvcontour import ncvContour
from .ncvmap     import ncvMap

__all__ = ['ncvMain']


# --------------------------------------------------------------------
# Window with plot panels
#

[docs]class ncvMain(ttk.Frame): """ Main ncvue notebook window with the plotting panels. Sets up the notebook layout with the panels and analyses the netcdf file, e.g. determining the unlimited dimensions, calculating dates, etc. in __init__. Contains the method to analyse the netcdf file. """ # # Window setup # def __init__(self, fi, master=None, miss=np.nan, **kwargs): super().__init__(master, **kwargs) self.pack(fill=tk.BOTH, expand=1) self.name = 'ncvMain' self.fi = fi # netcdf file self.master = master # master window = root self.root = master # root window self.miss = master.miss self.dunlim = '' # name of unlimited dimension self.time = None # datetime variable self.tname = '' # datetime variable name self.tvar = '' # datetime variable name in netcdf file self.dtime = None # decimal year self.latvar = '' # name of latitude variable self.lonvar = '' # name of longitude variable self.latdim = '' # name of latitude dimension self.londim = '' # name of longitude dimension self.maxdim = 0 # maximum number of dimensions of all variables self.cols = [] # variable list # Analyse netcdf file self.analyse_netcdf() # Notebook for tabs for future plot types self.tabs = ttk.Notebook(self) self.tabs.pack(side=tk.TOP, fill=tk.BOTH, expand=1) self.tab_scatter = ncvScatter(self) self.tab_contour = ncvContour(self) self.tab_map = ncvMap(self) mapfirst = False if self.latvar: vl = vardim2var(self.latvar) if np.prod(self.fi.variables[vl].shape) > 1: mapfirst = True if self.lonvar: vl = vardim2var(self.lonvar) if np.prod(self.fi.variables[vl].shape) > 1: mapfirst = True if mapfirst: self.tabs.add(self.tab_map, text=self.tab_map.name) self.tabs.add(self.tab_scatter, text=self.tab_scatter.name) self.tabs.add(self.tab_contour, text=self.tab_contour.name) if not mapfirst: self.tabs.add(self.tab_map, text=self.tab_map.name) # # Methods # # analyse netcdf file
[docs] def analyse_netcdf(self): """ Analyse the netcdf file. Determining the unlimited dimensions, calculate dates, make list of variables. """ import datetime as dt try: import cftime as cf except ModuleNotFoundError: import netCDF4 as cf # # search unlimited dimension self.dunlim = '' for dd in self.fi.dimensions: if self.fi.dimensions[dd].isunlimited(): self.dunlim = dd break # # search for time variable and make datetime variable self.time = None self.tname = '' self.tvar = '' self.dtime = None for vv in self.fi.variables: isunlim = False if self.dunlim: if vv.lower() == self.fi.dimensions[self.dunlim].name.lower(): isunlim = True if ( isunlim or vv.lower().startswith('time_') or (vv.lower() == 'time') or (vv.lower() == 'datetime') or (vv.lower() == 'date') ): self.tvar = vv if vv.lower() == 'datetime': self.tname = 'date' else: self.tname = 'datetime' try: tunit = self.fi.variables[self.tvar].units except AttributeError: tunit = '' # assure 01, etc. if values < 10 if tunit.find('since') > 0: tt = tunit.split() dd = tt[2].split('-') dd[0] = ('000'+dd[0])[-4:] dd[1] = ('0'+dd[1])[-2:] dd[2] = ('0'+dd[1])[-2:] tt[2] = '-'.join(dd) tunit = ' '.join(tt) try: tcal = self.fi.variables[self.tvar].calendar except AttributeError: tcal = 'standard' time = self.fi.variables[self.tvar][:] # time dimension "day as %Y%m%d.%f" from cdo. if ' as ' in tunit: itunit = tunit.split()[2] dtime = [] for tt in time: stt = str(tt).split('.') sstt = ('00'+stt[0])[-8:] + '.' + stt[1] dtime.append(dt.datetime.strptime(sstt, itunit)) ntime = cf.date2num(dtime, 'days since 0001-01-01 00:00:00') self.dtime = cf.num2date(ntime, 'days since 0001-01-01 00:00:00') else: try: self.dtime = cf.num2date(time, tunit, calendar=tcal) except ValueError: self.dtime = None if self.dtime is not None: ntime = len(self.dtime) if (tcal == '360_day'): ndays = [360.]*ntime elif (tcal == '365_day'): ndays = [365.]*ntime elif (tcal == 'noleap'): ndays = [365.]*ntime elif (tcal == '366_day'): ndays = [366.]*ntime elif (tcal == 'all_leap'): ndays = [366.]*ntime else: ndays = [ 365. + float((((t.year%4) == 0) & ((t.year%100) != 0)) | ((t.year%400) == 0)) for t in self.dtime ] self.dtime = np.array([ t.year + (t.dayofyr-1 + t.hour / 24. + t.minute / 1440 + t.second / 86400.) / ndays[i] for i, t in enumerate(self.dtime) ]) # make datetime variable if self.time is None: try: self.time = cf.num2date( time, tunit, calendar=tcal, only_use_cftime_datetimes=False, only_use_python_datetimes=True) except: self.time = None if self.time is None: try: self.time = cf.num2date(time, tunit, calendar=tcal) except: self.time = None if self.time is None: # if not possible use decimal year self.time = self.dtime if self.time is None: # could not interpret time at all, # e.g. if units = "months since ..." self.time = time self.dtime = time break # # construct list of variable names with dimensions if self.time is not None: addt = [ self.tname + ' ' + SEPCHAR + str(tuple(zip_dim_name_length(self.fi.variables[self.tvar])))] self.cols += addt ivars = [] for vv in self.fi.variables: # ss = self.fi.variables[vv].shape ss = tuple(zip_dim_name_length(self.fi.variables[vv])) self.maxdim = max(self.maxdim, len(ss)) ivars.append((vv, ss, len(ss))) self.cols += sorted([ vv[0] + ' ' + SEPCHAR + str(vv[1]) for vv in ivars ]) # # search for lat/lon variables self.latvar = '' self.lonvar = '' # first sweep: *name must be "latitude" and # units must be "degrees_north" if not self.latvar: for vv in self.fi.variables: try: sname = self.fi.variables[vv].standard_name except AttributeError: try: sname = self.fi.variables[vv].long_name except AttributeError: sname = self.fi.variables[vv].name if sname.lower() == 'latitude': try: sunit = self.fi.variables[vv].units except AttributeError: sunit = '' if sunit.lower() == 'degrees_north': self.latvar = vv if not self.lonvar: for vv in self.fi.variables: try: sname = self.fi.variables[vv].standard_name except AttributeError: try: sname = self.fi.variables[vv].long_name except AttributeError: sname = self.fi.variables[vv].name if sname.lower() == 'longitude': try: sunit = self.fi.variables[vv].units except AttributeError: sunit = '' if sunit.lower() == 'degrees_east': self.lonvar = vv # second sweep: name must start with lat and # units must be "degrees_north" if not self.latvar: for vv in self.fi.variables: sname = self.fi.variables[vv].name if sname[0:3].lower() == 'lat': try: sunit = self.fi.variables[vv].units except AttributeError: sunit = '' if sunit.lower() == 'degrees_north': self.latvar = vv if not self.lonvar: for vv in self.fi.variables: sname = self.fi.variables[vv].name if sname[0:3].lower() == 'lon': try: sunit = self.fi.variables[vv].units except AttributeError: sunit = '' if sunit.lower() == 'degrees_east': self.lonvar = vv # third sweep: name must contain lat and # units must be "degrees_north" if not self.latvar: for vv in self.fi.variables: sname = self.fi.variables[vv].name sname = sname.lower() if sname.find('lat') >= 0: try: sunit = self.fi.variables[vv].units except AttributeError: sunit = '' if sunit.lower() == 'degrees_north': self.latvar = vv if not self.lonvar: for vv in self.fi.variables: sname = self.fi.variables[vv].name sname = sname.lower() if sname.find('lon') >= 0: try: sunit = self.fi.variables[vv].units except AttributeError: sunit = '' if sunit.lower() == 'degrees_east': self.lonvar = vv # fourth sweep: same as first but units can be "degrees" if not self.latvar: for vv in self.fi.variables: try: sname = self.fi.variables[vv].standard_name except AttributeError: try: sname = self.fi.variables[vv].long_name except AttributeError: sname = self.fi.variables[vv].name if sname.lower() == 'latitude': try: sunit = self.fi.variables[vv].units except AttributeError: sunit = '' if sunit.lower() == 'degrees': self.latvar = vv if not self.lonvar: for vv in self.fi.variables: try: sname = self.fi.variables[vv].standard_name except AttributeError: try: sname = self.fi.variables[vv].long_name except AttributeError: sname = self.fi.variables[vv].name if sname.lower() == 'longitude': try: sunit = self.fi.variables[vv].units except AttributeError: sunit = '' if sunit.lower() == 'degrees': self.lonvar = vv # fifth sweep: same as second but units can be "degrees" if not self.latvar: for vv in self.fi.variables: sname = self.fi.variables[vv].name if sname[0:3].lower() == 'lat': try: sunit = self.fi.variables[vv].units except AttributeError: sunit = '' if sunit.lower() == 'degrees': self.latvar = vv if not self.lonvar: for vv in self.fi.variables: sname = self.fi.variables[vv].name if sname[0:3].lower() == 'lon': try: sunit = self.fi.variables[vv].units except AttributeError: sunit = '' if sunit.lower() == 'degrees': self.lonvar = vv # sixth sweep: same as third but units can be "degrees" if not self.latvar: for vv in self.fi.variables: sname = self.fi.variables[vv].name sname = sname.lower() if sname.find('lat') >= 0: try: sunit = self.fi.variables[vv].units except AttributeError: sunit = '' if sunit.lower() == 'degrees': self.latvar = vv if not self.lonvar: for vv in self.fi.variables: sname = self.fi.variables[vv].name sname = sname.lower() if sname.find('lon') >= 0: try: sunit = self.fi.variables[vv].units except AttributeError: sunit = '' if sunit.lower() == 'degrees': self.lonvar = vv # # determine lat/lon dimensions self.latdim = '' self.londim = '' if self.latvar: latshape = self.fi.variables[self.lonvar].shape if (len(latshape) < 1) or (len(latshape) > 2): estr = 'Something went wrong determining lat/lon:' estr += ' latitude variable is not 1D or 2D.' print(estr) estr = 'latitude variable with dimensions:' ldim = self.fi.variables[self.latvar].dimensions print(estr, self.latvar, ldim) estr = 'longitude variable with dimensions:' ldim = self.fi.variables[self.lonvar].dimensions print(estr, self.lonvar, ldim) self.latvar = '' self.lonvar = '' else: self.latdim = self.fi.variables[self.latvar].dimensions[0] if self.lonvar: lonshape = self.fi.variables[self.lonvar].shape if len(lonshape) == 1: self.londim = self.fi.variables[self.lonvar].dimensions[0] elif len(lonshape) == 2: self.londim = self.fi.variables[self.lonvar].dimensions[1] else: estr = 'Something went wrong determining lat/lon:' estr += ' longitude variable is not 1D or 2D.' print(estr) estr = 'latitude variable with dimensions:' ldim = self.fi.variables[self.latvar].dimensions print(estr, self.latvar, ldim) estr = 'longitude variable with dimensions:' ldim = self.fi.variables[self.lonvar].dimensions print(estr, self.lonvar, ldim) self.latvar = '' self.lonvar = '' # # add units to lat/lon name if self.latvar: idim = tuple(zip_dim_name_length(self.fi.variables[self.latvar])) self.latvar = self.latvar + ' ' + SEPCHAR + str(idim) if self.lonvar: idim = tuple(zip_dim_name_length(self.fi.variables[self.lonvar])) self.lonvar = self.lonvar + ' ' + SEPCHAR + str(idim)