Fixed #28232 -- Made raster metadata readable and writable on GDALRaster/Band.

This commit is contained in:
Daniel Wiesmann 2017-05-23 20:07:16 +01:00 committed by Tim Graham
parent 23825b2494
commit e0b456bee7
8 changed files with 263 additions and 49 deletions

View File

@ -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

View File

@ -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'),

View File

@ -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.
"""

View File

@ -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,
)

View File

@ -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()

View File

@ -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

View File

@ -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`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -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({