Fixed #26753 -- Made GDAL a required dependency for contrib.gis
Thanks Tim Graham for the review.
This commit is contained in:
parent
7def55c3f6
commit
f7a363ee1d
|
@ -1,8 +1,7 @@
|
||||||
from django.contrib.admin import ModelAdmin
|
from django.contrib.admin import ModelAdmin
|
||||||
from django.contrib.gis.admin.widgets import OpenLayersWidget
|
from django.contrib.gis.admin.widgets import OpenLayersWidget
|
||||||
from django.contrib.gis.db import models
|
from django.contrib.gis.db import models
|
||||||
from django.contrib.gis.gdal import HAS_GDAL, OGRGeomType
|
from django.contrib.gis.gdal import OGRGeomType
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
|
|
||||||
spherical_mercator_srid = 3857
|
spherical_mercator_srid = 3857
|
||||||
|
|
||||||
|
@ -59,12 +58,6 @@ class GeoModelAdmin(ModelAdmin):
|
||||||
3D editing).
|
3D editing).
|
||||||
"""
|
"""
|
||||||
if isinstance(db_field, models.GeometryField) and db_field.dim < 3:
|
if isinstance(db_field, models.GeometryField) and db_field.dim < 3:
|
||||||
if not HAS_GDAL and db_field.srid != self.map_srid:
|
|
||||||
raise ImproperlyConfigured(
|
|
||||||
"Map SRID is %s and SRID of `%s` is %s. GDAL must be "
|
|
||||||
"installed to perform the transformation."
|
|
||||||
% (self.map_srid, db_field, db_field.srid)
|
|
||||||
)
|
|
||||||
# Setting the widget with the newly defined widget.
|
# Setting the widget with the newly defined widget.
|
||||||
kwargs['widget'] = self.get_map_widget(db_field)
|
kwargs['widget'] = self.get_map_widget(db_field)
|
||||||
return db_field.formfield(**kwargs)
|
return db_field.formfield(**kwargs)
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import re
|
|
||||||
|
|
||||||
from django.contrib.gis import gdal
|
from django.contrib.gis import gdal
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
|
@ -11,47 +9,32 @@ class SpatialRefSysMixin(object):
|
||||||
The SpatialRefSysMixin is a class used by the database-dependent
|
The SpatialRefSysMixin is a class used by the database-dependent
|
||||||
SpatialRefSys objects to reduce redundant code.
|
SpatialRefSys objects to reduce redundant code.
|
||||||
"""
|
"""
|
||||||
# For pulling out the spheroid from the spatial reference string. This
|
|
||||||
# regular expression is used only if the user does not have GDAL installed.
|
|
||||||
# TODO: Flattening not used in all ellipsoids, could also be a minor axis,
|
|
||||||
# or 'b' parameter.
|
|
||||||
spheroid_regex = re.compile(r'.+SPHEROID\[\"(?P<name>.+)\",(?P<major>\d+(\.\d+)?),(?P<flattening>\d{3}\.\d+),')
|
|
||||||
|
|
||||||
# For pulling out the units on platforms w/o GDAL installed.
|
|
||||||
# TODO: Figure out how to pull out angular units of projected coordinate system and
|
|
||||||
# fix for LOCAL_CS types. GDAL should be highly recommended for performing
|
|
||||||
# distance queries.
|
|
||||||
units_regex = re.compile(r'.+UNIT ?\["(?P<unit_name>[\w \.\'\(\)]+)", ?(?P<unit>[^ ,\]]+)', re.DOTALL)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def srs(self):
|
def srs(self):
|
||||||
"""
|
"""
|
||||||
Returns a GDAL SpatialReference object, if GDAL is installed.
|
Returns a GDAL SpatialReference object.
|
||||||
"""
|
"""
|
||||||
if gdal.HAS_GDAL:
|
# TODO: Is caching really necessary here? Is complexity worth it?
|
||||||
# TODO: Is caching really necessary here? Is complexity worth it?
|
if hasattr(self, '_srs'):
|
||||||
if hasattr(self, '_srs'):
|
# Returning a clone of the cached SpatialReference object.
|
||||||
# Returning a clone of the cached SpatialReference object.
|
return self._srs.clone()
|
||||||
return self._srs.clone()
|
|
||||||
else:
|
|
||||||
# Attempting to cache a SpatialReference object.
|
|
||||||
|
|
||||||
# Trying to get from WKT first.
|
|
||||||
try:
|
|
||||||
self._srs = gdal.SpatialReference(self.wkt)
|
|
||||||
return self.srs
|
|
||||||
except Exception as e:
|
|
||||||
msg = e
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._srs = gdal.SpatialReference(self.proj4text)
|
|
||||||
return self.srs
|
|
||||||
except Exception as e:
|
|
||||||
msg = e
|
|
||||||
|
|
||||||
raise Exception('Could not get OSR SpatialReference from WKT: %s\nError:\n%s' % (self.wkt, msg))
|
|
||||||
else:
|
else:
|
||||||
raise Exception('GDAL is not installed.')
|
# Attempting to cache a SpatialReference object.
|
||||||
|
|
||||||
|
# Trying to get from WKT first.
|
||||||
|
try:
|
||||||
|
self._srs = gdal.SpatialReference(self.wkt)
|
||||||
|
return self.srs
|
||||||
|
except Exception as e:
|
||||||
|
msg = e
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._srs = gdal.SpatialReference(self.proj4text)
|
||||||
|
return self.srs
|
||||||
|
except Exception as e:
|
||||||
|
msg = e
|
||||||
|
|
||||||
|
raise Exception('Could not get OSR SpatialReference from WKT: %s\nError:\n%s' % (self.wkt, msg))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ellipsoid(self):
|
def ellipsoid(self):
|
||||||
|
@ -59,14 +42,7 @@ class SpatialRefSysMixin(object):
|
||||||
Returns a tuple of the ellipsoid parameters:
|
Returns a tuple of the ellipsoid parameters:
|
||||||
(semimajor axis, semiminor axis, and inverse flattening).
|
(semimajor axis, semiminor axis, and inverse flattening).
|
||||||
"""
|
"""
|
||||||
if gdal.HAS_GDAL:
|
return self.srs.ellipsoid
|
||||||
return self.srs.ellipsoid
|
|
||||||
else:
|
|
||||||
m = self.spheroid_regex.match(self.wkt)
|
|
||||||
if m:
|
|
||||||
return (float(m.group('major')), float(m.group('flattening')))
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -86,70 +62,37 @@ class SpatialRefSysMixin(object):
|
||||||
@property
|
@property
|
||||||
def projected(self):
|
def projected(self):
|
||||||
"Is this Spatial Reference projected?"
|
"Is this Spatial Reference projected?"
|
||||||
if gdal.HAS_GDAL:
|
return self.srs.projected
|
||||||
return self.srs.projected
|
|
||||||
else:
|
|
||||||
return self.wkt.startswith('PROJCS')
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def local(self):
|
def local(self):
|
||||||
"Is this Spatial Reference local?"
|
"Is this Spatial Reference local?"
|
||||||
if gdal.HAS_GDAL:
|
return self.srs.local
|
||||||
return self.srs.local
|
|
||||||
else:
|
|
||||||
return self.wkt.startswith('LOCAL_CS')
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def geographic(self):
|
def geographic(self):
|
||||||
"Is this Spatial Reference geographic?"
|
"Is this Spatial Reference geographic?"
|
||||||
if gdal.HAS_GDAL:
|
return self.srs.geographic
|
||||||
return self.srs.geographic
|
|
||||||
else:
|
|
||||||
return self.wkt.startswith('GEOGCS')
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def linear_name(self):
|
def linear_name(self):
|
||||||
"Returns the linear units name."
|
"Returns the linear units name."
|
||||||
if gdal.HAS_GDAL:
|
return self.srs.linear_name
|
||||||
return self.srs.linear_name
|
|
||||||
elif self.geographic:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
m = self.units_regex.match(self.wkt)
|
|
||||||
return m.group('unit_name')
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def linear_units(self):
|
def linear_units(self):
|
||||||
"Returns the linear units."
|
"Returns the linear units."
|
||||||
if gdal.HAS_GDAL:
|
return self.srs.linear_units
|
||||||
return self.srs.linear_units
|
|
||||||
elif self.geographic:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
m = self.units_regex.match(self.wkt)
|
|
||||||
return m.group('unit')
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def angular_name(self):
|
def angular_name(self):
|
||||||
"Returns the name of the angular units."
|
"Returns the name of the angular units."
|
||||||
if gdal.HAS_GDAL:
|
return self.srs.angular_name
|
||||||
return self.srs.angular_name
|
|
||||||
elif self.projected:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
m = self.units_regex.match(self.wkt)
|
|
||||||
return m.group('unit_name')
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def angular_units(self):
|
def angular_units(self):
|
||||||
"Returns the angular units."
|
"Returns the angular units."
|
||||||
if gdal.HAS_GDAL:
|
return self.srs.angular_units
|
||||||
return self.srs.angular_units
|
|
||||||
elif self.projected:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
m = self.units_regex.match(self.wkt)
|
|
||||||
return m.group('unit')
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def units(self):
|
def units(self):
|
||||||
|
@ -167,11 +110,7 @@ class SpatialRefSysMixin(object):
|
||||||
Return a tuple of (unit_value, unit_name) for the given WKT without
|
Return a tuple of (unit_value, unit_name) for the given WKT without
|
||||||
using any of the database fields.
|
using any of the database fields.
|
||||||
"""
|
"""
|
||||||
if gdal.HAS_GDAL:
|
return gdal.SpatialReference(wkt).units
|
||||||
return gdal.SpatialReference(wkt).units
|
|
||||||
else:
|
|
||||||
m = cls.units_regex.match(wkt)
|
|
||||||
return float(m.group('unit')), m.group('unit_name')
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_spheroid(cls, wkt, string=True):
|
def get_spheroid(cls, wkt, string=True):
|
||||||
|
@ -179,17 +118,9 @@ class SpatialRefSysMixin(object):
|
||||||
Class method used by GeometryField on initialization to
|
Class method used by GeometryField on initialization to
|
||||||
retrieve the `SPHEROID[..]` parameters from the given WKT.
|
retrieve the `SPHEROID[..]` parameters from the given WKT.
|
||||||
"""
|
"""
|
||||||
if gdal.HAS_GDAL:
|
srs = gdal.SpatialReference(wkt)
|
||||||
srs = gdal.SpatialReference(wkt)
|
sphere_params = srs.ellipsoid
|
||||||
sphere_params = srs.ellipsoid
|
sphere_name = srs['spheroid']
|
||||||
sphere_name = srs['spheroid']
|
|
||||||
else:
|
|
||||||
m = cls.spheroid_regex.match(wkt)
|
|
||||||
if m:
|
|
||||||
sphere_params = (float(m.group('major')), float(m.group('flattening')))
|
|
||||||
sphere_name = m.group('name')
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not string:
|
if not string:
|
||||||
return sphere_name, sphere_params
|
return sphere_name, sphere_params
|
||||||
|
@ -203,10 +134,6 @@ class SpatialRefSysMixin(object):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""
|
"""
|
||||||
Returns the string representation. If GDAL is installed,
|
Returns the string representation, a 'pretty' OGC WKT.
|
||||||
it will be 'pretty' OGC WKT.
|
|
||||||
"""
|
"""
|
||||||
try:
|
return six.text_type(self.srs)
|
||||||
return six.text_type(self.srs)
|
|
||||||
except Exception:
|
|
||||||
return six.text_type(self.wkt)
|
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
from django.contrib.gis import forms
|
from django.contrib.gis import forms, gdal
|
||||||
from django.contrib.gis.db.models.lookups import (
|
from django.contrib.gis.db.models.lookups import (
|
||||||
RasterBandTransform, gis_lookups,
|
RasterBandTransform, gis_lookups,
|
||||||
)
|
)
|
||||||
from django.contrib.gis.db.models.proxy import SpatialProxy
|
from django.contrib.gis.db.models.proxy import SpatialProxy
|
||||||
from django.contrib.gis.gdal import HAS_GDAL
|
|
||||||
from django.contrib.gis.gdal.error import GDALException
|
from django.contrib.gis.gdal.error import GDALException
|
||||||
from django.contrib.gis.geometry.backend import Geometry, GeometryException
|
from django.contrib.gis.geometry.backend import Geometry, GeometryException
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
@ -186,18 +185,16 @@ class BaseSpatialField(Field):
|
||||||
"""
|
"""
|
||||||
Return a GDALRaster if conversion is successful, otherwise return None.
|
Return a GDALRaster if conversion is successful, otherwise return None.
|
||||||
"""
|
"""
|
||||||
from django.contrib.gis.gdal import GDALRaster
|
if isinstance(value, gdal.GDALRaster):
|
||||||
|
|
||||||
if isinstance(value, GDALRaster):
|
|
||||||
return value
|
return value
|
||||||
elif is_candidate:
|
elif is_candidate:
|
||||||
try:
|
try:
|
||||||
return GDALRaster(value)
|
return gdal.GDALRaster(value)
|
||||||
except GDALException:
|
except GDALException:
|
||||||
pass
|
pass
|
||||||
elif isinstance(value, dict):
|
elif isinstance(value, dict):
|
||||||
try:
|
try:
|
||||||
return GDALRaster(value)
|
return gdal.GDALRaster(value)
|
||||||
except GDALException:
|
except GDALException:
|
||||||
raise ValueError("Couldn't create spatial object from lookup value '%s'." % value)
|
raise ValueError("Couldn't create spatial object from lookup value '%s'." % value)
|
||||||
|
|
||||||
|
@ -228,10 +225,8 @@ class BaseSpatialField(Field):
|
||||||
else:
|
else:
|
||||||
# Check if input is a candidate for conversion to raster or geometry.
|
# Check if input is a candidate for conversion to raster or geometry.
|
||||||
is_candidate = isinstance(obj, (bytes, six.string_types)) or hasattr(obj, '__geo_interface__')
|
is_candidate = isinstance(obj, (bytes, six.string_types)) or hasattr(obj, '__geo_interface__')
|
||||||
# With GDAL installed, try to convert the input to raster.
|
# Try to convert the input to raster.
|
||||||
raster = False
|
raster = self.get_raster_prep_value(obj, is_candidate)
|
||||||
if HAS_GDAL:
|
|
||||||
raster = self.get_raster_prep_value(obj, is_candidate)
|
|
||||||
|
|
||||||
if raster:
|
if raster:
|
||||||
obj = raster
|
obj = raster
|
||||||
|
@ -425,11 +420,6 @@ class RasterField(BaseSpatialField):
|
||||||
geom_type = 'RASTER'
|
geom_type = 'RASTER'
|
||||||
geography = False
|
geography = False
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
if not HAS_GDAL:
|
|
||||||
raise ImproperlyConfigured('RasterField requires GDAL.')
|
|
||||||
super(RasterField, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def _check_connection(self, connection):
|
def _check_connection(self, connection):
|
||||||
# Make sure raster fields are used only on backends with raster support.
|
# Make sure raster fields are used only on backends with raster support.
|
||||||
if not connection.features.gis_enabled or not connection.features.supports_raster:
|
if not connection.features.gis_enabled or not connection.features.supports_raster:
|
||||||
|
@ -451,13 +441,11 @@ class RasterField(BaseSpatialField):
|
||||||
|
|
||||||
def contribute_to_class(self, cls, name, **kwargs):
|
def contribute_to_class(self, cls, name, **kwargs):
|
||||||
super(RasterField, self).contribute_to_class(cls, name, **kwargs)
|
super(RasterField, self).contribute_to_class(cls, name, **kwargs)
|
||||||
# Importing GDALRaster raises an exception on systems without gdal.
|
|
||||||
from django.contrib.gis.gdal import GDALRaster
|
|
||||||
# Setup for lazy-instantiated Raster object. For large querysets, the
|
# Setup for lazy-instantiated Raster object. For large querysets, the
|
||||||
# instantiation of all GDALRasters can potentially be expensive. This
|
# instantiation of all GDALRasters can potentially be expensive. This
|
||||||
# delays the instantiation of the objects to the moment of evaluation
|
# delays the instantiation of the objects to the moment of evaluation
|
||||||
# of the raster attribute.
|
# of the raster attribute.
|
||||||
setattr(cls, self.attname, SpatialProxy(GDALRaster, self))
|
setattr(cls, self.attname, SpatialProxy(gdal.GDALRaster, self))
|
||||||
|
|
||||||
def get_transform(self, name):
|
def get_transform(self, name):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -91,6 +91,7 @@ class OSMWidget(BaseGeometryWidget):
|
||||||
template_name = 'gis/openlayers-osm.html'
|
template_name = 'gis/openlayers-osm.html'
|
||||||
default_lon = 5
|
default_lon = 5
|
||||||
default_lat = 47
|
default_lat = 47
|
||||||
|
map_srid = 3857
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
js = (
|
js = (
|
||||||
|
@ -104,12 +105,3 @@ class OSMWidget(BaseGeometryWidget):
|
||||||
self.attrs[key] = getattr(self, key)
|
self.attrs[key] = getattr(self, key)
|
||||||
if attrs:
|
if attrs:
|
||||||
self.attrs.update(attrs)
|
self.attrs.update(attrs)
|
||||||
|
|
||||||
@property
|
|
||||||
def map_srid(self):
|
|
||||||
# Use the official spherical mercator projection SRID when GDAL is
|
|
||||||
# available; otherwise, fallback to 900913.
|
|
||||||
if gdal.HAS_GDAL:
|
|
||||||
return 3857
|
|
||||||
else:
|
|
||||||
return 900913
|
|
||||||
|
|
|
@ -24,12 +24,6 @@
|
||||||
library name for the current OS. The default library path may be overridden
|
library name for the current OS. The default library path may be overridden
|
||||||
by setting `GDAL_LIBRARY_PATH` in your settings with the path to the GDAL C
|
by setting `GDAL_LIBRARY_PATH` in your settings with the path to the GDAL C
|
||||||
library on your system.
|
library on your system.
|
||||||
|
|
||||||
GDAL links to a large number of external libraries that consume RAM when
|
|
||||||
loaded. Thus, it may desirable to disable GDAL on systems with limited
|
|
||||||
RAM resources -- this may be accomplished by setting `GDAL_LIBRARY_PATH`
|
|
||||||
to a non-existent file location (e.g., `GDAL_LIBRARY_PATH='/null/path'`;
|
|
||||||
setting to None/False/'' will not work as a string must be given).
|
|
||||||
"""
|
"""
|
||||||
from django.contrib.gis.gdal.envelope import Envelope
|
from django.contrib.gis.gdal.envelope import Envelope
|
||||||
from django.contrib.gis.gdal.error import ( # NOQA
|
from django.contrib.gis.gdal.error import ( # NOQA
|
||||||
|
|
|
@ -64,8 +64,6 @@ class GEOSGeometry(GEOSBase, ListMixin):
|
||||||
g = wkb_r().read(force_bytes(geo_input))
|
g = wkb_r().read(force_bytes(geo_input))
|
||||||
elif json_regex.match(geo_input):
|
elif json_regex.match(geo_input):
|
||||||
# Handling GeoJSON input.
|
# Handling GeoJSON input.
|
||||||
if not gdal.HAS_GDAL:
|
|
||||||
raise ValueError('Initializing geometry from JSON input requires GDAL.')
|
|
||||||
g = wkb_r().read(gdal.OGRGeometry(geo_input).wkb)
|
g = wkb_r().read(gdal.OGRGeometry(geo_input).wkb)
|
||||||
else:
|
else:
|
||||||
raise ValueError('String or unicode input unrecognized as WKT EWKT, and HEXEWKB.')
|
raise ValueError('String or unicode input unrecognized as WKT EWKT, and HEXEWKB.')
|
||||||
|
@ -476,8 +474,6 @@ class GEOSGeometry(GEOSBase, ListMixin):
|
||||||
@property
|
@property
|
||||||
def ogr(self):
|
def ogr(self):
|
||||||
"Returns the OGR Geometry for this Geometry."
|
"Returns the OGR Geometry for this Geometry."
|
||||||
if not gdal.HAS_GDAL:
|
|
||||||
raise GEOSException('GDAL required to convert to an OGRGeometry.')
|
|
||||||
if self.srid:
|
if self.srid:
|
||||||
try:
|
try:
|
||||||
return gdal.OGRGeometry(self.wkb, self.srid)
|
return gdal.OGRGeometry(self.wkb, self.srid)
|
||||||
|
@ -488,8 +484,6 @@ class GEOSGeometry(GEOSBase, ListMixin):
|
||||||
@property
|
@property
|
||||||
def srs(self):
|
def srs(self):
|
||||||
"Returns the OSR SpatialReference for SRID of this Geometry."
|
"Returns the OSR SpatialReference for SRID of this Geometry."
|
||||||
if not gdal.HAS_GDAL:
|
|
||||||
raise GEOSException('GDAL required to return a SpatialReference object.')
|
|
||||||
if self.srid:
|
if self.srid:
|
||||||
try:
|
try:
|
||||||
return gdal.SpatialReference(self.srid)
|
return gdal.SpatialReference(self.srid)
|
||||||
|
@ -520,9 +514,6 @@ class GEOSGeometry(GEOSBase, ListMixin):
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not gdal.HAS_GDAL:
|
|
||||||
raise GEOSException("GDAL library is not available to transform() geometry.")
|
|
||||||
|
|
||||||
if isinstance(ct, gdal.CoordTransform):
|
if isinstance(ct, gdal.CoordTransform):
|
||||||
# We don't care about SRID because CoordTransform presupposes
|
# We don't care about SRID because CoordTransform presupposes
|
||||||
# source SRS.
|
# source SRS.
|
||||||
|
|
|
@ -98,8 +98,6 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
data_source, model_name = options.pop('data_source'), options.pop('model_name')
|
data_source, model_name = options.pop('data_source'), options.pop('model_name')
|
||||||
if not gdal.HAS_GDAL:
|
|
||||||
raise CommandError('GDAL is required to inspect geospatial data sources.')
|
|
||||||
|
|
||||||
# Getting the OGR DataSource from the string parameter.
|
# Getting the OGR DataSource from the string parameter.
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.contrib.gis.gdal import HAS_GDAL
|
from django.contrib.gis.gdal import HAS_GDAL
|
||||||
from django.core.serializers.base import (
|
from django.core.serializers.base import SerializerDoesNotExist
|
||||||
SerializationError, SerializerDoesNotExist,
|
|
||||||
)
|
|
||||||
from django.core.serializers.json import Serializer as JSONSerializer
|
from django.core.serializers.json import Serializer as JSONSerializer
|
||||||
|
|
||||||
if HAS_GDAL:
|
if HAS_GDAL:
|
||||||
|
@ -53,10 +51,6 @@ class Serializer(JSONSerializer):
|
||||||
if self._geometry:
|
if self._geometry:
|
||||||
if self._geometry.srid != self.srid:
|
if self._geometry.srid != self.srid:
|
||||||
# If needed, transform the geometry in the srid of the global geojson srid
|
# If needed, transform the geometry in the srid of the global geojson srid
|
||||||
if not HAS_GDAL:
|
|
||||||
raise SerializationError(
|
|
||||||
'Unable to convert geometry to SRID %s when GDAL is not installed.' % self.srid
|
|
||||||
)
|
|
||||||
if self._geometry.srid not in self._cts:
|
if self._geometry.srid not in self._cts:
|
||||||
srs = SpatialReference(self.srid)
|
srs = SpatialReference(self.srid)
|
||||||
self._cts[self._geometry.srid] = CoordTransform(self._geometry.srs, srs)
|
self._cts[self._geometry.srid] = CoordTransform(self._geometry.srs, srs)
|
||||||
|
|
|
@ -91,9 +91,9 @@ transform procedure::
|
||||||
|
|
||||||
Thus, geometry parameters may be passed in using the ``GEOSGeometry`` object, WKT
|
Thus, geometry parameters may be passed in using the ``GEOSGeometry`` object, WKT
|
||||||
(Well Known Text [#fnwkt]_), HEXEWKB (PostGIS specific -- a WKB geometry in
|
(Well Known Text [#fnwkt]_), HEXEWKB (PostGIS specific -- a WKB geometry in
|
||||||
hexadecimal [#fnewkb]_), and GeoJSON [#fngeojson]_ (requires GDAL). Essentially,
|
hexadecimal [#fnewkb]_), and GeoJSON [#fngeojson]_. Essentially, if the input is
|
||||||
if the input is not a ``GEOSGeometry`` object, the geometry field will attempt to
|
not a ``GEOSGeometry`` object, the geometry field will attempt to create a
|
||||||
create a ``GEOSGeometry`` instance from the input.
|
``GEOSGeometry`` instance from the input.
|
||||||
|
|
||||||
For more information creating :class:`~django.contrib.gis.geos.GEOSGeometry`
|
For more information creating :class:`~django.contrib.gis.geos.GEOSGeometry`
|
||||||
objects, refer to the :ref:`GEOS tutorial <geos-tutorial>`.
|
objects, refer to the :ref:`GEOS tutorial <geos-tutorial>`.
|
||||||
|
|
|
@ -195,7 +195,7 @@ Format Input Type
|
||||||
WKT / EWKT ``str`` or ``unicode``
|
WKT / EWKT ``str`` or ``unicode``
|
||||||
HEX / HEXEWKB ``str`` or ``unicode``
|
HEX / HEXEWKB ``str`` or ``unicode``
|
||||||
WKB / EWKB ``buffer``
|
WKB / EWKB ``buffer``
|
||||||
GeoJSON (requires GDAL) ``str`` or ``unicode``
|
GeoJSON ``str`` or ``unicode``
|
||||||
======================= ======================
|
======================= ======================
|
||||||
|
|
||||||
Properties
|
Properties
|
||||||
|
@ -345,10 +345,6 @@ another object.
|
||||||
Returns an :class:`~django.contrib.gis.gdal.OGRGeometry` object
|
Returns an :class:`~django.contrib.gis.gdal.OGRGeometry` object
|
||||||
corresponding to the GEOS geometry.
|
corresponding to the GEOS geometry.
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Requires GDAL.
|
|
||||||
|
|
||||||
.. _wkb:
|
.. _wkb:
|
||||||
|
|
||||||
.. attribute:: GEOSGeometry.wkb
|
.. attribute:: GEOSGeometry.wkb
|
||||||
|
@ -618,10 +614,6 @@ Other Properties & Methods
|
||||||
Returns a :class:`~django.contrib.gis.gdal.SpatialReference` object
|
Returns a :class:`~django.contrib.gis.gdal.SpatialReference` object
|
||||||
corresponding to the SRID of the geometry or ``None``.
|
corresponding to the SRID of the geometry or ``None``.
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Requires GDAL.
|
|
||||||
|
|
||||||
.. method:: GEOSGeometry.transform(ct, clone=False)
|
.. method:: GEOSGeometry.transform(ct, clone=False)
|
||||||
|
|
||||||
Transforms the geometry according to the given coordinate transformation
|
Transforms the geometry according to the given coordinate transformation
|
||||||
|
@ -635,10 +627,10 @@ Other Properties & Methods
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
Requires GDAL. Raises :class:`~django.contrib.gis.geos.GEOSException` if
|
Raises :class:`~django.contrib.gis.geos.GEOSException` if GDAL is not
|
||||||
GDAL is not available or if the geometry's SRID is ``None`` or less than
|
available or if the geometry's SRID is ``None`` or less than 0. It
|
||||||
0. It doesn't impose any constraints on the geometry's SRID if called
|
doesn't impose any constraints on the geometry's SRID if called with a
|
||||||
with a :class:`~django.contrib.gis.gdal.CoordTransform` object.
|
:class:`~django.contrib.gis.gdal.CoordTransform` object.
|
||||||
|
|
||||||
.. versionchanged:: 1.10
|
.. versionchanged:: 1.10
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ Program Description Required
|
||||||
======================== ==================================== ================================ ===================================
|
======================== ==================================== ================================ ===================================
|
||||||
:doc:`GEOS <../geos>` Geometry Engine Open Source Yes 3.4, 3.3
|
:doc:`GEOS <../geos>` Geometry Engine Open Source Yes 3.4, 3.3
|
||||||
`PROJ.4`_ Cartographic Projections library Yes (PostgreSQL and SQLite only) 4.9, 4.8, 4.7, 4.6, 4.5, 4.4
|
`PROJ.4`_ Cartographic Projections library Yes (PostgreSQL and SQLite only) 4.9, 4.8, 4.7, 4.6, 4.5, 4.4
|
||||||
:doc:`GDAL <../gdal>` Geospatial Data Abstraction Library Yes (SQLite only) 2.1, 2.0, 1.11, 1.10, 1.9, 1.8, 1.7
|
:doc:`GDAL <../gdal>` Geospatial Data Abstraction Library Yes 2.1, 2.0, 1.11, 1.10, 1.9, 1.8, 1.7
|
||||||
:doc:`GeoIP <../geoip>` IP-based geolocation library No 1.4
|
:doc:`GeoIP <../geoip>` IP-based geolocation library No 1.4
|
||||||
`PostGIS`__ Spatial extensions for PostgreSQL Yes (PostgreSQL only) 2.2, 2.1
|
`PostGIS`__ Spatial extensions for PostgreSQL Yes (PostgreSQL only) 2.2, 2.1
|
||||||
`SpatiaLite`__ Spatial extensions for SQLite Yes (SQLite only) 4.3, 4.2, 4.1, 4.0
|
`SpatiaLite`__ Spatial extensions for SQLite Yes (SQLite only) 4.3, 4.2, 4.1, 4.0
|
||||||
|
@ -19,6 +19,11 @@ Program Description Required
|
||||||
Note that older or more recent versions of these libraries *may* also work
|
Note that older or more recent versions of these libraries *may* also work
|
||||||
totally fine with GeoDjango. Your mileage may vary.
|
totally fine with GeoDjango. Your mileage may vary.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.11
|
||||||
|
|
||||||
|
In older versions, GDAL is required only for SQLite. Now it's required for
|
||||||
|
all databases.
|
||||||
|
|
||||||
..
|
..
|
||||||
Libs release dates:
|
Libs release dates:
|
||||||
GEOS 3.3.0 2011-05-30
|
GEOS 3.3.0 2011-05-30
|
||||||
|
@ -37,13 +42,6 @@ totally fine with GeoDjango. Your mileage may vary.
|
||||||
Spatialite 4.2.0 2014-07-25
|
Spatialite 4.2.0 2014-07-25
|
||||||
Spatialite 4.3.0 2015-09-07
|
Spatialite 4.3.0 2015-09-07
|
||||||
|
|
||||||
.. admonition:: Install GDAL
|
|
||||||
|
|
||||||
While :ref:`gdalbuild` is technically not required, it is *recommended*.
|
|
||||||
Important features of GeoDjango (including the :doc:`../layermapping`,
|
|
||||||
geometry reprojection, and the geographic admin) depend on its
|
|
||||||
functionality.
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
The GeoDjango interfaces to GEOS, GDAL, and GeoIP may be used
|
The GeoDjango interfaces to GEOS, GDAL, and GeoIP may be used
|
||||||
|
|
|
@ -58,9 +58,9 @@ supported versions, and any notes for each of the supported database backends:
|
||||||
================== ============================== ================== =========================================
|
================== ============================== ================== =========================================
|
||||||
Database Library Requirements Supported Versions Notes
|
Database Library Requirements Supported Versions Notes
|
||||||
================== ============================== ================== =========================================
|
================== ============================== ================== =========================================
|
||||||
PostgreSQL GEOS, PROJ.4, PostGIS 9.3+ Requires PostGIS.
|
PostgreSQL GEOS, GDAL, PROJ.4, PostGIS 9.3+ Requires PostGIS.
|
||||||
MySQL GEOS 5.5+ Not OGC-compliant; :ref:`limited functionality <mysql-spatial-limitations>`.
|
MySQL GEOS, GDAL 5.5+ Not OGC-compliant; :ref:`limited functionality <mysql-spatial-limitations>`.
|
||||||
Oracle GEOS 11.2+ XE not supported.
|
Oracle GEOS, GDAL 11.2+ XE not supported.
|
||||||
SQLite GEOS, GDAL, PROJ.4, SpatiaLite 3.6.+ Requires SpatiaLite 4.0+, pysqlite2 2.5+
|
SQLite GEOS, GDAL, PROJ.4, SpatiaLite 3.6.+ Requires SpatiaLite 4.0+, pysqlite2 2.5+
|
||||||
================== ============================== ================== =========================================
|
================== ============================== ================== =========================================
|
||||||
|
|
||||||
|
|
|
@ -8,10 +8,6 @@
|
||||||
GeoDjango provides a specific serializer for the `GeoJSON`__ format. See
|
GeoDjango provides a specific serializer for the `GeoJSON`__ format. See
|
||||||
:doc:`/topics/serialization` for more information on serialization.
|
:doc:`/topics/serialization` for more information on serialization.
|
||||||
|
|
||||||
The GDAL library is required if any of the serialized geometries need
|
|
||||||
coordinate transformations (that is if the geometry's spatial reference system
|
|
||||||
differs from the ``srid`` serializer option).
|
|
||||||
|
|
||||||
__ http://geojson.org/
|
__ http://geojson.org/
|
||||||
|
|
||||||
The ``geojson`` serializer is not meant for round-tripping data, as it has no
|
The ``geojson`` serializer is not meant for round-tripping data, as it has no
|
||||||
|
|
|
@ -668,7 +668,7 @@ for popular geospatial formats::
|
||||||
MULTIPOLYGON (((12.4157980000000006 43.9579540000000009, 12.4505540000000003 43.9797209999999978, ...
|
MULTIPOLYGON (((12.4157980000000006 43.9579540000000009, 12.4505540000000003 43.9797209999999978, ...
|
||||||
>>> sm.mpoly.wkb # WKB (as Python binary buffer)
|
>>> sm.mpoly.wkb # WKB (as Python binary buffer)
|
||||||
<read-only buffer for 0x1fe2c70, size -1, offset 0 at 0x2564c40>
|
<read-only buffer for 0x1fe2c70, size -1, offset 0 at 0x2564c40>
|
||||||
>>> sm.mpoly.geojson # GeoJSON (requires GDAL)
|
>>> sm.mpoly.geojson # GeoJSON
|
||||||
'{ "type": "MultiPolygon", "coordinates": [ [ [ [ 12.415798, 43.957954 ], [ 12.450554, 43.979721 ], ...
|
'{ "type": "MultiPolygon", "coordinates": [ [ [ [ 12.415798, 43.957954 ], [ 12.450554, 43.979721 ], ...
|
||||||
|
|
||||||
This includes access to all of the advanced geometric operations provided by
|
This includes access to all of the advanced geometric operations provided by
|
||||||
|
@ -753,13 +753,8 @@ This provides more context (including street and thoroughfare details) than
|
||||||
available with the :class:`~django.contrib.gis.admin.GeoModelAdmin`
|
available with the :class:`~django.contrib.gis.admin.GeoModelAdmin`
|
||||||
(which uses the `Vector Map Level 0`_ WMS dataset hosted at `OSGeo`_).
|
(which uses the `Vector Map Level 0`_ WMS dataset hosted at `OSGeo`_).
|
||||||
|
|
||||||
First, there are some important requirements:
|
The PROJ.4 datum shifting files must be installed (see the :ref:`PROJ.4
|
||||||
|
installation instructions <proj4>` for more details).
|
||||||
* :class:`~django.contrib.gis.admin.OSMGeoAdmin` requires that
|
|
||||||
:doc:`GDAL <gdal>` is installed.
|
|
||||||
|
|
||||||
* The PROJ.4 datum shifting files must be installed (see the
|
|
||||||
:ref:`PROJ.4 installation instructions <proj4>` for more details).
|
|
||||||
|
|
||||||
If you meet this requirement, then just substitute the ``OSMGeoAdmin``
|
If you meet this requirement, then just substitute the ``OSMGeoAdmin``
|
||||||
option class in your ``admin.py`` file::
|
option class in your ``admin.py`` file::
|
||||||
|
|
|
@ -227,6 +227,13 @@ Validators
|
||||||
Backwards incompatible changes in 1.11
|
Backwards incompatible changes in 1.11
|
||||||
======================================
|
======================================
|
||||||
|
|
||||||
|
:mod:`django.contrib.gis`
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
* To simplify the codebase and because it's easier to install than when
|
||||||
|
``contrib.gis`` was first released, :ref:`gdalbuild` is now a required
|
||||||
|
dependency for GeoDjango. In older versions, it's only required for SQLite.
|
||||||
|
|
||||||
Database backend API
|
Database backend API
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from unittest import skipUnless
|
|
||||||
|
|
||||||
from django.contrib.gis.db.models.functions import (
|
from django.contrib.gis.db.models.functions import (
|
||||||
Area, Distance, Length, Perimeter, Transform,
|
Area, Distance, Length, Perimeter, Transform,
|
||||||
)
|
)
|
||||||
from django.contrib.gis.gdal import HAS_GDAL
|
|
||||||
from django.contrib.gis.geos import GEOSGeometry, LineString, Point
|
from django.contrib.gis.geos import GEOSGeometry, LineString, Point
|
||||||
from django.contrib.gis.measure import D # alias for Distance
|
from django.contrib.gis.measure import D # alias for Distance
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
from django.test import TestCase, ignore_warnings, mock, skipUnlessDBFeature
|
from django.test import TestCase, ignore_warnings, skipUnlessDBFeature
|
||||||
from django.utils.deprecation import RemovedInDjango20Warning
|
from django.utils.deprecation import RemovedInDjango20Warning
|
||||||
|
|
||||||
from ..utils import no_oracle, oracle, postgis
|
from ..utils import no_oracle, oracle, postgis
|
||||||
|
@ -497,7 +494,6 @@ class DistanceFunctionsTests(TestCase):
|
||||||
tol
|
tol
|
||||||
)
|
)
|
||||||
|
|
||||||
@skipUnless(HAS_GDAL, "GDAL is required.")
|
|
||||||
@skipUnlessDBFeature("has_Distance_function", "has_Transform_function")
|
@skipUnlessDBFeature("has_Distance_function", "has_Transform_function")
|
||||||
def test_distance_projected(self):
|
def test_distance_projected(self):
|
||||||
"""
|
"""
|
||||||
|
@ -520,26 +516,24 @@ class DistanceFunctionsTests(TestCase):
|
||||||
455411.438904354, 519386.252102563, 696139.009211594,
|
455411.438904354, 519386.252102563, 696139.009211594,
|
||||||
232513.278304279, 542445.630586414, 456679.155883207]
|
232513.278304279, 542445.630586414, 456679.155883207]
|
||||||
|
|
||||||
for has_gdal in [False, True]:
|
# Testing using different variations of parameters and using models
|
||||||
with mock.patch('django.contrib.gis.gdal.HAS_GDAL', has_gdal):
|
# with different projected coordinate systems.
|
||||||
# Testing using different variations of parameters and using models
|
dist1 = SouthTexasCity.objects.annotate(distance=Distance('point', lagrange)).order_by('id')
|
||||||
# with different projected coordinate systems.
|
if oracle:
|
||||||
dist1 = SouthTexasCity.objects.annotate(distance=Distance('point', lagrange)).order_by('id')
|
dist_qs = [dist1]
|
||||||
if oracle:
|
else:
|
||||||
dist_qs = [dist1]
|
dist2 = SouthTexasCityFt.objects.annotate(distance=Distance('point', lagrange)).order_by('id')
|
||||||
else:
|
dist_qs = [dist1, dist2]
|
||||||
dist2 = SouthTexasCityFt.objects.annotate(distance=Distance('point', lagrange)).order_by('id')
|
|
||||||
dist_qs = [dist1, dist2]
|
|
||||||
|
|
||||||
# Original query done on PostGIS, have to adjust AlmostEqual tolerance
|
# Original query done on PostGIS, have to adjust AlmostEqual tolerance
|
||||||
# for Oracle.
|
# for Oracle.
|
||||||
tol = 2 if oracle else 5
|
tol = 2 if oracle else 5
|
||||||
|
|
||||||
# Ensuring expected distances are returned for each distance queryset.
|
# Ensuring expected distances are returned for each distance queryset.
|
||||||
for qs in dist_qs:
|
for qs in dist_qs:
|
||||||
for i, c in enumerate(qs):
|
for i, c in enumerate(qs):
|
||||||
self.assertAlmostEqual(m_distances[i], c.distance.m, tol)
|
self.assertAlmostEqual(m_distances[i], c.distance.m, tol)
|
||||||
self.assertAlmostEqual(ft_distances[i], c.distance.survey_ft, tol)
|
self.assertAlmostEqual(ft_distances[i], c.distance.survey_ft, tol)
|
||||||
|
|
||||||
@skipUnlessDBFeature("has_Distance_function", "supports_distance_geodetic")
|
@skipUnlessDBFeature("has_Distance_function", "supports_distance_geodetic")
|
||||||
def test_distance_geodetic(self):
|
def test_distance_geodetic(self):
|
||||||
|
|
|
@ -2,13 +2,11 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from unittest import skipUnless
|
|
||||||
|
|
||||||
from django.contrib.gis.db.models import Extent3D, Union
|
from django.contrib.gis.db.models import Extent3D, Union
|
||||||
from django.contrib.gis.db.models.functions import (
|
from django.contrib.gis.db.models.functions import (
|
||||||
AsGeoJSON, AsKML, Length, Perimeter, Scale, Translate,
|
AsGeoJSON, AsKML, Length, Perimeter, Scale, Translate,
|
||||||
)
|
)
|
||||||
from django.contrib.gis.gdal import HAS_GDAL
|
|
||||||
from django.contrib.gis.geos import GEOSGeometry, LineString, Point, Polygon
|
from django.contrib.gis.geos import GEOSGeometry, LineString, Point, Polygon
|
||||||
from django.test import TestCase, ignore_warnings, skipUnlessDBFeature
|
from django.test import TestCase, ignore_warnings, skipUnlessDBFeature
|
||||||
from django.utils._os import upath
|
from django.utils._os import upath
|
||||||
|
@ -19,10 +17,6 @@ from .models import (
|
||||||
MultiPoint3D, Point2D, Point3D, Polygon2D, Polygon3D,
|
MultiPoint3D, Point2D, Point3D, Polygon2D, Polygon3D,
|
||||||
)
|
)
|
||||||
|
|
||||||
if HAS_GDAL:
|
|
||||||
from django.contrib.gis.utils import LayerMapping, LayerMapError
|
|
||||||
|
|
||||||
|
|
||||||
data_path = os.path.realpath(os.path.join(os.path.dirname(upath(__file__)), '..', 'data'))
|
data_path = os.path.realpath(os.path.join(os.path.dirname(upath(__file__)), '..', 'data'))
|
||||||
city_file = os.path.join(data_path, 'cities', 'cities.shp')
|
city_file = os.path.join(data_path, 'cities', 'cities.shp')
|
||||||
vrt_file = os.path.join(data_path, 'test_vrt', 'test_vrt.vrt')
|
vrt_file = os.path.join(data_path, 'test_vrt', 'test_vrt.vrt')
|
||||||
|
@ -101,7 +95,6 @@ class Geo3DLoadingHelper(object):
|
||||||
Polygon3D.objects.create(name='3D BBox', poly=bbox_3d)
|
Polygon3D.objects.create(name='3D BBox', poly=bbox_3d)
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(HAS_GDAL, "GDAL is required for Geo3DTest.")
|
|
||||||
@skipUnlessDBFeature("gis_enabled", "supports_3d_storage")
|
@skipUnlessDBFeature("gis_enabled", "supports_3d_storage")
|
||||||
class Geo3DTest(Geo3DLoadingHelper, TestCase):
|
class Geo3DTest(Geo3DLoadingHelper, TestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -147,6 +140,9 @@ class Geo3DTest(Geo3DLoadingHelper, TestCase):
|
||||||
"""
|
"""
|
||||||
Testing LayerMapping on 3D models.
|
Testing LayerMapping on 3D models.
|
||||||
"""
|
"""
|
||||||
|
# Import here as GDAL is required for those imports
|
||||||
|
from django.contrib.gis.utils import LayerMapping, LayerMapError
|
||||||
|
|
||||||
point_mapping = {'point': 'POINT'}
|
point_mapping = {'point': 'POINT'}
|
||||||
mpoint_mapping = {'mpoint': 'MULTIPOINT'}
|
mpoint_mapping = {'mpoint': 'MULTIPOINT'}
|
||||||
|
|
||||||
|
@ -310,7 +306,6 @@ class Geo3DTest(Geo3DLoadingHelper, TestCase):
|
||||||
self.assertEqual(city_dict[city.name][2] + ztrans, city.translate.z)
|
self.assertEqual(city_dict[city.name][2] + ztrans, city.translate.z)
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(HAS_GDAL, "GDAL is required for Geo3DTest.")
|
|
||||||
@skipUnlessDBFeature("gis_enabled", "supports_3d_functions")
|
@skipUnlessDBFeature("gis_enabled", "supports_3d_functions")
|
||||||
class Geo3DFunctionsTests(Geo3DLoadingHelper, TestCase):
|
class Geo3DFunctionsTests(Geo3DLoadingHelper, TestCase):
|
||||||
def test_kml(self):
|
def test_kml(self):
|
||||||
|
|
|
@ -17,17 +17,5 @@ class City(models.Model):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class CityMercator(models.Model):
|
|
||||||
name = models.CharField(max_length=30)
|
|
||||||
point = models.PointField(srid=3857)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
required_db_features = ['gis_enabled']
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
site = admin.AdminSite(name='admin_gis')
|
site = admin.AdminSite(name='admin_gis')
|
||||||
site.register(City, admin.OSMGeoAdmin)
|
site.register(City, admin.OSMGeoAdmin)
|
||||||
site.register(CityMercator, admin.OSMGeoAdmin)
|
|
||||||
|
|
|
@ -1,15 +1,11 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from unittest import skipUnless
|
|
||||||
|
|
||||||
from django.contrib.gis import admin
|
from django.contrib.gis import admin
|
||||||
from django.contrib.gis.gdal import HAS_GDAL
|
|
||||||
from django.contrib.gis.geos import Point
|
from django.contrib.gis.geos import Point
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.test import TestCase, override_settings, skipUnlessDBFeature
|
||||||
from django.test import TestCase, mock, override_settings, skipUnlessDBFeature
|
|
||||||
|
|
||||||
from .admin import UnmodifiableAdmin
|
from .admin import UnmodifiableAdmin
|
||||||
from .models import City, CityMercator, site
|
from .models import City, site
|
||||||
|
|
||||||
|
|
||||||
@skipUnlessDBFeature("gis_enabled")
|
@skipUnlessDBFeature("gis_enabled")
|
||||||
|
@ -56,22 +52,6 @@ class GeoAdminTest(TestCase):
|
||||||
""""http://vmap0.tiles.osgeo.org/wms/vmap0", {layers: 'basic', format: 'image/jpeg'});""",
|
""""http://vmap0.tiles.osgeo.org/wms/vmap0", {layers: 'basic', format: 'image/jpeg'});""",
|
||||||
result)
|
result)
|
||||||
|
|
||||||
@mock.patch('django.contrib.gis.admin.options.HAS_GDAL', False)
|
|
||||||
def test_no_gdal_admin_model_diffent_srid(self):
|
|
||||||
msg = (
|
|
||||||
'Map SRID is 3857 and SRID of `geoadmin.City.point` is 4326. '
|
|
||||||
'GDAL must be installed to perform the transformation.'
|
|
||||||
)
|
|
||||||
with self.assertRaisesMessage(ImproperlyConfigured, msg):
|
|
||||||
geoadmin = site._registry[City]
|
|
||||||
geoadmin.get_changelist_form(None)()
|
|
||||||
|
|
||||||
@mock.patch('django.contrib.gis.admin.options.HAS_GDAL', False)
|
|
||||||
def test_no_gdal_admin_model_same_srid(self):
|
|
||||||
geoadmin = site._registry[CityMercator]
|
|
||||||
geoadmin.get_changelist_form(None)()
|
|
||||||
|
|
||||||
@skipUnless(HAS_GDAL, "GDAL is required.")
|
|
||||||
def test_olwidget_has_changed(self):
|
def test_olwidget_has_changed(self):
|
||||||
"""
|
"""
|
||||||
Check that changes are accurately noticed by OpenLayersWidget.
|
Check that changes are accurately noticed by OpenLayersWidget.
|
||||||
|
|
|
@ -4,8 +4,7 @@ import json
|
||||||
|
|
||||||
from django.contrib.gis.geos import LinearRing, Point, Polygon
|
from django.contrib.gis.geos import LinearRing, Point, Polygon
|
||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
from django.test import TestCase, mock, skipUnlessDBFeature
|
from django.test import TestCase, skipUnlessDBFeature
|
||||||
from django.utils import six
|
|
||||||
|
|
||||||
from .models import City, MultiFields, PennsylvaniaCity
|
from .models import City, MultiFields, PennsylvaniaCity
|
||||||
|
|
||||||
|
@ -89,14 +88,6 @@ class GeoJSONSerializerTests(TestCase):
|
||||||
[1564802, 5613214]
|
[1564802, 5613214]
|
||||||
)
|
)
|
||||||
|
|
||||||
@mock.patch('django.contrib.gis.serializers.geojson.HAS_GDAL', False)
|
|
||||||
def test_without_gdal(self):
|
|
||||||
# Without coordinate transformation, the serialization should succeed:
|
|
||||||
serializers.serialize('geojson', City.objects.all())
|
|
||||||
with six.assertRaisesRegex(self, serializers.base.SerializationError, '.*GDAL is not installed'):
|
|
||||||
# Coordinate transformations need GDAL
|
|
||||||
serializers.serialize('geojson', City.objects.all(), srid=2847)
|
|
||||||
|
|
||||||
def test_deserialization_exception(self):
|
def test_deserialization_exception(self):
|
||||||
"""
|
"""
|
||||||
GeoJSON cannot be deserialized.
|
GeoJSON cannot be deserialized.
|
||||||
|
|
|
@ -8,7 +8,6 @@ from unittest import skipUnless
|
||||||
|
|
||||||
from django.contrib.gis.db import models
|
from django.contrib.gis.db import models
|
||||||
from django.contrib.gis.db.models.functions import Area, Distance
|
from django.contrib.gis.db.models.functions import Area, Distance
|
||||||
from django.contrib.gis.gdal import HAS_GDAL
|
|
||||||
from django.contrib.gis.measure import D
|
from django.contrib.gis.measure import D
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.db.models.functions import Cast
|
from django.db.models.functions import Cast
|
||||||
|
@ -70,7 +69,6 @@ class GeographyTest(TestCase):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
City.objects.get(point__exact=htown.point)
|
City.objects.get(point__exact=htown.point)
|
||||||
|
|
||||||
@skipUnless(HAS_GDAL, "GDAL is required.")
|
|
||||||
def test05_geography_layermapping(self):
|
def test05_geography_layermapping(self):
|
||||||
"Testing LayerMapping support on models with geography fields."
|
"Testing LayerMapping support on models with geography fields."
|
||||||
# There is a similar test in `layermap` that uses the same data set,
|
# There is a similar test in `layermap` that uses the same data set,
|
||||||
|
|
|
@ -1092,19 +1092,6 @@ class GEOSTest(SimpleTestCase, TestDataMixin):
|
||||||
self.assertEqual(g1.srid, 4326)
|
self.assertEqual(g1.srid, 4326)
|
||||||
self.assertIsNot(g1, g, "Clone didn't happen")
|
self.assertIsNot(g1, g, "Clone didn't happen")
|
||||||
|
|
||||||
with mock.patch('django.contrib.gis.gdal.HAS_GDAL', False):
|
|
||||||
g = GEOSGeometry('POINT (-104.609 38.255)', 4326)
|
|
||||||
gt = g.tuple
|
|
||||||
g.transform(4326)
|
|
||||||
self.assertEqual(g.tuple, gt)
|
|
||||||
self.assertEqual(g.srid, 4326)
|
|
||||||
|
|
||||||
g = GEOSGeometry('POINT (-104.609 38.255)', 4326)
|
|
||||||
g1 = g.transform(4326, clone=True)
|
|
||||||
self.assertEqual(g1.tuple, g.tuple)
|
|
||||||
self.assertEqual(g1.srid, 4326)
|
|
||||||
self.assertIsNot(g1, g, "Clone didn't happen")
|
|
||||||
|
|
||||||
@skipUnless(HAS_GDAL, "GDAL is required.")
|
@skipUnless(HAS_GDAL, "GDAL is required.")
|
||||||
def test_transform_nosrid(self):
|
def test_transform_nosrid(self):
|
||||||
""" Testing `transform` method (no SRID or negative SRID) """
|
""" Testing `transform` method (no SRID or negative SRID) """
|
||||||
|
@ -1125,17 +1112,6 @@ class GEOSTest(SimpleTestCase, TestDataMixin):
|
||||||
with self.assertRaises(GEOSException):
|
with self.assertRaises(GEOSException):
|
||||||
g.transform(2774, clone=True)
|
g.transform(2774, clone=True)
|
||||||
|
|
||||||
@mock.patch('django.contrib.gis.gdal.HAS_GDAL', False)
|
|
||||||
def test_transform_nogdal(self):
|
|
||||||
""" Testing `transform` method (GDAL not available) """
|
|
||||||
g = GEOSGeometry('POINT (-104.609 38.255)', 4326)
|
|
||||||
with self.assertRaises(GEOSException):
|
|
||||||
g.transform(2774)
|
|
||||||
|
|
||||||
g = GEOSGeometry('POINT (-104.609 38.255)', 4326)
|
|
||||||
with self.assertRaises(GEOSException):
|
|
||||||
g.transform(2774, clone=True)
|
|
||||||
|
|
||||||
def test_extent(self):
|
def test_extent(self):
|
||||||
"Testing `extent` method."
|
"Testing `extent` method."
|
||||||
# The xmin, ymin, xmax, ymax of the MultiPoint should be returned.
|
# The xmin, ymin, xmax, ymax of the MultiPoint should be returned.
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from unittest import skipUnless
|
|
||||||
|
|
||||||
from django.contrib.gis.db.models import fields
|
from django.contrib.gis.db.models import fields
|
||||||
from django.contrib.gis.gdal import HAS_GDAL
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.db import connection, migrations, models
|
from django.db import connection, migrations, models
|
||||||
from django.db.migrations.migration import Migration
|
from django.db.migrations.migration import Migration
|
||||||
|
@ -117,7 +114,6 @@ class OperationTests(TransactionTestCase):
|
||||||
self.assertSpatialIndexExists('gis_neighborhood', 'heatmap')
|
self.assertSpatialIndexExists('gis_neighborhood', 'heatmap')
|
||||||
|
|
||||||
@skipIfDBFeature('supports_raster')
|
@skipIfDBFeature('supports_raster')
|
||||||
@skipUnless(HAS_GDAL, 'A different error is raised if GDAL is not installed.')
|
|
||||||
def test_create_raster_model_on_db_without_raster_support(self):
|
def test_create_raster_model_on_db_without_raster_support(self):
|
||||||
"""
|
"""
|
||||||
Test creating a model with a raster field on a db without raster support.
|
Test creating a model with a raster field on a db without raster support.
|
||||||
|
@ -127,7 +123,6 @@ class OperationTests(TransactionTestCase):
|
||||||
self.set_up_test_model(True)
|
self.set_up_test_model(True)
|
||||||
|
|
||||||
@skipIfDBFeature('supports_raster')
|
@skipIfDBFeature('supports_raster')
|
||||||
@skipUnless(HAS_GDAL, 'A different error is raised if GDAL is not installed.')
|
|
||||||
def test_add_raster_field_on_db_without_raster_support(self):
|
def test_add_raster_field_on_db_without_raster_support(self):
|
||||||
"""
|
"""
|
||||||
Test adding a raster field on a db without raster support.
|
Test adding a raster field on a db without raster support.
|
||||||
|
|
|
@ -2,7 +2,6 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from unittest import skipUnless
|
|
||||||
|
|
||||||
from django.contrib.gis.gdal import HAS_GDAL
|
from django.contrib.gis.gdal import HAS_GDAL
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
|
@ -20,9 +19,8 @@ if HAS_GDAL:
|
||||||
from .models import AllOGRFields
|
from .models import AllOGRFields
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(HAS_GDAL, "InspectDbTests needs GDAL support")
|
@skipUnlessDBFeature("gis_enabled")
|
||||||
class InspectDbTests(TestCase):
|
class InspectDbTests(TestCase):
|
||||||
@skipUnlessDBFeature("gis_enabled")
|
|
||||||
def test_geom_columns(self):
|
def test_geom_columns(self):
|
||||||
"""
|
"""
|
||||||
Test the geo-enabled inspectdb command.
|
Test the geo-enabled inspectdb command.
|
||||||
|
@ -60,7 +58,7 @@ class InspectDbTests(TestCase):
|
||||||
self.assertIn('poly = models.GeometryField(', output)
|
self.assertIn('poly = models.GeometryField(', output)
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(HAS_GDAL, "OGRInspectTest needs GDAL support")
|
@skipUnlessDBFeature("gis_enabled")
|
||||||
@modify_settings(
|
@modify_settings(
|
||||||
INSTALLED_APPS={'append': 'django.contrib.gis'},
|
INSTALLED_APPS={'append': 'django.contrib.gis'},
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,7 +5,6 @@ import os
|
||||||
import unittest
|
import unittest
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from unittest import skipUnless
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.gis.gdal import HAS_GDAL
|
from django.contrib.gis.gdal import HAS_GDAL
|
||||||
|
@ -39,7 +38,6 @@ NUMS = [1, 2, 1, 19, 1] # Number of polygons for each.
|
||||||
STATES = ['Texas', 'Texas', 'Texas', 'Hawaii', 'Colorado']
|
STATES = ['Texas', 'Texas', 'Texas', 'Hawaii', 'Colorado']
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(HAS_GDAL, "LayerMapTest needs GDAL support")
|
|
||||||
@skipUnlessDBFeature("gis_enabled")
|
@skipUnlessDBFeature("gis_enabled")
|
||||||
class LayerMapTest(TestCase):
|
class LayerMapTest(TestCase):
|
||||||
|
|
||||||
|
@ -337,7 +335,6 @@ class OtherRouter(object):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(HAS_GDAL, "LayerMapRouterTest needs GDAL support")
|
|
||||||
@skipUnlessDBFeature("gis_enabled")
|
@skipUnlessDBFeature("gis_enabled")
|
||||||
@override_settings(DATABASE_ROUTERS=[OtherRouter()])
|
@override_settings(DATABASE_ROUTERS=[OtherRouter()])
|
||||||
class LayerMapRouterTest(TestCase):
|
class LayerMapRouterTest(TestCase):
|
||||||
|
|
|
@ -1,23 +1,24 @@
|
||||||
|
from django.contrib.gis.gdal import HAS_GDAL
|
||||||
|
|
||||||
from ..models import models
|
from ..models import models
|
||||||
|
|
||||||
|
if HAS_GDAL:
|
||||||
|
class RasterModel(models.Model):
|
||||||
|
rast = models.RasterField('A Verbose Raster Name', null=True, srid=4326, spatial_index=True, blank=True)
|
||||||
|
rastprojected = models.RasterField('A Projected Raster Table', srid=3086, null=True)
|
||||||
|
geom = models.PointField(null=True)
|
||||||
|
|
||||||
class RasterModel(models.Model):
|
class Meta:
|
||||||
rast = models.RasterField('A Verbose Raster Name', null=True, srid=4326, spatial_index=True, blank=True)
|
required_db_features = ['supports_raster']
|
||||||
rastprojected = models.RasterField('A Projected Raster Table', srid=3086, null=True)
|
|
||||||
geom = models.PointField(null=True)
|
|
||||||
|
|
||||||
class Meta:
|
def __str__(self):
|
||||||
required_db_features = ['supports_raster']
|
return str(self.id)
|
||||||
|
|
||||||
def __str__(self):
|
class RasterRelatedModel(models.Model):
|
||||||
return str(self.id)
|
rastermodel = models.ForeignKey(RasterModel, models.CASCADE)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
required_db_features = ['supports_raster']
|
||||||
|
|
||||||
class RasterRelatedModel(models.Model):
|
def __str__(self):
|
||||||
rastermodel = models.ForeignKey(RasterModel, models.CASCADE)
|
return str(self.id)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
required_db_features = ['supports_raster']
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(self.id)
|
|
||||||
|
|
|
@ -7,18 +7,14 @@ from django.contrib.gis.gdal import HAS_GDAL
|
||||||
from django.contrib.gis.geos import GEOSGeometry
|
from django.contrib.gis.geos import GEOSGeometry
|
||||||
from django.contrib.gis.measure import D
|
from django.contrib.gis.measure import D
|
||||||
from django.contrib.gis.shortcuts import numpy
|
from django.contrib.gis.shortcuts import numpy
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.test import (
|
from django.test import TransactionTestCase, skipUnlessDBFeature
|
||||||
TestCase, TransactionTestCase, mock, skipUnlessDBFeature,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ..data.rasters.textrasters import JSON_RASTER
|
from ..data.rasters.textrasters import JSON_RASTER
|
||||||
from ..models import models
|
|
||||||
from .models import RasterModel, RasterRelatedModel
|
|
||||||
|
|
||||||
if HAS_GDAL:
|
if HAS_GDAL:
|
||||||
from django.contrib.gis.gdal import GDALRaster
|
from django.contrib.gis.gdal import GDALRaster
|
||||||
|
from .models import RasterModel, RasterRelatedModel
|
||||||
|
|
||||||
|
|
||||||
@skipUnlessDBFeature('supports_raster')
|
@skipUnlessDBFeature('supports_raster')
|
||||||
|
@ -330,12 +326,3 @@ class RasterFieldTest(TransactionTestCase):
|
||||||
msg = "Couldn't create spatial object from lookup value '%s'." % obj
|
msg = "Couldn't create spatial object from lookup value '%s'." % obj
|
||||||
with self.assertRaisesMessage(ValueError, msg):
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
RasterModel.objects.filter(geom__intersects=obj)
|
RasterModel.objects.filter(geom__intersects=obj)
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('django.contrib.gis.db.models.fields.HAS_GDAL', False)
|
|
||||||
class RasterFieldWithoutGDALTest(TestCase):
|
|
||||||
|
|
||||||
def test_raster_field_without_gdal_exception(self):
|
|
||||||
msg = 'RasterField requires GDAL.'
|
|
||||||
with self.assertRaisesMessage(ImproperlyConfigured, msg):
|
|
||||||
models.OriginalRasterField()
|
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
from unittest import skipUnless
|
|
||||||
|
|
||||||
from django.contrib.gis import forms
|
from django.contrib.gis import forms
|
||||||
from django.contrib.gis.gdal import HAS_GDAL
|
|
||||||
from django.contrib.gis.geos import GEOSGeometry
|
from django.contrib.gis.geos import GEOSGeometry
|
||||||
from django.forms import ValidationError
|
from django.forms import ValidationError
|
||||||
from django.test import SimpleTestCase, override_settings, skipUnlessDBFeature
|
from django.test import SimpleTestCase, override_settings, skipUnlessDBFeature
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(HAS_GDAL, "GeometryFieldTest needs GDAL support")
|
|
||||||
@skipUnlessDBFeature("gis_enabled")
|
@skipUnlessDBFeature("gis_enabled")
|
||||||
class GeometryFieldTest(SimpleTestCase):
|
class GeometryFieldTest(SimpleTestCase):
|
||||||
|
|
||||||
|
@ -89,7 +85,6 @@ class GeometryFieldTest(SimpleTestCase):
|
||||||
self.assertFalse(form.has_changed())
|
self.assertFalse(form.has_changed())
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(HAS_GDAL, "SpecializedFieldTest needs GDAL support")
|
|
||||||
@skipUnlessDBFeature("gis_enabled")
|
@skipUnlessDBFeature("gis_enabled")
|
||||||
class SpecializedFieldTest(SimpleTestCase):
|
class SpecializedFieldTest(SimpleTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -260,7 +255,6 @@ class SpecializedFieldTest(SimpleTestCase):
|
||||||
self.assertFalse(GeometryForm(data={'g': invalid.wkt}).is_valid())
|
self.assertFalse(GeometryForm(data={'g': invalid.wkt}).is_valid())
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(HAS_GDAL, "OSMWidgetTest needs GDAL support")
|
|
||||||
@skipUnlessDBFeature("gis_enabled")
|
@skipUnlessDBFeature("gis_enabled")
|
||||||
class OSMWidgetTest(SimpleTestCase):
|
class OSMWidgetTest(SimpleTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -302,7 +296,6 @@ class OSMWidgetTest(SimpleTestCase):
|
||||||
rendered)
|
rendered)
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(HAS_GDAL, "CustomGeometryWidgetTest needs GDAL support")
|
|
||||||
@skipUnlessDBFeature("gis_enabled")
|
@skipUnlessDBFeature("gis_enabled")
|
||||||
class CustomGeometryWidgetTest(SimpleTestCase):
|
class CustomGeometryWidgetTest(SimpleTestCase):
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import re
|
import re
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from django.contrib.gis.gdal import HAS_GDAL
|
from django.test import skipUnlessDBFeature
|
||||||
from django.test import mock, skipUnlessDBFeature
|
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
|
|
||||||
from .utils import SpatialRefSys, oracle, postgis, spatialite
|
from .utils import SpatialRefSys, oracle, postgis, spatialite
|
||||||
|
@ -51,7 +50,6 @@ test_srs = ({
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipUnless(HAS_GDAL, "SpatialRefSysTest needs gdal support")
|
|
||||||
@skipUnlessDBFeature("has_spatialrefsys_table")
|
@skipUnlessDBFeature("has_spatialrefsys_table")
|
||||||
class SpatialRefSysTest(unittest.TestCase):
|
class SpatialRefSysTest(unittest.TestCase):
|
||||||
|
|
||||||
|
@ -61,13 +59,6 @@ class SpatialRefSysTest(unittest.TestCase):
|
||||||
self.assertEqual(unit_name, 'degree')
|
self.assertEqual(unit_name, 'degree')
|
||||||
self.assertAlmostEqual(unit, 0.01745329251994328)
|
self.assertAlmostEqual(unit, 0.01745329251994328)
|
||||||
|
|
||||||
@mock.patch('django.contrib.gis.gdal.HAS_GDAL', False)
|
|
||||||
def test_get_units_without_gdal(self):
|
|
||||||
epsg_4326 = next(f for f in test_srs if f['srid'] == 4326)
|
|
||||||
unit, unit_name = SpatialRefSys().get_units(epsg_4326['wkt'])
|
|
||||||
self.assertEqual(unit_name, 'degree')
|
|
||||||
self.assertAlmostEqual(unit, 0.01745329251994328)
|
|
||||||
|
|
||||||
def test_retrieve(self):
|
def test_retrieve(self):
|
||||||
"""
|
"""
|
||||||
Test retrieval of SpatialRefSys model objects.
|
Test retrieval of SpatialRefSys model objects.
|
||||||
|
|
Loading…
Reference in New Issue