mirror of https://github.com/django/django.git
Fixed #28232 -- Made raster metadata readable and writable on GDALRaster/Band.
This commit is contained in:
parent
23825b2494
commit
e0b456bee7
|
@ -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
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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,
|
||||
)
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
Loading…
Reference in New Issue