2009-12-24 04:45:08 +08:00
|
|
|
"""
|
2014-07-27 04:55:31 +08:00
|
|
|
Tests for geography support in PostGIS
|
2009-12-24 04:45:08 +08:00
|
|
|
"""
|
2009-12-25 22:37:57 +08:00
|
|
|
import os
|
2011-10-18 02:45:22 +08:00
|
|
|
|
2016-04-11 01:55:29 +08:00
|
|
|
from django.contrib.gis.db import models
|
2015-01-19 23:09:41 +08:00
|
|
|
from django.contrib.gis.db.models.functions import Area, Distance
|
2009-12-24 04:45:08 +08:00
|
|
|
from django.contrib.gis.measure import D
|
2017-10-07 00:47:08 +08:00
|
|
|
from django.db import NotSupportedError, connection
|
2016-04-11 01:55:29 +08:00
|
|
|
from django.db.models.functions import Cast
|
2016-11-07 20:00:40 +08:00
|
|
|
from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature
|
2011-10-18 02:45:22 +08:00
|
|
|
|
2020-11-14 22:08:30 +08:00
|
|
|
from ..utils import FuncTestMixin
|
2019-03-04 02:33:48 +08:00
|
|
|
from .models import City, County, Zipcode
|
2011-10-18 02:45:22 +08:00
|
|
|
|
2009-12-24 04:45:08 +08:00
|
|
|
|
|
|
|
class GeographyTest(TestCase):
|
2014-07-27 01:15:54 +08:00
|
|
|
fixtures = ["initial"]
|
2009-12-24 04:45:08 +08:00
|
|
|
|
|
|
|
def test01_fixture_load(self):
|
|
|
|
"Ensure geography features loaded properly."
|
|
|
|
self.assertEqual(8, City.objects.count())
|
|
|
|
|
2014-08-22 00:47:57 +08:00
|
|
|
@skipUnlessDBFeature("supports_distances_lookups", "supports_distance_geodetic")
|
2009-12-24 04:45:08 +08:00
|
|
|
def test02_distance_lookup(self):
|
2016-11-07 20:00:40 +08:00
|
|
|
"Testing distance lookup support on non-point geography fields."
|
2009-12-24 04:45:08 +08:00
|
|
|
z = Zipcode.objects.get(code="77002")
|
2009-12-25 22:37:57 +08:00
|
|
|
cities1 = list(
|
|
|
|
City.objects.filter(point__distance_lte=(z.poly, D(mi=500)))
|
|
|
|
.order_by("name")
|
|
|
|
.values_list("name", flat=True)
|
2022-02-04 03:24:19 +08:00
|
|
|
)
|
2009-12-25 22:37:57 +08:00
|
|
|
cities2 = list(
|
|
|
|
City.objects.filter(point__dwithin=(z.poly, D(mi=500)))
|
|
|
|
.order_by("name")
|
|
|
|
.values_list("name", flat=True)
|
2022-02-04 03:24:19 +08:00
|
|
|
)
|
2009-12-25 22:37:57 +08:00
|
|
|
for cities in [cities1, cities2]:
|
|
|
|
self.assertEqual(["Dallas", "Houston", "Oklahoma City"], cities)
|
2009-12-24 04:45:08 +08:00
|
|
|
|
|
|
|
def test04_invalid_operators_functions(self):
|
|
|
|
"""
|
|
|
|
Exceptions are raised for operators & functions invalid on geography
|
|
|
|
fields.
|
2022-02-04 15:08:27 +08:00
|
|
|
"""
|
2020-11-14 22:08:30 +08:00
|
|
|
if not connection.ops.postgis:
|
|
|
|
self.skipTest("This is a PostGIS-specific test.")
|
2009-12-24 04:45:08 +08:00
|
|
|
# Only a subset of the geometry functions & operator are available
|
|
|
|
# to PostGIS geography types. For more information, visit:
|
2013-11-03 05:02:56 +08:00
|
|
|
# http://postgis.refractions.net/documentation/manual-1.5/ch08.html#PostGIS_GeographyFunctions
|
2009-12-24 04:45:08 +08:00
|
|
|
z = Zipcode.objects.get(code="77002")
|
|
|
|
# ST_Within not available.
|
2016-01-17 19:26:39 +08:00
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
City.objects.filter(point__within=z.poly).count()
|
2009-12-24 04:45:08 +08:00
|
|
|
# `@` operator not available.
|
2016-01-17 19:26:39 +08:00
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
City.objects.filter(point__contained=z.poly).count()
|
2009-12-25 22:37:57 +08:00
|
|
|
|
2010-09-12 10:07:04 +08:00
|
|
|
# Regression test for #14060, `~=` was never really implemented for PostGIS.
|
|
|
|
htown = City.objects.get(name="Houston")
|
2016-01-17 19:26:39 +08:00
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
City.objects.get(point__exact=htown.point)
|
2010-09-12 10:07:04 +08:00
|
|
|
|
2009-12-25 22:37:57 +08:00
|
|
|
def test05_geography_layermapping(self):
|
|
|
|
"Testing LayerMapping support on models with geography fields."
|
|
|
|
# There is a similar test in `layermap` that uses the same data set,
|
|
|
|
# but the County model here is a bit different.
|
|
|
|
from django.contrib.gis.utils import LayerMapping
|
|
|
|
|
|
|
|
# Getting the shapefile and mapping dictionary.
|
2017-01-20 21:01:02 +08:00
|
|
|
shp_path = os.path.realpath(
|
|
|
|
os.path.join(os.path.dirname(__file__), "..", "data")
|
2022-02-04 03:24:19 +08:00
|
|
|
)
|
2009-12-25 22:37:57 +08:00
|
|
|
co_shp = os.path.join(shp_path, "counties", "counties.shp")
|
2019-01-03 07:18:19 +08:00
|
|
|
co_mapping = {
|
|
|
|
"name": "Name",
|
|
|
|
"state": "State",
|
|
|
|
"mpoly": "MULTIPOLYGON",
|
|
|
|
}
|
2009-12-25 22:37:57 +08:00
|
|
|
# Reference county names, number of polygons, and state names.
|
|
|
|
names = ["Bexar", "Galveston", "Harris", "Honolulu", "Pueblo"]
|
2013-11-03 05:02:56 +08:00
|
|
|
num_polys = [1, 2, 1, 19, 1] # Number of polygons for each.
|
2009-12-25 22:37:57 +08:00
|
|
|
st_names = ["Texas", "Texas", "Texas", "Hawaii", "Colorado"]
|
|
|
|
|
|
|
|
lm = LayerMapping(County, co_shp, co_mapping, source_srs=4269, unique="name")
|
|
|
|
lm.save(silent=True, strict=True)
|
|
|
|
|
|
|
|
for c, name, num_poly, state in zip(
|
|
|
|
County.objects.order_by("name"), names, num_polys, st_names
|
|
|
|
):
|
|
|
|
self.assertEqual(4326, c.mpoly.srid)
|
|
|
|
self.assertEqual(num_poly, len(c.mpoly))
|
|
|
|
self.assertEqual(name, c.name)
|
|
|
|
self.assertEqual(state, c.state)
|
2010-10-13 01:13:27 +08:00
|
|
|
|
2015-01-19 23:09:41 +08:00
|
|
|
|
2017-09-11 23:56:39 +08:00
|
|
|
class GeographyFunctionTests(FuncTestMixin, TestCase):
|
2015-01-19 23:09:41 +08:00
|
|
|
fixtures = ["initial"]
|
|
|
|
|
2016-04-11 01:55:29 +08:00
|
|
|
@skipUnlessDBFeature("supports_extent_aggr")
|
|
|
|
def test_cast_aggregate(self):
|
|
|
|
"""
|
|
|
|
Cast a geography to a geometry field for an aggregate function that
|
|
|
|
expects a geometry input.
|
|
|
|
"""
|
2020-11-09 08:55:50 +08:00
|
|
|
if not connection.features.supports_geography:
|
2016-04-11 01:55:29 +08:00
|
|
|
self.skipTest("This test needs geography support")
|
|
|
|
expected = (
|
|
|
|
-96.8016128540039,
|
|
|
|
29.7633724212646,
|
|
|
|
-95.3631439208984,
|
|
|
|
32.782058715820,
|
|
|
|
)
|
|
|
|
res = City.objects.filter(name__in=("Houston", "Dallas")).aggregate(
|
|
|
|
extent=models.Extent(Cast("point", models.PointField()))
|
|
|
|
)
|
|
|
|
for val, exp in zip(res["extent"], expected):
|
|
|
|
self.assertAlmostEqual(exp, val, 4)
|
|
|
|
|
2015-01-19 23:09:41 +08:00
|
|
|
@skipUnlessDBFeature("has_Distance_function", "supports_distance_geodetic")
|
|
|
|
def test_distance_function(self):
|
|
|
|
"""
|
|
|
|
Testing Distance() support on non-point geography fields.
|
|
|
|
"""
|
2020-11-14 22:08:30 +08:00
|
|
|
if connection.ops.oracle:
|
2015-11-19 18:57:49 +08:00
|
|
|
ref_dists = [0, 4899.68, 8081.30, 9115.15]
|
2020-11-14 22:08:30 +08:00
|
|
|
elif connection.ops.spatialite:
|
2021-04-03 22:23:19 +08:00
|
|
|
if connection.ops.spatial_version < (5,):
|
|
|
|
# SpatiaLite < 5 returns non-zero distance for polygons and points
|
|
|
|
# covered by that polygon.
|
|
|
|
ref_dists = [326.61, 4899.68, 8081.30, 9115.15]
|
|
|
|
else:
|
|
|
|
ref_dists = [0, 4899.68, 8081.30, 9115.15]
|
2015-11-19 18:57:49 +08:00
|
|
|
else:
|
|
|
|
ref_dists = [0, 4891.20, 8071.64, 9123.95]
|
2015-01-19 23:09:41 +08:00
|
|
|
htown = City.objects.get(name="Houston")
|
2017-04-01 21:47:49 +08:00
|
|
|
qs = Zipcode.objects.annotate(
|
|
|
|
distance=Distance("poly", htown.point),
|
|
|
|
distance2=Distance(htown.point, "poly"),
|
|
|
|
)
|
2015-01-19 23:09:41 +08:00
|
|
|
for z, ref in zip(qs, ref_dists):
|
|
|
|
self.assertAlmostEqual(z.distance.m, ref, 2)
|
2017-04-01 21:47:49 +08:00
|
|
|
|
2020-11-14 22:08:30 +08:00
|
|
|
if connection.ops.postgis:
|
2017-04-01 21:47:49 +08:00
|
|
|
# PostGIS casts geography to geometry when distance2 is calculated.
|
|
|
|
ref_dists = [0, 4899.68, 8081.30, 9115.15]
|
|
|
|
for z, ref in zip(qs, ref_dists):
|
|
|
|
self.assertAlmostEqual(z.distance2.m, ref, 2)
|
|
|
|
|
2020-11-14 22:08:30 +08:00
|
|
|
if not connection.ops.spatialite:
|
2016-11-16 20:07:36 +08:00
|
|
|
# Distance function combined with a lookup.
|
|
|
|
hzip = Zipcode.objects.get(code="77002")
|
|
|
|
self.assertEqual(qs.get(distance__lte=0), hzip)
|
2015-01-19 23:09:41 +08:00
|
|
|
|
2016-11-16 20:07:36 +08:00
|
|
|
@skipUnlessDBFeature("has_Area_function", "supports_area_geodetic")
|
2015-01-19 23:09:41 +08:00
|
|
|
def test_geography_area(self):
|
|
|
|
"""
|
|
|
|
Testing that Area calculations work on geography columns.
|
|
|
|
"""
|
|
|
|
# SELECT ST_Area(poly) FROM geogapp_zipcode WHERE code='77002';
|
|
|
|
z = Zipcode.objects.annotate(area=Area("poly")).get(code="77002")
|
2016-01-28 20:44:55 +08:00
|
|
|
# Round to the nearest thousand as possible values (depending on
|
|
|
|
# the database and geolib) include 5439084, 5439100, 5439101.
|
|
|
|
rounded_value = z.area.sq_m
|
|
|
|
rounded_value -= z.area.sq_m % 1000
|
|
|
|
self.assertEqual(rounded_value, 5439000)
|
2016-11-16 20:07:36 +08:00
|
|
|
|
|
|
|
@skipUnlessDBFeature("has_Area_function")
|
|
|
|
@skipIfDBFeature("supports_area_geodetic")
|
|
|
|
def test_geodetic_area_raises_if_not_supported(self):
|
2017-10-07 00:47:08 +08:00
|
|
|
with self.assertRaisesMessage(
|
|
|
|
NotSupportedError, "Area on geodetic coordinate systems not supported."
|
|
|
|
):
|
2016-11-16 20:07:36 +08:00
|
|
|
Zipcode.objects.annotate(area=Area("poly")).get(code="77002")
|