#!/usr/bin/env python
# Script collected from other scripts
#
#   ../vassal/vassal.py
#   latexexporter.py
#   main.py
#
# ====================================================================
# From ../vassal/vassal.py
# Script collected from other scripts
#
#   ../common/singleton.py
#   ../common/verbose.py
#   ../common/verboseguard.py
#   base.py
#   element.py
#   globalkey.py
#   gameelements.py
#   mapelements.py
#   globalproperty.py
#   turn.py
#   documentation.py
#   player.py
#   chessclock.py
#   widget.py
#   grid.py
#   zone.py
#   board.py
#   map.py
#   chart.py
#   command.py
#   trait.py
#   withtraits.py
#   traits/dynamicproperty.py
#   traits/globalproperty.py
#   traits/prototype.py
#   traits/place.py
#   traits/report.py
#   traits/calculatedproperty.py
#   traits/restrictcommand.py
#   traits/label.py
#   traits/layer.py
#   traits/globalcommand.py
#   traits/globalhotkey.py
#   traits/nostack.py
#   traits/deselect.py
#   traits/restrictaccess.py
#   traits/rotate.py
#   traits/stack.py
#   traits/mark.py
#   traits/mask.py
#   traits/trail.py
#   traits/delete.py
#   traits/sendto.py
#   traits/moved.py
#   traits/skel.py
#   traits/submenu.py
#   traits/basic.py
#   traits/trigger.py
#   traits/nonrect.py
#   traits/click.py
#   game.py
#   buildfile.py
#   moduledata.py
#   save.py
#   vsav.py
#   vmod.py
#   exporter.py
#
# ====================================================================
# From ../common/singleton.py
# ====================================================================
class Singleton(type):
    '''Meta base class for singletons'''
    _instances = {}
    def __call__(cls, *args, **kwargs):
        '''Create the singleton object or returned existing

        Parameters
        ----------
        args : tuple
            Arguments
        kwargs : dict
            Keyword arguments
        '''
        if cls not in cls._instances:
            cls._instances[cls] = \
                super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

#
# EOF
#
# ====================================================================
# From ../common/verbose.py
# --------------------------------------------------------------------

class Verbose(metaclass=Singleton):
    def __init__(self,verbose=False):
        '''Singleton for writing message to screen, contigent on setting

        Parameters
        ----------
        verbose : bool
             Whether to show messages or not
        '''
        self._indent  = ''
        self._verbose = verbose

    def setVerbose(self,verbose):
        '''Set whether to print or not

        Parameters
        ----------
        verbose : bool
             Whether to show messages or not
        '''
        self._verbose = verbose

    @property
    def verbose(self):
        '''Test if this is verbose'''
        return self._verbose

    def message(self,*args,**kwargs):
        '''Write messsage if verbose

        Parameters
        ----------
        args : tuple
            Arguments
        kwargs : dict
            Keyword arguments
        '''        
        if not self._verbose: return
        if not kwargs.pop('noindent', False):
            print(self._indent,end='')
        print(*args,**kwargs)

    def incr(self):
        '''Increment indention'''
        self._indent += ' '

    def decr(self):
        '''Decrement indention'''
        if len(self._indent) > 0:
            self._indent = self._indent[:-1]

#
# EOF
#
# ====================================================================
# From ../common/verboseguard.py
# --------------------------------------------------------------------

class VerboseGuard:
    def __init__(self,*args,**kwargs):
        '''A guard pattern that increases verbose indention

        This is a context manager.  The arguments passed are used for
        an initial message, before increasinig indention.

        Parameters
        ----------
        args : tuple
            Arguments
        kwargs : dict
            Keyword arguments
        '''                
        Verbose().message(*args,**kwargs)

    def __bool_(self):
        '''Test if verbose'''
        return Verbose().verbose
    
    def __enter__(self):
        '''Enter context'''
        Verbose().incr()
        return self

    def __exit__(self,*args):
        '''Exit context'''        
        Verbose().decr()

    def __call__(self,*args,**kwargs):
        '''Write a message at current indention level
        
        Parameters
        ----------
        args : tuple
            Arguments
        kwargs : dict
            Keyword arguments
        '''                
        Verbose().message(*args,**kwargs)

#
# EOF
#
# ====================================================================
# From base.py
# ====================================================================
# Key encoding
SHIFT = 65
CTRL = 130
ALT   = 520
CTRL_SHIFT = CTRL+SHIFT
ALT_SHIFT = ALT+SHIFT
NONE = '\ue004'
NONE_MOD = 0

# --------------------------------------------------------------------
def key(let,mod=CTRL):

    '''Encode a key sequence

    Parameters
    ----------
    let : str
        Key code (Letter)
    mod : int
        Modifier mask
    '''
    return f'{ord(let)},{mod}'

# --------------------------------------------------------------------
#
def hexcolor(s):
    if isinstance(s,str):
        s = s.replace('0x','')
        if len(s) == 3:
            r, g, b = [int(si,16)/16 for si in s]
        elif len(s) == 6:
            r = int(s[0:2],16) / 256
            g = int(s[2:4],16) / 256
            b = int(s[4:6],16) / 256
        else:
            raise RuntimeError('3 or 6 hexadecimal digits for color string')
    elif isinstance(s,int):
        r = ((s >> 16) & 0xFF) / 256
        g = ((s >>  8) & 0xFF) / 256
        b = ((s >>  0) & 0xFF) / 256
    else:
        raise RuntimeError('Hex colour must be string or integer')

    return rgb(int(r*256),int(g*256),int(b*256))
    
# --------------------------------------------------------------------
# Colour encoding 
def rgb(r,g,b):
    '''Encode RGB colour

    Parameters
    ----------
    r : int
        Red channel
    g : int
        Green channel
    b : int
        Blue channel

    Returns
    -------
    colour : str
        RGB colour as a string
    '''
    return ','.join([str(r),str(g),str(b)])

# --------------------------------------------------------------------
def rgba(r,g,b,a):
    '''Encode RGBA colour

    Parameters
    ----------
    r : int
        Red channel
    g : int
        Green channel
    b : int
        Blue channel
    a : int
        Alpha channel
    
    Returns
    -------
    colour : str
        RGBA colour as a string
    '''
    return ','.join([str(r),str(g),str(b),str(a)])

# --------------------------------------------------------------------
def dumpTree(node,ind=''):
    '''Dump the tree of nodes

    Parameters
    ----------
    node : xml.dom.Node
        Node to dump
    ind : str
        Current indent 
    '''
    print(f'{ind}{node}')
    for c in node.childNodes:
        dumpTree(c,ind+' ')

# --------------------------------------------------------------------
def registerElement(cls):
    
    Element.known_tags[cls.TAG] = cls
    
        
#
# EOF
#
# ====================================================================
# From element.py

# ====================================================================
class Element:
    BUILD  = 'VASSAL.build.'
    MODULE = BUILD  + 'module.'
    WIDGET = BUILD  + 'widget.'
    MAP    = MODULE + 'map.'    
    PICKER = MAP    + 'boardPicker.'
    BOARD  = PICKER + 'board.'
    known_tags = {}
    
    def __init__(self,parent,tag,node=None,**kwargs):
        '''Create a new element

        Parameters
        ----------
        parent : Element
            Parent element to add this element to
        tag : str
            Element tag
        node : xml.dom.Node
            If not None, then read attributes from that. Otherwise
            set elements according to kwargs
        kwargs : dict
            Attribute keys and values.  Only used if node is None
        '''
        if parent is not None:
            self._root = parent._root
            self._node = (node if node is not None else
                          parent.addNode(tag,**kwargs))
        else:
            self._root = None
            self._node = None

    # ----------------------------------------------------------------
    # Attributes
    def __contains__(self,key):
        '''Check if element has attribute key'''        
        return self.hasAttribute(key)
    
    def __getitem__(self,key):
        '''Get attribute key value'''
        return self.getAttribute(key)

    def __setitem__(self,key,value):
        '''Set attribute key value'''
        self.setAttribute(key,value)

    def hasAttribute(self,k):
        '''Check if element has attribute '''
        return self._node.hasAttribute(k)

    def getAttribute(self,k):
        '''Get attribute key value'''
        return self._node.getAttribute(k)
        
    def setAttribute(self,k,v):
        '''Set attribute key value'''
        self._node.setAttribute(k,str(v).lower()
                                if isinstance(v,bool) else str(v))
        
    def setAttributes(self,**kwargs):
        '''Set attributes to dictionary key and value'''
        for k,v in kwargs.items():
            self.setAttribute(k,v)

    def getAttributes(self):
        '''Get attributes as dict'''
        return self._node.attributes

    # ----------------------------------------------------------------
    # Plain nodes
    def getChildren(self):
        '''Get child nodes (xml.dom.Node)'''
        return self._node.childNodes

    # ----------------------------------------------------------------
    # Getters
    #
    # First generics 
    def getAsDict(self,tag='',key=None,enable=True):
        '''Get elements with a specific tag as a dictionary
        where the key is given by attribute key'''
        cont = self._node.getElementsByTagName(tag)
        if not enable or key is None:
            return cont

        return {e.getAttribute(key): e for e in cont}

    def getAsOne(self,tag='',single=True):
        '''Get elements with a specific tag, as a list.
        If single is true, then assume we only have one such
        child element, or fail.'''
        cont = self._node.getElementsByTagName(tag)
        if single and len(cont) != 1:
            return None
        return cont
    
    def getElementsByKey(self,cls,key='',asdict=True):
        '''Get elments of a specific class as a dictionary,
        where the key is set by the key attribute.'''
        cont = self.getAsDict(cls.TAG,key,asdict)
        if cont is None: return None
        
        if not asdict: return [cls(self,node=n) for n in cont]

        return {k : cls(self,node=n) for k, n in cont.items()}

    def getAllElements(self,cls,single=True):
        '''Get elements with a specific tag, as a list.  If single is
        true, then assume we only have one such child element, or
        fail.

        '''
        cont = self.getAsOne(cls.TAG,single=single)
        if cont is None: return None
        return [cls(self,node=n) for n in cont]

    def getSpecificElements(self,cls,key,*names,asdict=True):
        '''Get all elements of specific class and that has the
        attribute key, and the attribute value is in names

        '''
        cont = self.getAsOne(cls.TAG,single=False)
        cand = [cls(self,node=n) for n in cont
                if n.getAttribute(key) in names]
        if asdict:
            return {c[key] : c for c in cand}
        return cand
    
    def getParent(self,cls=None,checkTag=True):
        if self._node.parentNode is None:
            return None
        if cls is None:
            cls = self.getTagClass(self._node.parentNode.tagName)
            checkTag = False
        if cls is None:
            return None
        if checkTag and self._node.parentNode.tagName != cls.TAG:
            return None
        return cls(self,node=self._node.parentNode)

    def getParentOfClass(self,cls):
        '''Searches back until we find the parent with the right
        class, or none
        '''
        try:
            iter(cls)
        except:
            cls = [cls]
        t = {c.TAG: c for c in cls}
        p = self._node.parentNode
        while p is not None:
            c = t.get(p.tagName,None)
            if c is not None: return c(self,node=p)
            p = p.parentNode
        return None

    def getTagClass(self,tag):
        '''Get class corresponding to the tag'''
        if tag not in self.known_tags: return None;
        return self.known_tags[tag]
        
    # ----------------------------------------------------------------
    # Adders
    def addNode(self,tag,**attr):
        '''Add a note to this element

        Parameters
        ----------
        tag : str
            Node tag name
        attr : dict
            Attributes to set
        '''
        e = self._root.createElement(tag)
        if self._node: self._node.appendChild(e)

        for k, v in attr.items():
            e.setAttribute(k,str(v).lower() if isinstance(v,bool) else str(v))

        return e

    def addText(self,text):
        '''Add a text child node to an element'''
        t = self._root.createTextNode(text)
        self._node.appendChild(t)
        return t

    def getText(self):
        '''Get contained text node content'''
        if self._node.firstChild is None or \
           self._node.firstChild.nodeType != self._node.firstChild.TEXT_NODE:
            return ''
        return self._node.firstChild.nodeValue
        

    def add(self,cls,**kwargs):
        '''Add an element and return wrapped in cls object'''
        return cls(self,node=None,**kwargs)

    def append(self,elem):
        '''Append and element'''
        if self._node.appendChild(elem._node):
            return elem
        return False

    # ----------------------------------------------------------------
    def remove(self,elem):
        '''Remove an element'''
        try:
            self._node.removeChild(elem._node)
        except:
            return None
        return elem
    # ----------------------------------------------------------------
    def insertBefore(self,toadd,ref):
        '''Insert an element before another element'''
        try:
            self._node.insertBefore(toadd._node,ref._node)
        except:
            return None
        return toadd

# --------------------------------------------------------------------
class DummyElement(Element):
    def __init__(self,parent,node=None,**kwargs):
        '''A dummy element we can use to select elements of different
        classes

        '''  
        super(DummyElement,self).__init__(parent,'Dummy',node=node)

# --------------------------------------------------------------------
class ToolbarElement(Element):
    def __init__(self,
                 parent,
                 tag,
                 node         = None,
                 name         = '', # Toolbar element name
                 tooltip      = '', # Tool tip
                 text         = '', # Button text
                 icon         = '', # Button icon,
                 hotkey       = '', # Named key or key stroke
                 canDisable   = False,
                 propertyGate = '',
                 disabledIcon = '',
                 **kwargs):
        '''Base class for toolbar elements.

        Parameters
        ----------
        parent : Element
            Parent element if any
        tag : str
            Element tag
        node : XMLNode
            Possible node - when reading back
        name : str
            Name of element (user reminder).  If not set, and tooltip is set,
            set to tooltip
        toolttip : str        
            Tool tip when hovering. If not set, and name is set, then
            use name as tooltip.
        text : str
            Text of button
        icon : str
            Image path for button image
        hotkey : str
            Named key or key-sequence
        canDisable : bool
            If true, then the element can be disabled
        propertyGate : str        
            Name of a global property.  When this property is `true`,
            then this element is _disabled_.  Note that this _must_ be
            the name of a property - it cannot be a BeanShell
            expression.
        disabledIcon : str
            Path to image to use when the element is disabled.
        kwargs : dict
            Other attributes to set on the element
        '''
        if name == '' and tooltip != '': name    = tooltip
        if name != '' and tooltip == '': tooltip = name

        # Build arguments for super class 
        args = {
            'node':         node,
            'name':         name,
            'icon':         icon,
            'tooltip':      tooltip,
            'hotkey':       hotkey,
            'canDisable':   canDisable,
            'propertyGate': propertyGate,
            'disabledIcon': disabledIcon }
        bt = kwargs.pop('buttonText',None)
        # If the element expects buttonText attribute, then do not set
        # the text attribute - some elements interpret that as a
        # legacy name attribute,
        if bt is not None:
            args['buttonText'] = bt
        else:
            args['text']       = text
        args.update(kwargs)

        super(ToolbarElement,self).__init__(parent,
                                            tag,
                                            **args)
        # print('Attributes\n','\n'.join([f'- {k}="{v}"' for k,v in self._node.attributes.items()]))
        
#
# EOF
#
# ====================================================================
# From globalkey.py

# --------------------------------------------------------------------
class GlobalKey(ToolbarElement):
    SELECTED = 'MAP|false|MAP|||||0|0||true|Selected|true|EQUALS'
    def __init__(self,
                 parent,
                 tag,
                 node                 = None,
                 name                 = '',                
                 icon                 = '',
                 tooltip              = '',
                 buttonHotkey         = '',
                 buttonText           = '',
                 canDisable           = False,
                 propertyGate         = '',
                 disabledIcon         = '',
                 # Local
                 hotkey               = '',
                 deckCount            = '-1',
                 filter               = '',
                 reportFormat         = '',
                 reportSingle         = False,
                 singleMap            = True,
                 target               = SELECTED):
        '''
        Parameters
        ----------
        - tag          The XML tag to use
        - parent       Parent node
        - node         Optionally existing node
        - name         Name of key
        - buttonHotkey Key in "global" scope
        - hotkey       Key to send to targeted pieces
        - buttonText   Text on button
        - canDisable   If true, disabled when propertyGate is true
        - deckCount    Number of decks (-1 is all)
        - filter       Which units to target
        - propertyGate When true, disable
        - reportFormat Chat message
        - reportSingle Also show single piece reports
        - singleMap    Only originating map if True
        - target       Preselection filter (default selected pieces)
        - tooltip      Hover-over message
        - icon         Image to use as icon

        Default targets are selected units
        '''
        
        super(GlobalKey,self).\
            __init__(parent,
                     tag,
                     node                 = node,
                     name                 = name,
                     icon                 = icon,
                     tooltip              = tooltip,
                     buttonHotkey         = buttonHotkey, # This hot key
                     buttonText           = buttonText,
                     canDisable           = canDisable,
                     propertyGate         = propertyGate,
                     disabledIcon         = disabledIcon,
                     hotkey               = hotkey,       # Target hot key
                     deckCount            = deckCount,
                     filter               = filter,
                     reportFormat         = reportFormat,
                     reportSingle         = reportSingle,
                     singleMap            = singleMap,
                     target               = target)
#
# EOF
# 
# ====================================================================
# From gameelements.py

# --------------------------------------------------------------------
class GameElementService:
    def getGame(self):
        return self.getParentOfClass(Game)

# --------------------------------------------------------------------
class GameElement(Element,GameElementService):
    def __init__(self,game,tag,node=None,**kwargs):
        super(GameElement,self).__init__(game,tag,node=node,**kwargs)

# --------------------------------------------------------------------
class Notes(ToolbarElement,GameElementService):
    TAG = Element.MODULE+'NotesWindow'
    def __init__(self,elem,node=None,
                 name         = 'Notes', # Toolbar element name
                 tooltip      = 'Show notes window', # Tool tip
                 text         = '', # Button text
                 icon         = '/images/notes.gif', # Button icon,
                 hotkey       = key('N',ALT), # Named key or key stroke
                 canDisable   = False,
                 propertyGate = '',
                 disabledIcon = '',
                 description  = ''):
        super(Notes,self).__init__(elem,self.TAG,
                                   node         = node,
                                   name         = name,
                                   tooltip      = tooltip,
                                   text         = text,
                                   icon         = icon,
                                   hotkey       = hotkey,
                                   canDisable   = canDisable,
                                   propertyGate = propertyGate,
                                   disabledIcon = disabledIcon,
                                   description  = description)
    def encode(self):
        return ['NOTES\t\\','PNOTES']

registerElement(Notes)

# --------------------------------------------------------------------
class PredefinedSetup(GameElement):
    TAG = Element.MODULE+'PredefinedSetup'
    def __init__(self,elem,node=None,
                 name             = '',
                 file             = '',
                 useFile          = False,
                 isMenu           = False,
                 description      = ''):
        useFile = ((useFile or not isMenu) and
                   (file is not None and len(file) > 0))
        if file is None: file = ''
        super(PredefinedSetup,self).__init__(elem,self.TAG,node=node,
                                             name        = name,
                                             file        = file,
                                             useFile     = useFile,
                                             isMenu      = isMenu,
                                             description = description)
    def addPredefinedSetup(self,**kwargs):
        '''Add a `PredefinedSetup` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : PredefinedSetup
            The added element
        '''
        return self.add(PredefinedSetup,**kwargs)
    def getPredefinedSetups(self,asdict=True):
        '''Get all PredefinedSetup element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `PredefinedSetup` elements.  If `False`, return a list of all PredefinedSetup` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `PredefinedSetup` children
        '''
        return self.getElementsByKey(PredefinedSetup,'name',asdict)
        
    
        
                   
registerElement(PredefinedSetup)
                  
# --------------------------------------------------------------------
class GlobalTranslatableMessages(GameElement):
    TAG=Element.MODULE+'properties.GlobalTranslatableMessages'
    def __init__(self,elem,node=None):
        '''Translations

        Parameters
        ----------
        doc : Element
            Parent
        node : xml.dom.Element
            Node to read state from
        '''
        super(GlobalTranslatableMessages,self).\
            __init__(elem,self.TAG,node=node)

registerElement(GlobalTranslatableMessages)
        
# --------------------------------------------------------------------
class Language(GameElement):
    TAG = 'VASSAL.i18n.Language'
    def __init__(self,elem,node=None,**kwargs):
        super(Languate,self).__init__(sele,self.TAG,node=none,**kwargs)

registerElement(Language)
        
# --------------------------------------------------------------------
class Chatter(GameElement):
    TAG=Element.MODULE+'Chatter'
    def __init__(self,elem,node=None,**kwargs):
        '''Chat

        Parameters
        ----------
        doc : Element
            Parent
        node : xml.dom.Element
            Node to read state from
        kwargs : dict
            Attributes
        '''
        super(Chatter,self).__init__(elem,self.TAG,node=node,**kwargs)

registerElement(Chatter)
        
# --------------------------------------------------------------------
class KeyNamer(GameElement):
    TAG=Element.MODULE+'KeyNamer'
    def __init__(self,elem,node=None,**kwargs):
        '''Key namer (or help menu)

        Parameters
        ----------
        doc : Element
            Parent
        node : xml.dom.Element
            Node to read state from
        kwargs : dict
            Attributes
        '''
        super(KeyNamer,self).__init__(elem,self.TAG,node=node,**kwargs)
        
registerElement(KeyNamer)
        

# --------------------------------------------------------------------
#    <VASSAL.build.module.GlobalOptions
#      autoReport="Always"
#      centerOnMove="Use Preferences Setting"
#      chatterHTMLSupport="Always"
#      hotKeysOnClosedWindows="Always"
#      inventoryForAll="Never"
#      nonOwnerUnmaskable="Always"
#      playerIdFormat="$PlayerName$"
#      promptString="Opponents can unmask pieces"
#      sendToLocationMoveTrails="Always"
#      storeLeadingZeroIntegersAsStrings="true">
#        <option name="stepIcon">/images/StepForward16.gif</option>
#        <option name="stepHotKey">39,130</option>
#        <option name="undoIcon">/images/Undo16.gif</option>
#        <option name="undoHotKey">90,130</option>
#        <option name="serverControlsIcon">/images/connect.gif</option>
#        <option name="serverControlsHotKey">65,195</option>
#        <option name="debugControlsIcon"/>
#        <option name="debugControlsHotKey">68,195</option>
#    </VASSAL.build.module.GlobalOptions>
class GlobalOptions(GameElement):
    NEVER  = 'Never'
    ALWAYS = 'Always'
    PROMPT = 'Use Preferences Setting'
    TAG    = Element.MODULE+'GlobalOptions'
    def __init__(self,doc,node=None,
                 autoReport               = PROMPT,
                 centerOnMove             = PROMPT,
                 chatterHTMLSupport       = ALWAYS,
                 hotKeysOnClosedWindows   = NEVER,
                 inventoryForAll          = ALWAYS,
                 nonOwnerUnmaskable       = PROMPT,
                 playerIdFormat           = "$playerName$",
                 promptString             = "Opponents can unmask pieces",
                 sendToLocationMoveTrails = NEVER,
                 storeLeadingZeroIntegersAsStrings = False,
                 description                       = 'Global options',
                 dragThreshold                     = 10):
        '''Set global options on the module

        Parameters
        ----------
        doc : Element
            Parent
        node : xml.dom.Element
            Node to read state from
        
        autoReport                        : str='always'
        centerOnMove                      : str Option
        chatterHTMLSupport                : str='never'
        hotKeysOnClosedWindows            : str='never'
        inventoryForAll                   : str='always' 
        nonOwnerUnmaskable                : str='never'
        playerIdFormat                    : str='$PlayerName$'
        promptString                      : str=?
        sendToLocationMoveTrails          : bool=false
        storeLeadingZeroIntegersAsStrings : bool=False
        '''
        super(GlobalOptions,self).\
            __init__(doc,self.TAG,node=node,
                     autoReport               = autoReport,
                     centerOnMove             = centerOnMove,
                     chatterHTMLSupport       = chatterHTMLSupport,
                     hotKeysOnClosedWindows   = hotKeysOnClosedWindows,
                     inventoryForAll          = inventoryForAll,
                     nonOwnerUnmaskable       = nonOwnerUnmaskable,
                     playerIdFormat           = playerIdFormat,
                     promptString             = promptString,
                     sendToLocationMoveTrails = sendToLocationMoveTrails,
                     storeLeadingZeroIntegersAsStrings = storeLeadingZeroIntegersAsStrings,
                     dragThreshold            = dragThreshold,
                     description              = description)

    def addOption(self,**kwargs):
        '''Add a `Option` element to this

        Options known
        - stepIcon - image file name (/images/StepForward16.gif)
        - stepHotKey - key 
        - undoIcon - image file name (/images/Undo16.gif)
        - undoHotKey - key
        - serverControlsIcon - image file name (/images/connect.gif)
        - serverControlsHotKey - key
        - debugControlsIcon - image file name 
        - debugControlsHotKey - key 
        

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Option
            The added element
        '''
        return self.add(Option,**kwargs)
    def getOptions(self):
        return self.getElementsByKey(Option,'name')

    def addPreference(self,cls,**kwargs):
        return self.add(cls,**kwargs)

    def addIntPreference(self,**kwargs):
        return self.add(IntPreference,**kwargs)

    def addFloatPreference(self,**kwargs):
        return self.add(FloatPreference,**kwargs)
    
    def addBoolPreference(self,**kwargs):
        return self.add(BoolPreference,**kwargs)
    
    def addStrPreference(self,**kwargs):
        return self.add(StrPreference,**kwargs)
    
    def addTextPreference(self,**kwargs):
        return self.add(TextPreference,**kwargs)
    
    def addEnumPreference(self,**kwargs):
        return self.add(EnumPreference,**kwargs)
    
    def getIntPreferences(self):
        return self.getElementsByKey(IntPreference,'name')

    def getFloatPreferences(self):
        return self.getElementsByKey(FloatPreference,'name')

    def getBoolPreferences(self):
        return self.getElementsByKey(BoolPreference,'name')

    def getStrPreferences(self):
        return self.getElementsByKey(StrPreference,'name')

    def getTextPreferences(self):
        return self.getElementsByKey(TextPreference,'name')

    def getEnumPreferences(self):
        return self.getElementsByKey(EnumPreference,'name')

    def getPreferences(self):
        retd = {}
        for cls in [IntPreference,
                    FloatPreference,
                    BoolPreference,
                    StrPreference,
                    TextPreference,
                    EnumPreference]:
            retd.update(self.getElementsByKey(cls,'name'))

        return retd
    
registerElement(GlobalOptions)

# --------------------------------------------------------------------
class Option(Element):
    TAG = 'option'
    def __init__(self,doc,node=None,name='',value=''):
        super(Option,self).__init__(doc,tag=self.TAG,node=node,name=name)
        self.addText(value)

    def getGlobalOptions(self):
        return self.getParent(GlobalOptions)

registerElement(Option)
    
# --------------------------------------------------------------------
class Preference(Element):
    PREFS = 'VASSAL.preferences.'
    def __init__(self,
                 doc,
                 tag,
                 node    = None,
                 name    = '',
                 default = '',
                 desc    = '',
                 tab     = '',
                 **kwargs):
        '''Add a preference

        Parameters
        ----------
        name : str
            Name of property
        default : str
            Default value
        desc : str
            Description
        tab : str
            Preference tab to put in to
        '''
        super(Preference,self).__init__(doc,
                                        tag     = tag,
                                        node    = node,
                                        name    = name,
                                        default = default,
                                        desc    = desc,
                                        tab     = tab)

    def getGlobalOptions(self):
        return self.getParent(GlobalOptions)
    
# --------------------------------------------------------------------
class IntPreference(Preference):
    TAG = Preference.PREFS+'IntegerPreference'
    def __init__(self,
                 doc,
                 node    = None,
                 name    = '',
                 default = 0,
                 desc    = '',
                 tab     = ''):
        super(IntPreference,self).__init__(doc,
                                           tag     = self.TAG,
                                           node    = node,
                                           name    = name,
                                           default = str(default),
                                           desc    = desc,
                                           tab     = tab)

registerElement(IntPreference)
    
# --------------------------------------------------------------------
class FloatPreference(Preference):
    TAG = Preference.PREFS+'DoublePreference'
    def __init__(self,
                 doc,
                 node    = None,
                 name    = '',
                 default = 0.,
                 desc    = '',
                 tab     = ''):
        super(FloatPreference,self).__init__(doc,
                                             tag     = self.TAG,
                                             node    = node,
                                             name    = name,
                                             default = str(default),
                                             desc    = desc,
                                             tab     = tab)

registerElement(FloatPreference)
    
# --------------------------------------------------------------------
class BoolPreference(Preference):
    TAG = Preference.PREFS+'BooleanPreference'
    def __init__(self,
                 doc,
                 node    = None,
                 name    = '',
                 default = False,
                 desc    = '',
                 tab     = ''):
        super(BoolPreference,self).__init__(doc,
                                            tag     = self.TAG,
                                            node    = node,
                                            name    = name,
                                            default = ('true' if default
                                                       else 'false'),
                                            desc    = desc,
                                            tab     = tab)

registerElement(BoolPreference)
    
# --------------------------------------------------------------------
class StrPreference(Preference):
    TAG = Preference.PREFS+'StringPreference'
    def __init__(self,
                 doc,
                 node    = None,
                 name    = '',
                 default = '',
                 desc    = '',
                 tab     = ''):
        super(StrPreference,self).__init__(doc,
                                           tag     = self.TAG,
                                           node    = node,
                                           name    = name,
                                           default = default,
                                           desc    = desc,
                                           tab     = tab)

registerElement(StrPreference)
    
# --------------------------------------------------------------------
class TextPreference(Preference):
    TAG = Preference.PREFS+'TextPreference'
    def __init__(self,
                 doc,
                 node    = None,
                 name    = '',
                 default = '',
                 desc    = '',
                 tab     = ''):
        super(TextPreference,self).__init__(doc,
                                            tag     = self.TAG,
                                            node    = node,
                                            name    = name,
                                            default = (default
                                                       .replace('\n','&#10;')),
                                            desc    = desc,
                                            tab     = tab)

registerElement(TextPreference)
    
# --------------------------------------------------------------------
class EnumPreference(Preference):
    TAG = Preference.PREFS+'EnumPreference'
    def __init__(self,
                 doc,
                 node    = None,
                 name    = '',
                 values  = [],
                 default = '',
                 desc    = '',
                 tab     = ''):
        ce = lambda v : str(v).replace(',',r'\,')
        sl = [ce(v) for v in values]
        df = ce(v)
        assert df in sl, \
            f'Default value "{default}" not in list {":".join(values)}'
        super(EnumPreference,self).__init__(doc,
                                            tag     = self.TAG,
                                            node    = node,
                                            name    = name,
                                            default = df,
                                            desc    = desc,
                                            tab     = tab,
                                            list    = sl)


registerElement(EnumPreference)
    
    
# --------------------------------------------------------------------
# CurrentMap == &quot;Board&quot;
class Inventory(ToolbarElement,GameElementService):
    TAG = Element.MODULE+'Inventory'
    def __init__(self,doc,node=None,
                 name                = '',
                 icon                = '/images/inventory.gif',
                 text                = '',
                 tooltip             = 'Show inventory of all pieces',
                 hotkey              = key('I',ALT),
                 canDisable          = False,
                 propertyGate        = '',
                 disabledIcon        = '',                 
                 centerOnPiece       = True,
                 drawPieces          = True,
                 foldersOnly         = False,
                 forwardKeystroke    = True,
                 groupBy             = '',
                 include             = '{}',
                 launchFunction      = 'functionHide',
                 leafFormat          = '$PieceName$',
                 nonLeafFormat       = '$PropertyValue$',
                 pieceZoom           = '0.33',
                 pieceZoom2          = '0.5',
                 pieceZoom3          = '0.6',
                 refreshHotkey       = key('I',ALT_SHIFT),
                 showMenu            = True,
                 sides               = '',
                 sortFormat          = '$PieceName$',
                 sortPieces          = True,
                 sorting             = 'alpha',
                 zoomOn              = False):
        super(Inventory,self).__init__(doc,self.TAG,node=node,
                                       canDisable          = canDisable,
                                       centerOnPiece       = centerOnPiece,
                                       disabledIcon        = disabledIcon,
                                       drawPieces          = drawPieces,
                                       foldersOnly         = foldersOnly,
                                       forwardKeystroke    = forwardKeystroke,
                                       groupBy             = groupBy,
                                       hotkey              = hotkey,
                                       icon                = icon,
                                       include             = include,
                                       launchFunction      = launchFunction,
                                       leafFormat          = leafFormat,
                                       name                = name,
                                       nonLeafFormat       = nonLeafFormat,
                                       pieceZoom           = pieceZoom,
                                       pieceZoom2          = pieceZoom2,
                                       pieceZoom3          = pieceZoom3,
                                       propertyGate        = propertyGate,
                                       refreshHotkey       = refreshHotkey,
                                       showMenu            = showMenu,
                                       sides               = sides,
                                       sortFormat          = sortFormat,
                                       sortPieces          = sortPieces,
                                       sorting             = sorting,
                                       text                = text,
                                       tooltip             = tooltip,
                                       zoomOn              = zoomOn)
                  
registerElement(Inventory)

# --------------------------------------------------------------------
class Prototypes(GameElement):
    TAG = Element.MODULE+'PrototypesContainer'
    def __init__(self,game,node=None,**kwargs):
        super(Prototypes,self).\
            __init__(game,self.TAG,node=node,**kwargs)

    def addPrototype(self,**kwargs):
        '''Add a `Prototype` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Prototype
            The added element
        '''
        return self.add(Prototype,**kwargs)
    def getPrototypes(self,asdict=True):
        '''Get all Prototype element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Prototype` elements.  If `False`, return a list of all Prototype` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `Prototype` children
        '''
        return self.getElementsByKey(Prototype,'name',asdict=asdict)
        
registerElement(Prototypes)

# --------------------------------------------------------------------
class DiceButton(ToolbarElement,GameElementService):
    TAG=Element.MODULE+'DiceButton'
    def __init__(self,elem,node=None,
                 name                 = '1d6',
                 tooltip              = 'Roll a 1d6',
                 text                 = '1d6',
                 icon                 = '/images/die.gif',
                 hotkey               = key('6',ALT),
                 canDisable           = False,
                 propertyGate         = '',
                 disabledIcon         = '',
                 addToTotal           = 0,
                 keepCount            = 1,
                 keepDice             = False,
                 keepOption           = '>',
                 lockAdd              = False,
                 lockDice             = False,
                 lockPlus             = False,
                 lockSides            = False,
                 nDice                = 1,
                 nSides               = 6,
                 plus                 = 0,
                 prompt               = False,
                 reportFormat         = '$name$ = $result$',
                 reportTotal          = False,
                 sortDice             = False):
        super(DiceButton,self).\
            __init__(elem,self.TAG,node=node,
                     addToTotal           = addToTotal,
                     canDisable           = canDisable,
                     disabledIcon         = disabledIcon,
                     hotkey               = hotkey,
                     icon                 = icon,
                     keepCount            = keepCount,
                     keepDice             = keepDice,
                     keepOption           = keepOption,
                     lockAdd              = lockAdd,
                     lockDice             = lockDice,
                     lockPlus             = lockPlus,
                     lockSides            = lockSides,
                     nDice                = nDice,
                     nSides               = nSides,
                     name                 = name,
                     plus                 = plus,
                     prompt               = prompt,
                     propertyGate         = propertyGate,
                     reportFormat         = reportFormat,
                     reportTotal          = reportTotal,
                     sortDice             = sortDice,
                     text                 = text,
                     tooltip              = tooltip)

registerElement(DiceButton)

# --------------------------------------------------------------------
class GameMassKey(GlobalKey,GameElementService):
    TAG = Element.MODULE+'GlobalKeyCommand'
    def __init__(self,map,node=None,
                 name                 = '',                
                 buttonText           = '',
                 tooltip              = '',
                 icon                 = '',
                 canDisable           = False,
                 propertyGate         = '',
                 disabledIcon         = '',
                 buttonHotkey         = '',
                 hotkey               = '',
                 deckCount            = '-1',
                 filter               = '',
                 reportFormat         = '',
                 reportSingle         = False,
                 singleMap            = True,
                 target               = GlobalKey.SELECTED):
        '''Default targets are selected units'''
        super(GameMassKey,self).\
            __init__(map,
                     self.TAG,
                     node                 = node,
                     name                 = name,                
                     buttonHotkey         = buttonHotkey, # This hot key
                     hotkey               = hotkey,       # Target hot key
                     buttonText           = buttonText,
                     canDisable           = canDisable,
                     deckCount            = deckCount,
                     filter               = filter,
                     propertyGate         = propertyGate,
                     reportFormat         = reportFormat,
                     reportSingle         = reportSingle,
                     singleMap            = singleMap,
                     target               = target,
                     tooltip              = tooltip,
                     icon                 = icon)
        
registerElement(GameMassKey)

# --------------------------------------------------------------------
class StartupMassKey(GlobalKey,GameElementService):
    TAG = Element.MODULE+'StartupGlobalKeyCommand'
    FIRST_LAUNCH = 'firstLaunchOfSession'
    EVERY_LAUNCH = 'everyLaunchOfSession'
    START_GAME   = 'startOfGameOnly'
    def __init__(self,
                 map,
                 node                 = None,
                 name                 = '',                
                 buttonHotkey         = '',
                 hotkey               = '',
                 buttonText           = '',
                 canDisable           = False,
                 deckCount            = '-1',
                 filter               = '',
                 propertyGate         = '',
                 reportFormat         = '',
                 reportSingle         = False,
                 singleMap            = True,
                 target               = GlobalKey.SELECTED,
                 tooltip              = '',
                 icon                 = '',
                 whenToApply          = EVERY_LAUNCH):
        '''Default targets are selected units'''
        super(StartupMassKey,self).\
            __init__(map,
                     self.TAG,
                     node                 = node,
                     name                 = name,                
                     buttonHotkey         = buttonHotkey, # This hot key
                     hotkey               = hotkey,       # Target hot key
                     buttonText           = buttonText,
                     canDisable           = canDisable,
                     deckCount            = deckCount,
                     filter               = filter,
                     propertyGate         = propertyGate,
                     reportFormat         = reportFormat,
                     reportSingle         = reportSingle,
                     singleMap            = singleMap,
                     target               = target,
                     tooltip              = tooltip,
                     icon                 = icon)
        if node is None:
            self['whenToApply'] = whenToApply

registerElement(StartupMassKey)

# --------------------------------------------------------------------
class Menu(GameElement):
    TAG = Element.MODULE+'ToolbarMenu'
    def __init__(self,
                 game,
                 node                 = None,
                 name                 = '',
                 tooltip              = '',
                 text                 = '', # Menu name
                 canDisable           = False,
                 propertyGate         = '',
                 disabledIcon         = '',
                 description          = '',
                 hotkey               = '',
                 icon                 = '',
                 menuItems            = []):
        if len(description) <= 0 and len(tooltip) > 0:
            description = tooltip
        if len(tooltip) <= 0 and len(description) > 0:
            tooltip = description 
        super(Menu,self).\
            __init__(game,
                     self.TAG,
                     node                 = node,
                     name                 = name,
                     canDisable           = canDisable,
                     description          = description,
                     disabledIcon         = disabledIcon,
                     hotkey               = hotkey,
                     icon                 = icon,
                     menuItems            = ','.join(menuItems),
                     propertyGate         = propertyGate,
                     text                 = text,
                     tooltip              = tooltip)
                     
registerElement(Menu)

        
# --------------------------------------------------------------------
class SymbolicDice(GameElement):
    TAG = Element.MODULE+'SpecialDiceButton'
    def __init__(self,
                 game,
                 node                    = None,
                 canDisable	         = False,
                 disabledIcon            = '',
                 hotkey                  = key('6',ALT),
                 name                    = "Dice",  # GP prefix
                 text                    = '', # Text on button
                 icon                    = '/images/die.gif', # Icon on button
                 format                  = '{name+": "+result1}', # Report 
                 tooltip                 = 'Die roll', # Help
                 propertyGate            = '', # Property to disable when T
                 resultButton            = False, # Result on button?
                 resultChatter           = True,  # Result in Chatter?
                 resultWindow            = False, # Result window?
                 backgroundColor	 = rgb(0xdd,0xdd,0xdd),  # Window background
                 windowTitleResultFormat = "$name$", # Window title
                 windowX                 = '67', # Window size
                 windowY                 = '65'):
        super(SymbolicDice,self).\
            __init__(game,
                     self.TAG,
                     node                    = node,
                     canDisable	             = canDisable,
                     disabledIcon            = disabledIcon,
                     hotkey                  = hotkey,
                     name                    = name,
                     text                    = text,
                     icon                    = icon,
                     format                  = format,
                     tooltip                 = tooltip,
                     propertyGate            = propertyGate,
                     resultButton            = resultButton,
                     resultChatter           = resultChatter,
                     resultWindow            = resultWindow,
                     backgroundColor	     = backgroundColor,
                     windowTitleResultFormat = windowTitleResultFormat,
                     windowX                 = windowX,
                     windowY                 = windowY)

    def addDie(self,**kwargs):
        return self.add(SpecialDie,**kwargs)

    def getSymbolicDice(self):
        return self.getParent(SymbolicDice)
        
registerElement(SymbolicDice)

        
# --------------------------------------------------------------------
class SpecialDie(GameElement):
    TAG = Element.MODULE+'SpecialDie'
    def __init__(self,
                 symbolic,               # Symblic dice 
                 node                    = None,
                 name                    = '', # Name of dice (no GP)
                 report                  = '{name+": "+result}',
                 faces                   = None):
        super(SpecialDie,self).\
            __init__(symbolic,
                     self.TAG,
                     node = node,
                     name = name,
                     report = report)
        if node is not None or faces is None:
            return
        if isinstance(faces,list):
            faces = {i+1: f for i,f in enumerate(faces)}
        for v,f in faces:
            self.addFace(text = str(v), value = v, icon = f)

    def addFace(self,**kwargs):
        self.add(DieFace,**kwargs)

    def getSymbolicDice(self):
        return self.getParent(SymbolicDice)
        
registerElement(SpecialDie)
                     
# --------------------------------------------------------------------
class DieFace(GameElement):
    TAG = Element.MODULE+'SpecialDieFace'
    def __init__(self,
                 special,               # Special dice
                 node,                  # existing node
                 icon      = '',        # graphical representation
                 text      = '',        # Text representation
                 value     = 0):        # Value representation
        super(DieFace,self).\
            __init__(special,
                     self.TAG,
                     node      = node,
                     icon      = icon,
                     text      = text,
                     value     = value)
                     
    def getSpecialDie(self):
        return self.getParent(SpecialDie)

registerElement(DieFace)
                     
#
# EOF
#
# ====================================================================
# From mapelements.py

# --------------------------------------------------------------------
class MapElementService:
    def getMap(self):
        '''Get map - either a Map or WidgetMap'''
        return self.getParentOfClass([WidgetMap,Map])
        # if self._parent is None:
        #     return None
        # 
        # if 'WidgetMap' in self._parent.tagName:
        #     return self.getParent(WidgetMap)
        #     
        # return self.getParent(Map)
    def getGame(self):
        m = self.getMap()
        if m is not None: return m.getGame()
        return None

# --------------------------------------------------------------------
class MapElement(Element,MapElementService):
    def __init__(self,map,tag,node=None,**kwargs):
        super(MapElement,self).__init__(map,tag,node=node,**kwargs)


# --------------------------------------------------------------------
class PieceLayers(MapElement):
    TAG=Element.MAP+'LayeredPieceCollection'
    def __init__(self,map,node=None,
                 property = 'PieceLayer',
                 description = '',
                 layerOrder = []):
        super(PieceLayers,self).__init__(map,self.TAG,node=node,
                                         property    = property,
                                         description = description,
                                         layerOrder  = ','.join(layerOrder))

    def addControl(self,**kwargs):
        '''Add `LayerControl` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : LayerControl
            The added element
        '''
        return self.add(LayerControl,**kwargs)
    def getControls(self,asdict=True):
        '''Get all `LayerControl` element(s) from this

        Parameters
        ----------
        asdict : bool        
            If `True`, return a dictonary that maps name to
            `LayerControl` elements.  If `False`, return a list of all
            `LayerControl` children.
        
        Returns
        -------
        children : dict or list
            Dictionary or list of `LayerControl` children

        '''
        return self.getElementsByKey(LayerControl,'name',asdict)
                 
registerElement(PieceLayers)
    
# --------------------------------------------------------------------
class LayerControl(MapElement):
    TAG=Element.MAP+'LayerControl'
    CYCLE_UP='Rotate Layer Order Up'
    CYCLE_DOWN='Rotate Layer Order Down'
    ENABLE='Make Layer Active'
    DISABLE='Make Layer Inactive'
    TOGGLE='Switch Layer between Active and Inactive'
    RESET='Reset All Layers'
    def __init__(self,col,node=None,
                 name         = '',
                 tooltip      = '',
                 text         = '',
                 hotkey       = '',
                 icon         = '',
                 canDisable   = False,
                 propertyGate = '', #Property name, disable when property false
                 disabledIcon = '',
                 command      = TOGGLE,
                 skip         = False,
                 layers       = [],
                 description = ''):
        super(LayerControl,self).__init__(col,self.TAG,node=node,
                                          name         = name,
                                          tooltip      = tooltip,
                                          text         = text,
                                          buttonText   = text,
                                          hotkey       = hotkey,
                                          icon         = icon,
                                          canDisable   = canDisable,
                                          propertyGate = propertyGate,
                                          disabledIcon = disabledIcon,
                                          command      = command,
                                          skip         = skip,
                                          layers       = ','.join(layers),
                                          description  = description)

    def getLayers(self):
        '''Get map - either a Map or WidgetMap'''
        return self.getParentOfClass([PieceLayers])
        
registerElement(LayerControl)
        

# --------------------------------------------------------------------
class LineOfSight(MapElement):
    TAG=Element.MAP+'LOS_Thread'
    ROUND_UP        = 'Up'
    ROUND_DOWN      = 'Down'
    ROUND_NEAREST   = 'Nearest whole number'
    FROM_LOCATION   = 'FromLocation'
    TO_LOCATION     = 'ToLocation'
    CHECK_COUNT     = 'NumberOfLocationsChecked'
    CHECK_LIST      = 'AllLocationsChecked'
    RANGE           = 'Range'
    NEVER           = 'Never'
    ALWAYS          = 'Always'
    WHEN_PERSISTENT = 'When persistent'
    CTRL_CLICK      = 'Cltr-Click & Drag'
    
    def __init__(self,map,
                 node=None,
                 threadName         = 'LOS',
                 hotkey             = key('L',ALT),
                 tooltip            = 'Trace line of sight',
                 iconName           = '/images/thread.gif', #'los-icon.png',
                 label              = '',
                 snapLOS            = False,
                 snapStart          = True,
                 snapEnd            = True,
                 report             = (f'{{"Range from "+{FROM_LOCATION}'
                                       f'+" to "+{TO_LOCATION}+" is "'
                                       f'+{RANGE}+" (via "+{CHECK_LIST}+")"}}'),
                 persistent         = CTRL_CLICK,
                 persistentIconName = '/images/thread.gif',
                 globl              = ALWAYS,
                 losThickness       = 3,
                 threadColor        = rgb(255,0,0),
                 drawRange          = True,
                 # rangeBg            = rgb(255,255,255),
                 # rangeFg            = rgb(0,0,0),
                 rangeScale         = 0,
                 hideCounters       = True,
                 hideOpacity        = 50,
                 round              = ROUND_UP,
                 canDisable         = False,
                 propertyGate       = '',
                 disabledIcon       = ''):
        '''Make Line of Sight interface
        
        Parameters
        ----------
        threadName : str
            Name of interface
        hotkey : str
            Start LOS key
        tooltip : str
            Tool tip text
        iconName : str
            Path to button icon
        label : str
            Button text 
        snapLOS : bool
            Wether to snap both ends
        snapStart : bool
            Snap to start
        snapEnd: bool
            Snap to end
        report : str
            Report format
        persistent : str
            When persistent
        persistentIconName : str
            Icon when persistent(?)
        globl : str
            Visisble to opponents
        losThickness : int
            Thickness in pixels
        losColor : str
            Colour of line
        drawRange : bool
            Draw the range next to LOST thread
        rangeBg : str
            Range backgroung colour
        rangeFg : str
            Range foregrond colour
        rangeScale : int
            Scale of range - pixels per unit
        round : str
            How to round range
        hideCounters :bool
            If true, hide counters while making thread
        hideOpacity : int
            Opacity of hidden counters (percent)
        canDisable : bool
            IF true, then can be hidden
        propertyGate : str
            Name of property.  When that property is TRUE, then the
            interface is disabled.  Must be a property name, not an expression.
        disabledIcon : str
            Icon to use when disabled
        '''
        super(LineOfSight,self).__init__(map,self.TAG,
                                         node = node,
                                         threadName         = threadName,
                                         hotkey             = hotkey,
                                         tooltip            = tooltip,
                                         iconName           = iconName,
                                         label              = label,
                                         snapLOS            = snapLOS,
                                         snapStart          = snapStart,
                                         snapEnd            = snapEnd,
                                         report             = report,
                                         persistent         = persistent,
                                         persistentIconName = persistentIconName,
                                         losThickness       = losThickness,
                                         threadColor        = threadColor,
                                         drawRange          = drawRange,
                                         #rangeBg            = rangeBg,
                                         #rangeFg            = rangeFg,
                                         rangeScale         = rangeScale,
                                         hideCounters       = hideCounters,
                                         hideOpacity        = hideOpacity,
                                         round              = round,
                                         canDisable         = canDisable,
                                         propertyGate       = propertyGate,
                                         disabledIcon       = disabledIcon)
        self.setAttribute('global',globl)
                                     
    
registerElement(LineOfSight)
    
# --------------------------------------------------------------------
class StackMetrics(MapElement):
    TAG=Element.MAP+'StackMetrics'
    def __init__(self,map,node=None,
                 bottom               = key('(',0),
                 down                 = key('%',0),
                 top                  = key('&',0),
                 up                   = key("'",0),
                 disabled             = False,
                 exSepX               = 6,   # Expanded (after double click)
                 exSepY               = 18,  # Expanded (after double click)
                 unexSepX             = 8,   # Compact
                 unexSepY             = 16): # Compact
        super(StackMetrics,self).__init__(map,self.TAG,node=node,
                                          bottom               = bottom,
                                          disabled             = disabled,
                                          down                 = down,
                                          exSepX               = exSepX,
                                          exSepY               = exSepY,
                                          top                  = top,
                                          unexSepX             = unexSepX,
                                          unexSepY             = unexSepY,
                                          up                   = up)

registerElement(StackMetrics)

# --------------------------------------------------------------------
class ImageSaver(MapElement):
    TAG=Element.MAP+'ImageSaver'
    def __init__(self,map,node=None,
                 buttonText           = '',
                 canDisable           = False,
                 hotkey               = '',
                 icon                 = '/images/camera.gif',
                 propertyGate         = '',
                 tooltip              = 'Save map as PNG image'):
        super(ImageSaver,self).__init__(map,self.TAG,node=node,
                                        buttonText           = buttonText,
                                        canDisable           = canDisable,
                                        hotkey               = hotkey,
                                        icon                 = icon,
                                        propertyGate         = propertyGate,
                                        tooltip              = tooltip)

registerElement(ImageSaver)

# --------------------------------------------------------------------
class TextSaver(MapElement):
    TAG=Element.MAP+'TextSaver'
    def __init__(self,map,node=None,
                 buttonText           = '',
                 canDisable           = False,
                 hotkey               = '',
                 icon                 = '/images/camera.gif',
                 propertyGate         = '',
                 tooltip              = 'Save map as text'):
        super(TextSaver,self).__init__(map,self.TAG,node=node,
                                        buttonText           = buttonText,
                                        canDisable           = canDisable,
                                        hotkey               = hotkey,
                                        icon                 = icon,
                                        propertyGate         = propertyGate,
                                        tooltip              = tooltip)

registerElement(TextSaver)

# --------------------------------------------------------------------
class ForwardToChatter(MapElement):
    TAG=Element.MAP+'ForwardToChatter'
    def __init__(self,map,node=None,**kwargs):
        super(ForwardToChatter,self).__init__(map,self.TAG,node=node,**kwargs)

registerElement(ForwardToChatter)

# --------------------------------------------------------------------
class MenuDisplayer(MapElement):
    TAG=Element.MAP+'MenuDisplayer'
    def __init__(self,map,node=None,**kwargs):
        super(MenuDisplayer,self).__init__(map,self.TAG,node=node,**kwargs)

registerElement(MenuDisplayer)

# --------------------------------------------------------------------
class MapCenterer(MapElement):
    TAG=Element.MAP+'MapCenterer'
    def __init__(self,map,node=None,**kwargs):
        super(MapCenterer,self).__init__(map,self.TAG,node=node,**kwargs)

registerElement(MapCenterer)

# --------------------------------------------------------------------
class StackExpander(MapElement):
    TAG=Element.MAP+'StackExpander'
    def __init__(self,map,node=None,**kwargs):
        super(StackExpander,self).__init__(map,self.TAG,node=node,**kwargs)

registerElement(StackExpander)

# --------------------------------------------------------------------
class PieceMover(MapElement):
    TAG=Element.MAP+'PieceMover'
    def __init__(self,map,node=None,**kwargs):
        super(PieceMover,self).__init__(map,self.TAG,node=node,**kwargs)

registerElement(PieceMover)

# --------------------------------------------------------------------
class SelectionHighlighters(MapElement):
    TAG=Element.MAP+'SelectionHighlighters'
    def __init__(self,map,node=None,**kwargs):
        super(SelectionHighlighters,self).\
            __init__(map,self.TAG,node=node,**kwargs)

registerElement(SelectionHighlighters)

# --------------------------------------------------------------------
class KeyBufferer(MapElement):
    TAG=Element.MAP+'KeyBufferer'
    def __init__(self,map,node=None,**kwargs):
        super(KeyBufferer,self).__init__(map,self.TAG,node=node,**kwargs)

registerElement(KeyBufferer)

# --------------------------------------------------------------------
class HighlightLastMoved(MapElement):
    TAG=Element.MAP+'HighlightLastMoved'
    def __init__(self,map,node=None,
                 color     = rgb(255,0,0),
                 enabled   = True,
                 thickness = 2):
        super(HighlightLastMoved,self).__init__(map,self.TAG,node=node,
                                                color     = color,
                                                enabled   = enabled,
                                                thickness = thickness)

registerElement(HighlightLastMoved)

# --------------------------------------------------------------------
class CounterDetailViewer(MapElement):
    TAG=Element.MAP+'CounterDetailViewer'
    TOP_LAYER  = 'from top-most layer only'
    ALL_LAYERS = 'from all layers'
    INC_LAYERS = 'from listed layers only'
    EXC_LAYERS = 'from layers other than those listed'
    FILTER = 'by using a property filter'
    def __init__(self,map,node=None,
                 borderWidth          = 0,
                 centerAll            = False,
                 centerText           = False,
                 combineCounterSummary = False,
                 counterReportFormat  = '',
                 delay                = 700,
                 description          = '',
                 display              = TOP_LAYER,
                 emptyHexReportForma  = '$LocationName$',
                 enableHTML           = True,
                 extraTextPadding     = 0,
                 fgColor              = rgb(0,0,0),
                 fontSize             = 9,
                 graphicsZoom         = 1.0,# Zoom on counters
                 hotkey               = key('\n',0),
                 layerList            = '',
                 minDisplayPieces     = 2,
                 propertyFilter       = '',
                 showDeck             = False,
                 showDeckDepth        = 1,
                 showDeckMasked       = False,
                 showMoveSelectde     = False,
                 showNoStack          = False,
                 showNonMovable       = False,
                 showOverlap          = False,
                 showgraph            = True,
                 showgraphsingle      = False,
                 showtext             = False,
                 showtextsingle       = False,
                 stretchWidthSummary  = False,
                 summaryReportFormat  = '$LocationName$',
                 unrotatePieces       = False,
                 version              = 3,
                 verticalOffset       = 0,
                 verticalTopText      = 5,
                 zoomlevel            = 1.0): # showTerrain attributes
        super(CounterDetailViewer,self)\
            .__init__(map,self.TAG,node=node,
                      borderWidth           = borderWidth,
                      centerAll             = centerAll,
                      centerText            = centerText,
                      combineCounterSummary = combineCounterSummary,
                      counterReportFormat   = counterReportFormat,
                      delay                 = delay,
                      description           = description,
                      display               = display,
                      emptyHexReportForma   = emptyHexReportForma,
                      enableHTML            = enableHTML,
                      extraTextPadding      = extraTextPadding,
                      fgColor               = fgColor,
                      fontSize              = fontSize,
                      graphicsZoom          = graphicsZoom,
                      hotkey                = hotkey,
                      layerList             = layerList,
                      minDisplayPieces      = minDisplayPieces,
                      propertyFilter        = propertyFilter,
                      showDeck              = showDeck,
                      showDeckDepth         = showDeckDepth,
                      showDeckMasked        = showDeckMasked,
                      showMoveSelectde      = showMoveSelectde,
                      showNoStack           = showNoStack,
                      showNonMovable        = showNonMovable,
                      showOverlap           = showOverlap,
                      showgraph             = showgraph,
                      showgraphsingle       = showgraphsingle,
                      showtext              = showtext,
                      showtextsingle        = showtextsingle,
                      stretchWidthSummary   = stretchWidthSummary,
                      summaryReportFormat   = summaryReportFormat,
                      unrotatePieces        = unrotatePieces,
                      version               = version,
                      verticalOffset        = verticalOffset,
                      verticalTopText       = verticalTopText,
                      zoomlevel             = zoomlevel)

registerElement(CounterDetailViewer)

# --------------------------------------------------------------------
class GlobalMap(MapElement):
    TAG=Element.MAP+'GlobalMap'
    def __init__(self,map,node=None,
                 buttonText           = '',
                 color                = rgb(255,0,0),
                 hotkey               = key('O',CTRL_SHIFT),
                 icon                 = '/images/overview.gif',
                 scale                = 0.2,
                 tooltip              = 'Show/Hide overview window'):
        super(GlobalMap,self).\
            __init__(map,self.TAG,node=node,
                     buttonText           = buttonText,
                     color                = color,
                     hotkey               = hotkey,
                     icon                 = icon,
                     scale                = scale,
                     tooltip              = 'Show/Hide overview window')

registerElement(GlobalMap)

# --------------------------------------------------------------------
class Zoomer(MapElement):
    TAG = Element.MAP+'Zoomer'
    def __init__(self,map,node=None,
                 inButtonText         = '',
                 inIconName           = '/images/zoomIn.gif',
                 inTooltip            = 'Zoom in',
                 outButtonText        = '',
                 outIconName          = '/images/zoomOut.gif',
                 outTooltip           = 'Zoom out',
                 pickButtonText       = '',
                 pickIconName         = '/images/zoom.png',
                 pickTooltip          = 'Select Zoom',
                 zoomInKey            = key('=',CTRL_SHIFT),
                 zoomLevels           = [0.2,0.25,0.333,0.4,0.5,
                                         0.555,0.625,0.75,1.0,1.25,1.6],
                 zoomOutKey           = key('-'),
                 zoomPickKey          = key('='),
                 zoomStart            = 3):

        '''Zoom start is counting from the back (with default zoom levels,
        and zoom start, the default zoom is 1'''
        lvls = ','.join([str(z) for z in zoomLevels])
        super(Zoomer,self).\
            __init__(map,self.TAG,node=node,
                     inButtonText         = inButtonText,
                     inIconName           = inIconName,
                     inTooltip            = inTooltip,
                     outButtonText        = outButtonText,
                     outIconName          = outIconName,
                     outTooltip           = outTooltip,
                     pickButtonText       = pickButtonText,
                     pickIconName         = pickIconName,
                     pickTooltip          = pickTooltip,
                     zoomInKey            = zoomInKey,
                     zoomLevels           = lvls,
                     zoomOutKey           = zoomOutKey,
                     zoomPickKey          = zoomPickKey,
                     zoomStart            = zoomStart)

registerElement(Zoomer)

# --------------------------------------------------------------------
class HidePiecesButton(MapElement):
    TAG=Element.MAP+'HidePiecesButton'
    def __init__(self,map,node=None,
                 buttonText           = '',
                 hiddenIcon           = '/images/globe_selected.gif',
                 hotkey               = key('O'),
                 showingIcon          = '/images/globe_unselected.gif',
                 tooltip              = 'Hide all pieces on this map'):
        super(HidePiecesButton,self).\
            __init__(map,self.TAG,node=node,
                     buttonText           = buttonText,
                     hiddenIcon           = hiddenIcon,
                     hotkey               = hotkey,
                     showingIcon          = showingIcon,
                     tooltip              = tooltip)
        
registerElement(HidePiecesButton)

# --------------------------------------------------------------------
class MassKey(GlobalKey,MapElementService):
    TAG = Element.MAP+'MassKeyCommand'
    def __init__(self,map,node=None,
                 name                 = '',                
                 buttonHotkey         = '',
                 hotkey               = '',
                 buttonText           = '',
                 canDisable           = False,
                 deckCount            = '-1',
                 filter               = '',
                 propertyGate         = '',
                 reportFormat         = '',
                 reportSingle         = False,
                 singleMap            = True,
                 target               = GlobalKey.SELECTED,
                 tooltip              = '',
                 icon                 = ''):
        '''Default targets are selected units'''
        super(MassKey,self).\
            __init__(map,self.TAG,node=node,
                     name                 = name,                
                     buttonHotkey         = buttonHotkey, # This hot key
                     hotkey               = hotkey,       # Target hot key
                     buttonText           = buttonText,
                     canDisable           = canDisable,
                     deckCount            = deckCount,
                     filter               = filter,
                     propertyGate         = propertyGate,
                     reportFormat         = reportFormat,
                     reportSingle         = reportSingle,
                     singleMap            = singleMap,
                     target               = target,
                     tooltip              = tooltip,
                     icon                 = icon)

registerElement(MassKey)

# --------------------------------------------------------------------
class Flare(MapElement):
    TAG=Element.MAP+'Flare'
    def __init__(self,map,node=None,
                 circleColor          = rgb(255,0,0),
                 circleScale          = True,
                 circleSize           = 100,
                 flareKey             = 'keyAlt',
                 flareName            = 'Map Flare',
                 flarePulses          = 6,
                 flarePulsesPerSec    = 3,
                 reportFormat         = ''):
        super(Flare,self).__init__(map,self.TAG,node=node,
                                   circleColor          = circleColor,
                                   circleScale          = circleScale,
                                   circleSize           = circleSize,
                                   flareKey             = flareKey,
                                   flareName            = flareName,
                                   flarePulses          = flarePulses,
                                   flarePulsesPerSec    = flarePulsesPerSec,
                                   reportFormat         = '')

registerElement(Flare)

# --------------------------------------------------------------------
class AtStart(MapElement):
    TAG = Element.MODULE+'map.SetupStack'
    def __init__(self,map,
                 node            = None,
                 name            = '',
                 location        = '',
                 useGridLocation = True,
                 owningBoard     = '',
                 x               = 0,
                 y               = 0):
        '''Pieces are existing PieceSlot elements'''
        super(AtStart,self).\
            __init__(map,self.TAG,node=node,
                     name            = name,
                     location        = location,
                     owningBoard     = owningBoard,
                     useGridLocation = useGridLocation,
                     x               = x,
                     y               = y)

    def addPieces(self,*pieces):
        '''Add a `Pieces` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Pieces
            The added element
        '''
        # copy pieces here
        copies = []
        for p in pieces:
            c = self.addPiece(p)
            if c is not None:
                copies.append(c)
        return copies
        
    def addPiece(self,piece):
        '''Add a `Piece` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Piece
            The added element
        '''
        if not isinstance(piece,PieceSlot):
            print(f'Trying to add {type(piece)} to AtStart')
            return None
            
        p = piece.clone(self)
        # self._node.appendChild(p._node)
        return p
    
    def getPieces(self,asdict=True):
        '''Get all Piece element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Piece`
            elements.  If `False`, return a list of all Piece`
            children.

        Returns
        -------
        children : dict or list
            Dictionary or list of `Piece` children

        '''
        return self.getElementsWithKey(PieceSlot,'entryName',asdict)

registerElement(AtStart)

#
# EOF
#
# ====================================================================
# From globalproperty.py

# --------------------------------------------------------------------
class GlobalProperties(Element):
    TAG = Element.MODULE+'properties.GlobalProperties'
    def __init__(self,elem,node=None,**named):
        super(GlobalProperties,self).__init__(elem,self.TAG,node=node)
        
        for n, p in named:
            self.addProperty(n, **p)

    def getGame(self):
        return self.getParent(Game)
    def addProperty(self,**kwargs):
        '''Add a `Property` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Property
            The added element
        '''
        return GlobalProperty(self,node=None,**kwargs)
    def getProperties(self):
        return getElementsByKey(GlobalProperty,'name')
        
registerElement(GlobalProperties)

# --------------------------------------------------------------------
class GlobalProperty(Element):
    TAG = Element.MODULE+'properties.GlobalProperty'
    def __init__(self,elem,node=None,
                 name         = '',
                 initialValue = '',
                 isNumeric    = False,
                 min          = "null",
                 max          = "null",
                 wrap         = False,
                 description  = ""):
        super(GlobalProperty,self).__init__(elem,self.TAG,
                                            node         = node,
                                            name         = name,
                                            initialValue = initialValue,
                                            isNumeric    = isNumeric,
                                            min          = min,
                                            max          = max,
                                            wrap         = wrap,
                                            description  = description)

    def getGlobalProperties(self):
        return self.getParent(GlobalProperties)

registerElement(GlobalProperty)

#
# EOF
#
# ====================================================================
# From turn.py

# --------------------------------------------------------------------
class TurnLevel(Element):
    def __init__(self,elem,tag,node=None,**kwargs):
        super(TurnLevel,self).__init__(elem,tag,node=node,**kwargs)

    def addLevel(self,counter=None,phases=None):
        '''Add a `Level` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Level
            The added element
        '''
        if counter is None and phases is None:
            return self
        
        t = TurnCounter if counter is not None else TurnList
        o = counter     if counter is not None else phases

        subcounter = o.pop('counter',None)
        subphases  = o.pop('phases',None)

        s = t(self,node=None,**o)

        return s.addLevel(subcounter, subphases)

    def getUp(self):
        return self.getParent(TurnLevel)
    def addCounter(self,**kwargs):
        '''Add a `Counter` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Counter
            The added element
        '''
        return self.add(self,TurnCounter,**kwargs)
    def addList(self,**kwargs):
        '''Add a `List` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : List
            The added element
        '''
        return self.add(self,TurnList,**kwargs)
    def getCounter(self):
        return self.getAllElements(TurnCounter)
    def getList(self):
        return self.getAllElements(TurnList)

# --------------------------------------------------------------------
class TurnTrack(TurnLevel):
    TAG = Element.MODULE+'turn.TurnTracker'
    def __init__(self,elem,node=None,
                 name             = '',
                 buttonText       = 'Turn',
                 hotkey           = '',
                 icon             = '',
                 length           = -1,
                 lengthStyle      = 'Maximum',
                 nexthotkey       = key('T',ALT),
                 plusButtonSize   = 22,
                 prevhotkey       = key('T',ALT_SHIFT),
                 reportFormat     = 'Turn updated from $oldTurn$ to $newTurn$',
                 turnButtonHeight = 22,
                 fwdOnly          = True,
                 turnFormat       = None,
                 counter          = None,
                 phases           = None):
        levels = (counter if counter is not None else
                  phases if phases is not None else None)
        if levels is not None:
            lvl = 1
            lvls = [f'$level{lvl}$']
            sub  = levels
            while True:
                sub = sub.get('counter',sub.get('phases',None))
                if sub is None:
                    break
                lvl += 1
                lvls.append(f'$level{lvl}$')
            
            turnFormat = ' '.join(lvls)
        
        if turnFormat is None:
            turnFormat = '$level1$ $level2$ $level3$ $level4$'        
        
        super(TurnTrack,self).__init__(elem, self.TAG,
                                       node             = node,
                                       name             = name,
                                       buttonText       = buttonText,
                                       hotkey           = hotkey,
                                       icon             = icon,
                                       length           = length,
                                       lengthStyle      = lengthStyle,
                                       nexthotkey       = nexthotkey,
                                       plusButtonSize   = plusButtonSize,
                                       prevhotkey       = prevhotkey,
                                       reportFormat     = reportFormat,
                                       turnButtonHeight = turnButtonHeight,
                                       turnFormat       = turnFormat)

        self.addLevel(counter=counter, phases=phases)

    def getGame(self):
        return self.getParent(Game)
    def getLists(self,asdict=True):
        '''Get all List element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `List`
            elements.  If `False`, return a list of all List`
            children.
        
        Returns
        -------
        children : dict or list
            Dictionary or list of `List` children

        '''
        return self.getElementsByKey(TurnList,'property',asdict=asdict)
    def getCounters(self,asdict=True):
        '''Get all Counter element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Counter`
            elements.  If `False`, return a list of all Counter`
            children.

        Returns
        -------
        children : dict or list
            Dictionary or list of `Counter` children

        '''
        return self.getElementsByKey(TurnCounter,'property',asdict=asdict)
    def addHotkey(self,**kwargs):
        '''Add a `Hotkey` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Hotkey
            The added element
        '''
        return self.add(TurnGlobalHotkey,**kwargs)
    def getHotkeys(self,asdict):
        return self.getElementsByKey(TurnGlobalHotkey,'name',asdict=asdict)
    def encode(self):
        ret = f'TURN{self["name"]}\t'
        
        return []

registerElement(TurnTrack)

# --------------------------------------------------------------------
class TurnCounter(TurnLevel):
    TAG = Element.MODULE+"turn.CounterTurnLevel"
    def __init__(self,elem,node=None,
                 property      = '',
                 start         = 1,
                 incr          = 1,
                 loop          = False,
                 loopLimit     = -1,
                 turnFormat    = "$value$"):
        super(TurnCounter,self).__init__(elem,self.TAG,node=node,
                                         property       = property,
                                         start          = start,
                                         incr           = incr,
                                         loop           = loop,
                                         loopLimit      = loopLimit,
                                         turnFormat     = turnFormat)
                    
registerElement(TurnCounter)

# --------------------------------------------------------------------
class TurnList(TurnLevel):
    TAG = Element.MODULE+"turn.ListTurnLevel"
    def __init__(self,elem,node=None,
                 property      = '',
                 names         = [],
                 configFirst   = False,
                 configList    = False,
                 turnFormat    = '$value$'):
        super(TurnList,self).\
            __init__(elem,self.TAG,node=node,
                     property       = property,
                     list           = ','.join([str(p) for p in names]),
                     configFirst    = configFirst,
                     configList     = configList,
                     turnFormat     = turnFormat)
                  
registerElement(TurnList)

# --------------------------------------------------------------------
class TurnGlobalHotkey(Element):
    TAG = Element.MODULE+'turn.TurnGlobalHotkey'
    def __init__(self,elem,
                 node         = None,
                 hotkey       = '',
                 match        = '{true}',
                 reportFormat = '',
                 name         = ''):
        '''Global key activated by turn change

        Parameters
        ----------
        doc : Element
            Parent
        node : xml.dom.Element
            Node to read state from
        hotkey : str
            What to send (global command)
        match : str
            When to send
        reportFormat : str
            What to what
        name : str
            A free form name
        '''
        super(TurnGlobalHotkey,self).__init__(elem,self.TAG,
                                              node         = node,
                                              hotkey       = hotkey,
                                              match        = match,
                                              reportFormat = reportFormat,
                                              name         = name)

    def getTurnTrack(self):
        '''Get the turn track'''
        return self.getParent(TurnTrack)

registerElement(TurnGlobalHotkey)

#
# EOF
#
# ====================================================================
# From documentation.py

# ====================================================================
def createKeyHelp(*args,**kwargs):
    '''Creates a help file with key-bindings

    See Documentation.createKeyHelp
    '''
    return Documentation.createKeyHelp(*args,**kwargs)

# --------------------------------------------------------------------
class Documentation(GameElement):
    TAG=Element.MODULE+'Documentation'
    def __init__(self,doc,node=None,**kwargs):
        '''Documentation (or help menu)

        Parameters
        ----------
        doc : Element
            Parent
        node : xml.dom.Element
            Node to read state from
        kwargs : dict
            Attributes
        '''
        super(Documentation,self).__init__(doc,self.TAG,node=node,**kwargs)

    def addAboutScreen(self,**kwargs):
        '''Add a `AboutScreen` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : AboutScreen
            The added element
        '''
        return self.add(AboutScreen,**kwargs)
    def addHelpFile(self,**kwargs):
        '''Add a `HelpFile` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : HelpFile
            The added element
        '''
        return self.add(HelpFile,**kwargs)
    def addBrowserHelpFile(self,**kwargs):
        '''Add a `BrowserHelpFile` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : BrowserHelpFile
            The added element
        '''
        return self.add(HelpBrowserFile,**kwargs)
    def addBrowserPDFFile(self,**kwargs):
        '''Add a `BrowserPDFFile` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : BrowserPDFFile
            The added element
        '''
        return self.add(BrowserPDFFile,**kwargs)
    def addTutorial(self,**kwargs):
        '''Add a `Tutorial` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Tutorial
            The added element
        '''
        return self.add(Tutorial,**kwargs)
    def getAboutScreens(self):
        return self.getElementsByKey(AboutScreen,'title')
    def getHelpFiles(self):
        return self.getElementsByKey(HelpFile,'title')
    def getBrowserHelpFiles(self):
        return self.getElementsByKey(BrowserHelpFile,'title')
    def getBrowserPDFFiles(self):
        return self.getElementsByKey(BrowserPDFFile,'title')
    def getTutorials(self):
        return self.getElementsByKey(Tutorial,'name')

    @classmethod
    def createKeyHelp(cls,keys,title='',version=''):
        '''Creates a help file with key-bindings
        
        Parameters
        ----------
        keys : list of list of str
             List of key-binding documentation
        title : str
             Title of help file
        version : str
             Version number
        
        Returns
        -------
        txt : str
            File content
        '''
        txt = f'''
        <html>
         <body>
          <h1>{title} (Version {version}) Key bindings</h1>
          <table>
          <tr><th>Key</th><th>Where</th><th>Effect</th></tr>'''
        
        for key, where, description in keys:
            txt += (f'<tr><td>{key}</td>'
                    f'<td>{where}</td>'
                    f'<td>{description}</td></tr>')
        
        txt += '''
          </table>
         </body>
        </html>'''
        
        return txt 

registerElement(Documentation)

# --------------------------------------------------------------------
class AboutScreen(Element):
    TAG = Element.MODULE+'documentation.AboutScreen'
    def __init__(self,doc,node=None,title='',fileName=""):
        '''Create an about screen element that shows image

        Parameters
        ----------
        doc : Element
            Parent
        node : xml.dom.Element
            Node to read state from
        title : str
            Entry title
        fileName : str
            Internal file name        
        '''
        super(AboutScreen,self).__init__(doc,
                                         self.TAG,
                                         node     = node,
                                         fileName = fileName,
                                         title    = title)
    def getDocumentation(self):
        '''Get Parent element'''
        return self.getParent(Documentation)

registerElement(AboutScreen)

# --------------------------------------------------------------------
class BrowserPDFFile(Element):
    TAG = Element.MODULE+'documentation.BrowserPDFFile'
    def __init__(self,doc,node=None,title='',pdfFile=''):
        '''Create help menu item that opens an embedded PDF
        
        Parameters
        ----------
        doc : Element
            Parent
        node : xml.dom.Element
            Node to read state from
        title : str
            Entry title
        pdfFile : str
            Internal file name
        '''
        super(BrowserPDFFile,self).__init__(doc,self.TAG,
                                            node    = node,
                                            pdfFile = pdfFile,
                                            title   = title)
    def getDocumentation(self):
        '''Get Parent element'''
        return self.getParent(Documentation)

registerElement(BrowserPDFFile)
    
# --------------------------------------------------------------------
class HelpFile(Element):
    TAG = Element.MODULE+'documentation.HelpFile'
    ARCHIVE = 'archive'
    def __init__(self,doc,node=None,
                 title='',
                 fileName='',
                 fileType=ARCHIVE):
        '''Create a help menu entry that opens an embeddded file
        
        Parameters
        ----------
        doc : Element
            Parent
        node : xml.dom.Element
            Node to read state from
        title : str
            Entry title
        fileName : str
            Internal file name
        fileType : str
            How to find the file 
        '''
        super(HelpFile,self).__init__(doc,self.TAG,node=node,
                                      fileName = fileName,
                                      fileType = fileType,
                                      title    = title)

    def getDocumentation(self):
        '''Get Parent element'''
        return self.getParent(Documentation)

registerElement(HelpFile)
    
# --------------------------------------------------------------------
class BrowserHelpFile(Element):
    TAG = Element.MODULE+'documentation.BrowserHelpFile'
    def __init__(self,doc,node=None,
                 title='',
                 startingPage='index.html'):
        '''Create a help menu entry that opens an embeddded HTML
        page (with possible sub-pages) file
        
        Parameters
        ----------
        doc : Element
            Parent
        node : xml.dom.Element
            Node to read state from
        title : str
            Entry title
        startingPage : str
            which file to start at
        '''
        super(BrowserHelpFile,self).__init__(doc,self.TAG,node=node,
                                             startingPage=startingPage,
                                             title=title)

    def getDocumentation(self):
        '''Get Parent element'''
        return self.getParent(Documentation)

registerElement(BrowserHelpFile)
    
# --------------------------------------------------------------------
class Tutorial(Element):
    TAG = Element.MODULE+'documentation.Tutorial'
    def __init__(self,doc,node=None,
                 name            = 'Tutorial',
                 logfile         = 'tutorial.vlog',
                 promptMessage   = 'Load the tutorial?',
                 welcomeMessage  = 'Press "Step forward" (PnDn) to step through the tutorial',
                 launchOnStartup = True):
        '''Add a help menu item that loads the tutorial

        Also adds the start-up option to run the tutorial


        Parameters
        ----------
        doc : Element
            Parent
        node : xml.dom.Element
            Node to read state from
        name : str
            Name of entry
        logfile : str
            Internal file name
        promptMessage : str
            What to show
        launchOnStartup : bool
            By default, launch tutorial first time running module
        '''
        super(Tutorial,self).__init__(doc,self.TAG,node=node,
                                      name            = name,
                                      logfile         = logfile,
                                      promptMessage   = promptMessage,
                                      welcomeMessage  = welcomeMessage,
                                      launchOnStartup = launchOnStartup)

    def getDocumentation(self):
        '''Get Parent element'''
        return self.getParent(Documentation)

registerElement(Tutorial)
    

#
# EOF
#
# ====================================================================
# From player.py

# --------------------------------------------------------------------
class PlayerRoster(GameElement):
    TAG = Element.MODULE+'PlayerRoster'
    def __init__(self,doc,node=None,buttonKeyStroke='',
               buttonText='Retire',
               buttonToolTip='Switch sides, become observer, or release faction'):
        '''Add a player roster to the module
        Parameters
        ----------
        doc : Element
            Parent
        node : xml.dom.Element
            Node to read state from
        buttonText : str
            Text on button
        buttonTooltip : str
            Tool tip to show when hovering over button
        '''
        super(PlayerRoster,self).__init__(doc,self.TAG,node=node,
                                          buttonKeyStroke = buttonKeyStroke,
                                          buttonText      = buttonText,
                                          buttonToolTip   = buttonToolTip)
    def addSide(self,name):
        '''Add a `Side` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Side
            The added element
        '''
        return self.add(PlayerSide,name=name)
    def getSides(self):
        '''Get all sides'''
        return self.getAllElements(PlayerSide,False)
    def encode(self):
        '''Encode for save'''
        return ['PLAYER\ta\ta\t<observer>']

registerElement(PlayerRoster)

# --------------------------------------------------------------------
class PlayerSide(Element):
    TAG = 'entry'
    def __init__(self,doc,node=None,name=''):
        '''Adds a side to the player roster

        Parameters
        ----------
        doc : Element
            Parent
        node : xml.dom.Element
            Node to read state from
        name : str
            Name of side 
        '''
        super(PlayerSide,self).__init__(doc,self.TAG,node=node)
        if node is None:
            self.addText(name)

    def getPlayerRoster(self):
        '''Get Parent element'''
        return self.getParent(PlayerRoster)

registerElement(PlayerSide)


#
# EOF
#
# ====================================================================
# From chessclock.py

# ====================================================================
class ChessClock(Element):
    TAG=Element.MODULE+'chessclockcontrol.ChessClock'
    def __init__(self,
                 doc,
                 node                   = None,
                 icon                   = '',
                 description            = '',
                 side                   = '',
                 tooltip                = 'Individual clock control',
                 buttonText             = '',
                 startHotkey            = '',
                 stopHotkey             = '',
                 tickingBackgroundColor = rgb(255,255,0),
                 tickingFontColor       = rgb(0,0,0),
                 tockingFontColor       = rgb(51,51,51)):
        '''Individual clock for a side

        When the clock is running, the background colour may be
        changed, and the colour of the numbers alternate between
        `tickingFontColor` and `tockingFontColor`.
        
        Parameters
        ----------
        doc : Element
            Parent element 
        node : xml.dom.Element 
            Read from this node
        icon : str
            File name of button icon
        description : str
            Note on this clock
        side : str
            Name of side this clock belongs to
        tooltop : str
            Hover help text
        buttonText : str
            Text on button
        startHotkey : str (key code)
            Key or command to start timer
        stopHotkey : str (key code)
            Key or command to stop timer
        tickingBackgroundColor : str (color)
            Background color of time display when clock is running
        tickingFontColor : str (color)
            First color of numbers in display when clock is running.
        tockingFontColor : str (color)
            Second color of numbers in display when clock is running.
        '''
        super(ChessClock,self).__init__(#ChessClock
                 doc,
                 self.TAG,
                 node                   = node,
                 icon                   = icon,
                 description            = description,
                 side                   = side,
                 tooltip                = tooltip,
                 buttonText             = buttonText,
                 startHotkey            = startHotkey,
                 stopHotkey             = stopHotkey,
                 tickingBackgroundColor = tickingBackgroundColor,
                 tickingFontColor       = tickingFontColor,
                 tockingFontColor       = tockingFontColor)
            
    def getControl(self):
        '''Get Parent element'''
        return self.getParent(ChessClockControl)

registerElement(ChessClock)

# ====================================================================
class ChessClockControl(GameElement):
    TAG=Element.MODULE+'ChessClockControl'
    ALWAYS = 'Always'
    AUTO   = 'Auto'
    NEVER  = 'Never'
    def __init__(self,
                 doc,
                 node              = None,
                 name              = 'Chess clock',
                 description       = '',
                 buttonIcon        = 'chess_clock.png',
                 buttonText        = '',
                 buttonTooltip     = 'Show/stop/hide chess clocks',
                 showHotkey        = key('U',ALT),
                 pauseHotkey       = key('U',CTRL_SHIFT),
                 nextHotkey        = key('U'),
                 startOpponentKey  = '',
                 showTenths        = AUTO,
                 showSeconds       = AUTO,
                 showHours         = AUTO,
                 showDays          = AUTO,
                 allowReset        = False,
                 addClocks         = True):
        '''A set of chess clocs

        Parameters
        ----------
        doc : Element
            Parent
        node : xml.dom.Element
            Node to read state from
        name : str
            Name of clock control
        description : str
            Note on the chess clocks control
        buttonIcon : str
            Icon file name for button (chess_clock.png)
        buttonText : str
            Text on button 
        buttonTooltip : str
            Hower help
        showHotkey : str (key code)
            Show or hide interface hot key
        nextHotkey : str (key code)
            Start the next clock hot key
        pauseHotkey : str (key code)
            Pause all clocks hot key 
        startOpponentKey : str (key code)
            Start opponens clock 
        showTenths : one of AUTO, ALWAYS, NEVER
            Whether to show tenths of seconds
        showSeconds : one of AUTO, ALWAYS, NEVER
            Whether to show seconds in clock 
        showHours : one of AUTO, ALWAYS, NEVER
            Whether to show hours in clock
        showDays : one of AUTO, ALWAYS, NEVER
            Whether to show days in clock
        allowReset : boolean
            If true, allow manual reset of all clocks
        '''
        super(ChessClockControl,self).__init__(# ChessclockControl
            doc,
            self.TAG,
            node              = node,
            name              = name,
            description       = description,
            buttonIcon        = buttonIcon,
            buttonText        = buttonText,
            buttonTooltip     = buttonTooltip,
            showHotkey        = showHotkey,
            pauseHotkey       = pauseHotkey,
            nextHotkey        = nextHotkey,
            startOpponentKey  = startOpponentKey,
            showTenths        = showTenths,
            showSeconds       = showSeconds,
            showHours         = showHours,
            showDays          = showDays,
            allowReset        = allowReset)
        print(node,addClocks)
        if node is not None or not addClocks:
            return
        
        print('--- Will add clocks')
        game   = self.getGame()
        roster = game.getPlayerRoster()[0]
        sides  = roster.getSides()
        for side in sides:
            name = side.getText()
            self.addClock(side        = name,
                          tooltip     = f'Clock for {name}',
                          buttonText  = name,
                          startHotkey = key('U'),
                          stopHotkey  = key('U'))
    
    def addClock(self,**kwargs):
        '''Add a clock element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : AboutScreen
            The added element
        '''
        return self.add(ChessClock,**kwargs)
    def getClocks(self,asdict=True):
        '''Return dictionary of clocs'''
        return self.getElementsByKey(ChessClock,'side',asdict)

registerElement(ChessClockControl)

#
# EOF
#
# ====================================================================
# From widget.py

# --------------------------------------------------------------------
class WidgetElement:
    def __init__(self):
        pass

    def addTabs(self,**kwargs):
        '''Add a `Tabs` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Tabs
            The added element
        '''
        return self.add(TabWidget,**kwargs)
    def addCombo(self,**kwargs):
        '''Add a drop-down menu to this

        Parameters
        ----------
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Combo
            The added element
        '''
        return self.add(ComboWidget,**kwargs)
    def addPanel(self,**kwargs):
        '''Add a `Panel` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Panel
            The added element
        '''
        return self.add(PanelWidget,**kwargs)
    def addList(self,**kwargs):
        '''Add a `List` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : List
            The added element
        '''
        return self.add(ListWidget,**kwargs)
    def addMapWidget(self,**kwargs):
        '''Add a `MapWidget` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : MapWidget
            The added element
        '''
        return self.add(MapWidget,**kwargs)
    def addChart(self,**kwargs):
        '''Add a `Chart` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Chart
            The added element
        '''
        return self.add(Chart,**kwargs)
    def addPieceSlot(self,**kwargs):
        '''Add a `PieceSlot` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : PieceSlot
            The added element
        '''
        return self.add(PieceSlot,**kwargs)
    def addPiece(self,piece):
        '''Add a `Piece` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Piece
            The added element
        '''
        if not isinstance(piece,PieceSlot):
            print(f'Trying to add {type(piece)} to ListWidget')
            return None
            
        p = piece.clone(self)
        return p
    def getTabs(self,asdict=True):
        '''Get all Tab element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Tab` elements.  If `False`, return a list of all Tab` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `Tab` children
        '''
        return self.getElementsByKey(TabWidget,'entryName',asdict)
    def getCombos(self,asdict=True):
        '''Get all Combo element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Tab` elements.  If `False`, return a list of all Tab` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `Tab` children
        '''
        return self.getElementsByKey(ComboWidget,'entryName',asdict)
    def getLists(self,asdict=True):
        '''Get all List element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `List` elements.  If `False`, return a list of all List` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `List` children
        '''
        return self.getElementsByKey(ListWidget,'entryName',asdict)
    def getPanels(self,asdict=True):
        '''Get all Panel element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Panel` elements.  If `False`, return a list of all Panel` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `Panel` children
        '''
        return self.getElementsByKey(PanelWidget,'entryName',asdict)
    def getMapWidgets(self,asdict=True):
        '''Get all MapWidget element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `MapWidget` elements.  If `False`, return a list of all MapWidget` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `MapWidget` children
        '''
        return self.getElementsByKey(MapWidget,'entryName',asdict)
    def getCharts(self,asdict=True):
        '''Get all Chart element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Chart` elements.  If `False`, return a list of all Chart` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `Chart` children
        '''
        return self.getElementsByKey(Chart,'chartName',asdict)
    def getPieceSlots(self,asdict=True):
        '''Get all PieceSlot element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `PieceSlot` elements.  If `False`, return a list of all PieceSlot` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `PieceSlot` children
        '''
        return self.getElementsByKey(PieceSlot,'entryName',asdict)

# --------------------------------------------------------------------
class PieceWindow(GameElement,WidgetElement):
    TAG=Element.MODULE+'PieceWindow'
    def __init__(self,elem,node=None,
                 name         = '',
                 defaultWidth = 0,
                 hidden       = False,
                 hotkey       = key('C',ALT),
                 scale        = 1.,
                 text         = '',
                 tooltip      = 'Show/hide piece window',
                 icon         = '/images/counter.gif'):
        super(PieceWindow,self).__init__(elem,self.TAG,node=node,
                                         name = name,
                                         defaultWidth = defaultWidth,
                                         hidden       = hidden,
                                         hotkey       = hotkey,
                                         scale        = scale,
                                         text         = text,
                                         tooltip      = tooltip,
                                         icon         = icon)

registerElement(PieceWindow)

# --------------------------------------------------------------------
class ComboWidget(Element,WidgetElement):
    TAG=Element.WIDGET+'BoxWidget'
    def __init__(self,elem,node=None,entryName='',width=0,height=0):
        super(ComboWidget,self).__init__(elem,
                                       self.TAG,
                                       node = node,
                                       entryName = entryName,
                                       width     = width,
                                       height    = height)
        
registerElement(ComboWidget)

# --------------------------------------------------------------------
class TabWidget(Element,WidgetElement):
    TAG=Element.WIDGET+'TabWidget'
    def __init__(self,elem,node=None,entryName=''):
        super(TabWidget,self).__init__(elem,
                                       self.TAG,
                                       node      = node,
                                       entryName = entryName)

registerElement(TabWidget)

# --------------------------------------------------------------------
class ListWidget(Element,WidgetElement):
    TAG=Element.WIDGET+'ListWidget'
    def __init__(self,elem,node = None,
                 entryName = '',
                 height    = 0,
                 width     = 300,
                 scale     = 1.,
                 divider   = 150):
        super(ListWidget,self).__init__(elem,self.TAG,node=node,
                                        entryName = entryName,
                                        height    = height,
                                        width     = width,
                                        scale     = scale,
                                        divider   = divider)

registerElement(ListWidget)

# --------------------------------------------------------------------
class PanelWidget(Element,WidgetElement):
    TAG=Element.WIDGET+'PanelWidget'
    def __init__(self,elem,node=None,
                 entryName = '',
                 fixed     = False,
                 nColumns  = 1,
                 vert      = False):
        super(PanelWidget,self).__init__(elem,self.TAG,node=node,
                                         entryName = entryName,
                                         fixed     = fixed,
                                         nColumns  = nColumns,
                                         vert      = vert)

registerElement(PanelWidget)

# --------------------------------------------------------------------
class MapWidget(Element):
    TAG=Element.WIDGET+'MapWidget'
    def __init__(self,elem,node=None,entryName=''):
        super(MapWidget,self).__init__(elem,self.TAG,
                                       node      = node,
                                       entryName = entryName)

    def addWidgetMap(self,**kwargs):
        '''Add a `WidgetMap` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : WidgetMap
            The added element
        '''
        return self.add(WidgetMap,**kwargs)
    def getWidgetMaps(self,asdict=True):
        '''Get all WidgetMap element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `WidgetMap` elements.  If `False`, return a list of all WidgetMap` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `WidgetMap` children
        '''
        return self.getElementsByKey(WidgetMap,'mapName',asdict=asdict)
    
registerElement(MapWidget)

#
# EOF
#
# ====================================================================
# From grid.py

# --------------------------------------------------------------------
HEX_WIDTH = 88.50779626676963
HEX_HEIGHT = 102.2
RECT_WIDTH  = 80
RECT_HEIGHT = 80
# --------------------------------------------------------------------
class BaseGrid(Element):
    def __init__(self,zone,tag,node=None,
                 color        = rgb(0,0,0),
                 cornersLegal = False,
                 dotsVisible  = False,
                 dx           = HEX_WIDTH,  # Meaning seems reversed!
                 dy           = HEX_HEIGHT,
                 edgesLegal   = False,
                 sideways     = False,
                 snapTo       = True,
                 visible      = True,
                 x0           = 0,
                 y0           = 32):
        super(BaseGrid,self).__init__(zone,tag,node=node,
                                      color        = color,
                                      cornersLegal = cornersLegal,
                                      dotsVisible  = dotsVisible,
                                      dx           = dx,
                                      dy           = dy,
                                      edgesLegal   = edgesLegal,
                                      sideways     = sideways,
                                      snapTo       = snapTo,
                                      visible      = visible,
                                      x0           = x0,
                                      y0           = y0)
    def getZone(self):
        z = self.getParent(Zone)
        return z
    def getZonedGrid(self):
        z = self.getZone()
        if z is not None:
            return z.getZonedGrid()
        return None
    def getBoard(self):
        z = self.getZonedGrid()
        if z is not None:
            return z.getBoard()
        return self.getParent(Board)
    def getPicker(self):
        z = self.getBoard()
        if z is not None:
            return z.getPicker()
        return None
    def getMap(self):
        b = self.getPicker()
        if b is not None:
            return b.getMap()
        return None
    def getNumbering(self):
        pass 
    def getLocation(self,loc):
        numbering = self.getNumbering()
        if numbering is None or len(numbering) < 1:
            return None

        return numbering[0].getLocation(loc)
    
# --------------------------------------------------------------------
class BaseNumbering(Element):
    def __init__(self,grid,tag,node=None,
                 color                = rgb(255,0,0),
                 first                = 'H',
                 fontSize             = 24,
                 hDescend             = False,
                 hDrawOff             = 0,
                 hLeading             = 1,
                 hOff                 = 0,
                 hType                = 'A',
                 locationFormat       = '$gridLocation$',
                 rotateText           = 0,
                 sep                  = '',
                 stagger              = True,
                 vDescend             = False,
                 vDrawOff             = 32,
                 vLeading             = 0,
                 vOff                 = 0,
                 vType                = 'N',
                 visible              = True):
        super(BaseNumbering,self).__init__(grid,tag,node=node,
                                           color           = color,
                                           first           = first,
                                           fontSize        = fontSize,
                                           hDescend        = hDescend,
                                           hDrawOff        = hDrawOff,
                                           hLeading        = hLeading,
                                           hOff            = hOff,
                                           hType           = hType,
                                           locationFormat  = locationFormat,
                                           rotateText      = rotateText,
                                           sep             = sep,
                                           stagger         = stagger,
                                           vDescend        = vDescend,
                                           vDrawOff        = vDrawOff,
                                           vLeading        = vLeading,
                                           vOff            = vOff,
                                           vType           = vType,
                                           visible         = visible)
    def getGrid(self): return getParent(BaseGrid)

    def _getMatcher(self,tpe,leading):
        if tpe == 'A':
            return \
                '-?(?:A+|B+|C+|D+|E+|F+|G+|H+|I+|'  + \
                'J+|K+|L+|M+|N+|O+|P+|Q+|R+|S+|T+|' + \
                'U+|V+|W+|X+|Y+|Z+)'

        return f'-?[0-9]{{{int(leading)+1},}}'

    def _getIndex(self,name,tpe):
        if tpe == 'A':
            negative = name.startswith('-')
            if negative:
                name = name[1:]

            value = 0
            for num,let in enumerate(name):
                if not let.isupper():
                    continue
                if num < len(name) - 1:
                    value += 26
                else:
                    value += ord(let)-ord('A')

            if negative:
                value *= -1

            return value

        return int(name)

    def _getCenter(self,col,row):
        '''Convert col and row index to picture coordinates'''
        print('Dummy GetCenter')
        pass
    
    def getLocation(self,loc):
        '''Get picture coordinates from grid location'''
        from re import match

        first   = self['first']
        vType   = self['vType']
        hType   = self['hType']
        vOff    = int(self['vOff'])
        hOff    = int(self['hOff'])
        colPat  = self._getMatcher(hType,self['hLeading'])
        rowPat  = self._getMatcher(vType,self['vLeading'])
        patts   = ((colPat,rowPat) if first == 'H' else (rowPat,colPat))
        colGrp  = 1 if first == 'H' else 2
        rowGrp  = 2 if first == 'H' else 1
        patt    = ''.join([f'({p})' for p in patts])
        matched = match(patt,loc)
        if not matched:
            return None

        rowStr  = matched[rowGrp]
        colStr  = matched[colGrp]
        rowNum  = self._getIndex(rowStr,vType)
        colNum  = self._getIndex(colStr,hType)

        return self._getCenter(colNum-hOff, rowNum-vOff);
       
    
# --------------------------------------------------------------------
class HexGrid(BaseGrid):
    TAG = Element.BOARD+'HexGrid'
    def __init__(self,zone,node=None,**kwargs):
        super(HexGrid,self).__init__(zone,self.TAG,node=node,**kwargs)

    def addNumbering(self,**kwargs):
        '''Add a `Numbering` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Numbering
            The added element
        '''
        return self.add(HexNumbering,**kwargs)
    def getNumbering(self):
        return self.getAllElements(HexNumbering)
    def getDeltaX(self):
        return float(self['dx'])
    def getDeltaY(self):
        return float(self['dy'])
    def getXOffset(self):
        return int(self['x0'])
    def getYOffset(self):
        return int(self['y0'])
    def getMaxRows(self):
        from math import floor
        height    = self.getZone().getHeight()
        return floor(height / self.getDeltaX() + .5)
    def getMaxCols(self):
        from math import floor
        width    = self.getZone().getWidth()
        return floor(width / self.getDeltaY()  + .5)

registerElement(HexGrid)

# --------------------------------------------------------------------
class SquareGrid(BaseGrid):
    TAG = Element.BOARD+'SquareGrid'
    def __init__(self,zone,node=None,
                 dx           = RECT_WIDTH,
                 dy           = RECT_HEIGHT,
                 edgesLegal   = False,
                 x0           = 0,
                 y0           = int(0.4*RECT_HEIGHT),
                 **kwargs):
        super(SquareGrid,self).__init__(zone,self.TAG,node=node,
                                        dx         = dx,
                                        dy         = dy,
                                        edgesLegal = edgesLegal,
                                        x0         = 0,
                                        y0         = int(0.4*RECT_HEIGHT),
                                        **kwargs)
    def addNumbering(self,**kwargs):
        '''Add a `Numbering` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Numbering
            The added element
        '''
        return self.add(SquareNumbering,**kwargs)
    def getNumbering(self):
        return self.getAllElements(SquareNumbering)
    def getDeltaX(self):
        return float(self['dx'])
    def getDeltaY(self):
        return float(self['dy'])
    def getXOffset(self):
        return int(self['x0'])
    def getYOffset(self):
        return int(self['y0'])
    def getMaxRows(self):
        from math import floor
        height    = self.getZone().getHeight()
        return floor(height / self.getDeltaY() + .5)
    def getMaxCols(self):
        from math import floor
        width    = self.getZone().getWidth()
        return floor(width / self.getDeltaX()  + .5)

registerElement(SquareGrid)

# --------------------------------------------------------------------
class HexNumbering(BaseNumbering):
    TAG = Element.BOARD+'mapgrid.HexGridNumbering'
    def __init__(self,grid,node=None,**kwargs):
        super(HexNumbering,self).__init__(grid,self.TAG,node=node,**kwargs)
    def getGrid(self):
        g = self.getParent(HexGrid)
        return g

    def _getCenter(self,col,row):
        '''Convert col and row index to picture coordinates'''
        from math import floor
        
        stagger  = self['stagger'] == 'true'
        sideways = self.getGrid()['sideways'] == 'true'
        hDesc    = self['hDescend'] == 'true'
        vDesc    = self['vDescend'] == 'true'
        xOff     = self.getGrid().getXOffset()
        yOff     = self.getGrid().getYOffset()
        hexW     = self.getGrid().getDeltaX()
        hexH     = self.getGrid().getDeltaY()
        zxOff    = self.getGrid().getZone().getXOffset()
        zyOff    = self.getGrid().getZone().getYOffset()
        maxRows  = self.getGrid().getMaxRows()
        maxCols  = self.getGrid().getMaxCols()
        # print(f'  Col:         {col}')
        # print(f'  Row:         {row}')
        # print(f'  Stagger:     {stagger}')
        # print(f'  Sideways:    {sideways}')
        # print(f'  hDesc:       {hDesc}')
        # print(f'  vDesc:       {vDesc}')
        # print(f'  maxRows:     {maxRows}')
        # print(f'  maxCols:     {maxCols}')

        if sideways:
            maxRows, maxCols = maxCols, maxRows
            
        if stagger:
            if sideways:
                if col % 2 != 0:
                    row += 1 if hDesc else -1
            else:
                if col % 2 != 0:
                    row += 1 if vDesc else -1

        if hDesc:
            col = maxCols - col
        if vDesc:
            row = maxRows - row

        x = col * hexW + xOff
        y = row * hexH + yOff + (hexH/2 if col % 2 != 0 else 0)

        x = int(floor(x + .5))
        y = int(floor(y + .5))
        if sideways:
            # print(f'Swap coordinates because {sideways}')
            x, y = y, x

        return x,y

registerElement(HexNumbering)

# --------------------------------------------------------------------
class SquareNumbering(BaseNumbering):
    TAG = Element.BOARD+'mapgrid.SquareGridNumbering'
    def __init__(self,grid,node=None,hType='N',**kwargs):
        super(SquareNumbering,self).__init__(grid,self.TAG,node=node,
                                             hType=hType,**kwargs)
    def getGrid(self):
        return self.getParent(SquareGrid)

    def getCenter(self,col,row):
        hDesc    = self['hDescend'] == 'true'
        vDesc    = self['vDescend'] == 'true'
        xOff     = self.getGrid().getXOffset()
        yOff     = self.getGrid().getYOffset()
        squareW  = self.getGrid().getDeltaX()
        squareH  = self.getGrid().getDeltaY()
        maxRows  = self.getGrid().getMaxRows()
        maxCols  = self.getGrid().getMaxCols()

        if vDesc:  row = maxRows - row
        if hDesc:  col = maxCols - col

        x = col * squareW + xOff
        y = row * squareH + yOff

        return x,y
        
registerElement(SquareNumbering)
    
# --------------------------------------------------------------------
class RegionGrid(Element):
    TAG = Element.BOARD+'RegionGrid'
    def __init__(self,zone,node=None,snapto=True,fontsize=9,visible=True):
        super(RegionGrid,self).__init__(zone,self.TAG,node=node,
                                        snapto   = snapto,
                                        fontsize = fontsize,
                                        visible  = visible)

    def getZone(self):
        return self.getParent(Zone)
    def getZoneGrid(self):
        z = self.getZone()
        if z is not None:
            return z.getBoard()
        return None
    def getBoard(self):
        z = self.getZonedGrid()
        if z is not None:
            return z.getBoard()
        return self.getParent(Board)
    def getMap(self):
        b = self.getBoard()
        if b is not None:
            return b.getMap()
        return None
    def getRegions(self):
        return self.getElementsByKey(Region,'name')    
    def checkName(self,name):
        '''Get unique name'''
        poss = len([e for e in self.getRegions()
                    if e == name or e.startswith(name+'_')])
        if poss == 0:
            return name

        return name + f'_{poss}'
    def addRegion(self,**kwargs):
        '''Add a `Region` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Region
            The added element
        '''
        return self.add(Region,**kwargs)
    def getLocation(self,loc):
        for r in self.getRegions().values():
            if loc == r['name']:
                return int(r['originx']),int(r['originy'])

        return None
        
registerElement(RegionGrid)

# --------------------------------------------------------------------
class Region(Element):
    TAG = Element.BOARD+'Region'
    def __init__(self,grid,node=None,
                 name      = '',
                 originx   = 0,
                 originy   = 0,
                 alsoPiece = True,
                 piece     = None,
                 prefix    = ''):
        fullName = name + ("@"+prefix if len(prefix) else "")
        realName = grid.checkName(fullName) if node is None else fullName
        super(Region,self).__init__(grid,
                                    self.TAG,
                                    node    = node,
                                    name    = realName,
                                    originx = originx,
                                    originy = originy)

        if node is None and alsoPiece:
            m = self.getMap()
            b = self.getBoard()
            if m is not None and b is not None:
                if piece is None:
                    g      = m.getGame()
                    pieces = g.getSpecificPieces(name,asdict=False)
                    piece  = pieces[0] if len(pieces) > 0 else None
             
                if piece is not None:
                    # bname = m['mapName']
                    bname = b['name']
                    #print(f'Adding at-start name={name} location={realName} '
                    #      f'owning board={bname}')
                    a = m.addAtStart(name            = name,
                                     location        = realName,
                                     useGridLocation = True,
                                     owningBoard     = bname,
                                     x               = 0,
                                     y               = 0)
                    p = a.addPiece(piece)
                    if p is None:
                        print(f'EEE Failed to add piece {name} ({piece}) to add-start {a}')
                    #if p is not None:
                    #    print(f'Added piece {name} in region')
                #else:
                #    print(f'Could not find piece {name}')
            
    def getGrid(self):
        return self.getParent(RegionGrid)
    def getZone(self):
        g = self.getGrid()
        if g is not None:
            return g.getZone()
        return None
    def getZonedGrid(self):
        z = self.getZone()
        if z is not None:
            return z.getZonedGrid()
        return None
    def getBoard(self):
        z = self.getZonedGrid()
        if z is not None:
            return z.getBoard()
        return self.getParent(Board)
    def getPicker(self):
        z = self.getBoard()
        if z is not None:
            return z.getPicker()
        return None
    def getMap(self):
        b = self.getPicker()
        if b is not None:
            return b.getMap()
        return None

registerElement(Region)

#
# EOF
#
# ====================================================================
# From zone.py

# --------------------------------------------------------------------
class ZonedGrid(Element):
    TAG=Element.BOARD+'ZonedGrid'
    def __init__(self,board,node=None):
        super(ZonedGrid,self).__init__(board,self.TAG,node=node)

    def getBoard(self):
        b = self.getParent(Board)
        # print(f'Get Board of Zoned: {b}')        
        return b
    def getPicker(self):
        z = self.getBoard()
        if z is not None:
            return z.getPicker()
        return None
    def getMap(self):
        z = self.getPicker()
        if z is not None:
            return z.getMap()
        return None
    def addHighlighter(self,**kwargs):
        '''Add a `Highlighter` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Highlighter
            The added element
        '''
        return self.add(ZonedGridHighlighter,**kwargs)
    def getHighlighters(self,single=True):
        '''Get all or a sole `ZonedGridHighlighter` element(s) from this
        
        Parameters
        ----------
        single : bool
            If `True`, there can be only one `Highligter` child, otherwise fail.
            If `False` return all `Highligter` children in this element
        Returns
        -------
        children : list
            List of `Highligter` children (even if `single=True`)
        '''
        return self.getAllElements(ZonedGridHighlighter,single=single)
    def addZone(self,**kwargs):
        '''Add a `Zone` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Zone
            The added element
        '''
        return self.add(Zone,**kwargs)
    def getZones(self,asdict=True):
        '''Get all Zone element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Zone` elements.  If `False`, return a list of all Zone` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `Zone` children
        '''
        return self.getElementsByKey(Zone,'name',asdict=asdict)

registerElement(ZonedGrid)

# --------------------------------------------------------------------
class ZonedGridHighlighter(Element):
    TAG=Element.BOARD+'mapgrid.ZonedGridHighlighter'
    def __init__(self,zoned,node=None):
        super(ZonedGridHighlighter,self).__init__(zoned,self.TAG,node=node)
    def getZonedGrid(self): return self.getParent(ZonedGrid)

    def addZoneHighlight(self,**kwargs):
        '''Add a `ZoneHighlight` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : ZoneHighlight
            The added element
        '''
        return self.add(ZoneHighlight,**kwargs)
    def getZoneHighlights(self,asdict=True):
        '''Get all ZoneHighlight element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Zone` elements.  If `False`, return a list of all Zone` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `Zone` children
        '''
        return self.getElementsByKey(ZoneHighlight,'name',asdict=asdict)

registerElement(ZonedGridHighlighter)

# --------------------------------------------------------------------
class ZoneHighlight(Element):
    TAG=Element.BOARD+'mapgrid.ZoneHighlight'
    FULL='Entire Zone',
    BORDER='Zone Border',
    PLAIN='Plain',
    STRIPED='Striped'
    CROSS='Crosshatched',
    TILES='Tiled Image'
    def __init__(self,
                 highlighters,
                 node     = None,
                 name     = '',
                 color    = rgb(255,0,0),
                 coverage = FULL,
                 width    = 1,
                 style    = PLAIN,
                 image    = '',
                 opacity  = 50):
        super(ZoneHighlight,self).__init__(highlighters,
                                           self.TAG,
                                           node     = node,
                                           name     = name,
                                           color    = color,
                                           coverage = coverage,
                                           width    = width,
                                           style    = style,
                                           image    = image,
                                           opacity  = int(opacity))
    def getZonedGridHighlighter(self):
        return self.getParent(ZonedGridHighlighter)


registerElement(ZoneHighlight)


# --------------------------------------------------------------------
class ZoneProperty(Element):
    TAG = Element.MODULE+'properties.ZoneProperty'
    def __init__(self,zone,node=None,
                 name         = '',
                 initialValue = '',
                 isNumeric    = False,
                 min          = "null",
                 max          = "null",
                 wrap         = False,
                 description  = ""):
        super(ZoneProperty,self).__init__(zone,self.TAG,
                                            node         = node,
                                            name         = name,
                                            initialValue = initialValue,
                                            isNumeric    = isNumeric,
                                            min          = min,
                                            max          = max,
                                            wrap         = wrap,
                                            description  = description)

    def getZone(self):
        return self.getParent(Zone)

registerElement(ZoneProperty)

# --------------------------------------------------------------------
class Zone(Element):
    TAG = Element.BOARD+'mapgrid.Zone'
    def __init__(self,zoned,node=None,
                 name              = "",
                 highlightProperty = "",
                 locationFormat    = "$gridLocation$",
                 path              = "0,0;976,0;976,976;0,976",
                 useHighlight      = False,
                 useParentGrid     = False):
        super(Zone,self).\
            __init__(zoned,self.TAG,node=node,
                     name              = name,
                     highlightProperty = highlightProperty,
                     locationFormat    = locationFormat,
                     path              = path,
                     useHighlight      = useHighlight,
                     useParentGrid     = useParentGrid)

    def getZonedGrid(self):
        z = self.getParent(ZonedGrid)
        # print(f'Get Zoned of Zone {self["name"]}: {z}')        
        return z
    
    def getBoard(self):
        z = self.getZonedGrid()
        if z is not None:
            return z.getBoard()
        return None
    def getPicker(self):
        z = self.getBoard()
        if z is not None:
            return z.getPicker()
        return None
    def getMap(self):
        z = self.getPicker()
        if z is not None:
            return z.getMap()
        return None    
    def addHexGrid(self,**kwargs):
        '''Add a `HexGrid` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : HexGrid
            The added element
        '''
        return self.add(HexGrid,**kwargs)
    def addSquareGrid(self,**kwargs):
        '''Add a `SquareGrid` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : SquareGrid
            The added element
        '''
        return self.add(SquareGrid,**kwargs)
    def addRegionGrid(self,**kwargs):
        '''Add a `RegionGrid` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : RegionGrid
            The added element
        '''
        return self.add(RegionGrid,**kwargs)
    def addProperty(self,**kwargs):
        '''Add a `Property` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Property
            The added element
        '''
        return self.add(ZoneProperty,**kwargs)
    def getHexGrids(self,single=True):
        '''Get all or a sole `HexGrid` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `HexGrid` child, otherwise fail.
            If `False` return all `HexGrid` children in this element
        Returns
        -------
        children : list
            List of `HexGrid` children (even if `single=True`)
        '''
        return self.getAllElements(HexGrid,single=single)
    def getSquareGrids(self,single=True):
        '''Get all or a sole `SquareGrid` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `SquareGrid` child, otherwise fail.
            If `False` return all `SquareGrid` children in this element
        Returns
        -------
        children : list
            List of `SquareGrid` children (even if `single=True`)
        '''
        return self.getAllElements(SquareGrid,single=single)
    def getRegionGrids(self,single=True):
        '''Get all or a sole `RegionGrid` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `RegionGrid` child, otherwise fail.
            If `False` return all `RegionGrid` children in this element
        Returns
        -------
        children : list
            List of `RegionGrid` children (even if `single=True`)
        '''
        return self.getAllElements(RegionGrid,single=single)
    def getGrids(self,single=True):
        '''Get all or a sole `Grid` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `Grid` child, otherwise fail.
            If `False` return all `Grid` children in this element
        Returns
        -------
        children : list
            List of `Grid` children (even if `single=True`)
        '''
        g = self.getHexGrids(single=single)
        if g is not None: return g

        g = self.getSquareGrids(single=single)
        if g is not None: return g

        g = self.getRegionGrids(single=single)
        if g is not None: return g

        return g
    def getProperties(self):
        '''Get all `Property` element from this

        Parameters
        ----------
        Returns
        -------
        children : dict
            dict of `Property` children
        '''
        return getElementsByKey(ZoneProperty,'name')
    
    def getPath(self):
        p  = self['path'].split(';')
        r  = []
        for pp in p:
            c = pp.split(',')
            r.append([int(c[0]),int(c[1])])
        return r
    def getBB(self):
        from functools import reduce
        path = self.getPath()
        llx  = reduce(lambda old,point:min(point[0],old),path,100000000000)
        lly  = reduce(lambda old,point:min(point[1],old),path,100000000000)
        urx  = reduce(lambda old,point:max(point[0],old),path,-1)
        ury  = reduce(lambda old,point:max(point[1],old),path,-1)
        return llx,lly,urx,ury
    def getWidth(self):
        llx,_,urx,_ = self.getBB()
        return urx-llx
    def getHeight(self):
        _,lly,_,ury = self.getBB()
        return ury-lly
    def getXOffset(self):
        return self.getBB()[0]
    def getYOffset(self):
        return self.getBB()[1]

registerElement(Zone)

#
# EOF
#
# ====================================================================
# From board.py

# --------------------------------------------------------------------
class BoardPicker(MapElement):
    TAG = Element.MAP+'BoardPicker'
    def __init__(self,doc,node=None,
                 addColumnText        = 'Add column',
                 addRowText           = 'Add row',
                 boardPrompt          = 'Select board',
                 slotHeight           = 125,
                 slotScale            = 0.2,
                 slotWidth            = 350,
                 title                = 'Choose Boards'):
        super(BoardPicker,self).__init__(doc,self.TAG,node=node,
                                         addColumnText        = addColumnText,
                                         addRowText           = addRowText,
                                         boardPrompt          = boardPrompt,
                                         slotHeight           = slotHeight,
                                         slotScale            = slotScale,
                                         slotWidth            = slotWidth,
                                         title                = title)

    def addSetup(self,**kwargs):
        '''Add a `Setup` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Setup
            The added element
        '''
        if 'mapName' not in kwargs:
            m = self.getMap()
            kwargs['mapName'] = m.getAttribute('mapName')
            
        return self.add(Setup,**kwargs)
    def getSetups(self,single=False):
        '''Get all or a sole `Setup` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `Setup` child, otherwise fail.
            If `False` return all `Setup` children in this element
        Returns
        -------
        children : list
            List of `Setup` children (even if `single=True`)
        '''
        return self.getAllElements(Setup,single=single)
    def addBoard(self,**kwargs):
        '''Add a `Board` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Board
            The added element
        '''
        return self.add(Board,**kwargs)
    def getBoards(self,asdict=True):
        '''Get all Board element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Board` elements.  If `False`, return a list of all Board` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `Board` children
        '''
        return self.getElementsByKey(Board,'name',asdict=asdict)
    def encode(self):
        setups = self.getSetups()
        if setups is not None and len(setups)>0:
            return [setups[0]._node.childNodes[0].nodeValue]
        
        ret    = []
        for bn in self.getBoards().keys():
            ret.append(bn+'BoardPicker\t'+bn+'\t0\t0')

        return ret

registerElement(BoardPicker)

# --------------------------------------------------------------------
class Setup(Element):
    TAG = 'setup'
    def __init__(self,picker,node=None,
                 mapName = '',
                 maxColumns = 1,
                 boardNames = []):
        super(Setup,self).__init__(picker,self.TAG,node=node)
        col = 0
        row = 0
        lst = [f'{mapName}BoardPicker']
        for bn in boardNames:
            lst.extend([bn,str(col),str(row)])
            col += 1
            if col >= maxColumns:
                col = 0
                row += 1
                
        txt = r'	'.join(lst)
        self.addText(txt)

    def getPicker(self): return self.getParent(BoardPicker)

registerElement(Setup)
    
# --------------------------------------------------------------------
class Board(Element):
    TAG = Element.PICKER+'Board'
    def __init__(self,picker,node=None,
                 name       = '',
                 image      = '',
                 reversible = False,
                 color      = rgb(255,255,255),
                 width      = 0,
                 height     = 0):
        super(Board,self).__init__(picker,self.TAG,node=node,
                                   image      = image,
                                   name       = name,
                                   reversible = reversible,
                                   color      = color,
                                   width      = width,
                                   height     = height)

    def getPicker(self): return self.getParent(BoardPicker)
    def getMap(self):
        z = self.getPicker()
        if z is not None:
            return z.getMap()
        return None
    def addZonedGrid(self,**kwargs):
        '''Add a `ZonedGrid` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : ZonedGrid
            The added element
        '''
        return self.add(ZonedGrid,**kwargs)
    def getZonedGrids(self,single=True):
        '''Get all or a sole `ZonedGrid` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `ZonedGrid` child, otherwise fail.
            If `False` return all `ZonedGrid` children in this element
        Returns
        -------
        children : list
            List of `ZonedGrid` children (even if `single=True`)
        '''
        return self.getAllElements(ZonedGrid,single=single)
    def getZones(self,asdict=True):
        '''Get all Zone element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Zone` elements.  If `False`, return a list of all Zone` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `Zone` children
        '''
        zoned = self.getZonedGrids(single=True)
        if zoned is None: return None

        return zoned[0].getZones(asdict=asdict)

    def getWidth(self):
        # print(f'Getting width of {self}: {self["width"]}')
        if 'width' in self and int(self['width']) != 0:
            return int(self['width'])
        return 0

    def getHeight(self):
        # print(f'Getting height of {self}: {self["height"]}')
        if 'height' in self and int(self['height']) != 0:
            return int(self['height'])
        return 0

registerElement(Board)

#
# EOF
#
# ====================================================================
# From map.py

# --------------------------------------------------------------------
class BaseMap(Element):
    def __init__(self,doc,tag,node=None,
                 mapName              = '',
                 allowMultiple        = 'false',
                 backgroundcolor      = rgb(255,255,255),
                 buttonName           = '',
                 changeFormat         = '$message$',
                 color                = rgb(0,0,0), # Selected pieces
                 createFormat         = '$pieceName$ created in $location$ *',
                 edgeHeight           = '0',
                 edgeWidth            = '0',
                 hideKey              = '',
                 hotkey               = key('M',ALT),
                 icon                 = '/images/map.gif',
                 launch               = 'false',
                 markMoved            = 'Always',
                 markUnmovedHotkey    = '',
                 markUnmovedIcon      = '/images/unmoved.gif',
                 markUnmovedReport    = '',
                 markUnmovedText      = '',
                 markUnmovedTooltip   = 'Mark all pieces on this map as not moved',
                 moveKey              = '',
                 moveToFormat         = '$pieceName$ moves $previousLocation$ &rarr; $location$ *',
                 moveWithinFormat     = '$pieceName$ moves $previousLocation$ &rarr; $location$ *',
                 showKey              = '',
                 thickness            = '3'):
        super(BaseMap,self).__init__(doc,tag,node=node,
                                     allowMultiple        = allowMultiple,
                                     backgroundcolor      = backgroundcolor,
                                     buttonName           = buttonName,
                                     changeFormat         = changeFormat,
                                     color                = color,
                                     createFormat         = createFormat,
                                     edgeHeight           = edgeHeight,
                                     edgeWidth            = edgeWidth,
                                     hideKey              = hideKey,
                                     hotkey               = hotkey,
                                     icon                 = icon,
                                     launch               = launch,
                                     mapName              = mapName,
                                     markMoved            = markMoved,
                                     markUnmovedHotkey    = markUnmovedHotkey,
                                     markUnmovedIcon      = markUnmovedIcon,
                                     markUnmovedReport    = markUnmovedReport,
                                     markUnmovedText      = markUnmovedText,
                                     markUnmovedTooltip   = markUnmovedTooltip,
                                     moveKey              = moveKey,
                                     moveToFormat         = moveToFormat,
                                     moveWithinFormat     = moveWithinFormat,
                                     showKey              = showKey,
                                     thickness            = thickness)

    def getGame(self):
        '''Get the game'''
        return self.getParentOfClass([Game])
    def addPicker(self,**kwargs):
        '''Add a `Picker` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Picker
            The added element
        '''
        return self.add(BoardPicker,**kwargs)
    def getBoardPicker(self,single=True):
        '''Get all or a sole `BoardPicker` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `BoardPicker` child, otherwise fail.
            If `False` return all `BoardPicker` children in this element
        Returns
        -------
        children : list
            List of `BoardPicker` children (even if `single=True`)
        '''
        return self.getAllElements(BoardPicker,single)
    def getPicker(self,single=True):
        '''Get all or a sole `BoardPicker` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `BoardPicker` child, otherwise fail.
            If `False` return all `BoardPicker` children in this element
        Returns
        -------
        children : list
            List of `BoardPicker` children (even if `single=True`)
        '''
        return self.getAllElements(BoardPicker,single)
    def getStackMetrics(self,single=True):
        '''Get all or a sole `StackMetric` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `StackMetric` child, otherwise fail.
            If `False` return all `StackMetric` children in this element
        Returns
        -------
        children : list
            List of `StackMetric` children (even if `single=True`)
        '''
        return self.getAllElements(StackMetrics,single)
    def getImageSaver(self,single=True):
        '''Get all or a sole `ImageSaver` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `ImageSaver` child, otherwise fail.
            If `False` return all `ImageSaver` children in this element
        Returns
        -------
        children : list
            List of `ImageSaver` children (even if `single=True`)
        '''
        return self.getAllElements(ImageSaver,single)
    def getTextSaver(self,single=True):
        '''Get all or a sole `TextSaver` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `TextSaver` child, otherwise fail.
            If `False` return all `TextSaver` children in this element
        Returns
        -------
        children : list
            List of `TextSaver` children (even if `single=True`)
        '''
        return self.getAllElements(TextSaver,single)
    def getForwardToChatter(self,single=True):
        '''Get all or a sole `ForwardToChatter` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `ForwardToChatter` child, otherwise fail.
            If `False` return all `ForwardToChatter` children in this element
        Returns
        -------
        children : list
            List of `ForwardToChatter` children (even if `single=True`)
        '''
        return self.getAllElements(ForwardToChatter,single)
    def getMenuDisplayer(self,single=True):
        '''Get all or a sole `MenuDi` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `MenuDi` child, otherwise fail.
            If `False` return all `MenuDi` children in this element
        Returns
        -------
        children : list
            List of `MenuDi` children (even if `single=True`)
        '''
        return self.getAllElements(MenuDisplayer,single)
    def getMapCenterer(self,single=True):
        '''Get all or a sole `MapCenterer` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `MapCenterer` child, otherwise fail.
            If `False` return all `MapCenterer` children in this element
        Returns
        -------
        children : list
            List of `MapCenterer` children (even if `single=True`)
        '''
        return self.getAllElements(MapCenterer,single)
    def getStackExpander(self,single=True):
        '''Get all or a sole `StackExpander` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `StackExpander` child, otherwise fail.
            If `False` return all `StackExpander` children in this element
        Returns
        -------
        children : list
            List of `StackExpander` children (even if `single=True`)
        '''
        return self.getAllElements(StackExpander,single)
    def getPieceMover(self,single=True):
        '''Get all or a sole `PieceMover` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `PieceMover` child, otherwise fail.
            If `False` return all `PieceMover` children in this element
        Returns
        -------
        children : list
            List of `PieceMover` children (even if `single=True`)
        '''
        return self.getAllElements(PieceMover,single)
    def getSelectionHighlighters(self,single=True):
        '''Get all or a sole `SelectionHighlighter` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `SelectionHighlighter` child, otherwise fail.
            If `False` return all `SelectionHighlighter` children in this element
        Returns
        -------
        children : list
            List of `SelectionHighlighter` children (even if `single=True`)
        '''
        return self.getAllElements(SelectionHighlighters,single)
    def getKeyBufferer(self,single=True):
        return self.getAllElements(KeyBufferer,single)
    def getHighlightLastMoved(self,single=True):
        '''Get all or a sole `HighlightLa` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `HighlightLa` child, otherwise fail.
            If `False` return all `HighlightLa` children in this element
        Returns
        -------
        children : list
            List of `HighlightLa` children (even if `single=True`)
        '''
        return self.getAllElements(HighlightLastMoved,single)
    def getCounterDetailViewer(self,single=True):
        '''Get all or a sole `CounterDetailViewer` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `CounterDetailViewer` child, otherwise fail.
            If `False` return all `CounterDetailViewer` children in this element
        Returns
        -------
        children : list
            List of `CounterDetailViewer` children (even if `single=True`)
        '''
        return self.getAllElements(CounterDetailViewer,single)
    def getGlobalMap(self,single=True):
        '''Get all or a sole `GlobalMap` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `GlobalMap` child, otherwise fail.
            If `False` return all `GlobalMap` children in this element
        Returns
        -------
        children : list
            List of `GlobalMap` children (even if `single=True`)
        '''
        return self.getAllElements(GlobalMap,single)
    def getZoomer(self,single=True):
        '''Get all or a sole `Zoomer` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `Zoomer` child, otherwise fail.
            If `False` return all `Zoomer` children in this element
        Returns
        -------
        children : list
            List of `Zoomer` children (even if `single=True`)
        '''
        return self.getAllElements(Zoomer,single)
    def getHidePiecesButton(self,single=True):
        '''Get all or a sole `HidePiece` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `HidePiece` child, otherwise fail.
            If `False` return all `HidePiece` children in this element
        Returns
        -------
        children : list
            List of `HidePiece` children (even if `single=True`)
        '''
        return self.getAllElements(HidePiecesButton,single)
    def getMassKeys(self,asdict=True):
        '''Get all MassKey element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `MassKey` elements.  If `False`, return a list of all MassKey` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `MassKey` children
        '''
        return self.getElementsByKey(MassKey,'name',asdict)
    def getFlare(self,single=True):
        '''Get all or a sole `Flare` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `Flare` child, otherwise fail.
            If `False` return all `Flare` children in this element
        Returns
        -------
        children : list
            List of `Flare` children (even if `single=True`)
        '''
        return self.getAllElements(Flare,single)
    def getAtStarts(self,single=True):
        '''Get all or a sole `AtStart` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `AtStart` child, otherwise fail.
            If `False` return all `AtStart` children in this element
        Returns
        -------
        children : list
            List of `AtStart` children (even if `single=True`)
        '''
        return self.getAllElements(AtStart,single)
    def getBoards(self,asdict=True):
        '''Get all Board element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Board` elements.  If `False`, return a list of all Board` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `Board` children
        '''
        picker = self.getPicker()
        if picker is None:  return None
        return picker[0].getBoards(asdict=asdict)
    def getLayers(self,asdict=True):
        '''Get all `PieceLayer` element(s) from this

        Parameters
        ----------
        asdict : bool        
            If `True`, return a dictonary that maps property name
            `PieceLayers` elements.  If `False`, return a list of all
            `PieceLayers` children.
        
        Returns
        -------
        children : dict or list
            Dictionary or list of `PieceLayers` children

        '''
        return self.getElementsByKey(PieceLayers,'property',asdict)
    def getMenus(self,asdict=True):
        '''Get all Menu element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Board`
            elements.  If `False`, return a list of all Board`
            children.

        Returns
        -------
        children : dict or list
            Dictionary or list of `Board` children

        '''
        return self.getElementsByKey(Menu,'name',asdict)
    def getLOSs(self,asdict=True):
        '''Get all Menu element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Board`
            elements.  If `False`, return a list of all Board`
            children.

        Returns
        -------
        children : dict or list
            Dictionary or list of `Board` children

        '''
        return self.getElementsByKey(LineOfSight,'threadName',asdict)
    def addBoardPicker(self,**kwargs):
        '''Add a `BoardPicker` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : BoardPicker
            The added element
        '''
        return self.add(BoardPicker,**kwargs)
    def addStackMetrics(self,**kwargs):
        '''Add a `StackMetrics` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : StackMetrics
            The added element
        '''
        return self.add(StackMetrics,**kwargs)
    def addImageSaver(self,**kwargs):
        '''Add a `ImageSaver` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : ImageSaver
            The added element
        '''
        return self.add(ImageSaver,**kwargs)
    def addTextSaver(self,**kwargs):
        '''Add a `TextSaver` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : TextSaver
            The added element
        '''
        return self.add(TextSaver,**kwargs)
    def addForwardToChatter(self,**kwargs):
        '''Add a `ForwardToChatter` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : ForwardToChatter
            The added element
        '''
        return self.add(ForwardToChatter,**kwargs)
    def addMenuDisplayer(self,**kwargs):
        '''Add a `MenuDisplayer` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : MenuDisplayer
            The added element
        '''
        return self.add(MenuDisplayer,**kwargs)
    def addMapCenterer(self,**kwargs):
        '''Add a `MapCenterer` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : MapCenterer
            The added element
        '''
        return self.add(MapCenterer,**kwargs)
    def addStackExpander(self,**kwargs):
        '''Add a `StackExpander` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : StackExpander
            The added element
        '''
        return self.add(StackExpander,**kwargs)
    def addPieceMover(self,**kwargs):
        '''Add a `PieceMover` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : PieceMover
            The added element
        '''
        return self.add(PieceMover,**kwargs)
    def addSelectionHighlighters(self,**kwargs):
        '''Add a `SelectionHighlighters` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : SelectionHighlighters
            The added element
        '''
        return self.add(SelectionHighlighters,**kwargs)
    def addKeyBufferer(self,**kwargs):
        '''Add a `KeyBufferer` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : KeyBufferer
            The added element
        '''
        return self.add(KeyBufferer,**kwargs)
    def addHighlightLastMoved(self,**kwargs):
        '''Add a `HighlightLastMoved` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : HighlightLastMoved
            The added element
        '''
        return self.add(HighlightLastMoved,**kwargs)
    def addCounterDetailViewer(self,**kwargs):
        '''Add a `CounterDetailViewer` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : CounterDetailViewer
            The added element
        '''
        return self.add(CounterDetailViewer,**kwargs)
    def addGlobalMap(self,**kwargs):
        '''Add a `GlobalMap` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : GlobalMap
            The added element
        '''
        return self.add(GlobalMap,**kwargs)
    def addZoomer(self,**kwargs):
        '''Add a `Zoomer` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Zoomer
            The added element
        '''
        return self.add(Zoomer,**kwargs)
    def addHidePiecesButton(self,**kwargs):
        '''Add a `HidePiecesButton` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : HidePiecesButton
            The added element
        '''
        return self.add(HidePiecesButton,**kwargs)
    def addMassKey(self,**kwargs):
        '''Add a `MassKey` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : MassKey
            The added element
        '''
        return self.add(MassKey,**kwargs)
    def addStartupMassKey(self,**kwargs):
        '''Add a `StartupMassKey` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : StartupMassKey
            The added element
        '''
        return self.add(MassKey,**kwargs)
    def addFlare(self,**kwargs):
        '''Add a `Flare` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Flare
            The added element
        '''
        return self.add(Flare,**kwargs)
    def addAtStart(self,**kwargs):
        '''Add a `AtStart` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : AtStart
            The added element
        '''
        return self.add(AtStart,**kwargs)


    def addLayers(self,**kwargs):
        '''Add `PieceLayers` element to this
        
        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : PieceLayers
            The added element
        '''
        return self.add(PieceLayers,**kwargs)
    def addMenu(self,**kwargs):
        '''Add a `Menu` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Menu
            The added element
        '''
        return self.add(Menu,**kwargs)
    def addLOS(self,**kwargs):
        '''Add a `Menu` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Menu
            The added element
        '''
        return self.add(LineOfSight,**kwargs)
     
# --------------------------------------------------------------------
class Map(BaseMap):
    TAG = Element.MODULE+'Map'
    def __init__(self,doc,node=None,
                 mapName              = '',
                 allowMultiple        = 'false',
                 backgroundcolor      = rgb(255,255,255),
                 buttonName           = '',
                 changeFormat         = '$message$',
                 color                = rgb(0,0,0),
                 createFormat         = '$pieceName$ created in $location$ *',
                 edgeHeight           = '0',
                 edgeWidth            = '0',
                 hideKey              = '',
                 hotkey               = key('M',ALT),
                 icon                 = '/images/map.gif',
                 launch               = 'false',
                 markMoved            = 'Always',
                 markUnmovedHotkey    = '',
                 markUnmovedIcon      = '/images/unmoved.gif',
                 markUnmovedReport    = '',
                 markUnmovedText      = '',
                 markUnmovedTooltip   = 'Mark all pieces on this map as not moved',
                 moveKey              = '',
                 moveToFormat         = '$pieceName$ moves $previousLocation$ &rarr; $location$ *',
                 moveWithinFormat     = '$pieceName$ moves $previousLocation$ &rarr; $location$ *',
                 showKey              = '',
                 thickness            = '3'):
        super(Map,self).__init__(doc,self.TAG,node=node,
                                 allowMultiple        = allowMultiple,
                                 backgroundcolor      = backgroundcolor,
                                 buttonName           = buttonName,
                                 changeFormat         = changeFormat,
                                 color                = color,
                                 createFormat         = createFormat,
                                 edgeHeight           = edgeHeight,
                                 edgeWidth            = edgeWidth,
                                 hideKey              = hideKey,
                                 hotkey               = hotkey,
                                 icon                 = icon,
                                 launch               = launch,
                                 mapName              = mapName,
                                 markMoved            = markMoved,
                                 markUnmovedHotkey    = markUnmovedHotkey,
                                 markUnmovedIcon      = markUnmovedIcon,
                                 markUnmovedReport    = markUnmovedReport,
                                 markUnmovedText      = markUnmovedText,
                                 markUnmovedTooltip   = markUnmovedTooltip,
                                 moveKey              = moveKey,
                                 moveToFormat         = moveToFormat,
                                 moveWithinFormat     = moveWithinFormat,
                                 showKey              = showKey,
                                 thickness            = thickness)

    def getGame(self):
        return self.getParent(Game)

registerElement(Map)

# --------------------------------------------------------------------
class WidgetMap(BaseMap):
    TAG = Element.WIDGET+'WidgetMap'
    def __init__(self,doc,node=None,**attr):
        super(WidgetMap,self).__init__(doc,self.TAG,node=node,**attr)

    def getGame(self):
        return self.getParentOfClass([Game])
    def getMapWidget(self):
        return self.getParent(MapWidget)

registerElement(WidgetMap)


#
# EOF
#
# ====================================================================
# From chart.py

# --------------------------------------------------------------------
class ChartWindow(GameElement,WidgetElement):
    TAG=Element.MODULE+'ChartWindow'
    def __init__(self,elem,node=None,
                 name        = '',
                 hotkey      = key('A',ALT),
                 description = '',
                 text        = '',
                 tooltip     = 'Show/hide Charts',
                 icon        = '/images/chart.gif'):
        super(ChartWindow,self).__init__(elem,self.TAG,node=node,
                                         name        = name,
                                         hotkey      = hotkey,
                                         description = description,
                                         text        = text,
                                         tooltip     = tooltip,
                                         icon        = icon)

    def addChart(self,**kwargs):
        '''Add a `Chart` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Chart
            The added element
        '''
        return self.add(Chart,**kwargs)
    def getCharts(self,asdict=True):
        '''Get all Chart element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Chart` elements.  If `False`, return a list of all Chart` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `Chart` children
        '''
        return self.getElementsById(Chart,'chartName',asdict=asdict)
    
registerElement(ChartWindow)    

# --------------------------------------------------------------------
class Chart(Element):
    TAG=Element.WIDGET+'Chart'
    def __init__(self,elem,node=None,
                 chartName   = '',
                 fileName    = '',
                 description = ''):
        super(Chart,self).__init__(elem,self.TAG,node=node,
                                   chartName   = chartName,
                                   description = description,
                                   fileName    = fileName)

registerElement(Chart)

#
# EOF
#
# ====================================================================
# From command.py

# --------------------------------------------------------------------
class Command:
    def __init__(self,what,iden,tpe,state):
        self.cmd = '/'.join([what,iden,tpe,state])
    
# --------------------------------------------------------------------
class AddCommand(Command):
    ID = '+'
    def __init__(self,iden,tpe,state):
        super(AddCommand,self).__init__(self.ID,iden,tpe,state)
        

#
# EOF
#
# ====================================================================
# From trait.py
# ====================================================================
class Trait:
    known_traits = []
    def __init__(self):
        '''Base class for trait capture.  Unlike the Element classes,
        this actually holds state that isn't reflected elsewhere in
        the DOM.  This means that the data here is local to the
        object.   So when we do
            
            piece  = foo.getPieceSlots()[0]
            traits = p.getTraits()
            for trait in traits:
                if trait.ID == 'piece': 
                    trait["gpid"] = newPid
                    trait["lpid"] = newPid
        
        we do not actually change anything in the DOM.  To do that, we
        must add back _all_ the traits as
        
            piece.setTraits(traits)
        
        We can add traits to a piece, like
        
            piece.addTrait(MarkTrait('Hello','World'))
        
        But it is not particularly efficient.  Better to do
        (continuing from above)
        
            traits.append(MarkTrait('Hello','World;)
            piece.setTraits(traits)
        
        '''
        self._type  = None
        self._state = None

    def setType(self,**kwargs):
        '''Set types.  Dictionary of names and values.  Dictonary keys
        defines how we access the fields, which is internal here.
        What matters is the order of the values.

        '''
        self._type   = list(kwargs.values())
        self._tnames = list(kwargs.keys())
    def setState(self,**kwargs):
        '''Set states.  Dictionary of names and values.  Dictonary keys
        defines how we access the fields, which is internal here.
        What matters is the order of the values.
        '''
        self._state  = list(kwargs.values())
        self._snames = list(kwargs.keys())

    
    def __getitem__(self,key):
        '''Look up item in either type or state'''
        try:
            return self._type[self._tnames.index(key)]
        except:
            pass
        return self._state[self._snames.index(key)]

    def __setitem__(self,key,value):
        '''Set item in either type or state'''
        try:
            self._type[self._tnames.index(key)] = value
            return
        except:
            pass
        self._state[self._snames.index(key)] = value

    def encode(self,term=False):
        '''
        returns type and state encoded'''
        t = self.encodeFields(self.ID,*self._type,term=term)
        s = self.encodeFields(*self._state,term=term)
        return t,s

    @classmethod
    def findTrait(cls,traits,ID,key=None,value=None,verbose=False):
        for trait in traits:
            if trait.ID != ID:
                continue
            if verbose:
                print(f' {trait.ID}')
            if key is None or value is None:
                if verbose:
                    print(f' Return {trait.ID}')
                return trait
            if verbose:
                print(f' Check {key}={value}: {trait[key]}')
            if trait[key] == value:
                return trait
        if verbose:
            print(f' Trait of type {ID} with {key}={value} not found')
        return None
        
    @classmethod
    def take(cls,iden,t,s):
        '''If the first part of the string t matches the ID, then take it.

        t and s are lists of strings.
        ''' 
        if iden != cls.ID: return None

        ret = cls()
        ret._type = t
        ret._state = s
        ret.check() # Check if we're reasonable, or raise
        #print(f'Took {iden} {cls}\n'
        #      f'  {ret._tnames}\n'
        #      f'  {ret._snames}')
        return ret

    def check(self):
        '''Implement if trait should check that all is OK when cloning'''
        pass

    @classmethod
    def encodeFields(cls,*args,term=False):
        return ';'.join([str(e).lower() if isinstance(e,bool) else str(e)
                         for e in args])+(';' if term else '')

    @classmethod
    def decodeFields(cls,s):
        from re import split
        return split(r'(?<!\\);',s)
        # return s.split(';') # Probably too simple-minded 

    @classmethod
    def encodeKeys(cls,keys,sep=','):
        return sep.join([k.replace(',','\\'+f'{sep}') for k in keys])
        
    @classmethod
    def decodeKeys(cls,keys,sep=','):
        from re import split
        ks = split(r'(?<!\\)'+f'{sep}',keys)
        return [k.replace('\\'+f'{sep}',f'{sep}') for k in ks]

    @classmethod
    def flatten(cls,traits,game=None,prototypes=None):
        if prototypes is None:
            if game is None:
                print(f'Warning: Game or prototypes not passed')
                return None
            prototypes = game.getPrototypes()[0].getPrototypes()

        ret   = []        
        return cls._flatten(traits,ret,prototypes,False)
    
    @classmethod
    def _flatten(cls,traits,ret,prototypes,nobasic=True):
        '''Expand all prototype traits in traits'''
        for trait in traits:
            # Ignore recursive basic traits
            if nobasic and trait.ID == BasicTrait.ID:
                continue
            # Add normal traits
            if trait.ID != PrototypeTrait.ID:
                ret.append(trait)
                continue

            # Find prototype
            proto = prototypes.get(trait['name'],None)
            if proto is None:
                print(f'Warning, prototype {trait["name"]} not found')
                continue

            # Recursive call to add prototype traits (and possibly
            # more recursive calls 
            cls._flatten(proto.getTraits(), ret, prototypes,not nobasic)

        return ret
#
# EOF
#
# ====================================================================
# From withtraits.py

# --------------------------------------------------------------------
#
# Traits of this kind of object are
#
# - Evaluated from the start of the list to the end of the list,
#   skipping over report and trigger traits
# - Then evaluated from the end of the list to the start, only
#   evaluating report and trigger traits
# - The list _must_ end in a BasicTrait
#
# Traits are copied when making a copy of objects of this class, and
# are done so using a full decoding and encoding.  This means that
# copying is a bit susceptible to expansions of the strings of the traits,
# in particular if they contain special characters such as ',' or '/'.
#
class WithTraits(Element):
    def __init__(self,parent,tag,node=None,traits=[],**kwargs):
        '''Base class for things that have traits

        Parameters
        ----------
        parent : Element
            Parent to add this to
        node : xml.minidom.Element
            If not None, XML element to read definition from.
            Rest of the arguments are ignored if not None.
        traits : list of Trait objects
            The traits to set on this object
        kwargs : dict
            More attributes to set on element
        '''
        super(WithTraits,self).__init__(parent,tag,node=node,**kwargs)
        if node is None: self.setTraits(*traits)
        
    def addTrait(self,trait):
        '''Add a `Trait` element to this.  Note that this re-encodes
        all traits.

        Parameters
        ----------
        trait : Trait
            The trait to add 
        '''
        traits = self.getTraits()
        traits.append(trait)
        self.setTraits(*traits)

    def getTraits(self):
        '''Get all element traits as objects.  This decodes the trait
        definitions.  This is useful if we have read the element from
        the XML file, or similar.

        Note that the list of traits returned are _not_ tied to the
        XML nodes content.  Therefore, if one makes changes to the list,
        or to elements of the list, and these changes should be
        reflected in this object, then we _must_ call

            setTraits(traits)

        with the changed list of traits. 

        Returns
        -------
        traits : list of Trait objects
            The decoded traits

        '''
        from re import split
        code                = self._node.childNodes[0].nodeValue
        cmd, iden, typ, sta = split(fr'(?<!\\)/',code) #code.split('/')
        types               = typ.split(r'	')
        states              = sta.split(r'	')
        types               = [t.strip('\\').split(';') for t in types]
        states              = [s.strip('\\').split(';') for s in states]
        traits              = []
        
        for t, s in zip(types,states):
            tid   = t[0]
            trem  = t[1:]
            known = False
            
            for c in Trait.known_traits:
                t = c.take(tid,trem,s) # See if we have it
                if t is not None:
                    traits.append(t)  # Got it
                    known = True
                    break
                
            if not known:
                print(f'Warning: Unknown trait {tid}')

        return traits
    
    def setTraits(self,*traits,iden='null'):
        '''Set traits on this element.  This encodes the traits into
        this object.
        
        Parameters
        ----------
        traits : tuple of Trait objects
            The traits to set on this object.
        iden : str
            Identifier

        '''
        last = traits[-1]
        # A little hackish to use the name of the class, but needed
        # because of imports into patch scripts.
        if not isinstance(last,BasicTrait) and \
           not last.__class__.__name__.endswith('BasicTrait'):
            from sys import stderr
            print(f'Warning - last trait NOT a BasicTrait, but a {type(last)}',
                  file=stderr)
            
        types = []
        states = []
        for trait in traits:
            if trait is None:
                print(f'Trait is None (traits: {traits})')
                continue
            tpe, state = trait.encode()
            types.append(tpe)
            states.append(state)

        tpe   = WithTraits.encodeParts(*types)
        state = WithTraits.encodeParts(*states)
        add   = AddCommand(str(iden),tpe,state)
        if len(self._node.childNodes) < 1:
            self.addText('')
        self._node.childNodes[0].nodeValue = add.cmd

    def removeTrait(self,ID,key=None,value=None,verbose=False):
        '''Remove a trait from this object.

        Parameters
        ----------
        ID : str
            The type of trait to remove.  Must be a valid
            ID of a class derived from Trait.
        key : str
            Optional key to inspect to select trait that has 
            this key and the traits key value is the argument value,
        value :
            If specified, then only traits which key has this value
            are removed
        verbose : bool
            Be verbose if True

        Returns
        -------
        trait : Trait
            The removed trait or None
        '''
        traits = self.getTraits()
        trait  = Trait.findTrait(traits,ID,key,value,verbose)
        if trait is not None:
            traits.remove(trait)
            self.setTraits(traits)
        return trait

    def addTraits(self,*toadd):
        '''Add traits to this.  Note that this will
        decode and reencode the traits.  Only use this when
        adding traits on-mass.  Repeated use of this is inefficient.

        This member function takes care to push any basic trait to
        the end of the list.

        The added traits will not override existing triats. 

        Paramters
        ---------
        toAdd : tuple of Trait objects
            The traits to add 

        '''
        traits = self.getTraits()
        basic  = Trait.findTrait(traits,BasicTrait.ID)
        if basic:
            traits.remove(basic)
        traits.extend(toAdd)
        if basic:
            traits.append(basic)
        self.setTraits(traits)
        
        
    @classmethod
    def encodeParts(cls,*parts):
        '''Encode parts of a full piece definition

        Each trait (VASSAL.counter.Decorator,
        VASSAL.counter.BasicPiece) definition or state is separated by
        a litteral TAB character.  Beyond the first TAB separator,
        additional escape characters (BACKSLAH) are added in front of
        the separator.  This is to that VASSAL.utils.SequenceDecoder
        does not see consequitive TABs as a single TAB.
        '''
        ret = ''
        sep = r'	'
        for i, p in enumerate(parts):
            if i != 0:
                ret += '\\'*(i-1) + sep
            ret += str(p)

        return ret
        
        
    def cloneNode(self,parent):
        '''This clones the underlying XML node.

        Parameters
        ----------
        parent : Element
            The element to clone this element into

        Returns
        -------
        copy : xml.minidom.Element
            The newly created clone of this object's node
        '''
        copy = self._node.cloneNode(deep=True)
        if parent is not None:
            parent._node.appendChild(copy)
        else:
            print('WARNING: No parent to add copy to')
        return copy
    
# --------------------------------------------------------------------
class DummyWithTraits(WithTraits):
    TAG = 'dummy'
    def __init__(self,parent,node=None,traits=[]):
        '''An empty element.  Used when making searching'''
        super(DummyWithTraits,self).__init__(tag       = self.TAG,
                                             parent    = parent,
                                             node      = node,
                                             traits    = traits)
        if parent is not None:
            parent.remove(self)


registerElement(DummyWithTraits)

# --------------------------------------------------------------------
class PieceSlot(WithTraits):
    TAG = Element.WIDGET+'PieceSlot'
    def __init__(self,parent,node=None,
                 entryName      = '',
                 traits         = [],
                 gpid           = 0,
                 height         = 72,
                 width          = 72,
                 icon           = ''):
        '''A piece slot.  Used all the time.

        Parameters
        ----------
        parent : Element
            Parent to add this to
        node : xml.minidom.Element
            If not None, XML element to read definition from.
            Rest of the arguments are ignored if not None.
        entryName : str
            Name of this
        traits : list of Trait objects
            The traits to set on this object
        gpid : int
            Global Piece identifier. If 0, will be set by Game
        height : int
            Height size of the piece (in pixels)
        width : int
            Width size of the piece (in pixels)
        icon : str
            Piece image file name within 'image' sub-dir of archive
        '''
        super(PieceSlot,self).\
            __init__(parent,self.TAG,node=node,
                     traits    = traits,
                     entryName = entryName,
                     gpid      = gpid,
                     height    = height,
                     width     = width,
                     icon      = icon)

    def clone(self,parent):
        '''Adds copy of self to parent, possibly with new GPID'''
        game  = self.getParentOfClass([Game])
        gpid  = game.nextPieceSlotId()
        #opid  = int(self.getAttribute('gpid'))
        #print(f'Using GPID={gpid} for clone {opid}')
        
        node  = self.cloneNode(parent)
        piece = PieceSlot(parent,node=node)
        piece.setAttribute('gpid',gpid)
        
        traits = piece.getTraits()
        for trait in traits:
            if isinstance(trait,BasicTrait):
                trait['gpid'] = gpid

        piece.setTraits(*traits)
        return piece
        
registerElement(PieceSlot)

# --------------------------------------------------------------------
class Prototype(WithTraits):
    TAG = Element.MODULE+'PrototypeDefinition'
    def __init__(self,cont,node=None,
                 name          = '',
                 traits        = [],
                 description   = ''):
        '''A prototype.  Used all the time.

        Parameters
        ----------
        cont : Element
            Parent to add this to
        node : xml.minidom.Element
            If not None, XML element to read definition from.
            Rest of the arguments are ignored if not None.
        name : str
            Name of this
        traits : list of Trait objects
            The traits to set on this object
        description : str
            A free-form description of this prototype
        '''
        super(Prototype,self).__init__(cont,self.TAG,node=node,
                                       traits      = traits,
                                       name        = name,
                                       description = description)
    
registerElement(Prototype)

#
# EOF
#
# ====================================================================
# From traits/dynamicproperty.py

# --------------------------------------------------------------------
# 
# 
class ChangePropertyTrait(Trait):
    DIRECT = 'P'
    INCREMENT = 'I'
    def __init__(self,
                 *commands,
                 numeric     = False,
                 min         = 0,
                 max         = 100,
                 wrap        = False):
        '''Base class for property (piece or global) change traits.
        
           Encodes constraints and commands.
        '''
        # assert name is not None and len(name) > 0, \
        #     'No name specified for ChangePropertyTriat'
        super(ChangePropertyTrait,self).__init__()
        self._constraints = self.encodeConstraints(numeric,wrap,min,max)
        self._commands    = self.encodeCommands(commands)

    def encodeConstraints(self,numeric,wrap,min,max):
        isnum             = f'{numeric}'.lower()
        iswrap            = f'{wrap}'.lower()
        return f'{isnum},{min},{max},{iswrap}'

    def decodeConstraints(self,constraints):
        f = Trait.decodeKeys(constraints)
        return f[0]=='true',f[3]=='true',int(f[1]),int(f[2])
    
    def encodeCommands(self,commands):
        cmds              = []
        for cmd in commands:
            # print(cmd)
            com = cmd[0] + ':' + cmd[1].replace(',',r'\,') + ':' + cmd[2]
            if cmd[2] == self.DIRECT:
                com += f'\,'+cmd[3].replace(',',r'\\,').replace(':',r'\:')
            elif cmd[2] == self.INCREMENT:
                com += f'\,'+cmd[3].replace(',',r'\\,').replace(':',r'\:')
            cmds.append(com)
        # print(cmds)
        return ','.join(cmds)

    def decodeCommands(self,commands):
        cmds = Trait.decodeKeys(commands)
        ret  = []
        for cmd in cmds:
            parts = Trait.decodeKeys(cmd,':')
            # print('parts',parts)
            if parts[-1][0] == self.DIRECT:
                parts = parts[:-1]+Trait.decodeKeys(parts[-1],',')
            if parts[-1][0] == self.INCREMENT:
                parts = parts[:-1]+Trait.decodeKeys(parts[-1],',')
            ret.append(parts)
        # print(commands,parts)
        return ret
    
    def getCommands(self):
        return self.decodeCommands(self['commands'])

    def setCommands(self,commands):
        self['commands'] = self.encodeCommands(commands)
        
    def check(self):
        assert len(self['name']) > 0,\
            f'No name given for ChangePropertyTrait'
        
        
# --------------------------------------------------------------------
class DynamicPropertyTrait(ChangePropertyTrait):
    ID = 'PROP'
    def __init__(self,
                 *commands,
                 name        = '',
                 value       = 0,
                 numeric     = False,
                 min         = 0,
                 max         = 100,
                 wrap        = False,
                 description = ''):
        '''Commands are

            - menu
            - key
            - Type (only 'P' for now)
            - Expression
        '''
        super(DynamicPropertyTrait,self).__init__(*commands,
                                                  numeric = numeric,
                                                  min     = min,
                                                  max     = max,
                                                  wrap    = wrap)
        # print(commands,'Name',name)
        self.setType(name        = name,
                     constraints = self._constraints,
                     commands    = self._commands,
                     description = description)
        self.setState(value=value)

    
Trait.known_traits.append(DynamicPropertyTrait)

#
# EOF
#
# ====================================================================
# From traits/globalproperty.py

# --------------------------------------------------------------------
class GlobalPropertyTrait(ChangePropertyTrait):
    # The real value of CURRENT_ZONE causes problems when copying the
    # trait, since it contains slashes.  Maybe a solition is to make
    # it a raw string with escaped slashes?  No, that's already done
    # below when setting the type.  However, the default in the Java
    # code is the CURRENT_ZONE real value, so setting this to the
    # empty string should make it be that value.
    ID = 'setprop'
    CURRENT_ZONE = 'Current Zone/Current Map/Module'
    NAMED_ZONE   = 'Named Zone'
    NAMED_MAP    = 'Named Map'
    DIRECT       = 'P'
    def __init__(self,
                 *commands,
                 name        = '',
                 numeric     = False,
                 min         = 0,
                 max         = 100,
                 wrap        = False,
                 description = '',
                 level       = CURRENT_ZONE,
                 search      = ''):
        '''Commands are

            - menu
            - key
            - Type (only 'P' for now)
            - Expression
        '''
        super(GlobalPropertyTrait,self).__init__(*commands,
                                                 numeric = numeric,
                                                 min     = min,
                                                 max     = max,
                                                 wrap    = wrap)
        self.setType(name        = name,
                     constraints = self._constraints,
                     commands    = self._commands,
                     description = description,
                     level       = level.replace('/',r'\/'),
                     search      = search)
        self.setState()

Trait.known_traits.append(GlobalPropertyTrait)

#
# EOF
#
# ====================================================================
# From traits/prototype.py

# --------------------------------------------------------------------
class PrototypeTrait(Trait):
    ID = 'prototype'
    def __init__(self,name=''):
        '''Create a prototype trait (VASSAL.counter.UsePrototype)'''
        super(PrototypeTrait,self).__init__()
        self.setType(name = name)
        self.setState(ignored = '')


Trait.known_traits.append(PrototypeTrait)

#
# EOF
#
# ====================================================================
# From traits/place.py

# --------------------------------------------------------------------
class PlaceTrait(Trait):
    ID      = 'placemark'
    STACK_TOP = 0
    STACK_BOTTOM = 1
    ABOVE = 2
    BELOW = 3

    # How the LaTeX exporter organises the units.  Format with
    # 0: the group
    # 1: the piece name 
    # SKEL_PATH = (PieceWindow.TAG +r':Counters\/'        +
    #              TabWidget.TAG   +r':Counters\/'        +
    #              PanelWidget.TAG +':{0}'         +r'\/'+
    #              ListWidget.TAG  +':{0} counters'+r'\/'+
    #              PieceSlot.TAG   +':{1}')
    @classmethod
    @property
    def SKEL_PATH(cls):

        return (PieceWindow.TAG +r':Counters\/'        +
                TabWidget.TAG   +r':Counters\/'        +
                PanelWidget.TAG +':{0}'         +r'\/'+
                ListWidget.TAG  +':{0} counters'+r'\/'+
                PieceSlot.TAG   +':{1}')
    
    def __init__(self,
                 command         = '', # Context menu name
                 key             = '', # Context menu key
                 markerSpec      = '', # Full path in module
                 markerText      = 'null', # Hard coded message
                 xOffset         = 0,
                 yOffset         = 0,
                 matchRotation   = True,
                 afterKey        = '',
                 description     = '',
                 gpid            = '', # Set in JAVA, but with warning
                 placement       = ABOVE,
                 above           = False):
        '''Create a place marker trait (VASSAL.counter.PlaceMarker)'''
        super(PlaceTrait,self).__init__()
        self.setType(command         = command,          # Context menu name
                     key             = key,              # Context menu key
                     markerSpec      = markerSpec,
                     markerText      = markerText,
                     xOffset         = xOffset,
                     yOffset         = yOffset,
                     matchRotation   = matchRotation,
                     afterKey        = afterKey,
                     description     = description,
                     gpid            = gpid,
                     placement       = placement,
                     above           = above)
        self.setState()

Trait.known_traits.append(PlaceTrait)

# --------------------------------------------------------------------
class ReplaceTrait(PlaceTrait):
    ID = 'replace'
    def __init__(self,
                 command         = '', # Context menu name
                 key             = '', # Context menu key
                 markerSpec      = '', # Full path in module
                 markerText      = 'null', # Hard message
                 xOffset         = 0,
                 yOffset         = 0,
                 matchRotation   = True,
                 afterKey        = '',
                 description     = '',
                 gpid            = '', # Set in JAVA
                 placement       = PlaceTrait.ABOVE,
                 above           = False):
        super(ReplaceTrait,self).__init__(command         = command, 
                                          key             = key,  
                                          markerSpec      = markerSpec,
                                          markerText      = markerText,
                                          xOffset         = xOffset,
                                          yOffset         = yOffset,
                                          matchRotation   = matchRotation,
                                          afterKey        = afterKey,
                                          description     = description,
                                          gpid            = gpid,
                                          placement       = placement,
                                          above           = above)
    

Trait.known_traits.append(ReplaceTrait)

#
# EOF
#
# ====================================================================
# From traits/report.py

# --------------------------------------------------------------------
class ReportTrait (Trait):
    ID = 'report'
    def __init__(self,
                 *keys,
                 nosuppress = True,
                 description = '',
                 report      = '$location$: $newPieceName$ $menuCommand$ *',
                 cyclekeys   = '',
                 cyclereps   = ''):
        '''Create a report trait (VASSAL.counters.ReportActon)'''
        super(ReportTrait,self).__init__()
        esckeys = ','.join([k.replace(',',r'\,') for k in keys])
        esccycl = ','.join([k.replace(',',r'\,') for k in cyclekeys])
        escreps = ','.join([k.replace(',',r'\,') for k in cyclereps])
        
        self.setType(keys         = esckeys,
                     report       = report,
                     cycleKeys    = esccycl,
                     cycleReports = escreps,
                     description  = description,
                     nosuppress   = nosuppress)
        self.setState(cycle = -1)

Trait.known_traits.append(ReportTrait)

#
# EOF
#
# ====================================================================
# From traits/calculatedproperty.py

# --------------------------------------------------------------------
class CalculatedTrait(Trait):
    ID = 'calcProp'
    def __init__(self,name='',expression='',description=''):
        '''Define a trait that calculates a property'''
        super(CalculatedTrait,self).__init__()
        self.setType(name        = name,
                     expression  = expression,
                     description = description)
        self.setState()


Trait.known_traits.append(CalculatedTrait)

#
# EOF
#
# ====================================================================
# From traits/restrictcommand.py

# --------------------------------------------------------------------
class RestrictCommandsTrait(Trait):
    ID = 'hideCmd'
    HIDE = 'Hide'
    DISABLE = 'Disable'
    def __init__(self,
                 name          = '',
                 hideOrDisable = HIDE,
                 expression    = '',# Restrict when true
                 keys          = []):
        '''Create a layer trait (VASSAL.counter.RestrictCommands)'''
        super(RestrictCommandsTrait,self).__init__()
        encKeys = ','.join([k.replace(',',r'\,') for k in keys])
        self.setType(name          = name,
                     hideOrDisable = hideOrDisable,
                     expression    = expression,
                     keys          = encKeys)
        self.setState(state='')
    def setKeys(self,keys):
        self['keys'] = ','.join([k.replace(',',r'\,') for k in keys])
    

Trait.known_traits.append(RestrictCommandsTrait)

#
# EOF
#
# ====================================================================
# From traits/label.py

class LabelTraitCodes:
    TOP    = 't'
    BOTTOM = 'b'
    CENTER = 'c'
    LEFT   = 'l'
    RIGHT  = 'r'
    PLAIN  = 0
    BOLD   = 1
    ITALIC = 2
    
# --------------------------------------------------------------------
class LabelTrait(Trait):
    ID     = 'label'
    def __init__(self,
                 label           = None,
                 labelKey        = '',
                 menuCommand     ='Change label',
                 fontSize        = 10,
                 background      = 'none',
                 foreground      = '255,255,255',
                 vertical        = LabelTraitCodes.TOP,
                 verticalOff     = 0,
                 horizontal      = LabelTraitCodes.CENTER,
                 horizontalOff   = 0,
                 verticalJust    = LabelTraitCodes.BOTTOM,
                 horizontalJust  = LabelTraitCodes.CENTER,
                 nameFormat      = '$pieceName$ ($label$)',
                 fontFamily      = 'Dialog',
                 fontStyle       = LabelTraitCodes.PLAIN,
                 rotate          = 0,
                 propertyName    = 'TextLabel',
                 description     = '',
                 alwaysUseFormat = False):
        '''Create a label trait (can be edited property)'''
        super(LabelTrait,self).__init__()
        self.setType(labelKey		= labelKey,
                     menuCommand	= menuCommand,
                     fontSize		= fontSize,
                     background		= background,
                     foreground		= foreground,
                     vertical		= vertical,
                     verticalOff	= verticalOff,
                     horizontal		= horizontal,
                     horizontalOff	= horizontalOff,
                     verticalJust	= verticalJust,
                     horizontalJust	= horizontalJust,
                     nameFormat		= nameFormat,
                     fontFamily		= fontFamily,
                     fontStyle		= fontStyle,
                     rotate		= rotate,
                     propertyName	= propertyName,
                     description	= description,
                     alwaysUseFormat	= alwaysUseFormat)
        self.setState(label = (nameFormat if label is None else label))


Trait.known_traits.append(LabelTrait)

#
# EOF
#
# ====================================================================
# From traits/layer.py

# --------------------------------------------------------------------
class LayerTrait(Trait):
    ID = 'emb2'
    def __init__(self,
                 images       = [''],
                 newNames     = None,
                 activateName = 'Activate',
                 activateMask = CTRL,
                 activateChar = 'A',
                 increaseName = 'Increase',
                 increaseMask = CTRL,
                 increaseChar = '[',
                 decreaseName = '',
                 decreaseMask = CTRL,
                 decreaseChar  = ']',
                 resetName    = '',
                 resetKey     = '',
                 resetLevel   = 1,
                 under        = False,
                 underXoff    = 0,
                 underYoff    = 0,
                 loop         = True,
                 name         = '',
                 description  = '',
                 randomKey    = '',
                 randomName   = '',
                 follow       = False,
                 expression   = '',
                 first        = 1,
                 version      = 1, # 1:new, 0:old
                 always       = True,
                 activateKey  = key('A'),
                 increaseKey  = key('['),
                 decreaseKey  = key(']'),
                 scale        = 1.):
        '''Create a layer trait (VASSAL.counter.Embellishment)'''
        super(LayerTrait,self).__init__()
        if newNames is None and images is not None:
            newNames = ['']*len(images)
        self.setType(
            activateName        = activateName,
            activateMask        = activateMask,
            activateChar        = activateChar,
            increaseName        = increaseName,
            increaseMask        = increaseMask,
            increaseChar        = increaseChar,
            decreaseName        = decreaseName,
            decreaseMask        = decreaseMask,
            decreaseChar        = decreaseChar,
            resetName           = resetName,
            resetKey            = resetKey,
            resetLevel          = resetLevel,
            under               = under,
            underXoff           = underXoff,
            underYoff           = underYoff,
            images              = ','.join(images),
            newNames            = ','.join(newNames),
            loop                = loop,
            name                = name,
            randomKey           = randomKey,
            randomName          = randomName,
            follow              = follow,
            expression          = expression,
            first               = first,
            version             = version,
            always              = always,
            activateKey         = activateKey,
            increaseKey         = increaseKey,
            decreaseKey         = decreaseKey,
            description         = description,
            scale               = scale)
        self.setState(level=1)

Trait.known_traits.append(LayerTrait)

#
# EOF
#
# ====================================================================
# From traits/globalcommand.py

# --------------------------------------------------------------------
class GlobalCommandTrait(Trait):
    ID = 'globalkey'
    def __init__(self,
                 commandName   = '',
                 key           = '', # Command received
                 globalKey     = '', # Command to send to targets
                 properties    = '', # Filter target on this expression
                 ranged        = False,
                 range         = 1,
                 reportSingle  = True,
                 fixedRange    = True,
                 rangeProperty = '',
                 description   = '',
                 deskSelect    = '-1',
                 target        = ''):
        '''Create a global key command in piece
        (VASSAL.counters.CounterGlobalKeyCommand)'''
        self.setType(commandName   = commandName,
                     key           = key,
                     globalKey     = globalKey,
                     properties    = properties,
                     ranged        = ranged,
                     range         = range,
                     reportSingle  = reportSingle,
                     fixedRange    = fixedRange,
                     rangeProperty = rangeProperty,
                     description   = description,
                     deskSelect    = deskSelect,
                     target        = target)
        self.setState()
        
Trait.known_traits.append(GlobalCommandTrait)

#
# EOF
#
# ====================================================================
# From traits/globalhotkey.py

# --------------------------------------------------------------------
class GlobalHotkeyTrait(Trait):
    ID = 'globalhotkey'
    def __init__(self,
                 name          = '', # Command received
                 key           = '', # Command key received
                 globalHotkey  = '', # Key to send
                 description   = ''):
        '''Create a global key command in piece
        (VASSAL.counters.GlobalHotkey)'''
        self.setType(name          = name,
                     key           = key,
                     globalHotkey  = globalHotkey,
                     description   = description)
        self.setState()
        
Trait.known_traits.append(GlobalHotkeyTrait)

#
# EOF
#
# ====================================================================
# From traits/nostack.py

# --------------------------------------------------------------------
class NoStackTrait(Trait):
    ID = 'immob'
    NORMAL_SELECT         = ''
    SHIFT_SELECT          = 'i'
    CTRL_SELECT           = 't'
    ALT_SELECT            = 'c'
    NEVER_SELECT          = 'n'
    NORMAL_BAND_SELECT    = ''
    ALT_BAND_SELECT       = 'A'
    ALT_SHIFT_BAND_SELECT = 'B'
    NEVER_BAND_SELECT     = 'Z'
    NORMAL_MOVE           = 'N'
    SELECT_MOVE           = 'I'
    NEVER_MOVE            = 'V'
    NORMAL_STACK          = 'L'
    NEVER_STACK           = 'R'
    IGNORE_GRID           = 'g'
    def __init__(self,
                 select      = NORMAL_SELECT,
                 bandSelect  = NORMAL_BAND_SELECT,
                 move        = NORMAL_MOVE,
                 canStack    = False,
                 ignoreGrid  = False,
                 description = ''):
        '''No stacking trait'''
        selectionOptions = (select +
                            (self.IGNORE_GRID if ignoreGrid else '') +
                            bandSelect)
        movementOptions  = move
        stackingOptions  = self.NORMAL_STACK if canStack else self.NEVER_STACK
                 
        '''Create a mark trait (static property)'''
        super(NoStackTrait,self).__init__()
        
        self.setType(selectionOptions = selectionOptions,
                     movementOptions  = movementOptions,
                     stackingOptions  = stackingOptions,
                     description      = description)
        self.setState()


Trait.known_traits.append(NoStackTrait)

#
# EOF
#
# ====================================================================
# From traits/deselect.py

# --------------------------------------------------------------------
class DeselectTrait(Trait):
    ID = 'deselect'
    THIS = 'D' # Deselect only this piece
    ALL  = 'A' # Deselect all pieces 
    ONLY = 'S' # Select this piece only
    def __init__(self,
                 command     = '',
                 key         = '',
                 description = '',
                 unstack     = False,
                 deselect    = THIS):
        '''Create a deselect trait'''
        super(DeselectTrait,self).__init__()
        self.setType(command     = command,
                     key         = key,
                     description = description,
                     unstack     = unstack,
                     deselect    = deselect)
        self.setState()


Trait.known_traits.append(DeselectTrait)

#
# EOF
#
# ====================================================================
# From traits/restrictaccess.py

# --------------------------------------------------------------------
class RestrictAccessTrait(Trait):
    ID = 'restrict'
    def __init__(self,
                 sides         = [],
                 byPlayer      = False,
                 noMovement    = True,
                 description   = '',
                 owner         = '',):
        '''Create a layer trait (VASSAL.counter.Restricted)'''
        super(RestrictAccessTrait,self).__init__()
        encSides = ','.join(sides)
        self.setType(sides         = encSides,
                     byPlayer      = byPlayer,
                     noMovement    = noMovement,
                     description   = description)
        self.setState(owner=owner)

Trait.known_traits.append(RestrictAccessTrait)

#
# EOF
#
# ====================================================================
# From traits/rotate.py

# --------------------------------------------------------------------
class RotateTrait(Trait):
    ID = 'rotate'
    def __init__(self,
                 nangles          = 6,
                 rotateCWKey      = key(']'),
                 rotateCCWKey     = key('['),
                 rotateCW         = 'Rotate CW',
                 rotateCCW        = 'Rotate CCW',
                 rotateRndKey     = '',
                 rotateRnd        = '',
                 name             = 'Rotate',
                 description      = 'Rotate piece',
                 rotateDirectKey  = '',
                 rotateDirect     = '',
                 directExpression = '',
                 directIsFacing   = True,
                 angle            = 0):
        '''Create a Rotate trait'''
        super(RotateTrait,self).__init__()
        if nangles == 1:
            self.setType(nangles          = nangles,
                         rotateKey        = rotateCWKey,
                         rotate           = rotateCW,
                         rotateRndKey     = rotateRndKey,
                         rotateRnd        = rotateRnd,
                         name             = name,
                         description      = description,
                         rotateDirectKey  = rotateDirectKey,
                         rotateDirect     = rotateDirect,
                         directExpression = directExpression,
                         directIsFacing   = directIsFacing)
        else:
            self.setType(nangles          = nangles,
                         rotateCWKey      = rotateCWKey,
                         rotateCCWKey     = rotateCCWKey,
                         rotateCW         = rotateCW,
                         rotateCCW        = rotateCCW,
                         rotateRndKey     = rotateRndKey,
                         rotateRnd        = rotateRnd,
                         name             = name,
                         description      = description,
                         rotateDirectKey  = rotateDirectKey,
                         rotateDirect     = rotateDirect,
                         directExpression = directExpression,
                         directIsFacing   = directIsFacing)
            
        self.setState(angle = int(angle) if nangles > 1 else float(angle))

Trait.known_traits.append(RotateTrait)

#
# EOF
#
# ====================================================================
# From traits/stack.py

# --------------------------------------------------------------------
class StackTrait(Trait):
    ID = 'stack'
    def __init__(self,
                 board     = '',
                 x         = '',  
                 y         = '',  
                 pieceIds  = [],
                 layer     = -1): 
        '''Create a stack trait in a save file'''
        self.setType()       # NAME
        # print('Piece IDs:',pieceIds)
        self.setState(board      = board,
                      x          = x,
                      y          = y,
                      pieceIds   = ';'.join([str(p) for p in pieceIds]),
                      layer      = f'@@{layer}')
        
Trait.known_traits.append(StackTrait)

#
# EOF
#
# ====================================================================
# From traits/mark.py

# --------------------------------------------------------------------
class MarkTrait(Trait):
    ID = 'mark'
    def __init__(self,name='',value=''):
        '''Create a mark trait (static property)'''
        super(MarkTrait,self).__init__()
        self.setType(name = name)
        self.setState(value = value)


Trait.known_traits.append(MarkTrait)

#
# EOF
#
# ====================================================================
# From traits/mask.py

# --------------------------------------------------------------------
# Inset
# obs;88,130;ag_hide_1.png;Reveal;I;?;sides:Argentine;Peek;;true;;
# obs;88,130;ag_hide_1.png;Reveal;I;?;side:Argentine;;;true;;
#
# Peek
# obs;88,130;ag_hide_1.png;Reveal;P89,130;?;sides:Argentine;Peek;;true;;
#
# Image
#
class MaskTrait(Trait):
    ID = 'obs'
    INSET = 'I'
    BACKGROUND = 'B'
    PEEK = 'P'
    IMAGE = 'G'
    INSET2 = '2'
    PLAYER = 'player:'
    SIDE = 'side:'
    SIDES = 'sides:'
    def __init__(self,
                 keyCommand   = '',
                 imageName    = '',
                 hideCommand  = '',
                 displayStyle = '',
                 peekKey      = '',
                 ownerImage   = '',
                 maskedName   = '?',
                 access       = '',#?
                 peekCommand  = '',
                 description  = '',
                 autoPeek     = True,
                 dealKey      = '',
                 dealExpr     = ''):
        '''Create a mark trait (static property)'''
        super(MaskTrait,self).__init__()
        disp = displayStyle
        if displayStyle == self.PEEK:
            disp += peekKey
        elif displayStyle == self.IMAGE:
            disp += ownerImage
            
        acc = self.PLAYER
        if isinstance(access,list):
            acc = self.SIDES + ':'.join(access)
        elif access.startswith('player'):
            acc = self.PLAYER
        elif access.startswith('side'):
            acc = self.SIDE
                
        self.setType(keyCommand   = keyCommand,
                     imageImage   = imageName,
                     hideCommand  = hideCommand,
                     displayStyle = disp,
                     maskedName   = maskedName,
                     access       = acc, # ?
                     peekCommand  = peekCommand,
                     description  = description,
                     autoPeek     = autoPeek,
                     dealKey      = dealKey,
                     dealExpr     = dealExpr)
        self.setState(value='null')

    @classmethod
    def peekDisplay(cls,key):#Encoded key
        return cls.PEEK + key
    
    @classmethod
    def peekImage(cls,ownerImage):
        return cls.IMAGE + ownerImage

    @classmethod
    def sides(cls,*names):
        return cls.SIDES+':'.join(names)

Trait.known_traits.append(MaskTrait)

#
# EOF
#
# ====================================================================
# From traits/trail.py

# --------------------------------------------------------------------
class TrailTrait(Trait):
    ID = 'footprint'
    def __init__(self,
                 key             = key('T'),
                 name            = 'Movement Trail',
                 localVisible    = False,
                 globalVisible   = False,
                 radius          = 10,
                 fillColor       = rgb(255,255,255),
                 lineColor       = rgb(0,0,0),
                 activeOpacity   = 100,
                 inactiveOpacity = 50,
                 edgesBuffer     = 20,
                 displayBuffer   = 30,
                 lineWidth       = 1,
                 turnOn          = '',
                 turnOff         = '',
                 reset           = '',
                 description     = ''):        
        ''' Create a movement trail trait ( VASSAL.counters.Footprint)'''
        super(TrailTrait,self).__init__()
        self.setType(key               = key,# ENABLE KEY
                     name              = name,# MENU 
                     localVisible      = localVisible,# LOCAL VISABLE
                     globalVisible     = globalVisible,# GLOBAL VISABLE
                     radius            = radius,# RADIUS
                     fillColor         = fillColor,# FILL COLOR
                     lineColor         = lineColor,# LINE COLOR 
                     activeOpacity     = activeOpacity,# ACTIVE OPACITY
                     inactiveOpacity   = inactiveOpacity,# INACTIVE OPACITY
                     edgesBuffer       = edgesBuffer,# EDGES BUFFER
                     displayBuffer     = displayBuffer,# DISPLAY BUFFER
                     lineWidth         = int(lineWidth),# LINE WIDTH 
                     turnOn            = turnOn,# TURN ON KEY
                     turnOff           = turnOff,# TURN OFF KEY
                     reset             = reset,# RESET KEY
                     description       = description)       # DESC
        self.setState(isGlobal  = False,
                      map       = '',
                      points    = 0,     # POINTS (followed by [; [X,Y]*]
                      init      = False) 

Trait.known_traits.append(TrailTrait)

#
# EOF
#
# ====================================================================
# From traits/delete.py

# --------------------------------------------------------------------
class DeleteTrait(Trait):
    ID = 'delete'
    def __init__(self,
                 name   = 'Delete',
                 key = key('D')):
        '''Create a delete trait (VASSAL.counters.Delete)'''
        super(DeleteTrait,self).__init__()
        self.setType(name  = name,
                     key   = key,
                     dummy = '')
        self.setState()

Trait.known_traits.append(DeleteTrait)

#
# EOF
#
# ====================================================================
# From traits/sendto.py

# --------------------------------------------------------------------
class SendtoTrait(Trait):
    ID = 'sendto'
    LOCATION = 'L'
    ZONE     = 'Z'
    REGION   = 'R'
    GRID     = 'G'
    def __init__(self,
                 mapName     = '',
                 boardName   = '',
                 name        = '',
                 key         = key('E'),
                 restoreName = 'Restore',
                 restoreKey  = key('R'),
                 x           = 200,
                 y           = 200,
                 xidx        = 0,
                 yidx        = 0,
                 xoff        = 1,
                 yoff        = 1,
                 description = '',
                 destination = LOCATION,
                 zone        = '',
                 region      = '',
                 expression  = '',
                 position    = ''):
        '''Create a send to trait (VASSAL.counter.SendToLocation)'''
        self.setType(name           = name,# NAME
                     key            = key,# KEY , MODIFIER
                     mapName        = mapName,# MAP
                     boardName      = boardName,# BOARD
                     x              = x,
                     y              = y,# X ; Y
                     restoreName    = restoreName,# BACK
                     restoreKey     = restoreKey,# KEY , MODIFIER
                     xidx           = xidx,
                     yidx           = yidx,# XIDX ; YIDX
                     xoff           = xoff,
                     yoff           = yoff,# XOFF ; YOFF
                     description    = description,# DESC
                     destination    = destination,# DEST type
                     zone           = zone,# ZONE
                     region         = region,# REGION
                     expression     = expression,# EXPRESSION
                     position       = position)                   # GRIDPOS
        self.setState(backMap = '', backX = '', backY = '')

Trait.known_traits.append(SendtoTrait)

#
# EOF
#
# ====================================================================
# From traits/moved.py

# --------------------------------------------------------------------
class MovedTrait(Trait):
    ID = 'markmoved'
    def __init__(self,
                 image      = 'moved.gif',
                 xoff       = 36,
                 yoff       = -38,
                 name       = 'Mark moved',
                 key        = key('M'),
                 dummy      = ''  # Description
                 # ignoreSame = True
                 ):
        '''Create a moved trait (VASSAL.counters.MovementMarkable)'''
        super(MovedTrait,self).__init__()
        self.setType(image    = image,
                     xoff     = xoff,
                     yoff     = yoff,
                     name     = name,
                     key      = key,
                     dummy    = dummy, # Description
                     # ignoreSame = ignoreSame
                     )
        self.setState(moved = False)

Trait.known_traits.append(MovedTrait)

#
# EOF
#
# ====================================================================
# From traits/skel.py


#
# EOF
#
# ====================================================================
# From traits/submenu.py

# --------------------------------------------------------------------
class SubMenuTrait(Trait):
    ID = 'submenu'
    def __init__(self,
                 subMenu     = '',  # Title
                 keys        = [],  # Keys
                 description = ''):
        '''Create a sub menu (VASSAL.counters.SubMenu)'''
        self.setType(subMenu     = subMenu,   # CLONEKEY
                     keys        = ','.join([k.replace(',',r'\,')
                                             for k in keys]),
                     description = description)
        self.setState() # PROPERTY COUNT (followed by [; KEY; VALUE]+)
    def setKeys(self,keys):
        '''Set the keys'''
        self['keys'] = ','.join([k.replace(',',r'\,') for k in keys])
        
Trait.known_traits.append(SubMenuTrait)

#
# EOF
#
# ====================================================================
# From traits/basic.py

# --------------------------------------------------------------------
class BasicTrait(Trait):
    ID = 'piece'
    def __init__(self,
                 name      = '',
                 filename  = '',  # Can be empty
                 gpid      = '',  # Can be empty
                 cloneKey  = '',  # Deprecated
                 deleteKey = ''): # Deprecated
        '''Create a basic unit (VASSAL.counters.BasicPiece)'''
        self.setType(cloneKey  = cloneKey,   # CLONEKEY
                     deleteKey = deleteKey,  # DELETEKEY
                     filename  = filename,   # IMAGE  
                     name      = name)       # NAME
        self.setState(map        = 'null', # MAPID (possibly 'null')
                      x          = 0,
                      y          = 0,
                      gpid       = gpid,
                      properties = 0) # PROPERTY COUNT (followed by [; KEY; VALUE]+)
        
Trait.known_traits.append(BasicTrait)

#
# EOF
#
# ====================================================================
# From traits/trigger.py

# --------------------------------------------------------------------
class TriggerTrait(Trait):
    ID      = 'macro'
    WHILE   = 'while'
    UNTIL   = 'until'
    COUNTED = 'counted' # - Always one "do ... while"
    def __init__(self,
                 name            = '',
                 command         = '', # Context menu name
                 key             = '', # Context menu key
                 property        = '', # Enable/Disable
                 watchKeys       = [],
                 actionKeys      = [], # What to do
                 loop            = False,
                 preLoop         = '', # Key
                 postLoop        = '', # Key
                 loopType        = COUNTED, # Loop type
                 whileExpression = '',
                 untilExpression = '',
                 count           = 0,
                 index           = False,
                 indexProperty   = '',
                 indexStart      = '',
                 indexStep       = ''):
        '''Create a layer trait (VASSAL.counter.Trigger)'''
        super(TriggerTrait,self).__init__()
        encWKeys = Trait.encodeKeys(watchKeys, ',')
        encAKeys = Trait.encodeKeys(actionKeys,',')
        self.setType(name            = name,            
                     command         = command,          # Context menu name
                     key             = key,              # Context menu key
                     property        = property,         # Enable/Disable
                     watchKeys       = encWKeys,       
                     actionKeys      = encAKeys,         # What to do
                     loop            = loop,            
                     preLoop         = preLoop,          # Key
                     postLoop        = postLoop,         # Key
                     loopType        = loopType,         # Loop type
                     whileExpression = whileExpression, 
                     untilExpression = untilExpression, 
                     count           = count,           
                     index           = index,           
                     indexProperty   = indexProperty,   
                     indexStart      = indexStart,      
                     indexStep       = indexStep)       
        self.setState(state='')

    def getActionKeys(self):
        return Trait.decodeKeys(self['actionKeys'],',')
    
    def getWatchKeys(self):
        return Trait.decodeKeys(self['watchKeys'],',')
    
    def setActionKeys(self,keys):
        self['actionKeys'] = Trait.encodeKeys(keys,',')
        
    def setWatchKeys(self,keys):
        self['watchKeys'] = Trait.encodeKeys(keys,',')
        
        

Trait.known_traits.append(TriggerTrait)

#
# EOF
#
# ====================================================================
# From traits/nonrect.py

# --------------------------------------------------------------------
class NonRectangleTrait(Trait):
    ID      = 'nonRect2'
    CLOSE   = 'c'
    MOVETO  = 'm'
    LINETO  = 'l'
    CUBICTO = 'l'
    QUADTO  = 'l'
    def __init__(self,
                 scale    = 1.,
                 filename = '',
                 path     = [],
                 image    = None):
        '''Create a NonRectangle trait (static property)'''
        super(NonRectangleTrait,self).__init__()
        l = []
        if len(filename) > 0:
            l.append(f'n{filename}')

        if len(path) <= 0:
            path = self.getShape(image)

        if len(path) > 0:
            # print(path)
            l += [f'{p[0]},{int(p[1])},{int(p[2])}' if len(p) > 2 else p
                  for p in path]
    
        self.setType(scale = scale,
                     code  = ','.join(l))
        self.setState()

    @classmethod
    def getShape(cls,image):
        if image is None:
            return []

        from PIL import Image
        from io import BytesIO

        code = []
        with Image.open(BytesIO(image)) as img:
            alp = img.getchannel('A') # Alpha channel
            # Find least and largest non-transparent pixel in each row
            rows  = []
            w     = alp.width
            h     = alp.height
            bb    = alp.getbbox()
            for r in range(bb[1],bb[3]):
                ll, rr = bb[2], bb[0]
                for c in range(bb[0],bb[2]):
                    if alp.getpixel((c,r)) != 0:
                        ll = min(c,ll)
                        rr = max(c,rr)
                rows += [[r-h//2,ll-w//2,rr-w//2]]
                    
            # Now produce the code - we start with the top line
            code = [(cls.MOVETO,rows[0][1],rows[0][0]-1),
                    (cls.LINETO,rows[0][2],rows[0][0]-1)]
            
            # Now loop  down right side of image
            for c in rows:
                last = code[-1]
                if last[1] != c[2]:
                    code += [(cls.LINETO, c[2], last[2])]
                code += [(cls.LINETO, c[2], c[0])]
                
            # Now loop up left side of image
            for c in rows[::-1]:
                last = code[-1]
                if last[1] != c[1]:
                    code += [(cls.LINETO,c[1],last[2])]
                code += [(cls.LINETO,c[1],c[0])]

            # Terminate with close
            code += [(cls.CLOSE)]

        return code


Trait.known_traits.append(NonRectangleTrait)

#
# EOF
#
# ====================================================================
# From traits/click.py

# --------------------------------------------------------------------
class ClickTrait(Trait):
    ID = 'button'
    def __init__(self,
                 key         = '',
                 x           = 0,
                 y           = 0,
                 width       = 0,
                 height      = 0,
                 description = '',
                 context     = False,
                 whole       = True,
                 version     = 1,
                 points      = []):
        '''Create a click trait (static property)'''
        super(ClickTrait,self).__init__()
        self.setType(key          = key,
                     x            = x,
                     y            = y,
                     width        = width,
                     height       = height,
                     description  = description,
                     context      = context,
                     whole        = whole,
                     version      = version,
                     npoints      = len(points),
                     points       = ';'.join([f'{p[0]};{p[1]}'
                                              for p in points]))            
        self.setState()


Trait.known_traits.append(ClickTrait)

#
# EOF
#
# ====================================================================
# From game.py

# --------------------------------------------------------------------
class Game(Element):
    TAG = Element.BUILD+'GameModule'
    def __init__(self,build,node=None,
                 name            = '',
                 version         = '', 
                 ModuleOther1    = "",
                 ModuleOther2    = "",
                 VassalVersion   = "3.6.7",
                 description     = "",
                 nextPieceSlotId = 20):
        '''Create a new Game object

        Parameters
        ----------
        build : xml.dom.Document
            root note
        node : xml.dom.Node
            To read from, or None
        name : str
            Name of module
        version : str
            Version of module
        ModuleOther1 : str
            Free form string 
        ModuleOther2 : str
            Free form string
        VassalVersion : str
            VASSAL version this was created for
        description : str
            Speaks volumes
        nextPieceSlotId : int
            Starting slot ID.
        '''
        super(Game,self).__init__(build, self.TAG,
                                  node            = node,
                                  name            = name,
                                  version         = version,
                                  ModuleOther1    = ModuleOther1,
                                  ModuleOther2    = ModuleOther2,
                                  VassalVersion   = VassalVersion,
                                  description     = description,
                                  nextPieceSlotId = nextPieceSlotId)
    def nextPieceSlotId(self):
        '''Increment next piece slot ID'''
        ret = int(self.getAttribute('nextPieceSlotId'))
        self.setAttribute('nextPieceSlotId',str(ret+1))
        return ret
    #
    def addBasicCommandEncoder(self,**kwargs):
        '''Add a `BasicCommandEncoder` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : BasicCommandEncoder
            The added element
        '''
        return self.add(BasicCommandEncoder,**kwargs)
    def addGlobalTranslatableMessages(self,**kwargs):
        '''Add a `GlobalTranslatableMessages` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : GlobalTranslatableMessages
            The added element
        '''
        return self.add(GlobalTranslatableMessages,**kwargs)
    def addPlayerRoster(self,**kwargs):
        '''Add a `PlayerRoster` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : PlayerRoster
            The added element
        '''
        return self.add(PlayerRoster,**kwargs)
    def addChessClock(self,**kwargs):
        '''Add a `ChessClockControl` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : PlayerRoster
            The added element
        '''
        return self.add(ChessClockControl,**kwargs)
    def addLanguage(self,**kwargs):
        '''Add a `Language` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Language
            The added element
        '''
        return self.add(Language,**kwargs)
    def addChatter(self,**kwargs):
        '''Add a `Chatter` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Chatter
            The added element
        '''
        return self.add(Chatter,**kwargs)
    def addKeyNamer(self,**kwargs):
        '''Add a `KeyNamer` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : KeyNamer
            The added element
        '''
        return self.add(KeyNamer,**kwargs)
    def addNotes(self,**kwargs):
        '''Add a `Notes` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Notes
            The added element
        '''
        return self.add(Notes,**kwargs)
    def addLanguage(self,**kwargs):
        '''Add a `Language` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Language
            The added element
        '''
        return self.add(Language,**kwargs)
    def addChatter(self,**kwargs):
        '''Add a `Chatter` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Chatter
            The added element
        '''
        return self.add(Chatter,**kwargs)
    def addKeyNamer(self,**kwargs):
        '''Add a `KeyNamer` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : KeyNamer
            The added element
        '''
        return self.add(KeyNamer,**kwargs)
    def addGlobalProperties(self,**kwargs):
        '''Add a `GlobalProperties` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : GlobalProperties
            The added element
        '''
        return self.add(GlobalProperties,**kwargs)
    def addGlobalOptions(self,**kwargs):
        '''Add a `GlobalOptions` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : GlobalOptions
            The added element
        '''
        return self.add(GlobalOptions,**kwargs)
    def addTurnTrack(self,**kwargs):
        '''Add a `TurnTrack` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : TurnTrack
            The added element
        '''
        return self.add(TurnTrack,**kwargs)
    def addDocumentation(self,**kwargs):
        '''Add a `Documentation` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Documentation
            The added element
        '''
        return self.add(Documentation,**kwargs)
    def addPrototypes(self,**kwargs):
        '''Add a `Prototypes` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Prototypes
            The added element
        '''
        return self.add(Prototypes,**kwargs)
    def addPieceWindow(self,**kwargs):
        '''Add a `PieceWindow` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : PieceWindow
            The added element
        '''
        return self.add(PieceWindow,**kwargs)
    def addChartWindow(self,**kwargs):
        '''Add a `ChartWindow` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : ChartWindow
            The added element
        '''
        return self.add(ChartWindow,**kwargs)
    def addInventory(self,**kwargs):
        '''Add a `Inventory` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Inventory
            The added element
        '''
        return self.add(Inventory,**kwargs)
    def addMap(self,**kwargs):
        '''Add a `Map` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Map
            The added element
        '''
        return self.add(Map,**kwargs)
    def addDiceButton(self,**kwargs):
        '''Add a `DiceButton` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : DiceButton
            The added element
        '''
        return self.add(DiceButton,**kwargs)
    def addPredefinedSetup(self,**kwargs):
        '''Add a `PredefinedSetup` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : PredefinedSetup
            The added element
        '''
        return self.add(PredefinedSetup,**kwargs)
    def addGameMassKey(self,**kwargs):
        '''Add a `GameMassKey` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : GameMassKey
            The added element
        '''
        return self.add(GameMassKey,**kwargs)
    def addStartupMassKey(self,**kwargs):
        '''Add a `StartupMassKey` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : StartupMassKey
            The added element
        '''
        return self.add(StartupMassKey,**kwargs)
    def addMenu(self,**kwargs):
        '''Add a `Menu` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Menu
            The added element
        '''
        return self.add(Menu,**kwargs)
    def addSymbolicDice(self,**kwargs):
        '''Add a `SymbolicDice` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : SymbolicDice
            The added element
        '''
        return self.add(SymbolicDice,**kwargs)
    
    
    # ----------------------------------------------------------------
    def getGlobalProperties(self,single=True):
        '''Get all or a sole `GlobalPropertie` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `GlobalPropertie` child, otherwise fail.
            If `False` return all `GlobalPropertie` children in this element
        Returns
        -------
        children : list
            List of `GlobalPropertie` children (even if `single=True`)
        '''
        return self.getAllElements(GlobalProperties,single)
    def getBasicCommandEncoder(self,single=True):
        '''Get all or a sole `Ba` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `Ba` child, otherwise fail.
            If `False` return all `Ba` children in this element
        Returns
        -------
        children : list
            List of `Ba` children (even if `single=True`)
        '''
        return self.getAllElements(BasicCommandEncoder,single)
    def getGlobalTranslatableMessages(self,single=True):
        '''Get all or a sole `GlobalTranslatableMessage` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `GlobalTranslatableMessage` child, otherwise fail.
            If `False` return all `GlobalTranslatableMessage` children in this element
        Returns
        -------
        children : list
            List of `GlobalTranslatableMessage` children (even if `single=True`)
        '''
        return self.getAllElements(GlobalTranslatableMessages,single)
    def getLanguages(self,single=False):
        '''Get all or a sole `Language` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `Language` child, otherwise fail.
            If `False` return all `Language` children in this element
        Returns
        -------
        children : list
            List of `Language` children (even if `single=True`)
        '''
        return self.getAllElements(Language,single)
    def getChessClocks(self,asdict=False):
        '''Get all or a sole `Language` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `Language` child, otherwise fail.
            If `False` return all `Language` children in this element
        Returns
        -------
        children : list
            List of `Language` children (even if `single=True`)
        '''
        return self.getElementsByKey(ChessClockControl,'name',asdict)
    def getChatter(self,single=True):
        '''Get all or a sole `Chatter` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `Chatter` child, otherwise fail.
            If `False` return all `Chatter` children in this element
        Returns
        -------
        children : list
            List of `Chatter` children (even if `single=True`)
        '''
        return self.getAllElements(Chatter,single)
    def getKeyNamer(self,single=True):
        '''Get all or a sole `KeyNamer` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `KeyNamer` child, otherwise fail.
            If `False` return all `KeyNamer` children in this element
        Returns
        -------
        children : list
            List of `KeyNamer` children (even if `single=True`)
        '''
        return self.getAllElements(KeyNamer,single)
    def getDocumentation(self,single=True):
        '''Get all or a sole `Documentation` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `Documentation` child, otherwise fail.
            If `False` return all `Documentation` children in this element
        Returns
        -------
        children : list
            List of `Documentation` children (even if `single=True`)
        '''
        return self.getAllElements(Documentation,single)
    def getPrototypes(self,single=True):
        '''Get all or a sole `Prototypes` (i.e., the containers of
        prototypes, not a list of actual prototypes) element(s) from
        this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `Prototypes` child, otherwise fail.
            If `False` return all `Prototypes` children in this element
        Returns
        -------
        children : list
            List of `Prototype` children (even if `single=True`)

        '''
        return self.getAllElements(Prototypes,single)
    def getPlayerRoster(self,single=True):
        '''Get all or a sole `PlayerRo` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `PlayerRo` child, otherwise fail.
            If `False` return all `PlayerRo` children in this element
        Returns
        -------
        children : list
            List of `PlayerRo` children (even if `single=True`)
        '''
        return self.getAllElements(PlayerRoster,single)
    def getGlobalOptions(self,single=True):
        '''Get all or a sole `GlobalOption` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `GlobalOption` child, otherwise fail.
            If `False` return all `GlobalOption` children in this element
        Returns
        -------
        children : list
            List of `GlobalOption` children (even if `single=True`)
        '''
        return self.getAllElements(GlobalOptions,single)
    def getInventories(self,asdict=True):
        '''Get all Inventorie element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Inventorie` elements.  If `False`, return a list of all Inventorie` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `Inventorie` children
        '''
        return self.getElementsByKey(Inventory,'name',single)
    def getPieceWindows(self,asdict=True):
        '''Get all PieceWindow element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `PieceWindow` elements.  If `False`, return a list of all PieceWindow` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `PieceWindow` children
        '''
        return self.getElementsByKey(PieceWindow,'name',asdict)
    def getChartWindows(self,asdict=True):
        '''Get all ChartWindow element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `ChartWindow` elements.  If `False`, return a list of all ChartWindow` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `ChartWindow` children
        '''
        return self.getElementsByKey(ChartWindow,'name',asdict)
    def getDiceButtons(self,asdict=True):
        '''Get all DiceButton element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `DiceButton` elements.  If `False`, return a list of all DiceButton` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `DiceButton` children
        '''
        return self.getElementsByKey(DiceButton,'name',asdict)
    def getPredefinedSetups(self,asdict=True):
        '''Get all PredefinedSetup element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `PredefinedSetup` elements.  If `False`, return a list of all PredefinedSetup` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `PredefinedSetup` children
        '''
        return self.getElementsByKey(PredefinedSetup,'name',asdict)
    def getNotes(self,single=True):
        '''Get all or a sole `Note` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `Note` child, otherwise fail.
            If `False` return all `Note` children in this element
        Returns
        -------
        children : list
            List of `Note` children (even if `single=True`)
        '''
        return self.getAllElements(Notes,single)
    def getTurnTracks(self,asdict=True):
        '''Get all TurnTrack element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `TurnTrack` elements.  If `False`, return a list of all TurnTrack` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `TurnTrack` children
        '''
        return self.getElementsByKey(TurnTrack,'name',asdict)
    def getPieces(self,asdict=False):
        '''Get all Piece element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Piece` elements.  If `False`, return a list of all Piece` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `Piece` children
        '''
        return self.getElementsByKey(PieceSlot,'entryName',asdict)
    def getSpecificPieces(self,*names,asdict=False):
        '''Get all SpecificPiece element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `SpecificPiece` elements.  If `False`, return a list of all SpecificPiece` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `SpecificPiece` children
        '''
        return self.getSpecificElements(PieceSlot,'entryName',
                                        *names,asdict=asdict)
    def getMap(self,asdict=False):
        return self.getElementsByKey(Map,'mapName',asdict)
    def getWidgetMaps(self,asdict=True):
        '''Get all WidgetMap element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `WidgetMap` elements.  If `False`, return a list of all WidgetMap` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `WidgetMap` children
        '''
        return self.getElementsByKey(WidgetMap,'mapName',asdict=asdict)
    def getMaps(self,asdict=True):
        '''Get all Map element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Map` elements.  If `False`, return a list of all Map` children.
        Returns
        -------
        children : dict or list
            Dictionary or list of `Map` children
        '''
        maps = self.getMap(asdict=asdict)
        wmaps = self.getWidgetMaps(asdict=asdict)
        if asdict:
            maps.update(wmaps)
        else:
            maps.extend(wmaps)
        return maps
    def getBoards(self,asdict=True):
        '''Get all Board element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Board`
            elements.  If `False`, return a list of all Board`
            children.

        Returns
        -------
        children : dict or list
            Dictionary or list of `Board` children

        '''
        return self.getElementsByKey(Board,'name',asdict)
    def getGameMassKeys(self,asdict=True):
        '''Get all GameMassKey element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Board`
            elements.  If `False`, return a list of all Board`
            children.

        Returns
        -------
        children : dict or list
            Dictionary or list of `Board` children

        '''
        return self.getElementsByKey(GameMassKey,'name',asdict)
    def getStartupMassKeys(self,asdict=True):
        '''Get all StartupMassKey element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Board`
            elements.  If `False`, return a list of all Board`
            children.

        Returns
        -------
        children : dict or list
            Dictionary or list of `Board` children

        '''
        return self.getElementsByKey(StartupMassKey,'name',asdict)
    def getMenus(self,asdict=True):
        '''Get all Menu element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `Board`
            elements.  If `False`, return a list of all Board`
            children.

        Returns
        -------
        children : dict or list
            Dictionary or list of `Board` children

        '''
        return self.getElementsByKey(Menu,'text',asdict)
    def getSymbolicDices(self,asdict=True):
        '''Get all Menu element(s) from this

        Parameters
        ----------
        asdict : bool
            If `True`, return a dictonary that maps key to `SymbolicDice`
            elements.  If `False`, return a list of all `SymbolicDice`
            children.

        Returns
        -------
        children : dict or list
            Dictionary or list of `SymbolicDice` children

        '''
        return self.getElementsByKey(SymbolicDice,'name',asdict)
    def getAtStarts(self,single=False):
        '''Get all or a sole `AtStart` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `AtStart` child, otherwise fail.
            If `False` return all `AtStart` children in this element
        Returns
        -------
        children : list
            List of `AtStart` children (even if `single=True`)
        '''
        return self.getAllElements(AtStart,single)

registerElement(Game)

# --------------------------------------------------------------------
class BasicCommandEncoder(GameElement):
    TAG = Element.MODULE+'BasicCommandEncoder'
    def __init__(self,doc,node=None):
        super(BasicCommandEncoder,self).__init__(doc,self.TAG,node=node)

registerElement(BasicCommandEncoder)

#
# EOF
#
# ====================================================================
# From buildfile.py

# --------------------------------------------------------------------
class BuildFile(Element):
    def __init__(self,root=None):
        '''Construct from a DOM object, if given, otherwise make new'''
        from xml.dom.minidom import Document
        super(BuildFile,self).__init__(None,'',None)
        
        self._root = root
        if self._root is None:
            self._root = Document()

        self._node = self._root

    def addGame(self,**kwargs):
        '''Add a `Game` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Game
            The added element
        '''
        return Game(self,**kwargs)

    def getGame(self):
        '''Get the `Game`''' 
        return Game(self,
                    node=self._root.\
                    getElementsByTagName('VASSAL.build.GameModule')[0])

    def encode(self):
        '''Encode into XML'''
        return self._root.toprettyxml(indent=' ',
                                      encoding="UTF-8",
                                      standalone=False)


#
# EOF
#
# ====================================================================
# From moduledata.py

# --------------------------------------------------------------------
class Data(Element):
    TAG = 'data'
    def __init__(self,doc,node=None,version='1'):
        super(Data,self).__init__(doc,self.TAG,node=node,version=version)
        
    def addVersion(self,**kwargs):
        '''Add a `Version` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Version
            The added element
        '''
        return self.add(Version,**kwargs)
    def addVASSALVersion(self,**kwargs):
        '''Add a `VASSALVersion` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : VASSALVersion
            The added element
        '''
        return self.add(VASSALVersion,**kwargs)
    def addName(self,**kwargs):
        '''Add a `Name` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Name
            The added element
        '''
        return self.add(Name,**kwargs)
    def addDescription(self,**kwargs):
        '''Add a `Description` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Description
            The added element
        '''
        return self.add(Description,**kwargs)
    def addDateSaved(self,**kwargs):
        '''Add a `DateSaved` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : DateSaved
            The added element
        '''
        return self.add(DateSaved,**kwargs)
    def getVersion(self,single=True):
        '''Get all or a sole `Version` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `Version` child, otherwise fail.
            If `False` return all `Version` children in this element
        Returns
        -------
        children : list
            List of `Version` children (even if `single=True`)
        '''
        return self.getAllElements(Version,single=single)
    def getVASSALVersion(self,single=True):
        '''Get all or a sole `VASSALVersion` element(s) from this

        Parameters
        ----------
        single : bool        
            If `True`, there can be only one `VASSALVersion` child,
            otherwise fail.  If `False` return all `VASSALVersion`
            children in this element
        Returns
        -------
        children : list
            List of `VASSALVersion` children (even if `single=True`)

        '''
        return self.getAllElements(VASSALVersion,single=single)
    def getName(self,single=True):
        return self.getAllElements(Name,single=single)
    def getDescription(self,single=True):
        '''Get all or a sole `Description` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `Description` child,
            otherwise fail.  If `False` return all `De` children in
            this element
        Returns
        -------
        children : list
            List of `De` children (even if `single=True`)

        '''
        return self.getAllElements(Description,single=single)
    def getDateSaved(self,single=True):
        '''Get all or a sole `DateSaved` element(s) from this

        Parameters
        ----------
        single : bool
            If `True`, there can be only one `DateSaved` child, otherwise fail.
            If `False` return all `DateSaved` children in this element
        Returns
        -------
        children : list
            List of `DateSaved` children (even if `single=True`)
        '''
        return self.getAllElements(DateSaved,single=single)
    
# --------------------------------------------------------------------
class DataElement(Element):
    def __init__(self,data,tag,node=None,**kwargs):
        super(DataElement,self).__init__(data,tag,node=node,**kwargs)

    def getData(self):
        return self.getParent(Data)

# --------------------------------------------------------------------
class Version(DataElement):
    TAG = 'version'
    def __init__(self,data,node=None,version=''):
        super(Version,self).__init__(data,self.TAG,node=node)
        if node is None:
            self.addText(version)

# --------------------------------------------------------------------
class VASSALVersion(DataElement):
    TAG = 'VassalVersion'
    def __init__(self,data,node=None,version='3.6.7'):
        super(VASSALVersion,self).__init__(data,self.TAG,node=node)
        if node is None:
            self.addText(version)

# --------------------------------------------------------------------
class Name(DataElement):
    TAG = 'name'
    def __init__(self,data,node=None,name=''):
        super(Name,self).__init__(data,self.TAG,node=node)
        if node is None:
            self.addText(name)
            
# --------------------------------------------------------------------
class Description(DataElement):
    TAG = 'description'
    def __init__(self,data,node=None,description=''):
        super(Description,self).__init__(data,self.TAG,node=node)
        if node is None:
            self.addText(description)

# --------------------------------------------------------------------
class DateSaved(DataElement):
    TAG = 'dateSaved'
    def __init__(self,data,node=None,milisecondsSinceEpoch=-1):
        super(DateSaved,self).__init__(data,self.TAG,node=node)
        if node is None:
            from time import time
            s = f'{int(time()*1000)}' if milisecondsSinceEpoch < 0 else \
                str(milisecondsSinceEpoch)
            self.addText(s)
            
# --------------------------------------------------------------------
class ModuleData(Element):

    def __init__(self,root=None):
        '''Construct from a DOM object, if given, otherwise make new'''
        from xml.dom.minidom import Document
        super(ModuleData,self).__init__(None,'',None)
        
        self._root = root
        if self._root is None:
            self._root = Document()

        self._node = self._root

    def addData(self,**kwargs):
        '''Add a `Data` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Data
            The added element
        '''
        return Data(self,**kwargs)

    def getData(self):
        return Data(self,
                    node=self._root.getElementsByTagName(Data.TAG)[0])

    def encode(self):
        return self._root.toprettyxml(indent=' ',
                                      encoding="UTF-8",
                                      standalone=False)

#
# EOF
#
# ====================================================================
# From save.py

# ====================================================================
class SaveIO:
    '''Wrapper around a save file 
    
    Save file is
    
        "!VCSK" KEY content
    
    Key is two bytes drawn as a random number in 0-255.  Content is
    two bytes per character.  Content characters are encoded with the
    random key.
    
    Save file (.vsav) content is
    
        "begin_save" ESC
        "\" ESC
        [commands]* ESC
        "PLAYER" name password side ESC
        [map+"BoardPicker" name x y ESC]+
        "SETUP_STACK" ESC
        "TURN"+name state ESC
        "end_save"
    
    Commands are
    
        "+/" id "/" body "\"
    
    where body are
    
        "stack" "/" mapName ; x ; y ; ids "\"
        piece_type "/" piece_state   (x and y set here too) "\"
    
    x and y are pixel coordinates (sigh!).  This means we have to know
    
    - the pixel location of a hex
    - the hex coordinates of that hex
    - whether rows and columns are descending
    - if even hexes are higher or not
    
    The two first items _must_ be user supplied (I think).  When we
    add stacks or pieces, we must then get the numerical hex
    coordinates - not what the user specified in the VASSAL editor or
    the like.  Of course, this means opening the module before writing
    the patch.py script.
    
    It seems like every piece is added in a stack.
    
    The id is a numerical value.  Rather big (e.g., 1268518000806). It
    is the current number of miliseconds since epoch, with offset to
    disambiguate.
    
    The ID is the current time, taken from a milisecond clock,
    possibly adjusted up if there is a clash.  This is all managed by
    the GameState class.

    '''
    VCS_HEADER = b'!VCSK'
    VK_ESC     = chr(27)
    DEC_MAP    = {
        # 0-9
        0x30: 0x30,
        0x31: 0x30,
        0x32: 0x30,
        0x33: 0x30,
        0x34: 0x30,
        0x35: 0x30,
        0x36: 0x30,
        0x37: 0x30,
        0x38: 0x30,
        0x39: 0x30,
        # A-F
        0x41: 0x37,
        0x42: 0x37,
        0x43: 0x37,
        0x44: 0x37,
        0x45: 0x37,
        0x46: 0x37,
        # a-f
        0x61: 0x57,
        0x62: 0x57,
        0x63: 0x57,
        0x64: 0x57,
        0x65: 0x57,
        0x66: 0x57
    }
    ENC_MAP = [b'0',b'1',b'2',b'3',b'4',b'5',b'6',b'7',b'8',b'9',
               b'a',b'b',b'c',b'd',b'e',b'f']

    # ----------------------------------------------------------------
    @classmethod
    def decHex(cls,b):
        '''Decode a single char into a number

        If the encoded number is b, then the decoded number is
        
            b - off
    
        where off is an offset that depends on b
    
           off = 0x30   if 0x30 <= b <= 0x39
                 0x37   if 0x41 <= b <= 0x46
                 0x57   if 0x61 <= b <= 0x66
        '''
        return b - cls.DEC_MAP[b]
    # --------------------------------------------------------------------
    @classmethod
    def readByte(cls,inp,key):
        '''Read a single byte of information from input stream
    
        Two characters (c1 and c2) are read from input stream, and the
        decoded byte is then
    
            ((dechex(c1) << 4 | dechex(c2)) ^ key) & 0xFF
        
        Parameters
        ----------
        inp : stream
            Input to read from
        key : int
            Key to decode the input
    
        Returns
        -------
        b : int
            The read byte
        '''
        try:
            pair = inp.read(2)
        except Exception as e:
            from sys import stderr
            print(e,file=stderr)
            return None
    
        if len(pair) < 2:
            return None
    
        return ((cls.decHex(pair[0]) << 4 | cls.decHex(pair[1])) ^ key) & 0xFF
    # --------------------------------------------------------------------
    @classmethod
    def readSave(cls,file,alsometa=False):
        '''Read data from save file.  The data is read into lines
        returned as a list.

        '''
        from zipfile import ZipFile
        
        # We open the save file as a zip file 
        with ZipFile(file,'r') as z:
            # open the save file in the archive
            save = z.open('savedGame','r')
            
            # First, we check the header
            head = save.read(len(cls.VCS_HEADER))
            assert head == cls.VCS_HEADER, \
                f'Read header {head} is not {cls.VCS_HEADER}'
    
            # Then, read the key
            pair = save.read(2)
            key  = (cls.decHex(pair[0]) << 4 | cls.decHex(pair[1]))
    
            # Now read content, one byte at a time 
            content = ''
            while True:
                byte = cls.readByte(save,key)
                if byte is None:
                    break
    
                # Convert byte to character 
                content += chr(byte)
    
            lines = content.split(cls.VK_ESC)

            if alsometa:
                savedata = z.read(VSav.SAVE_DATA)
                moduledata = z.read(VMod.MODULE_DATA)

        if not alsometa:
            return key, lines

        return key,lines,savedata,moduledata

    # --------------------------------------------------------------------
    @classmethod
    def writeByte(cls,out,byte,key):
        '''Write a single byte

        Parameters
        ----------
        out : IOStream
            Stream to write to
        byte : char
            Single byte to write
        key : int
            Key to encode with (defaults to 0xAA - alternating 0's and 1's)
        '''
        b    = ord(byte) ^ key
        pair = cls.ENC_MAP[(b & 0xF0) >> 4], cls.ENC_MAP[b & 0x0F]
        out.write(pair[0])
        out.write(pair[1])

    # --------------------------------------------------------------------
    @classmethod
    def writeInZip(cls,z,key,lines,filename='savedGame'):
        '''Write a save file in a zip file (VMod)'''
        # open the save file in the archive
        with z.open(filename,'w') as save:
            # Write header
            save.write(cls.VCS_HEADER)
    
            # Split key
            pair = cls.ENC_MAP[(key & 0xF0) >> 4], cls.ENC_MAP[(key & 0x0F)]
            save.write(pair[0])
            save.write(pair[1])
    
            # Form content
            content = cls.VK_ESC.join(lines)
    
            # Write each character as two
            for c in content:
                cls.writeByte(save, c, key)
        
    # --------------------------------------------------------------------
    @classmethod
    def writeSave(cls,file,key,lines,savedata=None,moduledata=None):
        '''Write a save file'''
        from zipfile import ZipFile, ZIP_DEFLATED
        
        # We open the save file as a zip file 
        with ZipFile(file,'w',ZIP_DEFLATED) as z:
            cls.writeInZip(z,key,lines,filename='savedGame')

            if savedata is not None:
                z.writestr(VSav.SAVE_DATA,savedata)
                z.writestr(VMod.MODULE_DATA,moduledata)
        
# ====================================================================
#
# VSave file
#
class SaveFile:
    def __init__(self,game,firstid=None):
        '''Creates a save file to add positions to'''
        from time import time
        self._game     = game
        self._counters = {}
        self._stacks   = {}
        self._pieces   = self._game.getPieces(asdict=True)
        self._nextId   = (int(time()*1000) - 360000
                          if firstid is None else firstid)
        
    def add(self,grid,**kwargs):
        '''Add pieces to the save.

        Parameters
        ----------
        grid : BaseGrid
            Grid to add pieces to 
        kwargs : dict
            Either a map from piece name to hex position,
            Or a map from hex position to list of pieces
        '''
        for k,v in kwargs.items():
            # print('Add to save',k,v)
            self._add(grid,k,v)

    def _add(self,grid,k,v):
        '''Add to the save'''
        # print(f'Adding {k} -> {v}')
        loc       = None
        piece     = self._pieces.get(k,None)
        pieces    = []
        boardName = grid.getBoard()['name']
        # print(f'Board name: {boardName}')
        if piece is not None:
            # print(f'Key is piece: {k}->{piece}')
            pieces.append(piece)
            loc = v
        else:
            # Key is not a piece name, so a location
            loc = k
            # Convert value to iterable 
            try:
                iter(v)
            except:
                v = list(v)
            
            for vv in v:
                if isinstance(vv,PieceSlot):
                    pieces.append(vv)
                    continue
                if isinstance(vv,str):
                    piece = self._pieces.get(vv,None)
                    if piece is None:
                        continue
                    pieces.append(piece)

        # print(f'Loc: {loc}')
        if len(pieces) < 1:
            return
        
        if (boardName,loc) not in self._stacks:
            # print(f'Adding stack {boardName},{loc}')
            coord = grid.getLocation(loc)
            if coord is None:
                print(f'did not get coordinates from {loc}')
                return
            self._stacks[(boardName,loc)] = {
                'x': coord[0],
                'y': coord[1],
                'pids': [] }
                
        place = self._stacks[(boardName,loc)]
        for piece in pieces:
            name   = piece['entryName']
            count  = self._counters.get(name,None)
                            
            if count is None:
                count = {'pid':  self._nextId,
                         'piece': piece,
                         'board': boardName,
                         'x':     place['x'],
                         'y':     place['y'],
                         }
                self._counters[name] = count
                self._nextId += 1

                
            # print(f'Adding to stack {boardName},{loc}: {count[0]}')
            place['pids'].append(count['pid'])

    def getLines(self):
        '''Get the final lines of code'''
        key   = 0xAA # fixed key
        
        lines = ['begin_save',
                 '',
                 '\\']

        self._pieceLines(lines)
        self._otherLines(lines)
        
        lines.append('end_save')
        return lines

    def _pieceLines(self,lines):
        '''Add piece lines to save file

        Parameters
        ----------
        lines : list
            The lines to add
        '''
        # print(self._counters)
        for counter in self._counters.values():
            iden   = counter['pid']
            piece  = counter['piece']
            traits = piece.getTraits()
            traits = Trait.flatten(traits,self._game)
            # Get last - trait (basic piece), and modify coords
            basic  = traits[-1]
            basic['map'] = counter['board']
            basic['x']   = counter['x']
            basic['y']   = counter['y']
            # Set old location if possible
            parent = piece.getParent(DummyElement,checkTag=False)
            if parent is not None and parent._node.nodeName == AtStart.TAG:
                oldLoc   = parent['location']
                oldBoard = parent['owningBoard']
                oldMap   = self._game.getBoards()[oldBoard].getMap()['mapName']
                oldX     = parent['x']
                oldY     = parent['y']
                oldZone  = None
                zones    = self._game.getBoards()[oldBoard].getZones()
                for zone in zones.values():
                    grid = zone.getGrids()[0]
                    if grid is None: continue
                    
                    coord = grid.getLocation(oldLoc)
                    if coord is None: continue

                    oldZone = zone['name']
                    oldX    = coord[0]
                    oldY    = coord[1]
                    break

                if oldZone is not None:
                    basic['properties'] = \
                        f'6;OldZone;{oldZone};OldLocationName;{oldLoc};'+\
                        f'OldX;{oldX};OldY;{oldY};'+\
                        f'OldBoard;{oldBoard};OldMap;{oldMap}'
                else:
                    basic['properties'] = \
                        f'5;OldLocationName;{oldLoc};'+\
                        f'OldX;{oldX};OldY;{oldY};'+\
                        f'6;OldBoard;{oldBoard};OldMap;{oldMap}'

                for trait in traits:
                    if trait.ID == TrailTrait.ID:
                        trait['map']    = oldMap
                        trait['points'] = f'1;{oldX},{oldY}'
                        trait['init']   = True
                    
            # Wrapper 
            wrap   = DummyWithTraits(self._game,traits=[])
            wrap.setTraits(*traits,iden=str(iden))
            lines.append(wrap._node.childNodes[0].nodeValue+'\\')

        layer = -1
        for key,dat in self._stacks.items():
            pids = dat.get('pids',None)
            x    = dat['x']
            y    = dat['y']
            if pids is None or len(pids) < 1:
                print(f'No pieces at {key[0]},{key[1]}')
                continue
            
            iden         =  self._nextId
            self._nextId += 1
            stack        =  StackTrait(board=key[0],x=x,y=y,pieceIds=pids,layer=layer)
            layer        = 1
            wrap         =  DummyWithTraits(self._game,traits=[])
            wrap.setTraits(stack,iden=iden)
            lines.append(wrap._node.childNodes[0].nodeValue+'\\')
            
    def _otherLines(self,lines):
        '''Add other lines to save'''
        lines.append('UNMASK\tnull')
        for r in self._game.getPlayerRoster():
            lines.extend(r.encode())
        for n in self._game.getNotes(single=False):
            lines.extend(n.encode())
        setupStack = False
        for m in self._game.getMaps(asdict=False):
            for bp in m.getBoardPicker(single=False):
                lines.extend(bp.encode())
            if not setupStack:
                atstart = m.getAtStarts(single=False)
                if atstart and len(atstart) > 0:
                    lines.append('SETUP_STACK')
                    setupStack = True
                
        # for tk,tt in self._game.getTurnTracks(asdict=True):
        #     lines.extend(tt.encode())

            
# --------------------------------------------------------------------
class SaveData(ModuleData):
    def __init__(self,root=None):
        '''Convinience wrapper'''
        super(SaveData,self).__init__(root=root)
# ====================================================================
# From vsav.py

# --------------------------------------------------------------------
class VSav:
    SAVE_DATA = 'savedata'
    
    def __init__(self,build,vmod):
        '''Create a VASSAL save file programmatically

        Parameters
        ----------
        build : xml.dom.Document
            `buildFile.xml` as XML
        vmod : VMod
            Module file
        '''
        from time import time 
        self._vmod  = vmod
        self._game  = build.getGame()
        self._start = int(time()*1000)
        

    def createSaveData(self,description=None):
        '''Create `savedgame`'''
        desc           = (self._game['description']
                          if description is None else description)
        self._saveData = SaveData(root=None)
        data           = self._saveData.addData()
        data.addVersion      (version    =self._game['version'])
        data.addVASSALVersion(version    =self._game['VassalVersion'])
        data.addDescription  (description=desc)
        data.addDateSaved    (milisecondsSinceEpoch=self._start)
        return self._saveData

    def createModuleData(self):
        '''Create `moduleData`'''
        self._moduleData = ModuleData()
        data = self._moduleData.addData()
        data.addVersion      (version    =self._game['version'])
        data.addVASSALVersion(version    =self._game['VassalVersion'])
        data.addName         (name       =self._game['name'])
        data.addDescription  (description=self._game['description'])
        data.addDateSaved    (milisecondsSinceEpoch=self._start)
        return self._moduleData
        
    def addSaveFile(self):
        '''Add a save file to the module

        Returns
        -------
        vsav : SaveFile
            Save file to add content to        
        '''
        self._saveFile = SaveFile(game=self._game,firstid=self._start)
        return self._saveFile

    def run(self,savename='Save.vsav',description=None):
        '''Run this to generate the save file

        Parameters
        ----------
        savename : str
            Name of save file to write
        description : str
            Short description of the save file
        '''
        from zipfile import ZipFile, ZIP_DEFLATED
        
        self.createSaveData(description=description)
        self.createModuleData()
        
        with self._vmod.getInternalFile(savename,'w') as vsav:
            with ZipFile(vsav,'w',ZIP_DEFLATED) as zvsav:
                # The key is set to 0xAA (alternating ones and zeros)
                SaveIO.writeInZip(zvsav,0xAA,self._saveFile.getLines())
            
                zvsav.writestr(VMod.MODULE_DATA, self._moduleData.encode())
                zvsav.writestr(VSav.SAVE_DATA,   self._saveData.encode())
            
#
# EOF
#
# ====================================================================
# From vmod.py
# ====================================================================
#
# Wrapper around a module 
#
class VMod:
    BUILD_FILE = 'buildFile.xml'
    MODULE_DATA = 'moduledata'
    
    def __init__(self,filename,mode):
        '''Interface to VASSAL Module (a Zip file)'''
        self._mode = mode
        self._vmod = self._open(filename,mode)

    def __enter__(self):
        '''Enter context'''
        return self

    def __exit__(self,*e):
        '''Exit context'''
        self._vmod.close()
        return None

    def _open(self,filename,mode):
        '''Open a file in VMod'''
        from zipfile import ZipFile, ZIP_DEFLATED

        return ZipFile(filename,mode,compression=ZIP_DEFLATED)
        
    def removeFiles(self,*filenames):
        '''Open a temporary zip file, and copy content from there to
        that file, excluding filenames mentioned in the arguments.
        Then close current file, rename the temporary file to this,
        and reopen in 'append' mode.  The deleted files are returned
        as a dictionary.

        Parameters
        ----------
        filenames : tuple
            List of files to remove from the VMOD

        Returns
        -------
        files : dict
            Dictionary from filename to content of the removed files.

        Note, the VMOD is re-opened in append mode after this
        '''
        from tempfile import mkdtemp
        from zipfile import ZipFile
        from shutil import move, rmtree 
        from os import path

        tempdir = mkdtemp()
        ret     = {}

        try:
            tempname = path.join(tempdir, 'new.zip')
            with self._open(tempname, 'w') as tmp:

                for item in self._vmod.infolist():
                    data = self._vmod.read(item.filename)

                    if item.filename not in filenames:
                        tmp.writestr(item, data)
                    else:
                        ret[item.filename] = data

            name = self._vmod.filename
            self._vmod.close()
            move(tempname, name)

            self._mode = 'a'
            self._vmod = self._open(name,'a')
        finally:
            rmtree(tempdir)

        # Return the removed files 
        return ret

    def fileName(self):
        '''Get name of VMod file'''
        return self._vmod.filename
    
    def addFiles(self,**files):
        '''Add a set of files  to this

        Parameters
        ----------
        files : dict
            Dictionary that maps file name to content.
        '''
        for filename,data in files.items():
            self.addFile(filename,data)

    def addFile(self,filename,content):
        '''Add a file to this

        Parameters
        ----------
        filename : str
            File name in module
        content : str
            File content
        Returns
        -------
        element : File
            The added element
        '''
        self._vmod.writestr(filename,content)

    def addExternalFile(self,filename,target=None):
        '''Add an external file element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : ExternalFile
            The added element
        '''
        if target is None: target = filename
        self._vmod.write(filename,target)
        
    def getFileNames(self):
        '''Get all filenames in module'''
        return self._vmod.namelist()
    
    def getFiles(self,*filenames):
        '''Return named files as a dictionary.

        Parameters
        ----------
        filenames : tuple
            The files to get 
        Returns
        -------
        files : dict
            Mapping of file name to file content
        '''
        fn  = self.getFileNames()
        ret = {}
        for f in filenames:
            if f not in fn:
                continue

            ret[f] = self._vmod.read(f)

        return ret

    def getDOM(self,filename):
        '''Get content of a file decoded as XML DOM

        Parameters
        ----------
        filename : str
            Name of file in module 
        '''
        from xml.dom.minidom import parseString

        r = self.getFiles(filename)
        if filename not in r:
            raise RuntimeException(f'No {filename} found!')

        return parseString(r[filename])
        
    def getBuildFile(self):
        '''Get the buildFile.xml decoded as a DOM tree'''
        return self.getDOM(VMod.BUILD_FILE)

    def getModuleData(self):
        '''Get the moduledata decoded as a DOM tree'''
        return self.getDOM(VMod.MODULE_DATA)

    def getInternalFile(self,filename,mode):
        return self._vmod.open(filename,mode)

    def addVSav(self,build):
        '''Add a `VSav` element to this

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : VSav
            The added element
        '''
        return VSav(build=build,vmod=self)


#
# EOF
#
# ====================================================================
# From exporter.py

class Exporter:
    def __init__(self):
        '''Base class for exporters'''
        pass


    def setup(self):
        '''Should be defined to set-up for processing, for example
        generating images and such.  This is executed in a context
        where the VMod file has been opened for writing via
        `self._vmod`. Thus, files can be added to the module at this
        stage.
        '''         
        pass

    def createBuildFile(self,ignores='(.*markers?|all|commons|[ ]+)'):
        '''Should be defined to make the `buildFile.xml` document

        Parameters
        ----------
        ignores : str
            Regular expression to match ignored categories for factions
            determination. Python's re.fullmatch is applied to this
            regular exression against chit categories.  If the pattern
            is matched, then the chit is not considered to belong to a
            faction.

        '''
        pass

    def createModuleData(self):
        '''Should be defined to make the `moduledata` document'''
        pass
    
    def run(self,vmodname,patch=None):
        '''Run the exporter to generate the module
        '''
        with VMod(vmodname,'w') as vmod:
            self._vmod = vmod
            self.setup()
            self.createBuildFile() 
            self.createModuleData()
            self.runPatch(patch)
            self._vmod.addFiles(**{VMod.BUILD_FILE  :
                                   self._build.encode(),
                                   VMod.MODULE_DATA :
                                   self._moduleData.encode()})
        Verbose().message('Created VMOD')
        

    def runPatch(self,patch):
        '''Run user specified patch script.  The script should define

            ```
            def patch(buildFile,moduleData,vmod,verbose):
            ```

        where `buildFile` is the `buildFile.xml` and `moduleData` are
        the XML documents as `xml.dom.Document` objects, `vmod` is a
        `VMod` instance, and `verbose` is a `bool` selecting verbose
        mode or not.
        '''
        if patch is None or patch == '':
            return
        
        from importlib.util import spec_from_file_location, module_from_spec
        from pathlib import Path
        from sys import modules

        p = Path(patch)
        with VerboseGuard(f'Will patch module with {p.stem}.patch function') \
             as v:

            spec   = spec_from_file_location(p.stem, p.absolute())
            module = module_from_spec(spec)
            spec.loader.exec_module(module)
            modules[p.stem] = module
            
            # Patch must accept xml.dom.document,xml.dom.document,ZipFile
            module.patch(self._build,
                         self._moduleData,
                         self._vmod,
                         Verbose().verbose)
    
# ====================================================================
# From latexexporter.py

# ====================================================================
#
# Exporter class 
#
class LaTeXExporter(Exporter):
    def __init__(self,
                 vmodname      = 'Draft.vmod',
                 pdfname       = 'export.pdf',
                 infoname      = 'export.json',
                 title         = 'Draft',
                 version       = 'draft',
                 description   = '',     
                 rules         = None,
                 tutorial      = None,
                 patch         = None,
                 visible       = True,
                 vassalVersion = '3.6.7',
                 nonato        = False,
                 nochit        = False,
                 counterScale  = 1,
                 resolution    = 150):
        '''Exports a PDF and associated JSON files to a VASSAL module.

        Parameters
        ----------
        vmodname : str
            Name of module file to write
        pdfname : str
            Name of PDF file to read images from
        infoname : str
            Name of JSON file to read meta data from
        title : str
            Name of module
        version : str
            Version of midule
        description : str
            Short description of the module
        rules : str
            Optional name PDF file to attach as rules
        tutorial : str
            Optional name of a VASSAL log file to use as tutorial
        patch : str
            Optional name of Python script to post process the module
        visible : bool
            Make grids visible
        vassalVersion : str
            VASSAL version to encode this module for
        resolution : int
            Resolution for images (default 150)
        '''
        self._vmodname        = vmodname
        self._pdfname         = pdfname
        self._infoname        = infoname
        self._title           = title
        self._version         = version
        self._description     = description
        self._rules           = rules
        self._tutorial        = tutorial
        self._patch           = patch
        self._visible         = visible or version.lower() == 'draft'
        self._vassalVersion   = vassalVersion
        self._nonato          = nonato
        self._nochit          = nochit
        self._resolution      = resolution
        self._counterScale    = counterScale
        self._battleMark      = 'wgBattleMarker'
        self._battleMarks     = []
        self._oddsMarks       = []
        self._resultMarks     = []
        self._markBattle      = key(NONE,0)+',wgMarkBattle'
        self._clearBattle     = key(NONE,0)+',wgClearBattle'
        self._clearAllBattle  = key(NONE,0)+',wgClearAllBattle'
        self._zeroBattle      = key(NONE,0)+',wgZeroBattle'
        self._incrBattle      = key(NONE,0)+',wgIncrBattle'
        self._setBattle       = key(NONE,0)+',wgSetBattle'
        self._getBattle       = key(NONE,0)+',wgGetBattle'
        self._markOdds        = key(NONE,0)+',wgMarkOdds'
        self._markResult      = key(NONE,0)+',wgMarkResult'
        self._clearMoved      = key(NONE,0)+',wgClearMoved'
        self._zeroBattleAF    = key(NONE,0)+',wgZeroBattleAF'
        self._zeroBattleDF    = key(NONE,0)+',wgZeroBattleDF'
        self._zeroBattleFrac  = key(NONE,0)+',wgZeroBattleFrac'
        self._zeroBattleOdds  = key(NONE,0)+',wgZeroBattleOdds'
        self._zeroBattleShft  = key(NONE,0)+',wgZeroBattleShift'
        self._zeroBattleIdx   = key(NONE,0)+',wgZeroBattleIdx'
        self._calcBattleAF    = key(NONE,0)+',wgCalcBattleAF'
        self._calcBattleDF    = key(NONE,0)+',wgCalcBattleDF'
        self._calcBattleFrac  = key(NONE,0)+',wgCalcBattleFrac'
        self._calcBattleOdds  = key(NONE,0)+',wgCalcBattleOdds'
        self._calcBattleShft  = key(NONE,0)+',wgCalcBattleShift'
        self._calcBattleIdx   = key(NONE,0)+',wgCalcBattleIdx'
        self._calcBattleRes   = key(NONE,0)+',wgCalcBattleResult'
        self._clearBattlePhs  = key(NONE,0)+',wgClearBattlePhs'
        self._resolveBattle   = key(NONE,0)+',wgResolveBattle'
        self._rollDice        = key(NONE,0)+',wgRollDice'
        self._diceInitKey     = key(NONE,0)+',wgInitDice'
        self._clearKey        = key('C')
        self._clearAllKey     = key('C',CTRL_SHIFT)
        self._deleteKey       = key('D')
        self._eliminateKey    = key('E')
        self._flipKey         = key('F')
        self._trailKey        = key('M')
        self._restoreKey      = key('R') 
        self._markKey         = key('X')
        self._resolveKey      = key('Y')
        self._rotateCCWKey    = key('[')
        self._rotateCWKey     = key(']')
        self._chartsKey       = key('A',ALT)
        self._oobKey          = key('B',ALT)
        self._countersKey     = key('C',ALT)
        self._deadKey         = key('E',ALT)
        self._diceKey         = key('6',ALT)
        self._battleCounter   = 'wgBattleCounter'
        self._currentBattle   = 'wgCurrentBattle'
        self._currentAttacker = 'wgCurrentAttacker'
        self._battleNo        = 'wgBattleNo'
        self._battleAF        = 'wgBattleAF'
        self._battleDF        = 'wgBattleDF'
        self._battleFrac      = 'wgBattleFrac'
        self._battleIdx       = 'wgBattleIdx'
        self._battleOdds      = 'wgBattleOdds'
        self._battleOddsM     = 'wgBattleOddsMarker'
        self._battleShift     = 'wgBattleShift'
        self._battleCtrl      = 'wgBattleCtrl'
        self._battleCalc      = 'wgBattleCalc'
        self._battleUnit      = 'wgBattleUnit'
        self._battleResult    = 'wgBattleResult'
        self._autoOdds        = 'wgAutoOdds'
        self._autoResults     = 'wgAutoResults'
        self._noClearMoves    = 'wgNoClearMoves'
        self._noClearBattles  = 'wgNoClearBattles'
        self._debug           = 'wgDebug'
        self._verbose         = 'wgVerbose'
        self._hidden          = None
        self._hiddenName      = 'wg hidden unit'
        self._dice            = {}
        self._diceInit        = None
        
        with VerboseGuard('Overall settings') as v:
            v(f'Module file name:  {self._vmodname}')
            v(f'PDF file name:     {self._pdfname}')
            v(f'JSON file name:    {self._infoname}')
            v(f'Game title:        {self._title}')
            v(f'Game version:      {self._version}')
            v(f'Description:       {self._description}')
            v(f'Rules PDF file:    {self._rules}')
            v(f'Tutorial log:      {self._tutorial}')
            v(f'Patch scripts:     {self._patch}')
            v(f'Visible grids:     {self._visible}')
            v(f'Resolution:        {self._resolution}')
            v(f'Scale of counters: {self._counterScale}')
        
    def setup(self):
        # Start the processing 
        self._info       = self.convertPages()
        self._categories, \
            self._mains, \
            self._echelons, \
            self._commands = self.writeImages(self._counterScale)


    def run(self):
        super(LaTeXExporter,self).run(self._vmodname,self._patch)
        
            

    # ================================================================
    def createProcess(self,args):
        '''Spawn a process and pipe output here

        Parameters
        ----------
        args : list
            List of process command line elements
        Returns
        -------
        pipe : subprocess.Pipe
            Pipe to read from 
        '''
        from os import environ
        from subprocess import Popen, PIPE

        return Popen(args,env=environ.copy(),stdout=PIPE,stderr=PIPE)

    # ----------------------------------------------------------------
    def addPws(self,opw=None,upw=None):
        '''Add a `Pws` element to arguments

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        Returns
        -------
        element : Pws
            The added element
        '''
        '''Add password options'''
        args = []
        if upw is not None:  args.extend(['-upw',upw])
        if opw is not None:  args.extend(['-opw',opw])
        return args
            
    # ----------------------------------------------------------------
    def getPdfInfo(self,upw=None,opw=None,timeout=None):
        '''Get information about the PDF

        Parameters
        ----------
        opw : str
            Owner password (optional)
        upw : str
            User password (optional)
        timeout : int
            Time out in miliseconds for subprocesses

        Returns
        -------
        info : dict
             Image information 
        '''
        args = ['pdfinfo', self._pdfname ]
        args.extend(self.addPws(opw=opw,upw=upw))

        with VerboseGuard(f'Getting information from PDF {self._pdfname}'):
            proc = self.createProcess(args)
            try:
                out, err = proc.communicate(timeout=timeout)
            except:
                proc.kill()
                proc.communicate()
                raise RuntimeError(f'Failed to get PDF info: {e}')
            
            d = {}
            for field in out.decode('utf8','ignore').split('\n'):
                if field == '':
                    continue
                subfields  = field.split(':')
                key, value = subfields[0], ':'.join(subfields[1:])
                if key != '':
                    d[key] = (int(value.strip()) if key == 'Pages'
                              else value.strip())
            
            if 'Pages' not in d:
                raise ValueError(f'Page count not found from {self._pdfname}')

        return d

    # ----------------------------------------------------------------
    def getImagesInfo(self):
        '''Read in JSON information, and return as dictionary'''
        from json import load

        with VerboseGuard(f'Getting information from JSON {self._infoname}'):
            with open(self._infoname) as file:
                info = load(file)

        return info

    # ================================================================
    def convertPage(self,page,opw=None,upw=None,timeout=None):
        '''Convert a page from PDF into an image (bytes)

        Parameters
        ----------
        page : int
            Page number in the PDF to convert 
        opw : str
            Owner password (optional)
        upw : str
            User password (optional)
        timeout : int
            Time out in miliseconds for subprocesses

        Returns
        -------
        info : dict
             Image information 
        '''
        args = ['pdftocairo',
                '-transp',
                '-singlefile',
                '-r', str(self._resolution),
                '-f', str(page),
                '-l', str(page),
                '-png' ]
        args.extend(self.addPws(opw=opw,upw=upw))
        args.append(self._pdfname)
        args.append('-')

        proc = self.createProcess(args)

        try:
            out, err = proc.communicate(timeout=timeout)
        except Exception as e:
            proc.kill()
            proc.communicate()
            raise RuntimeError(f'Failed to convert page {page} of '
                               f'{self._pdfname}: {e}')

        # Just return the bytes
        return out
        
        
    # ----------------------------------------------------------------
    def ignoreEntry(self,info,ignores=['<<dummy>>','<<eol>>']):
        '''Check if we should ignore an entry in the JSON file'''
        return info['category'] in ignores

    # ----------------------------------------------------------------
    def scaleImage(self,buffer,factor):
        from PIL import Image
        from io import BytesIO
        from math import isclose

        if isclose(factor,1): return buffer

        # print(f'Scaling image by factor {factor}')
        with Image.open(BytesIO(buffer)) as img:
            w, h = img.width, img.height
            cpy  = img.resize((int(factor*w),int(factor*h)))

            with BytesIO() as out:
                cpy.save(out,format='PNG')
                return out.getvalue()

        
    # ----------------------------------------------------------------
    def convertPages(self,opw=None,upw=None,timeout=None):
        '''Reads in JSON and pages from PDF and stores information
        dictionary, which is returned

        Parameters
        ----------
        opw : str
            Owner password (optional)
        upw : str
            User password (optional)
        timeout : int
            Time out in miliseconds for subprocesses

        Returns
        -------
        info : dict
             Image information 
        '''
        oargs    = {'opw':opw,'upw':upw }
        docinfo  = self.getPdfInfo()
        imgsinfo = self.getImagesInfo()

        if len(imgsinfo) - 1 != docinfo['Pages']:
            raise RuntimeError(f'Number of pages in {self._pdfname} '
                               f'{docinfo["Pages"]} not matched in JSON '
                               f'{self._infoname} -> {len(imgsinfo)}')

        with VerboseGuard(f'Converting {docinfo["Pages"]} '
                          f'pages in {self._pdfname}') as v:
            for i,info in enumerate(imgsinfo):
                if self.ignoreEntry(info): continue

                if i == 0: v(end='')
                v(f'[{info["number"]}]',end=' ',flush=True)
                info['img'] = self.convertPage(info['number'],**oargs)

            v('done')

        return imgsinfo

    # ----------------------------------------------------------------
    def getBB(self,buffer):
        '''Get bounding box of image
    
        Parameters
        ----------
        buffer : bytes
             The image bytes
    
        Returns
        -------
        ulx, uly, lrx, lry : tuple
             The coordinates of the bounding box 
        '''
        from PIL import Image
        from io import BytesIO
    
        with Image.open(BytesIO(buffer)) as img:
            bb  = img.getbbox()
    
        return bb

    # ----------------------------------------------------------------
    def getWH(self,buffer):
        '''Get bounding box of image
    
        Parameters
        ----------
        buffer : bytes
             The image bytes
    
        Returns
        -------
        ulx, uly, lrx, lry : tuple
             The coordinates of the bounding box 
        '''
        from PIL import Image
        from io import BytesIO
    
        with Image.open(BytesIO(buffer)) as img:
            w, h  = img.width, img.height
    
        return w,h
    
    # ----------------------------------------------------------------
    def getOutline(self,buffer):
        '''Get bounding box of image
    
        Parameters
        ----------
        buffer : bytes
             The image bytes
    
        Returns
        -------
        ulx, uly, lrx, lry : tuple
             The coordinates of the bounding box 
        '''
        from PIL import Image
        from io import BytesIO

        # print(buffer)
        with Image.open(BytesIO(buffer)) as img:
            bb  = img.getbbox()

            for r in range(bb[0],bb[2]):
                for c in range(bb[1],bb[3]):
                    pass #print(img.getpixel((c,r)))
    
        return None
    
            
    # ================================================================
    def writeImages(self,counterScale=1):
        '''From the information gathered about the images (including
        their bitmap representation, generate image files in the
        module

        '''
        categories = {}
        unittypes  = []
        echelons   = []
        commands   = []
        
        with VerboseGuard(f'Writing images in VMod '
                          f'{self._vmod.fileName()}',end=' ') as v:
            for info in self._info:
                if self.ignoreEntry(info): continue
            
                typ = info.get('category','counter')
                sub = info.get('subcategory','all')
            
                info['filename'] = info['name'].replace(' ','_') + '.png'
                imgfn            = 'images/'+info['filename']
                if imgfn not in self._vmod.getFileNames():
                    if typ == 'counter':
                        # print(f'Possibly scale file {imgfn}')
                        info['img'] = self.scaleImage(info['img'],
                                                      counterScale)
                    # self.message(f'Writing image {imgfn}')
                    self._vmod.addFile(imgfn,info['img'])
            
                if sub == '':
                    info['subcategory'] = 'all'
                    sub                 = 'all'
            
                # Add into catalogue 
                if typ not in categories:
                    v('')
                    v(f'Adding category "{typ}"')
                    v('',end=' ')
                    categories[typ] = {}
                cat = categories[typ]
            
                if sub not in cat:
                    v('')
                    v(f'Adding sub-category "{sub}"')
                    v('',end=' ')
                    cat[sub] = {}
                tgt = cat[sub]

                v(f'[{info["name"]}]',end=' ',flush=True,noindent=True)
                #self.message(f'Adding "{info["name"]}" to catalogue')
                tgt[info['name']] = info

                if self._nonato: continue
                
                # Get NATO App6c information, if any
                natoapp6c = info.get('natoapp6c',None)
                if natoapp6c is not None:
                    from re import sub
                    def clean(s):
                        return sub('.*=','',
                                   (sub(r'\[[^]]+\]','',s.strip())
                                    .replace('{','')
                                    .replace('}','')
                                    .replace('/',' '))).strip()
                    mains   = clean(natoapp6c.get('main',   ''))
                    lower   = clean(natoapp6c.get('lower',  ''))
                    upper   = clean(natoapp6c.get('upper',  ''))
                    echelon = clean(natoapp6c.get('echelon',''))
                    command = clean(natoapp6c.get('command',''))

                    
                    if mains is not None:
                        if len(lower) > 0: mains += ' '+lower
                        if len(upper) > 0: mains += ' '+upper
                        mains = sub(r'\[[^]]+\]','',mains)\
                            .replace('{','').replace('}','')#.split(',')
                        unittypes.append(mains.replace(',',' '))
                        unittypes.extend([s.strip().replace(',',' ')
                                          for s in mains.split(',')])
                        #if len(mains) > 1:
                        #    unittypes.append('+'.join(mains))
                        info['mains'] = mains
                        
                    if len(echelon) > 0:
                        echelons.append(echelon)
                        info['echelon'] = echelon
            
                    if len(command) > 0:
                        commands.append(command)
                        info['command'] = command
            
            
            # Finished loop over infos. Make unit types, echelons,
            # commands unique  
            v('done')

        return categories, set(unittypes), set(echelons), set(commands)

    # ================================================================
    def createModuleData(self):
        '''Create the `moduleData` file in the module
        '''
        with VerboseGuard(f'Creating module data'):
            self._moduleData = ModuleData()
            data = self._moduleData.addData()
            data.addVersion      (version=self._version)
            data.addVASSALVersion(version=self._vassalVersion)
            data.addName         (name=self._title)
            data.addDescription  (description=self._description)
            data.addDateSaved    ()
        
    # ================================================================
    def createBuildFile(self,
                        ignores = '(.*markers?|all|commons|.*hidden|[ ]+)'):
        '''Create the `buildFile.xml` file in the module.

        Parameters
        ----------
        ignores : str
            Regular expression to match ignored categories for factions
            determination. Python's re.fullmatch is applied to this
            regular exression against chit categories.  If the pattern
            is matched, then the chit is not considered to belong to a
            faction.

        '''
        from re import fullmatch, IGNORECASE
        with VerboseGuard(f'Creating build file') as v:
            self._build = BuildFile() # 'buildFile.xml')
            self._game  = self._build.addGame(name        = self._title,
                                              version     = self._version,
                                              description = self._description)
            doc = self.addDocumentation()
            self._game.addBasicCommandEncoder()
            
            # Extract the sides
            self._sides = [ k
                            for k in self._categories.get('counter',{}).keys()
                            if fullmatch(ignores, k, IGNORECASE) is None]
            v(f'Got sides: {", ".join(self._sides)}')
            
            v(f'Adding Global options')
            go = self._game.addGlobalOptions(
                autoReport         = GlobalOptions.PROMPT,
                centerOnMove       = GlobalOptions.PROMPT,
                nonOwnerUnmaskable = GlobalOptions.PROMPT,
                playerIdFormat     = '$playerName$')
            go.addOption(name='undoHotKey',value=key('Z'))
            go.addOption(name='undoIcon',  value='/images/Undo16.gif')
            # go.addOptoin(name='stepHotKey',value='')
            go.addBoolPreference(name    = self._verbose,
                                 default = True,
                                 desc    = 'Be verbose',
                                 tab     = self._title)
            go.addBoolPreference(name    = self._debug,
                                 default = False,
                                 desc    = 'Show debug chat messages',
                                 tab     = self._title)
            go.addBoolPreference(name    = self._autoOdds,
                                 default = False,
                                 desc    = 'Calculate Odds on battle declaration',
                                 tab     = self._title)
            go.addBoolPreference(name    = self._autoResults,
                                 default = False,
                                 desc    = 'Resolve battle results automatically',
                                 tab     = self._title)
            go.addBoolPreference(name    = self._noClearMoves,
                                 default = False,
                                 desc    = ('Do not remove moved markers '
                                            'on phase change'),
                                 tab     = self._title)
            go.addBoolPreference(name    = self._noClearBattles,
                                 default = False,
                                 desc    = ('Do not remove battle markers '
                                            'on phase change'),
                                 tab     = self._title)
                                            
            v(f'Adding player roster')
            roster = self._game.addPlayerRoster()
            for side in self._sides:
                roster.addSide(side)
            
            v(f'Adding global properties')
            glob = self._game.addGlobalProperties()
            glob.addProperty(name='TurnTracker.defaultDocked',
                             initialValue=True)

            self._battleMarks   = self._categories\
                                      .get('counter',{})\
                                      .get('BattleMarkers',[])
            if len(self._battleMarks) > 0:
                v(f'We have battle markers')

                glob.addProperty(name         = self._battleCounter,
                                 initialValue = 0,
                                 isNumeric    = True,
                                 min          = 0,
                                 max          = len(self._battleMarks),
                                 wrap         = True,
                                 description  = 'Counter of battles')
                glob.addProperty(name         = self._currentBattle,
                                 initialValue = 0,
                                 isNumeric    = True,
                                 min          = 0,
                                 max          = len(self._battleMarks),
                                 wrap         = True,
                                 description  = 'Current battle number')
                glob.addProperty(name         = self._currentAttacker,
                                 initialValue = 0,
                                 isNumeric    = True,
                                 min          = 0,
                                 max          = 1,
                                 wrap         = True,
                                 description  = 'Current unit is attacker')
                glob.addProperty(name         = self._battleAF,
                                 initialValue = 0,
                                 isNumeric    = True,
                                 description  = 'Current battle AF')
                glob.addProperty(name         = self._battleDF,
                                 initialValue = 0,
                                 isNumeric    = True,
                                 description  = 'Current battle DF')
                glob.addProperty(name         = self._battleFrac,
                                 initialValue = 0,
                                 isNumeric    = True,
                                 description  = 'Current battle fraction')
                glob.addProperty(name         = self._battleShift,
                                 initialValue = 0,
                                 isNumeric    = True,
                                 description  = 'Current battle odds shift')
                glob.addProperty(name         = self._battleOdds,
                                 initialValue = '',
                                 isNumeric    = False,
                                 description  = 'Current battle odds')
                glob.addProperty(name         = self._battleResult,
                                 initialValue = '',
                                 isNumeric    = False,
                                 description  = 'Current battle results')
                glob.addProperty(name         = self._battleIdx,
                                 initialValue = 0,
                                 isNumeric    = True,
                                 description  = 'Current battle odds index')
            
            self._oddsMarks   = self._categories\
                                      .get('counter',{})\
                                      .get('OddsMarkers',[])
            if len(self._oddsMarks) > 0:
                v(f'We have odds markers')
                
            self._resultMarks   = self._categories\
                                      .get('counter',{})\
                                      .get('ResultMarkers',[])
            if len(self._resultMarks) > 0:
                v(f'We have result markers')
              
            self.addNotes()
            v(f'Adding turn track')
            turns = self._game.addTurnTrack(name='Turn',
                                            counter={
                                                'property': 'Turn',
                                                'phases': {
                                                    'property': 'Phase',
                                                    'names': self._sides } })
            turns.addHotkey(hotkey = self._clearMoved+'Phase',
                            name   = 'Clear moved markers',
                            reportFormat = (f'{{{self._verbose}?('
                                            f'"`Clear all moved markers, "+'
                                            f'""):""}}'))
            if len(self._battleMarks) > 0:
                turns.addHotkey(hotkey = self._clearBattlePhs,
                                name   = 'Clear battle markers',
                                reportFormat = (f'{{{self._verbose}?('
                                                f'"`Clear all battle markers, "+'
                                                f'""):""}}'))

            self._dice   = self._categories\
                               .get('die-roll',{})
            if len(self._dice) > 0:
                v(f'We have symbolic dice')
                self._diceInit = []
                # from pprint import pprint 
                # pprint(self._dice,depth=3)
                for die, faces in self._dice.items():
                    ico  = self.getIcon(die+'-die-icon','')
                    # print(f'Die {die} icon="{ico}"')
                               
                    dmin = +100000
                    dmax = -100000
                    symb = self._game.addSymbolicDice(
                        name         = die+'Dice',
                        text         = die if ico == '' else '',
                        icon         = ico,
                        tooltip      = f'{die} die roll',
                        format       = (f'{{"{die} die roll: "+result1'
                                        # f'+" <img src=\'{die}-"+result1'
                                        # f'+".png\' width=24 height=24>"'
                                        f'}}'),
                        resultWindow = True,
                        windowX      = str(int(67 * self._resolution/150)),
                        windowY      = str(int(65 * self._resolution/150)));
                    sdie = symb.addDie(name = die);
                    for face, fdata in faces.items():
                        fn   = fdata['filename']
                        val  = int(fn.replace('.png','').replace(die+'-',''))
                        dmin = min(dmin,val)
                        dmax = min(dmax,val)
                        sdie.addFace(icon  = fn,
                                     text  = str(val),
                                     value = val);

                    self._diceInit.extend([
                        GlobalPropertyTrait(
                            ['',self._diceInitKey,
                             GlobalPropertyTrait.DIRECT,
                             f'{{{dmin}}}'],
                            name        = die+'Dice_result',
                            numeric     = True,
                            min         = dmin,
                            max         = dmax,
                            description = f'Initialize {die}Dice'),
                        ReportTrait(
                            self._diceInitKey,
                            report=(f'{{{self._debug}?("Initialize '
                                    f'{die}Dice_result to {dmin}"):""}}'))
                    ])
                    

                # Add start-up key
                self._game.addStartupMassKey(
                    name        = 'Initialise dice results',
                    hotkey      = self._diceInitKey,
                    target      = '',
                    filter      = f'{{BasicName=="{self._hiddenName}"}}',
                    whenToApply = StartupMassKey.EVERY_LAUNCH,
                    reportFormat=f'{{{self._debug}?("Init Dice results"):""}}')
                
                                
                                

                
            
            self.addKeybindings(doc)
            self.addCounters()
            self.addInventory()
            self.addBoards()
            self.addDeadMap()
            self.addOOBs()
            self.addCharts()
            self.addDie()

    # ----------------------------------------------------------------
    def addDocumentation(self):
        '''Add documentation to the module.  This includes rules,
        key-bindings, and about elements.
        '''
        with VerboseGuard('Adding documentation') as v:
            doc = self._game.addDocumentation()
            if self._rules is not None:
                self._vmod.addExternalFile(self._rules,'rules.pdf')
                doc.addBrowserPDFFile(title   = 'Show rules',
                                      pdfFile = 'rules.pdf')
            
            if self._tutorial is not None:
                self._vmod.addExternalFile(self._tutorial,'tutorial.vlog')
                doc.addTutorial(name            = 'Tutorial',
                                logfile         = 'tutorial.vlog',
                                launchOnStartup = True)
            
            
            fronts    = self._categories.get('front',{}).get('all',[])
            front     = list(fronts.values())[0] if len(fronts) > 0 else None
            if front is not None:
                v(f'Adding about page')
                doc.addAboutScreen(title=f'About {self._title}',
                                   fileName = front['filename'])

            return doc

    # ----------------------------------------------------------------
    def addKeybindings(self,doc):
        keys = [
            ['Alt-A',	'-',	'Show the charts panel'],
            ['Alt-B',	'-',	'Show the OOBs'],
            ['Alt-C',	'-',	'Show the counters panel'],
            ['Alt-E',	'-',	'Show the eliminated units'],
            ['Alt-I',	'-',	'Show/refresh inventory window'],
            ['Alt-M',	'-',	'Show map'],
            ['Alt-T',	'-',	'Increase turn track'],
            ['Alt-Shift-T', '-',	'Decrease turn track'],
            ['Alt-6',	'-',	'Roll the dice'],
            ['Ctrl-D',	'Board,Counter','Delete counters'],
            ['Ctrl-E',	'Board,Counter','Eliminate counters'],
            ['Ctrl-F',	'Board,Counter','Flip counters'],
            ['Ctrl-M',	'Board,Counter','Toggle "moved" markers'],
            ['Ctrl-O',	'Board',	'Hide/show counters'],
            ['Ctrl-R',	'Board,Counter','Restore unit'],
            ['Ctrl-T',	'Board,Counter','Toggle move trail'],
            ['Ctrl-Z',	'Board',        'Undo last move'],
            ['Ctrl-+',	'Board',	'Zoom in'],
            ['Ctrl--',	'Board',	'Zoom out'],
            ['Ctrl-=',	'Board',	'Select zoom'],
            ['Ctrl-Shift-O',	'Board','Show overview map'],
            ['&larr;,&rarr;,&uarr;&darr;','Board',
             'Scroll board left, right, up, down (slowly)'],
            ['PnUp,PnDn','Board',	     'Scroll board up/down (fast)'],
            ['Ctrl-PnUp,Ctrl-PnDn','Board',  'Scroll board left/right (fast)'],
            ['Mouse-scroll up/down',	'Board',  'Scroll board up//down'],
            ['Shift-Mouse-scroll up/down','Board','Scroll board right/leftown'],
            ['Ctrl-Mouse-scroll up/down','Board','Zoom board out/in'],
            ['Mouse-2',	'Board',	'Centre on mouse']]
        if self._battleMarks:
            for a,l in zip(['Ctrl-D','Ctrl-Shift-O', 'Ctrl-+', 'Ctrl-+'],
                           [['Ctrl-C',      'Counter', 'Clear battle'],
                            ['Ctrl-Shift-C','Board',   'Clear all battle'],
                            ['Ctrl-X',      'Board,Counter','Mark battle'],
                            ['Ctrl-Y',      'Board,Counter','Resolve battle'],
                            ]):
                ks   = [k[0] for k in keys]
                didx = ks.index(a)
                keys.insert(didx,l)
            
        self._vmod.addFile('help/keys.html',
                           Documentation.createKeyHelp(
                               keys,
                               title=self._title,
                               version=self._version))
        doc.addHelpFile(title='Key bindings',fileName='help/keys.html')
        
    # ----------------------------------------------------------------
    def addNatoPrototypes(self,prototypes):
        # Add unit categories as prototypes 
        for n,c in zip(['Type','Echelon','Command'],
                       [self._mains, self._echelons, self._commands]):
            sc = set([cc.strip() for cc in c])
            with VerboseGuard(f'Adding prototypes for "{n}"') as vv:
                for i,cc in enumerate(sc):
                    cc = cc.strip()
                    if len(cc) <= 0: continue
                    vv(f'[{cc}] ',end='',flush=True,noindent=True)
                    p = prototypes.addPrototype(name        = f'{cc} prototype',
                                                description = '',
                                                traits      = [MarkTrait(n,cc),
                                                               BasicTrait()])
                vv('')
        
    # ----------------------------------------------------------------
    def addBattleControlPrototype(self,prototypes):
        # Control of battles.
        #
        # This has traits to
        #
        # - Zero battle counter
        # - Increment battle counter
        # - Set current battle number
        # - Mark battle
        # - Calculate odds
        #
        # When wgMarkBattle is issued to this piece, then
        #
        # - Increment battle counter
        # - Set global current battle
        # - Trampoline to GCK markBattle
        #   - For all selected pieces, issue markBattle
        #     - All wgBattleUnit pieces then
        #       - Get current battle # and store
        #       - Add marker on top of it self
        # - Issue calculateOddsAuto
        #   - If auto odds on, go to calcOddsStart,
        #     - Trampoline to GCK calcOddsAuto
        #       - Which sends calcOddsStart to all markers
        #         - For each mark
        #           - Set current battle to current global
        #           - Trampoline calcOdds via GKC
        #             - Send calcBattleOdds to wgBattleCalc
        #               - Zero odds
        #               - Calculate fraction
        #                 - Zero fraction
        #                 - Calculate total AF
        #                   - Zero AF
        #                   - via trampoline to GKC
        #                 - Calculate total DF
        #                   - Zero DF
        #                   - via trampoline to GKC
        #                 - Real fraction calculation
        #                   - From calculate fraction 
        #                   - Access via calculate trait
        #               - Calculate shift
        #                 - Zero shift
        #                 - Trampoline to GKC
        #                 - Access via calculate trait
        #               - Calculate index
        #                 - Via calculated OddsIndex
        #               - Calculate odds real
        #                 - Via calculated Index to odds 
        #           - Do markOddsAuto which selects between odds
        #             - Do markOddsReal+OddsIndex
        #               - Set global battle #
        #               - Place marker
        #                 - Take global battle #
        #            - De-select all other marks to prevent
        #              further propagation
        #              
        if len(self._battleMarks) <= 0:
            return False

        n = len(self._battleMarks)
        # --- Battle counter control - reset and increment -----------
        traits = [
            GlobalPropertyTrait(
                ['',self._zeroBattle,GlobalPropertyTrait.DIRECT,'{0}'],
                ['',self._incrBattle,GlobalPropertyTrait.DIRECT,
                 f'{{({self._battleCounter}%{n})+1}}'],
                name        = self._battleCounter,
                numeric     = True,
                min         = 0,
                max         = n,
                wrap        = True,
                description = 'Zero battle counter',
            ),
            GlobalPropertyTrait(
                ['',self._setBattle,GlobalPropertyTrait.DIRECT,
                 f'{{{self._battleCounter}}}'],
                name        = self._currentBattle,
                numeric     = True,
                min         = 0,
                max         = n,
                wrap        = True,
                description = 'Zero battle counter',
            ),
            ReportTrait(self._zeroBattle,
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+": zero battle counter: "'
                                f'+{self._battleCounter}):""}}')),
            ReportTrait(self._incrBattle,
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+": '
                                f'increment battle counter: "'
                                f'+{self._battleCounter}):""}}')),
            ReportTrait(self._setBattle,
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+": set current battle: "+'
                                f'{self._battleCounter}+" -> "+'
                                f'{self._currentBattle}):""}}')),
            GlobalHotkeyTrait(name         = '',
                              key          = self._markBattle+'Trampoline',
                              globalHotkey = self._markBattle,
                              description  = 'Mark selected for battle'),
            ReportTrait(self._markBattle+'Trampoline',
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+": forward mark battle: "+'
                                f'{self._battleCounter}):""}}')),
            GlobalHotkeyTrait(name         = '',
                              key          = self._calcBattleOdds+'Start',
                              globalHotkey = self._calcBattleOdds+'Auto',
                              description  = 'Trampoline to global'),
            ReportTrait(self._calcBattleOdds+'Start',
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+": forward odds: "+'
                                f'{self._battleCounter}):""}}')),
            DeselectTrait(command    = '',
                          key        = self._calcBattleOdds+'Deselect',
                          deselect   = DeselectTrait.ALL),
            ReportTrait(self._calcBattleOdds+'Deselect',
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+": select only this: "+'
                                f'{self._battleCounter}):""}}')),
            TriggerTrait(command    = '',
                         key        = self._calcBattleOdds+'Auto',
                         actionKeys = [self._calcBattleOdds+'Start'],
                         property   = f'{{{self._autoOdds}==true}}'),
            ReportTrait(self._calcBattleOdds+'Auto',
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+": forward odds: "+'
                                f'{self._battleCounter}):""}}')),
            TriggerTrait(command    = '',
                         key        = self._markBattle,
                         actionKeys = [self._incrBattle,
                                       self._setBattle,
                                       self._markBattle+'Trampoline',
                                       self._calcBattleOdds+'Auto']),
            ReportTrait(self._markBattle,
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+": mark battle: "+'
                                f'{self._battleCounter}):""}}')),
            GlobalHotkeyTrait(name         = '',
                              key          = self._clearAllBattle+'Trampoline',
                              globalHotkey = self._clearAllBattle,
                              description  = 'Mark selected for battle'),
            TriggerTrait(command    = '',
                         key        = self._clearAllBattle,
                         actionKeys = [self._clearAllBattle+'Trampoline',
                                       self._zeroBattle]),
            ReportTrait(self._clearBattle,
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+": clear battle: "+'
                                f'{self._battleCounter}):""}}')),
            GlobalHotkeyTrait(name         = '',
                              key          = self._clearMoved+'Trampoline',
                              globalHotkey = self._clearMoved,
                              description  = 'Clear moved markers'),
            MarkTrait(name=self._battleCtrl,value=True),            
            BasicTrait()]
        prototypes.addPrototype(name        = self._battleCtrl,
                                description = '',
                                traits      = traits)
        return True

    # ----------------------------------------------------------------
    def addBattleCalculatePrototype(self,prototypes):
        # --- Batttle AF, DF, Odds -----------------------------------
        # This calculate odds derivation from stated odds. 
        calcIdx   = 0
        maxIdx    = len(self._oddsMarks)+1
        minIdx    = 0
        idx2Odds  = '""'
        calcFrac  = 1
        if len(self._oddsMarks) > 0:
            odds = [o.replace('odds marker','').strip() for
                    o in self._oddsMarks]
            ratios = all([':' in o for o in odds])
            if ratios: # All is ratios!
                def calc(s):
                    num, den = [float(x.strip()) for x in s.split(':')]
                    return num/den
                ratios = [[calc(s),s] for s in odds]
                ind    = [i[0] for i in sorted(enumerate(ratios),
                                               key=lambda x:x[1][0])]
                #print(f'=== Rations: {ratios}, Index: {ind[::-1]}')
                calcIdx  = ':'.join([f'{self._battleFrac}>={ratios[i][0]}?'
                                     f'({i+1})'
                                     for i in ind[::-1]]) + ':0'
                idx2Odds = ':'.join([f'OddsIndex=={i+1}?'
                                   f'"{ratios[i][1]}"'
                                   for i in ind[::-1]]) + ':""'
                calcFrac = (f'{{{self._battleDF}==0?0:'
                            f'(((double)({self._battleAF}))'
                            fr'\/{self._battleDF})}}')
                #print(calcIdx,idx2Odds)
            else:
                try:
                    nums     = [[int(o),o] for o in odds]
                    calcFrac = f'{{{self._battleAF}-{self._battleDF}}}'
                    ind      = [i[0] for i in sorted(enumerate(nums),
                                                     key=lambda x:x[1])]
                    calcIdx  = ':'.join([f'{self._battleFrac}>={nums[i][0]}?'
                                         f'({i+1})'
                                         for i in ind[::-1]])+':0'
                    idx2Odds = ':'.join([f'OddsIndex=={i+1}?"{nums[i][1]}"'
                                         for i in ind[::-1]]) + ':""'
                    vidx2Odds = '\t'+idx2Odds.replace(':',':\n\t')
                    #print(f'Index to odds: {vidx2Odds}')
                except:
                    pass 
                
        traits = [
            CalculatedTrait(# This should be changed to game rules
                name        = 'OddsShift',
                expression  = f'{{{self._battleShift}}}',
                description = 'Calculated internal oddsshift'),
            CalculatedTrait(# This should be changed to game rules
                name        = 'OddsIndexRaw',
                expression  = f'{{{calcIdx}}}',
                description = 'Calculated internal odds index'),
            CalculatedTrait(# This should be changed to game rules
                name        = 'OddsIndexLimited',
                expression  = (f'{{OddsIndexRaw>{maxIdx}?{maxIdx}:'
                               f'OddsIndexRaw<{minIdx}?{minIdx}:'
                               f'OddsIndexRaw}}'),
                description = 'Calculated internal limited odds index'),
            CalculatedTrait(# This should be changed to game rules
                name        = 'OddsIndex',
                expression  = (f'{{OddsIndexLimited+OddsShift}}'),
                description = 'Calculated internal odds index (with shift)'),
            CalculatedTrait(# This should be changed to game rules
                name        = 'BattleFraction',
                expression  = calcFrac,
                description = 'Calculated fraction off battle'),
            GlobalPropertyTrait(
                ['',self._zeroBattleShft,GlobalPropertyTrait.DIRECT,'{0}'],
                name        = self._battleShift,
                numeric     = True,
                description = 'Zero battle odds shift',
            ),
            GlobalPropertyTrait(
                ['',self._zeroBattleAF,GlobalPropertyTrait.DIRECT,'{0}'],
                name        = self._battleAF,
                numeric     = True,
                description = 'Zero battle AF',
            ),
            GlobalPropertyTrait(
                ['',self._zeroBattleDF,GlobalPropertyTrait.DIRECT,'{0}'],
                name        = self._battleDF,
                numeric     = True,
                description = 'Zero battle AF',
            ),
            # {wgBattleDF==0?0:(double(wgBattleAF)/wgBattleDF)}
            GlobalPropertyTrait(
                ['',self._zeroBattleFrac,GlobalPropertyTrait.DIRECT,'{0}'],
                ['',self._calcBattleFrac+'Real',GlobalPropertyTrait.DIRECT,
                 '{BattleFraction}'],
                name        = self._battleFrac,
                description = 'Calculate battle fraction',
            ),
            GlobalPropertyTrait(
                ['',self._zeroBattleIdx,GlobalPropertyTrait.DIRECT,'{0}'],
                ['',self._calcBattleIdx,GlobalPropertyTrait.DIRECT,
                 '{OddsIndex}'],
                name        = self._battleIdx,
                description = 'Calculate battle odds index',
            ),
            GlobalPropertyTrait(
                ['',self._zeroBattleOdds,GlobalPropertyTrait.DIRECT,'{""}'],
                ['',self._calcBattleOdds+'Real',GlobalPropertyTrait.DIRECT,
                 f'{{{idx2Odds}}}'],
                name        = self._battleOdds,
                description = 'Calculate battle odds',
            ),
            GlobalHotkeyTrait(name         = '',# Forward to units
                              key          = self._calcBattleAF+'Trampoline',
                              globalHotkey = self._calcBattleAF,
                              description  = 'Calculate total AF'),
            GlobalHotkeyTrait(name         = '',# Forward to units
                              key          = self._calcBattleDF+'Trampoline',
                              globalHotkey = self._calcBattleDF,
                              description  = 'Calculate total DF'),
            GlobalHotkeyTrait(name         = '',# Forward to units
                              key          = self._calcBattleShft+'Trampoline',
                              globalHotkey = self._calcBattleShft,
                              description  = 'Calculate total shift'),
            TriggerTrait(command        = '',
                         key            = self._calcBattleAF,
                         actionKeys     = [self._zeroBattleAF,
                                           self._calcBattleAF+'Trampoline']),
            TriggerTrait(command        = '',
                         key            = self._calcBattleDF,
                         actionKeys     = [self._zeroBattleDF,
                                           self._calcBattleDF+'Trampoline']),
            TriggerTrait(command        = '',
                         key            = self._calcBattleShft,
                         actionKeys     = [self._zeroBattleShft,
                                           self._calcBattleShft+'Trampoline']),
            TriggerTrait(command        = '',
                         key            = self._calcBattleFrac,
                         actionKeys     = [self._zeroBattleFrac,
                                           self._calcBattleAF,
                                           self._calcBattleDF,
                                           self._calcBattleFrac+'Real']),
            TriggerTrait(command        = '',
                         key            = self._calcBattleOdds,
                         actionKeys     = [self._zeroBattleOdds,
                                           self._calcBattleFrac,
                                           self._calcBattleShft,
                                           self._calcBattleIdx,
                                           self._calcBattleOdds+'Real']),
            ReportTrait(self._zeroBattleAF,
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+": Reset AF: "+'
                                f'{self._battleAF}):""}}')),
            ReportTrait(self._zeroBattleDF,
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+": Reset DF: "+'
                                f'{self._battleDF}):""}}')),
            ReportTrait(self._zeroBattleFrac,
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+": Reset fraction: "+'
                                f'{self._battleFrac}):""}}')),
            ReportTrait(self._zeroBattleOdds,
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+": Reset odds: "+'
                                f'{self._battleOdds}):""}}')),
            ReportTrait(self._calcBattleAF,
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+": Total AF: "+'
                                f'{self._battleAF}):""}}')),
            ReportTrait(self._calcBattleDF,
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+": Total DF: "+'
                                f'{self._battleDF}):""}}')),
            ReportTrait(self._calcBattleShft,
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+": Battle odds shift: "+'
                                f'{self._battleShift}):""}}')),
            ReportTrait(self._calcBattleFrac,
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+": Battle fraction: "+'
                                f'{self._battleFrac}):""}}')),
            ReportTrait(self._calcBattleOdds,
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+": Battle odds: "+'
                                f'{self._battleOdds}+" ("+'
                                f'{self._battleIdx}+")"):""}}')),
            ReportTrait(self._calcBattleFrac+'Real',
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+": Battle fraction: "+'
                                f'{self._battleFrac}+'
                                f'" AF="+{self._battleAF}+'
                                f'" DF="+{self._battleDF}'
                                f'):""}}')),
            ReportTrait(self._calcBattleOdds+'Real',
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+": Battle odds: "+'
                                f'{self._battleOdds}+'
                                f'" ("+{self._battleIdx}+","+OddsShift+","+'
                                f'" raw="+OddsIndexRaw+","+'
                                f'" limited="+OddsIndexLimited+","+'
                                f'" -> "+OddsIndex+","+'
                                f'{self._battleShift}+")"+'
                                f'" Fraction="+{self._battleFrac}+'
                                f'" AF="+{self._battleAF}+'
                                f'" DF="+{self._battleDF}'
                                f'):""}}')),
            ReportTrait(self._calcBattleOdds+'Real',
                        report=(f'{{{self._verbose}?'
                                f'("! Battle # "'
                                f'+{self._battleNo}'
                                f'+" AF="+{self._battleAF}'
                                f'+" DF="+{self._battleDF}'
                                f'+" => "+{self._battleOdds}'
                                # f'+" <img src=\'odds_marker_"'
                                # f'+{self._battleOdds}+".png\' "'
                                # f'+" width=24 height=24>"'
                                f'):""}}')),
            MarkTrait(name=self._battleCalc,value=True),            
            BasicTrait()]
        prototypes.addPrototype(name        = self._battleCalc,
                                description = '',
                                traits      = traits)
        
    # ----------------------------------------------------------------
    def addBattleUnitPrototype(self,prototypes):
        # --- Battle units that set battle markers -------------------
        # 
        # - Trait to add battle number 1 to max
        #
        # - Trigger trait for each of these using the global property
        #   for the current battle
        #
        traits = [
            DynamicPropertyTrait(['',self._getBattle,
                                  DynamicPropertyTrait.DIRECT,
                                  f'{{{self._currentBattle}}}'],
                                 ['',self._clearBattle,
                                  DynamicPropertyTrait.DIRECT,
                                  f'{{-1}}'],
                                 name        = self._battleNo,
                                 numeric     = True,
                                 value       = f'{{-1}}',
                                 description = 'Set battle number'),
            GlobalPropertyTrait(['',self._setBattle, GlobalPropertyTrait.DIRECT,
                                 '{IsAttacker}'],
                                name        = self._currentAttacker,
                                numeric     = True,
                                description = 'Set attacker'),
            ReportTrait(self._getBattle,
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+" current battle # "+'
                                f'{self._currentBattle}+" -> "+'
                                f'{self._battleNo}):""}}')),
            ReportTrait(self._clearBattle,
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+" Clear this global="+'
                                f'{self._currentBattle}+" this="+'
                                f'{self._battleNo}):""}}')),
        ]
        place  = []
        trig   = []
        rept   = []
        for i, bm in enumerate(self._battleMarks):
            kn   = self._markBattle+str(i+1)
            path = PlaceTrait.SKEL_PATH.format('BattleMarkers',bm)

            place.append(
                PlaceTrait(command       = '',#f'Add battle marker {i}',
                           key           = kn,
                           markerSpec    = path,
                           markerText    = 'null',
                           xOffset       = -8,
                           yOffset       = -16,
                           matchRotation = False,
                           afterKey      = self._getBattle,
                           gpid          = self._game.nextPieceSlotId(),
                           description   = f'Add battle marker {i+1}',
                           placement     = PlaceTrait.ABOVE,
                           above         = False))
            trig.append(
                TriggerTrait(command     = '',#Mark battle',
                             key         = self._markBattle,
                             actionKeys  = [self._getBattle,
                                            self._setBattle,kn],
                             property    = f'{{{self._currentBattle}=={i+1}}}'))
            rept.append(
                ReportTrait(kn,
                            report=(f'{{{self._debug}?'
                                    f'("~ "+BasicName+" placing marker ({i+1})'
                                    f' ="+ {self._currentBattle}+"'
                                    f'={kn}"):""}}')))

        oth = [
            GlobalHotkeyTrait(name         = 'Declare battle',
                              key          = self._markKey,
                              globalHotkey = self._markKey,
                              description  = 'Mark for combat'),
            GlobalPropertyTrait(
                ['',self._calcBattleAF,GlobalPropertyTrait.DIRECT,
                f'{{EffectiveAF+{self._battleAF}}}'],
                name        = self._battleAF,
                numeric     = True,
                description = 'Update battle AF'),
            GlobalPropertyTrait(
                ['',self._calcBattleDF,GlobalPropertyTrait.DIRECT,
                 f'{{EffectiveDF+{self._battleDF}}}'],
                 name        = self._battleDF,
                numeric     = True,
                description = 'Update battle AF'),
            GlobalPropertyTrait(
                ['',self._calcBattleShft,GlobalPropertyTrait.DIRECT,
                 f'{{OddsShift}}'],
                name        = self._battleShift,
                numeric     = True,
                description = 'Update battle shift',
            ),
            CalculatedTrait(#This could be redefined in module 
                name        = 'EffectiveAF',
                expression  = '{CF}',
                description = 'Current attack factor'),
            CalculatedTrait(#This could be redefined in module 
                name        = 'EffectiveDF',
                expression  = '{DF}',
                description = 'Current defence factor'),
            CalculatedTrait(#This could be redefined in module 
                name        = 'IsAttacker',
                expression  = '{Phase.contains(Faction)}',
                description = 'Check if current phase belongs to faction'),
            CalculatedTrait(#This could be redefined in module 
                name        = 'OddsShift',
                expression  = f'{{{self._battleShift}}}',
                description = 'Check if current phase belongs to faction'),
            ReportTrait(self._calcBattleAF,
                        report=(f'{{{self._verbose}?'
                                f'("! "+BasicName+'
                                f'" add "+EffectiveAF+'
                                f'" to total attack factor -> "+'
                                f'{self._battleAF}'
                                f'):""}}')),
            ReportTrait(self._calcBattleDF,
                        report=(f'{{{self._verbose}?'
                                f'("! "+BasicName+'
                                f'" add "+EffectiveDF+'
                                f'" to total defence factor -> "+'
                                f'{self._battleDF}'
                                f'):""}}')),
            ReportTrait(self._calcBattleShft,
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+'
                                f'" Updating odds shift with "+OddsShift+'
                                f'" -> "+{self._battleShift}):""}}')),
        ]
        traits.extend(
            place+
            trig+
            oth+
            [MarkTrait(name=self._battleUnit,value=True),
             BasicTrait()])
        prototypes.addPrototype(name        = self._battleUnit,
                                description = '',
                                traits      = traits)
    # ----------------------------------------------------------------
    def addBattleCorePrototype(self,prototypes):
        # --- Core traits for battle markers (number, odds, results)
        # - Set the global current battle number
        # - Get the current global battle number
        # - Clear this counter
        # - Trampoline to global command to clear all marks for this battle
        traits = [
            # NoStackTrait(select     = NoStackTrait.NORMAL_SELECT,
            #              move       = NoStackTrait.NORMAL_MOVE,
            #              canStack   = False,
            #              ignoreGrid = False),
            GlobalPropertyTrait(['',self._setBattle,GlobalPropertyTrait.DIRECT,
                                 f'{{{self._battleNo}}}'],
                                name        = self._currentBattle,
                                numeric     = True,
                                description = 'Set current battle'),
            GlobalPropertyTrait(['',self._setBattle, GlobalPropertyTrait.DIRECT,
                                 '{IsAttacker}'],
                                name        = self._currentAttacker,
                                numeric     = True,
                                description = 'Set attacker'),
            DynamicPropertyTrait(['',self._getBattle,
                                  DynamicPropertyTrait.DIRECT,
                                  f'{{{self._currentBattle}}}'],
                                 name        = self._battleNo,
                                 numeric     = True,
                                 value       = f'{{{self._battleNo}}}',
                                 description = 'Set battle number'),
            DynamicPropertyTrait(['',self._getBattle,
                                  DynamicPropertyTrait.DIRECT,
                                  f'{{{self._currentAttacker}}}'],
                                 name        = 'IsAttacker',
                                 numeric     = True,
                                 value       = 'false',
                                 description = 'Set attacker'),
            DeleteTrait('',self._clearBattle),
            GlobalHotkeyTrait(name         = '',
                              key          = self._clearBattle+'Trampo',
                              globalHotkey = self._clearBattle,
                              description  = 'Clear selected battle'),
            TriggerTrait(command    = 'Clear',
                         key        = self._clearKey,
                         actionKeys = [self._setBattle,
                                       self._clearBattle+'Trampo']),
            ReportTrait(self._setBattle,
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+" battle # "+'
                                f'{self._battleNo}+" -> "+'
                                f'{self._currentBattle}):""}}')),
            ReportTrait(self._getBattle,
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+" current battle # "+'
                                f'{self._currentBattle}+" -> "+'
                                f'{self._battleNo}):""}}')),
            ReportTrait(self._clearBattle,
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+" Clear this global="+'
                                f'{self._currentBattle}+" this="+'
                                f'{self._battleNo}):""}}')),
            ReportTrait(self._clearKey,
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+" To clear battle # global="+'
                                f'{self._currentBattle}+" this="+'
                                f'{self._battleNo}):""}}')),
            ReportTrait(self._clearBattle+'Trampo',
                        report=(f'{{{self._debug}?'
                                f'("~ "+BasicName+" '
                                f'Forward clear battle # global="+'
                                f'{self._currentBattle}+" this="+'
                                f'{self._battleNo}):""}}')),
            MarkTrait(name=self._battleMark,value=True),
            BasicTrait()
        ]
        prototypes.addPrototype(name        = self._currentBattle,
                                description = '',
                                traits      = traits)
        
    # ----------------------------------------------------------------
    def addBattlePrototypes(self,prototypes):
        if not self.addBattleControlPrototype(prototypes):
            return
        
        self.addBattleCalculatePrototype(prototypes)
        self.addBattleUnitPrototype(prototypes)
        self.addBattleCorePrototype(prototypes)

    # ----------------------------------------------------------------
    def markerTraits(self):
        return [DeleteTrait(),
                RotateTrait()]

    # ----------------------------------------------------------------
    def battleMarkerTraits(self,c):
        '''Derives from the CurrentBattle prototype and adds a submenu
        to place odds counter on the battle marker'''
        traits = [PrototypeTrait(name=self._currentBattle),
                  NonRectangleTrait(filename = c['filename'],
                                    image    = c['img'])]
        
        subs  = []
        ukeys = []
        place = []
        trig  = []
        rept  = []
        repp  = []
        for i, odds in enumerate(self._oddsMarks):
            on   = odds.replace('odds marker','').strip()
            om   = odds.replace(':',r'\:')
            kn   = self._markOdds+str(i+1)
            gpid = self._game.nextPieceSlotId()
            path = PlaceTrait.SKEL_PATH.format('OddsMarkers',om)
            subs.append(on)

            place.append(
                PlaceTrait(command       = '',
                           key           = kn,
                           markerSpec    = path,
                           markerText    = 'null',
                           xOffset       = -6,
                           yOffset       = -8,
                           matchRotation = False,
                           afterKey      = self._getBattle+'Details',
                           gpid          = gpid,
                           placement     = PlaceTrait.ABOVE,
                           description   = f'Add odds marker {on}'))
            trig.append(
                TriggerTrait(name        = '',
                             command     = on,
                             key         = kn+'real',
                             actionKeys  = [
                                 self._setBattle,
                                 kn]))
            rept.append(
                ReportTrait(kn+'real',
                            report=(f'{{{self._debug}?'
                                    f'("~ "+BasicName+": Set odds '
                                    f'{on} ({kn})"):""}}')))
            repp.append(
                ReportTrait(kn,
                            report=(f'{{{self._debug}?'
                                    f'("~ "+BasicName+": Place odds '
                                    f'{on} ({kn})"):""}}')))
            ukeys.append(kn+'real')

        auto = []
        auton = []
        if len(self._oddsMarks) > 0:
            auton = ['Auto']
            for i, odds in enumerate(self._oddsMarks):
                trig.append(
                    TriggerTrait(name        = '',
                                 command     = '',
                                 key         = self._markOdds+'Auto',
                                 property    = f'{{{self._battleIdx}=={i+1}}}',
                                 actionKeys  = [self._markOdds+str(i+1)]))

            auto = [GlobalHotkeyTrait(name = '',
                                      key          = self._calcBattleOdds,
                                      globalHotkey = self._calcBattleOdds,
                                      description  = 'Calculate fraction'),
                    DeselectTrait(command    = '',
                                  key        = self._calcBattleOdds+'Deselect',
                                  deselect   = DeselectTrait.ONLY),
                    ReportTrait(self._calcBattleOdds+'Deselect',
                                report=(f'{{{self._debug}?'
                                        f'("~ "+BasicName+": Select only this "'
                                        f'+" Attacker="+IsAttacker'
                                        f'):""}}')),
                    TriggerTrait(name        = '',
                                 command     = '',
                                 key         = self._markOdds+'Trampoline',
                                 actionKeys  = [
                                     self._calcBattleOdds,
                                     self._markOdds+'Auto',
                                     self._calcBattleOdds+'Deselect'],
                                 property    = f'{{!IsAttacker}}'
                                 ),
                    TriggerTrait(name        = '',
                                 command     = 'Auto',
                                 key         = self._calcBattleOdds+'Start',
                                 actionKeys  = [
                                     self._setBattle,
                                     self._markOdds+'Trampoline',
                                 ]),
                    ReportTrait(self._calcBattleOdds+'Start',
                                report=(f'{{{self._debug}?'
                                        f'("~ "+BasicName+": Battle odds "+'
                                        f'{self._battleOdds}):""}}')),
                    ReportTrait(self._markOdds+'Auto',
                                report=(f'{{{self._debug}?'
                                        f'("~ "+BasicName+": '
                                        f'Auto battle odds "+'
                                        f'{self._battleOdds}):""}}'))
                    ]
            
        traits.extend([
            RestrictCommandsTrait(
                name='Hide when auto-odds are enabled',
                hideOrDisable = RestrictCommandsTrait.HIDE,
                expression    = f'{{{self._autoOdds}==true}}',
                keys          = ukeys)]+
                      place
                      +trig
                      +auto
                      +rept
                      +repp)
        if len(subs) > 0:
            traits.extend([
                SubMenuTrait(subMenu = 'Odds',
                             keys    = auton+subs),
            ])

        return traits

    # ----------------------------------------------------------------
    def oddsMarkerTraits(self,c=None):
        '''Derives from the CurrentBattle prototype and adds a submenu
        to replace odds counter with result marker'''
        traits = [PrototypeTrait(name=self._currentBattle),
                  NonRectangleTrait(filename = c['filename'],
                                    image    = c['img']),
                  DynamicPropertyTrait(
                      ['',self._getBattle+'More',DynamicPropertyTrait.DIRECT,
                       (f'{{{self._battleAF}+" vs "+{self._battleDF}+'
                        f'" (odds "+{self._battleOdds}+" shift "+'
                        f'{self._battleShift}+")"}}')],
                      name = 'BattleDetails',
                      value = '',
                      numeric = False,
                      description = 'Stored battle details'),
                  TriggerTrait(command = '',
                               key     = self._getBattle+'Details',
                               actionKeys = [self._getBattle,
                                             self._getBattle+'More'])]

        subs  = []
        place = []
        trig  = []
        rept  = []
        ukeys = []
        first = ''
        for i, result in enumerate(self._resultMarks):
            r    = result.replace('result marker','').strip()
            kn   = self._markResult+str(i+1)
            gpid = self._game.nextPieceSlotId()
            ukeys.append(kn+'real')
            subs.append(r)
            if first == '': first = r
            
            path = PlaceTrait.SKEL_PATH.format('ResultMarkers',result)

            place.append(
                ReplaceTrait(command    = '',
                             key        = kn,
                             markerSpec = path,
                             markerText = 'null',
                             xOffset    = -6,
                             yOffset    = -8,
                             matchRotation = False,
                             afterKey      = self._getBattle,
                             gpid          = gpid,
                             description   = f'Add result marker {r}'))
            trig.append(
                TriggerTrait(name        = '',
                             command     = r,
                             key         = kn+'real',
                             actionKeys  = [
                                 self._setBattle,
                                 kn]))
            rept.append(
                ReportTrait(kn+'real',
                            report=(f'{{{self._debug}?'
                                    f'("~ "+BasicName+" setting result '
                                    f'{r}"):""}}')))

        auto = []
        auton = []
        if len(self._resultMarks) > 0:
            auton = ['Auto']
            for i, res in enumerate(self._resultMarks):
                r = res.replace('result marker','').strip()
                trig.append(
                    TriggerTrait(
                        name        = '',
                        command     = '',
                        key         = self._markResult+'Auto',
                        property    = f'{{{self._battleResult}=="{r}"}}',
                        actionKeys  = [self._markResult+str(i+1)]))

            auto = [ # Override in the module
                CalculatedTrait(
                    name = 'Die',
                    expression = '{GetProperty("1d6_result")}',
                    description = 'Die roll'),
                GlobalHotkeyTrait(
                    name         = '',
                    key          = self._rollDice,
                    globalHotkey = self._diceKey,
                    description  = 'Roll dice'),        
                CalculatedTrait(
                    name       = 'BattleResult',
                    expression = f'{{"{first}"}}',
                ),
                GlobalPropertyTrait(
                    ['',self._calcBattleRes+'real',GlobalPropertyTrait.DIRECT,
                     '{BattleResult}'],
                    name = self._battleResult,
                    numeric = False,
                    description = 'Set combat result'),
                TriggerTrait(name        = '',
                             command     = 'Resolve',
                             key         = self._resolveKey,
                             property    = f'{{{self._autoResults}==true}}',
                             actionKeys  = [
                                 self._setBattle,
                                 self._rollDice,
                                 self._calcBattleRes+'real',
                                 self._markResult+'Auto',
                             ]),
                ReportTrait(self._calcBattleRes,
                            report=(f'{{{self._debug}?'
                                    f'("~ "+BasicName+": Battle result "+'
                                    f'{self._battleOdds}):""}}')),
                ReportTrait(self._markResult+'Auto',
                            report=(f'{{{self._debug}?'
                                    f'("~ "+BasicName+": Auto battle result "+'
                                    f'{self._battleResult}):""}}')),
                ReportTrait(self._markResult+'Auto',
                            report=(f'{{"` Battle # "+{self._battleNo}+": "+'
                                    f'BattleDetails+'
                                    f'" with die roll "+Die+": "+'
                                    f'{self._battleResult}'
                                    # f'+ "<img src=\'result_marker_"'
                                    # f'+{self._battleResult}+".png\'"'
                                    # f'+" width=24 height=24>"'
                                    f'}}')),
                MarkTrait(name=self._battleOddsM,value='true')
            ]

        traits.extend(
            [RestrictCommandsTrait(
                name='Hide when auto-results are enabled',
                hideOrDisable = RestrictCommandsTrait.HIDE,
                expression    = f'{{{self._autoResults}==true}}',
                keys          = ukeys)]+
            place
            +trig
            +auto)
        
        if len(subs) > 0:
            traits.append(SubMenuTrait(subMenu = 'Result',
                                       keys    = subs))

        return traits

    # ----------------------------------------------------------------
    def resultMarkerTraits(self,c=None):
        traits = [PrototypeTrait(name=self._currentBattle),
                  NonRectangleTrait(filename = c['filename'],
                                    image    = c['img'])]

        return traits

    # ----------------------------------------------------------------
    def factionTraits(self,faction):
        offX =  36 * self._counterScale * self._resolution/150
        offY = -38 * self._counterScale * self._resolution/150
        traits = [#ReportTrait(self._eliminateKey,
                  #            self._restoreKey,
                  #            self._trailKey),
                  TrailTrait(),
                  RotateTrait(),
                  MovedTrait(xoff = int(offX),yoff = int(offY)),
                  DeleteTrait(),
                  SendtoTrait(mapName     = 'DeadMap',
                              boardName   = f'{faction} pool',
                              name        = 'Eliminate',
                              key         = self._eliminateKey,
                              restoreName = 'Restore',
                              restoreKey  = self._restoreKey,
                              description = 'Eliminate unit'),
                  PrototypeTrait(name=self._battleUnit),                 
                  MarkTrait(name='Faction',value=faction)]

        return traits

    # ----------------------------------------------------------------
    def getFactors(self,val):
        cf = None
        mf = None
        df = None
        ra = None
        try:
            if 'chit 1 factor' in val:
                vv = val.replace('chit 1 factor=','')
                cf = int(vv)
            elif 'chit 2 factors artillery' in val:
                vv       = val.replace('chit 2 factors artillery=','')
                cf,mf,ra = [int(v) for v in vv.strip('=').split()]
            elif 'chit 2 factors' in val:
                vv    = val.replace('chit 2 factors=','')
                cf,mf = [int(v) for v in vv.split()]
            elif 'chit 3 factors' in val:
                vv       = val.replace('chit 3 factors=','')
                cf,df,mf = [int(v) for v in vv.split()]

            # Set defensive factor combat factor if not defined. 
            if df is None and cf is not None:
                df = cf
                
            
        except Exception as e:
            print(f'\nWarning when extracting factors: {e} '
                  f'in "{val}" -> "{vv}"')
            return None,None,None,None
            pass

        return cf,df,mf,ra
        
    # ----------------------------------------------------------------
    def pieceTraits(self,subn,subc,cn,c):
        from re import sub

        bb     = self.getBB(c['img'])
        height = bb[3]-bb[1] if bb is not None else 1
        width  = bb[2]-bb[0] if bb is not None else 1
        cf     = subc.get(cn + ' flipped', None)
        traits = [PrototypeTrait(name=f'{subn} prototype')]

        def clean(s):
            return s.strip().replace(',',' ').replace('/',' ').strip()
        
        if not self._nonato:
            mains = c.get('mains','')
            m     = set([clean(mains)] +
                        [clean(m) for m in mains.split(',')])
            traits.extend([PrototypeTrait(name=f'{m.strip()} prototype')
                           for m in set(m)])
            for p in ['echelon','command']:
                val = c.get(p,None)
                if val is not None:
                    pv = f'{val.strip()} prototype'
                    traits.append(PrototypeTrait(name=pv))
                    
        if cf is not None:
            traits.extend([
                LayerTrait(images = [c['filename'],
                                     cf['filename']],
                           newNames = ['','Reduced +'],
                           activateName = '',
                           decreaseName = '',
                           increaseName = 'Flip',
                           increaseKey  = self._flipKey,
                           decreaseKey  = '',
                           name         = 'Step'),
                ReportTrait(self._flipKey)])

        if not self._nochit:
            def clean(value):
                return sub(r'\[[^=]+\]=','',value)\
                    .replace('{','')\
                    .replace('}','')\
                    .replace('/',' ')\
                    .replace(',',' ')\
                    .replace('\\',' ')
            
            # Add extra marks.  This may be useful later on. 
            for field in ['upper left', 'upper right', 
                          'lower left', 'lower right',
                          'left',       'right',
                          'factors']:
                value = c.get('chit',{}).get(field,None)
                if value is None:
                    continue
                val = clean(value)
                val = val\
                    .replace('chit identifier=','')\
                    .replace('chit small identifier=','')
                
                traits.append(MarkTrait(name = field, value = val))

                if field != 'factors': continue

                af, df, mf, ra = self.getFactors(val)
                saf, sdf, smf, sra = None,None,None,None
                if cf is not None:
                    value = cf.get('chit',{}).get(field,None)
                    if value is not None:
                        val = clean(value)
                        val = val\
                            .replace('chit identifier=','')\
                            .replace('chit small identifier=','')
                        saf, sdf, smf, sra = self.getFactors(val)

                rf  = []
                srf = []
                for f,sf,n in [[af,saf,'CF'],
                               [df,sdf,'DF'],
                               [mf,smf,'MF'],
                               [ra,sra,'Range']]:
                    if f is None: continue
                    
                    if sf is None:
                        rf.append(MarkTrait(name=n,value=f))
                    else:
                        rf .append(MarkTrait(name='Full'+n,   value=f))
                        srf.append(MarkTrait(name='Reduced'+n,value=sf))
                        traits.append(CalculatedTrait(
                            name = n,
                            expression = (f'{{(Step_Level==2)?'
                                          f'Reduced{n}:Full{n}}}')))

                traits.extend(rf+srf)
                        

                

                
                        
        return height, width, traits
        
    # ----------------------------------------------------------------
    def addCounters(self):
        '''Add all counters (pieces) element to the module.
        Prototypes are also created as part of this.
        '''
        from re import sub

        with VerboseGuard('Adding counters') as v:
            protos       =  self._game.addPrototypes()
            
            self.addNatoPrototypes(protos)
            self.addBattlePrototypes(protos)
            
            pieces = self._game.addPieceWindow(name   = 'Counters',
                                               icon   = self.getIcon('unit-icon',
                                                                     '/images/counter.gif'),
                                               hotkey = self._countersKey)
            tabs   = pieces.addTabs(entryName='Counters')
            
            for subn, subc in self._categories.get('counter',{}).items():
            
                subn  = subn.strip()
                panel = tabs.addPanel(entryName = subn, fixed = False)
                plist = panel.addList(entryName = f'{subn} counters')

                traits = []
                if   subn in ['BattleMarkers']:
                    traits = self.battleMarkerTraits(list(subc.values())[0])
                elif subn in ['OddsMarkers']:
                    traits = self.oddsMarkerTraits(list(subc.values())[0])
                elif subn in ['ResultMarkers']:
                    traits = self.resultMarkerTraits(list(subc.values())[0])
                elif subn.lower() in ['marker', 'markers']:
                    traits = self.markerTraits()
                else:
                    traits = self.factionTraits(subn)

                traits.append(BasicTrait())
                
                p = protos.addPrototype(name        = f'{subn} prototype',
                                        description = f'Prototype for {subn}',
                                        traits      = traits)
                v('')
             
                with VerboseGuard(f'Adding pieces for "{subn}"') as vv:
                    for i, (cn, c) in enumerate(subc.items()):
                        if cn.endswith('flipped'): continue

                        if i == 0: v('',end='')
                        vv(f'[{cn}',end='',flush=True,noindent=True)
                    
                        height, width, traits = self.pieceTraits(subn,subc,cn,c)
                        if cn == self._hiddenName:
                            traits = [
                                PrototypeTrait(name=self._battleCtrl),
                                PrototypeTrait(name=self._battleCalc)]
                            if self._diceInit is not None:
                                traits.extend(self._diceInit)
                            traits.append(
                                RestrictAccessTrait(sides=[],
                                                    description='Fixed'))

                            
                        #if cn.startswith('odds marker'):
                        #    cn  = cn.replace(':','_')
                            
                        gpid = self._game.nextPieceSlotId()
                        traits.extend([BasicTrait(name     = c['name'],
                                                  filename = c['filename'],
                                                  gpid     = gpid)])
                        
                        ps = plist.addPieceSlot(entryName = cn,
                                                gpid      = gpid,
                                                height    = height, 
                                                width     = width,
                                                traits    = traits)
                        if cn == self._hiddenName:
                            self._hidden = ps
                        vv('] ',end='',flush=True,noindent=True)
                    
                    vv('')
            

    # ----------------------------------------------------------------
    def addNotes(self,**kwargs):
        '''Add a `Notes` element to the module

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        '''
        self._game.addNotes(**kwargs)
        
    # ----------------------------------------------------------------
    def addInventory(self,**kwargs):
        '''Add a `Inventory` element to module

        Parameters
        ----------
        kwargs : dict
            Dictionary of attribute key-value pairs
        '''
        filt = '{' + '||'.join([f'Faction=="{s}"' for s in self._sides])+'}'
        grp  = 'Faction,Command,Echelon,Type'
        self._game.addInventory(include = filt,
                                groupBy = grp,
                                sortFormat  = '$PieceName$',
                                tooltip     ='Show inventory of all pieces',
                                zoomOn      = True,
                                **kwargs)

    # ----------------------------------------------------------------
    def addBoard(self,name,info,hasFlipped=False):
        '''Add a `Board` element to module

        Parameters
        ----------
        name : str
            Name of board
        info : dict
            Information on board image
        hasFlipped : bool
            True if any piece can be flipped 
        '''
        with VerboseGuard(f'Adding board {name}') as v:
            map    = self._game.addMap(mapName=name,
                                       markUnmovedHotkey=self._clearMoved)
            map.addCounterDetailViewer(
                propertyFilter=f'{{{self._battleMark}!=true}}')
            map.addHidePiecesButton()
            map.addGlobalMap()
            # Basics
            map.addStackMetrics()
            map.addImageSaver()
            map.addTextSaver()
            map.addForwardToChatter()     
            map.addMenuDisplayer()        
            map.addMapCenterer()          
            map.addStackExpander()        
            map.addPieceMover()           
            map.addKeyBufferer()          
            map.addSelectionHighlighters()
            map.addHighlightLastMoved()   
            map.addZoomer()               
            
            map.addMassKey(name='Eliminate',
                           buttonHotkey = self._eliminateKey,
                           hotkey       = self._eliminateKey,
                           icon         = self.getIcon('eliminate-icon',
                                                       '/icons/16x16/edit-undo.png'),
                           tooltip      = 'Eliminate selected units')
            map.addMassKey(name='Delete',
                           buttonHotkey = self._deleteKey,
                           hotkey       = self._deleteKey,
                           icon         = self.getIcon('delete-icon',
                                                       '/icons/16x16/no.png'),
                           tooltip      = 'Delete selected units')
            map.addMassKey(name='Rotate CW',
                           buttonHotkey = self._rotateCWKey,
                           hotkey       = self._rotateCWKey,
                           icon         = '', #/icons/16x16/no.png',
                           tooltip      = 'Rotate selected units')
            map.addMassKey(name='Rotate CCW',
                           buttonHotkey = self._rotateCCWKey,
                           hotkey       = self._rotateCCWKey,
                           icon         = '', #/icons/16x16/no.png',
                           tooltip      = 'Rotate selected units')
            map.addMassKey(name='Phase clear moved markers',
                           buttonHotkey = self._clearMoved+'Phase',
                           hotkey       = self._clearMoved+'Trampoline',
                           canDisable   = True,
                           target       = '',
                           filter       = f'{{{self._battleCtrl}==true}}',
                           propertyGate = f'{self._noClearMoves}',
                           icon         = '', #/icons/16x16/no.png',
                           tooltip      = 'Phase clear moved markers',
                           reportFormat = (f'{{{self._debug}?'
                                           f'("~ {name}: '
                                           f'Phase Clear moved markers "+'
                                           f'{self._noClearMoves})'
                                           f':""}}'))
            if hasFlipped:
                map.addMassKey(name='Flip',
                               buttonHotkey = self._flipKey,
                               hotkey       = self._flipKey,
                               icon         = self.getIcon('flip-icon',
                                                           '/images/Undo16.gif'),
                               tooltip      = 'Flip selected units')

            if len(self._battleMarks) > 0:
                v(f'Adding battle mark interface')
                ctrlSel = f'{{{self._battleCtrl}==true}}'
                oddsSel = f'{{{self._battleMark}==true}}'
                calcSel = f'{{{self._battleCalc}==true}}'
                curSel  = (f'{{{self._battleNo}=={self._currentBattle}}}')
                curAtt  = (f'{{{self._battleNo}=={self._currentBattle}&&'
                           f'{self._battleUnit}==true&&'
                           f'IsAttacker==true}}')
                curDef  = (f'{{{self._battleNo}=={self._currentBattle}&&'
                           f'{self._battleUnit}==true&&'
                           f'IsAttacker==false}}')
                curUnt  = (f'{{{self._battleNo}=={self._currentBattle}&&'
                           f'{self._battleUnit}==true}}')
                markSel = (f'{{{self._battleNo}=={self._currentBattle}&&'
                           f'{self._battleMark}==true}}')

                # ctrlSel = '{BasicName=="wg hidden unit"}'
                map.addMassKey(name         = 'User mark battle',
                               buttonHotkey = self._markKey,
                               buttonText   = '',
                               hotkey       = self._markBattle,
                               icon         = 'battle-marker-icon.png',
                               tooltip      = 'Mark battle',
                               target       = '',
                               singleMap    = False,
                               filter       = ctrlSel,
                               reportFormat = (f'{{{self._debug}?'
                                               f'("~ {name}: '
                                               f'User marks battle # "+'
                                               f'{self._currentBattle})'
                                               f':""}}'))
                map.addMassKey(name         = 'Selected mark battle',
                               buttonHotkey = self._markBattle,
                               hotkey       = self._markBattle,
                               icon         = '',
                               tooltip      = '',
                               singleMap    = False,
                               reportFormat = (f'{{{self._debug}?'
                                               f'("~ {name}: '
                                               f'Mark battle # "+'
                                               f'{self._currentBattle})'
                                               f':""}}'))
                map.addMassKey(name         = 'Clear current battle',
                               buttonText   = '',
                               buttonHotkey = self._clearBattle,
                               hotkey       = self._clearBattle,
                               icon         = '',
                               tooltip      = '',
                               target       = '',
                               singleMap    = False,
                               filter       = curSel,
                               reportFormat = (f'{{{self._debug}?'
                                               f'("~ {name}: '
                                               f'Clear battle # "+'
                                               f'{self._currentBattle})'
                                               f':""}}'))
                map.addMassKey(name         = 'Clear selected battle',
                               buttonText   = '',
                               buttonHotkey = self._clearKey,
                               hotkey       = self._clearKey,
                               icon         = '',
                               tooltip      = '',
                               singleMap    = False,
                               reportFormat = (f'{{{self._debug}?'
                                               f'("~ {name}: '
                                               f'Clear battle # "+'
                                               f'{self._currentBattle})'
                                               f':""}}'))
                map.addMassKey(name         = 'Clear all battles',
                               buttonText   = '',
                               buttonHotkey = self._clearAllBattle,
                               hotkey       = self._clearBattle,
                               icon         = '',
                               tooltip      = '',
                               target       = '',
                               singleMap    = False,
                               reportFormat = (f'{{{self._debug}?'
                                               f'("~ {name}: '
                                               f'Clear all battle markers")'
                                               f':""}}'))
                map.addMassKey(name         = 'User clear all battles',
                               buttonText   = '',
                               buttonHotkey = self._clearAllKey,
                               hotkey       = self._clearAllBattle,
                               icon         = 'clear-battles-icon.png',
                               tooltip      = 'Clear all battles',
                               target       = '',
                               singleMap    = False,
                               filter       = ctrlSel,
                               reportFormat = (f'{{{self._debug}?'
                                               f'("~ {name}: '
                                               f'User clears battle markers")'
                                               f':""}}'))
                map.addMassKey(name         = 'Phase clear all battles',
                               buttonText   = '',
                               buttonHotkey = self._clearBattlePhs,
                               hotkey       = self._clearAllBattle,
                               icon         = '',
                               tooltip      = 'Clear all battles',
                               canDisable   = True,
                               propertyGate = f'{self._noClearBattles}',
                               target       = '',
                               singleMap    = False,
                               filter       = ctrlSel,
                               reportFormat = (f'{{{self._debug}?'
                                               f'("~ {name}: '
                                               f'Phase clears battle markers "+'
                                               f'{self._noClearBattles})'
                                               f':""}}'))
                map.addMassKey(name         = 'Selected resolve battle',
                               buttonHotkey = self._resolveKey,
                               hotkey       = self._resolveKey,
                               icon         = 'resolve-battles-icon.png',
                               tooltip      = 'Resolve battle',
                               singleMap    = False,
                               filter       = oddsSel,
                               reportFormat = (f'{{{self._debug}?'
                                               f'("~ {name}: '
                                               f'Resolve battle # "+'
                                               f'{self._currentBattle})'
                                               f':""}}'))
                map.addMassKey(name         = 'Sum AFs',
                               buttonText   = '',
                               buttonHotkey = self._calcBattleAF,
                               hotkey       = self._calcBattleAF,
                               icon         = '',
                               tooltip      = '',
                               target       = '',
                               singleMap    = False,
                               filter       = curAtt,
                               reportFormat = (f'{{{self._debug}?'
                                               f'("~ {name}: '
                                               f'Calculate total AF"):""}}'))
                map.addMassKey(name         = 'Sum DFs',
                               buttonText   = '',
                               buttonHotkey = self._calcBattleDF,
                               hotkey       = self._calcBattleDF,
                               icon         = '',
                               tooltip      = '',
                               target       = '',
                               singleMap    = False,
                               filter       = curDef,
                               reportFormat = (f'{{{self._debug}?'
                                               f'("~ {name}: '
                                               f'Calculate total DF"):""}}'))
                map.addMassKey(name         = 'Sum odds shifts',
                               buttonText   = '',
                               buttonHotkey = self._calcBattleShft,
                               hotkey       = self._calcBattleShft,
                               icon         = '',
                               tooltip      = '',
                               target       = '',
                               singleMap    = False,
                               filter       = curUnt,
                               reportFormat = (f'{{{self._debug}?'
                                               f'("~ {name}: '
                                               f'Calculate odds shift"):""}}'))
                map.addMassKey(name         = 'Calc battle odds',
                               buttonText   = '',
                               buttonHotkey = self._calcBattleOdds,
                               hotkey       = self._calcBattleOdds,
                               icon         = '',
                               tooltip      = '',
                               target       = '',
                               singleMap    = False,
                               filter       = calcSel,
                               reportFormat = (f'{{{self._debug}?'
                                               f'("~ {name}: '
                                               f'Calculate odds"):""}}'))
                map.addMassKey(name         = 'Auto calc battle odds',
                               buttonText   = '',
                               buttonHotkey = self._calcBattleOdds+'Auto',
                               hotkey       = self._calcBattleOdds+'Start',
                               icon         = '',
                               tooltip      = '',
                               # target       = '',
                               singleMap    = False,
                               filter       = markSel,
                               reportFormat = (f'{{{self._debug}?'
                                               f'("~ {name}: '
                                               f'Auto calculate odds"):""}}')) 
               
            
            ulx,uly,lrx,lry = self.getBB(info['img'])
            width           = abs(ulx - lrx)
            height          = abs(uly - lry)
            width, height   = self.getWH(info['img'])
            height          += 20
            width           += 5
                
            picker = map.addBoardPicker()
            board  = picker.addBoard(name   = name,
                                     image  = info['filename'],
                                     width  = width,
                                     height = height)
            zoned  = board.addZonedGrid()
            zoned.addHighlighter()
            
            if not 'zones' in info:
                full = zoned.addZone(name = full,
                                     useParentGrid = False,
                                     path=(f'{ulx},{uly};' +
                                           f'{lrx},{uly};' +
                                           f'{lrx},{lry};' +
                                           f'{ulx},{lry}'))
                grid = zone.addHexGrid(color   = color,
                                       dx      = HEX_WIDTH,
                                       dy      = HEX_HEIGHT,
                                       visible = self._visible)
                grid.addNumbering(color   = color,
                                  hType   = 'A',
                                  hOff    = -1,
                                  vType   = 'N',
                                  vOff    = -1,
                                  visible = self._visible)
                return
            
            w = abs(ulx-lrx)
            h = abs(uly-lry)
            self.addZones(zoned,name,info['zones'],w,h)

            if self._hidden is not None:
                v(f'Adding hidden unit to map {name}')
                at = map.addAtStart(name            = self._hiddenName,
                                    location        = '',
                                    useGridLocation = False,
                                    owningBoard     = name,
                                    x               = 0,
                                    y               = 0)
                at.addPieces(self._hidden)
            

    # ----------------------------------------------------------------
    def addDeadMap(self):
        '''Add a "Dead Map" element to the module 
        '''
        name = 'DeadMap'
        with VerboseGuard(f'Adding board {name}') as v:
            map    = self._game.addMap(mapName       = name,
                                       buttonName    = '',
                                       markMoved     = 'Never',
                                       launch        = True,
                                       icon          = self.getIcon('pool-icon',
                                                                    '/images/playerAway.gif'),
                                       allowMultiple = True,
                                       hotkey        = self._deadKey)
            # Basics
            map.addStackMetrics()
            map.addImageSaver()
            map.addTextSaver()
            map.addForwardToChatter()     
            map.addMenuDisplayer()        
            map.addMapCenterer()          
            map.addStackExpander()        
            map.addPieceMover()           
            map.addKeyBufferer()          
            map.addSelectionHighlighters()
            map.addHighlightLastMoved()   
            map.addZoomer()               
            
            map.addMassKey(name='Restore',
                           buttonHotkey = self._restoreKey,
                           hotkey       = self._restoreKey,
                           icon         = self.getIcon('restore-icon',
                                                       '/images/Undo16.gif'),
                           tooltip      = 'Restore selected units')
            
            picker = map.addBoardPicker()
            picker.addSetup(maxColumns=len(self._sides),mapName=name,
                            boardNames=[s+' pool' for s in self._sides])
            
            for i, s in enumerate(self._sides):
                v(f'Adding {s} pool')
                color        = [0,0,0,64]
                color[i % 3] = 255
                w            = 400
                h            = 400
                c            = rgba(*color)
                img          = ''
                dimg         = self._categories.get('pool',{}).get('all',{})\
                                                              .get(s,None)
            
                if dimg:
                    bb  = self.getBB(dimg['img'])
                    w   = bb[2] - bb[0]
                    h   = bb[3] - bb[1]
                    c   = ''
                    img = dimg['filename']
                    v(f'Using image provided by user {img}')
            
                board  = picker.addBoard(name   = f'{s} pool',
                                         image  = img,
                                         width  = w,
                                         height = h,
                                         color  = c)
            
                if dimg is None or not 'zones' in dimg:
                    continue
            
                zoned  = board.addZonedGrid()
                zoned.addHighlighter()
                w = abs(w)
                h = abs(h)
                self.addZones(zoned,board['name'],dimg['zones'],w,h)
                
        
    # --------------------------------------------------------------------
    def getPictureInfo(self,picture,name,width,height):
        '''
        Returns
        -------
        hex_width, hex_height : float, float
            Scale hex width
        scx, scy : float, float, float, float
            Scale to image and picture (x,y)
        rot90 : bool
            True if rotated +/-90 degrees
        tran : callable
            Translation function
        '''
        if picture is None:
            print(f'WARNING: No Tikz picture information.'
                  f"Are you sure you used the `[zoned]' option for the "
                  f"tikzpicture environment of {name}?")
            f = lambda x,y: (x,y)
            return HEX_WIDTH,HEX_HEIGHT,1,1,False,f
        
        # Get picture bounding box
        tll = picture['lower left']
        tur = picture['upper right']
        # Get picture transformation
        pa  = picture['xx']
        pb  = picture['xy']
        pc  = picture['yx']
        pd  = picture['yy']
        # Get picture offset (always 0,0?)
        pdx = picture['dx']
        pdy = picture['dy']
        # Define picture global transformation
        pr  = lambda x,y: (pa * x + pc * y, pb * x + pd * y)
        # Globally transform (rotate) picture bounding box 
        pll  = pr(*tll)
        pur  = pr(*tur)
        # Calculate widht, height, and scaling factors 
        pw   = pur[0] - pll[0]
        ph   = pur[1] - pll[1]
        scw  = width / pw
        sch  = height / ph
        # Extract picture scales and rotation
        # Courtesy of
        # https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix
        from math import sqrt, atan2, degrees, isclose
        psx   = sqrt(pa**2 + pb**2) # * (-1 if pa < 0 else 1)
        psy   = sqrt(pc**2 + pd**2) # * (-1 if pd < 0 else 1)
        prt   = degrees(atan2(pc,pd))
        if not any([isclose(abs(prt),a) for a in [0,90,180,270]]):
            raise RuntimeException('Rotations of Tikz pictures other than '
                                   '0 or +/-90,+/- 180, or +/-270 not supported. '
                                   'found {prt}')
        rot90      = int(prt // 90)
        if rot90 == 2: rot90 = -2
        # Now supported 
        # if any([isclose(prt,a) for a in [90,270,180,-180]]):
        #     print(f'WARNING: rotations by {prt} not fully supported')

        from math import sqrt
        hex_width  = psx * scw * 2  # HEX_WIDTH
        hex_height = psy * sch * sqrt(3) # HEX_HEIGHT
        with VerboseGuard('Picture') as v:
            v(f'Transformations:        {pa},{pb},{pc},{pd}')
            v(f'Scale (x,y):            {psx},{psy}')
            v(f'Rotation (degrees):     {prt} ({rot90})')
            v(f'Scale to pixels (x,y):  {scw},{sch}')
    
        # When translating the Tikz coordinates, it is important to note
        # that the Tikz y-axis point upwards, while the picture y-axis
        # point downwards.  This means that the upper right corner is at
        # (width,0) and the lower left corner is at (0,height).
        def tranx(x,off=-pll[0]):
            # print(f'x: {x} + {off} -> {x+off} -> {int(scw*(x+off))}')
            return int(scw * (x + off)+.5)
        def trany(y,off=-pur[1]):
            # print(f'y: {y} + {off} -> {y+off} -> {-int(sch*(y+off))}')
            return -int(sch * (y + off)+.5)
        tran  = lambda x,y : (tranx(x), trany(y))

        return hex_width, hex_height, scw * psx, sch * psy, rot90, tran
        
    # --------------------------------------------------------------------
    def getHexParams(self,
                     llx,
                     lly,
                     urx,
                     ury,
                     mx,
                     my,
                     hex_width,
                     hex_height,
                     rot90,
                     labels,
                     coords,
                     targs,
                     nargs):
        '''rot90 =  0  No rotation
                 =  1  Rotated -90 (clock-wise)
                 = -1  Rotated  90 (counter clock-wise)
                 = -2  Rotated 180
        '''
        with VerboseGuard('Hex parameters') as v:
            from math import sqrt
            isodd   = lambda x : (x % 2 == 1)
            iseven  = lambda x : (x % 2 == 0)
            isfalse = lambda x : False
            shorts  = {'isodd': isodd, 'iseven': iseven, 'isfalse': isfalse }
            
            # Funny scaling needed by VASSAL.  Seems like they only
            # really about the absolute value of 'dy' and then the
            # aspect ratio between dx and dy.
            pxfac               = sqrt(3)/2
            hex_pw              = hex_height * pxfac
            hex_ph              = hex_width  * pxfac
            stagger             = False
            #
            # Get parameters from coordinates. These should always be set  
            #
            rows                = coords  .get('row',   {})
            columns             = coords  .get('column',{})
            top_short           = columns .get('top short',   'isfalse')
            bot_short           = columns .get('bottom short','isfalse')
            inv_col             = columns .get('factor',1)
            inv_row             = rows    .get('factor',1)
            voff                = -rows   .get('offset',0) # 0:  from 0 -> -1
            hoff                = -columns.get('offset',0) # -1: from 1 -> -2
            vdesc               = inv_row == 1
            hdesc               = inv_col == -1
            #
            # Calculate total dimensions, and number of columns and rows
            #
            w                   =  abs((urx-llx) - 2 * mx)
            h                   =  abs((ury-lly) - 2 * my)
            if abs(rot90) == 1: h, w  = w, h
            nc                  = int(w // (hex_width  * 3 / 4))
            nr                  = int(h // (hex_height))
            namrot              = {0:   'none - 0',
                                   -1: '-90 - CCW',
                                   1:  '90 CW',
                                   -2: '180 - half-turn'}
            
            v(f'Width:    {w}')
            v(f'Height:   {h}')
            v(f'Margins:  x={mx} y={my}')
            v(f'Rotation: {rot90} ({namrot[rot90]})')
            v(f'Labels:   {labels}')
            v(f'Columns:')
            v(f' size:          {nc}')
            v(f' start:         {hoff}')
            v(f' direction:     {inv_col}')
            v(f' top short:     {top_short}')
            v(f' bottom short:  {bot_short}')
            v(f'Rows:')
            v(f' size:          {nr}')
            v(f' start:         {voff}')
            v(f' direction:     {inv_row}')
            v(f'Image:')
            v(f' BB: ({llx},{lly}) x ({urx},{ury})')
            #
            # X0 and Y0 are in the local (rotated) frame of the hex grid.
            # Thus X is always along hex breadth, and Y along the
            # height. Thus the base offset (rotated into the hex frame) differs.
            x0 =  ury if abs(rot90) == 1 else llx
            y0 =  llx if abs(rot90) == 1 else ury
            # Calculate column,row of corners
            llc     = hoff
            ulc     = hoff
            lrc     = hoff+nc-1
            urc     = hoff+nc-1
            #
            # Swap in directions
            if hdesc: llc, lrc, ulc, urc = lrc, llc, urc, ulc
            #
            is_short_top  = shorts[columns.get('top short',   'isfalse')]
            is_short_bot  = shorts[columns.get('bottom short','isfalse')]
            if is_short_top is isfalse:
                # Assume fully populated columns 
                is_short_top = isodd if iseven(hoff) else iseven
            if is_short_bot is isfalse:
                is_short_bot = isodd if isodd(hoff)  else iseven
            
            #
            # Now we have the hex coordinates of the corners.  We can
            # now check how things are offset.  Before rotation, we
            # will have that the first column is offset by hex_pw / 2.
            x0 += hex_width / 2
            #
            # If the first column is _not_ short on top, then off set
            # is simply hex_ph / 2. Otherwise, the offset is hex_ph
            y0   +=  hex_ph / 2
            voff -= 1
            voff -= inv_row
            v(f' Initial offset of image {x0},{y0}')
            
            # Treat each kind of rotation separately.  Note that -90 and
            # 180 uses the `is_short_bot' while 0 and 90 uses
            # `is_short_top'.  There might be a way to unify these, if
            # offsets and so on may warrent it, but it may be complete
            # overkill.
            is_off  = False
            col_map = {0  : (ulc, is_short_top, is_short_bot),
                       -1 : (urc, is_short_top, is_short_bot),
                       1  : (ulc, is_short_bot, is_short_top),
                       -2 : (urc, is_short_bot, is_short_top) }
            col_chk, is_s1, is_s2 = col_map[rot90]
            
            is_off = is_s1(col_chk)
            if is_off:
                y0     += hex_ph /2
            
            v(f'Is first column off: {is_off}')
            
            # For full columns, noting more is needed
            #
            # Below is if some columns are short both top and bottom.
            # VASSAL seems to start numbering from a given place, and
            # then use that for the rest numbering, and forgets to
            # take into account various offsets and the like.  hence,
            # we need to hack it hard.
            if iseven(nc):
                v(f'Even number of columns, perhaps hacks')
                if rot90 == 0:
                    # Hacks
                    #
                    # If the last column is short in both top and bottom,
                    # and we have inverse columns, but not inverse rows,
                    # then add to offset 
                    if inv_col == -1 and inv_row == 1 and \
                       is_s1(urc) and is_s2(urc):
                        voff += 1
                    # If the column we check for short is short both top
                    # and bottom, and we have inverse rows, but not
                    # inverse columns, then add offset
                    if inv_row == -1 and inv_col == 1 and \
                       is_s2(col_chk) and is_off:
                        voff += 1
                        
                if rot90 == -1:
                    # If the last column is short in both top and bottom,
                    # and we have inverse columns, then add to offset
                    if is_s1(urc) and inv_col == -1 and is_s2(urc):
                        voff -= inv_row
                        
                if rot90 == 1:
                    voff  += inv_row + (inv_row == 1)
                    # If the first column is short in both top and bottom,
                    # and we have inverse columns, then add to offset
                    if is_s1(ulc) and is_s2(ulc) and inv_col == -1:
                            voff += inv_row
                            
                if rot90 == -2:
                    voff    += inv_row * 2
                    # Hacks If the column we check for short is short both
                    # top and bottom, and we have either inverse rows and
                    # inverse columns, or rows and columns are normal,
                    # then add offset
                    if inv_col == inv_row and is_s1(col_chk) and is_s2(col_chk):
                        voff += 1
                    # If the first column is short in both top and bottom,
                    # and we have inverse columns and rows, then add to
                    # offset
                    if inv_col == inv_row and inv_col == -1 and \
                       is_s1(ulc) and is_s2(ulc):
                        voff += 1
            else:
                v(f'Odd number of columns')
                voff -= inv_row
                if rot90 == 1:
                    # If we offset in the column direction, add the
                    # inverse row direction, and if we have inverse rows,
                    # substract one, otherwise add 2.
                    voff  += (inv_row * hoff + (-1 if inv_row == -1 else 2))
                    # If we have a short column, and that column is even,
                    # then add, otherwise subtract, the inverse row
                    # direction, if the checked column is even.
                    voff  += ((1 if is_off else -1) *
                              inv_row if is_short_bot(2) else 0)
                if rot90 == 2:
                    voff    += inv_row * (2 + is_off) # OK for odd
                    
                    
            if rot90 == 0:
                if inv_col == -1 and iseven(nc): # OK
                    stagger =  not stagger
                hoff    -= (inv_col == -1) # OK
            
            if rot90 == -1: # CCW
                if inv_col == 1 and iseven(nc): # OK
                    stagger =  not stagger
                vdesc, hdesc =  hdesc, vdesc
                vdesc        =  not vdesc
                voff         += (inv_row == 1)
                hoff         -= (inv_col == 1) # OK
            
            if rot90 == 1: # CW
                if (inv_col == 1 and iseven(nc)) or isodd(nc): # OK
                    stagger =  not stagger
                vdesc, hdesc =  hdesc, vdesc
                hdesc        =  not hdesc
                hoff         -= (inv_col == -1) # OK
            
            if rot90 == -2:
                if (inv_col == -1 and iseven(nc)) or isodd(nc): # OK
                    stagger =  not stagger                
                vdesc, hdesc = not vdesc, not hdesc
                hoff    -= (inv_col == 1) # OK
                
            # Labels 
            if labels is not None:
                labmap = {
                    'auto': {
                        'hLeading': 1,'vLeading': 1,'hType': 'N','vType': 'N' },
                    'auto=numbers' : {
                        'hLeading': 1,'vLeading': 1,'hType': 'N','vType': 'N' },
                    'auto=alpha column': {
                        'hLeading': 0,'vLeading': 0,'hType': 'A','vType': 'N' },
                    'auto=alpha 2 column': {# Not supported
                        'hLeading': 1,'vLeading': 1,'hType': 'A','vType': 'N' },
                    'auto=inv y x plus 1': {
                        'hLeading': 1,'vLeading': 1,'hType': 'N','vType': 'N' },
                    'auto=x and y plus 1': {
                        'hLeading': 1,'vLeading': 1,'hType': 'N','vType': 'N' }
                }
                for l in labels.split(','): 
                    nargs.update(labmap.get(l,{}))
                    if 'alpha column' in l or 'alpha 2 column' in l:
                        hoff -= 1 # VASSAL 0->A, wargame 1->A
                    if l == 'auto=inv y x plus 1':
                        hoff += 1
                        #inv_row  =  not inv_row
                    if l == 'auto=x and y plus 1':
                        hoff -= 1
                        voff -= 1
            
            # Add margins 
            x0 += int(mx)
            y0 += int(my)
            
            targs['dx']         = hex_pw
            targs['dy']         = hex_ph
            nargs['vOff']       = voff 
            nargs['hOff']       = hoff 
            nargs['vDescend']   = vdesc
            nargs['hDescend']   = hdesc
            targs['edgesLegal'] = True
            targs['sideways']   = abs(rot90) == 1
            nargs['stagger']    = stagger
            targs['x0']         = int(x0+.5)
            targs['y0']         = int(y0+.5)
        
    # --------------------------------------------------------------------
    def getRectParams(self,i,llx,ury,width,height,targs,nargs):
        targs['dx']       = width
        targs['dy']       = height
        targs['x0']       = int(llx - width/2)
        targs['y0']       = int(ury + height/2)
        targs['color']    = rgb(0,255,0)
        nargs['color']    = rgb(0,255,0)
        nargs['vDescend'] = True
        nargs['vOff']     = -3
        nargs.update({'sep':',','vLeading':0,'hLeading':0})
        
    # ----------------------------------------------------------------
    def addZones(self,
                 zoned,
                 name,
                 info,
                 width,
                 height,
                 labels=None,
                 coords=None,
                 picinfo=None):
        '''Add zones to the Zoned element.

        Parameters
        ----------
        zoned : Zoned
            Parent element
        name : str
            Name of Zoned
        info : dict
            Dictionary of zones informatio
        width : int
            Width of parent
        height : int
            Height of parent
        labels : list
            On recursive call, list of labels
        coords : list
            On recursive call, coordinates
        picinfo : dict
            On recursive call, picture information
        '''
        grids = []
        picture = None

        with VerboseGuard(f'Adding zones to {name}') as v:
            for k, val in info.items():
                if k == 'labels': labels  = val;
                if k == 'coords': coords  = val
                if k == 'zoned':  picture = val
                if 'zone' not in k or k == 'zoned':
                    continue
            
                grids = [[k,val]] + grids  # Reverse order!
                # grids.append([k,v])
            
            if len(grids) < 1:
                return
            
            if picinfo is None:
                picinfo = self.getPictureInfo(picture,name,width,height)
                
            hex_width, hex_height, scx, scy, rot90, tran = picinfo 
                
            for g in grids:
                n, i = g
                v(f'Adding zone {n}')
            
                if 'scope' in n:
                    llx,lly = tran(*i['global lower left'])
                    urx,ury = tran(*i['global upper right'])
                    path    = [[llx,ury],[urx,ury],[urx,lly],[llx,lly]]
                    nm      = n.replace('zone scope ','')
                elif 'path' in n:
                    path    = [tran(*p) for p in i['path']]
                    llx     = min([px for px,py in path])
                    ury     = max([py for px,py in path])
                    nm      = n.replace('zone path ','')
            
                # Checkf if we have "point" type elements in this object and
                # add them to dict.
                points = [ val for k,val in i.items()
                           if (k.startswith('point') and
                               isinstance(val,dict) and \
                               val.get('type','') == 'point')]
            
                pathstr = ';'.join([f'{s[0]},{s[1]}' for s in path])
                v(f'Zone path ({llx},{ury}): {pathstr} ({len(points)})')
            
                ispool = 'pool' in n.lower() and len(points) <= 0
                zone = zoned.addZone(name           = nm,
                                     locationFormat = ("$name$"
                                                       if ispool else
                                                       "$gridLocation$"),
                                     useParentGrid  = False,
                                     path           = pathstr)
            
                # Do not add grids to pools 
                if ispool:
                    v('Board {n} is pool with no points')
                    continue
            
                targs  = {'color':rgb(255,0,0),'visible':self._visible}
                nargs  = {'color':rgb(255,0,0),'visible':self._visible}
                # print(targs,nargs)
                if 'turn' in n.lower(): nargs['sep'] = 'T'
                if 'oob' in n.lower():  nargs['sep'] = 'O'
            
                if len(points) > 0:
                    with VerboseGuard('Using region grid') as vv:
                        grid = zone.addRegionGrid(snapto  = True,
                                                  visible = self._visible)
                        for j,p in enumerate(points):
                            pn = p["name"].strip()
                            pp = p.get('parent','').strip()
                            pc = p["coords"]
                            if j == 0: vv(f'',end='')
                            vv(f'[{pn}] ',end='',flush=True,noindent=True)

                            if pn.endswith(' flipped'):
                                pn = pn[:-len(' flipped')]
                                
                            x, y = tran(*pc)
                            r = grid.addRegion(name      = pn,
                                               originx   = x,
                                               originy   = y,
                                               alsoPiece = True,
                                               prefix    = pp)
                        v('')
                    
                elif 'hex' in n.lower():
                    margin = i.get('board frame',{}).get('margin',0)
                    mx     = scx * margin
                    my     = scy * margin
                    # self.message(f'{margin} -> {scx},{scy} -> {mx},{my}')
                    w      = abs(urx - llx)-2*mx
                    h      = abs(ury - lly)-2*my
                    self.getHexParams(llx         = llx,         
                                      lly         = lly,         
                                      urx         = urx,         
                                      ury         = ury,         
                                      mx          = mx,          
                                      my          = my,          
                                      hex_width   = hex_width,   
                                      hex_height  = hex_height,  
                                      rot90       = rot90,       
                                      labels      = labels,      
                                      coords      = coords,      
                                      targs       = targs,       
                                      nargs       = nargs)       
            
                    v(f'Adding hex grid')
                    
                    grid = zone.addHexGrid(**targs)
                    grid.addNumbering(**nargs)
                    
                else:
                    width  = hex_width / HEX_WIDTH * RECT_WIDTH
                    height = hex_height / HEX_HEIGHT * RECT_HEIGHT
                    self.getRectParams(i,llx,ury,width,height,targs,nargs)
            
                    v(f'Adding rectangular grid')
                    
                    grid = zone.addSquareGrid(**targs)
                    grid.addNumbering(**nargs)
                
            
                # Once we've dealt with this grid, we should see if we have
                # any embedded zones we should deal with.
                self.addZones(zoned,name,i,width,height,
                              labels=labels,
                              coords=coords,
                              picinfo=picinfo)
            
    
    # ----------------------------------------------------------------
    def addBoards(self):
        '''Add Boards to the module
        '''
        with VerboseGuard('Adding boards') as v:
            hasFlipped = False
            for cn,cd in self._categories.get('counter',{}).items():
                for sn in cd:
                    if ' flipped' in sn:
                        hasFlipped = True
                        break
            
            v(f'Has flipped? {hasFlipped}')
            for bn, b in self._categories.get('board',{}).get('all',{}).items():
                self.addBoard(bn, b,hasFlipped=hasFlipped)


    # ----------------------------------------------------------------
    def getIcon(self,name,otherwise):
        with VerboseGuard(f'Get Icon {name}') as v:
            icon   = self._categories\
                         .get('icon',{})\
                         .get('all',{})\
                         .get(name,{
                             'filename':otherwise})['filename']
            v(f'Using "{icon}"')
            return icon
        
    # ----------------------------------------------------------------
    def addOOBs(self):
        '''Add OOBs  to the game'''
        oobc = self._categories.get('oob',{}).get('all',{}).items()
        if len(oobc) < 1:
            return

        with VerboseGuard(f'Adding OOBs') as v:
            icon   = self.getIcon('oob-icon','/images/inventory.gif')
            v(f'Using icon "{icon}" for OOB')
            charts = \
                self._game.addChartWindow(name='OOBs',
                                          hotkey      = self._oobKey,
                                          description = 'OOBs',
                                          text        = '',
                                          icon       = icon,
                                          tooltip     = 'Show/hide OOBs')
            tabs = charts.addTabs(entryName='OOBs')
            
            for on, o in oobc:
                widget = tabs.addMapWidget(entryName=on)
                self.addOOB(widget, on, o)


    # ----------------------------------------------------------------
    def addOOB(self,widget,name,info):
        '''Add a OOB elements to the game

        Parameters
        ----------
        widget : Widget
            Widget to add to
        name : str
            Name
        info : dict
            Information on the OOB image 
        '''
        map = widget.addWidgetMap(mapName   = name,
                                  markMoved = 'Never',
                                  hotkey    = '')
        map.addCounterDetailViewer()
        map.addStackMetrics()
        map.addImageSaver()
        map.addTextSaver()
        map.addForwardToChatter()     
        map.addMenuDisplayer()        
        map.addMapCenterer()          
        map.addStackExpander()        
        map.addPieceMover()           
        map.addKeyBufferer()          
        map.addSelectionHighlighters()
        map.addHighlightLastMoved()   
        map.addZoomer()
        
        picker          = map.addPicker()
        ulx,uly,lrx,lry = self.getBB(info['img'])
        board           = picker.addBoard(name  = name,
                                          image = info['filename'])
        zoned           = board.addZonedGrid()
        zoned.addHighlighter()
        
        if not 'zones' in info:
            zone = zoned.addZone(name = 'full',
                                 useParentGrid = False,
                                 path=(f'{ulx},{uly};' +
                                       f'{lrx},{uly};' +
                                       f'{lrx},{lry};' +
                                       f'{ulx},{lry}'))
            grid = zone.addSquareGrid()
            grid.addNumbering()

            return

        # If we get here, we have board info!
        w = abs(ulx-lrx)
        h = abs(uly-lry)
        self.addZones(zoned,name,info['zones'],w,h)

    # ----------------------------------------------------------------
    def addCharts(self):
        '''Add Charts elements to game
        '''
        chartc = self._categories.get('chart',{}).get('all',{}).items()
        if len(chartc) < 1:
            return

        with VerboseGuard('Adding charts') as v:
            charts = self._game.addChartWindow(name = 'Charts',
                                               hotkey = self._chartsKey,
                                               description = '',
                                               text = '',
                                               tooltip = 'Show/hide charts',
                                               icon    = self.getIcon('chart-icon',
                                                                      '/images/chart.gif'))
            tabs = charts.addTabs(entryName='Charts')
            for i, (cn, c) in enumerate(chartc):
                if i == 0: v('',end='')
                v(f'[{cn}] ',end='',flush=True,noindent=True)
            
                tabs.addChart(chartName   = cn,
                              description = cn,
                              fileName    = c['filename'])
            
            v('')

    # ----------------------------------------------------------------
    def addDie(self):
        '''Add a `Die` element to the module
        '''
        if self._dice is not None and len(self._dice) > 0:
            return
        self._game.addDiceButton(name       = '1d6',
                                 hotkey     = self._diceKey)

#
# EOF
#
# ====================================================================
# From main.py

# ====================================================================
if __name__ == '__main__':
    from argparse import ArgumentParser, FileType

    ap = ArgumentParser(description='Create draft VASSAL module')
    ap.add_argument('pdffile',
                    help='The PDF file to read images from',
                    type=FileType('r'),
                    default='export.pdf',
                    nargs='?')
    ap.add_argument('infofile',
                    help='The JSON file to read image information from',
                    type=FileType('r'),
                    default='export.json',
                    nargs='?')
    ap.add_argument('-p','--patch',
                    help='A python script to patch generated module',
                    type=FileType('r'))
    ap.add_argument('-o','--output',
                    help='Output file to write module to',
                    type=FileType('w'),
                    default='Draft.vmod')
    ap.add_argument('-t','--title',
                    help='Module title', default='Draft',
                    type=str)
    ap.add_argument('-v','--version',
                    help='Module version',
                    type=str,
                    default='draft')
    ap.add_argument('-r','--rules',
                    help='Rules PDF file',
                    type=FileType('r'))
    ap.add_argument('-T','--tutorial',
                    help='Tutorial (v)log file',
                    type=FileType('r'))
    ap.add_argument('-d','--description',
                    help='Short description of module',
                    type=str,
                    default='draft of module')
    ap.add_argument('-W','--vassal-version',
                    help='Vassal version number',
                    type=str,
                    default='3.6.7')
    ap.add_argument('-V','--verbose',
                    help='Be verbose',
                    action='store_true')
    ap.add_argument('-G','--visible-grids',
                    action='store_true',
                    help='Make grids visible in the module')
    ap.add_argument('-N','--no-nato-prototypes',
                    action='store_true',
                    help='Do not make prototypes for types,echelons,commands')
    ap.add_argument('-C','--no-chit-information',
                    action='store_true',
                    help='Do not make properties from chit information')
    ap.add_argument('-S','--counter-scale',
                    type=float, default=1,
                    help='Scale counters by factor')
    ap.add_argument('-R','--resolution',
                    type=int, default=150,
                    help='Resolution of images')


    args = ap.parse_args()
    
    vmodname  = args.output.name
    rulesname = args.rules.name    if args.rules    is not None else None
    tutname   = args.tutorial.name if args.tutorial is not None else None
    args.output.close()

    patchname = args.patch.name if args.patch is not None else None
    if args.patch is not None:
        args.patch.close()

    if args.version.lower() == 'draft':
        args.visible_grids = True

    Verbose().setVerbose(args.verbose)
        
    try:
        exporter = LaTeXExporter(vmodname      = vmodname,
                                 pdfname       = args.pdffile.name,
                                 infoname      = args.infofile.name,
                                 title         = args.title,
                                 version       = args.version,
                                 description   = args.description,
                                 rules         = rulesname,
                                 tutorial      = tutname,
                                 patch         = patchname,
                                 visible       = args.visible_grids,
                                 vassalVersion = args.vassal_version,
                                 nonato        = args.no_nato_prototypes,
                                 nochit        = args.no_chit_information,
                                 resolution    = args.resolution,
                                 counterScale  = args.counter_scale)
        exporter.run()
    except Exception as e:
        from sys import stderr 
        print(f'Failed to build {vmodname}: {e}',file=stderr)
        from os import unlink
        try:
            unlink(vmodname)
        except:
            pass

        raise e
        
#
# EOF
#
##
# End of generated script
##
