From c31d20858d8025d105748ef38c015d5371df97e7 Mon Sep 17 00:00:00 2001 From: Jeremy Dunck Date: Sun, 8 Jul 2007 08:56:29 +0000 Subject: [PATCH] gis: Added django.contrib.gis.measure, closing #4797. Thanks, Robert. Also removed dead test_with_swig suite and re-worked django.contrib.gis.tests.__init__ to drive tests a little more easily. git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@5635 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/gis/measure.py | 213 ++++++++++++++ django/contrib/gis/tests/__init__.py | 23 +- django/contrib/gis/tests/test_measure.py | 315 +++++++++++++++++++++ django/contrib/gis/tests/test_with_swig.py | 67 ----- 4 files changed, 544 insertions(+), 74 deletions(-) create mode 100644 django/contrib/gis/measure.py create mode 100644 django/contrib/gis/tests/test_measure.py delete mode 100644 django/contrib/gis/tests/test_with_swig.py diff --git a/django/contrib/gis/measure.py b/django/contrib/gis/measure.py new file mode 100644 index 0000000000..1feb63f081 --- /dev/null +++ b/django/contrib/gis/measure.py @@ -0,0 +1,213 @@ +""" +Distance and Area objects to allow for sensible and convienient calculation +and conversions. + +Inspired by GeoPy (http://exogen.case.edu/projects/geopy/) +and Geoff Biggs' PhD work on dimensioned units for robotics. + +Copyright (c) 2007, Robert Coup +""" + +from math import * +from decimal import Decimal + +class Distance(object): + UNITS = { + 'm': 1.0, + 'km': 1000.0, + 'mi': 1609.344, + 'ft': 0.3048, + 'yd': 0.9144, + 'nm': 1852.0, + } + + def __init__(self, default_unit=None, **kwargs): + self.m = 0.0 + self._default_unit = 'm' + + for unit,value in kwargs.items(): + if unit in self.UNITS: + self.m += self.UNITS[unit] * value + self._default_unit = unit + else: + raise AttributeError("Unknown unit type: " + unit) + + if default_unit: + self._default_unit = default_unit + + def __getattr__(self, name): + if name in self.UNITS: + return self.m / self.UNITS[name] + else: + raise AttributeError("Unknown unit type: " + name) + + def __repr__(self): + return "Distance(%s=%s)" % (self._default_unit, getattr(self, self._default_unit)) + + def __str__(self): + return "%s %s" % (getattr(self, self._default_unit), self._default_unit) + + def __cmp__(self, other): + if isinstance(other, Distance): + return cmp(self.m, other.m) + else: + return NotImplemented + + def __add__(self, other): + if isinstance(other, Distance): + return Distance(default_unit=self._default_unit, m=(self.m + other.m)) + else: + raise TypeError("Distance must be added with Distance") + + def __iadd__(self, other): + if isinstance(other, Distance): + self.m += other.m + return self + else: + raise TypeError("Distance must be added with Distance") + + def __sub__(self, other): + if isinstance(other, Distance): + return Distance(default_unit=self._default_unit, m=(self.m - other.m)) + else: + raise TypeError("Distance must be subtracted from Distance") + + def __isub__(self, other): + if isinstance(other, Distance): + self.m -= other.m + return self + else: + raise TypeError("Distance must be subtracted from Distance") + + def __mul__(self, other): + if isinstance(other, (int, float, long, Decimal)): + return Distance(default_unit=self._default_unit, m=(self.m * float(other))) + elif isinstance(other, Distance): + return Area(default_unit='sq_' + self._default_unit, sq_m=(self.m * other.m)) + else: + raise TypeError("Distance must be multiplied with number or Distance") + + def __imul__(self, other): + if isinstance(other, (int, float, long, Decimal)): + self.m *= float(other) + return self + else: + raise TypeError("Distance must be multiplied with number") + + def __div__(self, other): + if isinstance(other, (int, float, long, Decimal)): + return Distance(default_unit=self._default_unit, m=(self.m / float(other))) + else: + raise TypeError("Distance must be divided with number") + + def __idiv__(self, other): + if isinstance(other, (int, float, long, Decimal)): + self.m /= float(other) + return self + else: + raise TypeError("Distance must be divided with number") + + def __nonzero__(self): + return bool(self.m) + +class Area(object): + UNITS = { + 'sq_m': 1.0, + 'sq_km': 1000000.0, + 'sq_mi': 2589988.110336, + 'sq_ft': 0.09290304, + 'sq_yd': 0.83612736, + 'sq_nm': 3429904.0, + } + + def __init__(self, default_unit=None, **kwargs): + self.sq_m = 0.0 + self._default_unit = 'sq_m' + + for unit,value in kwargs.items(): + if unit in self.UNITS: + self.sq_m += self.UNITS[unit] * value + self._default_unit = unit + else: + raise AttributeError("Unknown unit type: " + unit) + + if default_unit: + self._default_unit = default_unit + + def __getattr__(self, name): + if name in self.UNITS: + return self.sq_m / self.UNITS[name] + else: + raise AttributeError("Unknown unit type: " + name) + + def __repr__(self): + return "Area(%s=%s)" % (self._default_unit, getattr(self, self._default_unit)) + + def __str__(self): + return "%s %s" % (getattr(self, self._default_unit), self._default_unit) + + def __cmp__(self, other): + if isinstance(other, Area): + return cmp(self.sq_m, other.sq_m) + else: + return NotImplemented + + def __add__(self, other): + if isinstance(other, Area): + return Area(default_unit=self._default_unit, sq_m=(self.sq_m + other.sq_m)) + else: + raise TypeError("Area must be added with Area") + + def __iadd__(self, other): + if isinstance(other, Area): + self.sq_m += other.sq_m + return self + else: + raise TypeError("Area must be added with Area") + + def __sub__(self, other): + if isinstance(other, Area): + return Area(default_unit=self._default_unit, sq_m=(self.sq_m - other.sq_m)) + else: + raise TypeError("Area must be subtracted from Area") + + def __isub__(self, other): + if isinstance(other, Area): + self.sq_m -= other.sq_m + return self + else: + raise TypeError("Area must be subtracted from Area") + + def __mul__(self, other): + if isinstance(other, (int, float, long, Decimal)): + return Area(default_unit=self._default_unit, sq_m=(self.sq_m * float(other))) + else: + raise TypeError("Area must be multiplied with number") + + def __imul__(self, other): + if isinstance(other, (int, float, long, Decimal)): + self.sq_m *= float(other) + return self + else: + raise TypeError("Area must be multiplied with number") + + def __div__(self, other): + if isinstance(other, (int, float, long, Decimal)): + return Area(default_unit=self._default_unit, sq_m=(self.sq_m / float(other))) + else: + raise TypeError("Area must be divided with number") + + def __idiv__(self, other): + if isinstance(other, (int, float, long, Decimal)): + self.sq_m /= float(other) + return self + else: + raise TypeError("Area must be divided with number") + + def __nonzero__(self): + return bool(self.sq_m) + + +# Shortcuts +D = Distance +A = Area diff --git a/django/contrib/gis/tests/__init__.py b/django/contrib/gis/tests/__init__.py index 4aed2e136b..351a6568f1 100644 --- a/django/contrib/gis/tests/__init__.py +++ b/django/contrib/gis/tests/__init__.py @@ -1,15 +1,24 @@ from unittest import TestSuite, makeSuite, TextTestRunner -import test_geos, test_gdal_ds, test_gdal_driver, test_gdal_srs, test_gdal_geom, test_spatialrefsys + +test_suite_names = ['test_gdal_driver', + 'test_gdal_ds', + 'test_gdal_geom', + 'test_gdal_srs', + 'test_geos', + 'test_measure', + 'test_spatialrefsys', + ] + def suite(): s = TestSuite() - s.addTest(test_geos.suite()) - s.addTest(test_gdal_ds.suite()) - s.addTest(test_gdal_driver.suite()) - s.addTest(test_gdal_srs.suite()) - s.addTest(test_gdal_geom.suite()) - s.addTest(test_spatialrefsys.suite()) + for test_suite in test_suite_names: + suite = getattr(__import__('django.contrib.gis.tests', fromlist=[test_suite]),test_suite) + s.addTest(suite.suite()) return s def run(verbosity=1): TextTestRunner(verbosity=verbosity).run(suite()) + +if __name__ == '__main__': + run() diff --git a/django/contrib/gis/tests/test_measure.py b/django/contrib/gis/tests/test_measure.py new file mode 100644 index 0000000000..3c71cc4834 --- /dev/null +++ b/django/contrib/gis/tests/test_measure.py @@ -0,0 +1,315 @@ +""" +Distance and Area objects to allow for sensible and convienient calculation +and conversions. Here are some tests. +""" + +import unittest +from django.contrib.gis.measure import Distance, Area, D, A + +class DistanceTest(unittest.TestCase): + "Testing the Distance object" + + def testInit(self): + "Testing initialisation from valid units" + d = Distance(m=100) + self.assertEqual(d.m, 100) + + d = D(m=100) + self.assertEqual(d.m, 100) + + d = D(nm=100) + self.assertEqual(d.m, 185200) + + def testInitInvalid(self): + "Testing initialisation from invalid units" + self.assertRaises(AttributeError, D, banana=100) + + def testAccess(self): + "Testing access in different units" + d = D(m=100) + self.assertEqual(d.km, 0.1) + self.assertAlmostEqual(d.ft, 328.084, 3) + + def testAccessInvalid(self): + "Testing access in invalid units" + d = D(m=100) + self.failIf(hasattr(d, 'banana')) + + def testAddition(self): + "Test addition & subtraction" + d1 = D(m=100) + d2 = D(m=200) + + d3 = d1 + d2 + self.assertEqual(d3.m, 300) + d3 += d1 + self.assertEqual(d3.m, 400) + + d4 = d1 - d2 + self.assertEqual(d4.m, -100) + d4 -= d1 + self.assertEqual(d4.m, -200) + + try: + d5 = d1 + 1 + except TypeError, e: + pass + else: + self.fail('Distance + number should raise TypeError') + + try: + d5 = d1 - 1 + except TypeError, e: + pass + else: + self.fail('Distance - number should raise TypeError') + + try: + d1 += 1 + except TypeError, e: + pass + else: + self.fail('Distance += number should raise TypeError') + + try: + d1 -= 1 + except TypeError, e: + pass + else: + self.fail('Distance -= number should raise TypeError') + + def testMultiplication(self): + "Test multiplication & division" + d1 = D(m=100) + + d3 = d1 * 2 + self.assertEqual(d3.m, 200) + d3 *= 5 + self.assertEqual(d3.m, 1000) + + d4 = d1 / 2 + self.assertEqual(d4.m, 50) + d4 /= 5 + self.assertEqual(d4.m, 10) + + a5 = d1 * D(m=10) + self.assert_(isinstance(a5, Area)) + self.assertEqual(a5.sq_m, 100*10) + + try: + d1 *= D(m=1) + except TypeError, e: + pass + else: + self.fail('Distance *= Distance should raise TypeError') + + try: + d5 = d1 / D(m=1) + except TypeError, e: + pass + else: + self.fail('Distance / Distance should raise TypeError') + + try: + d1 /= D(m=1) + except TypeError, e: + pass + else: + self.fail('Distance /= Distance should raise TypeError') + + def testUnitConversions(self): + "Testing default units during maths" + d1 = D(m=100) + d2 = D(km=1) + + d3 = d1 + d2 + self.assertEqual(d3._default_unit, 'm') + d4 = d2 + d1 + self.assertEqual(d4._default_unit, 'km') + d5 = d1 * 2 + self.assertEqual(d5._default_unit, 'm') + d6 = d1 / 2 + self.assertEqual(d6._default_unit, 'm') + + def testComparisons(self): + "Testing comparisons" + d1 = D(m=100) + d2 = D(km=1) + d3 = D(km=0) + + self.assert_(d2 > d1) + self.assert_(d1 == d1) + self.assert_(d1 < d2) + self.failIf(d3) + + def testUnitsStr(self): + "Testing conversion to strings" + d1 = D(m=100) + d2 = D(km=3.5) + + self.assertEqual(str(d1), '100.0 m') + self.assertEqual(str(d2), '3.5 km') + self.assertEqual(repr(d1), 'Distance(m=100.0)') + self.assertEqual(repr(d2), 'Distance(km=3.5)') + +class AreaTest(unittest.TestCase): + "Testing the Area object" + + def testInit(self): + "Testing initialisation from valid units" + a = Area(sq_m=100) + self.assertEqual(a.sq_m, 100) + + a = A(sq_m=100) + self.assertEqual(a.sq_m, 100) + + a = A(sq_mi=100) + self.assertEqual(a.sq_m, 258998811.0336) + + def testInitInvaliA(self): + "Testing initialisation from invalid units" + self.assertRaises(AttributeError, A, banana=100) + + def testAccess(self): + "Testing access in different units" + a = A(sq_m=100) + self.assertEqual(a.sq_km, 0.0001) + self.assertAlmostEqual(a.sq_ft, 1076.391, 3) + + def testAccessInvaliA(self): + "Testing access in invalid units" + a = A(sq_m=100) + self.failIf(hasattr(a, 'banana')) + + def testAddition(self): + "Test addition & subtraction" + a1 = A(sq_m=100) + a2 = A(sq_m=200) + + a3 = a1 + a2 + self.assertEqual(a3.sq_m, 300) + a3 += a1 + self.assertEqual(a3.sq_m, 400) + + a4 = a1 - a2 + self.assertEqual(a4.sq_m, -100) + a4 -= a1 + self.assertEqual(a4.sq_m, -200) + + try: + a5 = a1 + 1 + except TypeError, e: + pass + else: + self.fail('Area + number should raise TypeError') + + try: + a5 = a1 - 1 + except TypeError, e: + pass + else: + self.fail('Area - number should raise TypeError') + + try: + a1 += 1 + except TypeError, e: + pass + else: + self.fail('Area += number should raise TypeError') + + try: + a1 -= 1 + except TypeError, e: + pass + else: + self.fail('Area -= number should raise TypeError') + + def testMultiplication(self): + "Test multiplication & division" + a1 = A(sq_m=100) + + a3 = a1 * 2 + self.assertEqual(a3.sq_m, 200) + a3 *= 5 + self.assertEqual(a3.sq_m, 1000) + + a4 = a1 / 2 + self.assertEqual(a4.sq_m, 50) + a4 /= 5 + self.assertEqual(a4.sq_m, 10) + + try: + a5 = a1 * A(sq_m=1) + except TypeError, e: + pass + else: + self.fail('Area * Area should raise TypeError') + + try: + a1 *= A(sq_m=1) + except TypeError, e: + pass + else: + self.fail('Area *= Area should raise TypeError') + + try: + a5 = a1 / A(sq_m=1) + except TypeError, e: + pass + else: + self.fail('Area / Area should raise TypeError') + + try: + a1 /= A(sq_m=1) + except TypeError, e: + pass + else: + self.fail('Area /= Area should raise TypeError') + + def testUnitConversions(self): + "Testing default units during maths" + a1 = A(sq_m=100) + a2 = A(sq_km=1) + + a3 = a1 + a2 + self.assertEqual(a3._default_unit, 'sq_m') + a4 = a2 + a1 + self.assertEqual(a4._default_unit, 'sq_km') + a5 = a1 * 2 + self.assertEqual(a5._default_unit, 'sq_m') + a6 = a1 / 2 + self.assertEqual(a6._default_unit, 'sq_m') + + def testComparisons(self): + "Testing comparisons" + a1 = A(sq_m=100) + a2 = A(sq_km=1) + a3 = A(sq_km=0) + + self.assert_(a2 > a1) + self.assert_(a1 == a1) + self.assert_(a1 < a2) + self.failIf(a3) + + def testUnitsStr(self): + "Testing conversion to strings" + a1 = A(sq_m=100) + a2 = A(sq_km=3.5) + + self.assertEqual(str(a1), '100.0 sq_m') + self.assertEqual(str(a2), '3.5 sq_km') + self.assertEqual(repr(a1), 'Area(sq_m=100.0)') + self.assertEqual(repr(a2), 'Area(sq_km=3.5)') + + +def suite(): + s = unittest.TestSuite() + s.addTest(unittest.makeSuite(DistanceTest)) + s.addTest(unittest.makeSuite(AreaTest)) + return s + +def run(verbosity=2): + unittest.TextTestRunner(verbosity=verbosity).run(suite()) + +if __name__=="__main__": + run() \ No newline at end of file diff --git a/django/contrib/gis/tests/test_with_swig.py b/django/contrib/gis/tests/test_with_swig.py deleted file mode 100644 index 46195ad1c9..0000000000 --- a/django/contrib/gis/tests/test_with_swig.py +++ /dev/null @@ -1,67 +0,0 @@ -import unittest -from django.contrib.gis.geos import GEOSGeometry, hex_to_wkt, wkt_to_hex, centroid -from geos import geomToWKT, geomToHEX, geomFromWKT, geomFromHEX -from geometries import swig_geoms as geos_geoms - -class GeosTestWithSwig(unittest.TestCase): - - def test001_hex_to_wkt(self): - "Testing HEX to WKT conversion." - for g in geos_geoms: - wkt1 = geomToWKT(geomFromWKT(g.wkt)) - wkt2 = hex_to_wkt(GEOSGeometry(g.wkt, 'wkt').hex) - self.assertEqual(wkt1, wkt2) - - def test002_wkt_to_hex(self): - "Testing WKT to HEX conversion." - for g in geos_geoms: - self.assertEqual(geomToHEX(geomFromWKT(g.wkt)), wkt_to_hex(g.wkt)) - - def test003_centroid(self): - "Testing the centroid property." - for g in geos_geoms: - wkt1 = centroid(g.wkt, geom_type='wkt') - wkt2 = geomToWKT((geomFromWKT(g.wkt)).getCentroid()) - self.assertEqual(wkt1, wkt2) - - def test004_area(self): - "Testing the area property." - for g in geos_geoms: - g1 = geomFromWKT(g.wkt) - g2 = GEOSGeometry(g.wkt, 'wkt') - self.assertEqual(g1.area(), g2.area) - - def test005_geom_type(self): - "Testing the geom_type property." - for g in geos_geoms: - g1 = geomFromWKT(g.wkt) - g2 = GEOSGeometry(g.wkt, 'wkt') - self.assertEqual(g1.geomType(), g2.geom_type) - - def test005_geom_id(self): - "Testing the geom_typeid property." - for g in geos_geoms: - g1 = geomFromWKT(g.wkt) - g2 = GEOSGeometry(g.wkt, 'wkt') - self.assertEqual(g1.typeId(), g2.geom_typeid) - - def test006_ngeom(self): - "Testing the num_geom property." - for g in geos_geoms: - g1 = geomFromWKT(g.wkt) - g2 = GEOSGeometry(g.wkt, 'wkt') - self.assertEqual(g1.getNumGeometries(), g2.num_geom) - - def test007_ncoords(self): - "Testing the num_coords property." - for g in geos_geoms: - g2 = GEOSGeometry(g.wkt, 'wkt') - self.assertEqual(g.ncoords, g2.num_coords) - -def suite(): - s = unittest.TestSuite() - s.addTest(unittest.makeSuite(GeosTestWithSwig)) - return s - -def run(verbosity=2): - unittest.TextTestRunner(verbosity=verbosity).run(suite())