#
# Changes
#
# 2022-09, Arjo Segers
#   Added class to collect co-located observations.
#
# 2023, Lewis Blake, Arjo Segers
#   Formatted using "black".
#


########################################################################
###
### help
###
########################################################################

"""
.. _cso-colocate:

***********************
``cso_colocate`` module
***********************

The :py:mod:`cso_colocate` module provides classes to collect
observations and simulations from pixels that are co-located with surface locations.


Class hierchy
=============

The classes and are defined according to the following hierchy:

* :py:class:`.UtopyaRc`

  * :py:class:`.CSO_CoLocate`
  * :py:class:`.CSO_CoLocatePlotTimeSeries`



Classes
=======


"""


########################################################################
###
### modules
###
########################################################################

# modules:
import logging

# tools:
import utopya
import cso_catalogue


########################################################################
###
### timeseries of average over co-located pixels
###
########################################################################


class CSO_CoLocate(utopya.UtopyaRc):

    """
    Collect time series of observations and/or simulations co-located with
    surface locations. The value assigned to location and time is an average over all pixels
    within a specified distance and valid for the time.

    Location should be specified in a csv file with columns at least longitude and latiude::

      name                   ;longitude;latitude
      Peyrusse Vieille       ;     0.18;   43.62
      San Pablo de los Montes;    -4.35;   39.55
      Lahemaa                ;    25.9 ;   59.5

    The column names are not pre-defined, and additional columns are allowed too.
    Lines that start with ``'#'`` are ignored.
    By default the seperation character is ``';'``, with surrounding white-space ignored.
    In the settings, specifiy the name of the csv file and eventually other characters
    for the seperation and comment::

        ! table with station locations:
        <rcbase>.locations.file           :  /data/stations.csv
        ! special characters:
        <rcbase>.locations.sep            :  ;
        <rcbase>.locations.comment        :  #

    Also specify the column names that used for the longitude and latitude::

        ! column names:
        <rcbase>.locations.longitude      :  longitude
        <rcbase>.locations.latiutde       :  latiutde

    Time series are created within a time range::

      ! time range:
      <rcbase>.timerange.start        :  2018-06-01 00:00
      <rcbase>.timerange.end          :  2018-06-03 23:59

    Output is collected in files per time interval (daily, hourly).
    Specify the output freqency and a template for the name of the target file::

      ! time step: monthly, daily, ..
      <rcbase>.output.freq            :  monthly

      ! target file:
      <rcbase>.output.file            :   /scratch/colocated/timeseries_%Y%m.nc

    Specify a target time resolution; pixels are collected in intervals ``[t1,t2)`` ::

        ! time resolution: hourly
        <rcbase>.resolution             :  hourly

    Pixels are selected within a distance of a location::

        ! collect pixels within radion of km:
        <rcbase>.distance               :  100.0

    Input data could originate from different sources, for example a *data* file with footprints
    and retrieval, and a *state* file with simulations by a model.
    Specify a list with keywords describing the sources; the first source should be a file
    that contains the footprints::

        ! keywords for source files:
        <rcbase>.sources                :  data

    For each source, specify the input file(s) as a filename pattern::

        ! input file or filename pattern for each source type:
        <rcbase>.source.data         :  /scratch/output/CSO_output_%Y%m%d_%H%M_S5p-no2_data.nc

    The input could be filtered by applying selections, for example on quality value;
    see :py:meth:`CSO_File.SelectPixels` method for supported filter types::

        ! keywords for filters:
        <rcbase>.filters                :  quality

        ! minimum quality value required:
        <rcbase>.filter.quality.var       :  qa_value
        <rcbase>.filter.quality.type      :  min
        <rcbase>.filter.quality.min       :  0.8
        <rcbase>.filter.quality.units     :  1

    Specify a list of the variables to be created, and for each of them the source type and source variable::

        ! data variables to be created:
        <rcbase>.output.vars            :  xvmr

        ! input variables:
        !   data:vcd    : from data file
        !   state:hx    : from state file
        <rcbase>.output.xvmr.source     :  data:xvmr

    Existing files are replaced if the following flag is set::

        ! renew existing files?
        <rcbase>.renew                  :  True

    To reduce file size, by default all variables are written to file as short-integers (2 bytes)
    accompanied by ``add_offset`` and ``scale_factor`` attributes.
    A flag is available to disable packing.
    In addition, ``zlib`` compression is enabled.
    The default compression level is 1 (out of 9); set the following flag to a higher level
    to have stronger compression (on expense of computation time), or set to 0 to disable compression::

        ! pack floats as shorts:
        <rcbase>.output.packed              :  True
        ! zlib compression level (default 1, 0 for no compression):
        <rcbase>.output.complevel           :  1

    """

    def __init__(self, rcfile, rcbase="", env={}, indent=""):

        """
        Extract timeseries.
        """

        # modules:
        import os
        import numpy
        import datetime
        import glob
        import pandas
        import xarray

        # tools:
        import cso_file
        import cso_mapping

        # init base object:
        utopya.UtopyaRc.__init__(self, rcfile, rcbase=rcbase, env=env)

        # info ...
        logging.info(indent + "")
        logging.info(indent + "** Co-located CSO data")
        logging.info(indent + "")

        # time range:
        t1 = self.GetSetting("timerange.start", totype="datetime")
        t2 = self.GetSetting("timerange.end", totype="datetime")

        # info ...
        tfmt = "%Y-%m-%d %H:%M"
        logging.info(indent + "timerange: [%s,%s]" % (t1.strftime(tfmt), t2.strftime(tfmt)))

        # output time step:
        output_freq = self.GetSetting("output.freq")
        # info ...
        logging.info(indent + "output freqency: %s" % output_freq)
        # output time intervals:
        if output_freq in ["D", "day", "daily"]:
            outtimes1 = pandas.date_range(t1, t2, freq="D")
            outtimes2 = outtimes1 + pandas.Timedelta(days=1)
        elif output_freq in ["M", "month", "monthly"]:
            outtimes1 = pandas.date_range(t1, t2, freq="MS")
            outtimes2 = outtimes1 + pandas.DateOffset(months=1, seconds=-1)
        else:
            logging.error('unsupported output time step "%s"' % step)
            raise Exception
        # endif

        # target file:
        outfile_template = self.GetSetting("output.file")
        # info ...
        logging.info(indent + "target files: %s" % outfile_template)

        # renew?
        renew = self.GetSetting("renew", totype="bool")
        # info ...
        logging.info(indent + "renew output: %s" % renew)

        # pack floats as shorts?
        packed = self.GetSetting("output.packed", "bool")
        # compression level:
        complevel = self.GetSetting("output.complevel", "int")

        # global attributes
        gattrs = {}
        attrnames = self.GetSetting("output.attrs").split()
        for key in attrnames:
            gattrs[key] = self.GetSetting("output.attr.%s" % (key))
        # endfor

        # station file:
        locations_file = self.GetSetting("locations.file")
        # info ...
        logging.info(indent + "location file: %s" % locations_file)
        # check ..
        if not os.path.isfile(locations_file):
            logging.error("file not found: %s" % locations_file)
            raise Exception
        # endif
        # special characters:
        sep = self.GetSetting("locations.sep", default=";")
        comment = self.GetSetting("locations.sep", comment="#")
        # column names:
        loc_lon = self.GetSetting("locations.longitude")
        loc_lat = self.GetSetting("locations.latitude")
        # read:
        locations = pandas.read_csv(
            locations_file, sep="\s*" + sep + "\s*", engine="python", comment=comment
        )
        # count:
        nloc = len(locations)

        # time resolution
        resolution = self.GetSetting("resolution")

        # selection distance:
        distance_km = self.GetSetting("distance", "float")

        # source types:
        stypes = self.GetSetting("sources").split()
        # storage per source:
        sources = {}
        # loop:
        for stype in stypes:
            # tempate for input file:
            sources[stype] = {}
            sources[stype]["input"] = self.GetSetting("source.%s" % stype)
        # endfor

        # first source type, this should have the footprints:
        stype0 = stypes[0]

        # variables:
        vkeys = self.GetSetting("output.vars").split()
        # storage for input description:
        varsources = {}
        for vkey in vkeys:
            # source description:
            varsources[vkey] = self.GetSetting("output.%s.source" % (vkey))
        # endfor

        # loop over times for output files:
        for iout in range(len(outtimes1)):
            # current:
            outtime1 = outtimes1[iout]
            outtime2 = outtimes2[iout]
            # target file:
            outfile = outtime1.strftime(outfile_template)
            # create?
            if (not os.path.isfile(outfile)) or renew:
                # info ...
                logging.info(indent + "create %s .." % outfile)

                # averaging time intervals:
                if resolution == "hourly":
                    # start and end times:
                    tt1 = pandas.date_range(outtime1, outtime2, freq="H")
                    tt2 = t1 + pandas.Timedelta(hours=1)
                #
                elif resolution == "daily":
                    # start and end times:
                    tt1 = pandas.date_range(outtime1, outtime2, freq="D")
                    tt2 = tt1 + pandas.Timedelta(days=1)
                #
                else:
                    logging.error('unsupported resolution "%s"' % resolution)
                    raise Exception
                # endif
                # mid:
                ttm = tt1 + (tt2 - tt1) / 2
                # count:
                nt = len(ttm)

                # info ...
                logging.info(indent + "init output file ...")
                # init output file:
                outf = cso_file.CSO_File()
                # locations:
                outf.AddCoord(
                    "location",
                    xarray.DataArray(
                        numpy.arange(1, nloc + 1),
                        dims=("location",),
                        attrs={"long_name": "location", "units": "1"},
                    ),
                )
                # times:
                outf.AddTimeCoord(ttm, (tt1, tt2))
                # store location info:
                for key in locations.keys():
                    # variable name:
                    vname = "location_%s" % key
                    # add data:
                    outf.AddDataVariable(
                        vname,
                        ("location",),
                        values=locations[key],
                        attrs={"long_name": "location %s" % key, "units": "1"},
                    )
                # endfor
                # storage for number of selected pixels:
                outf.AddDataVariable(
                    "npixel",
                    ("time", "location"),
                    values=numpy.zeros((nt, nloc), dtype="i2"),
                    attrs={
                        "long_name": "number of pixels within %i km" % distance_km,
                        "units": "1",
                    },
                )
                # variables not defined yet, require attributes from input:
                outf_setup = False

                # init history:
                line = "averages over pixels with %i km from locations specified in %s;" % (
                    distance_km,
                    locations_file,
                )
                line = line + "data files:"
                for stype in stypes:
                    line = line + " " + sources[stype]["input"]
                # endfor
                # store:
                history = [line]

                # assume input has hourly timestamps ..
                times = pandas.date_range(outtime1, outtime2, freq="H")
                # info ...
                logging.info(
                    indent + "search for input files in [%s,%s] .." % (outtime1, outtime2)
                )
                logging.info(indent + "  search times: %s, .., %s" % (times[0], times[-1]))
                # list of processed files:
                donefiles = []
                # loop over input times:
                for time in times:

                    # data file with locations:
                    infile0 = time.strftime(sources[stype0]["input"])
                    # already done? then next time:
                    if infile0 in donefiles:
                        continue
                    # not all hours have simulations..
                    if not os.path.isfile(infile0):
                        continue

                    # info ...
                    logging.info(indent + "  read input file(s) ...")
                    # loop:
                    for stype in stypes:
                        # input file:
                        infile = time.strftime(sources[stype]["input"])
                        # info ..
                        logging.info(indent + "    %s" % infile)
                        # read:
                        sources[stype]["file"] = cso_file.CSO_File(filename=infile)
                        ## update history:
                        # history.insert( 0, 'pixel data from %s' % os.path.basename(infile) )
                    # endfor

                    # filter:
                    selected, history = sources[stype0]["file"].SelectPixels(
                        self.rcf, rcbase, indent=indent + "    "
                    )
                    # count:
                    npix = selected.size
                    nsel = selected.sum()
                    # none?
                    if nsel == 0:
                        loggin.info(indent + "      no pixels selected ..")
                        continue
                    # endif
                    # info ..
                    logging.info(indent + "      selected %i / %i pixels .." % (nsel, npix))

                    # target record:
                    (ii,) = numpy.where((time >= tt1) & (time < tt2))
                    # check ..
                    if len(ii) != 1:
                        logging.error("time %s found %i times within bounds:" % (time, len(ii)))
                        for it in range(len(tt1)):
                            logging.error("[%s,%s]" % (t1[it], t2[it]))
                        # endfor
                        raise Exception
                    # endif
                    # extract:
                    itime = ii[0]
                    # info ..
                    logging.info(
                        indent + "      target time interval: [%s,%s)" % (tt1[itime], tt2[itime])
                    )

                    # storage:
                    das_in = {}
                    # loop over target variables:
                    for vkey in vkeys:
                        # source format:  state:y
                        source = varsources[vkey]
                        stype, sname = source.split(":")
                        # link input data array:
                        das_in[vkey] = sources[stype]["file"].ds[sname]
                        # check ..
                        shp = das_in[vkey].shape[1:]
                        if numpy.product(shp) != 1:
                            logging.error(
                                'source variable "%s" has shape %s per pixel, but currently only scalars supported ...'
                                % (vkey, str(shp))
                            )
                            raise Exception
                        # endif
                    # endfor

                    # neet to define output? uses attributes from input, so could only be done here
                    if not outf_setup:
                        # loop over target variables:
                        for vkey in vkeys:
                            # empty array:
                            outf.AddDataVariable(
                                vkey, ("time", "location"), dtype="f4", attrs=das_in[vkey].attrs
                            )
                        # endfor
                        # reset:
                        outf_setup = True
                    # endif # init output?

                    # info ..
                    logging.info(indent + "      loop over locations ...")
                    # loop over locations:
                    for iloc in range(nloc):
                        # index:
                        indx = locations.index[iloc]
                        # extract:
                        lon = locations.at[indx, loc_lon]
                        lat = locations.at[indx, loc_lat]
                        # info ...
                        logging.info(indent + "        location % (%f,%f)" % (iloc + 1, lon, lat))
                        # distance in m from pixels to location:
                        dist_m = cso_mapping.LonLatDistance(
                            lon,
                            lat,
                            sources[stype0]["file"].ds["longitude"].values,
                            sources[stype0]["file"].ds["latitude"].values,
                        )
                        # over-all selection:
                        (iipix,) = numpy.where((dist_m / 1000.0 <= distance_km) & selected)
                        # any?
                        if len(iipix) == 0:
                            # info ...
                            logging.info(indent + "          no pixels within %i km" % distance_km)
                        else:
                            # count:
                            nval = len(iipix)
                            # info ...
                            logging.info(
                                indent
                                + "          found %i selected pixels within %i km"
                                % (nval, distance_km)
                            )
                            # add contribution:
                            outf.ds["npixel"].values[itime, iloc] += nval
                            # loop over target variables:
                            for vkey in vkeys:
                                # sum over selected pixels:
                                vsum = das_in[vkey].values[iipix, :].sum()
                                # add contribution:
                                if numpy.isnan(outf.ds[vkey].values[itime, iloc]):
                                    outf.ds[vkey].values[itime, iloc] = vsum
                                else:
                                    outf.ds[vkey].values[itime, iloc] += vsum
                                # endif
                            # endfor
                        # endif
                    # endfor # locations

                # endfor # input times

                # info ...
                logging.info(indent + "  averages ...")
                # times/locations with pixels found:
                kk, ii = numpy.where(outf.ds["npixel"].values > 0)
                # any?
                if len(ii) > 0:
                    # loop over target variables:
                    for vkey in vkeys:
                        # convert sum to average:
                        outf.ds[vkey].values[kk, ii] /= outf.ds["npixel"].values[kk, ii]
                    # endfor # vkey
                # endif # any pixels co-located

                # info ...
                logging.info(indent + "  write ...")
                # write:
                outf.Write(
                    outfile,
                    history=history,
                    attrs=gattrs,
                    unlimited_dims=["time"],
                    packed=packed,
                    complevel=complevel,
                )

            else:
                # info ...
                logging.info(indent + "keep  %s .." % outfile)
            # endif # renew

        # endfor # outtimes

        # endfor # time loop

        # info ...
        logging.info(indent + "")
        logging.info(indent + "** end")
        logging.info(indent + "")

    # enddef __init__


# endclass CSO_CoLocate


########################################################################
###
### CoLocatePlotTimeseries
###
########################################################################


class CSO_CoLocatePlotTimeSeries(cso_catalogue.CSO_CatalogueBase):

    """
    Plot timeseries of averages over co-located pixels,
    as collected by the :py:class:`CSO_CoLocate` class.

    The following is an example of a timeseries plot that shows averages
    over co-located pixels with 100 km of a site for at daily and hourly resolution:

    .. figure:: figs/HCHO/timeseries_001.png
       :scale: 70 %
       :align: center
       :alt: Timeseries of averages over pixels collected within 100 km of site.

    In the settings, specify the range of the time series::

        ! time range:
        <rcbase>.timerange.start        :  ${my.timerange.start}
        <rcbase>.timerange.end          :  ${my.timerange.end}

    Input files are created by the :py:class:`CSO_CoLocate` class; specify a file name pattern
    for the file(s)::

        ! filter for input files:
        <rcbase>.input.files            :  /scratch/colocated/timeseries_*_daily.nc

    For each location a single figure will be created, with time series at one or more resolutions
    (daily, monthly), and one or more variables (observations, simulations).
    The filename should include a pattern to substitute at least one location property,
    for example a station code; if no suitable properties are available, the location index (1-based)
    could be used::

        ! target figure;
        ! see 'str.format' for substitution of station codes etc,
        ! use 'location' for 1-based index:
        !    timeseries_{code}.png
        !    timeseries_{location:0>3}.png
        <rcbase>.fig.file            :  /scratch/colocated/figs/timeseries_{location:0>3}.png

    By default existing files are created again::

        ! renew existing files?
        <rcbase>.renew                  :  True

    Define the figure size with::

        ! figure size:
        <rcbase>.fig.size            :  (8,4)

    The title could include substitutions for location properties::

        ! title, include location tempates:
        <rcbase>.fig.title           :  {name} ({longitude:7.2f},{latitude:6.2f})

    Also define the label for the y-axis, as well as the value range::

        ! label, use '{units}' template
        <rcbase>.fig.ylabel          :  Glyoxal tropospheric column [{units}]
        <rcbase>.fig.ylim            :  [None,None]

    The plot could include multiple temporal resolutions.
    The :py:meth:``xarray.DataArray.resample`` method is used to create for example
    monthly average from daily time series.
    Specify a list of resolutions to be resampled, or use ``N`` for the native resolution::

        ! time resamplings:
        !   D=daily, M=monthly
        !   or "N" for native
        <rcbase>.resolutions         :  N M

    Specify a list of keywords for the variables to be plotted;
    for each variable, specify target units and line style (including label for legend) per temporal resolution::

        ! variables to be plotted:
        <rcbase>.vars                :  obs sim
        ! style for observations:
        <rcbase>.var.obs.units        :  umol/m2
        <rcbase>.var.obs.N.style      :  color='0.7', marker='.', linestyle='None'
        <rcbase>.var.obs.M.style      :  color='black', marker='o', linestyle='-', label='observations'
        ! style:
        <rcbase>.var.sim.units        :  umol/m2
        <rcbase>.var.sim.N.style      :  color='orange', alpha=0.5, marker='.', linestyle='None'
        <rcbase>.var.sim.M.style      :  color='orange', marker='o', linestyle='-', label='simulations'

    """

    def __init__(self, rcfile, rcbase="", env={}, indent=""):

        """
        Create catalogue of CSO figures.
        """

        # modules:
        import os
        import glob
        import xarray
        import pandas
        import warnings
        import datetime
        import matplotlib.pyplot as plt
        import matplotlib.dates as mdates

        # init base object:
        cso_catalogue.CSO_CatalogueBase.__init__(self, rcfile, rcbase=rcbase, env=env)

        # info ...
        logging.info(indent + "")
        logging.info(indent + "** Plot Co-located time series")
        logging.info(indent + "")

        # time range:
        t1 = self.GetSetting("timerange.start", totype="datetime")
        t2 = self.GetSetting("timerange.end", totype="datetime")

        # info ...
        tfmt = "%Y-%m-%d %H:%M"
        logging.info(indent + "timerange: [%s,%s]" % (t1.strftime(tfmt), t2.strftime(tfmt)))

        # renew?
        renew = self.GetSetting("renew", totype="bool")
        # info ...
        logging.info(indent + "renew output: %s" % renew)

        # filter for input files:
        fpattern = self.GetSetting("input.files")
        # list:
        fnames = glob.glob(fpattern)
        fnames.sort()
        # check ..
        if len(fnames) == 0:
            logging.error("no files matching: %s" % fpattern)
            raise Exception
        # endif

        # read files, use nested mode to no have a "time" dimension added to "location_name('location')" variables:
        ds = xarray.open_mfdataset(fnames, combine="nested")

        # count:
        nloc = ds.dims["location"]
        # collect location info dicts:
        #    loci = [ dict(name='AAA',lon=12.3,..), .. ]
        loci = [{"location": iloc + 1} for iloc in range(nloc)]
        # loop over all variables:
        for key in ds.keys():
            # location info?
            if key.startswith("location_"):
                # header name:
                name = key.replace("location_", "")
                # store:
                for iloc in range(nloc):
                    loci[iloc][name] = ds[key].values[iloc]
                # endfor
            # endif
        # endfor

        # resampling resolutions:
        resolutions = self.GetSetting("resolutions").split()

        # variabels:
        varnames = self.GetSetting("vars").split()

        # storage for variable info:
        vari = {}
        # loop over variables:
        for varname in varnames:
            # check ...
            if varname not in ds:
                logging.error('no variable "%s" in files: %s' % (varname, fpattern))
            # endif
            # settings:
            vari[varname] = {}
            vari[varname]["units"] = self.GetSetting("var.%s.units" % varname)
        # endfor

        # template for output file:
        figfile_template = self.GetSetting("fig.file")

        # style:
        figsize = eval(self.GetSetting("fig.size"))
        ylabel_template = self.GetSetting("fig.ylabel")
        ylim = eval(self.GetSetting("fig.ylim"))
        title_template = self.GetSetting("fig.title")
        styles = {}
        for varname in varnames:
            styles[varname] = {}
            for resolution in resolutions:
                styles[varname][resolution] = self.GetSetting(
                    "var.%s.%s.style" % (varname, resolution), totype="dict"
                )
            # endfor
        # endfor

        # info ...
        logging.info(indent + "resample ...")
        # resample (all locations):
        #   das['N']['yr'   ] = ..
        #   das['N']['times'] = ..
        das = {}
        das["times"] = {}
        # first variable, used to extract time axis:
        varname0 = varnames[0]
        # loop over time resolutions:
        for resolution in resolutions:
            # info ..
            logging.info('    resolution "%s" ..' % resolution)
            # storage:
            das[resolution] = {}
            # native or resample?
            if resolution == "N":
                # loop over variables:
                for varname in varnames:
                    # native data:
                    das[resolution][varname] = ds[varname]
                # endfor # varname
                # extract time values:
                das[resolution]["times"] = das[resolution][varname0].coords["time"].values
            #
            elif resolution == "M":
                # loop over variables:
                for varname in varnames:
                    # use start of month as timestamp ...
                    das[resolution][varname] = (
                        ds[varname]
                        .resample(time="MS", skipna=True)
                        .mean(skipna=True, keep_attrs=True)
                    )
                # endfor # varname
                # mid of month:
                das[resolution]["times"] = das[resolution][varname0].coords[
                    "time"
                ].values + pandas.Timedelta(days=15)
            #
            else:
                # loop over variables:
                for varname in varnames:
                    # resample variable:
                    das[resolution][varname] = (
                        ds[varname]
                        .resample(time=resolution, skipna=True)
                        .mean(skipna=True, keep_attrs=True)
                    )
                # endif
                # extract time values:
                das[resolution]["times"] = das[resolution][varname0].coords["time"].values
            # endif # native, monthly, ..
        # endfor # time resols

        # loop over locations:
        for iloc in range(ds.dims["location"]):
            # info ..
            logging.info("location %i / %i .." % (iloc + 1, ds.dims["location"]))

            # target file, replace templates:
            figfile = figfile_template.format(**loci[iloc])
            # renew?
            if (not os.path.isfile(figfile)) or renew:
                # info ..
                logging.info("  create %s .." % figfile)

                # new:
                fig = plt.figure(figsize=figsize)
                ax = fig.add_axes([0.10, 0.07, 0.87, 0.85])

                # loop over variables:
                for varname in varnames:
                    # loop over time resolutions:
                    for resolution in resolutions:
                        # trap warnings on division by zero in case no data is present at all:
                        with warnings.catch_warnings():
                            # no warnings:
                            warnings.simplefilter("ignore")
                            # extract:
                            values_in = das[resolution][varname].values[:, iloc]
                        # endwith
                        # values, convert if necessary:
                        values, units = self._ConvertUnits(
                            values_in, das[resolution][varname].attrs, vari[varname]["units"]
                        )
                        # add:
                        ax.plot_date(
                            das[resolution]["times"], values, **styles[varname][resolution]
                        )
                    # endfor # time resols
                # endfor # variables

                # annote:
                ax.set_title(title_template.format(**loci[iloc]))
                # lables:
                ax.legend()
                # time axis:
                ax.plot([t1, t2], [0, 0], color="black", linestyle="-", marker="None")
                ax.set_xlim([t1, t2])
                ax.xaxis.set_minor_locator(mdates.MonthLocator())
                ax.xaxis.set_major_locator(mdates.YearLocator())
                ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y"))
                # value axis:
                ax.set_ylim(ylim)
                ax.set_ylabel(ylabel_template.format(units=units))

                # target dir:
                figdir = os.path.dirname(figfile)
                if len(figdir) > 0:
                    if not os.path.isdir(figdir):
                        os.makedirs(figdir)
                # endif
                # save:
                fig.savefig(figfile)
                # clear:
                plt.close(fig)

            else:
                # info ...
                logging.info("  keep   %s .." % figfile)
            # endif

        # endfor # locations

        # info ...
        logging.info(indent + "")
        logging.info(indent + "** end")
        logging.info(indent + "")

    # enddef __init__


# endclass CSO_CoLocatePlotTimeSeries


########################################################################
###
### end
###
########################################################################
