diff --git a/django/contrib/admin/media/img/gis/move_vertex_off.png b/django/contrib/admin/media/img/gis/move_vertex_off.png new file mode 100644 index 0000000000..296b2e29c9 Binary files /dev/null and b/django/contrib/admin/media/img/gis/move_vertex_off.png differ diff --git a/django/contrib/admin/media/img/gis/move_vertex_on.png b/django/contrib/admin/media/img/gis/move_vertex_on.png new file mode 100644 index 0000000000..21f4758d9c Binary files /dev/null and b/django/contrib/admin/media/img/gis/move_vertex_on.png differ diff --git a/django/contrib/gis/__init__.py b/django/contrib/gis/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/gis/admin/__init__.py b/django/contrib/gis/admin/__init__.py new file mode 100644 index 0000000000..20315c5c79 --- /dev/null +++ b/django/contrib/gis/admin/__init__.py @@ -0,0 +1,12 @@ +# Getting the normal admin routines, classes, and `site` instance. +from django.contrib.admin import autodiscover, site, StackedInline, TabularInline, HORIZONTAL, VERTICAL + +# Geographic admin options classes and widgets. +from django.contrib.gis.admin.options import GeoModelAdmin +from django.contrib.gis.admin.widgets import OpenLayersWidget + +try: + from django.contrib.gis.admin.options import OSMGeoAdmin + HAS_OSM = True +except ImportError: + HAS_OSM = False diff --git a/django/contrib/gis/admin/options.py b/django/contrib/gis/admin/options.py new file mode 100644 index 0000000000..71fb87bf09 --- /dev/null +++ b/django/contrib/gis/admin/options.py @@ -0,0 +1,128 @@ +from django.conf import settings +from django.contrib.admin import ModelAdmin +from django.contrib.gis.admin.widgets import OpenLayersWidget +from django.contrib.gis.gdal import OGRGeomType +from django.contrib.gis.db import models + +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 + admin_media_prefix = settings.ADMIN_MEDIA_PREFIX + map_width = 600 + map_height = 400 + map_srid = 4326 + map_template = 'gis/admin/openlayers.html' + openlayers_url = 'http://openlayers.org/api/2.6/OpenLayers.js' + wms_url = 'http://labs.metacarta.com/wms/vmap0' + wms_layer = 'basic' + wms_name = 'OpenLayers WMS' + debug = False + widget = OpenLayersWidget + + def _media(self): + "Injects OpenLayers JavaScript into the admin." + media = super(GeoModelAdmin, self)._media() + media.add_js([self.openlayers_url]) + media.add_js(self.extra_js) + return media + media = property(_media) + + def formfield_for_dbfield(self, db_field, **kwargs): + """ + Overloaded from ModelAdmin so that an OpenLayersWidget is used + for viewing/editing GeometryFields. + """ + if isinstance(db_field, models.GeometryField): + # Setting the widget with the newly defined widget. + kwargs['widget'] = self.get_map_widget(db_field) + return db_field.formfield(**kwargs) + else: + return super(GeoModelAdmin, self).formfield_for_dbfield(db_field, **kwargs) + + def get_map_widget(self, db_field): + """ + Returns 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 in ('MULTIPOINT', 'MULTILINESTRING', 'MULTIPOLYGON', 'GEOMETRYCOLLECTION') + if is_collection: + if db_field._geom == 'GEOMETRYCOLLECTION': collection_type = 'Any' + else: collection_type = OGRGeomType(db_field._geom.replace('MULTI', '')) + else: + collection_type = 'None' + + class OLMap(self.widget): + template = self.map_template + geom_type = db_field._geom + params = {'admin_media_prefix' : self.admin_media_prefix, + '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), + 'field_name' : db_field.name, + 'is_collection' : is_collection, + 'scrollable' : self.scrollable, + 'layerswitcher' : self.layerswitcher, + 'collection_type' : collection_type, + 'is_linestring' : db_field._geom in ('LINESTRING', 'MULTILINESTRING'), + 'is_polygon' : db_field._geom in ('POLYGON', 'MULTIPOLYGON'), + 'is_point' : db_field._geom in ('POINT', 'MULTIPOINT'), + 'num_zoom' : self.num_zoom, + 'max_zoom' : self.max_zoom, + 'min_zoom' : self.min_zoom, + 'units' : self.units, #likely shoud 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, + 'srid' : self.map_srid, + 'display_srid' : self.display_srid, + 'wms_url' : self.wms_url, + 'wms_layer' : self.wms_layer, + 'wms_name' : self.wms_name, + 'debug' : self.debug, + } + return OLMap + +# Using the Beta OSM in the admin requires the following: +# (1) The Google Maps Mercator projection needs to be added +# to your `spatial_ref_sys` table. You'll need at least GDAL 1.5: +# >>> from django.contrib.gis.gdal import SpatialReference +# >>> from django.contrib.gis.utils import add_postgis_srs +# >>> add_postgis_srs(SpatialReference(900913)) # Adding the Google Projection +from django.contrib.gis import gdal +if gdal.HAS_GDAL: + class OSMGeoAdmin(GeoModelAdmin): + map_template = 'gis/admin/osm.html' + extra_js = ['http://openstreetmap.org/openlayers/OpenStreetMap.js'] + num_zoom = 20 + map_srid = 900913 + max_extent = '-20037508,-20037508,20037508,20037508' + max_resolution = 156543.0339 + units = 'm' diff --git a/django/contrib/gis/admin/widgets.py b/django/contrib/gis/admin/widgets.py new file mode 100644 index 0000000000..27abc8f59b --- /dev/null +++ b/django/contrib/gis/admin/widgets.py @@ -0,0 +1,92 @@ +from django.contrib.gis.gdal import OGRException +from django.contrib.gis.geos import GEOSGeometry, GEOSException +from django.forms.widgets import Textarea +from django.template.loader import render_to_string + +class OpenLayersWidget(Textarea): + """ + Renders an OpenLayers map using the WKT of the geometry. + """ + def render(self, name, value, attrs=None): + # Update the template parameters with any attributes passed in. + if attrs: self.params.update(attrs) + + # Defaulting the WKT value to a blank string -- this + # will be tested in the JavaScript and the appropriate + # interfaace will be constructed. + self.params['wkt'] = '' + + # If a string reaches here (via a validation error on another + # field) then just reconstruct the Geometry. + if isinstance(value, basestring): + try: + value = GEOSGeometry(value) + except (GEOSException, ValueError): + value = None + + if value and value.geom_type.upper() != self.geom_type: + value = None + + # Constructing the dictionary of the map options. + self.params['map_options'] = self.map_options() + + # Constructing the JavaScript module name using the ID of + # the GeometryField (passed in via the `attrs` keyword). + self.params['module'] = 'geodjango_%s' % self.params['field_name'] + + if value: + # Transforming the geometry to the projection used on the + # OpenLayers map. + srid = self.params['srid'] + if value.srid != srid: + try: + value.transform(srid) + wkt = value.wkt + except OGRException: + wkt = '' + else: + wkt = value.wkt + + # Setting the parameter WKT with that of the transformed + # geometry. + self.params['wkt'] = wkt + + return render_to_string(self.template, self.params) + + def map_options(self): + "Builds 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)' % str(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 diff --git a/django/contrib/gis/db/__init__.py b/django/contrib/gis/db/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/gis/db/backend/__init__.py b/django/contrib/gis/db/backend/__init__.py new file mode 100644 index 0000000000..172c1268a7 --- /dev/null +++ b/django/contrib/gis/db/backend/__init__.py @@ -0,0 +1,18 @@ +""" + This module provides the backend for spatial SQL construction with Django. + + Specifically, this module will import the correct routines and modules + needed for GeoDjango to interface with the spatial database. +""" +from django.conf import settings +from django.contrib.gis.db.backend.util import gqn + +# Retrieving the necessary settings from the backend. +if settings.DATABASE_ENGINE == 'postgresql_psycopg2': + from django.contrib.gis.db.backend.postgis import create_spatial_db, get_geo_where_clause, SpatialBackend +elif settings.DATABASE_ENGINE == 'oracle': + from django.contrib.gis.db.backend.oracle import create_spatial_db, get_geo_where_clause, SpatialBackend +elif settings.DATABASE_ENGINE == 'mysql': + from django.contrib.gis.db.backend.mysql import create_spatial_db, get_geo_where_clause, SpatialBackend +else: + raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE) diff --git a/django/contrib/gis/db/backend/adaptor.py b/django/contrib/gis/db/backend/adaptor.py new file mode 100644 index 0000000000..b2397e61dd --- /dev/null +++ b/django/contrib/gis/db/backend/adaptor.py @@ -0,0 +1,14 @@ +class WKTAdaptor(object): + """ + This provides 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 self.wkt == other.wkt and self.srid == other.srid + + def __str__(self): + return self.wkt diff --git a/django/contrib/gis/db/backend/base.py b/django/contrib/gis/db/backend/base.py new file mode 100644 index 0000000000..d45ac7b6f1 --- /dev/null +++ b/django/contrib/gis/db/backend/base.py @@ -0,0 +1,29 @@ +""" + This module holds the base `SpatialBackend` object, which is + instantiated by each spatial backend with the features it has. +""" +# TODO: Create a `Geometry` protocol and allow user to use +# different Geometry objects -- for now we just use GEOSGeometry. +from django.contrib.gis.geos import GEOSGeometry, GEOSException + +class BaseSpatialBackend(object): + Geometry = GEOSGeometry + GeometryException = GEOSException + + def __init__(self, **kwargs): + kwargs.setdefault('distance_functions', {}) + kwargs.setdefault('limited_where', {}) + for k, v in kwargs.iteritems(): setattr(self, k, v) + + def __getattr__(self, name): + """ + All attributes of the spatial backend return False by default. + """ + try: + return self.__dict__[name] + except KeyError: + return False + + + + diff --git a/django/contrib/gis/db/backend/mysql/__init__.py b/django/contrib/gis/db/backend/mysql/__init__.py new file mode 100644 index 0000000000..0484e5f9b2 --- /dev/null +++ b/django/contrib/gis/db/backend/mysql/__init__.py @@ -0,0 +1,13 @@ +__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend'] + +from django.contrib.gis.db.backend.base import BaseSpatialBackend +from django.contrib.gis.db.backend.adaptor import WKTAdaptor +from django.contrib.gis.db.backend.mysql.creation import create_spatial_db +from django.contrib.gis.db.backend.mysql.field import MySQLGeoField +from django.contrib.gis.db.backend.mysql.query import * + +SpatialBackend = BaseSpatialBackend(name='mysql', mysql=True, + gis_terms=MYSQL_GIS_TERMS, + select=GEOM_SELECT, + Adaptor=WKTAdaptor, + Field=MySQLGeoField) diff --git a/django/contrib/gis/db/backend/mysql/creation.py b/django/contrib/gis/db/backend/mysql/creation.py new file mode 100644 index 0000000000..e8f471df81 --- /dev/null +++ b/django/contrib/gis/db/backend/mysql/creation.py @@ -0,0 +1,5 @@ +from django.test.utils import create_test_db + +def create_spatial_db(test=True, verbosity=1, autoclobber=False): + if not test: raise NotImplementedError('This uses `create_test_db` from test/utils.py') + create_test_db(verbosity, autoclobber) diff --git a/django/contrib/gis/db/backend/mysql/field.py b/django/contrib/gis/db/backend/mysql/field.py new file mode 100644 index 0000000000..f3151f93ff --- /dev/null +++ b/django/contrib/gis/db/backend/mysql/field.py @@ -0,0 +1,53 @@ +from django.db import connection +from django.db.models.fields import Field # Django base Field class +from django.contrib.gis.db.backend.mysql.query import GEOM_FROM_TEXT + +# Quotename & geographic quotename, respectively. +qn = connection.ops.quote_name + +class MySQLGeoField(Field): + """ + The backend-specific geographic field for MySQL. + """ + + def _geom_index(self, style, db_table): + """ + Creates a spatial index for the geometry column. If MyISAM tables are + used an R-Tree index is created, otherwise a B-Tree index is created. + Thus, for best spatial performance, you should use MyISAM tables + (which do not support transactions). For more information, see Ch. + 16.6.1 of the MySQL 5.0 documentation. + """ + + # Getting the index name. + idx_name = '%s_%s_id' % (db_table, self.column) + + sql = style.SQL_KEYWORD('CREATE SPATIAL INDEX ') + \ + style.SQL_TABLE(qn(idx_name)) + \ + style.SQL_KEYWORD(' ON ') + \ + style.SQL_TABLE(qn(db_table)) + '(' + \ + style.SQL_FIELD(qn(self.column)) + ');' + return sql + + def _post_create_sql(self, style, db_table): + """ + Returns SQL that will be executed after the model has been + created. + """ + # Getting the geometric index for this Geometry column. + if self._index: + return (self._geom_index(style, db_table),) + else: + return () + + def db_type(self): + "The OpenGIS name is returned for the MySQL database column type." + return self._geom + + def get_placeholder(self, value): + """ + The placeholder here has to include MySQL's WKT constructor. Because + MySQL does not support spatial transformations, there is no need to + modify the placeholder based on the contents of the given value. + """ + return '%s(%%s)' % GEOM_FROM_TEXT diff --git a/django/contrib/gis/db/backend/mysql/query.py b/django/contrib/gis/db/backend/mysql/query.py new file mode 100644 index 0000000000..6522235135 --- /dev/null +++ b/django/contrib/gis/db/backend/mysql/query.py @@ -0,0 +1,59 @@ +""" + This module contains the spatial lookup types, and the `get_geo_where_clause` + routine for MySQL. + + Please note that MySQL only supports bounding box queries, also + known as MBRs (Minimum Bounding Rectangles). Moreover, spatial + indices may only be used on MyISAM tables -- if you need + transactions, take a look at PostGIS. +""" +from django.db import connection +qn = connection.ops.quote_name + +# To ease implementation, WKT is passed to/from MySQL. +GEOM_FROM_TEXT = 'GeomFromText' +GEOM_FROM_WKB = 'GeomFromWKB' +GEOM_SELECT = 'AsText(%s)' + +# WARNING: MySQL is NOT compliant w/the OpenGIS specification and +# _every_ one of these lookup types is on the _bounding box_ only. +MYSQL_GIS_FUNCTIONS = { + 'bbcontains' : 'MBRContains', # For consistency w/PostGIS API + 'bboverlaps' : 'MBROverlaps', # .. .. + 'contained' : 'MBRWithin', # .. .. + 'contains' : 'MBRContains', + 'disjoint' : 'MBRDisjoint', + 'equals' : 'MBREqual', + 'exact' : 'MBREqual', + 'intersects' : 'MBRIntersects', + 'overlaps' : 'MBROverlaps', + 'same_as' : 'MBREqual', + 'touches' : 'MBRTouches', + 'within' : 'MBRWithin', + } + +# This lookup type does not require a mapping. +MISC_TERMS = ['isnull'] + +# Assacceptable lookup types for Oracle spatial. +MYSQL_GIS_TERMS = MYSQL_GIS_FUNCTIONS.keys() +MYSQL_GIS_TERMS += MISC_TERMS +MYSQL_GIS_TERMS = dict((term, None) for term in MYSQL_GIS_TERMS) # Making dictionary + +def get_geo_where_clause(table_alias, name, lookup_type, geo_annot): + "Returns the SQL WHERE clause for use in MySQL spatial SQL construction." + # Getting the quoted field as `geo_col`. + geo_col = '%s.%s' % (qn(table_alias), qn(name)) + + # See if a MySQL Geometry function matches the lookup type next + lookup_info = MYSQL_GIS_FUNCTIONS.get(lookup_type, False) + if lookup_info: + return "%s(%s, %%s)" % (lookup_info, geo_col) + + # Handling 'isnull' lookup type + # TODO: Is this needed because MySQL cannot handle NULL + # geometries in its spatial indices. + if lookup_type == 'isnull': + return "%s IS %sNULL" % (geo_col, (not geo_annot.value and 'NOT ' or '')) + + raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) diff --git a/django/contrib/gis/db/backend/oracle/__init__.py b/django/contrib/gis/db/backend/oracle/__init__.py new file mode 100644 index 0000000000..3eee56ea23 --- /dev/null +++ b/django/contrib/gis/db/backend/oracle/__init__.py @@ -0,0 +1,31 @@ +__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend'] + +from django.contrib.gis.db.backend.base import BaseSpatialBackend +from django.contrib.gis.db.backend.oracle.adaptor import OracleSpatialAdaptor +from django.contrib.gis.db.backend.oracle.creation import create_spatial_db +from django.contrib.gis.db.backend.oracle.field import OracleSpatialField +from django.contrib.gis.db.backend.oracle.query import * + +SpatialBackend = BaseSpatialBackend(name='oracle', oracle=True, + area=AREA, + centroid=CENTROID, + difference=DIFFERENCE, + distance=DISTANCE, + distance_functions=DISTANCE_FUNCTIONS, + gis_terms=ORACLE_SPATIAL_TERMS, + gml=ASGML, + intersection=INTERSECTION, + length=LENGTH, + limited_where = {'relate' : None}, + num_geom=NUM_GEOM, + num_points=NUM_POINTS, + perimeter=LENGTH, + point_on_surface=POINT_ON_SURFACE, + select=GEOM_SELECT, + sym_difference=SYM_DIFFERENCE, + transform=TRANSFORM, + unionagg=UNIONAGG, + union=UNION, + Adaptor=OracleSpatialAdaptor, + Field=OracleSpatialField, + ) diff --git a/django/contrib/gis/db/backend/oracle/adaptor.py b/django/contrib/gis/db/backend/oracle/adaptor.py new file mode 100644 index 0000000000..95dc265795 --- /dev/null +++ b/django/contrib/gis/db/backend/oracle/adaptor.py @@ -0,0 +1,5 @@ +from cx_Oracle import CLOB +from django.contrib.gis.db.backend.adaptor import WKTAdaptor + +class OracleSpatialAdaptor(WKTAdaptor): + input_size = CLOB diff --git a/django/contrib/gis/db/backend/oracle/creation.py b/django/contrib/gis/db/backend/oracle/creation.py new file mode 100644 index 0000000000..4a05da7ec1 --- /dev/null +++ b/django/contrib/gis/db/backend/oracle/creation.py @@ -0,0 +1,8 @@ +from django.db.backends.oracle.creation import create_test_db + +def create_spatial_db(test=True, verbosity=1, autoclobber=False): + "A wrapper over the Oracle `create_test_db` routine." + if not test: raise NotImplementedError('This uses `create_test_db` from db/backends/oracle/creation.py') + from django.conf import settings + from django.db import connection + create_test_db(settings, connection, verbosity, autoclobber) diff --git a/django/contrib/gis/db/backend/oracle/field.py b/django/contrib/gis/db/backend/oracle/field.py new file mode 100644 index 0000000000..22625f5e10 --- /dev/null +++ b/django/contrib/gis/db/backend/oracle/field.py @@ -0,0 +1,103 @@ +from django.db import connection +from django.db.backends.util import truncate_name +from django.db.models.fields import Field # Django base Field class +from django.contrib.gis.db.backend.util import gqn +from django.contrib.gis.db.backend.oracle.query import TRANSFORM + +# Quotename & geographic quotename, respectively. +qn = connection.ops.quote_name + +class OracleSpatialField(Field): + """ + The backend-specific geographic field for Oracle Spatial. + """ + + empty_strings_allowed = False + + def __init__(self, extent=(-180.0, -90.0, 180.0, 90.0), tolerance=0.05, **kwargs): + """ + Oracle Spatial backend needs to have the extent -- for projected coordinate + systems _you must define the extent manually_, since the coordinates are + for geodetic systems. The `tolerance` keyword specifies the tolerance + for error (in meters), and defaults to 0.05 (5 centimeters). + """ + # Oracle Spatial specific keyword arguments. + self._extent = extent + self._tolerance = tolerance + # Calling the Django field initialization. + super(OracleSpatialField, self).__init__(**kwargs) + + def _add_geom(self, style, db_table): + """ + Adds this geometry column into the Oracle USER_SDO_GEOM_METADATA + table. + """ + + # Checking the dimensions. + # TODO: Add support for 3D geometries. + if self._dim != 2: + raise Exception('3D geometries not yet supported on Oracle Spatial backend.') + + # Constructing the SQL that will be used to insert information about + # the geometry column into the USER_GSDO_GEOM_METADATA table. + meta_sql = style.SQL_KEYWORD('INSERT INTO ') + \ + style.SQL_TABLE('USER_SDO_GEOM_METADATA') + \ + ' (%s, %s, %s, %s)\n ' % tuple(map(qn, ['TABLE_NAME', 'COLUMN_NAME', 'DIMINFO', 'SRID'])) + \ + style.SQL_KEYWORD(' VALUES ') + '(\n ' + \ + style.SQL_TABLE(gqn(db_table)) + ',\n ' + \ + style.SQL_FIELD(gqn(self.column)) + ',\n ' + \ + style.SQL_KEYWORD("MDSYS.SDO_DIM_ARRAY") + '(\n ' + \ + style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") + \ + ("('LONG', %s, %s, %s),\n " % (self._extent[0], self._extent[2], self._tolerance)) + \ + style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") + \ + ("('LAT', %s, %s, %s)\n ),\n" % (self._extent[1], self._extent[3], self._tolerance)) + \ + ' %s\n );' % self._srid + return meta_sql + + def _geom_index(self, style, db_table): + "Creates an Oracle Geometry index (R-tree) for this geometry field." + + # Getting the index name, Oracle doesn't allow object + # names > 30 characters. + idx_name = truncate_name('%s_%s_id' % (db_table, self.column), 30) + + sql = style.SQL_KEYWORD('CREATE INDEX ') + \ + style.SQL_TABLE(qn(idx_name)) + \ + style.SQL_KEYWORD(' ON ') + \ + style.SQL_TABLE(qn(db_table)) + '(' + \ + style.SQL_FIELD(qn(self.column)) + ') ' + \ + style.SQL_KEYWORD('INDEXTYPE IS ') + \ + style.SQL_TABLE('MDSYS.SPATIAL_INDEX') + ';' + return sql + + def post_create_sql(self, style, db_table): + """ + Returns SQL that will be executed after the model has been + created. + """ + # Getting the meta geometry information. + post_sql = self._add_geom(style, db_table) + + # Getting the geometric index for this Geometry column. + if self._index: + return (post_sql, self._geom_index(style, db_table)) + else: + return (post_sql,) + + def db_type(self): + "The Oracle geometric data type is MDSYS.SDO_GEOMETRY." + return 'MDSYS.SDO_GEOMETRY' + + def get_placeholder(self, value): + """ + Provides a proper substitution value for Geometries that are not in the + SRID of the field. Specifically, this routine will substitute in the + SDO_CS.TRANSFORM() function call. + """ + if value is None: + return '%s' + elif value.srid != self._srid: + # Adding Transform() to the SQL placeholder. + return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (TRANSFORM, value.srid, self._srid) + else: + return 'SDO_GEOMETRY(%%s, %s)' % self._srid diff --git a/django/contrib/gis/db/backend/oracle/models.py b/django/contrib/gis/db/backend/oracle/models.py new file mode 100644 index 0000000000..c740b48efe --- /dev/null +++ b/django/contrib/gis/db/backend/oracle/models.py @@ -0,0 +1,49 @@ +""" + 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.db import models +from django.contrib.gis.models import SpatialRefSysMixin + +class GeometryColumns(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: + db_table = 'USER_SDO_GEOM_METADATA' + + @classmethod + def table_name_col(cls): + return 'table_name' + + def __unicode__(self): + return '%s - %s (SRID: %s)' % (self.table_name, self.column_name, self.srid) + +class SpatialRefSys(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) + #cs_bounds = models.GeometryField() + + class Meta: + # TODO: Figure out way to have this be MDSYS.CS_SRS without + # having django's quoting mess up the SQL. + db_table = 'CS_SRS' + + @property + def wkt(self): + return self.wktext + + @classmethod + def wkt_col(cls): + return 'wktext' diff --git a/django/contrib/gis/db/backend/oracle/query.py b/django/contrib/gis/db/backend/oracle/query.py new file mode 100644 index 0000000000..35e30d40eb --- /dev/null +++ b/django/contrib/gis/db/backend/oracle/query.py @@ -0,0 +1,154 @@ +""" + 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 decimal import Decimal +from django.db import connection +from django.contrib.gis.db.backend.util import SpatialFunction +from django.contrib.gis.measure import Distance +qn = connection.ops.quote_name + +# The GML, distance, transform, and union procedures. +AREA = 'SDO_GEOM.SDO_AREA' +ASGML = 'SDO_UTIL.TO_GMLGEOMETRY' +CENTROID = 'SDO_GEOM.SDO_CENTROID' +DIFFERENCE = 'SDO_GEOM.SDO_DIFFERENCE' +DISTANCE = 'SDO_GEOM.SDO_DISTANCE' +EXTENT = 'SDO_AGGR_MBR' +INTERSECTION = 'SDO_GEOM.SDO_INTERSECTION' +LENGTH = 'SDO_GEOM.SDO_LENGTH' +NUM_GEOM = 'SDO_UTIL.GETNUMELEM' +NUM_POINTS = 'SDO_UTIL.GETNUMVERTICES' +POINT_ON_SURFACE = 'SDO_GEOM.SDO_POINTONSURFACE' +SYM_DIFFERENCE = 'SDO_GEOM.SDO_XOR' +TRANSFORM = 'SDO_CS.TRANSFORM' +UNION = 'SDO_GEOM.SDO_UNION' +UNIONAGG = 'SDO_AGGR_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 =) +GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)' + +#### Classes used in constructing Oracle spatial SQL #### +class SDOOperation(SpatialFunction): + "Base class for SDO* Oracle operations." + def __init__(self, func, **kwargs): + kwargs.setdefault('operator', '=') + kwargs.setdefault('result', 'TRUE') + kwargs.setdefault('end_subst', ") %s '%s'") + super(SDOOperation, self).__init__(func, **kwargs) + +class SDODistance(SpatialFunction): + "Class for Distance queries." + def __init__(self, op, tolerance=0.05): + super(SDODistance, self).__init__(DISTANCE, end_subst=', %s) %%s %%s' % tolerance, + operator=op, result='%%s') + +class SDOGeomRelate(SpatialFunction): + "Class for using SDO_GEOM.RELATE." + def __init__(self, mask, tolerance=0.05): + # SDO_GEOM.RELATE(...) has a peculiar argument order: column, mask, geom, tolerance. + # Moreover, the runction result is the mask (e.g., 'DISJOINT' instead of 'TRUE'). + end_subst = "%s%s) %s '%s'" % (', %%s, ', tolerance, '=', mask) + beg_subst = "%%s(%%s, '%s'" % mask + super(SDOGeomRelate, self).__init__('SDO_GEOM.RELATE', beg_subst=beg_subst, end_subst=end_subst) + +class SDORelate(SpatialFunction): + "Class for using SDO_RELATE." + masks = 'TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|CONTAINS|COVERS|ANYINTERACT|ON' + mask_regex = re.compile(r'^(%s)(\+(%s))*$' % (masks, masks), re.I) + def __init__(self, mask): + func = 'SDO_RELATE' + if not self.mask_regex.match(mask): + raise ValueError('Invalid %s mask: "%s"' % (func, mask)) + super(SDORelate, self).__init__(func, end_subst=", 'mask=%s') = 'TRUE'" % mask) + +#### Lookup type mapping dictionaries of Oracle spatial operations #### + +# Valid distance types and substitutions +dtypes = (Decimal, Distance, float, int, long) +DISTANCE_FUNCTIONS = { + 'distance_gt' : (SDODistance('>'), dtypes), + 'distance_gte' : (SDODistance('>='), dtypes), + 'distance_lt' : (SDODistance('<'), dtypes), + 'distance_lte' : (SDODistance('<='), dtypes), + 'dwithin' : (SDOOperation('SDO_WITHIN_DISTANCE', + beg_subst="%s(%s, %%s, 'distance=%%s'"), dtypes), + } + +ORACLE_GEOMETRY_FUNCTIONS = { + 'contains' : SDOOperation('SDO_CONTAINS'), + 'coveredby' : SDOOperation('SDO_COVEREDBY'), + 'covers' : SDOOperation('SDO_COVERS'), + 'disjoint' : SDOGeomRelate('DISJOINT'), + 'intersects' : SDOOperation('SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()? + 'equals' : SDOOperation('SDO_EQUAL'), + 'exact' : SDOOperation('SDO_EQUAL'), + 'overlaps' : SDOOperation('SDO_OVERLAPS'), + 'same_as' : SDOOperation('SDO_EQUAL'), + 'relate' : (SDORelate, basestring), # Oracle uses a different syntax, e.g., 'mask=inside+touch' + 'touches' : SDOOperation('SDO_TOUCH'), + 'within' : SDOOperation('SDO_INSIDE'), + } +ORACLE_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS) + +# This lookup type does not require a mapping. +MISC_TERMS = ['isnull'] + +# Acceptable lookup types for Oracle spatial. +ORACLE_SPATIAL_TERMS = ORACLE_GEOMETRY_FUNCTIONS.keys() +ORACLE_SPATIAL_TERMS += MISC_TERMS +ORACLE_SPATIAL_TERMS = dict((term, None) for term in ORACLE_SPATIAL_TERMS) # Making dictionary for fast lookups + +#### The `get_geo_where_clause` function for Oracle #### +def get_geo_where_clause(table_alias, name, lookup_type, geo_annot): + "Returns the SQL WHERE clause for use in Oracle spatial SQL construction." + # Getting the quoted table name as `geo_col`. + geo_col = '%s.%s' % (qn(table_alias), qn(name)) + + # See if a Oracle Geometry function matches the lookup type next + lookup_info = ORACLE_GEOMETRY_FUNCTIONS.get(lookup_type, False) + if lookup_info: + # Lookup types that are tuples take tuple arguments, e.g., 'relate' and + # 'dwithin' lookup types. + if isinstance(lookup_info, tuple): + # First element of tuple is lookup type, second element is the type + # of the expected argument (e.g., str, float) + sdo_op, arg_type = lookup_info + + # Ensuring that a tuple _value_ was passed in from the user + if not isinstance(geo_annot.value, tuple): + raise TypeError('Tuple required for `%s` lookup type.' % lookup_type) + if len(geo_annot.value) != 2: + raise ValueError('2-element tuple required for %s lookup type.' % lookup_type) + + # Ensuring the argument type matches what we expect. + if not isinstance(geo_annot.value[1], arg_type): + raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(geo_annot.value[1]))) + + if lookup_type == 'relate': + # The SDORelate class handles construction for these queries, + # and verifies the mask argument. + return sdo_op(geo_annot.value[1]).as_sql(geo_col) + else: + # Otherwise, just call the `as_sql` method on the SDOOperation instance. + return sdo_op.as_sql(geo_col) + else: + # Lookup info is a SDOOperation instance, whose `as_sql` method returns + # the SQL necessary for the geometry function call. For example: + # SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE' + return lookup_info.as_sql(geo_col) + elif lookup_type == 'isnull': + # Handling 'isnull' lookup type + return "%s IS %sNULL" % (geo_col, (not geo_annot.value and 'NOT ' or '')) + + raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) diff --git a/django/contrib/gis/db/backend/postgis/__init__.py b/django/contrib/gis/db/backend/postgis/__init__.py new file mode 100644 index 0000000000..8a4d09e0d5 --- /dev/null +++ b/django/contrib/gis/db/backend/postgis/__init__.py @@ -0,0 +1,42 @@ +__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend'] + +from django.contrib.gis.db.backend.base import BaseSpatialBackend +from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor +from django.contrib.gis.db.backend.postgis.creation import create_spatial_db +from django.contrib.gis.db.backend.postgis.field import PostGISField +from django.contrib.gis.db.backend.postgis.query import * + +SpatialBackend = BaseSpatialBackend(name='postgis', postgis=True, + area=AREA, + centroid=CENTROID, + difference=DIFFERENCE, + distance=DISTANCE, + distance_functions=DISTANCE_FUNCTIONS, + distance_sphere=DISTANCE_SPHERE, + distance_spheroid=DISTANCE_SPHEROID, + envelope=ENVELOPE, + extent=EXTENT, + gis_terms=POSTGIS_TERMS, + gml=ASGML, + intersection=INTERSECTION, + kml=ASKML, + length=LENGTH, + length_spheroid=LENGTH_SPHEROID, + make_line=MAKE_LINE, + mem_size=MEM_SIZE, + num_geom=NUM_GEOM, + num_points=NUM_POINTS, + perimeter=PERIMETER, + point_on_surface=POINT_ON_SURFACE, + scale=SCALE, + select=GEOM_SELECT, + svg=ASSVG, + sym_difference=SYM_DIFFERENCE, + transform=TRANSFORM, + translate=TRANSLATE, + union=UNION, + unionagg=UNIONAGG, + version=(MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2), + Adaptor=PostGISAdaptor, + Field=PostGISField, + ) diff --git a/django/contrib/gis/db/backend/postgis/adaptor.py b/django/contrib/gis/db/backend/postgis/adaptor.py new file mode 100644 index 0000000000..c094a9825a --- /dev/null +++ b/django/contrib/gis/db/backend/postgis/adaptor.py @@ -0,0 +1,33 @@ +""" + This object provides quoting for GEOS geometries into PostgreSQL/PostGIS. +""" + +from django.contrib.gis.db.backend.postgis.query import GEOM_FROM_WKB +from psycopg2 import Binary +from psycopg2.extensions import ISQLQuote + +class PostGISAdaptor(object): + def __init__(self, geom): + "Initializes on the geometry." + # Getting the WKB (in string form, to allow easy pickling of + # the adaptor) and the SRID from the geometry. + self.wkb = str(geom.wkb) + self.srid = geom.srid + + def __conform__(self, proto): + # Does the given protocol conform to what Psycopg2 expects? + if proto == ISQLQuote: + return self + else: + raise Exception('Error implementing psycopg2 protocol. Is psycopg2 installed?') + + def __eq__(self, other): + return (self.wkb == other.wkb) and (self.srid == other.srid) + + def __str__(self): + return self.getquoted() + + def getquoted(self): + "Returns a properly quoted string for use in PostgreSQL/PostGIS." + # Want to use WKB, so wrap with psycopg2 Binary() to quote properly. + return "%s(%s, %s)" % (GEOM_FROM_WKB, Binary(self.wkb), self.srid or -1) diff --git a/django/contrib/gis/db/backend/postgis/creation.py b/django/contrib/gis/db/backend/postgis/creation.py new file mode 100644 index 0000000000..a3884db187 --- /dev/null +++ b/django/contrib/gis/db/backend/postgis/creation.py @@ -0,0 +1,224 @@ +from django.conf import settings +from django.core.management import call_command +from django.db import connection +from django.test.utils import _set_autocommit, TEST_DATABASE_PREFIX +import os, re, sys + +def getstatusoutput(cmd): + "A simpler version of getstatusoutput that works on win32 platforms." + stdin, stdout, stderr = os.popen3(cmd) + output = stdout.read() + if output.endswith('\n'): output = output[:-1] + status = stdin.close() + return status, output + +def create_lang(db_name, verbosity=1): + "Sets up the pl/pgsql language on the given database." + + # Getting the command-line options for the shell command + options = get_cmd_options(db_name) + + # Constructing the 'createlang' command. + createlang_cmd = 'createlang %splpgsql' % options + if verbosity >= 1: print createlang_cmd + + # Must have database super-user privileges to execute createlang -- it must + # also be in your path. + status, output = getstatusoutput(createlang_cmd) + + # Checking the status of the command, 0 => execution successful + if status: + raise Exception("Error executing 'plpgsql' command: %s\n" % output) + +def _create_with_cursor(db_name, verbosity=1, autoclobber=False): + "Creates database with psycopg2 cursor." + + # Constructing the necessary SQL to create the database (the DATABASE_USER + # must possess the privileges to create a database) + create_sql = 'CREATE DATABASE %s' % connection.ops.quote_name(db_name) + if settings.DATABASE_USER: + create_sql += ' OWNER %s' % settings.DATABASE_USER + + cursor = connection.cursor() + _set_autocommit(connection) + + try: + # Trying to create the database first. + cursor.execute(create_sql) + #print create_sql + except Exception, e: + # Drop and recreate, if necessary. + if not autoclobber: + confirm = raw_input("\nIt appears the database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % db_name) + if autoclobber or confirm == 'yes': + if verbosity >= 1: print 'Destroying old spatial database...' + drop_db(db_name) + if verbosity >= 1: print 'Creating new spatial database...' + cursor.execute(create_sql) + else: + raise Exception('Spatial Database Creation canceled.') +foo = _create_with_cursor + +created_regex = re.compile(r'^createdb: database creation failed: ERROR: database ".+" already exists') +def _create_with_shell(db_name, verbosity=1, autoclobber=False): + """ + If no spatial database already exists, then using a cursor will not work. + Thus, a `createdb` command will be issued through the shell to bootstrap + creation of the spatial database. + """ + + # Getting the command-line options for the shell command + options = get_cmd_options(False) + create_cmd = 'createdb -O %s %s%s' % (settings.DATABASE_USER, options, db_name) + if verbosity >= 1: print create_cmd + + # Attempting to create the database. + status, output = getstatusoutput(create_cmd) + + if status: + if created_regex.match(output): + if not autoclobber: + confirm = raw_input("\nIt appears the database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % db_name) + if autoclobber or confirm == 'yes': + if verbosity >= 1: print 'Destroying old spatial database...' + drop_cmd = 'dropdb %s%s' % (options, db_name) + status, output = getstatusoutput(drop_cmd) + if status != 0: + raise Exception('Could not drop database %s: %s' % (db_name, output)) + if verbosity >= 1: print 'Creating new spatial database...' + status, output = getstatusoutput(create_cmd) + if status != 0: + raise Exception('Could not create database after dropping: %s' % output) + else: + raise Exception('Spatial Database Creation canceled.') + else: + raise Exception('Unknown error occurred in creating database: %s' % output) + +def create_spatial_db(test=False, verbosity=1, autoclobber=False, interactive=False): + "Creates a spatial database based on the settings." + + # Making sure we're using PostgreSQL and psycopg2 + if settings.DATABASE_ENGINE != 'postgresql_psycopg2': + raise Exception('Spatial database creation only supported postgresql_psycopg2 platform.') + + # Getting the spatial database name + if test: + db_name = get_spatial_db(test=True) + _create_with_cursor(db_name, verbosity=verbosity, autoclobber=autoclobber) + else: + db_name = get_spatial_db() + _create_with_shell(db_name, verbosity=verbosity, autoclobber=autoclobber) + + # Creating the db language, does not need to be done on NT platforms + # since the PostGIS installer enables this capability. + if os.name != 'nt': + create_lang(db_name, verbosity=verbosity) + + # Now adding in the PostGIS routines. + load_postgis_sql(db_name, verbosity=verbosity) + + if verbosity >= 1: print 'Creation of spatial database %s successful.' % db_name + + # Closing the connection + connection.close() + settings.DATABASE_NAME = db_name + + # Syncing the database + call_command('syncdb', verbosity=verbosity, interactive=interactive) + +def drop_db(db_name=False, test=False): + """ + Drops the given database (defaults to what is returned from + get_spatial_db()). All exceptions are propagated up to the caller. + """ + if not db_name: db_name = get_spatial_db(test=test) + cursor = connection.cursor() + cursor.execute('DROP DATABASE %s' % connection.ops.quote_name(db_name)) + +def get_cmd_options(db_name): + "Obtains the command-line PostgreSQL connection options for shell commands." + # The db_name parameter is optional + options = '' + if db_name: + options += '-d %s ' % db_name + if settings.DATABASE_USER: + options += '-U %s ' % settings.DATABASE_USER + if settings.DATABASE_HOST: + options += '-h %s ' % settings.DATABASE_HOST + if settings.DATABASE_PORT: + options += '-p %s ' % settings.DATABASE_PORT + return options + +def get_spatial_db(test=False): + """ + Returns the name of the spatial database. The 'test' keyword may be set + to return the test spatial database name. + """ + if test: + if settings.TEST_DATABASE_NAME: + test_db_name = settings.TEST_DATABASE_NAME + else: + test_db_name = TEST_DATABASE_PREFIX + settings.DATABASE_NAME + return test_db_name + else: + if not settings.DATABASE_NAME: + raise Exception('must configure DATABASE_NAME in settings.py') + return settings.DATABASE_NAME + +def load_postgis_sql(db_name, verbosity=1): + """ + This routine loads up the PostGIS SQL files lwpostgis.sql and + spatial_ref_sys.sql. + """ + + # Getting the path to the PostGIS SQL + try: + # POSTGIS_SQL_PATH may be placed in settings to tell GeoDjango where the + # PostGIS SQL files are located. This is especially useful on Win32 + # platforms since the output of pg_config looks like "C:/PROGRA~1/..". + sql_path = settings.POSTGIS_SQL_PATH + except AttributeError: + status, sql_path = getstatusoutput('pg_config --sharedir') + if status: + sql_path = '/usr/local/share' + + # The PostGIS SQL post-creation files. + lwpostgis_file = os.path.join(sql_path, 'lwpostgis.sql') + srefsys_file = os.path.join(sql_path, 'spatial_ref_sys.sql') + if not os.path.isfile(lwpostgis_file): + raise Exception('Could not find PostGIS function definitions in %s' % lwpostgis_file) + if not os.path.isfile(srefsys_file): + raise Exception('Could not find PostGIS spatial reference system definitions in %s' % srefsys_file) + + # Getting the psql command-line options, and command format. + options = get_cmd_options(db_name) + cmd_fmt = 'psql %s-f "%%s"' % options + + # Now trying to load up the PostGIS functions + cmd = cmd_fmt % lwpostgis_file + if verbosity >= 1: print cmd + status, output = getstatusoutput(cmd) + if status: + raise Exception('Error in loading PostGIS lwgeometry routines.') + + # Now trying to load up the Spatial Reference System table + cmd = cmd_fmt % srefsys_file + if verbosity >= 1: print cmd + status, output = getstatusoutput(cmd) + if status: + raise Exception('Error in loading PostGIS spatial_ref_sys table.') + + # Setting the permissions because on Windows platforms the owner + # of the spatial_ref_sys and geometry_columns tables is always + # the postgres user, regardless of how the db is created. + if os.name == 'nt': set_permissions(db_name) + +def set_permissions(db_name): + """ + Sets the permissions on the given database to that of the user specified + in the settings. Needed specifically for PostGIS on Win32 platforms. + """ + cursor = connection.cursor() + user = settings.DATABASE_USER + cursor.execute('ALTER TABLE geometry_columns OWNER TO %s' % user) + cursor.execute('ALTER TABLE spatial_ref_sys OWNER TO %s' % user) diff --git a/django/contrib/gis/db/backend/postgis/field.py b/django/contrib/gis/db/backend/postgis/field.py new file mode 100644 index 0000000000..9d6c0fad24 --- /dev/null +++ b/django/contrib/gis/db/backend/postgis/field.py @@ -0,0 +1,95 @@ +from django.db import connection +from django.db.models.fields import Field # Django base Field class +from django.contrib.gis.db.backend.util import gqn +from django.contrib.gis.db.backend.postgis.query import TRANSFORM + +# Quotename & geographic quotename, respectively +qn = connection.ops.quote_name + +class PostGISField(Field): + """ + The backend-specific geographic field for PostGIS. + """ + + def _add_geom(self, style, db_table): + """ + Constructs the addition of the geometry to the table using the + AddGeometryColumn(...) PostGIS (and OGC standard) stored procedure. + + Takes the style object (provides syntax highlighting) and the + database table as parameters. + """ + sql = style.SQL_KEYWORD('SELECT ') + \ + style.SQL_TABLE('AddGeometryColumn') + '(' + \ + style.SQL_TABLE(gqn(db_table)) + ', ' + \ + style.SQL_FIELD(gqn(self.column)) + ', ' + \ + style.SQL_FIELD(str(self._srid)) + ', ' + \ + style.SQL_COLTYPE(gqn(self._geom)) + ', ' + \ + style.SQL_KEYWORD(str(self._dim)) + ');' + + if not self.null: + # Add a NOT NULL constraint to the field + sql += '\n' + \ + style.SQL_KEYWORD('ALTER TABLE ') + \ + style.SQL_TABLE(qn(db_table)) + \ + style.SQL_KEYWORD(' ALTER ') + \ + style.SQL_FIELD(qn(self.column)) + \ + style.SQL_KEYWORD(' SET NOT NULL') + ';' + return sql + + def _geom_index(self, style, db_table, + index_type='GIST', index_opts='GIST_GEOMETRY_OPS'): + "Creates a GiST index for this geometry field." + sql = style.SQL_KEYWORD('CREATE INDEX ') + \ + style.SQL_TABLE(qn('%s_%s_id' % (db_table, self.column))) + \ + style.SQL_KEYWORD(' ON ') + \ + style.SQL_TABLE(qn(db_table)) + \ + style.SQL_KEYWORD(' USING ') + \ + style.SQL_COLTYPE(index_type) + ' ( ' + \ + style.SQL_FIELD(qn(self.column)) + ' ' + \ + style.SQL_KEYWORD(index_opts) + ' );' + return sql + + def post_create_sql(self, style, db_table): + """ + Returns SQL that will be executed after the model has been + created. Geometry columns must be added after creation with the + PostGIS AddGeometryColumn() function. + """ + + # Getting the AddGeometryColumn() SQL necessary to create a PostGIS + # geometry field. + post_sql = self._add_geom(style, db_table) + + # If the user wants to index this data, then get the indexing SQL as well. + if self._index: + return (post_sql, self._geom_index(style, db_table)) + else: + return (post_sql,) + + def _post_delete_sql(self, style, db_table): + "Drops the geometry column." + sql = style.SQL_KEYWORD('SELECT ') + \ + style.SQL_KEYWORD('DropGeometryColumn') + '(' + \ + style.SQL_TABLE(gqn(db_table)) + ', ' + \ + style.SQL_FIELD(gqn(self.column)) + ');' + return sql + + def db_type(self): + """ + PostGIS geometry columns are added by stored procedures, should be + None. + """ + return None + + def get_placeholder(self, value): + """ + Provides a proper substitution value for Geometries that are not in the + SRID of the field. Specifically, this routine will substitute in the + ST_Transform() function call. + """ + if value is None or value.srid == self._srid: + return '%s' + else: + # Adding Transform() to the SQL placeholder. + return '%s(%%s, %s)' % (TRANSFORM, self._srid) diff --git a/django/contrib/gis/db/backend/postgis/management.py b/django/contrib/gis/db/backend/postgis/management.py new file mode 100644 index 0000000000..c1cb32a04f --- /dev/null +++ b/django/contrib/gis/db/backend/postgis/management.py @@ -0,0 +1,54 @@ +""" + This utility module is for obtaining information about the PostGIS + installation. + + See PostGIS docs at Ch. 6.2.1 for more information on these functions. +""" +import re + +def _get_postgis_func(func): + "Helper routine for calling PostGIS functions and returning their result." + from django.db import connection + cursor = connection.cursor() + cursor.execute('SELECT %s()' % func) + row = cursor.fetchone() + cursor.close() + return row[0] + +### PostGIS management functions ### +def postgis_geos_version(): + "Returns the version of the GEOS library used with PostGIS." + return _get_postgis_func('postgis_geos_version') + +def postgis_lib_version(): + "Returns the version number of the PostGIS library used with PostgreSQL." + return _get_postgis_func('postgis_lib_version') + +def postgis_proj_version(): + "Returns the version of the PROJ.4 library used with PostGIS." + return _get_postgis_func('postgis_proj_version') + +def postgis_version(): + "Returns PostGIS version number and compile-time options." + return _get_postgis_func('postgis_version') + +def postgis_full_version(): + "Returns PostGIS version number and compile-time options." + return _get_postgis_func('postgis_full_version') + +### Routines for parsing output of management functions. ### +version_regex = re.compile('^(?P\d)\.(?P\d)\.(?P\d+)') +def postgis_version_tuple(): + "Returns the PostGIS version as a tuple." + + # Getting the PostGIS version + version = postgis_lib_version() + m = version_regex.match(version) + if m: + major = int(m.group('major')) + minor1 = int(m.group('minor1')) + minor2 = int(m.group('minor2')) + else: + raise Exception('Could not parse PostGIS version string: %s' % version) + + return (version, major, minor1, minor2) diff --git a/django/contrib/gis/db/backend/postgis/models.py b/django/contrib/gis/db/backend/postgis/models.py new file mode 100644 index 0000000000..e032da4d89 --- /dev/null +++ b/django/contrib/gis/db/backend/postgis/models.py @@ -0,0 +1,58 @@ +""" + The GeometryColumns and SpatialRefSys models for the PostGIS backend. +""" +from django.db import models +from django.contrib.gis.models import SpatialRefSysMixin + +# Checking for the presence of GDAL (needed for the SpatialReference object) +from django.contrib.gis.gdal import HAS_GDAL +if HAS_GDAL: + from django.contrib.gis.gdal import SpatialReference + +class GeometryColumns(models.Model): + """ + The 'geometry_columns' table from the PostGIS. See the PostGIS + documentation at Ch. 4.2.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: + db_table = 'geometry_columns' + + @classmethod + def table_name_col(cls): + "Class method for returning the table name column for this model." + return 'f_table_name' + + def __unicode__(self): + return "%s.%s - %dD %s field (SRID: %d)" % \ + (self.f_table_name, self.f_geometry_column, + self.coord_dimension, self.type, self.srid) + +class SpatialRefSys(models.Model, SpatialRefSysMixin): + """ + The 'spatial_ref_sys' table from PostGIS. See the PostGIS + documentaiton 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: + db_table = 'spatial_ref_sys' + + @property + def wkt(self): + return self.srtext + + @classmethod + def wkt_col(cls): + return 'srtext' diff --git a/django/contrib/gis/db/backend/postgis/query.py b/django/contrib/gis/db/backend/postgis/query.py new file mode 100644 index 0000000000..8780780402 --- /dev/null +++ b/django/contrib/gis/db/backend/postgis/query.py @@ -0,0 +1,287 @@ +""" + This module contains the spatial lookup types, and the get_geo_where_clause() + routine for PostGIS. +""" +import re +from decimal import Decimal +from django.db import connection +from django.contrib.gis.measure import Distance +from django.contrib.gis.db.backend.postgis.management import postgis_version_tuple +from django.contrib.gis.db.backend.util import SpatialOperation, SpatialFunction +qn = connection.ops.quote_name + +# Getting the PostGIS version information +POSTGIS_VERSION, MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 = postgis_version_tuple() + +# The supported PostGIS versions. +# TODO: Confirm tests with PostGIS versions 1.1.x -- should work. +# Versions <= 1.0.x do not use GEOS C API, and will not be supported. +if MAJOR_VERSION != 1 or (MAJOR_VERSION == 1 and MINOR_VERSION1 < 1): + raise Exception('PostGIS version %s not supported.' % POSTGIS_VERSION) + +# Versions of PostGIS >= 1.2.2 changed their naming convention to be +# 'SQL-MM-centric' to conform with the ISO standard. Practically, this +# means that 'ST_' prefixes geometry function names. +GEOM_FUNC_PREFIX = '' +if MAJOR_VERSION >= 1: + if (MINOR_VERSION1 > 2 or + (MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2)): + GEOM_FUNC_PREFIX = 'ST_' + + def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func) + + # Custom selection not needed for PostGIS because GEOS geometries are + # instantiated directly from the HEXEWKB returned by default. If + # WKT is needed for some reason in the future, this value may be changed, + # e.g,, 'AsText(%s)'. + GEOM_SELECT = None + + # Functions used by the GeoManager & GeoQuerySet + AREA = get_func('Area') + ASKML = get_func('AsKML') + ASGML = get_func('AsGML') + ASSVG = get_func('AsSVG') + CENTROID = get_func('Centroid') + DIFFERENCE = get_func('Difference') + DISTANCE = get_func('Distance') + DISTANCE_SPHERE = get_func('distance_sphere') + DISTANCE_SPHEROID = get_func('distance_spheroid') + ENVELOPE = get_func('Envelope') + EXTENT = get_func('extent') + GEOM_FROM_TEXT = get_func('GeomFromText') + GEOM_FROM_WKB = get_func('GeomFromWKB') + INTERSECTION = get_func('Intersection') + LENGTH = get_func('Length') + LENGTH_SPHEROID = get_func('length_spheroid') + MAKE_LINE = get_func('MakeLine') + MEM_SIZE = get_func('mem_size') + NUM_GEOM = get_func('NumGeometries') + NUM_POINTS = get_func('npoints') + PERIMETER = get_func('Perimeter') + POINT_ON_SURFACE = get_func('PointOnSurface') + SCALE = get_func('Scale') + SYM_DIFFERENCE = get_func('SymDifference') + TRANSFORM = get_func('Transform') + TRANSLATE = get_func('Translate') + + # Special cases for union and KML methods. + if MINOR_VERSION1 < 3: + UNIONAGG = 'GeomUnion' + UNION = 'Union' + else: + UNIONAGG = 'ST_Union' + UNION = 'ST_Union' + + if MINOR_VERSION1 == 1: + ASKML = False +else: + raise NotImplementedError('PostGIS versions < 1.0 are not supported.') + +#### Classes used in constructing PostGIS spatial SQL #### +class PostGISOperator(SpatialOperation): + "For PostGIS operators (e.g. `&&`, `~`)." + def __init__(self, operator): + super(PostGISOperator, self).__init__(operator=operator, beg_subst='%s %s %%s') + +class PostGISFunction(SpatialFunction): + "For PostGIS function calls (e.g., `ST_Contains(table, geom)`)." + def __init__(self, function, **kwargs): + super(PostGISFunction, self).__init__(get_func(function), **kwargs) + +class PostGISFunctionParam(PostGISFunction): + "For PostGIS functions that take another parameter (e.g. DWithin, Relate)." + def __init__(self, func): + super(PostGISFunctionParam, self).__init__(func, end_subst=', %%s)') + +class PostGISDistance(PostGISFunction): + "For PostGIS distance operations." + dist_func = 'Distance' + def __init__(self, operator): + super(PostGISDistance, self).__init__(self.dist_func, end_subst=') %s %s', + operator=operator, result='%%s') + +class PostGISSpheroidDistance(PostGISFunction): + "For PostGIS spherical distance operations (using the spheroid)." + dist_func = 'distance_spheroid' + def __init__(self, operator): + # An extra parameter in `end_subst` is needed for the spheroid string. + super(PostGISSpheroidDistance, self).__init__(self.dist_func, + beg_subst='%s(%s, %%s, %%s', + end_subst=') %s %s', + operator=operator, result='%%s') + +class PostGISSphereDistance(PostGISFunction): + "For PostGIS spherical distance operations." + dist_func = 'distance_sphere' + def __init__(self, operator): + super(PostGISSphereDistance, self).__init__(self.dist_func, end_subst=') %s %s', + operator=operator, result='%%s') + +class PostGISRelate(PostGISFunctionParam): + "For PostGIS Relate(, ) calls." + pattern_regex = re.compile(r'^[012TF\*]{9}$') + def __init__(self, pattern): + if not self.pattern_regex.match(pattern): + raise ValueError('Invalid intersection matrix pattern "%s".' % pattern) + super(PostGISRelate, self).__init__('Relate') + +#### Lookup type mapping dictionaries of PostGIS operations. #### + +# PostGIS-specific operators. The commented descriptions of these +# operators come from Section 6.2.2 of the official PostGIS documentation. +POSTGIS_OPERATORS = { + # The "&<" operator returns true if A's bounding box overlaps or + # is to the left of B's bounding box. + 'overlaps_left' : PostGISOperator('&<'), + # The "&>" operator returns true if A's bounding box overlaps or + # is to the right of B's bounding box. + 'overlaps_right' : PostGISOperator('&>'), + # The "<<" operator returns true if A's bounding box is strictly + # to the left of B's bounding box. + 'left' : PostGISOperator('<<'), + # The ">>" operator returns true if A's bounding box is strictly + # to the right of B's bounding box. + 'right' : PostGISOperator('>>'), + # The "&<|" operator returns true if A's bounding box overlaps or + # is below B's bounding box. + 'overlaps_below' : PostGISOperator('&<|'), + # The "|&>" operator returns true if A's bounding box overlaps or + # is above B's bounding box. + 'overlaps_above' : PostGISOperator('|&>'), + # The "<<|" operator returns true if A's bounding box is strictly + # below B's bounding box. + 'strictly_below' : PostGISOperator('<<|'), + # The "|>>" operator returns true if A's bounding box is strictly + # above B's bounding box. + 'strictly_above' : PostGISOperator('|>>'), + # 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. + 'same_as' : PostGISOperator('~='), + 'exact' : PostGISOperator('~='), + # The "@" operator returns true if A's bounding box is completely contained + # by B's bounding box. + 'contained' : PostGISOperator('@'), + # The "~" operator returns true if A's bounding box completely contains + # by B's bounding box. + 'bbcontains' : PostGISOperator('~'), + # The "&&" operator returns true if A's bounding box overlaps + # B's bounding box. + 'bboverlaps' : PostGISOperator('&&'), + } + +# For PostGIS >= 1.2.2 the following lookup types will do a bounding box query +# first before calling the more computationally expensive GEOS routines (called +# "inline index magic"): +# 'touches', 'crosses', 'contains', 'intersects', 'within', 'overlaps', and +# 'covers'. +POSTGIS_GEOMETRY_FUNCTIONS = { + 'equals' : PostGISFunction('Equals'), + 'disjoint' : PostGISFunction('Disjoint'), + 'touches' : PostGISFunction('Touches'), + 'crosses' : PostGISFunction('Crosses'), + 'within' : PostGISFunction('Within'), + 'overlaps' : PostGISFunction('Overlaps'), + 'contains' : PostGISFunction('Contains'), + 'intersects' : PostGISFunction('Intersects'), + 'relate' : (PostGISRelate, basestring), + } + +# Valid distance types and substitutions +dtypes = (Decimal, Distance, float, int, long) +def get_dist_ops(operator): + "Returns operations for both regular and spherical distances." + return (PostGISDistance(operator), PostGISSphereDistance(operator), PostGISSpheroidDistance(operator)) +DISTANCE_FUNCTIONS = { + 'distance_gt' : (get_dist_ops('>'), dtypes), + 'distance_gte' : (get_dist_ops('>='), dtypes), + 'distance_lt' : (get_dist_ops('<'), dtypes), + 'distance_lte' : (get_dist_ops('<='), dtypes), + } + +if GEOM_FUNC_PREFIX == 'ST_': + # The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+ + POSTGIS_GEOMETRY_FUNCTIONS.update( + {'coveredby' : PostGISFunction('CoveredBy'), + 'covers' : PostGISFunction('Covers'), + }) + DISTANCE_FUNCTIONS['dwithin'] = (PostGISFunctionParam('DWithin'), dtypes) + +# Distance functions are a part of PostGIS geometry functions. +POSTGIS_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS) + +# Any other lookup types that do not require a mapping. +MISC_TERMS = ['isnull'] + +# These are the PostGIS-customized QUERY_TERMS -- a list of the lookup types +# allowed for geographic queries. +POSTGIS_TERMS = POSTGIS_OPERATORS.keys() # Getting the operators first +POSTGIS_TERMS += POSTGIS_GEOMETRY_FUNCTIONS.keys() # Adding on the Geometry Functions +POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull') +POSTGIS_TERMS = dict((term, None) for term in POSTGIS_TERMS) # Making a dictionary for fast lookups + +# For checking tuple parameters -- not very pretty but gets job done. +def exactly_two(val): return val == 2 +def two_to_three(val): return val >= 2 and val <=3 +def num_params(lookup_type, val): + if lookup_type in DISTANCE_FUNCTIONS and lookup_type != 'dwithin': return two_to_three(val) + else: return exactly_two(val) + +#### The `get_geo_where_clause` function for PostGIS. #### +def get_geo_where_clause(table_alias, name, lookup_type, geo_annot): + "Returns the SQL WHERE clause for use in PostGIS SQL construction." + # Getting the quoted field as `geo_col`. + geo_col = '%s.%s' % (qn(table_alias), qn(name)) + if lookup_type in POSTGIS_OPERATORS: + # See if a PostGIS operator matches the lookup type. + return POSTGIS_OPERATORS[lookup_type].as_sql(geo_col) + elif lookup_type in POSTGIS_GEOMETRY_FUNCTIONS: + # See if a PostGIS geometry function matches the lookup type. + tmp = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type] + + # Lookup types that are tuples take tuple arguments, e.g., 'relate' and + # distance lookups. + if isinstance(tmp, tuple): + # First element of tuple is the PostGISOperation instance, and the + # second element is either the type or a tuple of acceptable types + # that may passed in as further parameters for the lookup type. + op, arg_type = tmp + + # Ensuring that a tuple _value_ was passed in from the user + if not isinstance(geo_annot.value, (tuple, list)): + raise TypeError('Tuple required for `%s` lookup type.' % lookup_type) + + # Number of valid tuple parameters depends on the lookup type. + nparams = len(geo_annot.value) + if not num_params(lookup_type, nparams): + raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type) + + # Ensuring the argument type matches what we expect. + if not isinstance(geo_annot.value[1], arg_type): + raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(geo_annot.value[1]))) + + # For lookup type `relate`, the op instance is not yet created (has + # to be instantiated here to check the pattern parameter). + if lookup_type == 'relate': + op = op(geo_annot.value[1]) + elif lookup_type in DISTANCE_FUNCTIONS and lookup_type != 'dwithin': + if geo_annot.geodetic: + # Geodetic distances are only availble from Points to PointFields. + if geo_annot.geom_type != 'POINT': + raise TypeError('PostGIS spherical operations are only valid on PointFields.') + if geo_annot.value[0].geom_typeid != 0: + raise TypeError('PostGIS geometry distance parameter is required to be of type Point.') + # Setting up the geodetic operation appropriately. + if nparams == 3 and geo_annot.value[2] == 'spheroid': op = op[2] + else: op = op[1] + else: + op = op[0] + else: + op = tmp + # Calling the `as_sql` function on the operation instance. + return op.as_sql(geo_col) + elif lookup_type == 'isnull': + # Handling 'isnull' lookup type + return "%s IS %sNULL" % (geo_col, (not geo_annot.value and 'NOT ' or '')) + + raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) diff --git a/django/contrib/gis/db/backend/util.py b/django/contrib/gis/db/backend/util.py new file mode 100644 index 0000000000..a19dd975c1 --- /dev/null +++ b/django/contrib/gis/db/backend/util.py @@ -0,0 +1,52 @@ +from types import UnicodeType + +def gqn(val): + """ + The geographic quote name function; used for quoting tables and + geometries (they use single rather than the double quotes of the + backend quotename function). + """ + if isinstance(val, basestring): + if isinstance(val, UnicodeType): val = val.encode('ascii') + return "'%s'" % val + else: + return str(val) + +class SpatialOperation(object): + """ + Base class for generating spatial SQL. + """ + def __init__(self, function='', operator='', result='', beg_subst='', end_subst=''): + self.function = function + self.operator = operator + self.result = result + self.beg_subst = beg_subst + try: + # Try and put the operator and result into to the + # end substitution. + self.end_subst = end_subst % (operator, result) + except TypeError: + self.end_subst = end_subst + + @property + def sql_subst(self): + return ''.join([self.beg_subst, self.end_subst]) + + def as_sql(self, geo_col): + return self.sql_subst % self.params(geo_col) + + def params(self, geo_col): + return (geo_col, self.operator) + +class SpatialFunction(SpatialOperation): + """ + Base class for generating spatial SQL related to a function. + """ + def __init__(self, func, beg_subst='%s(%s, %%s', end_subst=')', result='', operator=''): + # Getting the function prefix. + kwargs = {'function' : func, 'operator' : operator, 'result' : result, + 'beg_subst' : beg_subst, 'end_subst' : end_subst,} + super(SpatialFunction, self).__init__(**kwargs) + + def params(self, geo_col): + return (self.function, geo_col) diff --git a/django/contrib/gis/db/models/__init__.py b/django/contrib/gis/db/models/__init__.py new file mode 100644 index 0000000000..02a2c5318e --- /dev/null +++ b/django/contrib/gis/db/models/__init__.py @@ -0,0 +1,17 @@ +# Want to get everything from the 'normal' models package. +from django.db.models import * + +# The GeoManager +from django.contrib.gis.db.models.manager import GeoManager + +# The GeoQ object +from django.contrib.gis.db.models.query import GeoQ + +# The geographic-enabled fields. +from django.contrib.gis.db.models.fields import \ + GeometryField, PointField, LineStringField, PolygonField, \ + MultiPointField, MultiLineStringField, MultiPolygonField, \ + GeometryCollectionField + +# The geographic mixin class. +from mixin import GeoMixin diff --git a/django/contrib/gis/db/models/fields/__init__.py b/django/contrib/gis/db/models/fields/__init__.py new file mode 100644 index 0000000000..d32f1b118e --- /dev/null +++ b/django/contrib/gis/db/models/fields/__init__.py @@ -0,0 +1,214 @@ +from django.contrib.gis import forms +from django.db import connection +# Getting the SpatialBackend container and the geographic quoting method. +from django.contrib.gis.db.backend import SpatialBackend, gqn +# GeometryProxy, GEOS, Distance, and oldforms imports. +from django.contrib.gis.db.models.proxy import GeometryProxy +from django.contrib.gis.measure import Distance +from django.contrib.gis.oldforms import WKTField + +# The `get_srid_info` function gets SRID information from the spatial +# reference system table w/o using the ORM. +from django.contrib.gis.models import get_srid_info + +#TODO: Flesh out widgets; consider adding support for OGR Geometry proxies. +class GeometryField(SpatialBackend.Field): + "The base GIS field -- maps to the OpenGIS Specification Geometry type." + + # The OpenGIS Geometry name. + _geom = 'GEOMETRY' + + # Geodetic units. + geodetic_units = ('Decimal Degree', 'degree') + + def __init__(self, srid=4326, spatial_index=True, dim=2, **kwargs): + """ + The initialization function for geometry 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. + + dim: + The number of dimensions for this geometry. Defaults to 2. + """ + + # Setting the index flag with the value of the `spatial_index` keyword. + self._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 + self._unit, self._unit_name, self._spheroid = get_srid_info(srid) + + # Setting the dimension of the geometry field. + self._dim = dim + + super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function + + ### Routines specific to GeometryField ### + @property + def geodetic(self): + """ + Returns true if this field's SRID corresponds with a coordinate + system that uses non-projected units (e.g., latitude/longitude). + """ + return self._unit_name in self.geodetic_units + + def get_distance(self, dist_val, lookup_type): + """ + Returns a distance number in units of the field. For example, if + `D(km=1)` was passed in and the units of the field were in meters, + then 1000 would be returned. + """ + # Getting the distance parameter and any options. + if len(dist_val) == 1: dist, option = dist_val[0], None + else: dist, option = dist_val + + if isinstance(dist, Distance): + if self.geodetic: + # Won't allow Distance objects w/DWithin lookups on PostGIS. + if SpatialBackend.postgis and lookup_type == 'dwithin': + raise TypeError('Only numeric values of degree units are allowed on geographic DWithin queries.') + # Spherical distance calculation parameter should be in meters. + dist_param = dist.m + else: + dist_param = getattr(dist, Distance.unit_attname(self._unit_name)) + else: + # Assuming the distance is in the units of the field. + dist_param = dist + + if SpatialBackend.postgis and self.geodetic and lookup_type != 'dwithin' and option == 'spheroid': + # On PostGIS, by default `ST_distance_sphere` is used; but if the + # accuracy of `ST_distance_spheroid` is needed than the spheroid + # needs to be passed to the SQL stored procedure. + return [gqn(self._spheroid), dist_param] + else: + return [dist_param] + + def get_geometry(self, value): + """ + Retrieves the geometry, setting the default SRID from the given + lookup parameters. + """ + if isinstance(value, (tuple, list)): + geom = value[0] + else: + geom = value + + # When the input is not a GEOS geometry, attempt to construct one + # from the given string input. + if isinstance(geom, SpatialBackend.Geometry): + pass + elif isinstance(geom, basestring): + try: + geom = SpatialBackend.Geometry(geom) + except SpatialBackend.GeometryException: + raise ValueError('Could not create geometry from lookup value: %s' % str(value)) + else: + raise TypeError('Cannot use parameter of `%s` type as lookup parameter.' % type(value)) + + # Assigning the SRID value. + geom.srid = self.get_srid(geom) + + return geom + + def get_srid(self, geom): + """ + Returns the default SRID for the given geometry, taking into account + the SRID set for the field. For example, if the input geometry + has no SRID, then that of the field will be returned. + """ + gsrid = geom.srid # SRID of given geometry. + if gsrid is None or self._srid == -1 or (gsrid == -1 and self._srid != -1): + return self._srid + else: + return gsrid + + ### Routines overloaded from Field ### + def contribute_to_class(self, cls, name): + super(GeometryField, self).contribute_to_class(cls, name) + + # Setup for lazy-instantiated Geometry object. + setattr(cls, self.attname, GeometryProxy(SpatialBackend.Geometry, self)) + + def formfield(self, **kwargs): + defaults = {'form_class' : forms.GeometryField, + 'geom_type' : self._geom, + 'null' : self.null, + } + defaults.update(kwargs) + return super(GeometryField, self).formfield(**defaults) + + def get_db_prep_lookup(self, lookup_type, value): + """ + Returns the spatial WHERE clause and associated parameters for the + given lookup type and value. The value will be prepared for database + lookup (e.g., spatial transformation SQL will be added if necessary). + """ + if lookup_type in SpatialBackend.gis_terms: + # special case for isnull lookup + if lookup_type == 'isnull': return [], [] + + # Get the geometry with SRID; defaults SRID to that of the field + # if it is None. + geom = self.get_geometry(value) + + # Getting the WHERE clause list and the associated params list. The params + # list is populated with the Adaptor wrapping the Geometry for the + # backend. The WHERE clause list contains the placeholder for the adaptor + # (e.g. any transformation SQL). + where = [self.get_placeholder(geom)] + params = [SpatialBackend.Adaptor(geom)] + + if isinstance(value, (tuple, list)): + if lookup_type in SpatialBackend.distance_functions: + # Getting the distance parameter in the units of the field. + where += self.get_distance(value[1:], lookup_type) + elif lookup_type in SpatialBackend.limited_where: + pass + else: + # Otherwise, making sure any other parameters are properly quoted. + where += map(gqn, value[1:]) + return where, params + else: + raise TypeError("Field has invalid lookup: %s" % lookup_type) + + def get_db_prep_save(self, value): + "Prepares the value for saving in the database." + if value is None: + return None + else: + return SpatialBackend.Adaptor(self.get_geometry(value)) + + def get_manipulator_field_objs(self): + "Using the WKTField (oldforms) to be our manipulator." + return [WKTField] + +# The OpenGIS Geometry Type Fields +class PointField(GeometryField): + _geom = 'POINT' + +class LineStringField(GeometryField): + _geom = 'LINESTRING' + +class PolygonField(GeometryField): + _geom = 'POLYGON' + +class MultiPointField(GeometryField): + _geom = 'MULTIPOINT' + +class MultiLineStringField(GeometryField): + _geom = 'MULTILINESTRING' + +class MultiPolygonField(GeometryField): + _geom = 'MULTIPOLYGON' + +class GeometryCollectionField(GeometryField): + _geom = 'GEOMETRYCOLLECTION' diff --git a/django/contrib/gis/db/models/manager.py b/django/contrib/gis/db/models/manager.py new file mode 100644 index 0000000000..602d11251a --- /dev/null +++ b/django/contrib/gis/db/models/manager.py @@ -0,0 +1,82 @@ +from django.db.models.manager import Manager +from django.contrib.gis.db.models.query import GeoQuerySet + +class GeoManager(Manager): + "Overrides Manager to return Geographic QuerySets." + + # This manager should be used for queries on related fields + # so that geometry columns on Oracle and MySQL are selected + # properly. + use_for_related_fields = True + + def get_query_set(self): + return GeoQuerySet(model=self.model) + + def area(self, *args, **kwargs): + return self.get_query_set().area(*args, **kwargs) + + def centroid(self, *args, **kwargs): + return self.get_query_set().centroid(*args, **kwargs) + + def difference(self, *args, **kwargs): + return self.get_query_set().difference(*args, **kwargs) + + def distance(self, *args, **kwargs): + return self.get_query_set().distance(*args, **kwargs) + + def envelope(self, *args, **kwargs): + return self.get_query_set().envelope(*args, **kwargs) + + def extent(self, *args, **kwargs): + return self.get_query_set().extent(*args, **kwargs) + + def gml(self, *args, **kwargs): + return self.get_query_set().gml(*args, **kwargs) + + def intersection(self, *args, **kwargs): + return self.get_query_set().intersection(*args, **kwargs) + + def kml(self, *args, **kwargs): + return self.get_query_set().kml(*args, **kwargs) + + def length(self, *args, **kwargs): + return self.get_query_set().length(*args, **kwargs) + + def make_line(self, *args, **kwargs): + return self.get_query_set().make_line(*args, **kwargs) + + def mem_size(self, *args, **kwargs): + return self.get_query_set().mem_size(*args, **kwargs) + + def num_geom(self, *args, **kwargs): + return self.get_query_set().num_geom(*args, **kwargs) + + def num_points(self, *args, **kwargs): + return self.get_query_set().num_points(*args, **kwargs) + + def perimeter(self, *args, **kwargs): + return self.get_query_set().perimeter(*args, **kwargs) + + def point_on_surface(self, *args, **kwargs): + return self.get_query_set().point_on_surface(*args, **kwargs) + + def scale(self, *args, **kwargs): + return self.get_query_set().scale(*args, **kwargs) + + def svg(self, *args, **kwargs): + return self.get_query_set().svg(*args, **kwargs) + + def sym_difference(self, *args, **kwargs): + return self.get_query_set().sym_difference(*args, **kwargs) + + def transform(self, *args, **kwargs): + return self.get_query_set().transform(*args, **kwargs) + + def translate(self, *args, **kwargs): + return self.get_query_set().translate(*args, **kwargs) + + def union(self, *args, **kwargs): + return self.get_query_set().union(*args, **kwargs) + + def unionagg(self, *args, **kwargs): + return self.get_query_set().unionagg(*args, **kwargs) diff --git a/django/contrib/gis/db/models/mixin.py b/django/contrib/gis/db/models/mixin.py new file mode 100644 index 0000000000..475a053b8f --- /dev/null +++ b/django/contrib/gis/db/models/mixin.py @@ -0,0 +1,11 @@ +# Until model subclassing is a possibility, a mixin class is used to add +# the necessary functions that may be contributed for geographic objects. +class GeoMixin: + """ + The Geographic Mixin class provides routines for geographic objects, + however, it is no longer necessary, since all of its previous functions + may now be accessed via the GeometryProxy. This mixin is only provided + for backwards-compatibility purposes, and will be eventually removed + (unless the need arises again). + """ + pass diff --git a/django/contrib/gis/db/models/proxy.py b/django/contrib/gis/db/models/proxy.py new file mode 100644 index 0000000000..34276a6d63 --- /dev/null +++ b/django/contrib/gis/db/models/proxy.py @@ -0,0 +1,62 @@ +""" + The GeometryProxy object, allows for lazy-geometries. The proxy uses + Python descriptors for instantiating and setting Geometry objects + corresponding to geographic model fields. + + Thanks to Robert Coup for providing this functionality (see #4322). +""" + +from types import NoneType, StringType, UnicodeType + +class GeometryProxy(object): + def __init__(self, klass, field): + """ + Proxy initializes on the given Geometry class (not an instance) and + the GeometryField. + """ + self._field = field + self._klass = klass + + def __get__(self, obj, type=None): + """ + This accessor retrieves the geometry, initializing it using the geometry + class specified during initialization and the HEXEWKB value of the field. + Currently, only GEOS or OGR geometries are supported. + """ + # Getting the value of the field. + geom_value = obj.__dict__[self._field.attname] + + if isinstance(geom_value, self._klass): + geom = geom_value + elif (geom_value is None) or (geom_value==''): + geom = None + else: + # Otherwise, a Geometry object is built using the field's contents, + # and the model's corresponding attribute is set. + geom = self._klass(geom_value) + setattr(obj, self._field.attname, geom) + return geom + + def __set__(self, obj, value): + """ + This accessor sets the proxied geometry with the geometry class + specified during initialization. Values of None, HEXEWKB, or WKT may + be used to set the geometry as well. + """ + # The OGC Geometry type of the field. + gtype = self._field._geom + + # The geometry type must match that of the field -- unless the + # general GeometryField is used. + if isinstance(value, self._klass) and (str(value.geom_type).upper() == gtype or gtype == 'GEOMETRY'): + # Assigning the SRID to the geometry. + if value.srid is None: value.srid = self._field._srid + elif isinstance(value, (NoneType, StringType, UnicodeType)): + # Set with None, WKT, or HEX + pass + else: + raise TypeError('cannot set %s GeometryProxy with value of type: %s' % (obj.__class__.__name__, type(value))) + + # Setting the objects dictionary with the value, and returning. + obj.__dict__[self._field.attname] = value + return value diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py new file mode 100644 index 0000000000..8efc720333 --- /dev/null +++ b/django/contrib/gis/db/models/query.py @@ -0,0 +1,617 @@ +from django.core.exceptions import ImproperlyConfigured +from django.db import connection +from django.db.models.query import sql, QuerySet, Q + +from django.contrib.gis.db.backend import SpatialBackend +from django.contrib.gis.db.models.fields import GeometryField, PointField +from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery, GeoWhereNode +from django.contrib.gis.measure import Area, Distance +from django.contrib.gis.models import get_srid_info +qn = connection.ops.quote_name + +# For backwards-compatibility; Q object should work just fine +# after queryset-refactor. +class GeoQ(Q): pass + +class GeomSQL(object): + "Simple wrapper object for geometric SQL." + def __init__(self, geo_sql): + self.sql = geo_sql + + def as_sql(self, *args, **kwargs): + return self.sql + +class GeoQuerySet(QuerySet): + "The Geographic QuerySet." + + def __init__(self, model=None, query=None): + super(GeoQuerySet, self).__init__(model=model, query=query) + self.query = query or GeoQuery(self.model, connection) + + def area(self, tolerance=0.05, **kwargs): + """ + Returns the area of the geographic field in an `area` attribute on + each element of this GeoQuerySet. + """ + # Peforming setup here rather than in `_spatial_attribute` so that + # we can get the units for `AreaField`. + procedure_args, geo_field = self._spatial_setup('area', field_name=kwargs.get('field_name', None)) + s = {'procedure_args' : procedure_args, + 'geo_field' : geo_field, + 'setup' : False, + } + if SpatialBackend.oracle: + s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s' + s['procedure_args']['tolerance'] = tolerance + s['select_field'] = AreaField('sq_m') # Oracle returns area in units of meters. + elif SpatialBackend.postgis: + if not geo_field.geodetic: + # Getting the area units of the geographic field. + s['select_field'] = AreaField(Area.unit_attname(geo_field._unit_name)) + else: + # TODO: Do we want to support raw number areas for geodetic fields? + raise Exception('Area on geodetic coordinate systems not supported.') + return self._spatial_attribute('area', s, **kwargs) + + def centroid(self, **kwargs): + """ + Returns the centroid of the geographic field in a `centroid` + attribute on each element of this GeoQuerySet. + """ + return self._geom_attribute('centroid', **kwargs) + + def difference(self, geom, **kwargs): + """ + Returns the spatial difference of the geographic field in a `difference` + attribute on each element of this GeoQuerySet. + """ + return self._geomset_attribute('difference', geom, **kwargs) + + def distance(self, geom, **kwargs): + """ + Returns the distance from the given geographic field name to the + given geometry in a `distance` attribute on each element of the + GeoQuerySet. + + Keyword Arguments: + `spheroid` => If the geometry field is geodetic and PostGIS is + the spatial database, then the more accurate + spheroid calculation will be used instead of the + quicker sphere calculation. + + `tolerance` => Used only for Oracle. The tolerance is + in meters -- a default of 5 centimeters (0.05) + is used. + """ + return self._distance_attribute('distance', geom, **kwargs) + + def envelope(self, **kwargs): + """ + Returns a Geometry representing the bounding box of the + Geometry field in an `envelope` attribute on each element of + the GeoQuerySet. + """ + return self._geom_attribute('envelope', **kwargs) + + def extent(self, **kwargs): + """ + Returns the extent (aggregate) of the features in the GeoQuerySet. The + extent will be returned as a 4-tuple, consisting of (xmin, ymin, xmax, ymax). + """ + convert_extent = None + if SpatialBackend.postgis: + def convert_extent(box, geo_field): + # TODO: Parsing of BOX3D, Oracle support (patches welcome!) + # Box text will be something like "BOX(-90.0 30.0, -85.0 40.0)"; + # parsing out and returning as a 4-tuple. + ll, ur = box[4:-1].split(',') + xmin, ymin = map(float, ll.split()) + xmax, ymax = map(float, ur.split()) + return (xmin, ymin, xmax, ymax) + elif SpatialBackend.oracle: + def convert_extent(wkt, geo_field): + raise NotImplementedError + return self._spatial_aggregate('extent', convert_func=convert_extent, **kwargs) + + def gml(self, precision=8, version=2, **kwargs): + """ + Returns GML representation of the given field in a `gml` attribute + on each element of the GeoQuerySet. + """ + s = {'desc' : 'GML', 'procedure_args' : {'precision' : precision}} + if SpatialBackend.postgis: + # PostGIS AsGML() aggregate function parameter order depends on the + # version -- uggh. + major, minor1, minor2 = SpatialBackend.version + if major >= 1 and (minor1 > 3 or (minor1 == 3 and minor2 > 1)): + procedure_fmt = '%(version)s,%(geo_col)s,%(precision)s' + else: + procedure_fmt = '%(geo_col)s,%(precision)s,%(version)s' + s['procedure_args'] = {'precision' : precision, 'version' : version} + + return self._spatial_attribute('gml', s, **kwargs) + + def intersection(self, geom, **kwargs): + """ + Returns the spatial intersection of the Geometry field in + an `intersection` attribute on each element of this + GeoQuerySet. + """ + return self._geomset_attribute('intersection', geom, **kwargs) + + def kml(self, **kwargs): + """ + Returns KML representation of the geometry field in a `kml` + attribute on each element of this GeoQuerySet. + """ + s = {'desc' : 'KML', + 'procedure_fmt' : '%(geo_col)s,%(precision)s', + 'procedure_args' : {'precision' : kwargs.pop('precision', 8)}, + } + return self._spatial_attribute('kml', s, **kwargs) + + def length(self, **kwargs): + """ + Returns the length of the geometry field as a `Distance` object + stored in a `length` attribute on each element of this GeoQuerySet. + """ + return self._distance_attribute('length', None, **kwargs) + + def make_line(self, **kwargs): + """ + Creates a linestring from all of the PointField geometries in the + this GeoQuerySet and returns it. This is a spatial aggregate + method, and thus returns a geometry rather than a GeoQuerySet. + """ + kwargs['geo_field_type'] = PointField + kwargs['agg_field'] = GeometryField + return self._spatial_aggregate('make_line', **kwargs) + + def mem_size(self, **kwargs): + """ + Returns the memory size (number of bytes) that the geometry field takes + in a `mem_size` attribute on each element of this GeoQuerySet. + """ + return self._spatial_attribute('mem_size', {}, **kwargs) + + def num_geom(self, **kwargs): + """ + Returns the number of geometries if the field is a + GeometryCollection or Multi* Field in a `num_geom` + attribute on each element of this GeoQuerySet; otherwise + the sets with None. + """ + return self._spatial_attribute('num_geom', {}, **kwargs) + + def num_points(self, **kwargs): + """ + Returns the number of points in the first linestring in the + Geometry field in a `num_points` attribute on each element of + this GeoQuerySet; otherwise sets with None. + """ + return self._spatial_attribute('num_points', {}, **kwargs) + + def perimeter(self, **kwargs): + """ + Returns the perimeter of the geometry field as a `Distance` object + stored in a `perimeter` attribute on each element of this GeoQuerySet. + """ + return self._distance_attribute('perimeter', None, **kwargs) + + def point_on_surface(self, **kwargs): + """ + Returns a Point geometry guaranteed to lie on the surface of the + Geometry field in a `point_on_surface` attribute on each element + of this GeoQuerySet; otherwise sets with None. + """ + return self._geom_attribute('point_on_surface', **kwargs) + + def scale(self, x, y, z=0.0, **kwargs): + """ + Scales the geometry to a new size by multiplying the ordinates + with the given x,y,z scale factors. + """ + s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s', + 'procedure_args' : {'x' : x, 'y' : y, 'z' : z}, + 'select_field' : GeomField(), + } + return self._spatial_attribute('scale', s, **kwargs) + + def svg(self, **kwargs): + """ + Returns SVG representation of the geographic field in a `svg` + attribute on each element of this GeoQuerySet. + """ + s = {'desc' : 'SVG', + 'procedure_fmt' : '%(geo_col)s,%(rel)s,%(precision)s', + 'procedure_args' : {'rel' : int(kwargs.pop('relative', 0)), + 'precision' : kwargs.pop('precision', 8)}, + } + return self._spatial_attribute('svg', s, **kwargs) + + def sym_difference(self, geom, **kwargs): + """ + Returns the symmetric difference of the geographic field in a + `sym_difference` attribute on each element of this GeoQuerySet. + """ + return self._geomset_attribute('sym_difference', geom, **kwargs) + + def translate(self, x, y, z=0.0, **kwargs): + """ + Translates the geometry to a new location using the given numeric + parameters as offsets. + """ + s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s', + 'procedure_args' : {'x' : x, 'y' : y, 'z' : z}, + 'select_field' : GeomField(), + } + return self._spatial_attribute('translate', s, **kwargs) + + def transform(self, srid=4326, **kwargs): + """ + Transforms the given geometry field to the given SRID. If no SRID is + provided, the transformation will default to using 4326 (WGS84). + """ + if not isinstance(srid, (int, long)): + raise TypeError('An integer SRID must be provided.') + field_name = kwargs.get('field_name', None) + tmp, geo_field = self._spatial_setup('transform', field_name=field_name) + + # Getting the selection SQL for the given geographic field. + field_col = self._geocol_select(geo_field, field_name) + + # Why cascading substitutions? Because spatial backends like + # Oracle and MySQL already require a function call to convert to text, thus + # when there's also a transformation we need to cascade the substitutions. + # For example, 'SDO_UTIL.TO_WKTGEOMETRY(SDO_CS.TRANSFORM( ... )' + geo_col = self.query.custom_select.get(geo_field, field_col) + + # Setting the key for the field's column with the custom SELECT SQL to + # override the geometry column returned from the database. + custom_sel = '%s(%s, %s)' % (SpatialBackend.transform, geo_col, srid) + # TODO: Should we have this as an alias? + # custom_sel = '(%s(%s, %s)) AS %s' % (SpatialBackend.transform, geo_col, srid, qn(geo_field.name)) + self.query.transformed_srid = srid # So other GeoQuerySet methods + self.query.custom_select[geo_field] = custom_sel + return self._clone() + + def union(self, geom, **kwargs): + """ + Returns the union of the geographic field with the given + Geometry in a `union` attribute on each element of this GeoQuerySet. + """ + return self._geomset_attribute('union', geom, **kwargs) + + def unionagg(self, **kwargs): + """ + Performs an aggregate union on the given geometry field. Returns + None if the GeoQuerySet is empty. The `tolerance` keyword is for + Oracle backends only. + """ + kwargs['agg_field'] = GeometryField + return self._spatial_aggregate('unionagg', **kwargs) + + ### Private API -- Abstracted DRY routines. ### + def _spatial_setup(self, att, aggregate=False, desc=None, field_name=None, geo_field_type=None): + """ + Performs set up for executing the spatial function. + """ + # Does the spatial backend support this? + func = getattr(SpatialBackend, att, False) + if desc is None: desc = att + if not func: raise ImproperlyConfigured('%s stored procedure not available.' % desc) + + # Initializing the procedure arguments. + procedure_args = {'function' : func} + + # Is there a geographic field in the model to perform this + # operation on? + geo_field = self.query._geo_field(field_name) + if not geo_field: + raise TypeError('%s output only available on GeometryFields.' % func) + + # If the `geo_field_type` keyword was used, then enforce that + # type limitation. + if not geo_field_type is None and not isinstance(geo_field, geo_field_type): + raise TypeError('"%s" stored procedures may only be called on %ss.' % (func, geo_field_type.__name__)) + + # Setting the procedure args. + procedure_args['geo_col'] = self._geocol_select(geo_field, field_name, aggregate) + + return procedure_args, geo_field + + def _spatial_aggregate(self, att, field_name=None, + agg_field=None, convert_func=None, + geo_field_type=None, tolerance=0.0005): + """ + DRY routine for calling aggregate spatial stored procedures and + returning their result to the caller of the function. + """ + # Constructing the setup keyword arguments. + setup_kwargs = {'aggregate' : True, + 'field_name' : field_name, + 'geo_field_type' : geo_field_type, + } + procedure_args, geo_field = self._spatial_setup(att, **setup_kwargs) + + if SpatialBackend.oracle: + procedure_args['tolerance'] = tolerance + # Adding in selection SQL for Oracle geometry columns. + if agg_field is GeometryField: + agg_sql = '%s' % SpatialBackend.select + else: + agg_sql = '%s' + agg_sql = agg_sql % ('%(function)s(SDOAGGRTYPE(%(geo_col)s,%(tolerance)s))' % procedure_args) + else: + agg_sql = '%(function)s(%(geo_col)s)' % procedure_args + + # Wrapping our selection SQL in `GeomSQL` to bypass quoting, and + # specifying the type of the aggregate field. + self.query.select = [GeomSQL(agg_sql)] + self.query.select_fields = [agg_field] + + try: + # `asql` => not overriding `sql` module. + asql, params = self.query.as_sql() + except sql.datastructures.EmptyResultSet: + return None + + # Getting a cursor, executing the query, and extracting the returned + # value from the aggregate function. + cursor = connection.cursor() + cursor.execute(asql, params) + result = cursor.fetchone()[0] + + # If the `agg_field` is specified as a GeometryField, then autmatically + # set up the conversion function. + if agg_field is GeometryField and not callable(convert_func): + if SpatialBackend.postgis: + def convert_geom(hex, geo_field): + if hex: return SpatialBackend.Geometry(hex) + else: return None + elif SpatialBackend.oracle: + def convert_geom(clob, geo_field): + if clob: return SpatialBackend.Geometry(clob.read(), geo_field._srid) + else: return None + convert_func = convert_geom + + # Returning the callback function evaluated on the result culled + # from the executed cursor. + if callable(convert_func): + return convert_func(result, geo_field) + else: + return result + + def _spatial_attribute(self, att, settings, field_name=None, model_att=None): + """ + DRY routine for calling a spatial stored procedure on a geometry column + and attaching its output as an attribute of the model. + + Arguments: + att: + The name of the spatial attribute that holds the spatial + SQL function to call. + + settings: + Dictonary of internal settings to customize for the spatial procedure. + + Public Keyword Arguments: + + field_name: + The name of the geographic field to call the spatial + function on. May also be a lookup to a geometry field + as part of a foreign key relation. + + model_att: + The name of the model attribute to attach the output of + the spatial function to. + """ + # Default settings. + settings.setdefault('desc', None) + settings.setdefault('geom_args', ()) + settings.setdefault('geom_field', None) + settings.setdefault('procedure_args', {}) + settings.setdefault('procedure_fmt', '%(geo_col)s') + settings.setdefault('select_params', []) + + # Performing setup for the spatial column, unless told not to. + if settings.get('setup', True): + default_args, geo_field = self._spatial_setup(att, desc=settings['desc'], field_name=field_name) + for k, v in default_args.iteritems(): settings['procedure_args'].setdefault(k, v) + else: + geo_field = settings['geo_field'] + + # The attribute to attach to the model. + if not isinstance(model_att, basestring): model_att = att + + # Special handling for any argument that is a geometry. + for name in settings['geom_args']: + # Using the field's get_db_prep_lookup() to get any needed + # transformation SQL -- we pass in a 'dummy' `contains` lookup. + where, params = geo_field.get_db_prep_lookup('contains', settings['procedure_args'][name]) + # Replacing the procedure format with that of any needed + # transformation SQL. + old_fmt = '%%(%s)s' % name + new_fmt = where[0] % '%%s' + settings['procedure_fmt'] = settings['procedure_fmt'].replace(old_fmt, new_fmt) + settings['select_params'].extend(params) + + # Getting the format for the stored procedure. + fmt = '%%(function)s(%s)' % settings['procedure_fmt'] + + # If the result of this function needs to be converted. + if settings.get('select_field', False): + sel_fld = settings['select_field'] + if isinstance(sel_fld, GeomField) and SpatialBackend.select: + self.query.custom_select[model_att] = SpatialBackend.select + self.query.extra_select_fields[model_att] = sel_fld + + # Finally, setting the extra selection attribute with + # the format string expanded with the stored procedure + # arguments. + return self.extra(select={model_att : fmt % settings['procedure_args']}, + select_params=settings['select_params']) + + def _distance_attribute(self, func, geom=None, tolerance=0.05, spheroid=False, **kwargs): + """ + DRY routine for GeoQuerySet distance attribute routines. + """ + # Setting up the distance procedure arguments. + procedure_args, geo_field = self._spatial_setup(func, field_name=kwargs.get('field_name', None)) + + # If geodetic defaulting distance attribute to meters (Oracle and + # PostGIS spherical distances return meters). Otherwise, use the + # units of the geometry field. + if geo_field.geodetic: + dist_att = 'm' + else: + dist_att = Distance.unit_attname(geo_field._unit_name) + + # Shortcut booleans for what distance function we're using. + distance = func == 'distance' + length = func == 'length' + perimeter = func == 'perimeter' + if not (distance or length or perimeter): + raise ValueError('Unknown distance function: %s' % func) + + # The field's get_db_prep_lookup() is used to get any + # extra distance parameters. Here we set up the + # parameters that will be passed in to field's function. + lookup_params = [geom or 'POINT (0 0)', 0] + + # If the spheroid calculation is desired, either by the `spheroid` + # keyword or wehn calculating the length of geodetic field, make + # sure the 'spheroid' distance setting string is passed in so we + # get the correct spatial stored procedure. + if spheroid or (SpatialBackend.postgis and geo_field.geodetic and length): + lookup_params.append('spheroid') + where, params = geo_field.get_db_prep_lookup('distance_lte', lookup_params) + + # The `geom_args` flag is set to true if a geometry parameter was + # passed in. + geom_args = bool(geom) + + if SpatialBackend.oracle: + if distance: + procedure_fmt = '%(geo_col)s,%(geom)s,%(tolerance)s' + elif length or perimeter: + procedure_fmt = '%(geo_col)s,%(tolerance)s' + procedure_args['tolerance'] = tolerance + else: + # Getting whether this field is in units of degrees since the field may have + # been transformed via the `transform` GeoQuerySet method. + if self.query.transformed_srid: + u, unit_name, s = get_srid_info(self.query.transformed_srid) + geodetic = unit_name in geo_field.geodetic_units + else: + geodetic = geo_field.geodetic + + if distance: + if self.query.transformed_srid: + # Setting the `geom_args` flag to false because we want to handle + # transformation SQL here, rather than the way done by default + # (which will transform to the original SRID of the field rather + # than to what was transformed to). + geom_args = False + procedure_fmt = '%s(%%(geo_col)s, %s)' % (SpatialBackend.transform, self.query.transformed_srid) + if geom.srid is None or geom.srid == self.query.transformed_srid: + # If the geom parameter srid is None, it is assumed the coordinates + # are in the transformed units. A placeholder is used for the + # geometry parameter. + procedure_fmt += ', %%s' + else: + # We need to transform the geom to the srid specified in `transform()`, + # so wrapping the geometry placeholder in transformation SQL. + procedure_fmt += ', %s(%%%%s, %s)' % (SpatialBackend.transform, self.query.transformed_srid) + else: + # `transform()` was not used on this GeoQuerySet. + procedure_fmt = '%(geo_col)s,%(geom)s' + + if geodetic: + # Spherical distance calculation is needed (because the geographic + # field is geodetic). However, the PostGIS ST_distance_sphere/spheroid() + # procedures may only do queries from point columns to point geometries + # some error checking is required. + if not isinstance(geo_field, PointField): + raise TypeError('Spherical distance calculation only supported on PointFields.') + if not str(SpatialBackend.Geometry(buffer(params[0].wkb)).geom_type) == 'Point': + raise TypeError('Spherical distance calculation only supported with Point Geometry parameters') + # The `function` procedure argument needs to be set differently for + # geodetic distance calculations. + if spheroid: + # Call to distance_spheroid() requires spheroid param as well. + procedure_fmt += ',%(spheroid)s' + procedure_args.update({'function' : SpatialBackend.distance_spheroid, 'spheroid' : where[1]}) + else: + procedure_args.update({'function' : SpatialBackend.distance_sphere}) + elif length or perimeter: + procedure_fmt = '%(geo_col)s' + if geodetic and length: + # There's no `length_sphere` + procedure_fmt += ',%(spheroid)s' + procedure_args.update({'function' : SpatialBackend.length_spheroid, 'spheroid' : where[1]}) + + # Setting up the settings for `_spatial_attribute`. + s = {'select_field' : DistanceField(dist_att), + 'setup' : False, + 'geo_field' : geo_field, + 'procedure_args' : procedure_args, + 'procedure_fmt' : procedure_fmt, + } + if geom_args: + s['geom_args'] = ('geom',) + s['procedure_args']['geom'] = geom + elif geom: + # The geometry is passed in as a parameter because we handled + # transformation conditions in this routine. + s['select_params'] = [SpatialBackend.Adaptor(geom)] + return self._spatial_attribute(func, s, **kwargs) + + def _geom_attribute(self, func, tolerance=0.05, **kwargs): + """ + DRY routine for setting up a GeoQuerySet method that attaches a + Geometry attribute (e.g., `centroid`, `point_on_surface`). + """ + s = {'select_field' : GeomField(),} + if SpatialBackend.oracle: + s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s' + s['procedure_args'] = {'tolerance' : tolerance} + return self._spatial_attribute(func, s, **kwargs) + + def _geomset_attribute(self, func, geom, tolerance=0.05, **kwargs): + """ + DRY routine for setting up a GeoQuerySet method that attaches a + Geometry attribute and takes a Geoemtry parameter. This is used + for geometry set-like operations (e.g., intersection, difference, + union, sym_difference). + """ + s = {'geom_args' : ('geom',), + 'select_field' : GeomField(), + 'procedure_fmt' : '%(geo_col)s,%(geom)s', + 'procedure_args' : {'geom' : geom}, + } + if SpatialBackend.oracle: + s['procedure_fmt'] += ',%(tolerance)s' + s['procedure_args']['tolerance'] = tolerance + return self._spatial_attribute(func, s, **kwargs) + + def _geocol_select(self, geo_field, field_name, aggregate=False): + """ + Helper routine for constructing the SQL to select the geographic + column. Takes into account if the geographic field is in a + ForeignKey relation to the current model. + """ + # If this is an aggregate spatial query, the flag needs to be + # set on the `GeoQuery` object of this queryset. + if aggregate: self.query.aggregate = True + + # Is this operation going to be on a related geographic field? + if not geo_field in self.model._meta.fields: + # If so, it'll have to be added to the select related information + # (e.g., if 'location__point' was given as the field name). + self.query.add_select_related([field_name]) + self.query.pre_sql_setup() + rel_table, rel_col = self.query.related_select_cols[self.query.related_select_fields.index(geo_field)] + return self.query._field_column(geo_field, rel_table) + else: + return self.query._field_column(geo_field) diff --git a/django/contrib/gis/db/models/sql/__init__.py b/django/contrib/gis/db/models/sql/__init__.py new file mode 100644 index 0000000000..4a66b41664 --- /dev/null +++ b/django/contrib/gis/db/models/sql/__init__.py @@ -0,0 +1,2 @@ +from django.contrib.gis.db.models.sql.query import AreaField, DistanceField, GeomField, GeoQuery +from django.contrib.gis.db.models.sql.where import GeoWhereNode diff --git a/django/contrib/gis/db/models/sql/query.py b/django/contrib/gis/db/models/sql/query.py new file mode 100644 index 0000000000..b4892e3c15 --- /dev/null +++ b/django/contrib/gis/db/models/sql/query.py @@ -0,0 +1,327 @@ +from itertools import izip +from django.db.models.query import sql +from django.db.models.fields import FieldDoesNotExist +from django.db.models.fields.related import ForeignKey + +from django.contrib.gis.db.backend import SpatialBackend +from django.contrib.gis.db.models.fields import GeometryField +from django.contrib.gis.db.models.sql.where import GeoWhereNode +from django.contrib.gis.measure import Area, Distance + +# Valid GIS query types. +ALL_TERMS = sql.constants.QUERY_TERMS.copy() +ALL_TERMS.update(SpatialBackend.gis_terms) + +class GeoQuery(sql.Query): + """ + A single spatial SQL query. + """ + # Overridding the valid query terms. + query_terms = ALL_TERMS + + #### Methods overridden from the base Query class #### + def __init__(self, model, conn): + super(GeoQuery, self).__init__(model, conn, where=GeoWhereNode) + # The following attributes are customized for the GeoQuerySet. + # The GeoWhereNode and SpatialBackend classes contain backend-specific + # routines and functions. + self.aggregate = False + self.custom_select = {} + self.transformed_srid = None + self.extra_select_fields = {} + + def clone(self, *args, **kwargs): + obj = super(GeoQuery, self).clone(*args, **kwargs) + # Customized selection dictionary and transformed srid flag have + # to also be added to obj. + obj.aggregate = self.aggregate + obj.custom_select = self.custom_select.copy() + obj.transformed_srid = self.transformed_srid + obj.extra_select_fields = self.extra_select_fields.copy() + return obj + + def get_columns(self, with_aliases=False): + """ + Return the list of columns to use in the select statement. If no + columns have been specified, returns all columns relating to fields in + the model. + + If 'with_aliases' is true, any column names that are duplicated + (without the table names) are given unique aliases. This is needed in + some cases to avoid ambiguitity with nested queries. + + This routine is overridden from Query to handle customized selection of + geometry columns. + """ + qn = self.quote_name_unless_alias + qn2 = self.connection.ops.quote_name + result = ['(%s) AS %s' % (self.get_extra_select_format(alias) % col, qn2(alias)) + for alias, col in self.extra_select.iteritems()] + aliases = set(self.extra_select.keys()) + if with_aliases: + col_aliases = aliases.copy() + else: + col_aliases = set() + if self.select: + # This loop customized for GeoQuery. + for col, field in izip(self.select, self.select_fields): + if isinstance(col, (list, tuple)): + r = self.get_field_select(field, col[0]) + if with_aliases and col[1] in col_aliases: + c_alias = 'Col%d' % len(col_aliases) + result.append('%s AS %s' % (r, c_alias)) + aliases.add(c_alias) + col_aliases.add(c_alias) + else: + result.append(r) + aliases.add(r) + col_aliases.add(col[1]) + else: + result.append(col.as_sql(quote_func=qn)) + if hasattr(col, 'alias'): + aliases.add(col.alias) + col_aliases.add(col.alias) + elif self.default_cols: + cols, new_aliases = self.get_default_columns(with_aliases, + col_aliases) + result.extend(cols) + aliases.update(new_aliases) + # This loop customized for GeoQuery. + if not self.aggregate: + for (table, col), field in izip(self.related_select_cols, self.related_select_fields): + r = self.get_field_select(field, table) + if with_aliases and col in col_aliases: + c_alias = 'Col%d' % len(col_aliases) + result.append('%s AS %s' % (r, c_alias)) + aliases.add(c_alias) + col_aliases.add(c_alias) + else: + result.append(r) + aliases.add(r) + col_aliases.add(col) + + self._select_aliases = aliases + return result + + def get_default_columns(self, with_aliases=False, col_aliases=None, + start_alias=None, opts=None, as_pairs=False): + """ + Computes the default columns for selecting every field in the base + model. + + Returns a list of strings, quoted appropriately for use in SQL + directly, as well as a set of aliases used in the select statement. + + This routine is overridden from Query to handle customized selection of + geometry columns. + """ + result = [] + if opts is None: + opts = self.model._meta + if start_alias: + table_alias = start_alias + else: + table_alias = self.tables[0] + root_pk = self.model._meta.pk.column + seen = {None: table_alias} + aliases = set() + for field, model in opts.get_fields_with_model(): + try: + alias = seen[model] + except KeyError: + alias = self.join((table_alias, model._meta.db_table, + root_pk, model._meta.pk.column)) + seen[model] = alias + if as_pairs: + result.append((alias, field.column)) + continue + # This part of the function is customized for GeoQuery. We + # see if there was any custom selection specified in the + # dictionary, and set up the selection format appropriately. + field_sel = self.get_field_select(field, alias) + if with_aliases and field.column in col_aliases: + c_alias = 'Col%d' % len(col_aliases) + result.append('%s AS %s' % (field_sel, c_alias)) + col_aliases.add(c_alias) + aliases.add(c_alias) + else: + r = field_sel + result.append(r) + aliases.add(r) + if with_aliases: + col_aliases.add(field.column) + if as_pairs: + return result, None + return result, aliases + + def get_ordering(self): + """ + This routine is overridden to disable ordering for aggregate + spatial queries. + """ + if not self.aggregate: + return super(GeoQuery, self).get_ordering() + else: + return () + + def resolve_columns(self, row, fields=()): + """ + This routine is necessary so that distances and geometries returned + from extra selection SQL get resolved appropriately into Python + objects. + """ + values = [] + aliases = self.extra_select.keys() + index_start = len(aliases) + values = [self.convert_values(v, self.extra_select_fields.get(a, None)) + for v, a in izip(row[:index_start], aliases)] + if SpatialBackend.oracle: + # This is what happens normally in Oracle's `resolve_columns`. + for value, field in izip(row[index_start:], fields): + values.append(self.convert_values(value, field)) + else: + values.extend(row[index_start:]) + return values + + def convert_values(self, value, field): + """ + Using the same routines that Oracle does we can convert our + extra selection objects into Geometry and Distance objects. + TODO: Laziness. + """ + if SpatialBackend.oracle: + # Running through Oracle's first. + value = super(GeoQuery, self).convert_values(value, field) + if isinstance(field, DistanceField): + # Using the field's distance attribute, can instantiate + # `Distance` with the right context. + value = Distance(**{field.distance_att : value}) + elif isinstance(field, AreaField): + value = Area(**{field.area_att : value}) + elif isinstance(field, GeomField): + value = SpatialBackend.Geometry(value) + return value + + #### Routines unique to GeoQuery #### + def get_extra_select_format(self, alias): + sel_fmt = '%s' + if alias in self.custom_select: + sel_fmt = sel_fmt % self.custom_select[alias] + return sel_fmt + + def get_field_select(self, fld, alias=None): + """ + Returns the SELECT SQL string for the given field. Figures out + if any custom selection SQL is needed for the column The `alias` + keyword may be used to manually specify the database table where + the column exists, if not in the model associated with this + `GeoQuery`. + """ + sel_fmt = self.get_select_format(fld) + if fld in self.custom_select: + field_sel = sel_fmt % self.custom_select[fld] + else: + field_sel = sel_fmt % self._field_column(fld, alias) + return field_sel + + def get_select_format(self, fld): + """ + Returns 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 WKT. For all + other fields a simple '%s' format string is returned. + """ + if SpatialBackend.select and hasattr(fld, '_geom'): + # This allows operations to be done on fields in the SELECT, + # overriding their values -- used by the Oracle and MySQL + # spatial backends to get database values as WKT, and by the + # `transform` method. + sel_fmt = SpatialBackend.select + + # Because WKT doesn't contain spatial reference information, + # the SRID is prefixed to the returned WKT to ensure that the + # transformed geometries have an SRID different than that of the + # field -- this is only used by `transform` for Oracle backends. + if self.transformed_srid and SpatialBackend.oracle: + sel_fmt = "'SRID=%d;'||%s" % (self.transformed_srid, sel_fmt) + else: + sel_fmt = '%s' + return sel_fmt + + # Private API utilities, subject to change. + def _check_geo_field(self, model, name_param): + """ + Recursive utility routine for checking the given name parameter + on the given model. Initially, the name parameter is a string, + of the field on the given model e.g., 'point', 'the_geom'. + Related model field strings like 'address__point', may also be + used. + + If a GeometryField exists according to the given name parameter + it will be returned, otherwise returns False. + """ + if isinstance(name_param, basestring): + # This takes into account the situation where the name is a + # lookup to a related geographic field, e.g., 'address__point'. + name_param = name_param.split(sql.constants.LOOKUP_SEP) + name_param.reverse() # Reversing so list operates like a queue of related lookups. + elif not isinstance(name_param, list): + raise TypeError + try: + # Getting the name of the field for the model (by popping the first + # name from the `name_param` list created above). + fld, mod, direct, m2m = model._meta.get_field_by_name(name_param.pop()) + except (FieldDoesNotExist, IndexError): + return False + # TODO: ManyToManyField? + if isinstance(fld, GeometryField): + return fld # A-OK. + elif isinstance(fld, ForeignKey): + # ForeignKey encountered, return the output of this utility called + # on the _related_ model with the remaining name parameters. + return self._check_geo_field(fld.rel.to, name_param) # Recurse to check ForeignKey relation. + else: + return False + + def _field_column(self, field, table_alias=None): + """ + Helper function that returns the database column for the given field. + The table and column are returned (quoted) in the proper format, e.g., + `"geoapp_city"."point"`. If `table_alias` is not specified, the + database table associated with the model of this `GeoQuery` will be + used. + """ + if table_alias is None: table_alias = self.model._meta.db_table + return "%s.%s" % (self.quote_name_unless_alias(table_alias), + self.connection.ops.quote_name(field.column)) + + def _geo_field(self, field_name=None): + """ + Returns the first Geometry field encountered; or specified via the + `field_name` keyword. The `field_name` may be a string specifying + the geometry field on this GeoQuery's model, or a lookup string + to a geometry field via a ForeignKey relation. + """ + if field_name is None: + # Incrementing until the first geographic field is found. + for fld in self.model._meta.fields: + if isinstance(fld, GeometryField): return fld + return False + else: + # Otherwise, check by the given field name -- which may be + # a lookup to a _related_ geographic field. + return self._check_geo_field(self.model, field_name) + +### Field Classes for `convert_values` #### +class AreaField(object): + def __init__(self, area_att): + self.area_att = area_att + +class DistanceField(object): + def __init__(self, distance_att): + self.distance_att = distance_att + +# Rather than use GeometryField (which requires a SQL query +# upon instantiation), use this lighter weight class. +class GeomField(object): + pass diff --git a/django/contrib/gis/db/models/sql/where.py b/django/contrib/gis/db/models/sql/where.py new file mode 100644 index 0000000000..a1a28d9511 --- /dev/null +++ b/django/contrib/gis/db/models/sql/where.py @@ -0,0 +1,64 @@ +import datetime +from django.db.models.fields import Field +from django.db.models.sql.where import WhereNode +from django.contrib.gis.db.backend import get_geo_where_clause, SpatialBackend + +class GeoAnnotation(object): + """ + The annotation used for GeometryFields; basically a placeholder + for metadata needed by the `get_geo_where_clause` of the spatial + backend. + """ + def __init__(self, field, value, where): + self.geodetic = field.geodetic + self.geom_type = field._geom + self.value = value + self.where = tuple(where) + +class GeoWhereNode(WhereNode): + """ + Used to represent the SQL where-clause for spatial databases -- + these are tied to the GeoQuery class that created it. + """ + def add(self, data, connector): + """ + This is overridden from the regular WhereNode to handle the + peculiarties of GeometryFields, because they need a special + annotation object that contains the spatial metadata from the + field to generate the spatial SQL. + """ + if not isinstance(data, (list, tuple)): + return super(WhereNode, self).add(data, connector) + alias, col, field, lookup_type, value = data + if not hasattr(field, "_geom"): + # Not a geographic field, so call `WhereNode.add`. + return super(GeoWhereNode, self).add(data, connector) + else: + # `GeometryField.get_db_prep_lookup` returns a where clause + # substitution array in addition to the parameters. + where, params = field.get_db_prep_lookup(lookup_type, value) + + # The annotation will be a `GeoAnnotation` object that + # will contain the necessary geometry field metadata for + # the `get_geo_where_clause` to construct the appropriate + # spatial SQL when `make_atom` is called. + annotation = GeoAnnotation(field, value, where) + return super(WhereNode, self).add((alias, col, field.db_type(), lookup_type, + annotation, params), connector) + + def make_atom(self, child, qn): + table_alias, name, db_type, lookup_type, value_annot, params = child + + if isinstance(value_annot, GeoAnnotation): + if lookup_type in SpatialBackend.gis_terms: + # Getting the geographic where clause; substitution parameters + # will be populated in the GeoFieldSQL object returned by the + # GeometryField. + gwc = get_geo_where_clause(table_alias, name, lookup_type, value_annot) + return gwc % value_annot.where, params + else: + raise TypeError('Invalid lookup type: %r' % lookup_type) + else: + # If not a GeometryField, call the `make_atom` from the + # base class. + return super(GeoWhereNode, self).make_atom(child, qn) diff --git a/django/contrib/gis/forms/__init__.py b/django/contrib/gis/forms/__init__.py new file mode 100644 index 0000000000..5441e6078d --- /dev/null +++ b/django/contrib/gis/forms/__init__.py @@ -0,0 +1 @@ +from django.contrib.gis.forms.fields import GeometryField diff --git a/django/contrib/gis/forms/fields.py b/django/contrib/gis/forms/fields.py new file mode 100644 index 0000000000..a65e76d5d4 --- /dev/null +++ b/django/contrib/gis/forms/fields.py @@ -0,0 +1,37 @@ +from django import forms +from django.contrib.gis.geos import GEOSGeometry, GEOSException +from django.utils.translation import ugettext_lazy as _ + +class GeometryField(forms.Field): + # By default a Textarea widget is used. + widget = forms.Textarea + + default_error_messages = { + 'no_geom' : _(u'No geometry value provided.'), + 'invalid_geom' : _(u'Invalid Geometry value.'), + 'invalid_geom_type' : _(u'Invalid Geometry type.'), + } + def __init__(self, **kwargs): + self.null = kwargs.pop('null') + self.geom_type = kwargs.pop('geom_type') + super(GeometryField, self).__init__(**kwargs) + + def clean(self, value): + """ + Validates that the input value can be converted to a Geometry + object (which is returned). A ValidationError is raised if + the value cannot be instantiated as a Geometry. + """ + if not value: + if self.null: + # The geometry column allows NULL, return None. + return None + else: + raise forms.ValidationError(self.error_messages['no_geom']) + try: + geom = GEOSGeometry(value) + if geom.geom_type.upper() != self.geom_type: + raise forms.ValidationError(self.error_messages['invalid_geom_type']) + return geom + except GEOSException: + raise forms.ValidationError(self.error_messages['invalid_geom']) diff --git a/django/contrib/gis/gdal/LICENSE b/django/contrib/gis/gdal/LICENSE new file mode 100644 index 0000000000..4bd0bed44b --- /dev/null +++ b/django/contrib/gis/gdal/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2007, 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. + diff --git a/django/contrib/gis/gdal/__init__.py b/django/contrib/gis/gdal/__init__.py new file mode 100644 index 0000000000..cedf165ef1 --- /dev/null +++ b/django/contrib/gis/gdal/__init__.py @@ -0,0 +1,51 @@ +""" + 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: Layer for accessing OGR Geometry objects. + + 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. + + GDAL links to a large number of external libraries that consume RAM when + loaded. Thus, it may desirable to disable GDAL on systems with limited + RAM resources -- this may be accomplished by setting `GDAL_LIBRARY_PATH` + to a non-existant file location (e.g., `GDAL_LIBRARY_PATH='/null/path'`; + setting to None/False/'' will not work as a string must be given). +""" +# Attempting to import objects that depend on the GDAL library. The +# HAS_GDAL flag will be set to True if the library is present on +# the system. +try: + from django.contrib.gis.gdal.driver import Driver + from django.contrib.gis.gdal.datasource import DataSource + from django.contrib.gis.gdal.libgdal import gdal_version, gdal_full_version, gdal_release_date + from django.contrib.gis.gdal.srs import SpatialReference, CoordTransform + from django.contrib.gis.gdal.geometries import OGRGeometry, GEOJSON + HAS_GDAL = True +except: + HAS_GDAL, GEOJSON = False, False + +# The envelope, error, and geomtype modules do not actually require the +# GDAL library. +from django.contrib.gis.gdal.envelope import Envelope +from django.contrib.gis.gdal.error import check_err, OGRException, OGRIndexError, SRSException +from django.contrib.gis.gdal.geomtype import OGRGeomType diff --git a/django/contrib/gis/gdal/datasource.py b/django/contrib/gis/gdal/datasource.py new file mode 100644 index 0000000000..02363a075b --- /dev/null +++ b/django/contrib/gis/gdal/datasource.py @@ -0,0 +1,138 @@ +""" + 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 +""" +# ctypes prerequisites. +from ctypes import byref, c_void_p + +# The GDAL C library, OGR exceptions, and the Layer object. +from django.contrib.gis.gdal.driver import Driver +from django.contrib.gis.gdal.error import OGRException, OGRIndexError +from django.contrib.gis.gdal.layer import Layer + +# Getting the ctypes prototypes for the DataSource. +from django.contrib.gis.gdal.prototypes.ds import \ + destroy_ds, get_driver_count, register_all, open_ds, release_ds, \ + get_ds_name, get_layer, get_layer_count, get_layer_by_name + +# For more information, see the OGR C API source code: +# http://www.gdal.org/ogr/ogr__api_8h.html +# +# The OGR_DS_* routines are relevant here. +class DataSource(object): + "Wraps an OGR Data Source object." + + #### Python 'magic' routines #### + def __init__(self, ds_input, ds_driver=False, write=False): + + # DataSource pointer is initially NULL. + self._ptr = None + + # The write flag. + if write: + self._write = 1 + else: + self._write = 0 + + # Registering all the drivers, this needs to be done + # _before_ we try to open up a data source. + if not get_driver_count(): register_all() + + if isinstance(ds_input, basestring): + # The data source driver is a void pointer. + ds_driver = c_void_p() + try: + # OGROpen will auto-detect the data source type. + ds = open_ds(ds_input, self._write, byref(ds_driver)) + except OGRException: + # Making the error message more clear rather than something + # like "Invalid pointer returned from OGROpen". + raise OGRException('Could not open the datasource at "%s"' % ds_input) + elif isinstance(ds_input, c_void_p) and isinstance(ds_driver, c_void_p): + ds = ds_input + else: + raise OGRException('Invalid data source input type: %s' % type(ds_input)) + + if bool(ds): + self._ptr = ds + self._driver = Driver(ds_driver) + else: + # Raise an exception if the returned pointer is NULL + raise OGRException('Invalid data source file "%s"' % ds_input) + + def __del__(self): + "Destroys this DataStructure object." + if self._ptr: destroy_ds(self._ptr) + + def __iter__(self): + "Allows for iteration over the layers in a data source." + for i in xrange(self.layer_count): + yield self[i] + + def __getitem__(self, index): + "Allows use of the index [] operator to get a layer at the index." + if isinstance(index, basestring): + l = get_layer_by_name(self._ptr, index) + if not l: raise OGRIndexError('invalid OGR Layer name given: "%s"' % index) + elif isinstance(index, int): + if index < 0 or index >= self.layer_count: + raise OGRIndexError('index out of range') + l = get_layer(self._ptr, index) + else: + raise TypeError('Invalid index type: %s' % type(index)) + return Layer(l) + + def __len__(self): + "Returns the number of layers within the data source." + return self.layer_count + + def __str__(self): + "Returns OGR GetName and Driver for the Data Source." + return '%s (%s)' % (self.name, str(self.driver)) + + #### DataSource Properties #### + @property + def driver(self): + "Returns the Driver object for this Data Source." + return self._driver + + @property + def layer_count(self): + "Returns the number of layers in the data source." + return get_layer_count(self._ptr) + + @property + def name(self): + "Returns the name of the data source." + return get_ds_name(self._ptr) diff --git a/django/contrib/gis/gdal/driver.py b/django/contrib/gis/gdal/driver.py new file mode 100644 index 0000000000..2d5b3df807 --- /dev/null +++ b/django/contrib/gis/gdal/driver.py @@ -0,0 +1,66 @@ +# prerequisites imports +from ctypes import c_void_p +from django.contrib.gis.gdal.error import OGRException +from django.contrib.gis.gdal.prototypes.ds import \ + get_driver, get_driver_by_name, get_driver_count, get_driver_name, register_all + +# For more information, see the OGR C API source code: +# http://www.gdal.org/ogr/ogr__api_8h.html +# +# The OGR_Dr_* routines are relevant here. +class Driver(object): + "Wraps an OGR Data Source Driver." + + # Case-insensitive aliases for OGR Drivers. + _alias = {'esri' : 'ESRI Shapefile', + 'shp' : 'ESRI Shapefile', + 'shape' : 'ESRI Shapefile', + 'tiger' : 'TIGER', + 'tiger/line' : 'TIGER', + } + + def __init__(self, dr_input): + "Initializes an OGR driver on either a string or integer input." + + if isinstance(dr_input, basestring): + # If a string name of the driver was passed in + self._ptr = None # Initially NULL + self._register() + + # 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 OGR driver by the string name. + dr = get_driver_by_name(name) + elif isinstance(dr_input, int): + self._register() + dr = get_driver(dr_input) + elif isinstance(dr_input, c_void_p): + dr = dr_input + else: + raise OGRException('Unrecognized input type for OGR Driver: %s' % str(type(dr_input))) + + # Making sure we get a valid pointer to the OGR Driver + if not dr: + raise OGRException('Could not initialize OGR Driver on input: %s' % str(dr_input)) + self._ptr = dr + + def __str__(self): + "Returns the string name of the OGR Driver." + return get_driver_name(self._ptr) + + def _register(self): + "Attempts to register all the data source drivers." + # Only register all if the driver count is 0 (or else all drivers + # will be registered over and over again) + if not self.driver_count: register_all() + + # Driver properties + @property + def driver_count(self): + "Returns the number of OGR data source drivers registered." + return get_driver_count() diff --git a/django/contrib/gis/gdal/envelope.py b/django/contrib/gis/gdal/envelope.py new file mode 100644 index 0000000000..971f06da90 --- /dev/null +++ b/django/contrib/gis/gdal/envelope.py @@ -0,0 +1,134 @@ +""" + 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 types import TupleType, ListType +from django.contrib.gis.gdal.error import OGRException + +# The OGR definition of an Envelope is a C structure containing four doubles. +# See the 'ogr_core.h' source file for more information: +# http://www.gdal.org/ogr/ogr__core_8h-source.html +class OGREnvelope(Structure): + "Represents the OGREnvelope C Structure." + _fields_ = [("MinX", c_double), + ("MaxX", c_double), + ("MinY", c_double), + ("MaxY", c_double), + ] + +class Envelope(object): + """ + 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], (TupleType, ListType)): + # A tuple was passed in. + if len(args[0]) != 4: + raise OGRException('Incorrect number of tuple elements (%d).' % len(args[0])) + else: + self._from_sequence(args[0]) + else: + raise TypeError('Incorrect type of argument: %s' % str(type(args[0]))) + elif len(args) == 4: + # Individiual parameters passed in. + # Thanks to ww for the help + self._from_sequence(map(float, args)) + else: + raise OGRException('Incorrect number (%d) of arguments.' % len(args)) + + # Checking the x,y coordinates + if self.min_x >= self.max_x: + raise OGRException('Envelope minimum X >= maximum X.') + if self.min_y >= self.max_y: + raise OGRException('Envelope minimum Y >= maximum Y.') + + def __eq__(self, other): + """ + Returns 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, TupleType) 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 OGRException('Equivalence testing only works with other Envelopes.') + + def __str__(self): + "Returns a string representation of the tuple." + return str(self.tuple) + + def _from_sequence(self, seq): + "Initializes 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] + + @property + def min_x(self): + "Returns the value of the minimum X coordinate." + return self._envelope.MinX + + @property + def min_y(self): + "Returns the value of the minimum Y coordinate." + return self._envelope.MinY + + @property + def max_x(self): + "Returns the value of the maximum X coordinate." + return self._envelope.MaxX + + @property + def max_y(self): + "Returns the value of the maximum Y coordinate." + return self._envelope.MaxY + + @property + def ur(self): + "Returns the upper-right coordinate." + return (self.max_x, self.max_y) + + @property + def ll(self): + "Returns the lower-left coordinate." + return (self.min_x, self.min_y) + + @property + def tuple(self): + "Returns a tuple representing the envelope." + return (self.min_x, self.min_y, self.max_x, self.max_y) + + @property + def wkt(self): + "Returns 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) diff --git a/django/contrib/gis/gdal/error.py b/django/contrib/gis/gdal/error.py new file mode 100644 index 0000000000..4ec3f7e735 --- /dev/null +++ b/django/contrib/gis/gdal/error.py @@ -0,0 +1,41 @@ +""" + This module houses the OGR & SRS Exception objects, and the + check_err() routine which checks the status code returned by + OGR methods. +""" +#### OGR & SRS Exceptions #### +class OGRException(Exception): pass +class SRSException(Exception): pass +class OGRIndexError(OGRException, KeyError): + """ + This exception is raised when an invalid index is encountered, and has + the 'silent_variable_feature' attribute set to true. This ensures that + django's templates proceed to use the next lookup type gracefully when + an Exception is raised. Fixes ticket #4740. + """ + silent_variable_failure = True + +#### OGR error checking codes and routine #### + +# OGR Error Codes +OGRERR_DICT = { 1 : (OGRException, 'Not enough data.'), + 2 : (OGRException, 'Not enough memory.'), + 3 : (OGRException, 'Unsupported geometry type.'), + 4 : (OGRException, 'Unsupported operation.'), + 5 : (OGRException, 'Corrupt data.'), + 6 : (OGRException, 'OGR failure.'), + 7 : (SRSException, 'Unsupported SRS.'), + 8 : (OGRException, 'Invalid handle.'), + } +OGRERR_NONE = 0 + +def check_err(code): + "Checks the given OGRERR, and raises an exception where appropriate." + + if code == OGRERR_NONE: + return + elif code in OGRERR_DICT: + e, msg = OGRERR_DICT[code] + raise e, msg + else: + raise OGRException('Unknown error code: "%s"' % code) diff --git a/django/contrib/gis/gdal/feature.py b/django/contrib/gis/gdal/feature.py new file mode 100644 index 0000000000..bc3857f606 --- /dev/null +++ b/django/contrib/gis/gdal/feature.py @@ -0,0 +1,115 @@ +# The GDAL C library, OGR exception, and the Field object +from django.contrib.gis.gdal.error import OGRException, OGRIndexError +from django.contrib.gis.gdal.field import Field +from django.contrib.gis.gdal.geometries import OGRGeometry, OGRGeomType +from django.contrib.gis.gdal.srs import SpatialReference + +# ctypes function prototypes +from django.contrib.gis.gdal.prototypes.ds import \ + destroy_feature, feature_equal, get_fd_geom_type, get_feat_geom_ref, \ + get_feat_name, get_feat_field_count, get_fid, get_field_defn, \ + get_field_index, get_field_name +from django.contrib.gis.gdal.prototypes.geom import clone_geom, get_geom_srs +from django.contrib.gis.gdal.prototypes.srs import clone_srs + +# For more information, see the OGR C API source code: +# http://www.gdal.org/ogr/ogr__api_8h.html +# +# The OGR_F_* routines are relevant here. +class Feature(object): + "A class that wraps an OGR Feature, needs to be instantiated from a Layer object." + + #### Python 'magic' routines #### + def __init__(self, feat, fdefn): + "Initializes on the pointers for the feature and the layer definition." + self._ptr = None # Initially NULL + if not feat or not fdefn: + raise OGRException('Cannot create OGR Feature, invalid pointer given.') + self._ptr = feat + self._fdefn = fdefn + + def __del__(self): + "Releases a reference to this object." + if self._ptr: destroy_feature(self._ptr) + + def __getitem__(self, index): + """ + Gets 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, basestring): + i = self.index(index) + else: + if index < 0 or index > self.num_fields: + raise OGRIndexError('index out of range') + i = index + return Field(self._ptr, i) + + def __iter__(self): + "Iterates over each field in the Feature." + for i in xrange(self.num_fields): + yield self[i] + + def __len__(self): + "Returns 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): + "Does equivalence testing on the features." + return bool(feature_equal(self._ptr, other._ptr)) + + #### Feature Properties #### + @property + def fid(self): + "Returns the feature identifier." + return get_fid(self._ptr) + + @property + def layer_name(self): + "Returns the name of the layer for the feature." + return get_feat_name(self._fdefn) + + @property + def num_fields(self): + "Returns the number of fields in the Feature." + return get_feat_field_count(self._ptr) + + @property + def fields(self): + "Returns a list of fields in the Feature." + return [get_field_name(get_field_defn(self._fdefn, i)) + for i in xrange(self.num_fields)] + + @property + def geom(self): + "Returns the OGR Geometry for this Feature." + # Retrieving the geometry pointer for the feature. + geom_ptr = get_feat_geom_ref(self._ptr) + return OGRGeometry(clone_geom(geom_ptr)) + + @property + def geom_type(self): + "Returns the OGR Geometry Type for this Feture." + return OGRGeomType(get_fd_geom_type(self._fdefn)) + + #### Feature Methods #### + def get(self, field): + """ + Returns 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): + "Returns the index of the given field name." + i = get_field_index(self._ptr, field_name) + if i < 0: raise OGRIndexError('invalid OFT field name given: "%s"' % field_name) + return i diff --git a/django/contrib/gis/gdal/field.py b/django/contrib/gis/gdal/field.py new file mode 100644 index 0000000000..ad1feb3d22 --- /dev/null +++ b/django/contrib/gis/gdal/field.py @@ -0,0 +1,179 @@ +from ctypes import byref, c_int +from datetime import date, datetime, time +from django.contrib.gis.gdal.error import OGRException +from django.contrib.gis.gdal.prototypes.ds import \ + get_feat_field_defn, get_field_as_datetime, get_field_as_double, \ + get_field_as_integer, get_field_as_string, get_field_name, get_field_precision, \ + get_field_type, get_field_type_name, get_field_width + +# For more information, see the OGR C API source code: +# http://www.gdal.org/ogr/ogr__api_8h.html +# +# The OGR_Fld_* routines are relevant here. +class Field(object): + "A class that wraps an OGR Field, needs to be instantiated from a Feature object." + + #### Python 'magic' routines #### + def __init__(self, feat, index): + """ + Initializes on the feature pointer 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 = get_feat_field_defn(feat, index) + if not fld: + raise OGRException('Cannot create OGR Field, invalid pointer given.') + self._ptr = fld + + # Setting the class depending upon the OGR Field Type (OFT) + self.__class__ = FIELD_CLASSES[self.type] + + # OFTReal with no precision should be an OFTInteger. + if isinstance(self, OFTReal) and self.precision == 0: + self.__class__ = OFTInteger + + def __str__(self): + "Returns the string representation of the Field." + return str(self.value).strip() + + #### Field Methods #### + def as_double(self): + "Retrieves the Field's value as a double (float)." + return get_field_as_double(self._feat, self._index) + + def as_int(self): + "Retrieves the Field's value as an integer." + return get_field_as_integer(self._feat, self._index) + + def as_string(self): + "Retrieves the Field's value as a string." + return get_field_as_string(self._feat, self._index) + + def as_datetime(self): + "Retrieves the Field's value as a tuple of date & time components." + yy, mm, dd, hh, mn, ss, tz = [c_int() for i in range(7)] + status = get_field_as_datetime(self._feat, 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 OGRException('Unable to retrieve date & time information from the field.') + + #### Field Properties #### + @property + def name(self): + "Returns the name of this Field." + return get_field_name(self._ptr) + + @property + def precision(self): + "Returns the precision of this Field." + return get_field_precision(self._ptr) + + @property + def type(self): + "Returns the OGR type of this Field." + return get_field_type(self._ptr) + + @property + def type_name(self): + "Return the OGR field type name for this Field." + return get_field_type_name(self.type) + + @property + def value(self): + "Returns the value of this Field." + # Default is to get the field as a string. + return self.as_string() + + @property + def width(self): + "Returns the width of this Field." + return get_field_width(self._ptr) + +### The Field sub-classes for each OGR Field type. ### +class OFTInteger(Field): + @property + def value(self): + "Returns an integer contained in this field." + return self.as_int() + + @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): + "Returns 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): + "Returns a Python `date` object for the OFTDate field." + yy, mm, dd, hh, mn, ss, tz = self.as_datetime() + try: + return date(yy.value, mm.value, dd.value) + except ValueError: + return None + +class OFTDateTime(Field): + @property + def value(self): + "Returns a Python `datetime` object for this OFTDateTime field." + yy, mm, dd, hh, mn, ss, tz = self.as_datetime() + # TODO: Adapt timezone information. + # See http://lists.maptools.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: + return datetime(yy.value, mm.value, dd.value, hh.value, mn.value, ss.value) + except ValueError: + return None + +class OFTTime(Field): + @property + def value(self): + "Returns a Python `time` object for this OFTTime field." + yy, mm, dd, hh, mn, ss, tz = self.as_datetime() + try: + return time(hh.value, mn.value, ss.value) + except ValueError: + return None + +# List fields are also just subclasses +class OFTIntegerList(Field): pass +class OFTRealList(Field): pass +class OFTStringList(Field): pass +class OFTWideStringList(Field): pass + +# Class mapping dictionary for OFT Types +FIELD_CLASSES = { 0 : OFTInteger, + 1 : OFTIntegerList, + 2 : OFTReal, + 3 : OFTRealList, + 4 : OFTString, + 5 : OFTStringList, + 6 : OFTWideString, + 7 : OFTWideStringList, + 8 : OFTBinary, + 9 : OFTDate, + 10 : OFTTime, + 11 : OFTDateTime, + } diff --git a/django/contrib/gis/gdal/geometries.py b/django/contrib/gis/gdal/geometries.py new file mode 100644 index 0000000000..ee0ec17559 --- /dev/null +++ b/django/contrib/gis/gdal/geometries.py @@ -0,0 +1,643 @@ +""" + The OGRGeometry is a wrapper for using the OGR Geometry class + (see http://www.gdal.org/ogr/classOGRGeometry.html). 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_to(SpatialReference('NAD27')) + >>> print mpnt.proj + +proj=longlat +ellps=clrk66 +datum=NAD27 +no_defs + >>> print mpnt + MULTIPOINT (-89.999930378602485 29.999797886557641,-89.999930378602485 29.999797886557641) + + 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 +""" +# Python library requisites. +import re, sys +from binascii import a2b_hex +from ctypes import byref, string_at, c_char_p, c_double, c_ubyte, c_void_p +from types import UnicodeType + +# Getting GDAL prerequisites +from django.contrib.gis.gdal.envelope import Envelope, OGREnvelope +from django.contrib.gis.gdal.error import OGRException, OGRIndexError, SRSException +from django.contrib.gis.gdal.geomtype import OGRGeomType +from django.contrib.gis.gdal.srs import SpatialReference, CoordTransform + +# Getting the ctypes prototype functions that interface w/the GDAL C library. +from django.contrib.gis.gdal.prototypes.geom import * +from django.contrib.gis.gdal.prototypes.srs import clone_srs + +# For more information, see the OGR C API source code: +# http://www.gdal.org/ogr/ogr__api_8h.html +# +# The OGR_G_* routines are relevant here. + +# Regular expressions for recognizing HEXEWKB and WKT. +hex_regex = re.compile(r'^[0-9A-F]+$', re.I) +wkt_regex = re.compile(r'^(?PPOINT|LINESTRING|LINEARRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON|GEOMETRYCOLLECTION)[ACEGIMLONPSRUTY\d,\.\-\(\) ]+$', re.I) +json_regex = re.compile(r'^\{[\s\w,\-\.\"\'\:\[\]]+\}$') + +#### OGRGeometry Class #### +class OGRGeometry(object): + "Generally encapsulates an OGR geometry." + + def __init__(self, geom_input, srs=None): + "Initializes Geometry on either WKT or an OGR pointer as input." + + self._ptr = c_void_p(None) # Initially NULL + str_instance = isinstance(geom_input, basestring) + + # If HEX, unpack input to to a binary buffer. + if str_instance and hex_regex.match(geom_input): + geom_input = buffer(a2b_hex(geom_input.upper())) + str_instance = False + + # Constructing the geometry, + if str_instance: + # Checking if unicode + if isinstance(geom_input, UnicodeType): + # Encoding to ASCII, WKT or HEX doesn't need any more. + geo_input = geo_input.encode('ascii') + + wkt_m = wkt_regex.match(geom_input) + json_m = json_regex.match(geom_input) + if wkt_m: + if wkt_m.group('type').upper() == 'LINEARRING': + # OGR_G_CreateFromWkt doesn't work with LINEARRING WKT. + # See http://trac.osgeo.org/gdal/ticket/1992. + g = create_geom(OGRGeomType(wkt_m.group('type')).num) + import_wkt(g, byref(c_char_p(geom_input))) + else: + g = from_wkt(byref(c_char_p(geom_input)), None, byref(c_void_p())) + elif json_m: + if GEOJSON: + g = from_json(geom_input) + else: + raise NotImplementedError('GeoJSON input only supported on GDAL 1.5+.') + else: + # Seeing if the input is a valid short-hand string + # (e.g., 'Point', 'POLYGON'). + ogr_t = OGRGeomType(geom_input) + g = create_geom(OGRGeomType(geom_input).num) + elif isinstance(geom_input, buffer): + # WKB was passed in + g = from_wkb(str(geom_input), None, byref(c_void_p()), len(geom_input)) + elif isinstance(geom_input, OGRGeomType): + # OGRGeomType was passed in, an empty geometry will be created. + g = create_geom(geom_input.num) + elif isinstance(geom_input, c_void_p): + # OGR pointer (c_void_p) was the input. + g = geom_input + else: + raise OGRException('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 OGRException('Cannot create OGR Geometry from input: %s' % str(geom_input)) + self._ptr = g + + # Assigning the SpatialReference object to the geometry, if valid. + if bool(srs): self.srs = srs + + # Setting the class depending upon the OGR Geometry Type + self.__class__ = GEO_CLASSES[self.geom_type.num] + + def __del__(self): + "Deletes this Geometry." + if self._ptr: destroy_geom(self._ptr) + + ### Geometry set-like operations ### + # g = g1 | g2 + def __or__(self, other): + "Returns the union of the two geometries." + return self.union(other) + + # g = g1 & g2 + def __and__(self, other): + "Returns 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 self.equals(other) + + def __ne__(self, other): + "Tests for inequality." + return not self.equals(other) + + def __str__(self): + "WKT is used for the string representation." + return self.wkt + + #### Geometry Properties #### + @property + def dimension(self): + "Returns 0 for points, 1 for lines, and 2 for surfaces." + return get_dims(self._ptr) + + @property + def coord_dim(self): + "Returns the coordinate dimension of the Geometry." + return get_coord_dims(self._ptr) + + @property + def geom_count(self): + "The number of elements in this Geometry." + return get_geom_count(self._ptr) + + @property + def point_count(self): + "Returns the number of Points in this Geometry." + return 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): + "Alais for `point_count`." + return self.point_count + + @property + def geom_type(self): + "Returns the Type for this Geometry." + try: + return OGRGeomType(get_geom_type(self._ptr)) + except OGRException: + # VRT datasources return an invalid geometry type + # number, but a valid name -- we'll try that instead. + # See: http://trac.osgeo.org/gdal/ticket/2491 + return OGRGeomType(get_geom_name(self._ptr)) + + @property + def geom_name(self): + "Returns the Name of this Geometry." + return get_geom_name(self._ptr) + + @property + def area(self): + "Returns the area for a LinearRing, Polygon, or MultiPolygon; 0 otherwise." + return get_area(self._ptr) + + @property + def envelope(self): + "Returns the envelope for this Geometry." + # TODO: Fix Envelope() for Point geometries. + return Envelope(get_envelope(self._ptr, byref(OGREnvelope()))) + + @property + def extent(self): + "Returns 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): + "Returns the Spatial Reference for this Geometry." + try: + srs_ptr = get_geom_srs(self._ptr) + return SpatialReference(clone_srs(srs_ptr)) + except SRSException: + return None + + def set_srs(self, srs): + "Sets the SpatialReference for this geometry." + if isinstance(srs, SpatialReference): + srs_ptr = clone_srs(srs._ptr) + elif isinstance(srs, (int, long, basestring)): + sr = SpatialReference(srs) + srs_ptr = clone_srs(sr._ptr) + else: + raise TypeError('Cannot assign spatial reference with object of type: %s' % type(srs)) + assign_srs(self._ptr, srs_ptr) + + srs = property(get_srs, set_srs) + + # The SRID property + def get_srid(self): + if self.srs: return self.srs.srid + else: return None + + def set_srid(self, srid): + if isinstance(srid, (int, long)): + self.srs = srid + else: + raise TypeError('SRID must be set with an integer.') + + srid = property(get_srid, set_srid) + + #### Output Methods #### + @property + def geos(self): + "Returns a GEOSGeometry object from this OGRGeometry." + from django.contrib.gis.geos import GEOSGeometry + return GEOSGeometry(self.wkb, self.srid) + + @property + def gml(self): + "Returns the GML representation of the Geometry." + return to_gml(self._ptr) + + @property + def hex(self): + "Returns the hexadecimal representation of the WKB (a string)." + return str(self.wkb).encode('hex').upper() + #return b2a_hex(self.wkb).upper() + + @property + def json(self): + if GEOJSON: + return to_json(self._ptr) + else: + raise NotImplementedError('GeoJSON output only supported on GDAL 1.5+.') + geojson = json + + @property + def wkb_size(self): + "Returns the size of the WKB buffer." + return get_wkbsize(self._ptr) + + @property + def wkb(self): + "Returns 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)() + wkb = to_wkb(self._ptr, byteorder, byref(buf)) + # Returning a buffer of the string at the pointer. + return buffer(string_at(buf, sz)) + + @property + def wkt(self): + "Returns the WKT representation of the Geometry." + return to_wkt(self._ptr, byref(c_char_p())) + + #### Geometry Methods #### + def clone(self): + "Clones this OGR Geometry." + return OGRGeometry(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. + geom_close_rings(self._ptr) + + def transform(self, coord_trans, clone=False): + """ + Transforms this geometry to a different spatial reference system. + May take a CoordTransform object, a SpatialReference object, string + WKT or PROJ.4, and/or an integer SRID. By default nothing is returned + and the geometry is transformed in-place. However, if the `clone` + keyword is set, then a transformed clone of this geometry will be + returned. + """ + if clone: + klone = self.clone() + klone.transform(coord_trans) + return klone + if isinstance(coord_trans, CoordTransform): + geom_transform(self._ptr, coord_trans._ptr) + elif isinstance(coord_trans, SpatialReference): + geom_transform_to(self._ptr, coord_trans._ptr) + elif isinstance(coord_trans, (int, long, basestring)): + sr = SpatialReference(coord_trans) + geom_transform_to(self._ptr, sr._ptr) + else: + raise TypeError('Transform only accepts CoordTransform, SpatialReference, string, and integer objects.') + + def transform_to(self, srs): + "For backwards-compatibility." + self.transform(srs) + + #### 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): + "Returns True if this geometry intersects with the other." + return self._topology(ogr_intersects, other) + + def equals(self, other): + "Returns True if this geometry is equivalent to the other." + return self._topology(ogr_equals, other) + + def disjoint(self, other): + "Returns True if this geometry and the other are spatially disjoint." + return self._topology(ogr_disjoint, other) + + def touches(self, other): + "Returns True if this geometry touches the other." + return self._topology(ogr_touches, other) + + def crosses(self, other): + "Returns True if this geometry crosses the other." + return self._topology(ogr_crosses, other) + + def within(self, other): + "Returns True if this geometry is within the other." + return self._topology(ogr_within, other) + + def contains(self, other): + "Returns True if this geometry contains the other." + return self._topology(ogr_contains, other) + + def overlaps(self, other): + "Returns True if this geometry overlaps the other." + return self._topology(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): + "Returns the boundary of this geometry." + return self._geomgen(get_boundary) + + @property + def convex_hull(self): + """ + Returns the smallest convex Polygon that contains all the points in + this Geometry. + """ + return self._geomgen(geom_convex_hull) + + def difference(self, other): + """ + Returns a new geometry consisting of the region which is the difference + of this geometry and the other. + """ + return self._geomgen(geom_diff, other) + + def intersection(self, other): + """ + Returns a new geometry consisting of the region of intersection of this + geometry and the other. + """ + return self._geomgen(geom_intersection, other) + + def sym_difference(self, other): + """ + Returns a new geometry which is the symmetric difference of this + geometry and the other. + """ + return self._geomgen(geom_sym_diff, other) + + def union(self, other): + """ + Returns a new geometry consisting of the region which is the union of + this geometry and the other. + """ + return self._geomgen(geom_union, other) + +# The subclasses for OGR Geometry. +class Point(OGRGeometry): + + @property + def x(self): + "Returns the X coordinate for this Point." + return getx(self._ptr, 0) + + @property + def y(self): + "Returns the Y coordinate for this Point." + return gety(self._ptr, 0) + + @property + def z(self): + "Returns the Z coordinate for this Point." + if self.coord_dim == 3: + return getz(self._ptr, 0) + + @property + def tuple(self): + "Returns 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): + "Returns the Point at the given index." + if index >= 0 and index < self.point_count: + x, y, z = c_double(), c_double(), c_double() + 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 OGRIndexError('index out of range: %s' % str(index)) + + def __iter__(self): + "Iterates over each point in the LineString." + for i in xrange(self.point_count): + yield self[i] + + def __len__(self): + "The length returns the number of points in the LineString." + return self.point_count + + @property + def tuple(self): + "Returns the tuple representation of this LineString." + return tuple([self[i] for i in xrange(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 xrange(len(self))] + + @property + def x(self): + "Returns the X coordinates in a list." + return self._listarr(getx) + + @property + def y(self): + "Returns the Y coordinates in a list." + return self._listarr(gety) + + @property + def z(self): + "Returns the Z coordinates in a list." + if self.coord_dim == 3: + return self._listarr(getz) + +# LinearRings are used in Polygons. +class LinearRing(LineString): pass + +class Polygon(OGRGeometry): + + def __len__(self): + "The number of interior rings in this Polygon." + return self.geom_count + + def __iter__(self): + "Iterates through each ring in the Polygon." + for i in xrange(self.geom_count): + yield self[i] + + def __getitem__(self, index): + "Gets the ring at the specified index." + if index < 0 or index >= self.geom_count: + raise OGRIndexError('index out of range: %s' % index) + else: + return OGRGeometry(clone_geom(get_geom_ref(self._ptr, index)), self.srs) + + # Polygon Properties + @property + def shell(self): + "Returns the shell of this Polygon." + return self[0] # First ring is the shell + exterior_ring = shell + + @property + def tuple(self): + "Returns a tuple of LinearRing coordinate tuples." + return tuple([self[i].tuple for i in xrange(self.geom_count)]) + coords = tuple + + @property + def point_count(self): + "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 xrange(self.geom_count)]) + + @property + def centroid(self): + "Returns the centroid (a Point) of this Polygon." + # The centroid is a Point, create a geometry for this. + p = OGRGeometry(OGRGeomType('Point')) + get_centroid(self._ptr, p._ptr) + return p + +# Geometry Collection base class. +class GeometryCollection(OGRGeometry): + "The Geometry Collection class." + + def __getitem__(self, index): + "Gets the Geometry at the specified index." + if index < 0 or index >= self.geom_count: + raise OGRIndexError('index out of range: %s' % index) + else: + return OGRGeometry(clone_geom(get_geom_ref(self._ptr, index)), self.srs) + + def __iter__(self): + "Iterates over each Geometry." + for i in xrange(self.geom_count): + yield self[i] + + def __len__(self): + "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: add_geom(self._ptr, g._ptr) + else: + add_geom(self._ptr, geom._ptr) + elif isinstance(geom, basestring): + tmp = OGRGeometry(geom) + add_geom(self._ptr, tmp._ptr) + else: + raise OGRException('Must add an OGRGeometry.') + + @property + def point_count(self): + "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 xrange(self.geom_count)]) + + @property + def tuple(self): + "Returns a tuple representation of this Geometry Collection." + return tuple([self[i].tuple for i in xrange(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, + } diff --git a/django/contrib/gis/gdal/geomtype.py b/django/contrib/gis/gdal/geomtype.py new file mode 100644 index 0000000000..565326f5a8 --- /dev/null +++ b/django/contrib/gis/gdal/geomtype.py @@ -0,0 +1,73 @@ +from django.contrib.gis.gdal.error import OGRException + +#### OGRGeomType #### +class OGRGeomType(object): + "Encapulates OGR Geometry Types." + + # 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', + } + # Reverse type dictionary, keyed by lower-case of the name. + _str_types = dict([(v.lower(), k) for k, v in _types.items()]) + + def __init__(self, type_input): + "Figures out the correct OGR Type based upon the input." + if isinstance(type_input, OGRGeomType): + num = type_input.num + elif isinstance(type_input, basestring): + num = self._str_types.get(type_input.lower(), None) + if num is None: + raise OGRException('Invalid OGR String Type "%s"' % type_input) + elif isinstance(type_input, int): + if not type_input in self._types: + raise OGRException('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): + "Returns the value of the name property." + return self.name + + def __eq__(self, other): + """ + Does 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, basestring): + return self.name.lower() == other.lower() + elif isinstance(other, int): + return self.num == other + else: + return False + + def __ne__(self, other): + return not (self == other) + + @property + def name(self): + "Returns a short-hand string form of the OGR Geometry type." + return self._types[self.num] + + @property + def django(self): + "Returns the Django GeometryField for this OGR Type." + s = self.name + if s in ('Unknown', 'LinearRing', 'None'): + return None + else: + return s + 'Field' diff --git a/django/contrib/gis/gdal/layer.py b/django/contrib/gis/gdal/layer.py new file mode 100644 index 0000000000..d60e2cb7f6 --- /dev/null +++ b/django/contrib/gis/gdal/layer.py @@ -0,0 +1,187 @@ +# Needed ctypes routines +from ctypes import byref + +# Other GDAL imports. +from django.contrib.gis.gdal.envelope import Envelope, OGREnvelope +from django.contrib.gis.gdal.error import OGRException, OGRIndexError, SRSException +from django.contrib.gis.gdal.feature import Feature +from django.contrib.gis.gdal.field import FIELD_CLASSES +from django.contrib.gis.gdal.geometries import OGRGeomType +from django.contrib.gis.gdal.srs import SpatialReference + +# GDAL ctypes function prototypes. +from django.contrib.gis.gdal.prototypes.ds import \ + get_extent, get_fd_geom_type, get_fd_name, get_feature, get_feature_count, \ + get_field_count, get_field_defn, get_field_name, get_field_precision, \ + get_field_width, get_field_type, get_layer_defn, get_layer_srs, \ + get_next_feature, reset_reading, test_capability +from django.contrib.gis.gdal.prototypes.srs import clone_srs + +# For more information, see the OGR C API source code: +# http://www.gdal.org/ogr/ogr__api_8h.html +# +# The OGR_L_* routines are relevant here. +class Layer(object): + "A class that wraps an OGR Layer, needs to be instantiated from a DataSource object." + + #### Python 'magic' routines #### + def __init__(self, layer_ptr): + "Needs a C pointer (Python/ctypes integer) in order to initialize." + self._ptr = None # Initially NULL + if not layer_ptr: + raise OGRException('Cannot create Layer, invalid pointer given') + self._ptr = layer_ptr + self._ldefn = get_layer_defn(self._ptr) + # Does the Layer support random reading? + self._random_read = self.test_capability('RandomRead') + + def __getitem__(self, index): + "Gets the Feature at the specified index." + if isinstance(index, (int, long)): + # 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 OGRIndexError('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 xrange(start, stop, stride)] + else: + raise TypeError('Integers and slices may only be used when indexing OGR Layers.') + + def __iter__(self): + "Iterates over each Feature in the Layer." + # ResetReading() must be called before iteration is to begin. + reset_reading(self._ptr) + for i in xrange(self.num_feat): + yield Feature(get_next_feature(self._ptr), self._ldefn) + + 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(get_feature(self._ptr, feat_id), self._ldefn) + except OGRException: + 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 OGRIndexError. + raise OGRIndexError('Invalid feature id: %s.' % feat_id) + + #### Layer properties #### + @property + def extent(self): + "Returns the extent (an Envelope) of this layer." + env = OGREnvelope() + get_extent(self._ptr, byref(env), 1) + return Envelope(env) + + @property + def name(self): + "Returns the name of this layer in the Data Source." + return get_fd_name(self._ldefn) + + @property + def num_feat(self, force=1): + "Returns the number of features in the Layer." + return get_feature_count(self._ptr, force) + + @property + def num_fields(self): + "Returns the number of fields in the Layer." + return get_field_count(self._ldefn) + + @property + def geom_type(self): + "Returns the geometry type (OGRGeomType) of the Layer." + return OGRGeomType(get_fd_geom_type(self._ldefn)) + + @property + def srs(self): + "Returns the Spatial Reference used in this Layer." + try: + ptr = get_layer_srs(self._ptr) + return SpatialReference(clone_srs(ptr)) + except SRSException: + return None + + @property + def fields(self): + """ + Returns a list of string names corresponding to each of the Fields + available in this Layer. + """ + return [get_field_name(get_field_defn(self._ldefn, i)) + for i in xrange(self.num_fields) ] + + @property + def field_types(self): + """ + Returns a list of the types of fields in this Layer. For example, + the list [OFTInteger, OFTReal, OFTString] would be returned for + an OGR layer that had an integer, a floating-point, and string + fields. + """ + return [FIELD_CLASSES[get_field_type(get_field_defn(self._ldefn, i))] + for i in xrange(self.num_fields)] + + @property + def field_widths(self): + "Returns a list of the maximum field widths for the features." + return [get_field_width(get_field_defn(self._ldefn, i)) + for i in xrange(self.num_fields)] + + @property + def field_precisions(self): + "Returns the field precisions for the features." + return [get_field_precision(get_field_defn(self._ldefn, i)) + for i in xrange(self.num_fields)] + + #### Layer Methods #### + def get_fields(self, field_name): + """ + Returns a list containing the given field name for every Feature + in the Layer. + """ + if not field_name in self.fields: + raise OGRException('invalid field name: %s' % field_name) + return [feat.get(field_name) for feat in self] + + def get_geoms(self, geos=False): + """ + Returns 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): + """ + Returns 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(test_capability(self._ptr, capability)) diff --git a/django/contrib/gis/gdal/libgdal.py b/django/contrib/gis/gdal/libgdal.py new file mode 100644 index 0000000000..bf8933dffb --- /dev/null +++ b/django/contrib/gis/gdal/libgdal.py @@ -0,0 +1,83 @@ +import os, sys +from ctypes import c_char_p, CDLL +from ctypes.util import find_library +from django.contrib.gis.gdal.error import OGRException + +# Custom library path set? +try: + from django.conf import settings + lib_path = settings.GDAL_LIBRARY_PATH +except (AttributeError, EnvironmentError, ImportError): + lib_path = None + +if lib_path: + lib_names = None +elif os.name == 'nt': + # Windows NT shared library + lib_names = ['gdal15'] +elif os.name == 'posix': + # *NIX library names. + lib_names = ['gdal', 'gdal1.5.0'] +else: + raise OGRException('Unsupported 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 not lib_path is None: break + +if lib_path is None: + raise OGRException('Could not find the GDAL library (tried "%s"). ' + '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_name) + +def std_call(func): + """ + Returns 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. #### + +# Returns 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(): + "Returns only the GDAL version number information." + return _version_info('RELEASE_NAME') + +def gdal_full_version(): + "Returns the full GDAL version information." + return _version_info('') + +def gdal_release_date(date=False): + """ + Returns the release date in a string format, e.g, "2007/06/27". + If the date keyword argument is set to True, a Python datetime object + will be returned instead. + """ + from datetime import date as date_type + rel = _version_info('RELEASE_DATE') + yy, mm, dd = map(int, (rel[0:4], rel[4:6], rel[6:8])) + d = date_type(yy, mm, dd) + if date: return d + else: return d.strftime('%Y/%m/%d') diff --git a/django/contrib/gis/gdal/prototypes/__init__.py b/django/contrib/gis/gdal/prototypes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/gis/gdal/prototypes/ds.py b/django/contrib/gis/gdal/prototypes/ds.py new file mode 100644 index 0000000000..b64183eeb3 --- /dev/null +++ b/django/contrib/gis/gdal/prototypes/ds.py @@ -0,0 +1,68 @@ +""" + 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 c_char_p, c_int, c_long, c_void_p, POINTER +from django.contrib.gis.gdal.envelope import OGREnvelope +from django.contrib.gis.gdal.libgdal import lgdal +from django.contrib.gis.gdal.prototypes.generation import \ + const_string_output, double_output, geom_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]) +get_driver_count = int_output(lgdal.OGRGetDriverCount, []) +get_driver_name = const_string_output(lgdal.OGR_Dr_GetName, [c_void_p]) + +### 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]) + +### 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_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]) diff --git a/django/contrib/gis/gdal/prototypes/errcheck.py b/django/contrib/gis/gdal/prototypes/errcheck.py new file mode 100644 index 0000000000..c66be9adfb --- /dev/null +++ b/django/contrib/gis/gdal/prototypes/errcheck.py @@ -0,0 +1,125 @@ +""" + 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 check_err, OGRException, SRSException +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): + "Returns the pointer argument's by-refernece value." + return args[offset]._obj.value + +def ptr_byref(args, offset=-1): + "Returns the pointer argument passed in by-reference." + return args[offset]._obj + +def check_bool(result, func, cargs): + "Returns the boolean evaluation of the value." + if bool(result): return True + else: return False + +### String checking Routines ### +def check_const_string(result, func, cargs, offset=None): + """ + Similar functionality to `check_string`, but does not free the pointer. + """ + if offset: + check_err(result) + ptr = ptr_byref(cargs, offset) + return ptr.value + else: + return result + +def check_string(result, func, cargs, offset=-1, str_result=False): + """ + Checks the string output returned from the given function, and frees + 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 beind GDAL pointer + # w/the VSIFree routine. + if ptr: lgdal.VSIFree(ptr) + return s + +### DataSource, Layer error-checking ### + +### Envelope checking ### +def check_envelope(result, func, cargs, offset=-1): + "Checks a function that returns an OGR Envelope by reference." + env = ptr_byref(cargs, offset) + return env + +### Geometry error-checking routines ### +def check_geom(result, func, cargs): + "Checks 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 OGRException('Invalid geometry pointer returned from "%s".' % func.__name__) + return result + +def check_geom_offset(result, func, cargs, offset=-1): + "Chcks 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): + """ + 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)) + return result + +def check_errcode(result, func, cargs): + """ + Check the error code returned (c_int). + """ + check_err(result) + return + +def check_pointer(result, func, cargs): + "Makes sure the result pointer is valid." + if bool(result): + return result + else: + raise OGRException('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 tring values. + """ + dbl = result + ptr = cargs[-1]._obj + return dbl, ptr.value diff --git a/django/contrib/gis/gdal/prototypes/generation.py b/django/contrib/gis/gdal/prototypes/generation.py new file mode 100644 index 0000000000..bba715d67c --- /dev/null +++ b/django/contrib/gis/gdal/prototypes/generation.py @@ -0,0 +1,116 @@ +""" + This module contains functions that generate ctypes prototypes for the + GDAL routines. +""" + +from ctypes import c_char_p, c_double, c_int, c_void_p +from django.contrib.gis.gdal.prototypes.errcheck import \ + check_arg_errcode, check_errcode, check_geom, check_geom_offset, \ + check_pointer, check_srs, check_str_arg, check_string, check_const_string + +def double_output(func, argtypes, errcheck=False, strarg=False): + "Generates a ctypes function that returns a double value." + func.argtypes = argtypes + func.restype = c_double + if errcheck: func.errcheck = check_arg_errcode + if strarg: func.errcheck = check_str_arg + return func + +def geom_output(func, argtypes, offset=None): + """ + Generates 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): + "Generates a ctypes function that returns an integer value." + func.argtypes = argtypes + func.restype = c_int + return func + +def srs_output(func, argtypes): + """ + Generates 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): + func.argtypes = argtypes + if offset: + func.restype = c_int + else: + func.restype = c_char_p + + def _check_const(result, func, cargs): + return check_const_string(result, func, cargs, offset=offset) + func.errcheck = _check_const + + return func + +def string_output(func, argtypes, offset=-1, str_result=False): + """ + Generates 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: + # String is the result, don't explicitly define + # the argument type so we can get the pointer. + pass + 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): + return check_string(result, func, cargs, + offset=offset, str_result=str_result) + func.errcheck = _check_str + return func + +def void_output(func, argtypes, errcheck=True): + """ + 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 = check_errcode + else: + func.restype = None + + return func + +def voidptr_output(func, argtypes): + "For functions that return c_void_p." + func.argtypes = argtypes + func.restype = c_void_p + func.errcheck = check_pointer + return func diff --git a/django/contrib/gis/gdal/prototypes/geom.py b/django/contrib/gis/gdal/prototypes/geom.py new file mode 100644 index 0000000000..7757f557ad --- /dev/null +++ b/django/contrib/gis/gdal/prototypes/geom.py @@ -0,0 +1,109 @@ +from datetime import date +from ctypes import c_char, c_char_p, c_double, c_int, c_ubyte, c_void_p, POINTER +from django.contrib.gis.gdal.envelope import OGREnvelope +from django.contrib.gis.gdal.libgdal import lgdal, gdal_version +from django.contrib.gis.gdal.prototypes.errcheck import check_bool, 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 + +# Some prototypes need to be aware of what version GDAL we have. +major, minor = map(int, gdal_version().split('.')[:2]) +if major <= 1 and minor <= 4: + GEOJSON = False +else: + GEOJSON = True + +### 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.errchck = check_bool + return f + +### OGR_G ctypes function prototypes ### + +# GeoJSON routines, if supported. +if GEOJSON: + 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) +else: + from_json = False + to_json = False + +# 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) +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)]) +to_gml = string_output(lgdal.OGR_G_ExportToGML, [c_void_p], str_result=True) +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_dims = int_output(lgdal.OGR_G_GetCoordinateDimension, [c_void_p]) + +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]) +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)]) + diff --git a/django/contrib/gis/gdal/prototypes/srs.py b/django/contrib/gis/gdal/prototypes/srs.py new file mode 100644 index 0000000000..ff4d0add95 --- /dev/null +++ b/django/contrib/gis/gdal/prototypes/srs.py @@ -0,0 +1,71 @@ +from ctypes import c_char_p, c_int, c_void_p, POINTER +from django.contrib.gis.gdal.libgdal import 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): + """ + Creates a function prototype for the OSR routines that take + the OSRSpatialReference object and + """ + return double_output(f, [c_void_p, POINTER(c_int)], errcheck=True) + +def units_func(f): + """ + Creates 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]) + +# 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]) + +# 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.4, "Pretty" WKT, and XML. +to_wkt = string_output(std_call('OSRExportToWkt'), [c_void_p, POINTER(c_char_p)]) +to_proj = string_output(std_call('OSRExportToProj4'), [c_void_p, POINTER(c_char_p)]) +to_pretty_wkt = string_output(std_call('OSRExportToPrettyWkt'), [c_void_p, POINTER(c_char_p), c_int], offset=-2) + +# Memory leak fixed in GDAL 1.5; still exists in 1.4. +to_xml = string_output(lgdal.OSRExportToXML, [c_void_p, POINTER(c_char_p), c_char_p], offset=-2) + +# String attribute retrival routines. +get_attr_value = const_string_output(std_call('OSRGetAttrValue'), [c_void_p, c_char_p, c_int]) +get_auth_name = const_string_output(lgdal.OSRGetAuthorityName, [c_void_p, c_char_p]) +get_auth_code = const_string_output(lgdal.OSRGetAuthorityCode, [c_void_p, c_char_p]) + +# 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) diff --git a/django/contrib/gis/gdal/srs.py b/django/contrib/gis/gdal/srs.py new file mode 100644 index 0000000000..d70e71ebc7 --- /dev/null +++ b/django/contrib/gis/gdal/srs.py @@ -0,0 +1,360 @@ +""" + The Spatial Reference class, represensents 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 +""" +import re +from types import UnicodeType, TupleType +from ctypes import byref, c_char_p, c_int, c_void_p + +# Getting the error checking routine and exceptions +from django.contrib.gis.gdal.error import OGRException, SRSException +from django.contrib.gis.gdal.prototypes.srs import * + +#### Spatial Reference class. #### +class SpatialReference(object): + """ + A wrapper for the OGRSpatialReference object. According to the GDAL website, + the SpatialReference object "provide[s] services to represent coordinate + systems (projections and datums) and to transform between them." + """ + + # Well-Known Geographical Coordinate System Name + _well_known = {'WGS84':4326, 'WGS72':4322, 'NAD27':4267, 'NAD83':4269} + _epsg_regex = re.compile('^(EPSG:)?(?P\d+)$', re.I) + _proj_regex = re.compile(r'^\+proj') + + #### Python 'magic' routines #### + def __init__(self, srs_input='', srs_type='wkt'): + """ + Creates 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.4 string, and/or a projection "well known" shorthand + string (one of 'WGS84', 'WGS72', 'NAD27', 'NAD83'). + """ + # Intializing pointer and string buffer. + self._ptr = None + buf = c_char_p('') + + if isinstance(srs_input, basestring): + # Encoding to ASCII if unicode passed in. + if isinstance(srs_input, UnicodeType): + srs_input = srs_input.encode('ascii') + + epsg_m = self._epsg_regex.match(srs_input) + proj_m = self._proj_regex.match(srs_input) + if epsg_m: + # Is this an EPSG well known name? + srs_type = 'epsg' + srs_input = int(epsg_m.group('epsg')) + elif proj_m: + # Is the string a PROJ.4 string? + srs_type = 'proj' + elif srs_input in self._well_known: + # Is this a short-hand well known name? + srs_type = 'epsg' + srs_input = self._well_known[srs_input] + elif srs_type == 'proj': + pass + else: + # Setting the buffer with WKT, PROJ.4 string, etc. + buf = c_char_p(srs_input) + elif isinstance(srs_input, int): + # EPSG integer code was input. + if srs_type != 'epsg': srs_type = 'epsg' + elif isinstance(srs_input, c_void_p): + srs_type = 'ogr' + else: + raise TypeError('Invalid SRS type "%s"' % srs_type) + + if srs_type == 'ogr': + # SRS input is OGR pointer + srs = srs_input + else: + # Creating a new pointer, using the string buffer. + srs = 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 + + # Post-processing if in PROJ.4 or EPSG formats. + if srs_type == 'proj': self.import_proj(srs_input) + elif srs_type == 'epsg': self.import_epsg(srs_input) + + def __del__(self): + "Destroys this spatial reference." + if self._ptr: release_srs(self._ptr) + + def __getitem__(self, target): + """ + Returns 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 + >>> print srs['UNIT|AUTHORITY'] # For the units authority, have to use the pipe symbole. + EPSG + >>> print srs['UNIT|AUTHORITY', 1] # The authority value for the untis + 9122 + """ + if isinstance(target, TupleType): + return self.attr_value(*target) + else: + return self.attr_value(target) + + def __str__(self): + "The string representation uses '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 get_attr_value(self._ptr, target, index) + + def auth_name(self, target): + "Returns the authority name for the given string target node." + return get_auth_name(self._ptr, target) + + def auth_code(self, target): + "Returns the authority code for the given string target node." + return get_auth_code(self._ptr, target) + + def clone(self): + "Returns a clone of this SpatialReference object." + return SpatialReference(clone_srs(self._ptr)) + + def from_esri(self): + "Morphs this SpatialReference from ESRI's format to EPSG." + 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. + """ + identify_epsg(self._ptr) + + def to_esri(self): + "Morphs this SpatialReference to ESRI's format." + morph_to_esri(self._ptr) + + def validate(self): + "Checks to see if the given spatial reference is valid." + srs_validate(self._ptr) + + #### Name & SRID properties #### + @property + def name(self): + "Returns 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): + "Returns the SRID of top-level authority, or None if undefined." + try: + return int(self.attr_value('AUTHORITY', 1)) + except (TypeError, ValueError): + return None + + #### Unit Properties #### + @property + def linear_name(self): + "Returns the name of the linear units." + units, name = linear_units(self._ptr, byref(c_char_p())) + return name + + @property + def linear_units(self): + "Returns the value of the linear units." + units, name = linear_units(self._ptr, byref(c_char_p())) + return units + + @property + def angular_name(self): + "Returns the name of the angular units." + units, name = angular_units(self._ptr, byref(c_char_p())) + return name + + @property + def angular_units(self): + "Returns the value of the angular units." + units, name = angular_units(self._ptr, byref(c_char_p())) + return units + + @property + def units(self): + """ + Returns a 2-tuple of the units value and the units name, + and will automatically determines whether to return the linear + or angular units. + """ + if self.projected or self.local: + return linear_units(self._ptr, byref(c_char_p())) + elif self.geographic: + return angular_units(self._ptr, byref(c_char_p())) + else: + return (None, None) + + #### Spheroid/Ellipsoid Properties #### + @property + def ellipsoid(self): + """ + Returns 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): + "Returns the Semi Major Axis for this Spatial Reference." + return semi_major(self._ptr, byref(c_int())) + + @property + def semi_minor(self): + "Returns the Semi Minor Axis for this Spatial Reference." + return semi_minor(self._ptr, byref(c_int())) + + @property + def inverse_flattening(self): + "Returns the Inverse Flattening for this Spatial Reference." + return invflattening(self._ptr, byref(c_int())) + + #### Boolean Properties #### + @property + def geographic(self): + """ + Returns True if this SpatialReference is geographic + (root node is GEOGCS). + """ + return bool(isgeographic(self._ptr)) + + @property + def local(self): + "Returns True if this SpatialReference is local (root node is LOCAL_CS)." + return bool(islocal(self._ptr)) + + @property + def projected(self): + """ + Returns True if this SpatialReference is a projected coordinate system + (root node is PROJCS). + """ + return bool(isprojected(self._ptr)) + + #### Import Routines ##### + def import_wkt(self, wkt): + "Imports the Spatial Reference from OGC WKT (string)" + from_wkt(self._ptr, byref(c_char_p(wkt))) + + def import_proj(self, proj): + "Imports the Spatial Reference from a PROJ.4 string." + from_proj(self._ptr, proj) + + def import_epsg(self, epsg): + "Imports the Spatial Reference from the EPSG code (an integer)." + from_epsg(self._ptr, epsg) + + def import_xml(self, xml): + "Imports the Spatial Reference from an XML string." + from_xml(self._ptr, xml) + + #### Export Properties #### + @property + def wkt(self): + "Returns the WKT representation of this Spatial Reference." + return to_wkt(self._ptr, byref(c_char_p())) + + @property + def pretty_wkt(self, simplify=0): + "Returns the 'pretty' representation of the WKT." + return to_pretty_wkt(self._ptr, byref(c_char_p()), simplify) + + @property + def proj(self): + "Returns the PROJ.4 representation for this Spatial Reference." + return to_proj(self._ptr, byref(c_char_p())) + + @property + def proj4(self): + "Alias for proj()." + return self.proj + + @property + def xml(self, dialect=''): + "Returns the XML representation of this Spatial Reference." + # FIXME: This leaks memory, have to figure out why. + return to_xml(self._ptr, byref(c_char_p()), dialect) + + def to_esri(self): + "Morphs this SpatialReference to ESRI's format." + morph_to_esri(self._ptr) + + def from_esri(self): + "Morphs this SpatialReference from ESRI's format to EPSG." + morph_from_esri(self._ptr) + +class CoordTransform(object): + "The coordinate system transformation object." + + def __init__(self, source, target): + "Initializes on a source and target SpatialReference objects." + self._ptr = None # Initially NULL + if not isinstance(source, SpatialReference) or not isinstance(target, SpatialReference): + raise SRSException('source and target must be of type SpatialReference') + self._ptr = new_ct(source._ptr, target._ptr) + if not self._ptr: + raise SRSException('could not intialize CoordTransform object') + self._srs1_name = source.name + self._srs2_name = target.name + + def __del__(self): + "Deletes this Coordinate Transformation object." + if self._ptr: destroy_ct(self._ptr) + + def __str__(self): + return 'Transform from "%s" to "%s"' % (self._srs1_name, self._srs2_name) diff --git a/django/contrib/gis/geos/LICENSE b/django/contrib/gis/geos/LICENSE new file mode 100644 index 0000000000..84cf485d00 --- /dev/null +++ b/django/contrib/gis/geos/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2007, 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. diff --git a/django/contrib/gis/geos/__init__.py b/django/contrib/gis/geos/__init__.py new file mode 100644 index 0000000000..90e2605240 --- /dev/null +++ b/django/contrib/gis/geos/__init__.py @@ -0,0 +1,69 @@ +""" + The goal of this module is to be a ctypes wrapper around the GEOS library + that will work on both *NIX and Windows systems. Specifically, this uses + the GEOS C api. + + I have several motivations for doing this: + (1) The GEOS SWIG wrapper is no longer maintained, and requires the + installation of SWIG. + (2) The PCL implementation is over 2K+ lines of C and would make + PCL a requisite package for the GeoDjango application stack. + (3) Windows and Mac compatibility becomes substantially easier, and does not + require the additional compilation of PCL or GEOS and SWIG -- all that + is needed is a Win32 or Mac compiled GEOS C library (dll or dylib) + in a location that Python can read (e.g. 'C:\Python25'). + + In summary, I wanted to wrap GEOS in a more maintainable and portable way using + only Python and the excellent ctypes library (now standard in Python 2.5). + + In the spirit of loose coupling, this library does not require Django or + GeoDjango. Only the GEOS C library and ctypes are needed for the platform + of your choice. + + For more information about GEOS: + http://geos.refractions.net + + For more info about PCL and the discontinuation of the Python GEOS + library see Sean Gillies' writeup (and subsequent update) at: + http://zcologia.com/news/150/geometries-for-python/ + http://zcologia.com/news/429/geometries-for-python-update/ +""" +from django.contrib.gis.geos.base import GEOSGeometry, wkt_regex, hex_regex +from django.contrib.gis.geos.geometries import Point, LineString, LinearRing, Polygon, HAS_NUMPY +from django.contrib.gis.geos.collections import GeometryCollection, MultiPoint, MultiLineString, MultiPolygon +from django.contrib.gis.geos.error import GEOSException, GEOSIndexError +from django.contrib.gis.geos.libgeos import geos_version, geos_version_info + +def fromfile(file_name): + """ + Given a string file name, returns a GEOSGeometry. The file may contain WKB, + WKT, or HEX. + """ + fh = open(file_name, 'rb') + buf = fh.read() + fh.close() + if wkt_regex.match(buf) or hex_regex.match(buf): + return GEOSGeometry(buf) + else: + return GEOSGeometry(buffer(buf)) + +def fromstr(wkt_or_hex, **kwargs): + "Given a string value (wkt or hex), returns a GEOSGeometry object." + return GEOSGeometry(wkt_or_hex, **kwargs) + +def hex_to_wkt(hex): + "Converts HEXEWKB into WKT." + return GEOSGeometry(hex).wkt + +def wkt_to_hex(wkt): + "Converts WKT into HEXEWKB." + return GEOSGeometry(wkt).hex + +def centroid(input): + "Returns the centroid of the geometry (given in HEXEWKB)." + return GEOSGeometry(input).centroid.wkt + +def area(input): + "Returns the area of the geometry (given in HEXEWKB)." + return GEOSGeometry(input).area + diff --git a/django/contrib/gis/geos/base.py b/django/contrib/gis/geos/base.py new file mode 100644 index 0000000000..8200d59eec --- /dev/null +++ b/django/contrib/gis/geos/base.py @@ -0,0 +1,608 @@ +""" + This module contains the 'base' GEOSGeometry object -- all GEOS Geometries + inherit from this object. +""" +# Python, ctypes and types dependencies. +import re +from ctypes import addressof, byref, c_double, c_size_t +from types import UnicodeType + +# GEOS-related dependencies. +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 + +# All other functions in this module come from the ctypes +# prototypes module -- which handles all interaction with +# the underlying GEOS library. +from django.contrib.gis.geos.prototypes import * + +# Trying to import GDAL libraries, if available. Have to place in +# try/except since this package may be used outside GeoDjango. +try: + from django.contrib.gis.gdal import OGRGeometry, SpatialReference, GEOJSON + HAS_GDAL = True +except: + HAS_GDAL, GEOJSON = False, False + +# 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 = re.compile(r'^[0-9A-F]+$', re.I) +wkt_regex = re.compile(r'^(SRID=(?P\d+);)?(?P(POINT|LINESTRING|LINEARRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON|GEOMETRYCOLLECTION)[ACEGIMLONPSRUTY\d,\.\-\(\) ]+)$', re.I) +json_regex = re.compile(r'^\{.+\}$') + +class GEOSGeometry(object): + "A class that, generally, encapsulates a GEOS geometry." + + # Initially, the geometry pointer is NULL + _ptr = None + + #### Python 'magic' routines #### + def __init__(self, geo_input, srid=None): + """ + The base constructor for GEOS geometry objects, and may take the + following inputs: + + * string: WKT + * string: HEXEWKB (a PostGIS-specific canonical form) + * buffer: WKB + + The `srid` keyword is used to specify the Source Reference Identifier + (SRID) number for this Geometry. If not set, the SRID will be None. + """ + if isinstance(geo_input, basestring): + if isinstance(geo_input, UnicodeType): + # Encoding to ASCII, WKT or HEXEWKB doesn't need any more. + geo_input = geo_input.encode('ascii') + + wkt_m = wkt_regex.match(geo_input) + if wkt_m: + # Handling WKT input. + if wkt_m.group('srid'): srid = int(wkt_m.group('srid')) + g = from_wkt(wkt_m.group('wkt')) + elif hex_regex.match(geo_input): + # Handling HEXEWKB input. + g = from_hex(geo_input, len(geo_input)) + elif GEOJSON and json_regex.match(geo_input): + # Handling GeoJSON input. + wkb_input = str(OGRGeometry(geo_input).wkb) + g = from_wkb(wkb_input, len(wkb_input)) + else: + raise ValueError('String or unicode input unrecognized as WKT EWKT, and HEXEWKB.') + elif isinstance(geo_input, GEOM_PTR): + # When the input is a pointer to a geomtry (GEOM_PTR). + g = geo_input + elif isinstance(geo_input, buffer): + # When the input is a buffer (WKB). + wkb_input = str(geo_input) + g = from_wkb(wkb_input, len(wkb_input)) + else: + # Invalid geometry type. + raise TypeError('Improper geometry input type: %s' % str(type(geo_input))) + + if bool(g): + # Setting the pointer object with a valid pointer. + self._ptr = g + else: + raise GEOSException('Could not initialize GEOS Geometry with given input.') + + # Post-initialization setup. + self._post_init(srid) + + def _post_init(self, srid): + "Helper routine for performing post-initialization setup." + # Setting the SRID, if given. + if srid and isinstance(srid, int): self.srid = srid + + # Setting the class type (e.g., Point, Polygon, etc.) + self.__class__ = GEOS_CLASSES[self.geom_typeid] + + # Setting the coordinate sequence for the geometry (will be None on + # geometries that do not have coordinate sequences) + self._set_cs() + + @property + def ptr(self): + """ + Property for controlling access to the GEOS geometry pointer. Using + this raises an exception when the pointer is NULL, thus preventing + the C library from attempting to access an invalid memory location. + """ + if self._ptr: + return self._ptr + else: + raise GEOSException('NULL GEOS pointer encountered; was this geometry modified?') + + def __del__(self): + """ + Destroys this Geometry; in other words, frees the memory used by the + GEOS C++ object. + """ + if self._ptr: destroy_geom(self._ptr) + + def __copy__(self): + """ + Returns 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): + "WKT is used for the string representation." + return self.wkt + + 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 __getstate__(self): + # The pickled state is simply a tuple of the WKB (in string form) + # and the SRID. + return str(self.wkb), self.srid + + def __setstate__(self, state): + # Instantiating from the tuple state that was pickled. + wkb, srid = state + ptr = from_wkb(wkb, len(wkb)) + if not ptr: raise GEOSException('Invalid Geometry loaded from pickled state.') + self._ptr = ptr + self._post_init(srid) + + # Comparison operators + def __eq__(self, other): + """ + Equivalence testing, a Geometry may be compared with another Geometry + or a WKT representation. + """ + if isinstance(other, basestring): + return self.wkt == other + elif isinstance(other, GEOSGeometry): + return self.equals_exact(other) + else: + return False + + def __ne__(self, other): + "The not equals operator." + return not (self == other) + + ### 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): + "Returns the union of this Geometry and the other." + return self.union(other) + + # g = g1 & g2 + def __and__(self, other): + "Returns 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 has_cs(self): + "Returns True if this Geometry has a coordinate sequence, False if not." + # Only these geometries are allowed to have coordinate sequences. + if isinstance(self, (Point, LineString, LinearRing)): + return True + else: + return False + + def _set_cs(self): + "Sets the coordinate sequence for this Geometry." + if self.has_cs: + self._cs = GEOSCoordSeq(get_cs(self.ptr), self.hasz) + else: + self._cs = None + + @property + def coord_seq(self): + "Returns a clone of the coordinate sequence for this Geometry." + if self.has_cs: + return self._cs.clone() + + #### Geometry Info #### + @property + def geom_type(self): + "Returns a string representing the Geometry type, e.g. 'Polygon'" + return geos_type(self.ptr) + + @property + def geom_typeid(self): + "Returns an integer representing the Geometry type." + return geos_typeid(self.ptr) + + @property + def num_geom(self): + "Returns the number of geometries in the Geometry." + return get_num_geoms(self.ptr) + + @property + def num_coords(self): + "Returns the number of coordinates in the Geometry." + return get_num_coords(self.ptr) + + @property + def num_points(self): + "Returns the number points, or coordinates, in the Geometry." + return self.num_coords + + @property + def dims(self): + "Returns the dimension of this Geometry (0=point, 1=line, 2=surface)." + return get_dims(self.ptr) + + def normalize(self): + "Converts this Geometry to normal form (or canonical form)." + return geos_normalize(self.ptr) + + #### Unary predicates #### + @property + def empty(self): + """ + Returns a boolean indicating whether the set of points in this Geometry + are empty. + """ + return geos_isempty(self.ptr) + + @property + def hasz(self): + "Returns whether the geometry has a 3D dimension." + return geos_hasz(self.ptr) + + @property + def ring(self): + "Returns whether or not the geometry is a ring." + return geos_isring(self.ptr) + + @property + def simple(self): + "Returns false if the Geometry not simple." + return geos_issimple(self.ptr) + + @property + def valid(self): + "This property tests the validity of this Geometry." + return geos_isvalid(self.ptr) + + #### Binary predicates. #### + def contains(self, other): + "Returns true if other.within(this) returns true." + return geos_contains(self.ptr, other.ptr) + + def crosses(self, other): + """ + Returns 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 geos_crosses(self.ptr, other.ptr) + + def disjoint(self, other): + """ + Returns true if the DE-9IM intersection matrix for the two Geometries + is FF*FF****. + """ + return geos_disjoint(self.ptr, other.ptr) + + def equals(self, other): + """ + Returns true if the DE-9IM intersection matrix for the two Geometries + is T*F**FFF*. + """ + return geos_equals(self.ptr, other.ptr) + + def equals_exact(self, other, tolerance=0): + """ + Returns true if the two Geometries are exactly equal, up to a + specified tolerance. + """ + return geos_equalsexact(self.ptr, other.ptr, float(tolerance)) + + def intersects(self, other): + "Returns true if disjoint returns false." + return geos_intersects(self.ptr, other.ptr) + + def overlaps(self, other): + """ + Returns 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 geos_overlaps(self.ptr, other.ptr) + + def relate_pattern(self, other, pattern): + """ + Returns 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 geos_relatepattern(self.ptr, other.ptr, pattern) + + def touches(self, other): + """ + Returns true if the DE-9IM intersection matrix for the two Geometries + is FT*******, F**T***** or F***T****. + """ + return geos_touches(self.ptr, other.ptr) + + def within(self, other): + """ + Returns true if the DE-9IM intersection matrix for the two Geometries + is T*F**F***. + """ + return geos_within(self.ptr, other.ptr) + + #### SRID Routines #### + def get_srid(self): + "Gets the SRID for the geometry, returns None if no SRID is set." + s = geos_get_srid(self.ptr) + if s == 0: return None + else: return s + + def set_srid(self, srid): + "Sets the SRID for the geometry." + geos_set_srid(self.ptr, srid) + srid = property(get_srid, set_srid) + + #### Output Routines #### + @property + def ewkt(self): + "Returns the EWKT (WKT + SRID) of the Geometry." + if self.get_srid(): return 'SRID=%s;%s' % (self.srid, self.wkt) + else: return self.wkt + + @property + def wkt(self): + "Returns the WKT (Well-Known Text) of the Geometry." + return to_wkt(self.ptr) + + @property + def hex(self): + """ + Returns the HEX of the Geometry -- please note that the SRID is not + included in this representation, because the GEOS C library uses + -1 by default, even if the SRID is set. + """ + # A possible faster, all-python, implementation: + # str(self.wkb).encode('hex') + return to_hex(self.ptr, byref(c_size_t())) + + @property + def json(self): + """ + Returns GeoJSON representation of this Geometry if GDAL 1.5+ + is installed. + """ + if GEOJSON: return self.ogr.json + geojson = json + + @property + def wkb(self): + "Returns the WKB of the Geometry as a buffer." + bin = to_wkb(self.ptr, byref(c_size_t())) + return buffer(bin) + + @property + def kml(self): + "Returns the KML representation of this Geometry." + gtype = self.geom_type + return '<%s>%s' % (gtype, self.coord_seq.kml, gtype) + + #### GDAL-specific output routines #### + @property + def ogr(self): + "Returns the OGR Geometry for this Geometry." + if HAS_GDAL: + if self.srid: + return OGRGeometry(self.wkb, self.srid) + else: + return OGRGeometry(self.wkb) + else: + return None + + @property + def srs(self): + "Returns the OSR SpatialReference for SRID of this Geometry." + if HAS_GDAL and self.srid: + return SpatialReference(self.srid) + else: + return None + + @property + def crs(self): + "Alias for `srs` property." + return self.srs + + def transform(self, ct, clone=False): + """ + Requires GDAL. Transforms the geometry according to the given + transformation object, which may be an integer SRID, and WKT or + PROJ.4 string. By default, the geometry is transformed in-place and + nothing is returned. However if the `clone` keyword is set, then this + geometry will not be modified and a transformed clone will be returned + instead. + """ + srid = self.srid + if HAS_GDAL and srid: + g = OGRGeometry(self.wkb, srid) + g.transform(ct) + wkb = str(g.wkb) + ptr = from_wkb(wkb, len(wkb)) + 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. + destroy_geom(self.ptr) + self._ptr = ptr + self._post_init(g.srid) + else: + raise GEOSException('Transformed WKB was invalid.') + + #### Topology Routines #### + def _topology(self, gptr): + "Helper routine to return Geometry from the given pointer." + return GEOSGeometry(gptr, srid=self.srid) + + @property + def boundary(self): + "Returns the boundary as a newly allocated Geometry object." + return self._topology(geos_boundary(self.ptr)) + + def buffer(self, width, quadsegs=8): + """ + Returns 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(geos_buffer(self.ptr, width, quadsegs)) + + @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(geos_centroid(self.ptr)) + + @property + def convex_hull(self): + """ + Returns the smallest convex Polygon that contains all the points + in the Geometry. + """ + return self._topology(geos_convexhull(self.ptr)) + + def difference(self, other): + """ + Returns a Geometry representing the points making up this Geometry + that do not make up other. + """ + return self._topology(geos_difference(self.ptr, other.ptr)) + + @property + def envelope(self): + "Return the envelope for this geometry (a polygon)." + return self._topology(geos_envelope(self.ptr)) + + def intersection(self, other): + "Returns a Geometry representing the points shared by this Geometry and other." + return self._topology(geos_intersection(self.ptr, other.ptr)) + + @property + def point_on_surface(self): + "Computes an interior point of this Geometry." + return self._topology(geos_pointonsurface(self.ptr)) + + def relate(self, other): + "Returns the DE-9IM intersection matrix for this Geometry and the other." + return geos_relate(self.ptr, other.ptr) + + def simplify(self, tolerance=0.0, preserve_topology=False): + """ + Returns 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, this function does not 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(geos_preservesimplify(self.ptr, tolerance)) + else: + return self._topology(geos_simplify(self.ptr, tolerance)) + + def sym_difference(self, other): + """ + Returns a set combining the points in this Geometry not in other, + and the points in other not in this Geometry. + """ + return self._topology(geos_symdifference(self.ptr, other.ptr)) + + def union(self, other): + "Returns a Geometry representing all the points in this Geometry and other." + return self._topology(geos_union(self.ptr, other.ptr)) + + #### Other Routines #### + @property + def area(self): + "Returns the area of the Geometry." + return geos_area(self.ptr, byref(c_double())) + + def distance(self, other): + """ + Returns 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 geos_distance(self.ptr, other.ptr, byref(c_double())) + + @property + def extent(self): + """ + Returns the extent of this geometry as a 4-tuple, consisting of + (xmin, ymin, xmax, ymax). + """ + 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): + """ + Returns the length of this Geometry (e.g., 0 for point, or the + circumfrence of a Polygon). + """ + return geos_length(self.ptr, byref(c_double())) + + def clone(self): + "Clones this Geometry." + return GEOSGeometry(geom_clone(self.ptr), srid=self.srid) + +# Class mapping dictionary +from django.contrib.gis.geos.geometries import Point, Polygon, LineString, LinearRing +from django.contrib.gis.geos.collections import GeometryCollection, MultiPoint, MultiLineString, MultiPolygon +GEOS_CLASSES = {0 : Point, + 1 : LineString, + 2 : LinearRing, + 3 : Polygon, + 4 : MultiPoint, + 5 : MultiLineString, + 6 : MultiPolygon, + 7 : GeometryCollection, + } diff --git a/django/contrib/gis/geos/collections.py b/django/contrib/gis/geos/collections.py new file mode 100644 index 0000000000..a69b2e7c84 --- /dev/null +++ b/django/contrib/gis/geos/collections.py @@ -0,0 +1,105 @@ +""" + This module houses the Geometry Collection objects: + GeometryCollection, MultiPoint, MultiLineString, and MultiPolygon +""" +from ctypes import c_int, c_uint, byref +from types import TupleType, ListType +from django.contrib.gis.geos.base import GEOSGeometry +from django.contrib.gis.geos.error import GEOSException, GEOSIndexError +from django.contrib.gis.geos.geometries import Point, LineString, LinearRing, Polygon +from django.contrib.gis.geos.libgeos import get_pointer_arr, GEOM_PTR +from django.contrib.gis.geos.prototypes import create_collection, destroy_geom, geom_clone, geos_typeid, get_cs, get_geomn + +class GeometryCollection(GEOSGeometry): + _allowed = (Point, LineString, LinearRing, Polygon) + _typeid = 7 + + def __init__(self, *args, **kwargs): + "Initializes a Geometry Collection from a sequence of Geometry objects." + + # Checking the arguments + if not args: + raise TypeError, 'Must provide at least one Geometry to initialize %s.' % self.__class__.__name__ + + if len(args) == 1: + # If only one geometry provided or a list of geometries is provided + # in the first argument. + if isinstance(args[0], (TupleType, ListType)): + init_geoms = args[0] + else: + init_geoms = args + else: + init_geoms = args + + # Ensuring that only the permitted geometries are allowed in this collection + if False in [isinstance(geom, self._allowed) for geom in init_geoms]: + raise TypeError('Invalid Geometry type encountered in the arguments.') + + # Creating the geometry pointer array. + ngeoms = len(init_geoms) + geoms = get_pointer_arr(ngeoms) + for i in xrange(ngeoms): geoms[i] = geom_clone(init_geoms[i].ptr) + super(GeometryCollection, self).__init__(create_collection(c_int(self._typeid), byref(geoms), c_uint(ngeoms)), **kwargs) + + def __getitem__(self, index): + "Returns the Geometry from this Collection at the given index (0-based)." + # Checking the index and returning the corresponding GEOS geometry. + self._checkindex(index) + return GEOSGeometry(geom_clone(get_geomn(self.ptr, index)), srid=self.srid) + + def __setitem__(self, index, geom): + "Sets the Geometry at the specified index." + self._checkindex(index) + if not isinstance(geom, self._allowed): + raise TypeError('Incompatible Geometry for collection.') + + ngeoms = len(self) + geoms = get_pointer_arr(ngeoms) + for i in xrange(ngeoms): + if i == index: + geoms[i] = geom_clone(geom.ptr) + else: + geoms[i] = geom_clone(get_geomn(self.ptr, i)) + + # Creating a new collection, and destroying the contents of the previous poiner. + prev_ptr = self.ptr + srid = self.srid + self._ptr = create_collection(c_int(self._typeid), byref(geoms), c_uint(ngeoms)) + if srid: self.srid = srid + destroy_geom(prev_ptr) + + def __iter__(self): + "Iterates over each Geometry in the Collection." + for i in xrange(len(self)): + yield self.__getitem__(i) + + def __len__(self): + "Returns the number of geometries in this Collection." + return self.num_geom + + def _checkindex(self, index): + "Checks the given geometry index." + if index < 0 or index >= self.num_geom: + raise GEOSIndexError('invalid GEOS Geometry index: %s' % str(index)) + + @property + def kml(self): + "Returns the KML for this Geometry Collection." + return '%s' % ''.join([g.kml for g in self]) + + @property + def tuple(self): + "Returns 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(GeometryCollection): + _allowed = (LineString, LinearRing) + _typeid = 5 +class MultiPolygon(GeometryCollection): + _allowed = Polygon + _typeid = 6 diff --git a/django/contrib/gis/geos/coordseq.py b/django/contrib/gis/geos/coordseq.py new file mode 100644 index 0000000000..bc0c4794d4 --- /dev/null +++ b/django/contrib/gis/geos/coordseq.py @@ -0,0 +1,164 @@ +""" + 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 c_double, c_uint, byref +from types import ListType, TupleType +from django.contrib.gis.geos.error import GEOSException, GEOSIndexError +from django.contrib.gis.geos.libgeos import CS_PTR, HAS_NUMPY +from django.contrib.gis.geos.prototypes import cs_clone, cs_getdims, cs_getordinate, cs_getsize, cs_setordinate +if HAS_NUMPY: from numpy import ndarray + +class GEOSCoordSeq(object): + "The internal representation of a list of coordinates inside a Geometry." + + #### Python 'magic' routines #### + def __init__(self, ptr, z=False): + "Initializes 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): + "Iterates over each point in the coordinate sequence." + for i in xrange(self.size): + yield self[i] + + def __len__(self): + "Returns the number of points in the coordinate sequence." + return int(self.size) + + def __str__(self): + "Returns the string representation of the coordinate sequence." + return str(self.tuple) + + def __getitem__(self, index): + "Returns the coordinate sequence value at the given index." + coords = [self.getX(index), self.getY(index)] + if self.dims == 3 and self._z: + coords.append(self.getZ(index)) + return tuple(coords) + + def __setitem__(self, index, value): + "Sets the coordinate sequence value at the given index." + # Checking the input value + if isinstance(value, (ListType, TupleType)): + pass + elif HAS_NUMPY and isinstance(value, 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 + set_3d = True + else: + n_args = 2 + set_3d = False + if len(value) != n_args: + raise TypeError('Dimension of value does not match.') + # Setting the X, Y, Z + self.setX(index, value[0]) + self.setY(index, value[1]) + if set_3d: self.setZ(index, value[2]) + + #### Internal Routines #### + def _checkindex(self, index): + "Checks the given index." + sz = self.size + if (sz < 1) or (index < 0) or (index >= sz): + raise GEOSIndexError('invalid GEOS Geometry index: %s' % str(index)) + + def _checkdim(self, dim): + "Checks the given dimension." + if dim < 0 or dim > 2: + raise GEOSException('invalid ordinate dimension "%d"' % dim) + + @property + def ptr(self): + """ + Property for controlling access to coordinate sequence pointer, + preventing attempted access to a NULL memory location. + """ + if self._ptr: return self._ptr + else: raise GEOSException('NULL coordinate sequence pointer encountered.') + + #### Ordinate getting and setting routines #### + def getOrdinate(self, dimension, index): + "Returns the value for the given dimension and index." + self._checkindex(index) + self._checkdim(dimension) + return cs_getordinate(self.ptr, index, dimension, byref(c_double())) + + def setOrdinate(self, dimension, index, value): + "Sets the value for the given dimension and index." + self._checkindex(index) + self._checkdim(dimension) + 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): + "Returns the size of this coordinate sequence." + return cs_getsize(self.ptr, byref(c_uint())) + + @property + def dims(self): + "Returns the dimensions of this coordinate sequence." + return cs_getdims(self.ptr, byref(c_uint())) + + @property + def hasz(self): + """ + Returns whether this coordinate sequence is 3D. This property value is + inherited from the parent Geometry. + """ + return self._z + + ### Other Methods ### + def clone(self): + "Clones this coordinate sequence." + return GEOSCoordSeq(cs_clone(self.ptr), self.hasz) + + @property + def kml(self): + "Returns 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 '%s' % \ + ''.join([substr % self[i] for i in xrange(len(self))]).strip() + + @property + def tuple(self): + "Returns a tuple version of this coordinate sequence." + n = self.size + if n == 1: return self[0] + else: return tuple([self[i] for i in xrange(n)]) diff --git a/django/contrib/gis/geos/error.py b/django/contrib/gis/geos/error.py new file mode 100644 index 0000000000..46bdfe691a --- /dev/null +++ b/django/contrib/gis/geos/error.py @@ -0,0 +1,20 @@ +""" + This module houses the GEOS exceptions, specifically, GEOSException and + GEOSGeometryIndexError. +""" + +class GEOSException(Exception): + "The base GEOS exception, indicates a GEOS-related error." + pass + +class GEOSIndexError(GEOSException, KeyError): + """ + This exception is raised when an invalid index is encountered, and has + the 'silent_variable_feature' attribute set to true. This ensures that + django's templates proceed to use the next lookup type gracefully when + an Exception is raised. Fixes ticket #4740. + """ + # "If, during the method lookup, a method raises an exception, the exception + # will be propagated, unless the exception has an attribute + # `silent_variable_failure` whose value is True." -- Django template docs. + silent_variable_failure = True diff --git a/django/contrib/gis/geos/geometries.py b/django/contrib/gis/geos/geometries.py new file mode 100644 index 0000000000..c5420e93af --- /dev/null +++ b/django/contrib/gis/geos/geometries.py @@ -0,0 +1,391 @@ +""" + This module houses the Point, LineString, LinearRing, and Polygon OGC + geometry classes. All geometry classes in this module inherit from + GEOSGeometry. +""" +from ctypes import c_uint, byref +from django.contrib.gis.geos.base import GEOSGeometry +from django.contrib.gis.geos.coordseq import GEOSCoordSeq +from django.contrib.gis.geos.error import GEOSException, GEOSIndexError +from django.contrib.gis.geos.libgeos import get_pointer_arr, GEOM_PTR, HAS_NUMPY +from django.contrib.gis.geos.prototypes import * +if HAS_NUMPY: from numpy import ndarray, array + +class Point(GEOSGeometry): + + def __init__(self, x, y=None, z=None, srid=None): + """ + The Point object may be initialized with either a tuple, or individual + parameters. + + For Example: + >>> p = Point((5, 23)) # 2D point, passed in as a tuple + >>> p = Point(5, 23, 8) # 3D point, passed in with individual parameters + """ + + if isinstance(x, (tuple, list)): + # Here a tuple or list was passed in under the `x` parameter. + ndim = len(x) + if ndim < 2 or ndim > 3: + raise TypeError('Invalid sequence parameter: %s' % str(x)) + coords = x + elif isinstance(x, (int, float, long)) and isinstance(y, (int, float, long)): + # Here X, Y, and (optionally) Z were passed in individually, as parameters. + if isinstance(z, (int, float, long)): + ndim = 3 + coords = [x, y, z] + else: + ndim = 2 + coords = [x, y] + else: + raise TypeError('Invalid parameters given for Point initialization.') + + # Creating the coordinate sequence, and setting X, Y, [Z] + cs = create_cs(c_uint(1), c_uint(ndim)) + cs_setx(cs, 0, coords[0]) + cs_sety(cs, 0, coords[1]) + if ndim == 3: cs_setz(cs, 0, coords[2]) + + # Initializing using the address returned from the GEOS + # createPoint factory. + super(Point, self).__init__(create_point(cs), srid=srid) + + def __len__(self): + "Returns the number of dimensions for this Point (either 0, 2 or 3)." + if self.empty: return 0 + if self.hasz: return 3 + else: return 2 + + def get_x(self): + "Returns the X component of the Point." + return self._cs.getOrdinate(0, 0) + + def set_x(self, value): + "Sets the X component of the Point." + self._cs.setOrdinate(0, 0, value) + + def get_y(self): + "Returns the Y component of the Point." + return self._cs.getOrdinate(1, 0) + + def set_y(self, value): + "Sets the Y component of the Point." + self._cs.setOrdinate(1, 0, value) + + def get_z(self): + "Returns the Z component of the Point." + if self.hasz: + return self._cs.getOrdinate(2, 0) + else: + return None + + def set_z(self, value): + "Sets the Z component of the Point." + if self.hasz: + self._cs.setOrdinate(2, 0, value) + else: + raise GEOSException('Cannot set Z on 2D Point.') + + # X, Y, Z properties + x = property(get_x, set_x) + y = property(get_y, set_y) + z = property(get_z, set_z) + + ### Tuple setting and retrieval routines. ### + def get_coords(self): + "Returns a tuple of the point." + return self._cs.tuple + + def set_coords(self, tup): + "Sets the coordinates of the point with the given tuple." + self._cs[0] = tup + + # The tuple and coords properties + tuple = property(get_coords, set_coords) + coords = tuple + +class LineString(GEOSGeometry): + + #### Python 'magic' routines #### + def __init__(self, *args, **kwargs): + """ + Initializes 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 isinstance(coords, (tuple, list)): + # Getting the number of coords and the number of dimensions -- which + # must stay the same, e.g., no LineString((1, 2), (1, 2, 3)). + ncoords = len(coords) + if coords: ndim = len(coords[0]) + else: raise TypeError('Cannot initialize on empty sequence.') + self._checkdim(ndim) + # Incrementing through each of the coordinates and verifying + for i in xrange(1, ncoords): + if not isinstance(coords[i], (tuple, list, Point)): + raise TypeError('each coordinate should be a sequence (list or tuple)') + if len(coords[i]) != ndim: raise TypeError('Dimension mismatch.') + numpy_coords = False + elif HAS_NUMPY and isinstance(coords, ndarray): + shape = coords.shape # Using numpy's shape. + if len(shape) != 2: raise TypeError('Too many dimensions.') + self._checkdim(shape[1]) + ncoords = shape[0] + ndim = shape[1] + numpy_coords = True + else: + raise TypeError('Invalid initialization input for LineStrings.') + + # Creating a coordinate sequence object because it is easier to + # set the points using GEOSCoordSeq.__setitem__(). + cs = GEOSCoordSeq(create_cs(ncoords, ndim), z=bool(ndim==3)) + for i in xrange(ncoords): + if numpy_coords: cs[i] = coords[i,:] + elif isinstance(coords[i], Point): cs[i] = coords[i].tuple + else: cs[i] = coords[i] + + # Getting the correct initialization function + if kwargs.get('ring', False): + func = create_linearring + else: + func = create_linestring + + # If SRID was passed in with the keyword arguments + srid = kwargs.get('srid', None) + + # Calling the base geometry initialization with the returned pointer + # from the function. + super(LineString, self).__init__(func(cs.ptr), srid=srid) + + def __getitem__(self, index): + "Gets the point at the specified index." + return self._cs[index] + + def __setitem__(self, index, value): + "Sets the point at the specified index, e.g., line_str[0] = (1, 2)." + self._cs[index] = value + + def __iter__(self): + "Allows iteration over this LineString." + for i in xrange(len(self)): + yield self[i] + + def __len__(self): + "Returns the number of points in this LineString." + return len(self._cs) + + def _checkdim(self, dim): + if dim not in (2, 3): raise TypeError('Dimension mismatch.') + + #### Sequence Properties #### + @property + def tuple(self): + "Returns a tuple version of the geometry from the coordinate sequence." + return self._cs.tuple + coords = tuple + + def _listarr(self, func): + """ + Internal routine that returns a sequence (list) corresponding with + the given function. Will return a numpy array if possible. + """ + lst = [func(i) for i in xrange(len(self))] + if HAS_NUMPY: return array(lst) # ARRRR! + else: return lst + + @property + def array(self): + "Returns a numpy array for the LineString." + return self._listarr(self._cs.__getitem__) + + @property + def x(self): + "Returns a list or numpy array of the X variable." + return self._listarr(self._cs.getX) + + @property + def y(self): + "Returns a list or numpy array of the Y variable." + return self._listarr(self._cs.getY) + + @property + def z(self): + "Returns 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): + def __init__(self, *args, **kwargs): + "Overriding the initialization function to set the ring keyword." + kwargs['ring'] = True # Setting the ring keyword argument to True + super(LinearRing, self).__init__(*args, **kwargs) + +class Polygon(GEOSGeometry): + + def __init__(self, *args, **kwargs): + """ + Initializes on an exterior ring and a sequence of holes (both + instances may be either LinearRing instances, or a tuple/list + that may be constructed into a LinearRing). + + Examples of initialization, where shell, hole1, and hole2 are + valid LinearRing geometries: + >>> poly = Polygon(shell, hole1, hole2) + >>> poly = Polygon(shell, (hole1, hole2)) + + Example where a tuple parameters are used: + >>> poly = Polygon(((0, 0), (0, 10), (10, 10), (0, 10), (0, 0)), + ((4, 4), (4, 6), (6, 6), (6, 4), (4, 4))) + """ + if not args: + raise TypeError('Must provide at list one LinearRing instance to initialize Polygon.') + + # Getting the ext_ring and init_holes parameters from the argument list + ext_ring = args[0] + init_holes = args[1:] + n_holes = len(init_holes) + + # If initialized as Polygon(shell, (LinearRing, LinearRing)) [for backward-compatibility] + if n_holes == 1 and isinstance(init_holes[0], (tuple, list)) and \ + (len(init_holes[0]) == 0 or isinstance(init_holes[0][0], LinearRing)): + init_holes = init_holes[0] + n_holes = len(init_holes) + + # Ensuring the exterior ring and holes parameters are LinearRing objects + # or may be instantiated into LinearRings. + ext_ring = self._construct_ring(ext_ring, 'Exterior parameter must be a LinearRing or an object that can initialize a LinearRing.') + holes_list = [] # Create new list, cause init_holes is a tuple. + for i in xrange(n_holes): + holes_list.append(self._construct_ring(init_holes[i], 'Holes parameter must be a sequence of LinearRings or objects that can initialize to LinearRings')) + + # Why another loop? Because if a TypeError is raised, cloned pointers will + # be around that can't be cleaned up. + holes = get_pointer_arr(n_holes) + for i in xrange(n_holes): holes[i] = geom_clone(holes_list[i].ptr) + + # Getting the shell pointer address. + shell = geom_clone(ext_ring.ptr) + + # Calling with the GEOS createPolygon factory. + super(Polygon, self).__init__(create_polygon(shell, byref(holes), c_uint(n_holes)), **kwargs) + + def __getitem__(self, index): + """ + Returns the ring at the specified index. The first index, 0, will + always return the exterior ring. Indices > 0 will return the + interior ring at the given index (e.g., poly[1] and poly[2] would + return the first and second interior ring, respectively). + """ + if index == 0: + return self.exterior_ring + else: + # Getting the interior ring, have to subtract 1 from the index. + return self.get_interior_ring(index-1) + + def __setitem__(self, index, ring): + "Sets the ring at the specified index with the given ring." + # Checking the index and ring parameters. + self._checkindex(index) + if not isinstance(ring, LinearRing): + raise TypeError('must set Polygon index with a LinearRing object') + + # Getting the shell + if index == 0: + shell = geom_clone(ring.ptr) + else: + shell = geom_clone(get_extring(self.ptr)) + + # Getting the interior rings (holes) + nholes = len(self)-1 + if nholes > 0: + holes = get_pointer_arr(nholes) + for i in xrange(nholes): + if i == (index-1): + holes[i] = geom_clone(ring.ptr) + else: + holes[i] = geom_clone(get_intring(self.ptr, i)) + holes_param = byref(holes) + else: + holes_param = None + + # Getting the current pointer, replacing with the newly constructed + # geometry, and destroying the old geometry. + prev_ptr = self.ptr + srid = self.srid + self._ptr = create_polygon(shell, holes_param, c_uint(nholes)) + if srid: self.srid = srid + destroy_geom(prev_ptr) + + def __iter__(self): + "Iterates over each ring in the polygon." + for i in xrange(len(self)): + yield self[i] + + def __len__(self): + "Returns the number of rings in this Polygon." + return self.num_interior_rings + 1 + + def _checkindex(self, index): + "Internal routine for checking the given ring index." + if index < 0 or index >= len(self): + raise GEOSIndexError('invalid Polygon ring index: %s' % index) + + def _construct_ring(self, param, msg=''): + "Helper routine for trying to construct a ring from the given parameter." + if isinstance(param, LinearRing): return param + try: + ring = LinearRing(param) + return ring + except TypeError: + raise TypeError(msg) + + def get_interior_ring(self, ring_i): + """ + Gets the interior ring at the specified index, 0 is for the first + interior ring, not the exterior ring. + """ + self._checkindex(ring_i+1) + return GEOSGeometry(geom_clone(get_intring(self.ptr, ring_i)), srid=self.srid) + + #### Polygon Properties #### + @property + def num_interior_rings(self): + "Returns the number of interior rings." + # Getting the number of rings + return get_nrings(self.ptr) + + def get_ext_ring(self): + "Gets the exterior ring of the Polygon." + return GEOSGeometry(geom_clone(get_extring(self.ptr)), srid=self.srid) + + def set_ext_ring(self, ring): + "Sets the exterior ring of the Polygon." + self[0] = ring + + # properties for the exterior ring/shell + exterior_ring = property(get_ext_ring, set_ext_ring) + shell = exterior_ring + + @property + def tuple(self): + "Gets the tuple for each ring in this Polygon." + return tuple([self[i].tuple for i in xrange(len(self))]) + coords = tuple + + @property + def kml(self): + "Returns the KML representation of this Polygon." + inner_kml = ''.join(["%s" % self[i+1].kml + for i in xrange(self.num_interior_rings)]) + return "%s%s" % (self[0].kml, inner_kml) diff --git a/django/contrib/gis/geos/libgeos.py b/django/contrib/gis/geos/libgeos.py new file mode 100644 index 0000000000..d2990a6d0e --- /dev/null +++ b/django/contrib/gis/geos/libgeos.py @@ -0,0 +1,126 @@ +""" + 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 atexit, os, re, sys +from ctypes import c_char_p, Structure, CDLL, CFUNCTYPE, POINTER +from ctypes.util import find_library +from django.contrib.gis.geos.error import GEOSException + +# NumPy supported? +try: + from numpy import array, ndarray + HAS_NUMPY = True +except ImportError: + HAS_NUMPY = False + +# Custom library path set? +try: + from django.conf import settings + lib_path = settings.GEOS_LIBRARY_PATH +except (AttributeError, EnvironmentError, ImportError): + 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 = ['libgeos_c-1'] +elif os.name == 'posix': + # *NIX libraries + lib_names = ['geos_c'] +else: + raise GEOSException('Unsupported OS "%s"' % os.name) + +# Using the ctypes `find_library` utility to find the the path to the GEOS +# shared library. This is better than manually specifiying 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 not lib_path is None: break + +# No GEOS library could be found. +if lib_path is None: + raise GEOSException('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: +# http://geos.refractions.net/ro/doxygen_docs/html/geos__c_8h-source.html +lgeos = CDLL(lib_path) + +# 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, output_h=sys.stdout): + try: + warn_msg = fmt % lst + except: + warn_msg = fmt + output_h.write('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, output_h=sys.stderr): + try: + err_msg = fmt % lst + except: + err_msg = fmt + output_h.write('GEOS_ERROR: %s\n' % err_msg) +error_h = ERRORFUNC(error_h) + +# The initGEOS routine should be called first, however, that routine takes +# the notice and error functions as parameters. Here is the C code that +# is wrapped: +# "extern void GEOS_DLL initGEOS(GEOSMessageHandler notice_function, GEOSMessageHandler error_function);" +lgeos.initGEOS(notice_h, 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 GEOSCoordSeq_t(Structure): pass + +# Pointers to opaque GEOS geometry structures. +GEOM_PTR = POINTER(GEOSGeom_t) +CS_PTR = POINTER(GEOSCoordSeq_t) + +# Used specifically by the GEOSGeom_createPolygon and GEOSGeom_createCollection +# GEOS routines +def get_pointer_arr(n): + "Gets a ctypes pointer array (of length `n`) for GEOSGeom_t opaque pointer." + GeomArr = GEOM_PTR * n + return GeomArr() + +# Returns the string version of the GEOS library. Have to set the restype +# explicitly to c_char_p to ensure compatibility accross 32 and 64-bit platforms. +geos_version = lgeos.GEOSversion +geos_version.argtypes = None +geos_version.restype = c_char_p + +# Regular expression should be able to parse version strings such as +# '3.0.0rc4-CAPI-1.3.3', or '3.0.0-CAPI-1.4.1' +version_regex = re.compile(r'^(?P\d+\.\d+\.\d+)(rc(?P\d+))?-CAPI-(?P\d+\.\d+\.\d+)$') +def geos_version_info(): + """ + Returns a dictionary containing the various version metadata parsed from + the GEOS version string, including the version number, whether the version + is a release candidate (and what number release candidate), and the C API + version. + """ + ver = geos_version() + m = version_regex.match(ver) + if not m: raise GEOSException('Could not parse version info string "%s"' % ver) + return dict((key, m.group(key)) for key in ('version', 'release_candidate', 'capi_version')) + +# Calling the finishGEOS() upon exit of the interpreter. +atexit.register(lgeos.finishGEOS) diff --git a/django/contrib/gis/geos/prototypes/__init__.py b/django/contrib/gis/geos/prototypes/__init__.py new file mode 100644 index 0000000000..b4639f3d37 --- /dev/null +++ b/django/contrib/gis/geos/prototypes/__init__.py @@ -0,0 +1,33 @@ +""" + This module contains all of the GEOS ctypes function prototypes. Each + prototype handles the interaction between the GEOS library and Python + via ctypes. +""" + +# Coordinate sequence routines. +from django.contrib.gis.geos.prototypes.coordseq import create_cs, get_cs, \ + cs_clone, cs_getordinate, cs_setordinate, cs_getx, cs_gety, cs_getz, \ + cs_setx, cs_sety, cs_setz, cs_getsize, cs_getdims + +# Geometry routines. +from django.contrib.gis.geos.prototypes.geom import from_hex, from_wkb, from_wkt, \ + create_point, create_linestring, create_linearring, create_polygon, create_collection, \ + destroy_geom, get_extring, get_intring, get_nrings, get_geomn, geom_clone, \ + geos_normalize, geos_type, geos_typeid, geos_get_srid, geos_set_srid, \ + get_dims, get_num_coords, get_num_geoms, \ + to_hex, to_wkb, to_wkt + +# Miscellaneous routines. +from django.contrib.gis.geos.prototypes.misc import geos_area, geos_distance, geos_length + +# Predicates +from django.contrib.gis.geos.prototypes.predicates import geos_hasz, geos_isempty, \ + geos_isring, geos_issimple, geos_isvalid, geos_contains, geos_crosses, \ + geos_disjoint, geos_equals, geos_equalsexact, geos_intersects, \ + geos_intersects, geos_overlaps, geos_relatepattern, geos_touches, geos_within + +# Topology routines +from django.contrib.gis.geos.prototypes.topology import \ + geos_boundary, geos_buffer, geos_centroid, geos_convexhull, geos_difference, \ + geos_envelope, geos_intersection, geos_pointonsurface, geos_preservesimplify, \ + geos_simplify, geos_symdifference, geos_union, geos_relate diff --git a/django/contrib/gis/geos/prototypes/coordseq.py b/django/contrib/gis/geos/prototypes/coordseq.py new file mode 100644 index 0000000000..3b5fb2cf35 --- /dev/null +++ b/django/contrib/gis/geos/prototypes/coordseq.py @@ -0,0 +1,82 @@ +from ctypes import c_double, c_int, c_uint, POINTER +from django.contrib.gis.geos.libgeos import lgeos, GEOM_PTR, CS_PTR +from django.contrib.gis.geos.prototypes.errcheck import last_arg_byref, GEOSException + +## Error-checking routines specific to coordinate sequences. ## +def check_cs_ptr(result, func, cargs): + "Error checking on routines that return Geometries." + if not result: + raise GEOSException('Error encountered checking Coordinate Sequence returned from GEOS C function "%s".' % func.__name__) + return result + +def check_cs_op(result, func, cargs): + "Checks the status code of a coordinate sequence operation." + if result == 0: + raise GEOSException('Could not set value on coordinate sequence') + else: + return result + +def check_cs_get(result, func, cargs): + "Checking the coordinate sequence retrieval." + check_cs_op(result, func, cargs) + # Object in by reference, return its value. + return last_arg_byref(cargs) + +## Coordinate sequence prototype generation functions. ## +def cs_int(func): + "For coordinate sequence routines that return an integer." + func.argtypes = [CS_PTR, POINTER(c_uint)] + func.restype = c_int + func.errcheck = check_cs_get + return func + +def cs_operation(func, ordinate=False, get=False): + "For coordinate sequence operations." + if get: + # Get routines get double parameter passed-in by reference. + func.errcheck = check_cs_get + dbl_param = POINTER(c_double) + else: + func.errcheck = check_cs_op + dbl_param = c_double + + if ordinate: + # Get/Set ordinate routines have an extra uint parameter. + func.argtypes = [CS_PTR, c_uint, c_uint, dbl_param] + else: + func.argtypes = [CS_PTR, c_uint, dbl_param] + + func.restype = c_int + return func + +def cs_output(func, argtypes): + "For routines that return a coordinate sequence." + func.argtypes = argtypes + func.restype = CS_PTR + func.errcheck = check_cs_ptr + return func + +## Coordinate Sequence ctypes prototypes ## + +# Coordinate Sequence constructors & cloning. +cs_clone = cs_output(lgeos.GEOSCoordSeq_clone, [CS_PTR]) +create_cs = cs_output(lgeos.GEOSCoordSeq_create, [c_uint, c_uint]) +get_cs = cs_output(lgeos.GEOSGeom_getCoordSeq, [GEOM_PTR]) + +# Getting, setting ordinate +cs_getordinate = cs_operation(lgeos.GEOSCoordSeq_getOrdinate, ordinate=True, get=True) +cs_setordinate = cs_operation(lgeos.GEOSCoordSeq_setOrdinate, ordinate=True) + +# For getting, x, y, z +cs_getx = cs_operation(lgeos.GEOSCoordSeq_getX, get=True) +cs_gety = cs_operation(lgeos.GEOSCoordSeq_getY, get=True) +cs_getz = cs_operation(lgeos.GEOSCoordSeq_getZ, get=True) + +# For setting, x, y, z +cs_setx = cs_operation(lgeos.GEOSCoordSeq_setX) +cs_sety = cs_operation(lgeos.GEOSCoordSeq_setY) +cs_setz = cs_operation(lgeos.GEOSCoordSeq_setZ) + +# These routines return size & dimensions. +cs_getsize = cs_int(lgeos.GEOSCoordSeq_getSize) +cs_getdims = cs_int(lgeos.GEOSCoordSeq_getDimensions) diff --git a/django/contrib/gis/geos/prototypes/errcheck.py b/django/contrib/gis/geos/prototypes/errcheck.py new file mode 100644 index 0000000000..6fcc1a7a78 --- /dev/null +++ b/django/contrib/gis/geos/prototypes/errcheck.py @@ -0,0 +1,76 @@ +""" + Error checking functions for GEOS ctypes prototype functions. +""" +import os +from ctypes import string_at, CDLL +from ctypes.util import find_library +from django.contrib.gis.geos.error import GEOSException + +# Getting the C library, needed to free the string pointers +# returned from GEOS. +if os.name == 'nt': + libc_name = 'msvcrt' +else: + libc_name = 'libc' +libc = CDLL(find_library(libc_name)) + +### ctypes error checking routines ### +def last_arg_byref(args): + "Returns the last C argument's by reference value." + return args[-1]._obj.value + +def check_dbl(result, func, cargs): + "Checks the status code and returns the double value passed in by reference." + # Checking the status code + if result != 1: return None + # Double passed in by reference, return its value. + return last_arg_byref(cargs) + +def check_geom(result, func, cargs): + "Error checking on routines that return Geometries." + if not result: + raise GEOSException('Error encountered checking Geometry returned from GEOS C function "%s".' % func.__name__) + return result + +def check_minus_one(result, func, cargs): + "Error checking on routines that should not return -1." + if result == -1: + raise GEOSException('Error encountered in GEOS C function "%s".' % func.__name__) + else: + return result + +def check_predicate(result, func, cargs): + "Error checking for unary/binary predicate functions." + val = ord(result) # getting the ordinal from the character + if val == 1: return True + elif val == 0: return False + else: + raise GEOSException('Error encountered on GEOS C predicate function "%s".' % func.__name__) + +def check_sized_string(result, func, cargs): + "Error checking for routines that return explicitly sized strings." + if not result: + raise GEOSException('Invalid string pointer returned by GEOS C function "%s"' % func.__name__) + # A c_size_t object is passed in by reference for the second + # argument on these routines, and its needed to determine the + # correct size. + s = string_at(result, last_arg_byref(cargs)) + libc.free(result) + return s + +def check_string(result, func, cargs): + "Error checking for routines that return strings." + if not result: raise GEOSException('Error encountered checking string return value in GEOS C function "%s".' % func.__name__) + # Getting the string value at the pointer address. + s = string_at(result) + # Freeing the memory allocated by the GEOS library. + libc.free(result) + return s + +def check_zero(result, func, cargs): + "Error checking on routines that should not return 0." + if result == 0: + raise GEOSException('Error encountered in GEOS C function "%s".' % func.__name__) + else: + return result + diff --git a/django/contrib/gis/geos/prototypes/geom.py b/django/contrib/gis/geos/prototypes/geom.py new file mode 100644 index 0000000000..e5942a5957 --- /dev/null +++ b/django/contrib/gis/geos/prototypes/geom.py @@ -0,0 +1,111 @@ +from ctypes import c_char_p, c_int, c_size_t, c_uint, POINTER +from django.contrib.gis.geos.libgeos import lgeos, CS_PTR, GEOM_PTR +from django.contrib.gis.geos.prototypes.errcheck import \ + check_geom, check_minus_one, check_sized_string, check_string, check_zero + +### ctypes generation functions ### +def bin_constructor(func): + "Generates a prototype for binary construction (HEX, WKB) GEOS routines." + func.argtypes = [c_char_p, c_size_t] + func.restype = GEOM_PTR + func.errcheck = check_geom + return func + +# HEX & WKB output +def bin_output(func): + "Generates a prototype for the routines that return a a sized string." + func.argtypes = [GEOM_PTR, POINTER(c_size_t)] + func.errcheck = check_sized_string + return func + +def geom_output(func, argtypes): + "For GEOS routines that return a geometry." + if argtypes: func.argtypes = argtypes + func.restype = GEOM_PTR + func.errcheck = check_geom + return func + +def geom_index(func): + "For GEOS routines that return geometries from an index." + return geom_output(func, [GEOM_PTR, c_int]) + +def int_from_geom(func, zero=False): + "Argument is a geometry, return type is an integer." + func.argtypes = [GEOM_PTR] + func.restype = c_int + if zero: + func.errcheck = check_zero + else: + func.errcheck = check_minus_one + return func + +def string_from_geom(func): + "Argument is a Geometry, return type is a string." + # We do _not_ specify an argument type because we want just an + # address returned from the function. + func.argtypes = [GEOM_PTR] + func.errcheck = check_string + return func + +### ctypes prototypes ### + +# TODO: Tell all users to use GEOS 3.0.0, instead of the release +# candidates, and use the new Reader and Writer APIs (e.g., +# GEOSWKT[Reader|Writer], GEOSWKB[Reader|Writer]). A good time +# to do this will be when Refractions releases a Windows PostGIS +# installer using GEOS 3.0.0. + +# Creation routines from WKB, HEX, WKT +from_hex = bin_constructor(lgeos.GEOSGeomFromHEX_buf) +from_wkb = bin_constructor(lgeos.GEOSGeomFromWKB_buf) +from_wkt = geom_output(lgeos.GEOSGeomFromWKT, [c_char_p]) + +# Output routines +to_hex = bin_output(lgeos.GEOSGeomToHEX_buf) +to_wkb = bin_output(lgeos.GEOSGeomToWKB_buf) +to_wkt = string_from_geom(lgeos.GEOSGeomToWKT) + +# The GEOS geometry type, typeid, num_coordites and number of geometries +geos_normalize = int_from_geom(lgeos.GEOSNormalize) +geos_type = string_from_geom(lgeos.GEOSGeomType) +geos_typeid = int_from_geom(lgeos.GEOSGeomTypeId) +get_dims = int_from_geom(lgeos.GEOSGeom_getDimensions, zero=True) +get_num_coords = int_from_geom(lgeos.GEOSGetNumCoordinates) +get_num_geoms = int_from_geom(lgeos.GEOSGetNumGeometries) + +# Geometry creation factories +create_point = geom_output(lgeos.GEOSGeom_createPoint, [CS_PTR]) +create_linestring = geom_output(lgeos.GEOSGeom_createLineString, [CS_PTR]) +create_linearring = geom_output(lgeos.GEOSGeom_createLinearRing, [CS_PTR]) + +# Polygon and collection creation routines are special and will not +# have their argument types defined. +create_polygon = geom_output(lgeos.GEOSGeom_createPolygon, None) +create_collection = geom_output(lgeos.GEOSGeom_createCollection, None) + +# Ring routines +get_extring = geom_output(lgeos.GEOSGetExteriorRing, [GEOM_PTR]) +get_intring = geom_index(lgeos.GEOSGetInteriorRingN) +get_nrings = int_from_geom(lgeos.GEOSGetNumInteriorRings) + +# Collection Routines +get_geomn = geom_index(lgeos.GEOSGetGeometryN) + +# Cloning +geom_clone = lgeos.GEOSGeom_clone +geom_clone.argtypes = [GEOM_PTR] +geom_clone.restype = GEOM_PTR + +# Destruction routine. +destroy_geom = lgeos.GEOSGeom_destroy +destroy_geom.argtypes = [GEOM_PTR] +destroy_geom.restype = None + +# SRID routines +geos_get_srid = lgeos.GEOSGetSRID +geos_get_srid.argtypes = [GEOM_PTR] +geos_get_srid.restype = c_int + +geos_set_srid = lgeos.GEOSSetSRID +geos_set_srid.argtypes = [GEOM_PTR, c_int] +geos_set_srid.restype = None diff --git a/django/contrib/gis/geos/prototypes/misc.py b/django/contrib/gis/geos/prototypes/misc.py new file mode 100644 index 0000000000..255da91d9e --- /dev/null +++ b/django/contrib/gis/geos/prototypes/misc.py @@ -0,0 +1,27 @@ +""" + This module is for the miscellaneous GEOS routines, particularly the + ones that return the area, distance, and length. +""" +from ctypes import c_int, c_double, POINTER +from django.contrib.gis.geos.libgeos import lgeos, GEOM_PTR +from django.contrib.gis.geos.prototypes.errcheck import check_dbl + +### ctypes generator function ### +def dbl_from_geom(func, num_geom=1): + """ + Argument is a Geometry, return type is double that is passed + in by reference as the last argument. + """ + argtypes = [GEOM_PTR for i in xrange(num_geom)] + argtypes += [POINTER(c_double)] + func.argtypes = argtypes + func.restype = c_int # Status code returned + func.errcheck = check_dbl + return func + +### ctypes prototypes ### + +# Area, distance, and length prototypes. +geos_area = dbl_from_geom(lgeos.GEOSArea) +geos_distance = dbl_from_geom(lgeos.GEOSDistance, num_geom=2) +geos_length = dbl_from_geom(lgeos.GEOSLength) diff --git a/django/contrib/gis/geos/prototypes/predicates.py b/django/contrib/gis/geos/prototypes/predicates.py new file mode 100644 index 0000000000..45240d971a --- /dev/null +++ b/django/contrib/gis/geos/prototypes/predicates.py @@ -0,0 +1,43 @@ +""" + This module houses the GEOS ctypes prototype functions for the + unary and binary predicate operations on geometries. +""" +from ctypes import c_char, c_char_p, c_double +from django.contrib.gis.geos.libgeos import lgeos, GEOM_PTR +from django.contrib.gis.geos.prototypes.errcheck import check_predicate + +## Binary & unary predicate functions ## +def binary_predicate(func, *args): + "For GEOS binary predicate functions." + argtypes = [GEOM_PTR, GEOM_PTR] + if args: argtypes += args + func.argtypes = argtypes + func.restype = c_char + func.errcheck = check_predicate + return func + +def unary_predicate(func): + "For GEOS unary predicate functions." + func.argtypes = [GEOM_PTR] + func.restype = c_char + func.errcheck = check_predicate + return func + +## Unary Predicates ## +geos_hasz = unary_predicate(lgeos.GEOSHasZ) +geos_isempty = unary_predicate(lgeos.GEOSisEmpty) +geos_isring = unary_predicate(lgeos.GEOSisRing) +geos_issimple = unary_predicate(lgeos.GEOSisSimple) +geos_isvalid = unary_predicate(lgeos.GEOSisValid) + +## Binary Predicates ## +geos_contains = binary_predicate(lgeos.GEOSContains) +geos_crosses = binary_predicate(lgeos.GEOSCrosses) +geos_disjoint = binary_predicate(lgeos.GEOSDisjoint) +geos_equals = binary_predicate(lgeos.GEOSEquals) +geos_equalsexact = binary_predicate(lgeos.GEOSEqualsExact, c_double) +geos_intersects = binary_predicate(lgeos.GEOSIntersects) +geos_overlaps = binary_predicate(lgeos.GEOSOverlaps) +geos_relatepattern = binary_predicate(lgeos.GEOSRelatePattern, c_char_p) +geos_touches = binary_predicate(lgeos.GEOSTouches) +geos_within = binary_predicate(lgeos.GEOSWithin) diff --git a/django/contrib/gis/geos/prototypes/topology.py b/django/contrib/gis/geos/prototypes/topology.py new file mode 100644 index 0000000000..70cf900098 --- /dev/null +++ b/django/contrib/gis/geos/prototypes/topology.py @@ -0,0 +1,35 @@ +""" + This module houses the GEOS ctypes prototype functions for the + topological operations on geometries. +""" +from ctypes import c_char_p, c_double, c_int +from django.contrib.gis.geos.libgeos import lgeos, GEOM_PTR +from django.contrib.gis.geos.prototypes.errcheck import check_geom, check_string + +def topology(func, *args): + "For GEOS unary topology functions." + argtypes = [GEOM_PTR] + if args: argtypes += args + func.argtypes = argtypes + func.restype = GEOM_PTR + func.errcheck = check_geom + return func + +### Topology Routines ### +geos_boundary = topology(lgeos.GEOSBoundary) +geos_buffer = topology(lgeos.GEOSBuffer, c_double, c_int) +geos_centroid = topology(lgeos.GEOSGetCentroid) +geos_convexhull = topology(lgeos.GEOSConvexHull) +geos_difference = topology(lgeos.GEOSDifference, GEOM_PTR) +geos_envelope = topology(lgeos.GEOSEnvelope) +geos_intersection = topology(lgeos.GEOSIntersection, GEOM_PTR) +geos_pointonsurface = topology(lgeos.GEOSPointOnSurface) +geos_preservesimplify = topology(lgeos.GEOSTopologyPreserveSimplify, c_double) +geos_simplify = topology(lgeos.GEOSSimplify, c_double) +geos_symdifference = topology(lgeos.GEOSSymDifference, GEOM_PTR) +geos_union = topology(lgeos.GEOSUnion, GEOM_PTR) + +# GEOSRelate returns a string, not a geometry. +geos_relate = lgeos.GEOSRelate +geos_relate.argtypes = [GEOM_PTR, GEOM_PTR] +geos_relate.errcheck = check_string diff --git a/django/contrib/gis/management/__init__.py b/django/contrib/gis/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/gis/management/base.py b/django/contrib/gis/management/base.py new file mode 100644 index 0000000000..c998063af8 --- /dev/null +++ b/django/contrib/gis/management/base.py @@ -0,0 +1,15 @@ +from django.core.management.base import BaseCommand, CommandError + +class ArgsCommand(BaseCommand): + """ + Command class for commands that take multiple arguments. + """ + args = '' + + def handle(self, *args, **options): + if not args: + raise CommandError('Must provide the following arguments: %s' % self.args) + return self.handle_args(*args, **options) + + def handle_args(self, *args, **options): + raise NotImplementedError() diff --git a/django/contrib/gis/management/commands/__init__.py b/django/contrib/gis/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/gis/management/commands/inspectdb.py b/django/contrib/gis/management/commands/inspectdb.py new file mode 100644 index 0000000000..05e205353c --- /dev/null +++ b/django/contrib/gis/management/commands/inspectdb.py @@ -0,0 +1,190 @@ +""" + This overrides the traditional `inspectdb` command so that geographic databases + may be introspected. +""" + +from django.core.management.commands.inspectdb import Command as InspectCommand +from django.contrib.gis.db.backend import SpatialBackend + +class Command(InspectCommand): + + # Mapping from lower-case OGC type to the corresponding GeoDjango field. + geofield_mapping = {'point' : 'PointField', + 'linestring' : 'LineStringField', + 'polygon' : 'PolygonField', + 'multipoint' : 'MultiPointField', + 'multilinestring' : 'MultiLineStringField', + 'multipolygon' : 'MultiPolygonField', + 'geometrycollection' : 'GeometryCollectionField', + 'geometry' : 'GeometryField', + } + + def geometry_columns(self): + """ + Returns a datastructure of metadata information associated with the + `geometry_columns` (or equivalent) table. + """ + # The `geo_cols` is a dictionary data structure that holds information + # about any geographic columns in the database. + geo_cols = {} + def add_col(table, column, coldata): + if table in geo_cols: + # If table already has a geometry column. + geo_cols[table][column] = coldata + else: + # Otherwise, create a dictionary indexed by column. + geo_cols[table] = { column : coldata } + + if SpatialBackend.name == 'postgis': + # PostGIS holds all geographic column information in the `geometry_columns` table. + from django.contrib.gis.models import GeometryColumns + for geo_col in GeometryColumns.objects.all(): + table = geo_col.f_table_name + column = geo_col.f_geometry_column + coldata = {'type' : geo_col.type, 'srid' : geo_col.srid, 'dim' : geo_col.coord_dimension} + add_col(table, column, coldata) + return geo_cols + elif SpatialBackend.name == 'mysql': + # On MySQL have to get all table metadata before hand; this means walking through + # each table and seeing if any column types are spatial. Can't detect this with + # `cursor.description` (what the introspection module does) because all spatial types + # have the same integer type (255 for GEOMETRY). + from django.db import connection + cursor = connection.cursor() + cursor.execute('SHOW TABLES') + tables = cursor.fetchall(); + for table_tup in tables: + table = table_tup[0] + table_desc = cursor.execute('DESCRIBE `%s`' % table) + col_info = cursor.fetchall() + for column, typ, null, key, default, extra in col_info: + if typ in self.geofield_mapping: add_col(table, column, {'type' : typ}) + return geo_cols + else: + # TODO: Oracle (has incomplete `geometry_columns` -- have to parse + # SDO SQL to get specific type, SRID, and other information). + raise NotImplementedError('Geographic database inspection not available.') + + def handle_inspection(self): + "Overloaded from Django's version to handle geographic database tables." + from django.db import connection, get_introspection_module + import keyword + + introspection_module = get_introspection_module() + + geo_cols = self.geometry_columns() + + table2model = lambda table_name: table_name.title().replace('_', '') + + cursor = connection.cursor() + yield "# This is an auto-generated Django model module." + yield "# You'll have to do the following manually to clean this up:" + yield "# * Rearrange models' order" + yield "# * Make sure each model has one field with primary_key=True" + yield "# Feel free to rename the models, but don't rename db_table values or field names." + yield "#" + yield "# Also note: You'll have to insert the output of 'django-admin.py sqlcustom [appname]'" + yield "# into your database." + yield '' + yield 'from django.contrib.gis.db import models' + yield '' + for table_name in introspection_module.get_table_list(cursor): + # Getting the geographic table dictionary. + geo_table = geo_cols.get(table_name, {}) + + yield 'class %s(models.Model):' % table2model(table_name) + try: + relations = introspection_module.get_relations(cursor, table_name) + except NotImplementedError: + relations = {} + try: + indexes = introspection_module.get_indexes(cursor, table_name) + except NotImplementedError: + indexes = {} + for i, row in enumerate(introspection_module.get_table_description(cursor, table_name)): + att_name, iatt_name = row[0].lower(), row[0] + comment_notes = [] # Holds Field notes, to be displayed in a Python comment. + extra_params = {} # Holds Field parameters such as 'db_column'. + + if ' ' in att_name: + extra_params['db_column'] = att_name + att_name = att_name.replace(' ', '') + comment_notes.append('Field renamed to remove spaces.') + if keyword.iskeyword(att_name): + extra_params['db_column'] = att_name + att_name += '_field' + comment_notes.append('Field renamed because it was a Python reserved word.') + + if i in relations: + rel_to = relations[i][1] == table_name and "'self'" or table2model(relations[i][1]) + field_type = 'ForeignKey(%s' % rel_to + if att_name.endswith('_id'): + att_name = att_name[:-3] + else: + extra_params['db_column'] = att_name + else: + if iatt_name in geo_table: + ## Customization for Geographic Columns ## + geo_col = geo_table[iatt_name] + field_type = self.geofield_mapping[geo_col['type'].lower()] + # Adding extra keyword arguments for the SRID and dimension (if not defaults). + dim, srid = geo_col.get('dim', 2), geo_col.get('srid', 4326) + if dim != 2: extra_params['dim'] = dim + if srid != 4326: extra_params['srid'] = srid + else: + try: + field_type = introspection_module.DATA_TYPES_REVERSE[row[1]] + except KeyError: + field_type = 'TextField' + comment_notes.append('This field type is a guess.') + + # This is a hook for DATA_TYPES_REVERSE to return a tuple of + # (field_type, extra_params_dict). + if type(field_type) is tuple: + field_type, new_params = field_type + extra_params.update(new_params) + + # Add max_length for all CharFields. + if field_type == 'CharField' and row[3]: + extra_params['max_length'] = row[3] + + if field_type == 'DecimalField': + extra_params['max_digits'] = row[4] + extra_params['decimal_places'] = row[5] + + # Add primary_key and unique, if necessary. + column_name = extra_params.get('db_column', att_name) + if column_name in indexes: + if indexes[column_name]['primary_key']: + extra_params['primary_key'] = True + elif indexes[column_name]['unique']: + extra_params['unique'] = True + + field_type += '(' + + # Don't output 'id = meta.AutoField(primary_key=True)', because + # that's assumed if it doesn't exist. + if att_name == 'id' and field_type == 'AutoField(' and extra_params == {'primary_key': True}: + continue + + # Add 'null' and 'blank', if the 'null_ok' flag was present in the + # table description. + if row[6]: # If it's NULL... + extra_params['blank'] = True + if not field_type in ('TextField(', 'CharField('): + extra_params['null'] = True + + field_desc = '%s = models.%s' % (att_name, field_type) + if extra_params: + if not field_desc.endswith('('): + field_desc += ', ' + field_desc += ', '.join(['%s=%r' % (k, v) for k, v in extra_params.items()]) + field_desc += ')' + if comment_notes: + field_desc += ' # ' + ' '.join(comment_notes) + yield ' %s' % field_desc + if table_name in geo_cols: + yield ' objects = models.GeoManager()' + yield ' class Meta:' + yield ' db_table = %r' % table_name + yield '' diff --git a/django/contrib/gis/management/commands/ogrinspect.py b/django/contrib/gis/management/commands/ogrinspect.py new file mode 100644 index 0000000000..aa4f41831e --- /dev/null +++ b/django/contrib/gis/management/commands/ogrinspect.py @@ -0,0 +1,119 @@ +import os, sys +from optparse import make_option +from django.contrib.gis import gdal +from django.contrib.gis.management.base import ArgsCommand, CommandError + +def layer_option(option, opt, value, parser): + """ + Callback for `make_option` for the `ogrinspect` `layer_key` + keyword option which may be an integer or a string. + """ + try: + dest = int(value) + except ValueError: + dest = value + setattr(parser.values, option.dest, dest) + +def list_option(option, opt, value, parser): + """ + Callback for `make_option` for `ogrinspect` keywords that require + a string list. If the string is 'True'/'true' then the option + value will be a boolean instead. + """ + if value.lower() == 'true': + dest = True + else: + dest = [s for s in value.split(',')] + setattr(parser.values, option.dest, dest) + +class Command(ArgsCommand): + help = ('Inspects the given OGR-compatible data source (e.g., a shapefile) and outputs\n' + 'a GeoDjango model with the given model name. For example:\n' + ' ./manage.py ogrinspect zipcode.shp Zipcode') + args = '[data_source] [model_name]' + + option_list = ArgsCommand.option_list + ( + make_option('--blank', dest='blank', type='string', action='callback', + callback=list_option, default=False, + help='Use a comma separated list of OGR field names to add ' + 'the `blank=True` option to the field definition. Set with' + '`true` to apply to all applicable fields.'), + make_option('--decimal', dest='decimal', type='string', action='callback', + callback=list_option, default=False, + help='Use a comma separated list of OGR float fields to ' + 'generate `DecimalField` instead of the default ' + '`FloatField`. Set to `true` to apply to all OGR float fields.'), + make_option('--geom-name', dest='geom_name', type='string', default='geom', + help='Specifies the model name for the Geometry Field ' + '(defaults to `geom`)'), + make_option('--layer', dest='layer_key', type='string', action='callback', + callback=layer_option, default=0, + help='The key for specifying which layer in the OGR data ' + 'source to use. Defaults to 0 (the first layer). May be ' + 'an integer or a string identifier for the layer.'), + make_option('--multi-geom', action='store_true', dest='multi_geom', default=False, + help='Treat the geometry in the data source as a geometry collection.'), + make_option('--name-field', dest='name_field', + help='Specifies a field name to return for the `__unicode__` function.'), + make_option('--no-imports', action='store_false', dest='imports', default=True, + help='Do not include `from django.contrib.gis.db import models` ' + 'statement.'), + make_option('--null', dest='null', type='string', action='callback', + callback=list_option, default=False, + help='Use a comma separated list of OGR field names to add ' + 'the `null=True` option to the field definition. Set with' + '`true` to apply to all applicable fields.'), + make_option('--srid', dest='srid', + help='The SRID to use for the Geometry Field. If it can be ' + 'determined, the SRID of the data source is used.'), + make_option('--mapping', action='store_true', dest='mapping', + help='Generate mapping dictionary for use with `LayerMapping`.') + ) + + requires_model_validation = False + + def handle_args(self, *args, **options): + try: + data_source, model_name = args + except ValueError: + raise CommandError('Invalid arguments, must provide: %s' % self.args) + + if not gdal.HAS_GDAL: + raise CommandError('GDAL is required to inspect geospatial data sources.') + + # TODO: Support non file-based OGR datasources. + if not os.path.isfile(data_source): + raise CommandError('The given data source cannot be found: "%s"' % data_source) + + # Removing options with `None` values. + options = dict([(k, v) for k, v in options.items() if not v is None]) + + # Getting the OGR DataSource from the string parameter. + try: + ds = gdal.DataSource(data_source) + except gdal.OGRException, msg: + raise CommandError(msg) + + # Whether the user wants to generate the LayerMapping dictionary as well. + show_mapping = options.pop('mapping', False) + + # Returning the output of ogrinspect with the given arguments + # and options. + from django.contrib.gis.utils.ogrinspect import _ogrinspect, mapping + output = [s for s in _ogrinspect(ds, model_name, **options)] + if show_mapping: + # Constructing the keyword arguments for `mapping`, and + # calling it on the data source. + kwargs = {'geom_name' : options['geom_name'], + 'layer_key' : options['layer_key'], + 'multi_geom' : options['multi_geom'], + } + mapping_dict = mapping(ds, **kwargs) + # This extra legwork is so that the dictionary definition comes + # out in the same order as the fields in the model definition. + rev_mapping = dict([(v, k) for k, v in mapping_dict.items()]) + output.extend(['', '# Auto-generated `LayerMapping` dictionary for %s model' % model_name, + '%s_mapping = {' % model_name.lower()]) + output.extend([" '%s' : '%s'," % (rev_mapping[ogr_fld], ogr_fld) for ogr_fld in ds[options['layer_key']].fields]) + output.extend([" '%s' : '%s'," % (options['geom_name'], mapping_dict[options['geom_name']]), '}']) + return '\n'.join(output) diff --git a/django/contrib/gis/maps/__init__.py b/django/contrib/gis/maps/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/gis/maps/google/__init__.py b/django/contrib/gis/maps/google/__init__.py new file mode 100644 index 0000000000..dfbbedc3d6 --- /dev/null +++ b/django/contrib/gis/maps/google/__init__.py @@ -0,0 +1,61 @@ +""" + This module houses the GoogleMap object, used for generating + the needed javascript to embed Google Maps in a webpage. + + Google(R) is a registered trademark of Google, Inc. of Mountain View, California. + + Example: + + * In the view: + return render_to_response('template.html', {'google' : GoogleMap(key="abcdefg")}) + + * In the template: + + + {{ google.xhtml }} + + Google Maps via GeoDjango + {{ google.style }} + {{ google.scripts }} + + {{ google.body }} +
+ + + + Note: If you want to be more explicit in your templates, the following are + equivalent: + {{ google.body }} => "" + {{ google.xhtml }} => "" + {{ google.style }} => "" + + Explanation: + - The `xhtml` property provides the correct XML namespace needed for + Google Maps to operate in IE using XHTML. Google Maps on IE uses + VML to draw polylines. Returns, by default: + + + - The `style` property provides the correct style tag for the CSS + properties required by Google Maps on IE: + + + - The `scripts` property provides the necessary ' % (self.api_url, self.key)) + + @property + def scripts(self): + "Returns all tags required for Google Maps JavaScript." + return mark_safe('%s\n ' % (self.api_script, self.js)) + + @property + def style(self): + "Returns additional CSS styling needed for Google Maps on IE." + return mark_safe('' % self.vml_css) + + @property + def xhtml(self): + "Returns XHTML information needed for IE VML overlays." + return mark_safe('' % self.xmlns) diff --git a/django/contrib/gis/maps/google/overlays.py b/django/contrib/gis/maps/google/overlays.py new file mode 100644 index 0000000000..0efedf3632 --- /dev/null +++ b/django/contrib/gis/maps/google/overlays.py @@ -0,0 +1,220 @@ +from django.utils.safestring import mark_safe +from django.contrib.gis.geos import fromstr, Point, LineString, LinearRing, Polygon + +class GEvent(object): + """ + A Python wrapper for the Google GEvent object. + + Events can be attached to any object derived from GOverlayBase with the + add_event() call. + + For more information please see the Google Maps API Reference: + http://code.google.com/apis/maps/documentation/reference.html#GEvent + + Example: + + from django.shortcuts import render_to_response + from django.contrib.gis.maps.google import GoogleMap, GEvent, GPolyline + + def sample_request(request): + polyline = GPolyline('LINESTRING(101 26, 112 26, 102 31)') + event = GEvent('click', + 'function() { location.href = "http://www.google.com"}') + polyline.add_event(event) + return render_to_response('mytemplate.html', + {'google' : GoogleMap(polylines=[polyline])}) + """ + + def __init__(self, event, action): + """ + Initializes a GEvent object. + + Parameters: + + event: + string for the event, such as 'click'. The event must be a valid + event for the object in the Google Maps API. + There is no validation of the event type within Django. + + action: + string containing a Javascript function, such as + 'function() { location.href = "newurl";}' + The string must be a valid Javascript function. Again there is no + validation fo the function within Django. + """ + self.event = event + self.action = action + + def __unicode__(self): + "Returns the parameter part of a GEvent." + return mark_safe('"%s", %s' %(self.event, self.action)) + +class GOverlayBase(object): + def __init__(self): + self.events = [] + + def latlng_from_coords(self, coords): + "Generates a JavaScript array of GLatLng objects for the given coordinates." + return '[%s]' % ','.join(['new GLatLng(%s,%s)' % (y, x) for x, y in coords]) + + def add_event(self, event): + "Attaches a GEvent to the overlay object." + self.events.append(event) + + def __unicode__(self): + "The string representation is the JavaScript API call." + return mark_safe('%s(%s)' % (self.__class__.__name__, self.js_params)) + +class GPolygon(GOverlayBase): + """ + A Python wrapper for the Google GPolygon object. For more information + please see the Google Maps API Reference: + http://code.google.com/apis/maps/documentation/reference.html#GPolygon + """ + def __init__(self, poly, + stroke_color='#0000ff', stroke_weight=2, stroke_opacity=1, + fill_color='#0000ff', fill_opacity=0.4): + """ + The GPolygon object initializes on a GEOS Polygon or a parameter that + may be instantiated into GEOS Polygon. Please note that this will not + depict a Polygon's internal rings. + + Keyword Options: + + stroke_color: + The color of the polygon outline. Defaults to '#0000ff' (blue). + + stroke_weight: + The width of the polygon outline, in pixels. Defaults to 2. + + stroke_opacity: + The opacity of the polygon outline, between 0 and 1. Defaults to 1. + + fill_color: + The color of the polygon fill. Defaults to '#0000ff' (blue). + + fill_opacity: + The opacity of the polygon fill. Defaults to 0.4. + """ + if isinstance(poly, basestring): poly = fromstr(poly) + if isinstance(poly, (tuple, list)): poly = Polygon(poly) + if not isinstance(poly, Polygon): + raise TypeError('GPolygon may only initialize on GEOS Polygons.') + + # Getting the envelope of the input polygon (used for automatically + # determining the zoom level). + self.envelope = poly.envelope + + # Translating the coordinates into a JavaScript array of + # Google `GLatLng` objects. + self.points = self.latlng_from_coords(poly.shell.coords) + + # Stroke settings. + self.stroke_color, self.stroke_opacity, self.stroke_weight = stroke_color, stroke_opacity, stroke_weight + + # Fill settings. + self.fill_color, self.fill_opacity = fill_color, fill_opacity + + super(GPolygon, self).__init__() + + @property + def js_params(self): + return '%s, "%s", %s, %s, "%s", %s' % (self.points, self.stroke_color, self.stroke_weight, self.stroke_opacity, + self.fill_color, self.fill_opacity) + +class GPolyline(GOverlayBase): + """ + A Python wrapper for the Google GPolyline object. For more information + please see the Google Maps API Reference: + http://code.google.com/apis/maps/documentation/reference.html#GPolyline + """ + def __init__(self, geom, color='#0000ff', weight=2, opacity=1): + """ + The GPolyline object may be initialized on GEOS LineStirng, LinearRing, + and Polygon objects (internal rings not supported) or a parameter that + may instantiated into one of the above geometries. + + Keyword Options: + + color: + The color to use for the polyline. Defaults to '#0000ff' (blue). + + weight: + The width of the polyline, in pixels. Defaults to 2. + + opacity: + The opacity of the polyline, between 0 and 1. Defaults to 1. + """ + # If a GEOS geometry isn't passed in, try to contsruct one. + if isinstance(geom, basestring): geom = fromstr(geom) + if isinstance(geom, (tuple, list)): geom = Polygon(geom) + # Generating the lat/lng coordinate pairs. + if isinstance(geom, (LineString, LinearRing)): + self.latlngs = self.latlng_from_coords(geom.coords) + elif isinstance(geom, Polygon): + self.latlngs = self.latlng_from_coords(geom.shell.coords) + else: + raise TypeError('GPolyline may only initialize on GEOS LineString, LinearRing, and/or Polygon geometries.') + + # Getting the envelope for automatic zoom determination. + self.envelope = geom.envelope + self.color, self.weight, self.opacity = color, weight, opacity + super(GPolyline, self).__init__() + + @property + def js_params(self): + return '%s, "%s", %s, %s' % (self.latlngs, self.color, self.weight, self.opacity) + +class GMarker(GOverlayBase): + """ + A Python wrapper for the Google GMarker object. For more information + please see the Google Maps API Reference: + http://code.google.com/apis/maps/documentation/reference.html#GMarker + + Example: + + from django.shortcuts import render_to_response + from django.contrib.gis.maps.google.overlays import GMarker, GEvent + + def sample_request(request): + marker = GMarker('POINT(101 26)') + event = GEvent('click', + 'function() { location.href = "http://www.google.com"}') + marker.add_event(event) + return render_to_response('mytemplate.html', + {'google' : GoogleMap(markers=[marker])}) + """ + def __init__(self, geom, title=None): + """ + The GMarker object may initialize on GEOS Points or a parameter + that may be instantiated into a GEOS point. Keyword options map to + GMarkerOptions -- so far only the title option is supported. + + Keyword Options: + title: + Title option for GMarker, will be displayed as a tooltip. + """ + # If a GEOS geometry isn't passed in, try to construct one. + if isinstance(geom, basestring): geom = fromstr(geom) + if isinstance(geom, (tuple, list)): geom = Point(geom) + if isinstance(geom, Point): + self.latlng = self.latlng_from_coords(geom.coords) + else: + raise TypeError('GMarker may only initialize on GEOS Point geometry.') + # Getting the envelope for automatic zoom determination. + self.envelope = geom.envelope + # TODO: Add support for more GMarkerOptions + self.title = title + super(GMarker, self).__init__() + + def latlng_from_coords(self, coords): + return 'new GLatLng(%s,%s)' %(coords[1], coords[0]) + + def options(self): + result = [] + if self.title: result.append('title: "%s"' % self.title) + return '{%s}' % ','.join(result) + + @property + def js_params(self): + return '%s, %s' % (self.latlng, self.options()) diff --git a/django/contrib/gis/maps/google/zoom.py b/django/contrib/gis/maps/google/zoom.py new file mode 100644 index 0000000000..b94fa31157 --- /dev/null +++ b/django/contrib/gis/maps/google/zoom.py @@ -0,0 +1,164 @@ +from django.contrib.gis.geos import GEOSGeometry, LinearRing, Polygon, Point +from django.contrib.gis.maps.google.gmap import GoogleMapException +from math import pi, sin, cos, log, exp, atan + +# Constants used for degree to radian conversion, and vice-versa. +DTOR = pi / 180. +RTOD = 180. / pi + +def get_width_height(envelope): + # Getting the lower-left, upper-left, and upper-right + # coordinates of the envelope. + ll = Point(envelope[0][0]) + ul = Point(envelope[0][1]) + ur = Point(envelope[0][2]) + + height = ll.distance(ul) + width = ul.distance(ur) + return width, height + +class GoogleZoom(object): + """ + GoogleZoom is a utility for performing operations related to the zoom + levels on Google Maps. + + This class is inspired by the OpenStreetMap Mapnik tile generation routine + `generate_tiles.py`, and the article "How Big Is the World" (Hack #16) in + "Google Maps Hacks" by Rich Gibson and Schuyler Erle. + + `generate_tiles.py` may be found at: + http://trac.openstreetmap.org/browser/applications/rendering/mapnik/generate_tiles.py + + "Google Maps Hacks" may be found at http://safari.oreilly.com/0596101619 + """ + + def __init__(self, num_zoom=19, tilesize=256): + "Initializes the Google Zoom object." + + # Google's tilesize is 256x256, square tiles are assumed. + self._tilesize = tilesize + + # The number of zoom levels + self._nzoom = num_zoom + + # Initializing arrays to hold the parameters for each + # one of the zoom levels. + self._degpp = [] # Degrees per pixel + self._radpp = [] # Radians per pixel + self._npix = [] # 1/2 the number of pixels for a tile at the given zoom level + + # Incrementing through the zoom levels and populating the + # parameter arrays. + z = tilesize # The number of pixels per zoom level. + for i in xrange(num_zoom): + # Getting the degrees and radians per pixel, and the 1/2 the number of + # for every zoom level. + self._degpp.append(z / 360.) # degrees per pixel + self._radpp.append(z / (2 * pi)) # radians per pixl + self._npix.append(z / 2) # number of pixels to center of tile + + # Multiplying `z` by 2 for the next iteration. + z *= 2 + + def __len__(self): + "Returns the number of zoom levels." + return self._nzoom + + def get_lon_lat(self, lonlat): + "Unpacks longitude, latitude from GEOS Points and 2-tuples." + if isinstance(lonlat, Point): + lon, lat = lonlat.coords + else: + lon, lat = lonlat + return lon, lat + + def lonlat_to_pixel(self, lonlat, zoom): + "Converts a longitude, latitude coordinate pair for the given zoom level." + # Setting up, unpacking the longitude, latitude values and getting the + # number of pixels for the given zoom level. + lon, lat = self.get_lon_lat(lonlat) + npix = self._npix[zoom] + + # Calculating the pixel x coordinate by multiplying the longitude + # value with with the number of degrees/pixel at the given + # zoom level. + px_x = round(npix + (lon * self._degpp[zoom])) + + # Creating the factor, and ensuring that 1 or -1 is not passed in as the + # base to the logarithm. Here's why: + # if fac = -1, we'll get log(0) which is undefined; + # if fac = 1, our logarithm base will be divided by 0, also undefined. + fac = min(max(sin(DTOR * lat), -0.9999), 0.9999) + + # Calculating the pixel y coordinate. + px_y = round(npix + (0.5 * log((1 + fac)/(1 - fac)) * (-1.0 * self._radpp[zoom]))) + + # Returning the pixel x, y to the caller of the function. + return (px_x, px_y) + + def pixel_to_lonlat(self, px, zoom): + "Converts a pixel to a longitude, latitude pair at the given zoom level." + if len(px) != 2: + raise TypeError('Pixel should be a sequence of two elements.') + + # Getting the number of pixels for the given zoom level. + npix = self._npix[zoom] + + # Calculating the longitude value, using the degrees per pixel. + lon = (px[0] - npix) / self._degpp[zoom] + + # Calculating the latitude value. + lat = RTOD * ( 2 * atan(exp((px[1] - npix)/ (-1.0 * self._radpp[zoom]))) - 0.5 * pi) + + # Returning the longitude, latitude coordinate pair. + return (lon, lat) + + def tile(self, lonlat, zoom): + """ + Returns a Polygon corresponding to the region represented by a fictional + Google Tile for the given longitude/latitude pair and zoom level. This + tile is used to determine the size of a tile at the given point. + """ + # The given lonlat is the center of the tile. + delta = self._tilesize / 2 + + # Getting the pixel coordinates corresponding to the + # the longitude/latitude. + px = self.lonlat_to_pixel(lonlat, zoom) + + # Getting the lower-left and upper-right lat/lon coordinates + # for the bounding box of the tile. + ll = self.pixel_to_lonlat((px[0]-delta, px[1]-delta), zoom) + ur = self.pixel_to_lonlat((px[0]+delta, px[1]+delta), zoom) + + # Constructing the Polygon, representing the tile and returning. + return Polygon(LinearRing(ll, (ll[0], ur[1]), ur, (ur[0], ll[1]), ll), srid=4326) + + def get_zoom(self, geom): + "Returns the optimal Zoom level for the given geometry." + + # Checking the input type. + if not isinstance(geom, GEOSGeometry) or geom.srid != 4326: + raise TypeError('get_zoom() expects a GEOS Geometry with an SRID of 4326.') + + # Getting the envelope for the geometry, and its associated width, height + # and centroid. + env = geom.envelope + env_w, env_h = get_width_height(env) + center = env.centroid + + for z in xrange(self._nzoom): + # Getting the tile at the zoom level. + tile = self.tile(center, z) + tile_w, tile_h = get_width_height(tile) + + # When we span more than one tile, this is an approximately good + # zoom level. + if (env_w > tile_w) or (env_h > tile_h): + if z == 0: + raise GoogleMapException('Geometry width and height should not exceed that of the Earth.') + return z-1 + + # Otherwise, we've zoomed in to the max. + return self._nzoom-1 + diff --git a/django/contrib/gis/maps/openlayers/__init__.py b/django/contrib/gis/maps/openlayers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/gis/measure.py b/django/contrib/gis/measure.py new file mode 100644 index 0000000000..42c49ce154 --- /dev/null +++ b/django/contrib/gis/measure.py @@ -0,0 +1,329 @@ +# Copyright (c) 2007, Robert Coup +# 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 Distance 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. +# +""" +Distance and Area objects to allow for sensible and convienient calculation +and conversions. + +Authors: Robert Coup, Justin Bronn + +Inspired by GeoPy (http://exogen.case.edu/projects/geopy/) +and Geoff Biggs' PhD work on dimensioned units for robotics. +""" +__all__ = ['A', 'Area', 'D', 'Distance'] +from decimal import Decimal + +class MeasureBase(object): + def default_units(self, kwargs): + """ + Return the unit value and the the default units specified + from the given keyword arguments dictionary. + """ + val = 0.0 + for unit, value in kwargs.iteritems(): + if unit in self.UNITS: + val += self.UNITS[unit] * value + default_unit = unit + elif unit in self.ALIAS: + u = self.ALIAS[unit] + val += self.UNITS[u] * value + default_unit = u + else: + lower = unit.lower() + if lower in self.UNITS: + val += self.UNITS[lower] * value + default_unit = lower + elif lower in self.LALIAS: + u = self.LALIAS[lower] + val += self.UNITS[u] * value + default_unit = u + else: + raise AttributeError('Unknown unit type: %s' % unit) + return val, default_unit + + @classmethod + def unit_attname(cls, unit_str): + """ + Retrieves the unit attribute name for the given unit string. + For example, if the given unit string is 'metre', 'm' would be returned. + An exception is raised if an attribute cannot be found. + """ + lower = unit_str.lower() + if unit_str in cls.UNITS: + return unit_str + elif lower in cls.UNITS: + return lower + elif lower in cls.LALIAS: + return cls.LALIAS[lower] + else: + raise Exception('Could not find a unit keyword associated with "%s"' % unit_str) + +class Distance(MeasureBase): + UNITS = { + 'chain' : 20.1168, + 'chain_benoit' : 20.116782, + 'chain_sears' : 20.1167645, + 'british_chain_benoit' : 20.1167824944, + 'british_chain_sears' : 20.1167651216, + 'british_chain_sears_truncated' : 20.116756, + 'cm' : 0.01, + 'british_ft' : 0.304799471539, + 'british_yd' : 0.914398414616, + 'clarke_ft' : 0.3047972654, + 'clarke_link' : 0.201166195164, + 'fathom' : 1.8288, + 'ft': 0.3048, + 'german_m' : 1.0000135965, + 'gold_coast_ft' : 0.304799710181508, + 'indian_yd' : 0.914398530744, + 'inch' : 0.0254, + 'km': 1000.0, + 'link' : 0.201168, + 'link_benoit' : 0.20116782, + 'link_sears' : 0.20116765, + 'm': 1.0, + 'mi': 1609.344, + 'mm' : 0.001, + 'nm': 1852.0, + 'nm_uk' : 1853.184, + 'rod' : 5.0292, + 'sears_yd' : 0.91439841, + 'survey_ft' : 0.304800609601, + 'um' : 0.000001, + 'yd': 0.9144, + } + + # Unit aliases for `UNIT` terms encountered in Spatial Reference WKT. + ALIAS = { + 'centimeter' : 'cm', + 'foot' : 'ft', + 'inches' : 'inch', + 'kilometer' : 'km', + 'kilometre' : 'km', + 'meter' : 'm', + 'metre' : 'm', + 'micrometer' : 'um', + 'micrometre' : 'um', + 'millimeter' : 'mm', + 'millimetre' : 'mm', + 'mile' : 'mi', + 'yard' : 'yd', + 'British chain (Benoit 1895 B)' : 'british_chain_benoit', + 'British chain (Sears 1922)' : 'british_chain_sears', + 'British chain (Sears 1922 truncated)' : 'british_chain_sears_truncated', + 'British foot (Sears 1922)' : 'british_ft', + 'British foot' : 'british_ft', + 'British yard (Sears 1922)' : 'british_yd', + 'British yard' : 'british_yd', + "Clarke's Foot" : 'clarke_ft', + "Clarke's link" : 'clarke_link', + 'Chain (Benoit)' : 'chain_benoit', + 'Chain (Sears)' : 'chain_sears', + 'Foot (International)' : 'ft', + 'German legal metre' : 'german_m', + 'Gold Coast foot' : 'gold_coast_ft', + 'Indian yard' : 'indian_yd', + 'Link (Benoit)': 'link_benoit', + 'Link (Sears)': 'link_sears', + 'Nautical Mile' : 'nm', + 'Nautical Mile (UK)' : 'nm_uk', + 'US survey foot' : 'survey_ft', + 'U.S. Foot' : 'survey_ft', + 'Yard (Indian)' : 'indian_yd', + 'Yard (Sears)' : 'sears_yd' + } + LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()]) + + def __init__(self, default_unit=None, **kwargs): + # The base unit is in meters. + self.m, self._default_unit = self.default_units(kwargs) + if default_unit and isinstance(default_unit, str): + self._default_unit = default_unit + + def __getattr__(self, name): + if name in self.UNITS: + return self.m / self.UNITS[name] + else: + raise AttributeError('Unknown unit type: %s' % name) + + def __repr__(self): + return 'Distance(%s=%s)' % (self._default_unit, getattr(self, self._default_unit)) + + def __str__(self): + return '%s %s' % (getattr(self, self._default_unit), self._default_unit) + + def __cmp__(self, other): + if isinstance(other, Distance): + return cmp(self.m, other.m) + else: + return NotImplemented + + def __add__(self, other): + if isinstance(other, Distance): + return Distance(default_unit=self._default_unit, m=(self.m + other.m)) + else: + raise TypeError('Distance must be added with Distance') + + def __iadd__(self, other): + if isinstance(other, Distance): + self.m += other.m + return self + else: + raise TypeError('Distance must be added with Distance') + + def __sub__(self, other): + if isinstance(other, Distance): + return Distance(default_unit=self._default_unit, m=(self.m - other.m)) + else: + raise TypeError('Distance must be subtracted from Distance') + + def __isub__(self, other): + if isinstance(other, Distance): + self.m -= other.m + return self + else: + raise TypeError('Distance must be subtracted from Distance') + + def __mul__(self, other): + if isinstance(other, (int, float, long, Decimal)): + return Distance(default_unit=self._default_unit, m=(self.m * float(other))) + elif isinstance(other, Distance): + return Area(default_unit='sq_' + self._default_unit, sq_m=(self.m * other.m)) + else: + raise TypeError('Distance must be multiplied with number or Distance') + + def __imul__(self, other): + if isinstance(other, (int, float, long, Decimal)): + self.m *= float(other) + return self + else: + raise TypeError('Distance must be multiplied with number') + + def __div__(self, other): + if isinstance(other, (int, float, long, Decimal)): + return Distance(default_unit=self._default_unit, m=(self.m / float(other))) + else: + raise TypeError('Distance must be divided with number') + + def __idiv__(self, other): + if isinstance(other, (int, float, long, Decimal)): + self.m /= float(other) + return self + else: + raise TypeError('Distance must be divided with number') + + def __nonzero__(self): + return bool(self.m) + +class Area(MeasureBase): + # Getting the square units values and the alias dictionary. + UNITS = dict([('sq_%s' % k, v ** 2) for k, v in Distance.UNITS.items()]) + ALIAS = dict([(k, 'sq_%s' % v) for k, v in Distance.ALIAS.items()]) + LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()]) + + def __init__(self, default_unit=None, **kwargs): + self.sq_m, self._default_unit = self.default_units(kwargs) + if default_unit and isinstance(default_unit, str): + self._default_unit = default_unit + + def __getattr__(self, name): + if name in self.UNITS: + return self.sq_m / self.UNITS[name] + else: + raise AttributeError('Unknown unit type: ' + name) + + def __repr__(self): + return 'Area(%s=%s)' % (self._default_unit, getattr(self, self._default_unit)) + + def __str__(self): + return '%s %s' % (getattr(self, self._default_unit), self._default_unit) + + def __cmp__(self, other): + if isinstance(other, Area): + return cmp(self.sq_m, other.sq_m) + else: + return NotImplemented + + def __add__(self, other): + if isinstance(other, Area): + return Area(default_unit=self._default_unit, sq_m=(self.sq_m + other.sq_m)) + else: + raise TypeError('Area must be added with Area') + + def __iadd__(self, other): + if isinstance(other, Area): + self.sq_m += other.sq_m + return self + else: + raise TypeError('Area must be added with Area') + + def __sub__(self, other): + if isinstance(other, Area): + return Area(default_unit=self._default_unit, sq_m=(self.sq_m - other.sq_m)) + else: + raise TypeError('Area must be subtracted from Area') + + def __isub__(self, other): + if isinstance(other, Area): + self.sq_m -= other.sq_m + return self + else: + raise TypeError('Area must be subtracted from Area') + + def __mul__(self, other): + if isinstance(other, (int, float, long, Decimal)): + return Area(default_unit=self._default_unit, sq_m=(self.sq_m * float(other))) + else: + raise TypeError('Area must be multiplied with number') + + def __imul__(self, other): + if isinstance(other, (int, float, long, Decimal)): + self.sq_m *= float(other) + return self + else: + raise TypeError('Area must be multiplied with number') + + def __div__(self, other): + if isinstance(other, (int, float, long, Decimal)): + return Area(default_unit=self._default_unit, sq_m=(self.sq_m / float(other))) + else: + raise TypeError('Area must be divided with number') + + def __idiv__(self, other): + if isinstance(other, (int, float, long, Decimal)): + self.sq_m /= float(other) + return self + else: + raise TypeError('Area must be divided with number') + + def __nonzero__(self): + return bool(self.sq_m) + +# Shortcuts +D = Distance +A = Area diff --git a/django/contrib/gis/models.py b/django/contrib/gis/models.py new file mode 100644 index 0000000000..d3e8dbdb83 --- /dev/null +++ b/django/contrib/gis/models.py @@ -0,0 +1,284 @@ +""" + Imports the SpatialRefSys and GeometryColumns models dependent on the + spatial database backend. +""" +import re +from django.conf import settings + +# Checking for the presence of GDAL (needed for the SpatialReference object) +from django.contrib.gis.gdal import HAS_GDAL +if HAS_GDAL: + from django.contrib.gis.gdal import SpatialReference + +class SpatialRefSysMixin(object): + """ + The SpatialRefSysMixin is a class used by the database-dependent + SpatialRefSys objects to reduce redundnant code. + """ + + # For pulling out the spheroid from the spatial reference string. This + # regular expression is used only if the user does not have GDAL installed. + # TODO: Flattening not used in all ellipsoids, could also be a minor axis, or 'b' + # parameter. + spheroid_regex = re.compile(r'.+SPHEROID\[\"(?P.+)\",(?P\d+(\.\d+)?),(?P\d{3}\.\d+),') + + # For pulling out the units on platforms w/o GDAL installed. + # TODO: Figure out how to pull out angular units of projected coordinate system and + # fix for LOCAL_CS types. GDAL should be highly recommended for performing + # distance queries. + units_regex = re.compile(r'.+UNIT ?\["(?P[\w \'\(\)]+)", ?(?P[\d\.]+)(,AUTHORITY\["(?P[\w \'\(\)]+)","(?P\d+)"\])?\]([\w ]+)?(,AUTHORITY\["(?P[\w \'\(\)]+)","(?P\d+)"\])?\]$') + + @property + def srs(self): + """ + Returns a GDAL SpatialReference object, if GDAL is installed. + """ + if HAS_GDAL: + 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 = SpatialReference(self.wkt) + return self.srs + except Exception, msg: + pass + + raise Exception('Could not get OSR SpatialReference from WKT: %s\nError:\n%s' % (self.wkt, msg)) + else: + raise Exception('GDAL is not installed.') + + @property + def ellipsoid(self): + """ + Returns a tuple of the ellipsoid parameters: + (semimajor axis, semiminor axis, and inverse flattening). + """ + if HAS_GDAL: + return self.srs.ellipsoid + else: + m = self.spheroid_regex.match(self.wkt) + if m: return (float(m.group('major')), float(m.group('flattening'))) + else: return None + + @property + def name(self): + "Returns the projection name." + return self.srs.name + + @property + def spheroid(self): + "Returns the spheroid name for this spatial reference." + return self.srs['spheroid'] + + @property + def datum(self): + "Returns the datum for this spatial reference." + return self.srs['datum'] + + @property + def projected(self): + "Is this Spatial Reference projected?" + if HAS_GDAL: + return self.srs.projected + else: + return self.wkt.startswith('PROJCS') + + @property + def local(self): + "Is this Spatial Reference local?" + if HAS_GDAL: + return self.srs.local + else: + return self.wkt.startswith('LOCAL_CS') + + @property + def geographic(self): + "Is this Spatial Reference geographic?" + if HAS_GDAL: + return self.srs.geographic + else: + return self.wkt.startswith('GEOGCS') + + @property + def linear_name(self): + "Returns the linear units name." + if HAS_GDAL: + return self.srs.linear_name + elif self.geographic: + return None + else: + m = self.units_regex.match(self.wkt) + return m.group('unit_name') + + @property + def linear_units(self): + "Returns the linear units." + if HAS_GDAL: + return self.srs.linear_units + elif self.geographic: + return None + else: + m = self.units_regex.match(self.wkt) + return m.group('unit') + + @property + def angular_name(self): + "Returns the name of the angular units." + if HAS_GDAL: + return self.srs.angular_name + elif self.projected: + return None + else: + m = self.units_regex.match(self.wkt) + return m.group('unit_name') + + @property + def angular_units(self): + "Returns the angular units." + if HAS_GDAL: + return self.srs.angular_units + elif self.projected: + return None + else: + m = self.units_regex.match(self.wkt) + return m.group('unit') + + @property + def units(self): + "Returns 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): + """ + Class method used by GeometryField on initialization to + retrive the units on the given WKT, without having to use + any of the database fields. + """ + if HAS_GDAL: + return SpatialReference(wkt).units + else: + m = cls.units_regex.match(wkt) + return m.group('unit'), m.group('unit_name') + + @classmethod + def get_spheroid(cls, wkt, string=True): + """ + Class method used by GeometryField on initialization to + retrieve the `SPHEROID[..]` parameters from the given WKT. + """ + if HAS_GDAL: + srs = SpatialReference(wkt) + sphere_params = srs.ellipsoid + sphere_name = srs['spheroid'] + else: + m = cls.spheroid_regex.match(wkt) + if m: + sphere_params = (float(m.group('major')), float(m.group('flattening'))) + sphere_name = m.group('name') + else: + return None + + if not string: + 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 __unicode__(self): + """ + Returns the string representation. If GDAL is installed, + it will be 'pretty' OGC WKT. + """ + try: + return unicode(self.srs) + except: + return unicode(self.wkt) + +# The SpatialRefSys and GeometryColumns models +_srid_info = True +if settings.DATABASE_ENGINE == 'postgresql_psycopg2': + # Because the PostGIS version is checked when initializing the spatial + # backend a `ProgrammingError` will be raised if the PostGIS tables + # and functions are not installed. We catch here so it won't be raised when + # running the Django test suite. + from psycopg2 import ProgrammingError + try: + from django.contrib.gis.db.backend.postgis.models import GeometryColumns, SpatialRefSys + except ProgrammingError: + _srid_info = False +elif settings.DATABASE_ENGINE == 'oracle': + # Same thing as above, except the GEOS library is attempted to be loaded for + # `BaseSpatialBackend`, and an exception will be raised during the + # Django test suite if it doesn't exist. + try: + from django.contrib.gis.db.backend.oracle.models import GeometryColumns, SpatialRefSys + except: + _srid_info = False +else: + _srid_info = False + +if _srid_info: + def get_srid_info(srid): + """ + Returns the units, unit name, and spheroid WKT associated with the + given SRID from the `spatial_ref_sys` (or equivalent) spatial database + table. We use a database cursor to execute the query because this + function is used when it is not possible to use the ORM (for example, + during field initialization). + """ + # SRID=-1 is a common convention for indicating the geometry has no + # spatial reference information associated with it. Thus, we will + # return all None values without raising an exception. + if srid == -1: return None, None, None + + # Getting the spatial reference WKT associated with the SRID from the + # `spatial_ref_sys` (or equivalent) spatial database table. This query + # cannot be executed using the ORM because this information is needed + # when the ORM cannot be used (e.g., during the initialization of + # `GeometryField`). + from django.db import connection + cur = connection.cursor() + qn = connection.ops.quote_name + stmt = 'SELECT %(table)s.%(wkt_col)s FROM %(table)s WHERE (%(table)s.%(srid_col)s = %(srid)s)' + stmt = stmt % {'table' : qn(SpatialRefSys._meta.db_table), + 'wkt_col' : qn(SpatialRefSys.wkt_col()), + 'srid_col' : qn('srid'), + 'srid' : srid, + } + cur.execute(stmt) + + # Fetching the WKT from the cursor; if the query failed raise an Exception. + fetched = cur.fetchone() + if not fetched: + raise ValueError('Failed to find spatial reference entry in "%s" corresponding to SRID=%s.' % + (SpatialRefSys._meta.db_table, srid)) + srs_wkt = fetched[0] + + # Getting metadata associated with the spatial reference system identifier. + # Specifically, getting the unit information and spheroid information + # (both required for distance queries). + unit, unit_name = SpatialRefSys.get_units(srs_wkt) + spheroid = SpatialRefSys.get_spheroid(srs_wkt) + return unit, unit_name, spheroid +else: + def get_srid_info(srid): + """ + Dummy routine for the backends that do not have the OGC required + spatial metadata tables (like MySQL). + """ + return None, None, None + diff --git a/django/contrib/gis/oldforms/__init__.py b/django/contrib/gis/oldforms/__init__.py new file mode 100644 index 0000000000..94ef9acc1f --- /dev/null +++ b/django/contrib/gis/oldforms/__init__.py @@ -0,0 +1,29 @@ +from django.core.validators import ValidationError +from django.oldforms import LargeTextField +from django.contrib.gis.geos import GEOSException, GEOSGeometry + +class WKTField(LargeTextField): + "An oldforms LargeTextField for editing WKT text in the admin." + def __init__(self, *args, **kwargs): + super(WKTField, self).__init__(*args, **kwargs) + # Overridding the validator list. + self.validator_list = [self.isValidGeom] + + def render(self, data): + # Returns the WKT value for the geometry field. When no such data + # is present, return None to LargeTextField's render. + if isinstance(data, GEOSGeometry): + return super(WKTField, self).render(data.wkt) + elif isinstance(data, basestring): + return super(WKTField, self).render(data) + else: + return super(WKTField, self).render(None) + + def isValidGeom(self, field_data, all_data): + try: + g = GEOSGeometry(field_data) + except GEOSException: + raise ValidationError('Valid WKT or HEXEWKB is required for Geometry Fields.') + + + diff --git a/django/contrib/gis/shortcuts.py b/django/contrib/gis/shortcuts.py new file mode 100644 index 0000000000..8eeaed1aaa --- /dev/null +++ b/django/contrib/gis/shortcuts.py @@ -0,0 +1,12 @@ +from django.http import HttpResponse +from django.template import loader + +def render_to_kml(*args, **kwargs): + "Renders the response using the MIME type for KML." + return HttpResponse(loader.render_to_string(*args, **kwargs), + mimetype='application/vnd.google-earth.kml+xml kml') + +def render_to_text(*args, **kwargs): + "Renders the response using the MIME type for plain text." + return HttpResponse(loader.render_to_string(*args, **kwargs), + mimetype='text/plain') diff --git a/django/contrib/gis/sitemaps.py b/django/contrib/gis/sitemaps.py new file mode 100644 index 0000000000..d5d11b9c0f --- /dev/null +++ b/django/contrib/gis/sitemaps.py @@ -0,0 +1,55 @@ +from django.core import urlresolvers +from django.contrib.sitemaps import Sitemap +from django.contrib.gis.db.models.fields import GeometryField +from django.contrib.gis.shortcuts import render_to_kml +from django.db.models import get_model, get_models +from django.http import HttpResponse + +class KMLSitemap(Sitemap): + """ + A minimal hook to produce KML sitemaps. + """ + def __init__(self, locations=None): + if locations is None: + self.locations = _build_kml_sources() + else: + self.locations = locations + + def items(self): + return self.locations + + def location(self, obj): + return urlresolvers.reverse('django.contrib.gis.sitemaps.kml', + kwargs={'label':obj[0], + 'field_name':obj[1]}) + +def _build_kml_sources(): + "Make a mapping of all available KML sources." + ret = [] + for klass in get_models(): + for field in klass._meta.fields: + if isinstance(field, GeometryField): + label = "%s.%s" % (klass._meta.app_label, + klass._meta.module_name) + + ret.append((label, field.name)) + return ret + + +class KMLNotFound(Exception): + pass + +def kml(request, label, field_name): + placemarks = [] + klass = get_model(*label.split('.')) + if not klass: + raise KMLNotFound("You must supply a valid app.model label. Got %s" % label) + + #FIXME: GMaps apparently has a limit on size of displayed kml files + # check if paginating w/ external refs (i.e. linked list) helps. + placemarks.extend(list(klass._default_manager.kml(field_name)[:100])) + + #FIXME: other KML features? + return render_to_kml('gis/kml/placemarks.kml', {'places' : placemarks}) + + diff --git a/django/contrib/gis/templates/gis/admin/openlayers.html b/django/contrib/gis/templates/gis/admin/openlayers.html new file mode 100644 index 0000000000..acf82b284e --- /dev/null +++ b/django/contrib/gis/templates/gis/admin/openlayers.html @@ -0,0 +1,37 @@ +{% block extrastyle %} + + +{% endblock %} + + +
+Delete all Features +{% if display_wkt %}

WKT debugging window:

{% endif %} + + +
diff --git a/django/contrib/gis/templates/gis/admin/openlayers.js b/django/contrib/gis/templates/gis/admin/openlayers.js new file mode 100644 index 0000000000..719426127c --- /dev/null +++ b/django/contrib/gis/templates/gis/admin/openlayers.js @@ -0,0 +1,157 @@ +{# Author: Justin Bronn, Travis Pinney & Dane Springmeyer #} +{% block vars %}var {{ module }} = {}; +{{ module }}.map = null; {{ module }}.controls = null; {{ module }}.panel = null; {{ module }}.re = new RegExp("^SRID=\d+;(.+)", "i"); {{ module }}.layers = {}; +{{ module }}.wkt_f = new OpenLayers.Format.WKT(); +{{ module }}.is_collection = {% if is_collection %}true{% else %}false{% endif %}; +{{ module }}.collection_type = '{{ collection_type }}'; +{{ module }}.is_linestring = {% if is_linestring %}true{% else %}false{% endif %}; +{{ module }}.is_polygon = {% if is_polygon %}true{% else %}false{% endif %}; +{{ module }}.is_point = {% if is_point %}true{% else %}false{% endif %}; +{% endblock %} +{{ module }}.get_ewkt = function(feat){return 'SRID={{ srid }};' + {{ module }}.wkt_f.write(feat);} +{{ module }}.read_wkt = function(wkt){ + // OpenLayers cannot handle EWKT -- we make sure to strip it out. + // EWKT is only exposed to OL if there's a validation error in the admin. + var match = {{ module }}.re.exec(wkt); + if (match){wkt = match[1];} + return {{ module }}.wkt_f.read(wkt); +} +{{ module }}.write_wkt = function(feat){ + if ({{ module }}.is_collection){ {{ module }}.num_geom = feat.geometry.components.length;} + else { {{ module }}.num_geom = 1;} + document.getElementById('{{ id }}').value = {{ module }}.get_ewkt(feat); +} +{{ module }}.add_wkt = function(event){ + // This function will sync the contents of the `vector` layer with the + // WKT in the text field. + if ({{ module }}.is_collection){ + var feat = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.{{ geom_type }}()); + for (var i = 0; i < {{ module }}.layers.vector.features.length; i++){ + feat.geometry.addComponents([{{ module }}.layers.vector.features[i].geometry]); + } + {{ module }}.write_wkt(feat); + } else { + // Make sure to remove any previously added features. + if ({{ module }}.layers.vector.features.length > 1){ + old_feats = [{{ module }}.layers.vector.features[0]]; + {{ module }}.layers.vector.removeFeatures(old_feats); + {{ module }}.layers.vector.destroyFeatures(old_feats); + } + {{ module }}.write_wkt(event.feature); + } +} +{{ module }}.modify_wkt = function(event){ + if ({{ module }}.is_collection){ + if ({{ module }}.is_point){ + {{ module }}.add_wkt(event); + return; + } else { + // When modifying the selected components are added to the + // vector layer so we only increment to the `num_geom` value. + var feat = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.{{ geom_type }}()); + for (var i = 0; i < {{ module }}.num_geom; i++){ + feat.geometry.addComponents([{{ module }}.layers.vector.features[i].geometry]); + } + {{ module }}.write_wkt(feat); + } + } else { + {{ module }}.write_wkt(event.feature); + } +} +// Function to clear vector features and purge wkt from div +{{ module }}.deleteFeatures = function(){ + {{ module }}.layers.vector.removeFeatures({{ module }}.layers.vector.features); + {{ module }}.layers.vector.destroyFeatures(); +} +{{ module }}.clearFeatures = function (){ + {{ module }}.deleteFeatures(); + document.getElementById('{{ id }}').value = ''; + {{ module }}.map.setCenter(new OpenLayers.LonLat({{ default_lon }}, {{ default_lat }}), {{ default_zoom }}); +} +// Add Select control +{{ module }}.addSelectControl = function(){ + var select = new OpenLayers.Control.SelectFeature({{ module }}.layers.vector, {'toggle' : true, 'clickout' : true}); + {{ module }}.map.addControl(select); + select.activate(); +} +{{ module }}.enableDrawing = function(){ {{ module }}.map.getControlsByClass('OpenLayers.Control.DrawFeature')[0].activate();} +{{ module }}.enableEditing = function(){ {{ module }}.map.getControlsByClass('OpenLayers.Control.ModifyFeature')[0].activate();} +// Create an array of controls based on geometry type +{{ module }}.getControls = function(lyr){ + {{ module }}.panel = new OpenLayers.Control.Panel({'displayClass': 'olControlEditingToolbar'}); + var nav = new OpenLayers.Control.Navigation(); + var draw_ctl; + if ({{ module }}.is_linestring){ + draw_ctl = new OpenLayers.Control.DrawFeature(lyr, OpenLayers.Handler.Path, {'displayClass': 'olControlDrawFeaturePath'}); + } else if ({{ module }}.is_polygon){ + draw_ctl = new OpenLayers.Control.DrawFeature(lyr, OpenLayers.Handler.Polygon, {'displayClass': 'olControlDrawFeaturePolygon'}); + } else if ({{ module }}.is_point){ + draw_ctl = new OpenLayers.Control.DrawFeature(lyr, OpenLayers.Handler.Point, {'displayClass': 'olControlDrawFeaturePoint'}); + } + {% if modifiable %} + var mod = new OpenLayers.Control.ModifyFeature(lyr, {'displayClass': 'olControlModifyFeature'}); + {{ module }}.controls = [nav, draw_ctl, mod]; + {% else %} + {{ module }}.controls = [nav, darw_ctl]; + {% endif %} +} +{{ module }}.init = function(){ + {% block map_options %}// The options hash, w/ zoom, resolution, and projection settings. + var options = { +{% autoescape off %}{% for item in map_options.items %} '{{ item.0 }}' : {{ item.1 }}{% if not forloop.last %},{% endif %} +{% endfor %}{% endautoescape %} };{% endblock %} + // The admin map for this geometry field. + {{ module }}.map = new OpenLayers.Map('{{ id }}_map', options); + // Base Layer + {{ module }}.layers.base = {% block base_layer %}new OpenLayers.Layer.WMS( "{{ wms_name }}", "{{ wms_url }}", {layers: '{{ wms_layer }}'} );{% endblock %} + {{ module }}.map.addLayer({{ module }}.layers.base); + {% block extra_layers %}{% endblock %} + {% if is_linestring %}OpenLayers.Feature.Vector.style["default"]["strokeWidth"] = 3; // Default too thin for linestrings. {% endif %} + {{ module }}.layers.vector = new OpenLayers.Layer.Vector(" {{ field_name }}"); + {{ module }}.map.addLayer({{ module }}.layers.vector); + // Read WKT from the text field. + var wkt = document.getElementById('{{ id }}').value; + if (wkt){ + // After reading into geometry, immediately write back to + // WKT