summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Wiesmann <daniel.wiesmann@gmail.com>2017-05-23 20:07:16 +0100
committerTim Graham <timograham@gmail.com>2017-06-07 11:30:35 -0400
commite0b456bee7bcf37e2a55471255686ee59c6cf03c (patch)
tree69823c29c563e009abd5dad777ce6fb363cde331
parent23825b2494a1edb1e02d57dbdc7eca0614cefcc8 (diff)
Fixed #28232 -- Made raster metadata readable and writable on GDALRaster/Band.
-rw-r--r--django/contrib/gis/gdal/prototypes/generation.py11
-rw-r--r--django/contrib/gis/gdal/prototypes/raster.py20
-rw-r--r--django/contrib/gis/gdal/raster/band.py4
-rw-r--r--django/contrib/gis/gdal/raster/base.py78
-rw-r--r--django/contrib/gis/gdal/raster/source.py14
-rw-r--r--docs/ref/contrib/gis/gdal.txt41
-rw-r--r--docs/releases/2.0.txt5
-rw-r--r--tests/gis_tests/gdal_tests/test_raster.py139
8 files changed, 263 insertions, 49 deletions
diff --git a/django/contrib/gis/gdal/prototypes/generation.py b/django/contrib/gis/gdal/prototypes/generation.py
index b0135c9f48..012a5baab0 100644
--- a/django/contrib/gis/gdal/prototypes/generation.py
+++ b/django/contrib/gis/gdal/prototypes/generation.py
@@ -2,7 +2,7 @@
This module contains functions that generate ctypes prototypes for the
GDAL routines.
"""
-from ctypes import c_char_p, c_double, c_int, c_int64, c_void_p
+from ctypes import POINTER, c_char_p, c_double, c_int, c_int64, c_void_p
from functools import partial
from django.contrib.gis.gdal.prototypes.errcheck import (
@@ -147,3 +147,12 @@ def voidptr_output(func, argtypes, errcheck=True):
if errcheck:
func.errcheck = check_pointer
return func
+
+
+def chararray_output(func, argtypes, errcheck=True):
+ """For functions that return a c_char_p array."""
+ func.argtypes = argtypes
+ func.restype = POINTER(c_char_p)
+ if errcheck:
+ func.errcheck = check_pointer
+ return func
diff --git a/django/contrib/gis/gdal/prototypes/raster.py b/django/contrib/gis/gdal/prototypes/raster.py
index af710dfb6d..7e95cbc9a4 100644
--- a/django/contrib/gis/gdal/prototypes/raster.py
+++ b/django/contrib/gis/gdal/prototypes/raster.py
@@ -7,13 +7,14 @@ from functools import partial
from django.contrib.gis.gdal.libgdal import GDAL_VERSION, std_call
from django.contrib.gis.gdal.prototypes.generation import (
- const_string_output, double_output, int_output, void_output,
- voidptr_output,
+ chararray_output, const_string_output, double_output, int_output,
+ void_output, voidptr_output,
)
# For more detail about c function names and definitions see
# http://gdal.org/gdal_8h.html
# http://gdal.org/gdalwarper_8h.html
+# http://www.gdal.org/gdal__utils_8h.html
# Prepare partial functions that use cpl error codes
void_output = partial(void_output, cpl=True)
@@ -48,6 +49,21 @@ set_ds_projection_ref = void_output(std_call('GDALSetProjection'), [c_void_p, c_
get_ds_geotransform = void_output(std_call('GDALGetGeoTransform'), [c_void_p, POINTER(c_double * 6)], errcheck=False)
set_ds_geotransform = void_output(std_call('GDALSetGeoTransform'), [c_void_p, POINTER(c_double * 6)])
+get_ds_metadata = chararray_output(std_call('GDALGetMetadata'), [c_void_p, c_char_p], errcheck=False)
+set_ds_metadata = void_output(std_call('GDALSetMetadata'), [c_void_p, POINTER(c_char_p), c_char_p])
+if GDAL_VERSION >= (1, 11):
+ get_ds_metadata_domain_list = chararray_output(std_call('GDALGetMetadataDomainList'), [c_void_p], errcheck=False)
+else:
+ get_ds_metadata_domain_list = None
+get_ds_metadata_item = const_string_output(std_call('GDALGetMetadataItem'), [c_void_p, c_char_p, c_char_p])
+set_ds_metadata_item = const_string_output(std_call('GDALSetMetadataItem'), [c_void_p, c_char_p, c_char_p, c_char_p])
+free_dsl = void_output(std_call('CSLDestroy'), [POINTER(c_char_p)], errcheck=False)
+
+if GDAL_VERSION >= (2, 1):
+ get_ds_info = const_string_output(std_call('GDALInfo'), [c_void_p, c_void_p])
+else:
+ get_ds_info = None
+
# Raster Band Routines
band_io = void_output(
std_call('GDALRasterIO'),
diff --git a/django/contrib/gis/gdal/raster/band.py b/django/contrib/gis/gdal/raster/band.py
index e78b370c09..19d8ef41cf 100644
--- a/django/contrib/gis/gdal/raster/band.py
+++ b/django/contrib/gis/gdal/raster/band.py
@@ -1,15 +1,15 @@
from ctypes import byref, c_double, c_int, c_void_p
-from django.contrib.gis.gdal.base import GDALBase
from django.contrib.gis.gdal.error import GDALException
from django.contrib.gis.gdal.prototypes import raster as capi
+from django.contrib.gis.gdal.raster.base import GDALRasterBase
from django.contrib.gis.shortcuts import numpy
from django.utils.encoding import force_text
from .const import GDAL_INTEGER_TYPES, GDAL_PIXEL_TYPES, GDAL_TO_CTYPES
-class GDALBand(GDALBase):
+class GDALBand(GDALRasterBase):
"""
Wrap a GDAL raster band, needs to be obtained from a GDALRaster object.
"""
diff --git a/django/contrib/gis/gdal/raster/base.py b/django/contrib/gis/gdal/raster/base.py
new file mode 100644
index 0000000000..c98f636ec2
--- /dev/null
+++ b/django/contrib/gis/gdal/raster/base.py
@@ -0,0 +1,78 @@
+from django.contrib.gis.gdal.base import GDALBase
+from django.contrib.gis.gdal.prototypes import raster as capi
+
+
+class GDALRasterBase(GDALBase):
+ """
+ Attributes that exist on both GDALRaster and GDALBand.
+ """
+ @property
+ def metadata(self):
+ """
+ Return the metadata for this raster or band. The return value is a
+ nested dictionary, where the first-level key is the metadata domain and
+ the second-level is the metadata item names and values for that domain.
+ """
+ if not capi.get_ds_metadata_domain_list:
+ raise ValueError('GDAL ≥ 1.11 is required for using the metadata property.')
+
+ # The initial metadata domain list contains the default domain.
+ # The default is returned if domain name is None.
+ domain_list = ['DEFAULT']
+
+ # Get additional metadata domains from the raster.
+ meta_list = capi.get_ds_metadata_domain_list(self._ptr)
+ if meta_list:
+ # The number of domains is unknown, so retrieve data until there
+ # are no more values in the ctypes array.
+ counter = 0
+ domain = meta_list[counter]
+ while domain:
+ domain_list.append(domain.decode())
+ counter += 1
+ domain = meta_list[counter]
+
+ # Free domain list array.
+ capi.free_dsl(meta_list)
+
+ # Retrieve metadata values for each domain.
+ result = {}
+ for domain in domain_list:
+ # Get metadata for this domain.
+ data = capi.get_ds_metadata(
+ self._ptr,
+ (None if domain == 'DEFAULT' else domain.encode()),
+ )
+ if not data:
+ continue
+ # The number of metadata items is unknown, so retrieve data until
+ # there are no more values in the ctypes array.
+ domain_meta = {}
+ counter = 0
+ item = data[counter]
+ while item:
+ key, val = item.decode().split('=')
+ domain_meta[key] = val
+ counter += 1
+ item = data[counter]
+ # The default domain values are returned if domain is None.
+ result[domain if domain else 'DEFAULT'] = domain_meta
+ return result
+
+ @metadata.setter
+ def metadata(self, value):
+ """
+ Set the metadata. Update only the domains that are contained in the
+ value dictionary.
+ """
+ # Loop through domains.
+ for domain, metadata in value.items():
+ # Set the domain to None for the default, otherwise encode.
+ domain = None if domain == 'DEFAULT' else domain.encode()
+ # Set each metadata entry separately.
+ for meta_name, meta_value in metadata.items():
+ capi.set_ds_metadata_item(
+ self._ptr, meta_name.encode(),
+ meta_value.encode() if meta_value else None,
+ domain,
+ )
diff --git a/django/contrib/gis/gdal/raster/source.py b/django/contrib/gis/gdal/raster/source.py
index 03fa1f8f22..609c9d6b46 100644
--- a/django/contrib/gis/gdal/raster/source.py
+++ b/django/contrib/gis/gdal/raster/source.py
@@ -2,11 +2,11 @@ import json
import os
from ctypes import addressof, byref, c_double, c_void_p
-from django.contrib.gis.gdal.base import GDALBase
from django.contrib.gis.gdal.driver import Driver
from django.contrib.gis.gdal.error import GDALException
from django.contrib.gis.gdal.prototypes import raster as capi
from django.contrib.gis.gdal.raster.band import BandList
+from django.contrib.gis.gdal.raster.base import GDALRasterBase
from django.contrib.gis.gdal.raster.const import GDAL_RESAMPLE_ALGORITHMS
from django.contrib.gis.gdal.srs import SpatialReference, SRSException
from django.contrib.gis.geometry.regex import json_regex
@@ -49,7 +49,7 @@ class TransformPoint(list):
self._raster.geotransform = gtf
-class GDALRaster(GDALBase):
+class GDALRaster(GDALRasterBase):
"""
Wrap a raster GDAL Data Source object.
"""
@@ -403,3 +403,13 @@ class GDALRaster(GDALBase):
# Warp the raster into new srid
return self.warp(data, resampling=resampling, max_error=max_error)
+
+ @property
+ def info(self):
+ """
+ Return information about this raster in a string format equivalent
+ to the output of the gdalinfo command line utility.
+ """
+ if not capi.get_ds_info:
+ raise ValueError('GDAL ≥ 2.1 is required for using the info property.')
+ return capi.get_ds_info(self.ptr, None).decode()
diff --git a/docs/ref/contrib/gis/gdal.txt b/docs/ref/contrib/gis/gdal.txt
index 3edcf48bd6..d6501cbf48 100644
--- a/docs/ref/contrib/gis/gdal.txt
+++ b/docs/ref/contrib/gis/gdal.txt
@@ -1391,6 +1391,40 @@ blue.
>>> target.origin
[-82.98492744885776, 27.601924753080144]
+ .. attribute:: info
+
+ .. versionadded:: 2.0
+
+ Returns a string with a summary of the raster. This is equivalent to
+ the `gdalinfo`__ command line utility.
+
+ __ http://www.gdal.org/gdalinfo.html
+
+ .. attribute:: metadata
+
+ .. versionadded:: 2.0
+
+ The metadata of this raster, represented as a nested dictionary. The
+ first-level key is the metadata domain. The second-level contains the
+ metadata item names and values from each domain.
+
+ To set or update a metadata item, pass the corresponding metadata item
+ to the method using the nested structure described above. Only keys
+ that are in the specified dictionary are updated; the rest of the
+ metadata remains unchanged.
+
+ To remove a metadata item, use ``None`` as the metadata value.
+
+ >>> rst = GDALRaster({'width': 10, 'height': 20, 'srid': 4326})
+ >>> rst.metadata
+ {}
+ >>> rst.metadata = {'DEFAULT': {'OWNER': 'Django', 'VERSION': '1.0'}}
+ >>> rst.metadata
+ {'DEFAULT': {'OWNER': 'Django', 'VERSION': '1.0'}}
+ >>> rst.metadata = {'DEFAULT': {'OWNER': None, 'VERSION': '2.0'}}
+ >>> rst.metadata
+ {'DEFAULT': {'VERSION': '2.0'}}
+
``GDALBand``
------------
@@ -1539,6 +1573,13 @@ blue.
[2, 2, 2, 2],
[3, 3, 3, 3]], dtype=uint8)
+ .. attribute:: metadata
+
+ .. versionadded:: 2.0
+
+ The metadata of this band. The functionality is identical to
+ :attr:`GDALRaster.metadata`.
+
.. _gdal-raster-ds-input:
Creating rasters from data
diff --git a/docs/releases/2.0.txt b/docs/releases/2.0.txt
index fef59ba880..3b7a5a74f1 100644
--- a/docs/releases/2.0.txt
+++ b/docs/releases/2.0.txt
@@ -78,6 +78,11 @@ Minor features
* Added the :attr:`.OSMWidget.default_zoom` attribute to customize the map's
default zoom level.
+* Made metadata readable and editable on rasters through the
+ :attr:`~django.contrib.gis.gdal.GDALRaster.metadata`,
+ :attr:`~django.contrib.gis.gdal.GDALRaster.info`, and
+ :attr:`~django.contrib.gis.gdal.GDALBand.metadata` attributes.
+
:mod:`django.contrib.messages`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/gis_tests/gdal_tests/test_raster.py b/tests/gis_tests/gdal_tests/test_raster.py
index 5495178a13..f82b37a6a9 100644
--- a/tests/gis_tests/gdal_tests/test_raster.py
+++ b/tests/gis_tests/gdal_tests/test_raster.py
@@ -1,45 +1,3 @@
-"""
-gdalinfo tests/gis_tests/data/rasters/raster.tif:
-
-Driver: GTiff/GeoTIFF
-Files: tests/gis_tests/data/rasters/raster.tif
-Size is 163, 174
-Coordinate System is:
-PROJCS["NAD83 / Florida GDL Albers",
- GEOGCS["NAD83",
- DATUM["North_American_Datum_1983",
- SPHEROID["GRS 1980",6378137,298.2572221010002,
- AUTHORITY["EPSG","7019"]],
- TOWGS84[0,0,0,0,0,0,0],
- AUTHORITY["EPSG","6269"]],
- PRIMEM["Greenwich",0],
- UNIT["degree",0.0174532925199433],
- AUTHORITY["EPSG","4269"]],
- PROJECTION["Albers_Conic_Equal_Area"],
- PARAMETER["standard_parallel_1",24],
- PARAMETER["standard_parallel_2",31.5],
- PARAMETER["latitude_of_center",24],
- PARAMETER["longitude_of_center",-84],
- PARAMETER["false_easting",400000],
- PARAMETER["false_northing",0],
- UNIT["metre",1,
- AUTHORITY["EPSG","9001"]],
- AUTHORITY["EPSG","3086"]]
-Origin = (511700.468070655711927,435103.377123198588379)
-Pixel Size = (100.000000000000000,-100.000000000000000)
-Metadata:
- AREA_OR_POINT=Area
-Image Structure Metadata:
- INTERLEAVE=BAND
-Corner Coordinates:
-Upper Left ( 511700.468, 435103.377) ( 82d51'46.16"W, 27d55' 1.53"N)
-Lower Left ( 511700.468, 417703.377) ( 82d51'52.04"W, 27d45'37.50"N)
-Upper Right ( 528000.468, 435103.377) ( 82d41'48.81"W, 27d54'56.30"N)
-Lower Right ( 528000.468, 417703.377) ( 82d41'55.54"W, 27d45'32.28"N)
-Center ( 519850.468, 426403.377) ( 82d46'50.64"W, 27d50'16.99"N)
-Band 1 Block=163x50 Type=Byte, ColorInterp=Gray
- NoData Value=15
-"""
import os
import struct
import tempfile
@@ -255,6 +213,103 @@ class GDALRasterTests(SimpleTestCase):
# Band data is equal to zero becaues no nodata value has been specified.
self.assertEqual(result, [0] * 4)
+ def test_raster_metadata_property(self):
+ # Check for required gdal version.
+ if GDAL_VERSION < (1, 11):
+ msg = 'GDAL ≥ 1.11 is required for using the metadata property.'
+ with self.assertRaisesMessage(ValueError, msg):
+ self.rs.metadata
+ return
+
+ self.assertEqual(
+ self.rs.metadata,
+ {'DEFAULT': {'AREA_OR_POINT': 'Area'}, 'IMAGE_STRUCTURE': {'INTERLEAVE': 'BAND'}},
+ )
+
+ # Create file-based raster from scratch
+ source = GDALRaster({
+ 'datatype': 1,
+ 'width': 2,
+ 'height': 2,
+ 'srid': 4326,
+ 'bands': [{'data': range(4), 'nodata_value': 99}],
+ })
+ # Set metadata on raster and on a band.
+ metadata = {
+ 'DEFAULT': {'OWNER': 'Django', 'VERSION': '1.0', 'AREA_OR_POINT': 'Point', },
+ }
+ source.metadata = metadata
+ source.bands[0].metadata = metadata
+ self.assertEqual(source.metadata['DEFAULT'], metadata['DEFAULT'])
+ self.assertEqual(source.bands[0].metadata['DEFAULT'], metadata['DEFAULT'])
+ # Update metadata on raster.
+ metadata = {
+ 'DEFAULT': {'VERSION': '2.0', },
+ }
+ source.metadata = metadata
+ self.assertEqual(source.metadata['DEFAULT']['VERSION'], '2.0')
+ # Remove metadata on raster.
+ metadata = {
+ 'DEFAULT': {'OWNER': None, },
+ }
+ source.metadata = metadata
+ self.assertNotIn('OWNER', source.metadata['DEFAULT'])
+
+ def test_raster_info_accessor(self):
+ if GDAL_VERSION < (2, 1):
+ msg = 'GDAL ≥ 2.1 is required for using the info property.'
+ with self.assertRaisesMessage(ValueError, msg):
+ self.rs.info
+ return
+ gdalinfo = """
+ Driver: GTiff/GeoTIFF
+ Files: {0}
+ Size is 163, 174
+ Coordinate System is:
+ PROJCS["NAD83 / Florida GDL Albers",
+ GEOGCS["NAD83",
+ DATUM["North_American_Datum_1983",
+ SPHEROID["GRS 1980",6378137,298.257222101,
+ AUTHORITY["EPSG","7019"]],
+ TOWGS84[0,0,0,0,0,0,0],
+ AUTHORITY["EPSG","6269"]],
+ PRIMEM["Greenwich",0,
+ AUTHORITY["EPSG","8901"]],
+ UNIT["degree",0.0174532925199433,
+ AUTHORITY["EPSG","9122"]],
+ AUTHORITY["EPSG","4269"]],
+ PROJECTION["Albers_Conic_Equal_Area"],
+ PARAMETER["standard_parallel_1",24],
+ PARAMETER["standard_parallel_2",31.5],
+ PARAMETER["latitude_of_center",24],
+ PARAMETER["longitude_of_center",-84],
+ PARAMETER["false_easting",400000],
+ PARAMETER["false_northing",0],
+ UNIT["metre",1,
+ AUTHORITY["EPSG","9001"]],
+ AXIS["X",EAST],
+ AXIS["Y",NORTH],
+ AUTHORITY["EPSG","3086"]]
+ Origin = (511700.468070655711927,435103.377123198588379)
+ Pixel Size = (100.000000000000000,-100.000000000000000)
+ Metadata:
+ AREA_OR_POINT=Area
+ Image Structure Metadata:
+ INTERLEAVE=BAND
+ Corner Coordinates:
+ Upper Left ( 511700.468, 435103.377) ( 82d51'46.16"W, 27d55' 1.53"N)
+ Lower Left ( 511700.468, 417703.377) ( 82d51'52.04"W, 27d45'37.50"N)
+ Upper Right ( 528000.468, 435103.377) ( 82d41'48.81"W, 27d54'56.30"N)
+ Lower Right ( 528000.468, 417703.377) ( 82d41'55.54"W, 27d45'32.28"N)
+ Center ( 519850.468, 426403.377) ( 82d46'50.64"W, 27d50'16.99"N)
+ Band 1 Block=163x50 Type=Byte, ColorInterp=Gray
+ NoData Value=15
+ """.format(self.rs_path)
+ # Data
+ info_dyn = [line.strip() for line in self.rs.info.split('\n') if line.strip() != '']
+ info_ref = [line.strip() for line in gdalinfo.split('\n') if line.strip() != '']
+ self.assertEqual(info_dyn, info_ref)
+
def test_raster_warp(self):
# Create in memory raster
source = GDALRaster({