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

"""
Test the Thuban-specific Projection class
"""

__version__ = "$Revision: 2698 $"
# $Source$
# $Id: test_proj.py 2698 2006-09-18 00:56:26Z bernhard $

import unittest
import locale
import os

import localessupport
import xmlsupport
import support
support.initthuban()

from Thuban import _
from Thuban.Model.proj import Projection, ProjFile, \
     PROJ_UNITS_METERS, PROJ_UNITS_DEGREES
from Thuban.Model.messages import PROJECTION_ADDED, PROJECTION_REMOVED, \
     PROJECTION_REPLACED
import Thuban.Model.resource as resource

from xmlsupport import sax_eventlist

from xml.sax import SAXParseException


class TestProjection(unittest.TestCase, support.FloatComparisonMixin):

    """Test cases for the Thuban-specific Projection class
    """

    def test(self):
        """Test Projection"""
        params = ["zone=26", "proj=utm", "ellps=clrk66"]
        proj = Projection(params)
        self.assertEquals(proj.params, params)

        # It's not clear whether this value is really the correct one
        # but a test failure here probably still means a bug somewhere
        self.assertFloatSeqEqual(proj.Forward(0, 0),
                                 [3623101.8103431347, 0.0],
                                 epsilon = 1e-5)
        self.assertFloatSeqEqual(proj.Inverse(3623101.8103431347, 0.0),
                                 [-0.00065775699878736467, 0])

        self.assertFloatSeqEqual(proj.ForwardBBox((0, 0, 2, 2)),
                                 (3620891.3077618643, 0.0,
                                  3875381.8535437919, 252962.10480170773),
                                 epsilon = 1e-5)
        self.assertFloatSeqEqual(proj.InverseBBox((3620891.3077618643, 0.0,
                                                   3875381.8535437919,
                                                   252962.10480170773)),
                                 (-0.018341599754143501, 0.0,
                                  2.017992992681688, 2.0377390677846736),
                                 epsilon = 1e-5)

        # GetName()
        self.assertEquals(proj.GetName(), _("Unknown"))

        # GetParameter()
        self.assertEquals(proj.GetParameter("zone"), "26")
        self.assertEquals(proj.GetParameter("proj"), "utm")
        self.assertEquals(proj.GetParameter("ellps"), "clrk66")
        self.assertEquals(proj.GetParameter("hallo"), "")

        # GetAllParameters()
        self.assertEquals(proj.GetAllParameters(), params)

        # GetName()
        proj = Projection(params, "MyName")
        self.assertEquals(proj.GetName(), "MyName")

    def test_get_parameter_without_equals_sign(self):
        """Test Projection.GetParameter() for a parameter without '=' sign"""
        proj = Projection(["proj=utm", "zone=34", "south", "ellps=clrk66"])
        # The Projection class pretends that for parameters specified
        # without a value the value is the same as the parameter name.
        self.assertEquals(proj.GetParameter("south"), "south")

    def test_get_projection_units_geo(self):
        """Test Projection.GetProjectedUnits() for geographic projection.
        Test for the alias 'longlat' as well.
        """
        proj = Projection(["proj=latlong", "to_meter=0.017453292519943295",
                           "ellps=clrk66"])
        self.assertEquals(proj.GetProjectedUnits(), PROJ_UNITS_DEGREES)
        proj = Projection(["proj=longlat", "to_meter=0.017453292519943295",
                           "ellps=clrk66"])
        self.assertEquals(proj.GetProjectedUnits(), PROJ_UNITS_DEGREES)

    def test_lc_numeric_robustness(self):
        """Test if an LC_NUMERIC local with comma as decimal_point will work.

        Some versions of proj are not robust against this.
        Starting with Python 2.4 there is a different behaviour when
        calling C extensions and now they will see changes locales settings
        which might tickle the bug in proj.
        """
        params = ["proj=latlong", "to_meter=0.01745", "ellps=clrk66"]

        oldlocale = localessupport.setdecimalcommalocale()
        if oldlocale == None:
            raise support.SkipTest(
                    "No locale with comma as decimal_point found.")

        proj = Projection(params)
        #print proj.work_around_broken_proj
        result1 =  proj.Forward(1.2,3.2)

        locale.setlocale(locale.LC_NUMERIC, "C")
        proj = Projection(params)
        result2= proj.Forward(1.2,3.2)

        locale.setlocale(locale.LC_NUMERIC, oldlocale)
        self.assertFloatSeqEqual(result1, result2, epsilon = 1e-5 )

    def test_get_projection_units_normal(self):
        """Test Projection.GetProjectedUnits() for normal projection"""
        proj = Projection(["zone=26", "proj=utm", "ellps=clrk66"])
        self.assertEquals(proj.GetProjectedUnits(), PROJ_UNITS_METERS)

    def test_label(self):
        """Test Projection.Label() without epsg"""
        proj = Projection(["zone=26", "proj=utm", "ellps=clrk66"],
                           name = "My Projection")
        self.assertEquals(proj.Label(), "My Projection")

    def test_label_epsg(self):
        """Test Projection.Label() with epsg"""
        proj = Projection(["zone=26", "proj=utm", "ellps=clrk66"],
                           name = "My Projection", epsg="42")
        self.assertEquals(proj.Label(), "EPSG    42 My Projection")

    def test_epsgcode_for_non_epsg_projection(self):
        """Test Projection.EPSGCode() without epsg"""
        proj = Projection(["zone=26", "proj=utm", "ellps=clrk66"],
                           name = "My Projection")
        self.assertEquals(proj.EPSGCode(), None)

    def test_epsgcode_for_real_epsg_projection(self):
        """Test Projection.EPSGCode() with epsg"""
        proj = Projection(["zone=26", "proj=utm", "ellps=clrk66"],
                           name = "My Projection", epsg="42")
        self.assertEquals(proj.EPSGCode(), "42")



class TestProjFileSimple:

    def test_init(self):
        """Test ProjFile coinstructor"""
        proj_file = ProjFile("some_filename")
        self.assertEquals(proj_file.GetFilename(), "some_filename")
        self.assertEquals(len(proj_file.GetProjections()), 0)

    def test_set_filename(self):
        """Test ProjFile.SetFilename()"""
        proj_file = ProjFile("some_filename")
        proj.SetFilename("other_name")
        self.assertEquals(proj_file.GetFilename(), "other_name")


class TestProjFile(unittest.TestCase, support.SubscriberMixin):

    """Test cases for ProjFile objects"""

    def setUp(self):
        self.clear_messages()
        self.proj0 = Projection(["proj=tmerc", "ellps=clrk66"])
        self.proj1 = Projection(["proj=utm", "ellps=clrk66"])
        self.proj2 = Projection(["proj=lcc", "ellps=clrk66",
                                 "lat_1=0", "lat_2=20"])
        self.proj_file = ProjFile("some_filename")
        for msg in [PROJECTION_ADDED, PROJECTION_REMOVED, PROJECTION_REPLACED]:
            self.proj_file.Subscribe(msg, self.subscribe_with_params, msg)

    def tearDown(self):
        self.clear_messages()
        self.proj_file.Destroy()

    def test_add_remove(self):
        """Test ProjFile.Add() and ProjFile.Remove()"""
        self.proj_file.Add(self.proj0)
        self.proj_file.Add(self.proj1)
        self.assertEquals(self.proj_file.GetProjections(),
                          [self.proj0, self.proj1])
        self.check_messages([(self.proj0, PROJECTION_ADDED),
                             (self.proj1, PROJECTION_ADDED)])
        self.clear_messages()

        self.proj_file.Remove(self.proj0)
        self.assertEquals(self.proj_file.GetProjections(), [self.proj1])
        self.check_messages([(self.proj0, PROJECTION_REMOVED)])

    def test_remove_non_existing(self):
        """Test ProjFile.Remove(<proj not in projfile>)"""
        self.assertRaises(ValueError, self.proj_file.Remove, self.proj0)
        # Nothing happened, so no messages should have been sent
        self.check_messages([])

    def test_replace(self):
        """Test ProjFile.Replace()"""
        self.proj_file.Add(self.proj0)
        self.proj_file.Add(self.proj1)
        self.clear_messages()

        # Replace()
        self.proj_file.Replace(self.proj0, self.proj2)
        self.assertEquals(self.proj_file.GetProjections(),
                          [self.proj2, self.proj1])
        self.check_messages([(self.proj0, self.proj2, PROJECTION_REPLACED)])

    def test_replace_non_existing(self):
        """Test ProjFile.Replace(<proj not in projfile>, <some proj>)"""
        self.proj_file.Add(self.proj0)
        self.proj_file.Add(self.proj1)
        self.clear_messages()
        self.assertRaises(ValueError,
                          self.proj_file.Replace, self.proj2, self.proj0)
        # All projections should still be there
        self.assertEquals(self.proj_file.GetProjections(),
                          [self.proj0, self.proj1])
        # Nothing happened, so no messages should have been sent
        self.check_messages([])


class ProjFileTest(unittest.TestCase, support.FileTestMixin,
                   xmlsupport.ValidationTest):

    """Base class for the proj file tests that read or write files"""

    def filename(self):
        """Return the filename for the test"""
        return self.temp_file_name(self.id() + ".proj")


class ProjFileReadTests(ProjFileTest):

    """Test read ProjFile objects from files

    The files only cover error handling and the system projection file.
    """

    def test_read_non_existing_file(self):
        """Test read_proj_file with non-existing file"""
        self.assertRaises(IOError,
                          resource.read_proj_file,
                          self.temp_file_name("nonexistent.proj"))

    def test_read_unreadable_file(self):
        """Test read_proj_file with unreadable file

        As currently written this only works on unix-like systems and
        not e.g. on MS Windows.
        """
        if os.name != "posix":
            raise support.SkipTest("Test only works on posix systems")
        filename = self.filename()
        file = open(filename, "w")
        file.close()
        os.chmod(filename, 0200) # write-only
        self.assertRaises(IOError, resource.read_proj_file, filename)

    def test_read_empty_file(self):
        """Test read_proj_file with empty file"""
        filename = self.filename()
        file = open(filename, "w")
        file.close()

        self.assertRaises(SAXParseException, resource.read_proj_file, filename)

    def test_get_system_proj_file(self):
        """Test resource.get_system_proj_file(DEFAULT_PROJ_FILE)

        This is primarily to test whether the system proj file contains
        invalid projection paramers and whether the proj file is not
        empty
        """
        projfile, warnings\
                  = resource.get_system_proj_file(resource.DEFAULT_PROJ_FILE)
        self.assertEquals(warnings, [])
        self.assert_(len(projfile.GetProjections()) > 0)

        # see whether it got cached and we get the same projfile object
        # when we read the file again
        projfile2, warnings \
                   = resource.get_system_proj_file(resource.DEFAULT_PROJ_FILE)
        self.assert_(projfile is projfile2)


class WriteProjFileTests(ProjFileTest):

    """Test cases for writing proj files"""

    def compare_xml(self, xml1, xml2):
        self.assertEquals(sax_eventlist(xml1), sax_eventlist(xml2))

    def doTestWrite(self, projfile, expected):
        filename = self.filename()

        resource.write_proj_file(projfile)

        file = open(filename)
        written_contents = file.read()
        file.close()
        self.compare_xml(written_contents, expected)
        self.validate_data(written_contents)
        self.validate_data(expected)

    def test_write(self):
        """Test write_proj_file"""
        pf = ProjFile(self.filename())
        pf.Add(Projection(['proj=tmerc', 'ellps=clrk66',
                           'lat_0=90w', 'lon_0=90w', 'k=1'],
                          "Transverse Mercator",))
        pf.Add(Projection(["proj=tmerc",
                           "lat_0=0.000000000", "lon_0=-62.000000000",
                           "k=0.999500", "x_0=400000.000", "y_0=0.000",
                           "ellps=clrk80", "units=m"],
                          "Anguilla 1957 / British West Indies Grid",
                          epsg="200"))
        file_contents = '''<?xml version="1.0" encoding="UTF-8"?>
        <!DOCTYPE projectionlist SYSTEM "projfile.dtd">
        <projectionlist>
            <projection name="Transverse Mercator">
                <parameter value="proj=tmerc"/>
                <parameter value="ellps=clrk66"/>
                <parameter value="lat_0=90w"/>
                <parameter value="lon_0=90w"/>
                <parameter value="k=1"/>
            </projection>
            <projection epsg="200"
                        name="Anguilla 1957 / British West Indies Grid">
                <parameter value="proj=tmerc"/>
                <parameter value="lat_0=0.000000000"/>
                <parameter value="lon_0=-62.000000000"/>
                <parameter value="k=0.999500"/>
                <parameter value="x_0=400000.000"/>
                <parameter value="y_0=0.000"/>
                <parameter value="ellps=clrk80"/>
                <parameter value="units=m"/>
            </projection>
        </projectionlist>
        '''
        self.doTestWrite(pf, file_contents)

    def test_write_empty_file(self):
        """Test write empty ProjFile"""
        pf = ProjFile(self.filename())
        file_contents = '''<?xml version="1.0" encoding="UTF-8"?>
        <!DOCTYPE projectionlist SYSTEM "projfile.dtd">
        <projectionlist>
        </projectionlist>
        '''
        self.doTestWrite(pf, file_contents)


class ProjFileLoadTestCase(support.FileLoadTestCase):

    """Base class for the test cases that read specific test files"""

    file_extension = ".proj"

    def tearDown(self):
        """Clear the cache explicitly"""
        resource.clear_proj_file_cache()


class TestLoadingProjFile(ProjFileLoadTestCase):

    file_contents = '''\
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE projectionlist SYSTEM "projfile.dtd">
<projectionlist>
    <projection name="Transverse Mercator">
        <parameter value="proj=tmerc"/>
        <parameter value="ellps=clrk66"/>
        <parameter value="lat_0=90w"/>
        <parameter value="lon_0=90w"/>
        <parameter value="k=1"/>
    </projection>
    <projection epsg="200" name="Anguilla 1957 / British West Indies Grid">
        <parameter value="proj=tmerc"/>
        <parameter value="lat_0=0.000000000"/>
        <parameter value="lon_0=-62.000000000"/>
        <parameter value="k=0.999500"/>
        <parameter value="x_0=400000.000"/>
        <parameter value="y_0=0.000"/>
        <parameter value="ellps=clrk80"/>
        <parameter value="units=m"/>
    </projection>
</projectionlist>
'''

    def check_projection(self, proj, label, parameters):
        """Check the values of the proj's label and parameters"""
        self.assertEquals(proj.Label(), label)
        params = proj.GetAllParameters()[:]
        params.sort()
        self.assertEquals(params, parameters)

    def test(self):
        projfile, warnings = resource.read_proj_file(self.filename())
        # no warnings
        self.assertEquals(warnings, [])

        # There are two projections
        projs = projfile.GetProjections()
        self.assertEquals(len(projs), 2)

        self.check_projection(projs[0],
                              "Transverse Mercator",
                              ['ellps=clrk66', 'k=1', 'lat_0=90w', 'lon_0=90w',
                               'proj=tmerc'])
        self.check_projection(projs[1],
                         "EPSG   200 Anguilla 1957 / British West Indies Grid",
                              ["ellps=clrk80", "k=0.999500",
                               "lat_0=0.000000000", "lon_0=-62.000000000",
                               "proj=tmerc", "units=m",
                               "x_0=400000.000", "y_0=0.000"])

    def test_caching(self):
        # test whether the projfile cache works
        projfile, warnings = resource.read_proj_file(self.filename())
        projfile2, warnings = resource.read_proj_file(self.filename())

        # Both projfiles should be the same object
        self.assert_(projfile2 is projfile)

        # If we clear the cache we should get a new one.
        resource.clear_proj_file_cache()
        projfile3, warnings = resource.read_proj_file(self.filename())
        self.assert_(projfile3 is not projfile)


class TestLoadingProjFileWithEmptyProjectionlist(ProjFileLoadTestCase):

    file_contents = '''\
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE projectionlist SYSTEM "projfile.dtd">
<projectionlist>
</projectionlist>
'''

    def test(self):
        projfile, warnings = resource.read_proj_file(self.filename())
        # no warnings
        self.assertEquals(warnings, [])

        # There are no projections
        self.assertEquals(len(projfile.GetProjections()), 0)


class TestProjFileWithInvalidParameters(ProjFileLoadTestCase):

    file_contents = '''\
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE projectionlist SYSTEM "projfile.dtd">
<projectionlist>
    <projection name="Universal Transverse Mercator">
        <parameter value="proj=utm"/>
        <parameter value="ellps=clrk66"/>
        <!-- an invalid zone number to trigger the parameter checking
             in the proj library -->
        <parameter value="zone=1000"/>
    </projection>
    <projection name="Transverse Mercator">
        <parameter value="proj=tmerc"/>
        <parameter value="ellps=clrk66"/>
        <parameter value="lat_0=90w"/>
        <parameter value="lon_0=90w"/>
        <parameter value="k=1"/>
    </projection>
</projectionlist>
'''

    def setUp(self):
        support.FileLoadTestCase.setUp(self)

    def test(self):
        """Test reading a proj file with invalid parameters"""
        projfile, warnings = resource.read_proj_file(self.filename())
        projs = projfile.GetProjections()
        self.assertEquals(len(projs), 1)
        params = projs[0].GetAllParameters()[:]
        params.sort()
        self.assertEquals(params, ['ellps=clrk66', 'k=1', 'lat_0=90w',
                                   'lon_0=90w', 'proj=tmerc'])
        self.assertEquals(warnings,
                       ['Error in projection "Universal Transverse Mercator":'
                        ' invalid UTM zone number'])


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