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

"""
Test the interaction with the view
"""

__version__ = "$Revision: 2297 $"
# $Source$
# $Id: test_viewport.py 2297 2004-07-22 13:07:52Z bh $

import os
import unittest

import postgissupport
import support
support.initthuban()

from Thuban.UI.viewport import ViewPort, ZoomInTool, ZoomOutTool, \
                               PanTool, IdentifyTool, LabelTool

from Thuban.Model.map import Map
from Thuban.Model.proj import Projection
from Thuban.Model.layer import Layer
from Thuban.Model.session import Session
from Thuban.Model.color import Color
from Thuban.Model.postgisdb import PostGISConnection
from Thuban.UI.messages import SCALE_CHANGED, MAP_REPLACED
from Thuban.Model.messages import TITLE_CHANGED

class Event:
    pass


class MockView(ViewPort):

    def GetTextExtent(self, text):
        """Mock implementation so that the test cases work"""
        # arbitrary numbers, really just so the tests pass
        return 40, 20



class SimpleViewPortTest(unittest.TestCase):

    """Simple ViewPort tests"""

    def test_default_size(self):
        """Test ViewPort default size and scale"""
        port = ViewPort()
        try:
            self.assertEquals(port.GetPortSizeTuple(), (400, 300))
            self.assertEquals(port.scale, 1.0)
            self.assertEquals(port.offset, (0, 0))
            self.assertEquals(port.VisibleExtent(), (0.0, -300.0, 400.0, 0.0))
        finally:
            port.Destroy()

    def test_init_with_size(self):
        """Test ViewPort(<size>)"""
        port = ViewPort((1001, 1001))
        try:
            self.assertEquals(port.GetPortSizeTuple(), (1001, 1001))
            self.assertEquals(port.VisibleExtent(), (0.0, -1001.0, 1001.0, 0.0))
        finally:
            port.Destroy()

    def test_visible_extent(self):
        """Test ViewPort.VisibleExtent()"""
        class MockMap:
            def ProjectedBoundingBox(self):
                return (500, 400, 600, 500)
            # noops that the viewport expects but which aren't needed
            # here:
            Subscribe = Unsubscribe = lambda *args: None

        map = MockMap()
        port = ViewPort((1000, 1000))
        try:
            port.SetMap(map)
            # The viewport adjusts automatically to the map.  Since both
            # the map's bounding box and the viewport are square the map
            # fits exactly.
            self.assertEquals(port.VisibleExtent(), (500, 400, 600, 500))

            # Zoom in a bit
            port.ZoomFactor(2)
            self.assertEquals(port.VisibleExtent(), (525, 425, 575, 475))
        finally:
            port.Destroy()


class ViewPortTest(unittest.TestCase, support.SubscriberMixin,
                   support.FloatComparisonMixin):

    def build_path(self, filename):
        return os.path.join("..", "Data", "iceland", filename)

    def open_shapefile(self, filename):
        """Open and return a shapestore for filename in the iceland data set"""
        return self.session.OpenShapefile(self.build_path(filename))

    def setUp(self):
        self.session = Session("Test session for %s" % self.__class__)

        # make view port 1001x1001 so we have an exact center
        self.port = MockView((1001, 1001))

        proj = Projection(["proj=latlong", 
                           "to_meter=.017453292519943",
                           "ellps=clrk66"])

        self.map = map = Map("title", proj)
        layer = Layer("Polygon", self.open_shapefile("political.shp"))
        layer.GetClassification().GetDefaultGroup()\
                        .GetProperties().SetFill(Color(0,0,0))
        map.AddLayer(layer)
        layer = Layer("Point",
                      self.open_shapefile("cultural_landmark-point.shp"))
        layer.GetClassification().GetDefaultGroup()\
                        .GetProperties().SetFill(Color(0,0,0))
        map.AddLayer(layer)
        layer = Layer("Arc", self.open_shapefile("roads-line.shp"))
        layer.GetClassification().GetDefaultGroup()\
                        .GetProperties().SetFill(Color(0,0,0))
        map.AddLayer(layer)
        self.session.AddMap(map)

        self.layer = layer

        self.port.SetMap(map)
        for msg in (SCALE_CHANGED, MAP_REPLACED, TITLE_CHANGED):
            self.port.Subscribe(msg, self.subscribe_with_params, msg)
        self.clear_messages()

    def tearDown(self):
        self.port.Destroy()
        self.session.Destroy()
        self.map = self.session = self.port = self.layer = None

    def test_inital_settings(self):
        self.failIf(self.port.HasSelectedLayer())
        self.failIf(self.port.HasSelectedShapes())

    def test_win_to_proj(self):
        self.assertFloatSeqEqual(self.port.win_to_proj(0, 0),
                                 (-24.546524047851978, 70.450618743897664))
        self.assertFloatSeqEqual(self.port.win_to_proj(100, 0),
                                 (-23.442557137686929, 70.450618743897664))
        self.assertFloatSeqEqual(self.port.win_to_proj(0, 100),
                                 (-24.546524047851978, 69.346651833732622))

    def test_proj_to_win(self):
        self.assertFloatSeqEqual(self.port.proj_to_win(-24.546524047851978,
                                                       70.450618743897664),
                                 (0, 0))
        self.assertFloatSeqEqual(self.port.proj_to_win(-23.442557137686929,
                                                       70.450618743897664),
                                 (100, 0))
        self.assertFloatSeqEqual(self.port.proj_to_win(-24.546524047851978,
                                                       69.346651833732622),
                                 (0, 100))

    def test_set_map(self):
        """Test ViewPort.SetMap()"""
        # The port already has a map. So we set it to None before we set
        # it to self.map again.

        # Setting the map to None does not change the scale, but it will
        # issue a MAP_REPLACED message.
        self.port.SetMap(None)
        self.check_messages([(MAP_REPLACED,)])

        self.clear_messages()

        self.port.SetMap(self.map)
        self.check_messages([(90.582425142660739, SCALE_CHANGED),
                             (MAP_REPLACED,)])

    def test_changing_map_projection(self):
        """Test ViewPort behavior when changing the map's projection

        The viewport subscribe's to the map's MAP_PROJECTION_CHANGED
        messages and tries to adjust the viewport when the projection
        changes to make sure the map is still visible in the window.
        There was a bug at one point where the viewport couldn't cope
        with the map not having a meaningful bounding box in this
        situation.
        """
        # Create a projection and an empty map.  We can't use self.map
        # here because we do need an empty one.
        themap = Map("title", Projection(["proj=latlong",
                                          "to_meter=.017453292519943",
                                          "ellps=clrk66"]))
        # Add the map to self.session so that it's properly destroyed in
        # tearDown()
        self.session.AddMap(themap)

        # Add the map to the view port and clear the messages.  Then
        # we're set for the actual test.
        self.port.SetMap(themap)
        self.clear_messages()

        # The test: set another projection.  The viewport tries to
        # adjust the view so that the currently visible region stays
        # visible.  The viewport has to take into account that the map
        # is empty, which it didn't in Thuban/UI/viewport.py rev <= 1.16.
        # This part of the test is OK when the SetProjection call does
        # not lead to an exception.
        themap.SetProjection(Projection(["proj=latlong",
                                         "to_meter=.017453292519943",
                                         "ellps=clrk66"]))

        # If the map weren't empty the viewport might send SCALE_CHANGED
        # messages, but it must no do so in this case because the scale
        # doesn't change.
        self.check_messages([])

    def testFitRectToWindow(self):
        rect = self.port.win_to_proj(9, 990) + self.port.win_to_proj(990, 9)
        self.port.FitRectToWindow(rect)
        self.assertFloatSeqEqual(rect, self.port.win_to_proj(0, 1000)
                                 + self.port.win_to_proj(1000, 0), 1e-1)

    def testZoomFactor(self):
        self.port.FitMapToWindow()
        rect = self.port.win_to_proj(9, 990) + self.port.win_to_proj(990, 9)
        proj_rect = self.port.win_to_proj(0,1000)+self.port.win_to_proj(1000,0)
        self.port.ZoomFactor(2)
        self.port.ZoomFactor(.5)
        self.assertFloatSeqEqual(rect,
                                 self.port.win_to_proj(0, 1000)
                                 + self.port.win_to_proj(1000, 0), 1)

        point = self.port.win_to_proj(600, 600)
        self.port.ZoomFactor(2, (600, 600))
        self.assertFloatSeqEqual(point, self.port.win_to_proj(500, 500), 1e-3)
        self.port.FitMapToWindow()

        proj_rect = self.port.win_to_proj(-499, 1499)\
                    + self.port.win_to_proj(1499, -499)
        self.port.ZoomFactor(.5)
        self.assertFloatSeqEqual(proj_rect,
                                 self.port.win_to_proj(0, 1000)
                                 + self.port.win_to_proj(1000, 0), 1)

    def testZoomOutToRect(self):
        self.port.FitMapToWindow()
        rect   = self.port.win_to_proj(9, 990) + self.port.win_to_proj(990, 9)
        rectTo = self.port.win_to_proj(0, 1000) + self.port.win_to_proj(1000,
                                                                        0)
        self.port.ZoomOutToRect(rect)
        self.assertFloatSeqEqual(rect, rectTo, 1)

    def testTranslate(self):
        self.port.FitMapToWindow()
        orig_rect = self.port.win_to_proj(0,1000)+self.port.win_to_proj(1000,0)
        for delta in [(0, 0), (5, 0), (0, 5), (5,5),
                      (-5, 0), (0, -5), (-5, -5)]:
            rect = self.port.win_to_proj(0 + delta[0], 1000 + delta[1])  \
                   + self.port.win_to_proj(1000 + delta[0], 0 + delta[1])
            self.port.Translate(delta[0], delta[1])
            self.assertFloatSeqEqual(rect,
                                     self.port.win_to_proj(0, 1000)
                                     + self.port.win_to_proj(1000, 0), 1)
            self.port.Translate(-delta[0], -delta[1])
            self.assertFloatSeqEqual(rect, orig_rect, 1)

    def test_unprojected_rect_around_point(self):
        rect = self.port.unprojected_rect_around_point(500, 500, 5)
        self.assertFloatSeqEqual(rect,
                                 (-19.063379161960469, 64.924498140752377, 
                                  -18.95455127948528, 65.033326023227573),
                                 1e-1)

    def test_find_shape_at(self):
        eq = self.assertEquals
        x, y = self.port.proj_to_win(-18, 64.81418571)
        eq(self.port.find_shape_at(x, y, searched_layer=self.layer),
           (None, None))

        x, y = self.port.proj_to_win(-18.18776318, 64.81418571)
        eq(self.port.find_shape_at(x, y, searched_layer=self.layer),
           (self.layer, 610))

    def testLabelShapeAt(self):
        eq = self.assertEquals

        # select a road
        x, y = self.port.proj_to_win(-18.18776318, 64.81418571)
        eq(self.port.LabelShapeAt(x, y), False) # nothing to do
        eq(self.port.LabelShapeAt(x, y, "Hello world"), True) # add
        eq(self.port.LabelShapeAt(x, y), True) # remove

        # select a point
        x, y = self.port.proj_to_win(-19.140, 63.4055717)
        eq(self.port.LabelShapeAt(x, y), False) # nothing to do
        eq(self.port.LabelShapeAt(x, y, "Hello world"), True) # add
        eq(self.port.LabelShapeAt(x, y), True) # remove

        # select a polygon
        x, y = self.port.proj_to_win(-16.75286628, 64.67807745)
        eq(self.port.LabelShapeAt(x, y), False) # nothing to do
        eq(self.port.LabelShapeAt(x, y, "Hello world"), True) # add
        # for polygons the coordinates will be different, so
        # these numbers were copied
        x, y = self.port.proj_to_win(-18.5939850348, 64.990607973)
        eq(self.port.LabelShapeAt(x, y), True) # remove


    def test_set_pos(self):
        eq = self.assertEquals
        # set_current_position / CurrentPosition
        event = Event()
        event.m_x, event.m_y = 5, 5
        self.port.set_current_position(event)
        eq(self.port.current_position, (5, 5))
        eq(self.port.CurrentPosition(), self.port.win_to_proj(5, 5))
        self.port.set_current_position(None)
        eq(self.port.current_position, None)
        eq(self.port.CurrentPosition(), None)

        event.m_x, event.m_y = 15, 15
        self.port.MouseMove(event)
        eq(self.port.current_position, (15, 15))
        event.m_x, event.m_y = 25, 15
        self.port.MouseLeftDown(event)
        eq(self.port.current_position, (25, 15))
        event.m_x, event.m_y = 15, 25
        self.port.MouseLeftUp(event)
        eq(self.port.current_position, (15, 25))

    def testTools(self):
        eq = self.assertEquals
        event = Event()
        def test_tools(tool, shortcut):
            self.port.SelectTool(tool)
            eq(self.port.CurrentTool(), tool.Name())
            self.port.SelectTool(None)
            eq(self.port.CurrentTool(), None)
            shortcut()
            eq(self.port.CurrentTool(), tool.Name())

        test_tools(ZoomInTool(self.port), self.port.ZoomInTool)

        point = self.port.win_to_proj(600, 600)

        # one click zoom
        event.m_x, event.m_y = 600, 600
        self.port.MouseMove(event)
        self.port.MouseLeftDown(event)
        self.port.MouseLeftUp(event)
        self.assertFloatSeqEqual(point, self.port.win_to_proj(500, 500), 1e-3)
        self.port.FitMapToWindow()

        # zoom rectangle
        rect = self.port.win_to_proj(29, 970) + self.port.win_to_proj(970, 29)
        event.m_x, event.m_y = 29, 29
        self.port.MouseMove(event)
        self.port.MouseLeftDown(event)
        event.m_x, event.m_y = 970, 970
        self.port.MouseMove(event)
        self.port.MouseLeftUp(event)
        self.assertFloatSeqEqual(rect,
                                 self.port.win_to_proj(0, 1000)
                                 + self.port.win_to_proj(1000, 0), 1e-1)
        self.port.FitMapToWindow()

        test_tools(ZoomOutTool(self.port), self.port.ZoomOutTool)

        # one click zoom out
        proj_rect = self.port.win_to_proj(-499, 1499) \
                    + self.port.win_to_proj(1499, -499)
        event.m_x, event.m_y = 500, 500
        self.port.MouseMove(event)
        self.port.MouseLeftDown(event)
        self.port.MouseLeftUp(event)
        self.assertFloatSeqEqual(proj_rect,
                                 self.port.win_to_proj(0, 1000)
                                 + self.port.win_to_proj(1000, 0),1e-1)
        self.port.FitMapToWindow()

        # zoom out rectangle
        rect = self.port.win_to_proj(0, 1000) + self.port.win_to_proj(1000, 0)
        event.m_x, event.m_y = 29, 29
        self.port.MouseMove(event)
        self.port.MouseLeftDown(event)
        event.m_x, event.m_y = 970, 970
        self.port.MouseMove(event)
        self.port.MouseLeftUp(event)
        self.assertFloatSeqEqual(rect,
                                 self.port.win_to_proj(29, 970)
                                 + self.port.win_to_proj(970, 29))
        self.port.FitMapToWindow()

        test_tools(PanTool(self.port), self.port.PanTool)

        rect = self.port.win_to_proj(-25, 975) + self.port.win_to_proj(975,-25)
        event.m_x, event.m_y = 50, 50
        self.port.MouseMove(event)
        self.port.MouseLeftDown(event)
        event.m_x, event.m_y = 75, 75
        self.port.MouseMove(event)
        self.port.MouseLeftUp(event)
        self.assertFloatSeqEqual(rect,
                                 self.port.win_to_proj(0, 1000)
                                 + self.port.win_to_proj(1000, 0))

        test_tools(IdentifyTool(self.port), self.port.IdentifyTool)

        event.m_x, event.m_y = self.port.proj_to_win(-18.18776318, 64.81418571)
        self.port.MouseMove(event)
        self.port.MouseLeftDown(event)
        self.port.MouseLeftUp(event)
        eq(self.port.SelectedShapes(), [610])

        test_tools(LabelTool(self.port), self.port.LabelTool)

        # since adding a label requires use interaction with a dialog
        # we will insert a label and then only test whether clicking
        # removes the label

        x, y = self.port.proj_to_win(-19.140, 63.4055717)
        self.port.LabelShapeAt(x, y, "Hello world")
        event.m_x, event.m_y = x, y
        self.port.MouseMove(event)
        self.port.MouseLeftDown(event)
        self.port.MouseLeftUp(event)
        eq(self.port.LabelShapeAt(x, y), False) # should have done nothing

    def test_forwarding_title_changed(self):
        """Test whether ViewPort forwards the TITLE_CHANGED message of the map
        """
        self.map.SetTitle(self.map.Title() + " something to make it different")
        self.check_messages([(self.map, TITLE_CHANGED)])


class TestViewportWithPostGIS(unittest.TestCase):

    def setUp(self):
        """Start the server and create a database.

        The database name will be stored in self.dbname, the server
        object in self.server and the db object in self.db.
        """
        postgissupport.skip_if_no_postgis()
        self.server = postgissupport.get_test_server()
        self.dbref = self.server.get_default_static_data_db()
        self.dbname = self.dbref.dbname
        self.session = Session("PostGIS Session")
        self.db = PostGISConnection(dbname = self.dbname,
                                    **self.server.connection_params("user"))

        proj = Projection(["proj=latlong",
                           "to_meter=.017453292519943",
                           "ellps=clrk66"])
        self.map = Map("title", proj)

        self.port = ViewPort((1001, 1001))

    def tearDown(self):
        self.session.Destroy()
        self.port.Destroy()
        self.map.Destroy()
        self.map = self.port = None

    def test_find_shape_at_point(self):
        """Test ViewPort.find_shape_at() with postgis point layer"""
        layer = Layer("Point",
                      self.session.OpenDBShapeStore(self.db, "landmarks"))
        prop = layer.GetClassification().GetDefaultGroup().GetProperties()
        prop.SetFill(Color(0,0,0))
        self.map.AddLayer(layer)

        self.port.SetMap(self.map)

        x, y = self.port.proj_to_win(-22.54335021, 66.30889129)
        self.assertEquals(self.port.find_shape_at(x, y), (layer, 1001))

    def test_find_shape_at_arc(self):
        """Test ViewPort.find_shape_at() with postgis arc layer"""
        layer = Layer("Arc", self.session.OpenDBShapeStore(self.db, "roads"))
        self.map.AddLayer(layer)

        self.port.SetMap(self.map)

        x, y = self.port.proj_to_win(-18.18776318, 64.81418571)
        self.assertEquals(self.port.find_shape_at(x, y), (layer, 610))

    def test_find_shape_at_polygon(self):
        """Test ViewPort.find_shape_at() with postgis polygon layer"""
        layer = Layer("Poly",
                      self.session.OpenDBShapeStore(self.db, "political"))
        prop = layer.GetClassification().GetDefaultGroup().GetProperties()
        prop.SetFill(Color(0,0,0))
        self.map.AddLayer(layer)

        self.port.SetMap(self.map)

        x, y = self.port.proj_to_win(-19.78369, 65.1649143)
        self.assertEquals(self.port.find_shape_at(x, y), (layer, 144))


if __name__ == "__main__":
    support.run_tests()
