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

__version__ = "$Revision: 1.18 $"

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 simply a list of layers.

    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):
        # call 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."""
        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."""
        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"""
        return self.layers

    def HasLayers(self):
        """Return true if the map has at least one shape layer"""
        return len(self.layers) > 0

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

        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.

        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.

        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.

        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.

        Return None if there are no layers or no layer contains any shapes.
        """
        if not self.layers:
            return None
        llx = []
        lly = []
        urx = []
        ury = []
        for layer in self.layers:
            if layer is self.label_layer:
                continue
            # 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.

        Return None if there are no layers 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 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):
        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)

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

