tests.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662
  1. import tempfile
  2. import unittest
  3. from io import StringIO
  4. from django.contrib.gis import gdal
  5. from django.contrib.gis.db.models import Extent, MakeLine, Union, functions
  6. from django.contrib.gis.geos import (
  7. GeometryCollection, GEOSGeometry, LinearRing, LineString, MultiLineString,
  8. MultiPoint, MultiPolygon, Point, Polygon, fromstr,
  9. )
  10. from django.core.management import call_command
  11. from django.db import DatabaseError, NotSupportedError, connection
  12. from django.db.models import F, OuterRef, Subquery
  13. from django.test import TestCase, skipUnlessDBFeature
  14. from django.test.utils import CaptureQueriesContext
  15. from ..utils import (
  16. mariadb, mysql, oracle, postgis, skipUnlessGISLookup, spatialite,
  17. )
  18. from .models import (
  19. City, Country, Feature, MinusOneSRID, MultiFields, NonConcreteModel,
  20. PennsylvaniaCity, State, Track,
  21. )
  22. class GeoModelTest(TestCase):
  23. fixtures = ['initial']
  24. def test_fixtures(self):
  25. "Testing geographic model initialization from fixtures."
  26. # Ensuring that data was loaded from initial data fixtures.
  27. self.assertEqual(2, Country.objects.count())
  28. self.assertEqual(8, City.objects.count())
  29. self.assertEqual(2, State.objects.count())
  30. def test_proxy(self):
  31. "Testing Lazy-Geometry support (using the GeometryProxy)."
  32. # Testing on a Point
  33. pnt = Point(0, 0)
  34. nullcity = City(name='NullCity', point=pnt)
  35. nullcity.save()
  36. # Making sure TypeError is thrown when trying to set with an
  37. # incompatible type.
  38. for bad in [5, 2.0, LineString((0, 0), (1, 1))]:
  39. with self.assertRaisesMessage(TypeError, 'Cannot set'):
  40. nullcity.point = bad
  41. # Now setting with a compatible GEOS Geometry, saving, and ensuring
  42. # the save took, notice no SRID is explicitly set.
  43. new = Point(5, 23)
  44. nullcity.point = new
  45. # Ensuring that the SRID is automatically set to that of the
  46. # field after assignment, but before saving.
  47. self.assertEqual(4326, nullcity.point.srid)
  48. nullcity.save()
  49. # Ensuring the point was saved correctly after saving
  50. self.assertEqual(new, City.objects.get(name='NullCity').point)
  51. # Setting the X and Y of the Point
  52. nullcity.point.x = 23
  53. nullcity.point.y = 5
  54. # Checking assignments pre & post-save.
  55. self.assertNotEqual(Point(23, 5, srid=4326), City.objects.get(name='NullCity').point)
  56. nullcity.save()
  57. self.assertEqual(Point(23, 5, srid=4326), City.objects.get(name='NullCity').point)
  58. nullcity.delete()
  59. # Testing on a Polygon
  60. shell = LinearRing((0, 0), (0, 90), (100, 90), (100, 0), (0, 0))
  61. inner = LinearRing((40, 40), (40, 60), (60, 60), (60, 40), (40, 40))
  62. # Creating a State object using a built Polygon
  63. ply = Polygon(shell, inner)
  64. nullstate = State(name='NullState', poly=ply)
  65. self.assertEqual(4326, nullstate.poly.srid) # SRID auto-set from None
  66. nullstate.save()
  67. ns = State.objects.get(name='NullState')
  68. self.assertEqual(connection.ops.Adapter._fix_polygon(ply), ns.poly)
  69. # Testing the `ogr` and `srs` lazy-geometry properties.
  70. self.assertIsInstance(ns.poly.ogr, gdal.OGRGeometry)
  71. self.assertEqual(ns.poly.wkb, ns.poly.ogr.wkb)
  72. self.assertIsInstance(ns.poly.srs, gdal.SpatialReference)
  73. self.assertEqual('WGS 84', ns.poly.srs.name)
  74. # Changing the interior ring on the poly attribute.
  75. new_inner = LinearRing((30, 30), (30, 70), (70, 70), (70, 30), (30, 30))
  76. ns.poly[1] = new_inner
  77. ply[1] = new_inner
  78. self.assertEqual(4326, ns.poly.srid)
  79. ns.save()
  80. self.assertEqual(
  81. connection.ops.Adapter._fix_polygon(ply),
  82. State.objects.get(name='NullState').poly
  83. )
  84. ns.delete()
  85. @skipUnlessDBFeature("supports_transform")
  86. def test_lookup_insert_transform(self):
  87. "Testing automatic transform for lookups and inserts."
  88. # San Antonio in 'WGS84' (SRID 4326)
  89. sa_4326 = 'POINT (-98.493183 29.424170)'
  90. wgs_pnt = fromstr(sa_4326, srid=4326) # Our reference point in WGS84
  91. # San Antonio in 'WGS 84 / Pseudo-Mercator' (SRID 3857)
  92. other_srid_pnt = wgs_pnt.transform(3857, clone=True)
  93. # Constructing & querying with a point from a different SRID. Oracle
  94. # `SDO_OVERLAPBDYINTERSECT` operates differently from
  95. # `ST_Intersects`, so contains is used instead.
  96. if oracle:
  97. tx = Country.objects.get(mpoly__contains=other_srid_pnt)
  98. else:
  99. tx = Country.objects.get(mpoly__intersects=other_srid_pnt)
  100. self.assertEqual('Texas', tx.name)
  101. # Creating San Antonio. Remember the Alamo.
  102. sa = City.objects.create(name='San Antonio', point=other_srid_pnt)
  103. # Now verifying that San Antonio was transformed correctly
  104. sa = City.objects.get(name='San Antonio')
  105. self.assertAlmostEqual(wgs_pnt.x, sa.point.x, 6)
  106. self.assertAlmostEqual(wgs_pnt.y, sa.point.y, 6)
  107. # If the GeometryField SRID is -1, then we shouldn't perform any
  108. # transformation if the SRID of the input geometry is different.
  109. m1 = MinusOneSRID(geom=Point(17, 23, srid=4326))
  110. m1.save()
  111. self.assertEqual(-1, m1.geom.srid)
  112. def test_createnull(self):
  113. "Testing creating a model instance and the geometry being None"
  114. c = City()
  115. self.assertIsNone(c.point)
  116. def test_geometryfield(self):
  117. "Testing the general GeometryField."
  118. Feature(name='Point', geom=Point(1, 1)).save()
  119. Feature(name='LineString', geom=LineString((0, 0), (1, 1), (5, 5))).save()
  120. Feature(name='Polygon', geom=Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0)))).save()
  121. Feature(name='GeometryCollection',
  122. geom=GeometryCollection(Point(2, 2), LineString((0, 0), (2, 2)),
  123. Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0))))).save()
  124. f_1 = Feature.objects.get(name='Point')
  125. self.assertIsInstance(f_1.geom, Point)
  126. self.assertEqual((1.0, 1.0), f_1.geom.tuple)
  127. f_2 = Feature.objects.get(name='LineString')
  128. self.assertIsInstance(f_2.geom, LineString)
  129. self.assertEqual(((0.0, 0.0), (1.0, 1.0), (5.0, 5.0)), f_2.geom.tuple)
  130. f_3 = Feature.objects.get(name='Polygon')
  131. self.assertIsInstance(f_3.geom, Polygon)
  132. f_4 = Feature.objects.get(name='GeometryCollection')
  133. self.assertIsInstance(f_4.geom, GeometryCollection)
  134. self.assertEqual(f_3.geom, f_4.geom[2])
  135. @skipUnlessDBFeature("supports_transform")
  136. def test_inherited_geofields(self):
  137. "Database functions on inherited Geometry fields."
  138. # Creating a Pennsylvanian city.
  139. PennsylvaniaCity.objects.create(name='Mansfield', county='Tioga', point='POINT(-77.071445 41.823881)')
  140. # All transformation SQL will need to be performed on the
  141. # _parent_ table.
  142. qs = PennsylvaniaCity.objects.annotate(new_point=functions.Transform('point', srid=32128))
  143. self.assertEqual(1, qs.count())
  144. for pc in qs:
  145. self.assertEqual(32128, pc.new_point.srid)
  146. def test_raw_sql_query(self):
  147. "Testing raw SQL query."
  148. cities1 = City.objects.all()
  149. point_select = connection.ops.select % 'point'
  150. cities2 = list(City.objects.raw(
  151. 'select id, name, %s as point from geoapp_city' % point_select
  152. ))
  153. self.assertEqual(len(cities1), len(cities2))
  154. with self.assertNumQueries(0): # Ensure point isn't deferred.
  155. self.assertIsInstance(cities2[0].point, Point)
  156. def test_dumpdata_loaddata_cycle(self):
  157. """
  158. Test a dumpdata/loaddata cycle with geographic data.
  159. """
  160. out = StringIO()
  161. original_data = list(City.objects.all().order_by('name'))
  162. call_command('dumpdata', 'geoapp.City', stdout=out)
  163. result = out.getvalue()
  164. houston = City.objects.get(name='Houston')
  165. self.assertIn('"point": "%s"' % houston.point.ewkt, result)
  166. # Reload now dumped data
  167. with tempfile.NamedTemporaryFile(mode='w', suffix='.json') as tmp:
  168. tmp.write(result)
  169. tmp.seek(0)
  170. call_command('loaddata', tmp.name, verbosity=0)
  171. self.assertEqual(original_data, list(City.objects.all().order_by('name')))
  172. @skipUnlessDBFeature("supports_empty_geometries")
  173. def test_empty_geometries(self):
  174. geometry_classes = [
  175. Point,
  176. LineString,
  177. LinearRing,
  178. Polygon,
  179. MultiPoint,
  180. MultiLineString,
  181. MultiPolygon,
  182. GeometryCollection,
  183. ]
  184. for klass in geometry_classes:
  185. g = klass(srid=4326)
  186. feature = Feature.objects.create(name='Empty %s' % klass.__name__, geom=g)
  187. feature.refresh_from_db()
  188. if klass is LinearRing:
  189. # LinearRing isn't representable in WKB, so GEOSGeomtry.wkb
  190. # uses LineString instead.
  191. g = LineString(srid=4326)
  192. self.assertEqual(feature.geom, g)
  193. self.assertEqual(feature.geom.srid, g.srid)
  194. class GeoLookupTest(TestCase):
  195. fixtures = ['initial']
  196. def test_disjoint_lookup(self):
  197. "Testing the `disjoint` lookup type."
  198. if mysql and not mariadb and connection.mysql_version < (8, 0, 0):
  199. raise unittest.SkipTest('MySQL < 8 gives different results.')
  200. ptown = City.objects.get(name='Pueblo')
  201. qs1 = City.objects.filter(point__disjoint=ptown.point)
  202. self.assertEqual(7, qs1.count())
  203. qs2 = State.objects.filter(poly__disjoint=ptown.point)
  204. self.assertEqual(1, qs2.count())
  205. self.assertEqual('Kansas', qs2[0].name)
  206. def test_contains_contained_lookups(self):
  207. "Testing the 'contained', 'contains', and 'bbcontains' lookup types."
  208. # Getting Texas, yes we were a country -- once ;)
  209. texas = Country.objects.get(name='Texas')
  210. # Seeing what cities are in Texas, should get Houston and Dallas,
  211. # and Oklahoma City because 'contained' only checks on the
  212. # _bounding box_ of the Geometries.
  213. if connection.features.supports_contained_lookup:
  214. qs = City.objects.filter(point__contained=texas.mpoly)
  215. self.assertEqual(3, qs.count())
  216. cities = ['Houston', 'Dallas', 'Oklahoma City']
  217. for c in qs:
  218. self.assertIn(c.name, cities)
  219. # Pulling out some cities.
  220. houston = City.objects.get(name='Houston')
  221. wellington = City.objects.get(name='Wellington')
  222. pueblo = City.objects.get(name='Pueblo')
  223. okcity = City.objects.get(name='Oklahoma City')
  224. lawrence = City.objects.get(name='Lawrence')
  225. # Now testing contains on the countries using the points for
  226. # Houston and Wellington.
  227. tx = Country.objects.get(mpoly__contains=houston.point) # Query w/GEOSGeometry
  228. nz = Country.objects.get(mpoly__contains=wellington.point.hex) # Query w/EWKBHEX
  229. self.assertEqual('Texas', tx.name)
  230. self.assertEqual('New Zealand', nz.name)
  231. # Testing `contains` on the states using the point for Lawrence.
  232. ks = State.objects.get(poly__contains=lawrence.point)
  233. self.assertEqual('Kansas', ks.name)
  234. # Pueblo and Oklahoma City (even though OK City is within the bounding box of Texas)
  235. # are not contained in Texas or New Zealand.
  236. self.assertEqual(len(Country.objects.filter(mpoly__contains=pueblo.point)), 0) # Query w/GEOSGeometry object
  237. self.assertEqual(len(Country.objects.filter(mpoly__contains=okcity.point.wkt)), 0) # Query w/WKT
  238. # OK City is contained w/in bounding box of Texas.
  239. if connection.features.supports_bbcontains_lookup:
  240. qs = Country.objects.filter(mpoly__bbcontains=okcity.point)
  241. self.assertEqual(1, len(qs))
  242. self.assertEqual('Texas', qs[0].name)
  243. @skipUnlessDBFeature("supports_crosses_lookup")
  244. def test_crosses_lookup(self):
  245. Track.objects.create(
  246. name='Line1',
  247. line=LineString([(-95, 29), (-60, 0)])
  248. )
  249. self.assertEqual(
  250. Track.objects.filter(line__crosses=LineString([(-95, 0), (-60, 29)])).count(),
  251. 1
  252. )
  253. self.assertEqual(
  254. Track.objects.filter(line__crosses=LineString([(-95, 30), (0, 30)])).count(),
  255. 0
  256. )
  257. @skipUnlessDBFeature("supports_isvalid_lookup")
  258. def test_isvalid_lookup(self):
  259. invalid_geom = fromstr('POLYGON((0 0, 0 1, 1 1, 1 0, 1 1, 1 0, 0 0))')
  260. State.objects.create(name='invalid', poly=invalid_geom)
  261. qs = State.objects.all()
  262. if oracle or (mysql and connection.mysql_version < (8, 0, 0)):
  263. # Kansas has adjacent vertices with distance 6.99244813842e-12
  264. # which is smaller than the default Oracle tolerance.
  265. # It's invalid on MySQL < 8 also.
  266. qs = qs.exclude(name='Kansas')
  267. self.assertEqual(State.objects.filter(name='Kansas', poly__isvalid=False).count(), 1)
  268. self.assertEqual(qs.filter(poly__isvalid=False).count(), 1)
  269. self.assertEqual(qs.filter(poly__isvalid=True).count(), qs.count() - 1)
  270. @skipUnlessDBFeature("supports_left_right_lookups")
  271. def test_left_right_lookups(self):
  272. "Testing the 'left' and 'right' lookup types."
  273. # Left: A << B => true if xmax(A) < xmin(B)
  274. # Right: A >> B => true if xmin(A) > xmax(B)
  275. # See: BOX2D_left() and BOX2D_right() in lwgeom_box2dfloat4.c in PostGIS source.
  276. # Getting the borders for Colorado & Kansas
  277. co_border = State.objects.get(name='Colorado').poly
  278. ks_border = State.objects.get(name='Kansas').poly
  279. # Note: Wellington has an 'X' value of 174, so it will not be considered
  280. # to the left of CO.
  281. # These cities should be strictly to the right of the CO border.
  282. cities = ['Houston', 'Dallas', 'Oklahoma City',
  283. 'Lawrence', 'Chicago', 'Wellington']
  284. qs = City.objects.filter(point__right=co_border)
  285. self.assertEqual(6, len(qs))
  286. for c in qs:
  287. self.assertIn(c.name, cities)
  288. # These cities should be strictly to the right of the KS border.
  289. cities = ['Chicago', 'Wellington']
  290. qs = City.objects.filter(point__right=ks_border)
  291. self.assertEqual(2, len(qs))
  292. for c in qs:
  293. self.assertIn(c.name, cities)
  294. # Note: Wellington has an 'X' value of 174, so it will not be considered
  295. # to the left of CO.
  296. vic = City.objects.get(point__left=co_border)
  297. self.assertEqual('Victoria', vic.name)
  298. cities = ['Pueblo', 'Victoria']
  299. qs = City.objects.filter(point__left=ks_border)
  300. self.assertEqual(2, len(qs))
  301. for c in qs:
  302. self.assertIn(c.name, cities)
  303. @skipUnlessGISLookup("strictly_above", "strictly_below")
  304. def test_strictly_above_below_lookups(self):
  305. dallas = City.objects.get(name='Dallas')
  306. self.assertQuerysetEqual(
  307. City.objects.filter(point__strictly_above=dallas.point).order_by('name'),
  308. ['Chicago', 'Lawrence', 'Oklahoma City', 'Pueblo', 'Victoria'],
  309. lambda b: b.name
  310. )
  311. self.assertQuerysetEqual(
  312. City.objects.filter(point__strictly_below=dallas.point).order_by('name'),
  313. ['Houston', 'Wellington'],
  314. lambda b: b.name
  315. )
  316. def test_equals_lookups(self):
  317. "Testing the 'same_as' and 'equals' lookup types."
  318. pnt = fromstr('POINT (-95.363151 29.763374)', srid=4326)
  319. c1 = City.objects.get(point=pnt)
  320. c2 = City.objects.get(point__same_as=pnt)
  321. c3 = City.objects.get(point__equals=pnt)
  322. for c in [c1, c2, c3]:
  323. self.assertEqual('Houston', c.name)
  324. @skipUnlessDBFeature("supports_null_geometries")
  325. def test_null_geometries(self):
  326. "Testing NULL geometry support, and the `isnull` lookup type."
  327. # Creating a state with a NULL boundary.
  328. State.objects.create(name='Puerto Rico')
  329. # Querying for both NULL and Non-NULL values.
  330. nullqs = State.objects.filter(poly__isnull=True)
  331. validqs = State.objects.filter(poly__isnull=False)
  332. # Puerto Rico should be NULL (it's a commonwealth unincorporated territory)
  333. self.assertEqual(1, len(nullqs))
  334. self.assertEqual('Puerto Rico', nullqs[0].name)
  335. # GeometryField=None is an alias for __isnull=True.
  336. self.assertCountEqual(State.objects.filter(poly=None), nullqs)
  337. self.assertCountEqual(State.objects.exclude(poly=None), validqs)
  338. # The valid states should be Colorado & Kansas
  339. self.assertEqual(2, len(validqs))
  340. state_names = [s.name for s in validqs]
  341. self.assertIn('Colorado', state_names)
  342. self.assertIn('Kansas', state_names)
  343. # Saving another commonwealth w/a NULL geometry.
  344. nmi = State.objects.create(name='Northern Mariana Islands', poly=None)
  345. self.assertIsNone(nmi.poly)
  346. # Assigning a geometry and saving -- then UPDATE back to NULL.
  347. nmi.poly = 'POLYGON((0 0,1 0,1 1,1 0,0 0))'
  348. nmi.save()
  349. State.objects.filter(name='Northern Mariana Islands').update(poly=None)
  350. self.assertIsNone(State.objects.get(name='Northern Mariana Islands').poly)
  351. @skipUnlessDBFeature('supports_null_geometries', 'supports_crosses_lookup', 'supports_relate_lookup')
  352. def test_null_geometries_excluded_in_lookups(self):
  353. """NULL features are excluded in spatial lookup functions."""
  354. null = State.objects.create(name='NULL', poly=None)
  355. queries = [
  356. ('equals', Point(1, 1)),
  357. ('disjoint', Point(1, 1)),
  358. ('touches', Point(1, 1)),
  359. ('crosses', LineString((0, 0), (1, 1), (5, 5))),
  360. ('within', Point(1, 1)),
  361. ('overlaps', LineString((0, 0), (1, 1), (5, 5))),
  362. ('contains', LineString((0, 0), (1, 1), (5, 5))),
  363. ('intersects', LineString((0, 0), (1, 1), (5, 5))),
  364. ('relate', (Point(1, 1), 'T*T***FF*')),
  365. ('same_as', Point(1, 1)),
  366. ('exact', Point(1, 1)),
  367. ('coveredby', Point(1, 1)),
  368. ('covers', Point(1, 1)),
  369. ]
  370. for lookup, geom in queries:
  371. with self.subTest(lookup=lookup):
  372. self.assertNotIn(null, State.objects.filter(**{'poly__%s' % lookup: geom}))
  373. def test_wkt_string_in_lookup(self):
  374. # Valid WKT strings don't emit error logs.
  375. with self.assertRaisesMessage(AssertionError, 'no logs'):
  376. with self.assertLogs('django.contrib.gis', 'ERROR'):
  377. State.objects.filter(poly__intersects='LINESTRING(0 0, 1 1, 5 5)')
  378. @skipUnlessDBFeature("supports_relate_lookup")
  379. def test_relate_lookup(self):
  380. "Testing the 'relate' lookup type."
  381. # To make things more interesting, we will have our Texas reference point in
  382. # different SRIDs.
  383. pnt1 = fromstr('POINT (649287.0363174 4177429.4494686)', srid=2847)
  384. pnt2 = fromstr('POINT(-98.4919715741052 29.4333344025053)', srid=4326)
  385. # Not passing in a geometry as first param raises a TypeError when
  386. # initializing the QuerySet.
  387. with self.assertRaises(ValueError):
  388. Country.objects.filter(mpoly__relate=(23, 'foo'))
  389. # Making sure the right exception is raised for the given
  390. # bad arguments.
  391. for bad_args, e in [((pnt1, 0), ValueError), ((pnt2, 'T*T***FF*', 0), ValueError)]:
  392. qs = Country.objects.filter(mpoly__relate=bad_args)
  393. with self.assertRaises(e):
  394. qs.count()
  395. # Relate works differently for the different backends.
  396. if postgis or spatialite or mariadb:
  397. contains_mask = 'T*T***FF*'
  398. within_mask = 'T*F**F***'
  399. intersects_mask = 'T********'
  400. elif oracle:
  401. contains_mask = 'contains'
  402. within_mask = 'inside'
  403. # TODO: This is not quite the same as the PostGIS mask above
  404. intersects_mask = 'overlapbdyintersect'
  405. # Testing contains relation mask.
  406. if connection.features.supports_transform:
  407. self.assertEqual(
  408. Country.objects.get(mpoly__relate=(pnt1, contains_mask)).name,
  409. 'Texas',
  410. )
  411. self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, contains_mask)).name)
  412. # Testing within relation mask.
  413. ks = State.objects.get(name='Kansas')
  414. self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, within_mask)).name)
  415. # Testing intersection relation mask.
  416. if not oracle:
  417. if connection.features.supports_transform:
  418. self.assertEqual(
  419. Country.objects.get(mpoly__relate=(pnt1, intersects_mask)).name,
  420. 'Texas',
  421. )
  422. self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, intersects_mask)).name)
  423. self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, intersects_mask)).name)
  424. # With a complex geometry expression
  425. mask = 'anyinteract' if oracle else within_mask
  426. self.assertFalse(City.objects.exclude(point__relate=(functions.Union('point', 'point'), mask)))
  427. def test_gis_lookups_with_complex_expressions(self):
  428. multiple_arg_lookups = {'dwithin', 'relate'} # These lookups are tested elsewhere.
  429. lookups = connection.ops.gis_operators.keys() - multiple_arg_lookups
  430. self.assertTrue(lookups, 'No lookups found')
  431. for lookup in lookups:
  432. with self.subTest(lookup):
  433. City.objects.filter(**{'point__' + lookup: functions.Union('point', 'point')}).exists()
  434. def test_subquery_annotation(self):
  435. multifields = MultiFields.objects.create(
  436. city=City.objects.create(point=Point(1, 1)),
  437. point=Point(2, 2),
  438. poly=Polygon.from_bbox((0, 0, 2, 2)),
  439. )
  440. qs = MultiFields.objects.annotate(
  441. city_point=Subquery(City.objects.filter(
  442. id=OuterRef('city'),
  443. ).values('point')),
  444. ).filter(
  445. city_point__within=F('poly'),
  446. )
  447. self.assertEqual(qs.get(), multifields)
  448. class GeoQuerySetTest(TestCase):
  449. # TODO: GeoQuerySet is removed, organize these test better.
  450. fixtures = ['initial']
  451. @skipUnlessDBFeature("supports_extent_aggr")
  452. def test_extent(self):
  453. """
  454. Testing the `Extent` aggregate.
  455. """
  456. # Reference query:
  457. # `SELECT ST_extent(point) FROM geoapp_city WHERE (name='Houston' or name='Dallas');`
  458. # => BOX(-96.8016128540039 29.7633724212646,-95.3631439208984 32.7820587158203)
  459. expected = (-96.8016128540039, 29.7633724212646, -95.3631439208984, 32.782058715820)
  460. qs = City.objects.filter(name__in=('Houston', 'Dallas'))
  461. extent = qs.aggregate(Extent('point'))['point__extent']
  462. for val, exp in zip(extent, expected):
  463. self.assertAlmostEqual(exp, val, 4)
  464. self.assertIsNone(City.objects.filter(name=('Smalltown')).aggregate(Extent('point'))['point__extent'])
  465. @skipUnlessDBFeature("supports_extent_aggr")
  466. def test_extent_with_limit(self):
  467. """
  468. Testing if extent supports limit.
  469. """
  470. extent1 = City.objects.all().aggregate(Extent('point'))['point__extent']
  471. extent2 = City.objects.all()[:3].aggregate(Extent('point'))['point__extent']
  472. self.assertNotEqual(extent1, extent2)
  473. def test_make_line(self):
  474. """
  475. Testing the `MakeLine` aggregate.
  476. """
  477. if not connection.features.supports_make_line_aggr:
  478. with self.assertRaises(NotSupportedError):
  479. City.objects.all().aggregate(MakeLine('point'))
  480. return
  481. # MakeLine on an inappropriate field returns simply None
  482. self.assertIsNone(State.objects.aggregate(MakeLine('poly'))['poly__makeline'])
  483. # Reference query:
  484. # SELECT AsText(ST_MakeLine(geoapp_city.point)) FROM geoapp_city;
  485. ref_line = GEOSGeometry(
  486. 'LINESTRING(-95.363151 29.763374,-96.801611 32.782057,'
  487. '-97.521157 34.464642,174.783117 -41.315268,-104.609252 38.255001,'
  488. '-95.23506 38.971823,-87.650175 41.850385,-123.305196 48.462611)',
  489. srid=4326
  490. )
  491. # We check for equality with a tolerance of 10e-5 which is a lower bound
  492. # of the precisions of ref_line coordinates
  493. line = City.objects.aggregate(MakeLine('point'))['point__makeline']
  494. self.assertTrue(
  495. ref_line.equals_exact(line, tolerance=10e-5),
  496. "%s != %s" % (ref_line, line)
  497. )
  498. @skipUnlessDBFeature('supports_union_aggr')
  499. def test_unionagg(self):
  500. """
  501. Testing the `Union` aggregate.
  502. """
  503. tx = Country.objects.get(name='Texas').mpoly
  504. # Houston, Dallas -- Ordering may differ depending on backend or GEOS version.
  505. union = GEOSGeometry('MULTIPOINT(-96.801611 32.782057,-95.363151 29.763374)')
  506. qs = City.objects.filter(point__within=tx)
  507. with self.assertRaises(ValueError):
  508. qs.aggregate(Union('name'))
  509. # Using `field_name` keyword argument in one query and specifying an
  510. # order in the other (which should not be used because this is
  511. # an aggregate method on a spatial column)
  512. u1 = qs.aggregate(Union('point'))['point__union']
  513. u2 = qs.order_by('name').aggregate(Union('point'))['point__union']
  514. self.assertTrue(union.equals(u1))
  515. self.assertTrue(union.equals(u2))
  516. qs = City.objects.filter(name='NotACity')
  517. self.assertIsNone(qs.aggregate(Union('point'))['point__union'])
  518. @skipUnlessDBFeature('supports_union_aggr')
  519. def test_geoagg_subquery(self):
  520. ks = State.objects.get(name='Kansas')
  521. union = GEOSGeometry('MULTIPOINT(-95.235060 38.971823)')
  522. # Use distinct() to force the usage of a subquery for aggregation.
  523. with CaptureQueriesContext(connection) as ctx:
  524. self.assertIs(union.equals(
  525. City.objects.filter(point__within=ks.poly).distinct().aggregate(
  526. Union('point'),
  527. )['point__union'],
  528. ), True)
  529. self.assertIn('subquery', ctx.captured_queries[0]['sql'])
  530. @unittest.skipUnless(
  531. connection.vendor == 'oracle',
  532. 'Oracle supports tolerance parameter.',
  533. )
  534. def test_unionagg_tolerance(self):
  535. City.objects.create(
  536. point=fromstr('POINT(-96.467222 32.751389)', srid=4326),
  537. name='Forney',
  538. )
  539. tx = Country.objects.get(name='Texas').mpoly
  540. # Tolerance is greater than distance between Forney and Dallas, that's
  541. # why Dallas is ignored.
  542. forney_houston = GEOSGeometry(
  543. 'MULTIPOINT(-95.363151 29.763374, -96.467222 32.751389)',
  544. srid=4326,
  545. )
  546. self.assertIs(
  547. forney_houston.equals_exact(
  548. City.objects.filter(point__within=tx).aggregate(
  549. Union('point', tolerance=32000),
  550. )['point__union'],
  551. tolerance=10e-6,
  552. ),
  553. True,
  554. )
  555. @unittest.skipUnless(
  556. connection.vendor == 'oracle',
  557. 'Oracle supports tolerance parameter.',
  558. )
  559. def test_unionagg_tolerance_escaping(self):
  560. tx = Country.objects.get(name='Texas').mpoly
  561. with self.assertRaises(DatabaseError):
  562. City.objects.filter(point__within=tx).aggregate(
  563. Union('point', tolerance='0.05))), (((1'),
  564. )
  565. def test_within_subquery(self):
  566. """
  567. Using a queryset inside a geo lookup is working (using a subquery)
  568. (#14483).
  569. """
  570. tex_cities = City.objects.filter(
  571. point__within=Country.objects.filter(name='Texas').values('mpoly')).order_by('name')
  572. self.assertEqual(list(tex_cities.values_list('name', flat=True)), ['Dallas', 'Houston'])
  573. def test_non_concrete_field(self):
  574. NonConcreteModel.objects.create(point=Point(0, 0), name='name')
  575. list(NonConcreteModel.objects.all())
  576. def test_values_srid(self):
  577. for c, v in zip(City.objects.all(), City.objects.values()):
  578. self.assertEqual(c.point.srid, v['point'].srid)