diff options
Diffstat (limited to 'parts/django/django/contrib/gis')
205 files changed, 19908 insertions, 0 deletions
diff --git a/parts/django/django/contrib/gis/__init__.py b/parts/django/django/contrib/gis/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/django/contrib/gis/__init__.py diff --git a/parts/django/django/contrib/gis/admin/__init__.py b/parts/django/django/contrib/gis/admin/__init__.py new file mode 100644 index 0000000..2b56276 --- /dev/null +++ b/parts/django/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, AdminSite, ModelAdmin, 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/parts/django/django/contrib/gis/admin/options.py b/parts/django/django/contrib/gis/admin/options.py new file mode 100644 index 0000000..1814933 --- /dev/null +++ b/parts/django/django/contrib/gis/admin/options.py @@ -0,0 +1,124 @@ +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 + map_width = 600 + map_height = 400 + map_srid = 4326 + map_template = 'gis/admin/openlayers.html' + openlayers_url = 'http://openlayers.org/api/2.8/OpenLayers.js' + point_zoom = num_zoom - 6 + 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): + request = kwargs.pop('request', None) + # 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_type in ('MULTIPOINT', 'MULTILINESTRING', 'MULTIPOLYGON', 'GEOMETRYCOLLECTION') + if is_collection: + if db_field.geom_type == 'GEOMETRYCOLLECTION': collection_type = 'Any' + else: collection_type = OGRGeomType(db_field.geom_type.replace('MULTI', '')) + else: + collection_type = 'None' + + class OLMap(self.widget): + template = self.map_template + geom_type = db_field.geom_type + params = {'default_lon' : self.default_lon, + 'default_lat' : self.default_lat, + 'default_zoom' : self.default_zoom, + 'display_wkt' : self.debug or self.display_wkt, + 'geom_type' : OGRGeomType(db_field.geom_type), + 'field_name' : db_field.name, + 'is_collection' : is_collection, + 'scrollable' : self.scrollable, + 'layerswitcher' : self.layerswitcher, + 'collection_type' : collection_type, + 'is_linestring' : db_field.geom_type in ('LINESTRING', 'MULTILINESTRING'), + 'is_polygon' : db_field.geom_type in ('POLYGON', 'MULTIPOLYGON'), + 'is_point' : db_field.geom_type in ('POINT', 'MULTIPOINT'), + 'num_zoom' : self.num_zoom, + 'max_zoom' : self.max_zoom, + 'min_zoom' : self.min_zoom, + 'units' : self.units, #likely 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, + 'point_zoom' : self.point_zoom, + 'srid' : self.map_srid, + 'display_srid' : self.display_srid, + 'wms_url' : self.wms_url, + 'wms_layer' : self.wms_layer, + 'wms_name' : self.wms_name, + 'debug' : self.debug, + } + return OLMap + +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' + point_zoom = num_zoom - 6 + units = 'm' diff --git a/parts/django/django/contrib/gis/admin/widgets.py b/parts/django/django/contrib/gis/admin/widgets.py new file mode 100644 index 0000000..be26261 --- /dev/null +++ b/parts/django/django/contrib/gis/admin/widgets.py @@ -0,0 +1,107 @@ +from django.conf import settings +from django.contrib.gis.gdal import OGRException +from django.contrib.gis.geos import GEOSGeometry, GEOSException +from django.forms.widgets import Textarea +from django.template import loader, Context +from django.utils import translation + +# Creating a template context that contains Django settings +# values needed by admin map templates. +geo_context = Context({'ADMIN_MEDIA_PREFIX' : settings.ADMIN_MEDIA_PREFIX, + 'LANGUAGE_BIDI' : translation.get_language_bidi(), + }) + +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 + # interface will be constructed. + self.params['wkt'] = '' + + # If a string reaches here (via a validation error on another + # field) then just reconstruct the Geometry. + if 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 name of + # the GeometryField (passed in via the `attrs` keyword). + # Use the 'name' attr for the field name (rather than 'field') + self.params['name'] = name + # note: we must switch out dashes for underscores since js + # functions are created using the module variable + js_safe_name = self.params['name'].replace('-','_') + self.params['module'] = 'geodjango_%s' % js_safe_name + + if value: + # Transforming the geometry to the projection used on the + # OpenLayers map. + srid = self.params['srid'] + if value.srid != srid: + try: + ogr = value.ogr + ogr.transform(srid) + wkt = ogr.wkt + except OGRException: + wkt = '' + else: + wkt = value.wkt + + # Setting the parameter WKT with that of the transformed + # geometry. + self.params['wkt'] = wkt + + return loader.render_to_string(self.template, self.params, + context_instance=geo_context) + + 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/parts/django/django/contrib/gis/db/__init__.py b/parts/django/django/contrib/gis/db/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/django/contrib/gis/db/__init__.py diff --git a/parts/django/django/contrib/gis/db/backend/__init__.py b/parts/django/django/contrib/gis/db/backend/__init__.py new file mode 100644 index 0000000..72ebdfe --- /dev/null +++ b/parts/django/django/contrib/gis/db/backend/__init__.py @@ -0,0 +1,11 @@ +from django.db import connection + +if hasattr(connection.ops, 'spatial_version'): + from warnings import warn + warn('The `django.contrib.gis.db.backend` module was refactored and ' + 'renamed to `django.contrib.gis.db.backends` in 1.2. ' + 'All functionality of `SpatialBackend` ' + 'has been moved to the `ops` attribute of the spatial database ' + 'backend. A `SpatialBackend` alias is provided here for ' + 'backwards-compatibility, but will be removed in 1.3.') + SpatialBackend = connection.ops diff --git a/parts/django/django/contrib/gis/db/backends/__init__.py b/parts/django/django/contrib/gis/db/backends/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/__init__.py diff --git a/parts/django/django/contrib/gis/db/backends/adapter.py b/parts/django/django/contrib/gis/db/backends/adapter.py new file mode 100644 index 0000000..9766ef0 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/adapter.py @@ -0,0 +1,17 @@ +class WKTAdapter(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 + + def prepare_database_save(self, unused): + return self diff --git a/parts/django/django/contrib/gis/db/backends/base.py b/parts/django/django/contrib/gis/db/backends/base.py new file mode 100644 index 0000000..0eaacae --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/base.py @@ -0,0 +1,336 @@ +""" +Base/mixin classes for the spatial backend database operations and the +`SpatialRefSys` model the backend. +""" +import re +from django.conf import settings +from django.contrib.gis import gdal + +class BaseSpatialOperations(object): + """ + This module holds the base `BaseSpatialBackend` object, which is + instantiated by each spatial database backend with the features + it has. + """ + distance_functions = {} + geometry_functions = {} + geometry_operators = {} + geography_operators = {} + geography_functions = {} + gis_terms = {} + truncate_params = {} + + # Quick booleans for the type of this spatial backend, and + # an attribute for the spatial database version tuple (if applicable) + postgis = False + spatialite = False + mysql = False + oracle = False + spatial_version = None + + # How the geometry column should be selected. + select = None + + # Does the spatial database have a geography type? + geography = False + + area = False + centroid = False + difference = False + distance = False + distance_sphere = False + distance_spheroid = False + envelope = False + force_rhr = False + mem_size = False + bounding_circle = False + num_geom = False + num_points = False + perimeter = False + perimeter3d = False + point_on_surface = False + polygonize = False + reverse = False + scale = False + snap_to_grid = False + sym_difference = False + transform = False + translate = False + union = False + + # Aggregates + collect = False + extent = False + extent3d = False + make_line = False + unionagg = False + + # Serialization + geohash = False + geojson = False + gml = False + kml = False + svg = False + + # Constructors + from_text = False + from_wkb = False + + # Default conversion functions for aggregates; will be overridden if implemented + # for the spatial backend. + def convert_extent(self, box): + raise NotImplementedError('Aggregate extent not implemented for this spatial backend.') + + def convert_extent3d(self, box): + raise NotImplementedError('Aggregate 3D extent not implemented for this spatial backend.') + + def convert_geom(self, geom_val, geom_field): + raise NotImplementedError('Aggregate method not implemented for this spatial backend.') + + # For quoting column values, rather than columns. + def geo_quote_name(self, name): + if isinstance(name, unicode): + name = name.encode('ascii') + return "'%s'" % name + + # GeometryField operations + def geo_db_type(self, f): + """ + Returns the database column type for the geometry field on + the spatial backend. + """ + raise NotImplementedError + + def get_distance(self, f, value, lookup_type): + """ + Returns the distance parameters for the given geometry field, + lookup value, and lookup type. + """ + raise NotImplementedError('Distance operations not available on this spatial backend.') + + def get_geom_placeholder(self, f, value): + """ + Returns the placeholder for the given geometry field with the given + value. Depending on the spatial backend, the placeholder may contain a + stored procedure call to the transformation function of the spatial + backend. + """ + raise NotImplementedError + + # Spatial SQL Construction + def spatial_aggregate_sql(self, agg): + raise NotImplementedError('Aggregate support not implemented for this spatial backend.') + + def spatial_lookup_sql(self, lvalue, lookup_type, value, field): + raise NotImplmentedError + + # Routines for getting the OGC-compliant models. + def geometry_columns(self): + raise NotImplementedError + + def spatial_ref_sys(self): + raise NotImplementedError + +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<name>.+)\",(?P<major>\d+(\.\d+)?),(?P<flattening>\d{3}\.\d+),') + + # For pulling out the units on platforms w/o GDAL installed. + # TODO: Figure out how to pull out angular units of projected coordinate system and + # fix for LOCAL_CS types. GDAL should be highly recommended for performing + # distance queries. + units_regex = re.compile(r'.+UNIT ?\["(?P<unit_name>[\w \'\(\)]+)", ?(?P<unit>[\d\.]+)(,AUTHORITY\["(?P<unit_auth_name>[\w \'\(\)]+)","(?P<unit_auth_val>\d+)"\])?\]([\w ]+)?(,AUTHORITY\["(?P<auth_name>[\w \'\(\)]+)","(?P<auth_val>\d+)"\])?\]$') + + @property + def srs(self): + """ + Returns a GDAL SpatialReference object, if GDAL is installed. + """ + if gdal.HAS_GDAL: + # TODO: Is caching really necessary here? Is complexity worth it? + if hasattr(self, '_srs'): + # Returning a clone of the cached SpatialReference object. + return self._srs.clone() + else: + # Attempting to cache a SpatialReference object. + + # Trying to get from WKT first. + try: + self._srs = gdal.SpatialReference(self.wkt) + return self.srs + except Exception, msg: + pass + + try: + self._srs = gdal.SpatialReference(self.proj4text) + 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 gdal.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 gdal.HAS_GDAL: + return self.srs.projected + else: + return self.wkt.startswith('PROJCS') + + @property + def local(self): + "Is this Spatial Reference local?" + if gdal.HAS_GDAL: + return self.srs.local + else: + return self.wkt.startswith('LOCAL_CS') + + @property + def geographic(self): + "Is this Spatial Reference geographic?" + if gdal.HAS_GDAL: + return self.srs.geographic + else: + return self.wkt.startswith('GEOGCS') + + @property + def linear_name(self): + "Returns the linear units name." + if gdal.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 gdal.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 gdal.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 gdal.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 gdal.HAS_GDAL: + return gdal.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 gdal.HAS_GDAL: + srs = gdal.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) diff --git a/parts/django/django/contrib/gis/db/backends/mysql/__init__.py b/parts/django/django/contrib/gis/db/backends/mysql/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/mysql/__init__.py diff --git a/parts/django/django/contrib/gis/db/backends/mysql/base.py b/parts/django/django/contrib/gis/db/backends/mysql/base.py new file mode 100644 index 0000000..7f0fc48 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/mysql/base.py @@ -0,0 +1,13 @@ +from django.db.backends.mysql.base import * +from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper +from django.contrib.gis.db.backends.mysql.creation import MySQLCreation +from django.contrib.gis.db.backends.mysql.introspection import MySQLIntrospection +from django.contrib.gis.db.backends.mysql.operations import MySQLOperations + +class DatabaseWrapper(MySQLDatabaseWrapper): + + def __init__(self, *args, **kwargs): + super(DatabaseWrapper, self).__init__(*args, **kwargs) + self.creation = MySQLCreation(self) + self.ops = MySQLOperations() + self.introspection = MySQLIntrospection(self) diff --git a/parts/django/django/contrib/gis/db/backends/mysql/creation.py b/parts/django/django/contrib/gis/db/backends/mysql/creation.py new file mode 100644 index 0000000..dda77ea --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/mysql/creation.py @@ -0,0 +1,18 @@ +from django.db.backends.mysql.creation import DatabaseCreation + +class MySQLCreation(DatabaseCreation): + + def sql_indexes_for_field(self, model, f, style): + from django.contrib.gis.db.models.fields import GeometryField + output = super(MySQLCreation, self).sql_indexes_for_field(model, f, style) + + if isinstance(f, GeometryField) and f.spatial_index: + qn = self.connection.ops.quote_name + db_table = model._meta.db_table + idx_name = '%s_%s_id' % (db_table, f.column) + output.append(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(f.column)) + ');') + return output diff --git a/parts/django/django/contrib/gis/db/backends/mysql/introspection.py b/parts/django/django/contrib/gis/db/backends/mysql/introspection.py new file mode 100644 index 0000000..59d0f62 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/mysql/introspection.py @@ -0,0 +1,32 @@ +from MySQLdb.constants import FIELD_TYPE + +from django.contrib.gis.gdal import OGRGeomType +from django.db.backends.mysql.introspection import DatabaseIntrospection + +class MySQLIntrospection(DatabaseIntrospection): + # Updating the data_types_reverse dictionary with the appropriate + # type for Geometry fields. + data_types_reverse = DatabaseIntrospection.data_types_reverse.copy() + data_types_reverse[FIELD_TYPE.GEOMETRY] = 'GeometryField' + + def get_geometry_type(self, table_name, geo_col): + cursor = self.connection.cursor() + try: + # In order to get the specific geometry type of the field, + # we introspect on the table definition using `DESCRIBE`. + cursor.execute('DESCRIBE %s' % + self.connection.ops.quote_name(table_name)) + # Increment over description info until we get to the geometry + # column. + for column, typ, null, key, default, extra in cursor.fetchall(): + if column == geo_col: + # Using OGRGeomType to convert from OGC name to Django field. + # MySQL does not support 3D or SRIDs, so the field params + # are empty. + field_type = OGRGeomType(typ).django + field_params = {} + break + finally: + cursor.close() + + return field_type, field_params diff --git a/parts/django/django/contrib/gis/db/backends/mysql/operations.py b/parts/django/django/contrib/gis/db/backends/mysql/operations.py new file mode 100644 index 0000000..1653636 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/mysql/operations.py @@ -0,0 +1,65 @@ +from django.db.backends.mysql.base import DatabaseOperations + +from django.contrib.gis.db.backends.adapter import WKTAdapter +from django.contrib.gis.db.backends.base import BaseSpatialOperations + +class MySQLOperations(DatabaseOperations, BaseSpatialOperations): + + compiler_module = 'django.contrib.gis.db.models.sql.compiler' + mysql = True + name = 'mysql' + select = 'AsText(%s)' + from_wkb = 'GeomFromWKB' + from_text = 'GeomFromText' + + Adapter = WKTAdapter + Adaptor = Adapter # Backwards-compatibility alias. + + geometry_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', + } + + gis_terms = dict([(term, None) for term in geometry_functions.keys() + ['isnull']]) + + def geo_db_type(self, f): + return f.geom_type + + def get_geom_placeholder(self, value, srid): + """ + 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. + """ + if hasattr(value, 'expression'): + placeholder = '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression])) + else: + placeholder = '%s(%%s)' % self.from_text + return placeholder + + def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn): + alias, col, db_type = lvalue + + geo_col = '%s.%s' % (qn(alias), qn(col)) + + lookup_info = self.geometry_functions.get(lookup_type, False) + if lookup_info: + return "%s(%s, %s)" % (lookup_info, geo_col, + self.get_geom_placeholder(value, field.srid)) + + # TODO: Is this really necessary? MySQL can't handle NULL geometries + # in its spatial indexes anyways. + if lookup_type == 'isnull': + return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or '')) + + raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) diff --git a/parts/django/django/contrib/gis/db/backends/oracle/__init__.py b/parts/django/django/contrib/gis/db/backends/oracle/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/oracle/__init__.py diff --git a/parts/django/django/contrib/gis/db/backends/oracle/adapter.py b/parts/django/django/contrib/gis/db/backends/oracle/adapter.py new file mode 100644 index 0000000..ea340d9 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/oracle/adapter.py @@ -0,0 +1,5 @@ +from cx_Oracle import CLOB +from django.contrib.gis.db.backends.adapter import WKTAdapter + +class OracleSpatialAdapter(WKTAdapter): + input_size = CLOB diff --git a/parts/django/django/contrib/gis/db/backends/oracle/base.py b/parts/django/django/contrib/gis/db/backends/oracle/base.py new file mode 100644 index 0000000..398b3d3 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/oracle/base.py @@ -0,0 +1,12 @@ +from django.db.backends.oracle.base import * +from django.db.backends.oracle.base import DatabaseWrapper as OracleDatabaseWrapper +from django.contrib.gis.db.backends.oracle.creation import OracleCreation +from django.contrib.gis.db.backends.oracle.introspection import OracleIntrospection +from django.contrib.gis.db.backends.oracle.operations import OracleOperations + +class DatabaseWrapper(OracleDatabaseWrapper): + def __init__(self, *args, **kwargs): + super(DatabaseWrapper, self).__init__(*args, **kwargs) + self.ops = OracleOperations(self) + self.creation = OracleCreation(self) + self.introspection = OracleIntrospection(self) diff --git a/parts/django/django/contrib/gis/db/backends/oracle/compiler.py b/parts/django/django/contrib/gis/db/backends/oracle/compiler.py new file mode 100644 index 0000000..f0eb5ca --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/oracle/compiler.py @@ -0,0 +1,44 @@ +from django.contrib.gis.db.models.sql.compiler import GeoSQLCompiler as BaseGeoSQLCompiler +from django.db.backends.oracle import compiler + +SQLCompiler = compiler.SQLCompiler + +class GeoSQLCompiler(BaseGeoSQLCompiler, SQLCompiler): + pass + +class SQLInsertCompiler(compiler.SQLInsertCompiler, GeoSQLCompiler): + def placeholder(self, field, val): + if field is None: + # A field value of None means the value is raw. + return val + elif hasattr(field, 'get_placeholder'): + # Some fields (e.g. geo fields) need special munging before + # they can be inserted. + ph = field.get_placeholder(val, self.connection) + if ph == 'NULL': + # If the placeholder returned is 'NULL', then we need to + # to remove None from the Query parameters. Specifically, + # cx_Oracle will assume a CHAR type when a placeholder ('%s') + # is used for columns of MDSYS.SDO_GEOMETRY. Thus, we use + # 'NULL' for the value, and remove None from the query params. + # See also #10888. + param_idx = self.query.columns.index(field.column) + params = list(self.query.params) + params.pop(param_idx) + self.query.params = tuple(params) + return ph + else: + # Return the common case for the placeholder + return '%s' + +class SQLDeleteCompiler(compiler.SQLDeleteCompiler, GeoSQLCompiler): + pass + +class SQLUpdateCompiler(compiler.SQLUpdateCompiler, GeoSQLCompiler): + pass + +class SQLAggregateCompiler(compiler.SQLAggregateCompiler, GeoSQLCompiler): + pass + +class SQLDateCompiler(compiler.SQLDateCompiler, GeoSQLCompiler): + pass diff --git a/parts/django/django/contrib/gis/db/backends/oracle/creation.py b/parts/django/django/contrib/gis/db/backends/oracle/creation.py new file mode 100644 index 0000000..043da91 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/oracle/creation.py @@ -0,0 +1,42 @@ +from django.db.backends.oracle.creation import DatabaseCreation +from django.db.backends.util import truncate_name + +class OracleCreation(DatabaseCreation): + + def sql_indexes_for_field(self, model, f, style): + "Return any spatial index creation SQL for the field." + from django.contrib.gis.db.models.fields import GeometryField + + output = super(OracleCreation, self).sql_indexes_for_field(model, f, style) + + if isinstance(f, GeometryField): + gqn = self.connection.ops.geo_quote_name + qn = self.connection.ops.quote_name + db_table = model._meta.db_table + + output.append(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(f.column)) + ',\n ' + + style.SQL_KEYWORD("MDSYS.SDO_DIM_ARRAY") + '(\n ' + + style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") + + ("('LONG', %s, %s, %s),\n " % (f._extent[0], f._extent[2], f._tolerance)) + + style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") + + ("('LAT', %s, %s, %s)\n ),\n" % (f._extent[1], f._extent[3], f._tolerance)) + + ' %s\n );' % f.srid) + + if f.spatial_index: + # Getting the index name, Oracle doesn't allow object + # names > 30 characters. + idx_name = truncate_name('%s_%s_id' % (db_table, f.column), 30) + + output.append(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(f.column)) + ') ' + + style.SQL_KEYWORD('INDEXTYPE IS ') + + style.SQL_TABLE('MDSYS.SPATIAL_INDEX') + ';') + return output diff --git a/parts/django/django/contrib/gis/db/backends/oracle/introspection.py b/parts/django/django/contrib/gis/db/backends/oracle/introspection.py new file mode 100644 index 0000000..58dd3f3 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/oracle/introspection.py @@ -0,0 +1,39 @@ +import cx_Oracle +from django.db.backends.oracle.introspection import DatabaseIntrospection + +class OracleIntrospection(DatabaseIntrospection): + # Associating any OBJECTVAR instances with GeometryField. Of course, + # this won't work right on Oracle objects that aren't MDSYS.SDO_GEOMETRY, + # but it is the only object type supported within Django anyways. + data_types_reverse = DatabaseIntrospection.data_types_reverse.copy() + data_types_reverse[cx_Oracle.OBJECT] = 'GeometryField' + + def get_geometry_type(self, table_name, geo_col): + cursor = self.connection.cursor() + try: + # Querying USER_SDO_GEOM_METADATA to get the SRID and dimension information. + try: + cursor.execute('SELECT "DIMINFO", "SRID" FROM "USER_SDO_GEOM_METADATA" WHERE "TABLE_NAME"=%s AND "COLUMN_NAME"=%s', + (table_name.upper(), geo_col.upper())) + row = cursor.fetchone() + except Exception, msg: + raise Exception('Could not find entry in USER_SDO_GEOM_METADATA corresponding to "%s"."%s"\n' + 'Error message: %s.' % (table_name, geo_col, msg)) + + # TODO: Research way to find a more specific geometry field type for + # the column's contents. + field_type = 'GeometryField' + + # Getting the field parameters. + field_params = {} + dim, srid = row + if srid != 4326: + field_params['srid'] = srid + # Length of object array ( SDO_DIM_ARRAY ) is number of dimensions. + dim = len(dim) + if dim != 2: + field_params['dim'] = dim + finally: + cursor.close() + + return field_type, field_params diff --git a/parts/django/django/contrib/gis/db/backends/oracle/models.py b/parts/django/django/contrib/gis/db/backends/oracle/models.py new file mode 100644 index 0000000..de757ff --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/oracle/models.py @@ -0,0 +1,65 @@ +""" + The GeometryColumns and SpatialRefSys models for the Oracle spatial + backend. + + It should be noted that Oracle Spatial does not have database tables + named according to the OGC standard, so the closest analogs are used. + For example, the `USER_SDO_GEOM_METADATA` is used for the GeometryColumns + model and the `SDO_COORD_REF_SYS` is used for the SpatialRefSys model. +""" +from django.contrib.gis.db import models +from django.contrib.gis.db.models.fields import GeometryField +from django.contrib.gis.db.backends.base 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' + managed = False + + @classmethod + def table_name_col(cls): + """ + Returns the name of the metadata column used to store the + the feature table name. + """ + return 'table_name' + + @classmethod + def geom_col_name(cls): + """ + Returns the name of the metadata column used to store the + the feature geometry column. + """ + return 'column_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) + # Optional geometry representing the bounds of this coordinate + # system. By default, all are NULL in the table. + cs_bounds = models.PolygonField(null=True) + objects = models.GeoManager() + + class Meta: + db_table = 'CS_SRS' + managed = False + + @property + def wkt(self): + return self.wktext + + @classmethod + def wkt_col(cls): + return 'wktext' diff --git a/parts/django/django/contrib/gis/db/backends/oracle/operations.py b/parts/django/django/contrib/gis/db/backends/oracle/operations.py new file mode 100644 index 0000000..97f7b6c --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/oracle/operations.py @@ -0,0 +1,293 @@ +""" + 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.backends.oracle.base import DatabaseOperations +from django.contrib.gis.db.backends.base import BaseSpatialOperations +from django.contrib.gis.db.backends.oracle.adapter import OracleSpatialAdapter +from django.contrib.gis.db.backends.util import SpatialFunction +from django.contrib.gis.geometry.backend import Geometry +from django.contrib.gis.measure import Distance + +class SDOOperation(SpatialFunction): + "Base class for SDO* Oracle operations." + sql_template = "%(function)s(%(geo_col)s, %(geometry)s) %(operator)s '%(result)s'" + + def __init__(self, func, **kwargs): + kwargs.setdefault('operator', '=') + kwargs.setdefault('result', 'TRUE') + super(SDOOperation, self).__init__(func, **kwargs) + +class SDODistance(SpatialFunction): + "Class for Distance queries." + sql_template = ('%(function)s(%(geo_col)s, %(geometry)s, %(tolerance)s) ' + '%(operator)s %(result)s') + dist_func = 'SDO_GEOM.SDO_DISTANCE' + def __init__(self, op, tolerance=0.05): + super(SDODistance, self).__init__(self.dist_func, + tolerance=tolerance, + operator=op, result='%s') + +class SDODWithin(SpatialFunction): + dwithin_func = 'SDO_WITHIN_DISTANCE' + sql_template = "%(function)s(%(geo_col)s, %(geometry)s, %%s) = 'TRUE'" + def __init__(self): + super(SDODWithin, self).__init__(self.dwithin_func) + +class SDOGeomRelate(SpatialFunction): + "Class for using SDO_GEOM.RELATE." + relate_func = 'SDO_GEOM.RELATE' + sql_template = ("%(function)s(%(geo_col)s, '%(mask)s', %(geometry)s, " + "%(tolerance)s) %(operator)s '%(mask)s'") + 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'). + super(SDOGeomRelate, self).__init__(self.relate_func, operator='=', + mask=mask, tolerance=tolerance) + +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) + sql_template = "%(function)s(%(geo_col)s, %(geometry)s, 'mask=%(mask)s') = 'TRUE'" + relate_func = 'SDO_RELATE' + def __init__(self, mask): + if not self.mask_regex.match(mask): + raise ValueError('Invalid %s mask: "%s"' % (self.relate_func, mask)) + super(SDORelate, self).__init__(self.relate_func, mask=mask) + +# Valid distance types and substitutions +dtypes = (Decimal, Distance, float, int, long) + +class OracleOperations(DatabaseOperations, BaseSpatialOperations): + compiler_module = "django.contrib.gis.db.backends.oracle.compiler" + + name = 'oracle' + oracle = True + valid_aggregates = dict([(a, None) for a in ('Union', 'Extent')]) + + Adapter = OracleSpatialAdapter + Adaptor = Adapter # Backwards-compatibility alias. + + area = 'SDO_GEOM.SDO_AREA' + gml= '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' + perimeter = length + point_on_surface = 'SDO_GEOM.SDO_POINTONSURFACE' + reverse = 'SDO_UTIL.REVERSE_LINESTRING' + 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 =) + select = 'SDO_UTIL.TO_WKTGEOMETRY(%s)' + + distance_functions = { + 'distance_gt' : (SDODistance('>'), dtypes), + 'distance_gte' : (SDODistance('>='), dtypes), + 'distance_lt' : (SDODistance('<'), dtypes), + 'distance_lte' : (SDODistance('<='), dtypes), + 'dwithin' : (SDODWithin(), dtypes), + } + + 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'), + } + geometry_functions.update(distance_functions) + + gis_terms = ['isnull'] + gis_terms += geometry_functions.keys() + gis_terms = dict([(term, None) for term in gis_terms]) + + truncate_params = {'relate' : None} + + def __init__(self, connection): + super(OracleOperations, self).__init__() + self.connection = connection + + def convert_extent(self, clob): + if clob: + # Generally, Oracle returns a polygon for the extent -- however, + # it can return a single point if there's only one Point in the + # table. + ext_geom = Geometry(clob.read()) + gtype = str(ext_geom.geom_type) + if gtype == 'Polygon': + # Construct the 4-tuple from the coordinates in the polygon. + shell = ext_geom.shell + ll, ur = shell[0][:2], shell[2][:2] + elif gtype == 'Point': + ll = ext_geom.coords[:2] + ur = ll + else: + raise Exception('Unexpected geometry type returned for extent: %s' % gtype) + xmin, ymin = ll + xmax, ymax = ur + return (xmin, ymin, xmax, ymax) + else: + return None + + def convert_geom(self, clob, geo_field): + if clob: + return Geometry(clob.read(), geo_field.srid) + else: + return None + + def geo_db_type(self, f): + """ + Returns the geometry database type for Oracle. Unlike other spatial + backends, no stored procedure is necessary and it's the same for all + geometry types. + """ + return 'MDSYS.SDO_GEOMETRY' + + def get_distance(self, f, value, lookup_type): + """ + Returns the distance parameters given the value and the lookup type. + On Oracle, geometry columns with a geodetic coordinate system behave + implicitly like a geography column, and thus meters will be used as + the distance parameter on them. + """ + if not value: + return [] + value = value[0] + if isinstance(value, Distance): + if f.geodetic(self.connection): + dist_param = value.m + else: + dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection))) + else: + dist_param = value + + # dwithin lookups on oracle require a special string parameter + # that starts with "distance=". + if lookup_type == 'dwithin': + dist_param = 'distance=%s' % dist_param + + return [dist_param] + + def get_geom_placeholder(self, f, value): + """ + 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 'NULL' + + def transform_value(val, srid): + return val.srid != srid + + if hasattr(value, 'expression'): + if transform_value(value, f.srid): + placeholder = '%s(%%s, %s)' % (self.transform, f.srid) + else: + placeholder = '%s' + # No geometry value used for F expression, substitue in + # the column name instead. + return placeholder % '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression])) + else: + if transform_value(value, f.srid): + return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (self.transform, value.srid, f.srid) + else: + return 'SDO_GEOMETRY(%%s, %s)' % f.srid + + def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn): + "Returns the SQL WHERE clause for use in Oracle spatial SQL construction." + alias, col, db_type = lvalue + + # Getting the quoted table name as `geo_col`. + geo_col = '%s.%s' % (qn(alias), qn(col)) + + # See if a Oracle Geometry function matches the lookup type next + lookup_info = self.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 + geom = value[0] + + # Ensuring that a tuple _value_ was passed in from the user + if not isinstance(value, tuple): + raise ValueError('Tuple required for `%s` lookup type.' % lookup_type) + if len(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(value[1], arg_type): + raise ValueError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1]))) + + if lookup_type == 'relate': + # The SDORelate class handles construction for these queries, + # and verifies the mask argument. + return sdo_op(value[1]).as_sql(geo_col, self.get_geom_placeholder(field, geom)) + else: + # Otherwise, just call the `as_sql` method on the SDOOperation instance. + return sdo_op.as_sql(geo_col, self.get_geom_placeholder(field, geom)) + 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, self.get_geom_placeholder(field, value)) + elif lookup_type == 'isnull': + # Handling 'isnull' lookup type + return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or '')) + + raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) + + def spatial_aggregate_sql(self, agg): + """ + Returns the spatial aggregate SQL template and function for the + given Aggregate instance. + """ + agg_name = agg.__class__.__name__.lower() + if agg_name == 'union' : agg_name += 'agg' + if agg.is_extent: + sql_template = '%(function)s(%(field)s)' + else: + sql_template = '%(function)s(SDOAGGRTYPE(%(field)s,%(tolerance)s))' + sql_function = getattr(self, agg_name) + return self.select % sql_template, sql_function + + # Routines for getting the OGC-compliant models. + def geometry_columns(self): + from django.contrib.gis.db.backends.oracle.models import GeometryColumns + return GeometryColumns + + def spatial_ref_sys(self): + from django.contrib.gis.db.backends.oracle.models import SpatialRefSys + return SpatialRefSys diff --git a/parts/django/django/contrib/gis/db/backends/postgis/__init__.py b/parts/django/django/contrib/gis/db/backends/postgis/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/postgis/__init__.py diff --git a/parts/django/django/contrib/gis/db/backends/postgis/adapter.py b/parts/django/django/contrib/gis/db/backends/postgis/adapter.py new file mode 100644 index 0000000..3f8603e --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/postgis/adapter.py @@ -0,0 +1,35 @@ +""" + This object provides quoting for GEOS geometries into PostgreSQL/PostGIS. +""" + +from psycopg2 import Binary +from psycopg2.extensions import ISQLQuote + +class PostGISAdapter(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.ewkb = str(geom.ewkb) + 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.ewkb == other.ewkb) 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 'ST_GeomFromEWKB(E%s)' % Binary(self.ewkb) + + def prepare_database_save(self, unused): + return self diff --git a/parts/django/django/contrib/gis/db/backends/postgis/base.py b/parts/django/django/contrib/gis/db/backends/postgis/base.py new file mode 100644 index 0000000..634a7d5 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/postgis/base.py @@ -0,0 +1,12 @@ +from django.db.backends.postgresql_psycopg2.base import * +from django.db.backends.postgresql_psycopg2.base import DatabaseWrapper as Psycopg2DatabaseWrapper +from django.contrib.gis.db.backends.postgis.creation import PostGISCreation +from django.contrib.gis.db.backends.postgis.introspection import PostGISIntrospection +from django.contrib.gis.db.backends.postgis.operations import PostGISOperations + +class DatabaseWrapper(Psycopg2DatabaseWrapper): + def __init__(self, *args, **kwargs): + super(DatabaseWrapper, self).__init__(*args, **kwargs) + self.creation = PostGISCreation(self) + self.ops = PostGISOperations(self) + self.introspection = PostGISIntrospection(self) diff --git a/parts/django/django/contrib/gis/db/backends/postgis/creation.py b/parts/django/django/contrib/gis/db/backends/postgis/creation.py new file mode 100644 index 0000000..e14c792 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/postgis/creation.py @@ -0,0 +1,60 @@ +from django.conf import settings +from django.db.backends.postgresql.creation import DatabaseCreation + +class PostGISCreation(DatabaseCreation): + geom_index_type = 'GIST' + geom_index_opts = 'GIST_GEOMETRY_OPS' + + def sql_indexes_for_field(self, model, f, style): + "Return any spatial index creation SQL for the field." + from django.contrib.gis.db.models.fields import GeometryField + + output = super(PostGISCreation, self).sql_indexes_for_field(model, f, style) + + if isinstance(f, GeometryField): + gqn = self.connection.ops.geo_quote_name + qn = self.connection.ops.quote_name + db_table = model._meta.db_table + + if f.geography: + # Geogrophy columns are created normally. + pass + else: + # Geometry columns are created by `AddGeometryColumn` + # stored procedure. + output.append(style.SQL_KEYWORD('SELECT ') + + style.SQL_TABLE('AddGeometryColumn') + '(' + + style.SQL_TABLE(gqn(db_table)) + ', ' + + style.SQL_FIELD(gqn(f.column)) + ', ' + + style.SQL_FIELD(str(f.srid)) + ', ' + + style.SQL_COLTYPE(gqn(f.geom_type)) + ', ' + + style.SQL_KEYWORD(str(f.dim)) + ');') + + if not f.null: + # Add a NOT NULL constraint to the field + output.append(style.SQL_KEYWORD('ALTER TABLE ') + + style.SQL_TABLE(qn(db_table)) + + style.SQL_KEYWORD(' ALTER ') + + style.SQL_FIELD(qn(f.column)) + + style.SQL_KEYWORD(' SET NOT NULL') + ';') + + + if f.spatial_index: + # Spatial indexes created the same way for both Geometry and + # Geography columns + if f.geography: + index_opts = '' + else: + index_opts = ' ' + style.SQL_KEYWORD(self.geom_index_opts) + output.append(style.SQL_KEYWORD('CREATE INDEX ') + + style.SQL_TABLE(qn('%s_%s_id' % (db_table, f.column))) + + style.SQL_KEYWORD(' ON ') + + style.SQL_TABLE(qn(db_table)) + + style.SQL_KEYWORD(' USING ') + + style.SQL_COLTYPE(self.geom_index_type) + ' ( ' + + style.SQL_FIELD(qn(f.column)) + index_opts + ' );') + return output + + def sql_table_creation_suffix(self): + qn = self.connection.ops.quote_name + return ' TEMPLATE %s' % qn(getattr(settings, 'POSTGIS_TEMPLATE', 'template_postgis')) diff --git a/parts/django/django/contrib/gis/db/backends/postgis/introspection.py b/parts/django/django/contrib/gis/db/backends/postgis/introspection.py new file mode 100644 index 0000000..7962d19 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/postgis/introspection.py @@ -0,0 +1,95 @@ +from django.db.backends.postgresql_psycopg2.introspection import DatabaseIntrospection +from django.contrib.gis.gdal import OGRGeomType + +class GeoIntrospectionError(Exception): + pass + +class PostGISIntrospection(DatabaseIntrospection): + # Reverse dictionary for PostGIS geometry types not populated until + # introspection is actually performed. + postgis_types_reverse = {} + + def get_postgis_types(self): + """ + Returns a dictionary with keys that are the PostgreSQL object + identification integers for the PostGIS geometry and/or + geography types (if supported). + """ + cursor = self.connection.cursor() + # The OID integers associated with the geometry type may + # be different across versions; hence, this is why we have + # to query the PostgreSQL pg_type table corresponding to the + # PostGIS custom data types. + oid_sql = 'SELECT "oid" FROM "pg_type" WHERE "typname" = %s' + try: + cursor.execute(oid_sql, ('geometry',)) + GEOM_TYPE = cursor.fetchone()[0] + postgis_types = { GEOM_TYPE : 'GeometryField' } + if self.connection.ops.geography: + cursor.execute(oid_sql, ('geography',)) + GEOG_TYPE = cursor.fetchone()[0] + # The value for the geography type is actually a tuple + # to pass in the `geography=True` keyword to the field + # definition. + postgis_types[GEOG_TYPE] = ('GeometryField', {'geography' : True}) + finally: + cursor.close() + + return postgis_types + + def get_field_type(self, data_type, description): + if not self.postgis_types_reverse: + # If the PostGIS types reverse dictionary is not populated, do so + # now. In order to prevent unnecessary requests upon connection + # intialization, the `data_types_reverse` dictionary is not updated + # with the PostGIS custom types until introspection is actually + # performed -- in other words, when this function is called. + self.postgis_types_reverse = self.get_postgis_types() + self.data_types_reverse.update(self.postgis_types_reverse) + return super(PostGISIntrospection, self).get_field_type(data_type, description) + + def get_geometry_type(self, table_name, geo_col): + """ + The geometry type OID used by PostGIS does not indicate the particular + type of field that a geometry column is (e.g., whether it's a + PointField or a PolygonField). Thus, this routine queries the PostGIS + metadata tables to determine the geometry type, + """ + cursor = self.connection.cursor() + try: + try: + # First seeing if this geometry column is in the `geometry_columns` + cursor.execute('SELECT "coord_dimension", "srid", "type" ' + 'FROM "geometry_columns" ' + 'WHERE "f_table_name"=%s AND "f_geometry_column"=%s', + (table_name, geo_col)) + row = cursor.fetchone() + if not row: raise GeoIntrospectionError + except GeoIntrospectionError: + if self.connection.ops.geography: + cursor.execute('SELECT "coord_dimension", "srid", "type" ' + 'FROM "geography_columns" ' + 'WHERE "f_table_name"=%s AND "f_geography_column"=%s', + (table_name, geo_col)) + row = cursor.fetchone() + + if not row: + raise Exception('Could not find a geometry or geography column for "%s"."%s"' % + (table_name, geo_col)) + + # OGRGeomType does not require GDAL and makes it easy to convert + # from OGC geom type name to Django field. + field_type = OGRGeomType(row[2]).django + + # Getting any GeometryField keyword arguments that are not the default. + dim = row[0] + srid = row[1] + field_params = {} + if srid != 4326: + field_params['srid'] = srid + if dim != 2: + field_params['dim'] = dim + finally: + cursor.close() + + return field_type, field_params diff --git a/parts/django/django/contrib/gis/db/backends/postgis/models.py b/parts/django/django/contrib/gis/db/backends/postgis/models.py new file mode 100644 index 0000000..a385983 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/postgis/models.py @@ -0,0 +1,66 @@ +""" + The GeometryColumns and SpatialRefSys models for the PostGIS backend. +""" +from django.db import models +from django.contrib.gis.db.backends.base import SpatialRefSysMixin + +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' + managed = False + + @classmethod + def table_name_col(cls): + """ + Returns the name of the metadata column used to store the + the feature table name. + """ + return 'f_table_name' + + @classmethod + def geom_col_name(cls): + """ + Returns the name of the metadata column used to store the + the feature geometry column. + """ + return 'f_geometry_column' + + 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' + managed = False + + @property + def wkt(self): + return self.srtext + + @classmethod + def wkt_col(cls): + return 'srtext' diff --git a/parts/django/django/contrib/gis/db/backends/postgis/operations.py b/parts/django/django/contrib/gis/db/backends/postgis/operations.py new file mode 100644 index 0000000..5affcf9 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/postgis/operations.py @@ -0,0 +1,589 @@ +import re +from decimal import Decimal + +from django.conf import settings +from django.contrib.gis.db.backends.base import BaseSpatialOperations +from django.contrib.gis.db.backends.util import SpatialOperation, SpatialFunction +from django.contrib.gis.db.backends.postgis.adapter import PostGISAdapter +from django.contrib.gis.geometry.backend import Geometry +from django.contrib.gis.measure import Distance +from django.core.exceptions import ImproperlyConfigured +from django.db.backends.postgresql_psycopg2.base import DatabaseOperations +from django.db.utils import DatabaseError + +#### 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) + +class PostGISFunction(SpatialFunction): + "For PostGIS function calls (e.g., `ST_Contains(table, geom)`)." + def __init__(self, prefix, function, **kwargs): + super(PostGISFunction, self).__init__(prefix + function, **kwargs) + +class PostGISFunctionParam(PostGISFunction): + "For PostGIS functions that take another parameter (e.g. DWithin, Relate)." + sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s)' + +class PostGISDistance(PostGISFunction): + "For PostGIS distance operations." + dist_func = 'Distance' + sql_template = '%(function)s(%(geo_col)s, %(geometry)s) %(operator)s %%s' + + def __init__(self, prefix, operator): + super(PostGISDistance, self).__init__(prefix, self.dist_func, + operator=operator) + +class PostGISSpheroidDistance(PostGISFunction): + "For PostGIS spherical distance operations (using the spheroid)." + dist_func = 'distance_spheroid' + sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s) %(operator)s %%s' + def __init__(self, prefix, operator): + # An extra parameter in `end_subst` is needed for the spheroid string. + super(PostGISSpheroidDistance, self).__init__(prefix, self.dist_func, + operator=operator) + +class PostGISSphereDistance(PostGISDistance): + "For PostGIS spherical distance operations." + dist_func = 'distance_sphere' + +class PostGISRelate(PostGISFunctionParam): + "For PostGIS Relate(<geom>, <pattern>) calls." + pattern_regex = re.compile(r'^[012TF\*]{9}$') + def __init__(self, prefix, pattern): + if not self.pattern_regex.match(pattern): + raise ValueError('Invalid intersection matrix pattern "%s".' % pattern) + super(PostGISRelate, self).__init__(prefix, 'Relate') + + +class PostGISOperations(DatabaseOperations, BaseSpatialOperations): + compiler_module = 'django.contrib.gis.db.models.sql.compiler' + name = 'postgis' + postgis = True + version_regex = re.compile(r'^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)') + valid_aggregates = dict([(k, None) for k in + ('Collect', 'Extent', 'Extent3D', 'MakeLine', 'Union')]) + + Adapter = PostGISAdapter + Adaptor = Adapter # Backwards-compatibility alias. + + def __init__(self, connection): + super(PostGISOperations, self).__init__(connection) + + # Trying to get the PostGIS version because the function + # signatures will depend on the version used. The cost + # here is a database query to determine the version, which + # can be mitigated by setting `POSTGIS_VERSION` with a 3-tuple + # comprising user-supplied values for the major, minor, and + # subminor revision of PostGIS. + try: + if hasattr(settings, 'POSTGIS_VERSION'): + vtup = settings.POSTGIS_VERSION + if len(vtup) == 3: + # The user-supplied PostGIS version. + version = vtup + else: + # This was the old documented way, but it's stupid to + # include the string. + version = vtup[1:4] + else: + vtup = self.postgis_version_tuple() + version = vtup[1:] + + # Getting the prefix -- even though we don't officially support + # PostGIS 1.2 anymore, keeping it anyway in case a prefix change + # for something else is necessary. + if version >= (1, 2, 2): + prefix = 'ST_' + else: + prefix = '' + + self.geom_func_prefix = prefix + self.spatial_version = version + except DatabaseError: + raise ImproperlyConfigured('Cannot determine PostGIS version for database "%s". ' + 'GeoDjango requires at least PostGIS version 1.3. ' + 'Was the database created from a spatial database ' + 'template?' % self.connection.settings_dict['NAME'] + ) + except Exception, e: + # TODO: Raise helpful exceptions as they become known. + raise + + # PostGIS-specific operators. The commented descriptions of these + # operators come from Section 7.6 of the PostGIS 1.4 documentation. + self.geometry_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('&&'), + } + + self.geometry_functions = { + 'equals' : PostGISFunction(prefix, 'Equals'), + 'disjoint' : PostGISFunction(prefix, 'Disjoint'), + 'touches' : PostGISFunction(prefix, 'Touches'), + 'crosses' : PostGISFunction(prefix, 'Crosses'), + 'within' : PostGISFunction(prefix, 'Within'), + 'overlaps' : PostGISFunction(prefix, 'Overlaps'), + 'contains' : PostGISFunction(prefix, 'Contains'), + 'intersects' : PostGISFunction(prefix, '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 {'cartesian' : PostGISDistance(prefix, operator), + 'sphere' : PostGISSphereDistance(prefix, operator), + 'spheroid' : PostGISSpheroidDistance(prefix, operator), + } + self.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), + } + + # Versions 1.2.2+ have KML serialization support. + if version < (1, 2, 2): + ASKML = False + else: + ASKML = 'ST_AsKML' + self.geometry_functions.update( + {'coveredby' : PostGISFunction(prefix, 'CoveredBy'), + 'covers' : PostGISFunction(prefix, 'Covers'), + }) + self.distance_functions['dwithin'] = (PostGISFunctionParam(prefix, 'DWithin'), dtypes) + + # Adding the distance functions to the geometries lookup. + self.geometry_functions.update(self.distance_functions) + + # The union aggregate and topology operation use the same signature + # in versions 1.3+. + if version < (1, 3, 0): + UNIONAGG = 'GeomUnion' + UNION = 'Union' + MAKELINE = False + else: + UNIONAGG = 'ST_Union' + UNION = 'ST_Union' + MAKELINE = 'ST_MakeLine' + + # Only PostGIS versions 1.3.4+ have GeoJSON serialization support. + if version < (1, 3, 4): + GEOJSON = False + else: + GEOJSON = prefix + 'AsGeoJson' + + # ST_ContainsProperly ST_MakeLine, and ST_GeoHash added in 1.4. + if version >= (1, 4, 0): + GEOHASH = 'ST_GeoHash' + BOUNDINGCIRCLE = 'ST_MinimumBoundingCircle' + self.geometry_functions['contains_properly'] = PostGISFunction(prefix, 'ContainsProperly') + else: + GEOHASH, BOUNDINGCIRCLE = False, False + + # Geography type support added in 1.5. + if version >= (1, 5, 0): + self.geography = True + # Only a subset of the operators and functions are available + # for the geography type. + self.geography_functions = self.distance_functions.copy() + self.geography_functions.update({ + 'coveredby' : self.geometry_functions['coveredby'], + 'covers' : self.geometry_functions['covers'], + 'intersects' : self.geometry_functions['intersects'], + }) + self.geography_operators = { + 'bboverlaps' : PostGISOperator('&&'), + } + + # Creating a dictionary lookup of all GIS terms for PostGIS. + gis_terms = ['isnull'] + gis_terms += self.geometry_operators.keys() + gis_terms += self.geometry_functions.keys() + self.gis_terms = dict([(term, None) for term in gis_terms]) + + self.area = prefix + 'Area' + self.bounding_circle = BOUNDINGCIRCLE + self.centroid = prefix + 'Centroid' + self.collect = prefix + 'Collect' + self.difference = prefix + 'Difference' + self.distance = prefix + 'Distance' + self.distance_sphere = prefix + 'distance_sphere' + self.distance_spheroid = prefix + 'distance_spheroid' + self.envelope = prefix + 'Envelope' + self.extent = prefix + 'Extent' + self.extent3d = prefix + 'Extent3D' + self.force_rhr = prefix + 'ForceRHR' + self.geohash = GEOHASH + self.geojson = GEOJSON + self.gml = prefix + 'AsGML' + self.intersection = prefix + 'Intersection' + self.kml = ASKML + self.length = prefix + 'Length' + self.length3d = prefix + 'Length3D' + self.length_spheroid = prefix + 'length_spheroid' + self.makeline = MAKELINE + self.mem_size = prefix + 'mem_size' + self.num_geom = prefix + 'NumGeometries' + self.num_points =prefix + 'npoints' + self.perimeter = prefix + 'Perimeter' + self.perimeter3d = prefix + 'Perimeter3D' + self.point_on_surface = prefix + 'PointOnSurface' + self.polygonize = prefix + 'Polygonize' + self.reverse = prefix + 'Reverse' + self.scale = prefix + 'Scale' + self.snap_to_grid = prefix + 'SnapToGrid' + self.svg = prefix + 'AsSVG' + self.sym_difference = prefix + 'SymDifference' + self.transform = prefix + 'Transform' + self.translate = prefix + 'Translate' + self.union = UNION + self.unionagg = UNIONAGG + + def check_aggregate_support(self, aggregate): + """ + Checks if the given aggregate name is supported (that is, if it's + in `self.valid_aggregates`). + """ + agg_name = aggregate.__class__.__name__ + return agg_name in self.valid_aggregates + + def convert_extent(self, box): + """ + Returns a 4-tuple extent for the `Extent` aggregate by converting + the bounding box text returned by PostGIS (`box` argument), for + example: "BOX(-90.0 30.0, -85.0 40.0)". + """ + ll, ur = box[4:-1].split(',') + xmin, ymin = map(float, ll.split()) + xmax, ymax = map(float, ur.split()) + return (xmin, ymin, xmax, ymax) + + def convert_extent3d(self, box3d): + """ + Returns a 6-tuple extent for the `Extent3D` aggregate by converting + the 3d bounding-box text returnded by PostGIS (`box3d` argument), for + example: "BOX3D(-90.0 30.0 1, -85.0 40.0 2)". + """ + ll, ur = box3d[6:-1].split(',') + xmin, ymin, zmin = map(float, ll.split()) + xmax, ymax, zmax = map(float, ur.split()) + return (xmin, ymin, zmin, xmax, ymax, zmax) + + def convert_geom(self, hex, geo_field): + """ + Converts the geometry returned from PostGIS aggretates. + """ + if hex: + return Geometry(hex) + else: + return None + + def geo_db_type(self, f): + """ + Return the database field type for the given geometry field. + Typically this is `None` because geometry columns are added via + the `AddGeometryColumn` stored procedure, unless the field + has been specified to be of geography type instead. + """ + if f.geography: + if not self.geography: + raise NotImplementedError('PostGIS 1.5 required for geography column support.') + + if f.srid != 4326: + raise NotImplementedError('PostGIS 1.5 supports geography columns ' + 'only with an SRID of 4326.') + + return 'geography(%s,%d)'% (f.geom_type, f.srid) + else: + return None + + def get_distance(self, f, dist_val, lookup_type): + """ + Retrieve the distance parameters for the given geometry field, + distance lookup value, and the distance lookup type. + + This is the most complex implementation of the spatial backends due to + what is supported on geodetic geometry columns vs. what's available on + projected geometry columns. In addition, it has to take into account + the newly introduced geography column type introudced in PostGIS 1.5. + """ + # Getting the distance parameter and any options. + if len(dist_val) == 1: + value, option = dist_val[0], None + else: + value, option = dist_val + + # Shorthand boolean flags. + geodetic = f.geodetic(self.connection) + geography = f.geography and self.geography + + if isinstance(value, Distance): + if geography: + dist_param = value.m + elif geodetic: + if lookup_type == 'dwithin': + raise ValueError('Only numeric values of degree units are ' + 'allowed on geographic DWithin queries.') + dist_param = value.m + else: + dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection))) + else: + # Assuming the distance is in the units of the field. + dist_param = value + + if (not geography and geodetic and lookup_type != 'dwithin' + and option == 'spheroid'): + # using distance_spheroid requires the spheroid of the field as + # a parameter. + return [f._spheroid, dist_param] + else: + return [dist_param] + + def get_geom_placeholder(self, f, 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 == f.srid: + placeholder = '%s' + else: + # Adding Transform() to the SQL placeholder. + placeholder = '%s(%%s, %s)' % (self.transform, f.srid) + + if hasattr(value, 'expression'): + # If this is an F expression, then we don't really want + # a placeholder and instead substitute in the column + # of the expression. + placeholder = placeholder % '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression])) + + return placeholder + + def _get_postgis_func(self, func): + """ + Helper routine for calling PostGIS functions and returning their result. + """ + cursor = self.connection._cursor() + try: + try: + cursor.execute('SELECT %s()' % func) + row = cursor.fetchone() + except: + # Responsibility of callers to perform error handling. + raise + finally: + # Close out the connection. See #9437. + self.connection.close() + return row[0] + + def postgis_geos_version(self): + "Returns the version of the GEOS library used with PostGIS." + return self._get_postgis_func('postgis_geos_version') + + def postgis_lib_version(self): + "Returns the version number of the PostGIS library used with PostgreSQL." + return self._get_postgis_func('postgis_lib_version') + + def postgis_proj_version(self): + "Returns the version of the PROJ.4 library used with PostGIS." + return self._get_postgis_func('postgis_proj_version') + + def postgis_version(self): + "Returns PostGIS version number and compile-time options." + return self._get_postgis_func('postgis_version') + + def postgis_full_version(self): + "Returns PostGIS version number and compile-time options." + return self._get_postgis_func('postgis_full_version') + + def postgis_version_tuple(self): + """ + Returns the PostGIS version as a tuple (version string, major, + minor, subminor). + """ + # Getting the PostGIS version + version = self.postgis_lib_version() + m = self.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) + + def proj_version_tuple(self): + """ + Return the version of PROJ.4 used by PostGIS as a tuple of the + major, minor, and subminor release numbers. + """ + proj_regex = re.compile(r'(\d+)\.(\d+)\.(\d+)') + proj_ver_str = self.postgis_proj_version() + m = proj_regex.search(proj_ver_str) + if m: + return tuple(map(int, [m.group(1), m.group(2), m.group(3)])) + else: + raise Exception('Could not determine PROJ.4 version from PostGIS.') + + def num_params(self, lookup_type, num_param): + """ + Helper routine that returns a boolean indicating whether the number of + parameters is correct for the lookup type. + """ + def exactly_two(np): return np == 2 + def two_to_three(np): return np >= 2 and np <=3 + if (lookup_type in self.distance_functions and + lookup_type != 'dwithin'): + return two_to_three(num_param) + else: + return exactly_two(num_param) + + def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn): + """ + Constructs spatial SQL from the given lookup value tuple a + (alias, col, db_type), the lookup type string, lookup value, and + the geometry field. + """ + alias, col, db_type = lvalue + + # Getting the quoted geometry column. + geo_col = '%s.%s' % (qn(alias), qn(col)) + + if lookup_type in self.geometry_operators: + if field.geography and not lookup_type in self.geography_operators: + raise ValueError('PostGIS geography does not support the ' + '"%s" lookup.' % lookup_type) + # Handling a PostGIS operator. + op = self.geometry_operators[lookup_type] + return op.as_sql(geo_col, self.get_geom_placeholder(field, value)) + elif lookup_type in self.geometry_functions: + if field.geography and not lookup_type in self.geography_functions: + raise ValueError('PostGIS geography type does not support the ' + '"%s" lookup.' % lookup_type) + + # See if a PostGIS geometry function matches the lookup type. + tmp = self.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(value, (tuple, list)): + raise ValueError('Tuple required for `%s` lookup type.' % lookup_type) + + # Geometry is first element of lookup tuple. + geom = value[0] + + # Number of valid tuple parameters depends on the lookup type. + nparams = len(value) + if not self.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(value[1], arg_type): + raise ValueError('Argument type should be %s, got %s instead.' % (arg_type, type(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(self.geom_func_prefix, value[1]) + elif lookup_type in self.distance_functions and lookup_type != 'dwithin': + if not field.geography and field.geodetic(self.connection): + # Geodetic distances are only availble from Points to + # PointFields on PostGIS 1.4 and below. + if not self.connection.ops.geography: + if field.geom_type != 'POINT': + raise ValueError('PostGIS spherical operations are only valid on PointFields.') + + if str(geom.geom_type) != 'Point': + raise ValueError('PostGIS geometry distance parameter is required to be of type Point.') + + # Setting up the geodetic operation appropriately. + if nparams == 3 and value[2] == 'spheroid': + op = op['spheroid'] + else: + op = op['sphere'] + else: + op = op['cartesian'] + else: + op = tmp + geom = value + + # Calling the `as_sql` function on the operation instance. + return op.as_sql(geo_col, self.get_geom_placeholder(field, geom)) + + elif lookup_type == 'isnull': + # Handling 'isnull' lookup type + return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or '')) + + raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) + + def spatial_aggregate_sql(self, agg): + """ + Returns the spatial aggregate SQL template and function for the + given Aggregate instance. + """ + agg_name = agg.__class__.__name__ + if not self.check_aggregate_support(agg): + raise NotImplementedError('%s spatial aggregate is not implmented for this backend.' % agg_name) + agg_name = agg_name.lower() + if agg_name == 'union': agg_name += 'agg' + sql_template = '%(function)s(%(field)s)' + sql_function = getattr(self, agg_name) + return sql_template, sql_function + + # Routines for getting the OGC-compliant models. + def geometry_columns(self): + from django.contrib.gis.db.backends.postgis.models import GeometryColumns + return GeometryColumns + + def spatial_ref_sys(self): + from django.contrib.gis.db.backends.postgis.models import SpatialRefSys + return SpatialRefSys diff --git a/parts/django/django/contrib/gis/db/backends/spatialite/__init__.py b/parts/django/django/contrib/gis/db/backends/spatialite/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/spatialite/__init__.py diff --git a/parts/django/django/contrib/gis/db/backends/spatialite/adapter.py b/parts/django/django/contrib/gis/db/backends/spatialite/adapter.py new file mode 100644 index 0000000..d8fefba --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/spatialite/adapter.py @@ -0,0 +1,8 @@ +from django.db.backends.sqlite3.base import Database +from django.contrib.gis.db.backends.adapter import WKTAdapter + +class SpatiaLiteAdapter(WKTAdapter): + "SQLite adaptor for geometry objects." + def __conform__(self, protocol): + if protocol is Database.PrepareProtocol: + return str(self) diff --git a/parts/django/django/contrib/gis/db/backends/spatialite/base.py b/parts/django/django/contrib/gis/db/backends/spatialite/base.py new file mode 100644 index 0000000..729fc15 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/spatialite/base.py @@ -0,0 +1,77 @@ +from ctypes.util import find_library +from django.conf import settings + +from django.core.exceptions import ImproperlyConfigured +from django.db.backends.sqlite3.base import * +from django.db.backends.sqlite3.base import DatabaseWrapper as SqliteDatabaseWrapper, \ + _sqlite_extract, _sqlite_date_trunc, _sqlite_regexp +from django.contrib.gis.db.backends.spatialite.client import SpatiaLiteClient +from django.contrib.gis.db.backends.spatialite.creation import SpatiaLiteCreation +from django.contrib.gis.db.backends.spatialite.introspection import SpatiaLiteIntrospection +from django.contrib.gis.db.backends.spatialite.operations import SpatiaLiteOperations + +class DatabaseWrapper(SqliteDatabaseWrapper): + def __init__(self, *args, **kwargs): + # Before we get too far, make sure pysqlite 2.5+ is installed. + if Database.version_info < (2, 5, 0): + raise ImproperlyConfigured('Only versions of pysqlite 2.5+ are ' + 'compatible with SpatiaLite and GeoDjango.') + + # Trying to find the location of the SpatiaLite library. + # Here we are figuring out the path to the SpatiaLite library + # (`libspatialite`). If it's not in the system library path (e.g., it + # cannot be found by `ctypes.util.find_library`), then it may be set + # manually in the settings via the `SPATIALITE_LIBRARY_PATH` setting. + self.spatialite_lib = getattr(settings, 'SPATIALITE_LIBRARY_PATH', + find_library('spatialite')) + if not self.spatialite_lib: + raise ImproperlyConfigured('Unable to locate the SpatiaLite library. ' + 'Make sure it is in your library path, or set ' + 'SPATIALITE_LIBRARY_PATH in your settings.' + ) + super(DatabaseWrapper, self).__init__(*args, **kwargs) + self.ops = SpatiaLiteOperations(self) + self.client = SpatiaLiteClient(self) + self.creation = SpatiaLiteCreation(self) + self.introspection = SpatiaLiteIntrospection(self) + + def _cursor(self): + if self.connection is None: + ## The following is the same as in django.db.backends.sqlite3.base ## + settings_dict = self.settings_dict + if not settings_dict['NAME']: + raise ImproperlyConfigured("Please fill out the database NAME in the settings module before using the database.") + kwargs = { + 'database': settings_dict['NAME'], + 'detect_types': Database.PARSE_DECLTYPES | Database.PARSE_COLNAMES, + } + kwargs.update(settings_dict['OPTIONS']) + self.connection = Database.connect(**kwargs) + # Register extract, date_trunc, and regexp functions. + self.connection.create_function("django_extract", 2, _sqlite_extract) + self.connection.create_function("django_date_trunc", 2, _sqlite_date_trunc) + self.connection.create_function("regexp", 2, _sqlite_regexp) + connection_created.send(sender=self.__class__, connection=self) + + ## From here on, customized for GeoDjango ## + + # Enabling extension loading on the SQLite connection. + try: + self.connection.enable_load_extension(True) + except AttributeError: + raise ImproperlyConfigured('The pysqlite library does not support C extension loading. ' + 'Both SQLite and pysqlite must be configured to allow ' + 'the loading of extensions to use SpatiaLite.' + ) + + # Loading the SpatiaLite library extension on the connection, and returning + # the created cursor. + cur = self.connection.cursor(factory=SQLiteCursorWrapper) + try: + cur.execute("SELECT load_extension(%s)", (self.spatialite_lib,)) + except Exception, msg: + raise ImproperlyConfigured('Unable to load the SpatiaLite library extension ' + '"%s" because: %s' % (self.spatialite_lib, msg)) + return cur + else: + return self.connection.cursor(factory=SQLiteCursorWrapper) diff --git a/parts/django/django/contrib/gis/db/backends/spatialite/client.py b/parts/django/django/contrib/gis/db/backends/spatialite/client.py new file mode 100644 index 0000000..536065a --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/spatialite/client.py @@ -0,0 +1,5 @@ +from django.db.backends.sqlite3.client import DatabaseClient + +class SpatiaLiteClient(DatabaseClient): + executable_name = 'spatialite' + diff --git a/parts/django/django/contrib/gis/db/backends/spatialite/creation.py b/parts/django/django/contrib/gis/db/backends/spatialite/creation.py new file mode 100644 index 0000000..cbe4a29 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/spatialite/creation.py @@ -0,0 +1,96 @@ +import os +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.core.management import call_command +from django.db.backends.sqlite3.creation import DatabaseCreation + +class SpatiaLiteCreation(DatabaseCreation): + + def create_test_db(self, verbosity=1, autoclobber=False): + """ + Creates a test database, prompting the user for confirmation if the + database already exists. Returns the name of the test database created. + + This method is overloaded to load up the SpatiaLite initialization + SQL prior to calling the `syncdb` command. + """ + if verbosity >= 1: + print "Creating test database '%s'..." % self.connection.alias + + test_database_name = self._create_test_db(verbosity, autoclobber) + + self.connection.close() + + self.connection.settings_dict["NAME"] = test_database_name + can_rollback = self._rollback_works() + self.connection.settings_dict["SUPPORTS_TRANSACTIONS"] = can_rollback + # Need to load the SpatiaLite initialization SQL before running `syncdb`. + self.load_spatialite_sql() + call_command('syncdb', verbosity=verbosity, interactive=False, database=self.connection.alias) + + if settings.CACHE_BACKEND.startswith('db://'): + from django.core.cache import parse_backend_uri + _, cache_name, _ = parse_backend_uri(settings.CACHE_BACKEND) + call_command('createcachetable', cache_name) + + # Get a cursor (even though we don't need one yet). This has + # the side effect of initializing the test database. + cursor = self.connection.cursor() + + return test_database_name + + def sql_indexes_for_field(self, model, f, style): + "Return any spatial index creation SQL for the field." + from django.contrib.gis.db.models.fields import GeometryField + + output = super(SpatiaLiteCreation, self).sql_indexes_for_field(model, f, style) + + if isinstance(f, GeometryField): + gqn = self.connection.ops.geo_quote_name + qn = self.connection.ops.quote_name + db_table = model._meta.db_table + + output.append(style.SQL_KEYWORD('SELECT ') + + style.SQL_TABLE('AddGeometryColumn') + '(' + + style.SQL_TABLE(gqn(db_table)) + ', ' + + style.SQL_FIELD(gqn(f.column)) + ', ' + + style.SQL_FIELD(str(f.srid)) + ', ' + + style.SQL_COLTYPE(gqn(f.geom_type)) + ', ' + + style.SQL_KEYWORD(str(f.dim)) + ', ' + + style.SQL_KEYWORD(str(int(not f.null))) + + ');') + + if f.spatial_index: + output.append(style.SQL_KEYWORD('SELECT ') + + style.SQL_TABLE('CreateSpatialIndex') + '(' + + style.SQL_TABLE(gqn(db_table)) + ', ' + + style.SQL_FIELD(gqn(f.column)) + ');') + + return output + + def load_spatialite_sql(self): + """ + This routine loads up the SpatiaLite SQL file. + """ + # Getting the location of the SpatiaLite SQL file, and confirming + # it exists. + spatialite_sql = self.spatialite_init_file() + if not os.path.isfile(spatialite_sql): + raise ImproperlyConfigured('Could not find the required SpatiaLite initialization ' + 'SQL file (necessary for testing): %s' % spatialite_sql) + + # Opening up the SpatiaLite SQL initialization file and executing + # as a script. + sql_fh = open(spatialite_sql, 'r') + try: + cur = self.connection._cursor() + cur.executescript(sql_fh.read()) + finally: + sql_fh.close() + + def spatialite_init_file(self): + # SPATIALITE_SQL may be placed in settings to tell GeoDjango + # to use a specific path to the SpatiaLite initilization SQL. + return getattr(settings, 'SPATIALITE_SQL', + 'init_spatialite-%s.%s.sql' % + self.connection.ops.spatial_version[:2]) diff --git a/parts/django/django/contrib/gis/db/backends/spatialite/introspection.py b/parts/django/django/contrib/gis/db/backends/spatialite/introspection.py new file mode 100644 index 0000000..1b5952c --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/spatialite/introspection.py @@ -0,0 +1,51 @@ +from django.contrib.gis.gdal import OGRGeomType +from django.db.backends.sqlite3.introspection import DatabaseIntrospection, FlexibleFieldLookupDict + +class GeoFlexibleFieldLookupDict(FlexibleFieldLookupDict): + """ + Sublcass that includes updates the `base_data_types_reverse` dict + for geometry field types. + """ + base_data_types_reverse = FlexibleFieldLookupDict.base_data_types_reverse.copy() + base_data_types_reverse.update( + {'point' : 'GeometryField', + 'linestring' : 'GeometryField', + 'polygon' : 'GeometryField', + 'multipoint' : 'GeometryField', + 'multilinestring' : 'GeometryField', + 'multipolygon' : 'GeometryField', + 'geometrycollection' : 'GeometryField', + }) + +class SpatiaLiteIntrospection(DatabaseIntrospection): + data_types_reverse = GeoFlexibleFieldLookupDict() + + def get_geometry_type(self, table_name, geo_col): + cursor = self.connection.cursor() + try: + # Querying the `geometry_columns` table to get additional metadata. + cursor.execute('SELECT "coord_dimension", "srid", "type" ' + 'FROM "geometry_columns" ' + 'WHERE "f_table_name"=%s AND "f_geometry_column"=%s', + (table_name, geo_col)) + row = cursor.fetchone() + if not row: + raise Exception('Could not find a geometry column for "%s"."%s"' % + (table_name, geo_col)) + + # OGRGeomType does not require GDAL and makes it easy to convert + # from OGC geom type name to Django field. + field_type = OGRGeomType(row[2]).django + + # Getting any GeometryField keyword arguments that are not the default. + dim = row[0] + srid = row[1] + field_params = {} + if srid != 4326: + field_params['srid'] = srid + if isinstance(dim, basestring) and 'Z' in dim: + field_params['dim'] = 3 + finally: + cursor.close() + + return field_type, field_params diff --git a/parts/django/django/contrib/gis/db/backends/spatialite/models.py b/parts/django/django/contrib/gis/db/backends/spatialite/models.py new file mode 100644 index 0000000..684c5d8 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/spatialite/models.py @@ -0,0 +1,60 @@ +""" + The GeometryColumns and SpatialRefSys models for the SpatiaLite backend. +""" +from django.db import models +from django.contrib.gis.db.backends.base import SpatialRefSysMixin + +class GeometryColumns(models.Model): + """ + The 'geometry_columns' table from SpatiaLite. + """ + f_table_name = models.CharField(max_length=256) + f_geometry_column = models.CharField(max_length=256) + type = models.CharField(max_length=30) + coord_dimension = models.IntegerField() + srid = models.IntegerField(primary_key=True) + spatial_index_enabled = models.IntegerField() + + class Meta: + db_table = 'geometry_columns' + managed = False + + @classmethod + def table_name_col(cls): + """ + Returns the name of the metadata column used to store the + the feature table name. + """ + return 'f_table_name' + + @classmethod + def geom_col_name(cls): + """ + Returns the name of the metadata column used to store the + the feature geometry column. + """ + return 'f_geometry_column' + + 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 SpatiaLite. + """ + srid = models.IntegerField(primary_key=True) + auth_name = models.CharField(max_length=256) + auth_srid = models.IntegerField() + ref_sys_name = models.CharField(max_length=256) + proj4text = models.CharField(max_length=2048) + + @property + def wkt(self): + from django.contrib.gis.gdal import SpatialReference + return SpatialReference(self.proj4text).wkt + + class Meta: + db_table = 'spatial_ref_sys' + managed = False diff --git a/parts/django/django/contrib/gis/db/backends/spatialite/operations.py b/parts/django/django/contrib/gis/db/backends/spatialite/operations.py new file mode 100644 index 0000000..e6f8409 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/spatialite/operations.py @@ -0,0 +1,343 @@ +import re +from decimal import Decimal + +from django.contrib.gis.db.backends.base import BaseSpatialOperations +from django.contrib.gis.db.backends.util import SpatialOperation, SpatialFunction +from django.contrib.gis.db.backends.spatialite.adapter import SpatiaLiteAdapter +from django.contrib.gis.geometry.backend import Geometry +from django.contrib.gis.measure import Distance +from django.core.exceptions import ImproperlyConfigured +from django.db.backends.sqlite3.base import DatabaseOperations +from django.db.utils import DatabaseError + +class SpatiaLiteOperator(SpatialOperation): + "For SpatiaLite operators (e.g. `&&`, `~`)." + def __init__(self, operator): + super(SpatiaLiteOperator, self).__init__(operator=operator) + +class SpatiaLiteFunction(SpatialFunction): + "For SpatiaLite function calls." + def __init__(self, function, **kwargs): + super(SpatiaLiteFunction, self).__init__(function, **kwargs) + +class SpatiaLiteFunctionParam(SpatiaLiteFunction): + "For SpatiaLite functions that take another parameter." + sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s)' + +class SpatiaLiteDistance(SpatiaLiteFunction): + "For SpatiaLite distance operations." + dist_func = 'Distance' + sql_template = '%(function)s(%(geo_col)s, %(geometry)s) %(operator)s %%s' + + def __init__(self, operator): + super(SpatiaLiteDistance, self).__init__(self.dist_func, + operator=operator) + +class SpatiaLiteRelate(SpatiaLiteFunctionParam): + "For SpatiaLite Relate(<geom>, <pattern>) 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(SpatiaLiteRelate, self).__init__('Relate') + +# Valid distance types and substitutions +dtypes = (Decimal, Distance, float, int, long) +def get_dist_ops(operator): + "Returns operations for regular distances; spherical distances are not currently supported." + return (SpatiaLiteDistance(operator),) + +class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations): + compiler_module = 'django.contrib.gis.db.models.sql.compiler' + name = 'spatialite' + spatialite = True + version_regex = re.compile(r'^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)') + valid_aggregates = dict([(k, None) for k in ('Extent', 'Union')]) + + Adapter = SpatiaLiteAdapter + Adaptor = Adapter # Backwards-compatibility alias. + + area = 'Area' + centroid = 'Centroid' + contained = 'MbrWithin' + difference = 'Difference' + distance = 'Distance' + envelope = 'Envelope' + intersection = 'Intersection' + length = 'GLength' # OpenGis defines Length, but this conflicts with an SQLite reserved keyword + num_geom = 'NumGeometries' + num_points = 'NumPoints' + point_on_surface = 'PointOnSurface' + scale = 'ScaleCoords' + svg = 'AsSVG' + sym_difference = 'SymDifference' + transform = 'Transform' + translate = 'ShiftCoords' + union = 'GUnion' # OpenGis defines Union, but this conflicts with an SQLite reserved keyword + unionagg = 'GUnion' + + from_text = 'GeomFromText' + from_wkb = 'GeomFromWKB' + select = 'AsText(%s)' + + geometry_functions = { + 'equals' : SpatiaLiteFunction('Equals'), + 'disjoint' : SpatiaLiteFunction('Disjoint'), + 'touches' : SpatiaLiteFunction('Touches'), + 'crosses' : SpatiaLiteFunction('Crosses'), + 'within' : SpatiaLiteFunction('Within'), + 'overlaps' : SpatiaLiteFunction('Overlaps'), + 'contains' : SpatiaLiteFunction('Contains'), + 'intersects' : SpatiaLiteFunction('Intersects'), + 'relate' : (SpatiaLiteRelate, basestring), + # Retruns true if B's bounding box completely contains A's bounding box. + 'contained' : SpatiaLiteFunction('MbrWithin'), + # Returns true if A's bounding box completely contains B's bounding box. + 'bbcontains' : SpatiaLiteFunction('MbrContains'), + # Returns true if A's bounding box overlaps B's bounding box. + 'bboverlaps' : SpatiaLiteFunction('MbrOverlaps'), + # These are implemented here as synonyms for Equals + 'same_as' : SpatiaLiteFunction('Equals'), + 'exact' : SpatiaLiteFunction('Equals'), + } + + 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), + } + geometry_functions.update(distance_functions) + + def __init__(self, connection): + super(DatabaseOperations, self).__init__() + self.connection = connection + + # Determine the version of the SpatiaLite library. + try: + vtup = self.spatialite_version_tuple() + version = vtup[1:] + if version < (2, 3, 0): + raise ImproperlyConfigured('GeoDjango only supports SpatiaLite versions ' + '2.3.0 and above') + self.spatial_version = version + except ImproperlyConfigured: + raise + except Exception, msg: + raise ImproperlyConfigured('Cannot determine the SpatiaLite version for the "%s" ' + 'database (error was "%s"). Was the SpatiaLite initialization ' + 'SQL loaded on this database?' % + (self.connection.settings_dict['NAME'], msg)) + + # Creating the GIS terms dictionary. + gis_terms = ['isnull'] + gis_terms += self.geometry_functions.keys() + self.gis_terms = dict([(term, None) for term in gis_terms]) + + def check_aggregate_support(self, aggregate): + """ + Checks if the given aggregate name is supported (that is, if it's + in `self.valid_aggregates`). + """ + agg_name = aggregate.__class__.__name__ + return agg_name in self.valid_aggregates + + def convert_geom(self, wkt, geo_field): + """ + Converts geometry WKT returned from a SpatiaLite aggregate. + """ + if wkt: + return Geometry(wkt, geo_field.srid) + else: + return None + + def geo_db_type(self, f): + """ + Returns None because geometry columnas are added via the + `AddGeometryColumn` stored procedure on SpatiaLite. + """ + return None + + def get_distance(self, f, value, lookup_type): + """ + Returns the distance parameters for the given geometry field, + lookup value, and lookup type. SpatiaLite only supports regular + cartesian-based queries (no spheroid/sphere calculations for point + geometries like PostGIS). + """ + if not value: + return [] + value = value[0] + if isinstance(value, Distance): + if f.geodetic(self.connection): + raise ValueError('SpatiaLite does not support distance queries on ' + 'geometry fields with a geodetic coordinate system. ' + 'Distance objects; use a numeric value of your ' + 'distance in degrees instead.') + else: + dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection))) + else: + dist_param = value + return [dist_param] + + def get_geom_placeholder(self, f, value): + """ + Provides a proper substitution value for Geometries that are not in the + SRID of the field. Specifically, this routine will substitute in the + Transform() and GeomFromText() function call(s). + """ + def transform_value(value, srid): + return not (value is None or value.srid == srid) + if hasattr(value, 'expression'): + if transform_value(value, f.srid): + placeholder = '%s(%%s, %s)' % (self.transform, f.srid) + else: + placeholder = '%s' + # No geometry value used for F expression, substitue in + # the column name instead. + return placeholder % '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression])) + else: + if transform_value(value, f.srid): + # Adding Transform() to the SQL placeholder. + return '%s(%s(%%s,%s), %s)' % (self.transform, self.from_text, value.srid, f.srid) + else: + return '%s(%%s,%s)' % (self.from_text, f.srid) + + def _get_spatialite_func(self, func): + """ + Helper routine for calling SpatiaLite functions and returning + their result. + """ + cursor = self.connection._cursor() + try: + try: + cursor.execute('SELECT %s' % func) + row = cursor.fetchone() + except: + # Responsibility of caller to perform error handling. + raise + finally: + cursor.close() + return row[0] + + def geos_version(self): + "Returns the version of GEOS used by SpatiaLite as a string." + return self._get_spatialite_func('geos_version()') + + def proj4_version(self): + "Returns the version of the PROJ.4 library used by SpatiaLite." + return self._get_spatialite_func('proj4_version()') + + def spatialite_version(self): + "Returns the SpatiaLite library version as a string." + return self._get_spatialite_func('spatialite_version()') + + def spatialite_version_tuple(self): + """ + Returns the SpatiaLite version as a tuple (version string, major, + minor, subminor). + """ + # Getting the SpatiaLite version. + try: + version = self.spatialite_version() + except DatabaseError: + # The `spatialite_version` function first appeared in version 2.3.1 + # of SpatiaLite, so doing a fallback test for 2.3.0 (which is + # used by popular Debian/Ubuntu packages). + version = None + try: + tmp = self._get_spatialite_func("X(GeomFromText('POINT(1 1)'))") + if tmp == 1.0: version = '2.3.0' + except DatabaseError: + pass + # If no version string defined, then just re-raise the original + # exception. + if version is None: raise + + m = self.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 SpatiaLite version string: %s' % version) + + return (version, major, minor1, minor2) + + def spatial_aggregate_sql(self, agg): + """ + Returns the spatial aggregate SQL template and function for the + given Aggregate instance. + """ + agg_name = agg.__class__.__name__ + if not self.check_aggregate_support(agg): + raise NotImplementedError('%s spatial aggregate is not implmented for this backend.' % agg_name) + agg_name = agg_name.lower() + if agg_name == 'union': agg_name += 'agg' + sql_template = self.select % '%(function)s(%(field)s)' + sql_function = getattr(self, agg_name) + return sql_template, sql_function + + def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn): + """ + Returns the SpatiaLite-specific SQL for the given lookup value + [a tuple of (alias, column, db_type)], lookup type, lookup + value, the model field, and the quoting function. + """ + alias, col, db_type = lvalue + + # Getting the quoted field as `geo_col`. + geo_col = '%s.%s' % (qn(alias), qn(col)) + + if lookup_type in self.geometry_functions: + # See if a SpatiaLite geometry function matches the lookup type. + tmp = self.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 SpatiaLiteOperation 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(value, (tuple, list)): + raise ValueError('Tuple required for `%s` lookup type.' % lookup_type) + + # Geometry is first element of lookup tuple. + geom = value[0] + + # Number of valid tuple parameters depends on the lookup type. + if len(value) != 2: + raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type) + + # Ensuring the argument type matches what we expect. + if not isinstance(value[1], arg_type): + raise ValueError('Argument type should be %s, got %s instead.' % (arg_type, type(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(value[1]) + elif lookup_type in self.distance_functions: + op = op[0] + else: + op = tmp + geom = value + # Calling the `as_sql` function on the operation instance. + return op.as_sql(geo_col, self.get_geom_placeholder(field, geom)) + elif lookup_type == 'isnull': + # Handling 'isnull' lookup type + return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or '')) + + raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) + + # Routines for getting the OGC-compliant models. + def geometry_columns(self): + from django.contrib.gis.db.backends.spatialite.models import GeometryColumns + return GeometryColumns + + def spatial_ref_sys(self): + from django.contrib.gis.db.backends.spatialite.models import SpatialRefSys + return SpatialRefSys diff --git a/parts/django/django/contrib/gis/db/backends/util.py b/parts/django/django/contrib/gis/db/backends/util.py new file mode 100644 index 0000000..b50c8e2 --- /dev/null +++ b/parts/django/django/contrib/gis/db/backends/util.py @@ -0,0 +1,56 @@ +""" +A collection of utility routines and classes used by the spatial +backends. +""" + +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, unicode): val = val.encode('ascii') + return "'%s'" % val + else: + return str(val) + +class SpatialOperation(object): + """ + Base class for generating spatial SQL. + """ + sql_template = '%(geo_col)s %(operator)s %(geometry)s' + + def __init__(self, function='', operator='', result='', **kwargs): + self.function = function + self.operator = operator + self.result = result + self.extra = kwargs + + def as_sql(self, geo_col, geometry='%s'): + return self.sql_template % self.params(geo_col, geometry) + + def params(self, geo_col, geometry): + params = {'function' : self.function, + 'geo_col' : geo_col, + 'geometry' : geometry, + 'operator' : self.operator, + 'result' : self.result, + } + params.update(self.extra) + return params + +class SpatialFunction(SpatialOperation): + """ + Base class for generating spatial SQL related to a function. + """ + sql_template = '%(function)s(%(geo_col)s, %(geometry)s)' + + def __init__(self, func, result='', operator='', **kwargs): + # Getting the function prefix. + default = {'function' : func, + 'operator' : operator, + 'result' : result + } + kwargs.update(default) + super(SpatialFunction, self).__init__(**kwargs) diff --git a/parts/django/django/contrib/gis/db/models/__init__.py b/parts/django/django/contrib/gis/db/models/__init__.py new file mode 100644 index 0000000..87e2b68 --- /dev/null +++ b/parts/django/django/contrib/gis/db/models/__init__.py @@ -0,0 +1,14 @@ +# Want to get everything from the 'normal' models package. +from django.db.models import * + +# Geographic aggregate functions +from django.contrib.gis.db.models.aggregates import * + +# The GeoManager +from django.contrib.gis.db.models.manager import GeoManager + +# The geographic-enabled fields. +from django.contrib.gis.db.models.fields import \ + GeometryField, PointField, LineStringField, PolygonField, \ + MultiPointField, MultiLineStringField, MultiPolygonField, \ + GeometryCollectionField diff --git a/parts/django/django/contrib/gis/db/models/aggregates.py b/parts/django/django/contrib/gis/db/models/aggregates.py new file mode 100644 index 0000000..cd26839 --- /dev/null +++ b/parts/django/django/contrib/gis/db/models/aggregates.py @@ -0,0 +1,17 @@ +from django.db.models import Aggregate +from django.contrib.gis.db.models.sql import GeomField + +class Collect(Aggregate): + name = 'Collect' + +class Extent(Aggregate): + name = 'Extent' + +class Extent3D(Aggregate): + name = 'Extent3D' + +class MakeLine(Aggregate): + name = 'MakeLine' + +class Union(Aggregate): + name = 'Union' diff --git a/parts/django/django/contrib/gis/db/models/fields.py b/parts/django/django/contrib/gis/db/models/fields.py new file mode 100644 index 0000000..2b16607 --- /dev/null +++ b/parts/django/django/contrib/gis/db/models/fields.py @@ -0,0 +1,294 @@ +from django.db.models.fields import Field +from django.db.models.sql.expressions import SQLEvaluator +from django.utils.translation import ugettext_lazy as _ +from django.contrib.gis import forms +from django.contrib.gis.db.models.proxy import GeometryProxy +from django.contrib.gis.geometry.backend import Geometry, GeometryException + +# Local cache of the spatial_ref_sys table, which holds SRID data for each +# spatial database alias. This cache exists so that the database isn't queried +# for SRID info each time a distance query is constructed. +_srid_cache = {} + +def get_srid_info(srid, connection): + """ + Returns the units, unit name, and spheroid WKT associated with the + given SRID from the `spatial_ref_sys` (or equivalent) spatial database + table for the given database connection. These results are cached. + """ + global _srid_cache + + try: + # The SpatialRefSys model for the spatial backend. + SpatialRefSys = connection.ops.spatial_ref_sys() + except NotImplementedError: + # No `spatial_ref_sys` table in spatial backend (e.g., MySQL). + return None, None, None + + if not connection.alias in _srid_cache: + # Initialize SRID dictionary for database if it doesn't exist. + _srid_cache[connection.alias] = {} + + if not srid in _srid_cache[connection.alias]: + # Use `SpatialRefSys` model to query for spatial reference info. + sr = SpatialRefSys.objects.using(connection.alias).get(srid=srid) + units, units_name = sr.units + spheroid = SpatialRefSys.get_spheroid(sr.wkt) + _srid_cache[connection.alias][srid] = (units, units_name, spheroid) + + return _srid_cache[connection.alias][srid] + +class GeometryField(Field): + "The base GIS field -- maps to the OpenGIS Specification Geometry type." + + # The OpenGIS Geometry name. + geom_type = 'GEOMETRY' + + # Geodetic units. + geodetic_units = ('Decimal Degree', 'degree') + + description = _("The base GIS field -- maps to the OpenGIS Specification Geometry type.") + + def __init__(self, verbose_name=None, srid=4326, spatial_index=True, dim=2, + geography=False, **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. + + extent: + Customize the extent, in a 4-tuple of WGS 84 coordinates, for the + geometry field entry in the `USER_SDO_GEOM_METADATA` table. Defaults + to (-180.0, -90.0, 180.0, 90.0). + + tolerance: + Define the tolerance, in meters, to use for the geometry field + entry in the `USER_SDO_GEOM_METADATA` table. Defaults to 0.05. + """ + + # Setting the index flag with the value of the `spatial_index` keyword. + self.spatial_index = spatial_index + + # Setting the SRID and getting the units. Unit information must be + # easily available in the field instance for distance queries. + self.srid = srid + + # Setting the dimension of the geometry field. + self.dim = dim + + # Setting the verbose_name keyword argument with the positional + # first parameter, so this works like normal fields. + kwargs['verbose_name'] = verbose_name + + # Is this a geography rather than a geometry column? + self.geography = geography + + # Oracle-specific private attributes for creating the entrie in + # `USER_SDO_GEOM_METADATA` + self._extent = kwargs.pop('extent', (-180.0, -90.0, 180.0, 90.0)) + self._tolerance = kwargs.pop('tolerance', 0.05) + + super(GeometryField, self).__init__(**kwargs) + + # The following functions are used to get the units, their name, and + # the spheroid corresponding to the SRID of the GeometryField. + def _get_srid_info(self, connection): + # Get attributes from `get_srid_info`. + self._units, self._units_name, self._spheroid = get_srid_info(self.srid, connection) + + def spheroid(self, connection): + if not hasattr(self, '_spheroid'): + self._get_srid_info(connection) + return self._spheroid + + def units(self, connection): + if not hasattr(self, '_units'): + self._get_srid_info(connection) + return self._units + + def units_name(self, connection): + if not hasattr(self, '_units_name'): + self._get_srid_info(connection) + return self._units_name + + ### Routines specific to GeometryField ### + def geodetic(self, connection): + """ + Returns true if this field's SRID corresponds with a coordinate + system that uses non-projected units (e.g., latitude/longitude). + """ + return self.units_name(connection) in self.geodetic_units + + def get_distance(self, value, lookup_type, connection): + """ + 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. + """ + return connection.ops.get_distance(self, value, lookup_type) + + def get_prep_value(self, value): + """ + Spatial lookup values are either a parameter that is (or may be + converted to) a geometry, or a sequence of lookup values that + begins with a geometry. This routine will setup the geometry + value properly, and preserve any other lookup parameters before + returning to the caller. + """ + if isinstance(value, SQLEvaluator): + return value + elif isinstance(value, (tuple, list)): + geom = value[0] + seq_value = True + else: + geom = value + seq_value = False + + # When the input is not a GEOS geometry, attempt to construct one + # from the given string input. + if isinstance(geom, Geometry): + pass + elif isinstance(geom, basestring) or hasattr(geom, '__geo_interface__'): + try: + geom = Geometry(geom) + except GeometryException: + raise ValueError('Could not create geometry from lookup value.') + else: + raise ValueError('Cannot use object with type %s for a geometry lookup parameter.' % type(geom).__name__) + + # Assigning the SRID value. + geom.srid = self.get_srid(geom) + + if seq_value: + lookup_val = [geom] + lookup_val.extend(value[1:]) + return tuple(lookup_val) + else: + 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(Geometry, self)) + + def db_type(self, connection): + return connection.ops.geo_db_type(self) + + def formfield(self, **kwargs): + defaults = {'form_class' : forms.GeometryField, + 'null' : self.null, + 'geom_type' : self.geom_type, + 'srid' : self.srid, + } + defaults.update(kwargs) + return super(GeometryField, self).formfield(**defaults) + + def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False): + """ + Prepare for the database lookup, and return any spatial parameters + necessary for the query. This includes wrapping any geometry + parameters with a backend-specific adapter and formatting any distance + parameters into the correct units for the coordinate system of the + field. + """ + if lookup_type in connection.ops.gis_terms: + # special case for isnull lookup + if lookup_type == 'isnull': + return [] + + # Populating the parameters list, and wrapping the Geometry + # with the Adapter of the spatial backend. + if isinstance(value, (tuple, list)): + params = [connection.ops.Adapter(value[0])] + if lookup_type in connection.ops.distance_functions: + # Getting the distance parameter in the units of the field. + params += self.get_distance(value[1:], lookup_type, connection) + elif lookup_type in connection.ops.truncate_params: + # Lookup is one where SQL parameters aren't needed from the + # given lookup value. + pass + else: + params += value[1:] + elif isinstance(value, SQLEvaluator): + params = [] + else: + params = [connection.ops.Adapter(value)] + + return params + else: + raise ValueError('%s is not a valid spatial lookup for %s.' % + (lookup_type, self.__class__.__name__)) + + def get_prep_lookup(self, lookup_type, value): + if lookup_type == 'isnull': + return bool(value) + else: + return self.get_prep_value(value) + + def get_db_prep_save(self, value, connection): + "Prepares the value for saving in the database." + if value is None: + return None + else: + return connection.ops.Adapter(self.get_prep_value(value)) + + def get_placeholder(self, value, connection): + """ + Returns the placeholder for the geometry column for the + given value. + """ + return connection.ops.get_geom_placeholder(self, value) + +# The OpenGIS Geometry Type Fields +class PointField(GeometryField): + geom_type = 'POINT' + description = _("Point") + +class LineStringField(GeometryField): + geom_type = 'LINESTRING' + description = _("Line string") + +class PolygonField(GeometryField): + geom_type = 'POLYGON' + description = _("Polygon") + +class MultiPointField(GeometryField): + geom_type = 'MULTIPOINT' + description = _("Multi-point") + +class MultiLineStringField(GeometryField): + geom_type = 'MULTILINESTRING' + description = _("Multi-line string") + +class MultiPolygonField(GeometryField): + geom_type = 'MULTIPOLYGON' + description = _("Multi polygon") + +class GeometryCollectionField(GeometryField): + geom_type = 'GEOMETRYCOLLECTION' + description = _("Geometry collection") diff --git a/parts/django/django/contrib/gis/db/models/manager.py b/parts/django/django/contrib/gis/db/models/manager.py new file mode 100644 index 0000000..61fb821 --- /dev/null +++ b/parts/django/django/contrib/gis/db/models/manager.py @@ -0,0 +1,103 @@ +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(self.model, using=self._db) + + 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 collect(self, *args, **kwargs): + return self.get_query_set().collect(*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 extent3d(self, *args, **kwargs): + return self.get_query_set().extent3d(*args, **kwargs) + + def force_rhr(self, *args, **kwargs): + return self.get_query_set().force_rhr(*args, **kwargs) + + def geohash(self, *args, **kwargs): + return self.get_query_set().geohash(*args, **kwargs) + + def geojson(self, *args, **kwargs): + return self.get_query_set().geojson(*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 reverse_geom(self, *args, **kwargs): + return self.get_query_set().reverse_geom(*args, **kwargs) + + def scale(self, *args, **kwargs): + return self.get_query_set().scale(*args, **kwargs) + + def snap_to_grid(self, *args, **kwargs): + return self.get_query_set().snap_to_grid(*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/parts/django/django/contrib/gis/db/models/proxy.py b/parts/django/django/contrib/gis/db/models/proxy.py new file mode 100644 index 0000000..e569dd5 --- /dev/null +++ b/parts/django/django/contrib/gis/db/models/proxy.py @@ -0,0 +1,64 @@ +""" +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). +""" + +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. + """ + if obj is None: + # Accessed on a class, not an instance + return self + + # 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_type + + # 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 value is None or isinstance(value, (basestring, buffer)): + # Set with None, WKT, HEX, or WKB + 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/parts/django/django/contrib/gis/db/models/query.py b/parts/django/django/contrib/gis/db/models/query.py new file mode 100644 index 0000000..4df1a3a --- /dev/null +++ b/parts/django/django/contrib/gis/db/models/query.py @@ -0,0 +1,777 @@ +from django.db import connections +from django.db.models.query import QuerySet, Q, ValuesQuerySet, ValuesListQuerySet + +from django.contrib.gis.db.models import aggregates +from django.contrib.gis.db.models.fields import get_srid_info, GeometryField, PointField, LineStringField +from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery, GeoWhereNode +from django.contrib.gis.geometry.backend import Geometry +from django.contrib.gis.measure import Area, Distance + +class GeoQuerySet(QuerySet): + "The Geographic QuerySet." + + ### Methods overloaded from QuerySet ### + def __init__(self, model=None, query=None, using=None): + super(GeoQuerySet, self).__init__(model=model, query=query, using=using) + self.query = query or GeoQuery(self.model) + + def values(self, *fields): + return self._clone(klass=GeoValuesQuerySet, setup=True, _fields=fields) + + def values_list(self, *fields, **kwargs): + flat = kwargs.pop('flat', False) + if kwargs: + raise TypeError('Unexpected keyword arguments to values_list: %s' + % (kwargs.keys(),)) + if flat and len(fields) > 1: + raise TypeError("'flat' is not valid when values_list is called with more than one field.") + return self._clone(klass=GeoValuesListQuerySet, setup=True, flat=flat, + _fields=fields) + + ### GeoQuerySet Methods ### + 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, + } + connection = connections[self.db] + backend = connection.ops + if backend.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 backend.postgis or backend.spatialite: + if backend.geography: + # Geography fields support area calculation, returns square meters. + s['select_field'] = AreaField('sq_m') + elif not geo_field.geodetic(connection): + # Getting the area units of the geographic field. + s['select_field'] = AreaField(Area.unit_attname(geo_field.units_name(connection))) + 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 collect(self, **kwargs): + """ + Performs an aggregate collect operation on the given geometry field. + This is analagous to a union operation, but much faster because + boundaries are not dissolved. + """ + return self._spatial_aggregate(aggregates.Collect, **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). + """ + return self._spatial_aggregate(aggregates.Extent, **kwargs) + + def extent3d(self, **kwargs): + """ + Returns the aggregate extent, in 3D, of the features in the + GeoQuerySet. It is returned as a 6-tuple, comprising: + (xmin, ymin, zmin, xmax, ymax, zmax). + """ + return self._spatial_aggregate(aggregates.Extent3D, **kwargs) + + def force_rhr(self, **kwargs): + """ + Returns a modified version of the Polygon/MultiPolygon in which + all of the vertices follow the Right-Hand-Rule. By default, + this is attached as the `force_rhr` attribute on each element + of the GeoQuerySet. + """ + return self._geom_attribute('force_rhr', **kwargs) + + def geojson(self, precision=8, crs=False, bbox=False, **kwargs): + """ + Returns a GeoJSON representation of the geomtry field in a `geojson` + attribute on each element of the GeoQuerySet. + + The `crs` and `bbox` keywords may be set to True if the users wants + the coordinate reference system and the bounding box to be included + in the GeoJSON representation of the geometry. + """ + backend = connections[self.db].ops + if not backend.geojson: + raise NotImplementedError('Only PostGIS 1.3.4+ supports GeoJSON serialization.') + + if not isinstance(precision, (int, long)): + raise TypeError('Precision keyword must be set with an integer.') + + # Setting the options flag -- which depends on which version of + # PostGIS we're using. + if backend.spatial_version >= (1, 4, 0): + options = 0 + if crs and bbox: options = 3 + elif bbox: options = 1 + elif crs: options = 2 + else: + options = 0 + if crs and bbox: options = 3 + elif crs: options = 1 + elif bbox: options = 2 + s = {'desc' : 'GeoJSON', + 'procedure_args' : {'precision' : precision, 'options' : options}, + 'procedure_fmt' : '%(geo_col)s,%(precision)s,%(options)s', + } + return self._spatial_attribute('geojson', s, **kwargs) + + def geohash(self, precision=20, **kwargs): + """ + Returns a GeoHash representation of the given field in a `geohash` + attribute on each element of the GeoQuerySet. + + The `precision` keyword may be used to custom the number of + _characters_ used in the output GeoHash, the default is 20. + """ + s = {'desc' : 'GeoHash', + 'procedure_args': {'precision': precision}, + 'procedure_fmt': '%(geo_col)s,%(precision)s', + } + return self._spatial_attribute('geohash', s, **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. + """ + backend = connections[self.db].ops + s = {'desc' : 'GML', 'procedure_args' : {'precision' : precision}} + if backend.postgis: + # PostGIS AsGML() aggregate function parameter order depends on the + # version -- uggh. + if backend.spatial_version > (1, 3, 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. + """ + return self._spatial_aggregate(aggregates.MakeLine, geo_field_type=PointField, **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 reverse_geom(self, **kwargs): + """ + Reverses the coordinate order of the geometry, and attaches as a + `reverse` attribute on each element of this GeoQuerySet. + """ + s = {'select_field' : GeomField(),} + kwargs.setdefault('model_att', 'reverse_geom') + if connections[self.db].ops.oracle: + s['geo_field_type'] = LineStringField + return self._spatial_attribute('reverse', s, **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. + """ + if connections[self.db].ops.spatialite: + if z != 0.0: + raise NotImplementedError('SpatiaLite does not support 3D scaling.') + s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s', + 'procedure_args' : {'x' : x, 'y' : y}, + 'select_field' : GeomField(), + } + else: + 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 snap_to_grid(self, *args, **kwargs): + """ + Snap all points of the input geometry to the grid. How the + geometry is snapped to the grid depends on how many arguments + were given: + - 1 argument : A single size to snap both the X and Y grids to. + - 2 arguments: X and Y sizes to snap the grid to. + - 4 arguments: X, Y sizes and the X, Y origins. + """ + if False in [isinstance(arg, (float, int, long)) for arg in args]: + raise TypeError('Size argument(s) for the grid must be a float or integer values.') + + nargs = len(args) + if nargs == 1: + size = args[0] + procedure_fmt = '%(geo_col)s,%(size)s' + procedure_args = {'size' : size} + elif nargs == 2: + xsize, ysize = args + procedure_fmt = '%(geo_col)s,%(xsize)s,%(ysize)s' + procedure_args = {'xsize' : xsize, 'ysize' : ysize} + elif nargs == 4: + xsize, ysize, xorigin, yorigin = args + procedure_fmt = '%(geo_col)s,%(xorigin)s,%(yorigin)s,%(xsize)s,%(ysize)s' + procedure_args = {'xsize' : xsize, 'ysize' : ysize, + 'xorigin' : xorigin, 'yorigin' : yorigin} + else: + raise ValueError('Must provide 1, 2, or 4 arguments to `snap_to_grid`.') + + s = {'procedure_fmt' : procedure_fmt, + 'procedure_args' : procedure_args, + 'select_field' : GeomField(), + } + + return self._spatial_attribute('snap_to_grid', s, **kwargs) + + def svg(self, relative=False, precision=8, **kwargs): + """ + Returns SVG representation of the geographic field in a `svg` + attribute on each element of this GeoQuerySet. + + Keyword Arguments: + `relative` => If set to True, this will evaluate the path in + terms of relative moves (rather than absolute). + + `precision` => May be used to set the maximum number of decimal + digits used in output (defaults to 8). + """ + relative = int(bool(relative)) + if not isinstance(precision, (int, long)): + raise TypeError('SVG precision keyword argument must be an integer.') + s = {'desc' : 'SVG', + 'procedure_fmt' : '%(geo_col)s,%(rel)s,%(precision)s', + 'procedure_args' : {'rel' : relative, + 'precision' : precision, + } + } + 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. + """ + if connections[self.db].ops.spatialite: + if z != 0.0: + raise NotImplementedError('SpatiaLite does not support 3D translation.') + s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s', + 'procedure_args' : {'x' : x, 'y' : y}, + 'select_field' : GeomField(), + } + else: + 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)' % (connections[self.db].ops.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. + """ + return self._spatial_aggregate(aggregates.Union, **kwargs) + + ### Private API -- Abstracted DRY routines. ### + def _spatial_setup(self, att, desc=None, field_name=None, geo_field_type=None): + """ + Performs set up for executing the spatial function. + """ + # Does the spatial backend support this? + connection = connections[self.db] + func = getattr(connection.ops, att, False) + if desc is None: desc = att + if not func: + raise NotImplementedError('%s stored procedure not available on ' + 'the %s backend.' % + (desc, connection.ops.name)) + + # 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) + + return procedure_args, geo_field + + def _spatial_aggregate(self, aggregate, field_name=None, + geo_field_type=None, tolerance=0.05): + """ + DRY routine for calling aggregate spatial stored procedures and + returning their result to the caller of the function. + """ + # Getting the field the geographic aggregate will be called on. + geo_field = self.query._geo_field(field_name) + if not geo_field: + raise TypeError('%s aggregate only available on GeometryFields.' % aggregate.name) + + # Checking if there are any geo field type limitations on this + # aggregate (e.g. ST_Makeline only operates on PointFields). + if not geo_field_type is None and not isinstance(geo_field, geo_field_type): + raise TypeError('%s aggregate may only be called on %ss.' % (aggregate.name, geo_field_type.__name__)) + + # Getting the string expression of the field name, as this is the + # argument taken by `Aggregate` objects. + agg_col = field_name or geo_field.name + + # Adding any keyword parameters for the Aggregate object. Oracle backends + # in particular need an additional `tolerance` parameter. + agg_kwargs = {} + if connections[self.db].ops.oracle: agg_kwargs['tolerance'] = tolerance + + # Calling the QuerySet.aggregate, and returning only the value of the aggregate. + return self.aggregate(geoagg=aggregate(agg_col, **agg_kwargs))['geoagg'] + + 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', []) + + connection = connections[self.db] + backend = connection.ops + + # 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, + geo_field_type=settings.get('geo_field_type', None)) + 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_placeholder() routine to get any needed + # transformation SQL. + geom = geo_field.get_prep_value(settings['procedure_args'][name]) + params = geo_field.get_db_prep_lookup('contains', geom, connection=connection) + geom_placeholder = geo_field.get_placeholder(geom, connection) + + # Replacing the procedure format with that of any needed + # transformation SQL. + old_fmt = '%%(%s)s' % name + new_fmt = geom_placeholder % '%%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 backend.select: + self.query.custom_select[model_att] = backend.select + if connection.ops.oracle: + sel_fld.empty_strings_allowed = False + 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. + connection = connections[self.db] + geodetic = geo_field.geodetic(connection) + geography = geo_field.geography + + if geodetic: + dist_att = 'm' + else: + dist_att = Distance.unit_attname(geo_field.units_name(connection)) + + # Shortcut booleans for what distance function we're using and + # whether the geometry field is 3D. + distance = func == 'distance' + length = func == 'length' + perimeter = func == 'perimeter' + if not (distance or length or perimeter): + raise ValueError('Unknown distance function: %s' % func) + geom_3d = geo_field.dim == 3 + + # 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] + + # Getting the spatial backend operations. + backend = connection.ops + + # If the spheroid calculation is desired, either by the `spheroid` + # keyword or when 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 (backend.postgis and geodetic and + (not geography) and length): + lookup_params.append('spheroid') + lookup_params = geo_field.get_prep_value(lookup_params) + params = geo_field.get_db_prep_lookup('distance_lte', lookup_params, connection=connection) + + # The `geom_args` flag is set to true if a geometry parameter was + # passed in. + geom_args = bool(geom) + + if backend.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, connection) + geodetic = unit_name in geo_field.geodetic_units + + if backend.spatialite and geodetic: + raise ValueError('SQLite does not support linear distance calculations on geodetic coordinate systems.') + + 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)' % (backend.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. `GeomFromText` constructor is also needed + # to wrap geom placeholder for SpatiaLite. + if backend.spatialite: + procedure_fmt += ', %s(%%%%s, %s)' % (backend.from_text, self.query.transformed_srid) + else: + procedure_fmt += ', %%s' + else: + # We need to transform the geom to the srid specified in `transform()`, + # so wrapping the geometry placeholder in transformation SQL. + # SpatiaLite also needs geometry placeholder wrapped in `GeomFromText` + # constructor. + if backend.spatialite: + procedure_fmt += ', %s(%s(%%%%s, %s), %s)' % (backend.transform, backend.from_text, + geom.srid, self.query.transformed_srid) + else: + procedure_fmt += ', %s(%%%%s, %s)' % (backend.transform, self.query.transformed_srid) + else: + # `transform()` was not used on this GeoQuerySet. + procedure_fmt = '%(geo_col)s,%(geom)s' + + if not geography and 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 backend.geography: + if not isinstance(geo_field, PointField): + raise ValueError('Spherical distance calculation only supported on PointFields.') + if not str(Geometry(buffer(params[0].ewkb)).geom_type) == 'Point': + raise ValueError('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' : backend.distance_spheroid, 'spheroid' : params[1]}) + else: + procedure_args.update({'function' : backend.distance_sphere}) + elif length or perimeter: + procedure_fmt = '%(geo_col)s' + if not geography and geodetic and length: + # There's no `length_sphere`, and `length_spheroid` also + # works on 3D geometries. + procedure_fmt += ",'%(spheroid)s'" + procedure_args.update({'function' : backend.length_spheroid, 'spheroid' : params[1]}) + elif geom_3d and backend.postgis: + # Use 3D variants of perimeter and length routines on PostGIS. + if perimeter: + procedure_args.update({'function' : backend.perimeter3d}) + elif length: + procedure_args.update({'function' : backend.length3d}) + + # 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'] = [backend.Adapter(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 connections[self.db].ops.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 connections[self.db].ops.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): + """ + 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. + """ + opts = self.model._meta + if not geo_field in opts.fields: + # Is this operation going to be on a related geographic field? + # 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]) + compiler = self.query.get_compiler(self.db) + compiler.pre_sql_setup() + rel_table, rel_col = self.query.related_select_cols[self.query.related_select_fields.index(geo_field)] + return compiler._field_column(geo_field, rel_table) + elif not geo_field in opts.local_fields: + # This geographic field is inherited from another model, so we have to + # use the db table for the _parent_ model instead. + tmp_fld, parent_model, direct, m2m = opts.get_field_by_name(geo_field.name) + return self.query.get_compiler(self.db)._field_column(geo_field, parent_model._meta.db_table) + else: + return self.query.get_compiler(self.db)._field_column(geo_field) + +class GeoValuesQuerySet(ValuesQuerySet): + def __init__(self, *args, **kwargs): + super(GeoValuesQuerySet, self).__init__(*args, **kwargs) + # This flag tells `resolve_columns` to run the values through + # `convert_values`. This ensures that Geometry objects instead + # of string values are returned with `values()` or `values_list()`. + self.query.geo_values = True + +class GeoValuesListQuerySet(GeoValuesQuerySet, ValuesListQuerySet): + pass diff --git a/parts/django/django/contrib/gis/db/models/sql/__init__.py b/parts/django/django/contrib/gis/db/models/sql/__init__.py new file mode 100644 index 0000000..38d9507 --- /dev/null +++ b/parts/django/django/contrib/gis/db/models/sql/__init__.py @@ -0,0 +1,3 @@ +from django.contrib.gis.db.models.sql.conversion import AreaField, DistanceField, GeomField +from django.contrib.gis.db.models.sql.query import GeoQuery +from django.contrib.gis.db.models.sql.where import GeoWhereNode diff --git a/parts/django/django/contrib/gis/db/models/sql/aggregates.py b/parts/django/django/contrib/gis/db/models/sql/aggregates.py new file mode 100644 index 0000000..fed2a2e --- /dev/null +++ b/parts/django/django/contrib/gis/db/models/sql/aggregates.py @@ -0,0 +1,61 @@ +from django.db.models.sql.aggregates import * +from django.contrib.gis.db.models.fields import GeometryField +from django.contrib.gis.db.models.sql.conversion import GeomField + +class GeoAggregate(Aggregate): + # Default SQL template for spatial aggregates. + sql_template = '%(function)s(%(field)s)' + + # Conversion class, if necessary. + conversion_class = None + + # Flags for indicating the type of the aggregate. + is_extent = False + + def __init__(self, col, source=None, is_summary=False, tolerance=0.05, **extra): + super(GeoAggregate, self).__init__(col, source, is_summary, **extra) + + # Required by some Oracle aggregates. + self.tolerance = tolerance + + # Can't use geographic aggregates on non-geometry fields. + if not isinstance(self.source, GeometryField): + raise ValueError('Geospatial aggregates only allowed on geometry fields.') + + def as_sql(self, qn, connection): + "Return the aggregate, rendered as SQL." + + if connection.ops.oracle: + self.extra['tolerance'] = self.tolerance + + if hasattr(self.col, 'as_sql'): + field_name = self.col.as_sql(qn, connection) + elif isinstance(self.col, (list, tuple)): + field_name = '.'.join([qn(c) for c in self.col]) + else: + field_name = self.col + + sql_template, sql_function = connection.ops.spatial_aggregate_sql(self) + + params = { + 'function': sql_function, + 'field': field_name + } + params.update(self.extra) + + return sql_template % params + +class Collect(GeoAggregate): + pass + +class Extent(GeoAggregate): + is_extent = '2D' + +class Extent3D(GeoAggregate): + is_extent = '3D' + +class MakeLine(GeoAggregate): + pass + +class Union(GeoAggregate): + pass diff --git a/parts/django/django/contrib/gis/db/models/sql/compiler.py b/parts/django/django/contrib/gis/db/models/sql/compiler.py new file mode 100644 index 0000000..dea0fd3 --- /dev/null +++ b/parts/django/django/contrib/gis/db/models/sql/compiler.py @@ -0,0 +1,278 @@ +from itertools import izip +from django.db.backends.util import truncate_name +from django.db.models.sql import compiler +from django.db.models.sql.constants import TABLE_NAME +from django.db.models.sql.query import get_proxied_model + +SQLCompiler = compiler.SQLCompiler + +class GeoSQLCompiler(compiler.SQLCompiler): + + 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[0], qn2(alias)) + for alias, col in self.query.extra_select.iteritems()] + aliases = set(self.query.extra_select.keys()) + if with_aliases: + col_aliases = aliases.copy() + else: + col_aliases = set() + if self.query.select: + only_load = self.deferred_to_columns() + # This loop customized for GeoQuery. + for col, field in izip(self.query.select, self.query.select_fields): + if isinstance(col, (list, tuple)): + alias, column = col + table = self.query.alias_map[alias][TABLE_NAME] + if table in only_load and col not in only_load[table]: + continue + r = self.get_field_select(field, alias, column) + if with_aliases: + if 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('%s AS %s' % (r, qn2(col[1]))) + aliases.add(r) + col_aliases.add(col[1]) + else: + result.append(r) + aliases.add(r) + col_aliases.add(col[1]) + else: + result.append(col.as_sql(qn, self.connection)) + + if hasattr(col, 'alias'): + aliases.add(col.alias) + col_aliases.add(col.alias) + + elif self.query.default_cols: + cols, new_aliases = self.get_default_columns(with_aliases, + col_aliases) + result.extend(cols) + aliases.update(new_aliases) + + max_name_length = self.connection.ops.max_name_length() + result.extend([ + '%s%s' % ( + self.get_extra_select_format(alias) % aggregate.as_sql(qn, self.connection), + alias is not None + and ' AS %s' % qn(truncate_name(alias, max_name_length)) + or '' + ) + for alias, aggregate in self.query.aggregate_select.items() + ]) + + # This loop customized for GeoQuery. + for (table, col), field in izip(self.query.related_select_cols, self.query.related_select_fields): + r = self.get_field_select(field, table, col) + 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, local_only=False): + """ + Computes the default columns for selecting every field in the base + model. Will sometimes be called to pull in related models (e.g. via + select_related), in which case "opts" and "start_alias" will be given + to provide a starting point for the traversal. + + Returns a list of strings, quoted appropriately for use in SQL + directly, as well as a set of aliases used in the select statement (if + 'as_pairs' is True, returns a list of (alias, col_name) pairs instead + of strings as the first component and None as the second component). + + This routine is overridden from Query to handle customized selection of + geometry columns. + """ + result = [] + if opts is None: + opts = self.query.model._meta + aliases = set() + only_load = self.deferred_to_columns() + # Skip all proxy to the root proxied model + proxied_model = get_proxied_model(opts) + + if start_alias: + seen = {None: start_alias} + for field, model in opts.get_fields_with_model(): + if local_only and model is not None: + continue + if start_alias: + try: + alias = seen[model] + except KeyError: + if model is proxied_model: + alias = start_alias + else: + link_field = opts.get_ancestor_link(model) + alias = self.query.join((start_alias, model._meta.db_table, + link_field.column, model._meta.pk.column)) + seen[model] = alias + else: + # If we're starting from the base model of the queryset, the + # aliases will have already been set up in pre_sql_setup(), so + # we can save time here. + alias = self.query.included_inherited_models[model] + table = self.query.alias_map[alias][TABLE_NAME] + if table in only_load and field.column not in only_load[table]: + continue + if as_pairs: + result.append((alias, field.column)) + aliases.add(alias) + 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) + return result, aliases + + 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.query.extra_select.keys() + if self.query.aggregates: + # If we have an aggregate annotation, must extend the aliases + # so their corresponding row values are included. + aliases.extend([None for i in xrange(len(self.query.aggregates))]) + + # Have to set a starting row number offset that is used for + # determining the correct starting row index -- needed for + # doing pagination with Oracle. + rn_offset = 0 + if self.connection.ops.oracle: + if self.query.high_mark is not None or self.query.low_mark: rn_offset = 1 + index_start = rn_offset + len(aliases) + + # Converting any extra selection values (e.g., geometries and + # distance objects added by GeoQuerySet methods). + values = [self.query.convert_values(v, + self.query.extra_select_fields.get(a, None), + self.connection) + for v, a in izip(row[rn_offset:index_start], aliases)] + if self.connection.ops.oracle or getattr(self.query, 'geo_values', False): + # We resolve the rest of the columns if we're on Oracle or if + # the `geo_values` attribute is defined. + for value, field in map(None, row[index_start:], fields): + values.append(self.query.convert_values(value, field, connection=self.connection)) + else: + values.extend(row[index_start:]) + return tuple(values) + + #### Routines unique to GeoQuery #### + def get_extra_select_format(self, alias): + sel_fmt = '%s' + if alias in self.query.custom_select: + sel_fmt = sel_fmt % self.query.custom_select[alias] + return sel_fmt + + def get_field_select(self, field, alias=None, column=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`. Similarly, `column` may be used to specify the exact + column name, rather than using the `column` attribute on `field`. + """ + sel_fmt = self.get_select_format(field) + if field in self.query.custom_select: + field_sel = sel_fmt % self.query.custom_select[field] + else: + field_sel = sel_fmt % self._field_column(field, alias, column) + 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 self.connection.ops.select and hasattr(fld, 'geom_type'): + # 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 = self.connection.ops.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 and + # SpatiaLite backends. + if self.query.transformed_srid and ( self.connection.ops.oracle or + self.connection.ops.spatialite ): + sel_fmt = "'SRID=%d;'||%s" % (self.query.transformed_srid, sel_fmt) + else: + sel_fmt = '%s' + return sel_fmt + + # Private API utilities, subject to change. + def _field_column(self, field, table_alias=None, column=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 `column` is specified, it will be used instead of the value + in `field.column`. + """ + if table_alias is None: table_alias = self.query.model._meta.db_table + return "%s.%s" % (self.quote_name_unless_alias(table_alias), + self.connection.ops.quote_name(column or field.column)) + +class SQLInsertCompiler(compiler.SQLInsertCompiler, GeoSQLCompiler): + pass + +class SQLDeleteCompiler(compiler.SQLDeleteCompiler, GeoSQLCompiler): + pass + +class SQLUpdateCompiler(compiler.SQLUpdateCompiler, GeoSQLCompiler): + pass + +class SQLAggregateCompiler(compiler.SQLAggregateCompiler, GeoSQLCompiler): + pass + +class SQLDateCompiler(compiler.SQLDateCompiler, GeoSQLCompiler): + pass diff --git a/parts/django/django/contrib/gis/db/models/sql/conversion.py b/parts/django/django/contrib/gis/db/models/sql/conversion.py new file mode 100644 index 0000000..941c257 --- /dev/null +++ b/parts/django/django/contrib/gis/db/models/sql/conversion.py @@ -0,0 +1,27 @@ +""" +This module holds simple classes used by GeoQuery.convert_values +to convert geospatial values from the database. +""" + +class BaseField(object): + empty_strings_allowed = True + def get_internal_type(self): + "Overloaded method so OracleQuery.convert_values doesn't balk." + return None + +class AreaField(BaseField): + "Wrapper for Area values." + def __init__(self, area_att): + self.area_att = area_att + +class DistanceField(BaseField): + "Wrapper for Distance values." + def __init__(self, distance_att): + self.distance_att = distance_att + +class GeomField(BaseField): + """ + Wrapper for Geometry values. It is a lightweight alternative to + using GeometryField (which requires a SQL query upon instantiation). + """ + pass diff --git a/parts/django/django/contrib/gis/db/models/sql/query.py b/parts/django/django/contrib/gis/db/models/sql/query.py new file mode 100644 index 0000000..c300dcd --- /dev/null +++ b/parts/django/django/contrib/gis/db/models/sql/query.py @@ -0,0 +1,119 @@ +from django.db import connections +from django.db.models.query import sql + +from django.contrib.gis.db.models.fields import GeometryField +from django.contrib.gis.db.models.sql import aggregates as gis_aggregates +from django.contrib.gis.db.models.sql.conversion import AreaField, DistanceField, GeomField +from django.contrib.gis.db.models.sql.where import GeoWhereNode +from django.contrib.gis.geometry.backend import Geometry +from django.contrib.gis.measure import Area, Distance + + +ALL_TERMS = dict([(x, None) for x in ( + 'bbcontains', 'bboverlaps', 'contained', 'contains', + 'contains_properly', 'coveredby', 'covers', 'crosses', 'disjoint', + 'distance_gt', 'distance_gte', 'distance_lt', 'distance_lte', + 'dwithin', 'equals', 'exact', + 'intersects', 'overlaps', 'relate', 'same_as', 'touches', 'within', + 'left', 'right', 'overlaps_left', 'overlaps_right', + 'overlaps_above', 'overlaps_below', + 'strictly_above', 'strictly_below' + )]) +ALL_TERMS.update(sql.constants.QUERY_TERMS) + +class GeoQuery(sql.Query): + """ + A single spatial SQL query. + """ + # Overridding the valid query terms. + query_terms = ALL_TERMS + aggregates_module = gis_aggregates + + compiler = 'GeoSQLCompiler' + + #### Methods overridden from the base Query class #### + def __init__(self, model, where=GeoWhereNode): + super(GeoQuery, self).__init__(model, where) + # The following attributes are customized for the GeoQuerySet. + # The GeoWhereNode and SpatialBackend classes contain backend-specific + # routines and functions. + 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.custom_select = self.custom_select.copy() + obj.transformed_srid = self.transformed_srid + obj.extra_select_fields = self.extra_select_fields.copy() + return obj + + def convert_values(self, value, field, connection): + """ + Using the same routines that Oracle does we can convert our + extra selection objects into Geometry and Distance objects. + TODO: Make converted objects 'lazy' for less overhead. + """ + if connection.ops.oracle: + # Running through Oracle's first. + value = super(GeoQuery, self).convert_values(value, field or GeomField(), connection) + + if value is None: + # Output from spatial function is NULL (e.g., called + # function on a geometry field with NULL value). + pass + elif 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, GeometryField)) and value: + value = Geometry(value) + return value + + def get_aggregation(self, using): + # Remove any aggregates marked for reduction from the subquery + # and move them to the outer AggregateQuery. + connection = connections[using] + for alias, aggregate in self.aggregate_select.items(): + if isinstance(aggregate, gis_aggregates.GeoAggregate): + if not getattr(aggregate, 'is_extent', False) or connection.ops.oracle: + self.extra_select_fields[alias] = GeomField() + return super(GeoQuery, self).get_aggregation(using) + + def resolve_aggregate(self, value, aggregate, connection): + """ + Overridden from GeoQuery's normalize to handle the conversion of + GeoAggregate objects. + """ + if isinstance(aggregate, self.aggregates_module.GeoAggregate): + if aggregate.is_extent: + if aggregate.is_extent == '3D': + return connection.ops.convert_extent3d(value) + else: + return connection.ops.convert_extent(value) + else: + return connection.ops.convert_geom(value, aggregate.source) + else: + return super(GeoQuery, self).resolve_aggregate(value, aggregate, connection) + + # Private API utilities, subject to change. + 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 GeoWhereNode._check_geo_field(self.model._meta, field_name) diff --git a/parts/django/django/contrib/gis/db/models/sql/where.py b/parts/django/django/contrib/gis/db/models/sql/where.py new file mode 100644 index 0000000..17c210b --- /dev/null +++ b/parts/django/django/contrib/gis/db/models/sql/where.py @@ -0,0 +1,89 @@ +from django.db.models.fields import Field, FieldDoesNotExist +from django.db.models.sql.constants import LOOKUP_SEP +from django.db.models.sql.expressions import SQLEvaluator +from django.db.models.sql.where import Constraint, WhereNode +from django.contrib.gis.db.models.fields import GeometryField + +class GeoConstraint(Constraint): + """ + This subclass overrides `process` to better handle geographic SQL + construction. + """ + def __init__(self, init_constraint): + self.alias = init_constraint.alias + self.col = init_constraint.col + self.field = init_constraint.field + + def process(self, lookup_type, value, connection): + if isinstance(value, SQLEvaluator): + # Make sure the F Expression destination field exists, and + # set an `srid` attribute with the same as that of the + # destination. + geo_fld = GeoWhereNode._check_geo_field(value.opts, value.expression.name) + if not geo_fld: + raise ValueError('No geographic field found in expression.') + value.srid = geo_fld.srid + db_type = self.field.db_type(connection=connection) + params = self.field.get_db_prep_lookup(lookup_type, value, connection=connection) + return (self.alias, self.col, db_type), params + +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): + if isinstance(data, (list, tuple)): + obj, lookup_type, value = data + if ( isinstance(obj, Constraint) and + isinstance(obj.field, GeometryField) ): + data = (GeoConstraint(obj), lookup_type, value) + super(GeoWhereNode, self).add(data, connector) + + def make_atom(self, child, qn, connection): + lvalue, lookup_type, value_annot, params_or_value = child + if isinstance(lvalue, GeoConstraint): + data, params = lvalue.process(lookup_type, params_or_value, connection) + spatial_sql = connection.ops.spatial_lookup_sql(data, lookup_type, params_or_value, lvalue.field, qn) + return spatial_sql, params + else: + return super(GeoWhereNode, self).make_atom(child, qn, connection) + + @classmethod + def _check_geo_field(cls, opts, lookup): + """ + Utility for checking the given lookup with the given model options. + The lookup is a string either specifying the geographic field, e.g. + 'point, 'the_geom', or a related lookup on a geographic field like + 'address__point'. + + If a GeometryField exists according to the given lookup on the model + options, it will be returned. Otherwise returns None. + """ + # This takes into account the situation where the lookup is a + # lookup to a related geographic field, e.g., 'address__point'. + field_list = lookup.split(LOOKUP_SEP) + + # Reversing so list operates like a queue of related lookups, + # and popping the top lookup. + field_list.reverse() + fld_name = field_list.pop() + + try: + geo_fld = opts.get_field(fld_name) + # If the field list is still around, then it means that the + # lookup was for a geometry field across a relationship -- + # thus we keep on getting the related model options and the + # model field associated with the next field in the list + # until there's no more left. + while len(field_list): + opts = geo_fld.rel.to._meta + geo_fld = opts.get_field(field_list.pop()) + except (FieldDoesNotExist, AttributeError): + return False + + # Finally, make sure we got a Geographic field and return. + if isinstance(geo_fld, GeometryField): + return geo_fld + else: + return False diff --git a/parts/django/django/contrib/gis/feeds.py b/parts/django/django/contrib/gis/feeds.py new file mode 100644 index 0000000..4105ef7 --- /dev/null +++ b/parts/django/django/contrib/gis/feeds.py @@ -0,0 +1,135 @@ +from django.contrib.syndication.feeds import Feed as BaseFeed, FeedDoesNotExist +from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed + +class GeoFeedMixin(object): + """ + This mixin provides the necessary routines for SyndicationFeed subclasses + to produce simple GeoRSS or W3C Geo elements. + """ + + def georss_coords(self, coords): + """ + In GeoRSS coordinate pairs are ordered by lat/lon and separated by + a single white space. Given a tuple of coordinates, this will return + a unicode GeoRSS representation. + """ + return u' '.join([u'%f %f' % (coord[1], coord[0]) for coord in coords]) + + def add_georss_point(self, handler, coords, w3c_geo=False): + """ + Adds a GeoRSS point with the given coords using the given handler. + Handles the differences between simple GeoRSS and the more pouplar + W3C Geo specification. + """ + if w3c_geo: + lon, lat = coords[:2] + handler.addQuickElement(u'geo:lat', u'%f' % lat) + handler.addQuickElement(u'geo:lon', u'%f' % lon) + else: + handler.addQuickElement(u'georss:point', self.georss_coords((coords,))) + + def add_georss_element(self, handler, item, w3c_geo=False): + """ + This routine adds a GeoRSS XML element using the given item and handler. + """ + # Getting the Geometry object. + geom = item.get('geometry', None) + if not geom is None: + if isinstance(geom, (list, tuple)): + # Special case if a tuple/list was passed in. The tuple may be + # a point or a box + box_coords = None + if isinstance(geom[0], (list, tuple)): + # Box: ( (X0, Y0), (X1, Y1) ) + if len(geom) == 2: + box_coords = geom + else: + raise ValueError('Only should be two sets of coordinates.') + else: + if len(geom) == 2: + # Point: (X, Y) + self.add_georss_point(handler, geom, w3c_geo=w3c_geo) + elif len(geom) == 4: + # Box: (X0, Y0, X1, Y1) + box_coords = (geom[:2], geom[2:]) + else: + raise ValueError('Only should be 2 or 4 numeric elements.') + # If a GeoRSS box was given via tuple. + if not box_coords is None: + if w3c_geo: raise ValueError('Cannot use simple GeoRSS box in W3C Geo feeds.') + handler.addQuickElement(u'georss:box', self.georss_coords(box_coords)) + else: + # Getting the lower-case geometry type. + gtype = str(geom.geom_type).lower() + if gtype == 'point': + self.add_georss_point(handler, geom.coords, w3c_geo=w3c_geo) + else: + if w3c_geo: raise ValueError('W3C Geo only supports Point geometries.') + # For formatting consistent w/the GeoRSS simple standard: + # http://georss.org/1.0#simple + if gtype in ('linestring', 'linearring'): + handler.addQuickElement(u'georss:line', self.georss_coords(geom.coords)) + elif gtype in ('polygon',): + # Only support the exterior ring. + handler.addQuickElement(u'georss:polygon', self.georss_coords(geom[0].coords)) + else: + raise ValueError('Geometry type "%s" not supported.' % geom.geom_type) + +### SyndicationFeed subclasses ### +class GeoRSSFeed(Rss201rev2Feed, GeoFeedMixin): + def rss_attributes(self): + attrs = super(GeoRSSFeed, self).rss_attributes() + attrs[u'xmlns:georss'] = u'http://www.georss.org/georss' + return attrs + + def add_item_elements(self, handler, item): + super(GeoRSSFeed, self).add_item_elements(handler, item) + self.add_georss_element(handler, item) + + def add_root_elements(self, handler): + super(GeoRSSFeed, self).add_root_elements(handler) + self.add_georss_element(handler, self.feed) + +class GeoAtom1Feed(Atom1Feed, GeoFeedMixin): + def root_attributes(self): + attrs = super(GeoAtom1Feed, self).root_attributes() + attrs[u'xmlns:georss'] = u'http://www.georss.org/georss' + return attrs + + def add_item_elements(self, handler, item): + super(GeoAtom1Feed, self).add_item_elements(handler, item) + self.add_georss_element(handler, item) + + def add_root_elements(self, handler): + super(GeoAtom1Feed, self).add_root_elements(handler) + self.add_georss_element(handler, self.feed) + +class W3CGeoFeed(Rss201rev2Feed, GeoFeedMixin): + def rss_attributes(self): + attrs = super(W3CGeoFeed, self).rss_attributes() + attrs[u'xmlns:geo'] = u'http://www.w3.org/2003/01/geo/wgs84_pos#' + return attrs + + def add_item_elements(self, handler, item): + super(W3CGeoFeed, self).add_item_elements(handler, item) + self.add_georss_element(handler, item, w3c_geo=True) + + def add_root_elements(self, handler): + super(W3CGeoFeed, self).add_root_elements(handler) + self.add_georss_element(handler, self.feed, w3c_geo=True) + +### Feed subclass ### +class Feed(BaseFeed): + """ + This is a subclass of the `Feed` from `django.contrib.syndication`. + This allows users to define a `geometry(obj)` and/or `item_geometry(item)` + methods on their own subclasses so that geo-referenced information may + placed in the feed. + """ + feed_type = GeoRSSFeed + + def feed_extra_kwargs(self, obj): + return {'geometry' : self.__get_dynamic_attr('geometry', obj)} + + def item_extra_kwargs(self, item): + return {'geometry' : self.__get_dynamic_attr('item_geometry', item)} diff --git a/parts/django/django/contrib/gis/forms/__init__.py b/parts/django/django/contrib/gis/forms/__init__.py new file mode 100644 index 0000000..82971da --- /dev/null +++ b/parts/django/django/contrib/gis/forms/__init__.py @@ -0,0 +1,2 @@ +from django.forms import * +from django.contrib.gis.forms.fields import GeometryField diff --git a/parts/django/django/contrib/gis/forms/fields.py b/parts/django/django/contrib/gis/forms/fields.py new file mode 100644 index 0000000..f806dcb --- /dev/null +++ b/parts/django/django/contrib/gis/forms/fields.py @@ -0,0 +1,67 @@ +from django import forms +from django.utils.translation import ugettext_lazy as _ + +# While this couples the geographic forms to the GEOS library, +# it decouples from database (by not importing SpatialBackend). +from django.contrib.gis.geos import GEOSGeometry + +class GeometryField(forms.Field): + """ + This is the basic form field for a Geometry. Any textual input that is + accepted by GEOSGeometry is accepted by this form. By default, + this includes WKT, HEXEWKB, WKB (in a buffer), and GeoJSON. + """ + widget = 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.'), + 'transform_error' : _(u'An error occurred when transforming the geometry ' + 'to the SRID of the geometry form field.'), + } + + def __init__(self, **kwargs): + # Pop out attributes from the database field, or use sensible + # defaults (e.g., allow None). + self.srid = kwargs.pop('srid', None) + self.geom_type = kwargs.pop('geom_type', 'GEOMETRY') + self.null = kwargs.pop('null', True) + 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 and not self.required: + # The geometry column allows NULL and is not required. + return None + else: + raise forms.ValidationError(self.error_messages['no_geom']) + + # Trying to create a Geometry object from the form value. + try: + geom = GEOSGeometry(value) + except: + raise forms.ValidationError(self.error_messages['invalid_geom']) + + # Ensuring that the geometry is of the correct type (indicated + # using the OGC string label). + if str(geom.geom_type).upper() != self.geom_type and not self.geom_type == 'GEOMETRY': + raise forms.ValidationError(self.error_messages['invalid_geom_type']) + + # Transforming the geometry if the SRID was set. + if self.srid: + if not geom.srid: + # Should match that of the field if not given. + geom.srid = self.srid + elif self.srid != -1 and self.srid != geom.srid: + try: + geom.transform(self.srid) + except: + raise forms.ValidationError(self.error_messages['transform_error']) + + return geom diff --git a/parts/django/django/contrib/gis/gdal/LICENSE b/parts/django/django/contrib/gis/gdal/LICENSE new file mode 100644 index 0000000..30d410e --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2007-2009, Justin Bronn +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of OGRGeometry nor the names of its contributors may be used + to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/parts/django/django/contrib/gis/gdal/__init__.py b/parts/django/django/contrib/gis/gdal/__init__.py new file mode 100644 index 0000000..cc47ae9 --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/__init__.py @@ -0,0 +1,54 @@ +""" + This module houses ctypes interfaces for GDAL objects. The following GDAL + objects are supported: + + CoordTransform: Used for coordinate transformations from one spatial + reference system to another. + + Driver: Wraps an OGR data source driver. + + DataSource: Wrapper for the OGR data source object, supports + OGR-supported data sources. + + Envelope: A ctypes structure for bounding boxes (GDAL library + not required). + + OGRGeometry: Object for accessing OGR Geometry functionality. + + OGRGeomType: A class for representing the different OGR Geometry + types (GDAL library not required). + + SpatialReference: Represents OSR Spatial Reference objects. + + The GDAL library will be imported from the system path using the default + library name for the current OS. The default library path may be overridden + by setting `GDAL_LIBRARY_PATH` in your settings with the path to the GDAL C + library on your system. + + 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, GEOJSON, GDAL_VERSION + from django.contrib.gis.gdal.srs import SpatialReference, CoordTransform + from django.contrib.gis.gdal.geometries import OGRGeometry + HAS_GDAL = True +except: + HAS_GDAL, GEOJSON = False, False + +try: + from django.contrib.gis.gdal.envelope import Envelope +except ImportError: + # No ctypes, but don't raise an exception. + pass + +from django.contrib.gis.gdal.error import check_err, OGRException, OGRIndexError, SRSException +from django.contrib.gis.gdal.geomtype import OGRGeomType diff --git a/parts/django/django/contrib/gis/gdal/base.py b/parts/django/django/contrib/gis/gdal/base.py new file mode 100644 index 0000000..f9455c7 --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/base.py @@ -0,0 +1,35 @@ +from ctypes import c_void_p +from types import NoneType +from django.contrib.gis.gdal.error import GDALException + +class GDALBase(object): + """ + Base object for GDAL objects that has a pointer access property + that controls access to the underlying C pointer. + """ + # Initially the pointer is NULL. + _ptr = None + + # Default allowed pointer type. + ptr_type = c_void_p + + # Pointer access property. + def _get_ptr(self): + # Raise an exception if the pointer isn't valid don't + # want to be passing NULL pointers to routines -- + # that's very bad. + if self._ptr: return self._ptr + else: raise GDALException('GDAL %s pointer no longer valid.' % self.__class__.__name__) + + def _set_ptr(self, ptr): + # Only allow the pointer to be set with pointers of the + # compatible type or None (NULL). + if isinstance(ptr, (int, long)): + self._ptr = self.ptr_type(ptr) + elif isinstance(ptr, (self.ptr_type, NoneType)): + self._ptr = ptr + else: + raise TypeError('Incompatible pointer type') + + ptr = property(_get_ptr, _set_ptr) + diff --git a/parts/django/django/contrib/gis/gdal/datasource.py b/parts/django/django/contrib/gis/gdal/datasource.py new file mode 100644 index 0000000..7db5fd9 --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/datasource.py @@ -0,0 +1,128 @@ +""" + 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.base import GDALBase +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 import ds as capi + +# 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(GDALBase): + "Wraps an OGR Data Source object." + + #### Python 'magic' routines #### + def __init__(self, ds_input, ds_driver=False, write=False): + # 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 capi.get_driver_count(): + capi.register_all() + + if isinstance(ds_input, basestring): + # The data source driver is a void pointer. + ds_driver = Driver.ptr_type() + try: + # OGROpen will auto-detect the data source type. + ds = capi.open_ds(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, self.ptr_type) and isinstance(ds_driver, Driver.ptr_type): + 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: capi.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 = capi.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 = capi.get_layer(self._ptr, index) + else: + raise TypeError('Invalid index type: %s' % type(index)) + return Layer(l, self) + + 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)) + + @property + def layer_count(self): + "Returns the number of layers in the data source." + return capi.get_layer_count(self._ptr) + + @property + def name(self): + "Returns the name of the data source." + return capi.get_ds_name(self._ptr) diff --git a/parts/django/django/contrib/gis/gdal/driver.py b/parts/django/django/contrib/gis/gdal/driver.py new file mode 100644 index 0000000..1753db2 --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/driver.py @@ -0,0 +1,65 @@ +# prerequisites imports +from ctypes import c_void_p +from django.contrib.gis.gdal.base import GDALBase +from django.contrib.gis.gdal.error import OGRException +from django.contrib.gis.gdal.prototypes import ds as capi + +# 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(GDALBase): + "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._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 = capi.get_driver_by_name(name) + elif isinstance(dr_input, int): + self._register() + dr = capi.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 capi.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: capi.register_all() + + # Driver properties + @property + def driver_count(self): + "Returns the number of OGR data source drivers registered." + return capi.get_driver_count() diff --git a/parts/django/django/contrib/gis/gdal/envelope.py b/parts/django/django/contrib/gis/gdal/envelope.py new file mode 100644 index 0000000..0e6aa0e --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/envelope.py @@ -0,0 +1,175 @@ +""" + The GDAL/OGR library uses an Envelope structure to hold the bounding + box information for a geometry. The envelope (bounding box) contains + two pairs of coordinates, one for the lower left coordinate and one + for the upper right coordinate: + + +----------o Upper right; (max_x, max_y) + | | + | | + | | + Lower left (min_x, min_y) o----------+ +""" +from ctypes import Structure, c_double +from django.contrib.gis.gdal.error import 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], (tuple, list)): + # 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, tuple) and len(other) == 4: + return (self.min_x == other[0]) and (self.min_y == other[1]) and \ + (self.max_x == other[2]) and (self.max_y == other[3]) + else: + raise 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] + + def expand_to_include(self, *args): + """ + Modifies the envelope to expand to include the boundaries of + the passed-in 2-tuple (a point), 4-tuple (an extent) or + envelope. + """ + # We provide a number of different signatures for this method, + # and the logic here is all about converting them into a + # 4-tuple single parameter which does the actual work of + # expanding the envelope. + if len(args) == 1: + if isinstance(args[0], Envelope): + return self.expand_to_include(args[0].tuple) + elif hasattr(args[0], 'x') and hasattr(args[0], 'y'): + return self.expand_to_include(args[0].x, args[0].y, args[0].x, args[0].y) + elif isinstance(args[0], (tuple, list)): + # A tuple was passed in. + if len(args[0]) == 2: + return self.expand_to_include((args[0][0], args[0][1], args[0][0], args[0][1])) + elif len(args[0]) == 4: + (minx, miny, maxx, maxy) = args[0] + if minx < self._envelope.MinX: + self._envelope.MinX = minx + if miny < self._envelope.MinY: + self._envelope.MinY = miny + if maxx > self._envelope.MaxX: + self._envelope.MaxX = maxx + if maxy > self._envelope.MaxY: + self._envelope.MaxY = maxy + else: + raise OGRException('Incorrect number of tuple elements (%d).' % len(args[0])) + else: + raise TypeError('Incorrect type of argument: %s' % str(type(args[0]))) + elif len(args) == 2: + # An x and an y parameter were passed in + return self.expand_to_include((args[0], args[1], args[0], args[1])) + elif len(args) == 4: + # Individiual parameters passed in. + return self.expand_to_include(args) + else: + raise OGRException('Incorrect number (%d) of arguments.' % len(args[0])) + + @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/parts/django/django/contrib/gis/gdal/error.py b/parts/django/django/contrib/gis/gdal/error.py new file mode 100644 index 0000000..58ca891 --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/error.py @@ -0,0 +1,42 @@ +""" + 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 GDALException(Exception): pass +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/parts/django/django/contrib/gis/gdal/feature.py b/parts/django/django/contrib/gis/gdal/feature.py new file mode 100644 index 0000000..b5c173a --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/feature.py @@ -0,0 +1,110 @@ +# The GDAL C library, OGR exception, and the Field object +from django.contrib.gis.gdal.base import GDALBase +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 import ds as capi, geom as geom_api + +# 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(GDALBase): + "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." + 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: capi.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(capi.feature_equal(self.ptr, other._ptr)) + + #### Feature Properties #### + @property + def fid(self): + "Returns the feature identifier." + return capi.get_fid(self.ptr) + + @property + def layer_name(self): + "Returns the name of the layer for the feature." + return capi.get_feat_name(self._fdefn) + + @property + def num_fields(self): + "Returns the number of fields in the Feature." + return capi.get_feat_field_count(self.ptr) + + @property + def fields(self): + "Returns a list of fields in the Feature." + return [capi.get_field_name(capi.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 = capi.get_feat_geom_ref(self.ptr) + return OGRGeometry(geom_api.clone_geom(geom_ptr)) + + @property + def geom_type(self): + "Returns the OGR Geometry Type for this Feture." + return OGRGeomType(capi.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 = capi.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/parts/django/django/contrib/gis/gdal/field.py b/parts/django/django/contrib/gis/gdal/field.py new file mode 100644 index 0000000..46dbc86 --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/field.py @@ -0,0 +1,178 @@ +from ctypes import byref, c_int +from datetime import date, datetime, time +from django.contrib.gis.gdal.base import GDALBase +from django.contrib.gis.gdal.error import OGRException +from django.contrib.gis.gdal.prototypes import ds as capi + +# 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(GDALBase): + "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_ptr = capi.get_feat_field_defn(feat, index) + if not fld_ptr: + raise OGRException('Cannot create OGR Field, invalid pointer given.') + self.ptr = fld_ptr + + # Setting the class depending upon the OGR Field Type (OFT) + self.__class__ = OGRFieldTypes[self.type] + + # 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 capi.get_field_as_double(self._feat, self._index) + + def as_int(self): + "Retrieves the Field's value as an integer." + return capi.get_field_as_integer(self._feat, self._index) + + def as_string(self): + "Retrieves the Field's value as a string." + return capi.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 = capi.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 capi.get_field_name(self.ptr) + + @property + def precision(self): + "Returns the precision of this Field." + return capi.get_field_precision(self.ptr) + + @property + def type(self): + "Returns the OGR type of this Field." + return capi.get_field_type(self.ptr) + + @property + def type_name(self): + "Return the OGR field type name for this Field." + return capi.get_field_type_name(self.type) + + @property + def value(self): + "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 capi.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." + try: + yy, mm, dd, hh, mn, ss, tz = self.as_datetime() + return date(yy.value, mm.value, dd.value) + except (ValueError, OGRException): + return None + +class OFTDateTime(Field): + @property + def value(self): + "Returns a Python `datetime` object for this OFTDateTime field." + # 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: + yy, mm, dd, hh, mn, ss, tz = self.as_datetime() + return datetime(yy.value, mm.value, dd.value, hh.value, mn.value, ss.value) + except (ValueError, OGRException): + return None + +class OFTTime(Field): + @property + def value(self): + "Returns a Python `time` object for this OFTTime field." + try: + yy, mm, dd, hh, mn, ss, tz = self.as_datetime() + return time(hh.value, mn.value, ss.value) + except (ValueError, OGRException): + 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 and reverse mapping. +OGRFieldTypes = { 0 : OFTInteger, + 1 : OFTIntegerList, + 2 : OFTReal, + 3 : OFTRealList, + 4 : OFTString, + 5 : OFTStringList, + 6 : OFTWideString, + 7 : OFTWideStringList, + 8 : OFTBinary, + 9 : OFTDate, + 10 : OFTTime, + 11 : OFTDateTime, + } +ROGRFieldTypes = dict([(cls, num) for num, cls in OGRFieldTypes.items()]) diff --git a/parts/django/django/contrib/gis/gdal/geometries.py b/parts/django/django/contrib/gis/gdal/geometries.py new file mode 100644 index 0000000..30125d5 --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/geometries.py @@ -0,0 +1,737 @@ +""" + 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 sys +from binascii import a2b_hex +from ctypes import byref, string_at, c_char_p, c_double, c_ubyte, c_void_p + +# Getting GDAL prerequisites +from django.contrib.gis.gdal.base import GDALBase +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.libgdal import GEOJSON, GDAL_VERSION +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 import geom as capi, srs as srs_api + +# For recognizing geometry input. +from django.contrib.gis.geometry.regex import hex_regex, wkt_regex, json_regex + +# 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. + +#### OGRGeometry Class #### +class OGRGeometry(GDALBase): + "Generally encapsulates an OGR geometry." + + def __init__(self, geom_input, srs=None): + "Initializes Geometry on either WKT or an OGR pointer as input." + + 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, unicode): + # Encoding to ASCII, WKT or HEX doesn't need any more. + geom_input = geom_input.encode('ascii') + + wkt_m = wkt_regex.match(geom_input) + json_m = json_regex.match(geom_input) + if wkt_m: + if wkt_m.group('srid'): + # If there's EWKT, set the SRS w/value of the SRID. + srs = int(wkt_m.group('srid')) + 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 = capi.create_geom(OGRGeomType(wkt_m.group('type')).num) + capi.import_wkt(g, byref(c_char_p(wkt_m.group('wkt')))) + else: + g = capi.from_wkt(byref(c_char_p(wkt_m.group('wkt'))), None, byref(c_void_p())) + elif json_m: + if GEOJSON: + g = capi.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 = capi.create_geom(OGRGeomType(geom_input).num) + elif isinstance(geom_input, buffer): + # WKB was passed in + g = capi.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 = capi.create_geom(geom_input.num) + elif isinstance(geom_input, self.ptr_type): + # OGR pointer (c_void_p) was the input. + g = geom_input + else: + raise 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: capi.destroy_geom(self._ptr) + + # Pickle routines + def __getstate__(self): + srs = self.srs + if srs: + srs = srs.wkt + else: + srs = None + return str(self.wkb), srs + + def __setstate__(self, state): + wkb, srs = state + ptr = capi.from_wkb(wkb, None, byref(c_void_p()), len(wkb)) + if not ptr: raise OGRException('Invalid OGRGeometry loaded from pickled state.') + self.ptr = ptr + self.srs = srs + + @classmethod + def from_bbox(cls, bbox): + "Constructs a Polygon from a bounding box (4-tuple)." + x0, y0, x1, y1 = bbox + return OGRGeometry( 'POLYGON((%s %s, %s %s, %s %s, %s %s, %s %s))' % ( + x0, y0, x0, y1, x1, y1, x1, y0, x0, y0) ) + + ### 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?" + if isinstance(other, OGRGeometry): + return self.equals(other) + else: + return False + + def __ne__(self, other): + "Tests for inequality." + return not (self == 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 capi.get_dims(self.ptr) + + def _get_coord_dim(self): + "Returns the coordinate dimension of the Geometry." + if isinstance(self, GeometryCollection) and GDAL_VERSION < (1, 5, 2): + # On GDAL versions prior to 1.5.2, there exists a bug in which + # the coordinate dimension of geometry collections is always 2: + # http://trac.osgeo.org/gdal/ticket/2334 + # Here we workaround by returning the coordinate dimension of the + # first geometry in the collection instead. + if len(self): + return capi.get_coord_dim(capi.get_geom_ref(self.ptr, 0)) + return capi.get_coord_dim(self.ptr) + + def _set_coord_dim(self, dim): + "Sets the coordinate dimension of this Geometry." + if not dim in (2, 3): + raise ValueError('Geometry dimension must be either 2 or 3') + capi.set_coord_dim(self.ptr, dim) + + coord_dim = property(_get_coord_dim, _set_coord_dim) + + @property + def geom_count(self): + "The number of elements in this Geometry." + return capi.get_geom_count(self.ptr) + + @property + def point_count(self): + "Returns the number of Points in this Geometry." + return capi.get_point_count(self.ptr) + + @property + def num_points(self): + "Alias for `point_count` (same name method in GEOS API.)" + return self.point_count + + @property + def num_coords(self): + "Alais for `point_count`." + return self.point_count + + @property + def geom_type(self): + "Returns the Type for this Geometry." + return OGRGeomType(capi.get_geom_type(self.ptr)) + + @property + def geom_name(self): + "Returns the Name of this Geometry." + return capi.get_geom_name(self.ptr) + + @property + def area(self): + "Returns the area for a LinearRing, Polygon, or MultiPolygon; 0 otherwise." + return capi.get_area(self.ptr) + + @property + def envelope(self): + "Returns the envelope for this Geometry." + # TODO: Fix Envelope() for Point geometries. + return Envelope(capi.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 = capi.get_geom_srs(self.ptr) + return SpatialReference(srs_api.clone_srs(srs_ptr)) + except SRSException: + return None + + def _set_srs(self, srs): + "Sets the SpatialReference for this geometry." + # Do not have to clone the `SpatialReference` object pointer because + # when it is assigned to this `OGRGeometry` it's internal OGR + # reference count is incremented, and will likewise be released + # (decremented) when this geometry's destructor is called. + if isinstance(srs, SpatialReference): + srs_ptr = srs.ptr + elif isinstance(srs, (int, long, basestring)): + sr = SpatialReference(srs) + srs_ptr = sr.ptr + else: + raise TypeError('Cannot assign spatial reference with object of type: %s' % type(srs)) + capi.assign_srs(self.ptr, srs_ptr) + + srs = property(_get_srs, _set_srs) + + # The SRID property + def _get_srid(self): + srs = self.srs + if srs: return srs.srid + return None + + def _set_srid(self, srid): + if isinstance(srid, (int, 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 capi.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): + """ + Returns the GeoJSON representation of this Geometry (requires + GDAL 1.5+). + """ + if GEOJSON: + return capi.to_json(self.ptr) + else: + raise NotImplementedError('GeoJSON output only supported on GDAL 1.5+.') + geojson = json + + @property + def kml(self): + "Returns the KML representation of the Geometry." + if GEOJSON: + return capi.to_kml(self.ptr, None) + else: + raise NotImplementedError('KML output only supported on GDAL 1.5+.') + + @property + def wkb_size(self): + "Returns the size of the WKB buffer." + return capi.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 = capi.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 capi.to_wkt(self.ptr, byref(c_char_p())) + + @property + def ewkt(self): + "Returns the EWKT representation of the Geometry." + srs = self.srs + if srs and srs.srid: + return 'SRID=%s;%s' % (srs.srid, self.wkt) + else: + return self.wkt + + #### Geometry Methods #### + def clone(self): + "Clones this OGR Geometry." + return OGRGeometry(capi.clone_geom(self.ptr), self.srs) + + def close_rings(self): + """ + If there are any rings within this geometry that have not been + closed, this routine will do so by adding the starting point at the + end. + """ + # Closing the open rings. + capi.geom_close_rings(self.ptr) + + def transform(self, coord_trans, clone=False): + """ + 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 + + # Have to get the coordinate dimension of the original geometry + # so it can be used to reset the transformed geometry's dimension + # afterwards. This is done because of GDAL bug (in versions prior + # to 1.7) that turns geometries 3D after transformation, see: + # http://trac.osgeo.org/gdal/changeset/17792 + if GDAL_VERSION < (1, 7): + orig_dim = self.coord_dim + + # Depending on the input type, use the appropriate OGR routine + # to perform the transformation. + if isinstance(coord_trans, CoordTransform): + capi.geom_transform(self.ptr, coord_trans.ptr) + elif isinstance(coord_trans, SpatialReference): + capi.geom_transform_to(self.ptr, coord_trans.ptr) + elif isinstance(coord_trans, (int, long, basestring)): + sr = SpatialReference(coord_trans) + capi.geom_transform_to(self.ptr, sr.ptr) + else: + raise TypeError('Transform only accepts CoordTransform, ' + 'SpatialReference, string, and integer objects.') + + # Setting with original dimension, see comment above. + if GDAL_VERSION < (1, 7): + if isinstance(self, GeometryCollection): + # With geometry collections have to set dimension on + # each internal geometry reference, as the collection + # dimension isn't affected. + for i in xrange(len(self)): + internal_ptr = capi.get_geom_ref(self.ptr, i) + if orig_dim != capi.get_coord_dim(internal_ptr): + capi.set_coord_dim(internal_ptr, orig_dim) + else: + if self.coord_dim != orig_dim: + self.coord_dim = orig_dim + + 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(capi.ogr_intersects, other) + + def equals(self, other): + "Returns True if this geometry is equivalent to the other." + return self._topology(capi.ogr_equals, other) + + def disjoint(self, other): + "Returns True if this geometry and the other are spatially disjoint." + return self._topology(capi.ogr_disjoint, other) + + def touches(self, other): + "Returns True if this geometry touches the other." + return self._topology(capi.ogr_touches, other) + + def crosses(self, other): + "Returns True if this geometry crosses the other." + return self._topology(capi.ogr_crosses, other) + + def within(self, other): + "Returns True if this geometry is within the other." + return self._topology(capi.ogr_within, other) + + def contains(self, other): + "Returns True if this geometry contains the other." + return self._topology(capi.ogr_contains, other) + + def overlaps(self, other): + "Returns True if this geometry overlaps the other." + return self._topology(capi.ogr_overlaps, other) + + #### Geometry-generation Methods #### + def _geomgen(self, gen_func, other=None): + "A helper routine for the OGR routines that generate geometries." + if isinstance(other, OGRGeometry): + return OGRGeometry(gen_func(self.ptr, other.ptr), self.srs) + else: + return OGRGeometry(gen_func(self.ptr), self.srs) + + @property + def boundary(self): + "Returns the boundary of this geometry." + return self._geomgen(capi.get_boundary) + + @property + def convex_hull(self): + """ + Returns the smallest convex Polygon that contains all the points in + this Geometry. + """ + return self._geomgen(capi.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(capi.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(capi.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(capi.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(capi.geom_union, other) + +# The subclasses for OGR Geometry. +class Point(OGRGeometry): + + @property + def x(self): + "Returns the X coordinate for this Point." + return capi.getx(self.ptr, 0) + + @property + def y(self): + "Returns the Y coordinate for this Point." + return capi.gety(self.ptr, 0) + + @property + def z(self): + "Returns the Z coordinate for this Point." + if self.coord_dim == 3: + return capi.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() + capi.get_point(self.ptr, index, byref(x), byref(y), byref(z)) + dim = self.coord_dim + if dim == 1: + return (x.value,) + elif dim == 2: + return (x.value, y.value) + elif dim == 3: + return (x.value, y.value, z.value) + else: + raise 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(capi.getx) + + @property + def y(self): + "Returns the Y coordinates in a list." + return self._listarr(capi.gety) + + @property + def z(self): + "Returns the Z coordinates in a list." + if self.coord_dim == 3: + return self._listarr(capi.getz) + +# LinearRings are used in Polygons. +class LinearRing(LineString): pass + +class Polygon(OGRGeometry): + + def __len__(self): + "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(capi.clone_geom(capi.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')) + capi.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(capi.clone_geom(capi.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: capi.add_geom(self.ptr, g.ptr) + else: + capi.add_geom(self.ptr, geom.ptr) + elif isinstance(geom, basestring): + tmp = OGRGeometry(geom) + capi.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, + 1 + OGRGeomType.wkb25bit : Point, + 2 + OGRGeomType.wkb25bit : LineString, + 3 + OGRGeomType.wkb25bit : Polygon, + 4 + OGRGeomType.wkb25bit : MultiPoint, + 5 + OGRGeomType.wkb25bit : MultiLineString, + 6 + OGRGeomType.wkb25bit : MultiPolygon, + 7 + OGRGeomType.wkb25bit : GeometryCollection, + } diff --git a/parts/django/django/contrib/gis/gdal/geomtype.py b/parts/django/django/contrib/gis/gdal/geomtype.py new file mode 100644 index 0000000..3bf94d4 --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/geomtype.py @@ -0,0 +1,85 @@ +from django.contrib.gis.gdal.error import OGRException + +#### OGRGeomType #### +class OGRGeomType(object): + "Encapulates OGR Geometry Types." + + wkb25bit = -2147483648 + + # Dictionary of acceptable OGRwkbGeometryType s and their string names. + _types = {0 : 'Unknown', + 1 : 'Point', + 2 : 'LineString', + 3 : 'Polygon', + 4 : 'MultiPoint', + 5 : 'MultiLineString', + 6 : 'MultiPolygon', + 7 : 'GeometryCollection', + 100 : 'None', + 101 : 'LinearRing', + 1 + wkb25bit: 'Point25D', + 2 + wkb25bit: 'LineString25D', + 3 + wkb25bit: 'Polygon25D', + 4 + wkb25bit: 'MultiPoint25D', + 5 + wkb25bit : 'MultiLineString25D', + 6 + wkb25bit : 'MultiPolygon25D', + 7 + wkb25bit : 'GeometryCollection25D', + } + # Reverse type dictionary, keyed by 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): + type_input = type_input.lower() + if type_input == 'geometry': type_input='unknown' + num = self._str_types.get(type_input, 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.replace('25D', '') + if s in ('LinearRing', 'None'): + return None + elif s == 'Unknown': + s = 'Geometry' + return s + 'Field' diff --git a/parts/django/django/contrib/gis/gdal/layer.py b/parts/django/django/contrib/gis/gdal/layer.py new file mode 100644 index 0000000..a2163bc --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/layer.py @@ -0,0 +1,212 @@ +# Needed ctypes routines +from ctypes import c_double, byref + +# Other GDAL imports. +from django.contrib.gis.gdal.base import GDALBase +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 OGRFieldTypes +from django.contrib.gis.gdal.geomtype import OGRGeomType +from django.contrib.gis.gdal.geometries import OGRGeometry +from django.contrib.gis.gdal.srs import SpatialReference + +# GDAL ctypes function prototypes. +from django.contrib.gis.gdal.prototypes import ds as capi, geom as geom_api, srs as srs_api + +# 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(GDALBase): + "A class that wraps an OGR Layer, needs to be instantiated from a DataSource object." + + #### Python 'magic' routines #### + def __init__(self, layer_ptr, ds): + """ + Initializes on an OGR C pointer to the Layer and the `DataSource` object + that owns this layer. The `DataSource` object is required so that a + reference to it is kept with this Layer. This prevents garbage + collection of the `DataSource` while this Layer is still active. + """ + if not layer_ptr: + raise OGRException('Cannot create Layer, invalid pointer given') + self.ptr = layer_ptr + self._ds = ds + self._ldefn = capi.get_layer_defn(self._ptr) + # Does the Layer support random reading? + self._random_read = self.test_capability('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. + capi.reset_reading(self._ptr) + for i in xrange(self.num_feat): + yield Feature(capi.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(capi.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() + capi.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 capi.get_fd_name(self._ldefn) + + @property + def num_feat(self, force=1): + "Returns the number of features in the Layer." + return capi.get_feature_count(self.ptr, force) + + @property + def num_fields(self): + "Returns the number of fields in the Layer." + return capi.get_field_count(self._ldefn) + + @property + def geom_type(self): + "Returns the geometry type (OGRGeomType) of the Layer." + return OGRGeomType(capi.get_fd_geom_type(self._ldefn)) + + @property + def srs(self): + "Returns the Spatial Reference used in this Layer." + try: + ptr = capi.get_layer_srs(self.ptr) + return SpatialReference(srs_api.clone_srs(ptr)) + except SRSException: + return None + + @property + def fields(self): + """ + Returns a list of string names corresponding to each of the Fields + available in this Layer. + """ + return [capi.get_field_name(capi.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 [OGRFieldTypes[capi.get_field_type(capi.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 [capi.get_field_width(capi.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 [capi.get_field_precision(capi.get_field_defn(self._ldefn, i)) + for i in xrange(self.num_fields)] + + def _get_spatial_filter(self): + try: + return OGRGeometry(geom_api.clone_geom(capi.get_spatial_filter(self.ptr))) + except OGRException: + return None + + def _set_spatial_filter(self, filter): + if isinstance(filter, OGRGeometry): + capi.set_spatial_filter(self.ptr, filter.ptr) + elif isinstance(filter, (tuple, list)): + if not len(filter) == 4: + raise ValueError('Spatial filter list/tuple must have 4 elements.') + # Map c_double onto params -- if a bad type is passed in it + # will be caught here. + xmin, ymin, xmax, ymax = map(c_double, filter) + capi.set_spatial_filter_rect(self.ptr, xmin, ymin, xmax, ymax) + elif filter is None: + capi.set_spatial_filter(self.ptr, None) + else: + raise TypeError('Spatial filter must be either an OGRGeometry instance, a 4-tuple, or None.') + + spatial_filter = property(_get_spatial_filter, _set_spatial_filter) + + #### Layer Methods #### + def get_fields(self, field_name): + """ + 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(capi.test_capability(self.ptr, capability)) diff --git a/parts/django/django/contrib/gis/gdal/libgdal.py b/parts/django/django/contrib/gis/gdal/libgdal.py new file mode 100644 index 0000000..a7a5658 --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/libgdal.py @@ -0,0 +1,104 @@ +import os, re, 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 = ['gdal17', 'gdal16', 'gdal15'] +elif os.name == 'posix': + # *NIX library names. + lib_names = ['gdal', 'GDAL', 'gdal1.7.0', 'gdal1.6.0', 'gdal1.5.0', 'gdal1.4.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_path) + +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') + +version_regex = re.compile(r'^(?P<major>\d+)\.(?P<minor>\d+)(\.(?P<subminor>\d+))?') +def gdal_version_info(): + ver = gdal_version() + m = version_regex.match(ver) + if not m: raise OGRException('Could not parse GDAL version string "%s"' % ver) + return dict([(key, m.group(key)) for key in ('major', 'minor', 'subminor')]) + +_verinfo = gdal_version_info() +GDAL_MAJOR_VERSION = int(_verinfo['major']) +GDAL_MINOR_VERSION = int(_verinfo['minor']) +GDAL_SUBMINOR_VERSION = _verinfo['subminor'] and int(_verinfo['subminor']) +GDAL_VERSION = (GDAL_MAJOR_VERSION, GDAL_MINOR_VERSION, GDAL_SUBMINOR_VERSION) +del _verinfo + +# GeoJSON support is available only in GDAL 1.5+. +if GDAL_VERSION >= (1, 5): + GEOJSON = True +else: + GEOJSON = False + diff --git a/parts/django/django/contrib/gis/gdal/prototypes/__init__.py b/parts/django/django/contrib/gis/gdal/prototypes/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/prototypes/__init__.py diff --git a/parts/django/django/contrib/gis/gdal/prototypes/ds.py b/parts/django/django/contrib/gis/gdal/prototypes/ds.py new file mode 100644 index 0000000..44828ee --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/prototypes/ds.py @@ -0,0 +1,71 @@ +""" + 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_double, 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]) +get_spatial_filter = geom_output(lgdal.OGR_L_GetSpatialFilter, [c_void_p]) +set_spatial_filter = void_output(lgdal.OGR_L_SetSpatialFilter, [c_void_p, c_void_p], errcheck=False) +set_spatial_filter_rect = void_output(lgdal.OGR_L_SetSpatialFilterRect, [c_void_p, c_double, c_double, c_double, c_double], errcheck=False) + +### Feature Definition Routines ### +get_fd_geom_type = int_output(lgdal.OGR_FD_GetGeomType, [c_void_p]) +get_fd_name = const_string_output(lgdal.OGR_FD_GetName, [c_void_p]) +get_feat_name = const_string_output(lgdal.OGR_FD_GetName, [c_void_p]) +get_field_count = int_output(lgdal.OGR_FD_GetFieldCount, [c_void_p]) +get_field_defn = voidptr_output(lgdal.OGR_FD_GetFieldDefn, [c_void_p, c_int]) + +### Feature Routines ### +clone_feature = voidptr_output(lgdal.OGR_F_Clone, [c_void_p]) +destroy_feature = void_output(lgdal.OGR_F_Destroy, [c_void_p], errcheck=False) +feature_equal = int_output(lgdal.OGR_F_Equal, [c_void_p, c_void_p]) +get_feat_geom_ref = geom_output(lgdal.OGR_F_GetGeometryRef, [c_void_p]) +get_feat_field_count = int_output(lgdal.OGR_F_GetFieldCount, [c_void_p]) +get_feat_field_defn = voidptr_output(lgdal.OGR_F_GetFieldDefnRef, [c_void_p, c_int]) +get_fid = int_output(lgdal.OGR_F_GetFID, [c_void_p]) +get_field_as_datetime = int_output(lgdal.OGR_F_GetFieldAsDateTime, [c_void_p, c_int, c_int_p, c_int_p, c_int_p, c_int_p, c_int_p, c_int_p]) +get_field_as_double = double_output(lgdal.OGR_F_GetFieldAsDouble, [c_void_p, c_int]) +get_field_as_integer = int_output(lgdal.OGR_F_GetFieldAsInteger, [c_void_p, c_int]) +get_field_as_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/parts/django/django/contrib/gis/gdal/prototypes/errcheck.py b/parts/django/django/contrib/gis/gdal/prototypes/errcheck.py new file mode 100644 index 0000000..91858ea --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/prototypes/errcheck.py @@ -0,0 +1,127 @@ +""" + 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, long)): + 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, long)): + 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 isinstance(result, (int, long)): + result = c_void_p(result) + 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/parts/django/django/contrib/gis/gdal/prototypes/generation.py b/parts/django/django/contrib/gis/gdal/prototypes/generation.py new file mode 100644 index 0000000..1303532 --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/prototypes/generation.py @@ -0,0 +1,119 @@ +""" + 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 + +class gdal_char_p(c_char_p): + pass + +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: + # Use subclass of c_char_p so the error checking routine + # can free the memory at the pointer's address. + func.restype = gdal_char_p + else: + # Error code is returned + func.restype = c_int + + # Dynamically defining our error-checking function with the + # given offset. + def _check_str(result, func, cargs): + 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/parts/django/django/contrib/gis/gdal/prototypes/geom.py b/parts/django/django/contrib/gis/gdal/prototypes/geom.py new file mode 100644 index 0000000..e002590 --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/prototypes/geom.py @@ -0,0 +1,106 @@ +import re +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, GEOJSON +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 + +### 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) + to_kml = string_output(lgdal.OGR_G_ExportToKML, [c_void_p, c_char_p], str_result=True) +else: + from_json = False + to_json = False + to_kml = 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_dim = int_output(lgdal.OGR_G_GetCoordinateDimension, [c_void_p]) +set_coord_dim = void_output(lgdal.OGR_G_SetCoordinateDimension, [c_void_p, c_int], errcheck=False) + +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/parts/django/django/contrib/gis/gdal/prototypes/srs.py b/parts/django/django/contrib/gis/gdal/prototypes/srs.py new file mode 100644 index 0000000..411cec9 --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/prototypes/srs.py @@ -0,0 +1,72 @@ +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]) +from_user_input = void_output(std_call('OSRSetFromUserInput'), [c_void_p, c_char_p]) + +# Morphing to/from ESRI WKT. +morph_to_esri = void_output(lgdal.OSRMorphToESRI, [c_void_p]) +morph_from_esri = void_output(lgdal.OSRMorphFromESRI, [c_void_p]) + +# Identifying the EPSG +identify_epsg = void_output(lgdal.OSRAutoIdentifyEPSG, [c_void_p]) + +# Getting the angular_units, linear_units functions +linear_units = units_func(lgdal.OSRGetLinearUnits) +angular_units = units_func(lgdal.OSRGetAngularUnits) + +# For exporting to WKT, PROJ.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/parts/django/django/contrib/gis/gdal/srs.py b/parts/django/django/contrib/gis/gdal/srs.py new file mode 100644 index 0000000..95e71f1 --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/srs.py @@ -0,0 +1,337 @@ +""" + 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 ctypes import byref, c_char_p, c_int, c_void_p + +# Getting the error checking routine and exceptions +from django.contrib.gis.gdal.base import GDALBase +from django.contrib.gis.gdal.error import OGRException, SRSException +from django.contrib.gis.gdal.prototypes import srs as capi + +#### Spatial Reference class. #### +class SpatialReference(GDALBase): + """ + A wrapper for the OGRSpatialReference object. According to the GDAL Web site, + the SpatialReference object "provide[s] services to represent coordinate + systems (projections and datums) and to transform between them." + """ + + #### Python 'magic' routines #### + def __init__(self, srs_input=''): + """ + 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'). + """ + buf = c_char_p('') + srs_type = 'user' + + if isinstance(srs_input, basestring): + # Encoding to ASCII if unicode passed in. + if isinstance(srs_input, unicode): + srs_input = srs_input.encode('ascii') + try: + # If SRID is a string, e.g., '4326', then make acceptable + # as user input. + srid = int(srs_input) + srs_input = 'EPSG:%d' % srid + except ValueError: + pass + elif isinstance(srs_input, (int, long)): + # EPSG integer code was input. + srs_type = 'epsg' + elif isinstance(srs_input, self.ptr_type): + srs = srs_input + srs_type = 'ogr' + else: + raise TypeError('Invalid SRS type "%s"' % srs_type) + + if srs_type == 'ogr': + # Input is already an SRS pointer. + srs = srs_input + else: + # Creating a new SRS pointer, using the string buffer. + srs = capi.new_srs(buf) + + # If the pointer is NULL, throw an exception. + if not srs: + raise SRSException('Could not create spatial reference from: %s' % srs_input) + else: + self.ptr = srs + + # Importing from either the user input string or an integer SRID. + if srs_type == 'user': + self.import_user_input(srs_input) + elif srs_type == 'epsg': + self.import_epsg(srs_input) + + def __del__(self): + "Destroys this spatial reference." + if self._ptr: capi.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, tuple): + 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, basestring) or not isinstance(index, int): + raise TypeError + return capi.get_attr_value(self.ptr, target, index) + + def auth_name(self, target): + "Returns the authority name for the given string target node." + return capi.get_auth_name(self.ptr, target) + + def auth_code(self, target): + "Returns the authority code for the given string target node." + return capi.get_auth_code(self.ptr, target) + + def clone(self): + "Returns a clone of this SpatialReference object." + return SpatialReference(capi.clone_srs(self.ptr)) + + def from_esri(self): + "Morphs this SpatialReference from ESRI's format to EPSG." + capi.morph_from_esri(self.ptr) + + def identify_epsg(self): + """ + This method inspects the WKT of this SpatialReference, and will + add EPSG authority nodes where an EPSG identifier is applicable. + """ + capi.identify_epsg(self.ptr) + + def to_esri(self): + "Morphs this SpatialReference to ESRI's format." + capi.morph_to_esri(self.ptr) + + def validate(self): + "Checks to see if the given spatial reference is valid." + capi.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 = capi.linear_units(self.ptr, byref(c_char_p())) + return name + + @property + def linear_units(self): + "Returns the value of the linear units." + units, name = capi.linear_units(self.ptr, byref(c_char_p())) + return units + + @property + def angular_name(self): + "Returns the name of the angular units." + units, name = capi.angular_units(self.ptr, byref(c_char_p())) + return name + + @property + def angular_units(self): + "Returns the value of the angular units." + units, name = capi.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 capi.linear_units(self.ptr, byref(c_char_p())) + elif self.geographic: + return capi.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 capi.semi_major(self.ptr, byref(c_int())) + + @property + def semi_minor(self): + "Returns the Semi Minor Axis for this Spatial Reference." + return capi.semi_minor(self.ptr, byref(c_int())) + + @property + def inverse_flattening(self): + "Returns the Inverse Flattening for this Spatial Reference." + return capi.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(capi.isgeographic(self.ptr)) + + @property + def local(self): + "Returns True if this SpatialReference is local (root node is LOCAL_CS)." + return bool(capi.islocal(self.ptr)) + + @property + def projected(self): + """ + Returns True if this SpatialReference is a projected coordinate system + (root node is PROJCS). + """ + return bool(capi.isprojected(self.ptr)) + + #### Import Routines ##### + def import_epsg(self, epsg): + "Imports the Spatial Reference from the EPSG code (an integer)." + capi.from_epsg(self.ptr, epsg) + + def import_proj(self, proj): + "Imports the Spatial Reference from a PROJ.4 string." + capi.from_proj(self.ptr, proj) + + def import_user_input(self, user_input): + "Imports the Spatial Reference from the given user input string." + capi.from_user_input(self.ptr, user_input) + + def import_wkt(self, wkt): + "Imports the Spatial Reference from OGC WKT (string)" + capi.from_wkt(self.ptr, byref(c_char_p(wkt))) + + def import_xml(self, xml): + "Imports the Spatial Reference from an XML string." + capi.from_xml(self.ptr, xml) + + #### Export Properties #### + @property + def wkt(self): + "Returns the WKT representation of this Spatial Reference." + return capi.to_wkt(self.ptr, byref(c_char_p())) + + @property + def pretty_wkt(self, simplify=0): + "Returns the 'pretty' representation of the WKT." + return capi.to_pretty_wkt(self.ptr, byref(c_char_p()), simplify) + + @property + def proj(self): + "Returns the PROJ.4 representation for this Spatial Reference." + return capi.to_proj(self.ptr, byref(c_char_p())) + + @property + def proj4(self): + "Alias for proj()." + return self.proj + + @property + def xml(self, dialect=''): + "Returns the XML representation of this Spatial Reference." + return capi.to_xml(self.ptr, byref(c_char_p()), dialect) + +class CoordTransform(GDALBase): + "The coordinate system transformation object." + + def __init__(self, source, target): + "Initializes on a source and target SpatialReference objects." + if not isinstance(source, SpatialReference) or not isinstance(target, SpatialReference): + raise TypeError('source and target must be of type SpatialReference') + self.ptr = capi.new_ct(source._ptr, target._ptr) + self._srs1_name = source.name + self._srs2_name = target.name + + def __del__(self): + "Deletes this Coordinate Transformation object." + if self._ptr: capi.destroy_ct(self._ptr) + + def __str__(self): + return 'Transform from "%s" to "%s"' % (self._srs1_name, self._srs2_name) diff --git a/parts/django/django/contrib/gis/gdal/tests/__init__.py b/parts/django/django/contrib/gis/gdal/tests/__init__.py new file mode 100644 index 0000000..aada5f4 --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/tests/__init__.py @@ -0,0 +1,25 @@ +""" +Module for executing all of the GDAL tests. None +of these tests require the use of the database. +""" +from unittest import TestSuite, TextTestRunner + +# Importing the GDAL test modules. +import test_driver, test_ds, test_envelope, test_geom, test_srs + +test_suites = [test_driver.suite(), + test_ds.suite(), + test_envelope.suite(), + test_geom.suite(), + test_srs.suite(), + ] + +def suite(): + "Builds a test suite for the GDAL tests." + s = TestSuite() + map(s.addTest, test_suites) + return s + +def run(verbosity=1): + "Runs the GDAL tests." + TextTestRunner(verbosity=verbosity).run(suite()) diff --git a/parts/django/django/contrib/gis/gdal/tests/test_driver.py b/parts/django/django/contrib/gis/gdal/tests/test_driver.py new file mode 100644 index 0000000..1ff65ac --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/tests/test_driver.py @@ -0,0 +1,40 @@ +import os, os.path, unittest +from django.contrib.gis.gdal import Driver, OGRException + +valid_drivers = ('ESRI Shapefile', 'MapInfo File', 'TIGER', 'S57', 'DGN', + 'Memory', 'CSV', 'GML', 'KML') + +invalid_drivers = ('Foo baz', 'clucka', 'ESRI Shp') + +aliases = {'eSrI' : 'ESRI Shapefile', + 'TigER/linE' : 'TIGER', + 'SHAPE' : 'ESRI Shapefile', + 'sHp' : 'ESRI Shapefile', + } + +class DriverTest(unittest.TestCase): + + def test01_valid_driver(self): + "Testing valid OGR Data Source Drivers." + for d in valid_drivers: + dr = Driver(d) + self.assertEqual(d, str(dr)) + + def test02_invalid_driver(self): + "Testing invalid OGR Data Source Drivers." + for i in invalid_drivers: + self.assertRaises(OGRException, Driver, i) + + def test03_aliases(self): + "Testing driver aliases." + for alias, full_name in aliases.items(): + dr = Driver(alias) + self.assertEqual(full_name, str(dr)) + +def suite(): + s = unittest.TestSuite() + s.addTest(unittest.makeSuite(DriverTest)) + return s + +def run(verbosity=2): + unittest.TextTestRunner(verbosity=verbosity).run(suite()) diff --git a/parts/django/django/contrib/gis/gdal/tests/test_ds.py b/parts/django/django/contrib/gis/gdal/tests/test_ds.py new file mode 100644 index 0000000..e1083b2 --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/tests/test_ds.py @@ -0,0 +1,226 @@ +import os, os.path, unittest +from django.contrib.gis.gdal import DataSource, Envelope, OGRGeometry, OGRException, OGRIndexError, GDAL_VERSION +from django.contrib.gis.gdal.field import OFTReal, OFTInteger, OFTString +from django.contrib.gis.geometry.test_data import get_ds_file, TestDS + +# List of acceptable data sources. +ds_list = (TestDS('test_point', nfeat=5, nfld=3, geom='POINT', gtype=1, driver='ESRI Shapefile', + fields={'dbl' : OFTReal, 'int' : OFTInteger, 'str' : OFTString,}, + extent=(-1.35011,0.166623,-0.524093,0.824508), # Got extent from QGIS + srs_wkt='GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]', + field_values={'dbl' : [float(i) for i in range(1, 6)], 'int' : range(1, 6), 'str' : [str(i) for i in range(1, 6)]}, + fids=range(5)), + TestDS('test_vrt', ext='vrt', nfeat=3, nfld=3, geom='POINT', gtype='Point25D', driver='VRT', + fields={'POINT_X' : OFTString, 'POINT_Y' : OFTString, 'NUM' : OFTString}, # VRT uses CSV, which all types are OFTString. + extent=(1.0, 2.0, 100.0, 523.5), # Min/Max from CSV + field_values={'POINT_X' : ['1.0', '5.0', '100.0'], 'POINT_Y' : ['2.0', '23.0', '523.5'], 'NUM' : ['5', '17', '23']}, + fids=range(1,4)), + TestDS('test_poly', nfeat=3, nfld=3, geom='POLYGON', gtype=3, + driver='ESRI Shapefile', + fields={'float' : OFTReal, 'int' : OFTInteger, 'str' : OFTString,}, + extent=(-1.01513,-0.558245,0.161876,0.839637), # Got extent from QGIS + srs_wkt='GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]'), + ) + +bad_ds = (TestDS('foo'), + ) + +class DataSourceTest(unittest.TestCase): + + def test01_valid_shp(self): + "Testing valid SHP Data Source files." + + for source in ds_list: + # Loading up the data source + ds = DataSource(source.ds) + + # Making sure the layer count is what's expected (only 1 layer in a SHP file) + self.assertEqual(1, len(ds)) + + # Making sure GetName works + self.assertEqual(source.ds, ds.name) + + # Making sure the driver name matches up + self.assertEqual(source.driver, str(ds.driver)) + + # Making sure indexing works + try: + ds[len(ds)] + except OGRIndexError: + pass + else: + self.fail('Expected an IndexError!') + + def test02_invalid_shp(self): + "Testing invalid SHP files for the Data Source." + for source in bad_ds: + self.assertRaises(OGRException, DataSource, source.ds) + + def test03a_layers(self): + "Testing Data Source Layers." + print "\nBEGIN - expecting out of range feature id error; safe to ignore.\n" + for source in ds_list: + ds = DataSource(source.ds) + + # Incrementing through each layer, this tests DataSource.__iter__ + for layer in ds: + # Making sure we get the number of features we expect + self.assertEqual(len(layer), source.nfeat) + + # Making sure we get the number of fields we expect + self.assertEqual(source.nfld, layer.num_fields) + self.assertEqual(source.nfld, len(layer.fields)) + + # Testing the layer's extent (an Envelope), and it's properties + if source.driver == 'VRT' and (GDAL_VERSION > (1, 7, 0) and GDAL_VERSION < (1, 7, 3)): + # There's a known GDAL regression with retrieving the extent + # of a VRT layer in versions 1.7.0-1.7.2: + # http://trac.osgeo.org/gdal/ticket/3783 + pass + else: + self.assertEqual(True, isinstance(layer.extent, Envelope)) + self.assertAlmostEqual(source.extent[0], layer.extent.min_x, 5) + self.assertAlmostEqual(source.extent[1], layer.extent.min_y, 5) + self.assertAlmostEqual(source.extent[2], layer.extent.max_x, 5) + self.assertAlmostEqual(source.extent[3], layer.extent.max_y, 5) + + # Now checking the field names. + flds = layer.fields + for f in flds: self.assertEqual(True, f in source.fields) + + # Negative FIDs are not allowed. + self.assertRaises(OGRIndexError, layer.__getitem__, -1) + self.assertRaises(OGRIndexError, layer.__getitem__, 50000) + + if hasattr(source, 'field_values'): + fld_names = source.field_values.keys() + + # Testing `Layer.get_fields` (which uses Layer.__iter__) + for fld_name in fld_names: + self.assertEqual(source.field_values[fld_name], layer.get_fields(fld_name)) + + # Testing `Layer.__getitem__`. + for i, fid in enumerate(source.fids): + feat = layer[fid] + self.assertEqual(fid, feat.fid) + # Maybe this should be in the test below, but we might as well test + # the feature values here while in this loop. + for fld_name in fld_names: + self.assertEqual(source.field_values[fld_name][i], feat.get(fld_name)) + print "\nEND - expecting out of range feature id error; safe to ignore." + + def test03b_layer_slice(self): + "Test indexing and slicing on Layers." + # Using the first data-source because the same slice + # can be used for both the layer and the control values. + source = ds_list[0] + ds = DataSource(source.ds) + + sl = slice(1, 3) + feats = ds[0][sl] + + for fld_name in ds[0].fields: + test_vals = [feat.get(fld_name) for feat in feats] + control_vals = source.field_values[fld_name][sl] + self.assertEqual(control_vals, test_vals) + + def test03c_layer_references(self): + "Test to make sure Layer access is still available without the DataSource." + source = ds_list[0] + + # See ticket #9448. + def get_layer(): + # This DataSource object is not accessible outside this + # scope. However, a reference should still be kept alive + # on the `Layer` returned. + ds = DataSource(source.ds) + return ds[0] + + # Making sure we can call OGR routines on the Layer returned. + lyr = get_layer() + self.assertEqual(source.nfeat, len(lyr)) + self.assertEqual(source.gtype, lyr.geom_type.num) + + def test04_features(self): + "Testing Data Source Features." + for source in ds_list: + ds = DataSource(source.ds) + + # Incrementing through each layer + for layer in ds: + # Incrementing through each feature in the layer + for feat in layer: + # Making sure the number of fields, and the geometry type + # are what's expected. + self.assertEqual(source.nfld, len(list(feat))) + self.assertEqual(source.gtype, feat.geom_type) + + # Making sure the fields match to an appropriate OFT type. + for k, v in source.fields.items(): + # Making sure we get the proper OGR Field instance, using + # a string value index for the feature. + self.assertEqual(True, isinstance(feat[k], v)) + + # Testing Feature.__iter__ + for fld in feat: self.assertEqual(True, fld.name in source.fields.keys()) + + def test05_geometries(self): + "Testing Geometries from Data Source Features." + for source in ds_list: + ds = DataSource(source.ds) + + # Incrementing through each layer and feature. + for layer in ds: + for feat in layer: + g = feat.geom + + # Making sure we get the right Geometry name & type + self.assertEqual(source.geom, g.geom_name) + self.assertEqual(source.gtype, g.geom_type) + + # Making sure the SpatialReference is as expected. + if hasattr(source, 'srs_wkt'): + self.assertEqual(source.srs_wkt, g.srs.wkt) + + def test06_spatial_filter(self): + "Testing the Layer.spatial_filter property." + ds = DataSource(get_ds_file('cities', 'shp')) + lyr = ds[0] + + # When not set, it should be None. + self.assertEqual(None, lyr.spatial_filter) + + # Must be set a/an OGRGeometry or 4-tuple. + self.assertRaises(TypeError, lyr._set_spatial_filter, 'foo') + + # Setting the spatial filter with a tuple/list with the extent of + # a buffer centering around Pueblo. + self.assertRaises(ValueError, lyr._set_spatial_filter, range(5)) + filter_extent = (-105.609252, 37.255001, -103.609252, 39.255001) + lyr.spatial_filter = (-105.609252, 37.255001, -103.609252, 39.255001) + self.assertEqual(OGRGeometry.from_bbox(filter_extent), lyr.spatial_filter) + feats = [feat for feat in lyr] + self.assertEqual(1, len(feats)) + self.assertEqual('Pueblo', feats[0].get('Name')) + + # Setting the spatial filter with an OGRGeometry for buffer centering + # around Houston. + filter_geom = OGRGeometry('POLYGON((-96.363151 28.763374,-94.363151 28.763374,-94.363151 30.763374,-96.363151 30.763374,-96.363151 28.763374))') + lyr.spatial_filter = filter_geom + self.assertEqual(filter_geom, lyr.spatial_filter) + feats = [feat for feat in lyr] + self.assertEqual(1, len(feats)) + self.assertEqual('Houston', feats[0].get('Name')) + + # Clearing the spatial filter by setting it to None. Now + # should indicate that there are 3 features in the Layer. + lyr.spatial_filter = None + self.assertEqual(3, len(lyr)) + +def suite(): + s = unittest.TestSuite() + s.addTest(unittest.makeSuite(DataSourceTest)) + return s + +def run(verbosity=2): + unittest.TextTestRunner(verbosity=verbosity).run(suite()) diff --git a/parts/django/django/contrib/gis/gdal/tests/test_envelope.py b/parts/django/django/contrib/gis/gdal/tests/test_envelope.py new file mode 100644 index 0000000..f181fa2 --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/tests/test_envelope.py @@ -0,0 +1,94 @@ +import unittest +from django.contrib.gis.gdal import Envelope, OGRException + +class TestPoint(object): + def __init__(self, x, y): + self.x = x + self.y = y + +class EnvelopeTest(unittest.TestCase): + + def setUp(self): + self.e = Envelope(0, 0, 5, 5) + + def test01_init(self): + "Testing Envelope initilization." + e1 = Envelope((0, 0, 5, 5)) + e2 = Envelope(0, 0, 5, 5) + e3 = Envelope(0, '0', '5', 5) # Thanks to ww for this + e4 = Envelope(e1._envelope) + self.assertRaises(OGRException, Envelope, (5, 5, 0, 0)) + self.assertRaises(OGRException, Envelope, 5, 5, 0, 0) + self.assertRaises(OGRException, Envelope, (0, 0, 5, 5, 3)) + self.assertRaises(OGRException, Envelope, ()) + self.assertRaises(ValueError, Envelope, 0, 'a', 5, 5) + self.assertRaises(TypeError, Envelope, u'foo') + self.assertRaises(OGRException, Envelope, (1, 1, 0, 0)) + try: + Envelope(0, 0, 0, 0) + except OGRException: + self.fail("shouldn't raise an exception for min_x == max_x or min_y == max_y") + + def test02_properties(self): + "Testing Envelope properties." + e = Envelope(0, 0, 2, 3) + self.assertEqual(0, e.min_x) + self.assertEqual(0, e.min_y) + self.assertEqual(2, e.max_x) + self.assertEqual(3, e.max_y) + self.assertEqual((0, 0), e.ll) + self.assertEqual((2, 3), e.ur) + self.assertEqual((0, 0, 2, 3), e.tuple) + self.assertEqual('POLYGON((0.0 0.0,0.0 3.0,2.0 3.0,2.0 0.0,0.0 0.0))', e.wkt) + self.assertEqual('(0.0, 0.0, 2.0, 3.0)', str(e)) + + def test03_equivalence(self): + "Testing Envelope equivalence." + e1 = Envelope(0.523, 0.217, 253.23, 523.69) + e2 = Envelope((0.523, 0.217, 253.23, 523.69)) + self.assertEqual(e1, e2) + self.assertEqual((0.523, 0.217, 253.23, 523.69), e1) + + def test04_expand_to_include_pt_2_params(self): + "Testing Envelope expand_to_include -- point as two parameters." + self.e.expand_to_include(2, 6) + self.assertEqual((0, 0, 5, 6), self.e) + self.e.expand_to_include(-1, -1) + self.assertEqual((-1, -1, 5, 6), self.e) + + def test05_expand_to_include_pt_2_tuple(self): + "Testing Envelope expand_to_include -- point as a single 2-tuple parameter." + self.e.expand_to_include((10, 10)) + self.assertEqual((0, 0, 10, 10), self.e) + self.e.expand_to_include((-10, -10)) + self.assertEqual((-10, -10, 10, 10), self.e) + + def test06_expand_to_include_extent_4_params(self): + "Testing Envelope expand_to_include -- extent as 4 parameters." + self.e.expand_to_include(-1, 1, 3, 7) + self.assertEqual((-1, 0, 5, 7), self.e) + + def test06_expand_to_include_extent_4_tuple(self): + "Testing Envelope expand_to_include -- extent as a single 4-tuple parameter." + self.e.expand_to_include((-1, 1, 3, 7)) + self.assertEqual((-1, 0, 5, 7), self.e) + + def test07_expand_to_include_envelope(self): + "Testing Envelope expand_to_include with Envelope as parameter." + self.e.expand_to_include(Envelope(-1, 1, 3, 7)) + self.assertEqual((-1, 0, 5, 7), self.e) + + def test08_expand_to_include_point(self): + "Testing Envelope expand_to_include with Point as parameter." + self.e.expand_to_include(TestPoint(-1, 1)) + self.assertEqual((-1, 0, 5, 5), self.e) + self.e.expand_to_include(TestPoint(10, 10)) + self.assertEqual((-1, 0, 10, 10), self.e) + +def suite(): + s = unittest.TestSuite() + s.addTest(unittest.makeSuite(EnvelopeTest)) + return s + +def run(verbosity=2): + unittest.TextTestRunner(verbosity=verbosity).run(suite()) diff --git a/parts/django/django/contrib/gis/gdal/tests/test_geom.py b/parts/django/django/contrib/gis/gdal/tests/test_geom.py new file mode 100644 index 0000000..f3d1ffb --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/tests/test_geom.py @@ -0,0 +1,490 @@ +import unittest +from django.contrib.gis.gdal import OGRGeometry, OGRGeomType, \ + OGRException, OGRIndexError, SpatialReference, CoordTransform, \ + gdal_version +from django.contrib.gis.geometry.test_data import TestDataMixin + +class OGRGeomTest(unittest.TestCase, TestDataMixin): + "This tests the OGR Geometry." + + def test00a_geomtype(self): + "Testing OGRGeomType object." + + # OGRGeomType should initialize on all these inputs. + try: + g = OGRGeomType(1) + g = OGRGeomType(7) + g = OGRGeomType('point') + g = OGRGeomType('GeometrycollectioN') + g = OGRGeomType('LINearrING') + g = OGRGeomType('Unknown') + except: + self.fail('Could not create an OGRGeomType object!') + + # Should throw TypeError on this input + self.assertRaises(OGRException, OGRGeomType, 23) + self.assertRaises(OGRException, OGRGeomType, 'fooD') + self.assertRaises(OGRException, OGRGeomType, 9) + + # Equivalence can take strings, ints, and other OGRGeomTypes + self.assertEqual(True, OGRGeomType(1) == OGRGeomType(1)) + self.assertEqual(True, OGRGeomType(7) == 'GeometryCollection') + self.assertEqual(True, OGRGeomType('point') == 'POINT') + self.assertEqual(False, OGRGeomType('point') == 2) + self.assertEqual(True, OGRGeomType('unknown') == 0) + self.assertEqual(True, OGRGeomType(6) == 'MULtiPolyGON') + self.assertEqual(False, OGRGeomType(1) != OGRGeomType('point')) + self.assertEqual(True, OGRGeomType('POINT') != OGRGeomType(6)) + + # Testing the Django field name equivalent property. + self.assertEqual('PointField', OGRGeomType('Point').django) + self.assertEqual('GeometryField', OGRGeomType('Unknown').django) + self.assertEqual(None, OGRGeomType('none').django) + + # 'Geometry' initialization implies an unknown geometry type. + gt = OGRGeomType('Geometry') + self.assertEqual(0, gt.num) + self.assertEqual('Unknown', gt.name) + + def test00b_geomtype_25d(self): + "Testing OGRGeomType object with 25D types." + wkb25bit = OGRGeomType.wkb25bit + self.failUnless(OGRGeomType(wkb25bit + 1) == 'Point25D') + self.failUnless(OGRGeomType('MultiLineString25D') == (5 + wkb25bit)) + self.assertEqual('GeometryCollectionField', OGRGeomType('GeometryCollection25D').django) + + def test01a_wkt(self): + "Testing WKT output." + for g in self.geometries.wkt_out: + geom = OGRGeometry(g.wkt) + self.assertEqual(g.wkt, geom.wkt) + + def test01a_ewkt(self): + "Testing EWKT input/output." + for ewkt_val in ('POINT (1 2 3)', 'LINEARRING (0 0,1 1,2 1,0 0)'): + # First with ewkt output when no SRID in EWKT + self.assertEqual(ewkt_val, OGRGeometry(ewkt_val).ewkt) + # No test consumption with an SRID specified. + ewkt_val = 'SRID=4326;%s' % ewkt_val + geom = OGRGeometry(ewkt_val) + self.assertEqual(ewkt_val, geom.ewkt) + self.assertEqual(4326, geom.srs.srid) + + def test01b_gml(self): + "Testing GML output." + for g in self.geometries.wkt_out: + geom = OGRGeometry(g.wkt) + self.assertEqual(g.gml, geom.gml) + + def test01c_hex(self): + "Testing HEX input/output." + for g in self.geometries.hex_wkt: + geom1 = OGRGeometry(g.wkt) + self.assertEqual(g.hex, geom1.hex) + # Constructing w/HEX + geom2 = OGRGeometry(g.hex) + self.assertEqual(geom1, geom2) + + def test01d_wkb(self): + "Testing WKB input/output." + from binascii import b2a_hex + for g in self.geometries.hex_wkt: + geom1 = OGRGeometry(g.wkt) + wkb = geom1.wkb + self.assertEqual(b2a_hex(wkb).upper(), g.hex) + # Constructing w/WKB. + geom2 = OGRGeometry(wkb) + self.assertEqual(geom1, geom2) + + def test01e_json(self): + "Testing GeoJSON input/output." + from django.contrib.gis.gdal.prototypes.geom import GEOJSON + if not GEOJSON: return + for g in self.geometries.json_geoms: + geom = OGRGeometry(g.wkt) + if not hasattr(g, 'not_equal'): + self.assertEqual(g.json, geom.json) + self.assertEqual(g.json, geom.geojson) + self.assertEqual(OGRGeometry(g.wkt), OGRGeometry(geom.json)) + + def test02_points(self): + "Testing Point objects." + + prev = OGRGeometry('POINT(0 0)') + for p in self.geometries.points: + if not hasattr(p, 'z'): # No 3D + pnt = OGRGeometry(p.wkt) + self.assertEqual(1, pnt.geom_type) + self.assertEqual('POINT', pnt.geom_name) + self.assertEqual(p.x, pnt.x) + self.assertEqual(p.y, pnt.y) + self.assertEqual((p.x, p.y), pnt.tuple) + + def test03_multipoints(self): + "Testing MultiPoint objects." + for mp in self.geometries.multipoints: + mgeom1 = OGRGeometry(mp.wkt) # First one from WKT + self.assertEqual(4, mgeom1.geom_type) + self.assertEqual('MULTIPOINT', mgeom1.geom_name) + mgeom2 = OGRGeometry('MULTIPOINT') # Creating empty multipoint + mgeom3 = OGRGeometry('MULTIPOINT') + for g in mgeom1: + mgeom2.add(g) # adding each point from the multipoints + mgeom3.add(g.wkt) # should take WKT as well + self.assertEqual(mgeom1, mgeom2) # they should equal + self.assertEqual(mgeom1, mgeom3) + self.assertEqual(mp.coords, mgeom2.coords) + self.assertEqual(mp.n_p, mgeom2.point_count) + + def test04_linestring(self): + "Testing LineString objects." + prev = OGRGeometry('POINT(0 0)') + for ls in self.geometries.linestrings: + linestr = OGRGeometry(ls.wkt) + self.assertEqual(2, linestr.geom_type) + self.assertEqual('LINESTRING', linestr.geom_name) + self.assertEqual(ls.n_p, linestr.point_count) + self.assertEqual(ls.coords, linestr.tuple) + self.assertEqual(True, linestr == OGRGeometry(ls.wkt)) + self.assertEqual(True, linestr != prev) + self.assertRaises(OGRIndexError, linestr.__getitem__, len(linestr)) + prev = linestr + + # Testing the x, y properties. + x = [tmpx for tmpx, tmpy in ls.coords] + y = [tmpy for tmpx, tmpy in ls.coords] + self.assertEqual(x, linestr.x) + self.assertEqual(y, linestr.y) + + def test05_multilinestring(self): + "Testing MultiLineString objects." + prev = OGRGeometry('POINT(0 0)') + for mls in self.geometries.multilinestrings: + mlinestr = OGRGeometry(mls.wkt) + self.assertEqual(5, mlinestr.geom_type) + self.assertEqual('MULTILINESTRING', mlinestr.geom_name) + self.assertEqual(mls.n_p, mlinestr.point_count) + self.assertEqual(mls.coords, mlinestr.tuple) + self.assertEqual(True, mlinestr == OGRGeometry(mls.wkt)) + self.assertEqual(True, mlinestr != prev) + prev = mlinestr + for ls in mlinestr: + self.assertEqual(2, ls.geom_type) + self.assertEqual('LINESTRING', ls.geom_name) + self.assertRaises(OGRIndexError, mlinestr.__getitem__, len(mlinestr)) + + def test06_linearring(self): + "Testing LinearRing objects." + prev = OGRGeometry('POINT(0 0)') + for rr in self.geometries.linearrings: + lr = OGRGeometry(rr.wkt) + #self.assertEqual(101, lr.geom_type.num) + self.assertEqual('LINEARRING', lr.geom_name) + self.assertEqual(rr.n_p, len(lr)) + self.assertEqual(True, lr == OGRGeometry(rr.wkt)) + self.assertEqual(True, lr != prev) + prev = lr + + def test07a_polygons(self): + "Testing Polygon objects." + + # Testing `from_bbox` class method + bbox = (-180,-90,180,90) + p = OGRGeometry.from_bbox( bbox ) + self.assertEqual(bbox, p.extent) + + prev = OGRGeometry('POINT(0 0)') + for p in self.geometries.polygons: + poly = OGRGeometry(p.wkt) + self.assertEqual(3, poly.geom_type) + self.assertEqual('POLYGON', poly.geom_name) + self.assertEqual(p.n_p, poly.point_count) + self.assertEqual(p.n_i + 1, len(poly)) + + # Testing area & centroid. + self.assertAlmostEqual(p.area, poly.area, 9) + x, y = poly.centroid.tuple + self.assertAlmostEqual(p.centroid[0], x, 9) + self.assertAlmostEqual(p.centroid[1], y, 9) + + # Testing equivalence + self.assertEqual(True, poly == OGRGeometry(p.wkt)) + self.assertEqual(True, poly != prev) + + if p.ext_ring_cs: + ring = poly[0] + self.assertEqual(p.ext_ring_cs, ring.tuple) + self.assertEqual(p.ext_ring_cs, poly[0].tuple) + self.assertEqual(len(p.ext_ring_cs), ring.point_count) + + for r in poly: + self.assertEqual('LINEARRING', r.geom_name) + + def test07b_closepolygons(self): + "Testing closing Polygon objects." + # Both rings in this geometry are not closed. + poly = OGRGeometry('POLYGON((0 0, 5 0, 5 5, 0 5), (1 1, 2 1, 2 2, 2 1))') + self.assertEqual(8, poly.point_count) + print "\nBEGIN - expecting IllegalArgumentException; safe to ignore.\n" + try: + c = poly.centroid + except OGRException: + # Should raise an OGR exception, rings are not closed + pass + else: + self.fail('Should have raised an OGRException!') + print "\nEND - expecting IllegalArgumentException; safe to ignore.\n" + + # Closing the rings -- doesn't work on GDAL versions 1.4.1 and below: + # http://trac.osgeo.org/gdal/ticket/1673 + major, minor1, minor2 = gdal_version().split('.') + if major == '1': + iminor1 = int(minor1) + if iminor1 < 4 or (iminor1 == 4 and minor2.startswith('1')): return + poly.close_rings() + self.assertEqual(10, poly.point_count) # Two closing points should've been added + self.assertEqual(OGRGeometry('POINT(2.5 2.5)'), poly.centroid) + + def test08_multipolygons(self): + "Testing MultiPolygon objects." + prev = OGRGeometry('POINT(0 0)') + for mp in self.geometries.multipolygons: + mpoly = OGRGeometry(mp.wkt) + self.assertEqual(6, mpoly.geom_type) + self.assertEqual('MULTIPOLYGON', mpoly.geom_name) + if mp.valid: + self.assertEqual(mp.n_p, mpoly.point_count) + self.assertEqual(mp.num_geom, len(mpoly)) + self.assertRaises(OGRIndexError, mpoly.__getitem__, len(mpoly)) + for p in mpoly: + self.assertEqual('POLYGON', p.geom_name) + self.assertEqual(3, p.geom_type) + self.assertEqual(mpoly.wkt, OGRGeometry(mp.wkt).wkt) + + def test09a_srs(self): + "Testing OGR Geometries with Spatial Reference objects." + for mp in self.geometries.multipolygons: + # Creating a geometry w/spatial reference + sr = SpatialReference('WGS84') + mpoly = OGRGeometry(mp.wkt, sr) + self.assertEqual(sr.wkt, mpoly.srs.wkt) + + # Ensuring that SRS is propagated to clones. + klone = mpoly.clone() + self.assertEqual(sr.wkt, klone.srs.wkt) + + # Ensuring all children geometries (polygons and their rings) all + # return the assigned spatial reference as well. + for poly in mpoly: + self.assertEqual(sr.wkt, poly.srs.wkt) + for ring in poly: + self.assertEqual(sr.wkt, ring.srs.wkt) + + # Ensuring SRS propagate in topological ops. + a = OGRGeometry(self.geometries.topology_geoms[0].wkt_a, sr) + b = OGRGeometry(self.geometries.topology_geoms[0].wkt_b, sr) + diff = a.difference(b) + union = a.union(b) + self.assertEqual(sr.wkt, diff.srs.wkt) + self.assertEqual(sr.srid, union.srs.srid) + + # Instantiating w/an integer SRID + mpoly = OGRGeometry(mp.wkt, 4326) + self.assertEqual(4326, mpoly.srid) + mpoly.srs = SpatialReference(4269) + self.assertEqual(4269, mpoly.srid) + self.assertEqual('NAD83', mpoly.srs.name) + + # Incrementing through the multipolyogn after the spatial reference + # has been re-assigned. + for poly in mpoly: + self.assertEqual(mpoly.srs.wkt, poly.srs.wkt) + poly.srs = 32140 + for ring in poly: + # Changing each ring in the polygon + self.assertEqual(32140, ring.srs.srid) + self.assertEqual('NAD83 / Texas South Central', ring.srs.name) + ring.srs = str(SpatialReference(4326)) # back to WGS84 + self.assertEqual(4326, ring.srs.srid) + + # Using the `srid` property. + ring.srid = 4322 + self.assertEqual('WGS 72', ring.srs.name) + self.assertEqual(4322, ring.srid) + + def test09b_srs_transform(self): + "Testing transform()." + orig = OGRGeometry('POINT (-104.609 38.255)', 4326) + trans = OGRGeometry('POINT (992385.4472045 481455.4944650)', 2774) + + # Using an srid, a SpatialReference object, and a CoordTransform object + # or transformations. + t1, t2, t3 = orig.clone(), orig.clone(), orig.clone() + t1.transform(trans.srid) + t2.transform(SpatialReference('EPSG:2774')) + ct = CoordTransform(SpatialReference('WGS84'), SpatialReference(2774)) + t3.transform(ct) + + # Testing use of the `clone` keyword. + k1 = orig.clone() + k2 = k1.transform(trans.srid, clone=True) + self.assertEqual(k1, orig) + self.assertNotEqual(k1, k2) + + prec = 3 + for p in (t1, t2, t3, k2): + self.assertAlmostEqual(trans.x, p.x, prec) + self.assertAlmostEqual(trans.y, p.y, prec) + + def test09c_transform_dim(self): + "Testing coordinate dimension is the same on transformed geometries." + ls_orig = OGRGeometry('LINESTRING(-104.609 38.255)', 4326) + ls_trans = OGRGeometry('LINESTRING(992385.4472045 481455.4944650)', 2774) + + prec = 3 + ls_orig.transform(ls_trans.srs) + # Making sure the coordinate dimension is still 2D. + self.assertEqual(2, ls_orig.coord_dim) + self.assertAlmostEqual(ls_trans.x[0], ls_orig.x[0], prec) + self.assertAlmostEqual(ls_trans.y[0], ls_orig.y[0], prec) + + def test10_difference(self): + "Testing difference()." + for i in xrange(len(self.geometries.topology_geoms)): + a = OGRGeometry(self.geometries.topology_geoms[i].wkt_a) + b = OGRGeometry(self.geometries.topology_geoms[i].wkt_b) + d1 = OGRGeometry(self.geometries.diff_geoms[i].wkt) + d2 = a.difference(b) + self.assertEqual(d1, d2) + self.assertEqual(d1, a - b) # __sub__ is difference operator + a -= b # testing __isub__ + self.assertEqual(d1, a) + + def test11_intersection(self): + "Testing intersects() and intersection()." + for i in xrange(len(self.geometries.topology_geoms)): + a = OGRGeometry(self.geometries.topology_geoms[i].wkt_a) + b = OGRGeometry(self.geometries.topology_geoms[i].wkt_b) + i1 = OGRGeometry(self.geometries.intersect_geoms[i].wkt) + self.assertEqual(True, a.intersects(b)) + i2 = a.intersection(b) + self.assertEqual(i1, i2) + self.assertEqual(i1, a & b) # __and__ is intersection operator + a &= b # testing __iand__ + self.assertEqual(i1, a) + + def test12_symdifference(self): + "Testing sym_difference()." + for i in xrange(len(self.geometries.topology_geoms)): + a = OGRGeometry(self.geometries.topology_geoms[i].wkt_a) + b = OGRGeometry(self.geometries.topology_geoms[i].wkt_b) + d1 = OGRGeometry(self.geometries.sdiff_geoms[i].wkt) + d2 = a.sym_difference(b) + self.assertEqual(d1, d2) + self.assertEqual(d1, a ^ b) # __xor__ is symmetric difference operator + a ^= b # testing __ixor__ + self.assertEqual(d1, a) + + def test13_union(self): + "Testing union()." + for i in xrange(len(self.geometries.topology_geoms)): + a = OGRGeometry(self.geometries.topology_geoms[i].wkt_a) + b = OGRGeometry(self.geometries.topology_geoms[i].wkt_b) + u1 = OGRGeometry(self.geometries.union_geoms[i].wkt) + u2 = a.union(b) + self.assertEqual(u1, u2) + self.assertEqual(u1, a | b) # __or__ is union operator + a |= b # testing __ior__ + self.assertEqual(u1, a) + + def test14_add(self): + "Testing GeometryCollection.add()." + # Can't insert a Point into a MultiPolygon. + mp = OGRGeometry('MultiPolygon') + pnt = OGRGeometry('POINT(5 23)') + self.assertRaises(OGRException, mp.add, pnt) + + # GeometryCollection.add may take an OGRGeometry (if another collection + # of the same type all child geoms will be added individually) or WKT. + for mp in self.geometries.multipolygons: + mpoly = OGRGeometry(mp.wkt) + mp1 = OGRGeometry('MultiPolygon') + mp2 = OGRGeometry('MultiPolygon') + mp3 = OGRGeometry('MultiPolygon') + + for poly in mpoly: + mp1.add(poly) # Adding a geometry at a time + mp2.add(poly.wkt) # Adding WKT + mp3.add(mpoly) # Adding a MultiPolygon's entire contents at once. + for tmp in (mp1, mp2, mp3): self.assertEqual(mpoly, tmp) + + def test15_extent(self): + "Testing `extent` property." + # The xmin, ymin, xmax, ymax of the MultiPoint should be returned. + mp = OGRGeometry('MULTIPOINT(5 23, 0 0, 10 50)') + self.assertEqual((0.0, 0.0, 10.0, 50.0), mp.extent) + # Testing on the 'real world' Polygon. + poly = OGRGeometry(self.geometries.polygons[3].wkt) + ring = poly.shell + x, y = ring.x, ring.y + xmin, ymin = min(x), min(y) + xmax, ymax = max(x), max(y) + self.assertEqual((xmin, ymin, xmax, ymax), poly.extent) + + def test16_25D(self): + "Testing 2.5D geometries." + pnt_25d = OGRGeometry('POINT(1 2 3)') + self.assertEqual('Point25D', pnt_25d.geom_type.name) + self.assertEqual(3.0, pnt_25d.z) + self.assertEqual(3, pnt_25d.coord_dim) + ls_25d = OGRGeometry('LINESTRING(1 1 1,2 2 2,3 3 3)') + self.assertEqual('LineString25D', ls_25d.geom_type.name) + self.assertEqual([1.0, 2.0, 3.0], ls_25d.z) + self.assertEqual(3, ls_25d.coord_dim) + + def test17_pickle(self): + "Testing pickle support." + import cPickle + g1 = OGRGeometry('LINESTRING(1 1 1,2 2 2,3 3 3)', 'WGS84') + g2 = cPickle.loads(cPickle.dumps(g1)) + self.assertEqual(g1, g2) + self.assertEqual(4326, g2.srs.srid) + self.assertEqual(g1.srs.wkt, g2.srs.wkt) + + def test18_ogrgeometry_transform_workaround(self): + "Testing coordinate dimensions on geometries after transformation." + # A bug in GDAL versions prior to 1.7 changes the coordinate + # dimension of a geometry after it has been transformed. + # This test ensures that the bug workarounds employed within + # `OGRGeometry.transform` indeed work. + wkt_2d = "MULTILINESTRING ((0 0,1 1,2 2))" + wkt_3d = "MULTILINESTRING ((0 0 0,1 1 1,2 2 2))" + srid = 4326 + + # For both the 2D and 3D MultiLineString, ensure _both_ the dimension + # of the collection and the component LineString have the expected + # coordinate dimension after transform. + geom = OGRGeometry(wkt_2d, srid) + geom.transform(srid) + self.assertEqual(2, geom.coord_dim) + self.assertEqual(2, geom[0].coord_dim) + self.assertEqual(wkt_2d, geom.wkt) + + geom = OGRGeometry(wkt_3d, srid) + geom.transform(srid) + self.assertEqual(3, geom.coord_dim) + self.assertEqual(3, geom[0].coord_dim) + self.assertEqual(wkt_3d, geom.wkt) + + def test19_equivalence_regression(self): + "Testing equivalence methods with non-OGRGeometry instances." + self.assertNotEqual(None, OGRGeometry('POINT(0 0)')) + self.assertEqual(False, OGRGeometry('LINESTRING(0 0, 1 1)') == 3) + +def suite(): + s = unittest.TestSuite() + s.addTest(unittest.makeSuite(OGRGeomTest)) + return s + +def run(verbosity=2): + unittest.TextTestRunner(verbosity=verbosity).run(suite()) diff --git a/parts/django/django/contrib/gis/gdal/tests/test_srs.py b/parts/django/django/contrib/gis/gdal/tests/test_srs.py new file mode 100644 index 0000000..2742c7a --- /dev/null +++ b/parts/django/django/contrib/gis/gdal/tests/test_srs.py @@ -0,0 +1,169 @@ +import unittest +from django.contrib.gis.gdal import SpatialReference, CoordTransform, OGRException, SRSException + +class TestSRS: + def __init__(self, wkt, **kwargs): + self.wkt = wkt + for key, value in kwargs.items(): + setattr(self, key, value) + +# Some Spatial Reference examples +srlist = (TestSRS('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"]]', + proj='+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs ', + epsg=4326, projected=False, geographic=True, local=False, + lin_name='unknown', ang_name='degree', lin_units=1.0, ang_units=0.0174532925199, + auth={'GEOGCS' : ('EPSG', '4326'), 'spheroid' : ('EPSG', '7030')}, + attr=(('DATUM', 'WGS_1984'), (('SPHEROID', 1), '6378137'),('primem|authority', 'EPSG'),), + ), + TestSRS('PROJCS["NAD83 / Texas South Central",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",30.28333333333333],PARAMETER["standard_parallel_2",28.38333333333333],PARAMETER["latitude_of_origin",27.83333333333333],PARAMETER["central_meridian",-99],PARAMETER["false_easting",600000],PARAMETER["false_northing",4000000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AUTHORITY["EPSG","32140"]]', + proj='+proj=lcc +lat_1=30.28333333333333 +lat_2=28.38333333333333 +lat_0=27.83333333333333 +lon_0=-99 +x_0=600000 +y_0=4000000 +ellps=GRS80 +datum=NAD83 +units=m +no_defs ', + epsg=32140, projected=True, geographic=False, local=False, + lin_name='metre', ang_name='degree', lin_units=1.0, ang_units=0.0174532925199, + auth={'PROJCS' : ('EPSG', '32140'), 'spheroid' : ('EPSG', '7019'), 'unit' : ('EPSG', '9001'),}, + attr=(('DATUM', 'North_American_Datum_1983'),(('SPHEROID', 2), '298.257222101'),('PROJECTION','Lambert_Conformal_Conic_2SP'),), + ), + TestSRS('PROJCS["NAD_1983_StatePlane_Texas_South_Central_FIPS_4204_Feet",GEOGCS["GCS_North_American_1983",DATUM["North_American_Datum_1983",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["False_Easting",1968500.0],PARAMETER["False_Northing",13123333.33333333],PARAMETER["Central_Meridian",-99.0],PARAMETER["Standard_Parallel_1",28.38333333333333],PARAMETER["Standard_Parallel_2",30.28333333333334],PARAMETER["Latitude_Of_Origin",27.83333333333333],UNIT["Foot_US",0.3048006096012192]]', + proj='+proj=lcc +lat_1=28.38333333333333 +lat_2=30.28333333333334 +lat_0=27.83333333333333 +lon_0=-99 +x_0=600000 +y_0=3999999.999999999 +ellps=GRS80 +datum=NAD83 +to_meter=0.3048006096012192 +no_defs ', + epsg=None, projected=True, geographic=False, local=False, + lin_name='Foot_US', ang_name='Degree', lin_units=0.3048006096012192, ang_units=0.0174532925199, + auth={'PROJCS' : (None, None),}, + attr=(('PROJCS|GeOgCs|spheroid', 'GRS_1980'),(('projcs', 9), 'UNIT'), (('projcs', 11), None),), + ), + # This is really ESRI format, not WKT -- but the import should work the same + TestSRS('LOCAL_CS["Non-Earth (Meter)",LOCAL_DATUM["Local Datum",0],UNIT["Meter",1.0],AXIS["X",EAST],AXIS["Y",NORTH]]', + esri=True, proj=None, epsg=None, projected=False, geographic=False, local=True, + lin_name='Meter', ang_name='degree', lin_units=1.0, ang_units=0.0174532925199, + attr=(('LOCAL_DATUM', 'Local Datum'), ('unit', 'Meter')), + ), + ) + +# Well-Known Names +well_known = (TestSRS('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"]]', wk='WGS84', name='WGS 84', attrs=(('GEOGCS|AUTHORITY', 1, '4326'), ('SPHEROID', 'WGS 84'))), + TestSRS('GEOGCS["WGS 72",DATUM["WGS_1972",SPHEROID["WGS 72",6378135,298.26,AUTHORITY["EPSG","7043"]],AUTHORITY["EPSG","6322"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4322"]]', wk='WGS72', name='WGS 72', attrs=(('GEOGCS|AUTHORITY', 1, '4322'), ('SPHEROID', 'WGS 72'))), + TestSRS('GEOGCS["NAD27",DATUM["North_American_Datum_1927",SPHEROID["Clarke 1866",6378206.4,294.9786982138982,AUTHORITY["EPSG","7008"]],AUTHORITY["EPSG","6267"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4267"]]', wk='NAD27', name='NAD27', attrs=(('GEOGCS|AUTHORITY', 1, '4267'), ('SPHEROID', 'Clarke 1866'))), + TestSRS('GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]]', wk='NAD83', name='NAD83', attrs=(('GEOGCS|AUTHORITY', 1, '4269'), ('SPHEROID', 'GRS 1980'))), + TestSRS('PROJCS["NZGD49 / Karamea Circuit",GEOGCS["NZGD49",DATUM["New_Zealand_Geodetic_Datum_1949",SPHEROID["International 1924",6378388,297,AUTHORITY["EPSG","7022"]],TOWGS84[59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993],AUTHORITY["EPSG","6272"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4272"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",-41.28991152777778],PARAMETER["central_meridian",172.1090281944444],PARAMETER["scale_factor",1],PARAMETER["false_easting",300000],PARAMETER["false_northing",700000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AUTHORITY["EPSG","27216"]]', wk='EPSG:27216', name='NZGD49 / Karamea Circuit', attrs=(('PROJECTION','Transverse_Mercator'), ('SPHEROID', 'International 1924'))), + ) + +bad_srlist = ('Foobar', 'OOJCS["NAD83 / Texas South Central",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",30.28333333333333],PARAMETER["standard_parallel_2",28.38333333333333],PARAMETER["latitude_of_origin",27.83333333333333],PARAMETER["central_meridian",-99],PARAMETER["false_easting",600000],PARAMETER["false_northing",4000000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AUTHORITY["EPSG","32140"]]',) + +class SpatialRefTest(unittest.TestCase): + + def test01_wkt(self): + "Testing initialization on valid OGC WKT." + for s in srlist: + srs = SpatialReference(s.wkt) + + def test02_bad_wkt(self): + "Testing initialization on invalid WKT." + for bad in bad_srlist: + try: + srs = SpatialReference(bad) + srs.validate() + except (SRSException, OGRException): + pass + else: + self.fail('Should not have initialized on bad WKT "%s"!') + + def test03_get_wkt(self): + "Testing getting the WKT." + for s in srlist: + srs = SpatialReference(s.wkt) + self.assertEqual(s.wkt, srs.wkt) + + def test04_proj(self): + "Test PROJ.4 import and export." + + for s in srlist: + if s.proj: + srs1 = SpatialReference(s.wkt) + srs2 = SpatialReference(s.proj) + self.assertEqual(srs1.proj, srs2.proj) + + def test05_epsg(self): + "Test EPSG import." + for s in srlist: + if s.epsg: + srs1 = SpatialReference(s.wkt) + srs2 = SpatialReference(s.epsg) + srs3 = SpatialReference(str(s.epsg)) + srs4 = SpatialReference('EPSG:%d' % s.epsg) + #self.assertEqual(srs1.wkt, srs2.wkt) + for srs in (srs1, srs2, srs3, srs4): + for attr, expected in s.attr: + self.assertEqual(expected, srs[attr]) + + def test07_boolean_props(self): + "Testing the boolean properties." + for s in srlist: + srs = SpatialReference(s.wkt) + self.assertEqual(s.projected, srs.projected) + self.assertEqual(s.geographic, srs.geographic) + + def test08_angular_linear(self): + "Testing the linear and angular units routines." + for s in srlist: + srs = SpatialReference(s.wkt) + self.assertEqual(s.ang_name, srs.angular_name) + self.assertEqual(s.lin_name, srs.linear_name) + self.assertAlmostEqual(s.ang_units, srs.angular_units, 9) + self.assertAlmostEqual(s.lin_units, srs.linear_units, 9) + + def test09_authority(self): + "Testing the authority name & code routines." + for s in srlist: + if hasattr(s, 'auth'): + srs = SpatialReference(s.wkt) + for target, tup in s.auth.items(): + self.assertEqual(tup[0], srs.auth_name(target)) + self.assertEqual(tup[1], srs.auth_code(target)) + + def test10_attributes(self): + "Testing the attribute retrieval routines." + for s in srlist: + srs = SpatialReference(s.wkt) + for tup in s.attr: + att = tup[0] # Attribute to test + exp = tup[1] # Expected result + self.assertEqual(exp, srs[att]) + + def test11_wellknown(self): + "Testing Well Known Names of Spatial References." + for s in well_known: + srs = SpatialReference(s.wk) + self.assertEqual(s.name, srs.name) + for tup in s.attrs: + if len(tup) == 2: + key = tup[0] + exp = tup[1] + elif len(tup) == 3: + key = tup[:2] + exp = tup[2] + self.assertEqual(srs[key], exp) + + def test12_coordtransform(self): + "Testing initialization of a CoordTransform." + target = SpatialReference('WGS84') + for s in srlist: + if s.proj: + ct = CoordTransform(SpatialReference(s.wkt), target) + + def test13_attr_value(self): + "Testing the attr_value() method." + s1 = SpatialReference('WGS84') + self.assertRaises(TypeError, s1.__getitem__, 0) + self.assertRaises(TypeError, s1.__getitem__, ('GEOGCS', 'foo')) + self.assertEqual('WGS 84', s1['GEOGCS']) + self.assertEqual('WGS_1984', s1['DATUM']) + self.assertEqual('EPSG', s1['AUTHORITY']) + self.assertEqual(4326, int(s1['AUTHORITY', 1])) + #for i in range(7): self.assertEqual(0, int(s1['TOWGS84', i])) + self.assertEqual(None, s1['FOOBAR']) + +def suite(): + s = unittest.TestSuite() + s.addTest(unittest.makeSuite(SpatialRefTest)) + return s + +def run(verbosity=2): + unittest.TextTestRunner(verbosity=verbosity).run(suite()) diff --git a/parts/django/django/contrib/gis/geometry/__init__.py b/parts/django/django/contrib/gis/geometry/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/django/contrib/gis/geometry/__init__.py diff --git a/parts/django/django/contrib/gis/geometry/backend/__init__.py b/parts/django/django/contrib/gis/geometry/backend/__init__.py new file mode 100644 index 0000000..d79a556 --- /dev/null +++ b/parts/django/django/contrib/gis/geometry/backend/__init__.py @@ -0,0 +1,21 @@ +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.utils.importlib import import_module + +geom_backend = getattr(settings, 'GEOMETRY_BACKEND', 'geos') + +try: + module = import_module('.%s' % geom_backend, 'django.contrib.gis.geometry.backend') +except ImportError, e: + try: + module = import_module(geom_backend) + except ImportError, e_user: + raise ImproperlyConfigured('Could not import user-defined GEOMETRY_BACKEND ' + '"%s".' % geom_backend) + +try: + Geometry = module.Geometry + GeometryException = module.GeometryException +except AttributeError: + raise ImproperlyConfigured('Cannot import Geometry from the "%s" ' + 'geometry backend.' % geom_backend) diff --git a/parts/django/django/contrib/gis/geometry/backend/geos.py b/parts/django/django/contrib/gis/geometry/backend/geos.py new file mode 100644 index 0000000..a1ac096 --- /dev/null +++ b/parts/django/django/contrib/gis/geometry/backend/geos.py @@ -0,0 +1,3 @@ +from django.contrib.gis.geos import \ + GEOSGeometry as Geometry, \ + GEOSException as GeometryException diff --git a/parts/django/django/contrib/gis/geometry/regex.py b/parts/django/django/contrib/gis/geometry/regex.py new file mode 100644 index 0000000..1b9e2f4 --- /dev/null +++ b/parts/django/django/contrib/gis/geometry/regex.py @@ -0,0 +1,12 @@ +import re + +# 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<srid>\d+);)?' + r'(?P<wkt>' + r'(?P<type>POINT|LINESTRING|LINEARRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON|GEOMETRYCOLLECTION)' + r'[ACEGIMLONPSRUTYZ\d,\.\-\(\) ]+)$', + re.I) +json_regex = re.compile(r'^(\s+)?\{[\s\w,\[\]\{\}\-\."\':]+\}(\s+)?$') diff --git a/parts/django/django/contrib/gis/geometry/test_data.py b/parts/django/django/contrib/gis/geometry/test_data.py new file mode 100644 index 0000000..4e07348 --- /dev/null +++ b/parts/django/django/contrib/gis/geometry/test_data.py @@ -0,0 +1,105 @@ +""" +This module has the mock object definitions used to hold reference geometry +for the GEOS and GDAL tests. +""" +import gzip +import os + +from django.contrib import gis +from django.utils import simplejson + + +# This global used to store reference geometry data. +GEOMETRIES = None + +# Path where reference test data is located. +TEST_DATA = os.path.join(os.path.dirname(gis.__file__), 'tests', 'data') + + +def tuplize(seq): + "Turn all nested sequences to tuples in given sequence." + if isinstance(seq, (list, tuple)): + return tuple([tuplize(i) for i in seq]) + return seq + + +def strconvert(d): + "Converts all keys in dictionary to str type." + return dict([(str(k), v) for k, v in d.iteritems()]) + + +def get_ds_file(name, ext): + return os.path.join(TEST_DATA, + name, + name + '.%s' % ext + ) + + +class TestObj(object): + """ + Base testing object, turns keyword args into attributes. + """ + def __init__(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) + + +class TestDS(TestObj): + """ + Object for testing GDAL data sources. + """ + def __init__(self, name, **kwargs): + # Shapefile is default extension, unless specified otherwise. + ext = kwargs.pop('ext', 'shp') + self.ds = get_ds_file(name, ext) + super(TestDS, self).__init__(**kwargs) + + +class TestGeom(TestObj): + """ + Testing object used for wrapping reference geometry data + in GEOS/GDAL tests. + """ + def __init__(self, **kwargs): + # Converting lists to tuples of certain keyword args + # so coordinate test cases will match (JSON has no + # concept of tuple). + coords = kwargs.pop('coords', None) + if coords: + self.coords = tuplize(coords) + + centroid = kwargs.pop('centroid', None) + if centroid: + self.centroid = tuple(centroid) + + ext_ring_cs = kwargs.pop('ext_ring_cs', None) + if ext_ring_cs: + ext_ring_cs = tuplize(ext_ring_cs) + self.ext_ring_cs = ext_ring_cs + + super(TestGeom, self).__init__(**kwargs) + + +class TestGeomSet(object): + """ + Each attribute of this object is a list of `TestGeom` instances. + """ + def __init__(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, [TestGeom(**strconvert(kw)) for kw in value]) + + +class TestDataMixin(object): + """ + Mixin used for GEOS/GDAL test cases that defines a `geometries` + property, which returns and/or loads the reference geometry data. + """ + @property + def geometries(self): + global GEOMETRIES + if GEOMETRIES is None: + # Load up the test geometry data from fixture into global. + gzf = gzip.GzipFile(os.path.join(TEST_DATA, 'geometries.json.gz')) + geometries = simplejson.loads(gzf.read()) + GEOMETRIES = TestGeomSet(**strconvert(geometries)) + return GEOMETRIES diff --git a/parts/django/django/contrib/gis/geos/LICENSE b/parts/django/django/contrib/gis/geos/LICENSE new file mode 100644 index 0000000..0479b07 --- /dev/null +++ b/parts/django/django/contrib/gis/geos/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2007-2009 Justin Bronn +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of GEOSGeometry nor the names of its contributors may be used + to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/parts/django/django/contrib/gis/geos/__init__.py b/parts/django/django/contrib/gis/geos/__init__.py new file mode 100644 index 0000000..5885a30 --- /dev/null +++ b/parts/django/django/contrib/gis/geos/__init__.py @@ -0,0 +1,14 @@ +""" +The GeoDjango GEOS module. Please consult the GeoDjango documentation +for more details: + http://geodjango.org/docs/geos.html +""" +from django.contrib.gis.geos.geometry import GEOSGeometry, wkt_regex, hex_regex +from django.contrib.gis.geos.point import Point +from django.contrib.gis.geos.linestring import LineString, LinearRing +from django.contrib.gis.geos.polygon import Polygon +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.io import WKTReader, WKTWriter, WKBReader, WKBWriter +from django.contrib.gis.geos.factory import fromfile, fromstr +from django.contrib.gis.geos.libgeos import geos_version, geos_version_info, GEOS_PREPARE diff --git a/parts/django/django/contrib/gis/geos/base.py b/parts/django/django/contrib/gis/geos/base.py new file mode 100644 index 0000000..34c03c8 --- /dev/null +++ b/parts/django/django/contrib/gis/geos/base.py @@ -0,0 +1,52 @@ +from ctypes import c_void_p +from types import NoneType +from django.contrib.gis.geos.error import GEOSException, GEOSIndexError + +# 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 import gdal +except ImportError: + # A 'dummy' gdal module. + class GDALInfo(object): + HAS_GDAL = False + GEOJSON = False + gdal = GDALInfo() + +# NumPy supported? +try: + import numpy +except ImportError: + numpy = False + +class GEOSBase(object): + """ + Base object for GEOS objects that has a pointer access property + that controls access to the underlying C pointer. + """ + # Initially the pointer is NULL. + _ptr = None + + # Default allowed pointer type. + ptr_type = c_void_p + + # Pointer access property. + def _get_ptr(self): + # Raise an exception if the pointer isn't valid don't + # want to be passing NULL pointers to routines -- + # that's very bad. + if self._ptr: return self._ptr + else: raise GEOSException('NULL GEOS %s pointer encountered.' % self.__class__.__name__) + + def _set_ptr(self, ptr): + # Only allow the pointer to be set with pointers of the + # compatible type or None (NULL). + if isinstance(ptr, (self.ptr_type, NoneType)): + self._ptr = ptr + else: + raise TypeError('Incompatible pointer type') + + # Property for controlling access to the GEOS object pointers. Using + # this raises an exception when the pointer is NULL, thus preventing + # the C library from attempting to access an invalid memory location. + ptr = property(_get_ptr, _set_ptr) diff --git a/parts/django/django/contrib/gis/geos/collections.py b/parts/django/django/contrib/gis/geos/collections.py new file mode 100644 index 0000000..515f80e --- /dev/null +++ b/parts/django/django/contrib/gis/geos/collections.py @@ -0,0 +1,123 @@ +""" + This module houses the Geometry Collection objects: + GeometryCollection, MultiPoint, MultiLineString, and MultiPolygon +""" +from ctypes import c_int, c_uint, byref +from django.contrib.gis.geos.error import GEOSException, GEOSIndexError +from django.contrib.gis.geos.geometry import GEOSGeometry +from django.contrib.gis.geos.libgeos import get_pointer_arr, GEOM_PTR, GEOS_PREPARE +from django.contrib.gis.geos.linestring import LineString, LinearRing +from django.contrib.gis.geos.point import Point +from django.contrib.gis.geos.polygon import Polygon +from django.contrib.gis.geos import prototypes as capi + +class GeometryCollection(GEOSGeometry): + _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], (tuple, list)): + init_geoms = args[0] + else: + init_geoms = args + else: + init_geoms = args + + # Ensuring that only the permitted geometries are allowed in this collection + # this is moved to list mixin super class + self._check_allowed(init_geoms) + + # Creating the geometry pointer array. + collection = self._create_collection(len(init_geoms), iter(init_geoms)) + super(GeometryCollection, self).__init__(collection, **kwargs) + + def __iter__(self): + "Iterates over each Geometry in the Collection." + for i in xrange(len(self)): + yield self[i] + + def __len__(self): + "Returns the number of geometries in this Collection." + return self.num_geom + + ### Methods for compatibility with ListMixin ### + def _create_collection(self, length, items): + # Creating the geometry pointer array. + geoms = get_pointer_arr(length) + for i, g in enumerate(items): + # this is a little sloppy, but makes life easier + # allow GEOSGeometry types (python wrappers) or pointer types + geoms[i] = capi.geom_clone(getattr(g, 'ptr', g)) + + return capi.create_collection(c_int(self._typeid), byref(geoms), c_uint(length)) + + def _get_single_internal(self, index): + return capi.get_geomn(self.ptr, index) + + def _get_single_external(self, index): + "Returns the Geometry from this Collection at the given index (0-based)." + # Checking the index and returning the corresponding GEOS geometry. + return GEOSGeometry(capi.geom_clone(self._get_single_internal(index)), srid=self.srid) + + def _set_list(self, length, items): + "Create a new collection, and destroy the contents of the previous pointer." + prev_ptr = self.ptr + srid = self.srid + self.ptr = self._create_collection(length, items) + if srid: self.srid = srid + capi.destroy_geom(prev_ptr) + + _set_single = GEOSGeometry._set_single_rebuild + _assign_extended_slice = GEOSGeometry._assign_extended_slice_rebuild + + @property + def kml(self): + "Returns the KML for this Geometry Collection." + return '<MultiGeometry>%s</MultiGeometry>' % ''.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 + + @property + def merged(self): + """ + Returns a LineString representing the line merge of this + MultiLineString. + """ + return self._topology(capi.geos_linemerge(self.ptr)) + +class MultiPolygon(GeometryCollection): + _allowed = Polygon + _typeid = 6 + + @property + def cascaded_union(self): + "Returns a cascaded union of this MultiPolygon." + if GEOS_PREPARE: + return GEOSGeometry(capi.geos_cascaded_union(self.ptr), self.srid) + else: + raise GEOSException('The cascaded union operation requires GEOS 3.1+.') + +# Setting the allowed types here since GeometryCollection is defined before +# its subclasses. +GeometryCollection._allowed = (Point, LineString, LinearRing, Polygon, MultiPoint, MultiLineString, MultiPolygon) diff --git a/parts/django/django/contrib/gis/geos/coordseq.py b/parts/django/django/contrib/gis/geos/coordseq.py new file mode 100644 index 0000000..027d34e --- /dev/null +++ b/parts/django/django/contrib/gis/geos/coordseq.py @@ -0,0 +1,156 @@ +""" + 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 django.contrib.gis.geos.base import GEOSBase, numpy +from django.contrib.gis.geos.error import GEOSException, GEOSIndexError +from django.contrib.gis.geos.libgeos import CS_PTR +from django.contrib.gis.geos import prototypes as capi + +class GEOSCoordSeq(GEOSBase): + "The internal representation of a list of coordinates inside a Geometry." + + ptr_type = CS_PTR + + #### 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, (list, tuple)): + pass + elif numpy and isinstance(value, numpy.ndarray): + pass + else: + raise TypeError('Must set coordinate with a sequence (list, tuple, or numpy array).') + # Checking the dims of the input + if self.dims == 3 and self._z: + n_args = 3 + 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) + + #### 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 capi.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) + capi.cs_setordinate(self.ptr, index, dimension, value) + + def getX(self, index): + "Get the X value at the index." + return self.getOrdinate(0, index) + + def setX(self, index, value): + "Set X with the value at the given index." + self.setOrdinate(0, index, value) + + def getY(self, index): + "Get the Y value at the given index." + return self.getOrdinate(1, index) + + def setY(self, index, value): + "Set Y with the value at the given index." + self.setOrdinate(1, index, value) + + def getZ(self, index): + "Get Z with the value at the given index." + return self.getOrdinate(2, index) + + def setZ(self, index, value): + "Set Z with the value at the given index." + self.setOrdinate(2, index, value) + + ### Dimensions ### + @property + def size(self): + "Returns the size of this coordinate sequence." + return capi.cs_getsize(self.ptr, byref(c_uint())) + + @property + def dims(self): + "Returns the dimensions of this coordinate sequence." + return capi.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(capi.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 '<coordinates>%s</coordinates>' % \ + ''.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/parts/django/django/contrib/gis/geos/error.py b/parts/django/django/contrib/gis/geos/error.py new file mode 100644 index 0000000..46bdfe6 --- /dev/null +++ b/parts/django/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/parts/django/django/contrib/gis/geos/factory.py b/parts/django/django/contrib/gis/geos/factory.py new file mode 100644 index 0000000..df29976 --- /dev/null +++ b/parts/django/django/contrib/gis/geos/factory.py @@ -0,0 +1,23 @@ +from django.contrib.gis.geos.geometry import GEOSGeometry, wkt_regex, hex_regex + +def fromfile(file_h): + """ + Given a string file name, returns a GEOSGeometry. The file may contain WKB, + WKT, or HEX. + """ + # If given a file name, get a real handle. + if isinstance(file_h, basestring): + file_h = open(file_h, 'rb') + + # Reading in the file's contents, + buf = file_h.read() + + # If we get WKB need to wrap in buffer(), so run through regexes. + if wkt_regex.match(buf) or hex_regex.match(buf): + return GEOSGeometry(buf) + else: + return GEOSGeometry(buffer(buf)) + +def fromstr(string, **kwargs): + "Given a string value, returns a GEOSGeometry object." + return GEOSGeometry(string, **kwargs) diff --git a/parts/django/django/contrib/gis/geos/geometry.py b/parts/django/django/contrib/gis/geos/geometry.py new file mode 100644 index 0000000..51666bc --- /dev/null +++ b/parts/django/django/contrib/gis/geos/geometry.py @@ -0,0 +1,661 @@ +""" + 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 + +# super-class for mutable list behavior +from django.contrib.gis.geos.mutable_list import ListMixin + +# GEOS-related dependencies. +from django.contrib.gis.geos.base import GEOSBase, gdal +from django.contrib.gis.geos.coordseq import GEOSCoordSeq +from django.contrib.gis.geos.error import GEOSException, GEOSIndexError +from django.contrib.gis.geos.libgeos import GEOM_PTR, GEOS_PREPARE +from django.contrib.gis.geos.mutable_list import ListMixin + +# 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 import prototypes as capi + +# These functions provide access to a thread-local instance +# of their corresponding GEOS I/O class. +from django.contrib.gis.geos.prototypes.io import wkt_r, wkt_w, wkb_r, wkb_w, ewkb_w, ewkb_w3d + +# For recognizing geometry input. +from django.contrib.gis.geometry.regex import hex_regex, wkt_regex, json_regex + +class GEOSGeometry(GEOSBase, ListMixin): + "A class that, generally, encapsulates a GEOS geometry." + + # Raise GEOSIndexError instead of plain IndexError + # (see ticket #4740 and GEOSIndexError docstring) + _IndexError = GEOSIndexError + + ptr_type = GEOM_PTR + + #### Python 'magic' routines #### + def __init__(self, geo_input, srid=None): + """ + The base constructor for GEOS geometry objects, and may take the + following inputs: + + * strings: + - WKT + - HEXEWKB (a PostGIS-specific canonical form) + - GeoJSON (requires GDAL) + * 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, unicode): + # 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 = wkt_r().read(wkt_m.group('wkt')) + elif hex_regex.match(geo_input): + # Handling HEXEWKB input. + g = wkb_r().read(geo_input) + elif gdal.GEOJSON and json_regex.match(geo_input): + # Handling GeoJSON input. + g = wkb_r().read(gdal.OGRGeometry(geo_input).wkb) + 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). + g = wkb_r().read(geo_input) + elif isinstance(geo_input, GEOSGeometry): + g = capi.geom_clone(geo_input.ptr) + 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() + + def __del__(self): + """ + Destroys this Geometry; in other words, frees the memory used by the + GEOS C++ object. + """ + if self._ptr: capi.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 = wkb_r().read(buffer(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(capi.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 capi.geos_type(self.ptr) + + @property + def geom_typeid(self): + "Returns an integer representing the Geometry type." + return capi.geos_typeid(self.ptr) + + @property + def num_geom(self): + "Returns the number of geometries in the Geometry." + return capi.get_num_geoms(self.ptr) + + @property + def num_coords(self): + "Returns the number of coordinates in the Geometry." + return capi.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 capi.get_dims(self.ptr) + + def normalize(self): + "Converts this Geometry to normal form (or canonical form)." + return capi.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 capi.geos_isempty(self.ptr) + + @property + def hasz(self): + "Returns whether the geometry has a 3D dimension." + return capi.geos_hasz(self.ptr) + + @property + def ring(self): + "Returns whether or not the geometry is a ring." + return capi.geos_isring(self.ptr) + + @property + def simple(self): + "Returns false if the Geometry not simple." + return capi.geos_issimple(self.ptr) + + @property + def valid(self): + "This property tests the validity of this Geometry." + return capi.geos_isvalid(self.ptr) + + #### Binary predicates. #### + def contains(self, other): + "Returns true if other.within(this) returns true." + return capi.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 capi.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 capi.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 capi.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 capi.geos_equalsexact(self.ptr, other.ptr, float(tolerance)) + + def intersects(self, other): + "Returns true if disjoint returns false." + return capi.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 capi.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, basestring) or len(pattern) > 9: + raise GEOSException('invalid intersection matrix pattern') + return capi.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 capi.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 capi.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 = capi.geos_get_srid(self.ptr) + if s == 0: return None + else: return s + + def set_srid(self, srid): + "Sets the SRID for the geometry." + capi.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. Note that Z values + are *not* included in this representation because GEOS does not yet + support serializing them. + """ + 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) representation of this Geometry." + return wkt_w().write(self) + + @property + def hex(self): + """ + Returns the WKB of this Geometry in hexadecimal form. Please note + that the SRID and Z values are not included in this representation + because it is not a part of the OGC specification (use the `hexewkb` + property instead). + """ + # A possible faster, all-python, implementation: + # str(self.wkb).encode('hex') + return wkb_w().write_hex(self) + + @property + def hexewkb(self): + """ + Returns the EWKB of this Geometry in hexadecimal form. This is an + extension of the WKB specification that includes SRID and Z values + that are a part of this geometry. + """ + if self.hasz: + if not GEOS_PREPARE: + # See: http://trac.osgeo.org/geos/ticket/216 + raise GEOSException('Upgrade GEOS to 3.1 to get valid 3D HEXEWKB.') + return ewkb_w3d().write_hex(self) + else: + return ewkb_w().write_hex(self) + + @property + def json(self): + """ + Returns GeoJSON representation of this Geometry if GDAL 1.5+ + is installed. + """ + if gdal.GEOJSON: + return self.ogr.json + else: + raise GEOSException('GeoJSON output only supported on GDAL 1.5+.') + geojson = json + + @property + def wkb(self): + """ + Returns the WKB (Well-Known Binary) representation of this Geometry + as a Python buffer. SRID and Z values are not included, use the + `ewkb` property instead. + """ + return wkb_w().write(self) + + @property + def ewkb(self): + """ + Return the EWKB representation of this Geometry as a Python buffer. + This is an extension of the WKB specification that includes any SRID + and Z values that are a part of this geometry. + """ + if self.hasz: + if not GEOS_PREPARE: + # See: http://trac.osgeo.org/geos/ticket/216 + raise GEOSException('Upgrade GEOS to 3.1 to get valid 3D EWKB.') + return ewkb_w3d().write(self) + else: + return ewkb_w().write(self) + + @property + def kml(self): + "Returns the KML representation of this Geometry." + gtype = self.geom_type + return '<%s>%s</%s>' % (gtype, self.coord_seq.kml, gtype) + + @property + def prepared(self): + """ + Returns a PreparedGeometry corresponding to this geometry -- it is + optimized for the contains, intersects, and covers operations. + """ + if GEOS_PREPARE: + return PreparedGeometry(self) + else: + raise GEOSException('GEOS 3.1+ required for prepared geometry support.') + + #### GDAL-specific output routines #### + @property + def ogr(self): + "Returns the OGR Geometry for this Geometry." + if gdal.HAS_GDAL: + if self.srid: + return gdal.OGRGeometry(self.wkb, self.srid) + else: + return gdal.OGRGeometry(self.wkb) + else: + raise GEOSException('GDAL required to convert to an OGRGeometry.') + + @property + def srs(self): + "Returns the OSR SpatialReference for SRID of this Geometry." + if gdal.HAS_GDAL: + if self.srid: + return gdal.SpatialReference(self.srid) + else: + return None + else: + raise GEOSException('GDAL required to return a SpatialReference object.') + + @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 gdal.HAS_GDAL and srid: + # Creating an OGR Geometry, which is then transformed. + g = gdal.OGRGeometry(self.wkb, srid) + g.transform(ct) + # Getting a new GEOS pointer + ptr = wkb_r().read(g.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. + capi.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(capi.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(capi.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(capi.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(capi.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(capi.geos_difference(self.ptr, other.ptr)) + + @property + def envelope(self): + "Return the envelope for this geometry (a polygon)." + return self._topology(capi.geos_envelope(self.ptr)) + + def intersection(self, other): + "Returns a Geometry representing the points shared by this Geometry and other." + return self._topology(capi.geos_intersection(self.ptr, other.ptr)) + + @property + def point_on_surface(self): + "Computes an interior point of this Geometry." + return self._topology(capi.geos_pointonsurface(self.ptr)) + + def relate(self, other): + "Returns the DE-9IM intersection matrix for this Geometry and the other." + return capi.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(capi.geos_preservesimplify(self.ptr, tolerance)) + else: + return self._topology(capi.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(capi.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(capi.geos_union(self.ptr, other.ptr)) + + #### Other Routines #### + @property + def area(self): + "Returns the area of the Geometry." + return capi.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 capi.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 capi.geos_length(self.ptr, byref(c_double())) + + def clone(self): + "Clones this Geometry." + return GEOSGeometry(capi.geom_clone(self.ptr), srid=self.srid) + +# Class mapping dictionary. Has to be at the end to avoid import +# conflicts with GEOSGeometry. +from django.contrib.gis.geos.linestring import LineString, LinearRing +from django.contrib.gis.geos.point import Point +from django.contrib.gis.geos.polygon import Polygon +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, + } + +# If supported, import the PreparedGeometry class. +if GEOS_PREPARE: + from django.contrib.gis.geos.prepared import PreparedGeometry diff --git a/parts/django/django/contrib/gis/geos/io.py b/parts/django/django/contrib/gis/geos/io.py new file mode 100644 index 0000000..54ba6b4 --- /dev/null +++ b/parts/django/django/contrib/gis/geos/io.py @@ -0,0 +1,20 @@ +""" +Module that holds classes for performing I/O operations on GEOS geometry +objects. Specifically, this has Python implementations of WKB/WKT +reader and writer classes. +""" +from django.contrib.gis.geos.geometry import GEOSGeometry +from django.contrib.gis.geos.prototypes.io import _WKTReader, _WKBReader, WKBWriter, WKTWriter + +# Public classes for (WKB|WKT)Reader, which return GEOSGeometry +class WKBReader(_WKBReader): + def read(self, wkb): + "Returns a GEOSGeometry for the given WKB buffer." + return GEOSGeometry(super(WKBReader, self).read(wkb)) + +class WKTReader(_WKTReader): + def read(self, wkt): + "Returns a GEOSGeometry for the given WKT string." + return GEOSGeometry(super(WKTReader, self).read(wkt)) + + diff --git a/parts/django/django/contrib/gis/geos/libgeos.py b/parts/django/django/contrib/gis/geos/libgeos.py new file mode 100644 index 0000000..84299a0 --- /dev/null +++ b/parts/django/django/contrib/gis/geos/libgeos.py @@ -0,0 +1,141 @@ +""" + 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 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 + +# 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', 'GEOS'] +else: + raise ImportError('Unsupported OS "%s"' % os.name) + +# Using the ctypes `find_library` utility to find the path to the GEOS +# shared library. This is better than manually 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 ImportError('Could not find the GEOS library (tried "%s"). ' + 'Try setting GEOS_LIBRARY_PATH in your settings.' % + '", "'.join(lib_names)) + +# Getting the GEOS C library. The C interface (CDLL) is used for +# both *NIX and Windows. +# See the GEOS C API source code for more details on the library function calls: +# 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) + +#### GEOS Geometry C data structures, and utility functions. #### + +# Opaque GEOS geometry structures, used for GEOM_PTR and CS_PTR +class GEOSGeom_t(Structure): pass +class GEOSPrepGeom_t(Structure): pass +class GEOSCoordSeq_t(Structure): pass +class GEOSContextHandle_t(Structure): pass + +# Pointers to opaque GEOS geometry structures. +GEOM_PTR = POINTER(GEOSGeom_t) +PREPGEOM_PTR = POINTER(GEOSPrepGeom_t) +CS_PTR = POINTER(GEOSCoordSeq_t) +CONTEXT_PTR = POINTER(GEOSContextHandle_t) + +# 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<version>(?P<major>\d+)\.(?P<minor>\d+)\.(?P<subminor>\d+))(rc(?P<release_candidate>\d+))?-CAPI-(?P<capi_version>\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', 'major', 'minor', 'subminor')) + +# Version numbers and whether or not prepared geometry support is available. +_verinfo = geos_version_info() +GEOS_MAJOR_VERSION = int(_verinfo['major']) +GEOS_MINOR_VERSION = int(_verinfo['minor']) +GEOS_SUBMINOR_VERSION = int(_verinfo['subminor']) +del _verinfo +GEOS_VERSION = (GEOS_MAJOR_VERSION, GEOS_MINOR_VERSION, GEOS_SUBMINOR_VERSION) +GEOS_PREPARE = GEOS_VERSION >= (3, 1, 0) + +if GEOS_PREPARE: + # Here we set up the prototypes for the initGEOS_r and finishGEOS_r + # routines. These functions aren't actually called until they are + # attached to a GEOS context handle -- this actually occurs in + # geos/prototypes/threadsafe.py. + lgeos.initGEOS_r.restype = CONTEXT_PTR + lgeos.finishGEOS_r.argtypes = [CONTEXT_PTR] +else: + # When thread-safety isn't available, the initGEOS routine must be called + # first. This function takes the notice and error functions, defined + # as Python callbacks above, 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) + # Calling finishGEOS() upon exit of the interpreter. + import atexit + atexit.register(lgeos.finishGEOS) diff --git a/parts/django/django/contrib/gis/geos/linestring.py b/parts/django/django/contrib/gis/geos/linestring.py new file mode 100644 index 0000000..ecf7741 --- /dev/null +++ b/parts/django/django/contrib/gis/geos/linestring.py @@ -0,0 +1,152 @@ +from django.contrib.gis.geos.base import numpy +from django.contrib.gis.geos.coordseq import GEOSCoordSeq +from django.contrib.gis.geos.error import GEOSException +from django.contrib.gis.geos.geometry import GEOSGeometry +from django.contrib.gis.geos.point import Point +from django.contrib.gis.geos import prototypes as capi + +class LineString(GEOSGeometry): + _init_func = capi.create_linestring + _minlength = 2 + + #### 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 numpy and isinstance(coords, numpy.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(capi.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] + + # 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__(self._init_func(cs.ptr), srid=srid) + + 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 _get_single_external(self, index): + return self._cs[index] + + _get_single_internal = _get_single_external + + def _set_list(self, length, items): + ndim = self._cs.dims # + hasz = self._cs.hasz # I don't understand why these are different + + # create a new coordinate sequence and populate accordingly + cs = GEOSCoordSeq(capi.create_cs(length, ndim), z=hasz) + for i, c in enumerate(items): + cs[i] = c + + ptr = self._init_func(cs.ptr) + if ptr: + capi.destroy_geom(self.ptr) + self.ptr = ptr + self._post_init(self.srid) + else: + # can this happen? + raise GEOSException('Geometry resulting from slice deletion was invalid.') + + def _set_single(self, index, value): + self._checkindex(index) + self._cs[index] = value + + 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 numpy: return numpy.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 merged(self): + "Returns the line merge of this LineString." + return self._topology(capi.geos_linemerge(self.ptr)) + + @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): + _minLength = 4 + _init_func = capi.create_linearring diff --git a/parts/django/django/contrib/gis/geos/mutable_list.py b/parts/django/django/contrib/gis/geos/mutable_list.py new file mode 100644 index 0000000..cc28147 --- /dev/null +++ b/parts/django/django/contrib/gis/geos/mutable_list.py @@ -0,0 +1,309 @@ +# Copyright (c) 2008-2009 Aryeh Leib Taurog, all rights reserved. +# Released under the New BSD license. +""" +This module contains a base type which provides list-style mutations +without specific data storage methods. + +See also http://www.aryehleib.com/MutableLists.html + +Author: Aryeh Leib Taurog. +""" +class ListMixin(object): + """ + A base class which provides complete list interface. + Derived classes must call ListMixin's __init__() function + and implement the following: + + function _get_single_external(self, i): + Return single item with index i for general use. + The index i will always satisfy 0 <= i < len(self). + + function _get_single_internal(self, i): + Same as above, but for use within the class [Optional] + Note that if _get_single_internal and _get_single_internal return + different types of objects, _set_list must distinguish + between the two and handle each appropriately. + + function _set_list(self, length, items): + Recreate the entire object. + + NOTE: items may be a generator which calls _get_single_internal. + Therefore, it is necessary to cache the values in a temporary: + temp = list(items) + before clobbering the original storage. + + function _set_single(self, i, value): + Set the single item at index i to value [Optional] + If left undefined, all mutations will result in rebuilding + the object using _set_list. + + function __len__(self): + Return the length + + int _minlength: + The minimum legal length [Optional] + + int _maxlength: + The maximum legal length [Optional] + + type or tuple _allowed: + A type or tuple of allowed item types [Optional] + + class _IndexError: + The type of exception to be raise on invalid index [Optional] + """ + + _minlength = 0 + _maxlength = None + _IndexError = IndexError + + ### Python initialization and special list interface methods ### + + def __init__(self, *args, **kwargs): + if not hasattr(self, '_get_single_internal'): + self._get_single_internal = self._get_single_external + + if not hasattr(self, '_set_single'): + self._set_single = self._set_single_rebuild + self._assign_extended_slice = self._assign_extended_slice_rebuild + + super(ListMixin, self).__init__(*args, **kwargs) + + def __getitem__(self, index): + "Get the item(s) at the specified index/slice." + if isinstance(index, slice): + return [self._get_single_external(i) for i in xrange(*index.indices(len(self)))] + else: + index = self._checkindex(index) + return self._get_single_external(index) + + def __delitem__(self, index): + "Delete the item(s) at the specified index/slice." + if not isinstance(index, (int, long, slice)): + raise TypeError("%s is not a legal index" % index) + + # calculate new length and dimensions + origLen = len(self) + if isinstance(index, (int, long)): + index = self._checkindex(index) + indexRange = [index] + else: + indexRange = range(*index.indices(origLen)) + + newLen = origLen - len(indexRange) + newItems = ( self._get_single_internal(i) + for i in xrange(origLen) + if i not in indexRange ) + + self._rebuild(newLen, newItems) + + def __setitem__(self, index, val): + "Set the item(s) at the specified index/slice." + if isinstance(index, slice): + self._set_slice(index, val) + else: + index = self._checkindex(index) + self._check_allowed((val,)) + self._set_single(index, val) + + def __iter__(self): + "Iterate over the items in the list" + for i in xrange(len(self)): + yield self[i] + + ### Special methods for arithmetic operations ### + def __add__(self, other): + 'add another list-like object' + return self.__class__(list(self) + list(other)) + + def __radd__(self, other): + 'add to another list-like object' + return other.__class__(list(other) + list(self)) + + def __iadd__(self, other): + 'add another list-like object to self' + self.extend(list(other)) + return self + + def __mul__(self, n): + 'multiply' + return self.__class__(list(self) * n) + + def __rmul__(self, n): + 'multiply' + return self.__class__(list(self) * n) + + def __imul__(self, n): + 'multiply' + if n <= 0: + del self[:] + else: + cache = list(self) + for i in range(n-1): + self.extend(cache) + return self + + def __cmp__(self, other): + 'cmp' + slen = len(self) + for i in range(slen): + try: + c = cmp(self[i], other[i]) + except IndexError: + # must be other is shorter + return 1 + else: + # elements not equal + if c: return c + + return cmp(slen, len(other)) + + ### Public list interface Methods ### + ## Non-mutating ## + def count(self, val): + "Standard list count method" + count = 0 + for i in self: + if val == i: count += 1 + return count + + def index(self, val): + "Standard list index method" + for i in xrange(0, len(self)): + if self[i] == val: return i + raise ValueError('%s not found in object' % str(val)) + + ## Mutating ## + def append(self, val): + "Standard list append method" + self[len(self):] = [val] + + def extend(self, vals): + "Standard list extend method" + self[len(self):] = vals + + def insert(self, index, val): + "Standard list insert method" + if not isinstance(index, (int, long)): + raise TypeError("%s is not a legal index" % index) + self[index:index] = [val] + + def pop(self, index=-1): + "Standard list pop method" + result = self[index] + del self[index] + return result + + def remove(self, val): + "Standard list remove method" + del self[self.index(val)] + + def reverse(self): + "Standard list reverse method" + self[:] = self[-1::-1] + + def sort(self, cmp=cmp, key=None, reverse=False): + "Standard list sort method" + if key: + temp = [(key(v),v) for v in self] + temp.sort(cmp=cmp, key=lambda x: x[0], reverse=reverse) + self[:] = [v[1] for v in temp] + else: + temp = list(self) + temp.sort(cmp=cmp, reverse=reverse) + self[:] = temp + + ### Private routines ### + def _rebuild(self, newLen, newItems): + if newLen < self._minlength: + raise ValueError('Must have at least %d items' % self._minlength) + if self._maxlength is not None and newLen > self._maxlength: + raise ValueError('Cannot have more than %d items' % self._maxlength) + + self._set_list(newLen, newItems) + + def _set_single_rebuild(self, index, value): + self._set_slice(slice(index, index + 1, 1), [value]) + + def _checkindex(self, index, correct=True): + length = len(self) + if 0 <= index < length: + return index + if correct and -length <= index < 0: + return index + length + raise self._IndexError('invalid index: %s' % str(index)) + + def _check_allowed(self, items): + if hasattr(self, '_allowed'): + if False in [isinstance(val, self._allowed) for val in items]: + raise TypeError('Invalid type encountered in the arguments.') + + def _set_slice(self, index, values): + "Assign values to a slice of the object" + try: + iter(values) + except TypeError: + raise TypeError('can only assign an iterable to a slice') + + self._check_allowed(values) + + origLen = len(self) + valueList = list(values) + start, stop, step = index.indices(origLen) + + # CAREFUL: index.step and step are not the same! + # step will never be None + if index.step is None: + self._assign_simple_slice(start, stop, valueList) + else: + self._assign_extended_slice(start, stop, step, valueList) + + def _assign_extended_slice_rebuild(self, start, stop, step, valueList): + 'Assign an extended slice by rebuilding entire list' + indexList = range(start, stop, step) + # extended slice, only allow assigning slice of same size + if len(valueList) != len(indexList): + raise ValueError('attempt to assign sequence of size %d ' + 'to extended slice of size %d' + % (len(valueList), len(indexList))) + + # we're not changing the length of the sequence + newLen = len(self) + newVals = dict(zip(indexList, valueList)) + def newItems(): + for i in xrange(newLen): + if i in newVals: + yield newVals[i] + else: + yield self._get_single_internal(i) + + self._rebuild(newLen, newItems()) + + def _assign_extended_slice(self, start, stop, step, valueList): + 'Assign an extended slice by re-assigning individual items' + indexList = range(start, stop, step) + # extended slice, only allow assigning slice of same size + if len(valueList) != len(indexList): + raise ValueError('attempt to assign sequence of size %d ' + 'to extended slice of size %d' + % (len(valueList), len(indexList))) + + for i, val in zip(indexList, valueList): + self._set_single(i, val) + + def _assign_simple_slice(self, start, stop, valueList): + 'Assign a simple slice; Can assign slice of any length' + origLen = len(self) + stop = max(start, stop) + newLen = origLen - stop + start + len(valueList) + def newItems(): + for i in xrange(origLen + 1): + if i == start: + for val in valueList: + yield val + + if i < origLen: + if i < start or i >= stop: + yield self._get_single_internal(i) + + self._rebuild(newLen, newItems()) diff --git a/parts/django/django/contrib/gis/geos/point.py b/parts/django/django/contrib/gis/geos/point.py new file mode 100644 index 0000000..5c00a93 --- /dev/null +++ b/parts/django/django/contrib/gis/geos/point.py @@ -0,0 +1,135 @@ +from ctypes import c_uint +from django.contrib.gis.geos.error import GEOSException +from django.contrib.gis.geos.geometry import GEOSGeometry +from django.contrib.gis.geos import prototypes as capi + +class Point(GEOSGeometry): + _minlength = 2 + _maxlength = 3 + + 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) + 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.') + + point = self._create_point(ndim, coords) + + # Initializing using the address returned from the GEOS + # createPoint factory. + super(Point, self).__init__(point, srid=srid) + + def _create_point(self, ndim, coords): + """ + Create a coordinate sequence, set X, Y, [Z], and create point + """ + if ndim < 2 or ndim > 3: + raise TypeError('Invalid point dimension: %s' % str(ndim)) + + cs = capi.create_cs(c_uint(1), c_uint(ndim)) + i = iter(coords) + capi.cs_setx(cs, 0, i.next()) + capi.cs_sety(cs, 0, i.next()) + if ndim == 3: capi.cs_setz(cs, 0, i.next()) + + return capi.create_point(cs) + + def _set_list(self, length, items): + ptr = self._create_point(length, items) + if ptr: + capi.destroy_geom(self.ptr) + self._ptr = ptr + self._set_cs() + else: + # can this happen? + raise GEOSException('Geometry resulting from slice deletion was invalid.') + + def _set_single(self, index, value): + self._cs.setOrdinate(index, 0, value) + + def __iter__(self): + "Allows iteration over coordinates of this Point." + for i in xrange(len(self)): + yield self[i] + + 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_single_external(self, index): + if index == 0: + return self.x + elif index == 1: + return self.y + elif index == 2: + return self.z + + _get_single_internal = _get_single_external + + 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 diff --git a/parts/django/django/contrib/gis/geos/polygon.py b/parts/django/django/contrib/gis/geos/polygon.py new file mode 100644 index 0000000..92b2e4c --- /dev/null +++ b/parts/django/django/contrib/gis/geos/polygon.py @@ -0,0 +1,166 @@ +from ctypes import c_uint, byref +from django.contrib.gis.geos.error import GEOSIndexError +from django.contrib.gis.geos.geometry import GEOSGeometry +from django.contrib.gis.geos.libgeos import get_pointer_arr, GEOM_PTR +from django.contrib.gis.geos.linestring import LinearRing +from django.contrib.gis.geos import prototypes as capi + +class Polygon(GEOSGeometry): + _minlength = 1 + + 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 least one LinearRing, or a tuple, to initialize a 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)): + if len(init_holes[0]) == 0: + init_holes = () + n_holes = 0 + elif isinstance(init_holes[0][0], LinearRing): + init_holes = init_holes[0] + n_holes = len(init_holes) + + polygon = self._create_polygon(n_holes + 1, (ext_ring,) + init_holes) + super(Polygon, self).__init__(polygon, **kwargs) + + 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 + + @classmethod + def from_bbox(cls, bbox): + "Constructs a Polygon from a bounding box (4-tuple)." + x0, y0, x1, y1 = bbox + return GEOSGeometry( 'POLYGON((%s %s, %s %s, %s %s, %s %s, %s %s))' % ( + x0, y0, x0, y1, x1, y1, x1, y0, x0, y0) ) + + ### These routines are needed for list-like operation w/ListMixin ### + def _create_polygon(self, length, items): + # Instantiate LinearRing objects if necessary, but don't clone them yet + # _construct_ring will throw a TypeError if a parameter isn't a valid ring + # If we cloned the pointers here, we wouldn't be able to clean up + # in case of error. + rings = [] + for r in items: + if isinstance(r, GEOM_PTR): + rings.append(r) + else: + rings.append(self._construct_ring(r)) + + shell = self._clone(rings.pop(0)) + + n_holes = length - 1 + if n_holes: + holes = get_pointer_arr(n_holes) + for i, r in enumerate(rings): + holes[i] = self._clone(r) + holes_param = byref(holes) + else: + holes_param = None + + return capi.create_polygon(shell, holes_param, c_uint(n_holes)) + + def _clone(self, g): + if isinstance(g, GEOM_PTR): + return capi.geom_clone(g) + else: + return capi.geom_clone(g.ptr) + + def _construct_ring(self, param, msg='Parameter must be a sequence of LinearRings or objects that can initialize to LinearRings'): + "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 _set_list(self, length, items): + # Getting the current pointer, replacing with the newly constructed + # geometry, and destroying the old geometry. + prev_ptr = self.ptr + srid = self.srid + self.ptr = self._create_polygon(length, items) + if srid: self.srid = srid + capi.destroy_geom(prev_ptr) + + def _get_single_internal(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). + + CAREFUL: Internal/External are not the same as Interior/Exterior! + _get_single_internal returns a pointer from the existing geometries for use + internally by the object's methods. _get_single_external returns a clone + of the same geometry for use by external code. + """ + if index == 0: + return capi.get_extring(self.ptr) + else: + # Getting the interior ring, have to subtract 1 from the index. + return capi.get_intring(self.ptr, index-1) + + def _get_single_external(self, index): + return GEOSGeometry(capi.geom_clone(self._get_single_internal(index)), srid=self.srid) + + _set_single = GEOSGeometry._set_single_rebuild + _assign_extended_slice = GEOSGeometry._assign_extended_slice_rebuild + + #### Polygon Properties #### + @property + def num_interior_rings(self): + "Returns the number of interior rings." + # Getting the number of rings + return capi.get_nrings(self.ptr) + + def _get_ext_ring(self): + "Gets the exterior ring of the Polygon." + return self[0] + + 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(["<innerBoundaryIs>%s</innerBoundaryIs>" % self[i+1].kml + for i in xrange(self.num_interior_rings)]) + return "<Polygon><outerBoundaryIs>%s</outerBoundaryIs>%s</Polygon>" % (self[0].kml, inner_kml) diff --git a/parts/django/django/contrib/gis/geos/prepared.py b/parts/django/django/contrib/gis/geos/prepared.py new file mode 100644 index 0000000..68b812d --- /dev/null +++ b/parts/django/django/contrib/gis/geos/prepared.py @@ -0,0 +1,30 @@ +from django.contrib.gis.geos.base import GEOSBase +from django.contrib.gis.geos.geometry import GEOSGeometry +from django.contrib.gis.geos.prototypes import prepared as capi + +class PreparedGeometry(GEOSBase): + """ + A geometry that is prepared for performing certain operations. + At the moment this includes the contains covers, and intersects + operations. + """ + ptr_type = capi.PREPGEOM_PTR + + def __init__(self, geom): + if not isinstance(geom, GEOSGeometry): raise TypeError + self.ptr = capi.geos_prepare(geom.ptr) + + def __del__(self): + if self._ptr: capi.prepared_destroy(self._ptr) + + def contains(self, other): + return capi.prepared_contains(self.ptr, other.ptr) + + def contains_properly(self, other): + return capi.prepared_contains_properly(self.ptr, other.ptr) + + def covers(self, other): + return capi.prepared_covers(self.ptr, other.ptr) + + def intersects(self, other): + return capi.prepared_intersects(self.ptr, other.ptr) diff --git a/parts/django/django/contrib/gis/geos/prototypes/__init__.py b/parts/django/django/contrib/gis/geos/prototypes/__init__.py new file mode 100644 index 0000000..2355928 --- /dev/null +++ b/parts/django/django/contrib/gis/geos/prototypes/__init__.py @@ -0,0 +1,30 @@ +""" + 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 * diff --git a/parts/django/django/contrib/gis/geos/prototypes/coordseq.py b/parts/django/django/contrib/gis/geos/prototypes/coordseq.py new file mode 100644 index 0000000..68b9480 --- /dev/null +++ b/parts/django/django/contrib/gis/geos/prototypes/coordseq.py @@ -0,0 +1,83 @@ +from ctypes import c_double, c_int, c_uint, POINTER +from django.contrib.gis.geos.libgeos import GEOM_PTR, CS_PTR +from django.contrib.gis.geos.prototypes.errcheck import last_arg_byref, GEOSException +from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc + +## 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(GEOSFunc('GEOSCoordSeq_clone'), [CS_PTR]) +create_cs = cs_output(GEOSFunc('GEOSCoordSeq_create'), [c_uint, c_uint]) +get_cs = cs_output(GEOSFunc('GEOSGeom_getCoordSeq'), [GEOM_PTR]) + +# Getting, setting ordinate +cs_getordinate = cs_operation(GEOSFunc('GEOSCoordSeq_getOrdinate'), ordinate=True, get=True) +cs_setordinate = cs_operation(GEOSFunc('GEOSCoordSeq_setOrdinate'), ordinate=True) + +# For getting, x, y, z +cs_getx = cs_operation(GEOSFunc('GEOSCoordSeq_getX'), get=True) +cs_gety = cs_operation(GEOSFunc('GEOSCoordSeq_getY'), get=True) +cs_getz = cs_operation(GEOSFunc('GEOSCoordSeq_getZ'), get=True) + +# For setting, x, y, z +cs_setx = cs_operation(GEOSFunc('GEOSCoordSeq_setX')) +cs_sety = cs_operation(GEOSFunc('GEOSCoordSeq_setY')) +cs_setz = cs_operation(GEOSFunc('GEOSCoordSeq_setZ')) + +# These routines return size & dimensions. +cs_getsize = cs_int(GEOSFunc('GEOSCoordSeq_getSize')) +cs_getdims = cs_int(GEOSFunc('GEOSCoordSeq_getDimensions')) diff --git a/parts/django/django/contrib/gis/geos/prototypes/errcheck.py b/parts/django/django/contrib/gis/geos/prototypes/errcheck.py new file mode 100644 index 0000000..97fcd21 --- /dev/null +++ b/parts/django/django/contrib/gis/geos/prototypes/errcheck.py @@ -0,0 +1,95 @@ +""" + Error checking functions for GEOS ctypes prototype functions. +""" +import os +from ctypes import c_void_p, string_at, CDLL +from django.contrib.gis.geos.error import GEOSException +from django.contrib.gis.geos.libgeos import GEOS_VERSION +from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc + +# Getting the `free` routine used to free the memory allocated for +# string pointers returned by GEOS. +if GEOS_VERSION >= (3, 1, 1): + # In versions 3.1.1 and above, `GEOSFree` was added to the C API + # because `free` isn't always available on all platforms. + free = GEOSFunc('GEOSFree') + free.argtypes = [c_void_p] + free.restype = None +else: + # Getting the `free` routine from the C library of the platform. + if os.name == 'nt': + # On NT, use the MS C library. + libc = CDLL('msvcrt') + else: + # On POSIX platforms C library is obtained by passing None into `CDLL`. + libc = CDLL(None) + free = libc.free + +### ctypes error checking routines ### +def last_arg_byref(args): + "Returns the last C argument's value by reference." + 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. + + This frees the memory allocated by GEOS at the result pointer. + """ + 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)) + # Freeing the memory allocated within GEOS + free(result) + return s + +def check_string(result, func, cargs): + """ + Error checking for routines that return strings. + + This frees the memory allocated by GEOS at the result pointer. + """ + 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 within GEOS + 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/parts/django/django/contrib/gis/geos/prototypes/geom.py b/parts/django/django/contrib/gis/geos/prototypes/geom.py new file mode 100644 index 0000000..03f9897 --- /dev/null +++ b/parts/django/django/contrib/gis/geos/prototypes/geom.py @@ -0,0 +1,119 @@ +from ctypes import c_char_p, c_int, c_size_t, c_ubyte, c_uint, POINTER +from django.contrib.gis.geos.libgeos import CS_PTR, GEOM_PTR, PREPGEOM_PTR, GEOS_PREPARE +from django.contrib.gis.geos.prototypes.errcheck import \ + check_geom, check_minus_one, check_sized_string, check_string, check_zero +from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc + +# This is the return type used by binary output (WKB, HEX) routines. +c_uchar_p = POINTER(c_ubyte) + +# We create a simple subclass of c_char_p here because when the response +# type is set to c_char_p, you get a _Python_ string and there's no way +# to access the string's address inside the error checking function. +# In other words, you can't free the memory allocated inside GEOS. Previously, +# the return type would just be omitted and the integer address would be +# used -- but this allows us to be specific in the function definition and +# keeps the reference so it may be free'd. +class geos_char_p(c_char_p): + pass + +### 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 + func.restype = c_uchar_p + 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." + func.argtypes = [GEOM_PTR] + func.restype = geos_char_p + func.errcheck = check_string + return func + +### ctypes prototypes ### + +# Deprecated creation routines from WKB, HEX, WKT +from_hex = bin_constructor(GEOSFunc('GEOSGeomFromHEX_buf')) +from_wkb = bin_constructor(GEOSFunc('GEOSGeomFromWKB_buf')) +from_wkt = geom_output(GEOSFunc('GEOSGeomFromWKT'), [c_char_p]) + +# Deprecated output routines +to_hex = bin_output(GEOSFunc('GEOSGeomToHEX_buf')) +to_wkb = bin_output(GEOSFunc('GEOSGeomToWKB_buf')) +to_wkt = string_from_geom(GEOSFunc('GEOSGeomToWKT')) + +# The GEOS geometry type, typeid, num_coordites and number of geometries +geos_normalize = int_from_geom(GEOSFunc('GEOSNormalize')) +geos_type = string_from_geom(GEOSFunc('GEOSGeomType')) +geos_typeid = int_from_geom(GEOSFunc('GEOSGeomTypeId')) +get_dims = int_from_geom(GEOSFunc('GEOSGeom_getDimensions'), zero=True) +get_num_coords = int_from_geom(GEOSFunc('GEOSGetNumCoordinates')) +get_num_geoms = int_from_geom(GEOSFunc('GEOSGetNumGeometries')) + +# Geometry creation factories +create_point = geom_output(GEOSFunc('GEOSGeom_createPoint'), [CS_PTR]) +create_linestring = geom_output(GEOSFunc('GEOSGeom_createLineString'), [CS_PTR]) +create_linearring = geom_output(GEOSFunc('GEOSGeom_createLinearRing'), [CS_PTR]) + +# Polygon and collection creation routines are special and will not +# have their argument types defined. +create_polygon = geom_output(GEOSFunc('GEOSGeom_createPolygon'), None) +create_collection = geom_output(GEOSFunc('GEOSGeom_createCollection'), None) + +# Ring routines +get_extring = geom_output(GEOSFunc('GEOSGetExteriorRing'), [GEOM_PTR]) +get_intring = geom_index(GEOSFunc('GEOSGetInteriorRingN')) +get_nrings = int_from_geom(GEOSFunc('GEOSGetNumInteriorRings')) + +# Collection Routines +get_geomn = geom_index(GEOSFunc('GEOSGetGeometryN')) + +# Cloning +geom_clone = GEOSFunc('GEOSGeom_clone') +geom_clone.argtypes = [GEOM_PTR] +geom_clone.restype = GEOM_PTR + +# Destruction routine. +destroy_geom = GEOSFunc('GEOSGeom_destroy') +destroy_geom.argtypes = [GEOM_PTR] +destroy_geom.restype = None + +# SRID routines +geos_get_srid = GEOSFunc('GEOSGetSRID') +geos_get_srid.argtypes = [GEOM_PTR] +geos_get_srid.restype = c_int + +geos_set_srid = GEOSFunc('GEOSSetSRID') +geos_set_srid.argtypes = [GEOM_PTR, c_int] +geos_set_srid.restype = None diff --git a/parts/django/django/contrib/gis/geos/prototypes/io.py b/parts/django/django/contrib/gis/geos/prototypes/io.py new file mode 100644 index 0000000..5c0c8b5 --- /dev/null +++ b/parts/django/django/contrib/gis/geos/prototypes/io.py @@ -0,0 +1,242 @@ +import threading +from ctypes import byref, c_char_p, c_int, c_char, c_size_t, Structure, POINTER +from django.contrib.gis.geos.base import GEOSBase +from django.contrib.gis.geos.libgeos import GEOM_PTR +from django.contrib.gis.geos.prototypes.errcheck import check_geom, check_string, check_sized_string +from django.contrib.gis.geos.prototypes.geom import c_uchar_p, geos_char_p +from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc + +### The WKB/WKT Reader/Writer structures and pointers ### +class WKTReader_st(Structure): pass +class WKTWriter_st(Structure): pass +class WKBReader_st(Structure): pass +class WKBWriter_st(Structure): pass + +WKT_READ_PTR = POINTER(WKTReader_st) +WKT_WRITE_PTR = POINTER(WKTWriter_st) +WKB_READ_PTR = POINTER(WKBReader_st) +WKB_WRITE_PTR = POINTER(WKBReader_st) + +### WKTReader routines ### +wkt_reader_create = GEOSFunc('GEOSWKTReader_create') +wkt_reader_create.restype = WKT_READ_PTR + +wkt_reader_destroy = GEOSFunc('GEOSWKTReader_destroy') +wkt_reader_destroy.argtypes = [WKT_READ_PTR] + +wkt_reader_read = GEOSFunc('GEOSWKTReader_read') +wkt_reader_read.argtypes = [WKT_READ_PTR, c_char_p] +wkt_reader_read.restype = GEOM_PTR +wkt_reader_read.errcheck = check_geom + +### WKTWriter routines ### +wkt_writer_create = GEOSFunc('GEOSWKTWriter_create') +wkt_writer_create.restype = WKT_WRITE_PTR + +wkt_writer_destroy = GEOSFunc('GEOSWKTWriter_destroy') +wkt_writer_destroy.argtypes = [WKT_WRITE_PTR] + +wkt_writer_write = GEOSFunc('GEOSWKTWriter_write') +wkt_writer_write.argtypes = [WKT_WRITE_PTR, GEOM_PTR] +wkt_writer_write.restype = geos_char_p +wkt_writer_write.errcheck = check_string + +### WKBReader routines ### +wkb_reader_create = GEOSFunc('GEOSWKBReader_create') +wkb_reader_create.restype = WKB_READ_PTR + +wkb_reader_destroy = GEOSFunc('GEOSWKBReader_destroy') +wkb_reader_destroy.argtypes = [WKB_READ_PTR] + +def wkb_read_func(func): + # Although the function definitions take `const unsigned char *` + # as their parameter, we use c_char_p here so the function may + # take Python strings directly as parameters. Inside Python there + # is not a difference between signed and unsigned characters, so + # it is not a problem. + func.argtypes = [WKB_READ_PTR, c_char_p, c_size_t] + func.restype = GEOM_PTR + func.errcheck = check_geom + return func + +wkb_reader_read = wkb_read_func(GEOSFunc('GEOSWKBReader_read')) +wkb_reader_read_hex = wkb_read_func(GEOSFunc('GEOSWKBReader_readHEX')) + +### WKBWriter routines ### +wkb_writer_create = GEOSFunc('GEOSWKBWriter_create') +wkb_writer_create.restype = WKB_WRITE_PTR + +wkb_writer_destroy = GEOSFunc('GEOSWKBWriter_destroy') +wkb_writer_destroy.argtypes = [WKB_WRITE_PTR] + +# WKB Writing prototypes. +def wkb_write_func(func): + func.argtypes = [WKB_WRITE_PTR, GEOM_PTR, POINTER(c_size_t)] + func.restype = c_uchar_p + func.errcheck = check_sized_string + return func + +wkb_writer_write = wkb_write_func(GEOSFunc('GEOSWKBWriter_write')) +wkb_writer_write_hex = wkb_write_func(GEOSFunc('GEOSWKBWriter_writeHEX')) + +# WKBWriter property getter/setter prototypes. +def wkb_writer_get(func, restype=c_int): + func.argtypes = [WKB_WRITE_PTR] + func.restype = restype + return func + +def wkb_writer_set(func, argtype=c_int): + func.argtypes = [WKB_WRITE_PTR, argtype] + return func + +wkb_writer_get_byteorder = wkb_writer_get(GEOSFunc('GEOSWKBWriter_getByteOrder')) +wkb_writer_set_byteorder = wkb_writer_set(GEOSFunc('GEOSWKBWriter_setByteOrder')) +wkb_writer_get_outdim = wkb_writer_get(GEOSFunc('GEOSWKBWriter_getOutputDimension')) +wkb_writer_set_outdim = wkb_writer_set(GEOSFunc('GEOSWKBWriter_setOutputDimension')) +wkb_writer_get_include_srid = wkb_writer_get(GEOSFunc('GEOSWKBWriter_getIncludeSRID'), restype=c_char) +wkb_writer_set_include_srid = wkb_writer_set(GEOSFunc('GEOSWKBWriter_setIncludeSRID'), argtype=c_char) + +### Base I/O Class ### +class IOBase(GEOSBase): + "Base class for GEOS I/O objects." + def __init__(self): + # Getting the pointer with the constructor. + self.ptr = self._constructor() + + def __del__(self): + # Cleaning up with the appropriate destructor. + if self._ptr: self._destructor(self._ptr) + +### Base WKB/WKT Reading and Writing objects ### + +# Non-public WKB/WKT reader classes for internal use because +# their `read` methods return _pointers_ instead of GEOSGeometry +# objects. +class _WKTReader(IOBase): + _constructor = wkt_reader_create + _destructor = wkt_reader_destroy + ptr_type = WKT_READ_PTR + + def read(self, wkt): + if not isinstance(wkt, basestring): raise TypeError + return wkt_reader_read(self.ptr, wkt) + +class _WKBReader(IOBase): + _constructor = wkb_reader_create + _destructor = wkb_reader_destroy + ptr_type = WKB_READ_PTR + + def read(self, wkb): + "Returns a _pointer_ to C GEOS Geometry object from the given WKB." + if isinstance(wkb, buffer): + wkb_s = str(wkb) + return wkb_reader_read(self.ptr, wkb_s, len(wkb_s)) + elif isinstance(wkb, basestring): + return wkb_reader_read_hex(self.ptr, wkb, len(wkb)) + else: + raise TypeError + +### WKB/WKT Writer Classes ### +class WKTWriter(IOBase): + _constructor = wkt_writer_create + _destructor = wkt_writer_destroy + ptr_type = WKT_WRITE_PTR + + def write(self, geom): + "Returns the WKT representation of the given geometry." + return wkt_writer_write(self.ptr, geom.ptr) + +class WKBWriter(IOBase): + _constructor = wkb_writer_create + _destructor = wkb_writer_destroy + ptr_type = WKB_WRITE_PTR + + def write(self, geom): + "Returns the WKB representation of the given geometry." + return buffer(wkb_writer_write(self.ptr, geom.ptr, byref(c_size_t()))) + + def write_hex(self, geom): + "Returns the HEXEWKB representation of the given geometry." + return wkb_writer_write_hex(self.ptr, geom.ptr, byref(c_size_t())) + + ### WKBWriter Properties ### + + # Property for getting/setting the byteorder. + def _get_byteorder(self): + return wkb_writer_get_byteorder(self.ptr) + + def _set_byteorder(self, order): + if not order in (0, 1): raise ValueError('Byte order parameter must be 0 (Big Endian) or 1 (Little Endian).') + wkb_writer_set_byteorder(self.ptr, order) + + byteorder = property(_get_byteorder, _set_byteorder) + + # Property for getting/setting the output dimension. + def _get_outdim(self): + return wkb_writer_get_outdim(self.ptr) + + def _set_outdim(self, new_dim): + if not new_dim in (2, 3): raise ValueError('WKB output dimension must be 2 or 3') + wkb_writer_set_outdim(self.ptr, new_dim) + + outdim = property(_get_outdim, _set_outdim) + + # Property for getting/setting the include srid flag. + def _get_include_srid(self): + return bool(ord(wkb_writer_get_include_srid(self.ptr))) + + def _set_include_srid(self, include): + if bool(include): flag = chr(1) + else: flag = chr(0) + wkb_writer_set_include_srid(self.ptr, flag) + + srid = property(_get_include_srid, _set_include_srid) + +# `ThreadLocalIO` object holds instances of the WKT and WKB reader/writer +# objects that are local to the thread. The `GEOSGeometry` internals +# access these instances by calling the module-level functions, defined +# below. +class ThreadLocalIO(threading.local): + wkt_r = None + wkt_w = None + wkb_r = None + wkb_w = None + ewkb_w = None + ewkb_w3d = None + +thread_context = ThreadLocalIO() + +# These module-level routines return the I/O object that is local to the +# the thread. If the I/O object does not exist yet it will be initialized. +def wkt_r(): + if not thread_context.wkt_r: + thread_context.wkt_r = _WKTReader() + return thread_context.wkt_r + +def wkt_w(): + if not thread_context.wkt_w: + thread_context.wkt_w = WKTWriter() + return thread_context.wkt_w + +def wkb_r(): + if not thread_context.wkb_r: + thread_context.wkb_r = _WKBReader() + return thread_context.wkb_r + +def wkb_w(): + if not thread_context.wkb_w: + thread_context.wkb_w = WKBWriter() + return thread_context.wkb_w + +def ewkb_w(): + if not thread_context.ewkb_w: + thread_context.ewkb_w = WKBWriter() + thread_context.ewkb_w.srid = True + return thread_context.ewkb_w + +def ewkb_w3d(): + if not thread_context.ewkb_w3d: + thread_context.ewkb_w3d = WKBWriter() + thread_context.ewkb_w3d.srid = True + thread_context.ewkb_w3d.outdim = 3 + return thread_context.ewkb_w3d diff --git a/parts/django/django/contrib/gis/geos/prototypes/misc.py b/parts/django/django/contrib/gis/geos/prototypes/misc.py new file mode 100644 index 0000000..5b3b658 --- /dev/null +++ b/parts/django/django/contrib/gis/geos/prototypes/misc.py @@ -0,0 +1,28 @@ +""" + 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 GEOM_PTR +from django.contrib.gis.geos.prototypes.errcheck import check_dbl +from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc + +### 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(GEOSFunc('GEOSArea')) +geos_distance = dbl_from_geom(GEOSFunc('GEOSDistance'), num_geom=2) +geos_length = dbl_from_geom(GEOSFunc('GEOSLength')) diff --git a/parts/django/django/contrib/gis/geos/prototypes/predicates.py b/parts/django/django/contrib/gis/geos/prototypes/predicates.py new file mode 100644 index 0000000..bf69bb1 --- /dev/null +++ b/parts/django/django/contrib/gis/geos/prototypes/predicates.py @@ -0,0 +1,44 @@ +""" + 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 GEOM_PTR +from django.contrib.gis.geos.prototypes.errcheck import check_predicate +from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc + +## 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(GEOSFunc('GEOSHasZ')) +geos_isempty = unary_predicate(GEOSFunc('GEOSisEmpty')) +geos_isring = unary_predicate(GEOSFunc('GEOSisRing')) +geos_issimple = unary_predicate(GEOSFunc('GEOSisSimple')) +geos_isvalid = unary_predicate(GEOSFunc('GEOSisValid')) + +## Binary Predicates ## +geos_contains = binary_predicate(GEOSFunc('GEOSContains')) +geos_crosses = binary_predicate(GEOSFunc('GEOSCrosses')) +geos_disjoint = binary_predicate(GEOSFunc('GEOSDisjoint')) +geos_equals = binary_predicate(GEOSFunc('GEOSEquals')) +geos_equalsexact = binary_predicate(GEOSFunc('GEOSEqualsExact'), c_double) +geos_intersects = binary_predicate(GEOSFunc('GEOSIntersects')) +geos_overlaps = binary_predicate(GEOSFunc('GEOSOverlaps')) +geos_relatepattern = binary_predicate(GEOSFunc('GEOSRelatePattern'), c_char_p) +geos_touches = binary_predicate(GEOSFunc('GEOSTouches')) +geos_within = binary_predicate(GEOSFunc('GEOSWithin')) diff --git a/parts/django/django/contrib/gis/geos/prototypes/prepared.py b/parts/django/django/contrib/gis/geos/prototypes/prepared.py new file mode 100644 index 0000000..7342d7d --- /dev/null +++ b/parts/django/django/contrib/gis/geos/prototypes/prepared.py @@ -0,0 +1,25 @@ +from ctypes import c_char +from django.contrib.gis.geos.libgeos import GEOM_PTR, PREPGEOM_PTR +from django.contrib.gis.geos.prototypes.errcheck import check_predicate +from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc + +# Prepared geometry constructor and destructors. +geos_prepare = GEOSFunc('GEOSPrepare') +geos_prepare.argtypes = [GEOM_PTR] +geos_prepare.restype = PREPGEOM_PTR + +prepared_destroy = GEOSFunc('GEOSPreparedGeom_destroy') +prepared_destroy.argtpes = [PREPGEOM_PTR] +prepared_destroy.restype = None + +# Prepared geometry binary predicate support. +def prepared_predicate(func): + func.argtypes= [PREPGEOM_PTR, GEOM_PTR] + func.restype = c_char + func.errcheck = check_predicate + return func + +prepared_contains = prepared_predicate(GEOSFunc('GEOSPreparedContains')) +prepared_contains_properly = prepared_predicate(GEOSFunc('GEOSPreparedContainsProperly')) +prepared_covers = prepared_predicate(GEOSFunc('GEOSPreparedCovers')) +prepared_intersects = prepared_predicate(GEOSFunc('GEOSPreparedIntersects')) diff --git a/parts/django/django/contrib/gis/geos/prototypes/threadsafe.py b/parts/django/django/contrib/gis/geos/prototypes/threadsafe.py new file mode 100644 index 0000000..5888ed1 --- /dev/null +++ b/parts/django/django/contrib/gis/geos/prototypes/threadsafe.py @@ -0,0 +1,90 @@ +import threading +from django.contrib.gis.geos.libgeos import lgeos, notice_h, error_h, CONTEXT_PTR + +class GEOSContextHandle(object): + """ + Python object representing a GEOS context handle. + """ + def __init__(self): + # Initializing the context handler for this thread with + # the notice and error handler. + self.ptr = lgeos.initGEOS_r(notice_h, error_h) + + def __del__(self): + if self.ptr: lgeos.finishGEOS_r(self.ptr) + +# Defining a thread-local object and creating an instance +# to hold a reference to GEOSContextHandle for this thread. +class GEOSContext(threading.local): + handle = None + +thread_context = GEOSContext() + +def call_geos_threaded(cfunc, args): + """ + This module-level routine calls the specified GEOS C thread-safe + function with the context for this current thread. + """ + # If a context handle does not exist for this thread, initialize one. + if not thread_context.handle: + thread_context.handle = GEOSContextHandle() + # Call the threaded GEOS routine with pointer of the context handle + # as the first argument. + return cfunc(thread_context.handle.ptr, *args) + +class GEOSFunc(object): + """ + Class that serves as a wrapper for GEOS C Functions, and will + use thread-safe function variants when available. + """ + def __init__(self, func_name): + try: + # GEOS thread-safe function signatures end with '_r', and + # take an additional context handle parameter. + self.cfunc = getattr(lgeos, func_name + '_r') + self.threaded = True + except AttributeError: + # Otherwise, use usual function. + self.cfunc = getattr(lgeos, func_name) + self.threaded = False + + def __call__(self, *args): + if self.threaded: + return call_geos_threaded(self.cfunc, args) + else: + return self.cfunc(*args) + + def __str__(self): + return self.cfunc.__name__ + + # argtypes property + def _get_argtypes(self): + return self.cfunc.argtypes + + def _set_argtypes(self, argtypes): + if self.threaded: + new_argtypes = [CONTEXT_PTR] + new_argtypes.extend(argtypes) + self.cfunc.argtypes = new_argtypes + else: + self.cfunc.argtypes = argtypes + + argtypes = property(_get_argtypes, _set_argtypes) + + # restype property + def _get_restype(self): + return self.cfunc.restype + + def _set_restype(self, restype): + self.cfunc.restype = restype + + restype = property(_get_restype, _set_restype) + + # errcheck property + def _get_errcheck(self): + return self.cfunc.errcheck + + def _set_errcheck(self, errcheck): + self.cfunc.errcheck = errcheck + + errcheck = property(_get_errcheck, _set_errcheck) diff --git a/parts/django/django/contrib/gis/geos/prototypes/topology.py b/parts/django/django/contrib/gis/geos/prototypes/topology.py new file mode 100644 index 0000000..50817f9 --- /dev/null +++ b/parts/django/django/contrib/gis/geos/prototypes/topology.py @@ -0,0 +1,51 @@ +""" + This module houses the GEOS ctypes prototype functions for the + topological operations on geometries. +""" +__all__ = ['geos_boundary', 'geos_buffer', 'geos_centroid', 'geos_convexhull', + 'geos_difference', 'geos_envelope', 'geos_intersection', + 'geos_linemerge', 'geos_pointonsurface', 'geos_preservesimplify', + 'geos_simplify', 'geos_symdifference', 'geos_union', 'geos_relate'] + +from ctypes import c_char_p, c_double, c_int +from django.contrib.gis.geos.libgeos import GEOM_PTR, GEOS_PREPARE +from django.contrib.gis.geos.prototypes.errcheck import check_geom, check_string +from django.contrib.gis.geos.prototypes.geom import geos_char_p +from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc + +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(GEOSFunc('GEOSBoundary')) +geos_buffer = topology(GEOSFunc('GEOSBuffer'), c_double, c_int) +geos_centroid = topology(GEOSFunc('GEOSGetCentroid')) +geos_convexhull = topology(GEOSFunc('GEOSConvexHull')) +geos_difference = topology(GEOSFunc('GEOSDifference'), GEOM_PTR) +geos_envelope = topology(GEOSFunc('GEOSEnvelope')) +geos_intersection = topology(GEOSFunc('GEOSIntersection'), GEOM_PTR) +geos_linemerge = topology(GEOSFunc('GEOSLineMerge')) +geos_pointonsurface = topology(GEOSFunc('GEOSPointOnSurface')) +geos_preservesimplify = topology(GEOSFunc('GEOSTopologyPreserveSimplify'), c_double) +geos_simplify = topology(GEOSFunc('GEOSSimplify'), c_double) +geos_symdifference = topology(GEOSFunc('GEOSSymDifference'), GEOM_PTR) +geos_union = topology(GEOSFunc('GEOSUnion'), GEOM_PTR) + +# GEOSRelate returns a string, not a geometry. +geos_relate = GEOSFunc('GEOSRelate') +geos_relate.argtypes = [GEOM_PTR, GEOM_PTR] +geos_relate.restype = geos_char_p +geos_relate.errcheck = check_string + +# Routines only in GEOS 3.1+ +if GEOS_PREPARE: + geos_cascaded_union = GEOSFunc('GEOSUnionCascaded') + geos_cascaded_union.argtypes = [GEOM_PTR] + geos_cascaded_union.restype = GEOM_PTR + __all__.append('geos_cascaded_union') diff --git a/parts/django/django/contrib/gis/geos/tests/__init__.py b/parts/django/django/contrib/gis/geos/tests/__init__.py new file mode 100644 index 0000000..44c8e26 --- /dev/null +++ b/parts/django/django/contrib/gis/geos/tests/__init__.py @@ -0,0 +1,25 @@ +""" +GEOS Testing module. +""" +from unittest import TestSuite, TextTestRunner +import test_geos, test_io, test_geos_mutation, test_mutable_list + +test_suites = [ + test_geos.suite(), + test_io.suite(), + test_geos_mutation.suite(), + test_mutable_list.suite(), + ] + +def suite(): + "Builds a test suite for the GEOS tests." + s = TestSuite() + map(s.addTest, test_suites) + return s + +def run(verbosity=1): + "Runs the GEOS tests." + TextTestRunner(verbosity=verbosity).run(suite()) + +if __name__ == '__main__': + run(2) diff --git a/parts/django/django/contrib/gis/geos/tests/test_geos.py b/parts/django/django/contrib/gis/geos/tests/test_geos.py new file mode 100644 index 0000000..3cd021e --- /dev/null +++ b/parts/django/django/contrib/gis/geos/tests/test_geos.py @@ -0,0 +1,926 @@ +import ctypes, random, unittest, sys +from django.contrib.gis.geos import * +from django.contrib.gis.geos.base import gdal, numpy, GEOSBase +from django.contrib.gis.geometry.test_data import TestDataMixin + +class GEOSTest(unittest.TestCase, TestDataMixin): + + @property + def null_srid(self): + """ + Returns the proper null SRID depending on the GEOS version. + See the comments in `test15_srid` for more details. + """ + info = geos_version_info() + if info['version'] == '3.0.0' and info['release_candidate']: + return -1 + else: + return None + + def test00_base(self): + "Tests out the GEOSBase class." + # Testing out GEOSBase class, which provides a `ptr` property + # that abstracts out access to underlying C pointers. + class FakeGeom1(GEOSBase): + pass + + # This one only accepts pointers to floats + c_float_p = ctypes.POINTER(ctypes.c_float) + class FakeGeom2(GEOSBase): + ptr_type = c_float_p + + # Default ptr_type is `c_void_p`. + fg1 = FakeGeom1() + # Default ptr_type is C float pointer + fg2 = FakeGeom2() + + # These assignments are OK -- None is allowed because + # it's equivalent to the NULL pointer. + fg1.ptr = ctypes.c_void_p() + fg1.ptr = None + fg2.ptr = c_float_p(ctypes.c_float(5.23)) + fg2.ptr = None + + # Because pointers have been set to NULL, an exception should be + # raised when we try to access it. Raising an exception is + # preferrable to a segmentation fault that commonly occurs when + # a C method is given a NULL memory reference. + for fg in (fg1, fg2): + # Equivalent to `fg.ptr` + self.assertRaises(GEOSException, fg._get_ptr) + + # Anything that is either not None or the acceptable pointer type will + # result in a TypeError when trying to assign it to the `ptr` property. + # Thus, memmory addresses (integers) and pointers of the incorrect type + # (in `bad_ptrs`) will not be allowed. + bad_ptrs = (5, ctypes.c_char_p('foobar')) + for bad_ptr in bad_ptrs: + # Equivalent to `fg.ptr = bad_ptr` + self.assertRaises(TypeError, fg1._set_ptr, bad_ptr) + self.assertRaises(TypeError, fg2._set_ptr, bad_ptr) + + def test01a_wkt(self): + "Testing WKT output." + for g in self.geometries.wkt_out: + geom = fromstr(g.wkt) + self.assertEqual(g.ewkt, geom.wkt) + + def test01b_hex(self): + "Testing HEX output." + for g in self.geometries.hex_wkt: + geom = fromstr(g.wkt) + self.assertEqual(g.hex, geom.hex) + + def test01b_hexewkb(self): + "Testing (HEX)EWKB output." + from binascii import a2b_hex + + # For testing HEX(EWKB). + ogc_hex = '01010000000000000000000000000000000000F03F' + # `SELECT ST_AsHEXEWKB(ST_GeomFromText('POINT(0 1)', 4326));` + hexewkb_2d = '0101000020E61000000000000000000000000000000000F03F' + # `SELECT ST_AsHEXEWKB(ST_GeomFromEWKT('SRID=4326;POINT(0 1 2)'));` + hexewkb_3d = '01010000A0E61000000000000000000000000000000000F03F0000000000000040' + + pnt_2d = Point(0, 1, srid=4326) + pnt_3d = Point(0, 1, 2, srid=4326) + + # OGC-compliant HEX will not have SRID nor Z value. + self.assertEqual(ogc_hex, pnt_2d.hex) + self.assertEqual(ogc_hex, pnt_3d.hex) + + # HEXEWKB should be appropriate for its dimension -- have to use an + # a WKBWriter w/dimension set accordingly, else GEOS will insert + # garbage into 3D coordinate if there is none. Also, GEOS has a + # a bug in versions prior to 3.1 that puts the X coordinate in + # place of Z; an exception should be raised on those versions. + self.assertEqual(hexewkb_2d, pnt_2d.hexewkb) + if GEOS_PREPARE: + self.assertEqual(hexewkb_3d, pnt_3d.hexewkb) + self.assertEqual(True, GEOSGeometry(hexewkb_3d).hasz) + else: + try: + hexewkb = pnt_3d.hexewkb + except GEOSException: + pass + else: + self.fail('Should have raised GEOSException.') + + # Same for EWKB. + self.assertEqual(buffer(a2b_hex(hexewkb_2d)), pnt_2d.ewkb) + if GEOS_PREPARE: + self.assertEqual(buffer(a2b_hex(hexewkb_3d)), pnt_3d.ewkb) + else: + try: + ewkb = pnt_3d.ewkb + except GEOSException: + pass + else: + self.fail('Should have raised GEOSException') + + # Redundant sanity check. + self.assertEqual(4326, GEOSGeometry(hexewkb_2d).srid) + + def test01c_kml(self): + "Testing KML output." + for tg in self.geometries.wkt_out: + geom = fromstr(tg.wkt) + kml = getattr(tg, 'kml', False) + if kml: self.assertEqual(kml, geom.kml) + + def test01d_errors(self): + "Testing the Error handlers." + # string-based + print "\nBEGIN - expecting GEOS_ERROR; safe to ignore.\n" + for err in self.geometries.errors: + try: + g = fromstr(err.wkt) + except (GEOSException, ValueError): + pass + + # Bad WKB + self.assertRaises(GEOSException, GEOSGeometry, buffer('0')) + + print "\nEND - expecting GEOS_ERROR; safe to ignore.\n" + + class NotAGeometry(object): + pass + + # Some other object + self.assertRaises(TypeError, GEOSGeometry, NotAGeometry()) + # None + self.assertRaises(TypeError, GEOSGeometry, None) + + def test01e_wkb(self): + "Testing WKB output." + from binascii import b2a_hex + for g in self.geometries.hex_wkt: + geom = fromstr(g.wkt) + wkb = geom.wkb + self.assertEqual(b2a_hex(wkb).upper(), g.hex) + + def test01f_create_hex(self): + "Testing creation from HEX." + for g in self.geometries.hex_wkt: + geom_h = GEOSGeometry(g.hex) + # we need to do this so decimal places get normalised + geom_t = fromstr(g.wkt) + self.assertEqual(geom_t.wkt, geom_h.wkt) + + def test01g_create_wkb(self): + "Testing creation from WKB." + from binascii import a2b_hex + for g in self.geometries.hex_wkt: + wkb = buffer(a2b_hex(g.hex)) + geom_h = GEOSGeometry(wkb) + # we need to do this so decimal places get normalised + geom_t = fromstr(g.wkt) + self.assertEqual(geom_t.wkt, geom_h.wkt) + + def test01h_ewkt(self): + "Testing EWKT." + srid = 32140 + for p in self.geometries.polygons: + ewkt = 'SRID=%d;%s' % (srid, p.wkt) + poly = fromstr(ewkt) + self.assertEqual(srid, poly.srid) + self.assertEqual(srid, poly.shell.srid) + self.assertEqual(srid, fromstr(poly.ewkt).srid) # Checking export + + def test01i_json(self): + "Testing GeoJSON input/output (via GDAL)." + if not gdal or not gdal.GEOJSON: return + for g in self.geometries.json_geoms: + geom = GEOSGeometry(g.wkt) + if not hasattr(g, 'not_equal'): + self.assertEqual(g.json, geom.json) + self.assertEqual(g.json, geom.geojson) + self.assertEqual(GEOSGeometry(g.wkt), GEOSGeometry(geom.json)) + + def test01k_fromfile(self): + "Testing the fromfile() factory." + from StringIO import StringIO + ref_pnt = GEOSGeometry('POINT(5 23)') + + wkt_f = StringIO() + wkt_f.write(ref_pnt.wkt) + wkb_f = StringIO() + wkb_f.write(str(ref_pnt.wkb)) + + # Other tests use `fromfile()` on string filenames so those + # aren't tested here. + for fh in (wkt_f, wkb_f): + fh.seek(0) + pnt = fromfile(fh) + self.assertEqual(ref_pnt, pnt) + + def test01k_eq(self): + "Testing equivalence." + p = fromstr('POINT(5 23)') + self.assertEqual(p, p.wkt) + self.assertNotEqual(p, 'foo') + ls = fromstr('LINESTRING(0 0, 1 1, 5 5)') + self.assertEqual(ls, ls.wkt) + self.assertNotEqual(p, 'bar') + # Error shouldn't be raise on equivalence testing with + # an invalid type. + for g in (p, ls): + self.assertNotEqual(g, None) + self.assertNotEqual(g, {'foo' : 'bar'}) + self.assertNotEqual(g, False) + + def test02a_points(self): + "Testing Point objects." + prev = fromstr('POINT(0 0)') + for p in self.geometries.points: + # Creating the point from the WKT + pnt = fromstr(p.wkt) + self.assertEqual(pnt.geom_type, 'Point') + self.assertEqual(pnt.geom_typeid, 0) + self.assertEqual(p.x, pnt.x) + self.assertEqual(p.y, pnt.y) + self.assertEqual(True, pnt == fromstr(p.wkt)) + self.assertEqual(False, pnt == prev) + + # Making sure that the point's X, Y components are what we expect + self.assertAlmostEqual(p.x, pnt.tuple[0], 9) + self.assertAlmostEqual(p.y, pnt.tuple[1], 9) + + # Testing the third dimension, and getting the tuple arguments + if hasattr(p, 'z'): + self.assertEqual(True, pnt.hasz) + self.assertEqual(p.z, pnt.z) + self.assertEqual(p.z, pnt.tuple[2], 9) + tup_args = (p.x, p.y, p.z) + set_tup1 = (2.71, 3.14, 5.23) + set_tup2 = (5.23, 2.71, 3.14) + else: + self.assertEqual(False, pnt.hasz) + self.assertEqual(None, pnt.z) + tup_args = (p.x, p.y) + set_tup1 = (2.71, 3.14) + set_tup2 = (3.14, 2.71) + + # Centroid operation on point should be point itself + self.assertEqual(p.centroid, pnt.centroid.tuple) + + # Now testing the different constructors + pnt2 = Point(tup_args) # e.g., Point((1, 2)) + pnt3 = Point(*tup_args) # e.g., Point(1, 2) + self.assertEqual(True, pnt == pnt2) + self.assertEqual(True, pnt == pnt3) + + # Now testing setting the x and y + pnt.y = 3.14 + pnt.x = 2.71 + self.assertEqual(3.14, pnt.y) + self.assertEqual(2.71, pnt.x) + + # Setting via the tuple/coords property + pnt.tuple = set_tup1 + self.assertEqual(set_tup1, pnt.tuple) + pnt.coords = set_tup2 + self.assertEqual(set_tup2, pnt.coords) + + prev = pnt # setting the previous geometry + + def test02b_multipoints(self): + "Testing MultiPoint objects." + for mp in self.geometries.multipoints: + mpnt = fromstr(mp.wkt) + self.assertEqual(mpnt.geom_type, 'MultiPoint') + self.assertEqual(mpnt.geom_typeid, 4) + + self.assertAlmostEqual(mp.centroid[0], mpnt.centroid.tuple[0], 9) + self.assertAlmostEqual(mp.centroid[1], mpnt.centroid.tuple[1], 9) + + self.assertRaises(GEOSIndexError, mpnt.__getitem__, len(mpnt)) + self.assertEqual(mp.centroid, mpnt.centroid.tuple) + self.assertEqual(mp.coords, tuple(m.tuple for m in mpnt)) + for p in mpnt: + self.assertEqual(p.geom_type, 'Point') + self.assertEqual(p.geom_typeid, 0) + self.assertEqual(p.empty, False) + self.assertEqual(p.valid, True) + + def test03a_linestring(self): + "Testing LineString objects." + prev = fromstr('POINT(0 0)') + for l in self.geometries.linestrings: + ls = fromstr(l.wkt) + self.assertEqual(ls.geom_type, 'LineString') + self.assertEqual(ls.geom_typeid, 1) + self.assertEqual(ls.empty, False) + self.assertEqual(ls.ring, False) + if hasattr(l, 'centroid'): + self.assertEqual(l.centroid, ls.centroid.tuple) + if hasattr(l, 'tup'): + self.assertEqual(l.tup, ls.tuple) + + self.assertEqual(True, ls == fromstr(l.wkt)) + self.assertEqual(False, ls == prev) + self.assertRaises(GEOSIndexError, ls.__getitem__, len(ls)) + prev = ls + + # Creating a LineString from a tuple, list, and numpy array + self.assertEqual(ls, LineString(ls.tuple)) # tuple + self.assertEqual(ls, LineString(*ls.tuple)) # as individual arguments + self.assertEqual(ls, LineString([list(tup) for tup in ls.tuple])) # as list + self.assertEqual(ls.wkt, LineString(*tuple(Point(tup) for tup in ls.tuple)).wkt) # Point individual arguments + if numpy: self.assertEqual(ls, LineString(numpy.array(ls.tuple))) # as numpy array + + def test03b_multilinestring(self): + "Testing MultiLineString objects." + prev = fromstr('POINT(0 0)') + for l in self.geometries.multilinestrings: + ml = fromstr(l.wkt) + self.assertEqual(ml.geom_type, 'MultiLineString') + self.assertEqual(ml.geom_typeid, 5) + + self.assertAlmostEqual(l.centroid[0], ml.centroid.x, 9) + self.assertAlmostEqual(l.centroid[1], ml.centroid.y, 9) + + self.assertEqual(True, ml == fromstr(l.wkt)) + self.assertEqual(False, ml == prev) + prev = ml + + for ls in ml: + self.assertEqual(ls.geom_type, 'LineString') + self.assertEqual(ls.geom_typeid, 1) + self.assertEqual(ls.empty, False) + + self.assertRaises(GEOSIndexError, ml.__getitem__, len(ml)) + self.assertEqual(ml.wkt, MultiLineString(*tuple(s.clone() for s in ml)).wkt) + self.assertEqual(ml, MultiLineString(*tuple(LineString(s.tuple) for s in ml))) + + def test04_linearring(self): + "Testing LinearRing objects." + for rr in self.geometries.linearrings: + lr = fromstr(rr.wkt) + self.assertEqual(lr.geom_type, 'LinearRing') + self.assertEqual(lr.geom_typeid, 2) + self.assertEqual(rr.n_p, len(lr)) + self.assertEqual(True, lr.valid) + self.assertEqual(False, lr.empty) + + # Creating a LinearRing from a tuple, list, and numpy array + self.assertEqual(lr, LinearRing(lr.tuple)) + self.assertEqual(lr, LinearRing(*lr.tuple)) + self.assertEqual(lr, LinearRing([list(tup) for tup in lr.tuple])) + if numpy: self.assertEqual(lr, LinearRing(numpy.array(lr.tuple))) + + def test05a_polygons(self): + "Testing Polygon objects." + + # Testing `from_bbox` class method + bbox = (-180, -90, 180, 90) + p = Polygon.from_bbox( bbox ) + self.assertEqual(bbox, p.extent) + + prev = fromstr('POINT(0 0)') + for p in self.geometries.polygons: + # Creating the Polygon, testing its properties. + poly = fromstr(p.wkt) + self.assertEqual(poly.geom_type, 'Polygon') + self.assertEqual(poly.geom_typeid, 3) + self.assertEqual(poly.empty, False) + self.assertEqual(poly.ring, False) + self.assertEqual(p.n_i, poly.num_interior_rings) + self.assertEqual(p.n_i + 1, len(poly)) # Testing __len__ + self.assertEqual(p.n_p, poly.num_points) + + # Area & Centroid + self.assertAlmostEqual(p.area, poly.area, 9) + self.assertAlmostEqual(p.centroid[0], poly.centroid.tuple[0], 9) + self.assertAlmostEqual(p.centroid[1], poly.centroid.tuple[1], 9) + + # Testing the geometry equivalence + self.assertEqual(True, poly == fromstr(p.wkt)) + self.assertEqual(False, poly == prev) # Should not be equal to previous geometry + self.assertEqual(True, poly != prev) + + # Testing the exterior ring + ring = poly.exterior_ring + self.assertEqual(ring.geom_type, 'LinearRing') + self.assertEqual(ring.geom_typeid, 2) + if p.ext_ring_cs: + self.assertEqual(p.ext_ring_cs, ring.tuple) + self.assertEqual(p.ext_ring_cs, poly[0].tuple) # Testing __getitem__ + + # Testing __getitem__ and __setitem__ on invalid indices + self.assertRaises(GEOSIndexError, poly.__getitem__, len(poly)) + self.assertRaises(GEOSIndexError, poly.__setitem__, len(poly), False) + self.assertRaises(GEOSIndexError, poly.__getitem__, -1 * len(poly) - 1) + + # Testing __iter__ + for r in poly: + self.assertEqual(r.geom_type, 'LinearRing') + self.assertEqual(r.geom_typeid, 2) + + # Testing polygon construction. + self.assertRaises(TypeError, Polygon.__init__, 0, [1, 2, 3]) + self.assertRaises(TypeError, Polygon.__init__, 'foo') + + # Polygon(shell, (hole1, ... holeN)) + rings = tuple(r for r in poly) + self.assertEqual(poly, Polygon(rings[0], rings[1:])) + + # Polygon(shell_tuple, hole_tuple1, ... , hole_tupleN) + ring_tuples = tuple(r.tuple for r in poly) + self.assertEqual(poly, Polygon(*ring_tuples)) + + # Constructing with tuples of LinearRings. + self.assertEqual(poly.wkt, Polygon(*tuple(r for r in poly)).wkt) + self.assertEqual(poly.wkt, Polygon(*tuple(LinearRing(r.tuple) for r in poly)).wkt) + + def test05b_multipolygons(self): + "Testing MultiPolygon objects." + print "\nBEGIN - expecting GEOS_NOTICE; safe to ignore.\n" + prev = fromstr('POINT (0 0)') + for mp in self.geometries.multipolygons: + mpoly = fromstr(mp.wkt) + self.assertEqual(mpoly.geom_type, 'MultiPolygon') + self.assertEqual(mpoly.geom_typeid, 6) + self.assertEqual(mp.valid, mpoly.valid) + + if mp.valid: + self.assertEqual(mp.num_geom, mpoly.num_geom) + self.assertEqual(mp.n_p, mpoly.num_coords) + self.assertEqual(mp.num_geom, len(mpoly)) + self.assertRaises(GEOSIndexError, mpoly.__getitem__, len(mpoly)) + for p in mpoly: + self.assertEqual(p.geom_type, 'Polygon') + self.assertEqual(p.geom_typeid, 3) + self.assertEqual(p.valid, True) + self.assertEqual(mpoly.wkt, MultiPolygon(*tuple(poly.clone() for poly in mpoly)).wkt) + + print "\nEND - expecting GEOS_NOTICE; safe to ignore.\n" + + def test06a_memory_hijinks(self): + "Testing Geometry __del__() on rings and polygons." + #### Memory issues with rings and polygons + + # These tests are needed to ensure sanity with writable geometries. + + # Getting a polygon with interior rings, and pulling out the interior rings + poly = fromstr(self.geometries.polygons[1].wkt) + ring1 = poly[0] + ring2 = poly[1] + + # These deletes should be 'harmless' since they are done on child geometries + del ring1 + del ring2 + ring1 = poly[0] + ring2 = poly[1] + + # Deleting the polygon + del poly + + # Access to these rings is OK since they are clones. + s1, s2 = str(ring1), str(ring2) + + def test08_coord_seq(self): + "Testing Coordinate Sequence objects." + for p in self.geometries.polygons: + if p.ext_ring_cs: + # Constructing the polygon and getting the coordinate sequence + poly = fromstr(p.wkt) + cs = poly.exterior_ring.coord_seq + + self.assertEqual(p.ext_ring_cs, cs.tuple) # done in the Polygon test too. + self.assertEqual(len(p.ext_ring_cs), len(cs)) # Making sure __len__ works + + # Checks __getitem__ and __setitem__ + for i in xrange(len(p.ext_ring_cs)): + c1 = p.ext_ring_cs[i] # Expected value + c2 = cs[i] # Value from coordseq + self.assertEqual(c1, c2) + + # Constructing the test value to set the coordinate sequence with + if len(c1) == 2: tset = (5, 23) + else: tset = (5, 23, 8) + cs[i] = tset + + # Making sure every set point matches what we expect + for j in range(len(tset)): + cs[i] = tset + self.assertEqual(tset[j], cs[i][j]) + + def test09_relate_pattern(self): + "Testing relate() and relate_pattern()." + g = fromstr('POINT (0 0)') + self.assertRaises(GEOSException, g.relate_pattern, 0, 'invalid pattern, yo') + for rg in self.geometries.relate_geoms: + a = fromstr(rg.wkt_a) + b = fromstr(rg.wkt_b) + self.assertEqual(rg.result, a.relate_pattern(b, rg.pattern)) + self.assertEqual(rg.pattern, a.relate(b)) + + def test10_intersection(self): + "Testing intersects() and intersection()." + for i in xrange(len(self.geometries.topology_geoms)): + a = fromstr(self.geometries.topology_geoms[i].wkt_a) + b = fromstr(self.geometries.topology_geoms[i].wkt_b) + i1 = fromstr(self.geometries.intersect_geoms[i].wkt) + self.assertEqual(True, a.intersects(b)) + i2 = a.intersection(b) + self.assertEqual(i1, i2) + self.assertEqual(i1, a & b) # __and__ is intersection operator + a &= b # testing __iand__ + self.assertEqual(i1, a) + + def test11_union(self): + "Testing union()." + for i in xrange(len(self.geometries.topology_geoms)): + a = fromstr(self.geometries.topology_geoms[i].wkt_a) + b = fromstr(self.geometries.topology_geoms[i].wkt_b) + u1 = fromstr(self.geometries.union_geoms[i].wkt) + u2 = a.union(b) + self.assertEqual(u1, u2) + self.assertEqual(u1, a | b) # __or__ is union operator + a |= b # testing __ior__ + self.assertEqual(u1, a) + + def test12_difference(self): + "Testing difference()." + for i in xrange(len(self.geometries.topology_geoms)): + a = fromstr(self.geometries.topology_geoms[i].wkt_a) + b = fromstr(self.geometries.topology_geoms[i].wkt_b) + d1 = fromstr(self.geometries.diff_geoms[i].wkt) + d2 = a.difference(b) + self.assertEqual(d1, d2) + self.assertEqual(d1, a - b) # __sub__ is difference operator + a -= b # testing __isub__ + self.assertEqual(d1, a) + + def test13_symdifference(self): + "Testing sym_difference()." + for i in xrange(len(self.geometries.topology_geoms)): + a = fromstr(self.geometries.topology_geoms[i].wkt_a) + b = fromstr(self.geometries.topology_geoms[i].wkt_b) + d1 = fromstr(self.geometries.sdiff_geoms[i].wkt) + d2 = a.sym_difference(b) + self.assertEqual(d1, d2) + self.assertEqual(d1, a ^ b) # __xor__ is symmetric difference operator + a ^= b # testing __ixor__ + self.assertEqual(d1, a) + + def test14_buffer(self): + "Testing buffer()." + for bg in self.geometries.buffer_geoms: + g = fromstr(bg.wkt) + + # The buffer we expect + exp_buf = fromstr(bg.buffer_wkt) + quadsegs = bg.quadsegs + width = bg.width + + # Can't use a floating-point for the number of quadsegs. + self.assertRaises(ctypes.ArgumentError, g.buffer, width, float(quadsegs)) + + # Constructing our buffer + buf = g.buffer(width, quadsegs) + self.assertEqual(exp_buf.num_coords, buf.num_coords) + self.assertEqual(len(exp_buf), len(buf)) + + # Now assuring that each point in the buffer is almost equal + for j in xrange(len(exp_buf)): + exp_ring = exp_buf[j] + buf_ring = buf[j] + self.assertEqual(len(exp_ring), len(buf_ring)) + for k in xrange(len(exp_ring)): + # Asserting the X, Y of each point are almost equal (due to floating point imprecision) + self.assertAlmostEqual(exp_ring[k][0], buf_ring[k][0], 9) + self.assertAlmostEqual(exp_ring[k][1], buf_ring[k][1], 9) + + def test15_srid(self): + "Testing the SRID property and keyword." + # Testing SRID keyword on Point + pnt = Point(5, 23, srid=4326) + self.assertEqual(4326, pnt.srid) + pnt.srid = 3084 + self.assertEqual(3084, pnt.srid) + self.assertRaises(ctypes.ArgumentError, pnt.set_srid, '4326') + + # Testing SRID keyword on fromstr(), and on Polygon rings. + poly = fromstr(self.geometries.polygons[1].wkt, srid=4269) + self.assertEqual(4269, poly.srid) + for ring in poly: self.assertEqual(4269, ring.srid) + poly.srid = 4326 + self.assertEqual(4326, poly.shell.srid) + + # Testing SRID keyword on GeometryCollection + gc = GeometryCollection(Point(5, 23), LineString((0, 0), (1.5, 1.5), (3, 3)), srid=32021) + self.assertEqual(32021, gc.srid) + for i in range(len(gc)): self.assertEqual(32021, gc[i].srid) + + # GEOS may get the SRID from HEXEWKB + # 'POINT(5 23)' at SRID=4326 in hex form -- obtained from PostGIS + # using `SELECT GeomFromText('POINT (5 23)', 4326);`. + hex = '0101000020E610000000000000000014400000000000003740' + p1 = fromstr(hex) + self.assertEqual(4326, p1.srid) + + # In GEOS 3.0.0rc1-4 when the EWKB and/or HEXEWKB is exported, + # the SRID information is lost and set to -1 -- this is not a + # problem on the 3.0.0 version (another reason to upgrade). + exp_srid = self.null_srid + + p2 = fromstr(p1.hex) + self.assertEqual(exp_srid, p2.srid) + p3 = fromstr(p1.hex, srid=-1) # -1 is intended. + self.assertEqual(-1, p3.srid) + + def test16_mutable_geometries(self): + "Testing the mutability of Polygons and Geometry Collections." + ### Testing the mutability of Polygons ### + for p in self.geometries.polygons: + poly = fromstr(p.wkt) + + # Should only be able to use __setitem__ with LinearRing geometries. + self.assertRaises(TypeError, poly.__setitem__, 0, LineString((1, 1), (2, 2))) + + # Constructing the new shell by adding 500 to every point in the old shell. + shell_tup = poly.shell.tuple + new_coords = [] + for point in shell_tup: new_coords.append((point[0] + 500., point[1] + 500.)) + new_shell = LinearRing(*tuple(new_coords)) + + # Assigning polygon's exterior ring w/the new shell + poly.exterior_ring = new_shell + s = str(new_shell) # new shell is still accessible + self.assertEqual(poly.exterior_ring, new_shell) + self.assertEqual(poly[0], new_shell) + + ### Testing the mutability of Geometry Collections + for tg in self.geometries.multipoints: + mp = fromstr(tg.wkt) + for i in range(len(mp)): + # Creating a random point. + pnt = mp[i] + new = Point(random.randint(1, 100), random.randint(1, 100)) + # Testing the assignment + mp[i] = new + s = str(new) # what was used for the assignment is still accessible + self.assertEqual(mp[i], new) + self.assertEqual(mp[i].wkt, new.wkt) + self.assertNotEqual(pnt, mp[i]) + + # MultiPolygons involve much more memory management because each + # Polygon w/in the collection has its own rings. + for tg in self.geometries.multipolygons: + mpoly = fromstr(tg.wkt) + for i in xrange(len(mpoly)): + poly = mpoly[i] + old_poly = mpoly[i] + # Offsetting the each ring in the polygon by 500. + for j in xrange(len(poly)): + r = poly[j] + for k in xrange(len(r)): r[k] = (r[k][0] + 500., r[k][1] + 500.) + poly[j] = r + + self.assertNotEqual(mpoly[i], poly) + # Testing the assignment + mpoly[i] = poly + s = str(poly) # Still accessible + self.assertEqual(mpoly[i], poly) + self.assertNotEqual(mpoly[i], old_poly) + + # Extreme (!!) __setitem__ -- no longer works, have to detect + # in the first object that __setitem__ is called in the subsequent + # objects -- maybe mpoly[0, 0, 0] = (3.14, 2.71)? + #mpoly[0][0][0] = (3.14, 2.71) + #self.assertEqual((3.14, 2.71), mpoly[0][0][0]) + # Doing it more slowly.. + #self.assertEqual((3.14, 2.71), mpoly[0].shell[0]) + #del mpoly + + def test17_threed(self): + "Testing three-dimensional geometries." + # Testing a 3D Point + pnt = Point(2, 3, 8) + self.assertEqual((2.,3.,8.), pnt.coords) + self.assertRaises(TypeError, pnt.set_coords, (1.,2.)) + pnt.coords = (1.,2.,3.) + self.assertEqual((1.,2.,3.), pnt.coords) + + # Testing a 3D LineString + ls = LineString((2., 3., 8.), (50., 250., -117.)) + self.assertEqual(((2.,3.,8.), (50.,250.,-117.)), ls.tuple) + self.assertRaises(TypeError, ls.__setitem__, 0, (1.,2.)) + ls[0] = (1.,2.,3.) + self.assertEqual((1.,2.,3.), ls[0]) + + def test18_distance(self): + "Testing the distance() function." + # Distance to self should be 0. + pnt = Point(0, 0) + self.assertEqual(0.0, pnt.distance(Point(0, 0))) + + # Distance should be 1 + self.assertEqual(1.0, pnt.distance(Point(0, 1))) + + # Distance should be ~ sqrt(2) + self.assertAlmostEqual(1.41421356237, pnt.distance(Point(1, 1)), 11) + + # Distances are from the closest vertex in each geometry -- + # should be 3 (distance from (2, 2) to (5, 2)). + ls1 = LineString((0, 0), (1, 1), (2, 2)) + ls2 = LineString((5, 2), (6, 1), (7, 0)) + self.assertEqual(3, ls1.distance(ls2)) + + def test19_length(self): + "Testing the length property." + # Points have 0 length. + pnt = Point(0, 0) + self.assertEqual(0.0, pnt.length) + + # Should be ~ sqrt(2) + ls = LineString((0, 0), (1, 1)) + self.assertAlmostEqual(1.41421356237, ls.length, 11) + + # Should be circumfrence of Polygon + poly = Polygon(LinearRing((0, 0), (0, 1), (1, 1), (1, 0), (0, 0))) + self.assertEqual(4.0, poly.length) + + # Should be sum of each element's length in collection. + mpoly = MultiPolygon(poly.clone(), poly) + self.assertEqual(8.0, mpoly.length) + + def test20a_emptyCollections(self): + "Testing empty geometries and collections." + gc1 = GeometryCollection([]) + gc2 = fromstr('GEOMETRYCOLLECTION EMPTY') + pnt = fromstr('POINT EMPTY') + ls = fromstr('LINESTRING EMPTY') + poly = fromstr('POLYGON EMPTY') + mls = fromstr('MULTILINESTRING EMPTY') + mpoly1 = fromstr('MULTIPOLYGON EMPTY') + mpoly2 = MultiPolygon(()) + + for g in [gc1, gc2, pnt, ls, poly, mls, mpoly1, mpoly2]: + self.assertEqual(True, g.empty) + + # Testing len() and num_geom. + if isinstance(g, Polygon): + self.assertEqual(1, len(g)) # Has one empty linear ring + self.assertEqual(1, g.num_geom) + self.assertEqual(0, len(g[0])) + elif isinstance(g, (Point, LineString)): + self.assertEqual(1, g.num_geom) + self.assertEqual(0, len(g)) + else: + self.assertEqual(0, g.num_geom) + self.assertEqual(0, len(g)) + + # Testing __getitem__ (doesn't work on Point or Polygon) + if isinstance(g, Point): + self.assertRaises(GEOSIndexError, g.get_x) + elif isinstance(g, Polygon): + lr = g.shell + self.assertEqual('LINEARRING EMPTY', lr.wkt) + self.assertEqual(0, len(lr)) + self.assertEqual(True, lr.empty) + self.assertRaises(GEOSIndexError, lr.__getitem__, 0) + else: + self.assertRaises(GEOSIndexError, g.__getitem__, 0) + + def test20b_collections_of_collections(self): + "Testing GeometryCollection handling of other collections." + # Creating a GeometryCollection WKT string composed of other + # collections and polygons. + coll = [mp.wkt for mp in self.geometries.multipolygons if mp.valid] + coll.extend([mls.wkt for mls in self.geometries.multilinestrings]) + coll.extend([p.wkt for p in self.geometries.polygons]) + coll.extend([mp.wkt for mp in self.geometries.multipoints]) + gc_wkt = 'GEOMETRYCOLLECTION(%s)' % ','.join(coll) + + # Should construct ok from WKT + gc1 = GEOSGeometry(gc_wkt) + + # Should also construct ok from individual geometry arguments. + gc2 = GeometryCollection(*tuple(g for g in gc1)) + + # And, they should be equal. + self.assertEqual(gc1, gc2) + + def test21_test_gdal(self): + "Testing `ogr` and `srs` properties." + if not gdal.HAS_GDAL: return + g1 = fromstr('POINT(5 23)') + self.assertEqual(True, isinstance(g1.ogr, gdal.OGRGeometry)) + self.assertEqual(g1.srs, None) + + g2 = fromstr('LINESTRING(0 0, 5 5, 23 23)', srid=4326) + self.assertEqual(True, isinstance(g2.ogr, gdal.OGRGeometry)) + self.assertEqual(True, isinstance(g2.srs, gdal.SpatialReference)) + self.assertEqual(g2.hex, g2.ogr.hex) + self.assertEqual('WGS 84', g2.srs.name) + + def test22_copy(self): + "Testing use with the Python `copy` module." + import django.utils.copycompat as copy + poly = GEOSGeometry('POLYGON((0 0, 0 23, 23 23, 23 0, 0 0), (5 5, 5 10, 10 10, 10 5, 5 5))') + cpy1 = copy.copy(poly) + cpy2 = copy.deepcopy(poly) + self.assertNotEqual(poly._ptr, cpy1._ptr) + self.assertNotEqual(poly._ptr, cpy2._ptr) + + def test23_transform(self): + "Testing `transform` method." + if not gdal.HAS_GDAL: return + orig = GEOSGeometry('POINT (-104.609 38.255)', 4326) + trans = GEOSGeometry('POINT (992385.4472045 481455.4944650)', 2774) + + # Using a srid, a SpatialReference object, and a CoordTransform object + # for transformations. + t1, t2, t3 = orig.clone(), orig.clone(), orig.clone() + t1.transform(trans.srid) + t2.transform(gdal.SpatialReference('EPSG:2774')) + ct = gdal.CoordTransform(gdal.SpatialReference('WGS84'), gdal.SpatialReference(2774)) + t3.transform(ct) + + # Testing use of the `clone` keyword. + k1 = orig.clone() + k2 = k1.transform(trans.srid, clone=True) + self.assertEqual(k1, orig) + self.assertNotEqual(k1, k2) + + prec = 3 + for p in (t1, t2, t3, k2): + self.assertAlmostEqual(trans.x, p.x, prec) + self.assertAlmostEqual(trans.y, p.y, prec) + + def test24_extent(self): + "Testing `extent` method." + # The xmin, ymin, xmax, ymax of the MultiPoint should be returned. + mp = MultiPoint(Point(5, 23), Point(0, 0), Point(10, 50)) + self.assertEqual((0.0, 0.0, 10.0, 50.0), mp.extent) + pnt = Point(5.23, 17.8) + # Extent of points is just the point itself repeated. + self.assertEqual((5.23, 17.8, 5.23, 17.8), pnt.extent) + # Testing on the 'real world' Polygon. + poly = fromstr(self.geometries.polygons[3].wkt) + ring = poly.shell + x, y = ring.x, ring.y + xmin, ymin = min(x), min(y) + xmax, ymax = max(x), max(y) + self.assertEqual((xmin, ymin, xmax, ymax), poly.extent) + + def test25_pickle(self): + "Testing pickling and unpickling support." + # Using both pickle and cPickle -- just 'cause. + import pickle, cPickle + + # Creating a list of test geometries for pickling, + # and setting the SRID on some of them. + def get_geoms(lst, srid=None): + return [GEOSGeometry(tg.wkt, srid) for tg in lst] + tgeoms = get_geoms(self.geometries.points) + tgeoms.extend(get_geoms(self.geometries.multilinestrings, 4326)) + tgeoms.extend(get_geoms(self.geometries.polygons, 3084)) + tgeoms.extend(get_geoms(self.geometries.multipolygons, 900913)) + + # The SRID won't be exported in GEOS 3.0 release candidates. + no_srid = self.null_srid == -1 + for geom in tgeoms: + s1, s2 = cPickle.dumps(geom), pickle.dumps(geom) + g1, g2 = cPickle.loads(s1), pickle.loads(s2) + for tmpg in (g1, g2): + self.assertEqual(geom, tmpg) + if not no_srid: self.assertEqual(geom.srid, tmpg.srid) + + def test26_prepared(self): + "Testing PreparedGeometry support." + if not GEOS_PREPARE: return + # Creating a simple multipolygon and getting a prepared version. + mpoly = GEOSGeometry('MULTIPOLYGON(((0 0,0 5,5 5,5 0,0 0)),((5 5,5 10,10 10,10 5,5 5)))') + prep = mpoly.prepared + + # A set of test points. + pnts = [Point(5, 5), Point(7.5, 7.5), Point(2.5, 7.5)] + covers = [True, True, False] # No `covers` op for regular GEOS geoms. + for pnt, c in zip(pnts, covers): + # Results should be the same (but faster) + self.assertEqual(mpoly.contains(pnt), prep.contains(pnt)) + self.assertEqual(mpoly.intersects(pnt), prep.intersects(pnt)) + self.assertEqual(c, prep.covers(pnt)) + + def test26_line_merge(self): + "Testing line merge support" + ref_geoms = (fromstr('LINESTRING(1 1, 1 1, 3 3)'), + fromstr('MULTILINESTRING((1 1, 3 3), (3 3, 4 2))'), + ) + ref_merged = (fromstr('LINESTRING(1 1, 3 3)'), + fromstr('LINESTRING (1 1, 3 3, 4 2)'), + ) + for geom, merged in zip(ref_geoms, ref_merged): + self.assertEqual(merged, geom.merged) + +def suite(): + s = unittest.TestSuite() + s.addTest(unittest.makeSuite(GEOSTest)) + return s + +def run(verbosity=2): + unittest.TextTestRunner(verbosity=verbosity).run(suite()) diff --git a/parts/django/django/contrib/gis/geos/tests/test_geos_mutation.py b/parts/django/django/contrib/gis/geos/tests/test_geos_mutation.py new file mode 100644 index 0000000..28f484d --- /dev/null +++ b/parts/django/django/contrib/gis/geos/tests/test_geos_mutation.py @@ -0,0 +1,137 @@ +# Copyright (c) 2008-2009 Aryeh Leib Taurog, all rights reserved. +# Modified from original contribution by Aryeh Leib Taurog, which was +# released under the New BSD license. +import unittest + +import django.utils.copycompat as copy + +from django.contrib.gis.geos import * +from django.contrib.gis.geos.error import GEOSIndexError + +def getItem(o,i): return o[i] +def delItem(o,i): del o[i] +def setItem(o,i,v): o[i] = v + +def api_get_distance(x): return x.distance(Point(-200,-200)) +def api_get_buffer(x): return x.buffer(10) +def api_get_geom_typeid(x): return x.geom_typeid +def api_get_num_coords(x): return x.num_coords +def api_get_centroid(x): return x.centroid +def api_get_empty(x): return x.empty +def api_get_valid(x): return x.valid +def api_get_simple(x): return x.simple +def api_get_ring(x): return x.ring +def api_get_boundary(x): return x.boundary +def api_get_convex_hull(x): return x.convex_hull +def api_get_extent(x): return x.extent +def api_get_area(x): return x.area +def api_get_length(x): return x.length + +geos_function_tests = [ val for name, val in vars().items() + if hasattr(val, '__call__') + and name.startswith('api_get_') ] + +class GEOSMutationTest(unittest.TestCase): + """ + Tests Pythonic Mutability of Python GEOS geometry wrappers + get/set/delitem on a slice, normal list methods + """ + + def test00_GEOSIndexException(self): + 'Testing Geometry GEOSIndexError' + p = Point(1,2) + for i in range(-2,2): p._checkindex(i) + self.assertRaises(GEOSIndexError, p._checkindex, 2) + self.assertRaises(GEOSIndexError, p._checkindex, -3) + + def test01_PointMutations(self): + 'Testing Point mutations' + for p in (Point(1,2,3), fromstr('POINT (1 2 3)')): + self.assertEqual(p._get_single_external(1), 2.0, 'Point _get_single_external') + + # _set_single + p._set_single(0,100) + self.assertEqual(p.coords, (100.0,2.0,3.0), 'Point _set_single') + + # _set_list + p._set_list(2,(50,3141)) + self.assertEqual(p.coords, (50.0,3141.0), 'Point _set_list') + + def test02_PointExceptions(self): + 'Testing Point exceptions' + self.assertRaises(TypeError, Point, range(1)) + self.assertRaises(TypeError, Point, range(4)) + + def test03_PointApi(self): + 'Testing Point API' + q = Point(4,5,3) + for p in (Point(1,2,3), fromstr('POINT (1 2 3)')): + p[0:2] = [4,5] + for f in geos_function_tests: + self.assertEqual(f(q), f(p), 'Point ' + f.__name__) + + def test04_LineStringMutations(self): + 'Testing LineString mutations' + for ls in (LineString((1,0),(4,1),(6,-1)), + fromstr('LINESTRING (1 0,4 1,6 -1)')): + self.assertEqual(ls._get_single_external(1), (4.0,1.0), 'LineString _get_single_external') + + # _set_single + ls._set_single(0,(-50,25)) + self.assertEqual(ls.coords, ((-50.0,25.0),(4.0,1.0),(6.0,-1.0)), 'LineString _set_single') + + # _set_list + ls._set_list(2, ((-50.0,25.0),(6.0,-1.0))) + self.assertEqual(ls.coords, ((-50.0,25.0),(6.0,-1.0)), 'LineString _set_list') + + lsa = LineString(ls.coords) + for f in geos_function_tests: + self.assertEqual(f(lsa), f(ls), 'LineString ' + f.__name__) + + def test05_Polygon(self): + 'Testing Polygon mutations' + for pg in (Polygon(((1,0),(4,1),(6,-1),(8,10),(1,0)), + ((5,4),(6,4),(6,3),(5,4))), + fromstr('POLYGON ((1 0,4 1,6 -1,8 10,1 0),(5 4,6 4,6 3,5 4))')): + self.assertEqual(pg._get_single_external(0), + LinearRing((1,0),(4,1),(6,-1),(8,10),(1,0)), + 'Polygon _get_single_external(0)') + self.assertEqual(pg._get_single_external(1), + LinearRing((5,4),(6,4),(6,3),(5,4)), + 'Polygon _get_single_external(1)') + + # _set_list + pg._set_list(2, (((1,2),(10,0),(12,9),(-1,15),(1,2)), + ((4,2),(5,2),(5,3),(4,2)))) + self.assertEqual(pg.coords, + (((1.0,2.0),(10.0,0.0),(12.0,9.0),(-1.0,15.0),(1.0,2.0)), + ((4.0,2.0),(5.0,2.0),(5.0,3.0),(4.0,2.0))), + 'Polygon _set_list') + + lsa = Polygon(*pg.coords) + for f in geos_function_tests: + self.assertEqual(f(lsa), f(pg), 'Polygon ' + f.__name__) + + def test06_Collection(self): + 'Testing Collection mutations' + for mp in (MultiPoint(*map(Point,((3,4),(-1,2),(5,-4),(2,8)))), + fromstr('MULTIPOINT (3 4,-1 2,5 -4,2 8)')): + self.assertEqual(mp._get_single_external(2), Point(5,-4), 'Collection _get_single_external') + + mp._set_list(3, map(Point,((5,5),(3,-2),(8,1)))) + self.assertEqual(mp.coords, ((5.0,5.0),(3.0,-2.0),(8.0,1.0)), 'Collection _set_list') + + lsa = MultiPoint(*map(Point,((5,5),(3,-2),(8,1)))) + for f in geos_function_tests: + self.assertEqual(f(lsa), f(mp), 'MultiPoint ' + f.__name__) + +def suite(): + s = unittest.TestSuite() + s.addTest(unittest.makeSuite(GEOSMutationTest)) + return s + +def run(verbosity=2): + unittest.TextTestRunner(verbosity=verbosity).run(suite()) + +if __name__ == '__main__': + run() diff --git a/parts/django/django/contrib/gis/geos/tests/test_io.py b/parts/django/django/contrib/gis/geos/tests/test_io.py new file mode 100644 index 0000000..cc0f1ed --- /dev/null +++ b/parts/django/django/contrib/gis/geos/tests/test_io.py @@ -0,0 +1,112 @@ +import binascii, ctypes, unittest +from django.contrib.gis.geos import GEOSGeometry, WKTReader, WKTWriter, WKBReader, WKBWriter, geos_version_info + +class GEOSIOTest(unittest.TestCase): + + def test01_wktreader(self): + # Creating a WKTReader instance + wkt_r = WKTReader() + wkt = 'POINT (5 23)' + + # read() should return a GEOSGeometry + ref = GEOSGeometry(wkt) + g1 = wkt_r.read(wkt) + g2 = wkt_r.read(unicode(wkt)) + + for geom in (g1, g2): + self.assertEqual(ref, geom) + + # Should only accept basestring objects. + self.assertRaises(TypeError, wkt_r.read, 1) + self.assertRaises(TypeError, wkt_r.read, buffer('foo')) + + def test02_wktwriter(self): + # Creating a WKTWriter instance, testing its ptr property. + wkt_w = WKTWriter() + self.assertRaises(TypeError, wkt_w._set_ptr, WKTReader.ptr_type()) + + ref = GEOSGeometry('POINT (5 23)') + ref_wkt = 'POINT (5.0000000000000000 23.0000000000000000)' + self.assertEqual(ref_wkt, wkt_w.write(ref)) + + def test03_wkbreader(self): + # Creating a WKBReader instance + wkb_r = WKBReader() + + hex = '000000000140140000000000004037000000000000' + wkb = buffer(binascii.a2b_hex(hex)) + ref = GEOSGeometry(hex) + + # read() should return a GEOSGeometry on either a hex string or + # a WKB buffer. + g1 = wkb_r.read(wkb) + g2 = wkb_r.read(hex) + for geom in (g1, g2): + self.assertEqual(ref, geom) + + bad_input = (1, 5.23, None, False) + for bad_wkb in bad_input: + self.assertRaises(TypeError, wkb_r.read, bad_wkb) + + def test04_wkbwriter(self): + wkb_w = WKBWriter() + + # Representations of 'POINT (5 23)' in hex -- one normal and + # the other with the byte order changed. + g = GEOSGeometry('POINT (5 23)') + hex1 = '010100000000000000000014400000000000003740' + wkb1 = buffer(binascii.a2b_hex(hex1)) + hex2 = '000000000140140000000000004037000000000000' + wkb2 = buffer(binascii.a2b_hex(hex2)) + + self.assertEqual(hex1, wkb_w.write_hex(g)) + self.assertEqual(wkb1, wkb_w.write(g)) + + # Ensuring bad byteorders are not accepted. + for bad_byteorder in (-1, 2, 523, 'foo', None): + # Equivalent of `wkb_w.byteorder = bad_byteorder` + self.assertRaises(ValueError, wkb_w._set_byteorder, bad_byteorder) + + # Setting the byteorder to 0 (for Big Endian) + wkb_w.byteorder = 0 + self.assertEqual(hex2, wkb_w.write_hex(g)) + self.assertEqual(wkb2, wkb_w.write(g)) + + # Back to Little Endian + wkb_w.byteorder = 1 + + # Now, trying out the 3D and SRID flags. + g = GEOSGeometry('POINT (5 23 17)') + g.srid = 4326 + + hex3d = '0101000080000000000000144000000000000037400000000000003140' + wkb3d = buffer(binascii.a2b_hex(hex3d)) + hex3d_srid = '01010000A0E6100000000000000000144000000000000037400000000000003140' + wkb3d_srid = buffer(binascii.a2b_hex(hex3d_srid)) + + # Ensuring bad output dimensions are not accepted + for bad_outdim in (-1, 0, 1, 4, 423, 'foo', None): + # Equivalent of `wkb_w.outdim = bad_outdim` + self.assertRaises(ValueError, wkb_w._set_outdim, bad_outdim) + + # These tests will fail on 3.0.0 because of a bug that was fixed in 3.1: + # http://trac.osgeo.org/geos/ticket/216 + if not geos_version_info()['version'].startswith('3.0.'): + # Now setting the output dimensions to be 3 + wkb_w.outdim = 3 + + self.assertEqual(hex3d, wkb_w.write_hex(g)) + self.assertEqual(wkb3d, wkb_w.write(g)) + + # Telling the WKBWriter to inlcude the srid in the representation. + wkb_w.srid = True + self.assertEqual(hex3d_srid, wkb_w.write_hex(g)) + self.assertEqual(wkb3d_srid, wkb_w.write(g)) + +def suite(): + s = unittest.TestSuite() + s.addTest(unittest.makeSuite(GEOSIOTest)) + return s + +def run(verbosity=2): + unittest.TextTestRunner(verbosity=verbosity).run(suite()) diff --git a/parts/django/django/contrib/gis/geos/tests/test_mutable_list.py b/parts/django/django/contrib/gis/geos/tests/test_mutable_list.py new file mode 100644 index 0000000..ebbe8ff --- /dev/null +++ b/parts/django/django/contrib/gis/geos/tests/test_mutable_list.py @@ -0,0 +1,398 @@ +# Copyright (c) 2008-2009 Aryeh Leib Taurog, http://www.aryehleib.com +# All rights reserved. +# +# Modified from original contribution by Aryeh Leib Taurog, which was +# released under the New BSD license. +import unittest +from django.contrib.gis.geos.mutable_list import ListMixin + +class UserListA(ListMixin): + _mytype = tuple + def __init__(self, i_list, *args, **kwargs): + self._list = self._mytype(i_list) + super(UserListA, self).__init__(*args, **kwargs) + + def __len__(self): return len(self._list) + + def __str__(self): return str(self._list) + + def __repr__(self): return repr(self._list) + + def _set_list(self, length, items): + # this would work: + # self._list = self._mytype(items) + # but then we wouldn't be testing length parameter + itemList = ['x'] * length + for i, v in enumerate(items): + itemList[i] = v + + self._list = self._mytype(itemList) + + def _get_single_external(self, index): + return self._list[index] + +class UserListB(UserListA): + _mytype = list + + def _set_single(self, index, value): + self._list[index] = value + +def nextRange(length): + nextRange.start += 100 + return range(nextRange.start, nextRange.start + length) + +nextRange.start = 0 + +class ListMixinTest(unittest.TestCase): + """ + Tests base class ListMixin by comparing a list clone which is + a ListMixin subclass with a real Python list. + """ + limit = 3 + listType = UserListA + + def lists_of_len(self, length=None): + if length is None: length = self.limit + pl = range(length) + return pl, self.listType(pl) + + def limits_plus(self, b): + return range(-self.limit - b, self.limit + b) + + def step_range(self): + return range(-1 - self.limit, 0) + range(1, 1 + self.limit) + + def test01_getslice(self): + 'Slice retrieval' + pl, ul = self.lists_of_len() + for i in self.limits_plus(1): + self.assertEqual(pl[i:], ul[i:], 'slice [%d:]' % (i)) + self.assertEqual(pl[:i], ul[:i], 'slice [:%d]' % (i)) + + for j in self.limits_plus(1): + self.assertEqual(pl[i:j], ul[i:j], 'slice [%d:%d]' % (i,j)) + for k in self.step_range(): + self.assertEqual(pl[i:j:k], ul[i:j:k], 'slice [%d:%d:%d]' % (i,j,k)) + + for k in self.step_range(): + self.assertEqual(pl[i::k], ul[i::k], 'slice [%d::%d]' % (i,k)) + self.assertEqual(pl[:i:k], ul[:i:k], 'slice [:%d:%d]' % (i,k)) + + for k in self.step_range(): + self.assertEqual(pl[::k], ul[::k], 'slice [::%d]' % (k)) + + def test02_setslice(self): + 'Slice assignment' + def setfcn(x,i,j,k,L): x[i:j:k] = range(L) + pl, ul = self.lists_of_len() + for slen in range(self.limit + 1): + ssl = nextRange(slen) + ul[:] = ssl + pl[:] = ssl + self.assertEqual(pl, ul[:], 'set slice [:]') + + for i in self.limits_plus(1): + ssl = nextRange(slen) + ul[i:] = ssl + pl[i:] = ssl + self.assertEqual(pl, ul[:], 'set slice [%d:]' % (i)) + + ssl = nextRange(slen) + ul[:i] = ssl + pl[:i] = ssl + self.assertEqual(pl, ul[:], 'set slice [:%d]' % (i)) + + for j in self.limits_plus(1): + ssl = nextRange(slen) + ul[i:j] = ssl + pl[i:j] = ssl + self.assertEqual(pl, ul[:], 'set slice [%d:%d]' % (i, j)) + + for k in self.step_range(): + ssl = nextRange( len(ul[i:j:k]) ) + ul[i:j:k] = ssl + pl[i:j:k] = ssl + self.assertEqual(pl, ul[:], 'set slice [%d:%d:%d]' % (i, j, k)) + + sliceLen = len(ul[i:j:k]) + self.assertRaises(ValueError, setfcn, ul, i, j, k, sliceLen + 1) + if sliceLen > 2: + self.assertRaises(ValueError, setfcn, ul, i, j, k, sliceLen - 1) + + for k in self.step_range(): + ssl = nextRange( len(ul[i::k]) ) + ul[i::k] = ssl + pl[i::k] = ssl + self.assertEqual(pl, ul[:], 'set slice [%d::%d]' % (i, k)) + + ssl = nextRange( len(ul[:i:k]) ) + ul[:i:k] = ssl + pl[:i:k] = ssl + self.assertEqual(pl, ul[:], 'set slice [:%d:%d]' % (i, k)) + + for k in self.step_range(): + ssl = nextRange(len(ul[::k])) + ul[::k] = ssl + pl[::k] = ssl + self.assertEqual(pl, ul[:], 'set slice [::%d]' % (k)) + + + def test03_delslice(self): + 'Delete slice' + for Len in range(self.limit): + pl, ul = self.lists_of_len(Len) + del pl[:] + del ul[:] + self.assertEqual(pl[:], ul[:], 'del slice [:]') + for i in range(-Len - 1, Len + 1): + pl, ul = self.lists_of_len(Len) + del pl[i:] + del ul[i:] + self.assertEqual(pl[:], ul[:], 'del slice [%d:]' % (i)) + pl, ul = self.lists_of_len(Len) + del pl[:i] + del ul[:i] + self.assertEqual(pl[:], ul[:], 'del slice [:%d]' % (i)) + for j in range(-Len - 1, Len + 1): + pl, ul = self.lists_of_len(Len) + del pl[i:j] + del ul[i:j] + self.assertEqual(pl[:], ul[:], 'del slice [%d:%d]' % (i,j)) + for k in range(-Len - 1,0) + range(1,Len): + pl, ul = self.lists_of_len(Len) + del pl[i:j:k] + del ul[i:j:k] + self.assertEqual(pl[:], ul[:], 'del slice [%d:%d:%d]' % (i,j,k)) + + for k in range(-Len - 1,0) + range(1,Len): + pl, ul = self.lists_of_len(Len) + del pl[:i:k] + del ul[:i:k] + self.assertEqual(pl[:], ul[:], 'del slice [:%d:%d]' % (i,k)) + + pl, ul = self.lists_of_len(Len) + del pl[i::k] + del ul[i::k] + self.assertEqual(pl[:], ul[:], 'del slice [%d::%d]' % (i,k)) + + for k in range(-Len - 1,0) + range(1,Len): + pl, ul = self.lists_of_len(Len) + del pl[::k] + del ul[::k] + self.assertEqual(pl[:], ul[:], 'del slice [::%d]' % (k)) + + def test04_get_set_del_single(self): + 'Get/set/delete single item' + pl, ul = self.lists_of_len() + for i in self.limits_plus(0): + self.assertEqual(pl[i], ul[i], 'get single item [%d]' % i) + + for i in self.limits_plus(0): + pl, ul = self.lists_of_len() + pl[i] = 100 + ul[i] = 100 + self.assertEqual(pl[:], ul[:], 'set single item [%d]' % i) + + for i in self.limits_plus(0): + pl, ul = self.lists_of_len() + del pl[i] + del ul[i] + self.assertEqual(pl[:], ul[:], 'del single item [%d]' % i) + + def test05_out_of_range_exceptions(self): + 'Out of range exceptions' + def setfcn(x, i): x[i] = 20 + def getfcn(x, i): return x[i] + def delfcn(x, i): del x[i] + pl, ul = self.lists_of_len() + for i in (-1 - self.limit, self.limit): + self.assertRaises(IndexError, setfcn, ul, i) # 'set index %d' % i) + self.assertRaises(IndexError, getfcn, ul, i) # 'get index %d' % i) + self.assertRaises(IndexError, delfcn, ul, i) # 'del index %d' % i) + + def test06_list_methods(self): + 'List methods' + pl, ul = self.lists_of_len() + pl.append(40) + ul.append(40) + self.assertEqual(pl[:], ul[:], 'append') + + pl.extend(range(50,55)) + ul.extend(range(50,55)) + self.assertEqual(pl[:], ul[:], 'extend') + + pl.reverse() + ul.reverse() + self.assertEqual(pl[:], ul[:], 'reverse') + + for i in self.limits_plus(1): + pl, ul = self.lists_of_len() + pl.insert(i,50) + ul.insert(i,50) + self.assertEqual(pl[:], ul[:], 'insert at %d' % i) + + for i in self.limits_plus(0): + pl, ul = self.lists_of_len() + self.assertEqual(pl.pop(i), ul.pop(i), 'popped value at %d' % i) + self.assertEqual(pl[:], ul[:], 'after pop at %d' % i) + + pl, ul = self.lists_of_len() + self.assertEqual(pl.pop(), ul.pop(i), 'popped value') + self.assertEqual(pl[:], ul[:], 'after pop') + + pl, ul = self.lists_of_len() + def popfcn(x, i): x.pop(i) + self.assertRaises(IndexError, popfcn, ul, self.limit) + self.assertRaises(IndexError, popfcn, ul, -1 - self.limit) + + pl, ul = self.lists_of_len() + for val in range(self.limit): + self.assertEqual(pl.index(val), ul.index(val), 'index of %d' % val) + + for val in self.limits_plus(2): + self.assertEqual(pl.count(val), ul.count(val), 'count %d' % val) + + for val in range(self.limit): + pl, ul = self.lists_of_len() + pl.remove(val) + ul.remove(val) + self.assertEqual(pl[:], ul[:], 'after remove val %d' % val) + + def indexfcn(x, v): return x.index(v) + def removefcn(x, v): return x.remove(v) + self.assertRaises(ValueError, indexfcn, ul, 40) + self.assertRaises(ValueError, removefcn, ul, 40) + + def test07_allowed_types(self): + 'Type-restricted list' + pl, ul = self.lists_of_len() + ul._allowed = (int, long) + ul[1] = 50 + ul[:2] = [60, 70, 80] + def setfcn(x, i, v): x[i] = v + self.assertRaises(TypeError, setfcn, ul, 2, 'hello') + self.assertRaises(TypeError, setfcn, ul, slice(0,3,2), ('hello','goodbye')) + + def test08_min_length(self): + 'Length limits' + pl, ul = self.lists_of_len() + ul._minlength = 1 + def delfcn(x,i): del x[:i] + def setfcn(x,i): x[:i] = [] + for i in range(self.limit - ul._minlength + 1, self.limit + 1): + self.assertRaises(ValueError, delfcn, ul, i) + self.assertRaises(ValueError, setfcn, ul, i) + del ul[:ul._minlength] + + ul._maxlength = 4 + for i in range(0, ul._maxlength - len(ul)): + ul.append(i) + self.assertRaises(ValueError, ul.append, 10) + + def test09_iterable_check(self): + 'Error on assigning non-iterable to slice' + pl, ul = self.lists_of_len(self.limit + 1) + def setfcn(x, i, v): x[i] = v + self.assertRaises(TypeError, setfcn, ul, slice(0,3,2), 2) + + def test10_checkindex(self): + 'Index check' + pl, ul = self.lists_of_len() + for i in self.limits_plus(0): + if i < 0: + self.assertEqual(ul._checkindex(i), i + self.limit, '_checkindex(neg index)') + else: + self.assertEqual(ul._checkindex(i), i, '_checkindex(pos index)') + + for i in (-self.limit - 1, self.limit): + self.assertRaises(IndexError, ul._checkindex, i) + + ul._IndexError = TypeError + self.assertRaises(TypeError, ul._checkindex, -self.limit - 1) + + def test_11_sorting(self): + 'Sorting' + pl, ul = self.lists_of_len() + pl.insert(0, pl.pop()) + ul.insert(0, ul.pop()) + pl.sort() + ul.sort() + self.assertEqual(pl[:], ul[:], 'sort') + mid = pl[len(pl) / 2] + pl.sort(key=lambda x: (mid-x)**2) + ul.sort(key=lambda x: (mid-x)**2) + self.assertEqual(pl[:], ul[:], 'sort w/ key') + + pl.insert(0, pl.pop()) + ul.insert(0, ul.pop()) + pl.sort(reverse=True) + ul.sort(reverse=True) + self.assertEqual(pl[:], ul[:], 'sort w/ reverse') + mid = pl[len(pl) / 2] + pl.sort(key=lambda x: (mid-x)**2) + ul.sort(key=lambda x: (mid-x)**2) + self.assertEqual(pl[:], ul[:], 'sort w/ key') + + def test_12_arithmetic(self): + 'Arithmetic' + pl, ul = self.lists_of_len() + al = range(10,14) + self.assertEqual(list(pl + al), list(ul + al), 'add') + self.assertEqual(type(ul), type(ul + al), 'type of add result') + self.assertEqual(list(al + pl), list(al + ul), 'radd') + self.assertEqual(type(al), type(al + ul), 'type of radd result') + objid = id(ul) + pl += al + ul += al + self.assertEqual(pl[:], ul[:], 'in-place add') + self.assertEqual(objid, id(ul), 'in-place add id') + + for n in (-1,0,1,3): + pl, ul = self.lists_of_len() + self.assertEqual(list(pl * n), list(ul * n), 'mul by %d' % n) + self.assertEqual(type(ul), type(ul * n), 'type of mul by %d result' % n) + self.assertEqual(list(n * pl), list(n * ul), 'rmul by %d' % n) + self.assertEqual(type(ul), type(n * ul), 'type of rmul by %d result' % n) + objid = id(ul) + pl *= n + ul *= n + self.assertEqual(pl[:], ul[:], 'in-place mul by %d' % n) + self.assertEqual(objid, id(ul), 'in-place mul by %d id' % n) + + pl, ul = self.lists_of_len() + self.assertEqual(pl, ul, 'cmp for equal') + self.assert_(pl >= ul, 'cmp for gte self') + self.assert_(pl <= ul, 'cmp for lte self') + self.assert_(ul >= pl, 'cmp for self gte') + self.assert_(ul <= pl, 'cmp for self lte') + + self.assert_(pl + [5] > ul, 'cmp') + self.assert_(pl + [5] >= ul, 'cmp') + self.assert_(pl < ul + [2], 'cmp') + self.assert_(pl <= ul + [2], 'cmp') + self.assert_(ul + [5] > pl, 'cmp') + self.assert_(ul + [5] >= pl, 'cmp') + self.assert_(ul < pl + [2], 'cmp') + self.assert_(ul <= pl + [2], 'cmp') + + pl[1] = 20 + self.assert_(pl > ul, 'cmp for gt self') + self.assert_(ul < pl, 'cmp for self lt') + pl[1] = -20 + self.assert_(pl < ul, 'cmp for lt self') + self.assert_(pl < ul, 'cmp for lt self') + +class ListMixinTestSingle(ListMixinTest): + listType = UserListB + +def suite(): + s = unittest.TestSuite() + s.addTest(unittest.makeSuite(ListMixinTest)) + s.addTest(unittest.makeSuite(ListMixinTestSingle)) + return s + +def run(verbosity=2): + unittest.TextTestRunner(verbosity=verbosity).run(suite()) + +if __name__ == '__main__': + run() diff --git a/parts/django/django/contrib/gis/management/__init__.py b/parts/django/django/contrib/gis/management/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/django/contrib/gis/management/__init__.py diff --git a/parts/django/django/contrib/gis/management/base.py b/parts/django/django/contrib/gis/management/base.py new file mode 100644 index 0000000..c998063 --- /dev/null +++ b/parts/django/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 = '<arg arg ...>' + + 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/parts/django/django/contrib/gis/management/commands/__init__.py b/parts/django/django/contrib/gis/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/django/contrib/gis/management/commands/__init__.py diff --git a/parts/django/django/contrib/gis/management/commands/inspectdb.py b/parts/django/django/contrib/gis/management/commands/inspectdb.py new file mode 100644 index 0000000..937bb8e --- /dev/null +++ b/parts/django/django/contrib/gis/management/commands/inspectdb.py @@ -0,0 +1,32 @@ +from optparse import make_option + +from django.core.management.base import CommandError +from django.core.management.commands.inspectdb import Command as InspectDBCommand + +class Command(InspectDBCommand): + db_module = 'django.contrib.gis.db' + gis_tables = {} + + def get_field_type(self, connection, table_name, row): + field_type, field_params, field_notes = super(Command, self).get_field_type(connection, table_name, row) + if field_type == 'GeometryField': + geo_col = row[0] + # Getting a more specific field type and any additional parameters + # from the `get_geometry_type` routine for the spatial backend. + field_type, geo_params = connection.introspection.get_geometry_type(table_name, geo_col) + field_params.update(geo_params) + # Adding the table name and column to the `gis_tables` dictionary, this + # allows us to track which tables need a GeoManager. + if table_name in self.gis_tables: + self.gis_tables[table_name].append(geo_col) + else: + self.gis_tables[table_name] = [geo_col] + return field_type, field_params, field_notes + + def get_meta(self, table_name): + meta_lines = super(Command, self).get_meta(table_name) + if table_name in self.gis_tables: + # If the table is a geographic one, then we need make + # GeoManager the default manager for the model. + meta_lines.insert(0, ' objects = models.GeoManager()') + return meta_lines diff --git a/parts/django/django/contrib/gis/management/commands/ogrinspect.py b/parts/django/django/contrib/gis/management/commands/ogrinspect.py new file mode 100644 index 0000000..a495787 --- /dev/null +++ b/parts/django/django/contrib/gis/management/commands/ogrinspect.py @@ -0,0 +1,122 @@ +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) + + # Popping the verbosity global option, as it's not accepted by `_ogrinspect`. + verbosity = options.pop('verbosity', 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/parts/django/django/contrib/gis/maps/__init__.py b/parts/django/django/contrib/gis/maps/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/django/contrib/gis/maps/__init__.py diff --git a/parts/django/django/contrib/gis/maps/google/__init__.py b/parts/django/django/contrib/gis/maps/google/__init__.py new file mode 100644 index 0000000..9be689c --- /dev/null +++ b/parts/django/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 Web page. + + 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: + + <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + {{ google.xhtml }} + <head> + <title>Google Maps via GeoDjango</title> + {{ google.style }} + {{ google.scripts }} + </head> + {{ google.body }} + <div id="{{ google.dom_id }}" style="width:600px;height:400px;"></div> + </body> + </html> + + Note: If you want to be more explicit in your templates, the following are + equivalent: + {{ google.body }} => "<body {{ google.onload }} {{ google.onunload }}>" + {{ google.xhtml }} => "<html xmlns="http://www.w3.org/1999/xhtml" {{ google.xmlns }}>" + {{ google.style }} => "<style>{{ google.vml_css }}</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: + <html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"> + + - The `style` property provides the correct style tag for the CSS + properties required by Google Maps on IE: + <style type="text/css">v\:* {behavior:url(#default#VML);}</style> + + - The `scripts` property provides the necessary <script> tags for + including the Google Maps javascript, as well as including the + generated javascript. + + - The `body` property provides the correct attributes for the + body tag to load the generated javascript. By default, returns: + <body onload="gmap_load()" onunload="GUnload()"> + + - The `dom_id` property returns the DOM id for the map. Defaults to "map". + + The following attributes may be set or customized in your local settings: + * GOOGLE_MAPS_API_KEY: String of your Google Maps API key. These are tied to + to a domain. May be obtained from http://www.google.com/apis/maps/ + * GOOGLE_MAPS_API_VERSION (optional): Defaults to using "2.x" + * GOOGLE_MAPS_URL (optional): Must have a substitution ('%s') for the API + version. +""" +from django.contrib.gis.maps.google.gmap import GoogleMap, GoogleMapSet +from django.contrib.gis.maps.google.overlays import GEvent, GIcon, GMarker, GPolygon, GPolyline +from django.contrib.gis.maps.google.zoom import GoogleZoom diff --git a/parts/django/django/contrib/gis/maps/google/gmap.py b/parts/django/django/contrib/gis/maps/google/gmap.py new file mode 100644 index 0000000..cca5dc9 --- /dev/null +++ b/parts/django/django/contrib/gis/maps/google/gmap.py @@ -0,0 +1,226 @@ +from django.conf import settings +from django.contrib.gis import geos +from django.template.loader import render_to_string +from django.utils.safestring import mark_safe + +class GoogleMapException(Exception): pass +from django.contrib.gis.maps.google.overlays import GPolygon, GPolyline, GMarker, GIcon + +# The default Google Maps URL (for the API javascript) +# TODO: Internationalize for Japan, UK, etc. +GOOGLE_MAPS_URL='http://maps.google.com/maps?file=api&v=%s&key=' + +class GoogleMap(object): + "A class for generating Google Maps JavaScript." + + # String constants + onunload = mark_safe('onunload="GUnload()"') # Cleans up after Google Maps + vml_css = mark_safe('v\:* {behavior:url(#default#VML);}') # CSS for IE VML + xmlns = mark_safe('xmlns:v="urn:schemas-microsoft-com:vml"') # XML Namespace (for IE VML). + + def __init__(self, key=None, api_url=None, version=None, + center=None, zoom=None, dom_id='map', + kml_urls=[], polylines=None, polygons=None, markers=None, + template='gis/google/google-map.js', + js_module='geodjango', + extra_context={}): + + # The Google Maps API Key defined in the settings will be used + # if not passed in as a parameter. The use of an API key is + # _required_. + if not key: + try: + self.key = settings.GOOGLE_MAPS_API_KEY + except AttributeError: + raise GoogleMapException('Google Maps API Key not found (try adding GOOGLE_MAPS_API_KEY to your settings).') + else: + self.key = key + + # Getting the Google Maps API version, defaults to using the latest ("2.x"), + # this is not necessarily the most stable. + if not version: + self.version = getattr(settings, 'GOOGLE_MAPS_API_VERSION', '2.x') + else: + self.version = version + + # Can specify the API URL in the `api_url` keyword. + if not api_url: + self.api_url = mark_safe(getattr(settings, 'GOOGLE_MAPS_URL', GOOGLE_MAPS_URL) % self.version) + else: + self.api_url = api_url + + # Setting the DOM id of the map, the load function, the JavaScript + # template, and the KML URLs array. + self.dom_id = dom_id + self.extra_context = extra_context + self.js_module = js_module + self.template = template + self.kml_urls = kml_urls + + # Does the user want any GMarker, GPolygon, and/or GPolyline overlays? + overlay_info = [[GMarker, markers, 'markers'], + [GPolygon, polygons, 'polygons'], + [GPolyline, polylines, 'polylines']] + + for overlay_class, overlay_list, varname in overlay_info: + setattr(self, varname, []) + if overlay_list: + for overlay in overlay_list: + if isinstance(overlay, overlay_class): + getattr(self, varname).append(overlay) + else: + getattr(self, varname).append(overlay_class(overlay)) + + # If GMarker, GPolygons, and/or GPolylines are used the zoom will be + # automatically calculated via the Google Maps API. If both a zoom + # level and a center coordinate are provided with polygons/polylines, + # no automatic determination will occur. + self.calc_zoom = False + if self.polygons or self.polylines or self.markers: + if center is None or zoom is None: + self.calc_zoom = True + + # Defaults for the zoom level and center coordinates if the zoom + # is not automatically calculated. + if zoom is None: zoom = 4 + self.zoom = zoom + if center is None: center = (0, 0) + self.center = center + + def render(self): + """ + Generates the JavaScript necessary for displaying this Google Map. + """ + params = {'calc_zoom' : self.calc_zoom, + 'center' : self.center, + 'dom_id' : self.dom_id, + 'js_module' : self.js_module, + 'kml_urls' : self.kml_urls, + 'zoom' : self.zoom, + 'polygons' : self.polygons, + 'polylines' : self.polylines, + 'icons': self.icons, + 'markers' : self.markers, + } + params.update(self.extra_context) + return render_to_string(self.template, params) + + @property + def body(self): + "Returns HTML body tag for loading and unloading Google Maps javascript." + return mark_safe('<body %s %s>' % (self.onload, self.onunload)) + + @property + def onload(self): + "Returns the `onload` HTML <body> attribute." + return mark_safe('onload="%s.%s_load()"' % (self.js_module, self.dom_id)) + + @property + def api_script(self): + "Returns the <script> tag for the Google Maps API javascript." + return mark_safe('<script src="%s%s" type="text/javascript"></script>' % (self.api_url, self.key)) + + @property + def js(self): + "Returns only the generated Google Maps JavaScript (no <script> tags)." + return self.render() + + @property + def scripts(self): + "Returns all <script></script> tags required with Google Maps JavaScript." + return mark_safe('%s\n <script type="text/javascript">\n//<![CDATA[\n%s//]]>\n </script>' % (self.api_script, self.js)) + + @property + def style(self): + "Returns additional CSS styling needed for Google Maps on IE." + return mark_safe('<style type="text/css">%s</style>' % self.vml_css) + + @property + def xhtml(self): + "Returns XHTML information needed for IE VML overlays." + return mark_safe('<html xmlns="http://www.w3.org/1999/xhtml" %s>' % self.xmlns) + + @property + def icons(self): + "Returns a sequence of GIcon objects in this map." + return set([marker.icon for marker in self.markers if marker.icon]) + +class GoogleMapSet(GoogleMap): + + def __init__(self, *args, **kwargs): + """ + A class for generating sets of Google Maps that will be shown on the + same page together. + + Example: + gmapset = GoogleMapSet( GoogleMap( ... ), GoogleMap( ... ) ) + gmapset = GoogleMapSet( [ gmap1, gmap2] ) + """ + # The `google-multi.js` template is used instead of `google-single.js` + # by default. + template = kwargs.pop('template', 'gis/google/google-multi.js') + + # This is the template used to generate the GMap load JavaScript for + # each map in the set. + self.map_template = kwargs.pop('map_template', 'gis/google/google-single.js') + + # Running GoogleMap.__init__(), and resetting the template + # value with default obtained above. + super(GoogleMapSet, self).__init__(**kwargs) + self.template = template + + # If a tuple/list passed in as first element of args, then assume + if isinstance(args[0], (tuple, list)): + self.maps = args[0] + else: + self.maps = args + + # Generating DOM ids for each of the maps in the set. + self.dom_ids = ['map%d' % i for i in xrange(len(self.maps))] + + def load_map_js(self): + """ + Returns JavaScript containing all of the loading routines for each + map in this set. + """ + result = [] + for dom_id, gmap in zip(self.dom_ids, self.maps): + # Backup copies the GoogleMap DOM id and template attributes. + # They are overridden on each GoogleMap instance in the set so + # that only the loading JavaScript (and not the header variables) + # is used with the generated DOM ids. + tmp = (gmap.template, gmap.dom_id) + gmap.template = self.map_template + gmap.dom_id = dom_id + result.append(gmap.js) + # Restoring the backup values. + gmap.template, gmap.dom_id = tmp + return mark_safe(''.join(result)) + + def render(self): + """ + Generates the JavaScript for the collection of Google Maps in + this set. + """ + params = {'js_module' : self.js_module, + 'dom_ids' : self.dom_ids, + 'load_map_js' : self.load_map_js(), + 'icons' : self.icons, + } + params.update(self.extra_context) + return render_to_string(self.template, params) + + @property + def onload(self): + "Returns the `onload` HTML <body> attribute." + # Overloaded to use the `load` function defined in the + # `google-multi.js`, which calls the load routines for + # each one of the individual maps in the set. + return mark_safe('onload="%s.load()"' % self.js_module) + + @property + def icons(self): + "Returns a sequence of all icons in each map of the set." + icons = set() + for map in self.maps: icons |= map.icons + return icons diff --git a/parts/django/django/contrib/gis/maps/google/overlays.py b/parts/django/django/contrib/gis/maps/google/overlays.py new file mode 100644 index 0000000..c2ebb3c --- /dev/null +++ b/parts/django/django/contrib/gis/maps/google/overlays.py @@ -0,0 +1,301 @@ +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 GIcon(object): + """ + Creates a GIcon object to pass into a Gmarker object. + + The keyword arguments map to instance attributes of the same name. These, + in turn, correspond to a subset of the attributes of the official GIcon + javascript object: + + http://code.google.com/apis/maps/documentation/reference.html#GIcon + + Because a Google map often uses several different icons, a name field has + been added to the required arguments. + + Required Arguments: + varname: + A string which will become the basis for the js variable name of + the marker, for this reason, your code should assign a unique + name for each GIcon you instantiate, otherwise there will be + name space collisions in your javascript. + + Keyword Options: + image: + The url of the image to be used as the icon on the map defaults + to 'G_DEFAULT_ICON' + + iconsize: + a tuple representing the pixel size of the foreground (not the + shadow) image of the icon, in the format: (width, height) ex.: + + GIcon('fast_food', + image="/media/icon/star.png", + iconsize=(15,10)) + + Would indicate your custom icon was 15px wide and 10px height. + + shadow: + the url of the image of the icon's shadow + + shadowsize: + a tuple representing the pixel size of the shadow image, format is + the same as ``iconsize`` + + iconanchor: + a tuple representing the pixel coordinate relative to the top left + corner of the icon image at which this icon is anchored to the map. + In (x, y) format. x increases to the right in the Google Maps + coordinate system and y increases downwards in the Google Maps + coordinate system.) + + infowindowanchor: + The pixel coordinate relative to the top left corner of the icon + image at which the info window is anchored to this icon. + + """ + def __init__(self, varname, image=None, iconsize=None, + shadow=None, shadowsize=None, iconanchor=None, + infowindowanchor=None): + self.varname = varname + self.image = image + self.iconsize = iconsize + self.shadow = shadow + self.shadowsize = shadowsize + self.iconanchor = iconanchor + self.infowindowanchor = infowindowanchor + + def __cmp__(self, other): + return cmp(self.varname, other.varname) + + def __hash__(self): + # XOR with hash of GIcon type so that hash('varname') won't + # equal hash(GIcon('varname')). + return hash(self.__class__) ^ hash(self.varname) + +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, draggable=False, icon=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. + + draggable: + Draggable option for GMarker, disabled by default. + """ + # 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 + self.draggable = draggable + self.icon = icon + 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) + if self.icon: result.append('icon: %s' % self.icon.varname) + if self.draggable: result.append('draggable: true') + return '{%s}' % ','.join(result) + + @property + def js_params(self): + return '%s, %s' % (self.latlng, self.options()) diff --git a/parts/django/django/contrib/gis/maps/google/zoom.py b/parts/django/django/contrib/gis/maps/google/zoom.py new file mode 100644 index 0000000..abc3fbf --- /dev/null +++ b/parts/django/django/contrib/gis/maps/google/zoom.py @@ -0,0 +1,161 @@ +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 + +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 = self.get_width_height(env.extent) + center = env.centroid + + for z in xrange(self._nzoom): + # Getting the tile at the zoom level. + tile_w, tile_h = self.get_width_height(self.tile(center, z).extent) + + # 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 + + def get_width_height(self, extent): + """ + Returns the width and height for the given extent. + """ + # Getting the lower-left, upper-left, and upper-right + # coordinates from the extent. + ll = Point(extent[:2]) + ul = Point(extent[0], extent[3]) + ur = Point(extent[2:]) + # Calculating the width and height. + height = ll.distance(ul) + width = ul.distance(ur) + return width, height diff --git a/parts/django/django/contrib/gis/maps/openlayers/__init__.py b/parts/django/django/contrib/gis/maps/openlayers/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/django/contrib/gis/maps/openlayers/__init__.py diff --git a/parts/django/django/contrib/gis/measure.py b/parts/django/django/contrib/gis/measure.py new file mode 100644 index 0000000..a60398b --- /dev/null +++ b/parts/django/django/contrib/gis/measure.py @@ -0,0 +1,336 @@ +# Copyright (c) 2007, Robert Coup <robert.coup@onetrackmind.co.nz> +# 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 default units specified + from the given keyword arguments dictionary. + """ + val = 0.0 + for unit, value in kwargs.iteritems(): + if not isinstance(value, float): value = float(value) + 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 __rmul__(self, other): + return self * other + + 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 __rmul__(self, other): + return self * other + + 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/parts/django/django/contrib/gis/models.py b/parts/django/django/contrib/gis/models.py new file mode 100644 index 0000000..e379e82 --- /dev/null +++ b/parts/django/django/contrib/gis/models.py @@ -0,0 +1,9 @@ +from django.db import connection + +if (hasattr(connection.ops, 'spatial_version') and + not connection.ops.mysql): + # Getting the `SpatialRefSys` and `GeometryColumns` + # models for the default spatial backend. These + # aliases are provided for backwards-compatibility. + SpatialRefSys = connection.ops.spatial_ref_sys() + GeometryColumns = connection.ops.geometry_columns() diff --git a/parts/django/django/contrib/gis/shortcuts.py b/parts/django/django/contrib/gis/shortcuts.py new file mode 100644 index 0000000..a6fb892 --- /dev/null +++ b/parts/django/django/contrib/gis/shortcuts.py @@ -0,0 +1,32 @@ +import cStringIO, zipfile +from django.conf import settings +from django.http import HttpResponse +from django.template import loader + +def compress_kml(kml): + "Returns compressed KMZ from the given KML string." + kmz = cStringIO.StringIO() + zf = zipfile.ZipFile(kmz, 'a', zipfile.ZIP_DEFLATED) + zf.writestr('doc.kml', kml.encode(settings.DEFAULT_CHARSET)) + zf.close() + kmz.seek(0) + return kmz.read() + +def render_to_kml(*args, **kwargs): + "Renders the response as KML (using the correct MIME type)." + return HttpResponse(loader.render_to_string(*args, **kwargs), + mimetype='application/vnd.google-earth.kml+xml') + +def render_to_kmz(*args, **kwargs): + """ + Compresses the KML content and returns as KMZ (using the correct + MIME type). + """ + return HttpResponse(compress_kml(loader.render_to_string(*args, **kwargs)), + mimetype='application/vnd.google-earth.kmz') + + +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/parts/django/django/contrib/gis/sitemaps/__init__.py b/parts/django/django/contrib/gis/sitemaps/__init__.py new file mode 100644 index 0000000..9b6287f --- /dev/null +++ b/parts/django/django/contrib/gis/sitemaps/__init__.py @@ -0,0 +1,4 @@ +# Geo-enabled Sitemap classes. +from django.contrib.gis.sitemaps.georss import GeoRSSSitemap +from django.contrib.gis.sitemaps.kml import KMLSitemap, KMZSitemap + diff --git a/parts/django/django/contrib/gis/sitemaps/georss.py b/parts/django/django/contrib/gis/sitemaps/georss.py new file mode 100644 index 0000000..f75cf80 --- /dev/null +++ b/parts/django/django/contrib/gis/sitemaps/georss.py @@ -0,0 +1,53 @@ +from django.core import urlresolvers +from django.contrib.sitemaps import Sitemap + +class GeoRSSSitemap(Sitemap): + """ + A minimal hook to produce sitemaps for GeoRSS feeds. + """ + def __init__(self, feed_dict, slug_dict=None): + """ + This sitemap object initializes on a feed dictionary (as would be passed + to `django.contrib.syndication.views.feed`) and a slug dictionary. + If the slug dictionary is not defined, then it's assumed the keys provide + the URL parameter to the feed. However, if you have a complex feed (e.g., + you override `get_object`, then you'll need to provide a slug dictionary. + The slug dictionary should have the same keys as the feed dictionary, but + each value in the slug dictionary should be a sequence of slugs that may + be used for valid feeds. For example, let's say we have a feed that + returns objects for a specific ZIP code in our feed dictionary: + + feed_dict = {'zipcode' : ZipFeed} + + Then we would use a slug dictionary with a list of the zip code slugs + corresponding to feeds you want listed in the sitemap: + + slug_dict = {'zipcode' : ['77002', '77054']} + """ + # Setting up. + self.feed_dict = feed_dict + self.locations = [] + if slug_dict is None: slug_dict = {} + # Getting the feed locations. + for section in feed_dict.keys(): + if slug_dict.get(section, False): + for slug in slug_dict[section]: + self.locations.append('%s/%s' % (section, slug)) + else: + self.locations.append(section) + + def get_urls(self, page=1, site=None): + """ + This method is overrridden so the appropriate `geo_format` attribute + is placed on each URL element. + """ + urls = Sitemap.get_urls(self, page=page, site=site) + for url in urls: url['geo_format'] = 'georss' + return urls + + def items(self): + return self.locations + + def location(self, obj): + return urlresolvers.reverse('django.contrib.syndication.views.feed', args=(obj,)) + diff --git a/parts/django/django/contrib/gis/sitemaps/kml.py b/parts/django/django/contrib/gis/sitemaps/kml.py new file mode 100644 index 0000000..db30606 --- /dev/null +++ b/parts/django/django/contrib/gis/sitemaps/kml.py @@ -0,0 +1,63 @@ +from django.core import urlresolvers +from django.contrib.sitemaps import Sitemap +from django.contrib.gis.db.models.fields import GeometryField +from django.db import models + +class KMLSitemap(Sitemap): + """ + A minimal hook to produce KML sitemaps. + """ + geo_format = 'kml' + + def __init__(self, locations=None): + # If no locations specified, then we try to build for + # every model in installed applications. + self.locations = self._build_kml_sources(locations) + + def _build_kml_sources(self, sources): + """ + Goes through the given sources and returns a 3-tuple of + the application label, module name, and field name of every + GeometryField encountered in the sources. + + If no sources are provided, then all models. + """ + kml_sources = [] + if sources is None: + sources = models.get_models() + for source in sources: + if isinstance(source, models.base.ModelBase): + for field in source._meta.fields: + if isinstance(field, GeometryField): + kml_sources.append((source._meta.app_label, + source._meta.module_name, + field.name)) + elif isinstance(source, (list, tuple)): + if len(source) != 3: + raise ValueError('Must specify a 3-tuple of (app_label, module_name, field_name).') + kml_sources.append(source) + else: + raise TypeError('KML Sources must be a model or a 3-tuple.') + return kml_sources + + def get_urls(self, page=1, site=None): + """ + This method is overrridden so the appropriate `geo_format` attribute + is placed on each URL element. + """ + urls = Sitemap.get_urls(self, page=page, site=site) + for url in urls: url['geo_format'] = self.geo_format + return urls + + def items(self): + return self.locations + + def location(self, obj): + return urlresolvers.reverse('django.contrib.gis.sitemaps.views.%s' % self.geo_format, + kwargs={'label' : obj[0], + 'model' : obj[1], + 'field_name': obj[2], + } + ) +class KMZSitemap(KMLSitemap): + geo_format = 'kmz' diff --git a/parts/django/django/contrib/gis/sitemaps/views.py b/parts/django/django/contrib/gis/sitemaps/views.py new file mode 100644 index 0000000..02a0fc0 --- /dev/null +++ b/parts/django/django/contrib/gis/sitemaps/views.py @@ -0,0 +1,111 @@ +from django.http import HttpResponse, Http404 +from django.template import loader +from django.contrib.sites.models import get_current_site +from django.core import urlresolvers +from django.core.paginator import EmptyPage, PageNotAnInteger +from django.contrib.gis.db.models.fields import GeometryField +from django.db import connections, DEFAULT_DB_ALIAS +from django.db.models import get_model +from django.utils.encoding import smart_str + +from django.contrib.gis.shortcuts import render_to_kml, render_to_kmz + +def index(request, sitemaps): + """ + This view generates a sitemap index that uses the proper view + for resolving geographic section sitemap URLs. + """ + current_site = get_current_site(request) + sites = [] + protocol = request.is_secure() and 'https' or 'http' + for section, site in sitemaps.items(): + if callable(site): + pages = site().paginator.num_pages + else: + pages = site.paginator.num_pages + sitemap_url = urlresolvers.reverse('django.contrib.gis.sitemaps.views.sitemap', kwargs={'section': section}) + sites.append('%s://%s%s' % (protocol, current_site.domain, sitemap_url)) + + if pages > 1: + for page in range(2, pages+1): + sites.append('%s://%s%s?p=%s' % (protocol, current_site.domain, sitemap_url, page)) + xml = loader.render_to_string('sitemap_index.xml', {'sitemaps': sites}) + return HttpResponse(xml, mimetype='application/xml') + +def sitemap(request, sitemaps, section=None): + """ + This view generates a sitemap with additional geographic + elements defined by Google. + """ + maps, urls = [], [] + if section is not None: + if section not in sitemaps: + raise Http404("No sitemap available for section: %r" % section) + maps.append(sitemaps[section]) + else: + maps = sitemaps.values() + + page = request.GET.get("p", 1) + current_site = get_current_site(request) + for site in maps: + try: + if callable(site): + urls.extend(site().get_urls(page=page, site=current_site)) + else: + urls.extend(site.get_urls(page=page, site=current_site)) + except EmptyPage: + raise Http404("Page %s empty" % page) + except PageNotAnInteger: + raise Http404("No page '%s'" % page) + xml = smart_str(loader.render_to_string('gis/sitemaps/geo_sitemap.xml', {'urlset': urls})) + return HttpResponse(xml, mimetype='application/xml') + +def kml(request, label, model, field_name=None, compress=False, using=DEFAULT_DB_ALIAS): + """ + This view generates KML for the given app label, model, and field name. + + The model's default manager must be GeoManager, and the field name + must be that of a geographic field. + """ + placemarks = [] + klass = get_model(label, model) + if not klass: + raise Http404('You must supply a valid app label and module name. Got "%s.%s"' % (label, model)) + + if field_name: + try: + info = klass._meta.get_field_by_name(field_name) + if not isinstance(info[0], GeometryField): + raise Exception + except: + raise Http404('Invalid geometry field.') + + connection = connections[using] + + if connection.ops.postgis: + # PostGIS will take care of transformation. + placemarks = klass._default_manager.using(using).kml(field_name=field_name) + else: + # There's no KML method on Oracle or MySQL, so we use the `kml` + # attribute of the lazy geometry instead. + placemarks = [] + if connection.ops.oracle: + qs = klass._default_manager.using(using).transform(4326, field_name=field_name) + else: + qs = klass._default_manager.using(using).all() + for mod in qs: + setattr(mod, 'kml', getattr(mod, field_name).kml) + placemarks.append(mod) + + # Getting the render function and rendering to the correct. + if compress: + render = render_to_kmz + else: + render = render_to_kml + return render('gis/kml/placemarks.kml', {'places' : placemarks}) + +def kmz(request, label, model, field_name=None, using=DEFAULT_DB_ALIAS): + """ + This view returns KMZ for the given app label, model, and field name. + """ + return kml(request, label, model, field_name, compress=True, using=using) diff --git a/parts/django/django/contrib/gis/templates/gis/admin/openlayers.html b/parts/django/django/contrib/gis/templates/gis/admin/openlayers.html new file mode 100644 index 0000000..4292eb6 --- /dev/null +++ b/parts/django/django/contrib/gis/templates/gis/admin/openlayers.html @@ -0,0 +1,37 @@ +{% block extrastyle %} +<style type="text/css"> + #{{ id }}_map { width: {{ map_width }}px; height: {{ map_height }}px; } + #{{ id }}_map .aligned label { float:inherit; } + #{{ id }}_admin_map { position: relative; vertical-align: top; float: {{ LANGUAGE_BIDI|yesno:"right,left" }}; } + {% if not display_wkt %}#{{ id }} { display: none; }{% endif %} + .olControlEditingToolbar .olControlModifyFeatureItemActive { + background-image: url("{{ ADMIN_MEDIA_PREFIX }}img/gis/move_vertex_on.png"); + background-repeat: no-repeat; + } + .olControlEditingToolbar .olControlModifyFeatureItemInactive { + background-image: url("{{ ADMIN_MEDIA_PREFIX }}img/gis/move_vertex_off.png"); + background-repeat: no-repeat; + } +</style> +<!--[if IE]> +<style type="text/css"> + /* This fixes the mouse offset issues in IE. */ + #{{ id }}_admin_map { position: static; vertical-align: top; } + /* `font-size: 0` fixes the 1px border between tiles, but borks LayerSwitcher. + Thus, this is disabled until a better fix is found. + #{{ id }}_map { width: {{ map_width }}px; height: {{ map_height }}px; font-size: 0; } */ +</style> +<![endif]--> +{% endblock %} +<span id="{{ id }}_admin_map"> +<script type="text/javascript"> +//<![CDATA[ +{% block openlayers %}{% include "gis/admin/openlayers.js" %}{% endblock %} +//]]> +</script> +<div id="{{ id }}_map"{% if LANGUAGE_BIDI %} dir="ltr"{% endif %}></div> +<a href="javascript:{{ module }}.clearFeatures()">Delete all Features</a> +{% if display_wkt %}<p> WKT debugging window:</p>{% endif %} +<textarea id="{{ id }}" class="vWKTField required" cols="150" rows="10" name="{{ name }}">{{ wkt }}</textarea> +<script type="text/javascript">{% block init_function %}{{ module }}.init();{% endblock %}</script> +</span> diff --git a/parts/django/django/contrib/gis/templates/gis/admin/openlayers.js b/parts/django/django/contrib/gis/templates/gis/admin/openlayers.js new file mode 100644 index 0000000..4324693 --- /dev/null +++ b/parts/django/django/contrib/gis/templates/gis/admin/openlayers.js @@ -0,0 +1,167 @@ +{# 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 }}.modifiable = {{ modifiable|yesno:"true,false" }}; +{{ module }}.wkt_f = new OpenLayers.Format.WKT(); +{{ module }}.is_collection = {{ is_collection|yesno:"true,false" }}; +{{ module }}.collection_type = '{{ collection_type }}'; +{{ module }}.is_linestring = {{ is_linestring|yesno:"true,false" }}; +{{ module }}.is_polygon = {{ is_polygon|yesno:"true,false" }}; +{{ module }}.is_point = {{ is_point|yesno:"true,false" }}; +{% 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 ({{ module }}.modifiable){ + var mod = new OpenLayers.Control.ModifyFeature(lyr, {'displayClass': 'olControlModifyFeature'}); + {{ module }}.controls = [nav, draw_ctl, mod]; + } else { + if(!lyr.features.length){ + {{ module }}.controls = [nav, draw_ctl]; + } else { + {{ module }}.controls = [nav]; + } + } +} +{{ 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 <textarea> as EWKT (so that SRID is included). + var admin_geom = {{ module }}.read_wkt(wkt); + {{ module }}.write_wkt(admin_geom); + if ({{ module }}.is_collection){ + // If geometry collection, add each component individually so they may be + // edited individually. + for (var i = 0; i < {{ module }}.num_geom; i++){ + {{ module }}.layers.vector.addFeatures([new OpenLayers.Feature.Vector(admin_geom.geometry.components[i].clone())]); + } + } else { + {{ module }}.layers.vector.addFeatures([admin_geom]); + } + // Zooming to the bounds. + {{ module }}.map.zoomToExtent(admin_geom.geometry.getBounds()); + if ({{ module }}.is_point){ + {{ module }}.map.zoomTo({{ point_zoom }}); + } + } else { + {{ module }}.map.setCenter(new OpenLayers.LonLat({{ default_lon }}, {{ default_lat }}), {{ default_zoom }}); + } + // This allows editing of the geographic fields -- the modified WKT is + // written back to the content field (as EWKT, so that the ORM will know + // to transform back to original SRID). + {{ module }}.layers.vector.events.on({"featuremodified" : {{ module }}.modify_wkt}); + {{ module }}.layers.vector.events.on({"featureadded" : {{ module }}.add_wkt}); + {% block controls %} + // Map controls: + // Add geometry specific panel of toolbar controls + {{ module }}.getControls({{ module }}.layers.vector); + {{ module }}.panel.addControls({{ module }}.controls); + {{ module }}.map.addControl({{ module }}.panel); + {{ module }}.addSelectControl(); + // Then add optional visual controls + {% if mouse_position %}{{ module }}.map.addControl(new OpenLayers.Control.MousePosition());{% endif %} + {% if scale_text %}{{ module }}.map.addControl(new OpenLayers.Control.Scale());{% endif %} + {% if layerswitcher %}{{ module }}.map.addControl(new OpenLayers.Control.LayerSwitcher());{% endif %} + // Then add optional behavior controls + {% if not scrollable %}{{ module }}.map.getControlsByClass('OpenLayers.Control.Navigation')[0].disableZoomWheel();{% endif %} + {% endblock %} + if (wkt){ + if ({{ module }}.modifiable){ + {{ module }}.enableEditing(); + } + } else { + {{ module }}.enableDrawing(); + } +} diff --git a/parts/django/django/contrib/gis/templates/gis/admin/osm.html b/parts/django/django/contrib/gis/templates/gis/admin/osm.html new file mode 100644 index 0000000..b74b41f --- /dev/null +++ b/parts/django/django/contrib/gis/templates/gis/admin/osm.html @@ -0,0 +1,2 @@ +{% extends "gis/admin/openlayers.html" %} +{% block openlayers %}{% include "gis/admin/osm.js" %}{% endblock %}
\ No newline at end of file diff --git a/parts/django/django/contrib/gis/templates/gis/admin/osm.js b/parts/django/django/contrib/gis/templates/gis/admin/osm.js new file mode 100644 index 0000000..2a1f59e --- /dev/null +++ b/parts/django/django/contrib/gis/templates/gis/admin/osm.js @@ -0,0 +1,2 @@ +{% extends "gis/admin/openlayers.js" %} +{% block base_layer %}new OpenLayers.Layer.OSM.Mapnik("OpenStreetMap (Mapnik)");{% endblock %} diff --git a/parts/django/django/contrib/gis/templates/gis/google/google-map.html b/parts/django/django/contrib/gis/templates/gis/google/google-map.html new file mode 100644 index 0000000..fb60e44 --- /dev/null +++ b/parts/django/django/contrib/gis/templates/gis/google/google-map.html @@ -0,0 +1,12 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" {{ gmap.xmlns }}> +<head> + <title>{% block title %}Google Maps via GeoDjango{% endblock %}</title> + {{ gmap.style }} + {{ gmap.scripts }} +</head> +<body {{ gmap.onload }} {{ gmap.onunload }}> +{% if gmap.dom_ids %}{% for dom_id in gmap.dom_ids %}<div id="{{ dom_id }}" style="width:600px;height:400px;"></div>{% endfor %} +{% else %}<div id="{{ gmap.dom_id }}" style="width:600px;height:400px;"></div>{% endif %} +</body> +</html> diff --git a/parts/django/django/contrib/gis/templates/gis/google/google-map.js b/parts/django/django/contrib/gis/templates/gis/google/google-map.js new file mode 100644 index 0000000..06f11e3 --- /dev/null +++ b/parts/django/django/contrib/gis/templates/gis/google/google-map.js @@ -0,0 +1,35 @@ +{% autoescape off %} +{% block vars %}var geodjango = {};{% for icon in icons %} +var {{ icon.varname }} = new GIcon(G_DEFAULT_ICON); +{% if icon.image %}{{ icon.varname }}.image = "{{ icon.image }}";{% endif %} +{% if icon.shadow %}{{ icon.varname }}.shadow = "{{ icon.shadow }}";{% endif %} {% if icon.shadowsize %}{{ icon.varname }}.shadowSize = new GSize({{ icon.shadowsize.0 }}, {{ icon.shadowsize.1 }});{% endif %} +{% if icon.iconanchor %}{{ icon.varname }}.iconAnchor = new GPoint({{ icon.iconanchor.0 }}, {{ icon.iconanchor.1 }});{% endif %} {% if icon.iconsize %}{{ icon.varname }}.iconSize = new GSize({{ icon.iconsize.0 }}, {{ icon.iconsize.1 }});{% endif %} +{% if icon.infowindowanchor %}{{ icon.varname }}.infoWindowAnchor = new GPoint({{ icon.infowindowanchor.0 }}, {{ icon.infowindowanchor.1 }});{% endif %}{% endfor %} +{% endblock vars %}{% block functions %} +{% block load %}{{ js_module }}.{{ dom_id }}_load = function(){ + if (GBrowserIsCompatible()) { + {{ js_module }}.{{ dom_id }} = new GMap2(document.getElementById("{{ dom_id }}")); + {{ js_module }}.{{ dom_id }}.setCenter(new GLatLng({{ center.1 }}, {{ center.0 }}), {{ zoom }}); + {% block controls %}{{ js_module }}.{{ dom_id }}.setUIToDefault();{% endblock %} + {% if calc_zoom %}var bounds = new GLatLngBounds(); var tmp_bounds = new GLatLngBounds();{% endif %} + {% for kml_url in kml_urls %}{{ js_module }}.{{ dom_id }}_kml{{ forloop.counter }} = new GGeoXml("{{ kml_url }}"); + {{ js_module }}.{{ dom_id }}.addOverlay({{ js_module }}.{{ dom_id }}_kml{{ forloop.counter }});{% endfor %} + {% for polygon in polygons %}{{ js_module }}.{{ dom_id }}_poly{{ forloop.counter }} = new {{ polygon }}; + {{ js_module }}.{{ dom_id }}.addOverlay({{ js_module }}.{{ dom_id }}_poly{{ forloop.counter }}); + {% for event in polygon.events %}GEvent.addListener({{ js_module }}.{{ dom_id }}_poly{{ forloop.parentloop.counter }}, {{ event }});{% endfor %} + {% if calc_zoom %}tmp_bounds = {{ js_module }}.{{ dom_id }}_poly{{ forloop.counter }}.getBounds(); bounds.extend(tmp_bounds.getSouthWest()); bounds.extend(tmp_bounds.getNorthEast());{% endif %}{% endfor %} + {% for polyline in polylines %}{{ js_module }}.{{ dom_id }}_polyline{{ forloop.counter }} = new {{ polyline }}; + {{ js_module }}.{{ dom_id }}.addOverlay({{ js_module }}.{{ dom_id }}_polyline{{ forloop.counter }}); + {% for event in polyline.events %}GEvent.addListener({{ js_module }}.{{ dom_id }}_polyline{{ forloop.parentloop.counter }}, {{ event }}); {% endfor %} + {% if calc_zoom %}tmp_bounds = {{ js_module }}.{{ dom_id }}_polyline{{ forloop.counter }}.getBounds(); bounds.extend(tmp_bounds.getSouthWest()); bounds.extend(tmp_bounds.getNorthEast());{% endif %}{% endfor %} + {% for marker in markers %}{{ js_module }}.{{ dom_id }}_marker{{ forloop.counter }} = new {{ marker }}; + {{ js_module }}.{{ dom_id }}.addOverlay({{ js_module }}.{{ dom_id }}_marker{{ forloop.counter }}); + {% for event in marker.events %}GEvent.addListener({{ js_module }}.{{ dom_id }}_marker{{ forloop.parentloop.counter }}, {{ event }}); {% endfor %} + {% if calc_zoom %}bounds.extend({{ js_module }}.{{ dom_id }}_marker{{ forloop.counter }}.getLatLng()); {% endif %}{% endfor %} + {% if calc_zoom %}{{ js_module }}.{{ dom_id }}.setCenter(bounds.getCenter(), {{ js_module }}.{{ dom_id }}.getBoundsZoomLevel(bounds));{% endif %} + {% block load_extra %}{% endblock %} + }else { + alert("Sorry, the Google Maps API is not compatible with this browser."); + } +} +{% endblock load %}{% endblock functions %}{% endautoescape %} diff --git a/parts/django/django/contrib/gis/templates/gis/google/google-multi.js b/parts/django/django/contrib/gis/templates/gis/google/google-multi.js new file mode 100644 index 0000000..e3c7e8f --- /dev/null +++ b/parts/django/django/contrib/gis/templates/gis/google/google-multi.js @@ -0,0 +1,8 @@ +{% extends "gis/google/google-map.js" %} +{% block functions %} +{{ load_map_js }} +{{ js_module }}.load = function(){ + {% for dom_id in dom_ids %}{{ js_module }}.{{ dom_id }}_load(); + {% endfor %} +} +{% endblock %}
\ No newline at end of file diff --git a/parts/django/django/contrib/gis/templates/gis/google/google-single.js b/parts/django/django/contrib/gis/templates/gis/google/google-single.js new file mode 100644 index 0000000..b930e45 --- /dev/null +++ b/parts/django/django/contrib/gis/templates/gis/google/google-single.js @@ -0,0 +1,2 @@ +{% extends "gis/google/google-map.js" %} +{% block vars %}{# No vars here because used within GoogleMapSet #}{% endblock %}
\ No newline at end of file diff --git a/parts/django/django/contrib/gis/templates/gis/kml/base.kml b/parts/django/django/contrib/gis/templates/gis/kml/base.kml new file mode 100644 index 0000000..374404c --- /dev/null +++ b/parts/django/django/contrib/gis/templates/gis/kml/base.kml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<kml xmlns="http://earth.google.com/kml/{% block kml_version %}2.1{% endblock %}"> +<Document>{% block name %}{% endblock %} +{% block placemarks %}{% endblock %} +</Document> +</kml> diff --git a/parts/django/django/contrib/gis/templates/gis/kml/placemarks.kml b/parts/django/django/contrib/gis/templates/gis/kml/placemarks.kml new file mode 100644 index 0000000..ea2ac19 --- /dev/null +++ b/parts/django/django/contrib/gis/templates/gis/kml/placemarks.kml @@ -0,0 +1,8 @@ +{% extends "gis/kml/base.kml" %} +{% block placemarks %}{% for place in places %} + <Placemark> + <name>{% if place.name %}{{ place.name }}{% else %}{{ place }}{% endif %}</name> + <description>{% if place.description %}{{ place.description }}{% else %}{{ place }}{% endif %}</description> + {{ place.kml|safe }} + </Placemark>{% endfor %}{% endblock %} + diff --git a/parts/django/django/contrib/gis/templates/gis/sitemaps/geo_sitemap.xml b/parts/django/django/contrib/gis/templates/gis/sitemaps/geo_sitemap.xml new file mode 100644 index 0000000..dbf858e --- /dev/null +++ b/parts/django/django/contrib/gis/templates/gis/sitemaps/geo_sitemap.xml @@ -0,0 +1,17 @@ +{% autoescape off %}<?xml version="1.0" encoding="UTF-8"?> +<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:geo="http://www.google.com/geo/schemas/sitemap/1.0"> +{% spaceless %} +{% for url in urlset %} + <url> + <loc>{{ url.location|escape }}</loc> + {% if url.lastmod %}<lastmod>{{ url.lastmod|date:"Y-m-d" }}</lastmod>{% endif %} + {% if url.changefreq %}<changefreq>{{ url.changefreq }}</changefreq>{% endif %} + {% if url.priority %}<priority>{{ url.priority }}</priority>{% endif %} + {% if url.geo_format %}<geo:geo> + <geo:format>{{ url.geo_format }}</geo:format> + </geo:geo>{% endif %} + </url> +{% endfor %} +{% endspaceless %} +</urlset> +{% endautoescape %} diff --git a/parts/django/django/contrib/gis/tests/__init__.py b/parts/django/django/contrib/gis/tests/__init__.py new file mode 100644 index 0000000..138c291 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/__init__.py @@ -0,0 +1,141 @@ +import unittest + +from django.conf import settings +from django.test.simple import build_suite, DjangoTestSuiteRunner + + +def run_tests(*args, **kwargs): + from django.test.simple import run_tests as base_run_tests + return base_run_tests(*args, **kwargs) + + +def run_gis_tests(test_labels, verbosity=1, interactive=True, failfast=False, extra_tests=None): + import warnings + warnings.warn( + 'The run_gis_tests() test runner has been deprecated in favor of GeoDjangoTestSuiteRunner.', + PendingDeprecationWarning + ) + test_runner = GeoDjangoTestSuiteRunner(verbosity=verbosity, interactive=interactive, failfast=failfast) + return test_runner.run_tests(test_labels, extra_tests=extra_tests) + + +def geo_apps(namespace=True, runtests=False): + """ + Returns a list of GeoDjango test applications that reside in + `django.contrib.gis.tests` that can be used with the current + database and the spatial libraries that are installed. + """ + from django.db import connection + from django.contrib.gis.geos import GEOS_PREPARE + from django.contrib.gis.gdal import HAS_GDAL + + apps = ['geoapp', 'relatedapp'] + + # No distance queries on MySQL. + if not connection.ops.mysql: + apps.append('distapp') + + # Test geography support with PostGIS 1.5+. + if connection.ops.postgis and connection.ops.geography: + apps.append('geogapp') + + # The following GeoDjango test apps depend on GDAL support. + if HAS_GDAL: + # 3D apps use LayerMapping, which uses GDAL. + if connection.ops.postgis and GEOS_PREPARE: + apps.append('geo3d') + + apps.append('layermap') + + if runtests: + return [('django.contrib.gis.tests', app) for app in apps] + elif namespace: + return ['django.contrib.gis.tests.%s' % app + for app in apps] + else: + return apps + + +def geodjango_suite(apps=True): + """ + Returns a TestSuite consisting only of GeoDjango tests that can be run. + """ + import sys + from django.db.models import get_app + + suite = unittest.TestSuite() + + # Adding the GEOS tests. + from django.contrib.gis.geos import tests as geos_tests + suite.addTest(geos_tests.suite()) + + # Adding the measurment tests. + from django.contrib.gis.tests import test_measure + suite.addTest(test_measure.suite()) + + # Adding GDAL tests, and any test suite that depends on GDAL, to the + # suite if GDAL is available. + from django.contrib.gis.gdal import HAS_GDAL + if HAS_GDAL: + from django.contrib.gis.gdal import tests as gdal_tests + suite.addTest(gdal_tests.suite()) + + from django.contrib.gis.tests import test_spatialrefsys, test_geoforms + suite.addTest(test_spatialrefsys.suite()) + suite.addTest(test_geoforms.suite()) + else: + sys.stderr.write('GDAL not available - no tests requiring GDAL will be run.\n') + + # Add GeoIP tests to the suite, if the library and data is available. + from django.contrib.gis.utils import HAS_GEOIP + if HAS_GEOIP and hasattr(settings, 'GEOIP_PATH'): + from django.contrib.gis.tests import test_geoip + suite.addTest(test_geoip.suite()) + + # Finally, adding the suites for each of the GeoDjango test apps. + if apps: + for app_name in geo_apps(namespace=False): + suite.addTest(build_suite(get_app(app_name))) + + return suite + + +class GeoDjangoTestSuiteRunner(DjangoTestSuiteRunner): + + def setup_test_environment(self, **kwargs): + super(GeoDjangoTestSuiteRunner, self).setup_test_environment(**kwargs) + + # Saving original values of INSTALLED_APPS, ROOT_URLCONF, and SITE_ID. + self.old_installed = getattr(settings, 'INSTALLED_APPS', None) + self.old_root_urlconf = getattr(settings, 'ROOT_URLCONF', '') + self.old_site_id = getattr(settings, 'SITE_ID', None) + + # Constructing the new INSTALLED_APPS, and including applications + # within the GeoDjango test namespace. + new_installed = ['django.contrib.sites', + 'django.contrib.sitemaps', + 'django.contrib.gis', + ] + + # Calling out to `geo_apps` to get GeoDjango applications supported + # for testing. + new_installed.extend(geo_apps()) + settings.INSTALLED_APPS = new_installed + + # SITE_ID needs to be set + settings.SITE_ID = 1 + + # ROOT_URLCONF needs to be set, else `AttributeErrors` are raised + # when TestCases are torn down that have `urls` defined. + settings.ROOT_URLCONF = '' + + + def teardown_test_environment(self, **kwargs): + super(GeoDjangoTestSuiteRunner, self).teardown_test_environment(**kwargs) + settings.INSTALLED_APPS = self.old_installed + settings.ROOT_URLCONF = self.old_root_urlconf + settings.SITE_ID = self.old_site_id + + + def build_suite(self, test_labels, extra_tests=None, **kwargs): + return geodjango_suite() diff --git a/parts/django/django/contrib/gis/tests/data/cities/cities.dbf b/parts/django/django/contrib/gis/tests/data/cities/cities.dbf Binary files differnew file mode 100644 index 0000000..8b27633 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/cities/cities.dbf diff --git a/parts/django/django/contrib/gis/tests/data/cities/cities.prj b/parts/django/django/contrib/gis/tests/data/cities/cities.prj new file mode 100644 index 0000000..a30c00a --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/cities/cities.prj @@ -0,0 +1 @@ +GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]
\ No newline at end of file diff --git a/parts/django/django/contrib/gis/tests/data/cities/cities.shp b/parts/django/django/contrib/gis/tests/data/cities/cities.shp Binary files differnew file mode 100644 index 0000000..1c46ccc --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/cities/cities.shp diff --git a/parts/django/django/contrib/gis/tests/data/cities/cities.shx b/parts/django/django/contrib/gis/tests/data/cities/cities.shx Binary files differnew file mode 100644 index 0000000..6be3fd6 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/cities/cities.shx diff --git a/parts/django/django/contrib/gis/tests/data/counties/counties.dbf b/parts/django/django/contrib/gis/tests/data/counties/counties.dbf Binary files differnew file mode 100644 index 0000000..ccdbb26 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/counties/counties.dbf diff --git a/parts/django/django/contrib/gis/tests/data/counties/counties.shp b/parts/django/django/contrib/gis/tests/data/counties/counties.shp Binary files differnew file mode 100644 index 0000000..2e7dca5 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/counties/counties.shp diff --git a/parts/django/django/contrib/gis/tests/data/counties/counties.shx b/parts/django/django/contrib/gis/tests/data/counties/counties.shx Binary files differnew file mode 100644 index 0000000..46ed3bb --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/counties/counties.shx diff --git a/parts/django/django/contrib/gis/tests/data/geometries.json.gz b/parts/django/django/contrib/gis/tests/data/geometries.json.gz Binary files differnew file mode 100644 index 0000000..683dc83 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/geometries.json.gz diff --git a/parts/django/django/contrib/gis/tests/data/interstates/interstates.dbf b/parts/django/django/contrib/gis/tests/data/interstates/interstates.dbf Binary files differnew file mode 100644 index 0000000..a88d171 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/interstates/interstates.dbf diff --git a/parts/django/django/contrib/gis/tests/data/interstates/interstates.prj b/parts/django/django/contrib/gis/tests/data/interstates/interstates.prj new file mode 100644 index 0000000..a30c00a --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/interstates/interstates.prj @@ -0,0 +1 @@ +GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]
\ No newline at end of file diff --git a/parts/django/django/contrib/gis/tests/data/interstates/interstates.shp b/parts/django/django/contrib/gis/tests/data/interstates/interstates.shp Binary files differnew file mode 100644 index 0000000..6d93de7 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/interstates/interstates.shp diff --git a/parts/django/django/contrib/gis/tests/data/interstates/interstates.shx b/parts/django/django/contrib/gis/tests/data/interstates/interstates.shx Binary files differnew file mode 100644 index 0000000..7b9088a --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/interstates/interstates.shx diff --git a/parts/django/django/contrib/gis/tests/data/test_point/test_point.dbf b/parts/django/django/contrib/gis/tests/data/test_point/test_point.dbf Binary files differnew file mode 100644 index 0000000..b2b4eca --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/test_point/test_point.dbf diff --git a/parts/django/django/contrib/gis/tests/data/test_point/test_point.prj b/parts/django/django/contrib/gis/tests/data/test_point/test_point.prj new file mode 100644 index 0000000..a30c00a --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/test_point/test_point.prj @@ -0,0 +1 @@ +GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]
\ No newline at end of file diff --git a/parts/django/django/contrib/gis/tests/data/test_point/test_point.shp b/parts/django/django/contrib/gis/tests/data/test_point/test_point.shp Binary files differnew file mode 100644 index 0000000..95e8b0a --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/test_point/test_point.shp diff --git a/parts/django/django/contrib/gis/tests/data/test_point/test_point.shx b/parts/django/django/contrib/gis/tests/data/test_point/test_point.shx Binary files differnew file mode 100644 index 0000000..087f3da --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/test_point/test_point.shx diff --git a/parts/django/django/contrib/gis/tests/data/test_poly/test_poly.dbf b/parts/django/django/contrib/gis/tests/data/test_poly/test_poly.dbf Binary files differnew file mode 100644 index 0000000..7965bd6 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/test_poly/test_poly.dbf diff --git a/parts/django/django/contrib/gis/tests/data/test_poly/test_poly.prj b/parts/django/django/contrib/gis/tests/data/test_poly/test_poly.prj new file mode 100644 index 0000000..a30c00a --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/test_poly/test_poly.prj @@ -0,0 +1 @@ +GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]
\ No newline at end of file diff --git a/parts/django/django/contrib/gis/tests/data/test_poly/test_poly.shp b/parts/django/django/contrib/gis/tests/data/test_poly/test_poly.shp Binary files differnew file mode 100644 index 0000000..b22930b --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/test_poly/test_poly.shp diff --git a/parts/django/django/contrib/gis/tests/data/test_poly/test_poly.shx b/parts/django/django/contrib/gis/tests/data/test_poly/test_poly.shx Binary files differnew file mode 100644 index 0000000..c92f78b --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/test_poly/test_poly.shx diff --git a/parts/django/django/contrib/gis/tests/data/test_vrt/test_vrt.csv b/parts/django/django/contrib/gis/tests/data/test_vrt/test_vrt.csv new file mode 100644 index 0000000..dff648f --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/test_vrt/test_vrt.csv @@ -0,0 +1,4 @@ +POINT_X,POINT_Y,NUM +1.0,2.0,5 +5.0,23.0,17 +100.0,523.5,23 diff --git a/parts/django/django/contrib/gis/tests/data/test_vrt/test_vrt.vrt b/parts/django/django/contrib/gis/tests/data/test_vrt/test_vrt.vrt new file mode 100644 index 0000000..979c179 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/data/test_vrt/test_vrt.vrt @@ -0,0 +1,7 @@ +<OGRVRTDataSource> +<OGRVRTLayer name="test_vrt"> +<SrcDataSource relativeToVRT="1">test_vrt.csv</SrcDataSource> +<GeometryType>wkbPoint25D</GeometryType> +<GeometryField encoding="PointFromColumns" x="POINT_X" y="POINT_Y" z="NUM"/> +</OGRVRTLayer> +</OGRVRTDataSource>
\ No newline at end of file diff --git a/parts/django/django/contrib/gis/tests/distapp/__init__.py b/parts/django/django/contrib/gis/tests/distapp/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/distapp/__init__.py diff --git a/parts/django/django/contrib/gis/tests/distapp/models.py b/parts/django/django/contrib/gis/tests/distapp/models.py new file mode 100644 index 0000000..76e7d3a --- /dev/null +++ b/parts/django/django/contrib/gis/tests/distapp/models.py @@ -0,0 +1,50 @@ +from django.contrib.gis.db import models + +class SouthTexasCity(models.Model): + "City model on projected coordinate system for South Texas." + name = models.CharField(max_length=30) + point = models.PointField(srid=32140) + objects = models.GeoManager() + def __unicode__(self): return self.name + +class SouthTexasCityFt(models.Model): + "Same City model as above, but U.S. survey feet are the units." + name = models.CharField(max_length=30) + point = models.PointField(srid=2278) + objects = models.GeoManager() + def __unicode__(self): return self.name + +class AustraliaCity(models.Model): + "City model for Australia, using WGS84." + name = models.CharField(max_length=30) + point = models.PointField() + objects = models.GeoManager() + def __unicode__(self): return self.name + +class CensusZipcode(models.Model): + "Model for a few South Texas ZIP codes (in original Census NAD83)." + name = models.CharField(max_length=5) + poly = models.PolygonField(srid=4269) + objects = models.GeoManager() + def __unicode__(self): return self.name + +class SouthTexasZipcode(models.Model): + "Model for a few South Texas ZIP codes." + name = models.CharField(max_length=5) + poly = models.PolygonField(srid=32140, null=True) + objects = models.GeoManager() + def __unicode__(self): return self.name + +class Interstate(models.Model): + "Geodetic model for U.S. Interstates." + name = models.CharField(max_length=10) + path = models.LineStringField() + objects = models.GeoManager() + def __unicode__(self): return self.name + +class SouthTexasInterstate(models.Model): + "Projected model for South Texas Interstates." + name = models.CharField(max_length=10) + path = models.LineStringField(srid=32140) + objects = models.GeoManager() + def __unicode__(self): return self.name diff --git a/parts/django/django/contrib/gis/tests/distapp/tests.py b/parts/django/django/contrib/gis/tests/distapp/tests.py new file mode 100644 index 0000000..4f81a91 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/distapp/tests.py @@ -0,0 +1,358 @@ +import os +from decimal import Decimal + +from django.db import connection +from django.db.models import Q +from django.contrib.gis.geos import GEOSGeometry, Point, LineString +from django.contrib.gis.measure import D # alias for Distance +from django.contrib.gis.tests.utils import oracle, postgis, spatialite, no_oracle, no_spatialite +from django.test import TestCase + +from models import AustraliaCity, Interstate, SouthTexasInterstate, \ + SouthTexasCity, SouthTexasCityFt, CensusZipcode, SouthTexasZipcode + +class DistanceTest(TestCase): + + # A point we are testing distances with -- using a WGS84 + # coordinate that'll be implicitly transormed to that to + # the coordinate system of the field, EPSG:32140 (Texas South Central + # w/units in meters) + stx_pnt = GEOSGeometry('POINT (-95.370401017314293 29.704867409475465)', 4326) + # Another one for Australia + au_pnt = GEOSGeometry('POINT (150.791 -34.4919)', 4326) + + def get_names(self, qs): + cities = [c.name for c in qs] + cities.sort() + return cities + + def test01_init(self): + "Test initialization of distance models." + self.assertEqual(9, SouthTexasCity.objects.count()) + self.assertEqual(9, SouthTexasCityFt.objects.count()) + self.assertEqual(11, AustraliaCity.objects.count()) + self.assertEqual(4, SouthTexasZipcode.objects.count()) + self.assertEqual(4, CensusZipcode.objects.count()) + self.assertEqual(1, Interstate.objects.count()) + self.assertEqual(1, SouthTexasInterstate.objects.count()) + + @no_spatialite + def test02_dwithin(self): + "Testing the `dwithin` lookup type." + # Distances -- all should be equal (except for the + # degree/meter pair in au_cities, that's somewhat + # approximate). + tx_dists = [(7000, 22965.83), D(km=7), D(mi=4.349)] + au_dists = [(0.5, 32000), D(km=32), D(mi=19.884)] + + # Expected cities for Australia and Texas. + tx_cities = ['Downtown Houston', 'Southside Place'] + au_cities = ['Mittagong', 'Shellharbour', 'Thirroul', 'Wollongong'] + + # Performing distance queries on two projected coordinate systems one + # with units in meters and the other in units of U.S. survey feet. + for dist in tx_dists: + if isinstance(dist, tuple): dist1, dist2 = dist + else: dist1 = dist2 = dist + qs1 = SouthTexasCity.objects.filter(point__dwithin=(self.stx_pnt, dist1)) + qs2 = SouthTexasCityFt.objects.filter(point__dwithin=(self.stx_pnt, dist2)) + for qs in qs1, qs2: + self.assertEqual(tx_cities, self.get_names(qs)) + + # Now performing the `dwithin` queries on a geodetic coordinate system. + for dist in au_dists: + if isinstance(dist, D) and not oracle: type_error = True + else: type_error = False + + if isinstance(dist, tuple): + if oracle: dist = dist[1] + else: dist = dist[0] + + # Creating the query set. + qs = AustraliaCity.objects.order_by('name') + if type_error: + # A ValueError should be raised on PostGIS when trying to pass + # Distance objects into a DWithin query using a geodetic field. + self.assertRaises(ValueError, AustraliaCity.objects.filter(point__dwithin=(self.au_pnt, dist)).count) + else: + self.assertEqual(au_cities, self.get_names(qs.filter(point__dwithin=(self.au_pnt, dist)))) + + def test03a_distance_method(self): + "Testing the `distance` GeoQuerySet method on projected coordinate systems." + # The point for La Grange, TX + lagrange = GEOSGeometry('POINT(-96.876369 29.905320)', 4326) + # Reference distances in feet and in meters. Got these values from + # using the provided raw SQL statements. + # SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326), 32140)) FROM distapp_southtexascity; + m_distances = [147075.069813, 139630.198056, 140888.552826, + 138809.684197, 158309.246259, 212183.594374, + 70870.188967, 165337.758878, 139196.085105] + # SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326), 2278)) FROM distapp_southtexascityft; + # Oracle 11 thinks this is not a projected coordinate system, so it's s + # not tested. + ft_distances = [482528.79154625, 458103.408123001, 462231.860397575, + 455411.438904354, 519386.252102563, 696139.009211594, + 232513.278304279, 542445.630586414, 456679.155883207] + + # Testing using different variations of parameters and using models + # with different projected coordinate systems. + dist1 = SouthTexasCity.objects.distance(lagrange, field_name='point') + dist2 = SouthTexasCity.objects.distance(lagrange) # Using GEOSGeometry parameter + if spatialite or oracle: + dist_qs = [dist1, dist2] + else: + dist3 = SouthTexasCityFt.objects.distance(lagrange.ewkt) # Using EWKT string parameter. + dist4 = SouthTexasCityFt.objects.distance(lagrange) + dist_qs = [dist1, dist2, dist3, dist4] + + # Original query done on PostGIS, have to adjust AlmostEqual tolerance + # for Oracle. + if oracle: tol = 2 + else: tol = 5 + + # Ensuring expected distances are returned for each distance queryset. + for qs in dist_qs: + for i, c in enumerate(qs): + self.assertAlmostEqual(m_distances[i], c.distance.m, tol) + self.assertAlmostEqual(ft_distances[i], c.distance.survey_ft, tol) + + @no_spatialite + def test03b_distance_method(self): + "Testing the `distance` GeoQuerySet method on geodetic coordnate systems." + if oracle: tol = 2 + else: tol = 5 + + # Testing geodetic distance calculation with a non-point geometry + # (a LineString of Wollongong and Shellharbour coords). + ls = LineString( ( (150.902, -34.4245), (150.87, -34.5789) ) ) + if oracle or connection.ops.geography: + # Reference query: + # SELECT ST_distance_sphere(point, ST_GeomFromText('LINESTRING(150.9020 -34.4245,150.8700 -34.5789)', 4326)) FROM distapp_australiacity ORDER BY name; + distances = [1120954.92533513, 140575.720018241, 640396.662906304, + 60580.9693849269, 972807.955955075, 568451.8357838, + 40435.4335201384, 0, 68272.3896586844, 12375.0643697706, 0] + qs = AustraliaCity.objects.distance(ls).order_by('name') + for city, distance in zip(qs, distances): + # Testing equivalence to within a meter. + self.assertAlmostEqual(distance, city.distance.m, 0) + else: + # PostGIS 1.4 and below is limited to disance queries only + # to/from point geometries, check for raising of ValueError. + self.assertRaises(ValueError, AustraliaCity.objects.distance, ls) + self.assertRaises(ValueError, AustraliaCity.objects.distance, ls.wkt) + + # Got the reference distances using the raw SQL statements: + # SELECT ST_distance_spheroid(point, ST_GeomFromText('POINT(151.231341 -33.952685)', 4326), 'SPHEROID["WGS 84",6378137.0,298.257223563]') FROM distapp_australiacity WHERE (NOT (id = 11)); + # SELECT ST_distance_sphere(point, ST_GeomFromText('POINT(151.231341 -33.952685)', 4326)) FROM distapp_australiacity WHERE (NOT (id = 11)); st_distance_sphere + if connection.ops.postgis and connection.ops.proj_version_tuple() >= (4, 7, 0): + # PROJ.4 versions 4.7+ have updated datums, and thus different + # distance values. + spheroid_distances = [60504.0628957201, 77023.9489850262, 49154.8867574404, + 90847.4358768573, 217402.811919332, 709599.234564757, + 640011.483550888, 7772.00667991925, 1047861.78619339, + 1165126.55236034] + sphere_distances = [60580.9693849267, 77144.0435286473, 49199.4415344719, + 90804.7533823494, 217713.384600405, 709134.127242793, + 639828.157159169, 7786.82949717788, 1049204.06569028, + 1162623.7238134] + + else: + spheroid_distances = [60504.0628825298, 77023.948962654, 49154.8867507115, + 90847.435881812, 217402.811862568, 709599.234619957, + 640011.483583758, 7772.00667666425, 1047861.7859506, + 1165126.55237647] + sphere_distances = [60580.7612632291, 77143.7785056615, 49199.2725132184, + 90804.4414289463, 217712.63666124, 709131.691061906, + 639825.959074112, 7786.80274606706, 1049200.46122281, + 1162619.7297006] + + # Testing with spheroid distances first. + hillsdale = AustraliaCity.objects.get(name='Hillsdale') + qs = AustraliaCity.objects.exclude(id=hillsdale.id).distance(hillsdale.point, spheroid=True) + for i, c in enumerate(qs): + self.assertAlmostEqual(spheroid_distances[i], c.distance.m, tol) + if postgis: + # PostGIS uses sphere-only distances by default, testing these as well. + qs = AustraliaCity.objects.exclude(id=hillsdale.id).distance(hillsdale.point) + for i, c in enumerate(qs): + self.assertAlmostEqual(sphere_distances[i], c.distance.m, tol) + + @no_oracle # Oracle already handles geographic distance calculation. + def test03c_distance_method(self): + "Testing the `distance` GeoQuerySet method used with `transform` on a geographic field." + # Normally you can't compute distances from a geometry field + # that is not a PointField (on PostGIS 1.4 and below). + if not connection.ops.geography: + self.assertRaises(ValueError, CensusZipcode.objects.distance, self.stx_pnt) + + # We'll be using a Polygon (created by buffering the centroid + # of 77005 to 100m) -- which aren't allowed in geographic distance + # queries normally, however our field has been transformed to + # a non-geographic system. + z = SouthTexasZipcode.objects.get(name='77005') + + # Reference query: + # SELECT ST_Distance(ST_Transform("distapp_censuszipcode"."poly", 32140), ST_GeomFromText('<buffer_wkt>', 32140)) FROM "distapp_censuszipcode"; + dists_m = [3553.30384972258, 1243.18391525602, 2186.15439472242] + + # Having our buffer in the SRID of the transformation and of the field + # -- should get the same results. The first buffer has no need for + # transformation SQL because it is the same SRID as what was given + # to `transform()`. The second buffer will need to be transformed, + # however. + buf1 = z.poly.centroid.buffer(100) + buf2 = buf1.transform(4269, clone=True) + ref_zips = ['77002', '77025', '77401'] + + for buf in [buf1, buf2]: + qs = CensusZipcode.objects.exclude(name='77005').transform(32140).distance(buf) + self.assertEqual(ref_zips, self.get_names(qs)) + for i, z in enumerate(qs): + self.assertAlmostEqual(z.distance.m, dists_m[i], 5) + + def test04_distance_lookups(self): + "Testing the `distance_lt`, `distance_gt`, `distance_lte`, and `distance_gte` lookup types." + # Retrieving the cities within a 20km 'donut' w/a 7km radius 'hole' + # (thus, Houston and Southside place will be excluded as tested in + # the `test02_dwithin` above). + qs1 = SouthTexasCity.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter(point__distance_lte=(self.stx_pnt, D(km=20))) + + # Can't determine the units on SpatiaLite from PROJ.4 string, and + # Oracle 11 incorrectly thinks it is not projected. + if spatialite or oracle: + dist_qs = (qs1,) + else: + qs2 = SouthTexasCityFt.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter(point__distance_lte=(self.stx_pnt, D(km=20))) + dist_qs = (qs1, qs2) + + for qs in dist_qs: + cities = self.get_names(qs) + self.assertEqual(cities, ['Bellaire', 'Pearland', 'West University Place']) + + # Doing a distance query using Polygons instead of a Point. + z = SouthTexasZipcode.objects.get(name='77005') + qs = SouthTexasZipcode.objects.exclude(name='77005').filter(poly__distance_lte=(z.poly, D(m=275))) + self.assertEqual(['77025', '77401'], self.get_names(qs)) + # If we add a little more distance 77002 should be included. + qs = SouthTexasZipcode.objects.exclude(name='77005').filter(poly__distance_lte=(z.poly, D(m=300))) + self.assertEqual(['77002', '77025', '77401'], self.get_names(qs)) + + def test05_geodetic_distance_lookups(self): + "Testing distance lookups on geodetic coordinate systems." + # Line is from Canberra to Sydney. Query is for all other cities within + # a 100km of that line (which should exclude only Hobart & Adelaide). + line = GEOSGeometry('LINESTRING(144.9630 -37.8143,151.2607 -33.8870)', 4326) + dist_qs = AustraliaCity.objects.filter(point__distance_lte=(line, D(km=100))) + + if oracle or connection.ops.geography: + # Oracle and PostGIS 1.5 can do distance lookups on arbitrary geometries. + self.assertEqual(9, dist_qs.count()) + self.assertEqual(['Batemans Bay', 'Canberra', 'Hillsdale', + 'Melbourne', 'Mittagong', 'Shellharbour', + 'Sydney', 'Thirroul', 'Wollongong'], + self.get_names(dist_qs)) + else: + # PostGIS 1.4 and below only allows geodetic distance queries (utilizing + # ST_Distance_Sphere/ST_Distance_Spheroid) from Points to PointFields + # on geometry columns. + self.assertRaises(ValueError, dist_qs.count) + + # Ensured that a ValueError was raised, none of the rest of the test is + # support on this backend, so bail now. + if spatialite: return + + # Too many params (4 in this case) should raise a ValueError. + self.assertRaises(ValueError, len, + AustraliaCity.objects.filter(point__distance_lte=('POINT(5 23)', D(km=100), 'spheroid', '4'))) + + # Not enough params should raise a ValueError. + self.assertRaises(ValueError, len, + AustraliaCity.objects.filter(point__distance_lte=('POINT(5 23)',))) + + # Getting all cities w/in 550 miles of Hobart. + hobart = AustraliaCity.objects.get(name='Hobart') + qs = AustraliaCity.objects.exclude(name='Hobart').filter(point__distance_lte=(hobart.point, D(mi=550))) + cities = self.get_names(qs) + self.assertEqual(cities, ['Batemans Bay', 'Canberra', 'Melbourne']) + + # Cities that are either really close or really far from Wollongong -- + # and using different units of distance. + wollongong = AustraliaCity.objects.get(name='Wollongong') + d1, d2 = D(yd=19500), D(nm=400) # Yards (~17km) & Nautical miles. + + # Normal geodetic distance lookup (uses `distance_sphere` on PostGIS. + gq1 = Q(point__distance_lte=(wollongong.point, d1)) + gq2 = Q(point__distance_gte=(wollongong.point, d2)) + qs1 = AustraliaCity.objects.exclude(name='Wollongong').filter(gq1 | gq2) + + # Geodetic distance lookup but telling GeoDjango to use `distance_spheroid` + # instead (we should get the same results b/c accuracy variance won't matter + # in this test case). + if postgis: + gq3 = Q(point__distance_lte=(wollongong.point, d1, 'spheroid')) + gq4 = Q(point__distance_gte=(wollongong.point, d2, 'spheroid')) + qs2 = AustraliaCity.objects.exclude(name='Wollongong').filter(gq3 | gq4) + querysets = [qs1, qs2] + else: + querysets = [qs1] + + for qs in querysets: + cities = self.get_names(qs) + self.assertEqual(cities, ['Adelaide', 'Hobart', 'Shellharbour', 'Thirroul']) + + def test06_area(self): + "Testing the `area` GeoQuerySet method." + # Reference queries: + # SELECT ST_Area(poly) FROM distapp_southtexaszipcode; + area_sq_m = [5437908.90234375, 10183031.4389648, 11254471.0073242, 9881708.91772461] + # Tolerance has to be lower for Oracle and differences + # with GEOS 3.0.0RC4 + tol = 2 + for i, z in enumerate(SouthTexasZipcode.objects.area()): + self.assertAlmostEqual(area_sq_m[i], z.area.sq_m, tol) + + def test07_length(self): + "Testing the `length` GeoQuerySet method." + # Reference query (should use `length_spheroid`). + # SELECT ST_length_spheroid(ST_GeomFromText('<wkt>', 4326) 'SPHEROID["WGS 84",6378137,298.257223563, AUTHORITY["EPSG","7030"]]'); + len_m1 = 473504.769553813 + len_m2 = 4617.668 + + if spatialite: + # Does not support geodetic coordinate systems. + self.assertRaises(ValueError, Interstate.objects.length) + else: + qs = Interstate.objects.length() + if oracle: tol = 2 + else: tol = 5 + self.assertAlmostEqual(len_m1, qs[0].length.m, tol) + + # Now doing length on a projected coordinate system. + i10 = SouthTexasInterstate.objects.length().get(name='I-10') + self.assertAlmostEqual(len_m2, i10.length.m, 2) + + @no_spatialite + def test08_perimeter(self): + "Testing the `perimeter` GeoQuerySet method." + # Reference query: + # SELECT ST_Perimeter(distapp_southtexaszipcode.poly) FROM distapp_southtexaszipcode; + perim_m = [18404.3550889361, 15627.2108551001, 20632.5588368978, 17094.5996143697] + if oracle: tol = 2 + else: tol = 7 + for i, z in enumerate(SouthTexasZipcode.objects.perimeter()): + self.assertAlmostEqual(perim_m[i], z.perimeter.m, tol) + + # Running on points; should return 0. + for i, c in enumerate(SouthTexasCity.objects.perimeter(model_att='perim')): + self.assertEqual(0, c.perim.m) + + def test09_measurement_null_fields(self): + "Testing the measurement GeoQuerySet methods on fields with NULL values." + # Creating SouthTexasZipcode w/NULL value. + SouthTexasZipcode.objects.create(name='78212') + # Performing distance/area queries against the NULL PolygonField, + # and ensuring the result of the operations is None. + htown = SouthTexasCity.objects.get(name='Downtown Houston') + z = SouthTexasZipcode.objects.distance(htown.point).area().get(name='78212') + self.assertEqual(None, z.distance) + self.assertEqual(None, z.area) diff --git a/parts/django/django/contrib/gis/tests/geo3d/__init__.py b/parts/django/django/contrib/gis/tests/geo3d/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/geo3d/__init__.py diff --git a/parts/django/django/contrib/gis/tests/geo3d/models.py b/parts/django/django/contrib/gis/tests/geo3d/models.py new file mode 100644 index 0000000..3c4f77e --- /dev/null +++ b/parts/django/django/contrib/gis/tests/geo3d/models.py @@ -0,0 +1,69 @@ +from django.contrib.gis.db import models + +class City3D(models.Model): + name = models.CharField(max_length=30) + point = models.PointField(dim=3) + objects = models.GeoManager() + + def __unicode__(self): + return self.name + +class Interstate2D(models.Model): + name = models.CharField(max_length=30) + line = models.LineStringField(srid=4269) + objects = models.GeoManager() + + def __unicode__(self): + return self.name + +class Interstate3D(models.Model): + name = models.CharField(max_length=30) + line = models.LineStringField(dim=3, srid=4269) + objects = models.GeoManager() + + def __unicode__(self): + return self.name + +class InterstateProj2D(models.Model): + name = models.CharField(max_length=30) + line = models.LineStringField(srid=32140) + objects = models.GeoManager() + + def __unicode__(self): + return self.name + +class InterstateProj3D(models.Model): + name = models.CharField(max_length=30) + line = models.LineStringField(dim=3, srid=32140) + objects = models.GeoManager() + + def __unicode__(self): + return self.name + +class Polygon2D(models.Model): + name = models.CharField(max_length=30) + poly = models.PolygonField(srid=32140) + objects = models.GeoManager() + + def __unicode__(self): + return self.name + +class Polygon3D(models.Model): + name = models.CharField(max_length=30) + poly = models.PolygonField(dim=3, srid=32140) + objects = models.GeoManager() + + def __unicode__(self): + return self.name + +class Point2D(models.Model): + point = models.PointField() + objects = models.GeoManager() + +class Point3D(models.Model): + point = models.PointField(dim=3) + objects = models.GeoManager() + +class MultiPoint3D(models.Model): + mpoint = models.MultiPointField(dim=3) + objects = models.GeoManager() diff --git a/parts/django/django/contrib/gis/tests/geo3d/tests.py b/parts/django/django/contrib/gis/tests/geo3d/tests.py new file mode 100644 index 0000000..f57445c --- /dev/null +++ b/parts/django/django/contrib/gis/tests/geo3d/tests.py @@ -0,0 +1,231 @@ +import os +import re +from django.utils.unittest import TestCase +from django.contrib.gis.db.models import Union, Extent3D +from django.contrib.gis.geos import GEOSGeometry, Point, Polygon +from django.contrib.gis.utils import LayerMapping, LayerMapError + +from models import City3D, Interstate2D, Interstate3D, \ + InterstateProj2D, InterstateProj3D, \ + Point2D, Point3D, MultiPoint3D, Polygon2D, Polygon3D + +data_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'data')) +city_file = os.path.join(data_path, 'cities', 'cities.shp') +vrt_file = os.path.join(data_path, 'test_vrt', 'test_vrt.vrt') + +# The coordinates of each city, with Z values corresponding to their +# altitude in meters. +city_data = ( + ('Houston', (-95.363151, 29.763374, 18)), + ('Dallas', (-96.801611, 32.782057, 147)), + ('Oklahoma City', (-97.521157, 34.464642, 380)), + ('Wellington', (174.783117, -41.315268, 14)), + ('Pueblo', (-104.609252, 38.255001, 1433)), + ('Lawrence', (-95.235060, 38.971823, 251)), + ('Chicago', (-87.650175, 41.850385, 181)), + ('Victoria', (-123.305196, 48.462611, 15)), +) + +# Reference mapping of city name to its altitude (Z value). +city_dict = dict((name, coords) for name, coords in city_data) + +# 3D freeway data derived from the National Elevation Dataset: +# http://seamless.usgs.gov/products/9arc.php +interstate_data = ( + ('I-45', + 'LINESTRING(-95.3708481 29.7765870 11.339,-95.3694580 29.7787980 4.536,-95.3690305 29.7797359 9.762,-95.3691886 29.7812450 12.448,-95.3696447 29.7850144 10.457,-95.3702511 29.7868518 9.418,-95.3706724 29.7881286 14.858,-95.3711632 29.7896157 15.386,-95.3714525 29.7936267 13.168,-95.3717848 29.7955007 15.104,-95.3717719 29.7969804 16.516,-95.3717305 29.7982117 13.923,-95.3717254 29.8000778 14.385,-95.3719875 29.8013539 15.160,-95.3720575 29.8026785 15.544,-95.3721321 29.8040912 14.975,-95.3722074 29.8050998 15.688,-95.3722779 29.8060430 16.099,-95.3733818 29.8076750 15.197,-95.3741563 29.8103686 17.268,-95.3749458 29.8129927 19.857,-95.3763564 29.8144557 15.435)', + ( 11.339, 4.536, 9.762, 12.448, 10.457, 9.418, 14.858, + 15.386, 13.168, 15.104, 16.516, 13.923, 14.385, 15.16 , + 15.544, 14.975, 15.688, 16.099, 15.197, 17.268, 19.857, + 15.435), + ), + ) + +# Bounding box polygon for inner-loop of Houston (in projected coordinate +# system 32140), with elevation values from the National Elevation Dataset +# (see above). +bbox_wkt = 'POLYGON((941527.97 4225693.20,962596.48 4226349.75,963152.57 4209023.95,942051.75 4208366.38,941527.97 4225693.20))' +bbox_z = (21.71, 13.21, 9.12, 16.40, 21.71) +def gen_bbox(): + bbox_2d = GEOSGeometry(bbox_wkt, srid=32140) + bbox_3d = Polygon(tuple((x, y, z) for (x, y), z in zip(bbox_2d[0].coords, bbox_z)), srid=32140) + return bbox_2d, bbox_3d + +class Geo3DTest(TestCase): + """ + Only a subset of the PostGIS routines are 3D-enabled, and this TestCase + tries to test the features that can handle 3D and that are also + available within GeoDjango. For more information, see the PostGIS docs + on the routines that support 3D: + + http://postgis.refractions.net/documentation/manual-1.4/ch08.html#PostGIS_3D_Functions + """ + + def test01_3d(self): + "Test the creation of 3D models." + # 3D models for the rest of the tests will be populated in here. + # For each 3D data set create model (and 2D version if necessary), + # retrieve, and assert geometry is in 3D and contains the expected + # 3D values. + for name, pnt_data in city_data: + x, y, z = pnt_data + pnt = Point(x, y, z, srid=4326) + City3D.objects.create(name=name, point=pnt) + city = City3D.objects.get(name=name) + self.failUnless(city.point.hasz) + self.assertEqual(z, city.point.z) + + # Interstate (2D / 3D and Geographic/Projected variants) + for name, line, exp_z in interstate_data: + line_3d = GEOSGeometry(line, srid=4269) + # Using `hex` attribute because it omits 3D. + line_2d = GEOSGeometry(line_3d.hex, srid=4269) + + # Creating a geographic and projected version of the + # interstate in both 2D and 3D. + Interstate3D.objects.create(name=name, line=line_3d) + InterstateProj3D.objects.create(name=name, line=line_3d) + Interstate2D.objects.create(name=name, line=line_2d) + InterstateProj2D.objects.create(name=name, line=line_2d) + + # Retrieving and making sure it's 3D and has expected + # Z values -- shouldn't change because of coordinate system. + interstate = Interstate3D.objects.get(name=name) + interstate_proj = InterstateProj3D.objects.get(name=name) + for i in [interstate, interstate_proj]: + self.failUnless(i.line.hasz) + self.assertEqual(exp_z, tuple(i.line.z)) + + # Creating 3D Polygon. + bbox2d, bbox3d = gen_bbox() + Polygon2D.objects.create(name='2D BBox', poly=bbox2d) + Polygon3D.objects.create(name='3D BBox', poly=bbox3d) + p3d = Polygon3D.objects.get(name='3D BBox') + self.failUnless(p3d.poly.hasz) + self.assertEqual(bbox3d, p3d.poly) + + def test01a_3d_layermapping(self): + "Testing LayerMapping on 3D models." + from models import Point2D, Point3D + + point_mapping = {'point' : 'POINT'} + mpoint_mapping = {'mpoint' : 'MULTIPOINT'} + + # The VRT is 3D, but should still be able to map sans the Z. + lm = LayerMapping(Point2D, vrt_file, point_mapping, transform=False) + lm.save() + self.assertEqual(3, Point2D.objects.count()) + + # The city shapefile is 2D, and won't be able to fill the coordinates + # in the 3D model -- thus, a LayerMapError is raised. + self.assertRaises(LayerMapError, LayerMapping, + Point3D, city_file, point_mapping, transform=False) + + # 3D model should take 3D data just fine. + lm = LayerMapping(Point3D, vrt_file, point_mapping, transform=False) + lm.save() + self.assertEqual(3, Point3D.objects.count()) + + # Making sure LayerMapping.make_multi works right, by converting + # a Point25D into a MultiPoint25D. + lm = LayerMapping(MultiPoint3D, vrt_file, mpoint_mapping, transform=False) + lm.save() + self.assertEqual(3, MultiPoint3D.objects.count()) + + def test02a_kml(self): + "Test GeoQuerySet.kml() with Z values." + h = City3D.objects.kml(precision=6).get(name='Houston') + # KML should be 3D. + # `SELECT ST_AsKML(point, 6) FROM geo3d_city3d WHERE name = 'Houston';` + ref_kml_regex = re.compile(r'^<Point><coordinates>-95.363\d+,29.763\d+,18</coordinates></Point>$') + self.failUnless(ref_kml_regex.match(h.kml)) + + def test02b_geojson(self): + "Test GeoQuerySet.geojson() with Z values." + h = City3D.objects.geojson(precision=6).get(name='Houston') + # GeoJSON should be 3D + # `SELECT ST_AsGeoJSON(point, 6) FROM geo3d_city3d WHERE name='Houston';` + ref_json_regex = re.compile(r'^{"type":"Point","coordinates":\[-95.363151,29.763374,18(\.0+)?\]}$') + self.failUnless(ref_json_regex.match(h.geojson)) + + def test03a_union(self): + "Testing the Union aggregate of 3D models." + # PostGIS query that returned the reference EWKT for this test: + # `SELECT ST_AsText(ST_Union(point)) FROM geo3d_city3d;` + ref_ewkt = 'SRID=4326;MULTIPOINT(-123.305196 48.462611 15,-104.609252 38.255001 1433,-97.521157 34.464642 380,-96.801611 32.782057 147,-95.363151 29.763374 18,-95.23506 38.971823 251,-87.650175 41.850385 181,174.783117 -41.315268 14)' + ref_union = GEOSGeometry(ref_ewkt) + union = City3D.objects.aggregate(Union('point'))['point__union'] + self.failUnless(union.hasz) + self.assertEqual(ref_union, union) + + def test03b_extent(self): + "Testing the Extent3D aggregate for 3D models." + # `SELECT ST_Extent3D(point) FROM geo3d_city3d;` + ref_extent3d = (-123.305196, -41.315268, 14,174.783117, 48.462611, 1433) + extent1 = City3D.objects.aggregate(Extent3D('point'))['point__extent3d'] + extent2 = City3D.objects.extent3d() + + def check_extent3d(extent3d, tol=6): + for ref_val, ext_val in zip(ref_extent3d, extent3d): + self.assertAlmostEqual(ref_val, ext_val, tol) + + for e3d in [extent1, extent2]: + check_extent3d(e3d) + + def test04_perimeter(self): + "Testing GeoQuerySet.perimeter() on 3D fields." + # Reference query for values below: + # `SELECT ST_Perimeter3D(poly), ST_Perimeter2D(poly) FROM geo3d_polygon3d;` + ref_perim_3d = 76859.2620451 + ref_perim_2d = 76859.2577803 + tol = 6 + self.assertAlmostEqual(ref_perim_2d, + Polygon2D.objects.perimeter().get(name='2D BBox').perimeter.m, + tol) + self.assertAlmostEqual(ref_perim_3d, + Polygon3D.objects.perimeter().get(name='3D BBox').perimeter.m, + tol) + + def test05_length(self): + "Testing GeoQuerySet.length() on 3D fields." + # ST_Length_Spheroid Z-aware, and thus does not need to use + # a separate function internally. + # `SELECT ST_Length_Spheroid(line, 'SPHEROID["GRS 1980",6378137,298.257222101]') + # FROM geo3d_interstate[2d|3d];` + tol = 3 + ref_length_2d = 4368.1721949481 + ref_length_3d = 4368.62547052088 + self.assertAlmostEqual(ref_length_2d, + Interstate2D.objects.length().get(name='I-45').length.m, + tol) + self.assertAlmostEqual(ref_length_3d, + Interstate3D.objects.length().get(name='I-45').length.m, + tol) + + # Making sure `ST_Length3D` is used on for a projected + # and 3D model rather than `ST_Length`. + # `SELECT ST_Length(line) FROM geo3d_interstateproj2d;` + ref_length_2d = 4367.71564892392 + # `SELECT ST_Length3D(line) FROM geo3d_interstateproj3d;` + ref_length_3d = 4368.16897234101 + self.assertAlmostEqual(ref_length_2d, + InterstateProj2D.objects.length().get(name='I-45').length.m, + tol) + self.assertAlmostEqual(ref_length_3d, + InterstateProj3D.objects.length().get(name='I-45').length.m, + tol) + + def test06_scale(self): + "Testing GeoQuerySet.scale() on Z values." + # Mapping of City name to reference Z values. + zscales = (-3, 4, 23) + for zscale in zscales: + for city in City3D.objects.scale(1.0, 1.0, zscale): + self.assertEqual(city_dict[city.name][2] * zscale, city.scale.z) + + def test07_translate(self): + "Testing GeoQuerySet.translate() on Z values." + ztranslations = (5.23, 23, -17) + for ztrans in ztranslations: + for city in City3D.objects.translate(0, 0, ztrans): + self.assertEqual(city_dict[city.name][2] + ztrans, city.translate.z) diff --git a/parts/django/django/contrib/gis/tests/geo3d/views.py b/parts/django/django/contrib/gis/tests/geo3d/views.py new file mode 100644 index 0000000..60f00ef --- /dev/null +++ b/parts/django/django/contrib/gis/tests/geo3d/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/parts/django/django/contrib/gis/tests/geoapp/__init__.py b/parts/django/django/contrib/gis/tests/geoapp/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/geoapp/__init__.py diff --git a/parts/django/django/contrib/gis/tests/geoapp/feeds.py b/parts/django/django/contrib/gis/tests/geoapp/feeds.py new file mode 100644 index 0000000..942b140 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/geoapp/feeds.py @@ -0,0 +1,63 @@ +from django.contrib.gis import feeds +from django.contrib.gis.tests.utils import mysql +from models import City, Country + +class TestGeoRSS1(feeds.Feed): + link = '/city/' + title = 'Test GeoDjango Cities' + + def items(self): + return City.objects.all() + + def item_link(self, item): + return '/city/%s/' % item.pk + + def item_geometry(self, item): + return item.point + +class TestGeoRSS2(TestGeoRSS1): + def geometry(self, obj): + # This should attach a <georss:box> element for the extent of + # of the cities in the database. This tuple came from + # calling `City.objects.extent()` -- we can't do that call here + # because `extent` is not implemented for MySQL/Oracle. + return (-123.30, -41.32, 174.78, 48.46) + + def item_geometry(self, item): + # Returning a simple tuple for the geometry. + return item.point.x, item.point.y + +class TestGeoAtom1(TestGeoRSS1): + feed_type = feeds.GeoAtom1Feed + +class TestGeoAtom2(TestGeoRSS2): + feed_type = feeds.GeoAtom1Feed + + def geometry(self, obj): + # This time we'll use a 2-tuple of coordinates for the box. + return ((-123.30, -41.32), (174.78, 48.46)) + +class TestW3CGeo1(TestGeoRSS1): + feed_type = feeds.W3CGeoFeed + +# The following feeds are invalid, and will raise exceptions. +class TestW3CGeo2(TestGeoRSS2): + feed_type = feeds.W3CGeoFeed + +class TestW3CGeo3(TestGeoRSS1): + feed_type = feeds.W3CGeoFeed + + def item_geometry(self, item): + from django.contrib.gis.geos import Polygon + return Polygon(((0, 0), (0, 1), (1, 1), (1, 0), (0, 0))) + +# The feed dictionary to use for URLs. +feed_dict = { + 'rss1' : TestGeoRSS1, + 'rss2' : TestGeoRSS2, + 'atom1' : TestGeoAtom1, + 'atom2' : TestGeoAtom2, + 'w3cgeo1' : TestW3CGeo1, + 'w3cgeo2' : TestW3CGeo2, + 'w3cgeo3' : TestW3CGeo3, +} diff --git a/parts/django/django/contrib/gis/tests/geoapp/fixtures/initial_data.json.gz b/parts/django/django/contrib/gis/tests/geoapp/fixtures/initial_data.json.gz Binary files differnew file mode 100644 index 0000000..c695082 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/geoapp/fixtures/initial_data.json.gz diff --git a/parts/django/django/contrib/gis/tests/geoapp/models.py b/parts/django/django/contrib/gis/tests/geoapp/models.py new file mode 100644 index 0000000..89027ee --- /dev/null +++ b/parts/django/django/contrib/gis/tests/geoapp/models.py @@ -0,0 +1,45 @@ +from django.contrib.gis.db import models +from django.contrib.gis.tests.utils import mysql, spatialite + +# MySQL spatial indices can't handle NULL geometries. +null_flag = not mysql + +class Country(models.Model): + name = models.CharField(max_length=30) + mpoly = models.MultiPolygonField() # SRID, by default, is 4326 + objects = models.GeoManager() + def __unicode__(self): return self.name + +class City(models.Model): + name = models.CharField(max_length=30) + point = models.PointField() + objects = models.GeoManager() + def __unicode__(self): return self.name + +# This is an inherited model from City +class PennsylvaniaCity(City): + county = models.CharField(max_length=30) + objects = models.GeoManager() # TODO: This should be implicitly inherited. + +class State(models.Model): + name = models.CharField(max_length=30) + poly = models.PolygonField(null=null_flag) # Allowing NULL geometries here. + objects = models.GeoManager() + def __unicode__(self): return self.name + +class Track(models.Model): + name = models.CharField(max_length=30) + line = models.LineStringField() + objects = models.GeoManager() + def __unicode__(self): return self.name + +if not spatialite: + class Feature(models.Model): + name = models.CharField(max_length=20) + geom = models.GeometryField() + objects = models.GeoManager() + def __unicode__(self): return self.name + + class MinusOneSRID(models.Model): + geom = models.PointField(srid=-1) # Minus one SRID. + objects = models.GeoManager() diff --git a/parts/django/django/contrib/gis/tests/geoapp/sitemaps.py b/parts/django/django/contrib/gis/tests/geoapp/sitemaps.py new file mode 100644 index 0000000..ca785f2 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/geoapp/sitemaps.py @@ -0,0 +1,8 @@ +from django.contrib.gis.sitemaps import GeoRSSSitemap, KMLSitemap, KMZSitemap +from models import City, Country +from feeds import feed_dict + +sitemaps = {'kml' : KMLSitemap([City, Country]), + 'kmz' : KMZSitemap([City, Country]), + 'georss' : GeoRSSSitemap(feed_dict), + } diff --git a/parts/django/django/contrib/gis/tests/geoapp/test_feeds.py b/parts/django/django/contrib/gis/tests/geoapp/test_feeds.py new file mode 100644 index 0000000..7ec9a3c --- /dev/null +++ b/parts/django/django/contrib/gis/tests/geoapp/test_feeds.py @@ -0,0 +1,78 @@ +from xml.dom import minidom +from django.test import TestCase + +from models import City + + +class GeoFeedTest(TestCase): + + urls = 'django.contrib.gis.tests.geoapp.urls' + + def assertChildNodes(self, elem, expected): + "Taken from regressiontests/syndication/tests.py." + actual = set([n.nodeName for n in elem.childNodes]) + expected = set(expected) + self.assertEqual(actual, expected) + + def test_geofeed_rss(self): + "Tests geographic feeds using GeoRSS over RSSv2." + # Uses `GEOSGeometry` in `item_geometry` + doc1 = minidom.parseString(self.client.get('/feeds/rss1/').content) + # Uses a 2-tuple in `item_geometry` + doc2 = minidom.parseString(self.client.get('/feeds/rss2/').content) + feed1, feed2 = doc1.firstChild, doc2.firstChild + + # Making sure the box got added to the second GeoRSS feed. + self.assertChildNodes(feed2.getElementsByTagName('channel')[0], + ['title', 'link', 'description', 'language', + 'lastBuildDate', 'item', 'georss:box', 'atom:link'] + ) + + # Incrementing through the feeds. + for feed in [feed1, feed2]: + # Ensuring the georss namespace was added to the <rss> element. + self.assertEqual(feed.getAttribute(u'xmlns:georss'), u'http://www.georss.org/georss') + chan = feed.getElementsByTagName('channel')[0] + items = chan.getElementsByTagName('item') + self.assertEqual(len(items), City.objects.count()) + + # Ensuring the georss element was added to each item in the feed. + for item in items: + self.assertChildNodes(item, ['title', 'link', 'description', 'guid', 'georss:point']) + + def test_geofeed_atom(self): + "Testing geographic feeds using GeoRSS over Atom." + doc1 = minidom.parseString(self.client.get('/feeds/atom1/').content) + doc2 = minidom.parseString(self.client.get('/feeds/atom2/').content) + feed1, feed2 = doc1.firstChild, doc2.firstChild + + # Making sure the box got added to the second GeoRSS feed. + self.assertChildNodes(feed2, ['title', 'link', 'id', 'updated', 'entry', 'georss:box']) + + for feed in [feed1, feed2]: + # Ensuring the georsss namespace was added to the <feed> element. + self.assertEqual(feed.getAttribute(u'xmlns:georss'), u'http://www.georss.org/georss') + entries = feed.getElementsByTagName('entry') + self.assertEqual(len(entries), City.objects.count()) + + # Ensuring the georss element was added to each entry in the feed. + for entry in entries: + self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'georss:point']) + + def test_geofeed_w3c(self): + "Testing geographic feeds using W3C Geo." + doc = minidom.parseString(self.client.get('/feeds/w3cgeo1/').content) + feed = doc.firstChild + # Ensuring the geo namespace was added to the <feed> element. + self.assertEqual(feed.getAttribute(u'xmlns:geo'), u'http://www.w3.org/2003/01/geo/wgs84_pos#') + chan = feed.getElementsByTagName('channel')[0] + items = chan.getElementsByTagName('item') + self.assertEqual(len(items), City.objects.count()) + + # Ensuring the geo:lat and geo:lon element was added to each item in the feed. + for item in items: + self.assertChildNodes(item, ['title', 'link', 'description', 'guid', 'geo:lat', 'geo:lon']) + + # Boxes and Polygons aren't allowed in W3C Geo feeds. + self.assertRaises(ValueError, self.client.get, '/feeds/w3cgeo2/') # Box in <channel> + self.assertRaises(ValueError, self.client.get, '/feeds/w3cgeo3/') # Polygons in <entry> diff --git a/parts/django/django/contrib/gis/tests/geoapp/test_regress.py b/parts/django/django/contrib/gis/tests/geoapp/test_regress.py new file mode 100644 index 0000000..0295526 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/geoapp/test_regress.py @@ -0,0 +1,37 @@ +import os, unittest +from django.contrib.gis.tests.utils import no_mysql, no_oracle, no_postgis, no_spatialite +from django.contrib.gis.shortcuts import render_to_kmz +from models import City + +class GeoRegressionTests(unittest.TestCase): + + def test01_update(self): + "Testing GeoQuerySet.update(), see #10411." + pnt = City.objects.get(name='Pueblo').point + bak = pnt.clone() + pnt.y += 0.005 + pnt.x += 0.005 + + City.objects.filter(name='Pueblo').update(point=pnt) + self.assertEqual(pnt, City.objects.get(name='Pueblo').point) + City.objects.filter(name='Pueblo').update(point=bak) + self.assertEqual(bak, City.objects.get(name='Pueblo').point) + + def test02_kmz(self): + "Testing `render_to_kmz` with non-ASCII data, see #11624." + name = '\xc3\x85land Islands'.decode('iso-8859-1') + places = [{'name' : name, + 'description' : name, + 'kml' : '<Point><coordinates>5.0,23.0</coordinates></Point>' + }] + kmz = render_to_kmz('gis/kml/placemarks.kml', {'places' : places}) + + @no_spatialite + @no_mysql + def test03_extent(self): + "Testing `extent` on a table with a single point, see #11827." + pnt = City.objects.get(name='Pueblo').point + ref_ext = (pnt.x, pnt.y, pnt.x, pnt.y) + extent = City.objects.filter(name='Pueblo').extent() + for ref_val, val in zip(ref_ext, extent): + self.assertAlmostEqual(ref_val, val, 4) diff --git a/parts/django/django/contrib/gis/tests/geoapp/test_sitemaps.py b/parts/django/django/contrib/gis/tests/geoapp/test_sitemaps.py new file mode 100644 index 0000000..c8c43f4 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/geoapp/test_sitemaps.py @@ -0,0 +1,86 @@ +import cStringIO +from xml.dom import minidom +import zipfile +from django.test import TestCase + +from models import City, Country + + +class GeoSitemapTest(TestCase): + + urls = 'django.contrib.gis.tests.geoapp.urls' + + def assertChildNodes(self, elem, expected): + "Taken from regressiontests/syndication/tests.py." + actual = set([n.nodeName for n in elem.childNodes]) + expected = set(expected) + self.assertEqual(actual, expected) + + def test_geositemap_index(self): + "Tests geographic sitemap index." + # Getting the geo index. + doc = minidom.parseString(self.client.get('/sitemap.xml').content) + index = doc.firstChild + self.assertEqual(index.getAttribute(u'xmlns'), u'http://www.sitemaps.org/schemas/sitemap/0.9') + self.assertEqual(3, len(index.getElementsByTagName('sitemap'))) + + def test_geositemap_kml(self): + "Tests KML/KMZ geographic sitemaps." + for kml_type in ('kml', 'kmz'): + doc = minidom.parseString(self.client.get('/sitemaps/%s.xml' % kml_type).content) + + # Ensuring the right sitemaps namespaces are present. + urlset = doc.firstChild + self.assertEqual(urlset.getAttribute(u'xmlns'), u'http://www.sitemaps.org/schemas/sitemap/0.9') + self.assertEqual(urlset.getAttribute(u'xmlns:geo'), u'http://www.google.com/geo/schemas/sitemap/1.0') + + urls = urlset.getElementsByTagName('url') + self.assertEqual(2, len(urls)) # Should only be 2 sitemaps. + for url in urls: + self.assertChildNodes(url, ['loc', 'geo:geo']) + # Making sure the 'geo:format' element was properly set. + geo_elem = url.getElementsByTagName('geo:geo')[0] + geo_format = geo_elem.getElementsByTagName('geo:format')[0] + self.assertEqual(kml_type, geo_format.childNodes[0].data) + + # Getting the relative URL since we don't have a real site. + kml_url = url.getElementsByTagName('loc')[0].childNodes[0].data.split('http://example.com')[1] + + if kml_type == 'kml': + kml_doc = minidom.parseString(self.client.get(kml_url).content) + elif kml_type == 'kmz': + # Have to decompress KMZ before parsing. + buf = cStringIO.StringIO(self.client.get(kml_url).content) + zf = zipfile.ZipFile(buf) + self.assertEqual(1, len(zf.filelist)) + self.assertEqual('doc.kml', zf.filelist[0].filename) + kml_doc = minidom.parseString(zf.read('doc.kml')) + + # Ensuring the correct number of placemarks are in the KML doc. + if 'city' in kml_url: + model = City + elif 'country' in kml_url: + model = Country + self.assertEqual(model.objects.count(), len(kml_doc.getElementsByTagName('Placemark'))) + + def test_geositemap_georss(self): + "Tests GeoRSS geographic sitemaps." + from feeds import feed_dict + + doc = minidom.parseString(self.client.get('/sitemaps/georss.xml').content) + + # Ensuring the right sitemaps namespaces are present. + urlset = doc.firstChild + self.assertEqual(urlset.getAttribute(u'xmlns'), u'http://www.sitemaps.org/schemas/sitemap/0.9') + self.assertEqual(urlset.getAttribute(u'xmlns:geo'), u'http://www.google.com/geo/schemas/sitemap/1.0') + + # Making sure the correct number of feed URLs were included. + urls = urlset.getElementsByTagName('url') + self.assertEqual(len(feed_dict), len(urls)) + + for url in urls: + self.assertChildNodes(url, ['loc', 'geo:geo']) + # Making sure the 'geo:format' element was properly set to 'georss'. + geo_elem = url.getElementsByTagName('geo:geo')[0] + geo_format = geo_elem.getElementsByTagName('geo:format')[0] + self.assertEqual('georss', geo_format.childNodes[0].data) diff --git a/parts/django/django/contrib/gis/tests/geoapp/tests.py b/parts/django/django/contrib/gis/tests/geoapp/tests.py new file mode 100644 index 0000000..a2b81c6 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/geoapp/tests.py @@ -0,0 +1,735 @@ +import re +from django.db import connection +from django.contrib.gis import gdal +from django.contrib.gis.geos import fromstr, GEOSGeometry, \ + Point, LineString, LinearRing, Polygon, GeometryCollection +from django.contrib.gis.measure import Distance +from django.contrib.gis.tests.utils import \ + no_mysql, no_oracle, no_spatialite, \ + mysql, oracle, postgis, spatialite +from django.test import TestCase + +from models import Country, City, PennsylvaniaCity, State, Track + +if not spatialite: + from models import Feature, MinusOneSRID + +class GeoModelTest(TestCase): + + def test01_fixtures(self): + "Testing geographic model initialization from fixtures." + # Ensuring that data was loaded from initial data fixtures. + self.assertEqual(2, Country.objects.count()) + self.assertEqual(8, City.objects.count()) + self.assertEqual(2, State.objects.count()) + + def test02_proxy(self): + "Testing Lazy-Geometry support (using the GeometryProxy)." + ## Testing on a Point + pnt = Point(0, 0) + nullcity = City(name='NullCity', point=pnt) + nullcity.save() + + # Making sure TypeError is thrown when trying to set with an + # incompatible type. + for bad in [5, 2.0, LineString((0, 0), (1, 1))]: + try: + nullcity.point = bad + except TypeError: + pass + else: + self.fail('Should throw a TypeError') + + # Now setting with a compatible GEOS Geometry, saving, and ensuring + # the save took, notice no SRID is explicitly set. + new = Point(5, 23) + nullcity.point = new + + # Ensuring that the SRID is automatically set to that of the + # field after assignment, but before saving. + self.assertEqual(4326, nullcity.point.srid) + nullcity.save() + + # Ensuring the point was saved correctly after saving + self.assertEqual(new, City.objects.get(name='NullCity').point) + + # Setting the X and Y of the Point + nullcity.point.x = 23 + nullcity.point.y = 5 + # Checking assignments pre & post-save. + self.assertNotEqual(Point(23, 5), City.objects.get(name='NullCity').point) + nullcity.save() + self.assertEqual(Point(23, 5), City.objects.get(name='NullCity').point) + nullcity.delete() + + ## Testing on a Polygon + shell = LinearRing((0, 0), (0, 100), (100, 100), (100, 0), (0, 0)) + inner = LinearRing((40, 40), (40, 60), (60, 60), (60, 40), (40, 40)) + + # Creating a State object using a built Polygon + ply = Polygon(shell, inner) + nullstate = State(name='NullState', poly=ply) + self.assertEqual(4326, nullstate.poly.srid) # SRID auto-set from None + nullstate.save() + + ns = State.objects.get(name='NullState') + self.assertEqual(ply, ns.poly) + + # Testing the `ogr` and `srs` lazy-geometry properties. + if gdal.HAS_GDAL: + self.assertEqual(True, isinstance(ns.poly.ogr, gdal.OGRGeometry)) + self.assertEqual(ns.poly.wkb, ns.poly.ogr.wkb) + self.assertEqual(True, isinstance(ns.poly.srs, gdal.SpatialReference)) + self.assertEqual('WGS 84', ns.poly.srs.name) + + # Changing the interior ring on the poly attribute. + new_inner = LinearRing((30, 30), (30, 70), (70, 70), (70, 30), (30, 30)) + ns.poly[1] = new_inner + ply[1] = new_inner + self.assertEqual(4326, ns.poly.srid) + ns.save() + self.assertEqual(ply, State.objects.get(name='NullState').poly) + ns.delete() + + def test03a_kml(self): + "Testing KML output from the database using GeoQuerySet.kml()." + # Only PostGIS supports KML serialization + if not postgis: + self.assertRaises(NotImplementedError, State.objects.all().kml, field_name='poly') + return + + # Should throw a TypeError when trying to obtain KML from a + # non-geometry field. + qs = City.objects.all() + self.assertRaises(TypeError, qs.kml, 'name') + + # The reference KML depends on the version of PostGIS used + # (the output stopped including altitude in 1.3.3). + if connection.ops.spatial_version >= (1, 3, 3): + ref_kml = '<Point><coordinates>-104.609252,38.255001</coordinates></Point>' + else: + ref_kml = '<Point><coordinates>-104.609252,38.255001,0</coordinates></Point>' + + # Ensuring the KML is as expected. + ptown1 = City.objects.kml(field_name='point', precision=9).get(name='Pueblo') + ptown2 = City.objects.kml(precision=9).get(name='Pueblo') + for ptown in [ptown1, ptown2]: + self.assertEqual(ref_kml, ptown.kml) + + def test03b_gml(self): + "Testing GML output from the database using GeoQuerySet.gml()." + if mysql or spatialite: + self.assertRaises(NotImplementedError, Country.objects.all().gml, field_name='mpoly') + return + + # Should throw a TypeError when tyring to obtain GML from a + # non-geometry field. + qs = City.objects.all() + self.assertRaises(TypeError, qs.gml, field_name='name') + ptown1 = City.objects.gml(field_name='point', precision=9).get(name='Pueblo') + ptown2 = City.objects.gml(precision=9).get(name='Pueblo') + + if oracle: + # No precision parameter for Oracle :-/ + gml_regex = re.compile(r'^<gml:Point srsName="SDO:4326" xmlns:gml="http://www.opengis.net/gml"><gml:coordinates decimal="\." cs="," ts=" ">-104.60925\d+,38.25500\d+ </gml:coordinates></gml:Point>') + for ptown in [ptown1, ptown2]: + self.failUnless(gml_regex.match(ptown.gml)) + else: + gml_regex = re.compile(r'^<gml:Point srsName="EPSG:4326"><gml:coordinates>-104\.60925\d+,38\.255001</gml:coordinates></gml:Point>') + for ptown in [ptown1, ptown2]: + self.failUnless(gml_regex.match(ptown.gml)) + + def test03c_geojson(self): + "Testing GeoJSON output from the database using GeoQuerySet.geojson()." + # Only PostGIS 1.3.4+ supports GeoJSON. + if not connection.ops.geojson: + self.assertRaises(NotImplementedError, Country.objects.all().geojson, field_name='mpoly') + return + + if connection.ops.spatial_version >= (1, 4, 0): + pueblo_json = '{"type":"Point","coordinates":[-104.609252,38.255001]}' + houston_json = '{"type":"Point","crs":{"type":"name","properties":{"name":"EPSG:4326"}},"coordinates":[-95.363151,29.763374]}' + victoria_json = '{"type":"Point","bbox":[-123.30519600,48.46261100,-123.30519600,48.46261100],"coordinates":[-123.305196,48.462611]}' + chicago_json = '{"type":"Point","crs":{"type":"name","properties":{"name":"EPSG:4326"}},"bbox":[-87.65018,41.85039,-87.65018,41.85039],"coordinates":[-87.65018,41.85039]}' + else: + pueblo_json = '{"type":"Point","coordinates":[-104.60925200,38.25500100]}' + houston_json = '{"type":"Point","crs":{"type":"EPSG","properties":{"EPSG":4326}},"coordinates":[-95.36315100,29.76337400]}' + victoria_json = '{"type":"Point","bbox":[-123.30519600,48.46261100,-123.30519600,48.46261100],"coordinates":[-123.30519600,48.46261100]}' + chicago_json = '{"type":"Point","crs":{"type":"EPSG","properties":{"EPSG":4326}},"bbox":[-87.65018,41.85039,-87.65018,41.85039],"coordinates":[-87.65018,41.85039]}' + + # Precision argument should only be an integer + self.assertRaises(TypeError, City.objects.geojson, precision='foo') + + # Reference queries and values. + # SELECT ST_AsGeoJson("geoapp_city"."point", 8, 0) FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Pueblo'; + self.assertEqual(pueblo_json, City.objects.geojson().get(name='Pueblo').geojson) + + # 1.3.x: SELECT ST_AsGeoJson("geoapp_city"."point", 8, 1) FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Houston'; + # 1.4.x: SELECT ST_AsGeoJson("geoapp_city"."point", 8, 2) FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Houston'; + # This time we want to include the CRS by using the `crs` keyword. + self.assertEqual(houston_json, City.objects.geojson(crs=True, model_att='json').get(name='Houston').json) + + # 1.3.x: SELECT ST_AsGeoJson("geoapp_city"."point", 8, 2) FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Victoria'; + # 1.4.x: SELECT ST_AsGeoJson("geoapp_city"."point", 8, 1) FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Houston'; + # This time we include the bounding box by using the `bbox` keyword. + self.assertEqual(victoria_json, City.objects.geojson(bbox=True).get(name='Victoria').geojson) + + # 1.(3|4).x: SELECT ST_AsGeoJson("geoapp_city"."point", 5, 3) FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Chicago'; + # Finally, we set every available keyword. + self.assertEqual(chicago_json, City.objects.geojson(bbox=True, crs=True, precision=5).get(name='Chicago').geojson) + + def test03d_svg(self): + "Testing SVG output using GeoQuerySet.svg()." + if mysql or oracle: + self.assertRaises(NotImplementedError, City.objects.svg) + return + + self.assertRaises(TypeError, City.objects.svg, precision='foo') + # SELECT AsSVG(geoapp_city.point, 0, 8) FROM geoapp_city WHERE name = 'Pueblo'; + svg1 = 'cx="-104.609252" cy="-38.255001"' + # Even though relative, only one point so it's practically the same except for + # the 'c' letter prefix on the x,y values. + svg2 = svg1.replace('c', '') + self.assertEqual(svg1, City.objects.svg().get(name='Pueblo').svg) + self.assertEqual(svg2, City.objects.svg(relative=5).get(name='Pueblo').svg) + + @no_mysql + def test04_transform(self): + "Testing the transform() GeoManager method." + # Pre-transformed points for Houston and Pueblo. + htown = fromstr('POINT(1947516.83115183 6322297.06040572)', srid=3084) + ptown = fromstr('POINT(992363.390841912 481455.395105533)', srid=2774) + prec = 3 # Precision is low due to version variations in PROJ and GDAL. + + # Asserting the result of the transform operation with the values in + # the pre-transformed points. Oracle does not have the 3084 SRID. + if not oracle: + h = City.objects.transform(htown.srid).get(name='Houston') + self.assertEqual(3084, h.point.srid) + self.assertAlmostEqual(htown.x, h.point.x, prec) + self.assertAlmostEqual(htown.y, h.point.y, prec) + + p1 = City.objects.transform(ptown.srid, field_name='point').get(name='Pueblo') + p2 = City.objects.transform(srid=ptown.srid).get(name='Pueblo') + for p in [p1, p2]: + self.assertEqual(2774, p.point.srid) + self.assertAlmostEqual(ptown.x, p.point.x, prec) + self.assertAlmostEqual(ptown.y, p.point.y, prec) + + @no_mysql + @no_spatialite # SpatiaLite does not have an Extent function + def test05_extent(self): + "Testing the `extent` GeoQuerySet method." + # Reference query: + # `SELECT ST_extent(point) FROM geoapp_city WHERE (name='Houston' or name='Dallas');` + # => BOX(-96.8016128540039 29.7633724212646,-95.3631439208984 32.7820587158203) + expected = (-96.8016128540039, 29.7633724212646, -95.3631439208984, 32.782058715820) + + qs = City.objects.filter(name__in=('Houston', 'Dallas')) + extent = qs.extent() + + for val, exp in zip(extent, expected): + self.assertAlmostEqual(exp, val, 4) + + # Only PostGIS has support for the MakeLine aggregate. + @no_mysql + @no_oracle + @no_spatialite + def test06_make_line(self): + "Testing the `make_line` GeoQuerySet method." + # Ensuring that a `TypeError` is raised on models without PointFields. + self.assertRaises(TypeError, State.objects.make_line) + self.assertRaises(TypeError, Country.objects.make_line) + # Reference query: + # SELECT AsText(ST_MakeLine(geoapp_city.point)) FROM geoapp_city; + ref_line = GEOSGeometry('LINESTRING(-95.363151 29.763374,-96.801611 32.782057,-97.521157 34.464642,174.783117 -41.315268,-104.609252 38.255001,-95.23506 38.971823,-87.650175 41.850385,-123.305196 48.462611)', srid=4326) + self.assertEqual(ref_line, City.objects.make_line()) + + @no_mysql + def test09_disjoint(self): + "Testing the `disjoint` lookup type." + ptown = City.objects.get(name='Pueblo') + qs1 = City.objects.filter(point__disjoint=ptown.point) + self.assertEqual(7, qs1.count()) + + qs2 = State.objects.filter(poly__disjoint=ptown.point) + self.assertEqual(1, qs2.count()) + self.assertEqual('Kansas', qs2[0].name) + + def test10_contains_contained(self): + "Testing the 'contained', 'contains', and 'bbcontains' lookup types." + # Getting Texas, yes we were a country -- once ;) + texas = Country.objects.get(name='Texas') + + # Seeing what cities are in Texas, should get Houston and Dallas, + # and Oklahoma City because 'contained' only checks on the + # _bounding box_ of the Geometries. + if not oracle: + qs = City.objects.filter(point__contained=texas.mpoly) + self.assertEqual(3, qs.count()) + cities = ['Houston', 'Dallas', 'Oklahoma City'] + for c in qs: self.assertEqual(True, c.name in cities) + + # Pulling out some cities. + houston = City.objects.get(name='Houston') + wellington = City.objects.get(name='Wellington') + pueblo = City.objects.get(name='Pueblo') + okcity = City.objects.get(name='Oklahoma City') + lawrence = City.objects.get(name='Lawrence') + + # Now testing contains on the countries using the points for + # Houston and Wellington. + tx = Country.objects.get(mpoly__contains=houston.point) # Query w/GEOSGeometry + nz = Country.objects.get(mpoly__contains=wellington.point.hex) # Query w/EWKBHEX + self.assertEqual('Texas', tx.name) + self.assertEqual('New Zealand', nz.name) + + # Spatialite 2.3 thinks that Lawrence is in Puerto Rico (a NULL geometry). + if not spatialite: + ks = State.objects.get(poly__contains=lawrence.point) + self.assertEqual('Kansas', ks.name) + + # Pueblo and Oklahoma City (even though OK City is within the bounding box of Texas) + # are not contained in Texas or New Zealand. + self.assertEqual(0, len(Country.objects.filter(mpoly__contains=pueblo.point))) # Query w/GEOSGeometry object + self.assertEqual((mysql and 1) or 0, + len(Country.objects.filter(mpoly__contains=okcity.point.wkt))) # Qeury w/WKT + + # OK City is contained w/in bounding box of Texas. + if not oracle: + qs = Country.objects.filter(mpoly__bbcontains=okcity.point) + self.assertEqual(1, len(qs)) + self.assertEqual('Texas', qs[0].name) + + @no_mysql + def test11_lookup_insert_transform(self): + "Testing automatic transform for lookups and inserts." + # San Antonio in 'WGS84' (SRID 4326) + sa_4326 = 'POINT (-98.493183 29.424170)' + wgs_pnt = fromstr(sa_4326, srid=4326) # Our reference point in WGS84 + + # Oracle doesn't have SRID 3084, using 41157. + if oracle: + # San Antonio in 'Texas 4205, Southern Zone (1983, meters)' (SRID 41157) + # Used the following Oracle SQL to get this value: + # SELECT SDO_UTIL.TO_WKTGEOMETRY(SDO_CS.TRANSFORM(SDO_GEOMETRY('POINT (-98.493183 29.424170)', 4326), 41157)) FROM DUAL; + nad_wkt = 'POINT (300662.034646583 5416427.45974934)' + nad_srid = 41157 + else: + # San Antonio in 'NAD83(HARN) / Texas Centric Lambert Conformal' (SRID 3084) + nad_wkt = 'POINT (1645978.362408288754523 6276356.025927528738976)' # Used ogr.py in gdal 1.4.1 for this transform + nad_srid = 3084 + + # Constructing & querying with a point from a different SRID. Oracle + # `SDO_OVERLAPBDYINTERSECT` operates differently from + # `ST_Intersects`, so contains is used instead. + nad_pnt = fromstr(nad_wkt, srid=nad_srid) + if oracle: + tx = Country.objects.get(mpoly__contains=nad_pnt) + else: + tx = Country.objects.get(mpoly__intersects=nad_pnt) + self.assertEqual('Texas', tx.name) + + # Creating San Antonio. Remember the Alamo. + sa = City.objects.create(name='San Antonio', point=nad_pnt) + + # Now verifying that San Antonio was transformed correctly + sa = City.objects.get(name='San Antonio') + self.assertAlmostEqual(wgs_pnt.x, sa.point.x, 6) + self.assertAlmostEqual(wgs_pnt.y, sa.point.y, 6) + + # If the GeometryField SRID is -1, then we shouldn't perform any + # transformation if the SRID of the input geometry is different. + # SpatiaLite does not support missing SRID values. + if not spatialite: + m1 = MinusOneSRID(geom=Point(17, 23, srid=4326)) + m1.save() + self.assertEqual(-1, m1.geom.srid) + + @no_mysql + def test12_null_geometries(self): + "Testing NULL geometry support, and the `isnull` lookup type." + # Creating a state with a NULL boundary. + State.objects.create(name='Puerto Rico') + + # Querying for both NULL and Non-NULL values. + nullqs = State.objects.filter(poly__isnull=True) + validqs = State.objects.filter(poly__isnull=False) + + # Puerto Rico should be NULL (it's a commonwealth unincorporated territory) + self.assertEqual(1, len(nullqs)) + self.assertEqual('Puerto Rico', nullqs[0].name) + + # The valid states should be Colorado & Kansas + self.assertEqual(2, len(validqs)) + state_names = [s.name for s in validqs] + self.assertEqual(True, 'Colorado' in state_names) + self.assertEqual(True, 'Kansas' in state_names) + + # Saving another commonwealth w/a NULL geometry. + nmi = State.objects.create(name='Northern Mariana Islands', poly=None) + self.assertEqual(nmi.poly, None) + + # Assigning a geomery and saving -- then UPDATE back to NULL. + nmi.poly = 'POLYGON((0 0,1 0,1 1,1 0,0 0))' + nmi.save() + State.objects.filter(name='Northern Mariana Islands').update(poly=None) + self.assertEqual(None, State.objects.get(name='Northern Mariana Islands').poly) + + # Only PostGIS has `left` and `right` lookup types. + @no_mysql + @no_oracle + @no_spatialite + def test13_left_right(self): + "Testing the 'left' and 'right' lookup types." + # Left: A << B => true if xmax(A) < xmin(B) + # Right: A >> B => true if xmin(A) > xmax(B) + # See: BOX2D_left() and BOX2D_right() in lwgeom_box2dfloat4.c in PostGIS source. + + # Getting the borders for Colorado & Kansas + co_border = State.objects.get(name='Colorado').poly + ks_border = State.objects.get(name='Kansas').poly + + # Note: Wellington has an 'X' value of 174, so it will not be considered + # to the left of CO. + + # These cities should be strictly to the right of the CO border. + cities = ['Houston', 'Dallas', 'Oklahoma City', + 'Lawrence', 'Chicago', 'Wellington'] + qs = City.objects.filter(point__right=co_border) + self.assertEqual(6, len(qs)) + for c in qs: self.assertEqual(True, c.name in cities) + + # These cities should be strictly to the right of the KS border. + cities = ['Chicago', 'Wellington'] + qs = City.objects.filter(point__right=ks_border) + self.assertEqual(2, len(qs)) + for c in qs: self.assertEqual(True, c.name in cities) + + # Note: Wellington has an 'X' value of 174, so it will not be considered + # to the left of CO. + vic = City.objects.get(point__left=co_border) + self.assertEqual('Victoria', vic.name) + + cities = ['Pueblo', 'Victoria'] + qs = City.objects.filter(point__left=ks_border) + self.assertEqual(2, len(qs)) + for c in qs: self.assertEqual(True, c.name in cities) + + def test14_equals(self): + "Testing the 'same_as' and 'equals' lookup types." + pnt = fromstr('POINT (-95.363151 29.763374)', srid=4326) + c1 = City.objects.get(point=pnt) + c2 = City.objects.get(point__same_as=pnt) + c3 = City.objects.get(point__equals=pnt) + for c in [c1, c2, c3]: self.assertEqual('Houston', c.name) + + @no_mysql + def test15_relate(self): + "Testing the 'relate' lookup type." + # To make things more interesting, we will have our Texas reference point in + # different SRIDs. + pnt1 = fromstr('POINT (649287.0363174 4177429.4494686)', srid=2847) + pnt2 = fromstr('POINT(-98.4919715741052 29.4333344025053)', srid=4326) + + # Not passing in a geometry as first param shoud + # raise a type error when initializing the GeoQuerySet + self.assertRaises(ValueError, Country.objects.filter, mpoly__relate=(23, 'foo')) + + # Making sure the right exception is raised for the given + # bad arguments. + for bad_args, e in [((pnt1, 0), ValueError), ((pnt2, 'T*T***FF*', 0), ValueError)]: + qs = Country.objects.filter(mpoly__relate=bad_args) + self.assertRaises(e, qs.count) + + # Relate works differently for the different backends. + if postgis or spatialite: + contains_mask = 'T*T***FF*' + within_mask = 'T*F**F***' + intersects_mask = 'T********' + elif oracle: + contains_mask = 'contains' + within_mask = 'inside' + # TODO: This is not quite the same as the PostGIS mask above + intersects_mask = 'overlapbdyintersect' + + # Testing contains relation mask. + self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, contains_mask)).name) + self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, contains_mask)).name) + + # Testing within relation mask. + ks = State.objects.get(name='Kansas') + self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, within_mask)).name) + + # Testing intersection relation mask. + if not oracle: + self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, intersects_mask)).name) + self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, intersects_mask)).name) + self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, intersects_mask)).name) + + def test16_createnull(self): + "Testing creating a model instance and the geometry being None" + c = City() + self.assertEqual(c.point, None) + + @no_mysql + def test17_unionagg(self): + "Testing the `unionagg` (aggregate union) GeoManager method." + tx = Country.objects.get(name='Texas').mpoly + # Houston, Dallas -- Oracle has different order. + union1 = fromstr('MULTIPOINT(-96.801611 32.782057,-95.363151 29.763374)') + union2 = fromstr('MULTIPOINT(-96.801611 32.782057,-95.363151 29.763374)') + qs = City.objects.filter(point__within=tx) + self.assertRaises(TypeError, qs.unionagg, 'name') + # Using `field_name` keyword argument in one query and specifying an + # order in the other (which should not be used because this is + # an aggregate method on a spatial column) + u1 = qs.unionagg(field_name='point') + u2 = qs.order_by('name').unionagg() + tol = 0.00001 + if oracle: + union = union2 + else: + union = union1 + self.assertEqual(True, union.equals_exact(u1, tol)) + self.assertEqual(True, union.equals_exact(u2, tol)) + qs = City.objects.filter(name='NotACity') + self.assertEqual(None, qs.unionagg(field_name='point')) + + @no_spatialite # SpatiaLite does not support abstract geometry columns + def test18_geometryfield(self): + "Testing the general GeometryField." + Feature(name='Point', geom=Point(1, 1)).save() + Feature(name='LineString', geom=LineString((0, 0), (1, 1), (5, 5))).save() + Feature(name='Polygon', geom=Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0)))).save() + Feature(name='GeometryCollection', + geom=GeometryCollection(Point(2, 2), LineString((0, 0), (2, 2)), + Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0))))).save() + + f_1 = Feature.objects.get(name='Point') + self.assertEqual(True, isinstance(f_1.geom, Point)) + self.assertEqual((1.0, 1.0), f_1.geom.tuple) + f_2 = Feature.objects.get(name='LineString') + self.assertEqual(True, isinstance(f_2.geom, LineString)) + self.assertEqual(((0.0, 0.0), (1.0, 1.0), (5.0, 5.0)), f_2.geom.tuple) + + f_3 = Feature.objects.get(name='Polygon') + self.assertEqual(True, isinstance(f_3.geom, Polygon)) + f_4 = Feature.objects.get(name='GeometryCollection') + self.assertEqual(True, isinstance(f_4.geom, GeometryCollection)) + self.assertEqual(f_3.geom, f_4.geom[2]) + + @no_mysql + def test19_centroid(self): + "Testing the `centroid` GeoQuerySet method." + qs = State.objects.exclude(poly__isnull=True).centroid() + if oracle: + tol = 0.1 + elif spatialite: + tol = 0.000001 + else: + tol = 0.000000001 + for s in qs: + self.assertEqual(True, s.poly.centroid.equals_exact(s.centroid, tol)) + + @no_mysql + def test20_pointonsurface(self): + "Testing the `point_on_surface` GeoQuerySet method." + # Reference values. + if oracle: + # SELECT SDO_UTIL.TO_WKTGEOMETRY(SDO_GEOM.SDO_POINTONSURFACE(GEOAPP_COUNTRY.MPOLY, 0.05)) FROM GEOAPP_COUNTRY; + ref = {'New Zealand' : fromstr('POINT (174.616364 -36.100861)', srid=4326), + 'Texas' : fromstr('POINT (-103.002434 36.500397)', srid=4326), + } + + elif postgis or spatialite: + # Using GEOSGeometry to compute the reference point on surface values + # -- since PostGIS also uses GEOS these should be the same. + ref = {'New Zealand' : Country.objects.get(name='New Zealand').mpoly.point_on_surface, + 'Texas' : Country.objects.get(name='Texas').mpoly.point_on_surface + } + + for c in Country.objects.point_on_surface(): + if spatialite: + # XXX This seems to be a WKT-translation-related precision issue? + tol = 0.00001 + else: + tol = 0.000000001 + self.assertEqual(True, ref[c.name].equals_exact(c.point_on_surface, tol)) + + @no_mysql + @no_oracle + def test21_scale(self): + "Testing the `scale` GeoQuerySet method." + xfac, yfac = 2, 3 + tol = 5 # XXX The low precision tolerance is for SpatiaLite + qs = Country.objects.scale(xfac, yfac, model_att='scaled') + for c in qs: + for p1, p2 in zip(c.mpoly, c.scaled): + for r1, r2 in zip(p1, p2): + for c1, c2 in zip(r1.coords, r2.coords): + self.assertAlmostEqual(c1[0] * xfac, c2[0], tol) + self.assertAlmostEqual(c1[1] * yfac, c2[1], tol) + + @no_mysql + @no_oracle + def test22_translate(self): + "Testing the `translate` GeoQuerySet method." + xfac, yfac = 5, -23 + qs = Country.objects.translate(xfac, yfac, model_att='translated') + for c in qs: + for p1, p2 in zip(c.mpoly, c.translated): + for r1, r2 in zip(p1, p2): + for c1, c2 in zip(r1.coords, r2.coords): + # XXX The low precision is for SpatiaLite + self.assertAlmostEqual(c1[0] + xfac, c2[0], 5) + self.assertAlmostEqual(c1[1] + yfac, c2[1], 5) + + @no_mysql + def test23_numgeom(self): + "Testing the `num_geom` GeoQuerySet method." + # Both 'countries' only have two geometries. + for c in Country.objects.num_geom(): self.assertEqual(2, c.num_geom) + for c in City.objects.filter(point__isnull=False).num_geom(): + # Oracle will return 1 for the number of geometries on non-collections, + # whereas PostGIS will return None. + if postgis: + self.assertEqual(None, c.num_geom) + else: + self.assertEqual(1, c.num_geom) + + @no_mysql + @no_spatialite # SpatiaLite can only count vertices in LineStrings + def test24_numpoints(self): + "Testing the `num_points` GeoQuerySet method." + for c in Country.objects.num_points(): + self.assertEqual(c.mpoly.num_points, c.num_points) + + if not oracle: + # Oracle cannot count vertices in Point geometries. + for c in City.objects.num_points(): self.assertEqual(1, c.num_points) + + @no_mysql + def test25_geoset(self): + "Testing the `difference`, `intersection`, `sym_difference`, and `union` GeoQuerySet methods." + geom = Point(5, 23) + tol = 1 + qs = Country.objects.all().difference(geom).sym_difference(geom).union(geom) + + # XXX For some reason SpatiaLite does something screwey with the Texas geometry here. Also, + # XXX it doesn't like the null intersection. + if spatialite: + qs = qs.exclude(name='Texas') + else: + qs = qs.intersection(geom) + + for c in qs: + if oracle: + # Should be able to execute the queries; however, they won't be the same + # as GEOS (because Oracle doesn't use GEOS internally like PostGIS or + # SpatiaLite). + pass + else: + self.assertEqual(c.mpoly.difference(geom), c.difference) + if not spatialite: + self.assertEqual(c.mpoly.intersection(geom), c.intersection) + self.assertEqual(c.mpoly.sym_difference(geom), c.sym_difference) + self.assertEqual(c.mpoly.union(geom), c.union) + + @no_mysql + def test26_inherited_geofields(self): + "Test GeoQuerySet methods on inherited Geometry fields." + # Creating a Pennsylvanian city. + mansfield = PennsylvaniaCity.objects.create(name='Mansfield', county='Tioga', point='POINT(-77.071445 41.823881)') + + # All transformation SQL will need to be performed on the + # _parent_ table. + qs = PennsylvaniaCity.objects.transform(32128) + + self.assertEqual(1, qs.count()) + for pc in qs: self.assertEqual(32128, pc.point.srid) + + @no_mysql + @no_oracle + @no_spatialite + def test27_snap_to_grid(self): + "Testing GeoQuerySet.snap_to_grid()." + # Let's try and break snap_to_grid() with bad combinations of arguments. + for bad_args in ((), range(3), range(5)): + self.assertRaises(ValueError, Country.objects.snap_to_grid, *bad_args) + for bad_args in (('1.0',), (1.0, None), tuple(map(unicode, range(4)))): + self.assertRaises(TypeError, Country.objects.snap_to_grid, *bad_args) + + # Boundary for San Marino, courtesy of Bjorn Sandvik of thematicmapping.org + # from the world borders dataset he provides. + wkt = ('MULTIPOLYGON(((12.41580 43.95795,12.45055 43.97972,12.45389 43.98167,' + '12.46250 43.98472,12.47167 43.98694,12.49278 43.98917,' + '12.50555 43.98861,12.51000 43.98694,12.51028 43.98277,' + '12.51167 43.94333,12.51056 43.93916,12.49639 43.92333,' + '12.49500 43.91472,12.48778 43.90583,12.47444 43.89722,' + '12.46472 43.89555,12.45917 43.89611,12.41639 43.90472,' + '12.41222 43.90610,12.40782 43.91366,12.40389 43.92667,' + '12.40500 43.94833,12.40889 43.95499,12.41580 43.95795)))') + sm = Country.objects.create(name='San Marino', mpoly=fromstr(wkt)) + + # Because floating-point arithmitic isn't exact, we set a tolerance + # to pass into GEOS `equals_exact`. + tol = 0.000000001 + + # SELECT AsText(ST_SnapToGrid("geoapp_country"."mpoly", 0.1)) FROM "geoapp_country" WHERE "geoapp_country"."name" = 'San Marino'; + ref = fromstr('MULTIPOLYGON(((12.4 44,12.5 44,12.5 43.9,12.4 43.9,12.4 44)))') + self.failUnless(ref.equals_exact(Country.objects.snap_to_grid(0.1).get(name='San Marino').snap_to_grid, tol)) + + # SELECT AsText(ST_SnapToGrid("geoapp_country"."mpoly", 0.05, 0.23)) FROM "geoapp_country" WHERE "geoapp_country"."name" = 'San Marino'; + ref = fromstr('MULTIPOLYGON(((12.4 43.93,12.45 43.93,12.5 43.93,12.45 43.93,12.4 43.93)))') + self.failUnless(ref.equals_exact(Country.objects.snap_to_grid(0.05, 0.23).get(name='San Marino').snap_to_grid, tol)) + + # SELECT AsText(ST_SnapToGrid("geoapp_country"."mpoly", 0.5, 0.17, 0.05, 0.23)) FROM "geoapp_country" WHERE "geoapp_country"."name" = 'San Marino'; + ref = fromstr('MULTIPOLYGON(((12.4 43.87,12.45 43.87,12.45 44.1,12.5 44.1,12.5 43.87,12.45 43.87,12.4 43.87)))') + self.failUnless(ref.equals_exact(Country.objects.snap_to_grid(0.05, 0.23, 0.5, 0.17).get(name='San Marino').snap_to_grid, tol)) + + @no_mysql + @no_spatialite + def test28_reverse(self): + "Testing GeoQuerySet.reverse_geom()." + coords = [ (-95.363151, 29.763374), (-95.448601, 29.713803) ] + Track.objects.create(name='Foo', line=LineString(coords)) + t = Track.objects.reverse_geom().get(name='Foo') + coords.reverse() + self.assertEqual(tuple(coords), t.reverse_geom.coords) + if oracle: + self.assertRaises(TypeError, State.objects.reverse_geom) + + @no_mysql + @no_oracle + @no_spatialite + def test29_force_rhr(self): + "Testing GeoQuerySet.force_rhr()." + rings = ( ( (0, 0), (5, 0), (0, 5), (0, 0) ), + ( (1, 1), (1, 3), (3, 1), (1, 1) ), + ) + rhr_rings = ( ( (0, 0), (0, 5), (5, 0), (0, 0) ), + ( (1, 1), (3, 1), (1, 3), (1, 1) ), + ) + State.objects.create(name='Foo', poly=Polygon(*rings)) + s = State.objects.force_rhr().get(name='Foo') + self.assertEqual(rhr_rings, s.force_rhr.coords) + + @no_mysql + @no_oracle + @no_spatialite + def test29_force_rhr(self): + "Testing GeoQuerySet.geohash()." + if not connection.ops.geohash: return + # Reference query: + # SELECT ST_GeoHash(point) FROM geoapp_city WHERE name='Houston'; + # SELECT ST_GeoHash(point, 5) FROM geoapp_city WHERE name='Houston'; + ref_hash = '9vk1mfq8jx0c8e0386z6' + h1 = City.objects.geohash().get(name='Houston') + h2 = City.objects.geohash(precision=5).get(name='Houston') + self.assertEqual(ref_hash, h1.geohash) + self.assertEqual(ref_hash[:5], h2.geohash) + +from test_feeds import GeoFeedTest +from test_regress import GeoRegressionTests +from test_sitemaps import GeoSitemapTest diff --git a/parts/django/django/contrib/gis/tests/geoapp/urls.py b/parts/django/django/contrib/gis/tests/geoapp/urls.py new file mode 100644 index 0000000..edaf280 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/geoapp/urls.py @@ -0,0 +1,14 @@ +from django.conf.urls.defaults import * +from feeds import feed_dict + +urlpatterns = patterns('', + (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': feed_dict}), +) + +from sitemaps import sitemaps +urlpatterns += patterns('django.contrib.gis.sitemaps.views', + (r'^sitemap.xml$', 'index', {'sitemaps' : sitemaps}), + (r'^sitemaps/(?P<section>\w+)\.xml$', 'sitemap', {'sitemaps' : sitemaps}), + (r'^sitemaps/kml/(?P<label>\w+)/(?P<model>\w+)/(?P<field_name>\w+)\.kml$', 'kml'), + (r'^sitemaps/kml/(?P<label>\w+)/(?P<model>\w+)/(?P<field_name>\w+)\.kmz$', 'kmz'), +) diff --git a/parts/django/django/contrib/gis/tests/geogapp/__init__.py b/parts/django/django/contrib/gis/tests/geogapp/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/geogapp/__init__.py diff --git a/parts/django/django/contrib/gis/tests/geogapp/fixtures/initial_data.json b/parts/django/django/contrib/gis/tests/geogapp/fixtures/initial_data.json new file mode 100644 index 0000000..0664411 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/geogapp/fixtures/initial_data.json @@ -0,0 +1,98 @@ +[ + { + "pk": 1, + "model": "geogapp.city", + "fields": { + "name": "Houston", + "point": "POINT (-95.363151 29.763374)" + } + }, + { + "pk": 2, + "model": "geogapp.city", + "fields": { + "name": "Dallas", + "point": "POINT (-96.801611 32.782057)" + } + }, + { + "pk": 3, + "model": "geogapp.city", + "fields": { + "name": "Oklahoma City", + "point": "POINT (-97.521157 34.464642)" + } + }, + { + "pk": 4, + "model": "geogapp.city", + "fields": { + "name": "Wellington", + "point": "POINT (174.783117 -41.315268)" + } + }, + { + "pk": 5, + "model": "geogapp.city", + "fields": { + "name": "Pueblo", + "point": "POINT (-104.609252 38.255001)" + } + }, + { + "pk": 6, + "model": "geogapp.city", + "fields": { + "name": "Lawrence", + "point": "POINT (-95.235060 38.971823)" + } + }, + { + "pk": 7, + "model": "geogapp.city", + "fields": { + "name": "Chicago", + "point": "POINT (-87.650175 41.850385)" + } + }, + { + "pk": 8, + "model": "geogapp.city", + "fields": { + "name": "Victoria", + "point": "POINT (-123.305196 48.462611)" + } + }, + { + "pk": 1, + "model": "geogapp.zipcode", + "fields" : { + "code" : "77002", + "poly" : "SRID=4269;POLYGON ((-95.365015 29.772327, -95.362415 29.772327, -95.360915 29.771827, -95.354615 29.771827, -95.351515 29.772527, -95.350915 29.765327, -95.351015 29.762436, -95.350115 29.760328, -95.347515 29.758528, -95.352315 29.753928, -95.356415 29.756328, -95.358215 29.754028, -95.360215 29.756328, -95.363415 29.757128, -95.364014 29.75638, -95.363415 29.753928, -95.360015 29.751828, -95.361815 29.749528, -95.362715 29.750028, -95.367516 29.744128, -95.369316 29.745128, -95.373916 29.744128, -95.380116 29.738028, -95.387916 29.727929, -95.388516 29.729629, -95.387916 29.732129, -95.382916 29.737428, -95.376616 29.742228, -95.372616 29.747228, -95.378601 29.750846, -95.378616 29.752028, -95.378616 29.754428, -95.376016 29.754528, -95.374616 29.759828, -95.373616 29.761128, -95.371916 29.763928, -95.372316 29.768727, -95.365884 29.76791, -95.366015 29.767127, -95.358715 29.765327, -95.358615 29.766327, -95.359115 29.767227, -95.360215 29.767027, -95.362783 29.768267, -95.365315 29.770527, -95.365015 29.772327))" + } + }, + { + "pk": 2, + "model": "geogapp.zipcode", + "fields" : { + "code" : "77005", + "poly" : "SRID=4269;POLYGON ((-95.447918 29.727275, -95.428017 29.728729, -95.421117 29.729029, -95.418617 29.727629, -95.418517 29.726429, -95.402117 29.726629, -95.402117 29.725729, -95.395316 29.725729, -95.391916 29.726229, -95.389716 29.725829, -95.396517 29.715429, -95.397517 29.715929, -95.400917 29.711429, -95.411417 29.715029, -95.418417 29.714729, -95.418317 29.70623, -95.440818 29.70593, -95.445018 29.70683, -95.446618 29.70763, -95.447418 29.71003, -95.447918 29.727275))" + } + }, + { + "pk": 3, + "model": "geogapp.zipcode", + "fields" : { + "code" : "77025", + "poly" : "SRID=4269;POLYGON ((-95.418317 29.70623, -95.414717 29.706129, -95.414617 29.70533, -95.418217 29.70533, -95.419817 29.69533, -95.419484 29.694196, -95.417166 29.690901, -95.414517 29.69433, -95.413317 29.69263, -95.412617 29.68973, -95.412817 29.68753, -95.414087 29.685055, -95.419165 29.685428, -95.421617 29.68513, -95.425717 29.67983, -95.425017 29.67923, -95.424517 29.67763, -95.427418 29.67763, -95.438018 29.664631, -95.436713 29.664411, -95.440118 29.662231, -95.439218 29.661031, -95.437718 29.660131, -95.435718 29.659731, -95.431818 29.660331, -95.441418 29.656631, -95.441318 29.656331, -95.441818 29.656131, -95.441718 29.659031, -95.441118 29.661031, -95.446718 29.656431, -95.446518 29.673431, -95.446918 29.69013, -95.447418 29.71003, -95.446618 29.70763, -95.445018 29.70683, -95.440818 29.70593, -95.418317 29.70623))" + } + }, + { + "pk": 4, + "model": "geogapp.zipcode", + "fields" : { + "code" : "77401", + "poly" : "SRID=4269;POLYGON ((-95.447918 29.727275, -95.447418 29.71003, -95.446918 29.69013, -95.454318 29.68893, -95.475819 29.68903, -95.475819 29.69113, -95.484419 29.69103, -95.484519 29.69903, -95.480419 29.70133, -95.480419 29.69833, -95.474119 29.69833, -95.474119 29.70453, -95.472719 29.71283, -95.468019 29.71293, -95.468219 29.720229, -95.464018 29.720229, -95.464118 29.724529, -95.463018 29.725929, -95.459818 29.726129, -95.459918 29.720329, -95.451418 29.720429, -95.451775 29.726303, -95.451318 29.727029, -95.447918 29.727275))" + } + } +]
\ No newline at end of file diff --git a/parts/django/django/contrib/gis/tests/geogapp/models.py b/parts/django/django/contrib/gis/tests/geogapp/models.py new file mode 100644 index 0000000..3696ba2 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/geogapp/models.py @@ -0,0 +1,20 @@ +from django.contrib.gis.db import models + +class City(models.Model): + name = models.CharField(max_length=30) + point = models.PointField(geography=True) + objects = models.GeoManager() + def __unicode__(self): return self.name + +class Zipcode(models.Model): + code = models.CharField(max_length=10) + poly = models.PolygonField(geography=True) + objects = models.GeoManager() + def __unicode__(self): return self.code + +class County(models.Model): + name = models.CharField(max_length=25) + state = models.CharField(max_length=20) + mpoly = models.MultiPolygonField(geography=True) + objects = models.GeoManager() + def __unicode__(self): return ' County, '.join([self.name, self.state]) diff --git a/parts/django/django/contrib/gis/tests/geogapp/tests.py b/parts/django/django/contrib/gis/tests/geogapp/tests.py new file mode 100644 index 0000000..cb69ce9 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/geogapp/tests.py @@ -0,0 +1,87 @@ +""" +Tests for geography support in PostGIS 1.5+ +""" +import os +from django.contrib.gis import gdal +from django.contrib.gis.measure import D +from django.test import TestCase +from models import City, County, Zipcode + +class GeographyTest(TestCase): + + def test01_fixture_load(self): + "Ensure geography features loaded properly." + self.assertEqual(8, City.objects.count()) + + def test02_distance_lookup(self): + "Testing GeoQuerySet distance lookup support on non-point geography fields." + z = Zipcode.objects.get(code='77002') + cities1 = list(City.objects + .filter(point__distance_lte=(z.poly, D(mi=500))) + .order_by('name') + .values_list('name', flat=True)) + cities2 = list(City.objects + .filter(point__dwithin=(z.poly, D(mi=500))) + .order_by('name') + .values_list('name', flat=True)) + for cities in [cities1, cities2]: + self.assertEqual(['Dallas', 'Houston', 'Oklahoma City'], cities) + + def test03_distance_method(self): + "Testing GeoQuerySet.distance() support on non-point geography fields." + # `GeoQuerySet.distance` is not allowed geometry fields. + htown = City.objects.get(name='Houston') + qs = Zipcode.objects.distance(htown.point) + + def test04_invalid_operators_functions(self): + "Ensuring exceptions are raised for operators & functions invalid on geography fields." + # Only a subset of the geometry functions & operator are available + # to PostGIS geography types. For more information, visit: + # http://postgis.refractions.net/documentation/manual-1.5/ch08.html#PostGIS_GeographyFunctions + z = Zipcode.objects.get(code='77002') + # ST_Within not available. + self.assertRaises(ValueError, City.objects.filter(point__within=z.poly).count) + # `@` operator not available. + self.assertRaises(ValueError, City.objects.filter(point__contained=z.poly).count) + + # Regression test for #14060, `~=` was never really implemented for PostGIS. + htown = City.objects.get(name='Houston') + self.assertRaises(ValueError, City.objects.get, point__exact=htown.point) + + def test05_geography_layermapping(self): + "Testing LayerMapping support on models with geography fields." + # There is a similar test in `layermap` that uses the same data set, + # but the County model here is a bit different. + if not gdal.HAS_GDAL: return + from django.contrib.gis.utils import LayerMapping + + # Getting the shapefile and mapping dictionary. + shp_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'data')) + co_shp = os.path.join(shp_path, 'counties', 'counties.shp') + co_mapping = {'name' : 'Name', + 'state' : 'State', + 'mpoly' : 'MULTIPOLYGON', + } + + # Reference county names, number of polygons, and state names. + names = ['Bexar', 'Galveston', 'Harris', 'Honolulu', 'Pueblo'] + num_polys = [1, 2, 1, 19, 1] # Number of polygons for each. + st_names = ['Texas', 'Texas', 'Texas', 'Hawaii', 'Colorado'] + + lm = LayerMapping(County, co_shp, co_mapping, source_srs=4269, unique='name') + lm.save(silent=True, strict=True) + + for c, name, num_poly, state in zip(County.objects.order_by('name'), names, num_polys, st_names): + self.assertEqual(4326, c.mpoly.srid) + self.assertEqual(num_poly, len(c.mpoly)) + self.assertEqual(name, c.name) + self.assertEqual(state, c.state) + + def test06_geography_area(self): + "Testing that Area calculations work on geography columns." + from django.contrib.gis.measure import A + # SELECT ST_Area(poly) FROM geogapp_zipcode WHERE code='77002'; + ref_area = 5439084.70637573 + tol = 5 + z = Zipcode.objects.area().get(code='77002') + self.assertAlmostEqual(z.area.sq_m, ref_area, tol) diff --git a/parts/django/django/contrib/gis/tests/layermap/__init__.py b/parts/django/django/contrib/gis/tests/layermap/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/layermap/__init__.py diff --git a/parts/django/django/contrib/gis/tests/layermap/models.py b/parts/django/django/contrib/gis/tests/layermap/models.py new file mode 100644 index 0000000..3a34d16 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/layermap/models.py @@ -0,0 +1,66 @@ +from django.contrib.gis.db import models + +class State(models.Model): + name = models.CharField(max_length=20) + objects = models.GeoManager() + +class County(models.Model): + name = models.CharField(max_length=25) + state = models.ForeignKey(State) + mpoly = models.MultiPolygonField(srid=4269) # Multipolygon in NAD83 + objects = models.GeoManager() + +class CountyFeat(models.Model): + name = models.CharField(max_length=25) + poly = models.PolygonField(srid=4269) + objects = models.GeoManager() + +class City(models.Model): + name = models.CharField(max_length=25) + population = models.IntegerField() + density = models.DecimalField(max_digits=7, decimal_places=1) + dt = models.DateField() + point = models.PointField() + objects = models.GeoManager() + +class Interstate(models.Model): + name = models.CharField(max_length=20) + length = models.DecimalField(max_digits=6, decimal_places=2) + path = models.LineStringField() + objects = models.GeoManager() + +# Same as `City` above, but for testing model inheritance. +class CityBase(models.Model): + name = models.CharField(max_length=25) + population = models.IntegerField() + density = models.DecimalField(max_digits=7, decimal_places=1) + point = models.PointField() + objects = models.GeoManager() + +class ICity1(CityBase): + dt = models.DateField() + +class ICity2(ICity1): + dt_time = models.DateTimeField(auto_now=True) + +# Mapping dictionaries for the models above. +co_mapping = {'name' : 'Name', + 'state' : {'name' : 'State'}, # ForeignKey's use another mapping dictionary for the _related_ Model (State in this case). + 'mpoly' : 'MULTIPOLYGON', # Will convert POLYGON features into MULTIPOLYGONS. + } + +cofeat_mapping = {'name' : 'Name', + 'poly' : 'POLYGON', + } + +city_mapping = {'name' : 'Name', + 'population' : 'Population', + 'density' : 'Density', + 'dt' : 'Created', + 'point' : 'POINT', + } + +inter_mapping = {'name' : 'Name', + 'length' : 'Length', + 'path' : 'LINESTRING', + } diff --git a/parts/django/django/contrib/gis/tests/layermap/tests.py b/parts/django/django/contrib/gis/tests/layermap/tests.py new file mode 100644 index 0000000..6394b04 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/layermap/tests.py @@ -0,0 +1,268 @@ +import os +import unittest +from decimal import Decimal + +from django.utils.copycompat import copy + +from django.contrib.gis.gdal import DataSource +from django.contrib.gis.tests.utils import mysql +from django.contrib.gis.utils.layermapping import LayerMapping, LayerMapError, InvalidDecimal, MissingForeignKey + +from models import City, County, CountyFeat, Interstate, ICity1, ICity2, State, city_mapping, co_mapping, cofeat_mapping, inter_mapping + +shp_path = os.path.realpath(os.path.join(os.path.dirname(__file__), os.pardir, 'data')) +city_shp = os.path.join(shp_path, 'cities', 'cities.shp') +co_shp = os.path.join(shp_path, 'counties', 'counties.shp') +inter_shp = os.path.join(shp_path, 'interstates', 'interstates.shp') + +# Dictionaries to hold what's expected in the county shapefile. +NAMES = ['Bexar', 'Galveston', 'Harris', 'Honolulu', 'Pueblo'] +NUMS = [1, 2, 1, 19, 1] # Number of polygons for each. +STATES = ['Texas', 'Texas', 'Texas', 'Hawaii', 'Colorado'] + +class LayerMapTest(unittest.TestCase): + + def test01_init(self): + "Testing LayerMapping initialization." + + # Model field that does not exist. + bad1 = copy(city_mapping) + bad1['foobar'] = 'FooField' + + # Shapefile field that does not exist. + bad2 = copy(city_mapping) + bad2['name'] = 'Nombre' + + # Nonexistent geographic field type. + bad3 = copy(city_mapping) + bad3['point'] = 'CURVE' + + # Incrementing through the bad mapping dictionaries and + # ensuring that a LayerMapError is raised. + for bad_map in (bad1, bad2, bad3): + try: + lm = LayerMapping(City, city_shp, bad_map) + except LayerMapError: + pass + else: + self.fail('Expected a LayerMapError.') + + # A LookupError should be thrown for bogus encodings. + try: + lm = LayerMapping(City, city_shp, city_mapping, encoding='foobar') + except LookupError: + pass + else: + self.fail('Expected a LookupError') + + def test02_simple_layermap(self): + "Test LayerMapping import of a simple point shapefile." + # Setting up for the LayerMapping. + lm = LayerMapping(City, city_shp, city_mapping) + lm.save() + + # There should be three cities in the shape file. + self.assertEqual(3, City.objects.count()) + + # Opening up the shapefile, and verifying the values in each + # of the features made it to the model. + ds = DataSource(city_shp) + layer = ds[0] + for feat in layer: + city = City.objects.get(name=feat['Name'].value) + self.assertEqual(feat['Population'].value, city.population) + self.assertEqual(Decimal(str(feat['Density'])), city.density) + self.assertEqual(feat['Created'].value, city.dt) + + # Comparing the geometries. + pnt1, pnt2 = feat.geom, city.point + self.assertAlmostEqual(pnt1.x, pnt2.x, 6) + self.assertAlmostEqual(pnt1.y, pnt2.y, 6) + + def test03_layermap_strict(self): + "Testing the `strict` keyword, and import of a LineString shapefile." + # When the `strict` keyword is set an error encountered will force + # the importation to stop. + try: + lm = LayerMapping(Interstate, inter_shp, inter_mapping) + lm.save(silent=True, strict=True) + except InvalidDecimal: + # No transactions for geoms on MySQL; delete added features. + if mysql: Interstate.objects.all().delete() + else: + self.fail('Should have failed on strict import with invalid decimal values.') + + # This LayerMapping should work b/c `strict` is not set. + lm = LayerMapping(Interstate, inter_shp, inter_mapping) + lm.save(silent=True) + + # Two interstate should have imported correctly. + self.assertEqual(2, Interstate.objects.count()) + + # Verifying the values in the layer w/the model. + ds = DataSource(inter_shp) + + # Only the first two features of this shapefile are valid. + valid_feats = ds[0][:2] + for feat in valid_feats: + istate = Interstate.objects.get(name=feat['Name'].value) + + if feat.fid == 0: + self.assertEqual(Decimal(str(feat['Length'])), istate.length) + elif feat.fid == 1: + # Everything but the first two decimal digits were truncated, + # because the Interstate model's `length` field has decimal_places=2. + self.assertAlmostEqual(feat.get('Length'), float(istate.length), 2) + + for p1, p2 in zip(feat.geom, istate.path): + self.assertAlmostEqual(p1[0], p2[0], 6) + self.assertAlmostEqual(p1[1], p2[1], 6) + + def county_helper(self, county_feat=True): + "Helper function for ensuring the integrity of the mapped County models." + for name, n, st in zip(NAMES, NUMS, STATES): + # Should only be one record b/c of `unique` keyword. + c = County.objects.get(name=name) + self.assertEqual(n, len(c.mpoly)) + self.assertEqual(st, c.state.name) # Checking ForeignKey mapping. + + # Multiple records because `unique` was not set. + if county_feat: + qs = CountyFeat.objects.filter(name=name) + self.assertEqual(n, qs.count()) + + def test04_layermap_unique_multigeometry_fk(self): + "Testing the `unique`, and `transform`, geometry collection conversion, and ForeignKey mappings." + # All the following should work. + try: + # Telling LayerMapping that we want no transformations performed on the data. + lm = LayerMapping(County, co_shp, co_mapping, transform=False) + + # Specifying the source spatial reference system via the `source_srs` keyword. + lm = LayerMapping(County, co_shp, co_mapping, source_srs=4269) + lm = LayerMapping(County, co_shp, co_mapping, source_srs='NAD83') + + # Unique may take tuple or string parameters. + for arg in ('name', ('name', 'mpoly')): + lm = LayerMapping(County, co_shp, co_mapping, transform=False, unique=arg) + except: + self.fail('No exception should be raised for proper use of keywords.') + + # Testing invalid params for the `unique` keyword. + for e, arg in ((TypeError, 5.0), (ValueError, 'foobar'), (ValueError, ('name', 'mpolygon'))): + self.assertRaises(e, LayerMapping, County, co_shp, co_mapping, transform=False, unique=arg) + + # No source reference system defined in the shapefile, should raise an error. + if not mysql: + self.assertRaises(LayerMapError, LayerMapping, County, co_shp, co_mapping) + + # Passing in invalid ForeignKey mapping parameters -- must be a dictionary + # mapping for the model the ForeignKey points to. + bad_fk_map1 = copy(co_mapping); bad_fk_map1['state'] = 'name' + bad_fk_map2 = copy(co_mapping); bad_fk_map2['state'] = {'nombre' : 'State'} + self.assertRaises(TypeError, LayerMapping, County, co_shp, bad_fk_map1, transform=False) + self.assertRaises(LayerMapError, LayerMapping, County, co_shp, bad_fk_map2, transform=False) + + # There exist no State models for the ForeignKey mapping to work -- should raise + # a MissingForeignKey exception (this error would be ignored if the `strict` + # keyword is not set). + lm = LayerMapping(County, co_shp, co_mapping, transform=False, unique='name') + self.assertRaises(MissingForeignKey, lm.save, silent=True, strict=True) + + # Now creating the state models so the ForeignKey mapping may work. + co, hi, tx = State(name='Colorado'), State(name='Hawaii'), State(name='Texas') + co.save(), hi.save(), tx.save() + + # If a mapping is specified as a collection, all OGR fields that + # are not collections will be converted into them. For example, + # a Point column would be converted to MultiPoint. Other things being done + # w/the keyword args: + # `transform=False`: Specifies that no transform is to be done; this + # has the effect of ignoring the spatial reference check (because the + # county shapefile does not have implicit spatial reference info). + # + # `unique='name'`: Creates models on the condition that they have + # unique county names; geometries from each feature however will be + # appended to the geometry collection of the unique model. Thus, + # all of the various islands in Honolulu county will be in in one + # database record with a MULTIPOLYGON type. + lm = LayerMapping(County, co_shp, co_mapping, transform=False, unique='name') + lm.save(silent=True, strict=True) + + # A reference that doesn't use the unique keyword; a new database record will + # created for each polygon. + lm = LayerMapping(CountyFeat, co_shp, cofeat_mapping, transform=False) + lm.save(silent=True, strict=True) + + # The county helper is called to ensure integrity of County models. + self.county_helper() + + def test05_test_fid_range_step(self): + "Tests the `fid_range` keyword and the `step` keyword of .save()." + # Function for clearing out all the counties before testing. + def clear_counties(): County.objects.all().delete() + + # Initializing the LayerMapping object to use in these tests. + lm = LayerMapping(County, co_shp, co_mapping, transform=False, unique='name') + + # Bad feature id ranges should raise a type error. + clear_counties() + bad_ranges = (5.0, 'foo', co_shp) + for bad in bad_ranges: + self.assertRaises(TypeError, lm.save, fid_range=bad) + + # Step keyword should not be allowed w/`fid_range`. + fr = (3, 5) # layer[3:5] + self.assertRaises(LayerMapError, lm.save, fid_range=fr, step=10) + lm.save(fid_range=fr) + + # Features IDs 3 & 4 are for Galveston County, Texas -- only + # one model is returned because the `unique` keyword was set. + qs = County.objects.all() + self.assertEqual(1, qs.count()) + self.assertEqual('Galveston', qs[0].name) + + # Features IDs 5 and beyond for Honolulu County, Hawaii, and + # FID 0 is for Pueblo County, Colorado. + clear_counties() + lm.save(fid_range=slice(5, None), silent=True, strict=True) # layer[5:] + lm.save(fid_range=slice(None, 1), silent=True, strict=True) # layer[:1] + + # Only Pueblo & Honolulu counties should be present because of + # the `unique` keyword. Have to set `order_by` on this QuerySet + # or else MySQL will return a different ordering than the other dbs. + qs = County.objects.order_by('name') + self.assertEqual(2, qs.count()) + hi, co = tuple(qs) + hi_idx, co_idx = tuple(map(NAMES.index, ('Honolulu', 'Pueblo'))) + self.assertEqual('Pueblo', co.name); self.assertEqual(NUMS[co_idx], len(co.mpoly)) + self.assertEqual('Honolulu', hi.name); self.assertEqual(NUMS[hi_idx], len(hi.mpoly)) + + # Testing the `step` keyword -- should get the same counties + # regardless of we use a step that divides equally, that is odd, + # or that is larger than the dataset. + for st in (4,7,1000): + clear_counties() + lm.save(step=st, strict=True) + self.county_helper(county_feat=False) + + def test06_model_inheritance(self): + "Tests LayerMapping on inherited models. See #12093." + icity_mapping = {'name' : 'Name', + 'population' : 'Population', + 'density' : 'Density', + 'point' : 'POINT', + 'dt' : 'Created', + } + + # Parent model has geometry field. + lm1 = LayerMapping(ICity1, city_shp, icity_mapping) + lm1.save() + + # Grandparent has geometry field. + lm2 = LayerMapping(ICity2, city_shp, icity_mapping) + lm2.save() + + self.assertEqual(6, ICity1.objects.count()) + self.assertEqual(3, ICity2.objects.count()) + diff --git a/parts/django/django/contrib/gis/tests/relatedapp/__init__.py b/parts/django/django/contrib/gis/tests/relatedapp/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/relatedapp/__init__.py diff --git a/parts/django/django/contrib/gis/tests/relatedapp/models.py b/parts/django/django/contrib/gis/tests/relatedapp/models.py new file mode 100644 index 0000000..2e9a62b --- /dev/null +++ b/parts/django/django/contrib/gis/tests/relatedapp/models.py @@ -0,0 +1,49 @@ +from django.contrib.gis.db import models +from django.contrib.localflavor.us.models import USStateField + +class Location(models.Model): + point = models.PointField() + objects = models.GeoManager() + def __unicode__(self): return self.point.wkt + +class City(models.Model): + name = models.CharField(max_length=50) + state = USStateField() + location = models.ForeignKey(Location) + objects = models.GeoManager() + def __unicode__(self): return self.name + +class AugmentedLocation(Location): + extra_text = models.TextField(blank=True) + objects = models.GeoManager() + +class DirectoryEntry(models.Model): + listing_text = models.CharField(max_length=50) + location = models.ForeignKey(AugmentedLocation) + objects = models.GeoManager() + +class Parcel(models.Model): + name = models.CharField(max_length=30) + city = models.ForeignKey(City) + center1 = models.PointField() + # Throwing a curveball w/`db_column` here. + center2 = models.PointField(srid=2276, db_column='mycenter') + border1 = models.PolygonField() + border2 = models.PolygonField(srid=2276) + objects = models.GeoManager() + def __unicode__(self): return self.name + +# These use the GeoManager but do not have any geographic fields. +class Author(models.Model): + name = models.CharField(max_length=100) + objects = models.GeoManager() + +class Article(models.Model): + title = models.CharField(max_length=100) + author = models.ForeignKey(Author, unique=True) + objects = models.GeoManager() + +class Book(models.Model): + title = models.CharField(max_length=100) + author = models.ForeignKey(Author, related_name='books', null=True) + objects = models.GeoManager() diff --git a/parts/django/django/contrib/gis/tests/relatedapp/tests.py b/parts/django/django/contrib/gis/tests/relatedapp/tests.py new file mode 100644 index 0000000..c8aeb28 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/relatedapp/tests.py @@ -0,0 +1,284 @@ +from django.test import TestCase + +from django.contrib.gis.geos import GEOSGeometry, Point, MultiPoint +from django.contrib.gis.db.models import Collect, Count, Extent, F, Union +from django.contrib.gis.geometry.backend import Geometry +from django.contrib.gis.tests.utils import mysql, oracle, no_mysql, no_oracle, no_spatialite + +from models import City, Location, DirectoryEntry, Parcel, Book, Author, Article + +class RelatedGeoModelTest(TestCase): + + def test02_select_related(self): + "Testing `select_related` on geographic models (see #7126)." + qs1 = City.objects.all() + qs2 = City.objects.select_related() + qs3 = City.objects.select_related('location') + + # Reference data for what's in the fixtures. + cities = ( + ('Aurora', 'TX', -97.516111, 33.058333), + ('Roswell', 'NM', -104.528056, 33.387222), + ('Kecksburg', 'PA', -79.460734, 40.18476), + ) + + for qs in (qs1, qs2, qs3): + for ref, c in zip(cities, qs): + nm, st, lon, lat = ref + self.assertEqual(nm, c.name) + self.assertEqual(st, c.state) + self.assertEqual(Point(lon, lat), c.location.point) + + @no_mysql + def test03_transform_related(self): + "Testing the `transform` GeoQuerySet method on related geographic models." + # All the transformations are to state plane coordinate systems using + # US Survey Feet (thus a tolerance of 0 implies error w/in 1 survey foot). + tol = 0 + + def check_pnt(ref, pnt): + self.assertAlmostEqual(ref.x, pnt.x, tol) + self.assertAlmostEqual(ref.y, pnt.y, tol) + self.assertEqual(ref.srid, pnt.srid) + + # Each city transformed to the SRID of their state plane coordinate system. + transformed = (('Kecksburg', 2272, 'POINT(1490553.98959621 314792.131023984)'), + ('Roswell', 2257, 'POINT(481902.189077221 868477.766629735)'), + ('Aurora', 2276, 'POINT(2269923.2484839 7069381.28722222)'), + ) + + for name, srid, wkt in transformed: + # Doing this implicitly sets `select_related` select the location. + # TODO: Fix why this breaks on Oracle. + qs = list(City.objects.filter(name=name).transform(srid, field_name='location__point')) + check_pnt(GEOSGeometry(wkt, srid), qs[0].location.point) + + @no_mysql + @no_spatialite + def test04a_related_extent_aggregate(self): + "Testing the `extent` GeoQuerySet aggregates on related geographic models." + # This combines the Extent and Union aggregates into one query + aggs = City.objects.aggregate(Extent('location__point')) + + # One for all locations, one that excludes New Mexico (Roswell). + all_extent = (-104.528056, 29.763374, -79.460734, 40.18476) + txpa_extent = (-97.516111, 29.763374, -79.460734, 40.18476) + e1 = City.objects.extent(field_name='location__point') + e2 = City.objects.exclude(state='NM').extent(field_name='location__point') + e3 = aggs['location__point__extent'] + + # The tolerance value is to four decimal places because of differences + # between the Oracle and PostGIS spatial backends on the extent calculation. + tol = 4 + for ref, e in [(all_extent, e1), (txpa_extent, e2), (all_extent, e3)]: + for ref_val, e_val in zip(ref, e): self.assertAlmostEqual(ref_val, e_val, tol) + + @no_mysql + def test04b_related_union_aggregate(self): + "Testing the `unionagg` GeoQuerySet aggregates on related geographic models." + # This combines the Extent and Union aggregates into one query + aggs = City.objects.aggregate(Union('location__point')) + + # These are the points that are components of the aggregate geographic + # union that is returned. Each point # corresponds to City PK. + p1 = Point(-104.528056, 33.387222) + p2 = Point(-97.516111, 33.058333) + p3 = Point(-79.460734, 40.18476) + p4 = Point(-96.801611, 32.782057) + p5 = Point(-95.363151, 29.763374) + + # Creating the reference union geometry depending on the spatial backend, + # as Oracle will have a different internal ordering of the component + # geometries than PostGIS. The second union aggregate is for a union + # query that includes limiting information in the WHERE clause (in other + # words a `.filter()` precedes the call to `.unionagg()`). + if oracle: + ref_u1 = MultiPoint(p4, p5, p3, p1, p2, srid=4326) + ref_u2 = MultiPoint(p3, p2, srid=4326) + else: + # Looks like PostGIS points by longitude value. + ref_u1 = MultiPoint(p1, p2, p4, p5, p3, srid=4326) + ref_u2 = MultiPoint(p2, p3, srid=4326) + + u1 = City.objects.unionagg(field_name='location__point') + u2 = City.objects.exclude(name__in=('Roswell', 'Houston', 'Dallas', 'Fort Worth')).unionagg(field_name='location__point') + u3 = aggs['location__point__union'] + + self.assertEqual(ref_u1, u1) + self.assertEqual(ref_u2, u2) + self.assertEqual(ref_u1, u3) + + def test05_select_related_fk_to_subclass(self): + "Testing that calling select_related on a query over a model with an FK to a model subclass works" + # Regression test for #9752. + l = list(DirectoryEntry.objects.all().select_related()) + + def test06_f_expressions(self): + "Testing F() expressions on GeometryFields." + # Constructing a dummy parcel border and getting the City instance for + # assigning the FK. + b1 = GEOSGeometry('POLYGON((-97.501205 33.052520,-97.501205 33.052576,-97.501150 33.052576,-97.501150 33.052520,-97.501205 33.052520))', srid=4326) + pcity = City.objects.get(name='Aurora') + + # First parcel has incorrect center point that is equal to the City; + # it also has a second border that is different from the first as a + # 100ft buffer around the City. + c1 = pcity.location.point + c2 = c1.transform(2276, clone=True) + b2 = c2.buffer(100) + p1 = Parcel.objects.create(name='P1', city=pcity, center1=c1, center2=c2, border1=b1, border2=b2) + + # Now creating a second Parcel where the borders are the same, just + # in different coordinate systems. The center points are also the + # the same (but in different coordinate systems), and this time they + # actually correspond to the centroid of the border. + c1 = b1.centroid + c2 = c1.transform(2276, clone=True) + p2 = Parcel.objects.create(name='P2', city=pcity, center1=c1, center2=c2, border1=b1, border2=b1) + + # Should return the second Parcel, which has the center within the + # border. + qs = Parcel.objects.filter(center1__within=F('border1')) + self.assertEqual(1, len(qs)) + self.assertEqual('P2', qs[0].name) + + if not mysql: + # This time center2 is in a different coordinate system and needs + # to be wrapped in transformation SQL. + qs = Parcel.objects.filter(center2__within=F('border1')) + self.assertEqual(1, len(qs)) + self.assertEqual('P2', qs[0].name) + + # Should return the first Parcel, which has the center point equal + # to the point in the City ForeignKey. + qs = Parcel.objects.filter(center1=F('city__location__point')) + self.assertEqual(1, len(qs)) + self.assertEqual('P1', qs[0].name) + + if not mysql: + # This time the city column should be wrapped in transformation SQL. + qs = Parcel.objects.filter(border2__contains=F('city__location__point')) + self.assertEqual(1, len(qs)) + self.assertEqual('P1', qs[0].name) + + def test07_values(self): + "Testing values() and values_list() and GeoQuerySets." + # GeoQuerySet and GeoValuesQuerySet, and GeoValuesListQuerySet respectively. + gqs = Location.objects.all() + gvqs = Location.objects.values() + gvlqs = Location.objects.values_list() + + # Incrementing through each of the models, dictionaries, and tuples + # returned by the different types of GeoQuerySets. + for m, d, t in zip(gqs, gvqs, gvlqs): + # The values should be Geometry objects and not raw strings returned + # by the spatial database. + self.failUnless(isinstance(d['point'], Geometry)) + self.failUnless(isinstance(t[1], Geometry)) + self.assertEqual(m.point, d['point']) + self.assertEqual(m.point, t[1]) + + def test08_defer_only(self): + "Testing defer() and only() on Geographic models." + qs = Location.objects.all() + def_qs = Location.objects.defer('point') + for loc, def_loc in zip(qs, def_qs): + self.assertEqual(loc.point, def_loc.point) + + def test09_pk_relations(self): + "Ensuring correct primary key column is selected across relations. See #10757." + # The expected ID values -- notice the last two location IDs + # are out of order. Dallas and Houston have location IDs that differ + # from their PKs -- this is done to ensure that the related location + # ID column is selected instead of ID column for the city. + city_ids = (1, 2, 3, 4, 5) + loc_ids = (1, 2, 3, 5, 4) + ids_qs = City.objects.order_by('id').values('id', 'location__id') + for val_dict, c_id, l_id in zip(ids_qs, city_ids, loc_ids): + self.assertEqual(val_dict['id'], c_id) + self.assertEqual(val_dict['location__id'], l_id) + + def test10_combine(self): + "Testing the combination of two GeoQuerySets. See #10807." + buf1 = City.objects.get(name='Aurora').location.point.buffer(0.1) + buf2 = City.objects.get(name='Kecksburg').location.point.buffer(0.1) + qs1 = City.objects.filter(location__point__within=buf1) + qs2 = City.objects.filter(location__point__within=buf2) + combined = qs1 | qs2 + names = [c.name for c in combined] + self.assertEqual(2, len(names)) + self.failUnless('Aurora' in names) + self.failUnless('Kecksburg' in names) + + def test11_geoquery_pickle(self): + "Ensuring GeoQuery objects are unpickled correctly. See #10839." + import pickle + from django.contrib.gis.db.models.sql import GeoQuery + qs = City.objects.all() + q_str = pickle.dumps(qs.query) + q = pickle.loads(q_str) + self.assertEqual(GeoQuery, q.__class__) + + # TODO: fix on Oracle -- get the following error because the SQL is ordered + # by a geometry object, which Oracle apparently doesn't like: + # ORA-22901: cannot compare nested table or VARRAY or LOB attributes of an object type + @no_oracle + def test12a_count(self): + "Testing `Count` aggregate use with the `GeoManager` on geo-fields." + # The City, 'Fort Worth' uses the same location as Dallas. + dallas = City.objects.get(name='Dallas') + + # Count annotation should be 2 for the Dallas location now. + loc = Location.objects.annotate(num_cities=Count('city')).get(id=dallas.location.id) + self.assertEqual(2, loc.num_cities) + + def test12b_count(self): + "Testing `Count` aggregate use with the `GeoManager` on non geo-fields. See #11087." + # Should only be one author (Trevor Paglen) returned by this query, and + # the annotation should have 3 for the number of books, see #11087. + # Also testing with a `GeoValuesQuerySet`, see #11489. + qs = Author.objects.annotate(num_books=Count('books')).filter(num_books__gt=1) + vqs = Author.objects.values('name').annotate(num_books=Count('books')).filter(num_books__gt=1) + self.assertEqual(1, len(qs)) + self.assertEqual(3, qs[0].num_books) + self.assertEqual(1, len(vqs)) + self.assertEqual(3, vqs[0]['num_books']) + + # TODO: The phantom model does appear on Oracle. + @no_oracle + def test13_select_related_null_fk(self): + "Testing `select_related` on a nullable ForeignKey via `GeoManager`. See #11381." + no_author = Book.objects.create(title='Without Author') + b = Book.objects.select_related('author').get(title='Without Author') + # Should be `None`, and not a 'dummy' model. + self.assertEqual(None, b.author) + + @no_mysql + @no_oracle + @no_spatialite + def test14_collect(self): + "Testing the `collect` GeoQuerySet method and `Collect` aggregate." + # Reference query: + # SELECT AsText(ST_Collect("relatedapp_location"."point")) FROM "relatedapp_city" LEFT OUTER JOIN + # "relatedapp_location" ON ("relatedapp_city"."location_id" = "relatedapp_location"."id") + # WHERE "relatedapp_city"."state" = 'TX'; + ref_geom = GEOSGeometry('MULTIPOINT(-97.516111 33.058333,-96.801611 32.782057,-95.363151 29.763374,-96.801611 32.782057)') + + c1 = City.objects.filter(state='TX').collect(field_name='location__point') + c2 = City.objects.filter(state='TX').aggregate(Collect('location__point'))['location__point__collect'] + + for coll in (c1, c2): + # Even though Dallas and Ft. Worth share same point, Collect doesn't + # consolidate -- that's why 4 points in MultiPoint. + self.assertEqual(4, len(coll)) + self.assertEqual(ref_geom, coll) + + def test15_invalid_select_related(self): + "Testing doing select_related on the related name manager of a unique FK. See #13934." + qs = Article.objects.select_related('author__article') + # This triggers TypeError when `get_default_columns` has no `local_only` + # keyword. The TypeError is swallowed if QuerySet is actually + # evaluated as list generation swallows TypeError in CPython. + sql = str(qs.query) + + # TODO: Related tests for KML, GML, and distance lookups. diff --git a/parts/django/django/contrib/gis/tests/test_geoforms.py b/parts/django/django/contrib/gis/tests/test_geoforms.py new file mode 100644 index 0000000..aa6b25e --- /dev/null +++ b/parts/django/django/contrib/gis/tests/test_geoforms.py @@ -0,0 +1,65 @@ +import unittest + +from django.forms import ValidationError +from django.contrib.gis import forms +from django.contrib.gis.geos import GEOSGeometry + +class GeometryFieldTest(unittest.TestCase): + + def test00_init(self): + "Testing GeometryField initialization with defaults." + fld = forms.GeometryField() + for bad_default in ('blah', 3, 'FoO', None, 0): + self.assertRaises(ValidationError, fld.clean, bad_default) + + def test01_srid(self): + "Testing GeometryField with a SRID set." + # Input that doesn't specify the SRID is assumed to be in the SRID + # of the input field. + fld = forms.GeometryField(srid=4326) + geom = fld.clean('POINT(5 23)') + self.assertEqual(4326, geom.srid) + # Making the field in a different SRID from that of the geometry, and + # asserting it transforms. + fld = forms.GeometryField(srid=32140) + tol = 0.0000001 + xform_geom = GEOSGeometry('POINT (951640.547328465 4219369.26171664)', srid=32140) + # The cleaned geometry should be transformed to 32140. + cleaned_geom = fld.clean('SRID=4326;POINT (-95.363151 29.763374)') + self.failUnless(xform_geom.equals_exact(cleaned_geom, tol)) + + def test02_null(self): + "Testing GeometryField's handling of null (None) geometries." + # Form fields, by default, are required (`required=True`) + fld = forms.GeometryField() + self.assertRaises(forms.ValidationError, fld.clean, None) + + # Still not allowed if `null=False`. + fld = forms.GeometryField(required=False, null=False) + self.assertRaises(forms.ValidationError, fld.clean, None) + + # This will clean None as a geometry (See #10660). + fld = forms.GeometryField(required=False) + self.assertEqual(None, fld.clean(None)) + + def test03_geom_type(self): + "Testing GeometryField's handling of different geometry types." + # By default, all geometry types are allowed. + fld = forms.GeometryField() + for wkt in ('POINT(5 23)', 'MULTIPOLYGON(((0 0, 0 1, 1 1, 1 0, 0 0)))', 'LINESTRING(0 0, 1 1)'): + self.assertEqual(GEOSGeometry(wkt), fld.clean(wkt)) + + pnt_fld = forms.GeometryField(geom_type='POINT') + self.assertEqual(GEOSGeometry('POINT(5 23)'), pnt_fld.clean('POINT(5 23)')) + self.assertRaises(forms.ValidationError, pnt_fld.clean, 'LINESTRING(0 0, 1 1)') + +def suite(): + s = unittest.TestSuite() + s.addTest(unittest.makeSuite(GeometryFieldTest)) + return s + +def run(verbosity=2): + unittest.TextTestRunner(verbosity=verbosity).run(suite()) + +if __name__=="__main__": + run() diff --git a/parts/django/django/contrib/gis/tests/test_geoip.py b/parts/django/django/contrib/gis/tests/test_geoip.py new file mode 100644 index 0000000..a9ab6a6 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/test_geoip.py @@ -0,0 +1,103 @@ +import os, unittest +from django.db import settings +from django.contrib.gis.geos import GEOSGeometry +from django.contrib.gis.utils import GeoIP, GeoIPException + +# Note: Requires use of both the GeoIP country and city datasets. +# The GEOIP_DATA path should be the only setting set (the directory +# should contain links or the actual database files 'GeoIP.dat' and +# 'GeoLiteCity.dat'. +class GeoIPTest(unittest.TestCase): + + def test01_init(self): + "Testing GeoIP initialization." + g1 = GeoIP() # Everything inferred from GeoIP path + path = settings.GEOIP_PATH + g2 = GeoIP(path, 0) # Passing in data path explicitly. + g3 = GeoIP.open(path, 0) # MaxMind Python API syntax. + + for g in (g1, g2, g3): + self.assertEqual(True, bool(g._country)) + self.assertEqual(True, bool(g._city)) + + # Only passing in the location of one database. + city = os.path.join(path, 'GeoLiteCity.dat') + cntry = os.path.join(path, 'GeoIP.dat') + g4 = GeoIP(city, country='') + self.assertEqual(None, g4._country) + g5 = GeoIP(cntry, city='') + self.assertEqual(None, g5._city) + + # Improper parameters. + bad_params = (23, 'foo', 15.23) + for bad in bad_params: + self.assertRaises(GeoIPException, GeoIP, cache=bad) + if isinstance(bad, basestring): + e = GeoIPException + else: + e = TypeError + self.assertRaises(e, GeoIP, bad, 0) + + def test02_bad_query(self): + "Testing GeoIP query parameter checking." + cntry_g = GeoIP(city='<foo>') + # No city database available, these calls should fail. + self.assertRaises(GeoIPException, cntry_g.city, 'google.com') + self.assertRaises(GeoIPException, cntry_g.coords, 'yahoo.com') + + # Non-string query should raise TypeError + self.assertRaises(TypeError, cntry_g.country_code, 17) + self.assertRaises(TypeError, cntry_g.country_name, GeoIP) + + def test03_country(self): + "Testing GeoIP country querying methods." + g = GeoIP(city='<foo>') + + fqdn = 'www.google.com' + addr = '12.215.42.19' + + for query in (fqdn, addr): + for func in (g.country_code, g.country_code_by_addr, g.country_code_by_name): + self.assertEqual('US', func(query)) + for func in (g.country_name, g.country_name_by_addr, g.country_name_by_name): + self.assertEqual('United States', func(query)) + self.assertEqual({'country_code' : 'US', 'country_name' : 'United States'}, + g.country(query)) + + def test04_city(self): + "Testing GeoIP city querying methods." + g = GeoIP(country='<foo>') + + addr = '130.80.29.3' + fqdn = 'chron.com' + for query in (fqdn, addr): + # Country queries should still work. + for func in (g.country_code, g.country_code_by_addr, g.country_code_by_name): + self.assertEqual('US', func(query)) + for func in (g.country_name, g.country_name_by_addr, g.country_name_by_name): + self.assertEqual('United States', func(query)) + self.assertEqual({'country_code' : 'US', 'country_name' : 'United States'}, + g.country(query)) + + # City information dictionary. + d = g.city(query) + self.assertEqual('USA', d['country_code3']) + self.assertEqual('Houston', d['city']) + self.assertEqual('TX', d['region']) + self.assertEqual(713, d['area_code']) + geom = g.geos(query) + self.failIf(not isinstance(geom, GEOSGeometry)) + lon, lat = (-95.3670, 29.7523) + lat_lon = g.lat_lon(query) + lat_lon = (lat_lon[1], lat_lon[0]) + for tup in (geom.tuple, g.coords(query), g.lon_lat(query), lat_lon): + self.assertAlmostEqual(lon, tup[0], 4) + self.assertAlmostEqual(lat, tup[1], 4) + +def suite(): + s = unittest.TestSuite() + s.addTest(unittest.makeSuite(GeoIPTest)) + return s + +def run(verbosity=2): + unittest.TextTestRunner(verbosity=verbosity).run(suite()) diff --git a/parts/django/django/contrib/gis/tests/test_measure.py b/parts/django/django/contrib/gis/tests/test_measure.py new file mode 100644 index 0000000..28d5048 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/test_measure.py @@ -0,0 +1,336 @@ +""" +Distance and Area objects to allow for sensible and convienient calculation +and conversions. Here are some tests. +""" + +import unittest +from django.contrib.gis.measure import Distance, Area, D, A + +class DistanceTest(unittest.TestCase): + "Testing the Distance object" + + def testInit(self): + "Testing initialisation from valid units" + d = Distance(m=100) + self.assertEqual(d.m, 100) + + d1, d2, d3 = D(m=100), D(meter=100), D(metre=100) + for d in (d1, d2, d3): + self.assertEqual(d.m, 100) + + d = D(nm=100) + self.assertEqual(d.m, 185200) + + y1, y2, y3 = D(yd=100), D(yard=100), D(Yard=100) + for d in (y1, y2, y3): + self.assertEqual(d.yd, 100) + + mm1, mm2 = D(millimeter=1000), D(MiLLiMeTeR=1000) + for d in (mm1, mm2): + self.assertEqual(d.m, 1.0) + self.assertEqual(d.mm, 1000.0) + + + def testInitInvalid(self): + "Testing initialisation from invalid units" + self.assertRaises(AttributeError, D, banana=100) + + def testAccess(self): + "Testing access in different units" + d = D(m=100) + self.assertEqual(d.km, 0.1) + self.assertAlmostEqual(d.ft, 328.084, 3) + + def testAccessInvalid(self): + "Testing access in invalid units" + d = D(m=100) + self.failIf(hasattr(d, 'banana')) + + def testAddition(self): + "Test addition & subtraction" + d1 = D(m=100) + d2 = D(m=200) + + d3 = d1 + d2 + self.assertEqual(d3.m, 300) + d3 += d1 + self.assertEqual(d3.m, 400) + + d4 = d1 - d2 + self.assertEqual(d4.m, -100) + d4 -= d1 + self.assertEqual(d4.m, -200) + + try: + d5 = d1 + 1 + except TypeError, e: + pass + else: + self.fail('Distance + number should raise TypeError') + + try: + d5 = d1 - 1 + except TypeError, e: + pass + else: + self.fail('Distance - number should raise TypeError') + + try: + d1 += 1 + except TypeError, e: + pass + else: + self.fail('Distance += number should raise TypeError') + + try: + d1 -= 1 + except TypeError, e: + pass + else: + self.fail('Distance -= number should raise TypeError') + + def testMultiplication(self): + "Test multiplication & division" + d1 = D(m=100) + + d3 = d1 * 2 + self.assertEqual(d3.m, 200) + d3 = 2 * d1 + self.assertEqual(d3.m, 200) + d3 *= 5 + self.assertEqual(d3.m, 1000) + + d4 = d1 / 2 + self.assertEqual(d4.m, 50) + d4 /= 5 + self.assertEqual(d4.m, 10) + + a5 = d1 * D(m=10) + self.assert_(isinstance(a5, Area)) + self.assertEqual(a5.sq_m, 100*10) + + try: + d1 *= D(m=1) + except TypeError, e: + pass + else: + self.fail('Distance *= Distance should raise TypeError') + + try: + d5 = d1 / D(m=1) + except TypeError, e: + pass + else: + self.fail('Distance / Distance should raise TypeError') + + try: + d1 /= D(m=1) + except TypeError, e: + pass + else: + self.fail('Distance /= Distance should raise TypeError') + + def testUnitConversions(self): + "Testing default units during maths" + d1 = D(m=100) + d2 = D(km=1) + + d3 = d1 + d2 + self.assertEqual(d3._default_unit, 'm') + d4 = d2 + d1 + self.assertEqual(d4._default_unit, 'km') + d5 = d1 * 2 + self.assertEqual(d5._default_unit, 'm') + d6 = d1 / 2 + self.assertEqual(d6._default_unit, 'm') + + def testComparisons(self): + "Testing comparisons" + d1 = D(m=100) + d2 = D(km=1) + d3 = D(km=0) + + self.assert_(d2 > d1) + self.assert_(d1 == d1) + self.assert_(d1 < d2) + self.failIf(d3) + + def testUnitsStr(self): + "Testing conversion to strings" + d1 = D(m=100) + d2 = D(km=3.5) + + self.assertEqual(str(d1), '100.0 m') + self.assertEqual(str(d2), '3.5 km') + self.assertEqual(repr(d1), 'Distance(m=100.0)') + self.assertEqual(repr(d2), 'Distance(km=3.5)') + + def testUnitAttName(self): + "Testing the `unit_attname` class method" + unit_tuple = [('Yard', 'yd'), ('Nautical Mile', 'nm'), ('German legal metre', 'german_m'), + ('Indian yard', 'indian_yd'), ('Chain (Sears)', 'chain_sears'), ('Chain', 'chain')] + for nm, att in unit_tuple: + self.assertEqual(att, D.unit_attname(nm)) + +class AreaTest(unittest.TestCase): + "Testing the Area object" + + def testInit(self): + "Testing initialisation from valid units" + a = Area(sq_m=100) + self.assertEqual(a.sq_m, 100) + + a = A(sq_m=100) + self.assertEqual(a.sq_m, 100) + + a = A(sq_mi=100) + self.assertEqual(a.sq_m, 258998811.0336) + + def testInitInvaliA(self): + "Testing initialisation from invalid units" + self.assertRaises(AttributeError, A, banana=100) + + def testAccess(self): + "Testing access in different units" + a = A(sq_m=100) + self.assertEqual(a.sq_km, 0.0001) + self.assertAlmostEqual(a.sq_ft, 1076.391, 3) + + def testAccessInvaliA(self): + "Testing access in invalid units" + a = A(sq_m=100) + self.failIf(hasattr(a, 'banana')) + + def testAddition(self): + "Test addition & subtraction" + a1 = A(sq_m=100) + a2 = A(sq_m=200) + + a3 = a1 + a2 + self.assertEqual(a3.sq_m, 300) + a3 += a1 + self.assertEqual(a3.sq_m, 400) + + a4 = a1 - a2 + self.assertEqual(a4.sq_m, -100) + a4 -= a1 + self.assertEqual(a4.sq_m, -200) + + try: + a5 = a1 + 1 + except TypeError, e: + pass + else: + self.fail('Area + number should raise TypeError') + + try: + a5 = a1 - 1 + except TypeError, e: + pass + else: + self.fail('Area - number should raise TypeError') + + try: + a1 += 1 + except TypeError, e: + pass + else: + self.fail('Area += number should raise TypeError') + + try: + a1 -= 1 + except TypeError, e: + pass + else: + self.fail('Area -= number should raise TypeError') + + def testMultiplication(self): + "Test multiplication & division" + a1 = A(sq_m=100) + + a3 = a1 * 2 + self.assertEqual(a3.sq_m, 200) + a3 = 2 * a1 + self.assertEqual(a3.sq_m, 200) + a3 *= 5 + self.assertEqual(a3.sq_m, 1000) + + a4 = a1 / 2 + self.assertEqual(a4.sq_m, 50) + a4 /= 5 + self.assertEqual(a4.sq_m, 10) + + try: + a5 = a1 * A(sq_m=1) + except TypeError, e: + pass + else: + self.fail('Area * Area should raise TypeError') + + try: + a1 *= A(sq_m=1) + except TypeError, e: + pass + else: + self.fail('Area *= Area should raise TypeError') + + try: + a5 = a1 / A(sq_m=1) + except TypeError, e: + pass + else: + self.fail('Area / Area should raise TypeError') + + try: + a1 /= A(sq_m=1) + except TypeError, e: + pass + else: + self.fail('Area /= Area should raise TypeError') + + def testUnitConversions(self): + "Testing default units during maths" + a1 = A(sq_m=100) + a2 = A(sq_km=1) + + a3 = a1 + a2 + self.assertEqual(a3._default_unit, 'sq_m') + a4 = a2 + a1 + self.assertEqual(a4._default_unit, 'sq_km') + a5 = a1 * 2 + self.assertEqual(a5._default_unit, 'sq_m') + a6 = a1 / 2 + self.assertEqual(a6._default_unit, 'sq_m') + + def testComparisons(self): + "Testing comparisons" + a1 = A(sq_m=100) + a2 = A(sq_km=1) + a3 = A(sq_km=0) + + self.assert_(a2 > a1) + self.assert_(a1 == a1) + self.assert_(a1 < a2) + self.failIf(a3) + + def testUnitsStr(self): + "Testing conversion to strings" + a1 = A(sq_m=100) + a2 = A(sq_km=3.5) + + self.assertEqual(str(a1), '100.0 sq_m') + self.assertEqual(str(a2), '3.5 sq_km') + self.assertEqual(repr(a1), 'Area(sq_m=100.0)') + self.assertEqual(repr(a2), 'Area(sq_km=3.5)') + +def suite(): + s = unittest.TestSuite() + s.addTest(unittest.makeSuite(DistanceTest)) + s.addTest(unittest.makeSuite(AreaTest)) + return s + +def run(verbosity=2): + unittest.TextTestRunner(verbosity=verbosity).run(suite()) + +if __name__=="__main__": + run() diff --git a/parts/django/django/contrib/gis/tests/test_spatialrefsys.py b/parts/django/django/contrib/gis/tests/test_spatialrefsys.py new file mode 100644 index 0000000..a9fcbff --- /dev/null +++ b/parts/django/django/contrib/gis/tests/test_spatialrefsys.py @@ -0,0 +1,113 @@ +import unittest + +from django.db import connection +from django.contrib.gis.tests.utils import mysql, no_mysql, oracle, postgis, spatialite + +test_srs = ({'srid' : 4326, + 'auth_name' : ('EPSG', True), + 'auth_srid' : 4326, + 'srtext' : '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"]]', + 'srtext14' : 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]', + 'proj4' : '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs ', + 'spheroid' : 'WGS 84', 'name' : 'WGS 84', + 'geographic' : True, 'projected' : False, 'spatialite' : True, + 'ellipsoid' : (6378137.0, 6356752.3, 298.257223563), # From proj's "cs2cs -le" and Wikipedia (semi-minor only) + 'eprec' : (1, 1, 9), + }, + {'srid' : 32140, + 'auth_name' : ('EPSG', False), + 'auth_srid' : 32140, + 'srtext' : 'PROJCS["NAD83 / Texas South Central",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",30.28333333333333],PARAMETER["standard_parallel_2",28.38333333333333],PARAMETER["latitude_of_origin",27.83333333333333],PARAMETER["central_meridian",-99],PARAMETER["false_easting",600000],PARAMETER["false_northing",4000000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AUTHORITY["EPSG","32140"]]', + 'srtext14': 'PROJCS["NAD83 / Texas South Central",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",30.28333333333333],PARAMETER["standard_parallel_2",28.38333333333333],PARAMETER["latitude_of_origin",27.83333333333333],PARAMETER["central_meridian",-99],PARAMETER["false_easting",600000],PARAMETER["false_northing",4000000],AUTHORITY["EPSG","32140"],AXIS["X",EAST],AXIS["Y",NORTH]]', + 'proj4' : '+proj=lcc +lat_1=30.28333333333333 +lat_2=28.38333333333333 +lat_0=27.83333333333333 +lon_0=-99 +x_0=600000 +y_0=4000000 +ellps=GRS80 +datum=NAD83 +units=m +no_defs ', + 'spheroid' : 'GRS 1980', 'name' : 'NAD83 / Texas South Central', + 'geographic' : False, 'projected' : True, 'spatialite' : False, + 'ellipsoid' : (6378137.0, 6356752.31414, 298.257222101), # From proj's "cs2cs -le" and Wikipedia (semi-minor only) + 'eprec' : (1, 5, 10), + }, + ) + +if oracle: + from django.contrib.gis.db.backends.oracle.models import SpatialRefSys +elif postgis: + from django.contrib.gis.db.backends.postgis.models import SpatialRefSys +elif spatialite: + from django.contrib.gis.db.backends.spatialite.models import SpatialRefSys + +class SpatialRefSysTest(unittest.TestCase): + + @no_mysql + def test01_retrieve(self): + "Testing retrieval of SpatialRefSys model objects." + for sd in test_srs: + srs = SpatialRefSys.objects.get(srid=sd['srid']) + self.assertEqual(sd['srid'], srs.srid) + + # Some of the authority names are borked on Oracle, e.g., SRID=32140. + # also, Oracle Spatial seems to add extraneous info to fields, hence the + # the testing with the 'startswith' flag. + auth_name, oracle_flag = sd['auth_name'] + if postgis or (oracle and oracle_flag): + self.assertEqual(True, srs.auth_name.startswith(auth_name)) + + self.assertEqual(sd['auth_srid'], srs.auth_srid) + + # No proj.4 and different srtext on oracle backends :( + if postgis: + if connection.ops.spatial_version >= (1, 4, 0): + srtext = sd['srtext14'] + else: + srtext = sd['srtext'] + self.assertEqual(srtext, srs.wkt) + self.assertEqual(sd['proj4'], srs.proj4text) + + @no_mysql + def test02_osr(self): + "Testing getting OSR objects from SpatialRefSys model objects." + for sd in test_srs: + sr = SpatialRefSys.objects.get(srid=sd['srid']) + self.assertEqual(True, sr.spheroid.startswith(sd['spheroid'])) + self.assertEqual(sd['geographic'], sr.geographic) + self.assertEqual(sd['projected'], sr.projected) + + if not (spatialite and not sd['spatialite']): + # Can't get 'NAD83 / Texas South Central' from PROJ.4 string + # on SpatiaLite + self.assertEqual(True, sr.name.startswith(sd['name'])) + + # Testing the SpatialReference object directly. + if postgis or spatialite: + srs = sr.srs + self.assertEqual(sd['proj4'], srs.proj4) + # No `srtext` field in the `spatial_ref_sys` table in SpatiaLite + if not spatialite: + if connection.ops.spatial_version >= (1, 4, 0): + srtext = sd['srtext14'] + else: + srtext = sd['srtext'] + self.assertEqual(srtext, srs.wkt) + + @no_mysql + def test03_ellipsoid(self): + "Testing the ellipsoid property." + for sd in test_srs: + # Getting the ellipsoid and precision parameters. + ellps1 = sd['ellipsoid'] + prec = sd['eprec'] + + # Getting our spatial reference and its ellipsoid + srs = SpatialRefSys.objects.get(srid=sd['srid']) + ellps2 = srs.ellipsoid + + for i in range(3): + param1 = ellps1[i] + param2 = ellps2[i] + self.assertAlmostEqual(ellps1[i], ellps2[i], prec[i]) + +def suite(): + s = unittest.TestSuite() + s.addTest(unittest.makeSuite(SpatialRefSysTest)) + return s + +def run(verbosity=2): + unittest.TextTestRunner(verbosity=verbosity).run(suite()) diff --git a/parts/django/django/contrib/gis/tests/utils.py b/parts/django/django/contrib/gis/tests/utils.py new file mode 100644 index 0000000..b758fd0 --- /dev/null +++ b/parts/django/django/contrib/gis/tests/utils.py @@ -0,0 +1,26 @@ +from django.conf import settings +from django.db import DEFAULT_DB_ALIAS + +# function that will pass a test. +def pass_test(*args): return + +def no_backend(test_func, backend): + "Use this decorator to disable test on specified backend." + if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'].rsplit('.')[-1] == backend: + return pass_test + else: + return test_func + +# Decorators to disable entire test functions for specific +# spatial backends. +def no_oracle(func): return no_backend(func, 'oracle') +def no_postgis(func): return no_backend(func, 'postgis') +def no_mysql(func): return no_backend(func, 'mysql') +def no_spatialite(func): return no_backend(func, 'spatialite') + +# Shortcut booleans to omit only portions of tests. +_default_db = settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'].rsplit('.')[-1] +oracle = _default_db == 'oracle' +postgis = _default_db == 'postgis' +mysql = _default_db == 'mysql' +spatialite = _default_db == 'spatialite' diff --git a/parts/django/django/contrib/gis/utils/__init__.py b/parts/django/django/contrib/gis/utils/__init__.py new file mode 100644 index 0000000..1cff4d9 --- /dev/null +++ b/parts/django/django/contrib/gis/utils/__init__.py @@ -0,0 +1,25 @@ +""" + This module contains useful utilities for GeoDjango. +""" +# Importing the utilities that depend on GDAL, if available. +from django.contrib.gis.gdal import HAS_GDAL +if HAS_GDAL: + from django.contrib.gis.utils.ogrinfo import ogrinfo, sample + from django.contrib.gis.utils.ogrinspect import mapping, ogrinspect + from django.contrib.gis.utils.srs import add_postgis_srs, add_srs_entry + try: + # LayerMapping requires DJANGO_SETTINGS_MODULE to be set, + # so this needs to be in try/except. + from django.contrib.gis.utils.layermapping import LayerMapping, LayerMapError + except: + pass + +# Attempting to import the GeoIP class. +try: + from django.contrib.gis.utils.geoip import GeoIP, GeoIPException + HAS_GEOIP = True +except: + HAS_GEOIP = False + +from django.contrib.gis.utils.wkt import precision_wkt + diff --git a/parts/django/django/contrib/gis/utils/geoip.py b/parts/django/django/contrib/gis/utils/geoip.py new file mode 100644 index 0000000..eedaef9 --- /dev/null +++ b/parts/django/django/contrib/gis/utils/geoip.py @@ -0,0 +1,361 @@ +""" + This module houses the GeoIP object, a ctypes wrapper for the MaxMind GeoIP(R) + C API (http://www.maxmind.com/app/c). This is an alternative to the GPL + licensed Python GeoIP interface provided by MaxMind. + + GeoIP(R) is a registered trademark of MaxMind, LLC of Boston, Massachusetts. + + For IP-based geolocation, this module requires the GeoLite Country and City + datasets, in binary format (CSV will not work!). The datasets may be + downloaded from MaxMind at http://www.maxmind.com/download/geoip/database/. + Grab GeoIP.dat.gz and GeoLiteCity.dat.gz, and unzip them in the directory + corresponding to settings.GEOIP_PATH. See the GeoIP docstring and examples + below for more details. + + TODO: Verify compatibility with Windows. + + Example: + + >>> from django.contrib.gis.utils import GeoIP + >>> g = GeoIP() + >>> g.country('google.com') + {'country_code': 'US', 'country_name': 'United States'} + >>> g.city('72.14.207.99') + {'area_code': 650, + 'city': 'Mountain View', + 'country_code': 'US', + 'country_code3': 'USA', + 'country_name': 'United States', + 'dma_code': 807, + 'latitude': 37.419200897216797, + 'longitude': -122.05740356445312, + 'postal_code': '94043', + 'region': 'CA'} + >>> g.lat_lon('salon.com') + (37.789798736572266, -122.39420318603516) + >>> g.lon_lat('uh.edu') + (-95.415199279785156, 29.77549934387207) + >>> g.geos('24.124.1.80').wkt + 'POINT (-95.2087020874023438 39.0392990112304688)' +""" +import os, re +from ctypes import c_char_p, c_float, c_int, Structure, CDLL, POINTER +from ctypes.util import find_library +from django.conf import settings +if not settings.configured: settings.configure() + +# Creating the settings dictionary with any settings, if needed. +GEOIP_SETTINGS = dict((key, getattr(settings, key)) + for key in ('GEOIP_PATH', 'GEOIP_LIBRARY_PATH', 'GEOIP_COUNTRY', 'GEOIP_CITY') + if hasattr(settings, key)) +lib_path = GEOIP_SETTINGS.get('GEOIP_LIBRARY_PATH', None) + +# GeoIP Exception class. +class GeoIPException(Exception): pass + +# The shared library for the GeoIP C API. May be downloaded +# from http://www.maxmind.com/download/geoip/api/c/ +if lib_path: + lib_name = None +else: + # TODO: Is this really the library name for Windows? + lib_name = 'GeoIP' + +# Getting the path to the GeoIP library. +if lib_name: lib_path = find_library(lib_name) +if lib_path is None: raise GeoIPException('Could not find the GeoIP library (tried "%s"). ' + 'Try setting GEOIP_LIBRARY_PATH in your settings.' % lib_name) +lgeoip = CDLL(lib_path) + +# Regular expressions for recognizing IP addresses and the GeoIP +# free database editions. +ipregex = re.compile(r'^(?P<w>\d\d?\d?)\.(?P<x>\d\d?\d?)\.(?P<y>\d\d?\d?)\.(?P<z>\d\d?\d?)$') +free_regex = re.compile(r'^GEO-\d{3}FREE') +lite_regex = re.compile(r'^GEO-\d{3}LITE') + +#### GeoIP C Structure definitions #### +class GeoIPRecord(Structure): + _fields_ = [('country_code', c_char_p), + ('country_code3', c_char_p), + ('country_name', c_char_p), + ('region', c_char_p), + ('city', c_char_p), + ('postal_code', c_char_p), + ('latitude', c_float), + ('longitude', c_float), + # TODO: In 1.4.6 this changed from `int dma_code;` to + # `union {int metro_code; int dma_code;};`. Change + # to a `ctypes.Union` in to accomodate in future when + # pre-1.4.6 versions are no longer distributed. + ('dma_code', c_int), + ('area_code', c_int), + # TODO: The following structure fields were added in 1.4.3 -- + # uncomment these fields when sure previous versions are no + # longer distributed by package maintainers. + #('charset', c_int), + #('continent_code', c_char_p), + ] +class GeoIPTag(Structure): pass + +#### ctypes function prototypes #### +RECTYPE = POINTER(GeoIPRecord) +DBTYPE = POINTER(GeoIPTag) + +# For retrieving records by name or address. +def record_output(func): + func.restype = RECTYPE + return func +rec_by_addr = record_output(lgeoip.GeoIP_record_by_addr) +rec_by_name = record_output(lgeoip.GeoIP_record_by_name) + +# For opening & closing GeoIP database files. +geoip_open = lgeoip.GeoIP_open +geoip_open.restype = DBTYPE +geoip_close = lgeoip.GeoIP_delete +geoip_close.argtypes = [DBTYPE] +geoip_close.restype = None + +# String output routines. +def string_output(func): + func.restype = c_char_p + return func +geoip_dbinfo = string_output(lgeoip.GeoIP_database_info) +cntry_code_by_addr = string_output(lgeoip.GeoIP_country_code_by_addr) +cntry_code_by_name = string_output(lgeoip.GeoIP_country_code_by_name) +cntry_name_by_addr = string_output(lgeoip.GeoIP_country_name_by_addr) +cntry_name_by_name = string_output(lgeoip.GeoIP_country_name_by_name) + +#### GeoIP class #### +class GeoIP(object): + # The flags for GeoIP memory caching. + # GEOIP_STANDARD - read database from filesystem, uses least memory. + # + # GEOIP_MEMORY_CACHE - load database into memory, faster performance + # but uses more memory + # + # GEOIP_CHECK_CACHE - check for updated database. If database has been updated, + # reload filehandle and/or memory cache. + # + # GEOIP_INDEX_CACHE - just cache + # the most frequently accessed index portion of the database, resulting + # in faster lookups than GEOIP_STANDARD, but less memory usage than + # GEOIP_MEMORY_CACHE - useful for larger databases such as + # GeoIP Organization and GeoIP City. Note, for GeoIP Country, Region + # and Netspeed databases, GEOIP_INDEX_CACHE is equivalent to GEOIP_MEMORY_CACHE + # + GEOIP_STANDARD = 0 + GEOIP_MEMORY_CACHE = 1 + GEOIP_CHECK_CACHE = 2 + GEOIP_INDEX_CACHE = 4 + cache_options = dict((opt, None) for opt in (0, 1, 2, 4)) + _city_file = '' + _country_file = '' + + # Initially, pointers to GeoIP file references are NULL. + _city = None + _country = None + + def __init__(self, path=None, cache=0, country=None, city=None): + """ + Initializes the GeoIP object, no parameters are required to use default + settings. Keyword arguments may be passed in to customize the locations + of the GeoIP data sets. + + * path: Base directory to where GeoIP data is located or the full path + to where the city or country data files (*.dat) are located. + Assumes that both the city and country data sets are located in + this directory; overrides the GEOIP_PATH settings attribute. + + * cache: The cache settings when opening up the GeoIP datasets, + and may be an integer in (0, 1, 2, 4) corresponding to + the GEOIP_STANDARD, GEOIP_MEMORY_CACHE, GEOIP_CHECK_CACHE, + and GEOIP_INDEX_CACHE `GeoIPOptions` C API settings, + respectively. Defaults to 0, meaning that the data is read + from the disk. + + * country: The name of the GeoIP country data file. Defaults to + 'GeoIP.dat'; overrides the GEOIP_COUNTRY settings attribute. + + * city: The name of the GeoIP city data file. Defaults to + 'GeoLiteCity.dat'; overrides the GEOIP_CITY settings attribute. + """ + # Checking the given cache option. + if cache in self.cache_options: + self._cache = self.cache_options[cache] + else: + raise GeoIPException('Invalid caching option: %s' % cache) + + # Getting the GeoIP data path. + if not path: + path = GEOIP_SETTINGS.get('GEOIP_PATH', None) + if not path: raise GeoIPException('GeoIP path must be provided via parameter or the GEOIP_PATH setting.') + if not isinstance(path, basestring): + raise TypeError('Invalid path type: %s' % type(path).__name__) + + if os.path.isdir(path): + # Constructing the GeoIP database filenames using the settings + # dictionary. If the database files for the GeoLite country + # and/or city datasets exist, then try and open them. + country_db = os.path.join(path, country or GEOIP_SETTINGS.get('GEOIP_COUNTRY', 'GeoIP.dat')) + if os.path.isfile(country_db): + self._country = geoip_open(country_db, cache) + self._country_file = country_db + + city_db = os.path.join(path, city or GEOIP_SETTINGS.get('GEOIP_CITY', 'GeoLiteCity.dat')) + if os.path.isfile(city_db): + self._city = geoip_open(city_db, cache) + self._city_file = city_db + elif os.path.isfile(path): + # Otherwise, some detective work will be needed to figure + # out whether the given database path is for the GeoIP country + # or city databases. + ptr = geoip_open(path, cache) + info = geoip_dbinfo(ptr) + if lite_regex.match(info): + # GeoLite City database detected. + self._city = ptr + self._city_file = path + elif free_regex.match(info): + # GeoIP Country database detected. + self._country = ptr + self._country_file = path + else: + raise GeoIPException('Unable to recognize database edition: %s' % info) + else: + raise GeoIPException('GeoIP path must be a valid file or directory.') + + def __del__(self): + # Cleaning any GeoIP file handles lying around. + if self._country: geoip_close(self._country) + if self._city: geoip_close(self._city) + + def _check_query(self, query, country=False, city=False, city_or_country=False): + "Helper routine for checking the query and database availability." + # Making sure a string was passed in for the query. + if not isinstance(query, basestring): + raise TypeError('GeoIP query must be a string, not type %s' % type(query).__name__) + + # Extra checks for the existence of country and city databases. + if city_or_country and not (self._country or self._city): + raise GeoIPException('Invalid GeoIP country and city data files.') + elif country and not self._country: + raise GeoIPException('Invalid GeoIP country data file: %s' % self._country_file) + elif city and not self._city: + raise GeoIPException('Invalid GeoIP city data file: %s' % self._city_file) + + def city(self, query): + """ + Returns a dictionary of city information for the given IP address or + Fully Qualified Domain Name (FQDN). Some information in the dictionary + may be undefined (None). + """ + self._check_query(query, city=True) + if ipregex.match(query): + # If an IP address was passed in + ptr = rec_by_addr(self._city, c_char_p(query)) + else: + # If a FQDN was passed in. + ptr = rec_by_name(self._city, c_char_p(query)) + + # Checking the pointer to the C structure, if valid pull out elements + # into a dicionary and return. + if bool(ptr): + record = ptr.contents + return dict((tup[0], getattr(record, tup[0])) for tup in record._fields_) + else: + return None + + def country_code(self, query): + "Returns the country code for the given IP Address or FQDN." + self._check_query(query, city_or_country=True) + if self._country: + if ipregex.match(query): return cntry_code_by_addr(self._country, query) + else: return cntry_code_by_name(self._country, query) + else: + return self.city(query)['country_code'] + + def country_name(self, query): + "Returns the country name for the given IP Address or FQDN." + self._check_query(query, city_or_country=True) + if self._country: + if ipregex.match(query): return cntry_name_by_addr(self._country, query) + else: return cntry_name_by_name(self._country, query) + else: + return self.city(query)['country_name'] + + def country(self, query): + """ + Returns a dictonary with with the country code and name when given an + IP address or a Fully Qualified Domain Name (FQDN). For example, both + '24.124.1.80' and 'djangoproject.com' are valid parameters. + """ + # Returning the country code and name + return {'country_code' : self.country_code(query), + 'country_name' : self.country_name(query), + } + + #### Coordinate retrieval routines #### + def coords(self, query, ordering=('longitude', 'latitude')): + cdict = self.city(query) + if cdict is None: return None + else: return tuple(cdict[o] for o in ordering) + + def lon_lat(self, query): + "Returns a tuple of the (longitude, latitude) for the given query." + return self.coords(query) + + def lat_lon(self, query): + "Returns a tuple of the (latitude, longitude) for the given query." + return self.coords(query, ('latitude', 'longitude')) + + def geos(self, query): + "Returns a GEOS Point object for the given query." + ll = self.lon_lat(query) + if ll: + from django.contrib.gis.geos import Point + return Point(ll, srid=4326) + else: + return None + + #### GeoIP Database Information Routines #### + def country_info(self): + "Returns information about the GeoIP country database." + if self._country is None: + ci = 'No GeoIP Country data in "%s"' % self._country_file + else: + ci = geoip_dbinfo(self._country) + return ci + country_info = property(country_info) + + def city_info(self): + "Retuns information about the GeoIP city database." + if self._city is None: + ci = 'No GeoIP City data in "%s"' % self._city_file + else: + ci = geoip_dbinfo(self._city) + return ci + city_info = property(city_info) + + def info(self): + "Returns information about all GeoIP databases in use." + return 'Country:\n\t%s\nCity:\n\t%s' % (self.country_info, self.city_info) + info = property(info) + + #### Methods for compatibility w/the GeoIP-Python API. #### + @classmethod + def open(cls, full_path, cache): + return GeoIP(full_path, cache) + + def _rec_by_arg(self, arg): + if self._city: + return self.city(arg) + else: + return self.country(arg) + region_by_addr = city + region_by_name = city + record_by_addr = _rec_by_arg + record_by_name = _rec_by_arg + country_code_by_addr = country_code + country_code_by_name = country_code + country_name_by_addr = country_name + country_name_by_name = country_name diff --git a/parts/django/django/contrib/gis/utils/layermapping.py b/parts/django/django/contrib/gis/utils/layermapping.py new file mode 100644 index 0000000..cec1989 --- /dev/null +++ b/parts/django/django/contrib/gis/utils/layermapping.py @@ -0,0 +1,602 @@ +# LayerMapping -- A Django Model/OGR Layer Mapping Utility +""" + The LayerMapping class provides a way to map the contents of OGR + vector files (e.g. SHP files) to Geographic-enabled Django models. + + For more information, please consult the GeoDjango documentation: + http://geodjango.org/docs/layermapping.html +""" +import sys +from datetime import date, datetime +from decimal import Decimal +from django.core.exceptions import ObjectDoesNotExist +from django.db import connections, DEFAULT_DB_ALIAS +from django.contrib.gis.db.models import GeometryField +from django.contrib.gis.gdal import CoordTransform, DataSource, \ + OGRException, OGRGeometry, OGRGeomType, SpatialReference +from django.contrib.gis.gdal.field import \ + OFTDate, OFTDateTime, OFTInteger, OFTReal, OFTString, OFTTime +from django.db import models, transaction +from django.contrib.localflavor.us.models import USStateField + +# LayerMapping exceptions. +class LayerMapError(Exception): pass +class InvalidString(LayerMapError): pass +class InvalidDecimal(LayerMapError): pass +class InvalidInteger(LayerMapError): pass +class MissingForeignKey(LayerMapError): pass + +class LayerMapping(object): + "A class that maps OGR Layers to GeoDjango Models." + + # Acceptable 'base' types for a multi-geometry type. + MULTI_TYPES = {1 : OGRGeomType('MultiPoint'), + 2 : OGRGeomType('MultiLineString'), + 3 : OGRGeomType('MultiPolygon'), + OGRGeomType('Point25D').num : OGRGeomType('MultiPoint25D'), + OGRGeomType('LineString25D').num : OGRGeomType('MultiLineString25D'), + OGRGeomType('Polygon25D').num : OGRGeomType('MultiPolygon25D'), + } + + # Acceptable Django field types and corresponding acceptable OGR + # counterparts. + FIELD_TYPES = { + models.AutoField : OFTInteger, + models.IntegerField : (OFTInteger, OFTReal, OFTString), + models.FloatField : (OFTInteger, OFTReal), + models.DateField : OFTDate, + models.DateTimeField : OFTDateTime, + models.EmailField : OFTString, + models.TimeField : OFTTime, + models.DecimalField : (OFTInteger, OFTReal), + models.CharField : OFTString, + models.SlugField : OFTString, + models.TextField : OFTString, + models.URLField : OFTString, + USStateField : OFTString, + models.XMLField : OFTString, + models.SmallIntegerField : (OFTInteger, OFTReal, OFTString), + models.PositiveSmallIntegerField : (OFTInteger, OFTReal, OFTString), + } + + # The acceptable transaction modes. + TRANSACTION_MODES = {'autocommit' : transaction.autocommit, + 'commit_on_success' : transaction.commit_on_success, + } + + def __init__(self, model, data, mapping, layer=0, + source_srs=None, encoding=None, + transaction_mode='commit_on_success', + transform=True, unique=None, using=DEFAULT_DB_ALIAS): + """ + A LayerMapping object is initialized using the given Model (not an instance), + a DataSource (or string path to an OGR-supported data file), and a mapping + dictionary. See the module level docstring for more details and keyword + argument usage. + """ + # Getting the DataSource and the associated Layer. + if isinstance(data, basestring): + self.ds = DataSource(data) + else: + self.ds = data + self.layer = self.ds[layer] + + self.using = using + self.spatial_backend = connections[using].ops + + # Setting the mapping & model attributes. + self.mapping = mapping + self.model = model + + # Checking the layer -- intitialization of the object will fail if + # things don't check out before hand. + self.check_layer() + + # Getting the geometry column associated with the model (an + # exception will be raised if there is no geometry column). + if self.spatial_backend.mysql: + transform = False + else: + self.geo_field = self.geometry_field() + + # Checking the source spatial reference system, and getting + # the coordinate transformation object (unless the `transform` + # keyword is set to False) + if transform: + self.source_srs = self.check_srs(source_srs) + self.transform = self.coord_transform() + else: + self.transform = transform + + # Setting the encoding for OFTString fields, if specified. + if encoding: + # Making sure the encoding exists, if not a LookupError + # exception will be thrown. + from codecs import lookup + lookup(encoding) + self.encoding = encoding + else: + self.encoding = None + + if unique: + self.check_unique(unique) + transaction_mode = 'autocommit' # Has to be set to autocommit. + self.unique = unique + else: + self.unique = None + + # Setting the transaction decorator with the function in the + # transaction modes dictionary. + if transaction_mode in self.TRANSACTION_MODES: + self.transaction_decorator = self.TRANSACTION_MODES[transaction_mode] + self.transaction_mode = transaction_mode + else: + raise LayerMapError('Unrecognized transaction mode: %s' % transaction_mode) + + if using is None: + pass + + #### Checking routines used during initialization #### + def check_fid_range(self, fid_range): + "This checks the `fid_range` keyword." + if fid_range: + if isinstance(fid_range, (tuple, list)): + return slice(*fid_range) + elif isinstance(fid_range, slice): + return fid_range + else: + raise TypeError + else: + return None + + def check_layer(self): + """ + This checks the Layer metadata, and ensures that it is compatible + with the mapping information and model. Unlike previous revisions, + there is no need to increment through each feature in the Layer. + """ + # The geometry field of the model is set here. + # TODO: Support more than one geometry field / model. However, this + # depends on the GDAL Driver in use. + self.geom_field = False + self.fields = {} + + # Getting lists of the field names and the field types available in + # the OGR Layer. + ogr_fields = self.layer.fields + ogr_field_types = self.layer.field_types + + # Function for determining if the OGR mapping field is in the Layer. + def check_ogr_fld(ogr_map_fld): + try: + idx = ogr_fields.index(ogr_map_fld) + except ValueError: + raise LayerMapError('Given mapping OGR field "%s" not found in OGR Layer.' % ogr_map_fld) + return idx + + # No need to increment through each feature in the model, simply check + # the Layer metadata against what was given in the mapping dictionary. + for field_name, ogr_name in self.mapping.items(): + # Ensuring that a corresponding field exists in the model + # for the given field name in the mapping. + try: + model_field = self.model._meta.get_field(field_name) + except models.fields.FieldDoesNotExist: + raise LayerMapError('Given mapping field "%s" not in given Model fields.' % field_name) + + # Getting the string name for the Django field class (e.g., 'PointField'). + fld_name = model_field.__class__.__name__ + + if isinstance(model_field, GeometryField): + if self.geom_field: + raise LayerMapError('LayerMapping does not support more than one GeometryField per model.') + + # Getting the coordinate dimension of the geometry field. + coord_dim = model_field.dim + + try: + if coord_dim == 3: + gtype = OGRGeomType(ogr_name + '25D') + else: + gtype = OGRGeomType(ogr_name) + except OGRException: + raise LayerMapError('Invalid mapping for GeometryField "%s".' % field_name) + + # Making sure that the OGR Layer's Geometry is compatible. + ltype = self.layer.geom_type + if not (ltype.name.startswith(gtype.name) or self.make_multi(ltype, model_field)): + raise LayerMapError('Invalid mapping geometry; model has %s%s, ' + 'layer geometry type is %s.' % + (fld_name, (coord_dim == 3 and '(dim=3)') or '', ltype)) + + # Setting the `geom_field` attribute w/the name of the model field + # that is a Geometry. Also setting the coordinate dimension + # attribute. + self.geom_field = field_name + self.coord_dim = coord_dim + fields_val = model_field + elif isinstance(model_field, models.ForeignKey): + if isinstance(ogr_name, dict): + # Is every given related model mapping field in the Layer? + rel_model = model_field.rel.to + for rel_name, ogr_field in ogr_name.items(): + idx = check_ogr_fld(ogr_field) + try: + rel_field = rel_model._meta.get_field(rel_name) + except models.fields.FieldDoesNotExist: + raise LayerMapError('ForeignKey mapping field "%s" not in %s fields.' % + (rel_name, rel_model.__class__.__name__)) + fields_val = rel_model + else: + raise TypeError('ForeignKey mapping must be of dictionary type.') + else: + # Is the model field type supported by LayerMapping? + if not model_field.__class__ in self.FIELD_TYPES: + raise LayerMapError('Django field type "%s" has no OGR mapping (yet).' % fld_name) + + # Is the OGR field in the Layer? + idx = check_ogr_fld(ogr_name) + ogr_field = ogr_field_types[idx] + + # Can the OGR field type be mapped to the Django field type? + if not issubclass(ogr_field, self.FIELD_TYPES[model_field.__class__]): + raise LayerMapError('OGR field "%s" (of type %s) cannot be mapped to Django %s.' % + (ogr_field, ogr_field.__name__, fld_name)) + fields_val = model_field + + self.fields[field_name] = fields_val + + def check_srs(self, source_srs): + "Checks the compatibility of the given spatial reference object." + + if isinstance(source_srs, SpatialReference): + sr = source_srs + elif isinstance(source_srs, self.spatial_backend.spatial_ref_sys()): + sr = source_srs.srs + elif isinstance(source_srs, (int, basestring)): + sr = SpatialReference(source_srs) + else: + # Otherwise just pulling the SpatialReference from the layer + sr = self.layer.srs + + if not sr: + raise LayerMapError('No source reference system defined.') + else: + return sr + + def check_unique(self, unique): + "Checks the `unique` keyword parameter -- may be a sequence or string." + if isinstance(unique, (list, tuple)): + # List of fields to determine uniqueness with + for attr in unique: + if not attr in self.mapping: raise ValueError + elif isinstance(unique, basestring): + # Only a single field passed in. + if unique not in self.mapping: raise ValueError + else: + raise TypeError('Unique keyword argument must be set with a tuple, list, or string.') + + #### Keyword argument retrieval routines #### + def feature_kwargs(self, feat): + """ + Given an OGR Feature, this will return a dictionary of keyword arguments + for constructing the mapped model. + """ + # The keyword arguments for model construction. + kwargs = {} + + # Incrementing through each model field and OGR field in the + # dictionary mapping. + for field_name, ogr_name in self.mapping.items(): + model_field = self.fields[field_name] + + if isinstance(model_field, GeometryField): + # Verify OGR geometry. + val = self.verify_geom(feat.geom, model_field) + elif isinstance(model_field, models.base.ModelBase): + # The related _model_, not a field was passed in -- indicating + # another mapping for the related Model. + val = self.verify_fk(feat, model_field, ogr_name) + else: + # Otherwise, verify OGR Field type. + val = self.verify_ogr_field(feat[ogr_name], model_field) + + # Setting the keyword arguments for the field name with the + # value obtained above. + kwargs[field_name] = val + + return kwargs + + def unique_kwargs(self, kwargs): + """ + Given the feature keyword arguments (from `feature_kwargs`) this routine + will construct and return the uniqueness keyword arguments -- a subset + of the feature kwargs. + """ + if isinstance(self.unique, basestring): + return {self.unique : kwargs[self.unique]} + else: + return dict((fld, kwargs[fld]) for fld in self.unique) + + #### Verification routines used in constructing model keyword arguments. #### + def verify_ogr_field(self, ogr_field, model_field): + """ + Verifies if the OGR Field contents are acceptable to the Django + model field. If they are, the verified value is returned, + otherwise the proper exception is raised. + """ + if (isinstance(ogr_field, OFTString) and + isinstance(model_field, (models.CharField, models.TextField))): + if self.encoding: + # The encoding for OGR data sources may be specified here + # (e.g., 'cp437' for Census Bureau boundary files). + val = unicode(ogr_field.value, self.encoding) + else: + val = ogr_field.value + if len(val) > model_field.max_length: + raise InvalidString('%s model field maximum string length is %s, given %s characters.' % + (model_field.name, model_field.max_length, len(val))) + elif isinstance(ogr_field, OFTReal) and isinstance(model_field, models.DecimalField): + try: + # Creating an instance of the Decimal value to use. + d = Decimal(str(ogr_field.value)) + except: + raise InvalidDecimal('Could not construct decimal from: %s' % ogr_field.value) + + # Getting the decimal value as a tuple. + dtup = d.as_tuple() + digits = dtup[1] + d_idx = dtup[2] # index where the decimal is + + # Maximum amount of precision, or digits to the left of the decimal. + max_prec = model_field.max_digits - model_field.decimal_places + + # Getting the digits to the left of the decimal place for the + # given decimal. + if d_idx < 0: + n_prec = len(digits[:d_idx]) + else: + n_prec = len(digits) + d_idx + + # If we have more than the maximum digits allowed, then throw an + # InvalidDecimal exception. + if n_prec > max_prec: + raise InvalidDecimal('A DecimalField with max_digits %d, decimal_places %d must round to an absolute value less than 10^%d.' % + (model_field.max_digits, model_field.decimal_places, max_prec)) + val = d + elif isinstance(ogr_field, (OFTReal, OFTString)) and isinstance(model_field, models.IntegerField): + # Attempt to convert any OFTReal and OFTString value to an OFTInteger. + try: + val = int(ogr_field.value) + except: + raise InvalidInteger('Could not construct integer from: %s' % ogr_field.value) + else: + val = ogr_field.value + return val + + def verify_fk(self, feat, rel_model, rel_mapping): + """ + Given an OGR Feature, the related model and its dictionary mapping, + this routine will retrieve the related model for the ForeignKey + mapping. + """ + # TODO: It is expensive to retrieve a model for every record -- + # explore if an efficient mechanism exists for caching related + # ForeignKey models. + + # Constructing and verifying the related model keyword arguments. + fk_kwargs = {} + for field_name, ogr_name in rel_mapping.items(): + fk_kwargs[field_name] = self.verify_ogr_field(feat[ogr_name], rel_model._meta.get_field(field_name)) + + # Attempting to retrieve and return the related model. + try: + return rel_model.objects.get(**fk_kwargs) + except ObjectDoesNotExist: + raise MissingForeignKey('No ForeignKey %s model found with keyword arguments: %s' % (rel_model.__name__, fk_kwargs)) + + def verify_geom(self, geom, model_field): + """ + Verifies the geometry -- will construct and return a GeometryCollection + if necessary (for example if the model field is MultiPolygonField while + the mapped shapefile only contains Polygons). + """ + # Downgrade a 3D geom to a 2D one, if necessary. + if self.coord_dim != geom.coord_dim: + geom.coord_dim = self.coord_dim + + if self.make_multi(geom.geom_type, model_field): + # Constructing a multi-geometry type to contain the single geometry + multi_type = self.MULTI_TYPES[geom.geom_type.num] + g = OGRGeometry(multi_type) + g.add(geom) + else: + g = geom + + # Transforming the geometry with our Coordinate Transformation object, + # but only if the class variable `transform` is set w/a CoordTransform + # object. + if self.transform: g.transform(self.transform) + + # Returning the WKT of the geometry. + return g.wkt + + #### Other model methods #### + def coord_transform(self): + "Returns the coordinate transformation object." + SpatialRefSys = self.spatial_backend.spatial_ref_sys() + try: + # Getting the target spatial reference system + target_srs = SpatialRefSys.objects.get(srid=self.geo_field.srid).srs + + # Creating the CoordTransform object + return CoordTransform(self.source_srs, target_srs) + except Exception, msg: + raise LayerMapError('Could not translate between the data source and model geometry: %s' % msg) + + def geometry_field(self): + "Returns the GeometryField instance associated with the geographic column." + # Use the `get_field_by_name` on the model's options so that we + # get the correct field instance if there's model inheritance. + opts = self.model._meta + fld, model, direct, m2m = opts.get_field_by_name(self.geom_field) + return fld + + def make_multi(self, geom_type, model_field): + """ + Given the OGRGeomType for a geometry and its associated GeometryField, + determine whether the geometry should be turned into a GeometryCollection. + """ + return (geom_type.num in self.MULTI_TYPES and + model_field.__class__.__name__ == 'Multi%s' % geom_type.django) + + def save(self, verbose=False, fid_range=False, step=False, + progress=False, silent=False, stream=sys.stdout, strict=False): + """ + Saves the contents from the OGR DataSource Layer into the database + according to the mapping dictionary given at initialization. + + Keyword Parameters: + verbose: + If set, information will be printed subsequent to each model save + executed on the database. + + fid_range: + May be set with a slice or tuple of (begin, end) feature ID's to map + from the data source. In other words, this keyword enables the user + to selectively import a subset range of features in the geographic + data source. + + step: + If set with an integer, transactions will occur at every step + interval. For example, if step=1000, a commit would occur after + the 1,000th feature, the 2,000th feature etc. + + progress: + When this keyword is set, status information will be printed giving + the number of features processed and sucessfully saved. By default, + progress information will pe printed every 1000 features processed, + however, this default may be overridden by setting this keyword with an + integer for the desired interval. + + stream: + Status information will be written to this file handle. Defaults to + using `sys.stdout`, but any object with a `write` method is supported. + + silent: + By default, non-fatal error notifications are printed to stdout, but + this keyword may be set to disable these notifications. + + strict: + Execution of the model mapping will cease upon the first error + encountered. The default behavior is to attempt to continue. + """ + # Getting the default Feature ID range. + default_range = self.check_fid_range(fid_range) + + # Setting the progress interval, if requested. + if progress: + if progress is True or not isinstance(progress, int): + progress_interval = 1000 + else: + progress_interval = progress + + # Defining the 'real' save method, utilizing the transaction + # decorator created during initialization. + @self.transaction_decorator + def _save(feat_range=default_range, num_feat=0, num_saved=0): + if feat_range: + layer_iter = self.layer[feat_range] + else: + layer_iter = self.layer + + for feat in layer_iter: + num_feat += 1 + # Getting the keyword arguments + try: + kwargs = self.feature_kwargs(feat) + except LayerMapError, msg: + # Something borked the validation + if strict: raise + elif not silent: + stream.write('Ignoring Feature ID %s because: %s\n' % (feat.fid, msg)) + else: + # Constructing the model using the keyword args + is_update = False + if self.unique: + # If we want unique models on a particular field, handle the + # geometry appropriately. + try: + # Getting the keyword arguments and retrieving + # the unique model. + u_kwargs = self.unique_kwargs(kwargs) + m = self.model.objects.using(self.using).get(**u_kwargs) + is_update = True + + # Getting the geometry (in OGR form), creating + # one from the kwargs WKT, adding in additional + # geometries, and update the attribute with the + # just-updated geometry WKT. + geom = getattr(m, self.geom_field).ogr + new = OGRGeometry(kwargs[self.geom_field]) + for g in new: geom.add(g) + setattr(m, self.geom_field, geom.wkt) + except ObjectDoesNotExist: + # No unique model exists yet, create. + m = self.model(**kwargs) + else: + m = self.model(**kwargs) + + try: + # Attempting to save. + m.save(using=self.using) + num_saved += 1 + if verbose: stream.write('%s: %s\n' % (is_update and 'Updated' or 'Saved', m)) + except SystemExit: + raise + except Exception, msg: + if self.transaction_mode == 'autocommit': + # Rolling back the transaction so that other model saves + # will work. + transaction.rollback_unless_managed() + if strict: + # Bailing out if the `strict` keyword is set. + if not silent: + stream.write('Failed to save the feature (id: %s) into the model with the keyword arguments:\n' % feat.fid) + stream.write('%s\n' % kwargs) + raise + elif not silent: + stream.write('Failed to save %s:\n %s\nContinuing\n' % (kwargs, msg)) + + # Printing progress information, if requested. + if progress and num_feat % progress_interval == 0: + stream.write('Processed %d features, saved %d ...\n' % (num_feat, num_saved)) + + # Only used for status output purposes -- incremental saving uses the + # values returned here. + return num_saved, num_feat + + nfeat = self.layer.num_feat + if step and isinstance(step, int) and step < nfeat: + # Incremental saving is requested at the given interval (step) + if default_range: + raise LayerMapError('The `step` keyword may not be used in conjunction with the `fid_range` keyword.') + beg, num_feat, num_saved = (0, 0, 0) + indices = range(step, nfeat, step) + n_i = len(indices) + + for i, end in enumerate(indices): + # Constructing the slice to use for this step; the last slice is + # special (e.g, [100:] instead of [90:100]). + if i+1 == n_i: step_slice = slice(beg, None) + else: step_slice = slice(beg, end) + + try: + num_feat, num_saved = _save(step_slice, num_feat, num_saved) + beg = end + except: + stream.write('%s\nFailed to save slice: %s\n' % ('=-' * 20, step_slice)) + raise + else: + # Otherwise, just calling the previously defined _save() function. + _save() diff --git a/parts/django/django/contrib/gis/utils/ogrinfo.py b/parts/django/django/contrib/gis/utils/ogrinfo.py new file mode 100644 index 0000000..1e4c42d --- /dev/null +++ b/parts/django/django/contrib/gis/utils/ogrinfo.py @@ -0,0 +1,53 @@ +""" +This module includes some utility functions for inspecting the layout +of a GDAL data source -- the functionality is analogous to the output +produced by the `ogrinfo` utility. +""" + +from django.contrib.gis.gdal import DataSource +from django.contrib.gis.gdal.geometries import GEO_CLASSES + +def ogrinfo(data_source, num_features=10): + """ + Walks the available layers in the supplied `data_source`, displaying + the fields for the first `num_features` features. + """ + + # Checking the parameters. + if isinstance(data_source, str): + data_source = DataSource(data_source) + elif isinstance(data_source, DataSource): + pass + else: + raise Exception('Data source parameter must be a string or a DataSource object.') + + for i, layer in enumerate(data_source): + print "data source : %s" % data_source.name + print "==== layer %s" % i + print " shape type: %s" % GEO_CLASSES[layer.geom_type.num].__name__ + print " # features: %s" % len(layer) + print " srs: %s" % layer.srs + extent_tup = layer.extent.tuple + print " extent: %s - %s" % (extent_tup[0:2], extent_tup[2:4]) + print "Displaying the first %s features ====" % num_features + + width = max(*map(len,layer.fields)) + fmt = " %%%ss: %%s" % width + for j, feature in enumerate(layer[:num_features]): + print "=== Feature %s" % j + for fld_name in layer.fields: + type_name = feature[fld_name].type_name + output = fmt % (fld_name, type_name) + val = feature.get(fld_name) + if val: + if isinstance(val, str): + val_fmt = ' ("%s")' + else: + val_fmt = ' (%s)' + output += val_fmt % val + else: + output += ' (None)' + print output + +# For backwards compatibility. +sample = ogrinfo diff --git a/parts/django/django/contrib/gis/utils/ogrinspect.py b/parts/django/django/contrib/gis/utils/ogrinspect.py new file mode 100644 index 0000000..145bd22 --- /dev/null +++ b/parts/django/django/contrib/gis/utils/ogrinspect.py @@ -0,0 +1,225 @@ +""" +This module is for inspecting OGR data sources and generating either +models for GeoDjango and/or mapping dictionaries for use with the +`LayerMapping` utility. + +Author: Travis Pinney, Dane Springmeyer, & Justin Bronn +""" +from itertools import izip +# Requires GDAL to use. +from django.contrib.gis.gdal import DataSource +from django.contrib.gis.gdal.field import OFTDate, OFTDateTime, OFTInteger, OFTReal, OFTString, OFTTime + +def mapping(data_source, geom_name='geom', layer_key=0, multi_geom=False): + """ + Given a DataSource, generates a dictionary that may be used + for invoking the LayerMapping utility. + + Keyword Arguments: + `geom_name` => The name of the geometry field to use for the model. + + `layer_key` => The key for specifying which layer in the DataSource to use; + defaults to 0 (the first layer). May be an integer index or a string + identifier for the layer. + + `multi_geom` => Boolean (default: False) - specify as multigeometry. + """ + if isinstance(data_source, basestring): + # Instantiating the DataSource from the string. + data_source = DataSource(data_source) + elif isinstance(data_source, DataSource): + pass + else: + raise TypeError('Data source parameter must be a string or a DataSource object.') + + # Creating the dictionary. + _mapping = {} + + # Generating the field name for each field in the layer. + for field in data_source[layer_key].fields: + mfield = field.lower() + if mfield[-1:] == '_': mfield += 'field' + _mapping[mfield] = field + gtype = data_source[layer_key].geom_type + if multi_geom and gtype.num in (1, 2, 3): prefix = 'MULTI' + else: prefix = '' + _mapping[geom_name] = prefix + str(gtype).upper() + return _mapping + +def ogrinspect(*args, **kwargs): + """ + Given a data source (either a string or a DataSource object) and a string + model name this function will generate a GeoDjango model. + + Usage: + + >>> from django.contrib.gis.utils import ogrinspect + >>> ogrinspect('/path/to/shapefile.shp','NewModel') + + ...will print model definition to stout + + or put this in a python script and use to redirect the output to a new + model like: + + $ python generate_model.py > myapp/models.py + + # generate_model.py + from django.contrib.gis.utils import ogrinspect + shp_file = 'data/mapping_hacks/world_borders.shp' + model_name = 'WorldBorders' + + print ogrinspect(shp_file, model_name, multi_geom=True, srid=4326, + geom_name='shapes', blank=True) + + Required Arguments + `datasource` => string or DataSource object to file pointer + + `model name` => string of name of new model class to create + + Optional Keyword Arguments + `geom_name` => For specifying the model name for the Geometry Field. + Otherwise will default to `geom` + + `layer_key` => The key for specifying which layer in the DataSource to use; + defaults to 0 (the first layer). May be an integer index or a string + identifier for the layer. + + `srid` => The SRID to use for the Geometry Field. If it can be determined, + the SRID of the datasource is used. + + `multi_geom` => Boolean (default: False) - specify as multigeometry. + + `name_field` => String - specifies a field name to return for the + `__unicode__` function (which will be generated if specified). + + `imports` => Boolean (default: True) - set to False to omit the + `from django.contrib.gis.db import models` code from the + autogenerated models thus avoiding duplicated imports when building + more than one model by batching ogrinspect() + + `decimal` => Boolean or sequence (default: False). When set to True + all generated model fields corresponding to the `OFTReal` type will + be `DecimalField` instead of `FloatField`. A sequence of specific + field names to generate as `DecimalField` may also be used. + + `blank` => Boolean or sequence (default: False). When set to True all + generated model fields will have `blank=True`. If the user wants to + give specific fields to have blank, then a list/tuple of OGR field + names may be used. + + `null` => Boolean (default: False) - When set to True all generated + model fields will have `null=True`. If the user wants to specify + give specific fields to have null, then a list/tuple of OGR field + names may be used. + + Note: This routine calls the _ogrinspect() helper to do the heavy lifting. + """ + return '\n'.join(s for s in _ogrinspect(*args, **kwargs)) + +def _ogrinspect(data_source, model_name, geom_name='geom', layer_key=0, srid=None, + multi_geom=False, name_field=None, imports=True, + decimal=False, blank=False, null=False): + """ + Helper routine for `ogrinspect` that generates GeoDjango models corresponding + to the given data source. See the `ogrinspect` docstring for more details. + """ + # Getting the DataSource + if isinstance(data_source, str): + data_source = DataSource(data_source) + elif isinstance(data_source, DataSource): + pass + else: + raise TypeError('Data source parameter must be a string or a DataSource object.') + + # Getting the layer corresponding to the layer key and getting + # a string listing of all OGR fields in the Layer. + layer = data_source[layer_key] + ogr_fields = layer.fields + + # Creating lists from the `null`, `blank`, and `decimal` + # keyword arguments. + def process_kwarg(kwarg): + if isinstance(kwarg, (list, tuple)): + return [s.lower() for s in kwarg] + elif kwarg: + return [s.lower() for s in ogr_fields] + else: + return [] + null_fields = process_kwarg(null) + blank_fields = process_kwarg(blank) + decimal_fields = process_kwarg(decimal) + + # Gets the `null` and `blank` keywords for the given field name. + def get_kwargs_str(field_name): + kwlist = [] + if field_name.lower() in null_fields: kwlist.append('null=True') + if field_name.lower() in blank_fields: kwlist.append('blank=True') + if kwlist: return ', ' + ', '.join(kwlist) + else: return '' + + # For those wishing to disable the imports. + if imports: + yield '# This is an auto-generated Django model module created by ogrinspect.' + yield 'from django.contrib.gis.db import models' + yield '' + + yield 'class %s(models.Model):' % model_name + + for field_name, width, precision, field_type in izip(ogr_fields, layer.field_widths, layer.field_precisions, layer.field_types): + # The model field name. + mfield = field_name.lower() + if mfield[-1:] == '_': mfield += 'field' + + # Getting the keyword args string. + kwargs_str = get_kwargs_str(field_name) + + if field_type is OFTReal: + # By default OFTReals are mapped to `FloatField`, however, they + # may also be mapped to `DecimalField` if specified in the + # `decimal` keyword. + if field_name.lower() in decimal_fields: + yield ' %s = models.DecimalField(max_digits=%d, decimal_places=%d%s)' % (mfield, width, precision, kwargs_str) + else: + yield ' %s = models.FloatField(%s)' % (mfield, kwargs_str[2:]) + elif field_type is OFTInteger: + yield ' %s = models.IntegerField(%s)' % (mfield, kwargs_str[2:]) + elif field_type is OFTString: + yield ' %s = models.CharField(max_length=%s%s)' % (mfield, width, kwargs_str) + elif field_type is OFTDate: + yield ' %s = models.DateField(%s)' % (mfield, kwargs_str[2:]) + elif field_type is OFTDateTime: + yield ' %s = models.DateTimeField(%s)' % (mfield, kwargs_str[2:]) + elif field_type is OFTDate: + yield ' %s = models.TimeField(%s)' % (mfield, kwargs_str[2:]) + else: + raise TypeError('Unknown field type %s in %s' % (field_type, mfield)) + + # TODO: Autodetection of multigeometry types (see #7218). + gtype = layer.geom_type + if multi_geom and gtype.num in (1, 2, 3): + geom_field = 'Multi%s' % gtype.django + else: + geom_field = gtype.django + + # Setting up the SRID keyword string. + if srid is None: + if layer.srs is None: + srid_str = 'srid=-1' + else: + srid = layer.srs.srid + if srid is None: + srid_str = 'srid=-1' + elif srid == 4326: + # WGS84 is already the default. + srid_str = '' + else: + srid_str = 'srid=%s' % srid + else: + srid_str = 'srid=%s' % srid + + yield ' %s = models.%s(%s)' % (geom_name, geom_field, srid_str) + yield ' objects = models.GeoManager()' + + if name_field: + yield '' + yield ' def __unicode__(self): return self.%s' % name_field diff --git a/parts/django/django/contrib/gis/utils/srs.py b/parts/django/django/contrib/gis/utils/srs.py new file mode 100644 index 0000000..989929e --- /dev/null +++ b/parts/django/django/contrib/gis/utils/srs.py @@ -0,0 +1,77 @@ +from django.contrib.gis.gdal import SpatialReference +from django.db import connections, DEFAULT_DB_ALIAS + +def add_srs_entry(srs, auth_name='EPSG', auth_srid=None, ref_sys_name=None, + database=DEFAULT_DB_ALIAS): + """ + This function takes a GDAL SpatialReference system and adds its information + to the `spatial_ref_sys` table of the spatial backend. Doing this enables + database-level spatial transformations for the backend. Thus, this utility + is useful for adding spatial reference systems not included by default with + the backend -- for example, the so-called "Google Maps Mercator Projection" + is excluded in PostGIS 1.3 and below, and the following adds it to the + `spatial_ref_sys` table: + + >>> from django.contrib.gis.utils import add_srs_entry + >>> add_srs_entry(900913) + + Keyword Arguments: + auth_name: + This keyword may be customized with the value of the `auth_name` field. + Defaults to 'EPSG'. + + auth_srid: + This keyword may be customized with the value of the `auth_srid` field. + Defaults to the SRID determined by GDAL. + + ref_sys_name: + For SpatiaLite users only, sets the value of the the `ref_sys_name` field. + Defaults to the name determined by GDAL. + + database: + The name of the database connection to use; the default is the value + of `django.db.DEFAULT_DB_ALIAS` (at the time of this writing, it's value + is 'default'). + """ + connection = connections[database] + if not hasattr(connection.ops, 'spatial_version'): + raise Exception('The `add_srs_entry` utility only works ' + 'with spatial backends.') + if connection.ops.oracle or connection.ops.mysql: + raise Exception('This utility does not support the ' + 'Oracle or MySQL spatial backends.') + SpatialRefSys = connection.ops.spatial_ref_sys() + + # If argument is not a `SpatialReference` instance, use it as parameter + # to construct a `SpatialReference` instance. + if not isinstance(srs, SpatialReference): + srs = SpatialReference(srs) + + if srs.srid is None: + raise Exception('Spatial reference requires an SRID to be ' + 'compatible with the spatial backend.') + + # Initializing the keyword arguments dictionary for both PostGIS + # and SpatiaLite. + kwargs = {'srid' : srs.srid, + 'auth_name' : auth_name, + 'auth_srid' : auth_srid or srs.srid, + 'proj4text' : srs.proj4, + } + + # Backend-specific fields for the SpatialRefSys model. + if connection.ops.postgis: + kwargs['srtext'] = srs.wkt + if connection.ops.spatialite: + kwargs['ref_sys_name'] = ref_sys_name or srs.name + + # Creating the spatial_ref_sys model. + try: + # Try getting via SRID only, because using all kwargs may + # differ from exact wkt/proj in database. + sr = SpatialRefSys.objects.get(srid=srs.srid) + except SpatialRefSys.DoesNotExist: + sr = SpatialRefSys.objects.create(**kwargs) + +# Alias is for backwards-compatibility purposes. +add_postgis_srs = add_srs_entry diff --git a/parts/django/django/contrib/gis/utils/wkt.py b/parts/django/django/contrib/gis/utils/wkt.py new file mode 100644 index 0000000..4aecc62 --- /dev/null +++ b/parts/django/django/contrib/gis/utils/wkt.py @@ -0,0 +1,55 @@ +""" + Utilities for manipulating Geometry WKT. +""" + +def precision_wkt(geom, prec): + """ + Returns WKT text of the geometry according to the given precision (an + integer or a string). If the precision is an integer, then the decimal + places of coordinates WKT will be truncated to that number: + + >>> pnt = Point(5, 23) + >>> pnt.wkt + 'POINT (5.0000000000000000 23.0000000000000000)' + >>> precision(geom, 1) + 'POINT (5.0 23.0)' + + If the precision is a string, it must be valid Python format string + (e.g., '%20.7f') -- thus, you should know what you're doing. + """ + if isinstance(prec, int): + num_fmt = '%%.%df' % prec + elif isinstance(prec, basestring): + num_fmt = prec + else: + raise TypeError + + # TODO: Support 3D geometries. + coord_fmt = ' '.join([num_fmt, num_fmt]) + + def formatted_coords(coords): + return ','.join([coord_fmt % c[:2] for c in coords]) + + def formatted_poly(poly): + return ','.join(['(%s)' % formatted_coords(r) for r in poly]) + + def formatted_geom(g): + gtype = str(g.geom_type).upper() + yield '%s(' % gtype + if gtype == 'POINT': + yield formatted_coords((g.coords,)) + elif gtype in ('LINESTRING', 'LINEARRING'): + yield formatted_coords(g.coords) + elif gtype in ('POLYGON', 'MULTILINESTRING'): + yield formatted_poly(g) + elif gtype == 'MULTIPOINT': + yield formatted_coords(g.coords) + elif gtype == 'MULTIPOLYGON': + yield ','.join(['(%s)' % formatted_poly(p) for p in g]) + elif gtype == 'GEOMETRYCOLLECTION': + yield ','.join([''.join([wkt for wkt in formatted_geom(child)]) for child in g]) + else: + raise TypeError + yield ')' + + return ''.join([wkt for wkt in formatted_geom(geom)]) |