1"""
2@package animation.temporal_manager
3
4@brief Management of temporal datasets used in animation
5
6Classes:
7 - temporal_manager::DataMode
8 - temporal_manager::GranularityMode
9 - temporal_manager::TemporalManager
10
11
12(C) 2012 by the GRASS Development Team
13
14This program is free software under the GNU General Public License
15(>=v2). Read the file COPYING that comes with GRASS for details.
16
17@author Anna Kratochvilova <kratochanna gmail.com>
18"""
19
20from __future__ import print_function
21
22import os
23import datetime
24
25import grass.script as grass
26import grass.temporal as tgis
27from core.gcmd import GException
28from core.settings import UserSettings
29from animation.utils import validateTimeseriesName, TemporalType
30
31
32class DataMode:
33    SIMPLE = 1
34    MULTIPLE = 2
35
36
37class GranularityMode:
38    ONE_UNIT = 1
39    ORIGINAL = 2
40
41
42class TemporalManager(object):
43    """Class for temporal data processing."""
44
45    def __init__(self):
46        self.timeseriesList = []
47        self.timeseriesInfo = {}
48
49        self.dataMode = None
50        self.temporalType = None
51
52        self.granularityMode = GranularityMode.ORIGINAL
53
54    def GetTemporalType(self):
55        """Get temporal type (TemporalType.ABSOLUTE,
56        TemporalType.RELATIVE)
57        """
58        return self._temporalType
59
60    def SetTemporalType(self, ttype):
61        self._temporalType = ttype
62
63    temporalType = property(fget=GetTemporalType, fset=SetTemporalType)
64
65    def AddTimeSeries(self, timeseries, etype):
66        """Add space time dataset
67        and collect basic information about it.
68
69        Raises GException (e.g. with invalid topology).
70
71        :param timeseries: name of timeseries (with or without mapset)
72        :param etype: element type (strds, stvds)
73        """
74        self._gatherInformation(
75            timeseries,
76            etype,
77            self.timeseriesList,
78            self.timeseriesInfo)
79
80    def EvaluateInputData(self):
81        """Checks if all timeseries are compatible (raises GException).
82
83        Sets internal variables.
84        """
85        timeseriesCount = len(self.timeseriesList)
86
87        if timeseriesCount == 1:
88            self.dataMode = DataMode.SIMPLE
89        elif timeseriesCount > 1:
90            self.dataMode = DataMode.MULTIPLE
91        else:
92            self.dataMode = None
93
94        ret, message = self._setTemporalState()
95        if not ret:
96            raise GException(message)
97        if message:  # warning
98            return message
99
100        return None
101
102    def _setTemporalState(self):
103        # check for absolute x relative
104        absolute, relative = 0, 0
105        for infoDict in self.timeseriesInfo.values():
106            if infoDict['temporal_type'] == 'absolute':
107                absolute += 1
108            else:
109                relative += 1
110        if bool(absolute) == bool(relative):
111            message = _("It is not allowed to display data with different "
112                        "temporal types (absolute and relative).")
113            return False, message
114        if absolute:
115            self.temporalType = TemporalType.ABSOLUTE
116        else:
117            self.temporalType = TemporalType.RELATIVE
118
119        # check for units for relative type
120        if relative:
121            units = set()
122            for infoDict in self.timeseriesInfo.values():
123                units.add(infoDict['unit'])
124            if len(units) > 1:
125                message = _(
126                    "It is not allowed to display data with different units (%s).") % ','.join(units)
127                return False, message
128
129        # check for interval x point
130        interval, point = 0, 0
131        for infoDict in self.timeseriesInfo.values():
132            if infoDict['map_time'] == 'interval':
133                interval += 1
134            else:
135                point += 1
136        if bool(interval) == bool(point):
137            message = _(
138                "You are going to display data with different "
139                "temporal types of maps (interval and point)."
140                " It is recommended to use data of one temporal type to avoid confusion.")
141            return True, message  # warning
142
143        return True, None
144
145    def GetGranularity(self):
146        """Returns temporal granularity of currently loaded timeseries.
147        """
148        if self.dataMode == DataMode.SIMPLE:
149            gran = self.timeseriesInfo[self.timeseriesList[0]]['granularity']
150            if 'unit' in self.timeseriesInfo[
151                    self.timeseriesList[0]]:  # relative:
152                granNum = gran
153                unit = self.timeseriesInfo[self.timeseriesList[0]]['unit']
154                if self.granularityMode == GranularityMode.ONE_UNIT:
155                    granNum = 1
156            else:  # absolute
157                granNum, unit = gran.split()
158                if self.granularityMode == GranularityMode.ONE_UNIT:
159                    granNum = 1
160
161            return (int(granNum), unit)
162
163        if self.dataMode == DataMode.MULTIPLE:
164            return self._getCommonGranularity()
165
166    def _getCommonGranularity(self):
167        allMaps = []
168        for dataset in self.timeseriesList:
169            maps = self.timeseriesInfo[dataset]['maps']
170            allMaps.extend(maps)
171
172        if self.temporalType == TemporalType.ABSOLUTE:
173            gran = tgis.compute_absolute_time_granularity(allMaps)
174            granNum, unit = gran.split()
175            if self.granularityMode == GranularityMode.ONE_UNIT:
176                granNum = 1
177            return int(granNum), unit
178        if self.temporalType == TemporalType.RELATIVE:
179            unit = self.timeseriesInfo[self.timeseriesList[0]]['unit']
180            granNum = tgis.compute_relative_time_granularity(allMaps)
181            if self.granularityMode == GranularityMode.ONE_UNIT:
182                granNum = 1
183            return (granNum, unit)
184
185    def GetLabelsAndMaps(self):
186        """Returns time labels and map names.
187        """
188        mapLists = []
189        labelLists = []
190        labelListSet = set()
191        for dataset in self.timeseriesList:
192            grassLabels, listOfMaps = self._getLabelsAndMaps(dataset)
193            mapLists.append(listOfMaps)
194            labelLists.append(tuple(grassLabels))
195            labelListSet.update(grassLabels)
196        # combine all timeLabels and fill missing maps with None
197        # BUT this does not work properly if the datasets have
198        # no temporal overlap! We would need to sample all datasets
199        # by a temporary dataset, I don't know how it would work with point
200        # data
201        if self.temporalType == TemporalType.ABSOLUTE:
202            timestamps = sorted(list(labelListSet), key=lambda x: x[0])
203        else:
204            timestamps = sorted(list(labelListSet), key=lambda x: x[0])
205
206        newMapLists = []
207        for mapList, labelList in zip(mapLists, labelLists):
208            newMapList = [None] * len(timestamps)
209            i = 0
210            # compare start time
211            while timestamps[i][0] != labelList[0][0]:  # compare
212                i += 1
213            newMapList[i:i + len(mapList)] = mapList
214            newMapLists.append(newMapList)
215
216        mapDict = {}
217        for i, dataset in enumerate(self.timeseriesList):
218            mapDict[dataset] = newMapLists[i]
219
220        if self.temporalType == TemporalType.ABSOLUTE:
221            # ('1996-01-01 00:00:00', '1997-01-01 00:00:00', 'year'),
222            formatString = UserSettings.Get(
223                group='animation', key='temporal', subkey='format')
224            timestamps = [
225                (datetime.datetime.strftime(
226                    st, formatString), datetime.datetime.strftime(
227                    end, formatString) if end is not None else None, unit) for (
228                    st, end, unit) in timestamps]
229        else:
230            # ('15', '16', u'years'),
231            timestamps = [(str(st), end if end is None else str(end), unit)
232                          for st, end, unit in timestamps]
233        return timestamps, mapDict
234
235    def _getLabelsAndMaps(self, timeseries):
236        """Returns time labels and map names (done by sampling)
237        for both interval and point data.
238        """
239        sp = tgis.dataset_factory(
240            self.timeseriesInfo[timeseries]['etype'], timeseries)
241        if sp.is_in_db() is False:
242            raise GException(
243                _("Space time dataset <%s> not found.") %
244                timeseries)
245        sp.select()
246
247        listOfMaps = []
248        timeLabels = []
249        granNum, unit = self.GetGranularity()
250        if self.temporalType == TemporalType.ABSOLUTE:
251            if self.granularityMode == GranularityMode.ONE_UNIT:
252                gran = '%(one)d %(unit)s' % {'one': 1, 'unit': unit}
253            else:
254                gran = '%(num)d %(unit)s' % {'num': granNum, 'unit': unit}
255
256        elif self.temporalType == TemporalType.RELATIVE:
257            unit = self.timeseriesInfo[timeseries]['unit']
258            if self.granularityMode == GranularityMode.ONE_UNIT:
259                gran = 1
260            else:
261                gran = granNum
262        # start sampling - now it can be used for both interval and point data
263        # after instance, there can be a gap or an interval
264        # if it is a gap we remove it and put there the previous instance instead
265        # however the first gap must be removed to avoid duplication
266        maps = sp.get_registered_maps_as_objects_by_granularity(gran=gran)
267        if maps and len(maps) > 0:
268            lastTimeseries = None
269            followsPoint = False  # indicates that we are just after finding a point
270            afterPoint = False  # indicates that we are after finding a point
271            for mymap in maps:
272                if isinstance(mymap, list):
273                    if len(mymap) > 0:
274                        map = mymap[0]
275                else:
276                    map = mymap
277
278                series = map.get_id()
279
280                start, end = map.get_temporal_extent_as_tuple()
281                if self.timeseriesInfo[timeseries]['map_time'] == 'point':
282                    # point data
283                    listOfMaps.append(series)
284                    afterPoint = True
285                    followsPoint = True
286                    lastTimeseries = series
287                    end = None
288                else:
289                    end = end
290                    # interval data
291                    if series:
292                        # map exists, stop point mode
293                        listOfMaps.append(series)
294                        afterPoint = False
295                    else:
296                        # check point mode
297                        if afterPoint:
298                            if followsPoint:
299                                # skip this one, already there
300                                followsPoint = False
301                                continue
302                            else:
303                                # append the last one (of point time)
304                                listOfMaps.append(lastTimeseries)
305                                end = None
306                        else:
307                            # append series which is None
308                            listOfMaps.append(series)
309                timeLabels.append((start, end, unit))
310
311        return timeLabels, listOfMaps
312
313    def _pretifyTimeLabels(self, labels):
314        """Convert absolute time labels to grass time and
315        leave only datum when time is 0.
316        """
317        grassLabels = []
318        isTime = False
319        for start, end, unit in labels:
320            start = tgis.string_to_datetime(start)
321            start = tgis.datetime_to_grass_datetime_string(start)
322            if end is not None:
323                end = tgis.string_to_datetime(end)
324                end = tgis.datetime_to_grass_datetime_string(end)
325            grassLabels.append((start, end, unit))
326            if '00:00:00' not in start or (
327                    end is not None and '00:00:00' not in end):
328                isTime = True
329        if not isTime:
330            for i, (start, end, unit) in enumerate(grassLabels):
331                start = start.replace('00:00:00', '').strip()
332                if end is not None:
333                    end = end.replace('00:00:00', '').strip()
334                grassLabels[i] = (start, end, unit)
335        return grassLabels
336
337    def _gatherInformation(self, timeseries, etype, timeseriesList, infoDict):
338        """Get info about timeseries and check topology (raises GException)"""
339        id = validateTimeseriesName(timeseries, etype)
340        sp = tgis.dataset_factory(etype, id)
341        # Insert content from db
342        sp.select()
343        # Get ordered map list
344        maps = sp.get_registered_maps_as_objects()
345
346        if not sp.check_temporal_topology(maps):
347            raise GException(
348                _("Topology of Space time dataset %s is invalid." % id))
349
350        timeseriesList.append(id)
351        infoDict[id] = {}
352        infoDict[id]['etype'] = etype
353        infoDict[id]['temporal_type'] = sp.get_temporal_type()
354        if sp.is_time_relative():
355            infoDict[id]['unit'] = sp.get_relative_time_unit()
356        infoDict[id]['granularity'] = sp.get_granularity()
357        infoDict[id]['map_time'] = sp.get_map_time()
358        infoDict[id]['maps'] = maps
359
360
361def test():
362    from pprint import pprint
363    # Make sure the temporal database exists
364    tgis.init()
365
366    temp = TemporalManager()
367#    timeseries = createAbsolutePoint()
368#    timeseries = createRelativePoint()
369#    timeseries1, timeseries2 = createAbsoluteInterval()
370    timeseries1, timeseries2 = createRelativeInterval()
371
372    temp.AddTimeSeries(timeseries1, 'strds')
373    temp.AddTimeSeries(timeseries2, 'strds')
374
375    try:
376        warn = temp.EvaluateInputData()
377        print(warn)
378    except GException as e:
379        print(e)
380        return
381
382    print('///////////////////////////')
383    gran = temp.GetGranularity()
384    print("granularity: " + str(gran))
385    pprint(temp.GetLabelsAndMaps())
386
387
388def createAbsoluteInterval():
389    grass.run_command(
390        'g.region',
391        s=0,
392        n=80,
393        w=0,
394        e=120,
395        b=0,
396        t=50,
397        res=10,
398        res3=10,
399        flags='p3',
400        quiet=True)
401
402    grass.mapcalc(exp="prec_1 = rand(0, 550)", overwrite=True)
403    grass.mapcalc(exp="prec_2 = rand(0, 450)", overwrite=True)
404    grass.mapcalc(exp="prec_3 = rand(0, 320)", overwrite=True)
405    grass.mapcalc(exp="prec_4 = rand(0, 510)", overwrite=True)
406    grass.mapcalc(exp="prec_5 = rand(0, 300)", overwrite=True)
407    grass.mapcalc(exp="prec_6 = rand(0, 650)", overwrite=True)
408
409    grass.mapcalc(exp="temp_1 = rand(0, 550)", overwrite=True)
410    grass.mapcalc(exp="temp_2 = rand(0, 450)", overwrite=True)
411    grass.mapcalc(exp="temp_3 = rand(0, 320)", overwrite=True)
412    grass.mapcalc(exp="temp_4 = rand(0, 510)", overwrite=True)
413    grass.mapcalc(exp="temp_5 = rand(0, 300)", overwrite=True)
414    grass.mapcalc(exp="temp_6 = rand(0, 650)", overwrite=True)
415
416    n1 = grass.read_command("g.tempfile", pid=1, flags='d').strip()
417    fd = open(n1, 'w')
418    fd.write(
419        "prec_1|2001-01-01|2001-02-01\n"
420        "prec_2|2001-04-01|2001-05-01\n"
421        "prec_3|2001-05-01|2001-09-01\n"
422        "prec_4|2001-09-01|2002-01-01\n"
423        "prec_5|2002-01-01|2002-05-01\n"
424        "prec_6|2002-05-01|2002-07-01\n"
425    )
426    fd.close()
427
428    n2 = grass.read_command("g.tempfile", pid=2, flags='d').strip()
429    fd = open(n2, 'w')
430    fd.write(
431        "temp_1|2000-10-01|2001-01-01\n"
432        "temp_2|2001-04-01|2001-05-01\n"
433        "temp_3|2001-05-01|2001-09-01\n"
434        "temp_4|2001-09-01|2002-01-01\n"
435        "temp_5|2002-01-01|2002-05-01\n"
436        "temp_6|2002-05-01|2002-07-01\n"
437    )
438    fd.close()
439    name1 = 'absinterval1'
440    name2 = 'absinterval2'
441    grass.run_command('t.unregister', type='raster',
442                      maps='prec_1,prec_2,prec_3,prec_4,prec_5,prec_6,'
443                      'temp_1,temp_2,temp_3,temp_4,temp_5,temp_6')
444    for name, fname in zip((name1, name2), (n1, n2)):
445        grass.run_command(
446            't.create',
447            overwrite=True,
448            type='strds',
449            temporaltype='absolute',
450            output=name,
451            title="A test with input files",
452            descr="A test with input files")
453        grass.run_command(
454            't.register',
455            flags='i',
456            input=name,
457            file=fname,
458            overwrite=True)
459
460    return name1, name2
461
462
463def createRelativeInterval():
464    grass.run_command(
465        'g.region',
466        s=0,
467        n=80,
468        w=0,
469        e=120,
470        b=0,
471        t=50,
472        res=10,
473        res3=10,
474        flags='p3',
475        quiet=True)
476
477    grass.mapcalc(exp="prec_1 = rand(0, 550)", overwrite=True)
478    grass.mapcalc(exp="prec_2 = rand(0, 450)", overwrite=True)
479    grass.mapcalc(exp="prec_3 = rand(0, 320)", overwrite=True)
480    grass.mapcalc(exp="prec_4 = rand(0, 510)", overwrite=True)
481    grass.mapcalc(exp="prec_5 = rand(0, 300)", overwrite=True)
482    grass.mapcalc(exp="prec_6 = rand(0, 650)", overwrite=True)
483
484    grass.mapcalc(exp="temp_1 = rand(0, 550)", overwrite=True)
485    grass.mapcalc(exp="temp_2 = rand(0, 450)", overwrite=True)
486    grass.mapcalc(exp="temp_3 = rand(0, 320)", overwrite=True)
487    grass.mapcalc(exp="temp_4 = rand(0, 510)", overwrite=True)
488    grass.mapcalc(exp="temp_5 = rand(0, 300)", overwrite=True)
489    grass.mapcalc(exp="temp_6 = rand(0, 650)", overwrite=True)
490
491    n1 = grass.read_command("g.tempfile", pid=1, flags='d').strip()
492    fd = open(n1, 'w')
493    fd.write(
494        "prec_1|1|4\n"
495        "prec_2|6|7\n"
496        "prec_3|7|10\n"
497        "prec_4|10|11\n"
498        "prec_5|11|14\n"
499        "prec_6|14|17\n"
500    )
501    fd.close()
502
503    n2 = grass.read_command("g.tempfile", pid=2, flags='d').strip()
504    fd = open(n2, 'w')
505    fd.write(
506        "temp_1|5|6\n"
507        "temp_2|6|7\n"
508        "temp_3|7|10\n"
509        "temp_4|10|11\n"
510        "temp_5|11|18\n"
511        "temp_6|19|22\n"
512    )
513    fd.close()
514    name1 = 'relinterval1'
515    name2 = 'relinterval2'
516    grass.run_command('t.unregister', type='raster',
517                      maps='prec_1,prec_2,prec_3,prec_4,prec_5,prec_6,'
518                      'temp_1,temp_2,temp_3,temp_4,temp_5,temp_6')
519    for name, fname in zip((name1, name2), (n1, n2)):
520        grass.run_command(
521            't.create',
522            overwrite=True,
523            type='strds',
524            temporaltype='relative',
525            output=name,
526            title="A test with input files",
527            descr="A test with input files")
528        grass.run_command(
529            't.register',
530            flags='i',
531            input=name,
532            file=fname,
533            unit="years",
534            overwrite=True)
535    return name1, name2
536
537
538def createAbsolutePoint():
539    grass.run_command(
540        'g.region',
541        s=0,
542        n=80,
543        w=0,
544        e=120,
545        b=0,
546        t=50,
547        res=10,
548        res3=10,
549        flags='p3',
550        quiet=True)
551
552    grass.mapcalc(exp="prec_1 = rand(0, 550)", overwrite=True)
553    grass.mapcalc(exp="prec_2 = rand(0, 450)", overwrite=True)
554    grass.mapcalc(exp="prec_3 = rand(0, 320)", overwrite=True)
555    grass.mapcalc(exp="prec_4 = rand(0, 510)", overwrite=True)
556    grass.mapcalc(exp="prec_5 = rand(0, 300)", overwrite=True)
557    grass.mapcalc(exp="prec_6 = rand(0, 650)", overwrite=True)
558
559    n1 = grass.read_command("g.tempfile", pid=1, flags='d').strip()
560    fd = open(n1, 'w')
561    fd.write(
562        "prec_1|2001-01-01\n"
563        "prec_2|2001-03-01\n"
564        "prec_3|2001-04-01\n"
565        "prec_4|2001-05-01\n"
566        "prec_5|2001-08-01\n"
567        "prec_6|2001-09-01\n"
568    )
569    fd.close()
570    name = 'abspoint'
571    grass.run_command(
572        't.create',
573        overwrite=True,
574        type='strds',
575        temporaltype='absolute',
576        output=name,
577        title="A test with input files",
578        descr="A test with input files")
579
580    grass.run_command(
581        't.register',
582        flags='i',
583        input=name,
584        file=n1,
585        overwrite=True)
586    return name
587
588
589def createRelativePoint():
590    grass.run_command(
591        'g.region',
592        s=0,
593        n=80,
594        w=0,
595        e=120,
596        b=0,
597        t=50,
598        res=10,
599        res3=10,
600        flags='p3',
601        quiet=True)
602
603    grass.mapcalc(exp="prec_1 = rand(0, 550)", overwrite=True)
604    grass.mapcalc(exp="prec_2 = rand(0, 450)", overwrite=True)
605    grass.mapcalc(exp="prec_3 = rand(0, 320)", overwrite=True)
606    grass.mapcalc(exp="prec_4 = rand(0, 510)", overwrite=True)
607    grass.mapcalc(exp="prec_5 = rand(0, 300)", overwrite=True)
608    grass.mapcalc(exp="prec_6 = rand(0, 650)", overwrite=True)
609
610    n1 = grass.read_command("g.tempfile", pid=1, flags='d').strip()
611    fd = open(n1, 'w')
612    fd.write(
613        "prec_1|1\n"
614        "prec_2|3\n"
615        "prec_3|5\n"
616        "prec_4|7\n"
617        "prec_5|11\n"
618        "prec_6|13\n"
619    )
620    fd.close()
621    name = 'relpoint'
622    grass.run_command(
623        't.create',
624        overwrite=True,
625        type='strds',
626        temporaltype='relative',
627        output=name,
628        title="A test with input files",
629        descr="A test with input files")
630
631    grass.run_command(
632        't.register',
633        unit="day",
634        input=name,
635        file=n1,
636        overwrite=True)
637    return name
638
639if __name__ == '__main__':
640
641    test()
642