# Copyright (c) 2001, 2003 by Intevation GmbH
# Authors:
# Jonathan Coles <jonathan@intevation.de>
#
# This program is free software under the GPL (>=v2)
# Read the file COPYING coming with Thuban for details.

"""Dialog for classifying how layers are displayed"""

__version__ = "$Revision: 1.64 $"

import copy

from Thuban.Model.table import FIELDTYPE_INT, FIELDTYPE_DOUBLE, \
     FIELDTYPE_STRING

from wxPython.wx import *
from wxPython.grid import *

from Thuban import _
from Thuban.UI.common import Color2wxColour, wxColour2Color

from Thuban.Model.messages import MAP_LAYERS_REMOVED, LAYER_SHAPESTORE_REPLACED
from Thuban.Model.range import Range
from Thuban.Model.classification import \
    Classification, ClassGroupDefault, \
    ClassGroupSingleton, ClassGroupRange, ClassGroupMap, \
    ClassGroupProperties

from Thuban.Model.color import Transparent

from Thuban.Model.layer import Layer, RasterLayer
from Thuban.Model.data import SHAPETYPE_ARC, SHAPETYPE_POLYGON, SHAPETYPE_POINT

from Thuban.UI.classgen import ClassGenDialog

from dialogs import NonModalNonParentDialog
from messages import MAP_REPLACED

ID_CLASS_TABLE = 40011


# table columns
COL_VISIBLE = 0
COL_SYMBOL  = 1
COL_VALUE   = 2
COL_LABEL   = 3
NUM_COLS    = 4

# indices into the client data lists in Classifier.fields
FIELD_CLASS = 0
FIELD_TYPE = 1
FIELD_NAME = 2

#
# this is a silly work around to ensure that the table that is
# passed into SetTable is the same that is returned by GetTable
#
import weakref
class ClassGrid(wxGrid):


    def __init__(self, parent, classifier):
        """Constructor.

        parent -- the parent window

        clazz -- the working classification that this grid should
                 use for display. 
        """

        wxGrid.__init__(self, parent, ID_CLASS_TABLE, style = 0)

        self.classifier = classifier

        self.currentSelection = []

        EVT_GRID_CELL_LEFT_DCLICK(self, self._OnCellDClick)
        EVT_GRID_RANGE_SELECT(self, self._OnSelectedRange)
        EVT_GRID_SELECT_CELL(self, self._OnSelectedCell)
        EVT_GRID_COL_SIZE(self, self._OnCellResize)
        EVT_GRID_ROW_SIZE(self, self._OnCellResize)

    #def GetCellAttr(self, row, col):
        #print "GetCellAttr ", row, col
        #wxGrid.GetCellAttr(self, row, col)

    def CreateTable(self, clazz, fieldType, shapeType, group = None):

        assert isinstance(clazz, Classification)

        table = self.GetTable()
        if table is None:
            w = self.GetDefaultColSize() * NUM_COLS \
                + self.GetDefaultRowLabelSize() 
            h = self.GetDefaultRowSize() * 4 \
                + self.GetDefaultColLabelSize()

            self.SetDimensions(-1, -1, w, h)
            self.SetSizeHints(w, h, -1, -1)
            table = ClassTable(self)
            self.SetTable(table, True)


        self.SetSelectionMode(wxGrid.wxGridSelectRows)
        self.ClearSelection()

        table.Reset(clazz, fieldType, shapeType, group)

    def GetCurrentSelection(self):
        """Return the currently highlighted rows as an increasing list
           of row numbers."""
        sel = copy.copy(self.currentSelection)
        sel.sort()
        return sel

    def GetSelectedRows(self):
        return self.GetCurrentSelection()

    #def SetCellRenderer(self, row, col, renderer):
        #raise ValueError(_("Must not allow setting of renderer in ClassGrid!"))

    #
    # [Set|Get]Table is taken from http://wiki.wxpython.org
    # they are needed as a work around to ensure that the table
    # that is passed to SetTable is the one that is returned 
    # by GetTable.
    #
    def SetTable(self, object, *attributes): 
        self.tableRef = weakref.ref(object) 
        return wxGrid.SetTable(self, object, *attributes) 

    def GetTable(self): 
        try:
            return self.tableRef() 
        except:
            return None

    def DeleteSelectedRows(self):
        """Deletes all highlighted rows.
  
        If only one row is highlighted then after it is deleted the
        row that was below the deleted row is highlighted."""

        sel = self.GetCurrentSelection()

        # nothing to do
        if len(sel) == 0: return 

        # if only one thing is selected check if it is the default
        # data row, because we can't remove that
        if len(sel) == 1:
            #group = self.GetTable().GetValueAsCustom(sel[0], COL_SYMBOL, None)
            group = self.GetTable().GetClassGroup(sel[0])
            if isinstance(group, ClassGroupDefault):
                wxMessageDialog(self, 
                                "The Default group cannot be removed.",
                                style = wxOK | wxICON_EXCLAMATION).ShowModal()
                return
        

        self.ClearSelection()

        # we need to remove things from the bottom up so we don't
        # change the indexes of rows that will be deleted next
        sel.reverse()

        #
        # actually remove the rows
        #
        table = self.GetTable()
        for row in sel:
            table.DeleteRows(row)

        #
        # if there was only one row selected highlight the row
        # that was directly below it, or move up one if the
        # deleted row was the last row.
        #
        if len(sel) == 1:
            r = sel[0]
            if r > self.GetNumberRows() - 1:
                r = self.GetNumberRows() - 1
            self.SelectRow(r)
        

    def SelectGroup(self, group, makeVisible = True):
        if group is None: return

        assert isinstance(group, ClassGroup) 

        table = self.GetTable()

        assert table is not None 

        for i in range(table.GetNumberRows()):
            g = table.GetClassGroup(i)
            if g is group:
                self.SelectRow(i)
                if makeVisible:
                    self.MakeCellVisible(i, 0)
                break

#
# XXX: This isn't working, and there is no way to deselect rows wxPython!
#
#   def DeselectRow(self, row):
#       self.ProcessEvent(
#           wxGridRangeSelectEvent(-1, 
#                                  wxEVT_GRID_RANGE_SELECT,
#                                  self,
#                                  (row, row), (row, row),
#                                  sel = False))

    def _OnCellDClick(self, event):
        """Handle a double click on a cell."""

        r = event.GetRow()
        c = event.GetCol()

        if c == COL_SYMBOL:
            self.classifier.EditSymbol(r)
        else:
            event.Skip()

    #
    # _OnSelectedRange() and _OnSelectedCell() were borrowed
    # from http://wiki.wxpython.org to keep track of which
    # cells are currently highlighted
    #
    def _OnSelectedRange(self, event): 
        """Internal update to the selection tracking list""" 
        if event.Selecting(): 
            for index in range( event.GetTopRow(), event.GetBottomRow()+1): 
                if index not in self.currentSelection: 
                    self.currentSelection.append( index ) 
        else: 
            for index in range( event.GetTopRow(), event.GetBottomRow()+1): 
                while index in self.currentSelection: 
                    self.currentSelection.remove( index ) 
        #self.ConfigureForSelection() 

        event.Skip() 

    def _OnSelectedCell( self, event ): 
        """Internal update to the selection tracking list""" 
        self.currentSelection = [ event.GetRow() ] 
        #self.ConfigureForSelection() 
        event.Skip() 

    def _OnCellResize(self, event):
        self.FitInside()
        event.Skip()

class ClassTable(wxPyGridTableBase):
    """Represents the underlying data structure for the grid."""

    __col_labels = [_("Visible"), _("Symbol"), _("Value"), _("Label")]


    def __init__(self, view = None):
        """Constructor.

        shapeType -- the type of shape that the layer uses

        view -- a wxGrid object that uses this class for its table
        """

        wxPyGridTableBase.__init__(self)

        assert len(ClassTable.__col_labels) == NUM_COLS

        self.clazz = None
        self.__colAttr = {}

        self.SetView(view)

    def Reset(self, clazz, fieldType, shapeType, group = None):
        """Reset the table with the given data.

        This is necessary because wxWindows does not allow a grid's
        table to change once it has been intially set and so we
        need a way of modifying the data.

        clazz -- the working classification that this table should
                 use for display. This may be different from the
                 classification in the layer.

        shapeType -- the type of shape that the layer uses
        """

        assert isinstance(clazz, Classification) 

        self.GetView().BeginBatch()

        self.fieldType = fieldType
        self.shapeType = shapeType

        self.SetClassification(clazz, group)
        self.__Modified(-1)

        self.__colAttr = {}

        attr = wxGridCellAttr()
        attr.SetEditor(wxGridCellBoolEditor())
        attr.SetRenderer(wxGridCellBoolRenderer())
        attr.SetAlignment(wxALIGN_CENTER, wxALIGN_CENTER)
        self.__colAttr[COL_VISIBLE] = attr

        attr = wxGridCellAttr()
        attr.SetRenderer(ClassRenderer(self.shapeType))
        attr.SetReadOnly()
        self.__colAttr[COL_SYMBOL] = attr

        self.GetView().EndBatch()
        self.GetView().FitInside()

    def GetClassification(self):
        """Return the current classification."""
        return self.clazz

    def SetClassification(self, clazz, group = None):
        """Fill in the table with the given classification.
        Select the given group if group is not None.
        """

        self.GetView().BeginBatch()

        old_len = self.GetNumberRows()

        row = -1
        self.clazz = clazz
 
        self.__NotifyRowChanges(old_len, self.GetNumberRows())

        #
        # XXX: this is dead code at the moment
        #
        if row > -1:
            self.GetView().ClearSelection()
            self.GetView().SelectRow(row)
            self.GetView().MakeCellVisible(row, 0)

        self.__Modified()

        self.GetView().EndBatch()
        self.GetView().FitInside()

    def __NotifyRowChanges(self, curRows, newRows):
        """Make sure table updates correctly if the number of 
        rows changes.
        """
        #
        # silly message processing for updates to the number of
        # rows and columns
        #
        if newRows > curRows:
            msg = wxGridTableMessage(self,
                        wxGRIDTABLE_NOTIFY_ROWS_APPENDED,
                        newRows - curRows)    # how many
            self.GetView().ProcessTableMessage(msg)
            self.GetView().FitInside()
        elif newRows < curRows:
            msg = wxGridTableMessage(self,
                        wxGRIDTABLE_NOTIFY_ROWS_DELETED,
                        curRows,              # position
                        curRows - newRows)    # how many
            self.GetView().ProcessTableMessage(msg)
            self.GetView().FitInside()

    def __SetRow(self, row, group):
        """Set a row's data to that of the group.

        The table is considered modified after this operation.

        row -- if row is < 0 'group' is inserted at the top of the table
               if row is >= GetNumberRows() or None 'group' is append to 
                    the end of the table.
               otherwise 'group' replaces row 'row'
        """

        # either append or replace
        if row is None or row >= self.GetNumberRows():
            self.clazz.AppendGroup(group)
        elif row < 0:
            self.clazz.InsertGroup(0, group)
        else:
            if row == 0:
                self.clazz.SetDefaultGroup(group)
            else:
                self.clazz.ReplaceGroup(row - 1, group)

        self.__Modified()

    def GetColLabelValue(self, col):
        """Return the label for the given column."""
        return self.__col_labels[col]

    def GetRowLabelValue(self, row):
        """Return the label for the given row."""

        if row == 0:
            return _("Default")
        else:
            group = self.clazz.GetGroup(row - 1)
            if isinstance(group, ClassGroupDefault):   return _("Default")
            if isinstance(group, ClassGroupSingleton): return _("Singleton")
            if isinstance(group, ClassGroupRange):     return _("Range")
            if isinstance(group, ClassGroupMap):       return _("Map")

        assert False # shouldn't get here
        return ""

    def GetNumberRows(self):
        """Return the number of rows."""
        if self.clazz is None:
            return 0

        return self.clazz.GetNumGroups() + 1 # +1 for default group

    def GetNumberCols(self):
        """Return the number of columns."""
        return NUM_COLS

    def IsEmptyCell(self, row, col):
        """Determine if a cell is empty. This is always false."""
        return False

    def GetValue(self, row, col):
        """Return the object that is used to represent the given
           cell coordinates. This may not be a string."""
        return self.GetValueAsCustom(row, col, None)

    def SetValue(self, row, col, value):
        """Assign 'value' to the cell specified by 'row' and 'col'.

        The table is considered modified after this operation.
        """

        self.SetValueAsCustom(row, col, None, value)
       
    def GetValueAsCustom(self, row, col, typeName):
        """Return the object that is used to represent the given
           cell coordinates. This may not be a string.
 
        typeName -- unused, but needed to overload wxPyGridTableBase
        """

        if row == 0:
            group = self.clazz.GetDefaultGroup()
        else:
            group = self.clazz.GetGroup(row - 1)


        if col == COL_VISIBLE:
            return group.IsVisible()

        if col == COL_SYMBOL:
            return group.GetProperties()

        if col == COL_LABEL:
            return group.GetLabel()

        # col must be COL_VALUE
        assert col == COL_VALUE 

        if isinstance(group, ClassGroupDefault):
            return _("DEFAULT")
        elif isinstance(group, ClassGroupSingleton):
            return group.GetValue()
        elif isinstance(group, ClassGroupRange):
            return group.GetRange()

        assert False # shouldn't get here
        return None

    def __ParseInput(self, value):
        """Try to determine what kind of input value is 
           (string, number, or range)

        Returns a tuple (type, data) where type is 0 if data is
        a singleton value, or 1 if is a range
        """

        type = self.fieldType

        if type == FIELDTYPE_STRING:
            return (0, value)
        elif type in (FIELDTYPE_INT, FIELDTYPE_DOUBLE):
            if type == FIELDTYPE_INT:
                # the float call allows the user to enter 1.0 for 1
                conv = lambda p: int(float(p))
            else:
                conv = float

            #
            # first try to take the input as a single number
            # if there's an exception try to break it into
            # a range. if there is an exception here, let it 
            # pass up to the calling function.
            #
            try:
                return (0, conv(value))
            except ValueError:
                return (1, Range(value))

        assert False  # shouldn't get here
        return (0,None)

    def SetValueAsCustom(self, row, col, typeName, value):
        """Set the cell specified by 'row' and 'col' to 'value'.

        If column represents the value column, the input is parsed
        to determine if a string, number, or range was entered.
        A new ClassGroup may be created if the type of data changes.

        The table is considered modified after this operation.

        typeName -- unused, but needed to overload wxPyGridTableBase
        """

        assert 0 <= col < self.GetNumberCols() 
        assert 0 <= row < self.GetNumberRows() 

        if row == 0:
            group = self.clazz.GetDefaultGroup()
        else:
            group = self.clazz.GetGroup(row - 1)

        mod = True # assume the data will change

        if col == COL_VISIBLE:
            group.SetVisible(value)
        elif col == COL_SYMBOL:
            group.SetProperties(value)
        elif col == COL_LABEL:
            group.SetLabel(value)
        elif col == COL_VALUE:
            if isinstance(group, ClassGroupDefault):
                # not allowed to modify the default value 
                pass
            elif isinstance(group, ClassGroupMap):
                # something special
                pass
            else: # SINGLETON, RANGE
                try:
                    dataInfo = self.__ParseInput(value)
                except ValueError:
                    # bad input, ignore the request
                    mod = False
                else:

                    changed = False
                    ngroup = group
                    props = group.GetProperties()

                    #
                    # try to update the values, which may include
                    # changing the underlying group type if the
                    # group was a singleton and a range was entered
                    #
                    if dataInfo[0] == 0:
                        if not isinstance(group, ClassGroupSingleton):
                            ngroup = ClassGroupSingleton(props = props)
                            changed = True
                        ngroup.SetValue(dataInfo[1])
                    elif dataInfo[0] == 1:
                        if not isinstance(group, ClassGroupRange):
                            ngroup = ClassGroupRange(props = props)
                            changed = True
                        ngroup.SetRange(dataInfo[1])
                    else:
                        assert False
                        pass

                    if changed:
                        ngroup.SetLabel(group.GetLabel())
                        self.SetClassGroup(row, ngroup)
        else:
            assert False # shouldn't be here
            pass

        if mod:
            self.__Modified()
            self.GetView().Refresh()

    def GetAttr(self, row, col, someExtraParameter):
        """Returns the cell attributes"""

        return self.__colAttr.get(col, wxGridCellAttr()).Clone()

    def GetClassGroup(self, row):
        """Return the ClassGroup object representing row 'row'."""

        #return self.GetValueAsCustom(row, COL_SYMBOL, None)
        if row == 0:
            return self.clazz.GetDefaultGroup()
        else:
            return self.clazz.GetGroup(row - 1)

    def SetClassGroup(self, row, group):
        """Set the given row to properties of group."""
        self.__SetRow(row, group)
        self.GetView().Refresh()

    def __Modified(self, mod = True):
        """Adjust the modified flag.

        mod -- if -1 set the modified flag to False, otherwise perform
               an 'or' operation with the current value of the flag and
               'mod'
        """

        if mod == -1:
            self.modified = False
        else:
            self.modified = mod or self.modified

    def IsModified(self):
        """True if this table is considered modified."""
        return self.modified

    def DeleteRows(self, pos, numRows = 1):
        """Deletes 'numRows' beginning at row 'pos'. 

        The row representing the default group is not removed.

        The table is considered modified if any rows are removed.
        """

        assert pos >= 0 
        old_len = self.GetNumberRows() 
        for row in range(pos, pos - numRows, -1):
            group = self.GetClassGroup(row)
            if row != 0:
                self.clazz.RemoveGroup(row - 1)
                self.__Modified()
    
        if self.IsModified():
            self.__NotifyRowChanges(old_len, self.GetNumberRows())

    def AppendRows(self, numRows = 1):
        """Append 'numRows' empty rows to the end of the table.

        The table is considered modified if any rows are appended.
        """

        old_len = self.GetNumberRows()
        for i in range(numRows):
            np = ClassGroupSingleton()
            self.__SetRow(None, np)

        if self.IsModified():
            self.__NotifyRowChanges(old_len, self.GetNumberRows())


ID_PROPERTY_REVERT = 4002
ID_PROPERTY_ADD = 4003
ID_PROPERTY_GENCLASS = 4004
ID_PROPERTY_REMOVE = 4005
ID_PROPERTY_MOVEUP = 4006
ID_PROPERTY_MOVEDOWN = 4007
ID_PROPERTY_TRY = 4008
ID_PROPERTY_EDITSYM = 4009
ID_PROPERTY_SELECT = 4011
ID_PROPERTY_TITLE = 4012
ID_PROPERTY_FIELDTEXT = 4013

BTN_ADD = 0
BTN_EDIT = 1
BTN_GEN = 2
BTN_UP = 3
BTN_DOWN = 4
BTN_RM = 5

EB_LAYER_TITLE = 0
EB_SELECT_FIELD = 1
EB_GEN_CLASS = 2

class Classifier(NonModalNonParentDialog):

    type2string = {None:             _("None"),
                   FIELDTYPE_STRING: _("Text"),
                   FIELDTYPE_INT:    _("Integer"),
                   FIELDTYPE_DOUBLE: _("Decimal")}

    def __init__(self, parent, name, map, layer, group = None):
        """Create a Properties/Classification dialog for a layer.
        The layer is part of map. If group is not None, select that
        group in the classification table.
        """

        NonModalNonParentDialog.__init__(self, parent, name, "")

        self.__SetTitle(layer.Title())

        self.parent.Subscribe(MAP_REPLACED, self.map_replaced)
        self.layer = layer
        self.map = map

        self.map.Subscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
        self.layer.Subscribe(LAYER_SHAPESTORE_REPLACED,
                             self.layer_shapestore_replaced)

        self.genDlg = None

        ############################
        # Create the controls
        #

        panel = wxPanel(self, -1)

        text_title = wxTextCtrl(panel, ID_PROPERTY_TITLE, layer.Title())
        self.fieldTypeText = wxStaticText(panel, -1, "")

        if layer.HasClassification():
            self.originalClass = self.layer.GetClassification()
            self.originalClassField = self.layer.GetClassificationColumn()
            field = self.originalClassField
            fieldType = self.layer.GetFieldType(field)

            table = layer.ShapeStore().Table()
            #
            # make field choice box
            #
            self.fields = wxChoice(panel, ID_PROPERTY_SELECT,)

            self.num_cols = table.NumColumns()
            # just assume the first field in case one hasn't been
            # specified in the file.
            self.__cur_field = 0 

            self.fields.Append("<None>")

            if fieldType is None:
                self.fields.SetClientData(0, copy.deepcopy(self.originalClass))
            else:
                self.fields.SetClientData(0, None)

            for i in range(self.num_cols):
                name = table.Column(i).name
                self.fields.Append(name)

                if name == field:
                    self.__cur_field = i + 1
                    self.fields.SetClientData(i + 1, 
                                              copy.deepcopy(self.originalClass))
                else:
                    self.fields.SetClientData(i + 1, None)

            button_gen = wxButton(panel, ID_PROPERTY_GENCLASS, 
                _("Generate Class"))
            button_add = wxButton(panel, ID_PROPERTY_ADD, 
                _("Add"))
            button_moveup = wxButton(panel, ID_PROPERTY_MOVEUP, 
                _("Move Up"))
            button_movedown = wxButton(panel, ID_PROPERTY_MOVEDOWN, 
                _("Move Down"))
            button_edit = wxButton(panel, ID_PROPERTY_EDITSYM, 
                _("Edit Symbol"))
            button_remove = wxButton(panel, ID_PROPERTY_REMOVE, 
                _("Remove"))

            self.classGrid = ClassGrid(panel, self)

            # calling __SelectField after creating the classGrid fills in the
            # grid with the correct information
            self.fields.SetSelection(self.__cur_field)
            self.__SelectField(self.__cur_field, group = group)

        button_try = wxButton(self, ID_PROPERTY_TRY, _("Try"))
        button_revert = wxButton(self, ID_PROPERTY_REVERT, _("Revert"))
        button_ok = wxButton(self, wxID_OK, _("OK"))
        button_close = wxButton(self, wxID_CANCEL, _("Close"))
        button_ok.SetDefault()

        ############################
        # Layout the controls
        #

        topBox = wxBoxSizer(wxVERTICAL)
        panelBox = wxBoxSizer(wxVERTICAL)

        sizer = wxBoxSizer(wxHORIZONTAL)
        sizer.Add(wxStaticText(panel, -1, _("Title: ")),
            0, wxALIGN_LEFT | wxALL | wxALIGN_CENTER_VERTICAL, 4)
        sizer.Add(text_title, 1, wxGROW, 0)

        panelBox.Add(sizer, 0, wxGROW, 4)

        if isinstance(layer, RasterLayer):
            type = "Image"
        else:
            type = layer.ShapeType()

        panelBox.Add(wxStaticText(panel, -1, _("Type: %s") % type),
            0, wxALIGN_LEFT | wxALL, 4) 

        if layer.HasClassification():

            classBox = wxStaticBoxSizer(
                        wxStaticBox(panel, -1, _("Classification")), wxVERTICAL)


            sizer = wxBoxSizer(wxHORIZONTAL)
            sizer.Add(wxStaticText(panel, ID_PROPERTY_FIELDTEXT, _("Field: ")),
                0, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL | wxALL, 4)
            sizer.Add(self.fields, 1, wxGROW | wxALL, 4)

            classBox.Add(sizer, 0, wxGROW, 4)

            classBox.Add(self.fieldTypeText, 0, 
                        wxGROW | wxALIGN_LEFT | wxALL | wxADJUST_MINSIZE, 4) 

            controlBox = wxBoxSizer(wxHORIZONTAL)
            controlButtonBox = wxBoxSizer(wxVERTICAL)

            controlButtonBox.Add(button_gen, 0, wxGROW|wxALL, 4)
            controlButtonBox.Add(button_add, 0, wxGROW|wxALL, 4)
            controlButtonBox.Add(button_moveup, 0, wxGROW|wxALL, 4)
            controlButtonBox.Add(button_movedown, 0, wxGROW|wxALL, 4)
            controlButtonBox.Add(button_edit, 0, wxGROW|wxALL, 4)
            controlButtonBox.Add(60, 20, 0, wxGROW|wxALL|wxALIGN_BOTTOM, 4)
            controlButtonBox.Add(button_remove, 0, 
                                 wxGROW|wxALL|wxALIGN_BOTTOM, 4)

            controlBox.Add(self.classGrid, 1, wxGROW, 0)
            controlBox.Add(controlButtonBox, 0, wxGROW, 10)

            classBox.Add(controlBox, 1, wxGROW, 10)
            panelBox.Add(classBox, 1, wxGROW, 0)


        buttonBox = wxBoxSizer(wxHORIZONTAL)
        buttonBox.Add(button_try, 0, wxRIGHT|wxEXPAND, 10)
        buttonBox.Add(button_revert, 0, wxRIGHT|wxEXPAND, 10)
        buttonBox.Add(button_ok, 0, wxRIGHT|wxEXPAND, 10)
        buttonBox.Add(button_close, 0, wxRIGHT|wxEXPAND, 10)

        panel.SetAutoLayout(True)
        panel.SetSizer(panelBox)
        panelBox.Fit(panel) 
        panelBox.SetSizeHints(panel) 

        topBox.Add(panel, 1, wxGROW | wxALL, 4)
        topBox.Add(buttonBox, 0, wxALIGN_RIGHT|wxBOTTOM|wxTOP, 10)

        self.SetAutoLayout(True)
        self.SetSizer(topBox)
        topBox.Fit(self)
        topBox.SetSizeHints(self)
        self.Layout()

        ###########

        EVT_CHOICE(self, ID_PROPERTY_SELECT, self._OnFieldSelect)
        EVT_TEXT(self, ID_PROPERTY_TITLE, self._OnTitleChanged)
        EVT_BUTTON(self, wxID_OK, self._OnOK)
        EVT_BUTTON(self, ID_PROPERTY_TRY, self._OnTry)
        EVT_BUTTON(self, wxID_CANCEL, self._OnCloseBtn)
        EVT_BUTTON(self, ID_PROPERTY_REVERT, self._OnRevert)

        EVT_BUTTON(self, ID_PROPERTY_ADD, self._OnAdd)
        EVT_BUTTON(self, ID_PROPERTY_EDITSYM, self._OnEditSymbol)
        EVT_BUTTON(self, ID_PROPERTY_REMOVE, self._OnRemove)
        EVT_BUTTON(self, ID_PROPERTY_GENCLASS, self._OnGenClass)
        EVT_BUTTON(self, ID_PROPERTY_MOVEUP, self._OnMoveUp)
        EVT_BUTTON(self, ID_PROPERTY_MOVEDOWN, self._OnMoveDown)

        ######################

        text_title.SetFocus()
        self.haveApplied = False

    def unsubscribe_messages(self):
        """Unsubscribe from all messages."""
        self.parent.Unsubscribe(MAP_REPLACED, self.map_replaced)
        self.map.Unsubscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
        self.layer.Unsubscribe(LAYER_SHAPESTORE_REPLACED,
                               self.layer_shapestore_replaced)

    def map_layers_removed(self, map):
        """Subscribed to MAP_LAYERS_REMOVED. If this layer was removed,
        Close self.
        """
        if self.layer not in self.map.Layers():
            self.Close()

    def layer_shapestore_replaced(self, *args):
        """Subscribed to the map's LAYER_SHAPESTORE_REPLACED message.

        Close self.
        """
        self.Close()

    def map_replaced(self, *args):
        """Subscribed to the mainwindow's MAP_REPLACED message. Close self."""
        self.Close()

    def EditSymbol(self, row):
        """Open up a dialog where the user can select the properties 
        for a group.
        """
        table = self.classGrid.GetTable()
        prop = table.GetValueAsCustom(row, COL_SYMBOL, None)

        # get a new ClassGroupProperties object and copy the 
        # values over to our current object
        propDlg = SelectPropertiesDialog(self, prop, self.layer.ShapeType())

        self.Enable(False)
        if propDlg.ShowModal() == wxID_OK:
            new_prop = propDlg.GetClassGroupProperties()
            table.SetValueAsCustom(row, COL_SYMBOL, None, new_prop)
        self.Enable(True)
        propDlg.Destroy()
        
    def _SetClassification(self, clazz):
        """Called from the ClassGen dialog when a new classification has
        been created and should be set in the table.
        """
        # FIXME: This could be implemented using a message
        
        self.fields.SetClientData(self.__cur_field, clazz)
        self.classGrid.GetTable().SetClassification(clazz)

    def __BuildClassification(self, fieldIndex, copyClass=False, force=False):
        """Pack the classification setting into a Classification object.
        Returns (Classification, fieldName) where fieldName is the selected
        field in the table that the classification should be used with.
        """

#       numRows = self.classGrid.GetNumberRows()
#       assert numRows > 0  # there should always be a default row

        if fieldIndex == 0:
            fieldName = None
            fieldType = None
        else:
            fieldName = self.fields.GetString(fieldIndex)
            fieldType = self.layer.GetFieldType(fieldName)

        clazz = self.fields.GetClientData(fieldIndex)
        if clazz is None or self.classGrid.GetTable().IsModified() or force:
            clazz = self.classGrid.GetTable().GetClassification()
            if copyClass:
                clazz = copy.deepcopy(clazz)

        return clazz, fieldName

    def __SetGridTable(self, fieldIndex, group = None):
        """Set the table with the classification associated with the
        selected field at fieldIndex. Select the specified group
        if group is not None.
        """

        clazz = self.fields.GetClientData(fieldIndex)

        if clazz is None:
            clazz = Classification()
            clazz.SetDefaultGroup(
                ClassGroupDefault(
                    self.layer.GetClassification().
                               GetDefaultGroup().GetProperties()))

        fieldName = self.fields.GetString(fieldIndex)
        fieldType = self.layer.GetFieldType(fieldName)
                
        self.classGrid.CreateTable(clazz, fieldType, 
                                   self.layer.ShapeType(), group)

    def __SetFieldTypeText(self, fieldIndex):
        """Set the field type string using the data type of the field
        at fieldIndex.
        """
        fieldName = self.fields.GetString(fieldIndex)
        fieldType = self.layer.GetFieldType(fieldName)

        assert Classifier.type2string.has_key(fieldType) 

        text = Classifier.type2string[fieldType]

        self.fieldTypeText.SetLabel(_("Data Type: %s") % text)

    def __SelectField(self, newIndex, oldIndex = -1, group = None):
        """This method assumes that the current selection for the
        combo has already been set by a call to SetSelection().
        """

        assert oldIndex >= -1

        if oldIndex != -1:
            clazz, name = self.__BuildClassification(oldIndex, force = True)
            self.fields.SetClientData(oldIndex, clazz)

        self.__SetGridTable(newIndex, group)

        self.__EnableButtons(EB_SELECT_FIELD)

        self.__SetFieldTypeText(newIndex)

    def __SetTitle(self, title):
        """Set the title of the dialog."""
        if title != "":
            title = ": " + title

        self.SetTitle(_("Layer Properties") + title)
 
    def _OnEditSymbol(self, event):
        """Open up a dialog for the user to select group properties."""
        sel = self.classGrid.GetCurrentSelection()

        if len(sel) == 1:
            self.EditSymbol(sel[0])

    def _OnFieldSelect(self, event): 
        index = self.fields.GetSelection()
        self.__SelectField(index, self.__cur_field)
        self.__cur_field = index

    def _OnTry(self, event):
        """Put the data from the table into a new Classification and hand
           it to the layer.
        """

        if self.layer.HasClassification():
            clazz = self.fields.GetClientData(self.__cur_field)

            #
            # only build the classification if there wasn't one to
            # to begin with or it has been modified
            #
            self.classGrid.SaveEditControlValue()
            clazz, name = self.__BuildClassification(self.__cur_field, True)

            self.layer.SetClassificationColumn(name)
            self.layer.SetClassification(clazz)

        self.haveApplied = True

    def _OnOK(self, event):
        self._OnTry(event)
        self.Close()

    def OnClose(self, event):
        self.unsubscribe_messages()
        NonModalNonParentDialog.OnClose(self, event)

    def _OnCloseBtn(self, event):
        """Close is similar to Cancel except that any changes that were
        made and applied remain applied, but the currently displayed
        classification is discarded.
        """

        self.Close()

    def _OnRevert(self, event):
        """The layer's current classification stays the same."""
        if self.haveApplied:
            self.layer.SetClassificationColumn(self.originalClassField)
            self.layer.SetClassification(self.originalClass)

        #self.Close()

    def _OnAdd(self, event): 
        self.classGrid.AppendRows()

    def _OnRemove(self, event):
        self.classGrid.DeleteSelectedRows()

    def _OnGenClass(self, event):
        """Open up a dialog for the user to generate classifications."""

        self.genDlg = ClassGenDialog(self, self.layer,
                          self.fields.GetString(self.__cur_field))

        EVT_CLOSE(self.genDlg, self._OnGenDialogClose)

        self.__EnableButtons(EB_GEN_CLASS)

        self.genDlg.Show()

    def _OnGenDialogClose(self, event):
        """Reenable buttons after the generate classification 
        dialog is closed.
        """
        self.genDlg.Destroy()
        self.genDlg = None
        self.__EnableButtons(EB_GEN_CLASS)

    def _OnMoveUp(self, event):
        """When the user clicks MoveUp, try to move a group up one row."""
        sel = self.classGrid.GetCurrentSelection()

        if len(sel) == 1:
            i = sel[0]
            if i > 1:
                table = self.classGrid.GetTable()
                x = table.GetClassGroup(i - 1)
                y = table.GetClassGroup(i)
                table.SetClassGroup(i - 1, y)
                table.SetClassGroup(i, x)
                self.classGrid.ClearSelection()
                self.classGrid.SelectRow(i - 1)
                self.classGrid.MakeCellVisible(i - 1, 0)

    def _OnMoveDown(self, event):
        """When the user clicks MoveDown, try to move a group down one row."""
        sel = self.classGrid.GetCurrentSelection()

        if len(sel) == 1:
            i = sel[0]
            table = self.classGrid.GetTable()
            if 0 < i < table.GetNumberRows() - 1:
                x = table.GetClassGroup(i)
                y = table.GetClassGroup(i + 1)
                table.SetClassGroup(i, y)
                table.SetClassGroup(i + 1, x)
                self.classGrid.ClearSelection()
                self.classGrid.SelectRow(i + 1)
                self.classGrid.MakeCellVisible(i + 1, 0)

    def _OnTitleChanged(self, event):
        """Update the dialog title when the user changed the layer name."""
        obj = event.GetEventObject()

        self.layer.SetTitle(obj.GetValue())
        self.__SetTitle(self.layer.Title())

        self.__EnableButtons(EB_LAYER_TITLE)

    def __EnableButtons(self, case):
        """Helper method that enables/disables the appropriate buttons
        based on the case provided. Cases are constants beginning with EB_.
        """

        list = {wxID_OK                 : True, 
                wxID_CANCEL             : True, 
                ID_PROPERTY_ADD         : True,
                ID_PROPERTY_MOVEUP      : True, 
                ID_PROPERTY_MOVEDOWN    : True, 
                ID_PROPERTY_REMOVE      : True,
                ID_PROPERTY_SELECT      : True, 
                ID_PROPERTY_FIELDTEXT   : True, 
                ID_PROPERTY_GENCLASS    : True,
                ID_PROPERTY_EDITSYM     : True}

        if case == EB_LAYER_TITLE:  
            if self.layer.Title() == "":
                list[wxID_OK] = False
                list[wxID_CANCEL] = False

        elif case == EB_SELECT_FIELD:
            if self.fields.GetSelection() == 0:
                list[ID_PROPERTY_GENCLASS] = False
                list[ID_PROPERTY_ADD] = False
                list[ID_PROPERTY_MOVEUP] = False
                list[ID_PROPERTY_MOVEDOWN] = False
                list[ID_PROPERTY_REMOVE] = False

        elif case == EB_GEN_CLASS:
            if self.genDlg is not None:
                list[ID_PROPERTY_SELECT] = False 
                list[ID_PROPERTY_FIELDTEXT] = False
                list[ID_PROPERTY_GENCLASS] = False 

        for id, enable in list.items():
            win = self.FindWindowById(id)
            if win:
                win.Enable(enable)

ID_SELPROP_SPINCTRL = 4002
ID_SELPROP_PREVIEW = 4003
ID_SELPROP_STROKECLR = 4004
ID_SELPROP_FILLCLR = 4005
ID_SELPROP_STROKECLRTRANS = 4006
ID_SELPROP_FILLCLRTRANS = 4007

class SelectPropertiesDialog(wxDialog):
    """Dialog that allows the user to select group properties."""

    def __init__(self, parent, prop, shapeType):
        """Open the dialog with the initial prop properties and shapeType."""

        wxDialog.__init__(self, parent, -1, _("Select Properties"),
                          style = wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)

        self.prop = ClassGroupProperties(prop)

        topBox = wxBoxSizer(wxVERTICAL)

        itemBox = wxBoxSizer(wxHORIZONTAL)

        # preview box
        previewBox = wxBoxSizer(wxVERTICAL)
        previewBox.Add(wxStaticText(self, -1, _("Preview:")),
            0, wxALIGN_LEFT | wxALL, 4)

        self.previewWin = ClassGroupPropertiesCtrl(
            self, ID_SELPROP_PREVIEW, self.prop, shapeType, 
            (40, 40), wxSIMPLE_BORDER)

        self.previewWin.AllowEdit(False)

        previewBox.Add(self.previewWin, 1, wxGROW | wxALL, 4)

        itemBox.Add(previewBox, 1, wxALIGN_LEFT | wxALL | wxGROW, 0)

        # control box
        ctrlBox = wxBoxSizer(wxVERTICAL)

        lineColorBox = wxBoxSizer(wxHORIZONTAL)
        button = wxButton(self, ID_SELPROP_STROKECLR, _("Change Line Color"))
        button.SetFocus()
        lineColorBox.Add(button, 1, wxALL | wxGROW, 4)
        EVT_BUTTON(self, ID_SELPROP_STROKECLR, self._OnChangeLineColor)

        lineColorBox.Add(
            wxButton(self, ID_SELPROP_STROKECLRTRANS, _("Transparent")),
            1, wxALL | wxGROW, 4)
        EVT_BUTTON(self, ID_SELPROP_STROKECLRTRANS,
                   self._OnChangeLineColorTrans)

        ctrlBox.Add(lineColorBox, 0, 
                    wxALIGN_CENTER_HORIZONTAL | wxALL | wxGROW, 4)

        if shapeType != SHAPETYPE_ARC:
            fillColorBox = wxBoxSizer(wxHORIZONTAL)
            fillColorBox.Add(
                wxButton(self, ID_SELPROP_FILLCLR, _("Change Fill Color")),
                1, wxALL | wxGROW, 4)
            EVT_BUTTON(self, ID_SELPROP_FILLCLR, self._OnChangeFillColor)
            fillColorBox.Add(
                wxButton(self, ID_SELPROP_FILLCLRTRANS, _("Transparent")),
                1, wxALL | wxGROW, 4)
            EVT_BUTTON(self, ID_SELPROP_FILLCLRTRANS,
                       self._OnChangeFillColorTrans)
            ctrlBox.Add(fillColorBox, 0, 
                        wxALIGN_CENTER_HORIZONTAL | wxALL | wxGROW, 4)

        spinBox = wxBoxSizer(wxHORIZONTAL)
        spinBox.Add(wxStaticText(self, -1, _("Line Width: ")),
                0, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL | wxALL, 4) 
        self.spinCtrl = wxSpinCtrl(self, ID_SELPROP_SPINCTRL, 
                                   min=1, max=10, 
                                   value=str(prop.GetLineWidth()),
                                   initial=prop.GetLineWidth())

        EVT_SPINCTRL(self, ID_SELPROP_SPINCTRL, self._OnSpin)

        spinBox.Add(self.spinCtrl, 0, wxALIGN_LEFT | wxALL, 4) 

        ctrlBox.Add(spinBox, 0, wxALIGN_RIGHT | wxALL, 0)
        itemBox.Add(ctrlBox, 0, wxALIGN_RIGHT | wxALL | wxGROW, 0)
        topBox.Add(itemBox, 1, wxALIGN_LEFT | wxALL | wxGROW, 0)

        #
        # Control buttons:
        #
        buttonBox = wxBoxSizer(wxHORIZONTAL)
        button_ok = wxButton(self, wxID_OK, _("OK"))
        buttonBox.Add(button_ok, 0, wxRIGHT|wxEXPAND, 10)
        buttonBox.Add(wxButton(self, wxID_CANCEL, _("Cancel")),
                      0, wxRIGHT|wxEXPAND, 10)
        topBox.Add(buttonBox, 0, wxALIGN_RIGHT|wxBOTTOM|wxTOP, 10)

        button_ok.SetDefault()
                                                                                
        #EVT_BUTTON(self, wxID_OK, self._OnOK)
        #EVT_BUTTON(self, ID_SELPROP_CANCEL, self._OnCancel)
                                                                                
        self.SetAutoLayout(True)
        self.SetSizer(topBox)
        topBox.Fit(self)
        topBox.SetSizeHints(self)

    def OnOK(self, event):
        self.EndModal(wxID_OK)

    def OnCancel(self, event):
        self.EndModal(wxID_CANCEL)

    def _OnSpin(self, event):
        self.prop.SetLineWidth(self.spinCtrl.GetValue())
        self.previewWin.Refresh()

    def __GetColor(self, cur):
        dialog = wxColourDialog(self)
        if cur is not Transparent:
            dialog.GetColourData().SetColour(Color2wxColour(cur))

        ret = None
        if dialog.ShowModal() == wxID_OK:
            ret = wxColour2Color(dialog.GetColourData().GetColour())

        dialog.Destroy()

        return ret
        
    def _OnChangeLineColor(self, event):
        clr = self.__GetColor(self.prop.GetLineColor())
        if clr is not None:
            self.prop.SetLineColor(clr)
        self.previewWin.Refresh() # XXX: work around, see ClassDataPreviewer

    def _OnChangeLineColorTrans(self, event):
        self.prop.SetLineColor(Transparent)
        self.previewWin.Refresh() # XXX: work around, see ClassDataPreviewer
        
    def _OnChangeFillColor(self, event):
        clr = self.__GetColor(self.prop.GetFill())
        if clr is not None:
            self.prop.SetFill(clr)
        self.previewWin.Refresh() # XXX: work around, see ClassDataPreviewer

    def _OnChangeFillColorTrans(self, event):
        self.prop.SetFill(Transparent)
        self.previewWin.Refresh() # XXX: work around, see ClassDataPreviewer

    def GetClassGroupProperties(self):
        return self.prop


class ClassDataPreviewWindow(wxWindow):
    """A custom window that draws group properties using the correct shape."""

    def __init__(self, rect, prop, shapeType, 
                       parent = None, id = -1, size = wxDefaultSize):
        """Draws the appropriate shape as specified with shapeType using
        prop properities.
        """
        if parent is not None:
            wxWindow.__init__(self, parent, id, (0, 0), size)
            EVT_PAINT(self, self._OnPaint)

        self.rect = rect

        self.prop = prop
        self.shapeType = shapeType
        self.previewer = ClassDataPreviewer()

    def GetProperties():
        return self.prop

    def _OnPaint(self, event):
        dc = wxPaintDC(self)

        # XXX: this doesn't seem to be having an effect:
        dc.DestroyClippingRegion() 

        if self.rect is None:
            w, h = self.GetSize()
            rect = wxRect(0, 0, w, h)
        else:
            rect = self.rect

        self.previewer.Draw(dc, rect, self.prop, self.shapeType)

class ClassDataPreviewer:
    """Class that actually draws a group property preview."""

    def Draw(self, dc, rect, prop, shapeType):

        assert dc is not None
        assert isinstance(prop, ClassGroupProperties)

        if rect is None:
            x = 0
            y = 0
            w, h = dc.GetSize()
        else:
            x = rect.GetX()
            y = rect.GetY()
            w = rect.GetWidth()
            h = rect.GetHeight()

        stroke = prop.GetLineColor()
        if stroke is Transparent:
            pen = wxTRANSPARENT_PEN
        else:
            pen = wxPen(Color2wxColour(stroke),
                        prop.GetLineWidth(),
                        wxSOLID)

        stroke = prop.GetFill()
        if stroke is Transparent:
            brush = wxTRANSPARENT_BRUSH
        else:
            brush = wxBrush(Color2wxColour(stroke), wxSOLID)

        dc.SetPen(pen)
        dc.SetBrush(brush)

        if shapeType == SHAPETYPE_ARC:
            dc.DrawSpline([wxPoint(x, y + h),
                           wxPoint(x + w/2, y + h/4),
                           wxPoint(x + w/2, y + h/4*3),
                           wxPoint(x + w, y)])

        elif shapeType == SHAPETYPE_POINT:

            dc.DrawCircle(x + w/2, y + h/2,
                          (min(w, h) - prop.GetLineWidth())/2)

        elif shapeType == SHAPETYPE_POLYGON:
            dc.DrawRectangle(x, y, w, h)

class ClassRenderer(wxPyGridCellRenderer):
    """A wrapper class that can be used to draw group properties in a
    grid table.
    """

    def __init__(self, shapeType):
        wxPyGridCellRenderer.__init__(self)
        self.shapeType = shapeType
        self.previewer = ClassDataPreviewer()

    def Draw(self, grid, attr, dc, rect, row, col, isSelected):
        data = grid.GetTable().GetClassGroup(row)

        dc.SetClippingRegion(rect.GetX(), rect.GetY(), 
                             rect.GetWidth(), rect.GetHeight())
        dc.SetPen(wxPen(wxLIGHT_GREY))
        dc.SetBrush(wxBrush(wxLIGHT_GREY, wxSOLID))
        dc.DrawRectangle(rect.GetX(), rect.GetY(), 
                         rect.GetWidth(), rect.GetHeight())

        if not isinstance(data, ClassGroupMap):
            self.previewer.Draw(dc, rect, data.GetProperties(), self.shapeType)

        if isSelected:
            dc.SetPen(wxPen(wxBLACK, 1, wxSOLID))
            dc.SetBrush(wxTRANSPARENT_BRUSH)

            dc.DrawRectangle(rect.GetX(), rect.GetY(), 
                             rect.GetWidth(), rect.GetHeight())

        dc.DestroyClippingRegion()


class ClassGroupPropertiesCtrl(wxWindow, wxControl):
    """A custom window and control that draw a preview of group properties
    and can open a dialog to modify the properties if the user double-clicks
    it.
    """

    def __init__(self, parent, id, props, shapeType, 
                 size = wxDefaultSize, style = 0):

        wxWindow.__init__(self, parent, id, size = size, style = style)

        self.parent = parent

        self.SetProperties(props)
        self.SetShapeType(shapeType)
        self.AllowEdit(True)

        EVT_PAINT(self, self._OnPaint)
        EVT_LEFT_DCLICK(self, self._OnLeftDClick) 

        self.previewer = ClassDataPreviewer()

    def _OnPaint(self, event):
        dc = wxPaintDC(self)

        # XXX: this doesn't seem to be having an effect:
        dc.DestroyClippingRegion() 

        w, h = self.GetClientSize()

        self.previewer.Draw(dc, 
                            wxRect(0, 0, w, h), 
                            self.GetProperties(), 
                            self.GetShapeType())


    def GetProperties(self):
        return self.props

    def SetProperties(self, props):
        self.props = props
        self.Refresh()

    def GetShapeType(self):
        return self.shapeType

    def SetShapeType(self, shapeType):
        self.shapeType = shapeType
        self.Refresh()

    def AllowEdit(self, allow):
        """Allow/Disallow double-clicking on the control."""
        self.allowEdit = allow

    def DoEdit(self):
        """Open the properties selector dialog."""

        if not self.allowEdit: return

        propDlg = SelectPropertiesDialog(self.parent, 
                                         self.GetProperties(), 
                                         self.GetShapeType())

        if propDlg.ShowModal() == wxID_OK:
            new_prop = propDlg.GetClassGroupProperties()
            self.SetProperties(new_prop)
            self.Refresh()

        propDlg.Destroy()

    def _OnLeftDClick(self, event):
        self.DoEdit()
