###############################################################################
# File:        guStarMap.py
# Appointment: Star map view support
# Project:     GalaxyU
# Author:      AR
# Created:     25.09.2012
# Last change: 27.10.2016
###############################################################################
import wxTools
import zx
import zxLog
import pgTools
import guReport
import pgWxDisplay
import guMain
import guApp
import guPlanetGenerator
import guPlanetView
import guStarDust
import guStarGen
import guObjects
import guTools

# Std modules
import wx
import ctypes
import os 
import platform
import sys
import math
import random
import time

import pygame.gfxdraw

from ctypes import *

#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# Map view
#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
class CMapViewFrame(wxTools.CCfgFrame):
    __displayTextScale = 5
    __displayShipGroupTextScale = 0.1
    __displayShipsScale = 2
    __displayShipsDestinationLineScale = 7.5
    __displayFlagScale = 5
    __displayRootsScale = 7.5
    __displayStarDustScale = 3
    __minStarImageSize = 10
    __flagScaledFactor = guObjects.CalcSizeFactor(10, 5)
    __mapObjectTypeStar = 1
    __mapObjectTypeStarDust = 2
    __mapObjectTypeShipGroup = 3
    __mapObjectTypeSpaceGroup = 4
    __mapObjectTypeFlag = 5

    __ShowLaunchedShipsTracksCfgItemName = "ShowLaunchedShipsTracks"
    __ShowIncomingAlienGroupsTracksCfgItemName = "ShowIncomingAlienGroupsTracks"
    __ShowIncomingOwnGroupTracksCfgItemName = "ShowIncomingOwnGroupTracks"
    
    #>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    # Internal helper class: flex line with star
    #>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    class CStarFlexLine(wxTools.CFlexLine):
        def __init__(self, aParent, aX, aY, aStar):
            wxTools.CFlexLine.__init__(self, aParent, aX, aY)
            self.Star = aStar
    #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<


    #---------------------------------------------------------------------------
    def __init__(self, parent):
        wxTools.CCfgFrame.__init__(self, parent, guApp.Application.Config, "GUI", "CMapViewFrame")
        self.Statusbar = self.CreateStatusBar()
        
        self._Report = guApp.Application.GetReport()
        
        self._StarImages = {} # List of generated star images
        self._xMin = sys.maxint
        self._yMin = self._xMin
        self._xMax = -sys.maxint - 1
        self._yMax = self._xMax

        # Currently highlighted space object
        self._highlightedSpaceObject = None
        # Star distance text
        self._starDistanceText = None
        # Flex line to point things on the screen
        self._flexLine = None
        
        # Fonts
        # (Note: the sizes are fixed due to pygame2exe problems)
        self.__StarFont = pgTools.LoadFont("Fonts/freesansbold.ttf", 10)
        self.__ShipsGroupFont = pgTools.LoadFont("Fonts/freesansbold.ttf", 10)
        
        # Image for unknown star
        unknownStarImage = guStarGen.LoadStarImage(\
            "unknown", 777, pygame.Color(50,50,50), pygame.Color(120,120,120))
        # Image for not ready star
        self.__notReadyStarImage = guStarGen.LoadStarImage(\
            "not_ready", 888, pygame.Color(255,255,255), pygame.Color(255,255,255))
        # Image for sector mark star
        sectorMarkStarImage = guStarGen.LoadStarImage(\
            "mark", 666, pygame.Color(0,0,255), pygame.Color(0,0,255))
        # Image for not ready/unknown planet
        self.__unknownPlanetImage = guStarGen.LoadPlanetImage(None)

        # Pre-created star images for small pictures (when scale is high)
        self.__smallStarImage = \
            pygame.transform.scale(\
                self.__notReadyStarImage, (self.__minStarImageSize, self.__minStarImageSize))
        self.__smallUnknownStarImage = \
            pygame.transform.scale(\
                unknownStarImage, (self.__minStarImageSize, self.__minStarImageSize))

        self.__smallSectorMarkStarImage = \
            pygame.transform.scale(\
                sectorMarkStarImage, (self.__minStarImageSize, self.__minStarImageSize))
        
        # Populate images map for unknown and mark stars only and check if we need generate images
        # All other images will be lazy loaded later
        self.__needsImagesGeneration = False
        for star in self._Report.VisibleStars:
            self.__SetBoundaries(star.X, star.Y) # Calculate bounderies
            if star.IsSectorMark():
                self._StarImages[star.Id] = sectorMarkStarImage
            elif not (star.IsOwn() or star.IsActualKnown() or self._Report.IsGM()):
                self._StarImages[star.Id] = unknownStarImage
            if not self.__needsImagesGeneration and guStarGen.NeedsImageGeneration(star):
                self.__needsImagesGeneration = True                 
        
        # Increase shown area to be filled by 'star dust' only
        spaceDelta = 1000;
        self._xMin -= spaceDelta;
        self._yMin -= spaceDelta;
        self._xMax += spaceDelta;
        self._yMax += spaceDelta;
        
        # Star dust
        self._StarDust = guStarDust.Generate(\
            len(self._Report.VisibleStars)*20, 
            zx.CArea(self._xMin, self._yMin, self._xMax - self._xMin, self._yMax - self._yMin),
            lambda sd: self.__SetBoundaries(sd.X, sd.Y))

        self.__distanceFromBorder = 30
        self.__axdelta = self._xMin - self.__distanceFromBorder
        self.__aydelta = self._yMin - self.__distanceFromBorder

        # 'Normalize' star dust
        for sd in self._StarDust:
            sd.X -= self.__axdelta
            sd.Y -= self.__aydelta
          
        self.__RefreshSpaceObjectsFromReport()

        # Local helper: creates a toolbar checkbox
        def addShowTrackCheckBoxToToolbar(aLabelName, aCfgItemName):
            def checkBoxEventFunc(event):
                # It's important to remove focus from the checkbox
                # to avoid reacting on keyboard
                self._display.SetFocus()
                # Repaint display
                self._display.Paint()

            checkBox = wx.CheckBox(toolbar, wx.ID_ANY, aLabelName)
            checkBox.SetValue(\
                wx.CHK_CHECKED if self.Cfg.GetValue(aCfgItemName, "1") == "1" \
                else wx.CHK_UNCHECKED)
            checkBox.Bind(wx.EVT_CHECKBOX, checkBoxEventFunc)
            toolbar.AddControl(checkBox);
            return checkBox

        toolbar = self.CreateToolBar()

        self.__checkBoxShowLaunchedShipsTracks = \
            addShowTrackCheckBoxToToolbar(\
                'Show launched tracks',
                self.__ShowLaunchedShipsTracksCfgItemName)

        self.__checkBoxShowIncomingAlienGroupsTracks = \
            addShowTrackCheckBoxToToolbar(\
                'Show incoming alien groups tracks',
                self.__ShowIncomingAlienGroupsTracksCfgItemName)

        self.__checkBoxShowIncomingOwnGroupsTracks = \
            addShowTrackCheckBoxToToolbar(\
                'Show incoming own group tracks',
                self.__ShowIncomingOwnGroupTracksCfgItemName)

        toolbar.Realize()

        # Create pygame display
        area = zx.CArea(\
            0, 0, self._xMax - self._xMin + self.__distanceFromBorder * 2,
            self._yMax - self._yMin + self.__distanceFromBorder * 2);
        self._display = pgWxDisplay.CPygameDisplay(\
            self, -1, self.PygameDraw, area, zx.CArea(), 0.01, 10, 0.05)
        self._display.SetBackgroundColour(wx.Colour(0,0,0))

        # Assign events to the display
        self._display.OnMouseMoveEvent = self.OnMouseMoveEvent
        self._display.OnMouseLeftDownEvent = self.OnMouseLeftDownEvent
        self._display.OnMouseLeftUpEvent = self.OnMouseLeftUpEvent
        self._display.OnMouseWheelEvent = self.OnMouseWheelEvent
        self._display.OnMouseLeftDoubleClickEvent = self.OnMouseLeftDoubleClickEvent
        self._display.OnScaleChanged = lambda a: self.__DisplayScale(a)

        self._display.Bind(wx.EVT_RIGHT_DOWN, self.OnMouseRightDownEvent)

        # Set delegate to control display change when trere are flex objects
        self._display.FuncCanChangeDisplay = self.CanChangeDisplay

        # Read scale and initial position from Cfg
        scale = float(self.Cfg.GetValue("scale", "0"))
        if scale <= 0:
            # Initial scale calculation
            scale = 0.01
            while (self._xMax - self._xMin + 1)/scale >= self.GetClientSize().GetWidth() or \
                  (self._yMax - self._yMin + 1)/scale >= self.GetClientSize().GetHeight():
                scale += 0.01
            print 'Current scale = ' + str(scale)

        # Make it re-draw properly
        self._display.SetScale(scale)

        # Get initial position from Cfg
        xPos = float(self.Cfg.GetValue("xPos", "0"))
        yPos = float(self.Cfg.GetValue("yPos", "0"))
        xv, yv = self._display.GetViewStart()
        
        # Recalculate position according to possibly changed area
        virtualWidth = float(self.Cfg.GetValue("virtualWidth", "0"))
        virtualHeight = float(self.Cfg.GetValue("virtualHeight", "0"))
        
        virtW, virtH = self._display.GetVirtualSize() # current virtual size
        if virtualWidth != 0 and virtW != 0:
            xPos *= virtW/virtualWidth
        if virtualHeight != 0 and virtH != 0:
            yPos *= virtH/virtualHeight
        # Scroll to initial position
        self._display.Scroll(xPos, yPos)
        
        # Set delegate to parent class to save config
        self.OnSaveCfg = self.__OnSaveCfgEvent

        # Min window size
        self.SetMinSize((400, 400))
        
        self._display.SetFocus()
        
        self.SetTitle("Star Map for " + self._Report.NationName + ", turn " + str(self._Report.Turn))
        
        # Do not close until application exit because other modules uses the map for some actions
        # This frame is MANDATORY for whole life of the application
        self.NeedVetoOnClose = True

        wxTools.PublisherSubscribe(\
            self.__MessageRefreshListener, guApp.MESSAGE_REFRESH_STAR_MAP_DATA)

        # Run generation process, if requered
        if self.__needsImagesGeneration:
            guStarGen.StartImagesGenerationProcess()

    #---------------------------------------------------------------------------
    def __RefreshSpaceObjectsFromReport(self): 
        # 'Normalize' report coordinates to (0,0) basis to simplify calculations
        self._Report.NormalizeCoordinates(self.__axdelta, self.__aydelta)

        # Reset scaled images hash
        self.__scaledImages = {}

        # Add stars to clasters
        self.__mapClasters = {}
        self.__mapClasterSize = len(self._Report.VisibleStars)/5
        if self.__mapClasterSize < 100:
            self.__mapClasterSize = 100

        for o in self._Report.VisibleStars:
            guTools.AddObjectToClaster(\
                self.__mapClasters,
                self.__mapClasterSize, o,
                self.__mapObjectTypeStar)
        # Add star dust
        for o in self._StarDust:
            guTools.AddObjectToClaster(\
                self.__mapClasters,
                self.__mapClasterSize, o,
                self.__mapObjectTypeStarDust)
        # Add ship groups                
        self.__shipGroupsWithDestination = []
        for o in reversed(self._Report.GetShipGroups()):
            guTools.AddObjectToClaster(\
                self.__mapClasters,
                self.__mapClasterSize, o,
                self.__mapObjectTypeShipGroup)
            if o.Ships[0].GetDestinationStar() is not None:
                self.__shipGroupsWithDestination.append(o)
        # Add space groups
        self.__spaceGroupsWithDestination = []
        for o in self._Report.SpaceGroups:
            guTools.AddObjectToClaster(\
                self.__mapClasters,
                self.__mapClasterSize, o,
                self.__mapObjectTypeSpaceGroup)
            if o.Destination != 0:
                self.__spaceGroupsWithDestination.append(o)


    #-----------------------------------------------------------------------
    def __MessageRefreshListener(self, arg = ""):
        self.__RefreshSpaceObjectsFromReport()
        self._display.RePaint()


    #---------------------------------------------------------------------------
    def __DisplayScale(self, aScale):
        # Note: displaying in the ToolBar has an unpleasant side effect - blinking
        # So, just trace it
        s = "StarMap Scale=" + "{0:.2f}".format(aScale)
        if int(guApp.Application.GetLog().Level) >= zxLog.LEVEL_DEBUG:
            guApp.Application.Log(zxLog.LOG_INFO, s, zxLog.LEVEL_DEBUG)
        else:
            print s

        # Enable/disable trace checkboxes
        self.__checkBoxShowLaunchedShipsTracks.Enable(\
            aScale <= self.__displayShipsDestinationLineScale)
        rootFlag = aScale <= self.__displayRootsScale
        self.__checkBoxShowIncomingOwnGroupsTracks.Enable(rootFlag)
        self.__checkBoxShowIncomingAlienGroupsTracks.Enable(rootFlag)


    #---------------------------------------------------------------------------
    def __UpdateStatusBar(self, aText):
        self.Statusbar.SetStatusText(aText)

    #---------------------------------------------------------------------------
    # OnClose event delegated to parent wxTools.CCfgFrame
    def __OnSaveCfgEvent(self):
        # Store scale
        self.Cfg.SetValue("scale", self._display.GetScale())

        # Store screen position
        xv, yv = self._display.GetViewStart()
        self.Cfg.SetValue("xPos", xv)
        self.Cfg.SetValue("yPos", yv)

        # 'Virtual' drawing width
        w, h = self._display.GetVirtualSize()
        self.Cfg.SetValue("virtualWidth", w)
        self.Cfg.SetValue("virtualHeight", h)

        # Tracks visibility
        self.Cfg.SetValue(\
            self.__ShowLaunchedShipsTracksCfgItemName,
            "1" if self.__checkBoxShowLaunchedShipsTracks.IsChecked() else "0") 
        self.Cfg.SetValue(\
            self.__ShowIncomingAlienGroupsTracksCfgItemName,
            "1" if self.__checkBoxShowIncomingAlienGroupsTracks.IsChecked() else "0") 
        self.Cfg.SetValue(\
            self.__ShowIncomingOwnGroupTracksCfgItemName,
            "1" if self.__checkBoxShowIncomingOwnGroupsTracks.IsChecked() else "0") 


    #---------------------------------------------------------------------------
    # Helper: removes star distance text
    def __DeleteStarDistanceText(self):
        if self._starDistanceText is not None:
            self._starDistanceText.Destroy()
            self._starDistanceText = None

    #---------------------------------------------------------------------------
    # Helper subfunction: sets boundaries
    def __SetBoundaries(self, x, y):
        if self._xMin > x:
            self._xMin = x
        if self._yMin > y:
            self._yMin = y
        if self._xMax < x:
            self._xMax = x
        if self._yMax < y:
            self._yMax = y

    #---------------------------------------------------------------------------
    # Helper: calculates size depending on scale
    def __CalcScaledSize(self, aKoef, aMinSize):
        d = aKoef/self._display.GetScale()
        return aMinSize if d < aMinSize else d

    #---------------------------------------------------------------------------
    # Helper: finds a space object on the screen by coordinates
    # Returns object or None if not found
    def __FindSpaceObjectOnScreen(self, xScreen, yScreen):
        def FindInList(aList):
            for so in aList:
                r = so.CalcOnScreenDiameter(self.__CalcScaledSize)/2
                x = self._display.VirtualToScreenX(so.X)
                y = self._display.VirtualToScreenY(so.Y)
                if xScreen >= x - r and xScreen <= x + r and yScreen >= y - r and yScreen <= y + r:
                    return so
            return None
        
        # Try groups first - they are smaller and can be overlapped by bigger stars
        so = FindInList(self._Report.SpaceGroups)
        # Then try ship groups
        if so is None and self._display.GetScale() <= self.__displayShipsScale:
            so = FindInList(self._Report.GetShipGroups())
        # Now try stars    
        return so if so is not None else FindInList(self._Report.VisibleStars)


    #---------------------------------------------------------------------------
    # Shows Space object in the center of the screen
    def ShowSpaceObject(self, aId):
        if aId is None:
            return
            
        # Find the star
        so = zx.Find(self._Report.VisibleStars, lambda e: e.Id == aId)
        if so is None:
            so = zx.Find(self._Report.GetShipGroups(), lambda e: e.Id == aId)
        if so is None:
            so = zx.Find(self._Report.SpaceGroups, lambda e: e.Id == aId)
        if so is not None:
            # Rescale
            if guApp.Application.Config.GetItem("GUI.ZoomWhenLocateOnMap", "1") == "1":
                self._display.SetScale(0.07)

            # Calculate screen centre coordinates
            xScreenCentre = self._display.GetParent().GetClientSize().GetWidth()/2
            yScreenCentre = self._display.GetParent().GetClientSize().GetHeight()/2
            
            # Get screen coordinates of the object
            xScreenObject = self._display.VirtualToScreenX(so.X)
            yScreenObject = self._display.VirtualToScreenY(so.Y)
            
            # Scroll to the centre
            x, y = self._display.GetViewStart()
            self._display.Scroll(x + (xScreenObject - xScreenCentre), y + (yScreenObject - yScreenCentre))
            self.Raise()
            

    #---------------------------------------------------------------------------
    # Returns star image by id (temporary or ready image)
    def GetStarImage(self, aStarId):
        if not aStarId in self._StarImages:
            self._StarImages[aStarId] = \
                guStarGen.LoadStarImage(str(aStarId), None, None, None, self.__notReadyStarImage)
        return self._StarImages[aStarId]

    
    #---------------------------------------------------------------------------
    # Returns a planet image for aStarId
    def GetPlanetImage(self, aStarId):
        return guStarGen.LoadPlanetImage(self._Report.FindStar(aStarId), self.__unknownPlanetImage)


    #---------------------------------------------------------------------------
    def PygameDraw(self, aScreen):
        # Clear screen
        aScreen.fill((0,0,0))

        scale = self._display.GetScale()
        # Scalable font (temporary locked)
        #fontSize = int(12 + 1/(8*scale))
        font = self.__StarFont
        shipGroupsFont = self.__ShipsGroupFont

        # Collect visible clasters
        visibleClasters = []
        flagAdjustingKoef = (30/scale)/3 # counts flag to be not cut
        if flagAdjustingKoef < 22:
            flagAdjustingKoef = 22
        if flagAdjustingKoef > 100:
            flagAdjustingKoef = 100
        for x, col in self.__mapClasters.items():
            for y, row in col.items():
                clusterRight = x + self.__mapClasterSize
                clusterBottom = y + self.__mapClasterSize
                x1, y1 = self._display.VirtualToScreenX(x),\
                         self._display.VirtualToScreenY(y)
                x2, y2 = self._display.VirtualToScreenX(clusterRight),\
                         self._display.VirtualToScreenY(clusterBottom)
                if not self._display.AreOffScreen(
                        x1 - flagAdjustingKoef,
                        y1 - flagAdjustingKoef,
                        x2 + flagAdjustingKoef,
                        y2 + flagAdjustingKoef):
                    visibleClasters.append(row)
        
        # Star dust
        #----------
        if scale <= self.__displayStarDustScale:
            for claster in visibleClasters:
                if self.__mapObjectTypeStarDust in claster:
                    guStarDust.Draw(\
                        claster[self.__mapObjectTypeStarDust], self._display._screen, 
                        lambda sd: \
                            (self._display.VirtualToScreenX(sd.X), self._display.VirtualToScreenY(sd.Y)))

        # Stars
        #------
        needShowFlag = scale <= self.__displayFlagScale
        for claster in visibleClasters:
            if self.__mapObjectTypeStar not in claster:
                continue;
            for star in claster[self.__mapObjectTypeStar]:
                # Get screen coordinates
                screenX, screenY = self._display.VirtualToScreenX(star.X), self._display.VirtualToScreenY(star.Y)
                # Calc diameter
                d = star.CalcOnScreenDiameter(self.__CalcScaledSize)
                if not self._display.IsVisibleOnScreen(\
                    screenX,
                    screenY,
                    d + (d+d if needShowFlag else 0)): # Note: give more space for flag
                    continue   
                
                ownership = guReport.CStarOwnership(star)
                starOwner = star.GetOwner()
                
                # Calculate a colour of text
                textColour = self._Report.GetStateColour(star)
                
                # Display star image
                image = \
                    (self.__smallStarImage \
                        if ownership.IsOwn or ownership.IsActualKnown \
                        else (self.__smallSectorMarkStarImage if star.IsSectorMark() else self.__smallUnknownStarImage)) \
                            if d <= self.__minStarImageSize \
                            else \
                                guTools.GetObject(\
                                    self.__scaledImages,
                                    star.Id, int(d), self.__mapObjectTypeStar,\
                                    lambda: pygame.transform.scale(self.GetStarImage(star.Id), (int(d), int(d))))
                    
                starRect = image.get_rect()
                starRect.centerx = screenX
                starRect.centery = screenY
                # Mark battle (under star image)
                if star.HasBattle():
                    # Get a number of fire actions to estimate a scale and set line size
                    n = 0
                    for seq in star.BattleSequences:
                        n += len(seq.FireActions) + len(seq.MissileHits)
                    lineSize = int(starRect.width/2 + math.pow(n, 0.3))
                    # Get remaining participants (in the last sequence)
                    seq = star.BattleSequences[-1]
                    participants = {} # set of participants
                    for unit in seq.Units:
                        # Extract nation name from unit id
                        name = unit.GetNation()
                        if name in participants:
                            participants[name] += 1
                        else:
                            participants[name] = 0
                    # Draw 'battle cross' for color of max present participant
                    n = None
                    for k in participants.keys():
                        n = k if n is None or participants[n] < participants[k] else n
                        
                    c = self._Report.GetNationColour(n)
                    pygame.draw.line(aScreen, c,
                                     (starRect.centerx - lineSize, starRect.centery - lineSize), 
                                     (starRect.centerx + lineSize, starRect.centery + lineSize))
                    pygame.draw.line(aScreen, c,
                                     (starRect.centerx - lineSize, starRect.centery + lineSize), 
                                     (starRect.centerx + lineSize, starRect.centery - lineSize))
                # Mark Bombing
                if star.HasBombing():
                    # Detect colour
                    # If owner is the report nation it means the nation is being bombed - enemy colour
                    # If attacker is the report nation then - nation colour
                    # otherwise - neutral colour
                    if star.BombRecords[0].Owner == self._Report.NationName:
                        c = self._Report.ENEMY_COLOUR
                    elif star.BombRecords[0].Attacker == self._Report.NationName:
                        c = self._Report.NATION_COLOUR
                    else:
                        c = self._Report.NEUTRAL_COLOUR
                    pygame.draw.circle(aScreen, c, (int(screenX), int(screenY)), int(d/1.5), 1);
                    
                # Mark Ground battles
                if star.HasGroundBattles():
                    # Detect colour
                    # If owner is the report nation and there is still population - that means the Nation has won: Nation colour
                    # If owner is the report nation and there is NO population - that means the Nation has lost: Enemy colour
                    # If attacker is the report nation and there is still population - that means the Nation has lost: Enemy colour
                    # If attacker is the report nation and there is no population - that means the Nation has won: Nation colour
                    # Otherwise - neutral colour
                    if star.GroundBattles[0].OwnerNationName == self._Report.NationName:
                        c = self._Report.NATION_COLOUR if star.Population > 0 else self._Report.ENEMY_COLOUR
                    elif star.GroundBattles[0].InvaderNationName == self._Report.NationName:
                        c = self._Report.ENEMY_COLOUR if star.Population > 0 else self._Report.NATION_COLOUR
                    else:
                        c = self._Report.NEUTRAL_COLOUR
                        
                    lineSize = starRect.width*1.5
                    pygame.draw.line(aScreen, c,
                                     (starRect.centerx - lineSize, starRect.centery), 
                                     (starRect.centerx + lineSize, starRect.centery))
                    pygame.draw.line(aScreen, c,
                                     (starRect.centerx, starRect.centery - lineSize), 
                                     (starRect.centerx, starRect.centery + lineSize))

                # Draw star
                aScreen.blit(image, starRect)
                
                # Display text
                if scale <= self.__displayTextScale:
                    font.set_bold(ownership.IsOwn or ownership.IsActualKnown)
                    font.set_underline(star.IsBanned())
                    starLabelText = star.GetFullName()
                    if star.IsBombingBanned():
                        starLabelText = "(" + starLabelText + ")"
                    if ownership.IsOwn:
                        maxPopReached = star.Population >= star.Size
                        maxIndReached = star.Industry >= star.Size
                        if maxPopReached or maxIndReached:
                            starLabelText += " {"
                            if maxPopReached and maxIndReached:
                                starLabelText += "MAX"
                            elif maxPopReached:
                                starLabelText += "POPMAX"
                            elif maxIndReached:
                                starLabelText += "INDMAX"
                            starLabelText += "}"
                    # Find any attached triggers
                    triggers = zx.GetSubListFor(\
                      self._Report.AdditionalData,
                      lambda x: x.Type == "OnStarBeforeTurn" and x.Id == str(star.Id))
                    triggerNames = ""
                    for ds in triggers:
                      triggerNames += ("," if triggerNames != "" else "" + ds.Name)
                    if len(triggerNames) > 0:
                      starLabelText += "<" + triggerNames + ">"
                                
                    pgTools.DrawText(aScreen, starLabelText, screenX, screenY + d/2, font, textColour, True)

                # Display flag
                if needShowFlag and (ownership.IsOwn and not self._Report.IsGM() or starOwner != ""):
                    d = self.__CalcScaledSize(self.__flagScaledFactor, 10)
                    flagImage = guTools.GetObject(\
                        self.__scaledImages,
                        starOwner, d, self.__mapObjectTypeFlag,\
                        lambda: pygame.transform.smoothscale(\
                            guApp.Application.GetReport().GetNationFlagImage(starOwner), (int(d*4.0/3), int(d))))
                    guTools.DrawNationFlag(\
                        aScreen, starOwner, starRect.centerx,
                        starRect.centery - starRect.height/2, textColour,
                        True, flagImage)


        # Display ship groups
        #--------------------
        if scale <= self.__displayShipsDestinationLineScale and \
                self.__checkBoxShowLaunchedShipsTracks.IsChecked():
            for sg in self.__shipGroupsWithDestination:
                screenX, screenY = \
                    self._display.VirtualToScreenX(sg.X), self._display.VirtualToScreenY(sg.Y)

                # Line to the destination
                star = sg.Ships[0].GetDestinationStar()
                if star is not None:
                    sx = self._display.VirtualToScreenX(star.X)
                    sy = self._display.VirtualToScreenY(star.Y)
                    if not self._display.AreOffScreen(screenX,screenY, sx,sy):
                        pygame.draw.line(\
                            aScreen,
                            self._Report.GetNationColour(\
                                sg.Ships[0].NationName, True), 
                                (screenX, screenY), (sx, sy), 1)

        if scale <= self.__displayShipsScale:
            for claster in visibleClasters:
                if self.__mapObjectTypeShipGroup not in claster:
                    continue;
                for sg in claster[self.__mapObjectTypeShipGroup]:
                    screenX, screenY = self._display.VirtualToScreenX(sg.X), self._display.VirtualToScreenY(sg.Y)
                    d = sg.CalcOnScreenDiameter(self.__CalcScaledSize)
                    if not self._display.IsVisibleOnScreen(screenX, screenY, d):
                        continue   
            
                    shipColour = self._Report.GetNationColour(sg.Ships[0].NationName) 

                    def GetShipImage():
                        shipImage = self._Report.GetShipImage(\
                            sg.Ships[0].Components, sg.Ships[0].NationName, sg.Ships[0].TypeName, shipColour)
                        k = shipImage.get_width()/shipImage.get_height()
                        return pygame.transform.scale(shipImage, (int(d), int(d)/k))

                    shipImage = guTools.GetObject(\
                                    self.__scaledImages,
                                    guObjects.CShipType.GetTypeKey(sg.Ships[0].NationName, sg.Ships[0].TypeName),
                                    d, self.__mapObjectTypeShipGroup, lambda: GetShipImage())

                    if not sg.IsFriendly():
                        shipImage = pygame.transform.rotate(shipImage, 180)
                    shipRect = shipImage.get_rect();
                    shipRect.centerx = screenX
                    shipRect.centery = screenY
                    aScreen.blit(shipImage, shipRect)

                    # Display a number of ships
                    if scale <= self.__displayShipGroupTextScale:
                        if not sg.SuppressLabel:
                            label = (str(len(sg.Ships)) if len(sg.Ships) > 1 \
                                                        else sg.Ships[0].GetFullShipName()) + \
                                    " (" + sg.Ships[0].NationName + ")" +\
                                    ((" (fleet: " + sg.Ships[0].FleetName + ")") \
                                        if sg.Ships[0].FleetName != "" else "")
                        else:
                            label = ("X " + str(len(sg.Ships))) if len(sg.Ships) > 1 else ""
                               
                        if len(label) > 0:         
                            text = shipGroupsFont.render(label, True, shipColour)
                            textRect = text.get_rect()
                            textRect.centery = shipRect.centery
                            if sg.IsFriendly():
                                textRect.right = shipRect.left - 3
                            else:
                                textRect.left = shipRect.right + 3
                            aScreen.blit(text, textRect)
                                    
        # Space groups
        #-------------

        # Line to destinaton
        if scale <= self.__displayRootsScale and \
                (self.__checkBoxShowIncomingOwnGroupsTracks.IsChecked() or \
                 self.__checkBoxShowIncomingAlienGroupsTracks.IsChecked()):
            for group in self.__spaceGroupsWithDestination:
                x = self._display.VirtualToScreenX(group.X)
                y = self._display.VirtualToScreenY(group.Y)
                star = group.GetDestinationStar()
                if star is not None:
                    sx = self._display.VirtualToScreenX(star.X)
                    sy = self._display.VirtualToScreenY(star.Y)
                    if not self._display.AreOffScreen(x,y, sx,sy):
                        if group.IsMyGroup:
                            if not self.__checkBoxShowIncomingOwnGroupsTracks.IsChecked():
                                continue
                            c = (0, 0, 150)
                        elif group.Destination != 0:
                            if not self.__checkBoxShowIncomingAlienGroupsTracks.IsChecked():
                                continue
                            c = (150, 0, 0)
                        else:
                            if not self.__checkBoxShowIncomingOwnGroupsTracks.IsChecked():
                                continue
                            c = (150, 0, 150)
                        pygame.draw.line(aScreen, c, (x,y), (sx, sy), 1)

        # Display groups
        for claster in visibleClasters:
            if self.__mapObjectTypeSpaceGroup not in claster:
                continue;
            for group in claster[self.__mapObjectTypeSpaceGroup]:
                x = self._display.VirtualToScreenX(group.X)
                y = self._display.VirtualToScreenY(group.Y)
                sz = int(group.CalcOnScreenDiameter(self.__CalcScaledSize))
                if sz < 5:
                    sz = 5

                if self._display.IsVisibleOnScreen(x, y, sz):
                    if group.IsMyGroup:
                        c = (0, 100, 255)
                    elif group.Destination != 0:
                        c = (255, 0, 0)
                    else:
                        c = (255, 0, 255)
        
                    # Draw the group
                    groupImage = guTools.GetObject(\
                                    self.__scaledImages,
                                    c, int(sz), self.__mapObjectTypeSpaceGroup,\
                                    lambda: pygame.transform.scale(group.CreateImage(c), (sz, sz)))

                    groupRect = groupImage.get_rect()
                    groupRect.centerx = x
                    groupRect.centery = y
                    aScreen.blit(groupImage, groupRect)


    #---------------------------------------------------------------------------
    def OnMouseMoveEvent(self, event):
        # Select a star, pointed by mouse
        star = self.__FindSpaceObjectOnScreen(event.GetX(), event.GetY())
        if star is not None and self._display._flexFrame is None: # when flex frame is active - no star selection!
            # Found a star
            # Note: to avoid of dirty painting we allow the highlighting only if flex line is not created yet or mouse is not down (means scrolling)
            if (self._highlightedSpaceObject is None or star.Id != self._highlightedSpaceObject.SpaceObject.Id) and \
               (not event.LeftIsDown() or self._flexLine is not None):
                # Selected star is to be change
                self.__DeleteStarDistanceText()
                self._highlightedSpaceObject = guTools.CHighlightedSpaceObject(\
                    self._display, 
                    self._display.VirtualToScreenX(star.X), 
                    self._display.VirtualToScreenY(star.Y), 
                    star, 
                    star.CalcOnScreenDiameter(self.__CalcScaledSize),
                    self.__UpdateStatusBar)
                self._highlightedSpaceObject.Draw()
        elif self._highlightedSpaceObject is not None:
            self.__DeleteStarDistanceText()
            self._highlightedSpaceObject = None

        # Drawing a flex line if defined
        if event.LeftIsDown() and self._flexLine is not None:
            # (Avoid drawing if coordinates are negative - bad visual effect)
            if event.GetX() < 0 or event.GetY() < 0:
                return True
                
            self._flexLine.Draw(event.GetX(), event.GetY())
           
            if self._highlightedSpaceObject is not None and \
               self._highlightedSpaceObject.SpaceObject.Id != self._flexLine.Star.Id and \
               self._highlightedSpaceObject.SpaceObject.GetShortDesc() != "":
                # We pointing now to another star - calculate distance
                if self._starDistanceText is None:
                    d = star.CalcOnScreenDiameter(self.__CalcScaledSize)

                    sign = -1 if star.Y < self._flexLine.Star.Y else 1
                    self._starDistanceText = wx.StaticText(\
                        self._display, 
                        label = "From " + self._flexLine.Star.GetShortDesc() + " to " + star.GetShortDesc() + ": " + \
                                str("%.2f" % self._Report.CalcDistanceBetweenSpaceObjects(self._highlightedSpaceObject.SpaceObject, \
                                    self._flexLine.Star)),
                        pos = (self._display.VirtualToScreenX(star.X) - d/2, 
                                self._display.VirtualToScreenY(star.Y) + sign*d/2 + sign*20 + (-10 if sign > 0 else -5)))

                    self._starDistanceText.SetForegroundColour((0,255,100)) # set text color
                    self._starDistanceText.Bind(wx.EVT_LEFT_UP, self.OnMouseLeftUpEvent)
            else:
                self.__DeleteStarDistanceText()
               
            return True # stop processing
        return False


    #---------------------------------------------------------------------------
    def OnMouseRightDownEvent(self, event):
        # Find a space object and process a menu
        so = self.__FindSpaceObjectOnScreen(event.GetX(), event.GetY())
        if so is not None:
            menu = so.GetPopupMenu(self)
            if menu is not None:
                self.PopupMenu(menu, event.GetPosition())

        event.Skip()
    
    
    #---------------------------------------------------------------------------
    def OnMouseLeftDownEvent(self, event):
        if self._highlightedSpaceObject is not None and self._flexLine is None:
            if self._highlightedSpaceObject.SpaceObject.GetShortDesc() != "":
                # Note, self._highlightedSpaceObject.SpaceObject.GetShortDesc() == "" meanse 'no flex line is needed'
                # because no information is provided
                self._flexLine = self.CStarFlexLine(\
                    self._display, 
                    self._display.VirtualToScreenX(self._highlightedSpaceObject.SpaceObject.X), 
                    self._display.VirtualToScreenY(self._highlightedSpaceObject.SpaceObject.Y), 
                    self._highlightedSpaceObject.SpaceObject)
            else:
                # Remove selected star (it must be a case when no flex line)
                # to avoid 'dirty' painting
                self._highlightedSpaceObject = None                                                 
            return True # stop processing in any case
        return False

    #---------------------------------------------------------------------------
    def OnMouseLeftUpEvent(self, event):
        if self._flexLine is not None:
            self._flexLine = None

        self.__DeleteStarDistanceText()

        return False # continue with other processing

    #---------------------------------------------------------------------------
    def OnMouseWheelEvent(self, event):
        # Remove all flex objects
        self.OnMouseLeftUpEvent(event) # removing flexline and distance text
        if self._highlightedSpaceObject is not None:
            self._highlightedSpaceObject = None

        return False # continue with other processing


    #---------------------------------------------------------------------------
    def OnMouseLeftDoubleClickEvent(self, event):
        # Displaying colony info
        if self._highlightedSpaceObject is not None and \
                isinstance(self._highlightedSpaceObject.SpaceObject, guObjects.CStar):
            guPlanetView.ShowPlanet(self._highlightedSpaceObject.SpaceObject.Id)
        return True
    
    #---------------------------------------------------------------------------
    # Delegate to 'pygame display'
    def CanChangeDisplay(self):
        return self._flexLine is None and \
            self._highlightedSpaceObject is None and self._starDistanceText is None
#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

