1"""
2@package vnet.vnet_data
3
4@brief Vector network analysis classes for data managment.
5
6Classes:
7 - vnet_data::VNETData
8 - vnet_data::VNETPointsData
9 - vnet_data::VNETAnalysisParameters
10 - vnet_data::VNETAnalysesProperties
11 - vnet_data::VNETTmpVectMaps
12 - vnet_data::VectMap
13 - vnet_data::History
14 - vnet_data::VNETGlobalTurnsData
15
16(C) 2013-2014 by the GRASS Development Team
17
18This program is free software under the GNU General Public License
19(>=v2). Read the file COPYING that comes with GRASS for details.
20
21@author Stepan Turek <stepan.turek seznam.cz> (GSoC 2012, mentor: Martin Landa)
22@author Lukas Bocan <silent_bob centrum.cz> (turn costs support)
23@author Eliska Kyzlikova <eliska.kyzlikova gmail.com> (turn costs support)
24"""
25import os
26import six
27from copy import deepcopy
28
29from grass.script.utils import try_remove
30from grass.script import core as grass
31from grass.script.task import cmdlist_to_tuple
32
33import wx
34
35
36from core import utils
37from core.gcmd import RunCommand, GMessage
38from core.settings import UserSettings
39
40from vnet.vnet_utils import ParseMapStr, SnapToNode
41
42from gui_core.gselect import VectorDBInfo
43from grass.pydispatch.signal import Signal
44
45from vnet.vnet_utils import DegreesToRadians, RadiansToDegrees
46
47
48class VNETData:
49
50    def __init__(self, guiparent, mapWin):
51
52        # setting initialization
53        self._initSettings()
54
55        self.guiparent = guiparent
56
57        self.an_props = VNETAnalysesProperties()
58        self.an_params = VNETAnalysisParameters(self.an_props)
59
60        self.an_points = VNETPointsData(mapWin, self.an_props, self.an_params)
61
62        self.global_turns = VNETGlobalTurnsData()
63
64        self.pointsChanged = self.an_points.pointsChanged
65        self.parametersChanged = self.an_params.parametersChanged
66
67    def CleanUp(self):
68        self.an_points.CleanUp()
69
70    def GetAnalyses(self):
71        return self.an_props.used_an
72
73    def GetPointsData(self):
74        return self.an_points
75
76    def GetGlobalTurnsData(self):
77        return self.global_turns
78
79    def GetRelevantParams(self, analysis=None):
80        if analysis:
81            return self.an_props.GetRelevantParams(analysis)
82        else:
83            analysis, valid = self.an_params.GetParam("analysis")
84            return self.an_props.GetRelevantParams(analysis)
85
86    def GetAnalysisProperties(self, analysis=None):
87        if analysis:
88            return self.an_props[analysis]
89        else:
90            analysis, valid = self.an_params.GetParam("analysis")
91            return self.an_props[analysis]
92
93    def GetParam(self, param):
94        return self.an_params.GetParam(param)
95
96    def GetParams(self):
97        return self.an_params.GetParams()
98
99    def SetParams(self, params, flags):
100        return self.an_params.SetParams(params, flags)
101
102    def SetSnapping(self, activate):
103        self.an_points.SetSnapping(activate)
104
105    def GetSnapping(self):
106        return self.an_points.GetSnapping()
107
108    def GetLayerStyle(self):
109        """Returns cmd for d.vect, with set style for analysis result"""
110        analysis, valid = self.an_params.GetParam("analysis")
111
112        resProps = self.an_props[analysis]["resultProps"]
113
114        width = UserSettings.Get(
115            group='vnet',
116            key='res_style',
117            subkey="line_width")
118        layerStyleCmd = ["layer=1", 'width=' + str(width)]
119
120        if "catColor" in resProps:
121            layerStyleCmd.append('flags=c')
122        elif "singleColor" in resProps:
123            col = UserSettings.Get(
124                group='vnet',
125                key='res_style',
126                subkey="line_color")
127            layerStyleCmd.append(
128                'color=' + str(col[0]) + ':' + str(col[1]) + ':' + str(col[2]))
129
130        layerStyleVnetColors = []
131        if "attrColColor" in resProps:
132            colorStyle = UserSettings.Get(
133                group='vnet', key='res_style', subkey="color_table")
134            invert = UserSettings.Get(
135                group='vnet',
136                key='res_style',
137                subkey="invert_colors")
138
139            layerStyleVnetColors = [
140                "v.colors",
141                "color=" + colorStyle,
142                "column=" + resProps["attrColColor"],
143            ]
144            if invert:
145                layerStyleVnetColors.append("-n")
146
147        return layerStyleCmd, layerStyleVnetColors
148
149    def InputsErrorMsgs(self, msg, analysis, params, flags,
150                        inv_params, relevant_params):
151        """Checks input data in Parameters tab and shows messages if some value is not valid
152
153            :param str msg: message added to start of message string
154            :return: True if checked inputs are OK
155            :return: False if some of checked inputs is not ok
156        """
157
158        if flags["t"] and "turn_layer" not in relevant_params:
159            GMessage(
160                parent=self.guiparent, message=_(
161                    "Module <%s> does not support turns costs." %
162                    analysis))
163            return False
164
165        errMapStr = ""
166        if 'input' in inv_params:
167            if params['input']:
168                errMapStr = _("Vector map '%s' does not exist.") % (
169                    params['input'])
170            else:
171                errMapStr = _("Vector map was not chosen.")
172
173        if errMapStr:
174            GMessage(parent=self.guiparent,
175                     message=msg + "\n" + errMapStr)
176            return False
177
178        errLayerStr = ""
179        vals = {
180                'arc_layer': _("arc layer"),
181                'node_layer': _("node layer"),
182                'turn_layer': _("turntable layer"),
183                'turn_cat_layer': _("unique categories layer")
184                }
185        for layer, layerLabel in six.iteritems(vals):
186
187            if layer in ["turn_layer", "turn_cat_layer"] and not flags["t"]:
188                continue
189            if layer in inv_params:
190                if params[layer]:
191                    errLayerStr += _("Chosen %s '%s' does not exist in vector map '%s'.\n") % (
192                        layerLabel, params[layer], params['input'])
193                else:
194                    errLayerStr += _("Choose existing %s.\n") % \
195                        (layerLabel)
196        if errLayerStr:
197            GMessage(parent=self.guiparent,
198                     message=msg + "\n" + errLayerStr)
199            return False
200
201        errColStr = ""
202        for col in ["arc_column", "arc_backward_column", "node_column"]:
203            if params[col] and col in inv_params and col in relevant_params:
204                errColStr += _("Chosen column '%s' does not exist in attribute table of layer '%s' of vector map '%s'.\n") % (
205                    params[col], params[layer], params['input'])
206
207        if errColStr:
208            GMessage(parent=self.guiparent,
209                     message=msg + "\n" + errColStr)
210            return False
211
212        return True
213
214    def _initSettings(self):
215        """Initialization of settings (if not already defined)"""
216        # initializes default settings
217        initSettings = [
218            ['res_style', 'line_width', 5],
219            ['res_style', 'line_color', (192, 0, 0)],
220            ['res_style', 'color_table', 'byr'],
221            ['res_style', 'invert_colors', False],
222            ['point_symbol', 'point_size', 10],
223            ['point_symbol', 'point_width', 2],
224            ['point_colors', "unused", (131, 139, 139)],
225            ['point_colors', "used1cat", (192, 0, 0)],
226            ['point_colors', "used2cat", (0, 0, 255)],
227            ['point_colors', "selected", (9, 249, 17)],
228            ['other', "snap_tresh", 10],
229            ['other', "max_hist_steps", 5]
230        ]
231
232        for init in initSettings:
233            UserSettings.ReadSettingsFile()
234            UserSettings.Append(dict=UserSettings.userSettings,
235                                group='vnet',
236                                key=init[0],
237                                subkey=init[1],
238                                value=init[2],
239                                overwrite=False)
240
241
242class VNETPointsData:
243
244    def __init__(self, mapWin, an_data, an_params):
245
246        self.mapWin = mapWin
247        self.an_data = an_data
248        self.an_params = an_params
249
250        # information, whether mouse event handler is registered in map window
251        self.handlerRegistered = False
252
253        self.pointsChanged = Signal('VNETPointsData.pointsChanged')
254        self.an_params.parametersChanged.connect(self.ParametersChanged)
255
256        self.snapping = False
257
258        self.data = []
259        self.cols = {"name": ['use', 'type', 'topology', 'e', 'n'],
260                     "label": [_('use'), _('type'), _('topology'), 'e', 'n'],
261                     # TDO
262                     "type": [None, ["", _("Start point"), _("End Point")], None, float, float],
263                     "def_vals": [False, 0, "new point", 0, 0]
264                     }
265
266        # registration graphics for drawing
267        self.pointsToDraw = self.mapWin.RegisterGraphicsToDraw(
268            graphicsType="point", setStatusFunc=self.SetPointStatus)
269
270        self.SetPointDrawSettings()
271
272        self.AddPoint()
273        self.AddPoint()
274
275        self.SetPointData(0, {'use': True, 'type': 1})
276        self.SetPointData(1, {'use': True, 'type': 2})
277
278        self.selected = 0
279
280    def __del__(self):
281        self.CleanUp()
282
283    def CleanUp(self):
284        self.mapWin.UnregisterGraphicsToDraw(self.pointsToDraw)
285
286        if self.handlerRegistered:
287            self.mapWin.UnregisterMouseEventHandler(wx.EVT_LEFT_DOWN,
288                                                    self.OnMapClickHandler)
289
290    def SetSnapping(self, activate):
291        self.snapping = activate
292
293    def GetSnapping(self):
294        return self.snapping
295
296    def AddPoint(self):
297
298        self.pointsToDraw.AddItem(
299            coords=(
300                self.cols["def_vals"][3],
301                self.cols["def_vals"][4]))
302        self.data.append(self.cols["def_vals"][:])
303
304        self.pointsChanged.emit(method="AddPoint", kwargs={})
305
306    def DeletePoint(self, pt_id):
307        item = self.pointsToDraw.GetItem(pt_id)
308        if item:
309            self.pointsToDraw.DeleteItem(item)
310            self.data.pop(pt_id)
311
312        self.pointsChanged.emit(method="DeletePoint", kwargs={"pt_id": pt_id})
313
314    def SetPoints(self, pts_data):
315
316        for item in self.pointsToDraw.GetAllItems():
317            self.pointsToDraw.DeleteItem(item)
318
319        self.data = []
320        for pt_data in pts_data:
321            pt_data_list = self._ptDataToList(pt_data)
322            self.data.append(pt_data_list)
323            self.pointsToDraw.AddItem(
324                coords=(pt_data_list[3], pt_data_list[4]))
325
326        self.pointsChanged.emit(
327            method="SetPoints", kwargs={
328                "pts_data": pts_data})
329
330    def SetPointData(self, pt_id, data):
331        for col, v in six.iteritems(data):
332            if col == 'use':
333                continue
334
335            idx = self.cols["name"].index(col)
336            self.data[pt_id][idx] = v
337
338        # if type is changed checked columns must be recalculated by _usePoint
339        if 'type' in data and 'use' not in data:
340            data["use"] = self.GetPointData(pt_id)['use']
341
342        if 'use' in data:
343            if self._usePoint(pt_id, data["use"]) == -1:
344                data["use"] = False
345            idx = self.cols["name"].index("use")
346            self.data[pt_id][idx] = data["use"]
347
348        self.pointsChanged.emit(
349            method="SetPointData", kwargs={
350                "pt_id": pt_id, "data": data})
351
352    def GetPointData(self, pt_id):
353        return self._ptListDataToPtData(self.data[pt_id])
354
355    def GetPointsCount(self):
356        return len(self.data)
357
358    def SetPointStatus(self, item, itemIndex):
359        """Before point is drawn, decides properties of drawing style"""
360        analysis, valid = self.an_params.GetParam("analysis")
361        cats = self.an_data[analysis]["cmdParams"]["cats"]
362
363        if itemIndex == self.selected:
364            wxPen = "selected"
365        elif not self.data[itemIndex][0]:
366            wxPen = "unused"
367            item.hide = False
368        elif len(cats) > 1:
369            idx = self.data[itemIndex][1]
370            if idx == 2:  # End/To/Sink point
371                wxPen = "used2cat"
372            else:
373                wxPen = "used1cat"
374        else:
375            wxPen = "used1cat"
376
377        item.SetPropertyVal('label', str(itemIndex + 1))
378        item.SetPropertyVal('penName', wxPen)
379
380    def SetSelected(self, pt_id):
381        self.selected = pt_id
382        self.pointsChanged.emit(method="SetSelected", kwargs={"pt_id": pt_id})
383
384    def GetSelected(self):
385        return self.selected
386
387    def SetPointDrawSettings(self):
388        """Set settings for drawing of points"""
389        ptSize = int(
390            UserSettings.Get(
391                group='vnet',
392                key='point_symbol',
393                subkey='point_size'))
394        self.pointsToDraw.SetPropertyVal("size", ptSize)
395
396        colors = UserSettings.Get(group='vnet', key='point_colors')
397        ptWidth = int(
398            UserSettings.Get(
399                group='vnet',
400                key='point_symbol',
401                subkey='point_width'))
402
403        textProp = self.pointsToDraw.GetPropertyVal("text")
404        textProp["font"].SetPointSize(ptSize + 2)
405
406        for colKey, col in six.iteritems(colors):
407            pen = self.pointsToDraw.GetPen(colKey)
408            if pen:
409                pen.SetColour(wx.Colour(col[0], col[1], col[2], 255))
410                pen.SetWidth(ptWidth)
411            else:
412                self.pointsToDraw.AddPen(
413                    colKey,
414                    wx.Pen(
415                        colour=wx.Colour(
416                            col[0],
417                            col[1],
418                            col[2],
419                            255),
420                        width=ptWidth))
421
422    def ParametersChanged(self, method, kwargs):
423        if "analysis" in list(kwargs["changed_params"].keys()):
424            self._updateTypeCol()
425
426            if self.an_params.GetParam("analysis")[0] == "v.net.path":
427                self._vnetPathUpdateUsePoints(None)
428
429    def _updateTypeCol(self):
430        """Rename category values when module is changed. Expample: Start point -> Sink point"""
431        colValues = [""]
432        analysis, valid = self.an_params.GetParam("analysis")
433        anParamsCats = self.an_data[analysis]["cmdParams"]["cats"]
434
435        for ptCat in anParamsCats:
436            colValues.append(ptCat[1])
437
438        type_idx = self.cols["name"].index("type")
439        self.cols['type'][type_idx] = colValues
440
441    def _ptDataToList(self, pt_data):
442
443        pt_list_data = [None] * len(self.cols['name'])
444
445        for k, val in six.iteritems(pt_data):
446            pt_list_data[self.cols["name"].index(k)] = val
447
448        return pt_list_data
449
450    def _ptListDataToPtData(self, pt_list_data):
451
452        pt_data = {}
453        for i, val in enumerate(pt_list_data):
454            pt_data[self.cols["name"][i]] = val
455
456        return pt_data
457
458    def _usePoint(self, pt_id, use):
459        """Item is checked/unchecked"""
460        analysis, valid = self.an_params.GetParam("analysis")
461        cats = self.an_data[analysis]["cmdParams"]["cats"]
462        # TODO move
463        # if self.updateMap:
464        #    up_map_evt = gUpdateMap(render = False, renderVector = False)
465        #    wx.PostEvent(self.dialog.mapWin, up_map_evt)
466
467        if len(cats) <= 1:
468            return 0
469
470        use_idx = self.cols["name"].index("use")
471        checkedVal = self.data[pt_id][1]
472
473        # point without given type cannot be selected
474        if checkedVal == 0:
475            self.data[pt_id][use_idx] = False
476            self.pointsChanged.emit(
477                method="SetPointData", kwargs={
478                    "pt_id": pt_id, "data": {
479                        "use": False}})
480            return -1
481
482        if analysis == "v.net.path" and use:
483            self._vnetPathUpdateUsePoints(pt_id)
484
485    def _vnetPathUpdateUsePoints(self, checked_pt_id):
486
487        alreadyChecked = []
488
489        type_idx = self.cols["name"].index("type")
490        use_idx = self.cols["name"].index("use")
491
492        if checked_pt_id is not None:
493            checkedKey = checked_pt_id
494            alreadyChecked.append(self.data[checked_pt_id][type_idx])
495        else:
496            checkedKey = -1
497
498        for iKey, dt in enumerate(self.data):
499            pt_type = dt[type_idx]
500
501            if ((pt_type in alreadyChecked and checkedKey != iKey)
502                    or pt_type == 0) and self.data[iKey][use_idx]:
503                self.data[iKey][use_idx] = False
504                self.pointsChanged.emit(
505                    method="SetPointData", kwargs={
506                        "pt_id": iKey, "data": {
507                            "use": False}})
508            elif self.data[iKey][use_idx]:
509                alreadyChecked.append(pt_type)
510
511    def EditPointMode(self, activate):
512        """Registers/unregisters mouse handler into map window"""
513
514        if activate == self.handlerRegistered:
515            return
516
517        if activate:
518            self.mapWin.RegisterMouseEventHandler(wx.EVT_LEFT_DOWN,
519                                                  self.OnMapClickHandler,
520                                                  'cross')
521            self.handlerRegistered = True
522        else:
523            self.mapWin.UnregisterMouseEventHandler(wx.EVT_LEFT_DOWN,
524                                                    self.OnMapClickHandler)
525            self.handlerRegistered = False
526
527        self.pointsChanged.emit(
528            method="EditMode", kwargs={
529                "activated": activate})
530
531    def IsEditPointModeActive(self):
532        return self.handlerRegistered
533
534    def OnMapClickHandler(self, event):
535        """Take coordinates from map window"""
536        # TODO update snapping after input change
537        if event == 'unregistered':
538            self.handlerRegistered = False
539            return
540
541        if not self.data:
542            self.AddPoint()
543
544        e, n = self.mapWin.GetLastEN()
545
546        if self.snapping:
547
548            # compute threshold
549            snapTreshPix = int(UserSettings.Get(group='vnet',
550                                                key='other',
551                                                subkey='snap_tresh'))
552            res = max(
553                self.mapWin.Map.region['nsres'],
554                self.mapWin.Map.region['ewres'])
555            snapTreshDist = snapTreshPix * res
556
557            params, err_params, flags = self.an_params.GetParams()
558            vectMap = params["input"]
559
560            if "input" in err_params:
561                msg = _("new point")
562
563            coords = SnapToNode(e, n, snapTreshDist, vectMap)
564            if coords:
565                e = coords[0]
566                n = coords[1]
567
568                msg = ("snapped to node")
569            else:
570                msg = _("new point")
571
572        else:
573            msg = _("new point")
574
575        self.SetPointData(self.selected,
576                          {'topology': msg,
577                           'e': e,
578                           'n': n})
579
580        self.pointsToDraw.GetItem(self.selected).SetCoords([e, n])
581
582        if self.selected == len(self.data) - 1:
583            self.SetSelected(0)
584        else:
585            self.SetSelected(self.GetSelected() + 1)
586
587    def GetColumns(self, only_relevant=True):
588
589        cols_data = deepcopy(self.cols)
590
591        hidden_cols = []
592        hidden_cols.append(self.cols["name"].index("e"))
593        hidden_cols.append(self.cols["name"].index("n"))
594
595        analysis, valid = self.an_params.GetParam("analysis")
596        if only_relevant and len(self.an_data[analysis][
597                                 "cmdParams"]["cats"]) <= 1:
598            hidden_cols.append(self.cols["name"].index("type"))
599
600        i_red = 0
601        hidden_cols.sort()
602        for idx in hidden_cols:
603            for dt in six.itervalues(cols_data):
604                dt.pop(idx - i_red)
605            i_red += 1
606
607        return cols_data
608
609
610class VNETAnalysisParameters:
611
612    def __init__(self, an_props):
613
614        self.an_props = an_props
615
616        self.params = {"analysis": self.an_props.used_an[0],
617                       "input": "",
618                       "arc_layer": "",
619                       "node_layer": "",
620                       "arc_column": "",
621                       "arc_backward_column": "",
622                       "node_column": "",
623                       "turn_layer": "",
624                       "turn_cat_layer": "",
625                       "iso_lines": "",  # TODO check validity
626                       "max_dist": 0}  # TODO check validity
627
628        self.flags = {"t": False}
629
630        self.parametersChanged = Signal(
631            'VNETAnalysisParameters.parametersChanged')
632
633    def SetParams(self, params, flags):
634
635        changed_params = {}
636        for p, v in six.iteritems(params):
637            if p == "analysis" and v not in self.an_props.used_an:
638                continue
639
640            if p == "input":
641                mapName, mapSet = ParseMapStr(v)
642                v = mapName + "@" + mapSet
643
644            if p in self.params:
645                if isinstance(v, str):
646                    v = v.strip()
647
648                self.params[p] = v
649                changed_params[p] = v
650
651        changed_flags = {}
652        for p, v in six.iteritems(flags):
653            if p in self.flags:
654                self.flags[p] = v
655                changed_flags[p] = v
656
657        self.parametersChanged.emit(
658            method="SetParams",
659            kwargs={
660                "changed_params": changed_params,
661                "changed_flags": changed_flags})
662
663        return changed_params, changed_flags
664
665    def GetParam(self, param):
666
667        invParams = []
668        if param in [
669                "input", "arc_layer", "node_layer", "arc_column",
670                "arc_backward_column", "node_column", "turn_layer",
671                "turn_cat_layer"]:
672            invParams = self._getInvalidParams(self.params)
673
674        if invParams:
675            return self.params[param], False
676
677        return self.params[param], True
678
679    def GetParams(self):
680
681        invParams = self._getInvalidParams(self.params)
682        return self.params, invParams, self.flags
683
684    def _getInvalidParams(self, params):
685        """Check of analysis input data for invalid values (Parameters tab)"""
686        # dict of invalid values {key from self.itemData (comboboxes from
687        # Parameters tab) : invalid value}
688        invParams = []
689
690        # check vector map
691        if params["input"]:
692            mapName, mapSet = params["input"].split("@")
693            if mapSet in grass.list_grouped('vector'):
694                vectMaps = grass.list_grouped('vector')[mapSet]
695
696        if not params["input"] or mapName not in vectMaps:
697            invParams = list(params.keys())[:]
698            return invParams
699
700        # check arc/node layer
701        layers = utils.GetVectorNumberOfLayers(params["input"])
702
703        for l in ['arc_layer', 'node_layer', 'turn_layer', 'turn_cat_layer']:
704            if not layers or params[l] not in layers:
705                invParams.append(l)
706
707        dbInfo = VectorDBInfo(params["input"])
708
709        try:
710            table = dbInfo.GetTable(int(params["arc_layer"]))
711            columnchoices = dbInfo.GetTableDesc(table)
712        except (KeyError, ValueError):
713            table = None
714
715        # check costs columns
716        for col in ["arc_column", "arc_backward_column", "node_column"]:
717
718            if col == "node_column":
719                try:
720                    table = dbInfo.GetTable(int(params["node_layer"]))
721                    columnchoices = dbInfo.GetTableDesc(table)
722                except (KeyError, ValueError):
723                    table = None
724
725            if not table or not params[col] in list(columnchoices.keys()):
726                invParams.append(col)
727                continue
728
729            if columnchoices[
730                    params[col]]['type'] not in [
731                    'integer', 'double precision']:
732                invParams.append(col)
733                continue
734
735        return invParams
736
737
738class VNETAnalysesProperties:
739
740    def __init__(self):
741        """Initializes parameters for different v.net.* modules """
742        # initialization of v.net.* analysis parameters (data which
743        # characterizes particular analysis)
744
745        self.attrCols = {
746            'arc_column': {
747                "label": _("Arc forward/both direction(s) cost column:"),
748                "name": _("arc forward/both")
749            },
750            'arc_backward_column': {
751                "label": _("Arc backward direction cost column:"),
752                "name": _("arc backward")
753            },
754            'acolumn': {
755                "label": _("Arcs' cost column (for both directions):"),
756                "name": _("arc"),
757                "inputField": 'arc_column',
758            },
759            'node_column': {
760                "label": _("Node cost column:"),
761                "name": _("node")
762            }
763        }
764
765        self.vnetProperties = {
766            "v.net.path": {
767                "label": _("Shortest path %s") % "(v.net.path)",
768                "cmdParams": {
769                    "cats": [
770                        ["st_pt", _("Start point")],
771                        ["end_pt", _("End point")]
772                    ],
773                    "cols": [
774                        'arc_column',
775                        'arc_backward_column',
776                        'node_column'
777                    ],
778                },
779                "resultProps": {
780                    "singleColor": None,
781                    "dbMgr": True  # TODO delete this property, this information can be get from result
782                },
783                "turns_support": True
784            },
785
786            "v.net.salesman": {
787                "label": _("Traveling salesman %s") % "(v.net.salesman)",
788                "cmdParams": {
789                    "cats": [["center_cats", None]],
790                    "cols": [
791                        'arc_column',
792                        'arc_backward_column'
793                    ],
794                },
795                "resultProps": {
796                    "singleColor": None,
797                    "dbMgr": False
798                },
799                "turns_support": True
800
801            },
802            "v.net.flow": {
803                "label": _("Maximum flow %s") % "(v.net.flow)",
804                "cmdParams": {
805                    "cats": [
806                        ["source_cats", _("Source point")],
807                        ["sink_cats", _("Sink point")]
808                    ],
809                    "cols": [
810                        'arc_column',
811                        'arc_backward_column',
812                        'node_column'
813                    ]
814                },
815                "resultProps": {
816                    "attrColColor": "flow",
817                    "dbMgr": True
818                },
819                "turns_support": False
820            },
821            "v.net.alloc": {
822                "label": _("Subnets for nearest centers %s") % "(v.net.alloc)",
823                "cmdParams": {
824                    "cats": [["center_cats", None]],
825                    "cols": [
826                        'arc_column',
827                        'arc_backward_column',
828                        'node_column'
829                    ]
830                },
831                "resultProps": {
832                    "catColor": None,
833                    "dbMgr": False
834                },
835                "turns_support": True
836            },
837            "v.net.steiner": {
838                "label": _("Steiner tree for the network and given terminals %s") % "(v.net.steiner)",
839                "cmdParams": {
840                    "cats": [["terminal_cats", None]],
841                    "cols": [
842                        'acolumn',
843                    ]
844                },
845                "resultProps": {
846                    "singleColor": None,
847                    "dbMgr": False
848                },
849                "turns_support": True
850            },
851            "v.net.distance": {
852                "label": _("Shortest distance via the network %s") % "(v.net.distance)",
853                "cmdParams": {
854                    "cats": [
855                        ["from_cats", "From point"],
856                        ["to_cats", "To point"]
857                    ],
858                    "cols": [
859                        'arc_column',
860                        'arc_backward_column',
861                        'node_column'
862                    ],
863                },
864                "resultProps": {
865                    "catColor": None,
866                    "dbMgr": True
867                },
868                "turns_support": False
869            },
870            "v.net.iso": {
871                "label": _("Cost isolines %s") % "(v.net.iso)",
872                "cmdParams": {
873                    "cats": [["center_cats", None]],
874                    "cols": [
875                        'arc_column',
876                        'arc_backward_column',
877                        'node_column'
878                    ]
879                },
880                "resultProps": {
881                    "catColor": None,
882                    "dbMgr": False
883                },
884                "turns_support": True
885            }
886        }
887
888        self.used_an = ["v.net.path",
889                        "v.net.salesman",
890                        "v.net.flow",
891                        "v.net.alloc",
892                        "v.net.distance",
893                        "v.net.iso",
894                        #"v.net.steiner"
895                        ]
896
897        for an in list(self.vnetProperties.keys()):
898            if an not in self.used_an:
899                del self.vnetProperties[an]
900                continue
901
902            cols = self.vnetProperties[an]["cmdParams"]["cols"]
903            self.vnetProperties[an]["cmdParams"]["cols"] = {}
904            for c in cols:
905                self.vnetProperties[an]["cmdParams"][
906                    "cols"][c] = self.attrCols[c]
907
908    def has_key(self, key):
909        return key in self.vnetProperties
910
911    def __getitem__(self, key):
912        return self.vnetProperties[key]
913
914    def GetRelevantParams(self, analysis):
915
916        if analysis not in self.vnetProperties:
917            return None
918
919        relevant_params = ["input", "arc_layer", "node_layer"]
920
921        if self.vnetProperties[analysis]["turns_support"]:
922            relevant_params += ["turn_layer", "turn_cat_layer"]
923
924        cols = self.vnetProperties[analysis]["cmdParams"]["cols"]
925
926        for col, v in six.iteritems(cols):
927            if "inputField" in col:
928                colInptF = v["inputField"]
929            else:
930                colInptF = col
931            relevant_params.append(colInptF)
932
933        return relevant_params
934
935
936class VNETTmpVectMaps:
937    """Class which creates, stores and destroys all tmp maps created during analysis"""
938
939    def __init__(self, parent, mapWin):
940        self.tmpMaps = []  # temporary maps
941        self.parent = parent
942        self.mapWin = mapWin
943
944    def AddTmpVectMap(self, mapName, msg):
945        """New temporary map
946
947        :return: instance of VectMap representing temporary map
948        """
949        currMapSet = grass.gisenv()['MAPSET']
950        tmpMap = grass.find_file(name=mapName,
951                                 element='vector',
952                                 mapset=currMapSet)
953
954        fullName = tmpMap["fullname"]
955        # map already exists
956        if fullName:
957            # TODO move dialog out of class, AddTmpVectMap(self, mapName,
958            # overvrite = False)
959            dlg = wx.MessageDialog(parent=self.parent,
960                                   message=msg,
961                                   caption=_("Overwrite map layer"),
962                                   style=wx.YES_NO | wx.NO_DEFAULT |
963                                   wx.ICON_QUESTION | wx.CENTRE)
964
965            ret = dlg.ShowModal()
966            dlg.Destroy()
967
968            if ret == wx.ID_NO:
969                return None
970        else:
971            fullName = mapName + "@" + currMapSet
972
973        newVectMap = VectMap(self.mapWin, fullName)
974        self.tmpMaps.append(newVectMap)
975
976        return newVectMap
977
978    def HasTmpVectMap(self, vectMapName):
979        """
980        :param: vectMapName name of vector map
981
982        :return: True if it contains the map
983        :return: False if not
984        """
985
986        mapValSpl = vectMapName.strip().split("@")
987        if len(mapValSpl) > 1:
988            mapSet = mapValSpl[1]
989        else:
990            mapSet = grass.gisenv()['MAPSET']
991        mapName = mapValSpl[0]
992        fullName = mapName + "@" + mapSet
993
994        for vectTmpMap in self.tmpMaps:
995            if vectTmpMap.GetVectMapName() == fullName:
996                return True
997        return False
998
999    def GetTmpVectMap(self, vectMapName):
1000        """Get instance of VectMap with name vectMapName"""
1001        for vectMap in self.tmpMaps:
1002            if vectMap.GetVectMapName() == vectMapName.strip():
1003                return vectMap
1004        return None
1005
1006    def RemoveFromTmpMaps(self, vectMap):
1007        """Temporary map is removed from the class instance however it is not deleted
1008
1009        :param vectMap: instance of VectMap class to be removed
1010
1011        :return: True if was removed
1012        :return: False if does not contain the map
1013        """
1014        try:
1015            self.tmpMaps.remove(vectMap)
1016            return True
1017        except ValueError:
1018            return False
1019
1020    def DeleteTmpMap(self, vectMap):
1021        """Temporary map is removed from the class and it is deleted
1022
1023        :param vectMap: instance of VectMap class to be deleted
1024
1025        :return: True if was removed
1026        :return: False if does not contain the map
1027        """
1028        if vectMap:
1029            vectMap.DeleteRenderLayer()
1030            RunCommand('g.remove', flags='f', type='vector',
1031                       name=vectMap.GetVectMapName())
1032            self.RemoveFromTmpMaps(vectMap)
1033            return True
1034        return False
1035
1036    def DeleteAllTmpMaps(self):
1037        """Delete all temporary maps in the class"""
1038        update = False
1039        for tmpMap in self.tmpMaps:
1040            RunCommand('g.remove', flags='f', type='vector',
1041                       name=tmpMap.GetVectMapName())
1042            if tmpMap.DeleteRenderLayer():
1043                update = True
1044        return update
1045
1046
1047class VectMap:
1048    """Represents map
1049        It can check if it was modified or render it
1050    """
1051
1052    def __init__(self, mapWin, fullName):
1053        self.fullName = fullName
1054        self.mapWin = mapWin
1055        self.renderLayer = None
1056        self.modifTime = None  # time, for modification check
1057
1058    def __del__(self):
1059
1060        self.DeleteRenderLayer()
1061
1062    def AddRenderLayer(self, cmd=None, colorsCmd=None):
1063        """Add map from map window layers to render """
1064
1065        if not self.mapWin:
1066            return False
1067
1068        existsMap = grass.find_file(name=self.fullName,
1069                                    element='vector',
1070                                    mapset=grass.gisenv()['MAPSET'])
1071
1072        if not existsMap["name"]:
1073            self.DeleteRenderLayer()
1074            return False
1075
1076        if not cmd:
1077            cmd = []
1078        cmd.insert(0, 'd.vect')
1079        cmd.append('map=%s' % self.fullName)
1080
1081        if self.renderLayer:
1082            self.DeleteRenderLayer()
1083
1084        if colorsCmd:
1085            colorsCmd.append('map=%s' % self.fullName)
1086            layerStyleVnetColors = cmdlist_to_tuple(colorsCmd)
1087
1088            RunCommand(layerStyleVnetColors[0],
1089                       **layerStyleVnetColors[1])
1090
1091        self.renderLayer = self.mapWin.Map.AddLayer(
1092            ltype="vector", command=cmd, name=self.fullName, active=True,
1093            opacity=1.0, render=False, pos=-1)
1094        return True
1095
1096    def DeleteRenderLayer(self):
1097        """Remove map from map window layers to render"""
1098        if not self.mapWin:
1099            return False
1100
1101        if self.renderLayer:
1102            self.mapWin.Map.DeleteLayer(self.renderLayer)
1103            self.renderLayer = None
1104            return True
1105        return False
1106
1107    def GetRenderLayer(self):
1108        return self.renderLayer
1109
1110    def GetVectMapName(self):
1111        return self.fullName
1112
1113    def SaveVectMapState(self):
1114        """Save modification time for vector map"""
1115        self.modifTime = self.GetLastModified()
1116
1117    def VectMapState(self):
1118        """Checks if map was modified
1119
1120        :return: -1 - if no modification time was saved
1121        :return:  0 - if map was modified
1122        :return:  1 - if map was not modified
1123        """
1124        if self.modifTime is None:
1125            return -1
1126        if self.modifTime != self.GetLastModified():
1127            return 0
1128        return 1
1129
1130    def GetLastModified(self):
1131        """Get modification time
1132
1133        :return: MAP DATE time string from vector map head file
1134        """
1135
1136        mapValSpl = self.fullName.split("@")
1137        mapSet = mapValSpl[1]
1138        mapName = mapValSpl[0]
1139
1140        headPath = os.path.join(grass.gisenv()['GISDBASE'],
1141                                grass.gisenv()['LOCATION_NAME'],
1142                                mapSet,
1143                                "vector",
1144                                mapName,
1145                                "head")
1146        try:
1147            head = open(headPath, 'r')
1148            for line in head.readlines():
1149                i = line.find('MAP DATE:', )
1150                if i == 0:
1151                    head.close()
1152                    return line.split(':', 1)[1].strip()
1153
1154            head.close()
1155            return ""
1156        except IOError:
1157            return ""
1158
1159
1160class History:
1161    """Class which reads and saves history data (based on gui.core.settings Settings class file save/load)
1162
1163    .. todo::
1164        Maybe it could be useful for other GRASS wxGUI tools.
1165    """
1166
1167    def __init__(self):
1168
1169        # max number of steps in history (zero based)
1170        self.maxHistSteps = 3
1171        # current history step
1172        self.currHistStep = 0
1173        # number of steps saved in history
1174        self.histStepsNum = 0
1175
1176        # dict contains data saved in history for current history step
1177        self.currHistStepData = {}
1178
1179        # buffer for data to be saved into history
1180        self.newHistStepData = {}
1181
1182        self.histFile = grass.tempfile()
1183
1184        # key/value separator
1185        self.sep = ';'
1186
1187    def __del__(self):
1188        try_remove(self.histFile)
1189
1190    def GetNext(self):
1191        """Go one step forward in history"""
1192        self.currHistStep -= 1
1193        self.currHistStepData.clear()
1194        self.currHistStepData = self._getHistStepData(self.currHistStep)
1195
1196        return self.currHistStepData
1197
1198    def GetPrev(self):
1199        """Go one step back in history"""
1200        self.currHistStep += 1
1201        self.currHistStepData.clear()
1202        self.currHistStepData = self._getHistStepData(self.currHistStep)
1203
1204        return self.currHistStepData
1205
1206    def GetStepsNum(self):
1207        """Get number of steps saved in history"""
1208        return self.histStepsNum
1209
1210    def GetCurrHistStep(self):
1211        """Get current history step"""
1212        return self.currHistStep
1213
1214    def Add(self, key, subkey, value):
1215        """Add new data into buffer"""
1216        if key not in self.newHistStepData:
1217            self.newHistStepData[key] = {}
1218
1219        if isinstance(subkey, list):
1220            if subkey[0] not in self.newHistStepData[key]:
1221                self.newHistStepData[key][subkey[0]] = {}
1222            self.newHistStepData[key][subkey[0]][subkey[1]] = value
1223        else:
1224            self.newHistStepData[key][subkey] = value
1225
1226    def SaveHistStep(self):
1227        """Create new history step with data in buffer"""
1228        self.maxHistSteps = UserSettings.Get(group='vnet',
1229                                             key='other',
1230                                             subkey='max_hist_steps')
1231        self.currHistStep = 0
1232
1233        newHistFile = grass.tempfile()
1234        newHist = open(newHistFile, "w")
1235
1236        self._saveNewHistStep(newHist)
1237
1238        oldHist = open(self.histFile)
1239        removedHistData = self._savePreviousHist(newHist, oldHist)
1240
1241        oldHist.close()
1242        newHist.close()
1243        try_remove(self.histFile)
1244        self.histFile = newHistFile
1245
1246        self.newHistStepData.clear()
1247
1248        return removedHistData
1249
1250    def _savePreviousHist(self, newHist, oldHist):
1251        """Save previous history into new file"""
1252        newHistStep = False
1253        removedHistData = {}
1254        newHistStepsNum = self.histStepsNum
1255
1256        for line in oldHist.readlines():
1257            if not line.strip():
1258                newHistStep = True
1259                newHistStepsNum += 1
1260                continue
1261
1262            if newHistStep:
1263                newHistStep = False
1264
1265                line = line.split("=")
1266                line[1] = str(newHistStepsNum)
1267                line = "=".join(line)
1268
1269                if newHistStepsNum >= self.maxHistSteps:
1270                    removedHistStep = removedHistData[line] = {}
1271                    continue
1272                else:
1273                    newHist.write('%s%s%s' % ('\n', line, '\n'))
1274                    self.histStepsNum = newHistStepsNum
1275            else:
1276                if newHistStepsNum >= self.maxHistSteps:
1277                    self._parseLine(line, removedHistStep)
1278                else:
1279                    newHist.write('%s' % line)
1280
1281        return removedHistData
1282
1283    def _saveNewHistStep(self, newHist):
1284        """Save buffer (new step) data into file"""
1285        newHist.write('%s%s%s' % ('\n', "history step=0", '\n'))
1286        for key in list(self.newHistStepData.keys()):
1287            subkeys = list(self.newHistStepData[key].keys())
1288            newHist.write('%s%s' % (key, self.sep))
1289            for idx in range(len(subkeys)):
1290                value = self.newHistStepData[key][subkeys[idx]]
1291                if isinstance(value, dict):
1292                    if idx > 0:
1293                        newHist.write('%s%s%s' % ('\n', key, self.sep))
1294                    newHist.write('%s%s' % (subkeys[idx], self.sep))
1295                    kvalues = list(self.newHistStepData[key][subkeys[idx]].keys())
1296                    srange = range(len(kvalues))
1297                    for sidx in srange:
1298                        svalue = self._parseValue(
1299                            self.newHistStepData[key][
1300                                subkeys[idx]][
1301                                kvalues[sidx]])
1302                        newHist.write(
1303                            '%s%s%s' %
1304                            (kvalues[sidx], self.sep, svalue))
1305                        if sidx < len(kvalues) - 1:
1306                            newHist.write('%s' % self.sep)
1307                else:
1308                    if idx > 0 and isinstance(
1309                            self.newHistStepData[key][subkeys[idx - 1]],
1310                            dict):
1311                        newHist.write('%s%s%s' % ('\n', key, self.sep))
1312                    value = self._parseValue(
1313                        self.newHistStepData[key][subkeys[idx]])
1314                    newHist.write('%s%s%s' % (subkeys[idx], self.sep, value))
1315                    if idx < len(subkeys) - 1 and not isinstance(
1316                            self.newHistStepData[key][subkeys[idx + 1]],
1317                            dict):
1318                        newHist.write('%s' % self.sep)
1319            newHist.write('\n')
1320        self.histStepsNum = 0
1321
1322    def _parseValue(self, value, read=False):
1323        """Parse value"""
1324        if read:  # -> read data (cast values)
1325
1326            if value:
1327                if value[
1328                        0] == '[' and value[-1] == ']':  # TODO, possible wrong interpretation
1329                    value = value[1:-1].split(',')
1330                    value = map(self._castValue, value)
1331                    return value
1332
1333            if value == 'True':
1334                value = True
1335            elif value == 'False':
1336                value = False
1337            elif value == 'None':
1338                value = None
1339            elif ':' in value:  # -> color
1340                try:
1341                    value = tuple(map(int, value.split(':')))
1342                except ValueError:  # -> string
1343                    pass
1344            else:
1345                try:
1346                    value = int(value)
1347                except ValueError:
1348                    try:
1349                        value = float(value)
1350                    except ValueError:
1351                        pass
1352        else:  # -> write data
1353            if isinstance(value, type(())):  # -> color
1354                value = str(value[0]) + ':' +\
1355                    str(value[1]) + ':' + \
1356                    str(value[2])
1357
1358        return value
1359
1360    def _castValue(self, value):
1361        """Cast value"""
1362        try:
1363            value = int(value)
1364        except ValueError:
1365            try:
1366                value = float(value)
1367            except ValueError:
1368                value = value[1:-1]
1369
1370        return value
1371
1372    def _getHistStepData(self, histStep):
1373        """Load data saved in history step"""
1374        hist = open(self.histFile)
1375        histStepData = {}
1376
1377        newHistStep = False
1378        isSearchedHistStep = False
1379        for line in hist.readlines():
1380
1381            if not line.strip() and isSearchedHistStep:
1382                break
1383            elif not line.strip():
1384                newHistStep = True
1385                continue
1386            elif isSearchedHistStep:
1387                self._parseLine(line, histStepData)
1388
1389            if newHistStep:
1390                line = line.split("=")
1391                if int(line[1]) == histStep:
1392                    isSearchedHistStep = True
1393                newHistStep = False
1394
1395        hist.close()
1396        return histStepData
1397
1398    def _parseLine(self, line, histStepData):
1399        """Parse line in file with history"""
1400        line = line.rstrip('%s' % os.linesep).split(self.sep)
1401        key = line[0]
1402        kv = line[1:]
1403        idx = 0
1404        subkeyMaster = None
1405        if len(kv) % 2 != 0:  # multiple (e.g. nviz)
1406            subkeyMaster = kv[0]
1407            del kv[0]
1408        idx = 0
1409        while idx < len(kv):
1410            if subkeyMaster:
1411                subkey = [subkeyMaster, kv[idx]]
1412            else:
1413                subkey = kv[idx]
1414            value = kv[idx + 1]
1415            value = self._parseValue(value, read=True)
1416            if key not in histStepData:
1417                histStepData[key] = {}
1418
1419            if isinstance(subkey, list):
1420                if subkey[0] not in histStepData[key]:
1421                    histStepData[key][subkey[0]] = {}
1422                histStepData[key][subkey[0]][subkey[1]] = value
1423            else:
1424                histStepData[key][subkey] = value
1425            idx += 2
1426
1427    def DeleteNewHistStepData(self):
1428        """Delete buffer data for new history step"""
1429        self.newHistStepData.clear()
1430
1431
1432class VNETGlobalTurnsData:
1433    """Turn Data"""
1434
1435    def __init__(self):
1436        # Definition of four basic directions
1437        self.turn_data = [
1438            ["Straight", DegreesToRadians(-30), DegreesToRadians(+30), 0.0],
1439            ["Right Turn", DegreesToRadians(+30), DegreesToRadians(+150), 0.0],
1440            ["Reverse", DegreesToRadians(+150), DegreesToRadians(-150), 0.0],
1441            ["Left Turn", DegreesToRadians(-150), DegreesToRadians(-30), 0.0]
1442        ]
1443
1444    def GetData(self):
1445        data = []
1446        for ival in self.turn_data:
1447            data.append(ival[1:])
1448
1449        return data
1450
1451    def GetValue(self, line, col):
1452        return self.turn_data[line][col]
1453
1454    def GetLinesCount(self):
1455        return len(self.turn_data)
1456
1457    def SetValue(self, value, line, col):
1458        self.DataValidator(line, col, value)
1459        self.turn_data[line][col] = value
1460
1461    def SetUTurns(self, value):
1462        """Checked if checeBox is checed"""
1463        useUTurns = value
1464
1465    def AppendRow(self, values):
1466        self.turn_data.append(values)
1467
1468    def InsertRow(self, line, values):
1469        self.turn_data.insert(line, values)
1470
1471    def PopRow(self, values):
1472        self.RemoveDataValidator(values)
1473        self.turn_data.pop(values)
1474
1475    def DataValidator(self, row, col, value):
1476        """Angle recalculation due to value changing"""
1477
1478        if col not in [1, 2]:
1479            return
1480
1481        if col == 1:
1482            new_from_angle = value
1483            old_from_angle = self.turn_data[row][1]
1484            new_to_angle = self.turn_data[row][2]
1485            if self.IsInInterval(old_from_angle, new_to_angle, new_from_angle):
1486
1487                prev_row = row - 1
1488                if prev_row == -1:
1489                    prev_row = len(self.turn_data) - 1
1490                self.turn_data[prev_row][2] = new_from_angle
1491                return
1492
1493        if col == 2:
1494            new_to_angle = value
1495            old_to_angle = self.turn_data[row][2]
1496            new_from_angle = self.turn_data[row][1]
1497            if self.IsInInterval(new_from_angle, old_to_angle, new_to_angle):
1498
1499                next_row = row + 1
1500                if len(self.turn_data) == next_row:
1501                    next_row = 0
1502                self.turn_data[next_row][1] = new_to_angle
1503                return
1504
1505        inside_new = []
1506        overlap_new_from = []
1507        overlap_new_to = []
1508
1509        for i in range(self.GetLinesCount()):
1510            if i == row:
1511                continue
1512            from_angle = self.turn_data[i][1]
1513            is_in_from = self.IsInInterval(
1514                new_from_angle, new_to_angle, from_angle)
1515
1516            to_angle = self.turn_data[i][2]
1517            is_in_to = self.IsInInterval(
1518                new_from_angle, new_to_angle, to_angle)
1519
1520            if is_in_from and is_in_to:
1521                inside_new.append(i)
1522            if is_in_from:
1523                overlap_new_to.append(i)
1524            if is_in_to:
1525                overlap_new_from.append(i)
1526
1527        for i_row in overlap_new_from:
1528            self.turn_data[i_row][2] = new_from_angle
1529
1530        for i_row in overlap_new_to:
1531            self.turn_data[i_row][1] = new_to_angle
1532
1533        for i_row in inside_new:
1534            if col == 1:
1535                angle = new_from_angle
1536            else:
1537                angle = new_to_angle
1538
1539            self.turn_data[i_row][1] = angle
1540            self.turn_data[i_row][2] = angle
1541
1542    def RemoveDataValidator(self, row):
1543        """Angle recalculation due to direction remove"""
1544        if row == 0:
1545            prev_row = self.GetLinesCount() - 1
1546        else:
1547            prev_row = row - 1
1548
1549        remove_to_angle = self.turn_data[row][2]
1550        self.turn_data[prev_row][2] = remove_to_angle
1551
1552    def IsInInterval(self, from_angle, to_angle, angle):
1553        """Test if a direction includes or not includes a value"""
1554        if to_angle < from_angle:
1555            to_angle = math.pi * 2 + to_angle
1556        if angle < from_angle:
1557            angle = math.pi * 2 + angle
1558
1559        if angle > from_angle and angle < to_angle:
1560            return True
1561        return False
1562