Skip to content

Commit

Permalink
Fixed #28232 -- Made raster metadata readable and writable on GDALRas…
Browse files Browse the repository at this point in the history
…ter/Band.
  • Loading branch information
yellowcap authored and timgraham committed Jun 7, 2017
1 parent 23825b2 commit e0b456b
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 49 deletions.
11 changes: 10 additions & 1 deletion django/contrib/gis/gdal/prototypes/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
20 changes: 18 additions & 2 deletions django/contrib/gis/gdal/prototypes/raster.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@

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)
Expand Down Expand Up @@ -48,6 +49,21 @@
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'),
Expand Down
4 changes: 2 additions & 2 deletions django/contrib/gis/gdal/raster/band.py
Original file line number Diff line number Diff line change
@@ -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.
"""
Expand Down
78 changes: 78 additions & 0 deletions django/contrib/gis/gdal/raster/base.py
Original file line number Diff line number Diff line change
@@ -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,
)
14 changes: 12 additions & 2 deletions django/contrib/gis/gdal/raster/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
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
Expand Down Expand Up @@ -49,7 +49,7 @@ def y(self, value):
self._raster.geotransform = gtf


class GDALRaster(GDALBase):
class GDALRaster(GDALRasterBase):
"""
Wrap a raster GDAL Data Source object.
"""
Expand Down Expand Up @@ -403,3 +403,13 @@ def transform(self, srid, driver=None, name=None, resampling='NearestNeighbour',

# 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()
41 changes: 41 additions & 0 deletions docs/ref/contrib/gis/gdal.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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``
------------

Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions docs/releases/2.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
Loading

0 comments on commit e0b456b

Please sign in to comment.