def test_all_gis_lookups_with_rasters(self): """ Evaluate all possible lookups for all input combinations (i.e. raster-raster, raster-geom, geom-raster) and for projected and unprojected coordinate systems. This test just checks that the lookup can be called, but doesn't check if the result makes logical sense. """ from djmodels.contrib.gis.db.backends.postgis.operations import PostGISOperations # Create test raster and geom. rast = GDALRaster(json.loads(JSON_RASTER)) stx_pnt = GEOSGeometry( 'POINT (-95.370401017314293 29.704867409475465)', 4326) stx_pnt.transform(3086) lookups = [(name, lookup) for name, lookup in BaseSpatialField.get_lookups().items() if issubclass(lookup, GISLookup)] self.assertNotEqual(lookups, [], 'No lookups found') # Loop through all the GIS lookups. for name, lookup in lookups: # Construct lookup filter strings. combo_keys = [ field + name for field in [ 'rast__', 'rast__', 'rastprojected__0__', 'rast__', 'rastprojected__', 'geom__', 'rast__', ] ] if issubclass(lookup, DistanceLookupBase): # Set lookup values for distance lookups. combo_values = [ (rast, 50, 'spheroid'), (rast, 0, 50, 'spheroid'), (rast, 0, D(km=1)), (stx_pnt, 0, 500), (stx_pnt, D(km=1000)), (rast, 500), (json.loads(JSON_RASTER), 500), ] elif name == 'relate': # Set lookup values for the relate lookup. combo_values = [ (rast, 'T*T***FF*'), (rast, 0, 'T*T***FF*'), (rast, 0, 'T*T***FF*'), (stx_pnt, 0, 'T*T***FF*'), (stx_pnt, 'T*T***FF*'), (rast, 'T*T***FF*'), (json.loads(JSON_RASTER), 'T*T***FF*'), ] elif name == 'isvalid': # The isvalid lookup doesn't make sense for rasters. continue elif PostGISOperations.gis_operators[name].func: # Set lookup values for all function based operators. combo_values = [ rast, (rast, 0), (rast, 0), (stx_pnt, 0), stx_pnt, rast, json.loads(JSON_RASTER) ] else: # Override band lookup for these, as it's not supported. combo_keys[2] = 'rastprojected__' + name # Set lookup values for all other operators. combo_values = [ rast, None, rast, stx_pnt, stx_pnt, rast, json.loads(JSON_RASTER) ] # Create query filter combinations. self.assertEqual( len(combo_keys), len(combo_values), 'Number of lookup names and values should be the same', ) combos = [x for x in zip(combo_keys, combo_values) if x[1]] self.assertEqual( [(n, x) for n, x in enumerate(combos) if x in combos[:n]], [], 'There are repeated test lookups', ) combos = [{k: v} for k, v in combos] for combo in combos: # Apply this query filter. qs = RasterModel.objects.filter(**combo) # Evaluate normal filter qs. self.assertIn(qs.count(), [0, 1]) # Evaluate on conditional Q expressions. qs = RasterModel.objects.filter(Q(**combos[0]) & Q(**combos[1])) self.assertIn(qs.count(), [0, 1])
def test_dwithin_gis_lookup_ouptut_with_rasters(self): """ Check the logical functionality of the dwithin lookup for different input parameters. """ # Create test raster and geom. rast = GDALRaster(json.loads(JSON_RASTER)) stx_pnt = GEOSGeometry( 'POINT (-95.370401017314293 29.704867409475465)', 4326) stx_pnt.transform(3086) # Filter raster with different lookup raster formats. qs = RasterModel.objects.filter(rastprojected__dwithin=(rast, D(km=1))) self.assertEqual(qs.count(), 1) qs = RasterModel.objects.filter( rastprojected__dwithin=(json.loads(JSON_RASTER), D(km=1))) self.assertEqual(qs.count(), 1) qs = RasterModel.objects.filter(rastprojected__dwithin=(JSON_RASTER, D(km=1))) self.assertEqual(qs.count(), 1) # Filter in an unprojected coordinate system. qs = RasterModel.objects.filter(rast__dwithin=(rast, 40)) self.assertEqual(qs.count(), 1) # Filter with band index transform. qs = RasterModel.objects.filter(rast__1__dwithin=(rast, 1, 40)) self.assertEqual(qs.count(), 1) qs = RasterModel.objects.filter(rast__1__dwithin=(rast, 40)) self.assertEqual(qs.count(), 1) qs = RasterModel.objects.filter(rast__dwithin=(rast, 1, 40)) self.assertEqual(qs.count(), 1) # Filter raster by geom. qs = RasterModel.objects.filter(rast__dwithin=(stx_pnt, 500)) self.assertEqual(qs.count(), 1) qs = RasterModel.objects.filter(rastprojected__dwithin=(stx_pnt, D(km=10000))) self.assertEqual(qs.count(), 1) qs = RasterModel.objects.filter(rast__dwithin=(stx_pnt, 5)) self.assertEqual(qs.count(), 0) qs = RasterModel.objects.filter(rastprojected__dwithin=(stx_pnt, D(km=100))) self.assertEqual(qs.count(), 0) # Filter geom by raster. qs = RasterModel.objects.filter(geom__dwithin=(rast, 500)) self.assertEqual(qs.count(), 1) # Filter through related model. qs = RasterRelatedModel.objects.filter( rastermodel__rast__dwithin=(rast, 40)) self.assertEqual(qs.count(), 1) # Filter through related model with band index transform qs = RasterRelatedModel.objects.filter( rastermodel__rast__1__dwithin=(rast, 40)) self.assertEqual(qs.count(), 1) # Filter through conditional statements. qs = RasterModel.objects.filter( Q(rast__dwithin=(rast, 40)) & Q(rastprojected__dwithin=(stx_pnt, D(km=10000)))) self.assertEqual(qs.count(), 1) # Filter through different lookup. qs = RasterModel.objects.filter(rastprojected__bbcontains=rast) self.assertEqual(qs.count(), 1)
class DistanceTest(TestCase): fixtures = ['initial'] def setUp(self): # A point we are testing distances with -- using a WGS84 # coordinate that'll be implicitly transformed to that to # the coordinate system of the field, EPSG:32140 (Texas South Central # w/units in meters) self.stx_pnt = GEOSGeometry( 'POINT (-95.370401017314293 29.704867409475465)', 4326) # Another one for Australia self.au_pnt = GEOSGeometry('POINT (150.791 -34.4919)', 4326) def get_names(self, qs): cities = [c.name for c in qs] cities.sort() return cities def test_init(self): """ Test initialization of distance models. """ self.assertEqual(9, SouthTexasCity.objects.count()) self.assertEqual(9, SouthTexasCityFt.objects.count()) self.assertEqual(11, AustraliaCity.objects.count()) self.assertEqual(4, SouthTexasZipcode.objects.count()) self.assertEqual(4, CensusZipcode.objects.count()) self.assertEqual(1, Interstate.objects.count()) self.assertEqual(1, SouthTexasInterstate.objects.count()) @skipUnlessDBFeature("supports_dwithin_lookup") def test_dwithin(self): """ Test the `dwithin` lookup type. """ # Distances -- all should be equal (except for the # degree/meter pair in au_cities, that's somewhat # approximate). tx_dists = [(7000, 22965.83), D(km=7), D(mi=4.349)] au_dists = [(0.5, 32000), D(km=32), D(mi=19.884)] # Expected cities for Australia and Texas. tx_cities = ['Downtown Houston', 'Southside Place'] au_cities = ['Mittagong', 'Shellharbour', 'Thirroul', 'Wollongong'] # Performing distance queries on two projected coordinate systems one # with units in meters and the other in units of U.S. survey feet. for dist in tx_dists: if isinstance(dist, tuple): dist1, dist2 = dist else: dist1 = dist2 = dist qs1 = SouthTexasCity.objects.filter(point__dwithin=(self.stx_pnt, dist1)) qs2 = SouthTexasCityFt.objects.filter(point__dwithin=(self.stx_pnt, dist2)) for qs in qs1, qs2: with self.subTest(dist=dist, qs=qs): self.assertEqual(tx_cities, self.get_names(qs)) # With a complex geometry expression self.assertFalse( SouthTexasCity.objects.exclude( point__dwithin=(Union('point', 'point'), 0))) # Now performing the `dwithin` queries on a geodetic coordinate system. for dist in au_dists: with self.subTest(dist=dist): type_error = isinstance(dist, D) and not oracle if isinstance(dist, tuple): if oracle or spatialite: # Result in meters dist = dist[1] else: # Result in units of the field dist = dist[0] # Creating the query set. qs = AustraliaCity.objects.order_by('name') if type_error: # A ValueError should be raised on PostGIS when trying to # pass Distance objects into a DWithin query using a # geodetic field. with self.assertRaises(ValueError): AustraliaCity.objects.filter( point__dwithin=(self.au_pnt, dist)).count() else: self.assertEqual( au_cities, self.get_names( qs.filter(point__dwithin=(self.au_pnt, dist)))) @skipUnlessDBFeature("supports_distances_lookups") def test_distance_lookups(self): """ Test the `distance_lt`, `distance_gt`, `distance_lte`, and `distance_gte` lookup types. """ # Retrieving the cities within a 20km 'donut' w/a 7km radius 'hole' # (thus, Houston and Southside place will be excluded as tested in # the `test02_dwithin` above). for model in [SouthTexasCity, SouthTexasCityFt]: stx_pnt = self.stx_pnt.transform( model._meta.get_field('point').srid, clone=True) qs = model.objects.filter(point__distance_gte=(stx_pnt, D( km=7))).filter(point__distance_lte=(stx_pnt, D(km=20)), ) cities = self.get_names(qs) self.assertEqual(cities, ['Bellaire', 'Pearland', 'West University Place']) # Doing a distance query using Polygons instead of a Point. z = SouthTexasZipcode.objects.get(name='77005') qs = SouthTexasZipcode.objects.exclude(name='77005').filter( poly__distance_lte=(z.poly, D(m=275))) self.assertEqual(['77025', '77401'], self.get_names(qs)) # If we add a little more distance 77002 should be included. qs = SouthTexasZipcode.objects.exclude(name='77005').filter( poly__distance_lte=(z.poly, D(m=300))) self.assertEqual(['77002', '77025', '77401'], self.get_names(qs)) @skipUnlessDBFeature("supports_distances_lookups", "supports_distance_geodetic") def test_geodetic_distance_lookups(self): """ Test distance lookups on geodetic coordinate systems. """ # Line is from Canberra to Sydney. Query is for all other cities within # a 100km of that line (which should exclude only Hobart & Adelaide). line = GEOSGeometry('LINESTRING(144.9630 -37.8143,151.2607 -33.8870)', 4326) dist_qs = AustraliaCity.objects.filter(point__distance_lte=(line, D(km=100))) expected_cities = [ 'Batemans Bay', 'Canberra', 'Hillsdale', 'Melbourne', 'Mittagong', 'Shellharbour', 'Sydney', 'Thirroul', 'Wollongong', ] if spatialite: # SpatiaLite is less accurate and returns 102.8km for Batemans Bay. expected_cities.pop(0) self.assertEqual(expected_cities, self.get_names(dist_qs)) msg = "2, 3, or 4-element tuple required for 'distance_lte' lookup." with self.assertRaisesMessage(ValueError, msg): # Too many params. len( AustraliaCity.objects.filter( point__distance_lte=('POINT(5 23)', D(km=100), 'spheroid', '4', None))) with self.assertRaisesMessage(ValueError, msg): # Too few params. len( AustraliaCity.objects.filter( point__distance_lte=('POINT(5 23)', ))) msg = "For 4-element tuples the last argument must be the 'spheroid' directive." with self.assertRaisesMessage(ValueError, msg): len( AustraliaCity.objects.filter( point__distance_lte=('POINT(5 23)', D(km=100), 'spheroid', '4'))) # Getting all cities w/in 550 miles of Hobart. hobart = AustraliaCity.objects.get(name='Hobart') qs = AustraliaCity.objects.exclude(name='Hobart').filter( point__distance_lte=(hobart.point, D(mi=550))) cities = self.get_names(qs) self.assertEqual(cities, ['Batemans Bay', 'Canberra', 'Melbourne']) # Cities that are either really close or really far from Wollongong -- # and using different units of distance. wollongong = AustraliaCity.objects.get(name='Wollongong') d1, d2 = D(yd=19500), D(nm=400) # Yards (~17km) & Nautical miles. # Normal geodetic distance lookup (uses `distance_sphere` on PostGIS. gq1 = Q(point__distance_lte=(wollongong.point, d1)) gq2 = Q(point__distance_gte=(wollongong.point, d2)) qs1 = AustraliaCity.objects.exclude(name='Wollongong').filter(gq1 | gq2) # Geodetic distance lookup but telling GeoDjango to use `distance_spheroid` # instead (we should get the same results b/c accuracy variance won't matter # in this test case). querysets = [qs1] if connection.features.has_DistanceSpheroid_function: gq3 = Q(point__distance_lte=(wollongong.point, d1, 'spheroid')) gq4 = Q(point__distance_gte=(wollongong.point, d2, 'spheroid')) qs2 = AustraliaCity.objects.exclude( name='Wollongong').filter(gq3 | gq4) querysets.append(qs2) for qs in querysets: cities = self.get_names(qs) self.assertEqual( cities, ['Adelaide', 'Hobart', 'Shellharbour', 'Thirroul']) @skipUnlessDBFeature("supports_distances_lookups") def test_distance_lookups_with_expression_rhs(self): stx_pnt = self.stx_pnt.transform( SouthTexasCity._meta.get_field('point').srid, clone=True) qs = SouthTexasCity.objects.filter( point__distance_lte=(stx_pnt, F('radius')), ).order_by('name') self.assertEqual(self.get_names(qs), [ 'Bellaire', 'Downtown Houston', 'Southside Place', 'West University Place' ]) # With a combined expression qs = SouthTexasCity.objects.filter( point__distance_lte=(stx_pnt, F('radius') * 2), ).order_by('name') self.assertEqual(len(qs), 5) self.assertIn('Pearland', self.get_names(qs)) # With spheroid param if connection.features.supports_distance_geodetic: hobart = AustraliaCity.objects.get(name='Hobart') qs = AustraliaCity.objects.filter( point__distance_lte=(hobart.point, F('radius') * 70, 'spheroid'), ).order_by('name') self.assertEqual(self.get_names(qs), ['Canberra', 'Hobart', 'Melbourne']) # With a complex geometry expression self.assertFalse( SouthTexasCity.objects.filter( point__distance_gt=(Union('point', 'point'), 0))) self.assertEqual( SouthTexasCity.objects.filter( point__distance_lte=(Union('point', 'point'), 0)).count(), SouthTexasCity.objects.count(), ) @unittest.skipUnless(mysql, 'This is a MySQL-specific test') def test_mysql_geodetic_distance_error(self): msg = 'Only numeric values of degree units are allowed on geodetic distance queries.' with self.assertRaisesMessage(ValueError, msg): AustraliaCity.objects.filter( point__distance_lte=(Point(0, 0), D(m=100))).exists()