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

__version__ = "$Revision: 1.20 $"

from messages import MAP_LAYERS_CHANGED, MAP_PROJECTION_CHANGED, \
     CHANGED, LAYER_PROJECTION_CHANGED, LAYER_LEGEND_CHANGED, \
     LAYER_VISIBILITY_CHANGED, LAYER_CHANGED, MAP_STACKING_CHANGED, \
     MAP_LAYERS_ADDED, MAP_LAYERS_REMOVED

from Thuban import _

from base import TitledObject, Modifiable

from label import LabelLayer


class Map(TitledObject, Modifiable):

    """Represent a map.

    A map is a list of layers. Additionally
    there is a special label layer containing all labels that
    are defined for the map.

    Map objects send the following message types:

        TITLE_CHANGED -- The title has changed. Parameter: the map.

        MAP_LAYERS_CHANGED -- Layers were added, removed or rearranged.
                        Parameters: the map

        MAP_PROJECTION_CHANGED -- the map's projection has changed.
                        Parameter: the map
    """

    forwarded_channels = (CHANGED,
                          LAYER_PROJECTION_CHANGED,
                          LAYER_LEGEND_CHANGED,
                          LAYER_CHANGED,
                          LAYER_VISIBILITY_CHANGED)

    def __init__(self, title, projection = None):
        """Initialize the map."""
        TitledObject.__init__(self, title)
        Modifiable.__init__(self)
        self.layers = []
        self.label_layer = LabelLayer(_("Labels"))
        self.label_layer.Subscribe(CHANGED, self.forward, MAP_LAYERS_CHANGED)
        self.projection = projection

    def Destroy(self):
        """Destroys the map object with all layers including the label layer.

        Calls Modifiable. Destroy first since it will call
        Publisher.Destroy which removes all subscriptions. Otherwise
        clearing the layers results in messages to be sent which can
        cause problems.
        """
        Modifiable.Destroy(self)
        self.ClearLayers()
        self.label_layer.Unsubscribe(CHANGED, self.forward, MAP_LAYERS_CHANGED)
        self.label_layer.Destroy()

    def AddLayer(self, layer):
        """Append layer to the map on top of all."""
        self.layers.append(layer)
        self.subscribe_layer_channels(layer)
        self.changed(MAP_LAYERS_CHANGED, self)
        self.changed(MAP_LAYERS_ADDED, self)

    def RemoveLayer(self, layer):
        """Remove layer from the map.

        This can not be applied for the label layer of the map.
        """
        self.unsubscribe_layer_channels(layer)
        self.layers.remove(layer)
        self.changed(MAP_LAYERS_CHANGED, self)
        self.changed(MAP_LAYERS_REMOVED, self)
        layer.Destroy()

    def CanRemoveLayer(self, layer):
        """Return true if the layer can be deleted.

        The default implementation always returns 1. Derived classes
        should override this method if they have e.g. special layers
        that the user should not be able to remove.
        """
        return 1

    def ClearLayers(self):
        """Delete all layers and also remove all labels from the label layer.
        """
        for layer in self.layers:
            self.unsubscribe_layer_channels(layer)
            layer.Destroy()
        del self.layers[:]
        self.label_layer.ClearLabels()
        self.changed(MAP_LAYERS_CHANGED, self)
        self.changed(MAP_LAYERS_REMOVED, self)

    def subscribe_layer_channels(self, layer):
        """Subscribe to some of layer's channels."""
        for channel in self.forwarded_channels:
            layer.Subscribe(channel, self.forward, channel)

    def unsubscribe_layer_channels(self, layer):
        """Unsubscribe to some of layer's channels."""
        for channel in self.forwarded_channels:
            layer.Unsubscribe(channel, self.forward, channel)

    def LabelLayer(self):
        """Return the Map's label layer"""
        return self.label_layer

    def Layers(self):
        """Return the list of layers contained in the map.

        The list does not include the label layer which
        can be retrieved by a separate method."""
        return self.layers

    def HasLayers(self):
        """Information whether this map has layers.

        Returns true if the map has at least one layer other
        than the label layer."""
        return len(self.layers) > 0

    def MoveLayerToTop(self, layer):
        """Put the layer on top of the layer stack.

        This can not be applied to the label layer.

        If the layer is already at the top do nothing. If the stacking
        order has been changed, issue a MAP_LAYERS_CHANGED message.
        """
        index = self.layers.index(layer)
        if index < len(self.layers) - 1:
            del self.layers[index]
            self.layers.append(layer)
            self.changed(MAP_LAYERS_CHANGED, self)
            self.changed(MAP_STACKING_CHANGED, self)

    def RaiseLayer(self, layer):
        """Swap the layer with the one above it.

        This does not apply to the label layer.

        If the layer is already at the top do nothing. If the stacking
        order has been changed, issue a MAP_LAYERS_CHANGED message.
        """
        index = self.layers.index(layer)
        if index < len(self.layers) - 1:
            del self.layers[index]
            self.layers.insert(index + 1, layer)
            self.changed(MAP_LAYERS_CHANGED, self)
            self.changed(MAP_STACKING_CHANGED, self)

    def LowerLayer(self, layer):
        """Swap the layer with the one below it.

        This does not apply to the label layer.

        If the layer is already at the bottom do nothing. If the
        stacking order has been changed, issue a MAP_LAYERS_CHANGED message.
        """
        index = self.layers.index(layer)
        if index > 0:
            del self.layers[index]
            self.layers.insert(index - 1, layer)
            self.changed(MAP_LAYERS_CHANGED, self)
            self.changed(MAP_STACKING_CHANGED, self)

    def MoveLayerToBottom(self, layer):
        """Put the layer at the bottom of the stack.

        This does not apply to the label layer.

        If the layer is already at the bottom do nothing. If the
        stacking order has been changed, issue a MAP_LAYERS_CHANGED message.
        """
        index = self.layers.index(layer)
        if index > 0:
            del self.layers[index]
            self.layers.insert(0, layer)
            self.changed(MAP_LAYERS_CHANGED, self)
            self.changed(MAP_STACKING_CHANGED, self)

    def BoundingBox(self):
        """Return the bounding box of the map in Lat/Lon coordinates.

        The label layer is not considered for the computation of the
        bounding box.

        Return None if there are no layers (except the label layer) or
        no layer contains any shapes.
        """
        if not self.layers:
            return None
        llx = []
        lly = []
        urx = []
        ury = []
        for layer in self.layers:
            # the layer's bbox may be None if it doesn't have any shapes
            bbox = layer.LatLongBoundingBox()
            if bbox is not None:
                left, bottom, right, top = bbox
                llx.append(left)
                lly.append(bottom)
                urx.append(right)
                ury.append(top)

        # check whether there were any empty layers.
        if llx:
            return (min(llx), min(lly), max(urx), max(ury))
        else:
            return None

    def ProjectedBoundingBox(self):
        """Return the bounding box of the map in projected coordinates.

        The label layer is not considered for the computation of the
        bounding box.

        Return None if there are no layers (except the label layer) or
        no layer contains any shapes.
        """
        # This simply returns the rectangle given by the projected
        # corners of the non-projected bbox.
        bbox = self.BoundingBox()
        if bbox is not None and self.projection is not None:
            bbox = self.projection.ForwardBBox(bbox)
        return bbox

    def GetProjection(self):
        """Return the projection of the map."""
        return self.projection

    def SetProjection(self, projection):
        """Set the projection of the map.

        Issue a MAP_PROJECTION_CHANGED message.
        """
        old_proj = self.projection
        self.projection = projection
        self.changed(MAP_PROJECTION_CHANGED, self, old_proj)

    def forward(self, *args):
        """Reissue events"""
        if len(args) > 1:
            args = (args[-1],) + args[:-1]
        apply(self.issue, args)

    def WasModified(self):
        """Return true if the map or one of the layers was modified"""
        if self.modified:
            return 1
        else:
            for layer in self.layers:
                if layer.WasModified():
                    return 1
            return self.label_layer.WasModified()

    def UnsetModified(self):
        """Unset the modified flag of the map and the layers"""
        Modifiable.UnsetModified(self)
        for layer in self.layers:
            layer.UnsetModified()
        self.label_layer.UnsetModified()

    def TreeInfo(self):
        """Return a description of the object.

        A tuple of (title, tupel) describing the contents
        of the object in a tree-structure is returned.
        """
        items = []
        if self.BoundingBox() != None:
            items.append(_("Extent (lat-lon): (%g, %g, %g, %g)")
                         % self.BoundingBox())
            if self.projection and len(self.projection.params) > 0:
                items.append(_("Extent (projected): (%g, %g, %g, %g)")
                             % self.ProjectedBoundingBox())
                items.append((_("Projection"),
                              [str(param)
                               for param in self.projection.params]))

        layers = self.layers[:]
        layers.reverse()
        items.extend(layers)
        items.append(self.label_layer)

        return (_("Map: %s") % self.title, items)
