"""!
@package vdigit.wxdigit

@brief wxGUI vector digitizer (base class)

Code based on wxVdigit C++ component from GRASS 6.4.0
(gui/wxpython/vdigit). Converted to Python in 2010/12-2011/01.

List of classes:
 - wxdigit::VDigitError
 - wxdigit::IVDigit

@todo Read large amounts of data from Vlib into arrays, which could
then be processed using NumPy and rendered using glDrawArrays or
glDrawElements, so no per-line/per-vertex processing in Python. Bulk
data processing with NumPy is much faster than iterating in Python
(and NumPy would be an excellent candidate for acceleration via
e.g. OpenCL or CUDA; I'm surprised it hasn't happened already).

(C) 2007-2011 by the GRASS Development Team

This program is free software under the GNU General Public License
(>=v2). Read the file COPYING that comes with GRASS for details.

@author Martin Landa <landa.martin gmail.com>
"""

import grass.script.core as grass

from core.gcmd        import GError
from core.debug       import Debug
from core.settings    import UserSettings
from vdigit.wxdisplay import DisplayDriver

try:
    from grass.lib.gis    import *
    from grass.lib.vector import *
    from grass.lib.vedit  import *
    from grass.lib.dbmi   import *
except ImportError:
    pass

class VDigitError:
    def __init__(self, parent):
        """!Class for managing error messages of vector digitizer

        @param parent parent window for dialogs
        """
        self.parent  = parent
        self.caption = _('Digitization Error')
    
    def NoMap(self, name = None):
        """!No map for editing"""
        if name:
            message = _('Unable to open vector map <%s>.') % name
        else:
            message =  _('No vector map open for editing.')
        GError(message + ' ' + _('Operation canceled.'),
               parent  = self.parent,
               caption = self.caption)

    def WriteLine(self):
        """!Writing line failed
        """
        GError(message = _('Writing new feature failed. '
                           'Operation cancelled.'),
               parent  = self.parent,
               caption = self.caption)

    def ReadLine(self, line):
        """!Reading line failed
        """
        GError(message = _('Reading feature id %d failed. '
                           'Operation canceled.') % line,
               parent  = self.parent,
               caption = self.caption)

    def DbLink(self, dblink):
        """!No dblink available
        """
        GError(message = _('Database link %d not available. '
                           'Operation canceled.') % dblink,
               parent  = self.parent,
               caption = self.caption)

    def Driver(self, driver):
        """!Staring driver failed
        """
        GError(message = _('Unable to start database driver <%s>. '
                           'Operation canceled.') % driver,
               parent  = self.parent,
               caption = self.caption)

    def Database(self, driver, database):
        """!Opening database failed
        """
        GError(message = _('Unable to open database <%(db)s> by driver <%(driver)s>. '
                           'Operation canceled.') % { 'db' : database, 'driver' : driver},
               parent  = self.parent,
               caption = self.caption)

    def DbExecute(self, sql):
        """!Sql query failed
        """
        GError(message = _("Unable to execute SQL query '%s'. "
                           "Operation canceled.") % sql,
               parent  = self.parent,
               caption = self.caption)

    def DeadLine(self, line):
        """!Dead line
        """
        GError(message = _("Feature id %d is marked as dead. "
                           "Operation canceled.") % line,
               parent  = self.parent,
               caption = self.caption)

    def FeatureType(self, ftype):
        """!Unknown feature type
        """
        GError(message = _("Unsupported feature type %d. "
                           "Operation canceled.") % ftype,
               parent  = self.parent,
               caption = self.caption)
        
class IVDigit:
    def __init__(self, mapwindow):
        """!Base class for vector digitizer (ctypes interface)
        
        @parem mapwindow reference for map window (BufferedWindow)
        """
        self.poMapInfo   = None      # pointer to Map_info
        self.mapWindow = mapwindow

        # background map
        self.bgMapInfo   = Map_info()
        self.poBgMapInfo = self.popoBgMapInfo = None
        
        if not mapwindow.parent.IsStandalone():
            goutput = mapwindow.parent.GetLayerManager().GetLogWindow()
            log = goutput.GetLog(err = True)
            progress = goutput.GetProgressBar()
        else:
            log = sys.stderr
            progress = None
        
        self.toolbar = mapwindow.parent.toolbars['vdigit']
        
        self._error   = VDigitError(parent = self.mapWindow)
        
        self._display = DisplayDriver(device    = mapwindow.pdcVector,
                                      deviceTmp = mapwindow.pdcTmp,
                                      mapObj    = mapwindow.Map,
                                      window    = mapwindow,
                                      glog      = log,
                                      gprogress = progress)
        
        # GRASS lib
        self.poPoints = Vect_new_line_struct()
        self.poCats   = Vect_new_cats_struct()
        
        # self.SetCategory()
        
        # layer / max category
        self.cats = dict()
        
        self._settings = dict()
        self.UpdateSettings() # -> self._settings
        
        # undo/redo
        self.changesets = dict()
        self.changesetCurrent = -1 # first changeset to apply
        self.changesetEnd     = -1 # last changeset to be applied
        
        if self.poMapInfo:
            self.InitCats()
        
    def __del__(self):
        Debug.msg(1, "IVDigit.__del__()")
        Vect_destroy_line_struct(self.poPoints)
        self.poPoints = None
        Vect_destroy_cats_struct(self.poCats)
        self.poCats = None
        
        if self.poBgMapInfo:
            Vect_close(self.poBgMapInfo)
            self.poBgMapInfo = self.popoBgMapInfo = None
            del self.bgMapInfo
        
    def CloseBackgroundMap(self):
        """!Close background vector map"""
        if not self.poBgMapInfo:
            return
        
        Vect_close(self.poBgMapInfo)
        self.poBgMapInfo = self.popoBgMapInfo = None
        
    def OpenBackgroundMap(self, bgmap):
        """!Open background vector map

        @todo support more background maps then only one
        
        @param bgmap name of vector map to be opened

        @return pointer to map_info
        @return None on error
        """
        name   = create_string_buffer(GNAME_MAX)
        mapset = create_string_buffer(GMAPSET_MAX)
        if not G__name_is_fully_qualified(bgmap, name, mapset):
            name   = str(bgmap)
            mapset = str(G_find_vector2(bgmap, ''))
        else:
            name   = str(name.value)
            mapset = str(mapset.value)
        
        if (name == Vect_get_name(self.poMapInfo) and \
                mapset == Vect_get_mapset(self.poMapInfo)):
            self.poBgMapInfo = self.popoBgMapInfo = None
            self._error.NoMap(bgmap)
            return
        
        self.poBgMapInfo = pointer(self.bgMapInfo)
        self.popoBgMapInfo = pointer(self.poBgMapInfo)
	if Vect_open_old(self.poBgMapInfo, name, mapset) == -1:
            self.poBgMapInfo = self.popoBgMapInfo = None
            self._error.NoMap(bgmap)
            return
        
    def _getSnapMode(self):
        """!Get snapping mode

         - snap to vertex
         - snap to nodes
         - no snapping
        
        @return snap mode
        """
        threshold = self._display.GetThreshold()
        if threshold > 0.0:
            if UserSettings.Get(group = 'vdigit', key = 'snapToVertex', subkey = 'enabled'):
                return SNAPVERTEX
            else:
                return SNAP
        else:
            return NO_SNAP
    
    def _breakLineAtIntersection(self, line, pointsLine, changeset):
        """!Break given line at intersection

        \param line line id
        \param pointsLine line geometry
        \param changeset id
  
        \return number of modified lines
        """
        if not self._checkMap():
            return -1
        
        if not Vect_line_alive(self.poMapInfo, line):
            return 0
        
        if not pointsLine:
            if Vect_read_line(self.poMapInfo, self.poPoints, None, line) < 0:
                self._error.ReadLine(line)
                return -1
            points = self.poPoints
        else:
            points = pointsLine
        
        listLine  = Vect_new_list()
        listRef   = Vect_new_list()
        listBreak = Vect_new_list()
    
        pointsCheck = Vect_new_line_struct()
    
        lineBox = bound_box()
        # find all relevant lines
        Vect_get_line_box(self.poMapInfo, line, byref(lineBox))
        Vect_select_lines_by_box(self.poMapInfo, byref(lineBox),
                                 GV_LINES, listLine)
    
        # check for intersection
        Vect_list_append(listBreak, line)
        Vect_list_append(listRef, line)
        for i in range(listLine.contents.n_values):
            lineBreak = listLine.contents.value[i]
            if lineBreak == line:
		continue
            
            ltype = Vect_read_line(self.poMapInfo, pointsCheck, None, lineBreak)
            if not (ltype & GV_LINES):
                continue
            
            if Vect_line_check_intersection(self.poPoints, pointsCheck,
                                            WITHOUT_Z):
                Vect_list_append(listBreak, lineBreak)
        
        nlines = Vect_get_num_lines(self.poMapInfo)
        
        for i in range(listBreak.contents.n_values):
            self._addActionToChangeset(changeset, listBreak.contents.value[i], add = False)
        
        ret = Vect_break_lines_list(self.poMapInfo, listBreak, listRef,
                                    GV_LINES, None)
        
        for i in range(listBreak.contents.n_values):
            if Vect_line_alive(self.poMapInfo, listBreak.contents.value[i]):
                self._removeActionFromChangeset(changeset, listBreak.contents.value[i],
                                                add = False)
        
        for line in range(nlines + 1, Vect_get_num_lines(self.poMapInfo) + 1):
            self._addActionToChangeset(changeset, line, add = True)
        
        Vect_destroy_line_struct(pointsCheck)

        Vect_destroy_list(listLine)
        Vect_destroy_list(listBreak)
        Vect_destroy_list(listRef)
        
        return ret
    
    def _addActionsBefore(self):
        """!Register action before operation
  
        @return changeset id
        """
        changeset = len(self.changesets)
        for line in self._display.selected['ids']:
            if Vect_line_alive(self.poMapInfo, line):
                self._addActionToChangeset(changeset, line, add = False)
        
        return changeset

    def _applyChangeset(self, changeset, undo):
        """!Apply changeset (undo/redo changeset)
        
        @param changeset changeset id
        @param undo True for undo otherwise redo

        @return 1 changeset applied
        @return 0 changeset not applied
        @return -1 on error
        """
        if changeset < 0 or changeset > len(self.changesets.keys()):
            return -1
        
        if self.changesetEnd < 0:
            self.changesetEnd = changeset
            
        ret = 0
        actions = self.changesets[changeset]
        for action in actions: 
            add = action['add']
            line = action['line']
            if (undo and add) or \
                    (not undo and not add):
                if Vect_line_alive(self.poMapInfo, line):
                    Debug.msg(3, "IVDigit._applyChangeset(): changeset=%d, action=add, line=%d -> deleted",
                              changeset, line)
                    Vect_delete_line(self.poMapInfo, line)
                    ret = 1
                else:
                    Debug.msg(3, "Digit.ApplyChangeset(): changeset=%d, action=add, line=%d dead",
                              changeset, line)
            else: # delete
                offset = action['offset']
                if not Vect_line_alive(self.poMapInfo, line):
                    Debug.msg(3, "Digit.ApplyChangeset(): changeset=%d, action=delete, line=%d -> added",
                              changeset, line)
                    if Vect_restore_line(self.poMapInfo, line, offset) < 0:
                        return -1
                    ret = 1
                else:
                    Debug.msg(3, "Digit.ApplyChangeset(): changeset=%d, action=delete, line=%d alive",
                              changeset, line)
        
        return ret
    
    def _addActionsAfter(self, changeset, nlines):
        """!Register action after operation

        @param changeset changeset id
        @param nline number of lines
        """
        for line in self._display.selected['ids']:
            if Vect_line_alive(self.poMapInfo, line):
                self._removeActionFromChangeset(changeset, line, add = False)
        
        for line in range(nlines + 1, Vect_get_num_lines(self.poMapInfo)):
            if Vect_line_alive(self.poMapInfo, line):
                self._addActionToChangeset(changeset, line, add = True)
        
    def _addActionToChangeset(self, changeset, line, add):
        """!Add action to changeset
        
        @param changeset id of changeset
        @param line feature id
        @param add True to add, otherwise delete
        """
        if not self._checkMap():
            return 
        
        if not Vect_line_alive(self.poMapInfo, line):
            return
        
        offset = Vect_get_line_offset(self.poMapInfo, line)
        
        if changeset not in self.changesets:
            self.changesets[changeset] = list()
            self.changesetCurrent = changeset
        
        self.changesets[changeset].append({ 'add'    : add,
                                            'line'   : line,
                                            'offset' : offset })
        
        Debug.msg(3, "IVDigit._addActionToChangeset(): changeset=%d, add=%d, line=%d, offset=%d",
                  changeset, add, line, offset)
        
    def _removeActionFromChangeset(self, changeset, line, add):
        """!Remove action from changeset
        
        @param changeset changeset id
        @param line line id
        @param add True for add, False for delete
        
        @return number of actions in changeset
        @return -1 on error
        """
        if changeset not in self.changesets.keys():
            return -1
        
        alist = self.changesets[changeset] 
        for action in alist:
            if action['add'] == add and action['line'] == line:
                alist.remove(action)
        
        return len(alist)

    def AddFeature(self, ftype, points):
        """!Add new feature
        
        @param ftype feature type (point, line, centroid, boundary)
        @param points tuple of points ((x, y), (x, y), ...)
        
        @return tuple (number of added features, feature ids)
        """
        if UserSettings.Get(group = 'vdigit', key = "categoryMode", subkey = 'selection') == 2:
            layer = -1 # -> no category
            cat   = -1
        else:
            layer = UserSettings.Get(group = 'vdigit', key = "layer", subkey = 'value')
            cat   = self.SetCategory()
        
        if ftype == 'point':
            vtype = GV_POINT
        elif ftype == 'line':
            vtype = GV_LINE
        elif ftype == 'centroid':
            vtype = GV_CENTROID
        elif ftype == 'boundary':
            vtype = GV_BOUNDARY
        elif ftype == 'area':
            vtype = GV_AREA
        else:
            GError(parent = self.mapWindow,
                   message = _("Unknown feature type '%s'") % ftype)
            return (-1, None)
        
        if vtype & GV_LINES and len(points) < 2:
            GError(parent = self.mapWindow,
                   message = _("Not enough points for line"))
            return (-1, None)
        
        self.toolbar.EnableUndo()
        
        return self._addFeature(vtype, points, layer, cat,
                                self._getSnapMode(), self._display.GetThreshold())
    
    def DeleteSelectedLines(self):
        """!Delete selected features

        @return number of deleted features
        """
        deleteRec = UserSettings.Get(group = 'vdigit', key = 'delRecord', subkey = 'enabled')
        if not self._checkMap():
            return -1
        
        n_dblinks = Vect_get_num_dblinks(self.poMapInfo)
        Cats_del = None
        
        # collect categories for delete if requested
        if deleteRec:
            poCatsDel = Vect_new_cats_struct()
            for i in self._display.selected['ids']:
                if Vect_read_line(self.poMapInfo, None, self.poCats, i) < 0:
                    Vect_destroy_cats_struct(poCatsDel)
                    self._error.ReadLine(i)
                    return -1
                
                cats = self.poCats.contents
                for j in range(cats.n_cats):
                    Vect_cat_set(poCatsDel, cats.field[j], cats.cat[j])
        
        # register changeset
        changeset = self._addActionsBefore()
        poList = self._display.GetSelectedIList()
        nlines = Vedit_delete_lines(self.poMapInfo, poList)
        
        Vect_destroy_list(poList)
        self._display.selected['ids'] = list()
        
        if nlines > 0 and deleteRec:
            handle  = dbHandle()
            poHandle = pointer(handle)
            stmt    = dbString()
            poStmt   = pointer(stmt)
            
            for dblink in range(n_dblinks):
                poFi = Vect_get_dblink(self.poMapInfo, dblink)
                if poFi is None:
                    self._error.DbLink(dblink)
                    return -1
                
                Fi = poFi.contents
                poDriver = db_start_driver(Fi.driver)
                if poDriver is None:
                    self._error.Driver(Fi.driver)
                    return -1
                
                db_init_handle(poHandle)
                db_set_handle(poHandle, Fi.database, None)
                if db_open_database(poDriver, poHandle) != DB_OK:
                    self._error.Database(Fi.driver, Fi.database)
                    return -1
                
                db_init_string(poStmt)
                db_set_string(poStmt, "DELETE FROM %s WHERE" % Fi.table)
                n_cats = 0;
                catsDel = poCatsDel.contents
                for c in range(catsDel.n_cats):
                    if catsDel.field[c] == Fi.number:
                        if n_cats > 0:
                            db_append_string(poStmt, " or")
                    
		    db_append_string(poStmt, " %s = %d" % (Fi.key, catsDel.cat[c]))
		    n_cats += 1
                
                Vect_cat_del(poCatsDel, Fi.number)
                
                if n_cats and \
                        db_execute_immediate(poDriver, poStmt) != DB_OK:
                    self._error.DbExecute(db_get_string(poStmt))
                    return -1
                
                db_close_database(poDriver)
                db_shutdown_driver(poDriver)
        
        if poCatsDel:
            Vect_destroy_cats_struct(poCatsDel)
        
        if nlines > 0:
            self.toolbar.EnableUndo()
        
        return nlines

    def MoveSelectedLines(self, move):
        """!Move selected features

        @param move direction (x, y)
        """
        if not self._checkMap():
            return -1
        
        thresh = self._display.GetThreshold()
        snap   = self._getSnapMode()
        
        nlines = Vect_get_num_lines(self.poMapInfo)
        
        # register changeset
        changeset = self._addActionsBefore()
        
        poList = self._display.GetSelectedIList()
        nlines = Vedit_move_lines(self.poMapInfo, self.popoBgMapInfo, int(self.poBgMapInfo is not None),
                                  poList,
                                  move[0], move[1], 0,
                                  snap, thresh)
        Vect_destroy_list(poList)
        
        if nlines > 0:
            self._addActionsAfter(changeset, nlines)
        else:
            del self.changesets[changeset]
        
        if nlines > 0 and self._settings['breakLines']:
            for i in range(1, nlines):
                self._breakLineAtIntersection(nlines + i, None, changeset)
        
        if nlines > 0:
            self.toolbar.EnableUndo()
        
        return nlines

    def MoveSelectedVertex(self, point, move):
        """!Move selected vertex of the line

        @param point location point
        @param move  x,y direction
        
        @return id of new feature
        @return 0 vertex not moved (not found, line is not selected)
        @return -1 on error
        """
        if not self._checkMap():
            return -1
        
        if len(self._display.selected['ids']) != 1:
            return -1
        
        Vect_reset_line(self.poPoints)
        Vect_append_point(self.poPoints, point[0], point[1], 0.0)
        
        nlines = Vect_get_num_lines(self.poMapInfo)
        
        changeset = self._addActionsBefore()
        
        # move only first found vertex in bbox 
        poList = self._display.GetSelectedIList()
        moved = Vedit_move_vertex(self.poMapInfo, self.popoBgMapInfo, int(self.poBgMapInfo is not None),
                                  poList, self.poPoints,
                                  self._display.GetThreshold(type = 'selectThresh'),
                                  self._display.GetThreshold(),
                                  move[0], move[1], 0.0,
                                  1, self._getSnapMode())
        Vect_destroy_list(poList)
        
        if moved > 0:
            self._addActionsAfter(changeset, nlines)
        else:
            del self.changesets[changeset]
        
        if moved > 0 and self._settings['breakLines']:
            self._breakLineAtIntersection(Vect_get_num_lines(self.poMapInfo),
                                          None, changeset)
        
        if moved > 0:
            self.toolbar.EnableUndo()
        
        return moved

    def AddVertex(self, coords):
        """!Add new vertex to the selected line/boundary on position 'coords'

        @param coords coordinates to add vertex

        @return id of new feature
        @return 0 nothing changed
        @return -1 on failure
        """
        added = self._ModifyLineVertex(coords, add = True)
        
        if added > 0:
            self.toolbar.EnableUndo()

        return added

    def RemoveVertex(self, coords):
        """!Remove vertex from the selected line/boundary on position 'coords'

        @param coords coordinates to remove vertex

        @return id of new feature
        @return 0 nothing changed
        @return -1 on failure
        """
        deleted = self._ModifyLineVertex(coords, add = False)
        
        if deleted > 0:
            self.toolbar.EnableUndo()

        return deleted


    def SplitLine(self, point):
        """!Split/break selected line/boundary on given position

        @param point point where to split line
        
        @return 1 line modified
        @return 0 nothing changed
        @return -1 error
        """
        thresh = self._display.GetThreshold('selectThresh')
        if not self._checkMap():
            return -1
        
        poList  = self._display.GetSelectedIList()
        Vect_reset_line(self.poPoints)
        Vect_append_point(self.poPoints, point[0], point[1], 0.0)
        
        nlines = Vect_get_num_lines(self.poMapInfo)
        
        changeset = self._addActionsBefore()
        
        ret = Vedit_split_lines(self.poMapInfo, poList,
                                self.poPoints, thresh, None)
        Vect_destroy_list(poList)
        
        if ret > 0:
            self._addActionsAfter(changeset, nlines)
            self.toolbar.EnableUndo()
        else:
            del self.changesets[changeset]
        
        return ret

    def EditLine(self, line, coords):
        """!Edit existing line/boundary

        @param line feature id to be modified
        @param coords list of coordinates of modified line

        @return feature id of new line
        @return -1 on error
        """
        if not self._checkMap():
            return -1
        
        if len(coords) < 2:
            self.DeleteSelectedLines()
            return 0
        
        if not Vect_line_alive(self.poMapInfo, line):
            self._error.DeadLine(line)
            return -1
        
        # read original feature
        ltype = Vect_read_line(self.poMapInfo, None, self.poCats, line)
        if ltype < 0:
            self._error.ReadLine(line)
            return -1
        
        # build feature geometry
        Vect_reset_line(self.poPoints)
        for p in coords:
            Vect_append_point(self.poPoints, p[0], p[1], 0.0)

        # apply snapping (node or vertex)
        snap = self._getSnapMode()
        if snap != NO_SNAP:
            modeSnap = not (snap == SNAP)
            Vedit_snap_line(self.poMapInfo, self.popoBgMapInfo,
                            int(self.poBgMapInfo is not None),
                           -1, self.poPoints, self._display.GetThreshold(), modeSnap)

        nlines = Vect_get_num_lines(self.poMapInfo)
        
        changeset = self._addActionsBefore()
        newline = Vect_rewrite_line(self.poMapInfo, line, ltype,
                                    self.poPoints, self.poCats)
        if newline > 0:
            self._addActionsAfter(changeset, nlines)
            self.toolbar.EnableUndo()
        else:
            del self.changesets[changeset]
        
        if newline > 0 and self._settings['breakLines']:
            self._breakLineAtIntersection(newline, None, changeset)
        
        return newline

    def FlipLine(self):
        """!Flip selected lines/boundaries

        @return number of modified lines
        @return -1 on error
        """
        if not self._checkMap():
            return -1
        
        nlines = Vect_get_num_lines(self.poMapInfo)
        
        # register changeset
        changeset = self._addActionsBefore()
        
        poList = self._display.GetSelectedIList()
        ret = Vedit_flip_lines(self.poMapInfo, poList)
        Vect_destroy_list(poList)
        
        if ret > 0:
            self._addActionsAfter(changeset, nlines)
            self.toolbar.EnableUndo()
        else:
            del self.changesets[changeset]
        
        return ret

    def MergeLine(self):
        """!Merge selected lines/boundaries

        @return number of modified lines
        @return -1 on error
        """
        if not self._checkMap():
            return -1
        
        nlines = Vect_get_num_lines(self.poMapInfo)
        
        changeset = self._addActionsBefore()
        
        poList = self._display.GetSelectedIList()
        ret = Vedit_merge_lines(self.poMapInfo, poList)
        Vect_destroy_list(poList)
        
        if ret > 0:
            self._addActionsAfter(changeset, nlines)
            self.toolbar.EnableUndo()
        else:
            del self.changesets[changeset]
                
        return ret

    def BreakLine(self):
        """!Break selected lines/boundaries

        @return number of modified lines
        @return -1 on error
        """
        if not self._checkMap():
            return -1
        
        nlines = Vect_get_num_lines(self.poMapInfo)
        
        changeset = self._addActionsBefore()
        
        poList = self._display.GetSelectedIList()
        ret = Vect_break_lines_list(self.poMapInfo, poList, None,
                                    GV_LINES, None)
        Vect_destroy_list(poList)
        
        if ret > 0:
            self._addActionsAfter(changeset, nlines)
            self.toolbar.EnableUndo()
        else:
            del self.changesets[changeset]
                
        return ret

    def SnapLine(self):
        """!Snap selected lines/boundaries

        @return on success
        @return -1 on error
        """
        if not self._checkMap():
            return -1
        
        nlines = Vect_get_num_lines(self.poMapInfo)
        
        changeset = self._addActionsBefore()
        
        poList = self._display.GetSelectedIList()
        Vect_snap_lines_list(self.poMapInfo, poList,
                             self._display.GetThreshold(), None)
        Vect_destroy_list(poList)
        
        if nlines < Vect_get_num_lines(self.poMapInfo):
            self._addActionsAfter(changeset, nlines)
            self.toolbar.EnableUndo()
        else:
            del self.changesets[changeset]
        
    def ConnectLine(self):
        """!Connect selected lines/boundaries

        @return 1 lines connected
        @return 0 lines not connected
        @return -1 on error
        """
        if not self._checkMap():
            return -1
        
        nlines = Vect_get_num_lines(self.poMapInfo)
        
        # register changeset
        changeset = self._addActionsBefore()
        
        poList = self._display.GetSelectedIList()
        ret = Vedit_connect_lines(self.poMapInfo, poList,
                                  self._display.GetThreshold())
        Vect_destroy_list(poList)
        
        if ret > 0:
            self._addActionsAfter(changeset, nlines)
            self.toolbar.EnableUndo()
        else:
            del self.changesets[changeset]
        
        return ret
        
    def CopyLine(self, ids = []):
        """!Copy features from (background) vector map

        @param ids list of line ids to be copied

        @return number of copied features
        @return -1 on error
        """
        if not self._checkMap():
            return -1
        
        nlines = Vect_get_num_lines(self.poMapInfo)
        
        poList = self._display.GetSelectedIList(ids)
        ret = Vedit_copy_lines(self.poMapInfo, self.poBgMapInfo,
                               poList)
        Vect_destroy_list(poList)
        
        if ret > 0:
            changeset = len(self.changesets)
            for line in (range(nlines + 1, Vect_get_num_lines(self.poMapInfo))):
                self._addActionToChangeset(changeset, line, add = True)
            self.toolbar.EnableUndo()
        else:
            del self.changesets[changeset]

        if ret > 0 and self.poBgMapInfo and self._settings['breakLines']:
            for i in range(1, ret):
                self._breakLineAtIntersection(nlines + i, None, changeset)
        
        return ret

    def CopyCats(self, fromId, toId, copyAttrb = False):
        """!Copy given categories to objects with id listed in ids

        @param cats ids of 'from' feature
        @param ids  ids of 'to' feature(s)

        @return number of modified features
        @return -1 on error
        """
        if len(fromId) < 1 or len(toId) < 1:
            return 0
        
        poCatsFrom = self.poCats
        poCatsTo = Vect_new_cats_struct();
        
        nlines = 0
        
        for fline in fromId:
            if not Vect_line_alive(self.poMapInfo, fline):
                continue
            
            if Vect_read_line(self.poMapInfo, None, poCatsFrom, fline) < 0:
                self._error.ReadLine(fline)
                return -1
            
            for tline in toId:
                if not Vect_line_alive(self.poMapInfo, tline):
                    continue
                
                ltype = Vect_read_line(self.poMapInfo, self.poPoints, poCatsTo, tline)
                if ltype < 0:
                    self._error.ReadLine(fline)
                    return -1
                
                catsFrom = poCatsFrom.contents
                for i in range(catsFrom.n_cats):
                    if not copyAttrb:
                        # duplicate category
                        cat = catsFrom.cat[i]
                    else:
                        # duplicate attributes
                        cat = self.cats[catsFrom.field[i]] + 1
                        self.cats[catsFrom.field[i]] = cat
                        poFi = Vect_get_field(self.poMapInfo, catsFrom.field[i])
                        if not poFi:
                            self._error.DbLink(i)
                            return -1
                        
                        fi = poFi.contents
                        driver = db_start_driver(fi.driver)
                        if not driver:
                            self._error.Driver(fi.driver)
                            return -1
                        
                        handle = dbHandle()
                        db_init_handle(byref(handle))
                        db_set_handle(byref(handle), fi.database, None)
                        if db_open_database(driver, byref(handle)) != DB_OK:
                            db_shutdown_driver(driver)
                            self._error.Database(fi.driver, fi.database)
                            return -1
                        
                        stmt = dbString()
                        db_init_string(byref(stmt))
                        db_set_string(byref(stmt),
                                      "SELECT * FROM %s WHERE %s=%d" % (fi.table, fi.key,
                                                                        catsFrom.cat[i]))
                        
                        cursor = dbCursor()
                        if db_open_select_cursor(driver, byref(stmt), byref(cursor),
                                                 DB_SEQUENTIAL) != DB_OK:
                                db_close_database_shutdown_driver(driver)
                                return -1
                        
                        table = db_get_cursor_table(byref(cursor))
                        ncols = db_get_table_number_of_columns(table)
                        
                        sql = "INSERT INTO %s VALUES (" % fi.table
                        # fetch the data
                        more = c_int()
                        while True:
                            if db_fetch(byref(cursor), DB_NEXT, byref(more)) != DB_OK:
                                db_close_database_shutdown_driver(driver)
                                return -1
                            if not more.value:
                                break
                            
                            value_string = dbString()
                            for col in range(ncols):
                                if col > 0:
                                    sql += ","
                                    
                                column = db_get_table_column(table, col)
                                if db_get_column_name(column) == fi.key:
                                    sql += "%d" % cat
                                    continue
                                
                                value = db_get_column_value(column)
                                db_convert_column_value_to_string(column, byref(value_string))
                                if db_test_value_isnull(value):
                                    sql += "NULL"
                                else:
                                    ctype = db_sqltype_to_Ctype(db_get_column_sqltype(column))
                                    if ctype != DB_C_TYPE_STRING:
                                        sql += db_get_string(byref(value_string))
                                    else:
                                        sql += "'%s'" % db_get_string(byref(value_string))
                        
                        sql += ")"
                        db_set_string(byref(stmt), sql)
                        if db_execute_immediate(driver, byref(stmt)) != DB_OK:
                            db_close_database_shutdown_driver(driver)
                            return -1
                        
                        db_close_database_shutdown_driver(driver)
                        G_free(poFi)
                
                if Vect_cat_set(poCatsTo, catsFrom.field[i], cat) < 1:
                    continue
                
                if Vect_rewrite_line(self.poMapInfo, tline, ltype, self.poPoints, poCatsTo) < 0:
                    self._error.WriteLine()
                    return -1
                
                nlines +=1
        
        Vect_destroy_cats_struct(poCatsTo)
        
        if nlines > 0:
            self.toolbar.EnableUndo()
        
        return nlines

    def _selectLinesByQueryThresh(self):
        """!Generic method used for SelectLinesByQuery() -- to get
        threshold value"""
        thresh = 0.0
        if UserSettings.Get(group = 'vdigit', key = 'query', subkey = 'selection') == 0:
            thresh = UserSettings.Get(group = 'vdigit', key = 'queryLength', subkey = 'thresh')
            if UserSettings.Get(group = 'vdigit', key = "queryLength", subkey = 'than-selection') == 0:
                thresh = -1 * thresh
        else:
            thresh = UserSettings.Get(group = 'vdigit', key = 'queryDangle', subkey = 'thresh')
            if UserSettings.Get(group = 'vdigit', key = "queryDangle", subkey = 'than-selection') == 0:
                thresh = -1 * thresh
        
        return thresh

    def SelectLinesByQuery(self, bbox):
        """!Select features by query
        
        @todo layer / 3D
        
        @param bbox bounding box definition
        """
        if not self._checkMap():
            return -1
        
        thresh = self._selectLinesByQueryThresh()
        
        query = QUERY_UNKNOWN
        if UserSettings.Get(group = 'vdigit', key = 'query', subkey = 'selection') == 0:
            query = QUERY_LENGTH
        else:
            query = QUERY_DANGLE
        
        ftype = GV_POINTS | GV_LINES # TODO: 3D
        layer = 1 # TODO
        
        ids = list()
        poList = Vect_new_list()
        coList = poList.contents
        if UserSettings.Get(group = 'vdigit', key = 'query', subkey = 'box'):
            Vect_reset_line(self.poPoints)
            x1, y1 = bbox[0]
            x2, y2 = bbox[1]
            z1 = z2 = 0.0
            
            Vect_append_point(self.poPoints, x1, y1, z1)
            Vect_append_point(self.poPoints, x2, y1, z2)
            Vect_append_point(self.poPoints, x2, y2, z1)
            Vect_append_point(self.poPoints, x1, y2, z2)
            Vect_append_point(self.poPoints, x1, y1, z1)
	
            Vect_select_lines_by_polygon(self.poMapInfo, self.poPoints, 0, None,
                                         ftype, poList)
            
            if coList.n_values == 0:
                return ids
        
        Vedit_select_by_query(self.poMapInfo,
                              ftype, layer, thresh, query,
                              poList)
        
        for i in range(coList.n_values):
            ids.append(int(coList.value[i]))
            
        Debug.msg(3, "IVDigit.SelectLinesByQuery(): lines=%d", coList.n_values)    
        Vect_destroy_list(poList)
        
        return ids

    def IsVector3D(self):
        """!Check if open vector map is 3D
        """
        if not self._checkMap():
            return False
        
        return Vect_is_3d(self.poMapInfo)
    
    def GetLineLength(self, line):
        """!Get line length

        @param line feature id

        @return line length
        @return -1 on error
        """
        if not self._checkMap():
            return -1
        
        if not Vect_line_alive(self.poMapInfo, line):
            return -1
    
        ltype = Vect_read_line(self.poMapInfo, self.poPoints, None, line)
        if ltype < 0:
            self._error.ReadLine(line)
            return ret
        
        length = -1
        if ltype & GV_LINES: # lines & boundaries
            length = Vect_line_length(self.poPoints)
        
        return length

    def GetAreaSize(self, centroid):
        """!Get area size

        @param centroid centroid id

        @return area size
        @return -1 on error
        """
        if not self._checkMap():
            return -1
        
        ltype = Vect_read_line(self.poMapInfo, None, None, centroid)
        if ltype < 0:
            self._error.ReadLine(line)
            return ret
        
        if ltype != GV_CENTROID:
            return -1
        
        area = Vect_get_centroid_area(self.poMapInfo, centroid)
        size = -1
        if area > 0:
            if not Vect_area_alive(self.poMapInfo, area):
                return size
            
            size = Vect_get_area_area(self.poMapInfo, area)
        
        return size
        
    def GetAreaPerimeter(self, centroid):
        """!Get area perimeter
        
        @param centroid centroid id
        
        @return area size
        @return -1 on error
        """
        if not self._checkMap():
            return -1
        
        ltype = Vect_read_line(self.poMapInfo, None, None, centroid)
        if ltype < 0:
            self._error.ReadLine(line)
            return ret
        
        if ltype != GV_CENTROID:
            return -1
        
        area = Vect_get_centroid_area(self.poMapInfo, centroid)
        perimeter = -1
        if area > 0:
            if not Vect_area_alive(self.poMapInfo, area):
                return -1
            
            Vect_get_area_points(self.poMapInfo, area, self.poPoints)
            perimeter = Vect_area_perimeter(self.poPoints)
        
        return perimeter
    
    def SetLineCats(self, line, layer, cats, add = True):
        """!Set categories for given line and layer

        @param line feature id
        @param layer layer number (-1 for first selected line)
        @param cats list of categories
        @param add if True to add, otherwise do delete categories

        @return new feature id (feature need to be rewritten)
        @return -1 on error
        """
        if not self._checkMap():
            return -1
        
        if line < 1 and len(self._display.selected['ids']) < 1:
            return -1
        
        update = False
        if line == -1:
            update = True
            line = self._display.selected['ids'][0]
	
        if not Vect_line_alive(self.poMapInfo, line):
            return -1
        
        ltype = Vect_read_line(self.poMapInfo, self.poPoints, self.poCats, line)
        if ltype < 0:
            self._error.ReadLine(line)
            return -1
        
        for c in cats:
            if add:
                Vect_cat_set(self.poCats, layer, c)
            else:
                Vect_field_cat_del(self.poCats, layer, c)
        
        nlines = Vect_get_num_lines(self.poMapInfo)
        changeset = self._addActionsBefore()
        newline = Vect_rewrite_line(self.poMapInfo, line, ltype,
                                    self.poPoints, self.poCats)
        
        if newline > 0:
            self._addActionsAfter(changeset, nlines)
            self.toolbar.EnableUndo()
        
        if update:
            # update line id since the line was rewritten
            self._display.selected['ids'][0] =  newline
        
        return newline

    def TypeConvForSelectedLines(self):
        """!Feature type conversion for selected objects.

        Supported conversions:
         - point <-> centroid
         - line <-> boundary

        @return number of modified features
        @return -1 on error
        """
        if not self._checkMap():
            return -1
        
        nlines = Vect_get_num_lines(self.poMapInfo)
        
        # register changeset
        changeset = self._addActionsBefore()
        
        poList = self._display.GetSelectedIList()
        ret = Vedit_chtype_lines(self.poMapInfo, poList)
        Vect_destroy_list(poList)
        
        if ret > 0:
            self._addActionsAfter(changeset, nlines)
            self.toolbar.EnableUndo()
        else:
            del self.changesets[changeset]
        
        return ret

    def Undo(self, level = -1):
        """!Undo action

        @param level levels to undo (0 to revert all)

        @return id of current changeset
        """
        changesetLast = len(self.changesets.keys()) - 1

        if changesetLast < 0:
            return changesetLast
        
        if self.changesetCurrent == -2: # value uninitialized 
            self.changesetCurrent = changesetLast
            
        if level > 0 and self.changesetCurrent < 0:
            self.changesetCurrent = 0
        
        if level == 0:
            # 0 -> undo all
            level = -1 * changesetLast + 1

        Debug.msg(2, "Digit.Undo(): changeset_last=%d, changeset_current=%d, level=%d",
                  changesetLast, self.changesetCurrent, level)
    
        if level < 0: # undo
            if self.changesetCurrent + level < -1:
                return changesetCurrent;
            for changeset in range(self.changesetCurrent, self.changesetCurrent + level, -1):
                self._applyChangeset(changeset, undo = True)
        elif level > 0: # redo 
            if self.changesetCurrent + level > len(self.changesets.keys()):
                return self.changesetCurrent
            for changeset in range(self.changesetCurrent, self.changesetCurrent + level):
                self._applyChangeset(changeset, undo = False)
        
        self.changesetCurrent += level

        Debug.msg(2, "Digit.Undo(): changeset_current=%d, changeset_last=%d, changeset_end=%d",
                  self.changesetCurrent, changesetLast, self.changesetEnd)
        
        if self.changesetCurrent == self.changesetEnd:
            self.changesetEnd = changesetLast
            return -1
        
        self.mapWindow.UpdateMap(render = False)
        
        if self.changesetCurrent < 0: # disable undo tool
            self.toolbar.EnableUndo(False)

    def ZBulkLines(self, pos1, pos2, start, step):
        """!Z-bulk labeling

        @param pos1 reference line (start point)
        @param pos1 reference line (end point)
        @param start starting value
        @param step step value

        @return number of modified lines
        @return -1 on error
        """
        if not self._checkMap():
            return -1
        
        nlines = Vect_get_num_lines(self.poMapInfo)
        
        # register changeset
        changeset = self._addActionsBefore()
        
        poList = self._display.GetSelectedIList()
        ret = Vedit_bulk_labeling(self.poMapInfo, poList,
                                  pos1[0], pos1[1], pos2[0], pos2[1],
                                  start, step)
        Vect_destroy_list(poList)
        
        if ret > 0:
            self._addActionsAfter(changeset, nlines)
            self.toolbar.EnableUndo()
        else:
            del self.changesets[changeset]
        
        return ret
    
    def GetDisplay(self):
        """!Get display driver instance"""
        return self._display
    
    def OpenMap(self, name):
        """!Open vector map for editing
        
        @param map name of vector map to be set up
        """
        Debug.msg (3, "AbstractDigit.SetMapName map=%s" % name)
        
        name, mapset = name.split('@')
        
        self.poMapInfo = self._display.OpenMap(str(name), str(mapset), True)
        
        if self.poMapInfo:
            self.InitCats()
        
        return self.poMapInfo
    
    def CloseMap(self):
        """!Close currently open vector map
        """
        if not self._checkMap():
            return
        
        self._display.CloseMap()

    def InitCats(self):
        """!Initialize categories information
        
        @return 0 on success
        @return -1 on error
        """
        self.cats.clear()
        if not self._checkMap():
            return -1
        
        ndblinks = Vect_get_num_dblinks(self.poMapInfo)
        for i in range(ndblinks):
            fi = Vect_get_dblink(self.poMapInfo, i).contents
            if fi:
                self.cats[fi.number] = None
        
        # find max category
        nfields = Vect_cidx_get_num_fields(self.poMapInfo)
        Debug.msg(2, "wxDigit.InitCats(): nfields=%d", nfields)
        
        for i in range(nfields):
            field = Vect_cidx_get_field_number(self.poMapInfo, i)
            ncats = Vect_cidx_get_num_cats_by_index(self.poMapInfo, i)
            if field <= 0:
                continue
            for j in range(ncats):
                cat = c_int()
                type = c_int()
                id = c_int()
                Vect_cidx_get_cat_by_index(self.poMapInfo, i, j,
                                           byref(cat), byref(type), byref(id))
                if field in self.cats:
                    if cat > self.cats[field]:
                        self.cats[field] = cat.value
                else:
                    self.cats[field] = cat.value
            Debug.msg(3, "wxDigit.InitCats(): layer=%d, cat=%d", field, self.cats[field])
            
        # set default values
        for field, cat in self.cats.iteritems():
            if cat == None:
                self.cats[field] = 0 # first category 1
	    Debug.msg(3, "wxDigit.InitCats(): layer=%d, cat=%d", field, self.cats[field])
        
    def _checkMap(self):
        """!Check if map is open
        """
        if not self.poMapInfo:
            self._error.NoMap()
            return False
        
        return True

    def _addFeature(self, ftype, coords, layer, cat, snap, threshold):
        """!Add new feature(s) to the vector map

        @param ftype feature type (GV_POINT, GV_LINE, GV_BOUNDARY, ...)
        @coords tuple of coordinates ((x, y), (x, y), ...)
        @param layer layer number (-1 for no cat)
        @param cat category number
        @param snap snap to node/vertex
        @param threshold threshold for snapping
        
        @return tuple (number of added features, list of fids)
        @return number of features -1 on error
        """
        fids = list()
        if not self._checkMap():
            return (-1, None)
        
        is3D = bool(Vect_is_3d(self.poMapInfo))
        
        Debug.msg(2, "IVDigit._addFeature(): npoints=%d, layer=%d, cat=%d, snap=%d",
                  len(coords), layer, cat, snap)
        
        if not (ftype & (GV_POINTS | GV_LINES | GV_AREA)): # TODO: 3D
            self._error.FeatureType(ftype)
            return (-1, None)
        
        # set category
        Vect_reset_cats(self.poCats)
        if layer > 0 and ftype != GV_AREA:
            Vect_cat_set(self.poCats, layer, cat)
            self.cats[layer] = max(cat, self.cats.get(layer, 1))
        
        # append points
        Vect_reset_line(self.poPoints)
        for c in coords:
            Vect_append_point(self.poPoints, c[0], c[1], 0.0)
        
        if ftype & (GV_BOUNDARY | GV_AREA):
            # close boundary
            points = self.poPoints.contents
            last = points.n_points - 1
            if Vect_points_distance(points.x[0], points.y[0], points.z[0],
                                    points.x[last], points.y[last], points.z[last],
                                    is3D) <= threshold:
                points.x[last] = points.x[0]
                points.y[last] = points.y[0]
                points.z[last] = points.z[0]
        
        if snap != NO_SNAP:
            # apply snapping (node or vertex)
            modeSnap = not (snap == SNAP)
            Vedit_snap_line(self.poMapInfo, self.popoBgMapInfo, int(self.poBgMapInfo is not None),
                            -1, self.poPoints, threshold, modeSnap)
        
        if ftype == GV_AREA:
            ltype = GV_BOUNDARY
        else:
            ltype = ftype
        newline = Vect_write_line(self.poMapInfo, ltype, self.poPoints, self.poCats)
        if newline < 0:
            self._error.WriteLine()
            return (-1, None)
        else:
            fids.append(newline)
        
        left = right = -1
        if ftype & GV_AREA:
            # add centroids for left/right area
            bpoints = Vect_new_line_struct()
            cleft = c_int()
            cright = c_int()
            
            Vect_get_line_areas(self.poMapInfo, newline,
                                byref(cleft), byref(cright))
            left = cleft.value
            right = cright.value
            
            Debug.msg(3, "IVDigit._addFeature(): area - left=%d right=%d",
                      left, right)
            
            # check if area exists and has no centroid inside
            if layer > 0 and (left > 0 or right > 0):
                Vect_cat_set(self.poCats, layer, cat)
                self.cats[layer] = max(cat, self.cats.get(layer, 0))
            
            x = c_double()
            y = c_double()
            if left > 0 and \
                    Vect_get_area_centroid(self.poMapInfo, left) == 0:
                # if Vect_get_area_points(self.poMapInfo, left, bpoints) > 0 and
                # Vect_find_poly_centroid(bpoints, byref(x), byref(y)) == 0:
                if Vect_get_point_in_area(self.poMapInfo, left, byref(x), byref(y)) == 0:
                    Vect_reset_line(bpoints)
                    Vect_append_point(bpoints, x.value, y.value, 0.0)
                    newline = Vect_write_line(self.poMapInfo, GV_CENTROID,
                                              bpoints, self.poCats)
                    if newline < 0:
                        self._error.WriteLine()
                        return (len(fids), fids)
                    else:
                        fids.append(newline)
                    
            if right > 0 and \
                    Vect_get_area_centroid(self.poMapInfo, right) == 0:
                # if Vect_get_area_points(byref(self.poMapInfo), right, bpoints) > 0 and 
                # Vect_find_poly_centroid(bpoints, byref(x), byref(y)) == 0:
                if Vect_get_point_in_area(self.poMapInfo, right, byref(x), byref(y)) == 0:
                    Vect_reset_line(bpoints)
                    Vect_append_point(bpoints, x.value, y.value, 0.0)
                    newline =  Vect_write_line(self.poMapInfo, GV_CENTROID,
                                               bpoints, self.poCats)
                    if newline < 0:
                        self._error.WriteLine()
                        return (len(fids, fids))
                    else:
                        fids.append(newline)
                    
            Vect_destroy_line_struct(bpoints)
        
        # register changeset
        changeset = len(self.changesets)
        self._addActionToChangeset(changeset, newline, add = True)
        
        # break at intersection
        if self._settings['breakLines']:
            self._breakLineAtIntersection(newline, self.poPoints, changeset)
        
        return (len(fids), fids)
    
    def _ModifyLineVertex(self, coords, add = True):
        """!Add or remove vertex
        
        Shape of line/boundary is not changed when adding new vertex.
        
        @param coords coordinates of point
        @param add True to add, False to remove
        
        @return id id of the new feature
        @return 0 nothing changed
        @return -1 error
        """
        if not self._checkMap():
            return -1
        
        selected = self._display.selected
        if len(selected['ids']) != 1:
            return 0
        
        poList  = self._display.GetSelectedIList()
        Vect_reset_line(self.poPoints)
        Vect_append_point(self.poPoints, coords[0], coords[1], 0.0)
        
        nlines = Vect_get_num_lines(self.poMapInfo)
        thresh = self._display.GetThreshold(type = 'selectThresh')
        
        changeset = self._addActionsBefore()
        
        if add:
            ret = Vedit_add_vertex(self.poMapInfo, poList,
                                   self.poPoints, thresh)
        else:
            ret = Vedit_remove_vertex(self.poMapInfo, poList,
                                      self.poPoints, thresh)
        Vect_destroy_list(poList)
        
        if ret > 0:
            self._addActionsAfter(changeset, nlines)
        else:
            del self.changesets[changeset]
        
        if not add and ret > 0 and self._settings['breakLines']:
            self._breakLineAtIntersection(Vect_get_num_lines(self.poMapInfo),
                                          None, changeset)
                
        return nlines + 1 # feature is write at the end of the file
    
    def GetLineCats(self, line):
        """!Get list of layer/category(ies) for selected feature.

        @param line feature id (-1 for first selected feature)

        @return list of layer/cats
        """
        ret = dict()
        if not self._checkMap():
            return ret
        
        if line == -1 and len(self._display.selected['ids']) < 1:
            return ret
        
        if line == -1:
            line = self._display.selected['ids'][0]
            
        if not Vect_line_alive(self.poMapInfo, line):
            self._error.DeadLine(line)
            return ret
        
        if Vect_read_line(self.poMapInfo, None, self.poCats, line) < 0:
            self._error.ReadLine(line)
            return ret
        
        cats = self.poCats.contents
        for i in range(cats.n_cats):
            field = cats.field[i]
            if field not in ret:
                ret[field] = list()
            ret[field].append(cats.cat[i])
        
        return ret

    def GetLayers(self):
        """!Get list of layers
        
        Requires self.InitCats() to be called.

        @return list of layers
        """
        return self.cats.keys()
    
    def UpdateSettings(self):
        """!Update digit (and display) settings
        """
        self._display.UpdateSettings()
        
        self._settings['breakLines']  = bool(UserSettings.Get(group = 'vdigit', key = "breakLines",
                                                              subkey = 'enabled'))
        
    def SetCategory(self):
        """!Update self.cats based on settings"""
        sel = UserSettings.Get(group = 'vdigit', key = 'categoryMode', subkey = 'selection')
        cat = None
        if sel == 0: # next to usep
            cat = self._setCategoryNextToUse()
        elif sel == 1:
            cat = UserSettings.Get(group = 'vdigit', key = 'category', subkey = 'value')
        
        if cat:
            layer = UserSettings.Get(group = 'vdigit', key = 'layer', subkey = 'value')
            self.cats[layer] = cat
        
        return cat
    
    def _setCategoryNextToUse(self):
        """!Find maximum category number for the given layer and
        update the settings

        @return category to be used
        """
        # get max category number for given layer and update the settings
        layer = UserSettings.Get(group = 'vdigit', key = 'layer', subkey = 'value')
        cat = self.cats.get(layer, 0) + 1
        UserSettings.Set(group = 'vdigit', key = 'category', subkey = 'value',
                         value = cat)
        Debug.msg(1, "IVDigit._setCategoryNextToUse(): cat=%d", cat)
        
        return cat

    def SelectLinesFromBackgroundMap(self, bbox):
        """!Select features from background map

        @param bbox bounding box definition
        
        @return list of selected feature ids
        """
        # try select features by box first
        if self._display.SelectLinesByBox(bbox, poMapInfo = self.poBgMapInfo) < 1:
            self._display.SelectLineByPoint(bbox[0], poMapInfo = self.poBgMapInfo)['line']
            
        return self._display.selected['ids']
        
    def GetUndoLevel(self):
        """!Get undo level (number of active changesets)
        
        Note: Changesets starts wiht 0
        """
        return self.changesetCurrent
