docker setup
This commit is contained in:
@ -0,0 +1,34 @@
|
||||
from django.contrib.admin import (
|
||||
HORIZONTAL,
|
||||
VERTICAL,
|
||||
AdminSite,
|
||||
ModelAdmin,
|
||||
StackedInline,
|
||||
TabularInline,
|
||||
action,
|
||||
autodiscover,
|
||||
display,
|
||||
register,
|
||||
site,
|
||||
)
|
||||
from django.contrib.gis.admin.options import GeoModelAdmin, GISModelAdmin, OSMGeoAdmin
|
||||
from django.contrib.gis.admin.widgets import OpenLayersWidget
|
||||
|
||||
__all__ = [
|
||||
"HORIZONTAL",
|
||||
"VERTICAL",
|
||||
"AdminSite",
|
||||
"ModelAdmin",
|
||||
"StackedInline",
|
||||
"TabularInline",
|
||||
"action",
|
||||
"autodiscover",
|
||||
"display",
|
||||
"register",
|
||||
"site",
|
||||
"GISModelAdmin",
|
||||
# RemovedInDjango50Warning.
|
||||
"GeoModelAdmin",
|
||||
"OpenLayersWidget",
|
||||
"OSMGeoAdmin",
|
||||
]
|
@ -0,0 +1,180 @@
|
||||
import warnings
|
||||
|
||||
from django.contrib.admin import ModelAdmin
|
||||
from django.contrib.gis.admin.widgets import OpenLayersWidget
|
||||
from django.contrib.gis.db import models
|
||||
from django.contrib.gis.forms import OSMWidget
|
||||
from django.contrib.gis.gdal import OGRGeomType
|
||||
from django.forms import Media
|
||||
from django.utils.deprecation import RemovedInDjango50Warning
|
||||
|
||||
|
||||
class GeoModelAdminMixin:
|
||||
gis_widget = OSMWidget
|
||||
gis_widget_kwargs = {}
|
||||
|
||||
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
||||
if isinstance(db_field, models.GeometryField) and (
|
||||
db_field.dim < 3 or self.gis_widget.supports_3d
|
||||
):
|
||||
kwargs["widget"] = self.gis_widget(**self.gis_widget_kwargs)
|
||||
return db_field.formfield(**kwargs)
|
||||
else:
|
||||
return super().formfield_for_dbfield(db_field, request, **kwargs)
|
||||
|
||||
|
||||
class GISModelAdmin(GeoModelAdminMixin, ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
# RemovedInDjango50Warning.
|
||||
spherical_mercator_srid = 3857
|
||||
|
||||
|
||||
# RemovedInDjango50Warning.
|
||||
class GeoModelAdmin(ModelAdmin):
|
||||
"""
|
||||
The administration options class for Geographic models. Map settings
|
||||
may be overloaded from their defaults to create custom maps.
|
||||
"""
|
||||
|
||||
# The default map settings that may be overloaded -- still subject
|
||||
# to API changes.
|
||||
default_lon = 0
|
||||
default_lat = 0
|
||||
default_zoom = 4
|
||||
display_wkt = False
|
||||
display_srid = False
|
||||
extra_js = []
|
||||
num_zoom = 18
|
||||
max_zoom = False
|
||||
min_zoom = False
|
||||
units = False
|
||||
max_resolution = False
|
||||
max_extent = False
|
||||
modifiable = True
|
||||
mouse_position = True
|
||||
scale_text = True
|
||||
layerswitcher = True
|
||||
scrollable = True
|
||||
map_width = 600
|
||||
map_height = 400
|
||||
map_srid = 4326
|
||||
map_template = "gis/admin/openlayers.html"
|
||||
openlayers_url = (
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/openlayers/2.13.1/OpenLayers.js"
|
||||
)
|
||||
point_zoom = num_zoom - 6
|
||||
wms_url = "http://vmap0.tiles.osgeo.org/wms/vmap0"
|
||||
wms_layer = "basic"
|
||||
wms_name = "OpenLayers WMS"
|
||||
wms_options = {"format": "image/jpeg"}
|
||||
debug = False
|
||||
widget = OpenLayersWidget
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
warnings.warn(
|
||||
"django.contrib.gis.admin.GeoModelAdmin and OSMGeoAdmin are "
|
||||
"deprecated in favor of django.contrib.admin.ModelAdmin and "
|
||||
"django.contrib.gis.admin.GISModelAdmin.",
|
||||
RemovedInDjango50Warning,
|
||||
stacklevel=2,
|
||||
)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
"Injects OpenLayers JavaScript into the admin."
|
||||
return super().media + Media(js=[self.openlayers_url] + self.extra_js)
|
||||
|
||||
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
||||
"""
|
||||
Overloaded from ModelAdmin so that an OpenLayersWidget is used
|
||||
for viewing/editing 2D GeometryFields (OpenLayers 2 does not support
|
||||
3D editing).
|
||||
"""
|
||||
if isinstance(db_field, models.GeometryField) and db_field.dim < 3:
|
||||
# Setting the widget with the newly defined widget.
|
||||
kwargs["widget"] = self.get_map_widget(db_field)
|
||||
return db_field.formfield(**kwargs)
|
||||
else:
|
||||
return super().formfield_for_dbfield(db_field, request, **kwargs)
|
||||
|
||||
def get_map_widget(self, db_field):
|
||||
"""
|
||||
Return a subclass of the OpenLayersWidget (or whatever was specified
|
||||
in the `widget` attribute) using the settings from the attributes set
|
||||
in this class.
|
||||
"""
|
||||
is_collection = db_field.geom_type in (
|
||||
"MULTIPOINT",
|
||||
"MULTILINESTRING",
|
||||
"MULTIPOLYGON",
|
||||
"GEOMETRYCOLLECTION",
|
||||
)
|
||||
if is_collection:
|
||||
if db_field.geom_type == "GEOMETRYCOLLECTION":
|
||||
collection_type = "Any"
|
||||
else:
|
||||
collection_type = OGRGeomType(db_field.geom_type.replace("MULTI", ""))
|
||||
else:
|
||||
collection_type = "None"
|
||||
|
||||
class OLMap(self.widget):
|
||||
template_name = self.map_template
|
||||
geom_type = db_field.geom_type
|
||||
|
||||
wms_options = ""
|
||||
if self.wms_options:
|
||||
wms_options = ["%s: '%s'" % pair for pair in self.wms_options.items()]
|
||||
wms_options = ", %s" % ", ".join(wms_options)
|
||||
|
||||
params = {
|
||||
"default_lon": self.default_lon,
|
||||
"default_lat": self.default_lat,
|
||||
"default_zoom": self.default_zoom,
|
||||
"display_wkt": self.debug or self.display_wkt,
|
||||
"geom_type": OGRGeomType(db_field.geom_type),
|
||||
"field_name": db_field.name,
|
||||
"is_collection": is_collection,
|
||||
"scrollable": self.scrollable,
|
||||
"layerswitcher": self.layerswitcher,
|
||||
"collection_type": collection_type,
|
||||
"is_generic": db_field.geom_type == "GEOMETRY",
|
||||
"is_linestring": db_field.geom_type
|
||||
in ("LINESTRING", "MULTILINESTRING"),
|
||||
"is_polygon": db_field.geom_type in ("POLYGON", "MULTIPOLYGON"),
|
||||
"is_point": db_field.geom_type in ("POINT", "MULTIPOINT"),
|
||||
"num_zoom": self.num_zoom,
|
||||
"max_zoom": self.max_zoom,
|
||||
"min_zoom": self.min_zoom,
|
||||
"units": self.units, # likely should get from object
|
||||
"max_resolution": self.max_resolution,
|
||||
"max_extent": self.max_extent,
|
||||
"modifiable": self.modifiable,
|
||||
"mouse_position": self.mouse_position,
|
||||
"scale_text": self.scale_text,
|
||||
"map_width": self.map_width,
|
||||
"map_height": self.map_height,
|
||||
"point_zoom": self.point_zoom,
|
||||
"srid": self.map_srid,
|
||||
"display_srid": self.display_srid,
|
||||
"wms_url": self.wms_url,
|
||||
"wms_layer": self.wms_layer,
|
||||
"wms_name": self.wms_name,
|
||||
"wms_options": wms_options,
|
||||
"debug": self.debug,
|
||||
}
|
||||
|
||||
return OLMap
|
||||
|
||||
|
||||
# RemovedInDjango50Warning.
|
||||
class OSMGeoAdmin(GeoModelAdmin):
|
||||
map_template = "gis/admin/osm.html"
|
||||
num_zoom = 20
|
||||
map_srid = spherical_mercator_srid
|
||||
max_extent = "-20037508,-20037508,20037508,20037508"
|
||||
max_resolution = "156543.0339"
|
||||
point_zoom = num_zoom - 6
|
||||
units = "m"
|
@ -0,0 +1,136 @@
|
||||
# RemovedInDjango50Warning.
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
from django.contrib.gis.gdal import GDALException
|
||||
from django.contrib.gis.geos import GEOSException, GEOSGeometry
|
||||
from django.forms.widgets import Textarea
|
||||
from django.utils import translation
|
||||
from django.utils.deprecation import RemovedInDjango50Warning
|
||||
|
||||
# Creating a template context that contains Django settings
|
||||
# values needed by admin map templates.
|
||||
geo_context = {"LANGUAGE_BIDI": translation.get_language_bidi()}
|
||||
logger = logging.getLogger("django.contrib.gis")
|
||||
|
||||
|
||||
class OpenLayersWidget(Textarea):
|
||||
"""
|
||||
Render an OpenLayers map using the WKT of the geometry.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
warnings.warn(
|
||||
"django.contrib.gis.admin.OpenLayersWidget is deprecated.",
|
||||
RemovedInDjango50Warning,
|
||||
stacklevel=2,
|
||||
)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
# Update the template parameters with any attributes passed in.
|
||||
if attrs:
|
||||
self.params.update(attrs)
|
||||
self.params["editable"] = self.params["modifiable"]
|
||||
else:
|
||||
self.params["editable"] = True
|
||||
|
||||
# Defaulting the WKT value to a blank string -- this
|
||||
# will be tested in the JavaScript and the appropriate
|
||||
# interface will be constructed.
|
||||
self.params["wkt"] = ""
|
||||
|
||||
# If a string reaches here (via a validation error on another
|
||||
# field) then just reconstruct the Geometry.
|
||||
if value and isinstance(value, str):
|
||||
try:
|
||||
value = GEOSGeometry(value)
|
||||
except (GEOSException, ValueError) as err:
|
||||
logger.error("Error creating geometry from value '%s' (%s)", value, err)
|
||||
value = None
|
||||
|
||||
if (
|
||||
value
|
||||
and value.geom_type.upper() != self.geom_type
|
||||
and self.geom_type != "GEOMETRY"
|
||||
):
|
||||
value = None
|
||||
|
||||
# Constructing the dictionary of the map options.
|
||||
self.params["map_options"] = self.map_options()
|
||||
|
||||
# Constructing the JavaScript module name using the name of
|
||||
# the GeometryField (passed in via the `attrs` keyword).
|
||||
# Use the 'name' attr for the field name (rather than 'field')
|
||||
self.params["name"] = name
|
||||
# note: we must switch out dashes for underscores since js
|
||||
# functions are created using the module variable
|
||||
js_safe_name = self.params["name"].replace("-", "_")
|
||||
self.params["module"] = "geodjango_%s" % js_safe_name
|
||||
|
||||
if value:
|
||||
# Transforming the geometry to the projection used on the
|
||||
# OpenLayers map.
|
||||
srid = self.params["srid"]
|
||||
if value.srid != srid:
|
||||
try:
|
||||
ogr = value.ogr
|
||||
ogr.transform(srid)
|
||||
wkt = ogr.wkt
|
||||
except GDALException as err:
|
||||
logger.error(
|
||||
"Error transforming geometry from srid '%s' to srid '%s' (%s)",
|
||||
value.srid,
|
||||
srid,
|
||||
err,
|
||||
)
|
||||
wkt = ""
|
||||
else:
|
||||
wkt = value.wkt
|
||||
|
||||
# Setting the parameter WKT with that of the transformed
|
||||
# geometry.
|
||||
self.params["wkt"] = wkt
|
||||
|
||||
self.params.update(geo_context)
|
||||
return self.params
|
||||
|
||||
def map_options(self):
|
||||
"""Build the map options hash for the OpenLayers template."""
|
||||
|
||||
# JavaScript construction utilities for the Bounds and Projection.
|
||||
def ol_bounds(extent):
|
||||
return "new OpenLayers.Bounds(%s)" % extent
|
||||
|
||||
def ol_projection(srid):
|
||||
return 'new OpenLayers.Projection("EPSG:%s")' % srid
|
||||
|
||||
# An array of the parameter name, the name of their OpenLayers
|
||||
# counterpart, and the type of variable they are.
|
||||
map_types = [
|
||||
("srid", "projection", "srid"),
|
||||
("display_srid", "displayProjection", "srid"),
|
||||
("units", "units", str),
|
||||
("max_resolution", "maxResolution", float),
|
||||
("max_extent", "maxExtent", "bounds"),
|
||||
("num_zoom", "numZoomLevels", int),
|
||||
("max_zoom", "maxZoomLevels", int),
|
||||
("min_zoom", "minZoomLevel", int),
|
||||
]
|
||||
|
||||
# Building the map options hash.
|
||||
map_options = {}
|
||||
for param_name, js_name, option_type in map_types:
|
||||
if self.params.get(param_name, False):
|
||||
if option_type == "srid":
|
||||
value = ol_projection(self.params[param_name])
|
||||
elif option_type == "bounds":
|
||||
value = ol_bounds(self.params[param_name])
|
||||
elif option_type in (float, int):
|
||||
value = self.params[param_name]
|
||||
elif option_type in (str,):
|
||||
value = '"%s"' % self.params[param_name]
|
||||
else:
|
||||
raise TypeError
|
||||
map_options[js_name] = value
|
||||
return map_options
|
@ -0,0 +1,14 @@
|
||||
from django.apps import AppConfig
|
||||
from django.core import serializers
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class GISConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.AutoField"
|
||||
name = "django.contrib.gis"
|
||||
verbose_name = _("GIS")
|
||||
|
||||
def ready(self):
|
||||
serializers.BUILTIN_SERIALIZERS.setdefault(
|
||||
"geojson", "django.contrib.gis.serializers.geojson"
|
||||
)
|
@ -0,0 +1,26 @@
|
||||
class WKTAdapter:
|
||||
"""
|
||||
An adaptor for Geometries sent to the MySQL and Oracle database backends.
|
||||
"""
|
||||
|
||||
def __init__(self, geom):
|
||||
self.wkt = geom.wkt
|
||||
self.srid = geom.srid
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
isinstance(other, WKTAdapter)
|
||||
and self.wkt == other.wkt
|
||||
and self.srid == other.srid
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.wkt, self.srid))
|
||||
|
||||
def __str__(self):
|
||||
return self.wkt
|
||||
|
||||
@classmethod
|
||||
def _fix_polygon(cls, poly):
|
||||
# Hook for Oracle.
|
||||
return poly
|
@ -0,0 +1,111 @@
|
||||
import re
|
||||
|
||||
from django.contrib.gis.db import models
|
||||
|
||||
|
||||
class BaseSpatialFeatures:
|
||||
gis_enabled = True
|
||||
|
||||
# Does the database contain a SpatialRefSys model to store SRID information?
|
||||
has_spatialrefsys_table = True
|
||||
|
||||
# Does the backend support the django.contrib.gis.utils.add_srs_entry() utility?
|
||||
supports_add_srs_entry = True
|
||||
# Does the backend introspect GeometryField to its subtypes?
|
||||
supports_geometry_field_introspection = True
|
||||
|
||||
# Does the database have a geography type?
|
||||
supports_geography = False
|
||||
# Does the backend support storing 3D geometries?
|
||||
supports_3d_storage = False
|
||||
# Reference implementation of 3D functions is:
|
||||
# https://postgis.net/docs/PostGIS_Special_Functions_Index.html#PostGIS_3D_Functions
|
||||
supports_3d_functions = False
|
||||
# Does the database support SRID transform operations?
|
||||
supports_transform = True
|
||||
# Can geometry fields be null?
|
||||
supports_null_geometries = True
|
||||
# Are empty geometries supported?
|
||||
supports_empty_geometries = False
|
||||
# Can the function be applied on geodetic coordinate systems?
|
||||
supports_distance_geodetic = True
|
||||
supports_length_geodetic = True
|
||||
supports_perimeter_geodetic = False
|
||||
supports_area_geodetic = True
|
||||
# Is the database able to count vertices on polygons (with `num_points`)?
|
||||
supports_num_points_poly = True
|
||||
|
||||
# Does the backend support expressions for specifying distance in the
|
||||
# dwithin lookup?
|
||||
supports_dwithin_distance_expr = True
|
||||
|
||||
# Does the database have raster support?
|
||||
supports_raster = False
|
||||
|
||||
# Does the database support a unique index on geometry fields?
|
||||
supports_geometry_field_unique_index = True
|
||||
|
||||
# Can SchemaEditor alter geometry fields?
|
||||
can_alter_geometry_field = True
|
||||
|
||||
# Do the database functions/aggregates support the tolerance parameter?
|
||||
supports_tolerance_parameter = False
|
||||
|
||||
# Set of options that AsGeoJSON() doesn't support.
|
||||
unsupported_geojson_options = {}
|
||||
|
||||
# Does Intersection() return None (rather than an empty GeometryCollection)
|
||||
# for empty results?
|
||||
empty_intersection_returns_none = True
|
||||
|
||||
@property
|
||||
def supports_bbcontains_lookup(self):
|
||||
return "bbcontains" in self.connection.ops.gis_operators
|
||||
|
||||
@property
|
||||
def supports_contained_lookup(self):
|
||||
return "contained" in self.connection.ops.gis_operators
|
||||
|
||||
@property
|
||||
def supports_crosses_lookup(self):
|
||||
return "crosses" in self.connection.ops.gis_operators
|
||||
|
||||
@property
|
||||
def supports_distances_lookups(self):
|
||||
return self.has_Distance_function
|
||||
|
||||
@property
|
||||
def supports_dwithin_lookup(self):
|
||||
return "dwithin" in self.connection.ops.gis_operators
|
||||
|
||||
@property
|
||||
def supports_relate_lookup(self):
|
||||
return "relate" in self.connection.ops.gis_operators
|
||||
|
||||
@property
|
||||
def supports_isvalid_lookup(self):
|
||||
return self.has_IsValid_function
|
||||
|
||||
# Is the aggregate supported by the database?
|
||||
@property
|
||||
def supports_collect_aggr(self):
|
||||
return models.Collect not in self.connection.ops.disallowed_aggregates
|
||||
|
||||
@property
|
||||
def supports_extent_aggr(self):
|
||||
return models.Extent not in self.connection.ops.disallowed_aggregates
|
||||
|
||||
@property
|
||||
def supports_make_line_aggr(self):
|
||||
return models.MakeLine not in self.connection.ops.disallowed_aggregates
|
||||
|
||||
@property
|
||||
def supports_union_aggr(self):
|
||||
return models.Union not in self.connection.ops.disallowed_aggregates
|
||||
|
||||
def __getattr__(self, name):
|
||||
m = re.match(r"has_(\w*)_function$", name)
|
||||
if m:
|
||||
func_name = m[1]
|
||||
return func_name not in self.connection.ops.unsupported_functions
|
||||
raise AttributeError
|
@ -0,0 +1,140 @@
|
||||
from django.contrib.gis import gdal
|
||||
|
||||
|
||||
class SpatialRefSysMixin:
|
||||
"""
|
||||
The SpatialRefSysMixin is a class used by the database-dependent
|
||||
SpatialRefSys objects to reduce redundant code.
|
||||
"""
|
||||
|
||||
@property
|
||||
def srs(self):
|
||||
"""
|
||||
Return a GDAL SpatialReference object.
|
||||
"""
|
||||
# TODO: Is caching really necessary here? Is complexity worth it?
|
||||
if hasattr(self, "_srs"):
|
||||
# Returning a clone of the cached SpatialReference object.
|
||||
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)
|
||||
)
|
||||
|
||||
@property
|
||||
def ellipsoid(self):
|
||||
"""
|
||||
Return a tuple of the ellipsoid parameters:
|
||||
(semimajor axis, semiminor axis, and inverse flattening).
|
||||
"""
|
||||
return self.srs.ellipsoid
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"Return the projection name."
|
||||
return self.srs.name
|
||||
|
||||
@property
|
||||
def spheroid(self):
|
||||
"Return the spheroid name for this spatial reference."
|
||||
return self.srs["spheroid"]
|
||||
|
||||
@property
|
||||
def datum(self):
|
||||
"Return the datum for this spatial reference."
|
||||
return self.srs["datum"]
|
||||
|
||||
@property
|
||||
def projected(self):
|
||||
"Is this Spatial Reference projected?"
|
||||
return self.srs.projected
|
||||
|
||||
@property
|
||||
def local(self):
|
||||
"Is this Spatial Reference local?"
|
||||
return self.srs.local
|
||||
|
||||
@property
|
||||
def geographic(self):
|
||||
"Is this Spatial Reference geographic?"
|
||||
return self.srs.geographic
|
||||
|
||||
@property
|
||||
def linear_name(self):
|
||||
"Return the linear units name."
|
||||
return self.srs.linear_name
|
||||
|
||||
@property
|
||||
def linear_units(self):
|
||||
"Return the linear units."
|
||||
return self.srs.linear_units
|
||||
|
||||
@property
|
||||
def angular_name(self):
|
||||
"Return the name of the angular units."
|
||||
return self.srs.angular_name
|
||||
|
||||
@property
|
||||
def angular_units(self):
|
||||
"Return the angular units."
|
||||
return self.srs.angular_units
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
"Return a tuple of the units and the name."
|
||||
if self.projected or self.local:
|
||||
return (self.linear_units, self.linear_name)
|
||||
elif self.geographic:
|
||||
return (self.angular_units, self.angular_name)
|
||||
else:
|
||||
return (None, None)
|
||||
|
||||
@classmethod
|
||||
def get_units(cls, wkt):
|
||||
"""
|
||||
Return a tuple of (unit_value, unit_name) for the given WKT without
|
||||
using any of the database fields.
|
||||
"""
|
||||
return gdal.SpatialReference(wkt).units
|
||||
|
||||
@classmethod
|
||||
def get_spheroid(cls, wkt, string=True):
|
||||
"""
|
||||
Class method used by GeometryField on initialization to
|
||||
retrieve the `SPHEROID[..]` parameters from the given WKT.
|
||||
"""
|
||||
srs = gdal.SpatialReference(wkt)
|
||||
sphere_params = srs.ellipsoid
|
||||
sphere_name = srs["spheroid"]
|
||||
|
||||
if not string:
|
||||
return sphere_name, sphere_params
|
||||
else:
|
||||
# `string` parameter used to place in format acceptable by PostGIS
|
||||
if len(sphere_params) == 3:
|
||||
radius, flattening = sphere_params[0], sphere_params[2]
|
||||
else:
|
||||
radius, flattening = sphere_params
|
||||
return 'SPHEROID["%s",%s,%s]' % (sphere_name, radius, flattening)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Return the string representation, a 'pretty' OGC WKT.
|
||||
"""
|
||||
return str(self.srs)
|
@ -0,0 +1,209 @@
|
||||
from django.contrib.gis.db.models import GeometryField
|
||||
from django.contrib.gis.db.models.functions import Distance
|
||||
from django.contrib.gis.measure import Area as AreaMeasure
|
||||
from django.contrib.gis.measure import Distance as DistanceMeasure
|
||||
from django.db import NotSupportedError
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class BaseSpatialOperations:
|
||||
# Quick booleans for the type of this spatial backend, and
|
||||
# an attribute for the spatial database version tuple (if applicable)
|
||||
postgis = False
|
||||
spatialite = False
|
||||
mariadb = False
|
||||
mysql = False
|
||||
oracle = False
|
||||
spatial_version = None
|
||||
|
||||
# How the geometry column should be selected.
|
||||
select = "%s"
|
||||
|
||||
@cached_property
|
||||
def select_extent(self):
|
||||
return self.select
|
||||
|
||||
# Aggregates
|
||||
disallowed_aggregates = ()
|
||||
|
||||
geom_func_prefix = ""
|
||||
|
||||
# Mapping between Django function names and backend names, when names do not
|
||||
# match; used in spatial_function_name().
|
||||
function_names = {}
|
||||
|
||||
# Set of known unsupported functions of the backend
|
||||
unsupported_functions = {
|
||||
"Area",
|
||||
"AsGeoJSON",
|
||||
"AsGML",
|
||||
"AsKML",
|
||||
"AsSVG",
|
||||
"Azimuth",
|
||||
"BoundingCircle",
|
||||
"Centroid",
|
||||
"Difference",
|
||||
"Distance",
|
||||
"Envelope",
|
||||
"FromWKB",
|
||||
"FromWKT",
|
||||
"GeoHash",
|
||||
"GeometryDistance",
|
||||
"Intersection",
|
||||
"IsEmpty",
|
||||
"IsValid",
|
||||
"Length",
|
||||
"LineLocatePoint",
|
||||
"MakeValid",
|
||||
"MemSize",
|
||||
"NumGeometries",
|
||||
"NumPoints",
|
||||
"Perimeter",
|
||||
"PointOnSurface",
|
||||
"Reverse",
|
||||
"Scale",
|
||||
"SnapToGrid",
|
||||
"SymDifference",
|
||||
"Transform",
|
||||
"Translate",
|
||||
"Union",
|
||||
}
|
||||
|
||||
# Constructors
|
||||
from_text = False
|
||||
|
||||
# Default conversion functions for aggregates; will be overridden if implemented
|
||||
# for the spatial backend.
|
||||
def convert_extent(self, box, srid):
|
||||
raise NotImplementedError(
|
||||
"Aggregate extent not implemented for this spatial backend."
|
||||
)
|
||||
|
||||
def convert_extent3d(self, box, srid):
|
||||
raise NotImplementedError(
|
||||
"Aggregate 3D extent not implemented for this spatial backend."
|
||||
)
|
||||
|
||||
# For quoting column values, rather than columns.
|
||||
def geo_quote_name(self, name):
|
||||
return "'%s'" % name
|
||||
|
||||
# GeometryField operations
|
||||
def geo_db_type(self, f):
|
||||
"""
|
||||
Return the database column type for the geometry field on
|
||||
the spatial backend.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"subclasses of BaseSpatialOperations must provide a geo_db_type() method"
|
||||
)
|
||||
|
||||
def get_distance(self, f, value, lookup_type):
|
||||
"""
|
||||
Return the distance parameters for the given geometry field,
|
||||
lookup value, and lookup type.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Distance operations not available on this spatial backend."
|
||||
)
|
||||
|
||||
def get_geom_placeholder(self, f, value, compiler):
|
||||
"""
|
||||
Return the placeholder for the given geometry field with the given
|
||||
value. Depending on the spatial backend, the placeholder may contain a
|
||||
stored procedure call to the transformation function of the spatial
|
||||
backend.
|
||||
"""
|
||||
|
||||
def transform_value(value, field):
|
||||
return value is not None and value.srid != field.srid
|
||||
|
||||
if hasattr(value, "as_sql"):
|
||||
return (
|
||||
"%s(%%s, %s)" % (self.spatial_function_name("Transform"), f.srid)
|
||||
if transform_value(value.output_field, f)
|
||||
else "%s"
|
||||
)
|
||||
if transform_value(value, f):
|
||||
# Add Transform() to the SQL placeholder.
|
||||
return "%s(%s(%%s,%s), %s)" % (
|
||||
self.spatial_function_name("Transform"),
|
||||
self.from_text,
|
||||
value.srid,
|
||||
f.srid,
|
||||
)
|
||||
elif self.connection.features.has_spatialrefsys_table:
|
||||
return "%s(%%s,%s)" % (self.from_text, f.srid)
|
||||
else:
|
||||
# For backwards compatibility on MySQL (#27464).
|
||||
return "%s(%%s)" % self.from_text
|
||||
|
||||
def check_expression_support(self, expression):
|
||||
if isinstance(expression, self.disallowed_aggregates):
|
||||
raise NotSupportedError(
|
||||
"%s spatial aggregation is not supported by this database backend."
|
||||
% expression.name
|
||||
)
|
||||
super().check_expression_support(expression)
|
||||
|
||||
def spatial_aggregate_name(self, agg_name):
|
||||
raise NotImplementedError(
|
||||
"Aggregate support not implemented for this spatial backend."
|
||||
)
|
||||
|
||||
def spatial_function_name(self, func_name):
|
||||
if func_name in self.unsupported_functions:
|
||||
raise NotSupportedError(
|
||||
"This backend doesn't support the %s function." % func_name
|
||||
)
|
||||
return self.function_names.get(func_name, self.geom_func_prefix + func_name)
|
||||
|
||||
# Routines for getting the OGC-compliant models.
|
||||
def geometry_columns(self):
|
||||
raise NotImplementedError(
|
||||
"Subclasses of BaseSpatialOperations must provide a geometry_columns() "
|
||||
"method."
|
||||
)
|
||||
|
||||
def spatial_ref_sys(self):
|
||||
raise NotImplementedError(
|
||||
"subclasses of BaseSpatialOperations must a provide spatial_ref_sys() "
|
||||
"method"
|
||||
)
|
||||
|
||||
distance_expr_for_lookup = staticmethod(Distance)
|
||||
|
||||
def get_db_converters(self, expression):
|
||||
converters = super().get_db_converters(expression)
|
||||
if isinstance(expression.output_field, GeometryField):
|
||||
converters.append(self.get_geometry_converter(expression))
|
||||
return converters
|
||||
|
||||
def get_geometry_converter(self, expression):
|
||||
raise NotImplementedError(
|
||||
"Subclasses of BaseSpatialOperations must provide a "
|
||||
"get_geometry_converter() method."
|
||||
)
|
||||
|
||||
def get_area_att_for_field(self, field):
|
||||
if field.geodetic(self.connection):
|
||||
if self.connection.features.supports_area_geodetic:
|
||||
return "sq_m"
|
||||
raise NotImplementedError(
|
||||
"Area on geodetic coordinate systems not supported."
|
||||
)
|
||||
else:
|
||||
units_name = field.units_name(self.connection)
|
||||
if units_name:
|
||||
return AreaMeasure.unit_attname(units_name)
|
||||
|
||||
def get_distance_att_for_field(self, field):
|
||||
dist_att = None
|
||||
if field.geodetic(self.connection):
|
||||
if self.connection.features.supports_distance_geodetic:
|
||||
dist_att = "m"
|
||||
else:
|
||||
units = field.units_name(self.connection)
|
||||
if units:
|
||||
dist_att = DistanceMeasure.unit_attname(units)
|
||||
return dist_att
|
@ -0,0 +1,14 @@
|
||||
from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper
|
||||
|
||||
from .features import DatabaseFeatures
|
||||
from .introspection import MySQLIntrospection
|
||||
from .operations import MySQLOperations
|
||||
from .schema import MySQLGISSchemaEditor
|
||||
|
||||
|
||||
class DatabaseWrapper(MySQLDatabaseWrapper):
|
||||
SchemaEditorClass = MySQLGISSchemaEditor
|
||||
# Classes instantiated in __init__().
|
||||
features_class = DatabaseFeatures
|
||||
introspection_class = MySQLIntrospection
|
||||
ops_class = MySQLOperations
|
@ -0,0 +1,21 @@
|
||||
from django.contrib.gis.db.backends.base.features import BaseSpatialFeatures
|
||||
from django.db.backends.mysql.features import DatabaseFeatures as MySQLDatabaseFeatures
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class DatabaseFeatures(BaseSpatialFeatures, MySQLDatabaseFeatures):
|
||||
empty_intersection_returns_none = False
|
||||
has_spatialrefsys_table = False
|
||||
supports_add_srs_entry = False
|
||||
supports_distance_geodetic = False
|
||||
supports_length_geodetic = False
|
||||
supports_area_geodetic = False
|
||||
supports_transform = False
|
||||
supports_null_geometries = False
|
||||
supports_num_points_poly = False
|
||||
unsupported_geojson_options = {"crs"}
|
||||
|
||||
@cached_property
|
||||
def supports_geometry_field_unique_index(self):
|
||||
# Not supported in MySQL since https://dev.mysql.com/worklog/task/?id=11808
|
||||
return self.connection.mysql_is_mariadb
|
@ -0,0 +1,33 @@
|
||||
from MySQLdb.constants import FIELD_TYPE
|
||||
|
||||
from django.contrib.gis.gdal import OGRGeomType
|
||||
from django.db.backends.mysql.introspection import DatabaseIntrospection
|
||||
|
||||
|
||||
class MySQLIntrospection(DatabaseIntrospection):
|
||||
# Updating the data_types_reverse dictionary with the appropriate
|
||||
# type for Geometry fields.
|
||||
data_types_reverse = DatabaseIntrospection.data_types_reverse.copy()
|
||||
data_types_reverse[FIELD_TYPE.GEOMETRY] = "GeometryField"
|
||||
|
||||
def get_geometry_type(self, table_name, description):
|
||||
with self.connection.cursor() as cursor:
|
||||
# In order to get the specific geometry type of the field,
|
||||
# we introspect on the table definition using `DESCRIBE`.
|
||||
cursor.execute("DESCRIBE %s" % self.connection.ops.quote_name(table_name))
|
||||
# Increment over description info until we get to the geometry
|
||||
# column.
|
||||
for column, typ, null, key, default, extra in cursor.fetchall():
|
||||
if column == description.name:
|
||||
# Using OGRGeomType to convert from OGC name to Django field.
|
||||
# MySQL does not support 3D or SRIDs, so the field params
|
||||
# are empty.
|
||||
field_type = OGRGeomType(typ).django
|
||||
field_params = {}
|
||||
break
|
||||
return field_type, field_params
|
||||
|
||||
def supports_spatial_index(self, cursor, table_name):
|
||||
# Supported with MyISAM, Aria, or InnoDB.
|
||||
storage_engine = self.get_storage_engine(cursor, table_name)
|
||||
return storage_engine in ("MyISAM", "Aria", "InnoDB")
|
@ -0,0 +1,129 @@
|
||||
from django.contrib.gis.db import models
|
||||
from django.contrib.gis.db.backends.base.adapter import WKTAdapter
|
||||
from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations
|
||||
from django.contrib.gis.db.backends.utils import SpatialOperator
|
||||
from django.contrib.gis.geos.geometry import GEOSGeometryBase
|
||||
from django.contrib.gis.geos.prototypes.io import wkb_r
|
||||
from django.contrib.gis.measure import Distance
|
||||
from django.db.backends.mysql.operations import DatabaseOperations
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
|
||||
name = "mysql"
|
||||
geom_func_prefix = "ST_"
|
||||
|
||||
Adapter = WKTAdapter
|
||||
|
||||
@cached_property
|
||||
def mariadb(self):
|
||||
return self.connection.mysql_is_mariadb
|
||||
|
||||
@cached_property
|
||||
def mysql(self):
|
||||
return not self.connection.mysql_is_mariadb
|
||||
|
||||
@cached_property
|
||||
def select(self):
|
||||
return self.geom_func_prefix + "AsBinary(%s)"
|
||||
|
||||
@cached_property
|
||||
def from_text(self):
|
||||
return self.geom_func_prefix + "GeomFromText"
|
||||
|
||||
@cached_property
|
||||
def gis_operators(self):
|
||||
operators = {
|
||||
"bbcontains": SpatialOperator(
|
||||
func="MBRContains"
|
||||
), # For consistency w/PostGIS API
|
||||
"bboverlaps": SpatialOperator(func="MBROverlaps"), # ...
|
||||
"contained": SpatialOperator(func="MBRWithin"), # ...
|
||||
"contains": SpatialOperator(func="ST_Contains"),
|
||||
"crosses": SpatialOperator(func="ST_Crosses"),
|
||||
"disjoint": SpatialOperator(func="ST_Disjoint"),
|
||||
"equals": SpatialOperator(func="ST_Equals"),
|
||||
"exact": SpatialOperator(func="ST_Equals"),
|
||||
"intersects": SpatialOperator(func="ST_Intersects"),
|
||||
"overlaps": SpatialOperator(func="ST_Overlaps"),
|
||||
"same_as": SpatialOperator(func="ST_Equals"),
|
||||
"touches": SpatialOperator(func="ST_Touches"),
|
||||
"within": SpatialOperator(func="ST_Within"),
|
||||
}
|
||||
if self.connection.mysql_is_mariadb:
|
||||
operators["relate"] = SpatialOperator(func="ST_Relate")
|
||||
return operators
|
||||
|
||||
disallowed_aggregates = (
|
||||
models.Collect,
|
||||
models.Extent,
|
||||
models.Extent3D,
|
||||
models.MakeLine,
|
||||
models.Union,
|
||||
)
|
||||
|
||||
function_names = {
|
||||
"FromWKB": "ST_GeomFromWKB",
|
||||
"FromWKT": "ST_GeomFromText",
|
||||
}
|
||||
|
||||
@cached_property
|
||||
def unsupported_functions(self):
|
||||
unsupported = {
|
||||
"AsGML",
|
||||
"AsKML",
|
||||
"AsSVG",
|
||||
"Azimuth",
|
||||
"BoundingCircle",
|
||||
"ForcePolygonCW",
|
||||
"GeometryDistance",
|
||||
"IsEmpty",
|
||||
"LineLocatePoint",
|
||||
"MakeValid",
|
||||
"MemSize",
|
||||
"Perimeter",
|
||||
"PointOnSurface",
|
||||
"Reverse",
|
||||
"Scale",
|
||||
"SnapToGrid",
|
||||
"Transform",
|
||||
"Translate",
|
||||
}
|
||||
if self.connection.mysql_is_mariadb:
|
||||
unsupported.remove("PointOnSurface")
|
||||
unsupported.update({"GeoHash", "IsValid"})
|
||||
return unsupported
|
||||
|
||||
def geo_db_type(self, f):
|
||||
return f.geom_type
|
||||
|
||||
def get_distance(self, f, value, lookup_type):
|
||||
value = value[0]
|
||||
if isinstance(value, Distance):
|
||||
if f.geodetic(self.connection):
|
||||
raise ValueError(
|
||||
"Only numeric values of degree units are allowed on "
|
||||
"geodetic distance queries."
|
||||
)
|
||||
dist_param = getattr(
|
||||
value, Distance.unit_attname(f.units_name(self.connection))
|
||||
)
|
||||
else:
|
||||
dist_param = value
|
||||
return [dist_param]
|
||||
|
||||
def get_geometry_converter(self, expression):
|
||||
read = wkb_r().read
|
||||
srid = expression.output_field.srid
|
||||
if srid == -1:
|
||||
srid = None
|
||||
geom_class = expression.output_field.geom_class
|
||||
|
||||
def converter(value, expression, connection):
|
||||
if value is not None:
|
||||
geom = GEOSGeometryBase(read(memoryview(value)), geom_class)
|
||||
if srid:
|
||||
geom.srid = srid
|
||||
return geom
|
||||
|
||||
return converter
|
@ -0,0 +1,87 @@
|
||||
import logging
|
||||
|
||||
from django.contrib.gis.db.models import GeometryField
|
||||
from django.db import OperationalError
|
||||
from django.db.backends.mysql.schema import DatabaseSchemaEditor
|
||||
|
||||
logger = logging.getLogger("django.contrib.gis")
|
||||
|
||||
|
||||
class MySQLGISSchemaEditor(DatabaseSchemaEditor):
|
||||
sql_add_spatial_index = "CREATE SPATIAL INDEX %(index)s ON %(table)s(%(column)s)"
|
||||
sql_drop_spatial_index = "DROP INDEX %(index)s ON %(table)s"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.geometry_sql = []
|
||||
|
||||
def skip_default(self, field):
|
||||
# Geometry fields are stored as BLOB/TEXT, for which MySQL < 8.0.13
|
||||
# doesn't support defaults.
|
||||
if (
|
||||
isinstance(field, GeometryField)
|
||||
and not self._supports_limited_data_type_defaults
|
||||
):
|
||||
return True
|
||||
return super().skip_default(field)
|
||||
|
||||
def quote_value(self, value):
|
||||
if isinstance(value, self.connection.ops.Adapter):
|
||||
return super().quote_value(str(value))
|
||||
return super().quote_value(value)
|
||||
|
||||
def column_sql(self, model, field, include_default=False):
|
||||
column_sql = super().column_sql(model, field, include_default)
|
||||
# MySQL doesn't support spatial indexes on NULL columns
|
||||
if isinstance(field, GeometryField) and field.spatial_index and not field.null:
|
||||
qn = self.connection.ops.quote_name
|
||||
db_table = model._meta.db_table
|
||||
self.geometry_sql.append(
|
||||
self.sql_add_spatial_index
|
||||
% {
|
||||
"index": qn(self._create_spatial_index_name(model, field)),
|
||||
"table": qn(db_table),
|
||||
"column": qn(field.column),
|
||||
}
|
||||
)
|
||||
return column_sql
|
||||
|
||||
def create_model(self, model):
|
||||
super().create_model(model)
|
||||
self.create_spatial_indexes()
|
||||
|
||||
def add_field(self, model, field):
|
||||
super().add_field(model, field)
|
||||
self.create_spatial_indexes()
|
||||
|
||||
def remove_field(self, model, field):
|
||||
if isinstance(field, GeometryField) and field.spatial_index:
|
||||
qn = self.connection.ops.quote_name
|
||||
sql = self.sql_drop_spatial_index % {
|
||||
"index": qn(self._create_spatial_index_name(model, field)),
|
||||
"table": qn(model._meta.db_table),
|
||||
}
|
||||
try:
|
||||
self.execute(sql)
|
||||
except OperationalError:
|
||||
logger.error(
|
||||
"Couldn't remove spatial index: %s (may be expected "
|
||||
"if your storage engine doesn't support them).",
|
||||
sql,
|
||||
)
|
||||
|
||||
super().remove_field(model, field)
|
||||
|
||||
def _create_spatial_index_name(self, model, field):
|
||||
return "%s_%s_id" % (model._meta.db_table, field.column)
|
||||
|
||||
def create_spatial_indexes(self):
|
||||
for sql in self.geometry_sql:
|
||||
try:
|
||||
self.execute(sql)
|
||||
except OperationalError:
|
||||
logger.error(
|
||||
f"Cannot create SPATIAL INDEX {sql}. Only MyISAM, Aria, and InnoDB "
|
||||
f"support them.",
|
||||
)
|
||||
self.geometry_sql = []
|
@ -0,0 +1,62 @@
|
||||
from cx_Oracle import CLOB
|
||||
|
||||
from django.contrib.gis.db.backends.base.adapter import WKTAdapter
|
||||
from django.contrib.gis.geos import GeometryCollection, Polygon
|
||||
|
||||
|
||||
class OracleSpatialAdapter(WKTAdapter):
|
||||
input_size = CLOB
|
||||
|
||||
def __init__(self, geom):
|
||||
"""
|
||||
Oracle requires that polygon rings are in proper orientation. This
|
||||
affects spatial operations and an invalid orientation may cause
|
||||
failures. Correct orientations are:
|
||||
* Outer ring - counter clockwise
|
||||
* Inner ring(s) - clockwise
|
||||
"""
|
||||
if isinstance(geom, Polygon):
|
||||
if self._polygon_must_be_fixed(geom):
|
||||
geom = self._fix_polygon(geom)
|
||||
elif isinstance(geom, GeometryCollection):
|
||||
if any(
|
||||
isinstance(g, Polygon) and self._polygon_must_be_fixed(g) for g in geom
|
||||
):
|
||||
geom = self._fix_geometry_collection(geom)
|
||||
|
||||
self.wkt = geom.wkt
|
||||
self.srid = geom.srid
|
||||
|
||||
@staticmethod
|
||||
def _polygon_must_be_fixed(poly):
|
||||
return not poly.empty and (
|
||||
not poly.exterior_ring.is_counterclockwise
|
||||
or any(x.is_counterclockwise for x in poly)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _fix_polygon(cls, poly, clone=True):
|
||||
"""Fix single polygon orientation as described in __init__()."""
|
||||
if clone:
|
||||
poly = poly.clone()
|
||||
|
||||
if not poly.exterior_ring.is_counterclockwise:
|
||||
poly.exterior_ring = list(reversed(poly.exterior_ring))
|
||||
|
||||
for i in range(1, len(poly)):
|
||||
if poly[i].is_counterclockwise:
|
||||
poly[i] = list(reversed(poly[i]))
|
||||
|
||||
return poly
|
||||
|
||||
@classmethod
|
||||
def _fix_geometry_collection(cls, coll):
|
||||
"""
|
||||
Fix polygon orientations in geometry collections as described in
|
||||
__init__().
|
||||
"""
|
||||
coll = coll.clone()
|
||||
for i, geom in enumerate(coll):
|
||||
if isinstance(geom, Polygon):
|
||||
coll[i] = cls._fix_polygon(geom, clone=False)
|
||||
return coll
|
@ -0,0 +1,14 @@
|
||||
from django.db.backends.oracle.base import DatabaseWrapper as OracleDatabaseWrapper
|
||||
|
||||
from .features import DatabaseFeatures
|
||||
from .introspection import OracleIntrospection
|
||||
from .operations import OracleOperations
|
||||
from .schema import OracleGISSchemaEditor
|
||||
|
||||
|
||||
class DatabaseWrapper(OracleDatabaseWrapper):
|
||||
SchemaEditorClass = OracleGISSchemaEditor
|
||||
# Classes instantiated in __init__().
|
||||
features_class = DatabaseFeatures
|
||||
introspection_class = OracleIntrospection
|
||||
ops_class = OracleOperations
|
@ -0,0 +1,28 @@
|
||||
from django.contrib.gis.db.backends.base.features import BaseSpatialFeatures
|
||||
from django.db.backends.oracle.features import (
|
||||
DatabaseFeatures as OracleDatabaseFeatures,
|
||||
)
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class DatabaseFeatures(BaseSpatialFeatures, OracleDatabaseFeatures):
|
||||
supports_add_srs_entry = False
|
||||
supports_geometry_field_introspection = False
|
||||
supports_geometry_field_unique_index = False
|
||||
supports_perimeter_geodetic = True
|
||||
supports_dwithin_distance_expr = False
|
||||
supports_tolerance_parameter = True
|
||||
unsupported_geojson_options = {"bbox", "crs", "precision"}
|
||||
|
||||
@cached_property
|
||||
def django_test_skips(self):
|
||||
skips = super().django_test_skips
|
||||
skips.update(
|
||||
{
|
||||
"Oracle doesn't support spatial operators in constraints.": {
|
||||
"gis_tests.gis_migrations.test_operations.OperationTests."
|
||||
"test_add_check_constraint",
|
||||
},
|
||||
}
|
||||
)
|
||||
return skips
|
@ -0,0 +1,47 @@
|
||||
import cx_Oracle
|
||||
|
||||
from django.db.backends.oracle.introspection import DatabaseIntrospection
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class OracleIntrospection(DatabaseIntrospection):
|
||||
# Associating any OBJECTVAR instances with GeometryField. This won't work
|
||||
# right on Oracle objects that aren't MDSYS.SDO_GEOMETRY, but it is the
|
||||
# only object type supported within Django anyways.
|
||||
@cached_property
|
||||
def data_types_reverse(self):
|
||||
return {
|
||||
**super().data_types_reverse,
|
||||
cx_Oracle.OBJECT: "GeometryField",
|
||||
}
|
||||
|
||||
def get_geometry_type(self, table_name, description):
|
||||
with self.connection.cursor() as cursor:
|
||||
# Querying USER_SDO_GEOM_METADATA to get the SRID and dimension information.
|
||||
try:
|
||||
cursor.execute(
|
||||
'SELECT "DIMINFO", "SRID" FROM "USER_SDO_GEOM_METADATA" '
|
||||
'WHERE "TABLE_NAME"=%s AND "COLUMN_NAME"=%s',
|
||||
(table_name.upper(), description.name.upper()),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
except Exception as exc:
|
||||
raise Exception(
|
||||
"Could not find entry in USER_SDO_GEOM_METADATA "
|
||||
'corresponding to "%s"."%s"' % (table_name, description.name)
|
||||
) from exc
|
||||
|
||||
# TODO: Research way to find a more specific geometry field type for
|
||||
# the column's contents.
|
||||
field_type = "GeometryField"
|
||||
|
||||
# Getting the field parameters.
|
||||
field_params = {}
|
||||
dim, srid = row
|
||||
if srid != 4326:
|
||||
field_params["srid"] = srid
|
||||
# Size of object array (SDO_DIM_ARRAY) is number of dimensions.
|
||||
dim = dim.size()
|
||||
if dim != 2:
|
||||
field_params["dim"] = dim
|
||||
return field_type, field_params
|
@ -0,0 +1,64 @@
|
||||
"""
|
||||
The GeometryColumns and SpatialRefSys models for the Oracle spatial
|
||||
backend.
|
||||
|
||||
It should be noted that Oracle Spatial does not have database tables
|
||||
named according to the OGC standard, so the closest analogs are used.
|
||||
For example, the `USER_SDO_GEOM_METADATA` is used for the GeometryColumns
|
||||
model and the `SDO_COORD_REF_SYS` is used for the SpatialRefSys model.
|
||||
"""
|
||||
from django.contrib.gis.db import models
|
||||
from django.contrib.gis.db.backends.base.models import SpatialRefSysMixin
|
||||
|
||||
|
||||
class OracleGeometryColumns(models.Model):
|
||||
"Maps to the Oracle USER_SDO_GEOM_METADATA table."
|
||||
table_name = models.CharField(max_length=32)
|
||||
column_name = models.CharField(max_length=1024)
|
||||
srid = models.IntegerField(primary_key=True)
|
||||
# TODO: Add support for `diminfo` column (type MDSYS.SDO_DIM_ARRAY).
|
||||
|
||||
class Meta:
|
||||
app_label = "gis"
|
||||
db_table = "USER_SDO_GEOM_METADATA"
|
||||
managed = False
|
||||
|
||||
def __str__(self):
|
||||
return "%s - %s (SRID: %s)" % (self.table_name, self.column_name, self.srid)
|
||||
|
||||
@classmethod
|
||||
def table_name_col(cls):
|
||||
"""
|
||||
Return the name of the metadata column used to store the feature table
|
||||
name.
|
||||
"""
|
||||
return "table_name"
|
||||
|
||||
@classmethod
|
||||
def geom_col_name(cls):
|
||||
"""
|
||||
Return the name of the metadata column used to store the feature
|
||||
geometry column.
|
||||
"""
|
||||
return "column_name"
|
||||
|
||||
|
||||
class OracleSpatialRefSys(models.Model, SpatialRefSysMixin):
|
||||
"Maps to the Oracle MDSYS.CS_SRS table."
|
||||
cs_name = models.CharField(max_length=68)
|
||||
srid = models.IntegerField(primary_key=True)
|
||||
auth_srid = models.IntegerField()
|
||||
auth_name = models.CharField(max_length=256)
|
||||
wktext = models.CharField(max_length=2046)
|
||||
# Optional geometry representing the bounds of this coordinate
|
||||
# system. By default, all are NULL in the table.
|
||||
cs_bounds = models.PolygonField(null=True)
|
||||
|
||||
class Meta:
|
||||
app_label = "gis"
|
||||
db_table = "CS_SRS"
|
||||
managed = False
|
||||
|
||||
@property
|
||||
def wkt(self):
|
||||
return self.wktext
|
@ -0,0 +1,245 @@
|
||||
"""
|
||||
This module contains the spatial lookup types, and the `get_geo_where_clause`
|
||||
routine for Oracle Spatial.
|
||||
|
||||
Please note that WKT support is broken on the XE version, and thus
|
||||
this backend will not work on such platforms. Specifically, XE lacks
|
||||
support for an internal JVM, and Java libraries are required to use
|
||||
the WKT constructors.
|
||||
"""
|
||||
import re
|
||||
|
||||
from django.contrib.gis.db import models
|
||||
from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations
|
||||
from django.contrib.gis.db.backends.oracle.adapter import OracleSpatialAdapter
|
||||
from django.contrib.gis.db.backends.utils import SpatialOperator
|
||||
from django.contrib.gis.geos.geometry import GEOSGeometry, GEOSGeometryBase
|
||||
from django.contrib.gis.geos.prototypes.io import wkb_r
|
||||
from django.contrib.gis.measure import Distance
|
||||
from django.db.backends.oracle.operations import DatabaseOperations
|
||||
|
||||
DEFAULT_TOLERANCE = "0.05"
|
||||
|
||||
|
||||
class SDOOperator(SpatialOperator):
|
||||
sql_template = "%(func)s(%(lhs)s, %(rhs)s) = 'TRUE'"
|
||||
|
||||
|
||||
class SDODWithin(SpatialOperator):
|
||||
sql_template = "SDO_WITHIN_DISTANCE(%(lhs)s, %(rhs)s, %%s) = 'TRUE'"
|
||||
|
||||
|
||||
class SDODisjoint(SpatialOperator):
|
||||
sql_template = (
|
||||
"SDO_GEOM.RELATE(%%(lhs)s, 'DISJOINT', %%(rhs)s, %s) = 'DISJOINT'"
|
||||
% DEFAULT_TOLERANCE
|
||||
)
|
||||
|
||||
|
||||
class SDORelate(SpatialOperator):
|
||||
sql_template = "SDO_RELATE(%(lhs)s, %(rhs)s, 'mask=%(mask)s') = 'TRUE'"
|
||||
|
||||
def check_relate_argument(self, arg):
|
||||
masks = (
|
||||
"TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|"
|
||||
"CONTAINS|COVERS|ANYINTERACT|ON"
|
||||
)
|
||||
mask_regex = re.compile(r"^(%s)(\+(%s))*$" % (masks, masks), re.I)
|
||||
if not isinstance(arg, str) or not mask_regex.match(arg):
|
||||
raise ValueError('Invalid SDO_RELATE mask: "%s"' % arg)
|
||||
|
||||
def as_sql(self, connection, lookup, template_params, sql_params):
|
||||
template_params["mask"] = sql_params[-1]
|
||||
return super().as_sql(connection, lookup, template_params, sql_params[:-1])
|
||||
|
||||
|
||||
class OracleOperations(BaseSpatialOperations, DatabaseOperations):
|
||||
name = "oracle"
|
||||
oracle = True
|
||||
disallowed_aggregates = (models.Collect, models.Extent3D, models.MakeLine)
|
||||
|
||||
Adapter = OracleSpatialAdapter
|
||||
|
||||
extent = "SDO_AGGR_MBR"
|
||||
unionagg = "SDO_AGGR_UNION"
|
||||
|
||||
from_text = "SDO_GEOMETRY"
|
||||
|
||||
function_names = {
|
||||
"Area": "SDO_GEOM.SDO_AREA",
|
||||
"AsGeoJSON": "SDO_UTIL.TO_GEOJSON",
|
||||
"AsWKB": "SDO_UTIL.TO_WKBGEOMETRY",
|
||||
"AsWKT": "SDO_UTIL.TO_WKTGEOMETRY",
|
||||
"BoundingCircle": "SDO_GEOM.SDO_MBC",
|
||||
"Centroid": "SDO_GEOM.SDO_CENTROID",
|
||||
"Difference": "SDO_GEOM.SDO_DIFFERENCE",
|
||||
"Distance": "SDO_GEOM.SDO_DISTANCE",
|
||||
"Envelope": "SDO_GEOM_MBR",
|
||||
"FromWKB": "SDO_UTIL.FROM_WKBGEOMETRY",
|
||||
"FromWKT": "SDO_UTIL.FROM_WKTGEOMETRY",
|
||||
"Intersection": "SDO_GEOM.SDO_INTERSECTION",
|
||||
"IsValid": "SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT",
|
||||
"Length": "SDO_GEOM.SDO_LENGTH",
|
||||
"NumGeometries": "SDO_UTIL.GETNUMELEM",
|
||||
"NumPoints": "SDO_UTIL.GETNUMVERTICES",
|
||||
"Perimeter": "SDO_GEOM.SDO_LENGTH",
|
||||
"PointOnSurface": "SDO_GEOM.SDO_POINTONSURFACE",
|
||||
"Reverse": "SDO_UTIL.REVERSE_LINESTRING",
|
||||
"SymDifference": "SDO_GEOM.SDO_XOR",
|
||||
"Transform": "SDO_CS.TRANSFORM",
|
||||
"Union": "SDO_GEOM.SDO_UNION",
|
||||
}
|
||||
|
||||
# We want to get SDO Geometries as WKT because it is much easier to
|
||||
# instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings.
|
||||
# However, this adversely affects performance (i.e., Java is called
|
||||
# to convert to WKT on every query). If someone wishes to write a
|
||||
# SDO_GEOMETRY(...) parser in Python, let me know =)
|
||||
select = "SDO_UTIL.TO_WKBGEOMETRY(%s)"
|
||||
|
||||
gis_operators = {
|
||||
"contains": SDOOperator(func="SDO_CONTAINS"),
|
||||
"coveredby": SDOOperator(func="SDO_COVEREDBY"),
|
||||
"covers": SDOOperator(func="SDO_COVERS"),
|
||||
"disjoint": SDODisjoint(),
|
||||
"intersects": SDOOperator(
|
||||
func="SDO_OVERLAPBDYINTERSECT"
|
||||
), # TODO: Is this really the same as ST_Intersects()?
|
||||
"equals": SDOOperator(func="SDO_EQUAL"),
|
||||
"exact": SDOOperator(func="SDO_EQUAL"),
|
||||
"overlaps": SDOOperator(func="SDO_OVERLAPS"),
|
||||
"same_as": SDOOperator(func="SDO_EQUAL"),
|
||||
# Oracle uses a different syntax, e.g., 'mask=inside+touch'
|
||||
"relate": SDORelate(),
|
||||
"touches": SDOOperator(func="SDO_TOUCH"),
|
||||
"within": SDOOperator(func="SDO_INSIDE"),
|
||||
"dwithin": SDODWithin(),
|
||||
}
|
||||
|
||||
unsupported_functions = {
|
||||
"AsKML",
|
||||
"AsSVG",
|
||||
"Azimuth",
|
||||
"ForcePolygonCW",
|
||||
"GeoHash",
|
||||
"GeometryDistance",
|
||||
"IsEmpty",
|
||||
"LineLocatePoint",
|
||||
"MakeValid",
|
||||
"MemSize",
|
||||
"Scale",
|
||||
"SnapToGrid",
|
||||
"Translate",
|
||||
}
|
||||
|
||||
def geo_quote_name(self, name):
|
||||
return super().geo_quote_name(name).upper()
|
||||
|
||||
def convert_extent(self, clob):
|
||||
if clob:
|
||||
# Generally, Oracle returns a polygon for the extent -- however,
|
||||
# it can return a single point if there's only one Point in the
|
||||
# table.
|
||||
ext_geom = GEOSGeometry(memoryview(clob.read()))
|
||||
gtype = str(ext_geom.geom_type)
|
||||
if gtype == "Polygon":
|
||||
# Construct the 4-tuple from the coordinates in the polygon.
|
||||
shell = ext_geom.shell
|
||||
ll, ur = shell[0][:2], shell[2][:2]
|
||||
elif gtype == "Point":
|
||||
ll = ext_geom.coords[:2]
|
||||
ur = ll
|
||||
else:
|
||||
raise Exception(
|
||||
"Unexpected geometry type returned for extent: %s" % gtype
|
||||
)
|
||||
xmin, ymin = ll
|
||||
xmax, ymax = ur
|
||||
return (xmin, ymin, xmax, ymax)
|
||||
else:
|
||||
return None
|
||||
|
||||
def geo_db_type(self, f):
|
||||
"""
|
||||
Return the geometry database type for Oracle. Unlike other spatial
|
||||
backends, no stored procedure is necessary and it's the same for all
|
||||
geometry types.
|
||||
"""
|
||||
return "MDSYS.SDO_GEOMETRY"
|
||||
|
||||
def get_distance(self, f, value, lookup_type):
|
||||
"""
|
||||
Return the distance parameters given the value and the lookup type.
|
||||
On Oracle, geometry columns with a geodetic coordinate system behave
|
||||
implicitly like a geography column, and thus meters will be used as
|
||||
the distance parameter on them.
|
||||
"""
|
||||
if not value:
|
||||
return []
|
||||
value = value[0]
|
||||
if isinstance(value, Distance):
|
||||
if f.geodetic(self.connection):
|
||||
dist_param = value.m
|
||||
else:
|
||||
dist_param = getattr(
|
||||
value, Distance.unit_attname(f.units_name(self.connection))
|
||||
)
|
||||
else:
|
||||
dist_param = value
|
||||
|
||||
# dwithin lookups on Oracle require a special string parameter
|
||||
# that starts with "distance=".
|
||||
if lookup_type == "dwithin":
|
||||
dist_param = "distance=%s" % dist_param
|
||||
|
||||
return [dist_param]
|
||||
|
||||
def get_geom_placeholder(self, f, value, compiler):
|
||||
if value is None:
|
||||
return "NULL"
|
||||
return super().get_geom_placeholder(f, value, compiler)
|
||||
|
||||
def spatial_aggregate_name(self, agg_name):
|
||||
"""
|
||||
Return the spatial aggregate SQL name.
|
||||
"""
|
||||
agg_name = "unionagg" if agg_name.lower() == "union" else agg_name.lower()
|
||||
return getattr(self, agg_name)
|
||||
|
||||
# Routines for getting the OGC-compliant models.
|
||||
def geometry_columns(self):
|
||||
from django.contrib.gis.db.backends.oracle.models import OracleGeometryColumns
|
||||
|
||||
return OracleGeometryColumns
|
||||
|
||||
def spatial_ref_sys(self):
|
||||
from django.contrib.gis.db.backends.oracle.models import OracleSpatialRefSys
|
||||
|
||||
return OracleSpatialRefSys
|
||||
|
||||
def modify_insert_params(self, placeholder, params):
|
||||
"""Drop out insert parameters for NULL placeholder. Needed for Oracle Spatial
|
||||
backend due to #10888.
|
||||
"""
|
||||
if placeholder == "NULL":
|
||||
return []
|
||||
return super().modify_insert_params(placeholder, params)
|
||||
|
||||
def get_geometry_converter(self, expression):
|
||||
read = wkb_r().read
|
||||
srid = expression.output_field.srid
|
||||
if srid == -1:
|
||||
srid = None
|
||||
geom_class = expression.output_field.geom_class
|
||||
|
||||
def converter(value, expression, connection):
|
||||
if value is not None:
|
||||
geom = GEOSGeometryBase(read(memoryview(value.read())), geom_class)
|
||||
if srid:
|
||||
geom.srid = srid
|
||||
return geom
|
||||
|
||||
return converter
|
||||
|
||||
def get_area_att_for_field(self, field):
|
||||
return "sq_m"
|
@ -0,0 +1,121 @@
|
||||
from django.contrib.gis.db.models import GeometryField
|
||||
from django.db.backends.oracle.schema import DatabaseSchemaEditor
|
||||
from django.db.backends.utils import strip_quotes, truncate_name
|
||||
|
||||
|
||||
class OracleGISSchemaEditor(DatabaseSchemaEditor):
|
||||
sql_add_geometry_metadata = """
|
||||
INSERT INTO USER_SDO_GEOM_METADATA
|
||||
("TABLE_NAME", "COLUMN_NAME", "DIMINFO", "SRID")
|
||||
VALUES (
|
||||
%(table)s,
|
||||
%(column)s,
|
||||
MDSYS.SDO_DIM_ARRAY(
|
||||
MDSYS.SDO_DIM_ELEMENT('LONG', %(dim0)s, %(dim2)s, %(tolerance)s),
|
||||
MDSYS.SDO_DIM_ELEMENT('LAT', %(dim1)s, %(dim3)s, %(tolerance)s)
|
||||
),
|
||||
%(srid)s
|
||||
)"""
|
||||
sql_add_spatial_index = (
|
||||
"CREATE INDEX %(index)s ON %(table)s(%(column)s) "
|
||||
"INDEXTYPE IS MDSYS.SPATIAL_INDEX"
|
||||
)
|
||||
sql_drop_spatial_index = "DROP INDEX %(index)s"
|
||||
sql_clear_geometry_table_metadata = (
|
||||
"DELETE FROM USER_SDO_GEOM_METADATA WHERE TABLE_NAME = %(table)s"
|
||||
)
|
||||
sql_clear_geometry_field_metadata = (
|
||||
"DELETE FROM USER_SDO_GEOM_METADATA WHERE TABLE_NAME = %(table)s "
|
||||
"AND COLUMN_NAME = %(column)s"
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.geometry_sql = []
|
||||
|
||||
def geo_quote_name(self, name):
|
||||
return self.connection.ops.geo_quote_name(name)
|
||||
|
||||
def quote_value(self, value):
|
||||
if isinstance(value, self.connection.ops.Adapter):
|
||||
return super().quote_value(str(value))
|
||||
return super().quote_value(value)
|
||||
|
||||
def column_sql(self, model, field, include_default=False):
|
||||
column_sql = super().column_sql(model, field, include_default)
|
||||
if isinstance(field, GeometryField):
|
||||
db_table = model._meta.db_table
|
||||
self.geometry_sql.append(
|
||||
self.sql_add_geometry_metadata
|
||||
% {
|
||||
"table": self.geo_quote_name(db_table),
|
||||
"column": self.geo_quote_name(field.column),
|
||||
"dim0": field._extent[0],
|
||||
"dim1": field._extent[1],
|
||||
"dim2": field._extent[2],
|
||||
"dim3": field._extent[3],
|
||||
"tolerance": field._tolerance,
|
||||
"srid": field.srid,
|
||||
}
|
||||
)
|
||||
if field.spatial_index:
|
||||
self.geometry_sql.append(
|
||||
self.sql_add_spatial_index
|
||||
% {
|
||||
"index": self.quote_name(
|
||||
self._create_spatial_index_name(model, field)
|
||||
),
|
||||
"table": self.quote_name(db_table),
|
||||
"column": self.quote_name(field.column),
|
||||
}
|
||||
)
|
||||
return column_sql
|
||||
|
||||
def create_model(self, model):
|
||||
super().create_model(model)
|
||||
self.run_geometry_sql()
|
||||
|
||||
def delete_model(self, model):
|
||||
super().delete_model(model)
|
||||
self.execute(
|
||||
self.sql_clear_geometry_table_metadata
|
||||
% {
|
||||
"table": self.geo_quote_name(model._meta.db_table),
|
||||
}
|
||||
)
|
||||
|
||||
def add_field(self, model, field):
|
||||
super().add_field(model, field)
|
||||
self.run_geometry_sql()
|
||||
|
||||
def remove_field(self, model, field):
|
||||
if isinstance(field, GeometryField):
|
||||
self.execute(
|
||||
self.sql_clear_geometry_field_metadata
|
||||
% {
|
||||
"table": self.geo_quote_name(model._meta.db_table),
|
||||
"column": self.geo_quote_name(field.column),
|
||||
}
|
||||
)
|
||||
if field.spatial_index:
|
||||
self.execute(
|
||||
self.sql_drop_spatial_index
|
||||
% {
|
||||
"index": self.quote_name(
|
||||
self._create_spatial_index_name(model, field)
|
||||
),
|
||||
}
|
||||
)
|
||||
super().remove_field(model, field)
|
||||
|
||||
def run_geometry_sql(self):
|
||||
for sql in self.geometry_sql:
|
||||
self.execute(sql)
|
||||
self.geometry_sql = []
|
||||
|
||||
def _create_spatial_index_name(self, model, field):
|
||||
# Oracle doesn't allow object names > 30 characters. Use this scheme
|
||||
# instead of self._create_index_name() for backwards compatibility.
|
||||
return truncate_name(
|
||||
"%s_%s_id" % (strip_quotes(model._meta.db_table), field.column), 30
|
||||
)
|
@ -0,0 +1,62 @@
|
||||
"""
|
||||
This object provides quoting for GEOS geometries into PostgreSQL/PostGIS.
|
||||
"""
|
||||
from django.contrib.gis.db.backends.postgis.pgraster import to_pgraster
|
||||
from django.contrib.gis.geos import GEOSGeometry
|
||||
from django.db.backends.postgresql.psycopg_any import sql
|
||||
|
||||
|
||||
class PostGISAdapter:
|
||||
def __init__(self, obj, geography=False):
|
||||
"""
|
||||
Initialize on the spatial object.
|
||||
"""
|
||||
self.is_geometry = isinstance(obj, (GEOSGeometry, PostGISAdapter))
|
||||
|
||||
# Getting the WKB (in string form, to allow easy pickling of
|
||||
# the adaptor) and the SRID from the geometry or raster.
|
||||
if self.is_geometry:
|
||||
self.ewkb = bytes(obj.ewkb)
|
||||
else:
|
||||
self.ewkb = to_pgraster(obj)
|
||||
|
||||
self.srid = obj.srid
|
||||
self.geography = geography
|
||||
|
||||
def __conform__(self, proto):
|
||||
"""Does the given protocol conform to what Psycopg2 expects?"""
|
||||
from psycopg2.extensions import ISQLQuote
|
||||
|
||||
if proto == ISQLQuote:
|
||||
return self
|
||||
else:
|
||||
raise Exception(
|
||||
"Error implementing psycopg2 protocol. Is psycopg2 installed?"
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, PostGISAdapter) and self.ewkb == other.ewkb
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.ewkb)
|
||||
|
||||
def __str__(self):
|
||||
return self.getquoted().decode()
|
||||
|
||||
@classmethod
|
||||
def _fix_polygon(cls, poly):
|
||||
return poly
|
||||
|
||||
def getquoted(self):
|
||||
"""
|
||||
Return a properly quoted string for use in PostgreSQL/PostGIS.
|
||||
"""
|
||||
if self.is_geometry:
|
||||
# Psycopg will figure out whether to use E'\\000' or '\000'.
|
||||
return b"%s(%s)" % (
|
||||
b"ST_GeogFromWKB" if self.geography else b"ST_GeomFromEWKB",
|
||||
sql.quote(self.ewkb).encode(),
|
||||
)
|
||||
else:
|
||||
# For rasters, add explicit type cast to WKB string.
|
||||
return b"'%s'::raster" % self.ewkb.hex().encode()
|
@ -0,0 +1,147 @@
|
||||
from functools import lru_cache
|
||||
|
||||
from django.db.backends.base.base import NO_DB_ALIAS
|
||||
from django.db.backends.postgresql.base import DatabaseWrapper as PsycopgDatabaseWrapper
|
||||
from django.db.backends.postgresql.psycopg_any import is_psycopg3
|
||||
|
||||
from .adapter import PostGISAdapter
|
||||
from .features import DatabaseFeatures
|
||||
from .introspection import PostGISIntrospection
|
||||
from .operations import PostGISOperations
|
||||
from .schema import PostGISSchemaEditor
|
||||
|
||||
if is_psycopg3:
|
||||
from psycopg.adapt import Dumper
|
||||
from psycopg.pq import Format
|
||||
from psycopg.types import TypeInfo
|
||||
from psycopg.types.string import TextBinaryLoader, TextLoader
|
||||
|
||||
class GeometryType:
|
||||
pass
|
||||
|
||||
class GeographyType:
|
||||
pass
|
||||
|
||||
class RasterType:
|
||||
pass
|
||||
|
||||
class BaseTextDumper(Dumper):
|
||||
def dump(self, obj):
|
||||
# Return bytes as hex for text formatting
|
||||
return obj.ewkb.hex().encode()
|
||||
|
||||
class BaseBinaryDumper(Dumper):
|
||||
format = Format.BINARY
|
||||
|
||||
def dump(self, obj):
|
||||
return obj.ewkb
|
||||
|
||||
@lru_cache
|
||||
def postgis_adapters(geo_oid, geog_oid, raster_oid):
|
||||
class BaseDumper(Dumper):
|
||||
def __init_subclass__(cls, base_dumper):
|
||||
super().__init_subclass__()
|
||||
|
||||
cls.GeometryDumper = type(
|
||||
"GeometryDumper", (base_dumper,), {"oid": geo_oid}
|
||||
)
|
||||
cls.GeographyDumper = type(
|
||||
"GeographyDumper", (base_dumper,), {"oid": geog_oid}
|
||||
)
|
||||
cls.RasterDumper = type(
|
||||
"RasterDumper", (BaseTextDumper,), {"oid": raster_oid}
|
||||
)
|
||||
|
||||
def get_key(self, obj, format):
|
||||
if obj.is_geometry:
|
||||
return GeographyType if obj.geography else GeometryType
|
||||
else:
|
||||
return RasterType
|
||||
|
||||
def upgrade(self, obj, format):
|
||||
if obj.is_geometry:
|
||||
if obj.geography:
|
||||
return self.GeographyDumper(GeographyType)
|
||||
else:
|
||||
return self.GeometryDumper(GeometryType)
|
||||
else:
|
||||
return self.RasterDumper(RasterType)
|
||||
|
||||
def dump(self, obj):
|
||||
raise NotImplementedError
|
||||
|
||||
class PostGISTextDumper(BaseDumper, base_dumper=BaseTextDumper):
|
||||
pass
|
||||
|
||||
class PostGISBinaryDumper(BaseDumper, base_dumper=BaseBinaryDumper):
|
||||
format = Format.BINARY
|
||||
|
||||
return PostGISTextDumper, PostGISBinaryDumper
|
||||
|
||||
|
||||
class DatabaseWrapper(PsycopgDatabaseWrapper):
|
||||
SchemaEditorClass = PostGISSchemaEditor
|
||||
|
||||
_type_infos = {
|
||||
"geometry": {},
|
||||
"geography": {},
|
||||
"raster": {},
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if kwargs.get("alias", "") != NO_DB_ALIAS:
|
||||
self.features = DatabaseFeatures(self)
|
||||
self.ops = PostGISOperations(self)
|
||||
self.introspection = PostGISIntrospection(self)
|
||||
|
||||
def prepare_database(self):
|
||||
super().prepare_database()
|
||||
# Check that postgis extension is installed.
|
||||
with self.cursor() as cursor:
|
||||
cursor.execute("SELECT 1 FROM pg_extension WHERE extname = %s", ["postgis"])
|
||||
if bool(cursor.fetchone()):
|
||||
return
|
||||
cursor.execute("CREATE EXTENSION IF NOT EXISTS postgis")
|
||||
if is_psycopg3:
|
||||
# Ensure adapters are registers if PostGIS is used within this
|
||||
# connection.
|
||||
self.register_geometry_adapters(self.connection, True)
|
||||
|
||||
def get_new_connection(self, conn_params):
|
||||
connection = super().get_new_connection(conn_params)
|
||||
if is_psycopg3:
|
||||
self.register_geometry_adapters(connection)
|
||||
return connection
|
||||
|
||||
if is_psycopg3:
|
||||
|
||||
def _register_type(self, pg_connection, typename):
|
||||
registry = self._type_infos[typename]
|
||||
try:
|
||||
info = registry[self.alias]
|
||||
except KeyError:
|
||||
info = TypeInfo.fetch(pg_connection, typename)
|
||||
registry[self.alias] = info
|
||||
|
||||
if info: # Can be None if the type does not exist (yet).
|
||||
info.register(pg_connection)
|
||||
pg_connection.adapters.register_loader(info.oid, TextLoader)
|
||||
pg_connection.adapters.register_loader(info.oid, TextBinaryLoader)
|
||||
|
||||
return info.oid if info else None
|
||||
|
||||
def register_geometry_adapters(self, pg_connection, clear_caches=False):
|
||||
if clear_caches:
|
||||
for typename in self._type_infos:
|
||||
self._type_infos[typename].pop(self.alias, None)
|
||||
|
||||
geo_oid = self._register_type(pg_connection, "geometry")
|
||||
geog_oid = self._register_type(pg_connection, "geography")
|
||||
raster_oid = self._register_type(pg_connection, "raster")
|
||||
|
||||
PostGISTextDumper, PostGISBinaryDumper = postgis_adapters(
|
||||
geo_oid, geog_oid, raster_oid
|
||||
)
|
||||
pg_connection.adapters.register_dumper(PostGISAdapter, PostGISTextDumper)
|
||||
pg_connection.adapters.register_dumper(PostGISAdapter, PostGISBinaryDumper)
|
@ -0,0 +1,62 @@
|
||||
"""
|
||||
PostGIS to GDAL conversion constant definitions
|
||||
"""
|
||||
# Lookup to convert pixel type values from GDAL to PostGIS
|
||||
GDAL_TO_POSTGIS = [None, 4, 6, 5, 8, 7, 10, 11, None, None, None, None]
|
||||
|
||||
# Lookup to convert pixel type values from PostGIS to GDAL
|
||||
POSTGIS_TO_GDAL = [1, 1, 1, 3, 1, 3, 2, 5, 4, None, 6, 7, None, None]
|
||||
|
||||
# Struct pack structure for raster header, the raster header has the
|
||||
# following structure:
|
||||
#
|
||||
# Endianness, PostGIS raster version, number of bands, scale, origin,
|
||||
# skew, srid, width, and height.
|
||||
#
|
||||
# Scale, origin, and skew have x and y values. PostGIS currently uses
|
||||
# a fixed endianness (1) and there is only one version (0).
|
||||
POSTGIS_HEADER_STRUCTURE = "B H H d d d d d d i H H"
|
||||
|
||||
# Lookup values to convert GDAL pixel types to struct characters. This is
|
||||
# used to pack and unpack the pixel values of PostGIS raster bands.
|
||||
GDAL_TO_STRUCT = [
|
||||
None,
|
||||
"B",
|
||||
"H",
|
||||
"h",
|
||||
"L",
|
||||
"l",
|
||||
"f",
|
||||
"d",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
]
|
||||
|
||||
# Size of the packed value in bytes for different numerical types.
|
||||
# This is needed to cut chunks of band data out of PostGIS raster strings
|
||||
# when decomposing them into GDALRasters.
|
||||
# See https://docs.python.org/library/struct.html#format-characters
|
||||
STRUCT_SIZE = {
|
||||
"b": 1, # Signed char
|
||||
"B": 1, # Unsigned char
|
||||
"?": 1, # _Bool
|
||||
"h": 2, # Short
|
||||
"H": 2, # Unsigned short
|
||||
"i": 4, # Integer
|
||||
"I": 4, # Unsigned Integer
|
||||
"l": 4, # Long
|
||||
"L": 4, # Unsigned Long
|
||||
"f": 4, # Float
|
||||
"d": 8, # Double
|
||||
}
|
||||
|
||||
# Pixel type specifies type of pixel values in a band. Storage flag specifies
|
||||
# whether the band data is stored as part of the datum or is to be found on the
|
||||
# server's filesystem. There are currently 11 supported pixel value types, so 4
|
||||
# bits are enough to account for all. Reserve the upper 4 bits for generic
|
||||
# flags. See
|
||||
# https://trac.osgeo.org/postgis/wiki/WKTRaster/RFC/RFC1_V0SerialFormat#Pixeltypeandstorageflag
|
||||
BANDTYPE_PIXTYPE_MASK = 0x0F
|
||||
BANDTYPE_FLAG_HASNODATA = 1 << 6
|
@ -0,0 +1,13 @@
|
||||
from django.contrib.gis.db.backends.base.features import BaseSpatialFeatures
|
||||
from django.db.backends.postgresql.features import (
|
||||
DatabaseFeatures as PsycopgDatabaseFeatures,
|
||||
)
|
||||
|
||||
|
||||
class DatabaseFeatures(BaseSpatialFeatures, PsycopgDatabaseFeatures):
|
||||
supports_geography = True
|
||||
supports_3d_storage = True
|
||||
supports_3d_functions = True
|
||||
supports_raster = True
|
||||
supports_empty_geometries = True
|
||||
empty_intersection_returns_none = False
|
@ -0,0 +1,71 @@
|
||||
from django.contrib.gis.gdal import OGRGeomType
|
||||
from django.db.backends.postgresql.introspection import DatabaseIntrospection
|
||||
|
||||
|
||||
class PostGISIntrospection(DatabaseIntrospection):
|
||||
postgis_oid_lookup = {} # Populated when introspection is performed.
|
||||
|
||||
ignored_tables = DatabaseIntrospection.ignored_tables + [
|
||||
"geography_columns",
|
||||
"geometry_columns",
|
||||
"raster_columns",
|
||||
"spatial_ref_sys",
|
||||
"raster_overviews",
|
||||
]
|
||||
|
||||
def get_field_type(self, data_type, description):
|
||||
if not self.postgis_oid_lookup:
|
||||
# Query PostgreSQL's pg_type table to determine the OID integers
|
||||
# for the PostGIS data types used in reverse lookup (the integers
|
||||
# may be different across versions). To prevent unnecessary
|
||||
# requests upon connection initialization, the `data_types_reverse`
|
||||
# dictionary isn't updated until introspection is performed here.
|
||||
with self.connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT oid, typname "
|
||||
"FROM pg_type "
|
||||
"WHERE typname IN ('geometry', 'geography')"
|
||||
)
|
||||
self.postgis_oid_lookup = dict(cursor.fetchall())
|
||||
self.data_types_reverse.update(
|
||||
(oid, "GeometryField") for oid in self.postgis_oid_lookup
|
||||
)
|
||||
return super().get_field_type(data_type, description)
|
||||
|
||||
def get_geometry_type(self, table_name, description):
|
||||
"""
|
||||
The geometry type OID used by PostGIS does not indicate the particular
|
||||
type of field that a geometry column is (e.g., whether it's a
|
||||
PointField or a PolygonField). Thus, this routine queries the PostGIS
|
||||
metadata tables to determine the geometry type.
|
||||
"""
|
||||
with self.connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT t.coord_dimension, t.srid, t.type FROM (
|
||||
SELECT * FROM geometry_columns
|
||||
UNION ALL
|
||||
SELECT * FROM geography_columns
|
||||
) AS t WHERE t.f_table_name = %s AND t.f_geometry_column = %s
|
||||
""",
|
||||
(table_name, description.name),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
raise Exception(
|
||||
'Could not find a geometry or geography column for "%s"."%s"'
|
||||
% (table_name, description.name)
|
||||
)
|
||||
dim, srid, field_type = row
|
||||
# OGRGeomType does not require GDAL and makes it easy to convert
|
||||
# from OGC geom type name to Django field.
|
||||
field_type = OGRGeomType(field_type).django
|
||||
# Getting any GeometryField keyword arguments that are not the default.
|
||||
field_params = {}
|
||||
if self.postgis_oid_lookup.get(description.type_code) == "geography":
|
||||
field_params["geography"] = True
|
||||
if srid != 4326:
|
||||
field_params["srid"] = srid
|
||||
if dim != 2:
|
||||
field_params["dim"] = dim
|
||||
return field_type, field_params
|
@ -0,0 +1,72 @@
|
||||
"""
|
||||
The GeometryColumns and SpatialRefSys models for the PostGIS backend.
|
||||
"""
|
||||
from django.contrib.gis.db.backends.base.models import SpatialRefSysMixin
|
||||
from django.db import models
|
||||
|
||||
|
||||
class PostGISGeometryColumns(models.Model):
|
||||
"""
|
||||
The 'geometry_columns' view from PostGIS. See the PostGIS
|
||||
documentation at Ch. 4.3.2.
|
||||
"""
|
||||
|
||||
f_table_catalog = models.CharField(max_length=256)
|
||||
f_table_schema = models.CharField(max_length=256)
|
||||
f_table_name = models.CharField(max_length=256)
|
||||
f_geometry_column = models.CharField(max_length=256)
|
||||
coord_dimension = models.IntegerField()
|
||||
srid = models.IntegerField(primary_key=True)
|
||||
type = models.CharField(max_length=30)
|
||||
|
||||
class Meta:
|
||||
app_label = "gis"
|
||||
db_table = "geometry_columns"
|
||||
managed = False
|
||||
|
||||
def __str__(self):
|
||||
return "%s.%s - %dD %s field (SRID: %d)" % (
|
||||
self.f_table_name,
|
||||
self.f_geometry_column,
|
||||
self.coord_dimension,
|
||||
self.type,
|
||||
self.srid,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def table_name_col(cls):
|
||||
"""
|
||||
Return the name of the metadata column used to store the feature table
|
||||
name.
|
||||
"""
|
||||
return "f_table_name"
|
||||
|
||||
@classmethod
|
||||
def geom_col_name(cls):
|
||||
"""
|
||||
Return the name of the metadata column used to store the feature
|
||||
geometry column.
|
||||
"""
|
||||
return "f_geometry_column"
|
||||
|
||||
|
||||
class PostGISSpatialRefSys(models.Model, SpatialRefSysMixin):
|
||||
"""
|
||||
The 'spatial_ref_sys' table from PostGIS. See the PostGIS
|
||||
documentation at Ch. 4.2.1.
|
||||
"""
|
||||
|
||||
srid = models.IntegerField(primary_key=True)
|
||||
auth_name = models.CharField(max_length=256)
|
||||
auth_srid = models.IntegerField()
|
||||
srtext = models.CharField(max_length=2048)
|
||||
proj4text = models.CharField(max_length=2048)
|
||||
|
||||
class Meta:
|
||||
app_label = "gis"
|
||||
db_table = "spatial_ref_sys"
|
||||
managed = False
|
||||
|
||||
@property
|
||||
def wkt(self):
|
||||
return self.srtext
|
@ -0,0 +1,423 @@
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations
|
||||
from django.contrib.gis.db.backends.utils import SpatialOperator
|
||||
from django.contrib.gis.db.models import GeometryField, RasterField
|
||||
from django.contrib.gis.gdal import GDALRaster
|
||||
from django.contrib.gis.geos.geometry import GEOSGeometryBase
|
||||
from django.contrib.gis.geos.prototypes.io import wkb_r
|
||||
from django.contrib.gis.measure import Distance
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import NotSupportedError, ProgrammingError
|
||||
from django.db.backends.postgresql.operations import DatabaseOperations
|
||||
from django.db.backends.postgresql.psycopg_any import is_psycopg3
|
||||
from django.db.models import Func, Value
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.version import get_version_tuple
|
||||
|
||||
from .adapter import PostGISAdapter
|
||||
from .models import PostGISGeometryColumns, PostGISSpatialRefSys
|
||||
from .pgraster import from_pgraster
|
||||
|
||||
# Identifier to mark raster lookups as bilateral.
|
||||
BILATERAL = "bilateral"
|
||||
|
||||
|
||||
class PostGISOperator(SpatialOperator):
|
||||
def __init__(self, geography=False, raster=False, **kwargs):
|
||||
# Only a subset of the operators and functions are available for the
|
||||
# geography type. Lookups that don't support geography will be cast to
|
||||
# geometry.
|
||||
self.geography = geography
|
||||
# Only a subset of the operators and functions are available for the
|
||||
# raster type. Lookups that don't support raster will be converted to
|
||||
# polygons. If the raster argument is set to BILATERAL, then the
|
||||
# operator cannot handle mixed geom-raster lookups.
|
||||
self.raster = raster
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def as_sql(self, connection, lookup, template_params, *args):
|
||||
template_params = self.check_raster(lookup, template_params)
|
||||
template_params = self.check_geography(lookup, template_params)
|
||||
return super().as_sql(connection, lookup, template_params, *args)
|
||||
|
||||
def check_raster(self, lookup, template_params):
|
||||
spheroid = lookup.rhs_params and lookup.rhs_params[-1] == "spheroid"
|
||||
|
||||
# Check which input is a raster.
|
||||
lhs_is_raster = lookup.lhs.field.geom_type == "RASTER"
|
||||
rhs_is_raster = isinstance(lookup.rhs, GDALRaster)
|
||||
|
||||
# Look for band indices and inject them if provided.
|
||||
if lookup.band_lhs is not None and lhs_is_raster:
|
||||
if not self.func:
|
||||
raise ValueError(
|
||||
"Band indices are not allowed for this operator, it works on bbox "
|
||||
"only."
|
||||
)
|
||||
template_params["lhs"] = "%s, %s" % (
|
||||
template_params["lhs"],
|
||||
lookup.band_lhs,
|
||||
)
|
||||
|
||||
if lookup.band_rhs is not None and rhs_is_raster:
|
||||
if not self.func:
|
||||
raise ValueError(
|
||||
"Band indices are not allowed for this operator, it works on bbox "
|
||||
"only."
|
||||
)
|
||||
template_params["rhs"] = "%s, %s" % (
|
||||
template_params["rhs"],
|
||||
lookup.band_rhs,
|
||||
)
|
||||
|
||||
# Convert rasters to polygons if necessary.
|
||||
if not self.raster or spheroid:
|
||||
# Operators without raster support.
|
||||
if lhs_is_raster:
|
||||
template_params["lhs"] = "ST_Polygon(%s)" % template_params["lhs"]
|
||||
if rhs_is_raster:
|
||||
template_params["rhs"] = "ST_Polygon(%s)" % template_params["rhs"]
|
||||
elif self.raster == BILATERAL:
|
||||
# Operators with raster support but don't support mixed (rast-geom)
|
||||
# lookups.
|
||||
if lhs_is_raster and not rhs_is_raster:
|
||||
template_params["lhs"] = "ST_Polygon(%s)" % template_params["lhs"]
|
||||
elif rhs_is_raster and not lhs_is_raster:
|
||||
template_params["rhs"] = "ST_Polygon(%s)" % template_params["rhs"]
|
||||
|
||||
return template_params
|
||||
|
||||
def check_geography(self, lookup, template_params):
|
||||
"""Convert geography fields to geometry types, if necessary."""
|
||||
if lookup.lhs.output_field.geography and not self.geography:
|
||||
template_params["lhs"] += "::geometry"
|
||||
return template_params
|
||||
|
||||
|
||||
class ST_Polygon(Func):
|
||||
function = "ST_Polygon"
|
||||
|
||||
def __init__(self, expr):
|
||||
super().__init__(expr)
|
||||
expr = self.source_expressions[0]
|
||||
if isinstance(expr, Value) and not expr._output_field_or_none:
|
||||
self.source_expressions[0] = Value(
|
||||
expr.value, output_field=RasterField(srid=expr.value.srid)
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def output_field(self):
|
||||
return GeometryField(srid=self.source_expressions[0].field.srid)
|
||||
|
||||
|
||||
class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
|
||||
name = "postgis"
|
||||
postgis = True
|
||||
geom_func_prefix = "ST_"
|
||||
|
||||
Adapter = PostGISAdapter
|
||||
|
||||
collect = geom_func_prefix + "Collect"
|
||||
extent = geom_func_prefix + "Extent"
|
||||
extent3d = geom_func_prefix + "3DExtent"
|
||||
length3d = geom_func_prefix + "3DLength"
|
||||
makeline = geom_func_prefix + "MakeLine"
|
||||
perimeter3d = geom_func_prefix + "3DPerimeter"
|
||||
unionagg = geom_func_prefix + "Union"
|
||||
|
||||
gis_operators = {
|
||||
"bbcontains": PostGISOperator(op="~", raster=True),
|
||||
"bboverlaps": PostGISOperator(op="&&", geography=True, raster=True),
|
||||
"contained": PostGISOperator(op="@", raster=True),
|
||||
"overlaps_left": PostGISOperator(op="&<", raster=BILATERAL),
|
||||
"overlaps_right": PostGISOperator(op="&>", raster=BILATERAL),
|
||||
"overlaps_below": PostGISOperator(op="&<|"),
|
||||
"overlaps_above": PostGISOperator(op="|&>"),
|
||||
"left": PostGISOperator(op="<<"),
|
||||
"right": PostGISOperator(op=">>"),
|
||||
"strictly_below": PostGISOperator(op="<<|"),
|
||||
"strictly_above": PostGISOperator(op="|>>"),
|
||||
"same_as": PostGISOperator(op="~=", raster=BILATERAL),
|
||||
"exact": PostGISOperator(op="~=", raster=BILATERAL), # alias of same_as
|
||||
"contains": PostGISOperator(func="ST_Contains", raster=BILATERAL),
|
||||
"contains_properly": PostGISOperator(
|
||||
func="ST_ContainsProperly", raster=BILATERAL
|
||||
),
|
||||
"coveredby": PostGISOperator(
|
||||
func="ST_CoveredBy", geography=True, raster=BILATERAL
|
||||
),
|
||||
"covers": PostGISOperator(func="ST_Covers", geography=True, raster=BILATERAL),
|
||||
"crosses": PostGISOperator(func="ST_Crosses"),
|
||||
"disjoint": PostGISOperator(func="ST_Disjoint", raster=BILATERAL),
|
||||
"equals": PostGISOperator(func="ST_Equals"),
|
||||
"intersects": PostGISOperator(
|
||||
func="ST_Intersects", geography=True, raster=BILATERAL
|
||||
),
|
||||
"overlaps": PostGISOperator(func="ST_Overlaps", raster=BILATERAL),
|
||||
"relate": PostGISOperator(func="ST_Relate"),
|
||||
"touches": PostGISOperator(func="ST_Touches", raster=BILATERAL),
|
||||
"within": PostGISOperator(func="ST_Within", raster=BILATERAL),
|
||||
"dwithin": PostGISOperator(func="ST_DWithin", geography=True, raster=BILATERAL),
|
||||
}
|
||||
|
||||
unsupported_functions = set()
|
||||
|
||||
select = "%s" if is_psycopg3 else "%s::bytea"
|
||||
|
||||
select_extent = None
|
||||
|
||||
@cached_property
|
||||
def function_names(self):
|
||||
function_names = {
|
||||
"AsWKB": "ST_AsBinary",
|
||||
"AsWKT": "ST_AsText",
|
||||
"BoundingCircle": "ST_MinimumBoundingCircle",
|
||||
"FromWKB": "ST_GeomFromWKB",
|
||||
"FromWKT": "ST_GeomFromText",
|
||||
"NumPoints": "ST_NPoints",
|
||||
}
|
||||
return function_names
|
||||
|
||||
@cached_property
|
||||
def spatial_version(self):
|
||||
"""Determine the version of the PostGIS library."""
|
||||
# Trying to get the PostGIS version because the function
|
||||
# signatures will depend on the version used. The cost
|
||||
# here is a database query to determine the version, which
|
||||
# can be mitigated by setting `POSTGIS_VERSION` with a 3-tuple
|
||||
# comprising user-supplied values for the major, minor, and
|
||||
# subminor revision of PostGIS.
|
||||
if hasattr(settings, "POSTGIS_VERSION"):
|
||||
version = settings.POSTGIS_VERSION
|
||||
else:
|
||||
# Run a basic query to check the status of the connection so we're
|
||||
# sure we only raise the error below if the problem comes from
|
||||
# PostGIS and not from PostgreSQL itself (see #24862).
|
||||
self._get_postgis_func("version")
|
||||
|
||||
try:
|
||||
vtup = self.postgis_version_tuple()
|
||||
except ProgrammingError:
|
||||
raise ImproperlyConfigured(
|
||||
'Cannot determine PostGIS version for database "%s" '
|
||||
'using command "SELECT postgis_lib_version()". '
|
||||
"GeoDjango requires at least PostGIS version 2.5. "
|
||||
"Was the database created from a spatial database "
|
||||
"template?" % self.connection.settings_dict["NAME"]
|
||||
)
|
||||
version = vtup[1:]
|
||||
return version
|
||||
|
||||
def convert_extent(self, box):
|
||||
"""
|
||||
Return a 4-tuple extent for the `Extent` aggregate by converting
|
||||
the bounding box text returned by PostGIS (`box` argument), for
|
||||
example: "BOX(-90.0 30.0, -85.0 40.0)".
|
||||
"""
|
||||
if box is None:
|
||||
return None
|
||||
ll, ur = box[4:-1].split(",")
|
||||
xmin, ymin = map(float, ll.split())
|
||||
xmax, ymax = map(float, ur.split())
|
||||
return (xmin, ymin, xmax, ymax)
|
||||
|
||||
def convert_extent3d(self, box3d):
|
||||
"""
|
||||
Return a 6-tuple extent for the `Extent3D` aggregate by converting
|
||||
the 3d bounding-box text returned by PostGIS (`box3d` argument), for
|
||||
example: "BOX3D(-90.0 30.0 1, -85.0 40.0 2)".
|
||||
"""
|
||||
if box3d is None:
|
||||
return None
|
||||
ll, ur = box3d[6:-1].split(",")
|
||||
xmin, ymin, zmin = map(float, ll.split())
|
||||
xmax, ymax, zmax = map(float, ur.split())
|
||||
return (xmin, ymin, zmin, xmax, ymax, zmax)
|
||||
|
||||
def geo_db_type(self, f):
|
||||
"""
|
||||
Return the database field type for the given spatial field.
|
||||
"""
|
||||
if f.geom_type == "RASTER":
|
||||
return "raster"
|
||||
|
||||
# Type-based geometries.
|
||||
# TODO: Support 'M' extension.
|
||||
if f.dim == 3:
|
||||
geom_type = f.geom_type + "Z"
|
||||
else:
|
||||
geom_type = f.geom_type
|
||||
if f.geography:
|
||||
if f.srid != 4326:
|
||||
raise NotSupportedError(
|
||||
"PostGIS only supports geography columns with an SRID of 4326."
|
||||
)
|
||||
|
||||
return "geography(%s,%d)" % (geom_type, f.srid)
|
||||
else:
|
||||
return "geometry(%s,%d)" % (geom_type, f.srid)
|
||||
|
||||
def get_distance(self, f, dist_val, lookup_type):
|
||||
"""
|
||||
Retrieve the distance parameters for the given geometry field,
|
||||
distance lookup value, and the distance lookup type.
|
||||
|
||||
This is the most complex implementation of the spatial backends due to
|
||||
what is supported on geodetic geometry columns vs. what's available on
|
||||
projected geometry columns. In addition, it has to take into account
|
||||
the geography column type.
|
||||
"""
|
||||
# Getting the distance parameter
|
||||
value = dist_val[0]
|
||||
|
||||
# Shorthand boolean flags.
|
||||
geodetic = f.geodetic(self.connection)
|
||||
geography = f.geography
|
||||
|
||||
if isinstance(value, Distance):
|
||||
if geography:
|
||||
dist_param = value.m
|
||||
elif geodetic:
|
||||
if lookup_type == "dwithin":
|
||||
raise ValueError(
|
||||
"Only numeric values of degree units are "
|
||||
"allowed on geographic DWithin queries."
|
||||
)
|
||||
dist_param = value.m
|
||||
else:
|
||||
dist_param = getattr(
|
||||
value, Distance.unit_attname(f.units_name(self.connection))
|
||||
)
|
||||
else:
|
||||
# Assuming the distance is in the units of the field.
|
||||
dist_param = value
|
||||
|
||||
return [dist_param]
|
||||
|
||||
def get_geom_placeholder(self, f, value, compiler):
|
||||
"""
|
||||
Provide a proper substitution value for Geometries or rasters that are
|
||||
not in the SRID of the field. Specifically, this routine will
|
||||
substitute in the ST_Transform() function call.
|
||||
"""
|
||||
transform_func = self.spatial_function_name("Transform")
|
||||
if hasattr(value, "as_sql"):
|
||||
if value.field.srid == f.srid:
|
||||
placeholder = "%s"
|
||||
else:
|
||||
placeholder = "%s(%%s, %s)" % (transform_func, f.srid)
|
||||
return placeholder
|
||||
|
||||
# Get the srid for this object
|
||||
if value is None:
|
||||
value_srid = None
|
||||
else:
|
||||
value_srid = value.srid
|
||||
|
||||
# Adding Transform() to the SQL placeholder if the value srid
|
||||
# is not equal to the field srid.
|
||||
if value_srid is None or value_srid == f.srid:
|
||||
placeholder = "%s"
|
||||
else:
|
||||
placeholder = "%s(%%s, %s)" % (transform_func, f.srid)
|
||||
|
||||
return placeholder
|
||||
|
||||
def _get_postgis_func(self, func):
|
||||
"""
|
||||
Helper routine for calling PostGIS functions and returning their result.
|
||||
"""
|
||||
# Close out the connection. See #9437.
|
||||
with self.connection.temporary_connection() as cursor:
|
||||
cursor.execute("SELECT %s()" % func)
|
||||
return cursor.fetchone()[0]
|
||||
|
||||
def postgis_geos_version(self):
|
||||
"Return the version of the GEOS library used with PostGIS."
|
||||
return self._get_postgis_func("postgis_geos_version")
|
||||
|
||||
def postgis_lib_version(self):
|
||||
"Return the version number of the PostGIS library used with PostgreSQL."
|
||||
return self._get_postgis_func("postgis_lib_version")
|
||||
|
||||
def postgis_proj_version(self):
|
||||
"""Return the version of the PROJ library used with PostGIS."""
|
||||
return self._get_postgis_func("postgis_proj_version")
|
||||
|
||||
def postgis_version(self):
|
||||
"Return PostGIS version number and compile-time options."
|
||||
return self._get_postgis_func("postgis_version")
|
||||
|
||||
def postgis_full_version(self):
|
||||
"Return PostGIS version number and compile-time options."
|
||||
return self._get_postgis_func("postgis_full_version")
|
||||
|
||||
def postgis_version_tuple(self):
|
||||
"""
|
||||
Return the PostGIS version as a tuple (version string, major,
|
||||
minor, subminor).
|
||||
"""
|
||||
version = self.postgis_lib_version()
|
||||
return (version,) + get_version_tuple(version)
|
||||
|
||||
def proj_version_tuple(self):
|
||||
"""
|
||||
Return the version of PROJ used by PostGIS as a tuple of the
|
||||
major, minor, and subminor release numbers.
|
||||
"""
|
||||
proj_regex = re.compile(r"(\d+)\.(\d+)\.(\d+)")
|
||||
proj_ver_str = self.postgis_proj_version()
|
||||
m = proj_regex.search(proj_ver_str)
|
||||
if m:
|
||||
return tuple(map(int, m.groups()))
|
||||
else:
|
||||
raise Exception("Could not determine PROJ version from PostGIS.")
|
||||
|
||||
def spatial_aggregate_name(self, agg_name):
|
||||
if agg_name == "Extent3D":
|
||||
return self.extent3d
|
||||
else:
|
||||
return self.geom_func_prefix + agg_name
|
||||
|
||||
# Routines for getting the OGC-compliant models.
|
||||
def geometry_columns(self):
|
||||
return PostGISGeometryColumns
|
||||
|
||||
def spatial_ref_sys(self):
|
||||
return PostGISSpatialRefSys
|
||||
|
||||
def parse_raster(self, value):
|
||||
"""Convert a PostGIS HEX String into a dict readable by GDALRaster."""
|
||||
return from_pgraster(value)
|
||||
|
||||
def distance_expr_for_lookup(self, lhs, rhs, **kwargs):
|
||||
return super().distance_expr_for_lookup(
|
||||
self._normalize_distance_lookup_arg(lhs),
|
||||
self._normalize_distance_lookup_arg(rhs),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _normalize_distance_lookup_arg(arg):
|
||||
is_raster = (
|
||||
arg.field.geom_type == "RASTER"
|
||||
if hasattr(arg, "field")
|
||||
else isinstance(arg, GDALRaster)
|
||||
)
|
||||
return ST_Polygon(arg) if is_raster else arg
|
||||
|
||||
def get_geometry_converter(self, expression):
|
||||
read = wkb_r().read
|
||||
geom_class = expression.output_field.geom_class
|
||||
|
||||
def converter(value, expression, connection):
|
||||
if isinstance(value, str): # Coming from hex strings.
|
||||
value = value.encode("ascii")
|
||||
return None if value is None else GEOSGeometryBase(read(value), geom_class)
|
||||
|
||||
return converter
|
||||
|
||||
def get_area_att_for_field(self, field):
|
||||
return "sq_m"
|
@ -0,0 +1,152 @@
|
||||
import struct
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from .const import (
|
||||
BANDTYPE_FLAG_HASNODATA,
|
||||
BANDTYPE_PIXTYPE_MASK,
|
||||
GDAL_TO_POSTGIS,
|
||||
GDAL_TO_STRUCT,
|
||||
POSTGIS_HEADER_STRUCTURE,
|
||||
POSTGIS_TO_GDAL,
|
||||
STRUCT_SIZE,
|
||||
)
|
||||
|
||||
|
||||
def pack(structure, data):
|
||||
"""
|
||||
Pack data into hex string with little endian format.
|
||||
"""
|
||||
return struct.pack("<" + structure, *data)
|
||||
|
||||
|
||||
def unpack(structure, data):
|
||||
"""
|
||||
Unpack little endian hexlified binary string into a list.
|
||||
"""
|
||||
return struct.unpack("<" + structure, bytes.fromhex(data))
|
||||
|
||||
|
||||
def chunk(data, index):
|
||||
"""
|
||||
Split a string into two parts at the input index.
|
||||
"""
|
||||
return data[:index], data[index:]
|
||||
|
||||
|
||||
def from_pgraster(data):
|
||||
"""
|
||||
Convert a PostGIS HEX String into a dictionary.
|
||||
"""
|
||||
if data is None:
|
||||
return
|
||||
|
||||
# Split raster header from data
|
||||
header, data = chunk(data, 122)
|
||||
header = unpack(POSTGIS_HEADER_STRUCTURE, header)
|
||||
|
||||
# Parse band data
|
||||
bands = []
|
||||
pixeltypes = []
|
||||
while data:
|
||||
# Get pixel type for this band
|
||||
pixeltype_with_flags, data = chunk(data, 2)
|
||||
pixeltype_with_flags = unpack("B", pixeltype_with_flags)[0]
|
||||
pixeltype = pixeltype_with_flags & BANDTYPE_PIXTYPE_MASK
|
||||
|
||||
# Convert datatype from PostGIS to GDAL & get pack type and size
|
||||
pixeltype = POSTGIS_TO_GDAL[pixeltype]
|
||||
pack_type = GDAL_TO_STRUCT[pixeltype]
|
||||
pack_size = 2 * STRUCT_SIZE[pack_type]
|
||||
|
||||
# Parse band nodata value. The nodata value is part of the
|
||||
# PGRaster string even if the nodata flag is True, so it always
|
||||
# has to be chunked off the data string.
|
||||
nodata, data = chunk(data, pack_size)
|
||||
nodata = unpack(pack_type, nodata)[0]
|
||||
|
||||
# Chunk and unpack band data (pack size times nr of pixels)
|
||||
band, data = chunk(data, pack_size * header[10] * header[11])
|
||||
band_result = {"data": bytes.fromhex(band)}
|
||||
|
||||
# Set the nodata value if the nodata flag is set.
|
||||
if pixeltype_with_flags & BANDTYPE_FLAG_HASNODATA:
|
||||
band_result["nodata_value"] = nodata
|
||||
|
||||
# Append band data to band list
|
||||
bands.append(band_result)
|
||||
|
||||
# Store pixeltype of this band in pixeltypes array
|
||||
pixeltypes.append(pixeltype)
|
||||
|
||||
# Check that all bands have the same pixeltype.
|
||||
# This is required by GDAL. PostGIS rasters could have different pixeltypes
|
||||
# for bands of the same raster.
|
||||
if len(set(pixeltypes)) != 1:
|
||||
raise ValidationError("Band pixeltypes are not all equal.")
|
||||
|
||||
return {
|
||||
"srid": int(header[9]),
|
||||
"width": header[10],
|
||||
"height": header[11],
|
||||
"datatype": pixeltypes[0],
|
||||
"origin": (header[5], header[6]),
|
||||
"scale": (header[3], header[4]),
|
||||
"skew": (header[7], header[8]),
|
||||
"bands": bands,
|
||||
}
|
||||
|
||||
|
||||
def to_pgraster(rast):
|
||||
"""
|
||||
Convert a GDALRaster into PostGIS Raster format.
|
||||
"""
|
||||
# Prepare the raster header data as a tuple. The first two numbers are
|
||||
# the endianness and the PostGIS Raster Version, both are fixed by
|
||||
# PostGIS at the moment.
|
||||
rasterheader = (
|
||||
1,
|
||||
0,
|
||||
len(rast.bands),
|
||||
rast.scale.x,
|
||||
rast.scale.y,
|
||||
rast.origin.x,
|
||||
rast.origin.y,
|
||||
rast.skew.x,
|
||||
rast.skew.y,
|
||||
rast.srs.srid,
|
||||
rast.width,
|
||||
rast.height,
|
||||
)
|
||||
|
||||
# Pack raster header.
|
||||
result = pack(POSTGIS_HEADER_STRUCTURE, rasterheader)
|
||||
|
||||
for band in rast.bands:
|
||||
# The PostGIS raster band header has exactly two elements, a 8BUI byte
|
||||
# and the nodata value.
|
||||
#
|
||||
# The 8BUI stores both the PostGIS pixel data type and a nodata flag.
|
||||
# It is composed as the datatype with BANDTYPE_FLAG_HASNODATA (1 << 6)
|
||||
# for existing nodata values:
|
||||
# 8BUI_VALUE = PG_PIXEL_TYPE (0-11) | BANDTYPE_FLAG_HASNODATA
|
||||
#
|
||||
# For example, if the byte value is 71, then the datatype is
|
||||
# 71 & ~BANDTYPE_FLAG_HASNODATA = 7 (32BSI)
|
||||
# and the nodata value is True.
|
||||
structure = "B" + GDAL_TO_STRUCT[band.datatype()]
|
||||
|
||||
# Get band pixel type in PostGIS notation
|
||||
pixeltype = GDAL_TO_POSTGIS[band.datatype()]
|
||||
|
||||
# Set the nodata flag
|
||||
if band.nodata_value is not None:
|
||||
pixeltype |= BANDTYPE_FLAG_HASNODATA
|
||||
|
||||
# Pack band header
|
||||
bandheader = pack(structure, (pixeltype, band.nodata_value or 0))
|
||||
|
||||
# Add packed header and band data to result
|
||||
result += bandheader + band.data(as_memoryview=True)
|
||||
|
||||
return result
|
@ -0,0 +1,81 @@
|
||||
from django.db.backends.postgresql.schema import DatabaseSchemaEditor
|
||||
from django.db.models.expressions import Col, Func
|
||||
|
||||
|
||||
class PostGISSchemaEditor(DatabaseSchemaEditor):
|
||||
geom_index_type = "GIST"
|
||||
geom_index_ops_nd = "GIST_GEOMETRY_OPS_ND"
|
||||
rast_index_template = "ST_ConvexHull(%(expressions)s)"
|
||||
|
||||
sql_alter_column_to_3d = (
|
||||
"ALTER COLUMN %(column)s TYPE %(type)s USING ST_Force3D(%(column)s)::%(type)s"
|
||||
)
|
||||
sql_alter_column_to_2d = (
|
||||
"ALTER COLUMN %(column)s TYPE %(type)s USING ST_Force2D(%(column)s)::%(type)s"
|
||||
)
|
||||
|
||||
def geo_quote_name(self, name):
|
||||
return self.connection.ops.geo_quote_name(name)
|
||||
|
||||
def _field_should_be_indexed(self, model, field):
|
||||
if getattr(field, "spatial_index", False):
|
||||
return True
|
||||
return super()._field_should_be_indexed(model, field)
|
||||
|
||||
def _create_index_sql(self, model, *, fields=None, **kwargs):
|
||||
if fields is None or len(fields) != 1 or not hasattr(fields[0], "geodetic"):
|
||||
return super()._create_index_sql(model, fields=fields, **kwargs)
|
||||
|
||||
field = fields[0]
|
||||
expressions = None
|
||||
opclasses = None
|
||||
if field.geom_type == "RASTER":
|
||||
# For raster fields, wrap index creation SQL statement with ST_ConvexHull.
|
||||
# Indexes on raster columns are based on the convex hull of the raster.
|
||||
expressions = Func(Col(None, field), template=self.rast_index_template)
|
||||
fields = None
|
||||
elif field.dim > 2 and not field.geography:
|
||||
# Use "nd" ops which are fast on multidimensional cases
|
||||
opclasses = [self.geom_index_ops_nd]
|
||||
name = kwargs.get("name")
|
||||
if not name:
|
||||
name = self._create_index_name(model._meta.db_table, [field.column], "_id")
|
||||
|
||||
return super()._create_index_sql(
|
||||
model,
|
||||
fields=fields,
|
||||
name=name,
|
||||
using=" USING %s" % self.geom_index_type,
|
||||
opclasses=opclasses,
|
||||
expressions=expressions,
|
||||
)
|
||||
|
||||
def _alter_column_type_sql(
|
||||
self, table, old_field, new_field, new_type, old_collation, new_collation
|
||||
):
|
||||
"""
|
||||
Special case when dimension changed.
|
||||
"""
|
||||
if not hasattr(old_field, "dim") or not hasattr(new_field, "dim"):
|
||||
return super()._alter_column_type_sql(
|
||||
table, old_field, new_field, new_type, old_collation, new_collation
|
||||
)
|
||||
|
||||
if old_field.dim == 2 and new_field.dim == 3:
|
||||
sql_alter = self.sql_alter_column_to_3d
|
||||
elif old_field.dim == 3 and new_field.dim == 2:
|
||||
sql_alter = self.sql_alter_column_to_2d
|
||||
else:
|
||||
sql_alter = self.sql_alter_column_type
|
||||
return (
|
||||
(
|
||||
sql_alter
|
||||
% {
|
||||
"column": self.quote_name(new_field.column),
|
||||
"type": new_type,
|
||||
"collation": "",
|
||||
},
|
||||
[],
|
||||
),
|
||||
[],
|
||||
)
|
@ -0,0 +1,10 @@
|
||||
from django.contrib.gis.db.backends.base.adapter import WKTAdapter
|
||||
from django.db.backends.sqlite3.base import Database
|
||||
|
||||
|
||||
class SpatiaLiteAdapter(WKTAdapter):
|
||||
"SQLite adapter for geometry objects."
|
||||
|
||||
def __conform__(self, protocol):
|
||||
if protocol is Database.PrepareProtocol:
|
||||
return str(self)
|
@ -0,0 +1,79 @@
|
||||
from ctypes.util import find_library
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db.backends.sqlite3.base import DatabaseWrapper as SQLiteDatabaseWrapper
|
||||
|
||||
from .client import SpatiaLiteClient
|
||||
from .features import DatabaseFeatures
|
||||
from .introspection import SpatiaLiteIntrospection
|
||||
from .operations import SpatiaLiteOperations
|
||||
from .schema import SpatialiteSchemaEditor
|
||||
|
||||
|
||||
class DatabaseWrapper(SQLiteDatabaseWrapper):
|
||||
SchemaEditorClass = SpatialiteSchemaEditor
|
||||
# Classes instantiated in __init__().
|
||||
client_class = SpatiaLiteClient
|
||||
features_class = DatabaseFeatures
|
||||
introspection_class = SpatiaLiteIntrospection
|
||||
ops_class = SpatiaLiteOperations
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Trying to find the location of the SpatiaLite library.
|
||||
# Here we are figuring out the path to the SpatiaLite library
|
||||
# (`libspatialite`). If it's not in the system library path (e.g., it
|
||||
# cannot be found by `ctypes.util.find_library`), then it may be set
|
||||
# manually in the settings via the `SPATIALITE_LIBRARY_PATH` setting.
|
||||
self.lib_spatialite_paths = [
|
||||
name
|
||||
for name in [
|
||||
getattr(settings, "SPATIALITE_LIBRARY_PATH", None),
|
||||
"mod_spatialite.so",
|
||||
"mod_spatialite",
|
||||
find_library("spatialite"),
|
||||
]
|
||||
if name is not None
|
||||
]
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_new_connection(self, conn_params):
|
||||
conn = super().get_new_connection(conn_params)
|
||||
# Enabling extension loading on the SQLite connection.
|
||||
try:
|
||||
conn.enable_load_extension(True)
|
||||
except AttributeError:
|
||||
raise ImproperlyConfigured(
|
||||
"SpatiaLite requires SQLite to be configured to allow "
|
||||
"extension loading."
|
||||
)
|
||||
# Load the SpatiaLite library extension on the connection.
|
||||
for path in self.lib_spatialite_paths:
|
||||
try:
|
||||
conn.load_extension(path)
|
||||
except Exception:
|
||||
if getattr(settings, "SPATIALITE_LIBRARY_PATH", None):
|
||||
raise ImproperlyConfigured(
|
||||
"Unable to load the SpatiaLite library extension "
|
||||
"as specified in your SPATIALITE_LIBRARY_PATH setting."
|
||||
)
|
||||
continue
|
||||
else:
|
||||
break
|
||||
else:
|
||||
raise ImproperlyConfigured(
|
||||
"Unable to load the SpatiaLite library extension. "
|
||||
"Library names tried: %s" % ", ".join(self.lib_spatialite_paths)
|
||||
)
|
||||
return conn
|
||||
|
||||
def prepare_database(self):
|
||||
super().prepare_database()
|
||||
# Check if spatial metadata have been initialized in the database
|
||||
with self.cursor() as cursor:
|
||||
cursor.execute("PRAGMA table_info(geometry_columns);")
|
||||
if cursor.fetchall() == []:
|
||||
if self.ops.spatial_version < (5,):
|
||||
cursor.execute("SELECT InitSpatialMetaData(1)")
|
||||
else:
|
||||
cursor.execute("SELECT InitSpatialMetaDataFull(1)")
|
@ -0,0 +1,5 @@
|
||||
from django.db.backends.sqlite3.client import DatabaseClient
|
||||
|
||||
|
||||
class SpatiaLiteClient(DatabaseClient):
|
||||
executable_name = "spatialite"
|
@ -0,0 +1,26 @@
|
||||
from django.contrib.gis.db.backends.base.features import BaseSpatialFeatures
|
||||
from django.db.backends.sqlite3.features import (
|
||||
DatabaseFeatures as SQLiteDatabaseFeatures,
|
||||
)
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class DatabaseFeatures(BaseSpatialFeatures, SQLiteDatabaseFeatures):
|
||||
can_alter_geometry_field = False # Not implemented
|
||||
supports_3d_storage = True
|
||||
|
||||
@cached_property
|
||||
def supports_area_geodetic(self):
|
||||
return bool(self.connection.ops.geom_lib_version())
|
||||
|
||||
@cached_property
|
||||
def django_test_skips(self):
|
||||
skips = super().django_test_skips
|
||||
skips.update(
|
||||
{
|
||||
"SpatiaLite doesn't support distance lookups with Distance objects.": {
|
||||
"gis_tests.geogapp.tests.GeographyTest.test02_distance_lookup",
|
||||
},
|
||||
}
|
||||
)
|
||||
return skips
|
@ -0,0 +1,82 @@
|
||||
from django.contrib.gis.gdal import OGRGeomType
|
||||
from django.db.backends.sqlite3.introspection import (
|
||||
DatabaseIntrospection,
|
||||
FlexibleFieldLookupDict,
|
||||
)
|
||||
|
||||
|
||||
class GeoFlexibleFieldLookupDict(FlexibleFieldLookupDict):
|
||||
"""
|
||||
Subclass that includes updates the `base_data_types_reverse` dict
|
||||
for geometry field types.
|
||||
"""
|
||||
|
||||
base_data_types_reverse = {
|
||||
**FlexibleFieldLookupDict.base_data_types_reverse,
|
||||
"point": "GeometryField",
|
||||
"linestring": "GeometryField",
|
||||
"polygon": "GeometryField",
|
||||
"multipoint": "GeometryField",
|
||||
"multilinestring": "GeometryField",
|
||||
"multipolygon": "GeometryField",
|
||||
"geometrycollection": "GeometryField",
|
||||
}
|
||||
|
||||
|
||||
class SpatiaLiteIntrospection(DatabaseIntrospection):
|
||||
data_types_reverse = GeoFlexibleFieldLookupDict()
|
||||
|
||||
def get_geometry_type(self, table_name, description):
|
||||
with self.connection.cursor() as cursor:
|
||||
# Querying the `geometry_columns` table to get additional metadata.
|
||||
cursor.execute(
|
||||
"SELECT coord_dimension, srid, geometry_type "
|
||||
"FROM geometry_columns "
|
||||
"WHERE f_table_name=%s AND f_geometry_column=%s",
|
||||
(table_name, description.name),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
raise Exception(
|
||||
'Could not find a geometry column for "%s"."%s"'
|
||||
% (table_name, description.name)
|
||||
)
|
||||
|
||||
# OGRGeomType does not require GDAL and makes it easy to convert
|
||||
# from OGC geom type name to Django field.
|
||||
ogr_type = row[2]
|
||||
if isinstance(ogr_type, int) and ogr_type > 1000:
|
||||
# SpatiaLite uses SFSQL 1.2 offsets 1000 (Z), 2000 (M), and
|
||||
# 3000 (ZM) to indicate the presence of higher dimensional
|
||||
# coordinates (M not yet supported by Django).
|
||||
ogr_type = ogr_type % 1000 + OGRGeomType.wkb25bit
|
||||
field_type = OGRGeomType(ogr_type).django
|
||||
|
||||
# Getting any GeometryField keyword arguments that are not the default.
|
||||
dim = row[0]
|
||||
srid = row[1]
|
||||
field_params = {}
|
||||
if srid != 4326:
|
||||
field_params["srid"] = srid
|
||||
if (isinstance(dim, str) and "Z" in dim) or dim == 3:
|
||||
field_params["dim"] = 3
|
||||
return field_type, field_params
|
||||
|
||||
def get_constraints(self, cursor, table_name):
|
||||
constraints = super().get_constraints(cursor, table_name)
|
||||
cursor.execute(
|
||||
"SELECT f_geometry_column "
|
||||
"FROM geometry_columns "
|
||||
"WHERE f_table_name=%s AND spatial_index_enabled=1",
|
||||
(table_name,),
|
||||
)
|
||||
for row in cursor.fetchall():
|
||||
constraints["%s__spatial__index" % row[0]] = {
|
||||
"columns": [row[0]],
|
||||
"primary_key": False,
|
||||
"unique": False,
|
||||
"foreign_key": None,
|
||||
"check": False,
|
||||
"index": True,
|
||||
}
|
||||
return constraints
|
@ -0,0 +1,70 @@
|
||||
"""
|
||||
The GeometryColumns and SpatialRefSys models for the SpatiaLite backend.
|
||||
"""
|
||||
from django.contrib.gis.db.backends.base.models import SpatialRefSysMixin
|
||||
from django.db import models
|
||||
|
||||
|
||||
class SpatialiteGeometryColumns(models.Model):
|
||||
"""
|
||||
The 'geometry_columns' table from SpatiaLite.
|
||||
"""
|
||||
|
||||
f_table_name = models.CharField(max_length=256)
|
||||
f_geometry_column = models.CharField(max_length=256)
|
||||
coord_dimension = models.IntegerField()
|
||||
srid = models.IntegerField(primary_key=True)
|
||||
spatial_index_enabled = models.IntegerField()
|
||||
type = models.IntegerField(db_column="geometry_type")
|
||||
|
||||
class Meta:
|
||||
app_label = "gis"
|
||||
db_table = "geometry_columns"
|
||||
managed = False
|
||||
|
||||
def __str__(self):
|
||||
return "%s.%s - %dD %s field (SRID: %d)" % (
|
||||
self.f_table_name,
|
||||
self.f_geometry_column,
|
||||
self.coord_dimension,
|
||||
self.type,
|
||||
self.srid,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def table_name_col(cls):
|
||||
"""
|
||||
Return the name of the metadata column used to store the feature table
|
||||
name.
|
||||
"""
|
||||
return "f_table_name"
|
||||
|
||||
@classmethod
|
||||
def geom_col_name(cls):
|
||||
"""
|
||||
Return the name of the metadata column used to store the feature
|
||||
geometry column.
|
||||
"""
|
||||
return "f_geometry_column"
|
||||
|
||||
|
||||
class SpatialiteSpatialRefSys(models.Model, SpatialRefSysMixin):
|
||||
"""
|
||||
The 'spatial_ref_sys' table from SpatiaLite.
|
||||
"""
|
||||
|
||||
srid = models.IntegerField(primary_key=True)
|
||||
auth_name = models.CharField(max_length=256)
|
||||
auth_srid = models.IntegerField()
|
||||
ref_sys_name = models.CharField(max_length=256)
|
||||
proj4text = models.CharField(max_length=2048)
|
||||
srtext = models.CharField(max_length=2048)
|
||||
|
||||
class Meta:
|
||||
app_label = "gis"
|
||||
db_table = "spatial_ref_sys"
|
||||
managed = False
|
||||
|
||||
@property
|
||||
def wkt(self):
|
||||
return self.srtext
|
@ -0,0 +1,227 @@
|
||||
"""
|
||||
SQL functions reference lists:
|
||||
https://www.gaia-gis.it/gaia-sins/spatialite-sql-4.3.0.html
|
||||
"""
|
||||
from django.contrib.gis.db import models
|
||||
from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations
|
||||
from django.contrib.gis.db.backends.spatialite.adapter import SpatiaLiteAdapter
|
||||
from django.contrib.gis.db.backends.utils import SpatialOperator
|
||||
from django.contrib.gis.geos.geometry import GEOSGeometry, GEOSGeometryBase
|
||||
from django.contrib.gis.geos.prototypes.io import wkb_r
|
||||
from django.contrib.gis.measure import Distance
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db.backends.sqlite3.operations import DatabaseOperations
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.version import get_version_tuple
|
||||
|
||||
|
||||
class SpatialiteNullCheckOperator(SpatialOperator):
|
||||
def as_sql(self, connection, lookup, template_params, sql_params):
|
||||
sql, params = super().as_sql(connection, lookup, template_params, sql_params)
|
||||
return "%s > 0" % sql, params
|
||||
|
||||
|
||||
class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations):
|
||||
name = "spatialite"
|
||||
spatialite = True
|
||||
|
||||
Adapter = SpatiaLiteAdapter
|
||||
|
||||
collect = "Collect"
|
||||
extent = "Extent"
|
||||
makeline = "MakeLine"
|
||||
unionagg = "GUnion"
|
||||
|
||||
from_text = "GeomFromText"
|
||||
|
||||
gis_operators = {
|
||||
# Binary predicates
|
||||
"equals": SpatialiteNullCheckOperator(func="Equals"),
|
||||
"disjoint": SpatialiteNullCheckOperator(func="Disjoint"),
|
||||
"touches": SpatialiteNullCheckOperator(func="Touches"),
|
||||
"crosses": SpatialiteNullCheckOperator(func="Crosses"),
|
||||
"within": SpatialiteNullCheckOperator(func="Within"),
|
||||
"overlaps": SpatialiteNullCheckOperator(func="Overlaps"),
|
||||
"contains": SpatialiteNullCheckOperator(func="Contains"),
|
||||
"intersects": SpatialiteNullCheckOperator(func="Intersects"),
|
||||
"relate": SpatialiteNullCheckOperator(func="Relate"),
|
||||
"coveredby": SpatialiteNullCheckOperator(func="CoveredBy"),
|
||||
"covers": SpatialiteNullCheckOperator(func="Covers"),
|
||||
# Returns true if B's bounding box completely contains A's bounding box.
|
||||
"contained": SpatialOperator(func="MbrWithin"),
|
||||
# Returns true if A's bounding box completely contains B's bounding box.
|
||||
"bbcontains": SpatialOperator(func="MbrContains"),
|
||||
# Returns true if A's bounding box overlaps B's bounding box.
|
||||
"bboverlaps": SpatialOperator(func="MbrOverlaps"),
|
||||
# These are implemented here as synonyms for Equals
|
||||
"same_as": SpatialiteNullCheckOperator(func="Equals"),
|
||||
"exact": SpatialiteNullCheckOperator(func="Equals"),
|
||||
# Distance predicates
|
||||
"dwithin": SpatialOperator(func="PtDistWithin"),
|
||||
}
|
||||
|
||||
disallowed_aggregates = (models.Extent3D,)
|
||||
|
||||
select = "CAST (AsEWKB(%s) AS BLOB)"
|
||||
|
||||
function_names = {
|
||||
"AsWKB": "St_AsBinary",
|
||||
"ForcePolygonCW": "ST_ForceLHR",
|
||||
"FromWKB": "ST_GeomFromWKB",
|
||||
"FromWKT": "ST_GeomFromText",
|
||||
"Length": "ST_Length",
|
||||
"LineLocatePoint": "ST_Line_Locate_Point",
|
||||
"NumPoints": "ST_NPoints",
|
||||
"Reverse": "ST_Reverse",
|
||||
"Scale": "ScaleCoords",
|
||||
"Translate": "ST_Translate",
|
||||
"Union": "ST_Union",
|
||||
}
|
||||
|
||||
@cached_property
|
||||
def unsupported_functions(self):
|
||||
unsupported = {"BoundingCircle", "GeometryDistance", "IsEmpty", "MemSize"}
|
||||
if not self.geom_lib_version():
|
||||
unsupported |= {"Azimuth", "GeoHash", "MakeValid"}
|
||||
return unsupported
|
||||
|
||||
@cached_property
|
||||
def spatial_version(self):
|
||||
"""Determine the version of the SpatiaLite library."""
|
||||
try:
|
||||
version = self.spatialite_version_tuple()[1:]
|
||||
except Exception as exc:
|
||||
raise ImproperlyConfigured(
|
||||
'Cannot determine the SpatiaLite version for the "%s" database. '
|
||||
"Was the SpatiaLite initialization SQL loaded on this database?"
|
||||
% (self.connection.settings_dict["NAME"],)
|
||||
) from exc
|
||||
if version < (4, 3, 0):
|
||||
raise ImproperlyConfigured("GeoDjango supports SpatiaLite 4.3.0 and above.")
|
||||
return version
|
||||
|
||||
def convert_extent(self, box):
|
||||
"""
|
||||
Convert the polygon data received from SpatiaLite to min/max values.
|
||||
"""
|
||||
if box is None:
|
||||
return None
|
||||
shell = GEOSGeometry(box).shell
|
||||
xmin, ymin = shell[0][:2]
|
||||
xmax, ymax = shell[2][:2]
|
||||
return (xmin, ymin, xmax, ymax)
|
||||
|
||||
def geo_db_type(self, f):
|
||||
"""
|
||||
Return None because geometry columns are added via the
|
||||
`AddGeometryColumn` stored procedure on SpatiaLite.
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_distance(self, f, value, lookup_type):
|
||||
"""
|
||||
Return the distance parameters for the given geometry field,
|
||||
lookup value, and lookup type.
|
||||
"""
|
||||
if not value:
|
||||
return []
|
||||
value = value[0]
|
||||
if isinstance(value, Distance):
|
||||
if f.geodetic(self.connection):
|
||||
if lookup_type == "dwithin":
|
||||
raise ValueError(
|
||||
"Only numeric values of degree units are allowed on "
|
||||
"geographic DWithin queries."
|
||||
)
|
||||
dist_param = value.m
|
||||
else:
|
||||
dist_param = getattr(
|
||||
value, Distance.unit_attname(f.units_name(self.connection))
|
||||
)
|
||||
else:
|
||||
dist_param = value
|
||||
return [dist_param]
|
||||
|
||||
def _get_spatialite_func(self, func):
|
||||
"""
|
||||
Helper routine for calling SpatiaLite functions and returning
|
||||
their result.
|
||||
Any error occurring in this method should be handled by the caller.
|
||||
"""
|
||||
cursor = self.connection._cursor()
|
||||
try:
|
||||
cursor.execute("SELECT %s" % func)
|
||||
row = cursor.fetchone()
|
||||
finally:
|
||||
cursor.close()
|
||||
return row[0]
|
||||
|
||||
def geos_version(self):
|
||||
"Return the version of GEOS used by SpatiaLite as a string."
|
||||
return self._get_spatialite_func("geos_version()")
|
||||
|
||||
def proj_version(self):
|
||||
"""Return the version of the PROJ library used by SpatiaLite."""
|
||||
return self._get_spatialite_func("proj4_version()")
|
||||
|
||||
def lwgeom_version(self):
|
||||
"""Return the version of LWGEOM library used by SpatiaLite."""
|
||||
return self._get_spatialite_func("lwgeom_version()")
|
||||
|
||||
def rttopo_version(self):
|
||||
"""Return the version of RTTOPO library used by SpatiaLite."""
|
||||
return self._get_spatialite_func("rttopo_version()")
|
||||
|
||||
def geom_lib_version(self):
|
||||
"""
|
||||
Return the version of the version-dependant geom library used by
|
||||
SpatiaLite.
|
||||
"""
|
||||
if self.spatial_version >= (5,):
|
||||
return self.rttopo_version()
|
||||
else:
|
||||
return self.lwgeom_version()
|
||||
|
||||
def spatialite_version(self):
|
||||
"Return the SpatiaLite library version as a string."
|
||||
return self._get_spatialite_func("spatialite_version()")
|
||||
|
||||
def spatialite_version_tuple(self):
|
||||
"""
|
||||
Return the SpatiaLite version as a tuple (version string, major,
|
||||
minor, subminor).
|
||||
"""
|
||||
version = self.spatialite_version()
|
||||
return (version,) + get_version_tuple(version)
|
||||
|
||||
def spatial_aggregate_name(self, agg_name):
|
||||
"""
|
||||
Return the spatial aggregate SQL template and function for the
|
||||
given Aggregate instance.
|
||||
"""
|
||||
agg_name = "unionagg" if agg_name.lower() == "union" else agg_name.lower()
|
||||
return getattr(self, agg_name)
|
||||
|
||||
# Routines for getting the OGC-compliant models.
|
||||
def geometry_columns(self):
|
||||
from django.contrib.gis.db.backends.spatialite.models import (
|
||||
SpatialiteGeometryColumns,
|
||||
)
|
||||
|
||||
return SpatialiteGeometryColumns
|
||||
|
||||
def spatial_ref_sys(self):
|
||||
from django.contrib.gis.db.backends.spatialite.models import (
|
||||
SpatialiteSpatialRefSys,
|
||||
)
|
||||
|
||||
return SpatialiteSpatialRefSys
|
||||
|
||||
def get_geometry_converter(self, expression):
|
||||
geom_class = expression.output_field.geom_class
|
||||
read = wkb_r().read
|
||||
|
||||
def converter(value, expression, connection):
|
||||
return None if value is None else GEOSGeometryBase(read(value), geom_class)
|
||||
|
||||
return converter
|
@ -0,0 +1,191 @@
|
||||
from django.db import DatabaseError
|
||||
from django.db.backends.sqlite3.schema import DatabaseSchemaEditor
|
||||
|
||||
|
||||
class SpatialiteSchemaEditor(DatabaseSchemaEditor):
|
||||
sql_add_geometry_column = (
|
||||
"SELECT AddGeometryColumn(%(table)s, %(column)s, %(srid)s, "
|
||||
"%(geom_type)s, %(dim)s, %(null)s)"
|
||||
)
|
||||
sql_add_spatial_index = "SELECT CreateSpatialIndex(%(table)s, %(column)s)"
|
||||
sql_drop_spatial_index = "DROP TABLE idx_%(table)s_%(column)s"
|
||||
sql_recover_geometry_metadata = (
|
||||
"SELECT RecoverGeometryColumn(%(table)s, %(column)s, %(srid)s, "
|
||||
"%(geom_type)s, %(dim)s)"
|
||||
)
|
||||
sql_remove_geometry_metadata = "SELECT DiscardGeometryColumn(%(table)s, %(column)s)"
|
||||
sql_discard_geometry_columns = (
|
||||
"DELETE FROM %(geom_table)s WHERE f_table_name = %(table)s"
|
||||
)
|
||||
sql_update_geometry_columns = (
|
||||
"UPDATE %(geom_table)s SET f_table_name = %(new_table)s "
|
||||
"WHERE f_table_name = %(old_table)s"
|
||||
)
|
||||
|
||||
geometry_tables = [
|
||||
"geometry_columns",
|
||||
"geometry_columns_auth",
|
||||
"geometry_columns_time",
|
||||
"geometry_columns_statistics",
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.geometry_sql = []
|
||||
|
||||
def geo_quote_name(self, name):
|
||||
return self.connection.ops.geo_quote_name(name)
|
||||
|
||||
def column_sql(self, model, field, include_default=False):
|
||||
from django.contrib.gis.db.models import GeometryField
|
||||
|
||||
if not isinstance(field, GeometryField):
|
||||
return super().column_sql(model, field, include_default)
|
||||
|
||||
# Geometry columns are created by the `AddGeometryColumn` function
|
||||
self.geometry_sql.append(
|
||||
self.sql_add_geometry_column
|
||||
% {
|
||||
"table": self.geo_quote_name(model._meta.db_table),
|
||||
"column": self.geo_quote_name(field.column),
|
||||
"srid": field.srid,
|
||||
"geom_type": self.geo_quote_name(field.geom_type),
|
||||
"dim": field.dim,
|
||||
"null": int(not field.null),
|
||||
}
|
||||
)
|
||||
|
||||
if field.spatial_index:
|
||||
self.geometry_sql.append(
|
||||
self.sql_add_spatial_index
|
||||
% {
|
||||
"table": self.quote_name(model._meta.db_table),
|
||||
"column": self.quote_name(field.column),
|
||||
}
|
||||
)
|
||||
return None, None
|
||||
|
||||
def remove_geometry_metadata(self, model, field):
|
||||
self.execute(
|
||||
self.sql_remove_geometry_metadata
|
||||
% {
|
||||
"table": self.quote_name(model._meta.db_table),
|
||||
"column": self.quote_name(field.column),
|
||||
}
|
||||
)
|
||||
self.execute(
|
||||
self.sql_drop_spatial_index
|
||||
% {
|
||||
"table": model._meta.db_table,
|
||||
"column": field.column,
|
||||
}
|
||||
)
|
||||
|
||||
def create_model(self, model):
|
||||
super().create_model(model)
|
||||
# Create geometry columns
|
||||
for sql in self.geometry_sql:
|
||||
self.execute(sql)
|
||||
self.geometry_sql = []
|
||||
|
||||
def delete_model(self, model, **kwargs):
|
||||
from django.contrib.gis.db.models import GeometryField
|
||||
|
||||
# Drop spatial metadata (dropping the table does not automatically remove them)
|
||||
for field in model._meta.local_fields:
|
||||
if isinstance(field, GeometryField):
|
||||
self.remove_geometry_metadata(model, field)
|
||||
# Make sure all geom stuff is gone
|
||||
for geom_table in self.geometry_tables:
|
||||
try:
|
||||
self.execute(
|
||||
self.sql_discard_geometry_columns
|
||||
% {
|
||||
"geom_table": geom_table,
|
||||
"table": self.quote_name(model._meta.db_table),
|
||||
}
|
||||
)
|
||||
except DatabaseError:
|
||||
pass
|
||||
super().delete_model(model, **kwargs)
|
||||
|
||||
def add_field(self, model, field):
|
||||
from django.contrib.gis.db.models import GeometryField
|
||||
|
||||
if isinstance(field, GeometryField):
|
||||
# Populate self.geometry_sql
|
||||
self.column_sql(model, field)
|
||||
for sql in self.geometry_sql:
|
||||
self.execute(sql)
|
||||
self.geometry_sql = []
|
||||
else:
|
||||
super().add_field(model, field)
|
||||
|
||||
def remove_field(self, model, field):
|
||||
from django.contrib.gis.db.models import GeometryField
|
||||
|
||||
# NOTE: If the field is a geometry field, the table is just recreated,
|
||||
# the parent's remove_field can't be used cause it will skip the
|
||||
# recreation if the field does not have a database type. Geometry fields
|
||||
# do not have a db type cause they are added and removed via stored
|
||||
# procedures.
|
||||
if isinstance(field, GeometryField):
|
||||
self._remake_table(model, delete_field=field)
|
||||
else:
|
||||
super().remove_field(model, field)
|
||||
|
||||
def alter_db_table(
|
||||
self, model, old_db_table, new_db_table, disable_constraints=True
|
||||
):
|
||||
from django.contrib.gis.db.models import GeometryField
|
||||
|
||||
# Remove geometry-ness from temp table
|
||||
for field in model._meta.local_fields:
|
||||
if isinstance(field, GeometryField):
|
||||
self.execute(
|
||||
self.sql_remove_geometry_metadata
|
||||
% {
|
||||
"table": self.quote_name(old_db_table),
|
||||
"column": self.quote_name(field.column),
|
||||
}
|
||||
)
|
||||
# Alter table
|
||||
super().alter_db_table(model, old_db_table, new_db_table, disable_constraints)
|
||||
# Repoint any straggler names
|
||||
for geom_table in self.geometry_tables:
|
||||
try:
|
||||
self.execute(
|
||||
self.sql_update_geometry_columns
|
||||
% {
|
||||
"geom_table": geom_table,
|
||||
"old_table": self.quote_name(old_db_table),
|
||||
"new_table": self.quote_name(new_db_table),
|
||||
}
|
||||
)
|
||||
except DatabaseError:
|
||||
pass
|
||||
# Re-add geometry-ness and rename spatial index tables
|
||||
for field in model._meta.local_fields:
|
||||
if isinstance(field, GeometryField):
|
||||
self.execute(
|
||||
self.sql_recover_geometry_metadata
|
||||
% {
|
||||
"table": self.geo_quote_name(new_db_table),
|
||||
"column": self.geo_quote_name(field.column),
|
||||
"srid": field.srid,
|
||||
"geom_type": self.geo_quote_name(field.geom_type),
|
||||
"dim": field.dim,
|
||||
}
|
||||
)
|
||||
if getattr(field, "spatial_index", False):
|
||||
self.execute(
|
||||
self.sql_rename_table
|
||||
% {
|
||||
"old_table": self.quote_name(
|
||||
"idx_%s_%s" % (old_db_table, field.column)
|
||||
),
|
||||
"new_table": self.quote_name(
|
||||
"idx_%s_%s" % (new_db_table, field.column)
|
||||
),
|
||||
}
|
||||
)
|
@ -0,0 +1,28 @@
|
||||
"""
|
||||
A collection of utility routines and classes used by the spatial
|
||||
backends.
|
||||
"""
|
||||
|
||||
|
||||
class SpatialOperator:
|
||||
"""
|
||||
Class encapsulating the behavior specific to a GIS operation (used by lookups).
|
||||
"""
|
||||
|
||||
sql_template = None
|
||||
|
||||
def __init__(self, op=None, func=None):
|
||||
self.op = op
|
||||
self.func = func
|
||||
|
||||
@property
|
||||
def default_template(self):
|
||||
if self.func:
|
||||
return "%(func)s(%(lhs)s, %(rhs)s)"
|
||||
else:
|
||||
return "%(lhs)s %(op)s %(rhs)s"
|
||||
|
||||
def as_sql(self, connection, lookup, template_params, sql_params):
|
||||
sql_template = self.sql_template or lookup.sql_template or self.default_template
|
||||
template_params.update({"op": self.op, "func": self.func})
|
||||
return sql_template % template_params, sql_params
|
@ -0,0 +1,30 @@
|
||||
from django.db.models import * # NOQA isort:skip
|
||||
from django.db.models import __all__ as models_all # isort:skip
|
||||
import django.contrib.gis.db.models.functions # NOQA
|
||||
import django.contrib.gis.db.models.lookups # NOQA
|
||||
from django.contrib.gis.db.models.aggregates import * # NOQA
|
||||
from django.contrib.gis.db.models.aggregates import __all__ as aggregates_all
|
||||
from django.contrib.gis.db.models.fields import (
|
||||
GeometryCollectionField,
|
||||
GeometryField,
|
||||
LineStringField,
|
||||
MultiLineStringField,
|
||||
MultiPointField,
|
||||
MultiPolygonField,
|
||||
PointField,
|
||||
PolygonField,
|
||||
RasterField,
|
||||
)
|
||||
|
||||
__all__ = models_all + aggregates_all
|
||||
__all__ += [
|
||||
"GeometryCollectionField",
|
||||
"GeometryField",
|
||||
"LineStringField",
|
||||
"MultiLineStringField",
|
||||
"MultiPointField",
|
||||
"MultiPolygonField",
|
||||
"PointField",
|
||||
"PolygonField",
|
||||
"RasterField",
|
||||
]
|
@ -0,0 +1,94 @@
|
||||
from django.contrib.gis.db.models.fields import (
|
||||
ExtentField,
|
||||
GeometryCollectionField,
|
||||
GeometryField,
|
||||
LineStringField,
|
||||
)
|
||||
from django.db.models import Aggregate, Value
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
__all__ = ["Collect", "Extent", "Extent3D", "MakeLine", "Union"]
|
||||
|
||||
|
||||
class GeoAggregate(Aggregate):
|
||||
function = None
|
||||
is_extent = False
|
||||
|
||||
@cached_property
|
||||
def output_field(self):
|
||||
return self.output_field_class(self.source_expressions[0].output_field.srid)
|
||||
|
||||
def as_sql(self, compiler, connection, function=None, **extra_context):
|
||||
# this will be called again in parent, but it's needed now - before
|
||||
# we get the spatial_aggregate_name
|
||||
connection.ops.check_expression_support(self)
|
||||
return super().as_sql(
|
||||
compiler,
|
||||
connection,
|
||||
function=function or connection.ops.spatial_aggregate_name(self.name),
|
||||
**extra_context,
|
||||
)
|
||||
|
||||
def as_oracle(self, compiler, connection, **extra_context):
|
||||
if not self.is_extent:
|
||||
tolerance = self.extra.get("tolerance") or getattr(self, "tolerance", 0.05)
|
||||
clone = self.copy()
|
||||
clone.set_source_expressions(
|
||||
[
|
||||
*self.get_source_expressions(),
|
||||
Value(tolerance),
|
||||
]
|
||||
)
|
||||
template = "%(function)s(SDOAGGRTYPE(%(expressions)s))"
|
||||
return clone.as_sql(
|
||||
compiler, connection, template=template, **extra_context
|
||||
)
|
||||
return self.as_sql(compiler, connection, **extra_context)
|
||||
|
||||
def resolve_expression(
|
||||
self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False
|
||||
):
|
||||
c = super().resolve_expression(query, allow_joins, reuse, summarize, for_save)
|
||||
for expr in c.get_source_expressions():
|
||||
if not hasattr(expr.field, "geom_type"):
|
||||
raise ValueError(
|
||||
"Geospatial aggregates only allowed on geometry fields."
|
||||
)
|
||||
return c
|
||||
|
||||
|
||||
class Collect(GeoAggregate):
|
||||
name = "Collect"
|
||||
output_field_class = GeometryCollectionField
|
||||
|
||||
|
||||
class Extent(GeoAggregate):
|
||||
name = "Extent"
|
||||
is_extent = "2D"
|
||||
|
||||
def __init__(self, expression, **extra):
|
||||
super().__init__(expression, output_field=ExtentField(), **extra)
|
||||
|
||||
def convert_value(self, value, expression, connection):
|
||||
return connection.ops.convert_extent(value)
|
||||
|
||||
|
||||
class Extent3D(GeoAggregate):
|
||||
name = "Extent3D"
|
||||
is_extent = "3D"
|
||||
|
||||
def __init__(self, expression, **extra):
|
||||
super().__init__(expression, output_field=ExtentField(), **extra)
|
||||
|
||||
def convert_value(self, value, expression, connection):
|
||||
return connection.ops.convert_extent3d(value)
|
||||
|
||||
|
||||
class MakeLine(GeoAggregate):
|
||||
name = "MakeLine"
|
||||
output_field_class = LineStringField
|
||||
|
||||
|
||||
class Union(GeoAggregate):
|
||||
name = "Union"
|
||||
output_field_class = GeometryField
|
@ -0,0 +1,436 @@
|
||||
from collections import defaultdict, namedtuple
|
||||
|
||||
from django.contrib.gis import forms, gdal
|
||||
from django.contrib.gis.db.models.proxy import SpatialProxy
|
||||
from django.contrib.gis.gdal.error import GDALException
|
||||
from django.contrib.gis.geos import (
|
||||
GeometryCollection,
|
||||
GEOSException,
|
||||
GEOSGeometry,
|
||||
LineString,
|
||||
MultiLineString,
|
||||
MultiPoint,
|
||||
MultiPolygon,
|
||||
Point,
|
||||
Polygon,
|
||||
)
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db.models import Field
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# Local cache of the spatial_ref_sys table, which holds SRID data for each
|
||||
# spatial database alias. This cache exists so that the database isn't queried
|
||||
# for SRID info each time a distance query is constructed.
|
||||
_srid_cache = defaultdict(dict)
|
||||
|
||||
|
||||
SRIDCacheEntry = namedtuple(
|
||||
"SRIDCacheEntry", ["units", "units_name", "spheroid", "geodetic"]
|
||||
)
|
||||
|
||||
|
||||
def get_srid_info(srid, connection):
|
||||
"""
|
||||
Return the units, unit name, and spheroid WKT associated with the
|
||||
given SRID from the `spatial_ref_sys` (or equivalent) spatial database
|
||||
table for the given database connection. These results are cached.
|
||||
"""
|
||||
from django.contrib.gis.gdal import SpatialReference
|
||||
|
||||
global _srid_cache
|
||||
|
||||
try:
|
||||
# The SpatialRefSys model for the spatial backend.
|
||||
SpatialRefSys = connection.ops.spatial_ref_sys()
|
||||
except NotImplementedError:
|
||||
SpatialRefSys = None
|
||||
|
||||
alias, get_srs = (
|
||||
(
|
||||
connection.alias,
|
||||
lambda srid: SpatialRefSys.objects.using(connection.alias)
|
||||
.get(srid=srid)
|
||||
.srs,
|
||||
)
|
||||
if SpatialRefSys
|
||||
else (None, SpatialReference)
|
||||
)
|
||||
if srid not in _srid_cache[alias]:
|
||||
srs = get_srs(srid)
|
||||
units, units_name = srs.units
|
||||
_srid_cache[alias][srid] = SRIDCacheEntry(
|
||||
units=units,
|
||||
units_name=units_name,
|
||||
spheroid='SPHEROID["%s",%s,%s]'
|
||||
% (srs["spheroid"], srs.semi_major, srs.inverse_flattening),
|
||||
geodetic=srs.geographic,
|
||||
)
|
||||
|
||||
return _srid_cache[alias][srid]
|
||||
|
||||
|
||||
class BaseSpatialField(Field):
|
||||
"""
|
||||
The Base GIS Field.
|
||||
|
||||
It's used as a base class for GeometryField and RasterField. Defines
|
||||
properties that are common to all GIS fields such as the characteristics
|
||||
of the spatial reference system of the field.
|
||||
"""
|
||||
|
||||
description = _("The base GIS field.")
|
||||
empty_strings_allowed = False
|
||||
|
||||
def __init__(self, verbose_name=None, srid=4326, spatial_index=True, **kwargs):
|
||||
"""
|
||||
The initialization function for base spatial fields. Takes the following
|
||||
as keyword arguments:
|
||||
|
||||
srid:
|
||||
The spatial reference system identifier, an OGC standard.
|
||||
Defaults to 4326 (WGS84).
|
||||
|
||||
spatial_index:
|
||||
Indicates whether to create a spatial index. Defaults to True.
|
||||
Set this instead of 'db_index' for geographic fields since index
|
||||
creation is different for geometry columns.
|
||||
"""
|
||||
|
||||
# Setting the index flag with the value of the `spatial_index` keyword.
|
||||
self.spatial_index = spatial_index
|
||||
|
||||
# Setting the SRID and getting the units. Unit information must be
|
||||
# easily available in the field instance for distance queries.
|
||||
self.srid = srid
|
||||
|
||||
# Setting the verbose_name keyword argument with the positional
|
||||
# first parameter, so this works like normal fields.
|
||||
kwargs["verbose_name"] = verbose_name
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
# Always include SRID for less fragility; include spatial index if it's
|
||||
# not the default value.
|
||||
kwargs["srid"] = self.srid
|
||||
if self.spatial_index is not True:
|
||||
kwargs["spatial_index"] = self.spatial_index
|
||||
return name, path, args, kwargs
|
||||
|
||||
def db_type(self, connection):
|
||||
return connection.ops.geo_db_type(self)
|
||||
|
||||
def spheroid(self, connection):
|
||||
return get_srid_info(self.srid, connection).spheroid
|
||||
|
||||
def units(self, connection):
|
||||
return get_srid_info(self.srid, connection).units
|
||||
|
||||
def units_name(self, connection):
|
||||
return get_srid_info(self.srid, connection).units_name
|
||||
|
||||
def geodetic(self, connection):
|
||||
"""
|
||||
Return true if this field's SRID corresponds with a coordinate
|
||||
system that uses non-projected units (e.g., latitude/longitude).
|
||||
"""
|
||||
return get_srid_info(self.srid, connection).geodetic
|
||||
|
||||
def get_placeholder(self, value, compiler, connection):
|
||||
"""
|
||||
Return the placeholder for the spatial column for the
|
||||
given value.
|
||||
"""
|
||||
return connection.ops.get_geom_placeholder(self, value, compiler)
|
||||
|
||||
def get_srid(self, obj):
|
||||
"""
|
||||
Return the default SRID for the given geometry or raster, taking into
|
||||
account the SRID set for the field. For example, if the input geometry
|
||||
or raster doesn't have an SRID, then the SRID of the field will be
|
||||
returned.
|
||||
"""
|
||||
srid = obj.srid # SRID of given geometry.
|
||||
if srid is None or self.srid == -1 or (srid == -1 and self.srid != -1):
|
||||
return self.srid
|
||||
else:
|
||||
return srid
|
||||
|
||||
def get_db_prep_value(self, value, connection, *args, **kwargs):
|
||||
if value is None:
|
||||
return None
|
||||
return connection.ops.Adapter(
|
||||
super().get_db_prep_value(value, connection, *args, **kwargs),
|
||||
**(
|
||||
{"geography": True}
|
||||
if self.geography and connection.features.supports_geography
|
||||
else {}
|
||||
),
|
||||
)
|
||||
|
||||
def get_raster_prep_value(self, value, is_candidate):
|
||||
"""
|
||||
Return a GDALRaster if conversion is successful, otherwise return None.
|
||||
"""
|
||||
if isinstance(value, gdal.GDALRaster):
|
||||
return value
|
||||
elif is_candidate:
|
||||
try:
|
||||
return gdal.GDALRaster(value)
|
||||
except GDALException:
|
||||
pass
|
||||
elif isinstance(value, dict):
|
||||
try:
|
||||
return gdal.GDALRaster(value)
|
||||
except GDALException:
|
||||
raise ValueError(
|
||||
"Couldn't create spatial object from lookup value '%s'." % value
|
||||
)
|
||||
|
||||
def get_prep_value(self, value):
|
||||
obj = super().get_prep_value(value)
|
||||
if obj is None:
|
||||
return None
|
||||
# When the input is not a geometry or raster, attempt to construct one
|
||||
# from the given string input.
|
||||
if isinstance(obj, GEOSGeometry):
|
||||
pass
|
||||
else:
|
||||
# Check if input is a candidate for conversion to raster or geometry.
|
||||
is_candidate = isinstance(obj, (bytes, str)) or hasattr(
|
||||
obj, "__geo_interface__"
|
||||
)
|
||||
# Try to convert the input to raster.
|
||||
raster = self.get_raster_prep_value(obj, is_candidate)
|
||||
|
||||
if raster:
|
||||
obj = raster
|
||||
elif is_candidate:
|
||||
try:
|
||||
obj = GEOSGeometry(obj)
|
||||
except (GEOSException, GDALException):
|
||||
raise ValueError(
|
||||
"Couldn't create spatial object from lookup value '%s'." % obj
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Cannot use object with type %s for a spatial lookup parameter."
|
||||
% type(obj).__name__
|
||||
)
|
||||
|
||||
# Assigning the SRID value.
|
||||
obj.srid = self.get_srid(obj)
|
||||
return obj
|
||||
|
||||
|
||||
class GeometryField(BaseSpatialField):
|
||||
"""
|
||||
The base Geometry field -- maps to the OpenGIS Specification Geometry type.
|
||||
"""
|
||||
|
||||
description = _(
|
||||
"The base Geometry field — maps to the OpenGIS Specification Geometry type."
|
||||
)
|
||||
form_class = forms.GeometryField
|
||||
# The OpenGIS Geometry name.
|
||||
geom_type = "GEOMETRY"
|
||||
geom_class = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
verbose_name=None,
|
||||
dim=2,
|
||||
geography=False,
|
||||
*,
|
||||
extent=(-180.0, -90.0, 180.0, 90.0),
|
||||
tolerance=0.05,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
The initialization function for geometry fields. In addition to the
|
||||
parameters from BaseSpatialField, it takes the following as keyword
|
||||
arguments:
|
||||
|
||||
dim:
|
||||
The number of dimensions for this geometry. Defaults to 2.
|
||||
|
||||
extent:
|
||||
Customize the extent, in a 4-tuple of WGS 84 coordinates, for the
|
||||
geometry field entry in the `USER_SDO_GEOM_METADATA` table. Defaults
|
||||
to (-180.0, -90.0, 180.0, 90.0).
|
||||
|
||||
tolerance:
|
||||
Define the tolerance, in meters, to use for the geometry field
|
||||
entry in the `USER_SDO_GEOM_METADATA` table. Defaults to 0.05.
|
||||
"""
|
||||
# Setting the dimension of the geometry field.
|
||||
self.dim = dim
|
||||
|
||||
# Is this a geography rather than a geometry column?
|
||||
self.geography = geography
|
||||
|
||||
# Oracle-specific private attributes for creating the entry in
|
||||
# `USER_SDO_GEOM_METADATA`
|
||||
self._extent = extent
|
||||
self._tolerance = tolerance
|
||||
|
||||
super().__init__(verbose_name=verbose_name, **kwargs)
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
# Include kwargs if they're not the default values.
|
||||
if self.dim != 2:
|
||||
kwargs["dim"] = self.dim
|
||||
if self.geography is not False:
|
||||
kwargs["geography"] = self.geography
|
||||
if self._extent != (-180.0, -90.0, 180.0, 90.0):
|
||||
kwargs["extent"] = self._extent
|
||||
if self._tolerance != 0.05:
|
||||
kwargs["tolerance"] = self._tolerance
|
||||
return name, path, args, kwargs
|
||||
|
||||
def contribute_to_class(self, cls, name, **kwargs):
|
||||
super().contribute_to_class(cls, name, **kwargs)
|
||||
|
||||
# Setup for lazy-instantiated Geometry object.
|
||||
setattr(
|
||||
cls,
|
||||
self.attname,
|
||||
SpatialProxy(self.geom_class or GEOSGeometry, self, load_func=GEOSGeometry),
|
||||
)
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {
|
||||
"form_class": self.form_class,
|
||||
"geom_type": self.geom_type,
|
||||
"srid": self.srid,
|
||||
**kwargs,
|
||||
}
|
||||
if self.dim > 2 and not getattr(
|
||||
defaults["form_class"].widget, "supports_3d", False
|
||||
):
|
||||
defaults.setdefault("widget", forms.Textarea)
|
||||
return super().formfield(**defaults)
|
||||
|
||||
def select_format(self, compiler, sql, params):
|
||||
"""
|
||||
Return the selection format string, depending on the requirements
|
||||
of the spatial backend. For example, Oracle and MySQL require custom
|
||||
selection formats in order to retrieve geometries in OGC WKB.
|
||||
"""
|
||||
if not compiler.query.subquery:
|
||||
return compiler.connection.ops.select % sql, params
|
||||
return sql, params
|
||||
|
||||
|
||||
# The OpenGIS Geometry Type Fields
|
||||
class PointField(GeometryField):
|
||||
geom_type = "POINT"
|
||||
geom_class = Point
|
||||
form_class = forms.PointField
|
||||
description = _("Point")
|
||||
|
||||
|
||||
class LineStringField(GeometryField):
|
||||
geom_type = "LINESTRING"
|
||||
geom_class = LineString
|
||||
form_class = forms.LineStringField
|
||||
description = _("Line string")
|
||||
|
||||
|
||||
class PolygonField(GeometryField):
|
||||
geom_type = "POLYGON"
|
||||
geom_class = Polygon
|
||||
form_class = forms.PolygonField
|
||||
description = _("Polygon")
|
||||
|
||||
|
||||
class MultiPointField(GeometryField):
|
||||
geom_type = "MULTIPOINT"
|
||||
geom_class = MultiPoint
|
||||
form_class = forms.MultiPointField
|
||||
description = _("Multi-point")
|
||||
|
||||
|
||||
class MultiLineStringField(GeometryField):
|
||||
geom_type = "MULTILINESTRING"
|
||||
geom_class = MultiLineString
|
||||
form_class = forms.MultiLineStringField
|
||||
description = _("Multi-line string")
|
||||
|
||||
|
||||
class MultiPolygonField(GeometryField):
|
||||
geom_type = "MULTIPOLYGON"
|
||||
geom_class = MultiPolygon
|
||||
form_class = forms.MultiPolygonField
|
||||
description = _("Multi polygon")
|
||||
|
||||
|
||||
class GeometryCollectionField(GeometryField):
|
||||
geom_type = "GEOMETRYCOLLECTION"
|
||||
geom_class = GeometryCollection
|
||||
form_class = forms.GeometryCollectionField
|
||||
description = _("Geometry collection")
|
||||
|
||||
|
||||
class ExtentField(Field):
|
||||
"Used as a return value from an extent aggregate"
|
||||
|
||||
description = _("Extent Aggregate Field")
|
||||
|
||||
def get_internal_type(self):
|
||||
return "ExtentField"
|
||||
|
||||
def select_format(self, compiler, sql, params):
|
||||
select = compiler.connection.ops.select_extent
|
||||
return select % sql if select else sql, params
|
||||
|
||||
|
||||
class RasterField(BaseSpatialField):
|
||||
"""
|
||||
Raster field for GeoDjango -- evaluates into GDALRaster objects.
|
||||
"""
|
||||
|
||||
description = _("Raster Field")
|
||||
geom_type = "RASTER"
|
||||
geography = False
|
||||
|
||||
def _check_connection(self, connection):
|
||||
# Make sure raster fields are used only on backends with raster support.
|
||||
if (
|
||||
not connection.features.gis_enabled
|
||||
or not connection.features.supports_raster
|
||||
):
|
||||
raise ImproperlyConfigured(
|
||||
"Raster fields require backends with raster support."
|
||||
)
|
||||
|
||||
def db_type(self, connection):
|
||||
self._check_connection(connection)
|
||||
return super().db_type(connection)
|
||||
|
||||
def from_db_value(self, value, expression, connection):
|
||||
return connection.ops.parse_raster(value)
|
||||
|
||||
def contribute_to_class(self, cls, name, **kwargs):
|
||||
super().contribute_to_class(cls, name, **kwargs)
|
||||
# Setup for lazy-instantiated Raster object. For large querysets, the
|
||||
# instantiation of all GDALRasters can potentially be expensive. This
|
||||
# delays the instantiation of the objects to the moment of evaluation
|
||||
# of the raster attribute.
|
||||
setattr(cls, self.attname, SpatialProxy(gdal.GDALRaster, self))
|
||||
|
||||
def get_transform(self, name):
|
||||
from django.contrib.gis.db.models.lookups import RasterBandTransform
|
||||
|
||||
try:
|
||||
band_index = int(name)
|
||||
return type(
|
||||
"SpecificRasterBandTransform",
|
||||
(RasterBandTransform,),
|
||||
{"band_index": band_index},
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
return super().get_transform(name)
|
@ -0,0 +1,564 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib.gis.db.models.fields import BaseSpatialField, GeometryField
|
||||
from django.contrib.gis.db.models.sql import AreaField, DistanceField
|
||||
from django.contrib.gis.geos import GEOSGeometry
|
||||
from django.core.exceptions import FieldError
|
||||
from django.db import NotSupportedError
|
||||
from django.db.models import (
|
||||
BinaryField,
|
||||
BooleanField,
|
||||
FloatField,
|
||||
Func,
|
||||
IntegerField,
|
||||
TextField,
|
||||
Transform,
|
||||
Value,
|
||||
)
|
||||
from django.db.models.functions import Cast
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
NUMERIC_TYPES = (int, float, Decimal)
|
||||
|
||||
|
||||
class GeoFuncMixin:
|
||||
function = None
|
||||
geom_param_pos = (0,)
|
||||
|
||||
def __init__(self, *expressions, **extra):
|
||||
super().__init__(*expressions, **extra)
|
||||
|
||||
# Ensure that value expressions are geometric.
|
||||
for pos in self.geom_param_pos:
|
||||
expr = self.source_expressions[pos]
|
||||
if not isinstance(expr, Value):
|
||||
continue
|
||||
try:
|
||||
output_field = expr.output_field
|
||||
except FieldError:
|
||||
output_field = None
|
||||
geom = expr.value
|
||||
if (
|
||||
not isinstance(geom, GEOSGeometry)
|
||||
or output_field
|
||||
and not isinstance(output_field, GeometryField)
|
||||
):
|
||||
raise TypeError(
|
||||
"%s function requires a geometric argument in position %d."
|
||||
% (self.name, pos + 1)
|
||||
)
|
||||
if not geom.srid and not output_field:
|
||||
raise ValueError("SRID is required for all geometries.")
|
||||
if not output_field:
|
||||
self.source_expressions[pos] = Value(
|
||||
geom, output_field=GeometryField(srid=geom.srid)
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
@cached_property
|
||||
def geo_field(self):
|
||||
return self.source_expressions[self.geom_param_pos[0]].field
|
||||
|
||||
def as_sql(self, compiler, connection, function=None, **extra_context):
|
||||
if self.function is None and function is None:
|
||||
function = connection.ops.spatial_function_name(self.name)
|
||||
return super().as_sql(compiler, connection, function=function, **extra_context)
|
||||
|
||||
def resolve_expression(self, *args, **kwargs):
|
||||
res = super().resolve_expression(*args, **kwargs)
|
||||
if not self.geom_param_pos:
|
||||
return res
|
||||
|
||||
# Ensure that expressions are geometric.
|
||||
source_fields = res.get_source_fields()
|
||||
for pos in self.geom_param_pos:
|
||||
field = source_fields[pos]
|
||||
if not isinstance(field, GeometryField):
|
||||
raise TypeError(
|
||||
"%s function requires a GeometryField in position %s, got %s."
|
||||
% (
|
||||
self.name,
|
||||
pos + 1,
|
||||
type(field).__name__,
|
||||
)
|
||||
)
|
||||
|
||||
base_srid = res.geo_field.srid
|
||||
for pos in self.geom_param_pos[1:]:
|
||||
expr = res.source_expressions[pos]
|
||||
expr_srid = expr.output_field.srid
|
||||
if expr_srid != base_srid:
|
||||
# Automatic SRID conversion so objects are comparable.
|
||||
res.source_expressions[pos] = Transform(
|
||||
expr, base_srid
|
||||
).resolve_expression(*args, **kwargs)
|
||||
return res
|
||||
|
||||
def _handle_param(self, value, param_name="", check_types=None):
|
||||
if not hasattr(value, "resolve_expression"):
|
||||
if check_types and not isinstance(value, check_types):
|
||||
raise TypeError(
|
||||
"The %s parameter has the wrong type: should be %s."
|
||||
% (param_name, check_types)
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class GeoFunc(GeoFuncMixin, Func):
|
||||
pass
|
||||
|
||||
|
||||
class GeomOutputGeoFunc(GeoFunc):
|
||||
@cached_property
|
||||
def output_field(self):
|
||||
return GeometryField(srid=self.geo_field.srid)
|
||||
|
||||
|
||||
class SQLiteDecimalToFloatMixin:
|
||||
"""
|
||||
By default, Decimal values are converted to str by the SQLite backend, which
|
||||
is not acceptable by the GIS functions expecting numeric values.
|
||||
"""
|
||||
|
||||
def as_sqlite(self, compiler, connection, **extra_context):
|
||||
copy = self.copy()
|
||||
copy.set_source_expressions(
|
||||
[
|
||||
Value(float(expr.value))
|
||||
if hasattr(expr, "value") and isinstance(expr.value, Decimal)
|
||||
else expr
|
||||
for expr in copy.get_source_expressions()
|
||||
]
|
||||
)
|
||||
return copy.as_sql(compiler, connection, **extra_context)
|
||||
|
||||
|
||||
class OracleToleranceMixin:
|
||||
tolerance = 0.05
|
||||
|
||||
def as_oracle(self, compiler, connection, **extra_context):
|
||||
tolerance = Value(
|
||||
self._handle_param(
|
||||
self.extra.get("tolerance", self.tolerance),
|
||||
"tolerance",
|
||||
NUMERIC_TYPES,
|
||||
)
|
||||
)
|
||||
clone = self.copy()
|
||||
clone.set_source_expressions([*self.get_source_expressions(), tolerance])
|
||||
return clone.as_sql(compiler, connection, **extra_context)
|
||||
|
||||
|
||||
class Area(OracleToleranceMixin, GeoFunc):
|
||||
arity = 1
|
||||
|
||||
@cached_property
|
||||
def output_field(self):
|
||||
return AreaField(self.geo_field)
|
||||
|
||||
def as_sql(self, compiler, connection, **extra_context):
|
||||
if not connection.features.supports_area_geodetic and self.geo_field.geodetic(
|
||||
connection
|
||||
):
|
||||
raise NotSupportedError(
|
||||
"Area on geodetic coordinate systems not supported."
|
||||
)
|
||||
return super().as_sql(compiler, connection, **extra_context)
|
||||
|
||||
def as_sqlite(self, compiler, connection, **extra_context):
|
||||
if self.geo_field.geodetic(connection):
|
||||
extra_context["template"] = "%(function)s(%(expressions)s, %(spheroid)d)"
|
||||
extra_context["spheroid"] = True
|
||||
return self.as_sql(compiler, connection, **extra_context)
|
||||
|
||||
|
||||
class Azimuth(GeoFunc):
|
||||
output_field = FloatField()
|
||||
arity = 2
|
||||
geom_param_pos = (0, 1)
|
||||
|
||||
|
||||
class AsGeoJSON(GeoFunc):
|
||||
output_field = TextField()
|
||||
|
||||
def __init__(self, expression, bbox=False, crs=False, precision=8, **extra):
|
||||
expressions = [expression]
|
||||
if precision is not None:
|
||||
expressions.append(self._handle_param(precision, "precision", int))
|
||||
options = 0
|
||||
if crs and bbox:
|
||||
options = 3
|
||||
elif bbox:
|
||||
options = 1
|
||||
elif crs:
|
||||
options = 2
|
||||
if options:
|
||||
expressions.append(options)
|
||||
super().__init__(*expressions, **extra)
|
||||
|
||||
def as_oracle(self, compiler, connection, **extra_context):
|
||||
source_expressions = self.get_source_expressions()
|
||||
clone = self.copy()
|
||||
clone.set_source_expressions(source_expressions[:1])
|
||||
return super(AsGeoJSON, clone).as_sql(compiler, connection, **extra_context)
|
||||
|
||||
|
||||
class AsGML(GeoFunc):
|
||||
geom_param_pos = (1,)
|
||||
output_field = TextField()
|
||||
|
||||
def __init__(self, expression, version=2, precision=8, **extra):
|
||||
expressions = [version, expression]
|
||||
if precision is not None:
|
||||
expressions.append(self._handle_param(precision, "precision", int))
|
||||
super().__init__(*expressions, **extra)
|
||||
|
||||
def as_oracle(self, compiler, connection, **extra_context):
|
||||
source_expressions = self.get_source_expressions()
|
||||
version = source_expressions[0]
|
||||
clone = self.copy()
|
||||
clone.set_source_expressions([source_expressions[1]])
|
||||
extra_context["function"] = (
|
||||
"SDO_UTIL.TO_GML311GEOMETRY"
|
||||
if version.value == 3
|
||||
else "SDO_UTIL.TO_GMLGEOMETRY"
|
||||
)
|
||||
return super(AsGML, clone).as_sql(compiler, connection, **extra_context)
|
||||
|
||||
|
||||
class AsKML(GeoFunc):
|
||||
output_field = TextField()
|
||||
|
||||
def __init__(self, expression, precision=8, **extra):
|
||||
expressions = [expression]
|
||||
if precision is not None:
|
||||
expressions.append(self._handle_param(precision, "precision", int))
|
||||
super().__init__(*expressions, **extra)
|
||||
|
||||
|
||||
class AsSVG(GeoFunc):
|
||||
output_field = TextField()
|
||||
|
||||
def __init__(self, expression, relative=False, precision=8, **extra):
|
||||
relative = (
|
||||
relative if hasattr(relative, "resolve_expression") else int(relative)
|
||||
)
|
||||
expressions = [
|
||||
expression,
|
||||
relative,
|
||||
self._handle_param(precision, "precision", int),
|
||||
]
|
||||
super().__init__(*expressions, **extra)
|
||||
|
||||
|
||||
class AsWKB(GeoFunc):
|
||||
output_field = BinaryField()
|
||||
arity = 1
|
||||
|
||||
|
||||
class AsWKT(GeoFunc):
|
||||
output_field = TextField()
|
||||
arity = 1
|
||||
|
||||
|
||||
class BoundingCircle(OracleToleranceMixin, GeomOutputGeoFunc):
|
||||
def __init__(self, expression, num_seg=48, **extra):
|
||||
super().__init__(expression, num_seg, **extra)
|
||||
|
||||
def as_oracle(self, compiler, connection, **extra_context):
|
||||
clone = self.copy()
|
||||
clone.set_source_expressions([self.get_source_expressions()[0]])
|
||||
return super(BoundingCircle, clone).as_oracle(
|
||||
compiler, connection, **extra_context
|
||||
)
|
||||
|
||||
|
||||
class Centroid(OracleToleranceMixin, GeomOutputGeoFunc):
|
||||
arity = 1
|
||||
|
||||
|
||||
class Difference(OracleToleranceMixin, GeomOutputGeoFunc):
|
||||
arity = 2
|
||||
geom_param_pos = (0, 1)
|
||||
|
||||
|
||||
class DistanceResultMixin:
|
||||
@cached_property
|
||||
def output_field(self):
|
||||
return DistanceField(self.geo_field)
|
||||
|
||||
def source_is_geography(self):
|
||||
return self.geo_field.geography and self.geo_field.srid == 4326
|
||||
|
||||
|
||||
class Distance(DistanceResultMixin, OracleToleranceMixin, GeoFunc):
|
||||
geom_param_pos = (0, 1)
|
||||
spheroid = None
|
||||
|
||||
def __init__(self, expr1, expr2, spheroid=None, **extra):
|
||||
expressions = [expr1, expr2]
|
||||
if spheroid is not None:
|
||||
self.spheroid = self._handle_param(spheroid, "spheroid", bool)
|
||||
super().__init__(*expressions, **extra)
|
||||
|
||||
def as_postgresql(self, compiler, connection, **extra_context):
|
||||
clone = self.copy()
|
||||
function = None
|
||||
expr2 = clone.source_expressions[1]
|
||||
geography = self.source_is_geography()
|
||||
if expr2.output_field.geography != geography:
|
||||
if isinstance(expr2, Value):
|
||||
expr2.output_field.geography = geography
|
||||
else:
|
||||
clone.source_expressions[1] = Cast(
|
||||
expr2,
|
||||
GeometryField(srid=expr2.output_field.srid, geography=geography),
|
||||
)
|
||||
|
||||
if not geography and self.geo_field.geodetic(connection):
|
||||
# Geometry fields with geodetic (lon/lat) coordinates need special
|
||||
# distance functions.
|
||||
if self.spheroid:
|
||||
# DistanceSpheroid is more accurate and resource intensive than
|
||||
# DistanceSphere.
|
||||
function = connection.ops.spatial_function_name("DistanceSpheroid")
|
||||
# Replace boolean param by the real spheroid of the base field
|
||||
clone.source_expressions.append(
|
||||
Value(self.geo_field.spheroid(connection))
|
||||
)
|
||||
else:
|
||||
function = connection.ops.spatial_function_name("DistanceSphere")
|
||||
return super(Distance, clone).as_sql(
|
||||
compiler, connection, function=function, **extra_context
|
||||
)
|
||||
|
||||
def as_sqlite(self, compiler, connection, **extra_context):
|
||||
if self.geo_field.geodetic(connection):
|
||||
# SpatiaLite returns NULL instead of zero on geodetic coordinates
|
||||
extra_context[
|
||||
"template"
|
||||
] = "COALESCE(%(function)s(%(expressions)s, %(spheroid)s), 0)"
|
||||
extra_context["spheroid"] = int(bool(self.spheroid))
|
||||
return super().as_sql(compiler, connection, **extra_context)
|
||||
|
||||
|
||||
class Envelope(GeomOutputGeoFunc):
|
||||
arity = 1
|
||||
|
||||
|
||||
class ForcePolygonCW(GeomOutputGeoFunc):
|
||||
arity = 1
|
||||
|
||||
|
||||
class FromWKB(GeoFunc):
|
||||
output_field = GeometryField(srid=0)
|
||||
arity = 1
|
||||
geom_param_pos = ()
|
||||
|
||||
|
||||
class FromWKT(GeoFunc):
|
||||
output_field = GeometryField(srid=0)
|
||||
arity = 1
|
||||
geom_param_pos = ()
|
||||
|
||||
|
||||
class GeoHash(GeoFunc):
|
||||
output_field = TextField()
|
||||
|
||||
def __init__(self, expression, precision=None, **extra):
|
||||
expressions = [expression]
|
||||
if precision is not None:
|
||||
expressions.append(self._handle_param(precision, "precision", int))
|
||||
super().__init__(*expressions, **extra)
|
||||
|
||||
def as_mysql(self, compiler, connection, **extra_context):
|
||||
clone = self.copy()
|
||||
# If no precision is provided, set it to the maximum.
|
||||
if len(clone.source_expressions) < 2:
|
||||
clone.source_expressions.append(Value(100))
|
||||
return clone.as_sql(compiler, connection, **extra_context)
|
||||
|
||||
|
||||
class GeometryDistance(GeoFunc):
|
||||
output_field = FloatField()
|
||||
arity = 2
|
||||
function = ""
|
||||
arg_joiner = " <-> "
|
||||
geom_param_pos = (0, 1)
|
||||
|
||||
|
||||
class Intersection(OracleToleranceMixin, GeomOutputGeoFunc):
|
||||
arity = 2
|
||||
geom_param_pos = (0, 1)
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class IsEmpty(GeoFuncMixin, Transform):
|
||||
lookup_name = "isempty"
|
||||
output_field = BooleanField()
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class IsValid(OracleToleranceMixin, GeoFuncMixin, Transform):
|
||||
lookup_name = "isvalid"
|
||||
output_field = BooleanField()
|
||||
|
||||
def as_oracle(self, compiler, connection, **extra_context):
|
||||
sql, params = super().as_oracle(compiler, connection, **extra_context)
|
||||
return "CASE %s WHEN 'TRUE' THEN 1 ELSE 0 END" % sql, params
|
||||
|
||||
|
||||
class Length(DistanceResultMixin, OracleToleranceMixin, GeoFunc):
|
||||
def __init__(self, expr1, spheroid=True, **extra):
|
||||
self.spheroid = spheroid
|
||||
super().__init__(expr1, **extra)
|
||||
|
||||
def as_sql(self, compiler, connection, **extra_context):
|
||||
if (
|
||||
self.geo_field.geodetic(connection)
|
||||
and not connection.features.supports_length_geodetic
|
||||
):
|
||||
raise NotSupportedError(
|
||||
"This backend doesn't support Length on geodetic fields"
|
||||
)
|
||||
return super().as_sql(compiler, connection, **extra_context)
|
||||
|
||||
def as_postgresql(self, compiler, connection, **extra_context):
|
||||
clone = self.copy()
|
||||
function = None
|
||||
if self.source_is_geography():
|
||||
clone.source_expressions.append(Value(self.spheroid))
|
||||
elif self.geo_field.geodetic(connection):
|
||||
# Geometry fields with geodetic (lon/lat) coordinates need length_spheroid
|
||||
function = connection.ops.spatial_function_name("LengthSpheroid")
|
||||
clone.source_expressions.append(Value(self.geo_field.spheroid(connection)))
|
||||
else:
|
||||
dim = min(f.dim for f in self.get_source_fields() if f)
|
||||
if dim > 2:
|
||||
function = connection.ops.length3d
|
||||
return super(Length, clone).as_sql(
|
||||
compiler, connection, function=function, **extra_context
|
||||
)
|
||||
|
||||
def as_sqlite(self, compiler, connection, **extra_context):
|
||||
function = None
|
||||
if self.geo_field.geodetic(connection):
|
||||
function = "GeodesicLength" if self.spheroid else "GreatCircleLength"
|
||||
return super().as_sql(compiler, connection, function=function, **extra_context)
|
||||
|
||||
|
||||
class LineLocatePoint(GeoFunc):
|
||||
output_field = FloatField()
|
||||
arity = 2
|
||||
geom_param_pos = (0, 1)
|
||||
|
||||
|
||||
class MakeValid(GeomOutputGeoFunc):
|
||||
pass
|
||||
|
||||
|
||||
class MemSize(GeoFunc):
|
||||
output_field = IntegerField()
|
||||
arity = 1
|
||||
|
||||
|
||||
class NumGeometries(GeoFunc):
|
||||
output_field = IntegerField()
|
||||
arity = 1
|
||||
|
||||
|
||||
class NumPoints(GeoFunc):
|
||||
output_field = IntegerField()
|
||||
arity = 1
|
||||
|
||||
|
||||
class Perimeter(DistanceResultMixin, OracleToleranceMixin, GeoFunc):
|
||||
arity = 1
|
||||
|
||||
def as_postgresql(self, compiler, connection, **extra_context):
|
||||
function = None
|
||||
if self.geo_field.geodetic(connection) and not self.source_is_geography():
|
||||
raise NotSupportedError(
|
||||
"ST_Perimeter cannot use a non-projected non-geography field."
|
||||
)
|
||||
dim = min(f.dim for f in self.get_source_fields())
|
||||
if dim > 2:
|
||||
function = connection.ops.perimeter3d
|
||||
return super().as_sql(compiler, connection, function=function, **extra_context)
|
||||
|
||||
def as_sqlite(self, compiler, connection, **extra_context):
|
||||
if self.geo_field.geodetic(connection):
|
||||
raise NotSupportedError("Perimeter cannot use a non-projected field.")
|
||||
return super().as_sql(compiler, connection, **extra_context)
|
||||
|
||||
|
||||
class PointOnSurface(OracleToleranceMixin, GeomOutputGeoFunc):
|
||||
arity = 1
|
||||
|
||||
|
||||
class Reverse(GeoFunc):
|
||||
arity = 1
|
||||
|
||||
|
||||
class Scale(SQLiteDecimalToFloatMixin, GeomOutputGeoFunc):
|
||||
def __init__(self, expression, x, y, z=0.0, **extra):
|
||||
expressions = [
|
||||
expression,
|
||||
self._handle_param(x, "x", NUMERIC_TYPES),
|
||||
self._handle_param(y, "y", NUMERIC_TYPES),
|
||||
]
|
||||
if z != 0.0:
|
||||
expressions.append(self._handle_param(z, "z", NUMERIC_TYPES))
|
||||
super().__init__(*expressions, **extra)
|
||||
|
||||
|
||||
class SnapToGrid(SQLiteDecimalToFloatMixin, GeomOutputGeoFunc):
|
||||
def __init__(self, expression, *args, **extra):
|
||||
nargs = len(args)
|
||||
expressions = [expression]
|
||||
if nargs in (1, 2):
|
||||
expressions.extend(
|
||||
[self._handle_param(arg, "", NUMERIC_TYPES) for arg in args]
|
||||
)
|
||||
elif nargs == 4:
|
||||
# Reverse origin and size param ordering
|
||||
expressions += [
|
||||
*(self._handle_param(arg, "", NUMERIC_TYPES) for arg in args[2:]),
|
||||
*(self._handle_param(arg, "", NUMERIC_TYPES) for arg in args[0:2]),
|
||||
]
|
||||
else:
|
||||
raise ValueError("Must provide 1, 2, or 4 arguments to `SnapToGrid`.")
|
||||
super().__init__(*expressions, **extra)
|
||||
|
||||
|
||||
class SymDifference(OracleToleranceMixin, GeomOutputGeoFunc):
|
||||
arity = 2
|
||||
geom_param_pos = (0, 1)
|
||||
|
||||
|
||||
class Transform(GeomOutputGeoFunc):
|
||||
def __init__(self, expression, srid, **extra):
|
||||
expressions = [
|
||||
expression,
|
||||
self._handle_param(srid, "srid", int),
|
||||
]
|
||||
if "output_field" not in extra:
|
||||
extra["output_field"] = GeometryField(srid=srid)
|
||||
super().__init__(*expressions, **extra)
|
||||
|
||||
|
||||
class Translate(Scale):
|
||||
def as_sqlite(self, compiler, connection, **extra_context):
|
||||
clone = self.copy()
|
||||
if len(self.source_expressions) < 4:
|
||||
# Always provide the z parameter for ST_Translate
|
||||
clone.source_expressions.append(Value(0))
|
||||
return super(Translate, clone).as_sqlite(compiler, connection, **extra_context)
|
||||
|
||||
|
||||
class Union(OracleToleranceMixin, GeomOutputGeoFunc):
|
||||
arity = 2
|
||||
geom_param_pos = (0, 1)
|
@ -0,0 +1,395 @@
|
||||
from django.contrib.gis.db.models.fields import BaseSpatialField
|
||||
from django.contrib.gis.measure import Distance
|
||||
from django.db import NotSupportedError
|
||||
from django.db.models import Expression, Lookup, Transform
|
||||
from django.db.models.sql.query import Query
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
|
||||
|
||||
class RasterBandTransform(Transform):
|
||||
def as_sql(self, compiler, connection):
|
||||
return compiler.compile(self.lhs)
|
||||
|
||||
|
||||
class GISLookup(Lookup):
|
||||
sql_template = None
|
||||
transform_func = None
|
||||
distance = False
|
||||
band_rhs = None
|
||||
band_lhs = None
|
||||
|
||||
def __init__(self, lhs, rhs):
|
||||
rhs, *self.rhs_params = rhs if isinstance(rhs, (list, tuple)) else [rhs]
|
||||
super().__init__(lhs, rhs)
|
||||
self.template_params = {}
|
||||
self.process_rhs_params()
|
||||
|
||||
def process_rhs_params(self):
|
||||
if self.rhs_params:
|
||||
# Check if a band index was passed in the query argument.
|
||||
if len(self.rhs_params) == (2 if self.lookup_name == "relate" else 1):
|
||||
self.process_band_indices()
|
||||
elif len(self.rhs_params) > 1:
|
||||
raise ValueError("Tuple too long for lookup %s." % self.lookup_name)
|
||||
elif isinstance(self.lhs, RasterBandTransform):
|
||||
self.process_band_indices(only_lhs=True)
|
||||
|
||||
def process_band_indices(self, only_lhs=False):
|
||||
"""
|
||||
Extract the lhs band index from the band transform class and the rhs
|
||||
band index from the input tuple.
|
||||
"""
|
||||
# PostGIS band indices are 1-based, so the band index needs to be
|
||||
# increased to be consistent with the GDALRaster band indices.
|
||||
if only_lhs:
|
||||
self.band_rhs = 1
|
||||
self.band_lhs = self.lhs.band_index + 1
|
||||
return
|
||||
|
||||
if isinstance(self.lhs, RasterBandTransform):
|
||||
self.band_lhs = self.lhs.band_index + 1
|
||||
else:
|
||||
self.band_lhs = 1
|
||||
|
||||
self.band_rhs, *self.rhs_params = self.rhs_params
|
||||
|
||||
def get_db_prep_lookup(self, value, connection):
|
||||
# get_db_prep_lookup is called by process_rhs from super class
|
||||
return ("%s", [connection.ops.Adapter(value)])
|
||||
|
||||
def process_rhs(self, compiler, connection):
|
||||
if isinstance(self.rhs, Query):
|
||||
# If rhs is some Query, don't touch it.
|
||||
return super().process_rhs(compiler, connection)
|
||||
if isinstance(self.rhs, Expression):
|
||||
self.rhs = self.rhs.resolve_expression(compiler.query)
|
||||
rhs, rhs_params = super().process_rhs(compiler, connection)
|
||||
placeholder = connection.ops.get_geom_placeholder(
|
||||
self.lhs.output_field, self.rhs, compiler
|
||||
)
|
||||
return placeholder % rhs, rhs_params
|
||||
|
||||
def get_rhs_op(self, connection, rhs):
|
||||
# Unlike BuiltinLookup, the GIS get_rhs_op() implementation should return
|
||||
# an object (SpatialOperator) with an as_sql() method to allow for more
|
||||
# complex computations (where the lhs part can be mixed in).
|
||||
return connection.ops.gis_operators[self.lookup_name]
|
||||
|
||||
def as_sql(self, compiler, connection):
|
||||
lhs_sql, lhs_params = self.process_lhs(compiler, connection)
|
||||
rhs_sql, rhs_params = self.process_rhs(compiler, connection)
|
||||
sql_params = (*lhs_params, *rhs_params)
|
||||
|
||||
template_params = {
|
||||
"lhs": lhs_sql,
|
||||
"rhs": rhs_sql,
|
||||
"value": "%s",
|
||||
**self.template_params,
|
||||
}
|
||||
rhs_op = self.get_rhs_op(connection, rhs_sql)
|
||||
return rhs_op.as_sql(connection, self, template_params, sql_params)
|
||||
|
||||
|
||||
# ------------------
|
||||
# Geometry operators
|
||||
# ------------------
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class OverlapsLeftLookup(GISLookup):
|
||||
"""
|
||||
The overlaps_left operator returns true if A's bounding box overlaps or is to the
|
||||
left of B's bounding box.
|
||||
"""
|
||||
|
||||
lookup_name = "overlaps_left"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class OverlapsRightLookup(GISLookup):
|
||||
"""
|
||||
The 'overlaps_right' operator returns true if A's bounding box overlaps or is to the
|
||||
right of B's bounding box.
|
||||
"""
|
||||
|
||||
lookup_name = "overlaps_right"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class OverlapsBelowLookup(GISLookup):
|
||||
"""
|
||||
The 'overlaps_below' operator returns true if A's bounding box overlaps or is below
|
||||
B's bounding box.
|
||||
"""
|
||||
|
||||
lookup_name = "overlaps_below"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class OverlapsAboveLookup(GISLookup):
|
||||
"""
|
||||
The 'overlaps_above' operator returns true if A's bounding box overlaps or is above
|
||||
B's bounding box.
|
||||
"""
|
||||
|
||||
lookup_name = "overlaps_above"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class LeftLookup(GISLookup):
|
||||
"""
|
||||
The 'left' operator returns true if A's bounding box is strictly to the left
|
||||
of B's bounding box.
|
||||
"""
|
||||
|
||||
lookup_name = "left"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class RightLookup(GISLookup):
|
||||
"""
|
||||
The 'right' operator returns true if A's bounding box is strictly to the right
|
||||
of B's bounding box.
|
||||
"""
|
||||
|
||||
lookup_name = "right"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class StrictlyBelowLookup(GISLookup):
|
||||
"""
|
||||
The 'strictly_below' operator returns true if A's bounding box is strictly below B's
|
||||
bounding box.
|
||||
"""
|
||||
|
||||
lookup_name = "strictly_below"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class StrictlyAboveLookup(GISLookup):
|
||||
"""
|
||||
The 'strictly_above' operator returns true if A's bounding box is strictly above B's
|
||||
bounding box.
|
||||
"""
|
||||
|
||||
lookup_name = "strictly_above"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class SameAsLookup(GISLookup):
|
||||
"""
|
||||
The "~=" operator is the "same as" operator. It tests actual geometric
|
||||
equality of two features. So if A and B are the same feature,
|
||||
vertex-by-vertex, the operator returns true.
|
||||
"""
|
||||
|
||||
lookup_name = "same_as"
|
||||
|
||||
|
||||
BaseSpatialField.register_lookup(SameAsLookup, "exact")
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class BBContainsLookup(GISLookup):
|
||||
"""
|
||||
The 'bbcontains' operator returns true if A's bounding box completely contains
|
||||
by B's bounding box.
|
||||
"""
|
||||
|
||||
lookup_name = "bbcontains"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class BBOverlapsLookup(GISLookup):
|
||||
"""
|
||||
The 'bboverlaps' operator returns true if A's bounding box overlaps B's
|
||||
bounding box.
|
||||
"""
|
||||
|
||||
lookup_name = "bboverlaps"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class ContainedLookup(GISLookup):
|
||||
"""
|
||||
The 'contained' operator returns true if A's bounding box is completely contained
|
||||
by B's bounding box.
|
||||
"""
|
||||
|
||||
lookup_name = "contained"
|
||||
|
||||
|
||||
# ------------------
|
||||
# Geometry functions
|
||||
# ------------------
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class ContainsLookup(GISLookup):
|
||||
lookup_name = "contains"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class ContainsProperlyLookup(GISLookup):
|
||||
lookup_name = "contains_properly"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class CoveredByLookup(GISLookup):
|
||||
lookup_name = "coveredby"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class CoversLookup(GISLookup):
|
||||
lookup_name = "covers"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class CrossesLookup(GISLookup):
|
||||
lookup_name = "crosses"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class DisjointLookup(GISLookup):
|
||||
lookup_name = "disjoint"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class EqualsLookup(GISLookup):
|
||||
lookup_name = "equals"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class IntersectsLookup(GISLookup):
|
||||
lookup_name = "intersects"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class OverlapsLookup(GISLookup):
|
||||
lookup_name = "overlaps"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class RelateLookup(GISLookup):
|
||||
lookup_name = "relate"
|
||||
sql_template = "%(func)s(%(lhs)s, %(rhs)s, %%s)"
|
||||
pattern_regex = _lazy_re_compile(r"^[012TF\*]{9}$")
|
||||
|
||||
def process_rhs(self, compiler, connection):
|
||||
# Check the pattern argument
|
||||
pattern = self.rhs_params[0]
|
||||
backend_op = connection.ops.gis_operators[self.lookup_name]
|
||||
if hasattr(backend_op, "check_relate_argument"):
|
||||
backend_op.check_relate_argument(pattern)
|
||||
elif not isinstance(pattern, str) or not self.pattern_regex.match(pattern):
|
||||
raise ValueError('Invalid intersection matrix pattern "%s".' % pattern)
|
||||
sql, params = super().process_rhs(compiler, connection)
|
||||
return sql, params + [pattern]
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class TouchesLookup(GISLookup):
|
||||
lookup_name = "touches"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class WithinLookup(GISLookup):
|
||||
lookup_name = "within"
|
||||
|
||||
|
||||
class DistanceLookupBase(GISLookup):
|
||||
distance = True
|
||||
sql_template = "%(func)s(%(lhs)s, %(rhs)s) %(op)s %(value)s"
|
||||
|
||||
def process_rhs_params(self):
|
||||
if not 1 <= len(self.rhs_params) <= 3:
|
||||
raise ValueError(
|
||||
"2, 3, or 4-element tuple required for '%s' lookup." % self.lookup_name
|
||||
)
|
||||
elif len(self.rhs_params) == 3 and self.rhs_params[2] != "spheroid":
|
||||
raise ValueError(
|
||||
"For 4-element tuples the last argument must be the 'spheroid' "
|
||||
"directive."
|
||||
)
|
||||
|
||||
# Check if the second parameter is a band index.
|
||||
if len(self.rhs_params) > 1 and self.rhs_params[1] != "spheroid":
|
||||
self.process_band_indices()
|
||||
|
||||
def process_distance(self, compiler, connection):
|
||||
dist_param = self.rhs_params[0]
|
||||
return (
|
||||
compiler.compile(dist_param.resolve_expression(compiler.query))
|
||||
if hasattr(dist_param, "resolve_expression")
|
||||
else (
|
||||
"%s",
|
||||
connection.ops.get_distance(
|
||||
self.lhs.output_field, self.rhs_params, self.lookup_name
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class DWithinLookup(DistanceLookupBase):
|
||||
lookup_name = "dwithin"
|
||||
sql_template = "%(func)s(%(lhs)s, %(rhs)s, %(value)s)"
|
||||
|
||||
def process_distance(self, compiler, connection):
|
||||
dist_param = self.rhs_params[0]
|
||||
if (
|
||||
not connection.features.supports_dwithin_distance_expr
|
||||
and hasattr(dist_param, "resolve_expression")
|
||||
and not isinstance(dist_param, Distance)
|
||||
):
|
||||
raise NotSupportedError(
|
||||
"This backend does not support expressions for specifying "
|
||||
"distance in the dwithin lookup."
|
||||
)
|
||||
return super().process_distance(compiler, connection)
|
||||
|
||||
def process_rhs(self, compiler, connection):
|
||||
dist_sql, dist_params = self.process_distance(compiler, connection)
|
||||
self.template_params["value"] = dist_sql
|
||||
rhs_sql, params = super().process_rhs(compiler, connection)
|
||||
return rhs_sql, params + dist_params
|
||||
|
||||
|
||||
class DistanceLookupFromFunction(DistanceLookupBase):
|
||||
def as_sql(self, compiler, connection):
|
||||
spheroid = (
|
||||
len(self.rhs_params) == 2 and self.rhs_params[-1] == "spheroid"
|
||||
) or None
|
||||
distance_expr = connection.ops.distance_expr_for_lookup(
|
||||
self.lhs, self.rhs, spheroid=spheroid
|
||||
)
|
||||
sql, params = compiler.compile(distance_expr.resolve_expression(compiler.query))
|
||||
dist_sql, dist_params = self.process_distance(compiler, connection)
|
||||
return (
|
||||
"%(func)s %(op)s %(dist)s" % {"func": sql, "op": self.op, "dist": dist_sql},
|
||||
params + dist_params,
|
||||
)
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class DistanceGTLookup(DistanceLookupFromFunction):
|
||||
lookup_name = "distance_gt"
|
||||
op = ">"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class DistanceGTELookup(DistanceLookupFromFunction):
|
||||
lookup_name = "distance_gte"
|
||||
op = ">="
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class DistanceLTLookup(DistanceLookupFromFunction):
|
||||
lookup_name = "distance_lt"
|
||||
op = "<"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class DistanceLTELookup(DistanceLookupFromFunction):
|
||||
lookup_name = "distance_lte"
|
||||
op = "<="
|
@ -0,0 +1,83 @@
|
||||
"""
|
||||
The SpatialProxy object allows for lazy-geometries and lazy-rasters. The proxy
|
||||
uses Python descriptors for instantiating and setting Geometry or Raster
|
||||
objects corresponding to geographic model fields.
|
||||
|
||||
Thanks to Robert Coup for providing this functionality (see #4322).
|
||||
"""
|
||||
from django.db.models.query_utils import DeferredAttribute
|
||||
|
||||
|
||||
class SpatialProxy(DeferredAttribute):
|
||||
def __init__(self, klass, field, load_func=None):
|
||||
"""
|
||||
Initialize on the given Geometry or Raster class (not an instance)
|
||||
and the corresponding field.
|
||||
"""
|
||||
self._klass = klass
|
||||
self._load_func = load_func or klass
|
||||
super().__init__(field)
|
||||
|
||||
def __get__(self, instance, cls=None):
|
||||
"""
|
||||
Retrieve the geometry or raster, initializing it using the
|
||||
corresponding class specified during initialization and the value of
|
||||
the field. Currently, GEOS or OGR geometries as well as GDALRasters are
|
||||
supported.
|
||||
"""
|
||||
if instance is None:
|
||||
# Accessed on a class, not an instance
|
||||
return self
|
||||
|
||||
# Getting the value of the field.
|
||||
try:
|
||||
geo_value = instance.__dict__[self.field.attname]
|
||||
except KeyError:
|
||||
geo_value = super().__get__(instance, cls)
|
||||
|
||||
if isinstance(geo_value, self._klass):
|
||||
geo_obj = geo_value
|
||||
elif (geo_value is None) or (geo_value == ""):
|
||||
geo_obj = None
|
||||
else:
|
||||
# Otherwise, a geometry or raster object is built using the field's
|
||||
# contents, and the model's corresponding attribute is set.
|
||||
geo_obj = self._load_func(geo_value)
|
||||
setattr(instance, self.field.attname, geo_obj)
|
||||
return geo_obj
|
||||
|
||||
def __set__(self, instance, value):
|
||||
"""
|
||||
Retrieve the proxied geometry or raster with the corresponding class
|
||||
specified during initialization.
|
||||
|
||||
To set geometries, use values of None, HEXEWKB, or WKT.
|
||||
To set rasters, use JSON or dict values.
|
||||
"""
|
||||
# The geographic type of the field.
|
||||
gtype = self.field.geom_type
|
||||
|
||||
if gtype == "RASTER" and (
|
||||
value is None or isinstance(value, (str, dict, self._klass))
|
||||
):
|
||||
# For raster fields, ensure input is None or a string, dict, or
|
||||
# raster instance.
|
||||
pass
|
||||
elif isinstance(value, self._klass):
|
||||
# The geometry type must match that of the field -- unless the
|
||||
# general GeometryField is used.
|
||||
if value.srid is None:
|
||||
# Assigning the field SRID if the geometry has no SRID.
|
||||
value.srid = self.field.srid
|
||||
elif value is None or isinstance(value, (str, memoryview)):
|
||||
# Set geometries with None, WKT, HEX, or WKB
|
||||
pass
|
||||
else:
|
||||
raise TypeError(
|
||||
"Cannot set %s SpatialProxy (%s) with value of type: %s"
|
||||
% (instance.__class__.__name__, gtype, type(value))
|
||||
)
|
||||
|
||||
# Setting the objects dictionary with the value, and returning.
|
||||
instance.__dict__[self.field.attname] = value
|
||||
return value
|
@ -0,0 +1,6 @@
|
||||
from django.contrib.gis.db.models.sql.conversion import AreaField, DistanceField
|
||||
|
||||
__all__ = [
|
||||
"AreaField",
|
||||
"DistanceField",
|
||||
]
|
@ -0,0 +1,73 @@
|
||||
"""
|
||||
This module holds simple classes to convert geospatial values from the
|
||||
database.
|
||||
"""
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib.gis.measure import Area, Distance
|
||||
from django.db import models
|
||||
|
||||
|
||||
class AreaField(models.FloatField):
|
||||
"Wrapper for Area values."
|
||||
|
||||
def __init__(self, geo_field):
|
||||
super().__init__()
|
||||
self.geo_field = geo_field
|
||||
|
||||
def get_prep_value(self, value):
|
||||
if not isinstance(value, Area):
|
||||
raise ValueError("AreaField only accepts Area measurement objects.")
|
||||
return value
|
||||
|
||||
def get_db_prep_value(self, value, connection, prepared=False):
|
||||
if value is None:
|
||||
return
|
||||
area_att = connection.ops.get_area_att_for_field(self.geo_field)
|
||||
return getattr(value, area_att) if area_att else value
|
||||
|
||||
def from_db_value(self, value, expression, connection):
|
||||
if value is None:
|
||||
return
|
||||
# If the database returns a Decimal, convert it to a float as expected
|
||||
# by the Python geometric objects.
|
||||
if isinstance(value, Decimal):
|
||||
value = float(value)
|
||||
# If the units are known, convert value into area measure.
|
||||
area_att = connection.ops.get_area_att_for_field(self.geo_field)
|
||||
return Area(**{area_att: value}) if area_att else value
|
||||
|
||||
def get_internal_type(self):
|
||||
return "AreaField"
|
||||
|
||||
|
||||
class DistanceField(models.FloatField):
|
||||
"Wrapper for Distance values."
|
||||
|
||||
def __init__(self, geo_field):
|
||||
super().__init__()
|
||||
self.geo_field = geo_field
|
||||
|
||||
def get_prep_value(self, value):
|
||||
if isinstance(value, Distance):
|
||||
return value
|
||||
return super().get_prep_value(value)
|
||||
|
||||
def get_db_prep_value(self, value, connection, prepared=False):
|
||||
if not isinstance(value, Distance):
|
||||
return value
|
||||
distance_att = connection.ops.get_distance_att_for_field(self.geo_field)
|
||||
if not distance_att:
|
||||
raise ValueError(
|
||||
"Distance measure is supplied, but units are unknown for result."
|
||||
)
|
||||
return getattr(value, distance_att)
|
||||
|
||||
def from_db_value(self, value, expression, connection):
|
||||
if value is None:
|
||||
return
|
||||
distance_att = connection.ops.get_distance_att_for_field(self.geo_field)
|
||||
return Distance(**{distance_att: value}) if distance_att else value
|
||||
|
||||
def get_internal_type(self):
|
||||
return "DistanceField"
|
@ -0,0 +1,151 @@
|
||||
from django.contrib.syndication.views import Feed as BaseFeed
|
||||
from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed
|
||||
|
||||
|
||||
class GeoFeedMixin:
|
||||
"""
|
||||
This mixin provides the necessary routines for SyndicationFeed subclasses
|
||||
to produce simple GeoRSS or W3C Geo elements.
|
||||
"""
|
||||
|
||||
def georss_coords(self, coords):
|
||||
"""
|
||||
In GeoRSS coordinate pairs are ordered by lat/lon and separated by
|
||||
a single white space. Given a tuple of coordinates, return a string
|
||||
GeoRSS representation.
|
||||
"""
|
||||
return " ".join("%f %f" % (coord[1], coord[0]) for coord in coords)
|
||||
|
||||
def add_georss_point(self, handler, coords, w3c_geo=False):
|
||||
"""
|
||||
Adds a GeoRSS point with the given coords using the given handler.
|
||||
Handles the differences between simple GeoRSS and the more popular
|
||||
W3C Geo specification.
|
||||
"""
|
||||
if w3c_geo:
|
||||
lon, lat = coords[:2]
|
||||
handler.addQuickElement("geo:lat", "%f" % lat)
|
||||
handler.addQuickElement("geo:lon", "%f" % lon)
|
||||
else:
|
||||
handler.addQuickElement("georss:point", self.georss_coords((coords,)))
|
||||
|
||||
def add_georss_element(self, handler, item, w3c_geo=False):
|
||||
"""Add a GeoRSS XML element using the given item and handler."""
|
||||
# Getting the Geometry object.
|
||||
geom = item.get("geometry")
|
||||
if geom is not None:
|
||||
if isinstance(geom, (list, tuple)):
|
||||
# Special case if a tuple/list was passed in. The tuple may be
|
||||
# a point or a box
|
||||
box_coords = None
|
||||
if isinstance(geom[0], (list, tuple)):
|
||||
# Box: ( (X0, Y0), (X1, Y1) )
|
||||
if len(geom) == 2:
|
||||
box_coords = geom
|
||||
else:
|
||||
raise ValueError("Only should be two sets of coordinates.")
|
||||
else:
|
||||
if len(geom) == 2:
|
||||
# Point: (X, Y)
|
||||
self.add_georss_point(handler, geom, w3c_geo=w3c_geo)
|
||||
elif len(geom) == 4:
|
||||
# Box: (X0, Y0, X1, Y1)
|
||||
box_coords = (geom[:2], geom[2:])
|
||||
else:
|
||||
raise ValueError("Only should be 2 or 4 numeric elements.")
|
||||
# If a GeoRSS box was given via tuple.
|
||||
if box_coords is not None:
|
||||
if w3c_geo:
|
||||
raise ValueError(
|
||||
"Cannot use simple GeoRSS box in W3C Geo feeds."
|
||||
)
|
||||
handler.addQuickElement(
|
||||
"georss:box", self.georss_coords(box_coords)
|
||||
)
|
||||
else:
|
||||
# Getting the lowercase geometry type.
|
||||
gtype = str(geom.geom_type).lower()
|
||||
if gtype == "point":
|
||||
self.add_georss_point(handler, geom.coords, w3c_geo=w3c_geo)
|
||||
else:
|
||||
if w3c_geo:
|
||||
raise ValueError("W3C Geo only supports Point geometries.")
|
||||
# For formatting consistent w/the GeoRSS simple standard:
|
||||
# http://georss.org/1.0#simple
|
||||
if gtype in ("linestring", "linearring"):
|
||||
handler.addQuickElement(
|
||||
"georss:line", self.georss_coords(geom.coords)
|
||||
)
|
||||
elif gtype in ("polygon",):
|
||||
# Only support the exterior ring.
|
||||
handler.addQuickElement(
|
||||
"georss:polygon", self.georss_coords(geom[0].coords)
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
'Geometry type "%s" not supported.' % geom.geom_type
|
||||
)
|
||||
|
||||
|
||||
# ### SyndicationFeed subclasses ###
|
||||
class GeoRSSFeed(Rss201rev2Feed, GeoFeedMixin):
|
||||
def rss_attributes(self):
|
||||
attrs = super().rss_attributes()
|
||||
attrs["xmlns:georss"] = "http://www.georss.org/georss"
|
||||
return attrs
|
||||
|
||||
def add_item_elements(self, handler, item):
|
||||
super().add_item_elements(handler, item)
|
||||
self.add_georss_element(handler, item)
|
||||
|
||||
def add_root_elements(self, handler):
|
||||
super().add_root_elements(handler)
|
||||
self.add_georss_element(handler, self.feed)
|
||||
|
||||
|
||||
class GeoAtom1Feed(Atom1Feed, GeoFeedMixin):
|
||||
def root_attributes(self):
|
||||
attrs = super().root_attributes()
|
||||
attrs["xmlns:georss"] = "http://www.georss.org/georss"
|
||||
return attrs
|
||||
|
||||
def add_item_elements(self, handler, item):
|
||||
super().add_item_elements(handler, item)
|
||||
self.add_georss_element(handler, item)
|
||||
|
||||
def add_root_elements(self, handler):
|
||||
super().add_root_elements(handler)
|
||||
self.add_georss_element(handler, self.feed)
|
||||
|
||||
|
||||
class W3CGeoFeed(Rss201rev2Feed, GeoFeedMixin):
|
||||
def rss_attributes(self):
|
||||
attrs = super().rss_attributes()
|
||||
attrs["xmlns:geo"] = "http://www.w3.org/2003/01/geo/wgs84_pos#"
|
||||
return attrs
|
||||
|
||||
def add_item_elements(self, handler, item):
|
||||
super().add_item_elements(handler, item)
|
||||
self.add_georss_element(handler, item, w3c_geo=True)
|
||||
|
||||
def add_root_elements(self, handler):
|
||||
super().add_root_elements(handler)
|
||||
self.add_georss_element(handler, self.feed, w3c_geo=True)
|
||||
|
||||
|
||||
# ### Feed subclass ###
|
||||
class Feed(BaseFeed):
|
||||
"""
|
||||
This is a subclass of the `Feed` from `django.contrib.syndication`.
|
||||
This allows users to define a `geometry(obj)` and/or `item_geometry(item)`
|
||||
methods on their own subclasses so that geo-referenced information may
|
||||
placed in the feed.
|
||||
"""
|
||||
|
||||
feed_type = GeoRSSFeed
|
||||
|
||||
def feed_extra_kwargs(self, obj):
|
||||
return {"geometry": self._get_dynamic_attr("geometry", obj)}
|
||||
|
||||
def item_extra_kwargs(self, item):
|
||||
return {"geometry": self._get_dynamic_attr("item_geometry", item)}
|
@ -0,0 +1,13 @@
|
||||
from django.forms import * # NOQA
|
||||
|
||||
from .fields import ( # NOQA
|
||||
GeometryCollectionField,
|
||||
GeometryField,
|
||||
LineStringField,
|
||||
MultiLineStringField,
|
||||
MultiPointField,
|
||||
MultiPolygonField,
|
||||
PointField,
|
||||
PolygonField,
|
||||
)
|
||||
from .widgets import BaseGeometryWidget, OpenLayersWidget, OSMWidget # NOQA
|
@ -0,0 +1,144 @@
|
||||
from django import forms
|
||||
from django.contrib.gis.gdal import GDALException
|
||||
from django.contrib.gis.geos import GEOSException, GEOSGeometry
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .widgets import OpenLayersWidget
|
||||
|
||||
|
||||
class GeometryField(forms.Field):
|
||||
"""
|
||||
This is the basic form field for a Geometry. Any textual input that is
|
||||
accepted by GEOSGeometry is accepted by this form. By default,
|
||||
this includes WKT, HEXEWKB, WKB (in a buffer), and GeoJSON.
|
||||
"""
|
||||
|
||||
widget = OpenLayersWidget
|
||||
geom_type = "GEOMETRY"
|
||||
|
||||
default_error_messages = {
|
||||
"required": _("No geometry value provided."),
|
||||
"invalid_geom": _("Invalid geometry value."),
|
||||
"invalid_geom_type": _("Invalid geometry type."),
|
||||
"transform_error": _(
|
||||
"An error occurred when transforming the geometry "
|
||||
"to the SRID of the geometry form field."
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *, srid=None, geom_type=None, **kwargs):
|
||||
self.srid = srid
|
||||
if geom_type is not None:
|
||||
self.geom_type = geom_type
|
||||
super().__init__(**kwargs)
|
||||
self.widget.attrs["geom_type"] = self.geom_type
|
||||
|
||||
def to_python(self, value):
|
||||
"""Transform the value to a Geometry object."""
|
||||
if value in self.empty_values:
|
||||
return None
|
||||
|
||||
if not isinstance(value, GEOSGeometry):
|
||||
if hasattr(self.widget, "deserialize"):
|
||||
try:
|
||||
value = self.widget.deserialize(value)
|
||||
except GDALException:
|
||||
value = None
|
||||
else:
|
||||
try:
|
||||
value = GEOSGeometry(value)
|
||||
except (GEOSException, ValueError, TypeError):
|
||||
value = None
|
||||
if value is None:
|
||||
raise ValidationError(
|
||||
self.error_messages["invalid_geom"], code="invalid_geom"
|
||||
)
|
||||
|
||||
# Try to set the srid
|
||||
if not value.srid:
|
||||
try:
|
||||
value.srid = self.widget.map_srid
|
||||
except AttributeError:
|
||||
if self.srid:
|
||||
value.srid = self.srid
|
||||
return value
|
||||
|
||||
def clean(self, value):
|
||||
"""
|
||||
Validate that the input value can be converted to a Geometry object
|
||||
and return it. Raise a ValidationError if the value cannot be
|
||||
instantiated as a Geometry.
|
||||
"""
|
||||
geom = super().clean(value)
|
||||
if geom is None:
|
||||
return geom
|
||||
|
||||
# Ensuring that the geometry is of the correct type (indicated
|
||||
# using the OGC string label).
|
||||
if (
|
||||
str(geom.geom_type).upper() != self.geom_type
|
||||
and self.geom_type != "GEOMETRY"
|
||||
):
|
||||
raise ValidationError(
|
||||
self.error_messages["invalid_geom_type"], code="invalid_geom_type"
|
||||
)
|
||||
|
||||
# Transforming the geometry if the SRID was set.
|
||||
if self.srid and self.srid != -1 and self.srid != geom.srid:
|
||||
try:
|
||||
geom.transform(self.srid)
|
||||
except GEOSException:
|
||||
raise ValidationError(
|
||||
self.error_messages["transform_error"], code="transform_error"
|
||||
)
|
||||
|
||||
return geom
|
||||
|
||||
def has_changed(self, initial, data):
|
||||
"""Compare geographic value of data with its initial value."""
|
||||
|
||||
try:
|
||||
data = self.to_python(data)
|
||||
initial = self.to_python(initial)
|
||||
except ValidationError:
|
||||
return True
|
||||
|
||||
# Only do a geographic comparison if both values are available
|
||||
if initial and data:
|
||||
data.transform(initial.srid)
|
||||
# If the initial value was not added by the browser, the geometry
|
||||
# provided may be slightly different, the first time it is saved.
|
||||
# The comparison is done with a very low tolerance.
|
||||
return not initial.equals_exact(data, tolerance=0.000001)
|
||||
else:
|
||||
# Check for change of state of existence
|
||||
return bool(initial) != bool(data)
|
||||
|
||||
|
||||
class GeometryCollectionField(GeometryField):
|
||||
geom_type = "GEOMETRYCOLLECTION"
|
||||
|
||||
|
||||
class PointField(GeometryField):
|
||||
geom_type = "POINT"
|
||||
|
||||
|
||||
class MultiPointField(GeometryField):
|
||||
geom_type = "MULTIPOINT"
|
||||
|
||||
|
||||
class LineStringField(GeometryField):
|
||||
geom_type = "LINESTRING"
|
||||
|
||||
|
||||
class MultiLineStringField(GeometryField):
|
||||
geom_type = "MULTILINESTRING"
|
||||
|
||||
|
||||
class PolygonField(GeometryField):
|
||||
geom_type = "POLYGON"
|
||||
|
||||
|
||||
class MultiPolygonField(GeometryField):
|
||||
geom_type = "MULTIPOLYGON"
|
@ -0,0 +1,140 @@
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.gis import gdal
|
||||
from django.contrib.gis.geometry import json_regex
|
||||
from django.contrib.gis.geos import GEOSException, GEOSGeometry
|
||||
from django.forms.widgets import Widget
|
||||
from django.utils import translation
|
||||
from django.utils.deprecation import RemovedInDjango51Warning
|
||||
|
||||
logger = logging.getLogger("django.contrib.gis")
|
||||
|
||||
|
||||
class BaseGeometryWidget(Widget):
|
||||
"""
|
||||
The base class for rich geometry widgets.
|
||||
Render a map using the WKT of the geometry.
|
||||
"""
|
||||
|
||||
geom_type = "GEOMETRY"
|
||||
map_srid = 4326
|
||||
map_width = 600 # RemovedInDjango51Warning
|
||||
map_height = 400 # RemovedInDjango51Warning
|
||||
display_raw = False
|
||||
|
||||
supports_3d = False
|
||||
template_name = "" # set on subclasses
|
||||
|
||||
def __init__(self, attrs=None):
|
||||
self.attrs = {}
|
||||
for key in ("geom_type", "map_srid", "map_width", "map_height", "display_raw"):
|
||||
self.attrs[key] = getattr(self, key)
|
||||
if (
|
||||
(attrs and ("map_width" in attrs or "map_height" in attrs))
|
||||
or self.map_width != 600
|
||||
or self.map_height != 400
|
||||
):
|
||||
warnings.warn(
|
||||
"The map_height and map_width widget attributes are deprecated. Please "
|
||||
"use CSS to size map widgets.",
|
||||
category=RemovedInDjango51Warning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if attrs:
|
||||
self.attrs.update(attrs)
|
||||
|
||||
def serialize(self, value):
|
||||
return value.wkt if value else ""
|
||||
|
||||
def deserialize(self, value):
|
||||
try:
|
||||
return GEOSGeometry(value)
|
||||
except (GEOSException, ValueError, TypeError) as err:
|
||||
logger.error("Error creating geometry from value '%s' (%s)", value, err)
|
||||
return None
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super().get_context(name, value, attrs)
|
||||
# If a string reaches here (via a validation error on another
|
||||
# field) then just reconstruct the Geometry.
|
||||
if value and isinstance(value, str):
|
||||
value = self.deserialize(value)
|
||||
|
||||
if value:
|
||||
# Check that srid of value and map match
|
||||
if value.srid and value.srid != self.map_srid:
|
||||
try:
|
||||
ogr = value.ogr
|
||||
ogr.transform(self.map_srid)
|
||||
value = ogr
|
||||
except gdal.GDALException as err:
|
||||
logger.error(
|
||||
"Error transforming geometry from srid '%s' to srid '%s' (%s)",
|
||||
value.srid,
|
||||
self.map_srid,
|
||||
err,
|
||||
)
|
||||
|
||||
geom_type = gdal.OGRGeomType(self.attrs["geom_type"]).name
|
||||
context.update(
|
||||
self.build_attrs(
|
||||
self.attrs,
|
||||
{
|
||||
"name": name,
|
||||
"module": "geodjango_%s" % name.replace("-", "_"), # JS-safe
|
||||
"serialized": self.serialize(value),
|
||||
"geom_type": "Geometry" if geom_type == "Unknown" else geom_type,
|
||||
"STATIC_URL": settings.STATIC_URL,
|
||||
"LANGUAGE_BIDI": translation.get_language_bidi(),
|
||||
**(attrs or {}),
|
||||
},
|
||||
)
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class OpenLayersWidget(BaseGeometryWidget):
|
||||
template_name = "gis/openlayers.html"
|
||||
map_srid = 3857
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
"all": (
|
||||
"https://cdn.jsdelivr.net/npm/ol@v7.2.2/ol.css",
|
||||
"gis/css/ol3.css",
|
||||
)
|
||||
}
|
||||
js = (
|
||||
"https://cdn.jsdelivr.net/npm/ol@v7.2.2/dist/ol.js",
|
||||
"gis/js/OLMapWidget.js",
|
||||
)
|
||||
|
||||
def serialize(self, value):
|
||||
return value.json if value else ""
|
||||
|
||||
def deserialize(self, value):
|
||||
geom = super().deserialize(value)
|
||||
# GeoJSON assumes WGS84 (4326). Use the map's SRID instead.
|
||||
if geom and json_regex.match(value) and self.map_srid != 4326:
|
||||
geom.srid = self.map_srid
|
||||
return geom
|
||||
|
||||
|
||||
class OSMWidget(OpenLayersWidget):
|
||||
"""
|
||||
An OpenLayers/OpenStreetMap-based widget.
|
||||
"""
|
||||
|
||||
template_name = "gis/openlayers-osm.html"
|
||||
default_lon = 5
|
||||
default_lat = 47
|
||||
default_zoom = 12
|
||||
|
||||
def __init__(self, attrs=None):
|
||||
super().__init__()
|
||||
for key in ("default_lon", "default_lat", "default_zoom"):
|
||||
self.attrs[key] = getattr(self, key)
|
||||
if attrs:
|
||||
self.attrs.update(attrs)
|
@ -0,0 +1,28 @@
|
||||
Copyright (c) 2007-2009, Justin Bronn
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of OGRGeometry nor the names of its contributors may be used
|
||||
to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
@ -0,0 +1,58 @@
|
||||
"""
|
||||
This module houses ctypes interfaces for GDAL objects. The following GDAL
|
||||
objects are supported:
|
||||
|
||||
CoordTransform: Used for coordinate transformations from one spatial
|
||||
reference system to another.
|
||||
|
||||
Driver: Wraps an OGR data source driver.
|
||||
|
||||
DataSource: Wrapper for the OGR data source object, supports
|
||||
OGR-supported data sources.
|
||||
|
||||
Envelope: A ctypes structure for bounding boxes (GDAL library
|
||||
not required).
|
||||
|
||||
OGRGeometry: Object for accessing OGR Geometry functionality.
|
||||
|
||||
OGRGeomType: A class for representing the different OGR Geometry
|
||||
types (GDAL library not required).
|
||||
|
||||
SpatialReference: Represents OSR Spatial Reference objects.
|
||||
|
||||
The GDAL library will be imported from the system path using the default
|
||||
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
|
||||
library on your system.
|
||||
"""
|
||||
from django.contrib.gis.gdal.datasource import DataSource
|
||||
from django.contrib.gis.gdal.driver import Driver
|
||||
from django.contrib.gis.gdal.envelope import Envelope
|
||||
from django.contrib.gis.gdal.error import GDALException, SRSException, check_err
|
||||
from django.contrib.gis.gdal.geometries import OGRGeometry
|
||||
from django.contrib.gis.gdal.geomtype import OGRGeomType
|
||||
from django.contrib.gis.gdal.libgdal import (
|
||||
GDAL_VERSION,
|
||||
gdal_full_version,
|
||||
gdal_version,
|
||||
)
|
||||
from django.contrib.gis.gdal.raster.source import GDALRaster
|
||||
from django.contrib.gis.gdal.srs import AxisOrder, CoordTransform, SpatialReference
|
||||
|
||||
__all__ = (
|
||||
"AxisOrder",
|
||||
"Driver",
|
||||
"DataSource",
|
||||
"CoordTransform",
|
||||
"Envelope",
|
||||
"GDALException",
|
||||
"GDALRaster",
|
||||
"GDAL_VERSION",
|
||||
"OGRGeometry",
|
||||
"OGRGeomType",
|
||||
"SpatialReference",
|
||||
"SRSException",
|
||||
"check_err",
|
||||
"gdal_version",
|
||||
"gdal_full_version",
|
||||
)
|
@ -0,0 +1,6 @@
|
||||
from django.contrib.gis.gdal.error import GDALException
|
||||
from django.contrib.gis.ptr import CPointerBase
|
||||
|
||||
|
||||
class GDALBase(CPointerBase):
|
||||
null_ptr_exception_class = GDALException
|
@ -0,0 +1,126 @@
|
||||
"""
|
||||
DataSource is a wrapper for the OGR Data Source object, which provides
|
||||
an interface for reading vector geometry data from many different file
|
||||
formats (including ESRI shapefiles).
|
||||
|
||||
When instantiating a DataSource object, use the filename of a
|
||||
GDAL-supported data source. For example, a SHP file or a
|
||||
TIGER/Line file from the government.
|
||||
|
||||
The ds_driver keyword is used internally when a ctypes pointer
|
||||
is passed in directly.
|
||||
|
||||
Example:
|
||||
ds = DataSource('/home/foo/bar.shp')
|
||||
for layer in ds:
|
||||
for feature in layer:
|
||||
# Getting the geometry for the feature.
|
||||
g = feature.geom
|
||||
|
||||
# Getting the 'description' field for the feature.
|
||||
desc = feature['description']
|
||||
|
||||
# We can also increment through all of the fields
|
||||
# attached to this feature.
|
||||
for field in feature:
|
||||
# Get the name of the field (e.g. 'description')
|
||||
nm = field.name
|
||||
|
||||
# Get the type (integer) of the field, e.g. 0 => OFTInteger
|
||||
t = field.type
|
||||
|
||||
# Returns the value the field; OFTIntegers return ints,
|
||||
# OFTReal returns floats, all else returns string.
|
||||
val = field.value
|
||||
"""
|
||||
from ctypes import byref
|
||||
from pathlib import Path
|
||||
|
||||
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.layer import Layer
|
||||
from django.contrib.gis.gdal.prototypes import ds as capi
|
||||
from django.utils.encoding import force_bytes, force_str
|
||||
|
||||
|
||||
# For more information, see the OGR C API documentation:
|
||||
# https://gdal.org/api/vector_c_api.html
|
||||
#
|
||||
# The OGR_DS_* routines are relevant here.
|
||||
class DataSource(GDALBase):
|
||||
"Wraps an OGR Data Source object."
|
||||
destructor = capi.destroy_ds
|
||||
|
||||
def __init__(self, ds_input, ds_driver=False, write=False, encoding="utf-8"):
|
||||
# The write flag.
|
||||
if write:
|
||||
self._write = 1
|
||||
else:
|
||||
self._write = 0
|
||||
# See also https://gdal.org/development/rfc/rfc23_ogr_unicode.html
|
||||
self.encoding = encoding
|
||||
|
||||
Driver.ensure_registered()
|
||||
|
||||
if isinstance(ds_input, (str, Path)):
|
||||
# The data source driver is a void pointer.
|
||||
ds_driver = Driver.ptr_type()
|
||||
try:
|
||||
# OGROpen will auto-detect the data source type.
|
||||
ds = capi.open_ds(force_bytes(ds_input), self._write, byref(ds_driver))
|
||||
except GDALException:
|
||||
# Making the error message more clear rather than something
|
||||
# like "Invalid pointer returned from OGROpen".
|
||||
raise GDALException('Could not open the datasource at "%s"' % ds_input)
|
||||
elif isinstance(ds_input, self.ptr_type) and isinstance(
|
||||
ds_driver, Driver.ptr_type
|
||||
):
|
||||
ds = ds_input
|
||||
else:
|
||||
raise GDALException("Invalid data source input type: %s" % type(ds_input))
|
||||
|
||||
if ds:
|
||||
self.ptr = ds
|
||||
self.driver = Driver(ds_driver)
|
||||
else:
|
||||
# Raise an exception if the returned pointer is NULL
|
||||
raise GDALException('Invalid data source file "%s"' % ds_input)
|
||||
|
||||
def __getitem__(self, index):
|
||||
"Allows use of the index [] operator to get a layer at the index."
|
||||
if isinstance(index, str):
|
||||
try:
|
||||
layer = capi.get_layer_by_name(self.ptr, force_bytes(index))
|
||||
except GDALException:
|
||||
raise IndexError("Invalid OGR layer name given: %s." % index)
|
||||
elif isinstance(index, int):
|
||||
if 0 <= index < self.layer_count:
|
||||
layer = capi.get_layer(self._ptr, index)
|
||||
else:
|
||||
raise IndexError(
|
||||
"Index out of range when accessing layers in a datasource: %s."
|
||||
% index
|
||||
)
|
||||
else:
|
||||
raise TypeError("Invalid index type: %s" % type(index))
|
||||
return Layer(layer, self)
|
||||
|
||||
def __len__(self):
|
||||
"Return the number of layers within the data source."
|
||||
return self.layer_count
|
||||
|
||||
def __str__(self):
|
||||
"Return OGR GetName and Driver for the Data Source."
|
||||
return "%s (%s)" % (self.name, self.driver)
|
||||
|
||||
@property
|
||||
def layer_count(self):
|
||||
"Return the number of layers in the data source."
|
||||
return capi.get_layer_count(self._ptr)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"Return the name of the data source."
|
||||
name = capi.get_ds_name(self._ptr)
|
||||
return force_str(name, self.encoding, strings_only=True)
|
@ -0,0 +1,103 @@
|
||||
from ctypes import 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 ds as vcapi
|
||||
from django.contrib.gis.gdal.prototypes import raster as rcapi
|
||||
from django.utils.encoding import force_bytes, force_str
|
||||
|
||||
|
||||
class Driver(GDALBase):
|
||||
"""
|
||||
Wrap a GDAL/OGR Data Source Driver.
|
||||
For more information, see the C API documentation:
|
||||
https://gdal.org/api/vector_c_api.html
|
||||
https://gdal.org/api/raster_c_api.html
|
||||
"""
|
||||
|
||||
# Case-insensitive aliases for some GDAL/OGR Drivers.
|
||||
# For a complete list of original driver names see
|
||||
# https://gdal.org/drivers/vector/
|
||||
# https://gdal.org/drivers/raster/
|
||||
_alias = {
|
||||
# vector
|
||||
"esri": "ESRI Shapefile",
|
||||
"shp": "ESRI Shapefile",
|
||||
"shape": "ESRI Shapefile",
|
||||
"tiger": "TIGER",
|
||||
"tiger/line": "TIGER",
|
||||
# raster
|
||||
"tiff": "GTiff",
|
||||
"tif": "GTiff",
|
||||
"jpeg": "JPEG",
|
||||
"jpg": "JPEG",
|
||||
}
|
||||
|
||||
def __init__(self, dr_input):
|
||||
"""
|
||||
Initialize an GDAL/OGR driver on either a string or integer input.
|
||||
"""
|
||||
if isinstance(dr_input, str):
|
||||
# If a string name of the driver was passed in
|
||||
self.ensure_registered()
|
||||
|
||||
# Checking the alias dictionary (case-insensitive) to see if an
|
||||
# alias exists for the given driver.
|
||||
if dr_input.lower() in self._alias:
|
||||
name = self._alias[dr_input.lower()]
|
||||
else:
|
||||
name = dr_input
|
||||
|
||||
# Attempting to get the GDAL/OGR driver by the string name.
|
||||
for iface in (vcapi, rcapi):
|
||||
driver = c_void_p(iface.get_driver_by_name(force_bytes(name)))
|
||||
if driver:
|
||||
break
|
||||
elif isinstance(dr_input, int):
|
||||
self.ensure_registered()
|
||||
for iface in (vcapi, rcapi):
|
||||
driver = iface.get_driver(dr_input)
|
||||
if driver:
|
||||
break
|
||||
elif isinstance(dr_input, c_void_p):
|
||||
driver = dr_input
|
||||
else:
|
||||
raise GDALException(
|
||||
"Unrecognized input type for GDAL/OGR Driver: %s" % type(dr_input)
|
||||
)
|
||||
|
||||
# Making sure we get a valid pointer to the OGR Driver
|
||||
if not driver:
|
||||
raise GDALException(
|
||||
"Could not initialize GDAL/OGR Driver on input: %s" % dr_input
|
||||
)
|
||||
self.ptr = driver
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def ensure_registered(cls):
|
||||
"""
|
||||
Attempt to register all the data source drivers.
|
||||
"""
|
||||
# Only register all if the driver counts are 0 (or else all drivers
|
||||
# will be registered over and over again)
|
||||
if not vcapi.get_driver_count():
|
||||
vcapi.register_all()
|
||||
if not rcapi.get_driver_count():
|
||||
rcapi.register_all()
|
||||
|
||||
@classmethod
|
||||
def driver_count(cls):
|
||||
"""
|
||||
Return the number of GDAL/OGR data source drivers registered.
|
||||
"""
|
||||
return vcapi.get_driver_count() + rcapi.get_driver_count()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""
|
||||
Return description/name string for this driver.
|
||||
"""
|
||||
return force_str(rcapi.get_driver_description(self.ptr))
|
@ -0,0 +1,203 @@
|
||||
"""
|
||||
The GDAL/OGR library uses an Envelope structure to hold the bounding
|
||||
box information for a geometry. The envelope (bounding box) contains
|
||||
two pairs of coordinates, one for the lower left coordinate and one
|
||||
for the upper right coordinate:
|
||||
|
||||
+----------o Upper right; (max_x, max_y)
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
Lower left (min_x, min_y) o----------+
|
||||
"""
|
||||
from ctypes import Structure, c_double
|
||||
|
||||
from django.contrib.gis.gdal.error import GDALException
|
||||
|
||||
|
||||
# The OGR definition of an Envelope is a C structure containing four doubles.
|
||||
# See the 'ogr_core.h' source file for more information:
|
||||
# https://gdal.org/doxygen/ogr__core_8h_source.html
|
||||
class OGREnvelope(Structure):
|
||||
"Represent the OGREnvelope C Structure."
|
||||
_fields_ = [
|
||||
("MinX", c_double),
|
||||
("MaxX", c_double),
|
||||
("MinY", c_double),
|
||||
("MaxY", c_double),
|
||||
]
|
||||
|
||||
|
||||
class Envelope:
|
||||
"""
|
||||
The Envelope object is a C structure that contains the minimum and
|
||||
maximum X, Y coordinates for a rectangle bounding box. The naming
|
||||
of the variables is compatible with the OGR Envelope structure.
|
||||
"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""
|
||||
The initialization function may take an OGREnvelope structure, 4-element
|
||||
tuple or list, or 4 individual arguments.
|
||||
"""
|
||||
|
||||
if len(args) == 1:
|
||||
if isinstance(args[0], OGREnvelope):
|
||||
# OGREnvelope (a ctypes Structure) was passed in.
|
||||
self._envelope = args[0]
|
||||
elif isinstance(args[0], (tuple, list)):
|
||||
# A tuple was passed in.
|
||||
if len(args[0]) != 4:
|
||||
raise GDALException(
|
||||
"Incorrect number of tuple elements (%d)." % len(args[0])
|
||||
)
|
||||
else:
|
||||
self._from_sequence(args[0])
|
||||
else:
|
||||
raise TypeError("Incorrect type of argument: %s" % type(args[0]))
|
||||
elif len(args) == 4:
|
||||
# Individual parameters passed in.
|
||||
# Thanks to ww for the help
|
||||
self._from_sequence([float(a) for a in args])
|
||||
else:
|
||||
raise GDALException("Incorrect number (%d) of arguments." % len(args))
|
||||
|
||||
# Checking the x,y coordinates
|
||||
if self.min_x > self.max_x:
|
||||
raise GDALException("Envelope minimum X > maximum X.")
|
||||
if self.min_y > self.max_y:
|
||||
raise GDALException("Envelope minimum Y > maximum Y.")
|
||||
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
Return True if the envelopes are equivalent; can compare against
|
||||
other Envelopes and 4-tuples.
|
||||
"""
|
||||
if isinstance(other, Envelope):
|
||||
return (
|
||||
(self.min_x == other.min_x)
|
||||
and (self.min_y == other.min_y)
|
||||
and (self.max_x == other.max_x)
|
||||
and (self.max_y == other.max_y)
|
||||
)
|
||||
elif isinstance(other, tuple) and len(other) == 4:
|
||||
return (
|
||||
(self.min_x == other[0])
|
||||
and (self.min_y == other[1])
|
||||
and (self.max_x == other[2])
|
||||
and (self.max_y == other[3])
|
||||
)
|
||||
else:
|
||||
raise GDALException("Equivalence testing only works with other Envelopes.")
|
||||
|
||||
def __str__(self):
|
||||
"Return a string representation of the tuple."
|
||||
return str(self.tuple)
|
||||
|
||||
def _from_sequence(self, seq):
|
||||
"Initialize the C OGR Envelope structure from the given sequence."
|
||||
self._envelope = OGREnvelope()
|
||||
self._envelope.MinX = seq[0]
|
||||
self._envelope.MinY = seq[1]
|
||||
self._envelope.MaxX = seq[2]
|
||||
self._envelope.MaxY = seq[3]
|
||||
|
||||
def expand_to_include(self, *args):
|
||||
"""
|
||||
Modify the envelope to expand to include the boundaries of
|
||||
the passed-in 2-tuple (a point), 4-tuple (an extent) or
|
||||
envelope.
|
||||
"""
|
||||
# We provide a number of different signatures for this method,
|
||||
# and the logic here is all about converting them into a
|
||||
# 4-tuple single parameter which does the actual work of
|
||||
# expanding the envelope.
|
||||
if len(args) == 1:
|
||||
if isinstance(args[0], Envelope):
|
||||
return self.expand_to_include(args[0].tuple)
|
||||
elif hasattr(args[0], "x") and hasattr(args[0], "y"):
|
||||
return self.expand_to_include(
|
||||
args[0].x, args[0].y, args[0].x, args[0].y
|
||||
)
|
||||
elif isinstance(args[0], (tuple, list)):
|
||||
# A tuple was passed in.
|
||||
if len(args[0]) == 2:
|
||||
return self.expand_to_include(
|
||||
(args[0][0], args[0][1], args[0][0], args[0][1])
|
||||
)
|
||||
elif len(args[0]) == 4:
|
||||
(minx, miny, maxx, maxy) = args[0]
|
||||
if minx < self._envelope.MinX:
|
||||
self._envelope.MinX = minx
|
||||
if miny < self._envelope.MinY:
|
||||
self._envelope.MinY = miny
|
||||
if maxx > self._envelope.MaxX:
|
||||
self._envelope.MaxX = maxx
|
||||
if maxy > self._envelope.MaxY:
|
||||
self._envelope.MaxY = maxy
|
||||
else:
|
||||
raise GDALException(
|
||||
"Incorrect number of tuple elements (%d)." % len(args[0])
|
||||
)
|
||||
else:
|
||||
raise TypeError("Incorrect type of argument: %s" % type(args[0]))
|
||||
elif len(args) == 2:
|
||||
# An x and an y parameter were passed in
|
||||
return self.expand_to_include((args[0], args[1], args[0], args[1]))
|
||||
elif len(args) == 4:
|
||||
# Individual parameters passed in.
|
||||
return self.expand_to_include(args)
|
||||
else:
|
||||
raise GDALException("Incorrect number (%d) of arguments." % len(args[0]))
|
||||
|
||||
@property
|
||||
def min_x(self):
|
||||
"Return the value of the minimum X coordinate."
|
||||
return self._envelope.MinX
|
||||
|
||||
@property
|
||||
def min_y(self):
|
||||
"Return the value of the minimum Y coordinate."
|
||||
return self._envelope.MinY
|
||||
|
||||
@property
|
||||
def max_x(self):
|
||||
"Return the value of the maximum X coordinate."
|
||||
return self._envelope.MaxX
|
||||
|
||||
@property
|
||||
def max_y(self):
|
||||
"Return the value of the maximum Y coordinate."
|
||||
return self._envelope.MaxY
|
||||
|
||||
@property
|
||||
def ur(self):
|
||||
"Return the upper-right coordinate."
|
||||
return (self.max_x, self.max_y)
|
||||
|
||||
@property
|
||||
def ll(self):
|
||||
"Return the lower-left coordinate."
|
||||
return (self.min_x, self.min_y)
|
||||
|
||||
@property
|
||||
def tuple(self):
|
||||
"Return a tuple representing the envelope."
|
||||
return (self.min_x, self.min_y, self.max_x, self.max_y)
|
||||
|
||||
@property
|
||||
def wkt(self):
|
||||
"Return WKT representing a Polygon for this envelope."
|
||||
# TODO: Fix significant figures.
|
||||
return "POLYGON((%s %s,%s %s,%s %s,%s %s,%s %s))" % (
|
||||
self.min_x,
|
||||
self.min_y,
|
||||
self.min_x,
|
||||
self.max_y,
|
||||
self.max_x,
|
||||
self.max_y,
|
||||
self.max_x,
|
||||
self.min_y,
|
||||
self.min_x,
|
||||
self.min_y,
|
||||
)
|
@ -0,0 +1,61 @@
|
||||
"""
|
||||
This module houses the GDAL & SRS Exception objects, and the
|
||||
check_err() routine which checks the status code returned by
|
||||
GDAL/OGR methods.
|
||||
"""
|
||||
|
||||
|
||||
# #### GDAL & SRS Exceptions ####
|
||||
class GDALException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SRSException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# #### GDAL/OGR error checking codes and routine ####
|
||||
|
||||
# OGR Error Codes
|
||||
OGRERR_DICT = {
|
||||
1: (GDALException, "Not enough data."),
|
||||
2: (GDALException, "Not enough memory."),
|
||||
3: (GDALException, "Unsupported geometry type."),
|
||||
4: (GDALException, "Unsupported operation."),
|
||||
5: (GDALException, "Corrupt data."),
|
||||
6: (GDALException, "OGR failure."),
|
||||
7: (SRSException, "Unsupported SRS."),
|
||||
8: (GDALException, "Invalid handle."),
|
||||
}
|
||||
|
||||
# CPL Error Codes
|
||||
# https://gdal.org/api/cpl.html#cpl-error-h
|
||||
CPLERR_DICT = {
|
||||
1: (GDALException, "AppDefined"),
|
||||
2: (GDALException, "OutOfMemory"),
|
||||
3: (GDALException, "FileIO"),
|
||||
4: (GDALException, "OpenFailed"),
|
||||
5: (GDALException, "IllegalArg"),
|
||||
6: (GDALException, "NotSupported"),
|
||||
7: (GDALException, "AssertionFailed"),
|
||||
8: (GDALException, "NoWriteAccess"),
|
||||
9: (GDALException, "UserInterrupt"),
|
||||
10: (GDALException, "ObjectNull"),
|
||||
}
|
||||
|
||||
ERR_NONE = 0
|
||||
|
||||
|
||||
def check_err(code, cpl=False):
|
||||
"""
|
||||
Check the given CPL/OGRERR and raise an exception where appropriate.
|
||||
"""
|
||||
err_dict = CPLERR_DICT if cpl else OGRERR_DICT
|
||||
|
||||
if code == ERR_NONE:
|
||||
return
|
||||
elif code in err_dict:
|
||||
e, msg = err_dict[code]
|
||||
raise e(msg)
|
||||
else:
|
||||
raise GDALException('Unknown error code: "%s"' % code)
|
@ -0,0 +1,120 @@
|
||||
from django.contrib.gis.gdal.base import GDALBase
|
||||
from django.contrib.gis.gdal.error import GDALException
|
||||
from django.contrib.gis.gdal.field import Field
|
||||
from django.contrib.gis.gdal.geometries import OGRGeometry, OGRGeomType
|
||||
from django.contrib.gis.gdal.prototypes import ds as capi
|
||||
from django.contrib.gis.gdal.prototypes import geom as geom_api
|
||||
from django.utils.encoding import force_bytes, force_str
|
||||
|
||||
|
||||
# For more information, see the OGR C API source code:
|
||||
# https://gdal.org/api/vector_c_api.html
|
||||
#
|
||||
# The OGR_F_* routines are relevant here.
|
||||
class Feature(GDALBase):
|
||||
"""
|
||||
This class that wraps an OGR Feature, needs to be instantiated
|
||||
from a Layer object.
|
||||
"""
|
||||
|
||||
destructor = capi.destroy_feature
|
||||
|
||||
def __init__(self, feat, layer):
|
||||
"""
|
||||
Initialize Feature from a pointer and its Layer object.
|
||||
"""
|
||||
if not feat:
|
||||
raise GDALException("Cannot create OGR Feature, invalid pointer given.")
|
||||
self.ptr = feat
|
||||
self._layer = layer
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""
|
||||
Get the Field object at the specified index, which may be either
|
||||
an integer or the Field's string label. Note that the Field object
|
||||
is not the field's _value_ -- use the `get` method instead to
|
||||
retrieve the value (e.g. an integer) instead of a Field instance.
|
||||
"""
|
||||
if isinstance(index, str):
|
||||
i = self.index(index)
|
||||
elif 0 <= index < self.num_fields:
|
||||
i = index
|
||||
else:
|
||||
raise IndexError(
|
||||
"Index out of range when accessing field in a feature: %s." % index
|
||||
)
|
||||
return Field(self, i)
|
||||
|
||||
def __len__(self):
|
||||
"Return the count of fields in this feature."
|
||||
return self.num_fields
|
||||
|
||||
def __str__(self):
|
||||
"The string name of the feature."
|
||||
return "Feature FID %d in Layer<%s>" % (self.fid, self.layer_name)
|
||||
|
||||
def __eq__(self, other):
|
||||
"Do equivalence testing on the features."
|
||||
return bool(capi.feature_equal(self.ptr, other._ptr))
|
||||
|
||||
# #### Feature Properties ####
|
||||
@property
|
||||
def encoding(self):
|
||||
return self._layer._ds.encoding
|
||||
|
||||
@property
|
||||
def fid(self):
|
||||
"Return the feature identifier."
|
||||
return capi.get_fid(self.ptr)
|
||||
|
||||
@property
|
||||
def layer_name(self):
|
||||
"Return the name of the layer for the feature."
|
||||
name = capi.get_feat_name(self._layer._ldefn)
|
||||
return force_str(name, self.encoding, strings_only=True)
|
||||
|
||||
@property
|
||||
def num_fields(self):
|
||||
"Return the number of fields in the Feature."
|
||||
return capi.get_feat_field_count(self.ptr)
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
"Return a list of fields in the Feature."
|
||||
return [
|
||||
force_str(
|
||||
capi.get_field_name(capi.get_field_defn(self._layer._ldefn, i)),
|
||||
self.encoding,
|
||||
strings_only=True,
|
||||
)
|
||||
for i in range(self.num_fields)
|
||||
]
|
||||
|
||||
@property
|
||||
def geom(self):
|
||||
"Return the OGR Geometry for this Feature."
|
||||
# Retrieving the geometry pointer for the feature.
|
||||
geom_ptr = capi.get_feat_geom_ref(self.ptr)
|
||||
return OGRGeometry(geom_api.clone_geom(geom_ptr))
|
||||
|
||||
@property
|
||||
def geom_type(self):
|
||||
"Return the OGR Geometry Type for this Feature."
|
||||
return OGRGeomType(capi.get_fd_geom_type(self._layer._ldefn))
|
||||
|
||||
# #### Feature Methods ####
|
||||
def get(self, field):
|
||||
"""
|
||||
Return the value of the field, instead of an instance of the Field
|
||||
object. May take a string of the field name or a Field object as
|
||||
parameters.
|
||||
"""
|
||||
field_name = getattr(field, "name", field)
|
||||
return self[field_name].value
|
||||
|
||||
def index(self, field_name):
|
||||
"Return the index of the given field name."
|
||||
i = capi.get_field_index(self.ptr, force_bytes(field_name))
|
||||
if i < 0:
|
||||
raise IndexError("Invalid OFT field name given: %s." % field_name)
|
||||
return i
|
@ -0,0 +1,253 @@
|
||||
from ctypes import byref, c_int
|
||||
from datetime import date, datetime, time
|
||||
|
||||
from django.contrib.gis.gdal.base import GDALBase
|
||||
from django.contrib.gis.gdal.error import GDALException
|
||||
from django.contrib.gis.gdal.prototypes import ds as capi
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
|
||||
# For more information, see the OGR C API source code:
|
||||
# https://gdal.org/api/vector_c_api.html
|
||||
#
|
||||
# The OGR_Fld_* routines are relevant here.
|
||||
class Field(GDALBase):
|
||||
"""
|
||||
Wrap an OGR Field. Needs to be instantiated from a Feature object.
|
||||
"""
|
||||
|
||||
def __init__(self, feat, index):
|
||||
"""
|
||||
Initialize on the feature object and the integer index of
|
||||
the field within the feature.
|
||||
"""
|
||||
# Setting the feature pointer and index.
|
||||
self._feat = feat
|
||||
self._index = index
|
||||
|
||||
# Getting the pointer for this field.
|
||||
fld_ptr = capi.get_feat_field_defn(feat.ptr, index)
|
||||
if not fld_ptr:
|
||||
raise GDALException("Cannot create OGR Field, invalid pointer given.")
|
||||
self.ptr = fld_ptr
|
||||
|
||||
# Setting the class depending upon the OGR Field Type (OFT)
|
||||
self.__class__ = OGRFieldTypes[self.type]
|
||||
|
||||
def __str__(self):
|
||||
"Return the string representation of the Field."
|
||||
return str(self.value).strip()
|
||||
|
||||
# #### Field Methods ####
|
||||
def as_double(self):
|
||||
"Retrieve the Field's value as a double (float)."
|
||||
return (
|
||||
capi.get_field_as_double(self._feat.ptr, self._index)
|
||||
if self.is_set
|
||||
else None
|
||||
)
|
||||
|
||||
def as_int(self, is_64=False):
|
||||
"Retrieve the Field's value as an integer."
|
||||
if is_64:
|
||||
return (
|
||||
capi.get_field_as_integer64(self._feat.ptr, self._index)
|
||||
if self.is_set
|
||||
else None
|
||||
)
|
||||
else:
|
||||
return (
|
||||
capi.get_field_as_integer(self._feat.ptr, self._index)
|
||||
if self.is_set
|
||||
else None
|
||||
)
|
||||
|
||||
def as_string(self):
|
||||
"Retrieve the Field's value as a string."
|
||||
if not self.is_set:
|
||||
return None
|
||||
string = capi.get_field_as_string(self._feat.ptr, self._index)
|
||||
return force_str(string, encoding=self._feat.encoding, strings_only=True)
|
||||
|
||||
def as_datetime(self):
|
||||
"Retrieve the Field's value as a tuple of date & time components."
|
||||
if not self.is_set:
|
||||
return None
|
||||
yy, mm, dd, hh, mn, ss, tz = [c_int() for i in range(7)]
|
||||
status = capi.get_field_as_datetime(
|
||||
self._feat.ptr,
|
||||
self._index,
|
||||
byref(yy),
|
||||
byref(mm),
|
||||
byref(dd),
|
||||
byref(hh),
|
||||
byref(mn),
|
||||
byref(ss),
|
||||
byref(tz),
|
||||
)
|
||||
if status:
|
||||
return (yy, mm, dd, hh, mn, ss, tz)
|
||||
else:
|
||||
raise GDALException(
|
||||
"Unable to retrieve date & time information from the field."
|
||||
)
|
||||
|
||||
# #### Field Properties ####
|
||||
@property
|
||||
def is_set(self):
|
||||
"Return True if the value of this field isn't null, False otherwise."
|
||||
return capi.is_field_set(self._feat.ptr, self._index)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"Return the name of this Field."
|
||||
name = capi.get_field_name(self.ptr)
|
||||
return force_str(name, encoding=self._feat.encoding, strings_only=True)
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
"Return the precision of this Field."
|
||||
return capi.get_field_precision(self.ptr)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
"Return the OGR type of this Field."
|
||||
return capi.get_field_type(self.ptr)
|
||||
|
||||
@property
|
||||
def type_name(self):
|
||||
"Return the OGR field type name for this Field."
|
||||
return capi.get_field_type_name(self.type)
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"Return the value of this Field."
|
||||
# Default is to get the field as a string.
|
||||
return self.as_string()
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
"Return the width of this Field."
|
||||
return capi.get_field_width(self.ptr)
|
||||
|
||||
|
||||
# ### The Field sub-classes for each OGR Field type. ###
|
||||
class OFTInteger(Field):
|
||||
_bit64 = False
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"Return an integer contained in this field."
|
||||
return self.as_int(self._bit64)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
"""
|
||||
GDAL uses OFTReals to represent OFTIntegers in created
|
||||
shapefiles -- forcing the type here since the underlying field
|
||||
type may actually be OFTReal.
|
||||
"""
|
||||
return 0
|
||||
|
||||
|
||||
class OFTReal(Field):
|
||||
@property
|
||||
def value(self):
|
||||
"Return a float contained in this field."
|
||||
return self.as_double()
|
||||
|
||||
|
||||
# String & Binary fields, just subclasses
|
||||
class OFTString(Field):
|
||||
pass
|
||||
|
||||
|
||||
class OFTWideString(Field):
|
||||
pass
|
||||
|
||||
|
||||
class OFTBinary(Field):
|
||||
pass
|
||||
|
||||
|
||||
# OFTDate, OFTTime, OFTDateTime fields.
|
||||
class OFTDate(Field):
|
||||
@property
|
||||
def value(self):
|
||||
"Return a Python `date` object for the OFTDate field."
|
||||
try:
|
||||
yy, mm, dd, hh, mn, ss, tz = self.as_datetime()
|
||||
return date(yy.value, mm.value, dd.value)
|
||||
except (TypeError, ValueError, GDALException):
|
||||
return None
|
||||
|
||||
|
||||
class OFTDateTime(Field):
|
||||
@property
|
||||
def value(self):
|
||||
"Return a Python `datetime` object for this OFTDateTime field."
|
||||
# TODO: Adapt timezone information.
|
||||
# See https://lists.osgeo.org/pipermail/gdal-dev/2006-February/007990.html
|
||||
# The `tz` variable has values of: 0=unknown, 1=localtime (ambiguous),
|
||||
# 100=GMT, 104=GMT+1, 80=GMT-5, etc.
|
||||
try:
|
||||
yy, mm, dd, hh, mn, ss, tz = self.as_datetime()
|
||||
return datetime(yy.value, mm.value, dd.value, hh.value, mn.value, ss.value)
|
||||
except (TypeError, ValueError, GDALException):
|
||||
return None
|
||||
|
||||
|
||||
class OFTTime(Field):
|
||||
@property
|
||||
def value(self):
|
||||
"Return a Python `time` object for this OFTTime field."
|
||||
try:
|
||||
yy, mm, dd, hh, mn, ss, tz = self.as_datetime()
|
||||
return time(hh.value, mn.value, ss.value)
|
||||
except (ValueError, GDALException):
|
||||
return None
|
||||
|
||||
|
||||
class OFTInteger64(OFTInteger):
|
||||
_bit64 = True
|
||||
|
||||
|
||||
# List fields are also just subclasses
|
||||
class OFTIntegerList(Field):
|
||||
pass
|
||||
|
||||
|
||||
class OFTRealList(Field):
|
||||
pass
|
||||
|
||||
|
||||
class OFTStringList(Field):
|
||||
pass
|
||||
|
||||
|
||||
class OFTWideStringList(Field):
|
||||
pass
|
||||
|
||||
|
||||
class OFTInteger64List(Field):
|
||||
pass
|
||||
|
||||
|
||||
# Class mapping dictionary for OFT Types and reverse mapping.
|
||||
OGRFieldTypes = {
|
||||
0: OFTInteger,
|
||||
1: OFTIntegerList,
|
||||
2: OFTReal,
|
||||
3: OFTRealList,
|
||||
4: OFTString,
|
||||
5: OFTStringList,
|
||||
6: OFTWideString,
|
||||
7: OFTWideStringList,
|
||||
8: OFTBinary,
|
||||
9: OFTDate,
|
||||
10: OFTTime,
|
||||
11: OFTDateTime,
|
||||
12: OFTInteger64,
|
||||
13: OFTInteger64List,
|
||||
}
|
||||
ROGRFieldTypes = {cls: num for num, cls in OGRFieldTypes.items()}
|
@ -0,0 +1,743 @@
|
||||
"""
|
||||
The OGRGeometry is a wrapper for using the OGR Geometry class
|
||||
(see https://gdal.org/api/ogrgeometry_cpp.html#_CPPv411OGRGeometry).
|
||||
OGRGeometry may be instantiated when reading geometries from OGR Data Sources
|
||||
(e.g. SHP files), or when given OGC WKT (a string).
|
||||
|
||||
While the 'full' API is not present yet, the API is "pythonic" unlike
|
||||
the traditional and "next-generation" OGR Python bindings. One major
|
||||
advantage OGR Geometries have over their GEOS counterparts is support
|
||||
for spatial reference systems and their transformation.
|
||||
|
||||
Example:
|
||||
>>> from django.contrib.gis.gdal import OGRGeometry, OGRGeomType, SpatialReference
|
||||
>>> wkt1, wkt2 = 'POINT(-90 30)', 'POLYGON((0 0, 5 0, 5 5, 0 5)'
|
||||
>>> pnt = OGRGeometry(wkt1)
|
||||
>>> print(pnt)
|
||||
POINT (-90 30)
|
||||
>>> mpnt = OGRGeometry(OGRGeomType('MultiPoint'), SpatialReference('WGS84'))
|
||||
>>> mpnt.add(wkt1)
|
||||
>>> mpnt.add(wkt1)
|
||||
>>> print(mpnt)
|
||||
MULTIPOINT (-90 30,-90 30)
|
||||
>>> print(mpnt.srs.name)
|
||||
WGS 84
|
||||
>>> print(mpnt.srs.proj)
|
||||
+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs
|
||||
>>> mpnt.transform(SpatialReference('NAD27'))
|
||||
>>> print(mpnt.proj)
|
||||
+proj=longlat +ellps=clrk66 +datum=NAD27 +no_defs
|
||||
>>> print(mpnt)
|
||||
MULTIPOINT (-89.99993037860248 29.99979788655764,-89.99993037860248 29.99979788655764)
|
||||
|
||||
The OGRGeomType class is to make it easy to specify an OGR geometry type:
|
||||
>>> from django.contrib.gis.gdal import OGRGeomType
|
||||
>>> gt1 = OGRGeomType(3) # Using an integer for the type
|
||||
>>> gt2 = OGRGeomType('Polygon') # Using a string
|
||||
>>> gt3 = OGRGeomType('POLYGON') # It's case-insensitive
|
||||
>>> print(gt1 == 3, gt1 == 'Polygon') # Equivalence works w/non-OGRGeomType objects
|
||||
True True
|
||||
"""
|
||||
import sys
|
||||
from binascii import b2a_hex
|
||||
from ctypes import byref, c_char_p, c_double, c_ubyte, c_void_p, string_at
|
||||
|
||||
from django.contrib.gis.gdal.base import GDALBase
|
||||
from django.contrib.gis.gdal.envelope import Envelope, OGREnvelope
|
||||
from django.contrib.gis.gdal.error import GDALException, SRSException
|
||||
from django.contrib.gis.gdal.geomtype import OGRGeomType
|
||||
from django.contrib.gis.gdal.prototypes import geom as capi
|
||||
from django.contrib.gis.gdal.prototypes import srs as srs_api
|
||||
from django.contrib.gis.gdal.srs import CoordTransform, SpatialReference
|
||||
from django.contrib.gis.geometry import hex_regex, json_regex, wkt_regex
|
||||
from django.utils.encoding import force_bytes
|
||||
|
||||
|
||||
# For more information, see the OGR C API source code:
|
||||
# https://gdal.org/api/vector_c_api.html
|
||||
#
|
||||
# The OGR_G_* routines are relevant here.
|
||||
class OGRGeometry(GDALBase):
|
||||
"""Encapsulate an OGR geometry."""
|
||||
|
||||
destructor = capi.destroy_geom
|
||||
|
||||
def __init__(self, geom_input, srs=None):
|
||||
"""Initialize Geometry on either WKT or an OGR pointer as input."""
|
||||
str_instance = isinstance(geom_input, str)
|
||||
|
||||
# If HEX, unpack input to a binary buffer.
|
||||
if str_instance and hex_regex.match(geom_input):
|
||||
geom_input = memoryview(bytes.fromhex(geom_input))
|
||||
str_instance = False
|
||||
|
||||
# Constructing the geometry,
|
||||
if str_instance:
|
||||
wkt_m = wkt_regex.match(geom_input)
|
||||
json_m = json_regex.match(geom_input)
|
||||
if wkt_m:
|
||||
if wkt_m["srid"]:
|
||||
# If there's EWKT, set the SRS w/value of the SRID.
|
||||
srs = int(wkt_m["srid"])
|
||||
if wkt_m["type"].upper() == "LINEARRING":
|
||||
# OGR_G_CreateFromWkt doesn't work with LINEARRING WKT.
|
||||
# See https://trac.osgeo.org/gdal/ticket/1992.
|
||||
g = capi.create_geom(OGRGeomType(wkt_m["type"]).num)
|
||||
capi.import_wkt(g, byref(c_char_p(wkt_m["wkt"].encode())))
|
||||
else:
|
||||
g = capi.from_wkt(
|
||||
byref(c_char_p(wkt_m["wkt"].encode())), None, byref(c_void_p())
|
||||
)
|
||||
elif json_m:
|
||||
g = self._from_json(geom_input.encode())
|
||||
else:
|
||||
# Seeing if the input is a valid short-hand string
|
||||
# (e.g., 'Point', 'POLYGON').
|
||||
OGRGeomType(geom_input)
|
||||
g = capi.create_geom(OGRGeomType(geom_input).num)
|
||||
elif isinstance(geom_input, memoryview):
|
||||
# WKB was passed in
|
||||
g = self._from_wkb(geom_input)
|
||||
elif isinstance(geom_input, OGRGeomType):
|
||||
# OGRGeomType was passed in, an empty geometry will be created.
|
||||
g = capi.create_geom(geom_input.num)
|
||||
elif isinstance(geom_input, self.ptr_type):
|
||||
# OGR pointer (c_void_p) was the input.
|
||||
g = geom_input
|
||||
else:
|
||||
raise GDALException(
|
||||
"Invalid input type for OGR Geometry construction: %s"
|
||||
% type(geom_input)
|
||||
)
|
||||
|
||||
# Now checking the Geometry pointer before finishing initialization
|
||||
# by setting the pointer for the object.
|
||||
if not g:
|
||||
raise GDALException(
|
||||
"Cannot create OGR Geometry from input: %s" % geom_input
|
||||
)
|
||||
self.ptr = g
|
||||
|
||||
# Assigning the SpatialReference object to the geometry, if valid.
|
||||
if srs:
|
||||
self.srs = srs
|
||||
|
||||
# Setting the class depending upon the OGR Geometry Type
|
||||
self.__class__ = GEO_CLASSES[self.geom_type.num]
|
||||
|
||||
# Pickle routines
|
||||
def __getstate__(self):
|
||||
srs = self.srs
|
||||
if srs:
|
||||
srs = srs.wkt
|
||||
else:
|
||||
srs = None
|
||||
return bytes(self.wkb), srs
|
||||
|
||||
def __setstate__(self, state):
|
||||
wkb, srs = state
|
||||
ptr = capi.from_wkb(wkb, None, byref(c_void_p()), len(wkb))
|
||||
if not ptr:
|
||||
raise GDALException("Invalid OGRGeometry loaded from pickled state.")
|
||||
self.ptr = ptr
|
||||
self.srs = srs
|
||||
|
||||
@classmethod
|
||||
def _from_wkb(cls, geom_input):
|
||||
return capi.from_wkb(
|
||||
bytes(geom_input), None, byref(c_void_p()), len(geom_input)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _from_json(geom_input):
|
||||
return capi.from_json(geom_input)
|
||||
|
||||
@classmethod
|
||||
def from_bbox(cls, bbox):
|
||||
"Construct a Polygon from a bounding box (4-tuple)."
|
||||
x0, y0, x1, y1 = bbox
|
||||
return OGRGeometry(
|
||||
"POLYGON((%s %s, %s %s, %s %s, %s %s, %s %s))"
|
||||
% (x0, y0, x0, y1, x1, y1, x1, y0, x0, y0)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_json(geom_input):
|
||||
return OGRGeometry(OGRGeometry._from_json(force_bytes(geom_input)))
|
||||
|
||||
@classmethod
|
||||
def from_gml(cls, gml_string):
|
||||
return cls(capi.from_gml(force_bytes(gml_string)))
|
||||
|
||||
# ### Geometry set-like operations ###
|
||||
# g = g1 | g2
|
||||
def __or__(self, other):
|
||||
"Return the union of the two geometries."
|
||||
return self.union(other)
|
||||
|
||||
# g = g1 & g2
|
||||
def __and__(self, other):
|
||||
"Return the intersection of this Geometry and the other."
|
||||
return self.intersection(other)
|
||||
|
||||
# g = g1 - g2
|
||||
def __sub__(self, other):
|
||||
"Return the difference this Geometry and the other."
|
||||
return self.difference(other)
|
||||
|
||||
# g = g1 ^ g2
|
||||
def __xor__(self, other):
|
||||
"Return the symmetric difference of this Geometry and the other."
|
||||
return self.sym_difference(other)
|
||||
|
||||
def __eq__(self, other):
|
||||
"Is this Geometry equal to the other?"
|
||||
return isinstance(other, OGRGeometry) and self.equals(other)
|
||||
|
||||
def __str__(self):
|
||||
"WKT is used for the string representation."
|
||||
return self.wkt
|
||||
|
||||
# #### Geometry Properties ####
|
||||
@property
|
||||
def dimension(self):
|
||||
"Return 0 for points, 1 for lines, and 2 for surfaces."
|
||||
return capi.get_dims(self.ptr)
|
||||
|
||||
def _get_coord_dim(self):
|
||||
"Return the coordinate dimension of the Geometry."
|
||||
return capi.get_coord_dim(self.ptr)
|
||||
|
||||
def _set_coord_dim(self, dim):
|
||||
"Set the coordinate dimension of this Geometry."
|
||||
if dim not in (2, 3):
|
||||
raise ValueError("Geometry dimension must be either 2 or 3")
|
||||
capi.set_coord_dim(self.ptr, dim)
|
||||
|
||||
coord_dim = property(_get_coord_dim, _set_coord_dim)
|
||||
|
||||
@property
|
||||
def geom_count(self):
|
||||
"Return the number of elements in this Geometry."
|
||||
return capi.get_geom_count(self.ptr)
|
||||
|
||||
@property
|
||||
def point_count(self):
|
||||
"Return the number of Points in this Geometry."
|
||||
return capi.get_point_count(self.ptr)
|
||||
|
||||
@property
|
||||
def num_points(self):
|
||||
"Alias for `point_count` (same name method in GEOS API.)"
|
||||
return self.point_count
|
||||
|
||||
@property
|
||||
def num_coords(self):
|
||||
"Alias for `point_count`."
|
||||
return self.point_count
|
||||
|
||||
@property
|
||||
def geom_type(self):
|
||||
"Return the Type for this Geometry."
|
||||
return OGRGeomType(capi.get_geom_type(self.ptr))
|
||||
|
||||
@property
|
||||
def geom_name(self):
|
||||
"Return the Name of this Geometry."
|
||||
return capi.get_geom_name(self.ptr)
|
||||
|
||||
@property
|
||||
def area(self):
|
||||
"Return the area for a LinearRing, Polygon, or MultiPolygon; 0 otherwise."
|
||||
return capi.get_area(self.ptr)
|
||||
|
||||
@property
|
||||
def envelope(self):
|
||||
"Return the envelope for this Geometry."
|
||||
# TODO: Fix Envelope() for Point geometries.
|
||||
return Envelope(capi.get_envelope(self.ptr, byref(OGREnvelope())))
|
||||
|
||||
@property
|
||||
def empty(self):
|
||||
return capi.is_empty(self.ptr)
|
||||
|
||||
@property
|
||||
def extent(self):
|
||||
"Return the envelope as a 4-tuple, instead of as an Envelope object."
|
||||
return self.envelope.tuple
|
||||
|
||||
# #### SpatialReference-related Properties ####
|
||||
|
||||
# The SRS property
|
||||
def _get_srs(self):
|
||||
"Return the Spatial Reference for this Geometry."
|
||||
try:
|
||||
srs_ptr = capi.get_geom_srs(self.ptr)
|
||||
return SpatialReference(srs_api.clone_srs(srs_ptr))
|
||||
except SRSException:
|
||||
return None
|
||||
|
||||
def _set_srs(self, srs):
|
||||
"Set the SpatialReference for this geometry."
|
||||
# Do not have to clone the `SpatialReference` object pointer because
|
||||
# when it is assigned to this `OGRGeometry` it's internal OGR
|
||||
# reference count is incremented, and will likewise be released
|
||||
# (decremented) when this geometry's destructor is called.
|
||||
if isinstance(srs, SpatialReference):
|
||||
srs_ptr = srs.ptr
|
||||
elif isinstance(srs, (int, str)):
|
||||
sr = SpatialReference(srs)
|
||||
srs_ptr = sr.ptr
|
||||
elif srs is None:
|
||||
srs_ptr = None
|
||||
else:
|
||||
raise TypeError(
|
||||
"Cannot assign spatial reference with object of type: %s" % type(srs)
|
||||
)
|
||||
capi.assign_srs(self.ptr, srs_ptr)
|
||||
|
||||
srs = property(_get_srs, _set_srs)
|
||||
|
||||
# The SRID property
|
||||
def _get_srid(self):
|
||||
srs = self.srs
|
||||
if srs:
|
||||
return srs.srid
|
||||
return None
|
||||
|
||||
def _set_srid(self, srid):
|
||||
if isinstance(srid, int) or srid is None:
|
||||
self.srs = srid
|
||||
else:
|
||||
raise TypeError("SRID must be set with an integer.")
|
||||
|
||||
srid = property(_get_srid, _set_srid)
|
||||
|
||||
# #### Output Methods ####
|
||||
def _geos_ptr(self):
|
||||
from django.contrib.gis.geos import GEOSGeometry
|
||||
|
||||
return GEOSGeometry._from_wkb(self.wkb)
|
||||
|
||||
@property
|
||||
def geos(self):
|
||||
"Return a GEOSGeometry object from this OGRGeometry."
|
||||
from django.contrib.gis.geos import GEOSGeometry
|
||||
|
||||
return GEOSGeometry(self._geos_ptr(), self.srid)
|
||||
|
||||
@property
|
||||
def gml(self):
|
||||
"Return the GML representation of the Geometry."
|
||||
return capi.to_gml(self.ptr)
|
||||
|
||||
@property
|
||||
def hex(self):
|
||||
"Return the hexadecimal representation of the WKB (a string)."
|
||||
return b2a_hex(self.wkb).upper()
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
"""
|
||||
Return the GeoJSON representation of this Geometry.
|
||||
"""
|
||||
return capi.to_json(self.ptr)
|
||||
|
||||
geojson = json
|
||||
|
||||
@property
|
||||
def kml(self):
|
||||
"Return the KML representation of the Geometry."
|
||||
return capi.to_kml(self.ptr, None)
|
||||
|
||||
@property
|
||||
def wkb_size(self):
|
||||
"Return the size of the WKB buffer."
|
||||
return capi.get_wkbsize(self.ptr)
|
||||
|
||||
@property
|
||||
def wkb(self):
|
||||
"Return the WKB representation of the Geometry."
|
||||
if sys.byteorder == "little":
|
||||
byteorder = 1 # wkbNDR (from ogr_core.h)
|
||||
else:
|
||||
byteorder = 0 # wkbXDR
|
||||
sz = self.wkb_size
|
||||
# Creating the unsigned character buffer, and passing it in by reference.
|
||||
buf = (c_ubyte * sz)()
|
||||
capi.to_wkb(self.ptr, byteorder, byref(buf))
|
||||
# Returning a buffer of the string at the pointer.
|
||||
return memoryview(string_at(buf, sz))
|
||||
|
||||
@property
|
||||
def wkt(self):
|
||||
"Return the WKT representation of the Geometry."
|
||||
return capi.to_wkt(self.ptr, byref(c_char_p()))
|
||||
|
||||
@property
|
||||
def ewkt(self):
|
||||
"Return the EWKT representation of the Geometry."
|
||||
srs = self.srs
|
||||
if srs and srs.srid:
|
||||
return "SRID=%s;%s" % (srs.srid, self.wkt)
|
||||
else:
|
||||
return self.wkt
|
||||
|
||||
# #### Geometry Methods ####
|
||||
def clone(self):
|
||||
"Clone this OGR Geometry."
|
||||
return OGRGeometry(capi.clone_geom(self.ptr), self.srs)
|
||||
|
||||
def close_rings(self):
|
||||
"""
|
||||
If there are any rings within this geometry that have not been
|
||||
closed, this routine will do so by adding the starting point at the
|
||||
end.
|
||||
"""
|
||||
# Closing the open rings.
|
||||
capi.geom_close_rings(self.ptr)
|
||||
|
||||
def transform(self, coord_trans, clone=False):
|
||||
"""
|
||||
Transform this geometry to a different spatial reference system.
|
||||
May take a CoordTransform object, a SpatialReference object, string
|
||||
WKT or PROJ, and/or an integer SRID. By default, return nothing
|
||||
and transform the geometry in-place. However, if the `clone` keyword is
|
||||
set, return a transformed clone of this geometry.
|
||||
"""
|
||||
if clone:
|
||||
klone = self.clone()
|
||||
klone.transform(coord_trans)
|
||||
return klone
|
||||
|
||||
# Depending on the input type, use the appropriate OGR routine
|
||||
# to perform the transformation.
|
||||
if isinstance(coord_trans, CoordTransform):
|
||||
capi.geom_transform(self.ptr, coord_trans.ptr)
|
||||
elif isinstance(coord_trans, SpatialReference):
|
||||
capi.geom_transform_to(self.ptr, coord_trans.ptr)
|
||||
elif isinstance(coord_trans, (int, str)):
|
||||
sr = SpatialReference(coord_trans)
|
||||
capi.geom_transform_to(self.ptr, sr.ptr)
|
||||
else:
|
||||
raise TypeError(
|
||||
"Transform only accepts CoordTransform, "
|
||||
"SpatialReference, string, and integer objects."
|
||||
)
|
||||
|
||||
# #### Topology Methods ####
|
||||
def _topology(self, func, other):
|
||||
"""A generalized function for topology operations, takes a GDAL function and
|
||||
the other geometry to perform the operation on."""
|
||||
if not isinstance(other, OGRGeometry):
|
||||
raise TypeError(
|
||||
"Must use another OGRGeometry object for topology operations!"
|
||||
)
|
||||
|
||||
# Returning the output of the given function with the other geometry's
|
||||
# pointer.
|
||||
return func(self.ptr, other.ptr)
|
||||
|
||||
def intersects(self, other):
|
||||
"Return True if this geometry intersects with the other."
|
||||
return self._topology(capi.ogr_intersects, other)
|
||||
|
||||
def equals(self, other):
|
||||
"Return True if this geometry is equivalent to the other."
|
||||
return self._topology(capi.ogr_equals, other)
|
||||
|
||||
def disjoint(self, other):
|
||||
"Return True if this geometry and the other are spatially disjoint."
|
||||
return self._topology(capi.ogr_disjoint, other)
|
||||
|
||||
def touches(self, other):
|
||||
"Return True if this geometry touches the other."
|
||||
return self._topology(capi.ogr_touches, other)
|
||||
|
||||
def crosses(self, other):
|
||||
"Return True if this geometry crosses the other."
|
||||
return self._topology(capi.ogr_crosses, other)
|
||||
|
||||
def within(self, other):
|
||||
"Return True if this geometry is within the other."
|
||||
return self._topology(capi.ogr_within, other)
|
||||
|
||||
def contains(self, other):
|
||||
"Return True if this geometry contains the other."
|
||||
return self._topology(capi.ogr_contains, other)
|
||||
|
||||
def overlaps(self, other):
|
||||
"Return True if this geometry overlaps the other."
|
||||
return self._topology(capi.ogr_overlaps, other)
|
||||
|
||||
# #### Geometry-generation Methods ####
|
||||
def _geomgen(self, gen_func, other=None):
|
||||
"A helper routine for the OGR routines that generate geometries."
|
||||
if isinstance(other, OGRGeometry):
|
||||
return OGRGeometry(gen_func(self.ptr, other.ptr), self.srs)
|
||||
else:
|
||||
return OGRGeometry(gen_func(self.ptr), self.srs)
|
||||
|
||||
@property
|
||||
def boundary(self):
|
||||
"Return the boundary of this geometry."
|
||||
return self._geomgen(capi.get_boundary)
|
||||
|
||||
@property
|
||||
def convex_hull(self):
|
||||
"""
|
||||
Return the smallest convex Polygon that contains all the points in
|
||||
this Geometry.
|
||||
"""
|
||||
return self._geomgen(capi.geom_convex_hull)
|
||||
|
||||
def difference(self, other):
|
||||
"""
|
||||
Return a new geometry consisting of the region which is the difference
|
||||
of this geometry and the other.
|
||||
"""
|
||||
return self._geomgen(capi.geom_diff, other)
|
||||
|
||||
def intersection(self, other):
|
||||
"""
|
||||
Return a new geometry consisting of the region of intersection of this
|
||||
geometry and the other.
|
||||
"""
|
||||
return self._geomgen(capi.geom_intersection, other)
|
||||
|
||||
def sym_difference(self, other):
|
||||
"""
|
||||
Return a new geometry which is the symmetric difference of this
|
||||
geometry and the other.
|
||||
"""
|
||||
return self._geomgen(capi.geom_sym_diff, other)
|
||||
|
||||
def union(self, other):
|
||||
"""
|
||||
Return a new geometry consisting of the region which is the union of
|
||||
this geometry and the other.
|
||||
"""
|
||||
return self._geomgen(capi.geom_union, other)
|
||||
|
||||
|
||||
# The subclasses for OGR Geometry.
|
||||
class Point(OGRGeometry):
|
||||
def _geos_ptr(self):
|
||||
from django.contrib.gis import geos
|
||||
|
||||
return geos.Point._create_empty() if self.empty else super()._geos_ptr()
|
||||
|
||||
@classmethod
|
||||
def _create_empty(cls):
|
||||
return capi.create_geom(OGRGeomType("point").num)
|
||||
|
||||
@property
|
||||
def x(self):
|
||||
"Return the X coordinate for this Point."
|
||||
return capi.getx(self.ptr, 0)
|
||||
|
||||
@property
|
||||
def y(self):
|
||||
"Return the Y coordinate for this Point."
|
||||
return capi.gety(self.ptr, 0)
|
||||
|
||||
@property
|
||||
def z(self):
|
||||
"Return the Z coordinate for this Point."
|
||||
if self.coord_dim == 3:
|
||||
return capi.getz(self.ptr, 0)
|
||||
|
||||
@property
|
||||
def tuple(self):
|
||||
"Return the tuple of this point."
|
||||
if self.coord_dim == 2:
|
||||
return (self.x, self.y)
|
||||
elif self.coord_dim == 3:
|
||||
return (self.x, self.y, self.z)
|
||||
|
||||
coords = tuple
|
||||
|
||||
|
||||
class LineString(OGRGeometry):
|
||||
def __getitem__(self, index):
|
||||
"Return the Point at the given index."
|
||||
if 0 <= index < self.point_count:
|
||||
x, y, z = c_double(), c_double(), c_double()
|
||||
capi.get_point(self.ptr, index, byref(x), byref(y), byref(z))
|
||||
dim = self.coord_dim
|
||||
if dim == 1:
|
||||
return (x.value,)
|
||||
elif dim == 2:
|
||||
return (x.value, y.value)
|
||||
elif dim == 3:
|
||||
return (x.value, y.value, z.value)
|
||||
else:
|
||||
raise IndexError(
|
||||
"Index out of range when accessing points of a line string: %s." % index
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"Return the number of points in the LineString."
|
||||
return self.point_count
|
||||
|
||||
@property
|
||||
def tuple(self):
|
||||
"Return the tuple representation of this LineString."
|
||||
return tuple(self[i] for i in range(len(self)))
|
||||
|
||||
coords = tuple
|
||||
|
||||
def _listarr(self, func):
|
||||
"""
|
||||
Internal routine that returns a sequence (list) corresponding with
|
||||
the given function.
|
||||
"""
|
||||
return [func(self.ptr, i) for i in range(len(self))]
|
||||
|
||||
@property
|
||||
def x(self):
|
||||
"Return the X coordinates in a list."
|
||||
return self._listarr(capi.getx)
|
||||
|
||||
@property
|
||||
def y(self):
|
||||
"Return the Y coordinates in a list."
|
||||
return self._listarr(capi.gety)
|
||||
|
||||
@property
|
||||
def z(self):
|
||||
"Return the Z coordinates in a list."
|
||||
if self.coord_dim == 3:
|
||||
return self._listarr(capi.getz)
|
||||
|
||||
|
||||
# LinearRings are used in Polygons.
|
||||
class LinearRing(LineString):
|
||||
pass
|
||||
|
||||
|
||||
class Polygon(OGRGeometry):
|
||||
def __len__(self):
|
||||
"Return the number of interior rings in this Polygon."
|
||||
return self.geom_count
|
||||
|
||||
def __getitem__(self, index):
|
||||
"Get the ring at the specified index."
|
||||
if 0 <= index < self.geom_count:
|
||||
return OGRGeometry(
|
||||
capi.clone_geom(capi.get_geom_ref(self.ptr, index)), self.srs
|
||||
)
|
||||
else:
|
||||
raise IndexError(
|
||||
"Index out of range when accessing rings of a polygon: %s." % index
|
||||
)
|
||||
|
||||
# Polygon Properties
|
||||
@property
|
||||
def shell(self):
|
||||
"Return the shell of this Polygon."
|
||||
return self[0] # First ring is the shell
|
||||
|
||||
exterior_ring = shell
|
||||
|
||||
@property
|
||||
def tuple(self):
|
||||
"Return a tuple of LinearRing coordinate tuples."
|
||||
return tuple(self[i].tuple for i in range(self.geom_count))
|
||||
|
||||
coords = tuple
|
||||
|
||||
@property
|
||||
def point_count(self):
|
||||
"Return the number of Points in this Polygon."
|
||||
# Summing up the number of points in each ring of the Polygon.
|
||||
return sum(self[i].point_count for i in range(self.geom_count))
|
||||
|
||||
@property
|
||||
def centroid(self):
|
||||
"Return the centroid (a Point) of this Polygon."
|
||||
# The centroid is a Point, create a geometry for this.
|
||||
p = OGRGeometry(OGRGeomType("Point"))
|
||||
capi.get_centroid(self.ptr, p.ptr)
|
||||
return p
|
||||
|
||||
|
||||
# Geometry Collection base class.
|
||||
class GeometryCollection(OGRGeometry):
|
||||
"The Geometry Collection class."
|
||||
|
||||
def __getitem__(self, index):
|
||||
"Get the Geometry at the specified index."
|
||||
if 0 <= index < self.geom_count:
|
||||
return OGRGeometry(
|
||||
capi.clone_geom(capi.get_geom_ref(self.ptr, index)), self.srs
|
||||
)
|
||||
else:
|
||||
raise IndexError(
|
||||
"Index out of range when accessing geometry in a collection: %s."
|
||||
% index
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"Return the number of geometries in this Geometry Collection."
|
||||
return self.geom_count
|
||||
|
||||
def add(self, geom):
|
||||
"Add the geometry to this Geometry Collection."
|
||||
if isinstance(geom, OGRGeometry):
|
||||
if isinstance(geom, self.__class__):
|
||||
for g in geom:
|
||||
capi.add_geom(self.ptr, g.ptr)
|
||||
else:
|
||||
capi.add_geom(self.ptr, geom.ptr)
|
||||
elif isinstance(geom, str):
|
||||
tmp = OGRGeometry(geom)
|
||||
capi.add_geom(self.ptr, tmp.ptr)
|
||||
else:
|
||||
raise GDALException("Must add an OGRGeometry.")
|
||||
|
||||
@property
|
||||
def point_count(self):
|
||||
"Return the number of Points in this Geometry Collection."
|
||||
# Summing up the number of points in each geometry in this collection
|
||||
return sum(self[i].point_count for i in range(self.geom_count))
|
||||
|
||||
@property
|
||||
def tuple(self):
|
||||
"Return a tuple representation of this Geometry Collection."
|
||||
return tuple(self[i].tuple for i in range(self.geom_count))
|
||||
|
||||
coords = tuple
|
||||
|
||||
|
||||
# Multiple Geometry types.
|
||||
class MultiPoint(GeometryCollection):
|
||||
pass
|
||||
|
||||
|
||||
class MultiLineString(GeometryCollection):
|
||||
pass
|
||||
|
||||
|
||||
class MultiPolygon(GeometryCollection):
|
||||
pass
|
||||
|
||||
|
||||
# Class mapping dictionary (using the OGRwkbGeometryType as the key)
|
||||
GEO_CLASSES = {
|
||||
1: Point,
|
||||
2: LineString,
|
||||
3: Polygon,
|
||||
4: MultiPoint,
|
||||
5: MultiLineString,
|
||||
6: MultiPolygon,
|
||||
7: GeometryCollection,
|
||||
101: LinearRing,
|
||||
1 + OGRGeomType.wkb25bit: Point,
|
||||
2 + OGRGeomType.wkb25bit: LineString,
|
||||
3 + OGRGeomType.wkb25bit: Polygon,
|
||||
4 + OGRGeomType.wkb25bit: MultiPoint,
|
||||
5 + OGRGeomType.wkb25bit: MultiLineString,
|
||||
6 + OGRGeomType.wkb25bit: MultiPolygon,
|
||||
7 + OGRGeomType.wkb25bit: GeometryCollection,
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
from django.contrib.gis.gdal.error import GDALException
|
||||
|
||||
|
||||
class OGRGeomType:
|
||||
"Encapsulate OGR Geometry Types."
|
||||
|
||||
wkb25bit = -2147483648
|
||||
|
||||
# Dictionary of acceptable OGRwkbGeometryType s and their string names.
|
||||
_types = {
|
||||
0: "Unknown",
|
||||
1: "Point",
|
||||
2: "LineString",
|
||||
3: "Polygon",
|
||||
4: "MultiPoint",
|
||||
5: "MultiLineString",
|
||||
6: "MultiPolygon",
|
||||
7: "GeometryCollection",
|
||||
100: "None",
|
||||
101: "LinearRing",
|
||||
102: "PointZ",
|
||||
1 + wkb25bit: "Point25D",
|
||||
2 + wkb25bit: "LineString25D",
|
||||
3 + wkb25bit: "Polygon25D",
|
||||
4 + wkb25bit: "MultiPoint25D",
|
||||
5 + wkb25bit: "MultiLineString25D",
|
||||
6 + wkb25bit: "MultiPolygon25D",
|
||||
7 + wkb25bit: "GeometryCollection25D",
|
||||
}
|
||||
# Reverse type dictionary, keyed by lowercase of the name.
|
||||
_str_types = {v.lower(): k for k, v in _types.items()}
|
||||
|
||||
def __init__(self, type_input):
|
||||
"Figure out the correct OGR Type based upon the input."
|
||||
if isinstance(type_input, OGRGeomType):
|
||||
num = type_input.num
|
||||
elif isinstance(type_input, str):
|
||||
type_input = type_input.lower()
|
||||
if type_input == "geometry":
|
||||
type_input = "unknown"
|
||||
num = self._str_types.get(type_input)
|
||||
if num is None:
|
||||
raise GDALException('Invalid OGR String Type "%s"' % type_input)
|
||||
elif isinstance(type_input, int):
|
||||
if type_input not in self._types:
|
||||
raise GDALException("Invalid OGR Integer Type: %d" % type_input)
|
||||
num = type_input
|
||||
else:
|
||||
raise TypeError("Invalid OGR input type given.")
|
||||
|
||||
# Setting the OGR geometry type number.
|
||||
self.num = num
|
||||
|
||||
def __str__(self):
|
||||
"Return the value of the name property."
|
||||
return self.name
|
||||
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
Do an equivalence test on the OGR type with the given
|
||||
other OGRGeomType, the short-hand string, or the integer.
|
||||
"""
|
||||
if isinstance(other, OGRGeomType):
|
||||
return self.num == other.num
|
||||
elif isinstance(other, str):
|
||||
return self.name.lower() == other.lower()
|
||||
elif isinstance(other, int):
|
||||
return self.num == other
|
||||
else:
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"Return a short-hand string form of the OGR Geometry type."
|
||||
return self._types[self.num]
|
||||
|
||||
@property
|
||||
def django(self):
|
||||
"Return the Django GeometryField for this OGR Type."
|
||||
s = self.name.replace("25D", "")
|
||||
if s in ("LinearRing", "None"):
|
||||
return None
|
||||
elif s == "Unknown":
|
||||
s = "Geometry"
|
||||
elif s == "PointZ":
|
||||
s = "Point"
|
||||
return s + "Field"
|
||||
|
||||
def to_multi(self):
|
||||
"""
|
||||
Transform Point, LineString, Polygon, and their 25D equivalents
|
||||
to their Multi... counterpart.
|
||||
"""
|
||||
if self.name.startswith(("Point", "LineString", "Polygon")):
|
||||
self.num += 3
|
@ -0,0 +1,234 @@
|
||||
from ctypes import byref, c_double
|
||||
|
||||
from django.contrib.gis.gdal.base import GDALBase
|
||||
from django.contrib.gis.gdal.envelope import Envelope, OGREnvelope
|
||||
from django.contrib.gis.gdal.error import GDALException, SRSException
|
||||
from django.contrib.gis.gdal.feature import Feature
|
||||
from django.contrib.gis.gdal.field import OGRFieldTypes
|
||||
from django.contrib.gis.gdal.geometries import OGRGeometry
|
||||
from django.contrib.gis.gdal.geomtype import OGRGeomType
|
||||
from django.contrib.gis.gdal.prototypes import ds as capi
|
||||
from django.contrib.gis.gdal.prototypes import geom as geom_api
|
||||
from django.contrib.gis.gdal.prototypes import srs as srs_api
|
||||
from django.contrib.gis.gdal.srs import SpatialReference
|
||||
from django.utils.encoding import force_bytes, force_str
|
||||
|
||||
|
||||
# For more information, see the OGR C API source code:
|
||||
# https://gdal.org/api/vector_c_api.html
|
||||
#
|
||||
# The OGR_L_* routines are relevant here.
|
||||
class Layer(GDALBase):
|
||||
"""
|
||||
A class that wraps an OGR Layer, needs to be instantiated from a DataSource
|
||||
object.
|
||||
"""
|
||||
|
||||
def __init__(self, layer_ptr, ds):
|
||||
"""
|
||||
Initialize on an OGR C pointer to the Layer and the `DataSource` object
|
||||
that owns this layer. The `DataSource` object is required so that a
|
||||
reference to it is kept with this Layer. This prevents garbage
|
||||
collection of the `DataSource` while this Layer is still active.
|
||||
"""
|
||||
if not layer_ptr:
|
||||
raise GDALException("Cannot create Layer, invalid pointer given")
|
||||
self.ptr = layer_ptr
|
||||
self._ds = ds
|
||||
self._ldefn = capi.get_layer_defn(self._ptr)
|
||||
# Does the Layer support random reading?
|
||||
self._random_read = self.test_capability(b"RandomRead")
|
||||
|
||||
def __getitem__(self, index):
|
||||
"Get the Feature at the specified index."
|
||||
if isinstance(index, int):
|
||||
# An integer index was given -- we cannot do a check based on the
|
||||
# number of features because the beginning and ending feature IDs
|
||||
# are not guaranteed to be 0 and len(layer)-1, respectively.
|
||||
if index < 0:
|
||||
raise IndexError("Negative indices are not allowed on OGR Layers.")
|
||||
return self._make_feature(index)
|
||||
elif isinstance(index, slice):
|
||||
# A slice was given
|
||||
start, stop, stride = index.indices(self.num_feat)
|
||||
return [self._make_feature(fid) for fid in range(start, stop, stride)]
|
||||
else:
|
||||
raise TypeError(
|
||||
"Integers and slices may only be used when indexing OGR Layers."
|
||||
)
|
||||
|
||||
def __iter__(self):
|
||||
"Iterate over each Feature in the Layer."
|
||||
# ResetReading() must be called before iteration is to begin.
|
||||
capi.reset_reading(self._ptr)
|
||||
for i in range(self.num_feat):
|
||||
yield Feature(capi.get_next_feature(self._ptr), self)
|
||||
|
||||
def __len__(self):
|
||||
"The length is the number of features."
|
||||
return self.num_feat
|
||||
|
||||
def __str__(self):
|
||||
"The string name of the layer."
|
||||
return self.name
|
||||
|
||||
def _make_feature(self, feat_id):
|
||||
"""
|
||||
Helper routine for __getitem__ that constructs a Feature from the given
|
||||
Feature ID. If the OGR Layer does not support random-access reading,
|
||||
then each feature of the layer will be incremented through until the
|
||||
a Feature is found matching the given feature ID.
|
||||
"""
|
||||
if self._random_read:
|
||||
# If the Layer supports random reading, return.
|
||||
try:
|
||||
return Feature(capi.get_feature(self.ptr, feat_id), self)
|
||||
except GDALException:
|
||||
pass
|
||||
else:
|
||||
# Random access isn't supported, have to increment through
|
||||
# each feature until the given feature ID is encountered.
|
||||
for feat in self:
|
||||
if feat.fid == feat_id:
|
||||
return feat
|
||||
# Should have returned a Feature, raise an IndexError.
|
||||
raise IndexError("Invalid feature id: %s." % feat_id)
|
||||
|
||||
# #### Layer properties ####
|
||||
@property
|
||||
def extent(self):
|
||||
"Return the extent (an Envelope) of this layer."
|
||||
env = OGREnvelope()
|
||||
capi.get_extent(self.ptr, byref(env), 1)
|
||||
return Envelope(env)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"Return the name of this layer in the Data Source."
|
||||
name = capi.get_fd_name(self._ldefn)
|
||||
return force_str(name, self._ds.encoding, strings_only=True)
|
||||
|
||||
@property
|
||||
def num_feat(self, force=1):
|
||||
"Return the number of features in the Layer."
|
||||
return capi.get_feature_count(self.ptr, force)
|
||||
|
||||
@property
|
||||
def num_fields(self):
|
||||
"Return the number of fields in the Layer."
|
||||
return capi.get_field_count(self._ldefn)
|
||||
|
||||
@property
|
||||
def geom_type(self):
|
||||
"Return the geometry type (OGRGeomType) of the Layer."
|
||||
return OGRGeomType(capi.get_fd_geom_type(self._ldefn))
|
||||
|
||||
@property
|
||||
def srs(self):
|
||||
"Return the Spatial Reference used in this Layer."
|
||||
try:
|
||||
ptr = capi.get_layer_srs(self.ptr)
|
||||
return SpatialReference(srs_api.clone_srs(ptr))
|
||||
except SRSException:
|
||||
return None
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
"""
|
||||
Return a list of string names corresponding to each of the Fields
|
||||
available in this Layer.
|
||||
"""
|
||||
return [
|
||||
force_str(
|
||||
capi.get_field_name(capi.get_field_defn(self._ldefn, i)),
|
||||
self._ds.encoding,
|
||||
strings_only=True,
|
||||
)
|
||||
for i in range(self.num_fields)
|
||||
]
|
||||
|
||||
@property
|
||||
def field_types(self):
|
||||
"""
|
||||
Return a list of the types of fields in this Layer. For example,
|
||||
return the list [OFTInteger, OFTReal, OFTString] for an OGR layer that
|
||||
has an integer, a floating-point, and string fields.
|
||||
"""
|
||||
return [
|
||||
OGRFieldTypes[capi.get_field_type(capi.get_field_defn(self._ldefn, i))]
|
||||
for i in range(self.num_fields)
|
||||
]
|
||||
|
||||
@property
|
||||
def field_widths(self):
|
||||
"Return a list of the maximum field widths for the features."
|
||||
return [
|
||||
capi.get_field_width(capi.get_field_defn(self._ldefn, i))
|
||||
for i in range(self.num_fields)
|
||||
]
|
||||
|
||||
@property
|
||||
def field_precisions(self):
|
||||
"Return the field precisions for the features."
|
||||
return [
|
||||
capi.get_field_precision(capi.get_field_defn(self._ldefn, i))
|
||||
for i in range(self.num_fields)
|
||||
]
|
||||
|
||||
def _get_spatial_filter(self):
|
||||
try:
|
||||
return OGRGeometry(geom_api.clone_geom(capi.get_spatial_filter(self.ptr)))
|
||||
except GDALException:
|
||||
return None
|
||||
|
||||
def _set_spatial_filter(self, filter):
|
||||
if isinstance(filter, OGRGeometry):
|
||||
capi.set_spatial_filter(self.ptr, filter.ptr)
|
||||
elif isinstance(filter, (tuple, list)):
|
||||
if not len(filter) == 4:
|
||||
raise ValueError("Spatial filter list/tuple must have 4 elements.")
|
||||
# Map c_double onto params -- if a bad type is passed in it
|
||||
# will be caught here.
|
||||
xmin, ymin, xmax, ymax = map(c_double, filter)
|
||||
capi.set_spatial_filter_rect(self.ptr, xmin, ymin, xmax, ymax)
|
||||
elif filter is None:
|
||||
capi.set_spatial_filter(self.ptr, None)
|
||||
else:
|
||||
raise TypeError(
|
||||
"Spatial filter must be either an OGRGeometry instance, a 4-tuple, or "
|
||||
"None."
|
||||
)
|
||||
|
||||
spatial_filter = property(_get_spatial_filter, _set_spatial_filter)
|
||||
|
||||
# #### Layer Methods ####
|
||||
def get_fields(self, field_name):
|
||||
"""
|
||||
Return a list containing the given field name for every Feature
|
||||
in the Layer.
|
||||
"""
|
||||
if field_name not in self.fields:
|
||||
raise GDALException("invalid field name: %s" % field_name)
|
||||
return [feat.get(field_name) for feat in self]
|
||||
|
||||
def get_geoms(self, geos=False):
|
||||
"""
|
||||
Return a list containing the OGRGeometry for every Feature in
|
||||
the Layer.
|
||||
"""
|
||||
if geos:
|
||||
from django.contrib.gis.geos import GEOSGeometry
|
||||
|
||||
return [GEOSGeometry(feat.geom.wkb) for feat in self]
|
||||
else:
|
||||
return [feat.geom for feat in self]
|
||||
|
||||
def test_capability(self, capability):
|
||||
"""
|
||||
Return a bool indicating whether the this Layer supports the given
|
||||
capability (a string). Valid capability strings include:
|
||||
'RandomRead', 'SequentialWrite', 'RandomWrite', 'FastSpatialFilter',
|
||||
'FastFeatureCount', 'FastGetExtent', 'CreateField', 'Transactions',
|
||||
'DeleteFeature', and 'FastSetNextByIndex'.
|
||||
"""
|
||||
return bool(capi.test_capability(self.ptr, force_bytes(capability)))
|
@ -0,0 +1,142 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from ctypes import CDLL, CFUNCTYPE, c_char_p, c_int
|
||||
from ctypes.util import find_library
|
||||
|
||||
from django.contrib.gis.gdal.error import GDALException
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
logger = logging.getLogger("django.contrib.gis")
|
||||
|
||||
# Custom library path set?
|
||||
try:
|
||||
from django.conf import settings
|
||||
|
||||
lib_path = settings.GDAL_LIBRARY_PATH
|
||||
except (AttributeError, ImportError, ImproperlyConfigured, OSError):
|
||||
lib_path = None
|
||||
|
||||
if lib_path:
|
||||
lib_names = None
|
||||
elif os.name == "nt":
|
||||
# Windows NT shared libraries
|
||||
lib_names = [
|
||||
"gdal306",
|
||||
"gdal305",
|
||||
"gdal304",
|
||||
"gdal303",
|
||||
"gdal302",
|
||||
"gdal301",
|
||||
"gdal300",
|
||||
"gdal204",
|
||||
"gdal203",
|
||||
"gdal202",
|
||||
]
|
||||
elif os.name == "posix":
|
||||
# *NIX library names.
|
||||
lib_names = [
|
||||
"gdal",
|
||||
"GDAL",
|
||||
"gdal3.6.0",
|
||||
"gdal3.5.0",
|
||||
"gdal3.4.0",
|
||||
"gdal3.3.0",
|
||||
"gdal3.2.0",
|
||||
"gdal3.1.0",
|
||||
"gdal3.0.0",
|
||||
"gdal2.4.0",
|
||||
"gdal2.3.0",
|
||||
"gdal2.2.0",
|
||||
]
|
||||
else:
|
||||
raise ImproperlyConfigured('GDAL is unsupported on OS "%s".' % os.name)
|
||||
|
||||
# Using the ctypes `find_library` utility to find the
|
||||
# path to the GDAL library from the list of library names.
|
||||
if lib_names:
|
||||
for lib_name in lib_names:
|
||||
lib_path = find_library(lib_name)
|
||||
if lib_path is not None:
|
||||
break
|
||||
|
||||
if lib_path is None:
|
||||
raise ImproperlyConfigured(
|
||||
'Could not find the GDAL library (tried "%s"). Is GDAL installed? '
|
||||
"If it is, try setting GDAL_LIBRARY_PATH in your settings."
|
||||
% '", "'.join(lib_names)
|
||||
)
|
||||
|
||||
# This loads the GDAL/OGR C library
|
||||
lgdal = CDLL(lib_path)
|
||||
|
||||
# On Windows, the GDAL binaries have some OSR routines exported with
|
||||
# STDCALL, while others are not. Thus, the library will also need to
|
||||
# be loaded up as WinDLL for said OSR functions that require the
|
||||
# different calling convention.
|
||||
if os.name == "nt":
|
||||
from ctypes import WinDLL
|
||||
|
||||
lwingdal = WinDLL(lib_path)
|
||||
|
||||
|
||||
def std_call(func):
|
||||
"""
|
||||
Return the correct STDCALL function for certain OSR routines on Win32
|
||||
platforms.
|
||||
"""
|
||||
if os.name == "nt":
|
||||
return lwingdal[func]
|
||||
else:
|
||||
return lgdal[func]
|
||||
|
||||
|
||||
# #### Version-information functions. ####
|
||||
|
||||
# Return GDAL library version information with the given key.
|
||||
_version_info = std_call("GDALVersionInfo")
|
||||
_version_info.argtypes = [c_char_p]
|
||||
_version_info.restype = c_char_p
|
||||
|
||||
|
||||
def gdal_version():
|
||||
"Return only the GDAL version number information."
|
||||
return _version_info(b"RELEASE_NAME")
|
||||
|
||||
|
||||
def gdal_full_version():
|
||||
"Return the full GDAL version information."
|
||||
return _version_info(b"")
|
||||
|
||||
|
||||
def gdal_version_info():
|
||||
ver = gdal_version()
|
||||
m = re.match(rb"^(?P<major>\d+)\.(?P<minor>\d+)(?:\.(?P<subminor>\d+))?", ver)
|
||||
if not m:
|
||||
raise GDALException('Could not parse GDAL version string "%s"' % ver)
|
||||
major, minor, subminor = m.groups()
|
||||
return (int(major), int(minor), subminor and int(subminor))
|
||||
|
||||
|
||||
GDAL_VERSION = gdal_version_info()
|
||||
|
||||
# Set library error handling so as errors are logged
|
||||
CPLErrorHandler = CFUNCTYPE(None, c_int, c_int, c_char_p)
|
||||
|
||||
|
||||
def err_handler(error_class, error_number, message):
|
||||
logger.error("GDAL_ERROR %d: %s", error_number, message)
|
||||
|
||||
|
||||
err_handler = CPLErrorHandler(err_handler)
|
||||
|
||||
|
||||
def function(name, args, restype):
|
||||
func = std_call(name)
|
||||
func.argtypes = args
|
||||
func.restype = restype
|
||||
return func
|
||||
|
||||
|
||||
set_error_handler = function("CPLSetErrorHandler", [CPLErrorHandler], CPLErrorHandler)
|
||||
set_error_handler(err_handler)
|
@ -0,0 +1,99 @@
|
||||
"""
|
||||
This module houses the ctypes function prototypes for OGR DataSource
|
||||
related data structures. OGR_Dr_*, OGR_DS_*, OGR_L_*, OGR_F_*,
|
||||
OGR_Fld_* routines are relevant here.
|
||||
"""
|
||||
from ctypes import POINTER, c_char_p, c_double, c_int, c_long, c_void_p
|
||||
|
||||
from django.contrib.gis.gdal.envelope import OGREnvelope
|
||||
from django.contrib.gis.gdal.libgdal import lgdal
|
||||
from django.contrib.gis.gdal.prototypes.generation import (
|
||||
bool_output,
|
||||
const_string_output,
|
||||
double_output,
|
||||
geom_output,
|
||||
int64_output,
|
||||
int_output,
|
||||
srs_output,
|
||||
void_output,
|
||||
voidptr_output,
|
||||
)
|
||||
|
||||
c_int_p = POINTER(c_int) # shortcut type
|
||||
|
||||
# Driver Routines
|
||||
register_all = void_output(lgdal.OGRRegisterAll, [], errcheck=False)
|
||||
cleanup_all = void_output(lgdal.OGRCleanupAll, [], errcheck=False)
|
||||
get_driver = voidptr_output(lgdal.OGRGetDriver, [c_int])
|
||||
get_driver_by_name = voidptr_output(
|
||||
lgdal.OGRGetDriverByName, [c_char_p], errcheck=False
|
||||
)
|
||||
get_driver_count = int_output(lgdal.OGRGetDriverCount, [])
|
||||
get_driver_name = const_string_output(
|
||||
lgdal.OGR_Dr_GetName, [c_void_p], decoding="ascii"
|
||||
)
|
||||
|
||||
# DataSource
|
||||
open_ds = voidptr_output(lgdal.OGROpen, [c_char_p, c_int, POINTER(c_void_p)])
|
||||
destroy_ds = void_output(lgdal.OGR_DS_Destroy, [c_void_p], errcheck=False)
|
||||
release_ds = void_output(lgdal.OGRReleaseDataSource, [c_void_p])
|
||||
get_ds_name = const_string_output(lgdal.OGR_DS_GetName, [c_void_p])
|
||||
get_layer = voidptr_output(lgdal.OGR_DS_GetLayer, [c_void_p, c_int])
|
||||
get_layer_by_name = voidptr_output(lgdal.OGR_DS_GetLayerByName, [c_void_p, c_char_p])
|
||||
get_layer_count = int_output(lgdal.OGR_DS_GetLayerCount, [c_void_p])
|
||||
|
||||
# Layer Routines
|
||||
get_extent = void_output(lgdal.OGR_L_GetExtent, [c_void_p, POINTER(OGREnvelope), c_int])
|
||||
get_feature = voidptr_output(lgdal.OGR_L_GetFeature, [c_void_p, c_long])
|
||||
get_feature_count = int_output(lgdal.OGR_L_GetFeatureCount, [c_void_p, c_int])
|
||||
get_layer_defn = voidptr_output(lgdal.OGR_L_GetLayerDefn, [c_void_p])
|
||||
get_layer_srs = srs_output(lgdal.OGR_L_GetSpatialRef, [c_void_p])
|
||||
get_next_feature = voidptr_output(lgdal.OGR_L_GetNextFeature, [c_void_p])
|
||||
reset_reading = void_output(lgdal.OGR_L_ResetReading, [c_void_p], errcheck=False)
|
||||
test_capability = int_output(lgdal.OGR_L_TestCapability, [c_void_p, c_char_p])
|
||||
get_spatial_filter = geom_output(lgdal.OGR_L_GetSpatialFilter, [c_void_p])
|
||||
set_spatial_filter = void_output(
|
||||
lgdal.OGR_L_SetSpatialFilter, [c_void_p, c_void_p], errcheck=False
|
||||
)
|
||||
set_spatial_filter_rect = void_output(
|
||||
lgdal.OGR_L_SetSpatialFilterRect,
|
||||
[c_void_p, c_double, c_double, c_double, c_double],
|
||||
errcheck=False,
|
||||
)
|
||||
|
||||
# Feature Definition Routines
|
||||
get_fd_geom_type = int_output(lgdal.OGR_FD_GetGeomType, [c_void_p])
|
||||
get_fd_name = const_string_output(lgdal.OGR_FD_GetName, [c_void_p])
|
||||
get_feat_name = const_string_output(lgdal.OGR_FD_GetName, [c_void_p])
|
||||
get_field_count = int_output(lgdal.OGR_FD_GetFieldCount, [c_void_p])
|
||||
get_field_defn = voidptr_output(lgdal.OGR_FD_GetFieldDefn, [c_void_p, c_int])
|
||||
|
||||
# Feature Routines
|
||||
clone_feature = voidptr_output(lgdal.OGR_F_Clone, [c_void_p])
|
||||
destroy_feature = void_output(lgdal.OGR_F_Destroy, [c_void_p], errcheck=False)
|
||||
feature_equal = int_output(lgdal.OGR_F_Equal, [c_void_p, c_void_p])
|
||||
get_feat_geom_ref = geom_output(lgdal.OGR_F_GetGeometryRef, [c_void_p])
|
||||
get_feat_field_count = int_output(lgdal.OGR_F_GetFieldCount, [c_void_p])
|
||||
get_feat_field_defn = voidptr_output(lgdal.OGR_F_GetFieldDefnRef, [c_void_p, c_int])
|
||||
get_fid = int_output(lgdal.OGR_F_GetFID, [c_void_p])
|
||||
get_field_as_datetime = int_output(
|
||||
lgdal.OGR_F_GetFieldAsDateTime,
|
||||
[c_void_p, c_int, c_int_p, c_int_p, c_int_p, c_int_p, c_int_p, c_int_p],
|
||||
)
|
||||
get_field_as_double = double_output(lgdal.OGR_F_GetFieldAsDouble, [c_void_p, c_int])
|
||||
get_field_as_integer = int_output(lgdal.OGR_F_GetFieldAsInteger, [c_void_p, c_int])
|
||||
get_field_as_integer64 = int64_output(
|
||||
lgdal.OGR_F_GetFieldAsInteger64, [c_void_p, c_int]
|
||||
)
|
||||
is_field_set = bool_output(lgdal.OGR_F_IsFieldSetAndNotNull, [c_void_p, c_int])
|
||||
get_field_as_string = const_string_output(
|
||||
lgdal.OGR_F_GetFieldAsString, [c_void_p, c_int]
|
||||
)
|
||||
get_field_index = int_output(lgdal.OGR_F_GetFieldIndex, [c_void_p, c_char_p])
|
||||
|
||||
# Field Routines
|
||||
get_field_name = const_string_output(lgdal.OGR_Fld_GetNameRef, [c_void_p])
|
||||
get_field_precision = int_output(lgdal.OGR_Fld_GetPrecision, [c_void_p])
|
||||
get_field_type = int_output(lgdal.OGR_Fld_GetType, [c_void_p])
|
||||
get_field_type_name = const_string_output(lgdal.OGR_GetFieldTypeName, [c_int])
|
||||
get_field_width = int_output(lgdal.OGR_Fld_GetWidth, [c_void_p])
|
@ -0,0 +1,141 @@
|
||||
"""
|
||||
This module houses the error-checking routines used by the GDAL
|
||||
ctypes prototypes.
|
||||
"""
|
||||
from ctypes import c_void_p, string_at
|
||||
|
||||
from django.contrib.gis.gdal.error import GDALException, SRSException, check_err
|
||||
from django.contrib.gis.gdal.libgdal import lgdal
|
||||
|
||||
|
||||
# Helper routines for retrieving pointers and/or values from
|
||||
# arguments passed in by reference.
|
||||
def arg_byref(args, offset=-1):
|
||||
"Return the pointer argument's by-reference value."
|
||||
return args[offset]._obj.value
|
||||
|
||||
|
||||
def ptr_byref(args, offset=-1):
|
||||
"Return the pointer argument passed in by-reference."
|
||||
return args[offset]._obj
|
||||
|
||||
|
||||
# ### String checking Routines ###
|
||||
def check_const_string(result, func, cargs, offset=None, cpl=False):
|
||||
"""
|
||||
Similar functionality to `check_string`, but does not free the pointer.
|
||||
"""
|
||||
if offset:
|
||||
check_err(result, cpl=cpl)
|
||||
ptr = ptr_byref(cargs, offset)
|
||||
return ptr.value
|
||||
else:
|
||||
return result
|
||||
|
||||
|
||||
def check_string(result, func, cargs, offset=-1, str_result=False):
|
||||
"""
|
||||
Check the string output returned from the given function, and free
|
||||
the string pointer allocated by OGR. The `str_result` keyword
|
||||
may be used when the result is the string pointer, otherwise
|
||||
the OGR error code is assumed. The `offset` keyword may be used
|
||||
to extract the string pointer passed in by-reference at the given
|
||||
slice offset in the function arguments.
|
||||
"""
|
||||
if str_result:
|
||||
# For routines that return a string.
|
||||
ptr = result
|
||||
if not ptr:
|
||||
s = None
|
||||
else:
|
||||
s = string_at(result)
|
||||
else:
|
||||
# Error-code return specified.
|
||||
check_err(result)
|
||||
ptr = ptr_byref(cargs, offset)
|
||||
# Getting the string value
|
||||
s = ptr.value
|
||||
# Correctly freeing the allocated memory behind GDAL pointer
|
||||
# with the VSIFree routine.
|
||||
if ptr:
|
||||
lgdal.VSIFree(ptr)
|
||||
return s
|
||||
|
||||
|
||||
# ### DataSource, Layer error-checking ###
|
||||
|
||||
|
||||
# ### Envelope checking ###
|
||||
def check_envelope(result, func, cargs, offset=-1):
|
||||
"Check a function that returns an OGR Envelope by reference."
|
||||
return ptr_byref(cargs, offset)
|
||||
|
||||
|
||||
# ### Geometry error-checking routines ###
|
||||
def check_geom(result, func, cargs):
|
||||
"Check a function that returns a geometry."
|
||||
# OGR_G_Clone may return an integer, even though the
|
||||
# restype is set to c_void_p
|
||||
if isinstance(result, int):
|
||||
result = c_void_p(result)
|
||||
if not result:
|
||||
raise GDALException(
|
||||
'Invalid geometry pointer returned from "%s".' % func.__name__
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def check_geom_offset(result, func, cargs, offset=-1):
|
||||
"Check the geometry at the given offset in the C parameter list."
|
||||
check_err(result)
|
||||
geom = ptr_byref(cargs, offset=offset)
|
||||
return check_geom(geom, func, cargs)
|
||||
|
||||
|
||||
# ### Spatial Reference error-checking routines ###
|
||||
def check_srs(result, func, cargs):
|
||||
if isinstance(result, int):
|
||||
result = c_void_p(result)
|
||||
if not result:
|
||||
raise SRSException(
|
||||
'Invalid spatial reference pointer returned from "%s".' % func.__name__
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
# ### Other error-checking routines ###
|
||||
def check_arg_errcode(result, func, cargs, cpl=False):
|
||||
"""
|
||||
The error code is returned in the last argument, by reference.
|
||||
Check its value with `check_err` before returning the result.
|
||||
"""
|
||||
check_err(arg_byref(cargs), cpl=cpl)
|
||||
return result
|
||||
|
||||
|
||||
def check_errcode(result, func, cargs, cpl=False):
|
||||
"""
|
||||
Check the error code returned (c_int).
|
||||
"""
|
||||
check_err(result, cpl=cpl)
|
||||
|
||||
|
||||
def check_pointer(result, func, cargs):
|
||||
"Make sure the result pointer is valid."
|
||||
if isinstance(result, int):
|
||||
result = c_void_p(result)
|
||||
if result:
|
||||
return result
|
||||
else:
|
||||
raise GDALException('Invalid pointer returned from "%s"' % func.__name__)
|
||||
|
||||
|
||||
def check_str_arg(result, func, cargs):
|
||||
"""
|
||||
This is for the OSRGet[Angular|Linear]Units functions, which
|
||||
require that the returned string pointer not be freed. This
|
||||
returns both the double and string values.
|
||||
"""
|
||||
dbl = result
|
||||
ptr = cargs[-1]._obj
|
||||
return dbl, ptr.value.decode()
|
@ -0,0 +1,177 @@
|
||||
"""
|
||||
This module contains functions that generate ctypes prototypes for the
|
||||
GDAL routines.
|
||||
"""
|
||||
from ctypes import POINTER, c_bool, c_char_p, c_double, c_int, c_int64, c_void_p
|
||||
from functools import partial
|
||||
|
||||
from django.contrib.gis.gdal.prototypes.errcheck import (
|
||||
check_arg_errcode,
|
||||
check_const_string,
|
||||
check_errcode,
|
||||
check_geom,
|
||||
check_geom_offset,
|
||||
check_pointer,
|
||||
check_srs,
|
||||
check_str_arg,
|
||||
check_string,
|
||||
)
|
||||
|
||||
|
||||
class gdal_char_p(c_char_p):
|
||||
pass
|
||||
|
||||
|
||||
def bool_output(func, argtypes, errcheck=None):
|
||||
"""Generate a ctypes function that returns a boolean value."""
|
||||
func.argtypes = argtypes
|
||||
func.restype = c_bool
|
||||
if errcheck:
|
||||
func.errcheck = errcheck
|
||||
return func
|
||||
|
||||
|
||||
def double_output(func, argtypes, errcheck=False, strarg=False, cpl=False):
|
||||
"Generate a ctypes function that returns a double value."
|
||||
func.argtypes = argtypes
|
||||
func.restype = c_double
|
||||
if errcheck:
|
||||
func.errcheck = partial(check_arg_errcode, cpl=cpl)
|
||||
if strarg:
|
||||
func.errcheck = check_str_arg
|
||||
return func
|
||||
|
||||
|
||||
def geom_output(func, argtypes, offset=None):
|
||||
"""
|
||||
Generate a function that returns a Geometry either by reference
|
||||
or directly (if the return_geom keyword is set to True).
|
||||
"""
|
||||
# Setting the argument types
|
||||
func.argtypes = argtypes
|
||||
|
||||
if not offset:
|
||||
# When a geometry pointer is directly returned.
|
||||
func.restype = c_void_p
|
||||
func.errcheck = check_geom
|
||||
else:
|
||||
# Error code returned, geometry is returned by-reference.
|
||||
func.restype = c_int
|
||||
|
||||
def geomerrcheck(result, func, cargs):
|
||||
return check_geom_offset(result, func, cargs, offset)
|
||||
|
||||
func.errcheck = geomerrcheck
|
||||
|
||||
return func
|
||||
|
||||
|
||||
def int_output(func, argtypes, errcheck=None):
|
||||
"Generate a ctypes function that returns an integer value."
|
||||
func.argtypes = argtypes
|
||||
func.restype = c_int
|
||||
if errcheck:
|
||||
func.errcheck = errcheck
|
||||
return func
|
||||
|
||||
|
||||
def int64_output(func, argtypes):
|
||||
"Generate a ctypes function that returns a 64-bit integer value."
|
||||
func.argtypes = argtypes
|
||||
func.restype = c_int64
|
||||
return func
|
||||
|
||||
|
||||
def srs_output(func, argtypes):
|
||||
"""
|
||||
Generate a ctypes prototype for the given function with
|
||||
the given C arguments that returns a pointer to an OGR
|
||||
Spatial Reference System.
|
||||
"""
|
||||
func.argtypes = argtypes
|
||||
func.restype = c_void_p
|
||||
func.errcheck = check_srs
|
||||
return func
|
||||
|
||||
|
||||
def const_string_output(func, argtypes, offset=None, decoding=None, cpl=False):
|
||||
func.argtypes = argtypes
|
||||
if offset:
|
||||
func.restype = c_int
|
||||
else:
|
||||
func.restype = c_char_p
|
||||
|
||||
def _check_const(result, func, cargs):
|
||||
res = check_const_string(result, func, cargs, offset=offset, cpl=cpl)
|
||||
if res and decoding:
|
||||
res = res.decode(decoding)
|
||||
return res
|
||||
|
||||
func.errcheck = _check_const
|
||||
|
||||
return func
|
||||
|
||||
|
||||
def string_output(func, argtypes, offset=-1, str_result=False, decoding=None):
|
||||
"""
|
||||
Generate a ctypes prototype for the given function with the
|
||||
given argument types that returns a string from a GDAL pointer.
|
||||
The `const` flag indicates whether the allocated pointer should
|
||||
be freed via the GDAL library routine VSIFree -- but only applies
|
||||
only when `str_result` is True.
|
||||
"""
|
||||
func.argtypes = argtypes
|
||||
if str_result:
|
||||
# Use subclass of c_char_p so the error checking routine
|
||||
# can free the memory at the pointer's address.
|
||||
func.restype = gdal_char_p
|
||||
else:
|
||||
# Error code is returned
|
||||
func.restype = c_int
|
||||
|
||||
# Dynamically defining our error-checking function with the
|
||||
# given offset.
|
||||
def _check_str(result, func, cargs):
|
||||
res = check_string(result, func, cargs, offset=offset, str_result=str_result)
|
||||
if res and decoding:
|
||||
res = res.decode(decoding)
|
||||
return res
|
||||
|
||||
func.errcheck = _check_str
|
||||
return func
|
||||
|
||||
|
||||
def void_output(func, argtypes, errcheck=True, cpl=False):
|
||||
"""
|
||||
For functions that don't only return an error code that needs to
|
||||
be examined.
|
||||
"""
|
||||
if argtypes:
|
||||
func.argtypes = argtypes
|
||||
if errcheck:
|
||||
# `errcheck` keyword may be set to False for routines that
|
||||
# return void, rather than a status code.
|
||||
func.restype = c_int
|
||||
func.errcheck = partial(check_errcode, cpl=cpl)
|
||||
else:
|
||||
func.restype = None
|
||||
|
||||
return func
|
||||
|
||||
|
||||
def voidptr_output(func, argtypes, errcheck=True):
|
||||
"For functions that return c_void_p."
|
||||
func.argtypes = argtypes
|
||||
func.restype = c_void_p
|
||||
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
|
@ -0,0 +1,139 @@
|
||||
from ctypes import POINTER, c_char_p, c_double, c_int, c_void_p
|
||||
|
||||
from django.contrib.gis.gdal.envelope import OGREnvelope
|
||||
from django.contrib.gis.gdal.libgdal import lgdal
|
||||
from django.contrib.gis.gdal.prototypes.errcheck import check_envelope
|
||||
from django.contrib.gis.gdal.prototypes.generation import (
|
||||
const_string_output,
|
||||
double_output,
|
||||
geom_output,
|
||||
int_output,
|
||||
srs_output,
|
||||
string_output,
|
||||
void_output,
|
||||
)
|
||||
|
||||
|
||||
# ### Generation routines specific to this module ###
|
||||
def env_func(f, argtypes):
|
||||
"For getting OGREnvelopes."
|
||||
f.argtypes = argtypes
|
||||
f.restype = None
|
||||
f.errcheck = check_envelope
|
||||
return f
|
||||
|
||||
|
||||
def pnt_func(f):
|
||||
"For accessing point information."
|
||||
return double_output(f, [c_void_p, c_int])
|
||||
|
||||
|
||||
def topology_func(f):
|
||||
f.argtypes = [c_void_p, c_void_p]
|
||||
f.restype = c_int
|
||||
f.errcheck = lambda result, func, cargs: bool(result)
|
||||
return f
|
||||
|
||||
|
||||
# ### OGR_G ctypes function prototypes ###
|
||||
|
||||
# GeoJSON routines.
|
||||
from_json = geom_output(lgdal.OGR_G_CreateGeometryFromJson, [c_char_p])
|
||||
to_json = string_output(
|
||||
lgdal.OGR_G_ExportToJson, [c_void_p], str_result=True, decoding="ascii"
|
||||
)
|
||||
to_kml = string_output(
|
||||
lgdal.OGR_G_ExportToKML, [c_void_p, c_char_p], str_result=True, decoding="ascii"
|
||||
)
|
||||
|
||||
# GetX, GetY, GetZ all return doubles.
|
||||
getx = pnt_func(lgdal.OGR_G_GetX)
|
||||
gety = pnt_func(lgdal.OGR_G_GetY)
|
||||
getz = pnt_func(lgdal.OGR_G_GetZ)
|
||||
|
||||
# Geometry creation routines.
|
||||
from_wkb = geom_output(
|
||||
lgdal.OGR_G_CreateFromWkb, [c_char_p, c_void_p, POINTER(c_void_p), c_int], offset=-2
|
||||
)
|
||||
from_wkt = geom_output(
|
||||
lgdal.OGR_G_CreateFromWkt,
|
||||
[POINTER(c_char_p), c_void_p, POINTER(c_void_p)],
|
||||
offset=-1,
|
||||
)
|
||||
from_gml = geom_output(lgdal.OGR_G_CreateFromGML, [c_char_p])
|
||||
create_geom = geom_output(lgdal.OGR_G_CreateGeometry, [c_int])
|
||||
clone_geom = geom_output(lgdal.OGR_G_Clone, [c_void_p])
|
||||
get_geom_ref = geom_output(lgdal.OGR_G_GetGeometryRef, [c_void_p, c_int])
|
||||
get_boundary = geom_output(lgdal.OGR_G_GetBoundary, [c_void_p])
|
||||
geom_convex_hull = geom_output(lgdal.OGR_G_ConvexHull, [c_void_p])
|
||||
geom_diff = geom_output(lgdal.OGR_G_Difference, [c_void_p, c_void_p])
|
||||
geom_intersection = geom_output(lgdal.OGR_G_Intersection, [c_void_p, c_void_p])
|
||||
geom_sym_diff = geom_output(lgdal.OGR_G_SymmetricDifference, [c_void_p, c_void_p])
|
||||
geom_union = geom_output(lgdal.OGR_G_Union, [c_void_p, c_void_p])
|
||||
|
||||
# Geometry modification routines.
|
||||
add_geom = void_output(lgdal.OGR_G_AddGeometry, [c_void_p, c_void_p])
|
||||
import_wkt = void_output(lgdal.OGR_G_ImportFromWkt, [c_void_p, POINTER(c_char_p)])
|
||||
|
||||
# Destroys a geometry
|
||||
destroy_geom = void_output(lgdal.OGR_G_DestroyGeometry, [c_void_p], errcheck=False)
|
||||
|
||||
# Geometry export routines.
|
||||
to_wkb = void_output(
|
||||
lgdal.OGR_G_ExportToWkb, None, errcheck=True
|
||||
) # special handling for WKB.
|
||||
to_wkt = string_output(
|
||||
lgdal.OGR_G_ExportToWkt, [c_void_p, POINTER(c_char_p)], decoding="ascii"
|
||||
)
|
||||
to_gml = string_output(
|
||||
lgdal.OGR_G_ExportToGML, [c_void_p], str_result=True, decoding="ascii"
|
||||
)
|
||||
get_wkbsize = int_output(lgdal.OGR_G_WkbSize, [c_void_p])
|
||||
|
||||
# Geometry spatial-reference related routines.
|
||||
assign_srs = void_output(
|
||||
lgdal.OGR_G_AssignSpatialReference, [c_void_p, c_void_p], errcheck=False
|
||||
)
|
||||
get_geom_srs = srs_output(lgdal.OGR_G_GetSpatialReference, [c_void_p])
|
||||
|
||||
# Geometry properties
|
||||
get_area = double_output(lgdal.OGR_G_GetArea, [c_void_p])
|
||||
get_centroid = void_output(lgdal.OGR_G_Centroid, [c_void_p, c_void_p])
|
||||
get_dims = int_output(lgdal.OGR_G_GetDimension, [c_void_p])
|
||||
get_coord_dim = int_output(lgdal.OGR_G_GetCoordinateDimension, [c_void_p])
|
||||
set_coord_dim = void_output(
|
||||
lgdal.OGR_G_SetCoordinateDimension, [c_void_p, c_int], errcheck=False
|
||||
)
|
||||
is_empty = int_output(
|
||||
lgdal.OGR_G_IsEmpty, [c_void_p], errcheck=lambda result, func, cargs: bool(result)
|
||||
)
|
||||
|
||||
get_geom_count = int_output(lgdal.OGR_G_GetGeometryCount, [c_void_p])
|
||||
get_geom_name = const_string_output(
|
||||
lgdal.OGR_G_GetGeometryName, [c_void_p], decoding="ascii"
|
||||
)
|
||||
get_geom_type = int_output(lgdal.OGR_G_GetGeometryType, [c_void_p])
|
||||
get_point_count = int_output(lgdal.OGR_G_GetPointCount, [c_void_p])
|
||||
get_point = void_output(
|
||||
lgdal.OGR_G_GetPoint,
|
||||
[c_void_p, c_int, POINTER(c_double), POINTER(c_double), POINTER(c_double)],
|
||||
errcheck=False,
|
||||
)
|
||||
geom_close_rings = void_output(lgdal.OGR_G_CloseRings, [c_void_p], errcheck=False)
|
||||
|
||||
# Topology routines.
|
||||
ogr_contains = topology_func(lgdal.OGR_G_Contains)
|
||||
ogr_crosses = topology_func(lgdal.OGR_G_Crosses)
|
||||
ogr_disjoint = topology_func(lgdal.OGR_G_Disjoint)
|
||||
ogr_equals = topology_func(lgdal.OGR_G_Equals)
|
||||
ogr_intersects = topology_func(lgdal.OGR_G_Intersects)
|
||||
ogr_overlaps = topology_func(lgdal.OGR_G_Overlaps)
|
||||
ogr_touches = topology_func(lgdal.OGR_G_Touches)
|
||||
ogr_within = topology_func(lgdal.OGR_G_Within)
|
||||
|
||||
# Transformation routines.
|
||||
geom_transform = void_output(lgdal.OGR_G_Transform, [c_void_p, c_void_p])
|
||||
geom_transform_to = void_output(lgdal.OGR_G_TransformTo, [c_void_p, c_void_p])
|
||||
|
||||
# For retrieving the envelope of the geometry.
|
||||
get_envelope = env_func(lgdal.OGR_G_GetEnvelope, [c_void_p, POINTER(OGREnvelope)])
|
@ -0,0 +1,177 @@
|
||||
"""
|
||||
This module houses the ctypes function prototypes for GDAL DataSource (raster)
|
||||
related data structures.
|
||||
"""
|
||||
from ctypes import POINTER, c_bool, c_char_p, c_double, c_int, c_void_p
|
||||
from functools import partial
|
||||
|
||||
from django.contrib.gis.gdal.libgdal import std_call
|
||||
from django.contrib.gis.gdal.prototypes.generation import (
|
||||
chararray_output,
|
||||
const_string_output,
|
||||
double_output,
|
||||
int_output,
|
||||
void_output,
|
||||
voidptr_output,
|
||||
)
|
||||
|
||||
# For more detail about c function names and definitions see
|
||||
# https://gdal.org/api/raster_c_api.html
|
||||
# https://gdal.org/doxygen/gdalwarper_8h.html
|
||||
# https://gdal.org/api/gdal_utils.html
|
||||
|
||||
# Prepare partial functions that use cpl error codes
|
||||
void_output = partial(void_output, cpl=True)
|
||||
const_string_output = partial(const_string_output, cpl=True)
|
||||
double_output = partial(double_output, cpl=True)
|
||||
|
||||
# Raster Driver Routines
|
||||
register_all = void_output(std_call("GDALAllRegister"), [], errcheck=False)
|
||||
get_driver = voidptr_output(std_call("GDALGetDriver"), [c_int])
|
||||
get_driver_by_name = voidptr_output(
|
||||
std_call("GDALGetDriverByName"), [c_char_p], errcheck=False
|
||||
)
|
||||
get_driver_count = int_output(std_call("GDALGetDriverCount"), [])
|
||||
get_driver_description = const_string_output(std_call("GDALGetDescription"), [c_void_p])
|
||||
|
||||
# Raster Data Source Routines
|
||||
create_ds = voidptr_output(
|
||||
std_call("GDALCreate"), [c_void_p, c_char_p, c_int, c_int, c_int, c_int, c_void_p]
|
||||
)
|
||||
open_ds = voidptr_output(std_call("GDALOpen"), [c_char_p, c_int])
|
||||
close_ds = void_output(std_call("GDALClose"), [c_void_p], errcheck=False)
|
||||
flush_ds = int_output(std_call("GDALFlushCache"), [c_void_p])
|
||||
copy_ds = voidptr_output(
|
||||
std_call("GDALCreateCopy"),
|
||||
[c_void_p, c_char_p, c_void_p, c_int, POINTER(c_char_p), c_void_p, c_void_p],
|
||||
)
|
||||
add_band_ds = void_output(std_call("GDALAddBand"), [c_void_p, c_int])
|
||||
get_ds_description = const_string_output(std_call("GDALGetDescription"), [c_void_p])
|
||||
get_ds_driver = voidptr_output(std_call("GDALGetDatasetDriver"), [c_void_p])
|
||||
get_ds_info = const_string_output(std_call("GDALInfo"), [c_void_p, c_void_p])
|
||||
get_ds_xsize = int_output(std_call("GDALGetRasterXSize"), [c_void_p])
|
||||
get_ds_ysize = int_output(std_call("GDALGetRasterYSize"), [c_void_p])
|
||||
get_ds_raster_count = int_output(std_call("GDALGetRasterCount"), [c_void_p])
|
||||
get_ds_raster_band = voidptr_output(std_call("GDALGetRasterBand"), [c_void_p, c_int])
|
||||
get_ds_projection_ref = const_string_output(
|
||||
std_call("GDALGetProjectionRef"), [c_void_p]
|
||||
)
|
||||
set_ds_projection_ref = void_output(std_call("GDALSetProjection"), [c_void_p, c_char_p])
|
||||
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]
|
||||
)
|
||||
get_ds_metadata_domain_list = chararray_output(
|
||||
std_call("GDALGetMetadataDomainList"), [c_void_p], errcheck=False
|
||||
)
|
||||
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)
|
||||
|
||||
# Raster Band Routines
|
||||
band_io = void_output(
|
||||
std_call("GDALRasterIO"),
|
||||
[
|
||||
c_void_p,
|
||||
c_int,
|
||||
c_int,
|
||||
c_int,
|
||||
c_int,
|
||||
c_int,
|
||||
c_void_p,
|
||||
c_int,
|
||||
c_int,
|
||||
c_int,
|
||||
c_int,
|
||||
c_int,
|
||||
],
|
||||
)
|
||||
get_band_xsize = int_output(std_call("GDALGetRasterBandXSize"), [c_void_p])
|
||||
get_band_ysize = int_output(std_call("GDALGetRasterBandYSize"), [c_void_p])
|
||||
get_band_index = int_output(std_call("GDALGetBandNumber"), [c_void_p])
|
||||
get_band_description = const_string_output(std_call("GDALGetDescription"), [c_void_p])
|
||||
get_band_ds = voidptr_output(std_call("GDALGetBandDataset"), [c_void_p])
|
||||
get_band_datatype = int_output(std_call("GDALGetRasterDataType"), [c_void_p])
|
||||
get_band_color_interp = int_output(
|
||||
std_call("GDALGetRasterColorInterpretation"), [c_void_p]
|
||||
)
|
||||
get_band_nodata_value = double_output(
|
||||
std_call("GDALGetRasterNoDataValue"), [c_void_p, POINTER(c_int)]
|
||||
)
|
||||
set_band_nodata_value = void_output(
|
||||
std_call("GDALSetRasterNoDataValue"), [c_void_p, c_double]
|
||||
)
|
||||
delete_band_nodata_value = void_output(
|
||||
std_call("GDALDeleteRasterNoDataValue"), [c_void_p]
|
||||
)
|
||||
get_band_statistics = void_output(
|
||||
std_call("GDALGetRasterStatistics"),
|
||||
[
|
||||
c_void_p,
|
||||
c_int,
|
||||
c_int,
|
||||
POINTER(c_double),
|
||||
POINTER(c_double),
|
||||
POINTER(c_double),
|
||||
POINTER(c_double),
|
||||
c_void_p,
|
||||
c_void_p,
|
||||
],
|
||||
)
|
||||
compute_band_statistics = void_output(
|
||||
std_call("GDALComputeRasterStatistics"),
|
||||
[
|
||||
c_void_p,
|
||||
c_int,
|
||||
POINTER(c_double),
|
||||
POINTER(c_double),
|
||||
POINTER(c_double),
|
||||
POINTER(c_double),
|
||||
c_void_p,
|
||||
c_void_p,
|
||||
],
|
||||
)
|
||||
|
||||
# Reprojection routine
|
||||
reproject_image = void_output(
|
||||
std_call("GDALReprojectImage"),
|
||||
[
|
||||
c_void_p,
|
||||
c_char_p,
|
||||
c_void_p,
|
||||
c_char_p,
|
||||
c_int,
|
||||
c_double,
|
||||
c_double,
|
||||
c_void_p,
|
||||
c_void_p,
|
||||
c_void_p,
|
||||
],
|
||||
)
|
||||
auto_create_warped_vrt = voidptr_output(
|
||||
std_call("GDALAutoCreateWarpedVRT"),
|
||||
[c_void_p, c_char_p, c_char_p, c_int, c_double, c_void_p],
|
||||
)
|
||||
|
||||
# Create VSI gdal raster files from in-memory buffers.
|
||||
# https://gdal.org/api/cpl.html#cpl-vsi-h
|
||||
create_vsi_file_from_mem_buffer = voidptr_output(
|
||||
std_call("VSIFileFromMemBuffer"), [c_char_p, c_void_p, c_int, c_int]
|
||||
)
|
||||
get_mem_buffer_from_vsi_file = voidptr_output(
|
||||
std_call("VSIGetMemFileBuffer"), [c_char_p, POINTER(c_int), c_bool]
|
||||
)
|
||||
unlink_vsi_file = int_output(std_call("VSIUnlink"), [c_char_p])
|
@ -0,0 +1,109 @@
|
||||
from ctypes import POINTER, c_char_p, c_int, c_void_p
|
||||
|
||||
from django.contrib.gis.gdal.libgdal import GDAL_VERSION, lgdal, std_call
|
||||
from django.contrib.gis.gdal.prototypes.generation import (
|
||||
const_string_output,
|
||||
double_output,
|
||||
int_output,
|
||||
srs_output,
|
||||
string_output,
|
||||
void_output,
|
||||
)
|
||||
|
||||
|
||||
# Shortcut generation for routines with known parameters.
|
||||
def srs_double(f):
|
||||
"""
|
||||
Create a function prototype for the OSR routines that take
|
||||
the OSRSpatialReference object and return a double value.
|
||||
"""
|
||||
return double_output(f, [c_void_p, POINTER(c_int)], errcheck=True)
|
||||
|
||||
|
||||
def units_func(f):
|
||||
"""
|
||||
Create a ctypes function prototype for OSR units functions, e.g.,
|
||||
OSRGetAngularUnits, OSRGetLinearUnits.
|
||||
"""
|
||||
return double_output(f, [c_void_p, POINTER(c_char_p)], strarg=True)
|
||||
|
||||
|
||||
# Creation & destruction.
|
||||
clone_srs = srs_output(std_call("OSRClone"), [c_void_p])
|
||||
new_srs = srs_output(std_call("OSRNewSpatialReference"), [c_char_p])
|
||||
release_srs = void_output(lgdal.OSRRelease, [c_void_p], errcheck=False)
|
||||
destroy_srs = void_output(
|
||||
std_call("OSRDestroySpatialReference"), [c_void_p], errcheck=False
|
||||
)
|
||||
srs_validate = void_output(lgdal.OSRValidate, [c_void_p])
|
||||
|
||||
if GDAL_VERSION >= (3, 0):
|
||||
set_axis_strategy = void_output(
|
||||
lgdal.OSRSetAxisMappingStrategy, [c_void_p, c_int], errcheck=False
|
||||
)
|
||||
|
||||
# Getting the semi_major, semi_minor, and flattening functions.
|
||||
semi_major = srs_double(lgdal.OSRGetSemiMajor)
|
||||
semi_minor = srs_double(lgdal.OSRGetSemiMinor)
|
||||
invflattening = srs_double(lgdal.OSRGetInvFlattening)
|
||||
|
||||
# WKT, PROJ, EPSG, XML importation routines.
|
||||
from_wkt = void_output(lgdal.OSRImportFromWkt, [c_void_p, POINTER(c_char_p)])
|
||||
from_proj = void_output(lgdal.OSRImportFromProj4, [c_void_p, c_char_p])
|
||||
from_epsg = void_output(std_call("OSRImportFromEPSG"), [c_void_p, c_int])
|
||||
from_xml = void_output(lgdal.OSRImportFromXML, [c_void_p, c_char_p])
|
||||
from_user_input = void_output(std_call("OSRSetFromUserInput"), [c_void_p, c_char_p])
|
||||
|
||||
# Morphing to/from ESRI WKT.
|
||||
morph_to_esri = void_output(lgdal.OSRMorphToESRI, [c_void_p])
|
||||
morph_from_esri = void_output(lgdal.OSRMorphFromESRI, [c_void_p])
|
||||
|
||||
# Identifying the EPSG
|
||||
identify_epsg = void_output(lgdal.OSRAutoIdentifyEPSG, [c_void_p])
|
||||
|
||||
# Getting the angular_units, linear_units functions
|
||||
linear_units = units_func(lgdal.OSRGetLinearUnits)
|
||||
angular_units = units_func(lgdal.OSRGetAngularUnits)
|
||||
|
||||
# For exporting to WKT, PROJ, "Pretty" WKT, and XML.
|
||||
to_wkt = string_output(
|
||||
std_call("OSRExportToWkt"), [c_void_p, POINTER(c_char_p)], decoding="utf-8"
|
||||
)
|
||||
to_proj = string_output(
|
||||
std_call("OSRExportToProj4"), [c_void_p, POINTER(c_char_p)], decoding="ascii"
|
||||
)
|
||||
to_pretty_wkt = string_output(
|
||||
std_call("OSRExportToPrettyWkt"),
|
||||
[c_void_p, POINTER(c_char_p), c_int],
|
||||
offset=-2,
|
||||
decoding="utf-8",
|
||||
)
|
||||
|
||||
to_xml = string_output(
|
||||
lgdal.OSRExportToXML,
|
||||
[c_void_p, POINTER(c_char_p), c_char_p],
|
||||
offset=-2,
|
||||
decoding="utf-8",
|
||||
)
|
||||
|
||||
# String attribute retrieval routines.
|
||||
get_attr_value = const_string_output(
|
||||
std_call("OSRGetAttrValue"), [c_void_p, c_char_p, c_int], decoding="utf-8"
|
||||
)
|
||||
get_auth_name = const_string_output(
|
||||
lgdal.OSRGetAuthorityName, [c_void_p, c_char_p], decoding="ascii"
|
||||
)
|
||||
get_auth_code = const_string_output(
|
||||
lgdal.OSRGetAuthorityCode, [c_void_p, c_char_p], decoding="ascii"
|
||||
)
|
||||
|
||||
# SRS Properties
|
||||
isgeographic = int_output(lgdal.OSRIsGeographic, [c_void_p])
|
||||
islocal = int_output(lgdal.OSRIsLocal, [c_void_p])
|
||||
isprojected = int_output(lgdal.OSRIsProjected, [c_void_p])
|
||||
|
||||
# Coordinate transformation
|
||||
new_ct = srs_output(std_call("OCTNewCoordinateTransformation"), [c_void_p, c_void_p])
|
||||
destroy_ct = void_output(
|
||||
std_call("OCTDestroyCoordinateTransformation"), [c_void_p], errcheck=False
|
||||
)
|
@ -0,0 +1,273 @@
|
||||
from ctypes import byref, c_double, c_int, c_void_p
|
||||
|
||||
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_str
|
||||
|
||||
from .const import (
|
||||
GDAL_COLOR_TYPES,
|
||||
GDAL_INTEGER_TYPES,
|
||||
GDAL_PIXEL_TYPES,
|
||||
GDAL_TO_CTYPES,
|
||||
)
|
||||
|
||||
|
||||
class GDALBand(GDALRasterBase):
|
||||
"""
|
||||
Wrap a GDAL raster band, needs to be obtained from a GDALRaster object.
|
||||
"""
|
||||
|
||||
def __init__(self, source, index):
|
||||
self.source = source
|
||||
self._ptr = capi.get_ds_raster_band(source._ptr, index)
|
||||
|
||||
def _flush(self):
|
||||
"""
|
||||
Call the flush method on the Band's parent raster and force a refresh
|
||||
of the statistics attribute when requested the next time.
|
||||
"""
|
||||
self.source._flush()
|
||||
self._stats_refresh = True
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
"""
|
||||
Return the description string of the band.
|
||||
"""
|
||||
return force_str(capi.get_band_description(self._ptr))
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
"""
|
||||
Width (X axis) in pixels of the band.
|
||||
"""
|
||||
return capi.get_band_xsize(self._ptr)
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
"""
|
||||
Height (Y axis) in pixels of the band.
|
||||
"""
|
||||
return capi.get_band_ysize(self._ptr)
|
||||
|
||||
@property
|
||||
def pixel_count(self):
|
||||
"""
|
||||
Return the total number of pixels in this band.
|
||||
"""
|
||||
return self.width * self.height
|
||||
|
||||
_stats_refresh = False
|
||||
|
||||
def statistics(self, refresh=False, approximate=False):
|
||||
"""
|
||||
Compute statistics on the pixel values of this band.
|
||||
|
||||
The return value is a tuple with the following structure:
|
||||
(minimum, maximum, mean, standard deviation).
|
||||
|
||||
If approximate=True, the statistics may be computed based on overviews
|
||||
or a subset of image tiles.
|
||||
|
||||
If refresh=True, the statistics will be computed from the data directly,
|
||||
and the cache will be updated where applicable.
|
||||
|
||||
For empty bands (where all pixel values are nodata), all statistics
|
||||
values are returned as None.
|
||||
|
||||
For raster formats using Persistent Auxiliary Metadata (PAM) services,
|
||||
the statistics might be cached in an auxiliary file.
|
||||
"""
|
||||
# Prepare array with arguments for capi function
|
||||
smin, smax, smean, sstd = c_double(), c_double(), c_double(), c_double()
|
||||
stats_args = [
|
||||
self._ptr,
|
||||
c_int(approximate),
|
||||
byref(smin),
|
||||
byref(smax),
|
||||
byref(smean),
|
||||
byref(sstd),
|
||||
c_void_p(),
|
||||
c_void_p(),
|
||||
]
|
||||
|
||||
if refresh or self._stats_refresh:
|
||||
func = capi.compute_band_statistics
|
||||
else:
|
||||
# Add additional argument to force computation if there is no
|
||||
# existing PAM file to take the values from.
|
||||
force = True
|
||||
stats_args.insert(2, c_int(force))
|
||||
func = capi.get_band_statistics
|
||||
|
||||
# Computation of statistics fails for empty bands.
|
||||
try:
|
||||
func(*stats_args)
|
||||
result = smin.value, smax.value, smean.value, sstd.value
|
||||
except GDALException:
|
||||
result = (None, None, None, None)
|
||||
|
||||
self._stats_refresh = False
|
||||
|
||||
return result
|
||||
|
||||
@property
|
||||
def min(self):
|
||||
"""
|
||||
Return the minimum pixel value for this band.
|
||||
"""
|
||||
return self.statistics()[0]
|
||||
|
||||
@property
|
||||
def max(self):
|
||||
"""
|
||||
Return the maximum pixel value for this band.
|
||||
"""
|
||||
return self.statistics()[1]
|
||||
|
||||
@property
|
||||
def mean(self):
|
||||
"""
|
||||
Return the mean of all pixel values of this band.
|
||||
"""
|
||||
return self.statistics()[2]
|
||||
|
||||
@property
|
||||
def std(self):
|
||||
"""
|
||||
Return the standard deviation of all pixel values of this band.
|
||||
"""
|
||||
return self.statistics()[3]
|
||||
|
||||
@property
|
||||
def nodata_value(self):
|
||||
"""
|
||||
Return the nodata value for this band, or None if it isn't set.
|
||||
"""
|
||||
# Get value and nodata exists flag
|
||||
nodata_exists = c_int()
|
||||
value = capi.get_band_nodata_value(self._ptr, nodata_exists)
|
||||
if not nodata_exists:
|
||||
value = None
|
||||
# If the pixeltype is an integer, convert to int
|
||||
elif self.datatype() in GDAL_INTEGER_TYPES:
|
||||
value = int(value)
|
||||
return value
|
||||
|
||||
@nodata_value.setter
|
||||
def nodata_value(self, value):
|
||||
"""
|
||||
Set the nodata value for this band.
|
||||
"""
|
||||
if value is None:
|
||||
capi.delete_band_nodata_value(self._ptr)
|
||||
elif not isinstance(value, (int, float)):
|
||||
raise ValueError("Nodata value must be numeric or None.")
|
||||
else:
|
||||
capi.set_band_nodata_value(self._ptr, value)
|
||||
self._flush()
|
||||
|
||||
def datatype(self, as_string=False):
|
||||
"""
|
||||
Return the GDAL Pixel Datatype for this band.
|
||||
"""
|
||||
dtype = capi.get_band_datatype(self._ptr)
|
||||
if as_string:
|
||||
dtype = GDAL_PIXEL_TYPES[dtype]
|
||||
return dtype
|
||||
|
||||
def color_interp(self, as_string=False):
|
||||
"""Return the GDAL color interpretation for this band."""
|
||||
color = capi.get_band_color_interp(self._ptr)
|
||||
if as_string:
|
||||
color = GDAL_COLOR_TYPES[color]
|
||||
return color
|
||||
|
||||
def data(self, data=None, offset=None, size=None, shape=None, as_memoryview=False):
|
||||
"""
|
||||
Read or writes pixel values for this band. Blocks of data can
|
||||
be accessed by specifying the width, height and offset of the
|
||||
desired block. The same specification can be used to update
|
||||
parts of a raster by providing an array of values.
|
||||
|
||||
Allowed input data types are bytes, memoryview, list, tuple, and array.
|
||||
"""
|
||||
offset = offset or (0, 0)
|
||||
size = size or (self.width - offset[0], self.height - offset[1])
|
||||
shape = shape or size
|
||||
if any(x <= 0 for x in size):
|
||||
raise ValueError("Offset too big for this raster.")
|
||||
|
||||
if size[0] > self.width or size[1] > self.height:
|
||||
raise ValueError("Size is larger than raster.")
|
||||
|
||||
# Create ctypes type array generator
|
||||
ctypes_array = GDAL_TO_CTYPES[self.datatype()] * (shape[0] * shape[1])
|
||||
|
||||
if data is None:
|
||||
# Set read mode
|
||||
access_flag = 0
|
||||
# Prepare empty ctypes array
|
||||
data_array = ctypes_array()
|
||||
else:
|
||||
# Set write mode
|
||||
access_flag = 1
|
||||
|
||||
# Instantiate ctypes array holding the input data
|
||||
if isinstance(data, (bytes, memoryview)) or (
|
||||
numpy and isinstance(data, numpy.ndarray)
|
||||
):
|
||||
data_array = ctypes_array.from_buffer_copy(data)
|
||||
else:
|
||||
data_array = ctypes_array(*data)
|
||||
|
||||
# Access band
|
||||
capi.band_io(
|
||||
self._ptr,
|
||||
access_flag,
|
||||
offset[0],
|
||||
offset[1],
|
||||
size[0],
|
||||
size[1],
|
||||
byref(data_array),
|
||||
shape[0],
|
||||
shape[1],
|
||||
self.datatype(),
|
||||
0,
|
||||
0,
|
||||
)
|
||||
|
||||
# Return data as numpy array if possible, otherwise as list
|
||||
if data is None:
|
||||
if as_memoryview:
|
||||
return memoryview(data_array)
|
||||
elif numpy:
|
||||
# reshape() needs a reshape parameter with the height first.
|
||||
return numpy.frombuffer(
|
||||
data_array, dtype=numpy.dtype(data_array)
|
||||
).reshape(tuple(reversed(size)))
|
||||
else:
|
||||
return list(data_array)
|
||||
else:
|
||||
self._flush()
|
||||
|
||||
|
||||
class BandList(list):
|
||||
def __init__(self, source):
|
||||
self.source = source
|
||||
super().__init__()
|
||||
|
||||
def __iter__(self):
|
||||
for idx in range(1, len(self) + 1):
|
||||
yield GDALBand(self.source, idx)
|
||||
|
||||
def __len__(self):
|
||||
return capi.get_ds_raster_count(self.source._ptr)
|
||||
|
||||
def __getitem__(self, index):
|
||||
try:
|
||||
return GDALBand(self.source, index + 1)
|
||||
except GDALException:
|
||||
raise GDALException("Unable to get band index %d" % index)
|
@ -0,0 +1,77 @@
|
||||
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.
|
||||
"""
|
||||
# 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 or "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,
|
||||
)
|
@ -0,0 +1,87 @@
|
||||
"""
|
||||
GDAL - Constant definitions
|
||||
"""
|
||||
from ctypes import c_double, c_float, c_int16, c_int32, c_ubyte, c_uint16, c_uint32
|
||||
|
||||
# See https://gdal.org/api/raster_c_api.html#_CPPv412GDALDataType
|
||||
GDAL_PIXEL_TYPES = {
|
||||
0: "GDT_Unknown", # Unknown or unspecified type
|
||||
1: "GDT_Byte", # Eight bit unsigned integer
|
||||
2: "GDT_UInt16", # Sixteen bit unsigned integer
|
||||
3: "GDT_Int16", # Sixteen bit signed integer
|
||||
4: "GDT_UInt32", # Thirty-two bit unsigned integer
|
||||
5: "GDT_Int32", # Thirty-two bit signed integer
|
||||
6: "GDT_Float32", # Thirty-two bit floating point
|
||||
7: "GDT_Float64", # Sixty-four bit floating point
|
||||
8: "GDT_CInt16", # Complex Int16
|
||||
9: "GDT_CInt32", # Complex Int32
|
||||
10: "GDT_CFloat32", # Complex Float32
|
||||
11: "GDT_CFloat64", # Complex Float64
|
||||
}
|
||||
|
||||
# A list of gdal datatypes that are integers.
|
||||
GDAL_INTEGER_TYPES = [1, 2, 3, 4, 5]
|
||||
|
||||
# Lookup values to convert GDAL pixel type indices into ctypes objects.
|
||||
# The GDAL band-io works with ctypes arrays to hold data to be written
|
||||
# or to hold the space for data to be read into. The lookup below helps
|
||||
# selecting the right ctypes object for a given gdal pixel type.
|
||||
GDAL_TO_CTYPES = [
|
||||
None,
|
||||
c_ubyte,
|
||||
c_uint16,
|
||||
c_int16,
|
||||
c_uint32,
|
||||
c_int32,
|
||||
c_float,
|
||||
c_double,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
]
|
||||
|
||||
# List of resampling algorithms that can be used to warp a GDALRaster.
|
||||
GDAL_RESAMPLE_ALGORITHMS = {
|
||||
"NearestNeighbour": 0,
|
||||
"Bilinear": 1,
|
||||
"Cubic": 2,
|
||||
"CubicSpline": 3,
|
||||
"Lanczos": 4,
|
||||
"Average": 5,
|
||||
"Mode": 6,
|
||||
}
|
||||
|
||||
# See https://gdal.org/api/raster_c_api.html#_CPPv415GDALColorInterp
|
||||
GDAL_COLOR_TYPES = {
|
||||
0: "GCI_Undefined", # Undefined, default value, i.e. not known
|
||||
1: "GCI_GrayIndex", # Grayscale
|
||||
2: "GCI_PaletteIndex", # Paletted
|
||||
3: "GCI_RedBand", # Red band of RGBA image
|
||||
4: "GCI_GreenBand", # Green band of RGBA image
|
||||
5: "GCI_BlueBand", # Blue band of RGBA image
|
||||
6: "GCI_AlphaBand", # Alpha (0=transparent, 255=opaque)
|
||||
7: "GCI_HueBand", # Hue band of HLS image
|
||||
8: "GCI_SaturationBand", # Saturation band of HLS image
|
||||
9: "GCI_LightnessBand", # Lightness band of HLS image
|
||||
10: "GCI_CyanBand", # Cyan band of CMYK image
|
||||
11: "GCI_MagentaBand", # Magenta band of CMYK image
|
||||
12: "GCI_YellowBand", # Yellow band of CMYK image
|
||||
13: "GCI_BlackBand", # Black band of CMLY image
|
||||
14: "GCI_YCbCr_YBand", # Y Luminance
|
||||
15: "GCI_YCbCr_CbBand", # Cb Chroma
|
||||
16: "GCI_YCbCr_CrBand", # Cr Chroma, also GCI_Max
|
||||
}
|
||||
|
||||
# GDAL virtual filesystems prefix.
|
||||
VSI_FILESYSTEM_PREFIX = "/vsi"
|
||||
|
||||
# Fixed base path for buffer-based GDAL in-memory files.
|
||||
VSI_MEM_FILESYSTEM_BASE_PATH = "/vsimem/"
|
||||
|
||||
# Should the memory file system take ownership of the buffer, freeing it when
|
||||
# the file is deleted? (No, GDALRaster.__del__() will delete the buffer.)
|
||||
VSI_TAKE_BUFFER_OWNERSHIP = False
|
||||
|
||||
# Should a VSI file be removed when retrieving its buffer?
|
||||
VSI_DELETE_BUFFER_ON_READ = False
|
@ -0,0 +1,541 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
from ctypes import (
|
||||
addressof,
|
||||
byref,
|
||||
c_buffer,
|
||||
c_char_p,
|
||||
c_double,
|
||||
c_int,
|
||||
c_void_p,
|
||||
string_at,
|
||||
)
|
||||
from pathlib import Path
|
||||
|
||||
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,
|
||||
VSI_DELETE_BUFFER_ON_READ,
|
||||
VSI_FILESYSTEM_PREFIX,
|
||||
VSI_MEM_FILESYSTEM_BASE_PATH,
|
||||
VSI_TAKE_BUFFER_OWNERSHIP,
|
||||
)
|
||||
from django.contrib.gis.gdal.srs import SpatialReference, SRSException
|
||||
from django.contrib.gis.geometry import json_regex
|
||||
from django.utils.encoding import force_bytes, force_str
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class TransformPoint(list):
|
||||
indices = {
|
||||
"origin": (0, 3),
|
||||
"scale": (1, 5),
|
||||
"skew": (2, 4),
|
||||
}
|
||||
|
||||
def __init__(self, raster, prop):
|
||||
x = raster.geotransform[self.indices[prop][0]]
|
||||
y = raster.geotransform[self.indices[prop][1]]
|
||||
super().__init__([x, y])
|
||||
self._raster = raster
|
||||
self._prop = prop
|
||||
|
||||
@property
|
||||
def x(self):
|
||||
return self[0]
|
||||
|
||||
@x.setter
|
||||
def x(self, value):
|
||||
gtf = self._raster.geotransform
|
||||
gtf[self.indices[self._prop][0]] = value
|
||||
self._raster.geotransform = gtf
|
||||
|
||||
@property
|
||||
def y(self):
|
||||
return self[1]
|
||||
|
||||
@y.setter
|
||||
def y(self, value):
|
||||
gtf = self._raster.geotransform
|
||||
gtf[self.indices[self._prop][1]] = value
|
||||
self._raster.geotransform = gtf
|
||||
|
||||
|
||||
class GDALRaster(GDALRasterBase):
|
||||
"""
|
||||
Wrap a raster GDAL Data Source object.
|
||||
"""
|
||||
|
||||
destructor = capi.close_ds
|
||||
|
||||
def __init__(self, ds_input, write=False):
|
||||
self._write = 1 if write else 0
|
||||
Driver.ensure_registered()
|
||||
|
||||
# Preprocess json inputs. This converts json strings to dictionaries,
|
||||
# which are parsed below the same way as direct dictionary inputs.
|
||||
if isinstance(ds_input, str) and json_regex.match(ds_input):
|
||||
ds_input = json.loads(ds_input)
|
||||
|
||||
# If input is a valid file path, try setting file as source.
|
||||
if isinstance(ds_input, (str, Path)):
|
||||
ds_input = str(ds_input)
|
||||
if not ds_input.startswith(VSI_FILESYSTEM_PREFIX) and not os.path.exists(
|
||||
ds_input
|
||||
):
|
||||
raise GDALException(
|
||||
'Unable to read raster source input "%s".' % ds_input
|
||||
)
|
||||
try:
|
||||
# GDALOpen will auto-detect the data source type.
|
||||
self._ptr = capi.open_ds(force_bytes(ds_input), self._write)
|
||||
except GDALException as err:
|
||||
raise GDALException(
|
||||
'Could not open the datasource at "{}" ({}).'.format(ds_input, err)
|
||||
)
|
||||
elif isinstance(ds_input, bytes):
|
||||
# Create a new raster in write mode.
|
||||
self._write = 1
|
||||
# Get size of buffer.
|
||||
size = sys.getsizeof(ds_input)
|
||||
# Pass data to ctypes, keeping a reference to the ctypes object so
|
||||
# that the vsimem file remains available until the GDALRaster is
|
||||
# deleted.
|
||||
self._ds_input = c_buffer(ds_input)
|
||||
# Create random name to reference in vsimem filesystem.
|
||||
vsi_path = os.path.join(VSI_MEM_FILESYSTEM_BASE_PATH, str(uuid.uuid4()))
|
||||
# Create vsimem file from buffer.
|
||||
capi.create_vsi_file_from_mem_buffer(
|
||||
force_bytes(vsi_path),
|
||||
byref(self._ds_input),
|
||||
size,
|
||||
VSI_TAKE_BUFFER_OWNERSHIP,
|
||||
)
|
||||
# Open the new vsimem file as a GDALRaster.
|
||||
try:
|
||||
self._ptr = capi.open_ds(force_bytes(vsi_path), self._write)
|
||||
except GDALException:
|
||||
# Remove the broken file from the VSI filesystem.
|
||||
capi.unlink_vsi_file(force_bytes(vsi_path))
|
||||
raise GDALException("Failed creating VSI raster from the input buffer.")
|
||||
elif isinstance(ds_input, dict):
|
||||
# A new raster needs to be created in write mode
|
||||
self._write = 1
|
||||
|
||||
# Create driver (in memory by default)
|
||||
driver = Driver(ds_input.get("driver", "MEM"))
|
||||
|
||||
# For out of memory drivers, check filename argument
|
||||
if driver.name != "MEM" and "name" not in ds_input:
|
||||
raise GDALException(
|
||||
'Specify name for creation of raster with driver "{}".'.format(
|
||||
driver.name
|
||||
)
|
||||
)
|
||||
|
||||
# Check if width and height where specified
|
||||
if "width" not in ds_input or "height" not in ds_input:
|
||||
raise GDALException(
|
||||
"Specify width and height attributes for JSON or dict input."
|
||||
)
|
||||
|
||||
# Check if srid was specified
|
||||
if "srid" not in ds_input:
|
||||
raise GDALException("Specify srid for JSON or dict input.")
|
||||
|
||||
# Create null terminated gdal options array.
|
||||
papsz_options = []
|
||||
for key, val in ds_input.get("papsz_options", {}).items():
|
||||
option = "{}={}".format(key, val)
|
||||
papsz_options.append(option.upper().encode())
|
||||
papsz_options.append(None)
|
||||
|
||||
# Convert papszlist to ctypes array.
|
||||
papsz_options = (c_char_p * len(papsz_options))(*papsz_options)
|
||||
|
||||
# Create GDAL Raster
|
||||
self._ptr = capi.create_ds(
|
||||
driver._ptr,
|
||||
force_bytes(ds_input.get("name", "")),
|
||||
ds_input["width"],
|
||||
ds_input["height"],
|
||||
ds_input.get("nr_of_bands", len(ds_input.get("bands", []))),
|
||||
ds_input.get("datatype", 6),
|
||||
byref(papsz_options),
|
||||
)
|
||||
|
||||
# Set band data if provided
|
||||
for i, band_input in enumerate(ds_input.get("bands", [])):
|
||||
band = self.bands[i]
|
||||
if "nodata_value" in band_input:
|
||||
band.nodata_value = band_input["nodata_value"]
|
||||
# Instantiate band filled with nodata values if only
|
||||
# partial input data has been provided.
|
||||
if band.nodata_value is not None and (
|
||||
"data" not in band_input
|
||||
or "size" in band_input
|
||||
or "shape" in band_input
|
||||
):
|
||||
band.data(data=(band.nodata_value,), shape=(1, 1))
|
||||
# Set band data values from input.
|
||||
band.data(
|
||||
data=band_input.get("data"),
|
||||
size=band_input.get("size"),
|
||||
shape=band_input.get("shape"),
|
||||
offset=band_input.get("offset"),
|
||||
)
|
||||
|
||||
# Set SRID
|
||||
self.srs = ds_input.get("srid")
|
||||
|
||||
# Set additional properties if provided
|
||||
if "origin" in ds_input:
|
||||
self.origin.x, self.origin.y = ds_input["origin"]
|
||||
|
||||
if "scale" in ds_input:
|
||||
self.scale.x, self.scale.y = ds_input["scale"]
|
||||
|
||||
if "skew" in ds_input:
|
||||
self.skew.x, self.skew.y = ds_input["skew"]
|
||||
elif isinstance(ds_input, c_void_p):
|
||||
# Instantiate the object using an existing pointer to a gdal raster.
|
||||
self._ptr = ds_input
|
||||
else:
|
||||
raise GDALException(
|
||||
'Invalid data source input type: "{}".'.format(type(ds_input))
|
||||
)
|
||||
|
||||
def __del__(self):
|
||||
if self.is_vsi_based:
|
||||
# Remove the temporary file from the VSI in-memory filesystem.
|
||||
capi.unlink_vsi_file(force_bytes(self.name))
|
||||
super().__del__()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Short-hand representation because WKB may be very large.
|
||||
"""
|
||||
return "<Raster object at %s>" % hex(addressof(self._ptr))
|
||||
|
||||
def _flush(self):
|
||||
"""
|
||||
Flush all data from memory into the source file if it exists.
|
||||
The data that needs flushing are geotransforms, coordinate systems,
|
||||
nodata_values and pixel values. This function will be called
|
||||
automatically wherever it is needed.
|
||||
"""
|
||||
# Raise an Exception if the value is being changed in read mode.
|
||||
if not self._write:
|
||||
raise GDALException(
|
||||
"Raster needs to be opened in write mode to change values."
|
||||
)
|
||||
capi.flush_ds(self._ptr)
|
||||
|
||||
@property
|
||||
def vsi_buffer(self):
|
||||
if not (
|
||||
self.is_vsi_based and self.name.startswith(VSI_MEM_FILESYSTEM_BASE_PATH)
|
||||
):
|
||||
return None
|
||||
# Prepare an integer that will contain the buffer length.
|
||||
out_length = c_int()
|
||||
# Get the data using the vsi file name.
|
||||
dat = capi.get_mem_buffer_from_vsi_file(
|
||||
force_bytes(self.name),
|
||||
byref(out_length),
|
||||
VSI_DELETE_BUFFER_ON_READ,
|
||||
)
|
||||
# Read the full buffer pointer.
|
||||
return string_at(dat, out_length.value)
|
||||
|
||||
@cached_property
|
||||
def is_vsi_based(self):
|
||||
return self._ptr and self.name.startswith(VSI_FILESYSTEM_PREFIX)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""
|
||||
Return the name of this raster. Corresponds to filename
|
||||
for file-based rasters.
|
||||
"""
|
||||
return force_str(capi.get_ds_description(self._ptr))
|
||||
|
||||
@cached_property
|
||||
def driver(self):
|
||||
"""
|
||||
Return the GDAL Driver used for this raster.
|
||||
"""
|
||||
ds_driver = capi.get_ds_driver(self._ptr)
|
||||
return Driver(ds_driver)
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
"""
|
||||
Width (X axis) in pixels.
|
||||
"""
|
||||
return capi.get_ds_xsize(self._ptr)
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
"""
|
||||
Height (Y axis) in pixels.
|
||||
"""
|
||||
return capi.get_ds_ysize(self._ptr)
|
||||
|
||||
@property
|
||||
def srs(self):
|
||||
"""
|
||||
Return the SpatialReference used in this GDALRaster.
|
||||
"""
|
||||
try:
|
||||
wkt = capi.get_ds_projection_ref(self._ptr)
|
||||
if not wkt:
|
||||
return None
|
||||
return SpatialReference(wkt, srs_type="wkt")
|
||||
except SRSException:
|
||||
return None
|
||||
|
||||
@srs.setter
|
||||
def srs(self, value):
|
||||
"""
|
||||
Set the spatial reference used in this GDALRaster. The input can be
|
||||
a SpatialReference or any parameter accepted by the SpatialReference
|
||||
constructor.
|
||||
"""
|
||||
if isinstance(value, SpatialReference):
|
||||
srs = value
|
||||
elif isinstance(value, (int, str)):
|
||||
srs = SpatialReference(value)
|
||||
else:
|
||||
raise ValueError("Could not create a SpatialReference from input.")
|
||||
capi.set_ds_projection_ref(self._ptr, srs.wkt.encode())
|
||||
self._flush()
|
||||
|
||||
@property
|
||||
def srid(self):
|
||||
"""
|
||||
Shortcut to access the srid of this GDALRaster.
|
||||
"""
|
||||
return self.srs.srid
|
||||
|
||||
@srid.setter
|
||||
def srid(self, value):
|
||||
"""
|
||||
Shortcut to set this GDALRaster's srs from an srid.
|
||||
"""
|
||||
self.srs = value
|
||||
|
||||
@property
|
||||
def geotransform(self):
|
||||
"""
|
||||
Return the geotransform of the data source.
|
||||
Return the default geotransform if it does not exist or has not been
|
||||
set previously. The default is [0.0, 1.0, 0.0, 0.0, 0.0, -1.0].
|
||||
"""
|
||||
# Create empty ctypes double array for data
|
||||
gtf = (c_double * 6)()
|
||||
capi.get_ds_geotransform(self._ptr, byref(gtf))
|
||||
return list(gtf)
|
||||
|
||||
@geotransform.setter
|
||||
def geotransform(self, values):
|
||||
"Set the geotransform for the data source."
|
||||
if len(values) != 6 or not all(isinstance(x, (int, float)) for x in values):
|
||||
raise ValueError("Geotransform must consist of 6 numeric values.")
|
||||
# Create ctypes double array with input and write data
|
||||
values = (c_double * 6)(*values)
|
||||
capi.set_ds_geotransform(self._ptr, byref(values))
|
||||
self._flush()
|
||||
|
||||
@property
|
||||
def origin(self):
|
||||
"""
|
||||
Coordinates of the raster origin.
|
||||
"""
|
||||
return TransformPoint(self, "origin")
|
||||
|
||||
@property
|
||||
def scale(self):
|
||||
"""
|
||||
Pixel scale in units of the raster projection.
|
||||
"""
|
||||
return TransformPoint(self, "scale")
|
||||
|
||||
@property
|
||||
def skew(self):
|
||||
"""
|
||||
Skew of pixels (rotation parameters).
|
||||
"""
|
||||
return TransformPoint(self, "skew")
|
||||
|
||||
@property
|
||||
def extent(self):
|
||||
"""
|
||||
Return the extent as a 4-tuple (xmin, ymin, xmax, ymax).
|
||||
"""
|
||||
# Calculate boundary values based on scale and size
|
||||
xval = self.origin.x + self.scale.x * self.width
|
||||
yval = self.origin.y + self.scale.y * self.height
|
||||
# Calculate min and max values
|
||||
xmin = min(xval, self.origin.x)
|
||||
xmax = max(xval, self.origin.x)
|
||||
ymin = min(yval, self.origin.y)
|
||||
ymax = max(yval, self.origin.y)
|
||||
|
||||
return xmin, ymin, xmax, ymax
|
||||
|
||||
@property
|
||||
def bands(self):
|
||||
return BandList(self)
|
||||
|
||||
def warp(self, ds_input, resampling="NearestNeighbour", max_error=0.0):
|
||||
"""
|
||||
Return a warped GDALRaster with the given input characteristics.
|
||||
|
||||
The input is expected to be a dictionary containing the parameters
|
||||
of the target raster. Allowed values are width, height, SRID, origin,
|
||||
scale, skew, datatype, driver, and name (filename).
|
||||
|
||||
By default, the warp functions keeps all parameters equal to the values
|
||||
of the original source raster. For the name of the target raster, the
|
||||
name of the source raster will be used and appended with
|
||||
_copy. + source_driver_name.
|
||||
|
||||
In addition, the resampling algorithm can be specified with the "resampling"
|
||||
input parameter. The default is NearestNeighbor. For a list of all options
|
||||
consult the GDAL_RESAMPLE_ALGORITHMS constant.
|
||||
"""
|
||||
# Get the parameters defining the geotransform, srid, and size of the raster
|
||||
ds_input.setdefault("width", self.width)
|
||||
ds_input.setdefault("height", self.height)
|
||||
ds_input.setdefault("srid", self.srs.srid)
|
||||
ds_input.setdefault("origin", self.origin)
|
||||
ds_input.setdefault("scale", self.scale)
|
||||
ds_input.setdefault("skew", self.skew)
|
||||
# Get the driver, name, and datatype of the target raster
|
||||
ds_input.setdefault("driver", self.driver.name)
|
||||
|
||||
if "name" not in ds_input:
|
||||
ds_input["name"] = self.name + "_copy." + self.driver.name
|
||||
|
||||
if "datatype" not in ds_input:
|
||||
ds_input["datatype"] = self.bands[0].datatype()
|
||||
|
||||
# Instantiate raster bands filled with nodata values.
|
||||
ds_input["bands"] = [{"nodata_value": bnd.nodata_value} for bnd in self.bands]
|
||||
|
||||
# Create target raster
|
||||
target = GDALRaster(ds_input, write=True)
|
||||
|
||||
# Select resampling algorithm
|
||||
algorithm = GDAL_RESAMPLE_ALGORITHMS[resampling]
|
||||
|
||||
# Reproject image
|
||||
capi.reproject_image(
|
||||
self._ptr,
|
||||
self.srs.wkt.encode(),
|
||||
target._ptr,
|
||||
target.srs.wkt.encode(),
|
||||
algorithm,
|
||||
0.0,
|
||||
max_error,
|
||||
c_void_p(),
|
||||
c_void_p(),
|
||||
c_void_p(),
|
||||
)
|
||||
|
||||
# Make sure all data is written to file
|
||||
target._flush()
|
||||
|
||||
return target
|
||||
|
||||
def clone(self, name=None):
|
||||
"""Return a clone of this GDALRaster."""
|
||||
if name:
|
||||
clone_name = name
|
||||
elif self.driver.name != "MEM":
|
||||
clone_name = self.name + "_copy." + self.driver.name
|
||||
else:
|
||||
clone_name = os.path.join(VSI_MEM_FILESYSTEM_BASE_PATH, str(uuid.uuid4()))
|
||||
return GDALRaster(
|
||||
capi.copy_ds(
|
||||
self.driver._ptr,
|
||||
force_bytes(clone_name),
|
||||
self._ptr,
|
||||
c_int(),
|
||||
c_char_p(),
|
||||
c_void_p(),
|
||||
c_void_p(),
|
||||
),
|
||||
write=self._write,
|
||||
)
|
||||
|
||||
def transform(
|
||||
self, srs, driver=None, name=None, resampling="NearestNeighbour", max_error=0.0
|
||||
):
|
||||
"""
|
||||
Return a copy of this raster reprojected into the given spatial
|
||||
reference system.
|
||||
"""
|
||||
# Convert the resampling algorithm name into an algorithm id
|
||||
algorithm = GDAL_RESAMPLE_ALGORITHMS[resampling]
|
||||
|
||||
if isinstance(srs, SpatialReference):
|
||||
target_srs = srs
|
||||
elif isinstance(srs, (int, str)):
|
||||
target_srs = SpatialReference(srs)
|
||||
else:
|
||||
raise TypeError(
|
||||
"Transform only accepts SpatialReference, string, and integer "
|
||||
"objects."
|
||||
)
|
||||
|
||||
if target_srs.srid == self.srid and (not driver or driver == self.driver.name):
|
||||
return self.clone(name)
|
||||
# Create warped virtual dataset in the target reference system
|
||||
target = capi.auto_create_warped_vrt(
|
||||
self._ptr,
|
||||
self.srs.wkt.encode(),
|
||||
target_srs.wkt.encode(),
|
||||
algorithm,
|
||||
max_error,
|
||||
c_void_p(),
|
||||
)
|
||||
target = GDALRaster(target)
|
||||
|
||||
# Construct the target warp dictionary from the virtual raster
|
||||
data = {
|
||||
"srid": target_srs.srid,
|
||||
"width": target.width,
|
||||
"height": target.height,
|
||||
"origin": [target.origin.x, target.origin.y],
|
||||
"scale": [target.scale.x, target.scale.y],
|
||||
"skew": [target.skew.x, target.skew.y],
|
||||
}
|
||||
|
||||
# Set the driver and filepath if provided
|
||||
if driver:
|
||||
data["driver"] = driver
|
||||
|
||||
if name:
|
||||
data["name"] = name
|
||||
|
||||
# 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.
|
||||
"""
|
||||
return capi.get_ds_info(self.ptr, None).decode()
|
@ -0,0 +1,364 @@
|
||||
"""
|
||||
The Spatial Reference class, represents OGR Spatial Reference objects.
|
||||
|
||||
Example:
|
||||
>>> from django.contrib.gis.gdal import SpatialReference
|
||||
>>> srs = SpatialReference('WGS84')
|
||||
>>> print(srs)
|
||||
GEOGCS["WGS 84",
|
||||
DATUM["WGS_1984",
|
||||
SPHEROID["WGS 84",6378137,298.257223563,
|
||||
AUTHORITY["EPSG","7030"]],
|
||||
TOWGS84[0,0,0,0,0,0,0],
|
||||
AUTHORITY["EPSG","6326"]],
|
||||
PRIMEM["Greenwich",0,
|
||||
AUTHORITY["EPSG","8901"]],
|
||||
UNIT["degree",0.01745329251994328,
|
||||
AUTHORITY["EPSG","9122"]],
|
||||
AUTHORITY["EPSG","4326"]]
|
||||
>>> print(srs.proj)
|
||||
+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs
|
||||
>>> print(srs.ellipsoid)
|
||||
(6378137.0, 6356752.3142451793, 298.25722356300003)
|
||||
>>> print(srs.projected, srs.geographic)
|
||||
False True
|
||||
>>> srs.import_epsg(32140)
|
||||
>>> print(srs.name)
|
||||
NAD83 / Texas South Central
|
||||
"""
|
||||
from ctypes import byref, c_char_p, c_int
|
||||
from enum import IntEnum
|
||||
|
||||
from django.contrib.gis.gdal.base import GDALBase
|
||||
from django.contrib.gis.gdal.error import SRSException
|
||||
from django.contrib.gis.gdal.libgdal import GDAL_VERSION
|
||||
from django.contrib.gis.gdal.prototypes import srs as capi
|
||||
from django.utils.encoding import force_bytes, force_str
|
||||
|
||||
|
||||
class AxisOrder(IntEnum):
|
||||
TRADITIONAL = 0
|
||||
AUTHORITY = 1
|
||||
|
||||
|
||||
class SpatialReference(GDALBase):
|
||||
"""
|
||||
A wrapper for the OGRSpatialReference object. According to the GDAL web site,
|
||||
the SpatialReference object "provide[s] services to represent coordinate
|
||||
systems (projections and datums) and to transform between them."
|
||||
"""
|
||||
|
||||
destructor = capi.release_srs
|
||||
|
||||
def __init__(self, srs_input="", srs_type="user", axis_order=None):
|
||||
"""
|
||||
Create a GDAL OSR Spatial Reference object from the given input.
|
||||
The input may be string of OGC Well Known Text (WKT), an integer
|
||||
EPSG code, a PROJ string, and/or a projection "well known" shorthand
|
||||
string (one of 'WGS84', 'WGS72', 'NAD27', 'NAD83').
|
||||
"""
|
||||
if not isinstance(axis_order, (type(None), AxisOrder)):
|
||||
raise ValueError(
|
||||
"SpatialReference.axis_order must be an AxisOrder instance."
|
||||
)
|
||||
self.axis_order = axis_order or AxisOrder.TRADITIONAL
|
||||
if srs_type == "wkt":
|
||||
self.ptr = capi.new_srs(c_char_p(b""))
|
||||
self.import_wkt(srs_input)
|
||||
if self.axis_order == AxisOrder.TRADITIONAL and GDAL_VERSION >= (3, 0):
|
||||
capi.set_axis_strategy(self.ptr, self.axis_order)
|
||||
elif self.axis_order != AxisOrder.TRADITIONAL and GDAL_VERSION < (3, 0):
|
||||
raise ValueError("%s is not supported in GDAL < 3.0." % self.axis_order)
|
||||
return
|
||||
elif isinstance(srs_input, str):
|
||||
try:
|
||||
# If SRID is a string, e.g., '4326', then make acceptable
|
||||
# as user input.
|
||||
srid = int(srs_input)
|
||||
srs_input = "EPSG:%d" % srid
|
||||
except ValueError:
|
||||
pass
|
||||
elif isinstance(srs_input, int):
|
||||
# EPSG integer code was input.
|
||||
srs_type = "epsg"
|
||||
elif isinstance(srs_input, self.ptr_type):
|
||||
srs = srs_input
|
||||
srs_type = "ogr"
|
||||
else:
|
||||
raise TypeError('Invalid SRS type "%s"' % srs_type)
|
||||
|
||||
if srs_type == "ogr":
|
||||
# Input is already an SRS pointer.
|
||||
srs = srs_input
|
||||
else:
|
||||
# Creating a new SRS pointer, using the string buffer.
|
||||
buf = c_char_p(b"")
|
||||
srs = capi.new_srs(buf)
|
||||
|
||||
# If the pointer is NULL, throw an exception.
|
||||
if not srs:
|
||||
raise SRSException(
|
||||
"Could not create spatial reference from: %s" % srs_input
|
||||
)
|
||||
else:
|
||||
self.ptr = srs
|
||||
|
||||
if self.axis_order == AxisOrder.TRADITIONAL and GDAL_VERSION >= (3, 0):
|
||||
capi.set_axis_strategy(self.ptr, self.axis_order)
|
||||
elif self.axis_order != AxisOrder.TRADITIONAL and GDAL_VERSION < (3, 0):
|
||||
raise ValueError("%s is not supported in GDAL < 3.0." % self.axis_order)
|
||||
# Importing from either the user input string or an integer SRID.
|
||||
if srs_type == "user":
|
||||
self.import_user_input(srs_input)
|
||||
elif srs_type == "epsg":
|
||||
self.import_epsg(srs_input)
|
||||
|
||||
def __getitem__(self, target):
|
||||
"""
|
||||
Return the value of the given string attribute node, None if the node
|
||||
doesn't exist. Can also take a tuple as a parameter, (target, child),
|
||||
where child is the index of the attribute in the WKT. For example:
|
||||
|
||||
>>> wkt = 'GEOGCS["WGS 84", DATUM["WGS_1984, ... AUTHORITY["EPSG","4326"]]'
|
||||
>>> srs = SpatialReference(wkt) # could also use 'WGS84', or 4326
|
||||
>>> print(srs['GEOGCS'])
|
||||
WGS 84
|
||||
>>> print(srs['DATUM'])
|
||||
WGS_1984
|
||||
>>> print(srs['AUTHORITY'])
|
||||
EPSG
|
||||
>>> print(srs['AUTHORITY', 1]) # The authority value
|
||||
4326
|
||||
>>> print(srs['TOWGS84', 4]) # the fourth value in this wkt
|
||||
0
|
||||
>>> # For the units authority, have to use the pipe symbole.
|
||||
>>> print(srs['UNIT|AUTHORITY'])
|
||||
EPSG
|
||||
>>> print(srs['UNIT|AUTHORITY', 1]) # The authority value for the units
|
||||
9122
|
||||
"""
|
||||
if isinstance(target, tuple):
|
||||
return self.attr_value(*target)
|
||||
else:
|
||||
return self.attr_value(target)
|
||||
|
||||
def __str__(self):
|
||||
"Use 'pretty' WKT."
|
||||
return self.pretty_wkt
|
||||
|
||||
# #### SpatialReference Methods ####
|
||||
def attr_value(self, target, index=0):
|
||||
"""
|
||||
The attribute value for the given target node (e.g. 'PROJCS'). The index
|
||||
keyword specifies an index of the child node to return.
|
||||
"""
|
||||
if not isinstance(target, str) or not isinstance(index, int):
|
||||
raise TypeError
|
||||
return capi.get_attr_value(self.ptr, force_bytes(target), index)
|
||||
|
||||
def auth_name(self, target):
|
||||
"Return the authority name for the given string target node."
|
||||
return capi.get_auth_name(
|
||||
self.ptr, target if target is None else force_bytes(target)
|
||||
)
|
||||
|
||||
def auth_code(self, target):
|
||||
"Return the authority code for the given string target node."
|
||||
return capi.get_auth_code(
|
||||
self.ptr, target if target is None else force_bytes(target)
|
||||
)
|
||||
|
||||
def clone(self):
|
||||
"Return a clone of this SpatialReference object."
|
||||
return SpatialReference(capi.clone_srs(self.ptr), axis_order=self.axis_order)
|
||||
|
||||
def from_esri(self):
|
||||
"Morph this SpatialReference from ESRI's format to EPSG."
|
||||
capi.morph_from_esri(self.ptr)
|
||||
|
||||
def identify_epsg(self):
|
||||
"""
|
||||
This method inspects the WKT of this SpatialReference, and will
|
||||
add EPSG authority nodes where an EPSG identifier is applicable.
|
||||
"""
|
||||
capi.identify_epsg(self.ptr)
|
||||
|
||||
def to_esri(self):
|
||||
"Morph this SpatialReference to ESRI's format."
|
||||
capi.morph_to_esri(self.ptr)
|
||||
|
||||
def validate(self):
|
||||
"Check to see if the given spatial reference is valid."
|
||||
capi.srs_validate(self.ptr)
|
||||
|
||||
# #### Name & SRID properties ####
|
||||
@property
|
||||
def name(self):
|
||||
"Return the name of this Spatial Reference."
|
||||
if self.projected:
|
||||
return self.attr_value("PROJCS")
|
||||
elif self.geographic:
|
||||
return self.attr_value("GEOGCS")
|
||||
elif self.local:
|
||||
return self.attr_value("LOCAL_CS")
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def srid(self):
|
||||
"Return the SRID of top-level authority, or None if undefined."
|
||||
try:
|
||||
return int(self.auth_code(target=None))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
# #### Unit Properties ####
|
||||
@property
|
||||
def linear_name(self):
|
||||
"Return the name of the linear units."
|
||||
units, name = capi.linear_units(self.ptr, byref(c_char_p()))
|
||||
return name
|
||||
|
||||
@property
|
||||
def linear_units(self):
|
||||
"Return the value of the linear units."
|
||||
units, name = capi.linear_units(self.ptr, byref(c_char_p()))
|
||||
return units
|
||||
|
||||
@property
|
||||
def angular_name(self):
|
||||
"Return the name of the angular units."
|
||||
units, name = capi.angular_units(self.ptr, byref(c_char_p()))
|
||||
return name
|
||||
|
||||
@property
|
||||
def angular_units(self):
|
||||
"Return the value of the angular units."
|
||||
units, name = capi.angular_units(self.ptr, byref(c_char_p()))
|
||||
return units
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
"""
|
||||
Return a 2-tuple of the units value and the units name. Automatically
|
||||
determine whether to return the linear or angular units.
|
||||
"""
|
||||
units, name = None, None
|
||||
if self.projected or self.local:
|
||||
units, name = capi.linear_units(self.ptr, byref(c_char_p()))
|
||||
elif self.geographic:
|
||||
units, name = capi.angular_units(self.ptr, byref(c_char_p()))
|
||||
if name is not None:
|
||||
name = force_str(name)
|
||||
return (units, name)
|
||||
|
||||
# #### Spheroid/Ellipsoid Properties ####
|
||||
@property
|
||||
def ellipsoid(self):
|
||||
"""
|
||||
Return a tuple of the ellipsoid parameters:
|
||||
(semimajor axis, semiminor axis, and inverse flattening)
|
||||
"""
|
||||
return (self.semi_major, self.semi_minor, self.inverse_flattening)
|
||||
|
||||
@property
|
||||
def semi_major(self):
|
||||
"Return the Semi Major Axis for this Spatial Reference."
|
||||
return capi.semi_major(self.ptr, byref(c_int()))
|
||||
|
||||
@property
|
||||
def semi_minor(self):
|
||||
"Return the Semi Minor Axis for this Spatial Reference."
|
||||
return capi.semi_minor(self.ptr, byref(c_int()))
|
||||
|
||||
@property
|
||||
def inverse_flattening(self):
|
||||
"Return the Inverse Flattening for this Spatial Reference."
|
||||
return capi.invflattening(self.ptr, byref(c_int()))
|
||||
|
||||
# #### Boolean Properties ####
|
||||
@property
|
||||
def geographic(self):
|
||||
"""
|
||||
Return True if this SpatialReference is geographic
|
||||
(root node is GEOGCS).
|
||||
"""
|
||||
return bool(capi.isgeographic(self.ptr))
|
||||
|
||||
@property
|
||||
def local(self):
|
||||
"Return True if this SpatialReference is local (root node is LOCAL_CS)."
|
||||
return bool(capi.islocal(self.ptr))
|
||||
|
||||
@property
|
||||
def projected(self):
|
||||
"""
|
||||
Return True if this SpatialReference is a projected coordinate system
|
||||
(root node is PROJCS).
|
||||
"""
|
||||
return bool(capi.isprojected(self.ptr))
|
||||
|
||||
# #### Import Routines #####
|
||||
def import_epsg(self, epsg):
|
||||
"Import the Spatial Reference from the EPSG code (an integer)."
|
||||
capi.from_epsg(self.ptr, epsg)
|
||||
|
||||
def import_proj(self, proj):
|
||||
"""Import the Spatial Reference from a PROJ string."""
|
||||
capi.from_proj(self.ptr, proj)
|
||||
|
||||
def import_user_input(self, user_input):
|
||||
"Import the Spatial Reference from the given user input string."
|
||||
capi.from_user_input(self.ptr, force_bytes(user_input))
|
||||
|
||||
def import_wkt(self, wkt):
|
||||
"Import the Spatial Reference from OGC WKT (string)"
|
||||
capi.from_wkt(self.ptr, byref(c_char_p(force_bytes(wkt))))
|
||||
|
||||
def import_xml(self, xml):
|
||||
"Import the Spatial Reference from an XML string."
|
||||
capi.from_xml(self.ptr, xml)
|
||||
|
||||
# #### Export Properties ####
|
||||
@property
|
||||
def wkt(self):
|
||||
"Return the WKT representation of this Spatial Reference."
|
||||
return capi.to_wkt(self.ptr, byref(c_char_p()))
|
||||
|
||||
@property
|
||||
def pretty_wkt(self, simplify=0):
|
||||
"Return the 'pretty' representation of the WKT."
|
||||
return capi.to_pretty_wkt(self.ptr, byref(c_char_p()), simplify)
|
||||
|
||||
@property
|
||||
def proj(self):
|
||||
"""Return the PROJ representation for this Spatial Reference."""
|
||||
return capi.to_proj(self.ptr, byref(c_char_p()))
|
||||
|
||||
@property
|
||||
def proj4(self):
|
||||
"Alias for proj()."
|
||||
return self.proj
|
||||
|
||||
@property
|
||||
def xml(self, dialect=""):
|
||||
"Return the XML representation of this Spatial Reference."
|
||||
return capi.to_xml(self.ptr, byref(c_char_p()), force_bytes(dialect))
|
||||
|
||||
|
||||
class CoordTransform(GDALBase):
|
||||
"The coordinate system transformation object."
|
||||
destructor = capi.destroy_ct
|
||||
|
||||
def __init__(self, source, target):
|
||||
"Initialize on a source and target SpatialReference objects."
|
||||
if not isinstance(source, SpatialReference) or not isinstance(
|
||||
target, SpatialReference
|
||||
):
|
||||
raise TypeError("source and target must be of type SpatialReference")
|
||||
self.ptr = capi.new_ct(source._ptr, target._ptr)
|
||||
self._srs1_name = source.name
|
||||
self._srs2_name = target.name
|
||||
|
||||
def __str__(self):
|
||||
return 'Transform from "%s" to "%s"' % (self._srs1_name, self._srs2_name)
|
@ -0,0 +1,24 @@
|
||||
"""
|
||||
This module houses the GeoIP2 object, a wrapper for the MaxMind GeoIP2(R)
|
||||
Python API (https://geoip2.readthedocs.io/). This is an alternative to the
|
||||
Python GeoIP2 interface provided by MaxMind.
|
||||
|
||||
GeoIP(R) is a registered trademark of MaxMind, Inc.
|
||||
|
||||
For IP-based geolocation, this module requires the GeoLite2 Country and City
|
||||
datasets, in binary format (CSV will not work!). The datasets may be
|
||||
downloaded from MaxMind at https://dev.maxmind.com/geoip/geoip2/geolite2/.
|
||||
Grab GeoLite2-Country.mmdb.gz and GeoLite2-City.mmdb.gz, and unzip them in the
|
||||
directory corresponding to settings.GEOIP_PATH.
|
||||
"""
|
||||
__all__ = ["HAS_GEOIP2"]
|
||||
|
||||
try:
|
||||
import geoip2 # NOQA
|
||||
except ImportError:
|
||||
HAS_GEOIP2 = False
|
||||
else:
|
||||
from .base import GeoIP2, GeoIP2Exception
|
||||
|
||||
HAS_GEOIP2 = True
|
||||
__all__ += ["GeoIP2", "GeoIP2Exception"]
|
@ -0,0 +1,240 @@
|
||||
import socket
|
||||
|
||||
import geoip2.database
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_ipv46_address
|
||||
from django.utils._os import to_path
|
||||
|
||||
from .resources import City, Country
|
||||
|
||||
# Creating the settings dictionary with any settings, if needed.
|
||||
GEOIP_SETTINGS = {
|
||||
"GEOIP_PATH": getattr(settings, "GEOIP_PATH", None),
|
||||
"GEOIP_CITY": getattr(settings, "GEOIP_CITY", "GeoLite2-City.mmdb"),
|
||||
"GEOIP_COUNTRY": getattr(settings, "GEOIP_COUNTRY", "GeoLite2-Country.mmdb"),
|
||||
}
|
||||
|
||||
|
||||
class GeoIP2Exception(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class GeoIP2:
|
||||
# The flags for GeoIP memory caching.
|
||||
# Try MODE_MMAP_EXT, MODE_MMAP, MODE_FILE in that order.
|
||||
MODE_AUTO = 0
|
||||
# Use the C extension with memory map.
|
||||
MODE_MMAP_EXT = 1
|
||||
# Read from memory map. Pure Python.
|
||||
MODE_MMAP = 2
|
||||
# Read database as standard file. Pure Python.
|
||||
MODE_FILE = 4
|
||||
# Load database into memory. Pure Python.
|
||||
MODE_MEMORY = 8
|
||||
cache_options = frozenset(
|
||||
(MODE_AUTO, MODE_MMAP_EXT, MODE_MMAP, MODE_FILE, MODE_MEMORY)
|
||||
)
|
||||
|
||||
# Paths to the city & country binary databases.
|
||||
_city_file = ""
|
||||
_country_file = ""
|
||||
|
||||
# Initially, pointers to GeoIP file references are NULL.
|
||||
_city = None
|
||||
_country = None
|
||||
|
||||
def __init__(self, path=None, cache=0, country=None, city=None):
|
||||
"""
|
||||
Initialize the GeoIP object. No parameters are required to use default
|
||||
settings. Keyword arguments may be passed in to customize the locations
|
||||
of the GeoIP datasets.
|
||||
|
||||
* path: Base directory to where GeoIP data is located or the full path
|
||||
to where the city or country data files (*.mmdb) are located.
|
||||
Assumes that both the city and country data sets are located in
|
||||
this directory; overrides the GEOIP_PATH setting.
|
||||
|
||||
* cache: The cache settings when opening up the GeoIP datasets. May be
|
||||
an integer in (0, 1, 2, 4, 8) corresponding to the MODE_AUTO,
|
||||
MODE_MMAP_EXT, MODE_MMAP, MODE_FILE, and MODE_MEMORY,
|
||||
`GeoIPOptions` C API settings, respectively. Defaults to 0,
|
||||
meaning MODE_AUTO.
|
||||
|
||||
* country: The name of the GeoIP country data file. Defaults to
|
||||
'GeoLite2-Country.mmdb'; overrides the GEOIP_COUNTRY setting.
|
||||
|
||||
* city: The name of the GeoIP city data file. Defaults to
|
||||
'GeoLite2-City.mmdb'; overrides the GEOIP_CITY setting.
|
||||
"""
|
||||
# Checking the given cache option.
|
||||
if cache not in self.cache_options:
|
||||
raise GeoIP2Exception("Invalid GeoIP caching option: %s" % cache)
|
||||
|
||||
# Getting the GeoIP data path.
|
||||
path = path or GEOIP_SETTINGS["GEOIP_PATH"]
|
||||
if not path:
|
||||
raise GeoIP2Exception(
|
||||
"GeoIP path must be provided via parameter or the GEOIP_PATH setting."
|
||||
)
|
||||
|
||||
path = to_path(path)
|
||||
if path.is_dir():
|
||||
# Constructing the GeoIP database filenames using the settings
|
||||
# dictionary. If the database files for the GeoLite country
|
||||
# and/or city datasets exist, then try to open them.
|
||||
country_db = path / (country or GEOIP_SETTINGS["GEOIP_COUNTRY"])
|
||||
if country_db.is_file():
|
||||
self._country = geoip2.database.Reader(str(country_db), mode=cache)
|
||||
self._country_file = country_db
|
||||
|
||||
city_db = path / (city or GEOIP_SETTINGS["GEOIP_CITY"])
|
||||
if city_db.is_file():
|
||||
self._city = geoip2.database.Reader(str(city_db), mode=cache)
|
||||
self._city_file = city_db
|
||||
if not self._reader:
|
||||
raise GeoIP2Exception("Could not load a database from %s." % path)
|
||||
elif path.is_file():
|
||||
# Otherwise, some detective work will be needed to figure out
|
||||
# whether the given database path is for the GeoIP country or city
|
||||
# databases.
|
||||
reader = geoip2.database.Reader(str(path), mode=cache)
|
||||
db_type = reader.metadata().database_type
|
||||
|
||||
if "City" in db_type:
|
||||
# GeoLite City database detected.
|
||||
self._city = reader
|
||||
self._city_file = path
|
||||
elif "Country" in db_type:
|
||||
# GeoIP Country database detected.
|
||||
self._country = reader
|
||||
self._country_file = path
|
||||
else:
|
||||
raise GeoIP2Exception(
|
||||
"Unable to recognize database edition: %s" % db_type
|
||||
)
|
||||
else:
|
||||
raise GeoIP2Exception("GeoIP path must be a valid file or directory.")
|
||||
|
||||
@property
|
||||
def _reader(self):
|
||||
return self._country or self._city
|
||||
|
||||
@property
|
||||
def _country_or_city(self):
|
||||
if self._country:
|
||||
return self._country.country
|
||||
else:
|
||||
return self._city.city
|
||||
|
||||
def __del__(self):
|
||||
# Cleanup any GeoIP file handles lying around.
|
||||
if self._reader:
|
||||
self._reader.close()
|
||||
|
||||
def __repr__(self):
|
||||
meta = self._reader.metadata()
|
||||
version = "[v%s.%s]" % (
|
||||
meta.binary_format_major_version,
|
||||
meta.binary_format_minor_version,
|
||||
)
|
||||
return (
|
||||
'<%(cls)s %(version)s _country_file="%(country)s", _city_file="%(city)s">'
|
||||
% {
|
||||
"cls": self.__class__.__name__,
|
||||
"version": version,
|
||||
"country": self._country_file,
|
||||
"city": self._city_file,
|
||||
}
|
||||
)
|
||||
|
||||
def _check_query(self, query, city=False, city_or_country=False):
|
||||
"Check the query and database availability."
|
||||
# Making sure a string was passed in for the query.
|
||||
if not isinstance(query, str):
|
||||
raise TypeError(
|
||||
"GeoIP query must be a string, not type %s" % type(query).__name__
|
||||
)
|
||||
|
||||
# Extra checks for the existence of country and city databases.
|
||||
if city_or_country and not (self._country or self._city):
|
||||
raise GeoIP2Exception("Invalid GeoIP country and city data files.")
|
||||
elif city and not self._city:
|
||||
raise GeoIP2Exception("Invalid GeoIP city data file: %s" % self._city_file)
|
||||
|
||||
# Return the query string back to the caller. GeoIP2 only takes IP addresses.
|
||||
try:
|
||||
validate_ipv46_address(query)
|
||||
except ValidationError:
|
||||
query = socket.gethostbyname(query)
|
||||
|
||||
return query
|
||||
|
||||
def city(self, query):
|
||||
"""
|
||||
Return a dictionary of city information for the given IP address or
|
||||
Fully Qualified Domain Name (FQDN). Some information in the dictionary
|
||||
may be undefined (None).
|
||||
"""
|
||||
enc_query = self._check_query(query, city=True)
|
||||
return City(self._city.city(enc_query))
|
||||
|
||||
def country_code(self, query):
|
||||
"Return the country code for the given IP Address or FQDN."
|
||||
return self.country(query)["country_code"]
|
||||
|
||||
def country_name(self, query):
|
||||
"Return the country name for the given IP Address or FQDN."
|
||||
return self.country(query)["country_name"]
|
||||
|
||||
def country(self, query):
|
||||
"""
|
||||
Return a dictionary with the country code and name when given an
|
||||
IP address or a Fully Qualified Domain Name (FQDN). For example, both
|
||||
'24.124.1.80' and 'djangoproject.com' are valid parameters.
|
||||
"""
|
||||
# Returning the country code and name
|
||||
enc_query = self._check_query(query, city_or_country=True)
|
||||
return Country(self._country_or_city(enc_query))
|
||||
|
||||
# #### Coordinate retrieval routines ####
|
||||
def coords(self, query, ordering=("longitude", "latitude")):
|
||||
cdict = self.city(query)
|
||||
if cdict is None:
|
||||
return None
|
||||
else:
|
||||
return tuple(cdict[o] for o in ordering)
|
||||
|
||||
def lon_lat(self, query):
|
||||
"Return a tuple of the (longitude, latitude) for the given query."
|
||||
return self.coords(query)
|
||||
|
||||
def lat_lon(self, query):
|
||||
"Return a tuple of the (latitude, longitude) for the given query."
|
||||
return self.coords(query, ("latitude", "longitude"))
|
||||
|
||||
def geos(self, query):
|
||||
"Return a GEOS Point object for the given query."
|
||||
ll = self.lon_lat(query)
|
||||
if ll:
|
||||
# Allows importing and using GeoIP2() when GEOS is not installed.
|
||||
from django.contrib.gis.geos import Point
|
||||
|
||||
return Point(ll, srid=4326)
|
||||
else:
|
||||
return None
|
||||
|
||||
# #### GeoIP Database Information Routines ####
|
||||
@property
|
||||
def info(self):
|
||||
"Return information about the GeoIP library and databases in use."
|
||||
meta = self._reader.metadata()
|
||||
return "GeoIP Library:\n\t%s.%s\n" % (
|
||||
meta.binary_format_major_version,
|
||||
meta.binary_format_minor_version,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def open(cls, full_path, cache):
|
||||
return GeoIP2(full_path, cache)
|
@ -0,0 +1,22 @@
|
||||
def City(response):
|
||||
return {
|
||||
"city": response.city.name,
|
||||
"continent_code": response.continent.code,
|
||||
"continent_name": response.continent.name,
|
||||
"country_code": response.country.iso_code,
|
||||
"country_name": response.country.name,
|
||||
"dma_code": response.location.metro_code,
|
||||
"is_in_european_union": response.country.is_in_european_union,
|
||||
"latitude": response.location.latitude,
|
||||
"longitude": response.location.longitude,
|
||||
"postal_code": response.postal.code,
|
||||
"region": response.subdivisions[0].iso_code if response.subdivisions else None,
|
||||
"time_zone": response.location.time_zone,
|
||||
}
|
||||
|
||||
|
||||
def Country(response):
|
||||
return {
|
||||
"country_code": response.country.iso_code,
|
||||
"country_name": response.country.name,
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import re
|
||||
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
|
||||
# Regular expression for recognizing HEXEWKB and WKT. A prophylactic measure
|
||||
# to prevent potentially malicious input from reaching the underlying C
|
||||
# library. Not a substitute for good web security programming practices.
|
||||
hex_regex = _lazy_re_compile(r"^[0-9A-F]+$", re.I)
|
||||
wkt_regex = _lazy_re_compile(
|
||||
r"^(SRID=(?P<srid>\-?[0-9]+);)?"
|
||||
r"(?P<wkt>"
|
||||
r"(?P<type>POINT|LINESTRING|LINEARRING|POLYGON|MULTIPOINT|"
|
||||
r"MULTILINESTRING|MULTIPOLYGON|GEOMETRYCOLLECTION)"
|
||||
r"[ACEGIMLONPSRUTYZ0-9,\.\-\+\(\) ]+)$",
|
||||
re.I,
|
||||
)
|
||||
json_regex = _lazy_re_compile(r"^(\s+)?\{.*}(\s+)?$", re.DOTALL)
|
@ -0,0 +1,27 @@
|
||||
Copyright (c) 2007-2009 Justin Bronn
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of GEOSGeometry nor the names of its contributors may be used
|
||||
to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@ -0,0 +1,18 @@
|
||||
"""
|
||||
The GeoDjango GEOS module. Please consult the GeoDjango documentation
|
||||
for more details: https://docs.djangoproject.com/en/dev/ref/contrib/gis/geos/
|
||||
"""
|
||||
from .collections import ( # NOQA
|
||||
GeometryCollection,
|
||||
MultiLineString,
|
||||
MultiPoint,
|
||||
MultiPolygon,
|
||||
)
|
||||
from .error import GEOSException # NOQA
|
||||
from .factory import fromfile, fromstr # NOQA
|
||||
from .geometry import GEOSGeometry, hex_regex, wkt_regex # NOQA
|
||||
from .io import WKBReader, WKBWriter, WKTReader, WKTWriter # NOQA
|
||||
from .libgeos import geos_version # NOQA
|
||||
from .linestring import LinearRing, LineString # NOQA
|
||||
from .point import Point # NOQA
|
||||
from .polygon import Polygon # NOQA
|
@ -0,0 +1,6 @@
|
||||
from django.contrib.gis.geos.error import GEOSException
|
||||
from django.contrib.gis.ptr import CPointerBase
|
||||
|
||||
|
||||
class GEOSBase(CPointerBase):
|
||||
null_ptr_exception_class = GEOSException
|
@ -0,0 +1,120 @@
|
||||
"""
|
||||
This module houses the Geometry Collection objects:
|
||||
GeometryCollection, MultiPoint, MultiLineString, and MultiPolygon
|
||||
"""
|
||||
from django.contrib.gis.geos import prototypes as capi
|
||||
from django.contrib.gis.geos.geometry import GEOSGeometry, LinearGeometryMixin
|
||||
from django.contrib.gis.geos.libgeos import GEOM_PTR
|
||||
from django.contrib.gis.geos.linestring import LinearRing, LineString
|
||||
from django.contrib.gis.geos.point import Point
|
||||
from django.contrib.gis.geos.polygon import Polygon
|
||||
|
||||
|
||||
class GeometryCollection(GEOSGeometry):
|
||||
_typeid = 7
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"Initialize a Geometry Collection from a sequence of Geometry objects."
|
||||
# Checking the arguments
|
||||
if len(args) == 1:
|
||||
# If only one geometry provided or a list of geometries is provided
|
||||
# in the first argument.
|
||||
if isinstance(args[0], (tuple, list)):
|
||||
init_geoms = args[0]
|
||||
else:
|
||||
init_geoms = args
|
||||
else:
|
||||
init_geoms = args
|
||||
|
||||
# Ensuring that only the permitted geometries are allowed in this collection
|
||||
# this is moved to list mixin super class
|
||||
self._check_allowed(init_geoms)
|
||||
|
||||
# Creating the geometry pointer array.
|
||||
collection = self._create_collection(len(init_geoms), init_geoms)
|
||||
super().__init__(collection, **kwargs)
|
||||
|
||||
def __iter__(self):
|
||||
"Iterate over each Geometry in the Collection."
|
||||
for i in range(len(self)):
|
||||
yield self[i]
|
||||
|
||||
def __len__(self):
|
||||
"Return the number of geometries in this Collection."
|
||||
return self.num_geom
|
||||
|
||||
# ### Methods for compatibility with ListMixin ###
|
||||
def _create_collection(self, length, items):
|
||||
# Creating the geometry pointer array.
|
||||
geoms = (GEOM_PTR * length)(
|
||||
*[
|
||||
# this is a little sloppy, but makes life easier
|
||||
# allow GEOSGeometry types (python wrappers) or pointer types
|
||||
capi.geom_clone(getattr(g, "ptr", g))
|
||||
for g in items
|
||||
]
|
||||
)
|
||||
return capi.create_collection(self._typeid, geoms, length)
|
||||
|
||||
def _get_single_internal(self, index):
|
||||
return capi.get_geomn(self.ptr, index)
|
||||
|
||||
def _get_single_external(self, index):
|
||||
"Return the Geometry from this Collection at the given index (0-based)."
|
||||
# Checking the index and returning the corresponding GEOS geometry.
|
||||
return GEOSGeometry(
|
||||
capi.geom_clone(self._get_single_internal(index)), srid=self.srid
|
||||
)
|
||||
|
||||
def _set_list(self, length, items):
|
||||
"Create a new collection, and destroy the contents of the previous pointer."
|
||||
prev_ptr = self.ptr
|
||||
srid = self.srid
|
||||
self.ptr = self._create_collection(length, items)
|
||||
if srid:
|
||||
self.srid = srid
|
||||
capi.destroy_geom(prev_ptr)
|
||||
|
||||
_set_single = GEOSGeometry._set_single_rebuild
|
||||
_assign_extended_slice = GEOSGeometry._assign_extended_slice_rebuild
|
||||
|
||||
@property
|
||||
def kml(self):
|
||||
"Return the KML for this Geometry Collection."
|
||||
return "<MultiGeometry>%s</MultiGeometry>" % "".join(g.kml for g in self)
|
||||
|
||||
@property
|
||||
def tuple(self):
|
||||
"Return a tuple of all the coordinates in this Geometry Collection"
|
||||
return tuple(g.tuple for g in self)
|
||||
|
||||
coords = tuple
|
||||
|
||||
|
||||
# MultiPoint, MultiLineString, and MultiPolygon class definitions.
|
||||
class MultiPoint(GeometryCollection):
|
||||
_allowed = Point
|
||||
_typeid = 4
|
||||
|
||||
|
||||
class MultiLineString(LinearGeometryMixin, GeometryCollection):
|
||||
_allowed = (LineString, LinearRing)
|
||||
_typeid = 5
|
||||
|
||||
|
||||
class MultiPolygon(GeometryCollection):
|
||||
_allowed = Polygon
|
||||
_typeid = 6
|
||||
|
||||
|
||||
# Setting the allowed types here since GeometryCollection is defined before
|
||||
# its subclasses.
|
||||
GeometryCollection._allowed = (
|
||||
Point,
|
||||
LineString,
|
||||
LinearRing,
|
||||
Polygon,
|
||||
MultiPoint,
|
||||
MultiLineString,
|
||||
MultiPolygon,
|
||||
)
|
@ -0,0 +1,220 @@
|
||||
"""
|
||||
This module houses the GEOSCoordSeq object, which is used internally
|
||||
by GEOSGeometry to house the actual coordinates of the Point,
|
||||
LineString, and LinearRing geometries.
|
||||
"""
|
||||
from ctypes import byref, c_byte, c_double, c_uint
|
||||
|
||||
from django.contrib.gis.geos import prototypes as capi
|
||||
from django.contrib.gis.geos.base import GEOSBase
|
||||
from django.contrib.gis.geos.error import GEOSException
|
||||
from django.contrib.gis.geos.libgeos import CS_PTR, geos_version_tuple
|
||||
from django.contrib.gis.shortcuts import numpy
|
||||
|
||||
|
||||
class GEOSCoordSeq(GEOSBase):
|
||||
"The internal representation of a list of coordinates inside a Geometry."
|
||||
|
||||
ptr_type = CS_PTR
|
||||
|
||||
def __init__(self, ptr, z=False):
|
||||
"Initialize from a GEOS pointer."
|
||||
if not isinstance(ptr, CS_PTR):
|
||||
raise TypeError("Coordinate sequence should initialize with a CS_PTR.")
|
||||
self._ptr = ptr
|
||||
self._z = z
|
||||
|
||||
def __iter__(self):
|
||||
"Iterate over each point in the coordinate sequence."
|
||||
for i in range(self.size):
|
||||
yield self[i]
|
||||
|
||||
def __len__(self):
|
||||
"Return the number of points in the coordinate sequence."
|
||||
return self.size
|
||||
|
||||
def __str__(self):
|
||||
"Return the string representation of the coordinate sequence."
|
||||
return str(self.tuple)
|
||||
|
||||
def __getitem__(self, index):
|
||||
"Return the coordinate sequence value at the given index."
|
||||
self._checkindex(index)
|
||||
return self._point_getter(index)
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
"Set the coordinate sequence value at the given index."
|
||||
# Checking the input value
|
||||
if isinstance(value, (list, tuple)):
|
||||
pass
|
||||
elif numpy and isinstance(value, numpy.ndarray):
|
||||
pass
|
||||
else:
|
||||
raise TypeError(
|
||||
"Must set coordinate with a sequence (list, tuple, or numpy array)."
|
||||
)
|
||||
# Checking the dims of the input
|
||||
if self.dims == 3 and self._z:
|
||||
n_args = 3
|
||||
point_setter = self._set_point_3d
|
||||
else:
|
||||
n_args = 2
|
||||
point_setter = self._set_point_2d
|
||||
if len(value) != n_args:
|
||||
raise TypeError("Dimension of value does not match.")
|
||||
self._checkindex(index)
|
||||
point_setter(index, value)
|
||||
|
||||
# #### Internal Routines ####
|
||||
def _checkindex(self, index):
|
||||
"Check the given index."
|
||||
if not (0 <= index < self.size):
|
||||
raise IndexError("invalid GEOS Geometry index: %s" % index)
|
||||
|
||||
def _checkdim(self, dim):
|
||||
"Check the given dimension."
|
||||
if dim < 0 or dim > 2:
|
||||
raise GEOSException('invalid ordinate dimension "%d"' % dim)
|
||||
|
||||
def _get_x(self, index):
|
||||
return capi.cs_getx(self.ptr, index, byref(c_double()))
|
||||
|
||||
def _get_y(self, index):
|
||||
return capi.cs_gety(self.ptr, index, byref(c_double()))
|
||||
|
||||
def _get_z(self, index):
|
||||
return capi.cs_getz(self.ptr, index, byref(c_double()))
|
||||
|
||||
def _set_x(self, index, value):
|
||||
capi.cs_setx(self.ptr, index, value)
|
||||
|
||||
def _set_y(self, index, value):
|
||||
capi.cs_sety(self.ptr, index, value)
|
||||
|
||||
def _set_z(self, index, value):
|
||||
capi.cs_setz(self.ptr, index, value)
|
||||
|
||||
@property
|
||||
def _point_getter(self):
|
||||
return self._get_point_3d if self.dims == 3 and self._z else self._get_point_2d
|
||||
|
||||
def _get_point_2d(self, index):
|
||||
return (self._get_x(index), self._get_y(index))
|
||||
|
||||
def _get_point_3d(self, index):
|
||||
return (self._get_x(index), self._get_y(index), self._get_z(index))
|
||||
|
||||
def _set_point_2d(self, index, value):
|
||||
x, y = value
|
||||
self._set_x(index, x)
|
||||
self._set_y(index, y)
|
||||
|
||||
def _set_point_3d(self, index, value):
|
||||
x, y, z = value
|
||||
self._set_x(index, x)
|
||||
self._set_y(index, y)
|
||||
self._set_z(index, z)
|
||||
|
||||
# #### Ordinate getting and setting routines ####
|
||||
def getOrdinate(self, dimension, index):
|
||||
"Return the value for the given dimension and index."
|
||||
self._checkindex(index)
|
||||
self._checkdim(dimension)
|
||||
return capi.cs_getordinate(self.ptr, index, dimension, byref(c_double()))
|
||||
|
||||
def setOrdinate(self, dimension, index, value):
|
||||
"Set the value for the given dimension and index."
|
||||
self._checkindex(index)
|
||||
self._checkdim(dimension)
|
||||
capi.cs_setordinate(self.ptr, index, dimension, value)
|
||||
|
||||
def getX(self, index):
|
||||
"Get the X value at the index."
|
||||
return self.getOrdinate(0, index)
|
||||
|
||||
def setX(self, index, value):
|
||||
"Set X with the value at the given index."
|
||||
self.setOrdinate(0, index, value)
|
||||
|
||||
def getY(self, index):
|
||||
"Get the Y value at the given index."
|
||||
return self.getOrdinate(1, index)
|
||||
|
||||
def setY(self, index, value):
|
||||
"Set Y with the value at the given index."
|
||||
self.setOrdinate(1, index, value)
|
||||
|
||||
def getZ(self, index):
|
||||
"Get Z with the value at the given index."
|
||||
return self.getOrdinate(2, index)
|
||||
|
||||
def setZ(self, index, value):
|
||||
"Set Z with the value at the given index."
|
||||
self.setOrdinate(2, index, value)
|
||||
|
||||
# ### Dimensions ###
|
||||
@property
|
||||
def size(self):
|
||||
"Return the size of this coordinate sequence."
|
||||
return capi.cs_getsize(self.ptr, byref(c_uint()))
|
||||
|
||||
@property
|
||||
def dims(self):
|
||||
"Return the dimensions of this coordinate sequence."
|
||||
return capi.cs_getdims(self.ptr, byref(c_uint()))
|
||||
|
||||
@property
|
||||
def hasz(self):
|
||||
"""
|
||||
Return whether this coordinate sequence is 3D. This property value is
|
||||
inherited from the parent Geometry.
|
||||
"""
|
||||
return self._z
|
||||
|
||||
# ### Other Methods ###
|
||||
def clone(self):
|
||||
"Clone this coordinate sequence."
|
||||
return GEOSCoordSeq(capi.cs_clone(self.ptr), self.hasz)
|
||||
|
||||
@property
|
||||
def kml(self):
|
||||
"Return the KML representation for the coordinates."
|
||||
# Getting the substitution string depending on whether the coordinates have
|
||||
# a Z dimension.
|
||||
if self.hasz:
|
||||
substr = "%s,%s,%s "
|
||||
else:
|
||||
substr = "%s,%s,0 "
|
||||
return (
|
||||
"<coordinates>%s</coordinates>"
|
||||
% "".join(substr % self[i] for i in range(len(self))).strip()
|
||||
)
|
||||
|
||||
@property
|
||||
def tuple(self):
|
||||
"Return a tuple version of this coordinate sequence."
|
||||
n = self.size
|
||||
get_point = self._point_getter
|
||||
if n == 1:
|
||||
return get_point(0)
|
||||
return tuple(get_point(i) for i in range(n))
|
||||
|
||||
@property
|
||||
def is_counterclockwise(self):
|
||||
"""Return whether this coordinate sequence is counterclockwise."""
|
||||
if geos_version_tuple() < (3, 7):
|
||||
# A modified shoelace algorithm to determine polygon orientation.
|
||||
# See https://en.wikipedia.org/wiki/Shoelace_formula.
|
||||
area = 0.0
|
||||
n = len(self)
|
||||
for i in range(n):
|
||||
j = (i + 1) % n
|
||||
area += self[i][0] * self[j][1]
|
||||
area -= self[j][0] * self[i][1]
|
||||
return area > 0.0
|
||||
ret = c_byte()
|
||||
if not capi.cs_is_ccw(self.ptr, byref(ret)):
|
||||
raise GEOSException(
|
||||
'Error encountered in GEOS C function "%s".' % capi.cs_is_ccw.func_name
|
||||
)
|
||||
return ret.value == 1
|
@ -0,0 +1,3 @@
|
||||
class GEOSException(Exception):
|
||||
"The base GEOS exception, indicates a GEOS-related error."
|
||||
pass
|
@ -0,0 +1,33 @@
|
||||
from django.contrib.gis.geos.geometry import GEOSGeometry, hex_regex, wkt_regex
|
||||
|
||||
|
||||
def fromfile(file_h):
|
||||
"""
|
||||
Given a string file name, returns a GEOSGeometry. The file may contain WKB,
|
||||
WKT, or HEX.
|
||||
"""
|
||||
# If given a file name, get a real handle.
|
||||
if isinstance(file_h, str):
|
||||
with open(file_h, "rb") as file_h:
|
||||
buf = file_h.read()
|
||||
else:
|
||||
buf = file_h.read()
|
||||
|
||||
# If we get WKB need to wrap in memoryview(), so run through regexes.
|
||||
if isinstance(buf, bytes):
|
||||
try:
|
||||
decoded = buf.decode()
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
else:
|
||||
if wkt_regex.match(decoded) or hex_regex.match(decoded):
|
||||
return GEOSGeometry(decoded)
|
||||
else:
|
||||
return GEOSGeometry(buf)
|
||||
|
||||
return GEOSGeometry(memoryview(buf))
|
||||
|
||||
|
||||
def fromstr(string, **kwargs):
|
||||
"Given a string value, return a GEOSGeometry object."
|
||||
return GEOSGeometry(string, **kwargs)
|
@ -0,0 +1,771 @@
|
||||
"""
|
||||
This module contains the 'base' GEOSGeometry object -- all GEOS Geometries
|
||||
inherit from this object.
|
||||
"""
|
||||
import re
|
||||
from ctypes import addressof, byref, c_double
|
||||
|
||||
from django.contrib.gis import gdal
|
||||
from django.contrib.gis.geometry import hex_regex, json_regex, wkt_regex
|
||||
from django.contrib.gis.geos import prototypes as capi
|
||||
from django.contrib.gis.geos.base import GEOSBase
|
||||
from django.contrib.gis.geos.coordseq import GEOSCoordSeq
|
||||
from django.contrib.gis.geos.error import GEOSException
|
||||
from django.contrib.gis.geos.libgeos import GEOM_PTR, geos_version_tuple
|
||||
from django.contrib.gis.geos.mutable_list import ListMixin
|
||||
from django.contrib.gis.geos.prepared import PreparedGeometry
|
||||
from django.contrib.gis.geos.prototypes.io import ewkb_w, wkb_r, wkb_w, wkt_r, wkt_w
|
||||
from django.utils.deconstruct import deconstructible
|
||||
from django.utils.encoding import force_bytes, force_str
|
||||
|
||||
|
||||
class GEOSGeometryBase(GEOSBase):
|
||||
_GEOS_CLASSES = None
|
||||
|
||||
ptr_type = GEOM_PTR
|
||||
destructor = capi.destroy_geom
|
||||
has_cs = False # Only Point, LineString, LinearRing have coordinate sequences
|
||||
|
||||
def __init__(self, ptr, cls):
|
||||
self._ptr = ptr
|
||||
|
||||
# Setting the class type (e.g., Point, Polygon, etc.)
|
||||
if type(self) in (GEOSGeometryBase, GEOSGeometry):
|
||||
if cls is None:
|
||||
if GEOSGeometryBase._GEOS_CLASSES is None:
|
||||
# Inner imports avoid import conflicts with GEOSGeometry.
|
||||
from .collections import (
|
||||
GeometryCollection,
|
||||
MultiLineString,
|
||||
MultiPoint,
|
||||
MultiPolygon,
|
||||
)
|
||||
from .linestring import LinearRing, LineString
|
||||
from .point import Point
|
||||
from .polygon import Polygon
|
||||
|
||||
GEOSGeometryBase._GEOS_CLASSES = {
|
||||
0: Point,
|
||||
1: LineString,
|
||||
2: LinearRing,
|
||||
3: Polygon,
|
||||
4: MultiPoint,
|
||||
5: MultiLineString,
|
||||
6: MultiPolygon,
|
||||
7: GeometryCollection,
|
||||
}
|
||||
cls = GEOSGeometryBase._GEOS_CLASSES[self.geom_typeid]
|
||||
self.__class__ = cls
|
||||
self._post_init()
|
||||
|
||||
def _post_init(self):
|
||||
"Perform post-initialization setup."
|
||||
# Setting the coordinate sequence for the geometry (will be None on
|
||||
# geometries that do not have coordinate sequences)
|
||||
self._cs = (
|
||||
GEOSCoordSeq(capi.get_cs(self.ptr), self.hasz) if self.has_cs else None
|
||||
)
|
||||
|
||||
def __copy__(self):
|
||||
"""
|
||||
Return a clone because the copy of a GEOSGeometry may contain an
|
||||
invalid pointer location if the original is garbage collected.
|
||||
"""
|
||||
return self.clone()
|
||||
|
||||
def __deepcopy__(self, memodict):
|
||||
"""
|
||||
The `deepcopy` routine is used by the `Node` class of django.utils.tree;
|
||||
thus, the protocol routine needs to be implemented to return correct
|
||||
copies (clones) of these GEOS objects, which use C pointers.
|
||||
"""
|
||||
return self.clone()
|
||||
|
||||
def __str__(self):
|
||||
"EWKT is used for the string representation."
|
||||
return self.ewkt
|
||||
|
||||
def __repr__(self):
|
||||
"Short-hand representation because WKT may be very large."
|
||||
return "<%s object at %s>" % (self.geom_type, hex(addressof(self.ptr)))
|
||||
|
||||
# Pickling support
|
||||
def _to_pickle_wkb(self):
|
||||
return bytes(self.wkb)
|
||||
|
||||
def _from_pickle_wkb(self, wkb):
|
||||
return wkb_r().read(memoryview(wkb))
|
||||
|
||||
def __getstate__(self):
|
||||
# The pickled state is simply a tuple of the WKB (in string form)
|
||||
# and the SRID.
|
||||
return self._to_pickle_wkb(), self.srid
|
||||
|
||||
def __setstate__(self, state):
|
||||
# Instantiating from the tuple state that was pickled.
|
||||
wkb, srid = state
|
||||
ptr = self._from_pickle_wkb(wkb)
|
||||
if not ptr:
|
||||
raise GEOSException("Invalid Geometry loaded from pickled state.")
|
||||
self.ptr = ptr
|
||||
self._post_init()
|
||||
self.srid = srid
|
||||
|
||||
@classmethod
|
||||
def _from_wkb(cls, wkb):
|
||||
return wkb_r().read(wkb)
|
||||
|
||||
@staticmethod
|
||||
def from_ewkt(ewkt):
|
||||
ewkt = force_bytes(ewkt)
|
||||
srid = None
|
||||
parts = ewkt.split(b";", 1)
|
||||
if len(parts) == 2:
|
||||
srid_part, wkt = parts
|
||||
match = re.match(rb"SRID=(?P<srid>\-?\d+)", srid_part)
|
||||
if not match:
|
||||
raise ValueError("EWKT has invalid SRID part.")
|
||||
srid = int(match["srid"])
|
||||
else:
|
||||
wkt = ewkt
|
||||
if not wkt:
|
||||
raise ValueError("Expected WKT but got an empty string.")
|
||||
return GEOSGeometry(GEOSGeometry._from_wkt(wkt), srid=srid)
|
||||
|
||||
@staticmethod
|
||||
def _from_wkt(wkt):
|
||||
return wkt_r().read(wkt)
|
||||
|
||||
@classmethod
|
||||
def from_gml(cls, gml_string):
|
||||
return gdal.OGRGeometry.from_gml(gml_string).geos
|
||||
|
||||
# Comparison operators
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
Equivalence testing, a Geometry may be compared with another Geometry
|
||||
or an EWKT representation.
|
||||
"""
|
||||
if isinstance(other, str):
|
||||
try:
|
||||
other = GEOSGeometry.from_ewkt(other)
|
||||
except (ValueError, GEOSException):
|
||||
return False
|
||||
return (
|
||||
isinstance(other, GEOSGeometry)
|
||||
and self.srid == other.srid
|
||||
and self.equals_exact(other)
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.srid, self.wkt))
|
||||
|
||||
# ### Geometry set-like operations ###
|
||||
# Thanks to Sean Gillies for inspiration:
|
||||
# http://lists.gispython.org/pipermail/community/2007-July/001034.html
|
||||
# g = g1 | g2
|
||||
def __or__(self, other):
|
||||
"Return the union of this Geometry and the other."
|
||||
return self.union(other)
|
||||
|
||||
# g = g1 & g2
|
||||
def __and__(self, other):
|
||||
"Return the intersection of this Geometry and the other."
|
||||
return self.intersection(other)
|
||||
|
||||
# g = g1 - g2
|
||||
def __sub__(self, other):
|
||||
"Return the difference this Geometry and the other."
|
||||
return self.difference(other)
|
||||
|
||||
# g = g1 ^ g2
|
||||
def __xor__(self, other):
|
||||
"Return the symmetric difference of this Geometry and the other."
|
||||
return self.sym_difference(other)
|
||||
|
||||
# #### Coordinate Sequence Routines ####
|
||||
@property
|
||||
def coord_seq(self):
|
||||
"Return a clone of the coordinate sequence for this Geometry."
|
||||
if self.has_cs:
|
||||
return self._cs.clone()
|
||||
|
||||
# #### Geometry Info ####
|
||||
@property
|
||||
def geom_type(self):
|
||||
"Return a string representing the Geometry type, e.g. 'Polygon'"
|
||||
return capi.geos_type(self.ptr).decode()
|
||||
|
||||
@property
|
||||
def geom_typeid(self):
|
||||
"Return an integer representing the Geometry type."
|
||||
return capi.geos_typeid(self.ptr)
|
||||
|
||||
@property
|
||||
def num_geom(self):
|
||||
"Return the number of geometries in the Geometry."
|
||||
return capi.get_num_geoms(self.ptr)
|
||||
|
||||
@property
|
||||
def num_coords(self):
|
||||
"Return the number of coordinates in the Geometry."
|
||||
return capi.get_num_coords(self.ptr)
|
||||
|
||||
@property
|
||||
def num_points(self):
|
||||
"Return the number points, or coordinates, in the Geometry."
|
||||
return self.num_coords
|
||||
|
||||
@property
|
||||
def dims(self):
|
||||
"Return the dimension of this Geometry (0=point, 1=line, 2=surface)."
|
||||
return capi.get_dims(self.ptr)
|
||||
|
||||
def normalize(self, clone=False):
|
||||
"""
|
||||
Convert this Geometry to normal form (or canonical form).
|
||||
If the `clone` keyword is set, then the geometry is not modified and a
|
||||
normalized clone of the geometry is returned instead.
|
||||
"""
|
||||
if clone:
|
||||
clone = self.clone()
|
||||
capi.geos_normalize(clone.ptr)
|
||||
return clone
|
||||
capi.geos_normalize(self.ptr)
|
||||
|
||||
def make_valid(self):
|
||||
"""
|
||||
Attempt to create a valid representation of a given invalid geometry
|
||||
without losing any of the input vertices.
|
||||
"""
|
||||
if geos_version_tuple() < (3, 8):
|
||||
raise GEOSException("GEOSGeometry.make_valid() requires GEOS >= 3.8.0.")
|
||||
return GEOSGeometry(capi.geos_makevalid(self.ptr), srid=self.srid)
|
||||
|
||||
# #### Unary predicates ####
|
||||
@property
|
||||
def empty(self):
|
||||
"""
|
||||
Return a boolean indicating whether the set of points in this Geometry
|
||||
are empty.
|
||||
"""
|
||||
return capi.geos_isempty(self.ptr)
|
||||
|
||||
@property
|
||||
def hasz(self):
|
||||
"Return whether the geometry has a 3D dimension."
|
||||
return capi.geos_hasz(self.ptr)
|
||||
|
||||
@property
|
||||
def ring(self):
|
||||
"Return whether or not the geometry is a ring."
|
||||
return capi.geos_isring(self.ptr)
|
||||
|
||||
@property
|
||||
def simple(self):
|
||||
"Return false if the Geometry isn't simple."
|
||||
return capi.geos_issimple(self.ptr)
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
"Test the validity of this Geometry."
|
||||
return capi.geos_isvalid(self.ptr)
|
||||
|
||||
@property
|
||||
def valid_reason(self):
|
||||
"""
|
||||
Return a string containing the reason for any invalidity.
|
||||
"""
|
||||
return capi.geos_isvalidreason(self.ptr).decode()
|
||||
|
||||
# #### Binary predicates. ####
|
||||
def contains(self, other):
|
||||
"Return true if other.within(this) returns true."
|
||||
return capi.geos_contains(self.ptr, other.ptr)
|
||||
|
||||
def covers(self, other):
|
||||
"""
|
||||
Return True if the DE-9IM Intersection Matrix for the two geometries is
|
||||
T*****FF*, *T****FF*, ***T**FF*, or ****T*FF*. If either geometry is
|
||||
empty, return False.
|
||||
"""
|
||||
return capi.geos_covers(self.ptr, other.ptr)
|
||||
|
||||
def crosses(self, other):
|
||||
"""
|
||||
Return true if the DE-9IM intersection matrix for the two Geometries
|
||||
is T*T****** (for a point and a curve,a point and an area or a line and
|
||||
an area) 0******** (for two curves).
|
||||
"""
|
||||
return capi.geos_crosses(self.ptr, other.ptr)
|
||||
|
||||
def disjoint(self, other):
|
||||
"""
|
||||
Return true if the DE-9IM intersection matrix for the two Geometries
|
||||
is FF*FF****.
|
||||
"""
|
||||
return capi.geos_disjoint(self.ptr, other.ptr)
|
||||
|
||||
def equals(self, other):
|
||||
"""
|
||||
Return true if the DE-9IM intersection matrix for the two Geometries
|
||||
is T*F**FFF*.
|
||||
"""
|
||||
return capi.geos_equals(self.ptr, other.ptr)
|
||||
|
||||
def equals_exact(self, other, tolerance=0):
|
||||
"""
|
||||
Return true if the two Geometries are exactly equal, up to a
|
||||
specified tolerance.
|
||||
"""
|
||||
return capi.geos_equalsexact(self.ptr, other.ptr, float(tolerance))
|
||||
|
||||
def intersects(self, other):
|
||||
"Return true if disjoint return false."
|
||||
return capi.geos_intersects(self.ptr, other.ptr)
|
||||
|
||||
def overlaps(self, other):
|
||||
"""
|
||||
Return true if the DE-9IM intersection matrix for the two Geometries
|
||||
is T*T***T** (for two points or two surfaces) 1*T***T** (for two curves).
|
||||
"""
|
||||
return capi.geos_overlaps(self.ptr, other.ptr)
|
||||
|
||||
def relate_pattern(self, other, pattern):
|
||||
"""
|
||||
Return true if the elements in the DE-9IM intersection matrix for the
|
||||
two Geometries match the elements in pattern.
|
||||
"""
|
||||
if not isinstance(pattern, str) or len(pattern) > 9:
|
||||
raise GEOSException("invalid intersection matrix pattern")
|
||||
return capi.geos_relatepattern(self.ptr, other.ptr, force_bytes(pattern))
|
||||
|
||||
def touches(self, other):
|
||||
"""
|
||||
Return true if the DE-9IM intersection matrix for the two Geometries
|
||||
is FT*******, F**T***** or F***T****.
|
||||
"""
|
||||
return capi.geos_touches(self.ptr, other.ptr)
|
||||
|
||||
def within(self, other):
|
||||
"""
|
||||
Return true if the DE-9IM intersection matrix for the two Geometries
|
||||
is T*F**F***.
|
||||
"""
|
||||
return capi.geos_within(self.ptr, other.ptr)
|
||||
|
||||
# #### SRID Routines ####
|
||||
@property
|
||||
def srid(self):
|
||||
"Get the SRID for the geometry. Return None if no SRID is set."
|
||||
s = capi.geos_get_srid(self.ptr)
|
||||
if s == 0:
|
||||
return None
|
||||
else:
|
||||
return s
|
||||
|
||||
@srid.setter
|
||||
def srid(self, srid):
|
||||
"Set the SRID for the geometry."
|
||||
capi.geos_set_srid(self.ptr, 0 if srid is None else srid)
|
||||
|
||||
# #### Output Routines ####
|
||||
@property
|
||||
def ewkt(self):
|
||||
"""
|
||||
Return the EWKT (SRID + WKT) of the Geometry.
|
||||
"""
|
||||
srid = self.srid
|
||||
return "SRID=%s;%s" % (srid, self.wkt) if srid else self.wkt
|
||||
|
||||
@property
|
||||
def wkt(self):
|
||||
"Return the WKT (Well-Known Text) representation of this Geometry."
|
||||
return wkt_w(dim=3 if self.hasz else 2, trim=True).write(self).decode()
|
||||
|
||||
@property
|
||||
def hex(self):
|
||||
"""
|
||||
Return the WKB of this Geometry in hexadecimal form. Please note
|
||||
that the SRID is not included in this representation because it is not
|
||||
a part of the OGC specification (use the `hexewkb` property instead).
|
||||
"""
|
||||
# A possible faster, all-python, implementation:
|
||||
# str(self.wkb).encode('hex')
|
||||
return wkb_w(dim=3 if self.hasz else 2).write_hex(self)
|
||||
|
||||
@property
|
||||
def hexewkb(self):
|
||||
"""
|
||||
Return the EWKB of this Geometry in hexadecimal form. This is an
|
||||
extension of the WKB specification that includes SRID value that are
|
||||
a part of this geometry.
|
||||
"""
|
||||
return ewkb_w(dim=3 if self.hasz else 2).write_hex(self)
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
"""
|
||||
Return GeoJSON representation of this Geometry.
|
||||
"""
|
||||
return self.ogr.json
|
||||
|
||||
geojson = json
|
||||
|
||||
@property
|
||||
def wkb(self):
|
||||
"""
|
||||
Return the WKB (Well-Known Binary) representation of this Geometry
|
||||
as a Python memoryview. SRID and Z values are not included, use the
|
||||
`ewkb` property instead.
|
||||
"""
|
||||
return wkb_w(3 if self.hasz else 2).write(self)
|
||||
|
||||
@property
|
||||
def ewkb(self):
|
||||
"""
|
||||
Return the EWKB representation of this Geometry as a Python memoryview.
|
||||
This is an extension of the WKB specification that includes any SRID
|
||||
value that are a part of this geometry.
|
||||
"""
|
||||
return ewkb_w(3 if self.hasz else 2).write(self)
|
||||
|
||||
@property
|
||||
def kml(self):
|
||||
"Return the KML representation of this Geometry."
|
||||
gtype = self.geom_type
|
||||
return "<%s>%s</%s>" % (gtype, self.coord_seq.kml, gtype)
|
||||
|
||||
@property
|
||||
def prepared(self):
|
||||
"""
|
||||
Return a PreparedGeometry corresponding to this geometry -- it is
|
||||
optimized for the contains, intersects, and covers operations.
|
||||
"""
|
||||
return PreparedGeometry(self)
|
||||
|
||||
# #### GDAL-specific output routines ####
|
||||
def _ogr_ptr(self):
|
||||
return gdal.OGRGeometry._from_wkb(self.wkb)
|
||||
|
||||
@property
|
||||
def ogr(self):
|
||||
"Return the OGR Geometry for this Geometry."
|
||||
return gdal.OGRGeometry(self._ogr_ptr(), self.srs)
|
||||
|
||||
@property
|
||||
def srs(self):
|
||||
"Return the OSR SpatialReference for SRID of this Geometry."
|
||||
if self.srid:
|
||||
try:
|
||||
return gdal.SpatialReference(self.srid)
|
||||
except (gdal.GDALException, gdal.SRSException):
|
||||
pass
|
||||
return None
|
||||
|
||||
@property
|
||||
def crs(self):
|
||||
"Alias for `srs` property."
|
||||
return self.srs
|
||||
|
||||
def transform(self, ct, clone=False):
|
||||
"""
|
||||
Requires GDAL. Transform the geometry according to the given
|
||||
transformation object, which may be an integer SRID, and WKT or
|
||||
PROJ string. By default, transform the geometry in-place and return
|
||||
nothing. However if the `clone` keyword is set, don't modify the
|
||||
geometry and return a transformed clone instead.
|
||||
"""
|
||||
srid = self.srid
|
||||
|
||||
if ct == srid:
|
||||
# short-circuit where source & dest SRIDs match
|
||||
if clone:
|
||||
return self.clone()
|
||||
else:
|
||||
return
|
||||
|
||||
if isinstance(ct, gdal.CoordTransform):
|
||||
# We don't care about SRID because CoordTransform presupposes
|
||||
# source SRS.
|
||||
srid = None
|
||||
elif srid is None or srid < 0:
|
||||
raise GEOSException("Calling transform() with no SRID set is not supported")
|
||||
|
||||
# Creating an OGR Geometry, which is then transformed.
|
||||
g = gdal.OGRGeometry(self._ogr_ptr(), srid)
|
||||
g.transform(ct)
|
||||
# Getting a new GEOS pointer
|
||||
ptr = g._geos_ptr()
|
||||
if clone:
|
||||
# User wants a cloned transformed geometry returned.
|
||||
return GEOSGeometry(ptr, srid=g.srid)
|
||||
if ptr:
|
||||
# Reassigning pointer, and performing post-initialization setup
|
||||
# again due to the reassignment.
|
||||
capi.destroy_geom(self.ptr)
|
||||
self.ptr = ptr
|
||||
self._post_init()
|
||||
self.srid = g.srid
|
||||
else:
|
||||
raise GEOSException("Transformed WKB was invalid.")
|
||||
|
||||
# #### Topology Routines ####
|
||||
def _topology(self, gptr):
|
||||
"Return Geometry from the given pointer."
|
||||
return GEOSGeometry(gptr, srid=self.srid)
|
||||
|
||||
@property
|
||||
def boundary(self):
|
||||
"Return the boundary as a newly allocated Geometry object."
|
||||
return self._topology(capi.geos_boundary(self.ptr))
|
||||
|
||||
def buffer(self, width, quadsegs=8):
|
||||
"""
|
||||
Return a geometry that represents all points whose distance from this
|
||||
Geometry is less than or equal to distance. Calculations are in the
|
||||
Spatial Reference System of this Geometry. The optional third parameter sets
|
||||
the number of segment used to approximate a quarter circle (defaults to 8).
|
||||
(Text from PostGIS documentation at ch. 6.1.3)
|
||||
"""
|
||||
return self._topology(capi.geos_buffer(self.ptr, width, quadsegs))
|
||||
|
||||
def buffer_with_style(
|
||||
self, width, quadsegs=8, end_cap_style=1, join_style=1, mitre_limit=5.0
|
||||
):
|
||||
"""
|
||||
Same as buffer() but allows customizing the style of the memoryview.
|
||||
|
||||
End cap style can be round (1), flat (2), or square (3).
|
||||
Join style can be round (1), mitre (2), or bevel (3).
|
||||
Mitre ratio limit only affects mitered join style.
|
||||
"""
|
||||
return self._topology(
|
||||
capi.geos_bufferwithstyle(
|
||||
self.ptr, width, quadsegs, end_cap_style, join_style, mitre_limit
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def centroid(self):
|
||||
"""
|
||||
The centroid is equal to the centroid of the set of component Geometries
|
||||
of highest dimension (since the lower-dimension geometries contribute zero
|
||||
"weight" to the centroid).
|
||||
"""
|
||||
return self._topology(capi.geos_centroid(self.ptr))
|
||||
|
||||
@property
|
||||
def convex_hull(self):
|
||||
"""
|
||||
Return the smallest convex Polygon that contains all the points
|
||||
in the Geometry.
|
||||
"""
|
||||
return self._topology(capi.geos_convexhull(self.ptr))
|
||||
|
||||
def difference(self, other):
|
||||
"""
|
||||
Return a Geometry representing the points making up this Geometry
|
||||
that do not make up other.
|
||||
"""
|
||||
return self._topology(capi.geos_difference(self.ptr, other.ptr))
|
||||
|
||||
@property
|
||||
def envelope(self):
|
||||
"Return the envelope for this geometry (a polygon)."
|
||||
return self._topology(capi.geos_envelope(self.ptr))
|
||||
|
||||
def intersection(self, other):
|
||||
"Return a Geometry representing the points shared by this Geometry and other."
|
||||
return self._topology(capi.geos_intersection(self.ptr, other.ptr))
|
||||
|
||||
@property
|
||||
def point_on_surface(self):
|
||||
"Compute an interior point of this Geometry."
|
||||
return self._topology(capi.geos_pointonsurface(self.ptr))
|
||||
|
||||
def relate(self, other):
|
||||
"Return the DE-9IM intersection matrix for this Geometry and the other."
|
||||
return capi.geos_relate(self.ptr, other.ptr).decode()
|
||||
|
||||
def simplify(self, tolerance=0.0, preserve_topology=False):
|
||||
"""
|
||||
Return the Geometry, simplified using the Douglas-Peucker algorithm
|
||||
to the specified tolerance (higher tolerance => less points). If no
|
||||
tolerance provided, defaults to 0.
|
||||
|
||||
By default, don't preserve topology - e.g. polygons can be split,
|
||||
collapse to lines or disappear holes can be created or disappear, and
|
||||
lines can cross. By specifying preserve_topology=True, the result will
|
||||
have the same dimension and number of components as the input. This is
|
||||
significantly slower.
|
||||
"""
|
||||
if preserve_topology:
|
||||
return self._topology(capi.geos_preservesimplify(self.ptr, tolerance))
|
||||
else:
|
||||
return self._topology(capi.geos_simplify(self.ptr, tolerance))
|
||||
|
||||
def sym_difference(self, other):
|
||||
"""
|
||||
Return a set combining the points in this Geometry not in other,
|
||||
and the points in other not in this Geometry.
|
||||
"""
|
||||
return self._topology(capi.geos_symdifference(self.ptr, other.ptr))
|
||||
|
||||
@property
|
||||
def unary_union(self):
|
||||
"Return the union of all the elements of this geometry."
|
||||
return self._topology(capi.geos_unary_union(self.ptr))
|
||||
|
||||
def union(self, other):
|
||||
"Return a Geometry representing all the points in this Geometry and other."
|
||||
return self._topology(capi.geos_union(self.ptr, other.ptr))
|
||||
|
||||
# #### Other Routines ####
|
||||
@property
|
||||
def area(self):
|
||||
"Return the area of the Geometry."
|
||||
return capi.geos_area(self.ptr, byref(c_double()))
|
||||
|
||||
def distance(self, other):
|
||||
"""
|
||||
Return the distance between the closest points on this Geometry
|
||||
and the other. Units will be in those of the coordinate system of
|
||||
the Geometry.
|
||||
"""
|
||||
if not isinstance(other, GEOSGeometry):
|
||||
raise TypeError("distance() works only on other GEOS Geometries.")
|
||||
return capi.geos_distance(self.ptr, other.ptr, byref(c_double()))
|
||||
|
||||
@property
|
||||
def extent(self):
|
||||
"""
|
||||
Return the extent of this geometry as a 4-tuple, consisting of
|
||||
(xmin, ymin, xmax, ymax).
|
||||
"""
|
||||
from .point import Point
|
||||
|
||||
env = self.envelope
|
||||
if isinstance(env, Point):
|
||||
xmin, ymin = env.tuple
|
||||
xmax, ymax = xmin, ymin
|
||||
else:
|
||||
xmin, ymin = env[0][0]
|
||||
xmax, ymax = env[0][2]
|
||||
return (xmin, ymin, xmax, ymax)
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
"""
|
||||
Return the length of this Geometry (e.g., 0 for point, or the
|
||||
circumference of a Polygon).
|
||||
"""
|
||||
return capi.geos_length(self.ptr, byref(c_double()))
|
||||
|
||||
def clone(self):
|
||||
"Clone this Geometry."
|
||||
return GEOSGeometry(capi.geom_clone(self.ptr))
|
||||
|
||||
|
||||
class LinearGeometryMixin:
|
||||
"""
|
||||
Used for LineString and MultiLineString.
|
||||
"""
|
||||
|
||||
def interpolate(self, distance):
|
||||
return self._topology(capi.geos_interpolate(self.ptr, distance))
|
||||
|
||||
def interpolate_normalized(self, distance):
|
||||
return self._topology(capi.geos_interpolate_normalized(self.ptr, distance))
|
||||
|
||||
def project(self, point):
|
||||
from .point import Point
|
||||
|
||||
if not isinstance(point, Point):
|
||||
raise TypeError("locate_point argument must be a Point")
|
||||
return capi.geos_project(self.ptr, point.ptr)
|
||||
|
||||
def project_normalized(self, point):
|
||||
from .point import Point
|
||||
|
||||
if not isinstance(point, Point):
|
||||
raise TypeError("locate_point argument must be a Point")
|
||||
return capi.geos_project_normalized(self.ptr, point.ptr)
|
||||
|
||||
@property
|
||||
def merged(self):
|
||||
"""
|
||||
Return the line merge of this Geometry.
|
||||
"""
|
||||
return self._topology(capi.geos_linemerge(self.ptr))
|
||||
|
||||
@property
|
||||
def closed(self):
|
||||
"""
|
||||
Return whether or not this Geometry is closed.
|
||||
"""
|
||||
return capi.geos_isclosed(self.ptr)
|
||||
|
||||
|
||||
@deconstructible
|
||||
class GEOSGeometry(GEOSGeometryBase, ListMixin):
|
||||
"A class that, generally, encapsulates a GEOS geometry."
|
||||
|
||||
def __init__(self, geo_input, srid=None):
|
||||
"""
|
||||
The base constructor for GEOS geometry objects. It may take the
|
||||
following inputs:
|
||||
|
||||
* strings:
|
||||
- WKT
|
||||
- HEXEWKB (a PostGIS-specific canonical form)
|
||||
- GeoJSON (requires GDAL)
|
||||
* memoryview:
|
||||
- WKB
|
||||
|
||||
The `srid` keyword specifies the Source Reference Identifier (SRID)
|
||||
number for this Geometry. If not provided, it defaults to None.
|
||||
"""
|
||||
input_srid = None
|
||||
if isinstance(geo_input, bytes):
|
||||
geo_input = force_str(geo_input)
|
||||
if isinstance(geo_input, str):
|
||||
wkt_m = wkt_regex.match(geo_input)
|
||||
if wkt_m:
|
||||
# Handle WKT input.
|
||||
if wkt_m["srid"]:
|
||||
input_srid = int(wkt_m["srid"])
|
||||
g = self._from_wkt(force_bytes(wkt_m["wkt"]))
|
||||
elif hex_regex.match(geo_input):
|
||||
# Handle HEXEWKB input.
|
||||
g = wkb_r().read(force_bytes(geo_input))
|
||||
elif json_regex.match(geo_input):
|
||||
# Handle GeoJSON input.
|
||||
ogr = gdal.OGRGeometry.from_json(geo_input)
|
||||
g = ogr._geos_ptr()
|
||||
input_srid = ogr.srid
|
||||
else:
|
||||
raise ValueError("String input unrecognized as WKT EWKT, and HEXEWKB.")
|
||||
elif isinstance(geo_input, GEOM_PTR):
|
||||
# When the input is a pointer to a geometry (GEOM_PTR).
|
||||
g = geo_input
|
||||
elif isinstance(geo_input, memoryview):
|
||||
# When the input is a memoryview (WKB).
|
||||
g = wkb_r().read(geo_input)
|
||||
elif isinstance(geo_input, GEOSGeometry):
|
||||
g = capi.geom_clone(geo_input.ptr)
|
||||
else:
|
||||
raise TypeError("Improper geometry input type: %s" % type(geo_input))
|
||||
|
||||
if not g:
|
||||
raise GEOSException("Could not initialize GEOS Geometry with given input.")
|
||||
|
||||
input_srid = input_srid or capi.geos_get_srid(g) or None
|
||||
if input_srid and srid and input_srid != srid:
|
||||
raise ValueError("Input geometry already has SRID: %d." % input_srid)
|
||||
|
||||
super().__init__(g, None)
|
||||
# Set the SRID, if given.
|
||||
srid = input_srid or srid
|
||||
if srid and isinstance(srid, int):
|
||||
self.srid = srid
|
@ -0,0 +1,27 @@
|
||||
"""
|
||||
Module that holds classes for performing I/O operations on GEOS geometry
|
||||
objects. Specifically, this has Python implementations of WKB/WKT
|
||||
reader and writer classes.
|
||||
"""
|
||||
from django.contrib.gis.geos.geometry import GEOSGeometry
|
||||
from django.contrib.gis.geos.prototypes.io import (
|
||||
WKBWriter,
|
||||
WKTWriter,
|
||||
_WKBReader,
|
||||
_WKTReader,
|
||||
)
|
||||
|
||||
__all__ = ["WKBWriter", "WKTWriter", "WKBReader", "WKTReader"]
|
||||
|
||||
|
||||
# Public classes for (WKB|WKT)Reader, which return GEOSGeometry
|
||||
class WKBReader(_WKBReader):
|
||||
def read(self, wkb):
|
||||
"Return a GEOSGeometry for the given WKB buffer."
|
||||
return GEOSGeometry(super().read(wkb))
|
||||
|
||||
|
||||
class WKTReader(_WKTReader):
|
||||
def read(self, wkt):
|
||||
"Return a GEOSGeometry for the given WKT string."
|
||||
return GEOSGeometry(super().read(wkt))
|
@ -0,0 +1,174 @@
|
||||
"""
|
||||
This module houses the ctypes initialization procedures, as well
|
||||
as the notice and error handler function callbacks (get called
|
||||
when an error occurs in GEOS).
|
||||
|
||||
This module also houses GEOS Pointer utilities, including
|
||||
get_pointer_arr(), and GEOM_PTR.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
from ctypes import CDLL, CFUNCTYPE, POINTER, Structure, c_char_p
|
||||
from ctypes.util import find_library
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.functional import SimpleLazyObject, cached_property
|
||||
from django.utils.version import get_version_tuple
|
||||
|
||||
logger = logging.getLogger("django.contrib.gis")
|
||||
|
||||
|
||||
def load_geos():
|
||||
# Custom library path set?
|
||||
try:
|
||||
from django.conf import settings
|
||||
|
||||
lib_path = settings.GEOS_LIBRARY_PATH
|
||||
except (AttributeError, ImportError, ImproperlyConfigured, OSError):
|
||||
lib_path = None
|
||||
|
||||
# Setting the appropriate names for the GEOS-C library.
|
||||
if lib_path:
|
||||
lib_names = None
|
||||
elif os.name == "nt":
|
||||
# Windows NT libraries
|
||||
lib_names = ["geos_c", "libgeos_c-1"]
|
||||
elif os.name == "posix":
|
||||
# *NIX libraries
|
||||
lib_names = ["geos_c", "GEOS"]
|
||||
else:
|
||||
raise ImportError('Unsupported OS "%s"' % os.name)
|
||||
|
||||
# Using the ctypes `find_library` utility to find the path to the GEOS
|
||||
# shared library. This is better than manually specifying each library name
|
||||
# and extension (e.g., libgeos_c.[so|so.1|dylib].).
|
||||
if lib_names:
|
||||
for lib_name in lib_names:
|
||||
lib_path = find_library(lib_name)
|
||||
if lib_path is not None:
|
||||
break
|
||||
|
||||
# No GEOS library could be found.
|
||||
if lib_path is None:
|
||||
raise ImportError(
|
||||
'Could not find the GEOS library (tried "%s"). '
|
||||
"Try setting GEOS_LIBRARY_PATH in your settings." % '", "'.join(lib_names)
|
||||
)
|
||||
# Getting the GEOS C library. The C interface (CDLL) is used for
|
||||
# both *NIX and Windows.
|
||||
# See the GEOS C API source code for more details on the library function calls:
|
||||
# https://libgeos.org/doxygen/geos__c_8h_source.html
|
||||
_lgeos = CDLL(lib_path)
|
||||
# Here we set up the prototypes for the initGEOS_r and finishGEOS_r
|
||||
# routines. These functions aren't actually called until they are
|
||||
# attached to a GEOS context handle -- this actually occurs in
|
||||
# geos/prototypes/threadsafe.py.
|
||||
_lgeos.initGEOS_r.restype = CONTEXT_PTR
|
||||
_lgeos.finishGEOS_r.argtypes = [CONTEXT_PTR]
|
||||
# Set restype for compatibility across 32 and 64-bit platforms.
|
||||
_lgeos.GEOSversion.restype = c_char_p
|
||||
return _lgeos
|
||||
|
||||
|
||||
# The notice and error handler C function callback definitions.
|
||||
# Supposed to mimic the GEOS message handler (C below):
|
||||
# typedef void (*GEOSMessageHandler)(const char *fmt, ...);
|
||||
NOTICEFUNC = CFUNCTYPE(None, c_char_p, c_char_p)
|
||||
|
||||
|
||||
def notice_h(fmt, lst):
|
||||
fmt, lst = fmt.decode(), lst.decode()
|
||||
try:
|
||||
warn_msg = fmt % lst
|
||||
except TypeError:
|
||||
warn_msg = fmt
|
||||
logger.warning("GEOS_NOTICE: %s\n", warn_msg)
|
||||
|
||||
|
||||
notice_h = NOTICEFUNC(notice_h)
|
||||
|
||||
ERRORFUNC = CFUNCTYPE(None, c_char_p, c_char_p)
|
||||
|
||||
|
||||
def error_h(fmt, lst):
|
||||
fmt, lst = fmt.decode(), lst.decode()
|
||||
try:
|
||||
err_msg = fmt % lst
|
||||
except TypeError:
|
||||
err_msg = fmt
|
||||
logger.error("GEOS_ERROR: %s\n", err_msg)
|
||||
|
||||
|
||||
error_h = ERRORFUNC(error_h)
|
||||
|
||||
# #### GEOS Geometry C data structures, and utility functions. ####
|
||||
|
||||
|
||||
# Opaque GEOS geometry structures, used for GEOM_PTR and CS_PTR
|
||||
class GEOSGeom_t(Structure):
|
||||
pass
|
||||
|
||||
|
||||
class GEOSPrepGeom_t(Structure):
|
||||
pass
|
||||
|
||||
|
||||
class GEOSCoordSeq_t(Structure):
|
||||
pass
|
||||
|
||||
|
||||
class GEOSContextHandle_t(Structure):
|
||||
pass
|
||||
|
||||
|
||||
# Pointers to opaque GEOS geometry structures.
|
||||
GEOM_PTR = POINTER(GEOSGeom_t)
|
||||
PREPGEOM_PTR = POINTER(GEOSPrepGeom_t)
|
||||
CS_PTR = POINTER(GEOSCoordSeq_t)
|
||||
CONTEXT_PTR = POINTER(GEOSContextHandle_t)
|
||||
|
||||
|
||||
lgeos = SimpleLazyObject(load_geos)
|
||||
|
||||
|
||||
class GEOSFuncFactory:
|
||||
"""
|
||||
Lazy loading of GEOS functions.
|
||||
"""
|
||||
|
||||
argtypes = None
|
||||
restype = None
|
||||
errcheck = None
|
||||
|
||||
def __init__(self, func_name, *, restype=None, errcheck=None, argtypes=None):
|
||||
self.func_name = func_name
|
||||
if restype is not None:
|
||||
self.restype = restype
|
||||
if errcheck is not None:
|
||||
self.errcheck = errcheck
|
||||
if argtypes is not None:
|
||||
self.argtypes = argtypes
|
||||
|
||||
def __call__(self, *args):
|
||||
return self.func(*args)
|
||||
|
||||
@cached_property
|
||||
def func(self):
|
||||
from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc
|
||||
|
||||
func = GEOSFunc(self.func_name)
|
||||
func.argtypes = self.argtypes or []
|
||||
func.restype = self.restype
|
||||
if self.errcheck:
|
||||
func.errcheck = self.errcheck
|
||||
return func
|
||||
|
||||
|
||||
def geos_version():
|
||||
"""Return the string version of the GEOS library."""
|
||||
return lgeos.GEOSversion()
|
||||
|
||||
|
||||
def geos_version_tuple():
|
||||
"""Return the GEOS version as a tuple (major, minor, subminor)."""
|
||||
return get_version_tuple(geos_version().decode())
|
@ -0,0 +1,193 @@
|
||||
from django.contrib.gis.geos import prototypes as capi
|
||||
from django.contrib.gis.geos.coordseq import GEOSCoordSeq
|
||||
from django.contrib.gis.geos.error import GEOSException
|
||||
from django.contrib.gis.geos.geometry import GEOSGeometry, LinearGeometryMixin
|
||||
from django.contrib.gis.geos.point import Point
|
||||
from django.contrib.gis.shortcuts import numpy
|
||||
|
||||
|
||||
class LineString(LinearGeometryMixin, GEOSGeometry):
|
||||
_init_func = capi.create_linestring
|
||||
_minlength = 2
|
||||
has_cs = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Initialize on the given sequence -- may take lists, tuples, NumPy arrays
|
||||
of X,Y pairs, or Point objects. If Point objects are used, ownership is
|
||||
_not_ transferred to the LineString object.
|
||||
|
||||
Examples:
|
||||
ls = LineString((1, 1), (2, 2))
|
||||
ls = LineString([(1, 1), (2, 2)])
|
||||
ls = LineString(array([(1, 1), (2, 2)]))
|
||||
ls = LineString(Point(1, 1), Point(2, 2))
|
||||
"""
|
||||
# If only one argument provided, set the coords array appropriately
|
||||
if len(args) == 1:
|
||||
coords = args[0]
|
||||
else:
|
||||
coords = args
|
||||
|
||||
if not (
|
||||
isinstance(coords, (tuple, list))
|
||||
or numpy
|
||||
and isinstance(coords, numpy.ndarray)
|
||||
):
|
||||
raise TypeError("Invalid initialization input for LineStrings.")
|
||||
|
||||
# If SRID was passed in with the keyword arguments
|
||||
srid = kwargs.get("srid")
|
||||
|
||||
ncoords = len(coords)
|
||||
if not ncoords:
|
||||
super().__init__(self._init_func(None), srid=srid)
|
||||
return
|
||||
|
||||
if ncoords < self._minlength:
|
||||
raise ValueError(
|
||||
"%s requires at least %d points, got %s."
|
||||
% (
|
||||
self.__class__.__name__,
|
||||
self._minlength,
|
||||
ncoords,
|
||||
)
|
||||
)
|
||||
|
||||
numpy_coords = not isinstance(coords, (tuple, list))
|
||||
if numpy_coords:
|
||||
shape = coords.shape # Using numpy's shape.
|
||||
if len(shape) != 2:
|
||||
raise TypeError("Too many dimensions.")
|
||||
self._checkdim(shape[1])
|
||||
ndim = shape[1]
|
||||
else:
|
||||
# Getting the number of coords and the number of dimensions -- which
|
||||
# must stay the same, e.g., no LineString((1, 2), (1, 2, 3)).
|
||||
ndim = None
|
||||
# Incrementing through each of the coordinates and verifying
|
||||
for coord in coords:
|
||||
if not isinstance(coord, (tuple, list, Point)):
|
||||
raise TypeError(
|
||||
"Each coordinate should be a sequence (list or tuple)"
|
||||
)
|
||||
|
||||
if ndim is None:
|
||||
ndim = len(coord)
|
||||
self._checkdim(ndim)
|
||||
elif len(coord) != ndim:
|
||||
raise TypeError("Dimension mismatch.")
|
||||
|
||||
# Creating a coordinate sequence object because it is easier to
|
||||
# set the points using its methods.
|
||||
cs = GEOSCoordSeq(capi.create_cs(ncoords, ndim), z=bool(ndim == 3))
|
||||
point_setter = cs._set_point_3d if ndim == 3 else cs._set_point_2d
|
||||
|
||||
for i in range(ncoords):
|
||||
if numpy_coords:
|
||||
point_coords = coords[i, :]
|
||||
elif isinstance(coords[i], Point):
|
||||
point_coords = coords[i].tuple
|
||||
else:
|
||||
point_coords = coords[i]
|
||||
point_setter(i, point_coords)
|
||||
|
||||
# Calling the base geometry initialization with the returned pointer
|
||||
# from the function.
|
||||
super().__init__(self._init_func(cs.ptr), srid=srid)
|
||||
|
||||
def __iter__(self):
|
||||
"Allow iteration over this LineString."
|
||||
for i in range(len(self)):
|
||||
yield self[i]
|
||||
|
||||
def __len__(self):
|
||||
"Return the number of points in this LineString."
|
||||
return len(self._cs)
|
||||
|
||||
def _get_single_external(self, index):
|
||||
return self._cs[index]
|
||||
|
||||
_get_single_internal = _get_single_external
|
||||
|
||||
def _set_list(self, length, items):
|
||||
ndim = self._cs.dims
|
||||
hasz = self._cs.hasz # I don't understand why these are different
|
||||
srid = self.srid
|
||||
|
||||
# create a new coordinate sequence and populate accordingly
|
||||
cs = GEOSCoordSeq(capi.create_cs(length, ndim), z=hasz)
|
||||
for i, c in enumerate(items):
|
||||
cs[i] = c
|
||||
|
||||
ptr = self._init_func(cs.ptr)
|
||||
if ptr:
|
||||
capi.destroy_geom(self.ptr)
|
||||
self.ptr = ptr
|
||||
if srid is not None:
|
||||
self.srid = srid
|
||||
self._post_init()
|
||||
else:
|
||||
# can this happen?
|
||||
raise GEOSException("Geometry resulting from slice deletion was invalid.")
|
||||
|
||||
def _set_single(self, index, value):
|
||||
self._cs[index] = value
|
||||
|
||||
def _checkdim(self, dim):
|
||||
if dim not in (2, 3):
|
||||
raise TypeError("Dimension mismatch.")
|
||||
|
||||
# #### Sequence Properties ####
|
||||
@property
|
||||
def tuple(self):
|
||||
"Return a tuple version of the geometry from the coordinate sequence."
|
||||
return self._cs.tuple
|
||||
|
||||
coords = tuple
|
||||
|
||||
def _listarr(self, func):
|
||||
"""
|
||||
Return a sequence (list) corresponding with the given function.
|
||||
Return a numpy array if possible.
|
||||
"""
|
||||
lst = [func(i) for i in range(len(self))]
|
||||
if numpy:
|
||||
return numpy.array(lst) # ARRRR!
|
||||
else:
|
||||
return lst
|
||||
|
||||
@property
|
||||
def array(self):
|
||||
"Return a numpy array for the LineString."
|
||||
return self._listarr(self._cs.__getitem__)
|
||||
|
||||
@property
|
||||
def x(self):
|
||||
"Return a list or numpy array of the X variable."
|
||||
return self._listarr(self._cs.getX)
|
||||
|
||||
@property
|
||||
def y(self):
|
||||
"Return a list or numpy array of the Y variable."
|
||||
return self._listarr(self._cs.getY)
|
||||
|
||||
@property
|
||||
def z(self):
|
||||
"Return a list or numpy array of the Z variable."
|
||||
if not self.hasz:
|
||||
return None
|
||||
else:
|
||||
return self._listarr(self._cs.getZ)
|
||||
|
||||
|
||||
# LinearRings are LineStrings used within Polygons.
|
||||
class LinearRing(LineString):
|
||||
_minlength = 4
|
||||
_init_func = capi.create_linearring
|
||||
|
||||
@property
|
||||
def is_counterclockwise(self):
|
||||
if self.empty:
|
||||
raise ValueError("Orientation of an empty LinearRing cannot be determined.")
|
||||
return self._cs.is_counterclockwise
|
@ -0,0 +1,314 @@
|
||||
# Copyright (c) 2008-2009 Aryeh Leib Taurog, all rights reserved.
|
||||
# Released under the New BSD license.
|
||||
"""
|
||||
This module contains a base type which provides list-style mutations
|
||||
without specific data storage methods.
|
||||
|
||||
See also http://static.aryehleib.com/oldsite/MutableLists.html
|
||||
|
||||
Author: Aryeh Leib Taurog.
|
||||
"""
|
||||
from functools import total_ordering
|
||||
|
||||
|
||||
@total_ordering
|
||||
class ListMixin:
|
||||
"""
|
||||
A base class which provides complete list interface.
|
||||
Derived classes must call ListMixin's __init__() function
|
||||
and implement the following:
|
||||
|
||||
function _get_single_external(self, i):
|
||||
Return single item with index i for general use.
|
||||
The index i will always satisfy 0 <= i < len(self).
|
||||
|
||||
function _get_single_internal(self, i):
|
||||
Same as above, but for use within the class [Optional]
|
||||
Note that if _get_single_internal and _get_single_internal return
|
||||
different types of objects, _set_list must distinguish
|
||||
between the two and handle each appropriately.
|
||||
|
||||
function _set_list(self, length, items):
|
||||
Recreate the entire object.
|
||||
|
||||
NOTE: items may be a generator which calls _get_single_internal.
|
||||
Therefore, it is necessary to cache the values in a temporary:
|
||||
temp = list(items)
|
||||
before clobbering the original storage.
|
||||
|
||||
function _set_single(self, i, value):
|
||||
Set the single item at index i to value [Optional]
|
||||
If left undefined, all mutations will result in rebuilding
|
||||
the object using _set_list.
|
||||
|
||||
function __len__(self):
|
||||
Return the length
|
||||
|
||||
int _minlength:
|
||||
The minimum legal length [Optional]
|
||||
|
||||
int _maxlength:
|
||||
The maximum legal length [Optional]
|
||||
|
||||
type or tuple _allowed:
|
||||
A type or tuple of allowed item types [Optional]
|
||||
"""
|
||||
|
||||
_minlength = 0
|
||||
_maxlength = None
|
||||
|
||||
# ### Python initialization and special list interface methods ###
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if not hasattr(self, "_get_single_internal"):
|
||||
self._get_single_internal = self._get_single_external
|
||||
|
||||
if not hasattr(self, "_set_single"):
|
||||
self._set_single = self._set_single_rebuild
|
||||
self._assign_extended_slice = self._assign_extended_slice_rebuild
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __getitem__(self, index):
|
||||
"Get the item(s) at the specified index/slice."
|
||||
if isinstance(index, slice):
|
||||
return [
|
||||
self._get_single_external(i) for i in range(*index.indices(len(self)))
|
||||
]
|
||||
else:
|
||||
index = self._checkindex(index)
|
||||
return self._get_single_external(index)
|
||||
|
||||
def __delitem__(self, index):
|
||||
"Delete the item(s) at the specified index/slice."
|
||||
if not isinstance(index, (int, slice)):
|
||||
raise TypeError("%s is not a legal index" % index)
|
||||
|
||||
# calculate new length and dimensions
|
||||
origLen = len(self)
|
||||
if isinstance(index, int):
|
||||
index = self._checkindex(index)
|
||||
indexRange = [index]
|
||||
else:
|
||||
indexRange = range(*index.indices(origLen))
|
||||
|
||||
newLen = origLen - len(indexRange)
|
||||
newItems = (
|
||||
self._get_single_internal(i) for i in range(origLen) if i not in indexRange
|
||||
)
|
||||
|
||||
self._rebuild(newLen, newItems)
|
||||
|
||||
def __setitem__(self, index, val):
|
||||
"Set the item(s) at the specified index/slice."
|
||||
if isinstance(index, slice):
|
||||
self._set_slice(index, val)
|
||||
else:
|
||||
index = self._checkindex(index)
|
||||
self._check_allowed((val,))
|
||||
self._set_single(index, val)
|
||||
|
||||
# ### Special methods for arithmetic operations ###
|
||||
def __add__(self, other):
|
||||
"add another list-like object"
|
||||
return self.__class__([*self, *other])
|
||||
|
||||
def __radd__(self, other):
|
||||
"add to another list-like object"
|
||||
return other.__class__([*other, *self])
|
||||
|
||||
def __iadd__(self, other):
|
||||
"add another list-like object to self"
|
||||
self.extend(other)
|
||||
return self
|
||||
|
||||
def __mul__(self, n):
|
||||
"multiply"
|
||||
return self.__class__(list(self) * n)
|
||||
|
||||
def __rmul__(self, n):
|
||||
"multiply"
|
||||
return self.__class__(list(self) * n)
|
||||
|
||||
def __imul__(self, n):
|
||||
"multiply"
|
||||
if n <= 0:
|
||||
del self[:]
|
||||
else:
|
||||
cache = list(self)
|
||||
for i in range(n - 1):
|
||||
self.extend(cache)
|
||||
return self
|
||||
|
||||
def __eq__(self, other):
|
||||
olen = len(other)
|
||||
for i in range(olen):
|
||||
try:
|
||||
c = self[i] == other[i]
|
||||
except IndexError:
|
||||
# self must be shorter
|
||||
return False
|
||||
if not c:
|
||||
return False
|
||||
return len(self) == olen
|
||||
|
||||
def __lt__(self, other):
|
||||
olen = len(other)
|
||||
for i in range(olen):
|
||||
try:
|
||||
c = self[i] < other[i]
|
||||
except IndexError:
|
||||
# self must be shorter
|
||||
return True
|
||||
if c:
|
||||
return c
|
||||
elif other[i] < self[i]:
|
||||
return False
|
||||
return len(self) < olen
|
||||
|
||||
# ### Public list interface Methods ###
|
||||
# ## Non-mutating ##
|
||||
def count(self, val):
|
||||
"Standard list count method"
|
||||
count = 0
|
||||
for i in self:
|
||||
if val == i:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
def index(self, val):
|
||||
"Standard list index method"
|
||||
for i in range(0, len(self)):
|
||||
if self[i] == val:
|
||||
return i
|
||||
raise ValueError("%s not found in object" % val)
|
||||
|
||||
# ## Mutating ##
|
||||
def append(self, val):
|
||||
"Standard list append method"
|
||||
self[len(self) :] = [val]
|
||||
|
||||
def extend(self, vals):
|
||||
"Standard list extend method"
|
||||
self[len(self) :] = vals
|
||||
|
||||
def insert(self, index, val):
|
||||
"Standard list insert method"
|
||||
if not isinstance(index, int):
|
||||
raise TypeError("%s is not a legal index" % index)
|
||||
self[index:index] = [val]
|
||||
|
||||
def pop(self, index=-1):
|
||||
"Standard list pop method"
|
||||
result = self[index]
|
||||
del self[index]
|
||||
return result
|
||||
|
||||
def remove(self, val):
|
||||
"Standard list remove method"
|
||||
del self[self.index(val)]
|
||||
|
||||
def reverse(self):
|
||||
"Standard list reverse method"
|
||||
self[:] = self[-1::-1]
|
||||
|
||||
def sort(self, key=None, reverse=False):
|
||||
"Standard list sort method"
|
||||
self[:] = sorted(self, key=key, reverse=reverse)
|
||||
|
||||
# ### Private routines ###
|
||||
def _rebuild(self, newLen, newItems):
|
||||
if newLen and newLen < self._minlength:
|
||||
raise ValueError("Must have at least %d items" % self._minlength)
|
||||
if self._maxlength is not None and newLen > self._maxlength:
|
||||
raise ValueError("Cannot have more than %d items" % self._maxlength)
|
||||
|
||||
self._set_list(newLen, newItems)
|
||||
|
||||
def _set_single_rebuild(self, index, value):
|
||||
self._set_slice(slice(index, index + 1, 1), [value])
|
||||
|
||||
def _checkindex(self, index):
|
||||
length = len(self)
|
||||
if 0 <= index < length:
|
||||
return index
|
||||
if -length <= index < 0:
|
||||
return index + length
|
||||
raise IndexError("invalid index: %s" % index)
|
||||
|
||||
def _check_allowed(self, items):
|
||||
if hasattr(self, "_allowed"):
|
||||
if False in [isinstance(val, self._allowed) for val in items]:
|
||||
raise TypeError("Invalid type encountered in the arguments.")
|
||||
|
||||
def _set_slice(self, index, values):
|
||||
"Assign values to a slice of the object"
|
||||
try:
|
||||
valueList = list(values)
|
||||
except TypeError:
|
||||
raise TypeError("can only assign an iterable to a slice")
|
||||
|
||||
self._check_allowed(valueList)
|
||||
|
||||
origLen = len(self)
|
||||
start, stop, step = index.indices(origLen)
|
||||
|
||||
# CAREFUL: index.step and step are not the same!
|
||||
# step will never be None
|
||||
if index.step is None:
|
||||
self._assign_simple_slice(start, stop, valueList)
|
||||
else:
|
||||
self._assign_extended_slice(start, stop, step, valueList)
|
||||
|
||||
def _assign_extended_slice_rebuild(self, start, stop, step, valueList):
|
||||
"Assign an extended slice by rebuilding entire list"
|
||||
indexList = range(start, stop, step)
|
||||
# extended slice, only allow assigning slice of same size
|
||||
if len(valueList) != len(indexList):
|
||||
raise ValueError(
|
||||
"attempt to assign sequence of size %d "
|
||||
"to extended slice of size %d" % (len(valueList), len(indexList))
|
||||
)
|
||||
|
||||
# we're not changing the length of the sequence
|
||||
newLen = len(self)
|
||||
newVals = dict(zip(indexList, valueList))
|
||||
|
||||
def newItems():
|
||||
for i in range(newLen):
|
||||
if i in newVals:
|
||||
yield newVals[i]
|
||||
else:
|
||||
yield self._get_single_internal(i)
|
||||
|
||||
self._rebuild(newLen, newItems())
|
||||
|
||||
def _assign_extended_slice(self, start, stop, step, valueList):
|
||||
"Assign an extended slice by re-assigning individual items"
|
||||
indexList = range(start, stop, step)
|
||||
# extended slice, only allow assigning slice of same size
|
||||
if len(valueList) != len(indexList):
|
||||
raise ValueError(
|
||||
"attempt to assign sequence of size %d "
|
||||
"to extended slice of size %d" % (len(valueList), len(indexList))
|
||||
)
|
||||
|
||||
for i, val in zip(indexList, valueList):
|
||||
self._set_single(i, val)
|
||||
|
||||
def _assign_simple_slice(self, start, stop, valueList):
|
||||
"Assign a simple slice; Can assign slice of any length"
|
||||
origLen = len(self)
|
||||
stop = max(start, stop)
|
||||
newLen = origLen - stop + start + len(valueList)
|
||||
|
||||
def newItems():
|
||||
for i in range(origLen + 1):
|
||||
if i == start:
|
||||
yield from valueList
|
||||
|
||||
if i < origLen:
|
||||
if i < start or i >= stop:
|
||||
yield self._get_single_internal(i)
|
||||
|
||||
self._rebuild(newLen, newItems())
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user