test_rasterfield.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. import json
  2. from django.contrib.gis.db.models.lookups import (
  3. DistanceLookupBase, gis_lookups,
  4. )
  5. from django.contrib.gis.gdal import HAS_GDAL
  6. from django.contrib.gis.geos import GEOSGeometry
  7. from django.contrib.gis.measure import D
  8. from django.contrib.gis.shortcuts import numpy
  9. from django.db.models import Q
  10. from django.test import TransactionTestCase, skipUnlessDBFeature
  11. from ..data.rasters.textrasters import JSON_RASTER
  12. if HAS_GDAL:
  13. from django.contrib.gis.gdal import GDALRaster
  14. from .models import RasterModel, RasterRelatedModel
  15. @skipUnlessDBFeature('supports_raster')
  16. class RasterFieldTest(TransactionTestCase):
  17. available_apps = ['gis_tests.rasterapp']
  18. def setUp(self):
  19. rast = GDALRaster({
  20. "srid": 4326,
  21. "origin": [0, 0],
  22. "scale": [-1, 1],
  23. "skew": [0, 0],
  24. "width": 5,
  25. "height": 5,
  26. "nr_of_bands": 2,
  27. "bands": [{"data": range(25)}, {"data": range(25, 50)}],
  28. })
  29. model_instance = RasterModel.objects.create(
  30. rast=rast,
  31. rastprojected=rast,
  32. geom="POINT (-95.37040 29.70486)",
  33. )
  34. RasterRelatedModel.objects.create(rastermodel=model_instance)
  35. def test_field_null_value(self):
  36. """
  37. Test creating a model where the RasterField has a null value.
  38. """
  39. r = RasterModel.objects.create(rast=None)
  40. r.refresh_from_db()
  41. self.assertIsNone(r.rast)
  42. def test_access_band_data_directly_from_queryset(self):
  43. RasterModel.objects.create(rast=JSON_RASTER)
  44. qs = RasterModel.objects.all()
  45. qs[0].rast.bands[0].data()
  46. def test_model_creation(self):
  47. """
  48. Test RasterField through a test model.
  49. """
  50. # Create model instance from JSON raster
  51. r = RasterModel.objects.create(rast=JSON_RASTER)
  52. r.refresh_from_db()
  53. # Test raster metadata properties
  54. self.assertEqual((5, 5), (r.rast.width, r.rast.height))
  55. self.assertEqual([0.0, -1.0, 0.0, 0.0, 0.0, 1.0], r.rast.geotransform)
  56. self.assertIsNone(r.rast.bands[0].nodata_value)
  57. # Compare srs
  58. self.assertEqual(r.rast.srs.srid, 4326)
  59. # Compare pixel values
  60. band = r.rast.bands[0].data()
  61. # If numpy, convert result to list
  62. if numpy:
  63. band = band.flatten().tolist()
  64. # Loop through rows in band data and assert single
  65. # value is as expected.
  66. self.assertEqual(
  67. [
  68. 0.0, 1.0, 2.0, 3.0, 4.0,
  69. 5.0, 6.0, 7.0, 8.0, 9.0,
  70. 10.0, 11.0, 12.0, 13.0, 14.0,
  71. 15.0, 16.0, 17.0, 18.0, 19.0,
  72. 20.0, 21.0, 22.0, 23.0, 24.0
  73. ],
  74. band
  75. )
  76. def test_implicit_raster_transformation(self):
  77. """
  78. Test automatic transformation of rasters with srid different from the
  79. field srid.
  80. """
  81. # Parse json raster
  82. rast = json.loads(JSON_RASTER)
  83. # Update srid to another value
  84. rast['srid'] = 3086
  85. # Save model and get it from db
  86. r = RasterModel.objects.create(rast=rast)
  87. r.refresh_from_db()
  88. # Confirm raster has been transformed to the default srid
  89. self.assertEqual(r.rast.srs.srid, 4326)
  90. # Confirm geotransform is in lat/lon
  91. self.assertEqual(
  92. r.rast.geotransform,
  93. [-87.9298551266551, 9.459646421449934e-06, 0.0,
  94. 23.94249275457565, 0.0, -9.459646421449934e-06]
  95. )
  96. def test_verbose_name_arg(self):
  97. """
  98. RasterField should accept a positional verbose name argument.
  99. """
  100. self.assertEqual(
  101. RasterModel._meta.get_field('rast').verbose_name,
  102. 'A Verbose Raster Name'
  103. )
  104. def test_all_gis_lookups_with_rasters(self):
  105. """
  106. Evaluate all possible lookups for all input combinations (i.e.
  107. raster-raster, raster-geom, geom-raster) and for projected and
  108. unprojected coordinate systems. This test just checks that the lookup
  109. can be called, but doesn't check if the result makes logical sense.
  110. """
  111. from django.contrib.gis.db.backends.postgis.operations import PostGISOperations
  112. # Create test raster and geom.
  113. rast = GDALRaster(json.loads(JSON_RASTER))
  114. stx_pnt = GEOSGeometry('POINT (-95.370401017314293 29.704867409475465)', 4326)
  115. stx_pnt.transform(3086)
  116. # Loop through all the GIS lookups.
  117. for name, lookup in gis_lookups.items():
  118. # Construct lookup filter strings.
  119. combo_keys = [
  120. field + name for field in [
  121. 'rast__', 'rast__', 'rastprojected__0__', 'rast__',
  122. 'rastprojected__', 'geom__', 'rast__',
  123. ]
  124. ]
  125. if issubclass(lookup, DistanceLookupBase):
  126. # Set lookup values for distance lookups.
  127. combo_values = [
  128. (rast, 50, 'spheroid'),
  129. (rast, 0, 50, 'spheroid'),
  130. (rast, 0, D(km=1)),
  131. (stx_pnt, 0, 500),
  132. (stx_pnt, D(km=1000)),
  133. (rast, 500),
  134. (json.loads(JSON_RASTER), 500),
  135. ]
  136. elif name == 'relate':
  137. # Set lookup values for the relate lookup.
  138. combo_values = [
  139. (rast, 'T*T***FF*'),
  140. (rast, 0, 'T*T***FF*'),
  141. (rast, 0, 'T*T***FF*'),
  142. (stx_pnt, 0, 'T*T***FF*'),
  143. (stx_pnt, 'T*T***FF*'),
  144. (rast, 'T*T***FF*'),
  145. (json.loads(JSON_RASTER), 'T*T***FF*'),
  146. ]
  147. elif name == 'isvalid':
  148. # The isvalid lookup doesn't make sense for rasters.
  149. continue
  150. elif PostGISOperations.gis_operators[name].func:
  151. # Set lookup values for all function based operators.
  152. combo_values = [
  153. rast, (rast, 0), (rast, 0), (stx_pnt, 0), stx_pnt,
  154. rast, rast, json.loads(JSON_RASTER)
  155. ]
  156. else:
  157. # Override band lookup for these, as it's not supported.
  158. combo_keys[2] = 'rastprojected__' + name
  159. # Set lookup values for all other operators.
  160. combo_values = [rast, rast, rast, stx_pnt, stx_pnt, rast, rast, json.loads(JSON_RASTER)]
  161. # Create query filter combinations.
  162. combos = [{x[0]: x[1]} for x in zip(combo_keys, combo_values)]
  163. for combo in combos:
  164. # Apply this query filter.
  165. qs = RasterModel.objects.filter(**combo)
  166. # Evaluate normal filter qs.
  167. self.assertIn(qs.count(), [0, 1])
  168. # Evaluate on conditional Q expressions.
  169. qs = RasterModel.objects.filter(Q(**combos[0]) & Q(**combos[1]))
  170. self.assertIn(qs.count(), [0, 1])
  171. def test_dwithin_gis_lookup_ouptut_with_rasters(self):
  172. """
  173. Check the logical functionality of the dwithin lookup for different
  174. input parameters.
  175. """
  176. # Create test raster and geom.
  177. rast = GDALRaster(json.loads(JSON_RASTER))
  178. stx_pnt = GEOSGeometry('POINT (-95.370401017314293 29.704867409475465)', 4326)
  179. stx_pnt.transform(3086)
  180. # Filter raster with different lookup raster formats.
  181. qs = RasterModel.objects.filter(rastprojected__dwithin=(rast, D(km=1)))
  182. self.assertEqual(qs.count(), 1)
  183. qs = RasterModel.objects.filter(rastprojected__dwithin=(json.loads(JSON_RASTER), D(km=1)))
  184. self.assertEqual(qs.count(), 1)
  185. qs = RasterModel.objects.filter(rastprojected__dwithin=(JSON_RASTER, D(km=1)))
  186. self.assertEqual(qs.count(), 1)
  187. # Filter in an unprojected coordinate system.
  188. qs = RasterModel.objects.filter(rast__dwithin=(rast, 40))
  189. self.assertEqual(qs.count(), 1)
  190. # Filter with band index transform.
  191. qs = RasterModel.objects.filter(rast__1__dwithin=(rast, 1, 40))
  192. self.assertEqual(qs.count(), 1)
  193. qs = RasterModel.objects.filter(rast__1__dwithin=(rast, 40))
  194. self.assertEqual(qs.count(), 1)
  195. qs = RasterModel.objects.filter(rast__dwithin=(rast, 1, 40))
  196. self.assertEqual(qs.count(), 1)
  197. # Filter raster by geom.
  198. qs = RasterModel.objects.filter(rast__dwithin=(stx_pnt, 500))
  199. self.assertEqual(qs.count(), 1)
  200. qs = RasterModel.objects.filter(rastprojected__dwithin=(stx_pnt, D(km=10000)))
  201. self.assertEqual(qs.count(), 1)
  202. qs = RasterModel.objects.filter(rast__dwithin=(stx_pnt, 5))
  203. self.assertEqual(qs.count(), 0)
  204. qs = RasterModel.objects.filter(rastprojected__dwithin=(stx_pnt, D(km=100)))
  205. self.assertEqual(qs.count(), 0)
  206. # Filter geom by raster.
  207. qs = RasterModel.objects.filter(geom__dwithin=(rast, 500))
  208. self.assertEqual(qs.count(), 1)
  209. # Filter through related model.
  210. qs = RasterRelatedModel.objects.filter(rastermodel__rast__dwithin=(rast, 40))
  211. self.assertEqual(qs.count(), 1)
  212. # Filter through related model with band index transform
  213. qs = RasterRelatedModel.objects.filter(rastermodel__rast__1__dwithin=(rast, 40))
  214. self.assertEqual(qs.count(), 1)
  215. # Filter through conditional statements.
  216. qs = RasterModel.objects.filter(Q(rast__dwithin=(rast, 40)) & Q(rastprojected__dwithin=(stx_pnt, D(km=10000))))
  217. self.assertEqual(qs.count(), 1)
  218. # Filter through different lookup.
  219. qs = RasterModel.objects.filter(rastprojected__bbcontains=rast)
  220. self.assertEqual(qs.count(), 1)
  221. def test_lookup_input_tuple_too_long(self):
  222. rast = GDALRaster(json.loads(JSON_RASTER))
  223. qs = RasterModel.objects.filter(rast__bbcontains=(rast, 1, 2))
  224. msg = 'Tuple too long for lookup bbcontains.'
  225. with self.assertRaisesMessage(ValueError, msg):
  226. qs.count()
  227. def test_lookup_input_band_not_allowed(self):
  228. rast = GDALRaster(json.loads(JSON_RASTER))
  229. qs = RasterModel.objects.filter(rast__bbcontains=(rast, 1))
  230. msg = 'Band indices are not allowed for this operator, it works on bbox only.'
  231. with self.assertRaisesMessage(ValueError, msg):
  232. qs.count()
  233. def test_isvalid_lookup_with_raster_error(self):
  234. qs = RasterModel.objects.filter(rast__isvalid=True)
  235. msg = 'The isvalid lookup is only available on geometry fields.'
  236. with self.assertRaisesMessage(ValueError, msg):
  237. qs.count()
  238. def test_result_of_gis_lookup_with_rasters(self):
  239. # Point is in the interior
  240. qs = RasterModel.objects.filter(rast__contains=GEOSGeometry('POINT (-0.5 0.5)', 4326))
  241. self.assertEqual(qs.count(), 1)
  242. # Point is in the exterior
  243. qs = RasterModel.objects.filter(rast__contains=GEOSGeometry('POINT (0.5 0.5)', 4326))
  244. self.assertEqual(qs.count(), 0)
  245. # A point on the boundary is not contained properly
  246. qs = RasterModel.objects.filter(rast__contains_properly=GEOSGeometry('POINT (0 0)', 4326))
  247. self.assertEqual(qs.count(), 0)
  248. # Raster is located left of the point
  249. qs = RasterModel.objects.filter(rast__left=GEOSGeometry('POINT (1 0)', 4326))
  250. self.assertEqual(qs.count(), 1)
  251. def test_lookup_with_raster_bbox(self):
  252. rast = GDALRaster(json.loads(JSON_RASTER))
  253. # Shift raster upwards
  254. rast.origin.y = 2
  255. # The raster in the model is not strictly below
  256. qs = RasterModel.objects.filter(rast__strictly_below=rast)
  257. self.assertEqual(qs.count(), 0)
  258. # Shift raster further upwards
  259. rast.origin.y = 6
  260. # The raster in the model is strictly below
  261. qs = RasterModel.objects.filter(rast__strictly_below=rast)
  262. self.assertEqual(qs.count(), 1)
  263. def test_lookup_with_polygonized_raster(self):
  264. rast = GDALRaster(json.loads(JSON_RASTER))
  265. # Move raster to overlap with the model point on the left side
  266. rast.origin.x = -95.37040 + 1
  267. rast.origin.y = 29.70486
  268. # Raster overlaps with point in model
  269. qs = RasterModel.objects.filter(geom__intersects=rast)
  270. self.assertEqual(qs.count(), 1)
  271. # Change left side of raster to be nodata values
  272. rast.bands[0].data(data=[0, 0, 0, 1, 1], shape=(5, 1))
  273. rast.bands[0].nodata_value = 0
  274. qs = RasterModel.objects.filter(geom__intersects=rast)
  275. # Raster does not overlap anymore after polygonization
  276. # where the nodata zone is not included.
  277. self.assertEqual(qs.count(), 0)
  278. def test_lookup_value_error(self):
  279. # Test with invalid dict lookup parameter
  280. obj = dict()
  281. msg = "Couldn't create spatial object from lookup value '%s'." % obj
  282. with self.assertRaisesMessage(ValueError, msg):
  283. RasterModel.objects.filter(geom__intersects=obj)
  284. # Test with invalid string lookup parameter
  285. obj = '00000'
  286. msg = "Couldn't create spatial object from lookup value '%s'." % obj
  287. with self.assertRaisesMessage(ValueError, msg):
  288. RasterModel.objects.filter(geom__intersects=obj)