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