source: flex_extract.git/python/plot_retrieved.py @ ff99eae

ctbtodev
Last change on this file since ff99eae was ff99eae, checked in by Anne Philipp <anne.philipp@…>, 6 years ago

completed application of pep8 style guide and pylint investigations. added documentation almost everywhere

  • Property mode set to 100755
File size: 23.1 KB
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#************************************************************************
4# ToDo AP
5# - documentation der Funktionen
6# - docu der progam functionality
7# - apply pep8
8#************************************************************************
9#*******************************************************************************
10# @Author: Leopold Haimberger (University of Vienna)
11#
12# @Date: November 2015
13#
14# @Change History:
15#
16#    February 2018 - Anne Philipp (University of Vienna):
17#        - applied PEP8 style guide
18#        - added documentation
19#        - created function main and moved the two function calls for
20#          arguments and plotting into it
21#        - added function get_basics to extract the boundary conditions
22#          of the data fields from the first grib file it gets.
23#
24# @License:
25#    (C) Copyright 2015-2018.
26#
27#    This software is licensed under the terms of the Apache Licence Version 2.0
28#    which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
29#
30# @Program Functionality:
31#    Simple tool for creating maps and time series of retrieved fields.
32#
33# @Program Content:
34#    - main
35#    - get_basics
36#    - plot_retrieved
37#    - plot_timeseries
38#    - plot_map
39#    - get_plot_args
40#
41#*******************************************************************************
42
43# ------------------------------------------------------------------------------
44# MODULES
45# ------------------------------------------------------------------------------
46import time
47import datetime
48import os
49import inspect
50import sys
51from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
52import matplotlib
53import matplotlib.pyplot as plt
54from mpl_toolkits.basemap import Basemap
55from eccodes import codes_grib_new_from_file, codes_get, codes_release, \
56                    codes_get_values
57import numpy as np
58
59# software specific classes and modules from flex_extract
60from ControlFile import ControlFile
61from UioFiles import UioFiles
62
63# add path to pythonpath so that python finds its buddies
64LOCAL_PYTHON_PATH = os.path.dirname(os.path.abspath(
65    inspect.getfile(inspect.currentframe())))
66if LOCAL_PYTHON_PATH not in sys.path:
67    sys.path.append(LOCAL_PYTHON_PATH)
68
69font = {'family': 'monospace', 'size': 12}
70matplotlib.rcParams['xtick.major.pad'] = '20'
71matplotlib.rc('font', **font)
72# ------------------------------------------------------------------------------
73# FUNCTION
74# ------------------------------------------------------------------------------
75def main():
76    '''
77    @Description:
78        If plot_retrieved is called from command line, this function controls
79        the program flow and calls the argumentparser function and
80        the plot_retrieved function for plotting the retrieved GRIB data.
81
82    @Input:
83        <nothing>
84
85    @Return:
86        <nothing>
87    '''
88    args, c = get_plot_args()
89    plot_retrieved(c)
90
91    return
92
93def get_basics(ifile, verb=False):
94    """
95    @Description:
96        An example grib file will be opened and basic information will
97        be extracted. These information are important for later use and the
98        initialization of numpy arrays for data storing.
99
100    @Input:
101        ifile: string
102            Contains the full absolute path to the ECMWF grib file.
103
104        verb (opt): bool
105            Is True if there should be extra output in verbose mode.
106            Default value is False.
107
108    @Return:
109        data: dict
110            Contains basic informations of the ECMWF grib files, e.g.
111            'Ni', 'Nj', 'latitudeOfFirstGridPointInDegrees',
112            'longitudeOfFirstGridPointInDegrees',
113            'latitudeOfLastGridPointInDegrees',
114            'longitudeOfLastGridPointInDegrees',
115            'jDirectionIncrementInDegrees',
116            'iDirectionIncrementInDegrees'
117    """
118
119    data = {}
120
121    # --- open file ---
122    print("Opening file for getting information data --- %s" %
123          os.path.basename(ifile))
124
125    with open(ifile) as f:
126
127        # load first message from file
128        gid = codes_grib_new_from_file(f)
129
130        # information needed from grib message
131        keys = ['Ni',
132                'Nj',
133                'latitudeOfFirstGridPointInDegrees',
134                'longitudeOfFirstGridPointInDegrees',
135                'latitudeOfLastGridPointInDegrees',
136                'longitudeOfLastGridPointInDegrees',
137                'jDirectionIncrementInDegrees',
138                'iDirectionIncrementInDegrees']
139
140        if verb:
141            print '\nInformations are: '
142        for key in keys:
143            # Get the value of the key in a grib message.
144            data[key] = codes_get(gid, key)
145            if verb:
146                print "%s = %s" % (key, data[key])
147        if verb:
148            print '\n'
149
150        # Free the memory for the message referred as gribid.
151        codes_release(gid)
152
153    return data
154
155def get_files_per_date(files, datelist):
156    '''
157    @Description:
158        The filenames contain dates which are used to select a list
159        of files for a specific time period specified in datelist.
160
161    @Input:
162        files: instance of UioFiles
163            For description see class documentation.
164            It contains the attribute "files" which is a list of pathes
165            to filenames.
166
167        datelist: list of datetimes
168            Contains the list of dates which should be processed for plotting.
169
170    @Return:
171        filelist: list of strings
172            Contains the selected files for the time period.
173    '''
174
175    filelist = []
176    for filename in files:
177        filedate = filename[-8:]
178        ddate = datetime.datetime.strptime(filedate, '%y%m%d%H')
179        if ddate in datelist:
180            filelist.append(filename)
181
182    return filelist
183
184def plot_retrieved(c):
185    '''
186    @Description:
187        Reads GRIB data from a specified time period, a list of levels
188        and a specified list of parameter.
189
190    @Input:
191        c: instance of class ControlFile
192            Contains all necessary information of a CONTROL file. The parameters
193            are: DAY1, DAY2, DTIME, MAXSTEP, TYPE, TIME, STEP, CLASS, STREAM,
194            NUMBER, EXPVER, GRID, LEFT, LOWER, UPPER, RIGHT, LEVEL, LEVELIST,
195            RESOL, GAUSS, ACCURACY, OMEGA, OMEGADIFF, ETA, ETADIFF, DPDETA,
196            SMOOTH, FORMAT, ADDPAR, WRF, CWC, PREFIX, ECSTORAGE, ECTRANS,
197            ECFSDIR, MAILOPS, MAILFAIL, GRIB2FLEXPART, DEBUG, INPUTDIR,
198            OUTPUTDIR, FLEXPART_ROOT_SCRIPTS
199            For more information about format and content of the parameter see
200            documentation.
201
202    @Return:
203        <nothing>
204    '''
205    start = datetime.datetime.strptime(c.start_date, '%Y%m%d%H')
206    end = datetime.datetime.strptime(c.end_date, '%Y%m%d%H')
207
208    # create datelist between start and end date
209    datelist = [start] # initialise datelist with first date
210    run_date = start
211    while run_date < end:
212        run_date += datetime.timedelta(hours=int(c.dtime))
213        datelist.append(run_date)
214
215    print 'datelist: ', datelist
216
217    c.paramIds = np.asarray(c.paramIds, dtype='int')
218    c.levels = np.asarray(c.levels, dtype='int')
219    c.area = np.asarray(c.area)
220
221    files = UioFiles(c.prefix+'*')
222    files.list_files(c.inputdir)
223    ifiles = get_files_per_date(files.files, datelist)
224    ifiles.sort()
225
226    gdict = get_basics(ifiles[0], verb=False)
227
228    fdict = dict()
229    fmeta = dict()
230    fstamp = dict()
231    for p in c.paramIds:
232        for l in c.levels:
233            key = '{:0>3}_{:0>3}'.format(p, l)
234            fdict[key] = []
235            fmeta[key] = []
236            fstamp[key] = []
237
238    for filename in ifiles:
239        f = open(filename)
240        print "Opening file for reading data --- %s" % filename
241        fdate = datetime.datetime.strptime(filename[-8:], "%y%m%d%H")
242
243        # Load in memory a grib message from a file.
244        gid = codes_grib_new_from_file(f)
245        while gid is not None:
246            gtype = codes_get(gid, 'type')
247            paramId = codes_get(gid, 'paramId')
248            parameterName = codes_get(gid, 'parameterName')
249            level = codes_get(gid, 'level')
250
251            if paramId in c.paramIds and level in c.levels:
252                key = '{:0>3}_{:0>3}'.format(paramId, level)
253                print 'key: ', key
254                if fstamp[key]:
255                    for i in range(len(fstamp[key])):
256                        if fdate < fstamp[key][i]:
257                            fstamp[key].insert(i, fdate)
258                            fmeta[key].insert(i, [paramId, parameterName, gtype,
259                                                  fdate, level])
260                            fdict[key].insert(i, np.flipud(np.reshape(
261                                codes_get_values(gid),
262                                [gdict['Nj'], gdict['Ni']])))
263                            break
264                        elif fdate > fstamp[key][i] and i == len(fstamp[key])-1:
265                            fstamp[key].append(fdate)
266                            fmeta[key].append([paramId, parameterName, gtype,
267                                               fdate, level])
268                            fdict[key].append(np.flipud(np.reshape(
269                                codes_get_values(gid),
270                                [gdict['Nj'], gdict['Ni']])))
271                            break
272                        elif fdate > fstamp[key][i] and i != len(fstamp[key])-1 \
273                             and fdate < fstamp[key][i+1]:
274                            fstamp[key].insert(i, fdate)
275                            fmeta[key].insert(i, [paramId, parameterName, gtype,
276                                                  fdate, level])
277                            fdict[key].insert(i, np.flipud(np.reshape(
278                                codes_get_values(gid),
279                                [gdict['Nj'], gdict['Ni']])))
280                            break
281                        else:
282                            pass
283                else:
284                    fstamp[key].append(fdate)
285                    fmeta[key].append((paramId, parameterName, gtype,
286                                       fdate, level))
287                    fdict[key].append(np.flipud(np.reshape(
288                        codes_get_values(gid), [gdict['Nj'], gdict['Ni']])))
289
290            codes_release(gid)
291
292            # Load in memory a grib message from a file.
293            gid = codes_grib_new_from_file(f)
294
295        f.close()
296
297    for k in fdict.iterkeys():
298        print 'fmeta: ', len(fmeta), fmeta
299        fml = fmeta[k]
300        fdl = fdict[k]
301        print 'fm1: ', len(fml), fml
302        for fd, fm in zip(fdl, fml):
303            print fm
304            ftitle = fm[1] + ' {} '.format(fm[-1]) + \
305                datetime.datetime.strftime(fm[3], '%Y%m%d%H')
306            pname = '_'.join(fm[1].split()) + '_{}_'.format(fm[-1]) + \
307                datetime.datetime.strftime(fm[3], '%Y%m%d%H')
308            plot_map(c, fd, fm, gdict, ftitle, pname, 'png')
309
310    for k in fdict.iterkeys():
311        fml = fmeta[k]
312        fdl = fdict[k]
313        fsl = fstamp[k]
314        if fdl:
315            fm = fml[0]
316            fd = fdl[0]
317            ftitle = fm[1] + ' {} '.format(fm[-1]) + \
318                datetime.datetime.strftime(fm[3], '%Y%m%d%H')
319            pname = '_'.join(fm[1].split()) + '_{}_'.format(fm[-1]) + \
320                datetime.datetime.strftime(fm[3], '%Y%m%d%H')
321            lat = -20.
322            lon = 20.
323            plot_timeseries(c, fdl, fml, fsl, lat, lon, gdict,
324                            ftitle, pname, 'png')
325
326    return
327
328def plot_timeseries(c, flist, fmetalist, ftimestamps, lat, lon,
329                    gdict, ftitle, filename, fending, show=False):
330    '''
331    @Description:
332        Creates a timeseries plot for a given lat/lon position.
333
334    @Input:
335        c: instance of class ControlFile
336            Contains all necessary information of a CONTROL file. The parameters
337            are: DAY1, DAY2, DTIME, MAXSTEP, TYPE, TIME, STEP, CLASS, STREAM,
338            NUMBER, EXPVER, GRID, LEFT, LOWER, UPPER, RIGHT, LEVEL, LEVELIST,
339            RESOL, GAUSS, ACCURACY, OMEGA, OMEGADIFF, ETA, ETADIFF, DPDETA,
340            SMOOTH, FORMAT, ADDPAR, WRF, CWC, PREFIX, ECSTORAGE, ECTRANS,
341            ECFSDIR, MAILOPS, MAILFAIL, GRIB2FLEXPART, DEBUG, INPUTDIR,
342            OUTPUTDIR, FLEXPART_ROOT_SCRIPTS
343            For more information about format and content of the parameter see
344            documentation.
345
346        flist: numpy array, 2d
347            The actual data values to be plotted from the grib messages.
348
349        fmetalist: list of strings
350            Contains some meta date for the data field to be plotted:
351            parameter id, parameter Name, grid type, datetime, level
352
353        ftimestamps: list of datetime
354            Contains the time stamps.
355
356        lat: float
357            The latitude for which the timeseries should be plotted.
358
359        lon: float
360            The longitude for which the timeseries should be plotted.
361
362        gdict: dict
363            Contains basic informations of the ECMWF grib files, e.g.
364            'Ni', 'Nj', 'latitudeOfFirstGridPointInDegrees',
365            'longitudeOfFirstGridPointInDegrees',
366            'latitudeOfLastGridPointInDegrees',
367            'longitudeOfLastGridPointInDegrees',
368            'jDirectionIncrementInDegrees',
369            'iDirectionIncrementInDegrees'
370
371        ftitle: string
372            The title of the timeseries.
373
374        filename: string
375            The time series is stored in a file with this name.
376
377        fending: string
378            Contains the type of plot, e.g. pdf or png
379
380        show: boolean
381            Decides if the plot is shown after plotting or hidden.
382
383    @Return:
384        <nothing>
385    '''
386    print 'plotting timeseries'
387
388    t1 = time.time()
389
390    #llx = gdict['longitudeOfFirstGridPointInDegrees']
391    #if llx > 180. :
392    #    llx -= 360.
393    #lly = gdict['latitudeOfLastGridPointInDegrees']
394    #dxout = gdict['iDirectionIncrementInDegrees']
395    #dyout = gdict['jDirectionIncrementInDegrees']
396    #urx = gdict['longitudeOfLastGridPointInDegrees']
397    #ury = gdict['latitudeOfFirstGridPointInDegrees']
398    #numxgrid = gdict['Ni']
399    #numygrid = gdict['Nj']
400
401    farr = np.asarray(flist)
402    #(time, lat, lon)
403
404    #lonindex = linspace(llx, urx, numxgrid)
405    #latindex = linspace(lly, ury, numygrid)
406
407    ts = farr[:, 0, 0]
408
409    fig = plt.figure(figsize=(12, 6.7))
410
411    plt.plot(ftimestamps, ts)
412    plt.title(ftitle)
413
414    plt.savefig(c.outputdir + '/' + filename + '_TS.' + fending,
415                facecolor=fig.get_facecolor(),
416                edgecolor='none',
417                format=fending)
418    print 'created ', c.outputdir + '/' + filename
419    if show:
420        plt.show()
421    fig.clf()
422    plt.close(fig)
423
424    print time.time() - t1, 's'
425
426    return
427
428def plot_map(c, flist, fmetalist, gdict, ftitle, filename, fending, show=False):
429    '''
430    @Description:
431        Creates a basemap plot with imshow for a given data field.
432
433    @Input:
434        c: instance of class ControlFile
435            Contains all necessary information of a CONTROL file. The parameters
436            are: DAY1, DAY2, DTIME, MAXSTEP, TYPE, TIME, STEP, CLASS, STREAM,
437            NUMBER, EXPVER, GRID, LEFT, LOWER, UPPER, RIGHT, LEVEL, LEVELIST,
438            RESOL, GAUSS, ACCURACY, OMEGA, OMEGADIFF, ETA, ETADIFF, DPDETA,
439            SMOOTH, FORMAT, ADDPAR, WRF, CWC, PREFIX, ECSTORAGE, ECTRANS,
440            ECFSDIR, MAILOPS, MAILFAIL, GRIB2FLEXPART, DEBUG, INPUTDIR,
441            OUTPUTDIR, FLEXPART_ROOT_SCRIPTS
442            For more information about format and content of the parameter see
443            documentation.
444
445        flist: numpy array, 2d
446            The actual data values to be plotted from the grib messages.
447
448        fmetalist: list of strings
449            Contains some meta date for the data field to be plotted:
450            parameter id, parameter Name, grid type, datetime, level
451
452        gdict: dict
453            Contains basic informations of the ECMWF grib files, e.g.
454            'Ni', 'Nj', 'latitudeOfFirstGridPointInDegrees',
455            'longitudeOfFirstGridPointInDegrees',
456            'latitudeOfLastGridPointInDegrees',
457            'longitudeOfLastGridPointInDegrees',
458            'jDirectionIncrementInDegrees',
459            'iDirectionIncrementInDegrees'
460
461        ftitle: string
462            The titel of the plot.
463
464        filename: string
465            The plot is stored in a file with this name.
466
467        fending: string
468            Contains the type of plot, e.g. pdf or png
469
470        show: boolean
471            Decides if the plot is shown after plotting or hidden.
472
473    @Return:
474        <nothing>
475    '''
476    print 'plotting map'
477
478    t1 = time.time()
479
480    fig = plt.figure(figsize=(12, 6.7))
481    #mbaxes = fig.add_axes([0.05, 0.15, 0.8, 0.7])
482
483    llx = gdict['longitudeOfFirstGridPointInDegrees'] #- 360
484    if llx > 180.:
485        llx -= 360.
486    lly = gdict['latitudeOfLastGridPointInDegrees']
487    #dxout = gdict['iDirectionIncrementInDegrees']
488    #dyout = gdict['jDirectionIncrementInDegrees']
489    urx = gdict['longitudeOfLastGridPointInDegrees']
490    ury = gdict['latitudeOfFirstGridPointInDegrees']
491    #numxgrid = gdict['Ni']
492    #numygrid = gdict['Nj']
493
494    m = Basemap(projection='cyl', llcrnrlon=llx, llcrnrlat=lly,
495                urcrnrlon=urx, urcrnrlat=ury, resolution='i')
496
497    #lw = 0.5
498    m.drawmapboundary()
499    #x = linspace(llx, urx, numxgrid)
500    #y = linspace(lly, ury, numygrid)
501
502    #xx, yy = m(*meshgrid(x, y))
503
504    #s = m.contourf(xx, yy, flist)
505
506    s = plt.imshow(flist.T,
507                   extent=(llx, urx, lly, ury),
508                   alpha=1.0,
509                   interpolation='nearest'
510                   #vmin=vn,
511                   #vmax=vx,
512                   #cmap=my_cmap,
513                   #levels=levels,
514                   #cmap=my_cmap,
515                   #norm=LogNorm(vn,vx)
516                  )
517
518    plt.title(ftitle, y=1.08)
519    cb = m.colorbar(s, location="right", pad="10%")
520    cb.set_label('label', size=14)
521
522    thickline = np.arange(lly, ury+1, 10.)
523    thinline = np.arange(lly, ury+1, 5.)
524    m.drawparallels(thickline,
525                    color='gray',
526                    dashes=[1, 1],
527                    linewidth=0.5,
528                    labels=[1, 1, 1, 1],
529                    xoffset=1.)
530    m.drawparallels(np.setdiff1d(thinline, thickline),
531                    color='lightgray',
532                    dashes=[1, 1],
533                    linewidth=0.5,
534                    labels=[0, 0, 0, 0])
535
536    thickline = np.arange(llx, urx+1, 10.)
537    thinline = np.arange(llx, urx+1, 5.)
538    m.drawmeridians(thickline,
539                    color='gray',
540                    dashes=[1, 1],
541                    linewidth=0.5,
542                    labels=[1, 1, 1, 1],
543                    yoffset=1.)
544    m.drawmeridians(np.setdiff1d(thinline, thickline),
545                    color='lightgray',
546                    dashes=[1, 1],
547                    linewidth=0.5,
548                    labels=[0, 0, 0, 0])
549
550    m.drawcoastlines()
551    m.drawcountries()
552
553    plt.savefig(c.outputdir + '/' + filename + '_MAP.' + fending,
554                facecolor=fig.get_facecolor(),
555                edgecolor='none',
556                format=fending)
557    print 'created ', c.outputdir + '/' + filename
558    if show:
559        plt.show()
560    fig.clf()
561    plt.close(fig)
562
563    print time.time() - t1, 's'
564
565    return
566
567def get_plot_args():
568    '''
569    @Description:
570        Assigns the command line arguments and reads CONTROL file
571        content. Apply default values for non mentioned arguments.
572
573    @Input:
574        <nothing>
575
576    @Return:
577        args: instance of ArgumentParser
578            Contains the commandline arguments from script/program call.
579
580        c: instance of class ControlFile
581            Contains all necessary information of a CONTROL file. The parameters
582            are: DAY1, DAY2, DTIME, MAXSTEP, TYPE, TIME, STEP, CLASS, STREAM,
583            NUMBER, EXPVER, GRID, LEFT, LOWER, UPPER, RIGHT, LEVEL, LEVELIST,
584            RESOL, GAUSS, ACCURACY, OMEGA, OMEGADIFF, ETA, ETADIFF, DPDETA,
585            SMOOTH, FORMAT, ADDPAR, WRF, CWC, PREFIX, ECSTORAGE, ECTRANS,
586            ECFSDIR, MAILOPS, MAILFAIL, GRIB2FLEXPART, DEBUG, INPUTDIR,
587            OUTPUTDIR, FLEXPART_ROOT_SCRIPTS
588            For more information about format and content of the parameter see
589            documentation.
590    '''
591    parser = ArgumentParser(description='Plot retrieved GRIB data from ' + \
592                            'ECMWF MARS archive',
593                            formatter_class=ArgumentDefaultsHelpFormatter)
594
595# the most important arguments
596    parser.add_argument("--start_date", dest="start_date",
597                        help="start date YYYYMMDD")
598    parser.add_argument("--end_date", dest="end_date",
599                        help="end_date YYYYMMDD")
600
601    parser.add_argument("--start_step", dest="start_step",
602                        help="start step in hours")
603    parser.add_argument("--end_step", dest="end_step",
604                        help="end step in hours")
605
606# some arguments that override the default in the CONTROL file
607    parser.add_argument("--levelist", dest="levelist",
608                        help="vertical levels to be retrieved, e.g. 30/to/60")
609    parser.add_argument("--area", dest="area",
610                        help="area defined as north/west/south/east")
611    parser.add_argument("--paramIds", dest="paramIds",
612                        help="parameter IDs")
613    parser.add_argument("--prefix", dest="prefix", default='EN',
614                        help="output file name prefix")
615
616# set the working directories
617    parser.add_argument("--inputdir", dest="inputdir", default=None,
618                        help="root directory for storing intermediate files")
619    parser.add_argument("--outputdir", dest="outputdir", default=None,
620                        help="root directory for storing output files")
621    parser.add_argument("--flexpart_root_scripts", dest="flexpart_root_scripts",
622                        help="FLEXPART root directory (to find \
623                        'grib2flexpart and COMMAND file)\n \
624                        Normally ECMWFDATA resides in the scripts directory \
625                        of the FLEXPART distribution")
626
627    parser.add_argument("--controlfile", dest="controlfile",
628                        default='CONTROL.temp',
629                        help="file with CONTROL parameters")
630    args = parser.parse_args()
631
632    try:
633        c = ControlFile(args.controlfile)
634    except IOError:
635        try:
636            c = ControlFile(LOCAL_PYTHON_PATH + args.controlfile)
637        except IOError:
638            print 'Could not read CONTROL file "' + args.controlfile + '"'
639            print 'Either it does not exist or its syntax is wrong.'
640            print 'Try "' + sys.argv[0].split('/')[-1] + \
641                  ' -h" to print usage information'
642            exit(1)
643
644    if args.levelist:
645        c.levels = args.levelist.split('/')
646    else:
647        c.levels = [0]
648
649    if args.area:
650        c.area = args.area.split('/')
651    else:
652        c.area = '[0,0]'
653
654    c.paramIds = args.paramIds.split('/')
655
656    if args.start_step:
657        c.start_step = int(args.start_step)
658    else:
659        c.start_step = 0
660
661    if args.end_step:
662        c.end_step = int(args.end_step)
663    else:
664        c.end_step = 0
665
666    c.start_date = args.start_date
667    c.end_date = args.end_date
668
669    c.prefix = args.prefix
670
671    c.inputdir = args.inputdir
672
673    if args.outputdir:
674        c.outputdir = args.outputdir
675    else:
676        c.outputdir = c.inputdir
677
678    return args, c
679
680if __name__ == "__main__":
681    main()
Note: See TracBrowser for help on using the repository browser.
hosted by ZAMG