Merged the gis branch into trunk.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@8219 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Justin Bronn 2008-08-05 18:13:06 +00:00
parent d0f57e7c73
commit 79e68c225b
163 changed files with 14939 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 B

View File

View File

@ -0,0 +1,12 @@
# Getting the normal admin routines, classes, and `site` instance.
from django.contrib.admin import autodiscover, site, StackedInline, TabularInline, HORIZONTAL, VERTICAL
# Geographic admin options classes and widgets.
from django.contrib.gis.admin.options import GeoModelAdmin
from django.contrib.gis.admin.widgets import OpenLayersWidget
try:
from django.contrib.gis.admin.options import OSMGeoAdmin
HAS_OSM = True
except ImportError:
HAS_OSM = False

View File

@ -0,0 +1,128 @@
from django.conf import settings
from django.contrib.admin import ModelAdmin
from django.contrib.gis.admin.widgets import OpenLayersWidget
from django.contrib.gis.gdal import OGRGeomType
from django.contrib.gis.db import models
class GeoModelAdmin(ModelAdmin):
"""
The administration options class for Geographic models. Map settings
may be overloaded from their defaults to create custom maps.
"""
# The default map settings that may be overloaded -- still subject
# to API changes.
default_lon = 0
default_lat = 0
default_zoom = 4
display_wkt = False
display_srid = False
extra_js = []
num_zoom = 18
max_zoom = False
min_zoom = False
units = False
max_resolution = False
max_extent = False
modifiable = True
mouse_position = True
scale_text = True
layerswitcher = True
scrollable = True
admin_media_prefix = settings.ADMIN_MEDIA_PREFIX
map_width = 600
map_height = 400
map_srid = 4326
map_template = 'gis/admin/openlayers.html'
openlayers_url = 'http://openlayers.org/api/2.6/OpenLayers.js'
wms_url = 'http://labs.metacarta.com/wms/vmap0'
wms_layer = 'basic'
wms_name = 'OpenLayers WMS'
debug = False
widget = OpenLayersWidget
def _media(self):
"Injects OpenLayers JavaScript into the admin."
media = super(GeoModelAdmin, self)._media()
media.add_js([self.openlayers_url])
media.add_js(self.extra_js)
return media
media = property(_media)
def formfield_for_dbfield(self, db_field, **kwargs):
"""
Overloaded from ModelAdmin so that an OpenLayersWidget is used
for viewing/editing GeometryFields.
"""
if isinstance(db_field, models.GeometryField):
# Setting the widget with the newly defined widget.
kwargs['widget'] = self.get_map_widget(db_field)
return db_field.formfield(**kwargs)
else:
return super(GeoModelAdmin, self).formfield_for_dbfield(db_field, **kwargs)
def get_map_widget(self, db_field):
"""
Returns a subclass of the OpenLayersWidget (or whatever was specified
in the `widget` attribute) using the settings from the attributes set
in this class.
"""
is_collection = db_field._geom in ('MULTIPOINT', 'MULTILINESTRING', 'MULTIPOLYGON', 'GEOMETRYCOLLECTION')
if is_collection:
if db_field._geom == 'GEOMETRYCOLLECTION': collection_type = 'Any'
else: collection_type = OGRGeomType(db_field._geom.replace('MULTI', ''))
else:
collection_type = 'None'
class OLMap(self.widget):
template = self.map_template
geom_type = db_field._geom
params = {'admin_media_prefix' : self.admin_media_prefix,
'default_lon' : self.default_lon,
'default_lat' : self.default_lat,
'default_zoom' : self.default_zoom,
'display_wkt' : self.debug or self.display_wkt,
'geom_type' : OGRGeomType(db_field._geom),
'field_name' : db_field.name,
'is_collection' : is_collection,
'scrollable' : self.scrollable,
'layerswitcher' : self.layerswitcher,
'collection_type' : collection_type,
'is_linestring' : db_field._geom in ('LINESTRING', 'MULTILINESTRING'),
'is_polygon' : db_field._geom in ('POLYGON', 'MULTIPOLYGON'),
'is_point' : db_field._geom in ('POINT', 'MULTIPOINT'),
'num_zoom' : self.num_zoom,
'max_zoom' : self.max_zoom,
'min_zoom' : self.min_zoom,
'units' : self.units, #likely shoud get from object
'max_resolution' : self.max_resolution,
'max_extent' : self.max_extent,
'modifiable' : self.modifiable,
'mouse_position' : self.mouse_position,
'scale_text' : self.scale_text,
'map_width' : self.map_width,
'map_height' : self.map_height,
'srid' : self.map_srid,
'display_srid' : self.display_srid,
'wms_url' : self.wms_url,
'wms_layer' : self.wms_layer,
'wms_name' : self.wms_name,
'debug' : self.debug,
}
return OLMap
# Using the Beta OSM in the admin requires the following:
# (1) The Google Maps Mercator projection needs to be added
# to your `spatial_ref_sys` table. You'll need at least GDAL 1.5:
# >>> from django.contrib.gis.gdal import SpatialReference
# >>> from django.contrib.gis.utils import add_postgis_srs
# >>> add_postgis_srs(SpatialReference(900913)) # Adding the Google Projection
from django.contrib.gis import gdal
if gdal.HAS_GDAL:
class OSMGeoAdmin(GeoModelAdmin):
map_template = 'gis/admin/osm.html'
extra_js = ['http://openstreetmap.org/openlayers/OpenStreetMap.js']
num_zoom = 20
map_srid = 900913
max_extent = '-20037508,-20037508,20037508,20037508'
max_resolution = 156543.0339
units = 'm'

View File

@ -0,0 +1,92 @@
from django.contrib.gis.gdal import OGRException
from django.contrib.gis.geos import GEOSGeometry, GEOSException
from django.forms.widgets import Textarea
from django.template.loader import render_to_string
class OpenLayersWidget(Textarea):
"""
Renders an OpenLayers map using the WKT of the geometry.
"""
def render(self, name, value, attrs=None):
# Update the template parameters with any attributes passed in.
if attrs: self.params.update(attrs)
# Defaulting the WKT value to a blank string -- this
# will be tested in the JavaScript and the appropriate
# interfaace will be constructed.
self.params['wkt'] = ''
# If a string reaches here (via a validation error on another
# field) then just reconstruct the Geometry.
if isinstance(value, basestring):
try:
value = GEOSGeometry(value)
except (GEOSException, ValueError):
value = None
if value and value.geom_type.upper() != self.geom_type:
value = None
# Constructing the dictionary of the map options.
self.params['map_options'] = self.map_options()
# Constructing the JavaScript module name using the ID of
# the GeometryField (passed in via the `attrs` keyword).
self.params['module'] = 'geodjango_%s' % self.params['field_name']
if value:
# Transforming the geometry to the projection used on the
# OpenLayers map.
srid = self.params['srid']
if value.srid != srid:
try:
value.transform(srid)
wkt = value.wkt
except OGRException:
wkt = ''
else:
wkt = value.wkt
# Setting the parameter WKT with that of the transformed
# geometry.
self.params['wkt'] = wkt
return render_to_string(self.template, self.params)
def map_options(self):
"Builds the map options hash for the OpenLayers template."
# JavaScript construction utilities for the Bounds and Projection.
def ol_bounds(extent):
return 'new OpenLayers.Bounds(%s)' % str(extent)
def ol_projection(srid):
return 'new OpenLayers.Projection("EPSG:%s")' % srid
# An array of the parameter name, the name of their OpenLayers
# counterpart, and the type of variable they are.
map_types = [('srid', 'projection', 'srid'),
('display_srid', 'displayProjection', 'srid'),
('units', 'units', str),
('max_resolution', 'maxResolution', float),
('max_extent', 'maxExtent', 'bounds'),
('num_zoom', 'numZoomLevels', int),
('max_zoom', 'maxZoomLevels', int),
('min_zoom', 'minZoomLevel', int),
]
# Building the map options hash.
map_options = {}
for param_name, js_name, option_type in map_types:
if self.params.get(param_name, False):
if option_type == 'srid':
value = ol_projection(self.params[param_name])
elif option_type == 'bounds':
value = ol_bounds(self.params[param_name])
elif option_type in (float, int):
value = self.params[param_name]
elif option_type in (str,):
value = '"%s"' % self.params[param_name]
else:
raise TypeError
map_options[js_name] = value
return map_options

View File

View File

@ -0,0 +1,18 @@
"""
This module provides the backend for spatial SQL construction with Django.
Specifically, this module will import the correct routines and modules
needed for GeoDjango to interface with the spatial database.
"""
from django.conf import settings
from django.contrib.gis.db.backend.util import gqn
# Retrieving the necessary settings from the backend.
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
from django.contrib.gis.db.backend.postgis import create_spatial_db, get_geo_where_clause, SpatialBackend
elif settings.DATABASE_ENGINE == 'oracle':
from django.contrib.gis.db.backend.oracle import create_spatial_db, get_geo_where_clause, SpatialBackend
elif settings.DATABASE_ENGINE == 'mysql':
from django.contrib.gis.db.backend.mysql import create_spatial_db, get_geo_where_clause, SpatialBackend
else:
raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)

View File

@ -0,0 +1,14 @@
class WKTAdaptor(object):
"""
This provides an adaptor for Geometries sent to the
MySQL and Oracle database backends.
"""
def __init__(self, geom):
self.wkt = geom.wkt
self.srid = geom.srid
def __eq__(self, other):
return self.wkt == other.wkt and self.srid == other.srid
def __str__(self):
return self.wkt

View File

@ -0,0 +1,29 @@
"""
This module holds the base `SpatialBackend` object, which is
instantiated by each spatial backend with the features it has.
"""
# TODO: Create a `Geometry` protocol and allow user to use
# different Geometry objects -- for now we just use GEOSGeometry.
from django.contrib.gis.geos import GEOSGeometry, GEOSException
class BaseSpatialBackend(object):
Geometry = GEOSGeometry
GeometryException = GEOSException
def __init__(self, **kwargs):
kwargs.setdefault('distance_functions', {})
kwargs.setdefault('limited_where', {})
for k, v in kwargs.iteritems(): setattr(self, k, v)
def __getattr__(self, name):
"""
All attributes of the spatial backend return False by default.
"""
try:
return self.__dict__[name]
except KeyError:
return False

View File

@ -0,0 +1,13 @@
__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
from django.contrib.gis.db.backend.base import BaseSpatialBackend
from django.contrib.gis.db.backend.adaptor import WKTAdaptor
from django.contrib.gis.db.backend.mysql.creation import create_spatial_db
from django.contrib.gis.db.backend.mysql.field import MySQLGeoField
from django.contrib.gis.db.backend.mysql.query import *
SpatialBackend = BaseSpatialBackend(name='mysql', mysql=True,
gis_terms=MYSQL_GIS_TERMS,
select=GEOM_SELECT,
Adaptor=WKTAdaptor,
Field=MySQLGeoField)

View File

@ -0,0 +1,5 @@
from django.test.utils import create_test_db
def create_spatial_db(test=True, verbosity=1, autoclobber=False):
if not test: raise NotImplementedError('This uses `create_test_db` from test/utils.py')
create_test_db(verbosity, autoclobber)

View File

@ -0,0 +1,53 @@
from django.db import connection
from django.db.models.fields import Field # Django base Field class
from django.contrib.gis.db.backend.mysql.query import GEOM_FROM_TEXT
# Quotename & geographic quotename, respectively.
qn = connection.ops.quote_name
class MySQLGeoField(Field):
"""
The backend-specific geographic field for MySQL.
"""
def _geom_index(self, style, db_table):
"""
Creates a spatial index for the geometry column. If MyISAM tables are
used an R-Tree index is created, otherwise a B-Tree index is created.
Thus, for best spatial performance, you should use MyISAM tables
(which do not support transactions). For more information, see Ch.
16.6.1 of the MySQL 5.0 documentation.
"""
# Getting the index name.
idx_name = '%s_%s_id' % (db_table, self.column)
sql = style.SQL_KEYWORD('CREATE SPATIAL INDEX ') + \
style.SQL_TABLE(qn(idx_name)) + \
style.SQL_KEYWORD(' ON ') + \
style.SQL_TABLE(qn(db_table)) + '(' + \
style.SQL_FIELD(qn(self.column)) + ');'
return sql
def _post_create_sql(self, style, db_table):
"""
Returns SQL that will be executed after the model has been
created.
"""
# Getting the geometric index for this Geometry column.
if self._index:
return (self._geom_index(style, db_table),)
else:
return ()
def db_type(self):
"The OpenGIS name is returned for the MySQL database column type."
return self._geom
def get_placeholder(self, value):
"""
The placeholder here has to include MySQL's WKT constructor. Because
MySQL does not support spatial transformations, there is no need to
modify the placeholder based on the contents of the given value.
"""
return '%s(%%s)' % GEOM_FROM_TEXT

View File

@ -0,0 +1,59 @@
"""
This module contains the spatial lookup types, and the `get_geo_where_clause`
routine for MySQL.
Please note that MySQL only supports bounding box queries, also
known as MBRs (Minimum Bounding Rectangles). Moreover, spatial
indices may only be used on MyISAM tables -- if you need
transactions, take a look at PostGIS.
"""
from django.db import connection
qn = connection.ops.quote_name
# To ease implementation, WKT is passed to/from MySQL.
GEOM_FROM_TEXT = 'GeomFromText'
GEOM_FROM_WKB = 'GeomFromWKB'
GEOM_SELECT = 'AsText(%s)'
# WARNING: MySQL is NOT compliant w/the OpenGIS specification and
# _every_ one of these lookup types is on the _bounding box_ only.
MYSQL_GIS_FUNCTIONS = {
'bbcontains' : 'MBRContains', # For consistency w/PostGIS API
'bboverlaps' : 'MBROverlaps', # .. ..
'contained' : 'MBRWithin', # .. ..
'contains' : 'MBRContains',
'disjoint' : 'MBRDisjoint',
'equals' : 'MBREqual',
'exact' : 'MBREqual',
'intersects' : 'MBRIntersects',
'overlaps' : 'MBROverlaps',
'same_as' : 'MBREqual',
'touches' : 'MBRTouches',
'within' : 'MBRWithin',
}
# This lookup type does not require a mapping.
MISC_TERMS = ['isnull']
# Assacceptable lookup types for Oracle spatial.
MYSQL_GIS_TERMS = MYSQL_GIS_FUNCTIONS.keys()
MYSQL_GIS_TERMS += MISC_TERMS
MYSQL_GIS_TERMS = dict((term, None) for term in MYSQL_GIS_TERMS) # Making dictionary
def get_geo_where_clause(table_alias, name, lookup_type, geo_annot):
"Returns the SQL WHERE clause for use in MySQL spatial SQL construction."
# Getting the quoted field as `geo_col`.
geo_col = '%s.%s' % (qn(table_alias), qn(name))
# See if a MySQL Geometry function matches the lookup type next
lookup_info = MYSQL_GIS_FUNCTIONS.get(lookup_type, False)
if lookup_info:
return "%s(%s, %%s)" % (lookup_info, geo_col)
# Handling 'isnull' lookup type
# TODO: Is this needed because MySQL cannot handle NULL
# geometries in its spatial indices.
if lookup_type == 'isnull':
return "%s IS %sNULL" % (geo_col, (not geo_annot.value and 'NOT ' or ''))
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))

View File

@ -0,0 +1,31 @@
__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
from django.contrib.gis.db.backend.base import BaseSpatialBackend
from django.contrib.gis.db.backend.oracle.adaptor import OracleSpatialAdaptor
from django.contrib.gis.db.backend.oracle.creation import create_spatial_db
from django.contrib.gis.db.backend.oracle.field import OracleSpatialField
from django.contrib.gis.db.backend.oracle.query import *
SpatialBackend = BaseSpatialBackend(name='oracle', oracle=True,
area=AREA,
centroid=CENTROID,
difference=DIFFERENCE,
distance=DISTANCE,
distance_functions=DISTANCE_FUNCTIONS,
gis_terms=ORACLE_SPATIAL_TERMS,
gml=ASGML,
intersection=INTERSECTION,
length=LENGTH,
limited_where = {'relate' : None},
num_geom=NUM_GEOM,
num_points=NUM_POINTS,
perimeter=LENGTH,
point_on_surface=POINT_ON_SURFACE,
select=GEOM_SELECT,
sym_difference=SYM_DIFFERENCE,
transform=TRANSFORM,
unionagg=UNIONAGG,
union=UNION,
Adaptor=OracleSpatialAdaptor,
Field=OracleSpatialField,
)

View File

@ -0,0 +1,5 @@
from cx_Oracle import CLOB
from django.contrib.gis.db.backend.adaptor import WKTAdaptor
class OracleSpatialAdaptor(WKTAdaptor):
input_size = CLOB

View File

@ -0,0 +1,8 @@
from django.db.backends.oracle.creation import create_test_db
def create_spatial_db(test=True, verbosity=1, autoclobber=False):
"A wrapper over the Oracle `create_test_db` routine."
if not test: raise NotImplementedError('This uses `create_test_db` from db/backends/oracle/creation.py')
from django.conf import settings
from django.db import connection
create_test_db(settings, connection, verbosity, autoclobber)

View File

@ -0,0 +1,103 @@
from django.db import connection
from django.db.backends.util import truncate_name
from django.db.models.fields import Field # Django base Field class
from django.contrib.gis.db.backend.util import gqn
from django.contrib.gis.db.backend.oracle.query import TRANSFORM
# Quotename & geographic quotename, respectively.
qn = connection.ops.quote_name
class OracleSpatialField(Field):
"""
The backend-specific geographic field for Oracle Spatial.
"""
empty_strings_allowed = False
def __init__(self, extent=(-180.0, -90.0, 180.0, 90.0), tolerance=0.05, **kwargs):
"""
Oracle Spatial backend needs to have the extent -- for projected coordinate
systems _you must define the extent manually_, since the coordinates are
for geodetic systems. The `tolerance` keyword specifies the tolerance
for error (in meters), and defaults to 0.05 (5 centimeters).
"""
# Oracle Spatial specific keyword arguments.
self._extent = extent
self._tolerance = tolerance
# Calling the Django field initialization.
super(OracleSpatialField, self).__init__(**kwargs)
def _add_geom(self, style, db_table):
"""
Adds this geometry column into the Oracle USER_SDO_GEOM_METADATA
table.
"""
# Checking the dimensions.
# TODO: Add support for 3D geometries.
if self._dim != 2:
raise Exception('3D geometries not yet supported on Oracle Spatial backend.')
# Constructing the SQL that will be used to insert information about
# the geometry column into the USER_GSDO_GEOM_METADATA table.
meta_sql = style.SQL_KEYWORD('INSERT INTO ') + \
style.SQL_TABLE('USER_SDO_GEOM_METADATA') + \
' (%s, %s, %s, %s)\n ' % tuple(map(qn, ['TABLE_NAME', 'COLUMN_NAME', 'DIMINFO', 'SRID'])) + \
style.SQL_KEYWORD(' VALUES ') + '(\n ' + \
style.SQL_TABLE(gqn(db_table)) + ',\n ' + \
style.SQL_FIELD(gqn(self.column)) + ',\n ' + \
style.SQL_KEYWORD("MDSYS.SDO_DIM_ARRAY") + '(\n ' + \
style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") + \
("('LONG', %s, %s, %s),\n " % (self._extent[0], self._extent[2], self._tolerance)) + \
style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") + \
("('LAT', %s, %s, %s)\n ),\n" % (self._extent[1], self._extent[3], self._tolerance)) + \
' %s\n );' % self._srid
return meta_sql
def _geom_index(self, style, db_table):
"Creates an Oracle Geometry index (R-tree) for this geometry field."
# Getting the index name, Oracle doesn't allow object
# names > 30 characters.
idx_name = truncate_name('%s_%s_id' % (db_table, self.column), 30)
sql = style.SQL_KEYWORD('CREATE INDEX ') + \
style.SQL_TABLE(qn(idx_name)) + \
style.SQL_KEYWORD(' ON ') + \
style.SQL_TABLE(qn(db_table)) + '(' + \
style.SQL_FIELD(qn(self.column)) + ') ' + \
style.SQL_KEYWORD('INDEXTYPE IS ') + \
style.SQL_TABLE('MDSYS.SPATIAL_INDEX') + ';'
return sql
def post_create_sql(self, style, db_table):
"""
Returns SQL that will be executed after the model has been
created.
"""
# Getting the meta geometry information.
post_sql = self._add_geom(style, db_table)
# Getting the geometric index for this Geometry column.
if self._index:
return (post_sql, self._geom_index(style, db_table))
else:
return (post_sql,)
def db_type(self):
"The Oracle geometric data type is MDSYS.SDO_GEOMETRY."
return 'MDSYS.SDO_GEOMETRY'
def get_placeholder(self, value):
"""
Provides a proper substitution value for Geometries that are not in the
SRID of the field. Specifically, this routine will substitute in the
SDO_CS.TRANSFORM() function call.
"""
if value is None:
return '%s'
elif value.srid != self._srid:
# Adding Transform() to the SQL placeholder.
return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (TRANSFORM, value.srid, self._srid)
else:
return 'SDO_GEOMETRY(%%s, %s)' % self._srid

View File

@ -0,0 +1,49 @@
"""
The GeometryColumns and SpatialRefSys models for the Oracle spatial
backend.
It should be noted that Oracle Spatial does not have database tables
named according to the OGC standard, so the closest analogs are used.
For example, the `USER_SDO_GEOM_METADATA` is used for the GeometryColumns
model and the `SDO_COORD_REF_SYS` is used for the SpatialRefSys model.
"""
from django.db import models
from django.contrib.gis.models import SpatialRefSysMixin
class GeometryColumns(models.Model):
"Maps to the Oracle USER_SDO_GEOM_METADATA table."
table_name = models.CharField(max_length=32)
column_name = models.CharField(max_length=1024)
srid = models.IntegerField(primary_key=True)
# TODO: Add support for `diminfo` column (type MDSYS.SDO_DIM_ARRAY).
class Meta:
db_table = 'USER_SDO_GEOM_METADATA'
@classmethod
def table_name_col(cls):
return 'table_name'
def __unicode__(self):
return '%s - %s (SRID: %s)' % (self.table_name, self.column_name, self.srid)
class SpatialRefSys(models.Model, SpatialRefSysMixin):
"Maps to the Oracle MDSYS.CS_SRS table."
cs_name = models.CharField(max_length=68)
srid = models.IntegerField(primary_key=True)
auth_srid = models.IntegerField()
auth_name = models.CharField(max_length=256)
wktext = models.CharField(max_length=2046)
#cs_bounds = models.GeometryField()
class Meta:
# TODO: Figure out way to have this be MDSYS.CS_SRS without
# having django's quoting mess up the SQL.
db_table = 'CS_SRS'
@property
def wkt(self):
return self.wktext
@classmethod
def wkt_col(cls):
return 'wktext'

View File

@ -0,0 +1,154 @@
"""
This module contains the spatial lookup types, and the `get_geo_where_clause`
routine for Oracle Spatial.
Please note that WKT support is broken on the XE version, and thus
this backend will not work on such platforms. Specifically, XE lacks
support for an internal JVM, and Java libraries are required to use
the WKT constructors.
"""
import re
from decimal import Decimal
from django.db import connection
from django.contrib.gis.db.backend.util import SpatialFunction
from django.contrib.gis.measure import Distance
qn = connection.ops.quote_name
# The GML, distance, transform, and union procedures.
AREA = 'SDO_GEOM.SDO_AREA'
ASGML = 'SDO_UTIL.TO_GMLGEOMETRY'
CENTROID = 'SDO_GEOM.SDO_CENTROID'
DIFFERENCE = 'SDO_GEOM.SDO_DIFFERENCE'
DISTANCE = 'SDO_GEOM.SDO_DISTANCE'
EXTENT = 'SDO_AGGR_MBR'
INTERSECTION = 'SDO_GEOM.SDO_INTERSECTION'
LENGTH = 'SDO_GEOM.SDO_LENGTH'
NUM_GEOM = 'SDO_UTIL.GETNUMELEM'
NUM_POINTS = 'SDO_UTIL.GETNUMVERTICES'
POINT_ON_SURFACE = 'SDO_GEOM.SDO_POINTONSURFACE'
SYM_DIFFERENCE = 'SDO_GEOM.SDO_XOR'
TRANSFORM = 'SDO_CS.TRANSFORM'
UNION = 'SDO_GEOM.SDO_UNION'
UNIONAGG = 'SDO_AGGR_UNION'
# We want to get SDO Geometries as WKT because it is much easier to
# instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings.
# However, this adversely affects performance (i.e., Java is called
# to convert to WKT on every query). If someone wishes to write a
# SDO_GEOMETRY(...) parser in Python, let me know =)
GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)'
#### Classes used in constructing Oracle spatial SQL ####
class SDOOperation(SpatialFunction):
"Base class for SDO* Oracle operations."
def __init__(self, func, **kwargs):
kwargs.setdefault('operator', '=')
kwargs.setdefault('result', 'TRUE')
kwargs.setdefault('end_subst', ") %s '%s'")
super(SDOOperation, self).__init__(func, **kwargs)
class SDODistance(SpatialFunction):
"Class for Distance queries."
def __init__(self, op, tolerance=0.05):
super(SDODistance, self).__init__(DISTANCE, end_subst=', %s) %%s %%s' % tolerance,
operator=op, result='%%s')
class SDOGeomRelate(SpatialFunction):
"Class for using SDO_GEOM.RELATE."
def __init__(self, mask, tolerance=0.05):
# SDO_GEOM.RELATE(...) has a peculiar argument order: column, mask, geom, tolerance.
# Moreover, the runction result is the mask (e.g., 'DISJOINT' instead of 'TRUE').
end_subst = "%s%s) %s '%s'" % (', %%s, ', tolerance, '=', mask)
beg_subst = "%%s(%%s, '%s'" % mask
super(SDOGeomRelate, self).__init__('SDO_GEOM.RELATE', beg_subst=beg_subst, end_subst=end_subst)
class SDORelate(SpatialFunction):
"Class for using SDO_RELATE."
masks = 'TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|CONTAINS|COVERS|ANYINTERACT|ON'
mask_regex = re.compile(r'^(%s)(\+(%s))*$' % (masks, masks), re.I)
def __init__(self, mask):
func = 'SDO_RELATE'
if not self.mask_regex.match(mask):
raise ValueError('Invalid %s mask: "%s"' % (func, mask))
super(SDORelate, self).__init__(func, end_subst=", 'mask=%s') = 'TRUE'" % mask)
#### Lookup type mapping dictionaries of Oracle spatial operations ####
# Valid distance types and substitutions
dtypes = (Decimal, Distance, float, int, long)
DISTANCE_FUNCTIONS = {
'distance_gt' : (SDODistance('>'), dtypes),
'distance_gte' : (SDODistance('>='), dtypes),
'distance_lt' : (SDODistance('<'), dtypes),
'distance_lte' : (SDODistance('<='), dtypes),
'dwithin' : (SDOOperation('SDO_WITHIN_DISTANCE',
beg_subst="%s(%s, %%s, 'distance=%%s'"), dtypes),
}
ORACLE_GEOMETRY_FUNCTIONS = {
'contains' : SDOOperation('SDO_CONTAINS'),
'coveredby' : SDOOperation('SDO_COVEREDBY'),
'covers' : SDOOperation('SDO_COVERS'),
'disjoint' : SDOGeomRelate('DISJOINT'),
'intersects' : SDOOperation('SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()?
'equals' : SDOOperation('SDO_EQUAL'),
'exact' : SDOOperation('SDO_EQUAL'),
'overlaps' : SDOOperation('SDO_OVERLAPS'),
'same_as' : SDOOperation('SDO_EQUAL'),
'relate' : (SDORelate, basestring), # Oracle uses a different syntax, e.g., 'mask=inside+touch'
'touches' : SDOOperation('SDO_TOUCH'),
'within' : SDOOperation('SDO_INSIDE'),
}
ORACLE_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS)
# This lookup type does not require a mapping.
MISC_TERMS = ['isnull']
# Acceptable lookup types for Oracle spatial.
ORACLE_SPATIAL_TERMS = ORACLE_GEOMETRY_FUNCTIONS.keys()
ORACLE_SPATIAL_TERMS += MISC_TERMS
ORACLE_SPATIAL_TERMS = dict((term, None) for term in ORACLE_SPATIAL_TERMS) # Making dictionary for fast lookups
#### The `get_geo_where_clause` function for Oracle ####
def get_geo_where_clause(table_alias, name, lookup_type, geo_annot):
"Returns the SQL WHERE clause for use in Oracle spatial SQL construction."
# Getting the quoted table name as `geo_col`.
geo_col = '%s.%s' % (qn(table_alias), qn(name))
# See if a Oracle Geometry function matches the lookup type next
lookup_info = ORACLE_GEOMETRY_FUNCTIONS.get(lookup_type, False)
if lookup_info:
# Lookup types that are tuples take tuple arguments, e.g., 'relate' and
# 'dwithin' lookup types.
if isinstance(lookup_info, tuple):
# First element of tuple is lookup type, second element is the type
# of the expected argument (e.g., str, float)
sdo_op, arg_type = lookup_info
# Ensuring that a tuple _value_ was passed in from the user
if not isinstance(geo_annot.value, tuple):
raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
if len(geo_annot.value) != 2:
raise ValueError('2-element tuple required for %s lookup type.' % lookup_type)
# Ensuring the argument type matches what we expect.
if not isinstance(geo_annot.value[1], arg_type):
raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(geo_annot.value[1])))
if lookup_type == 'relate':
# The SDORelate class handles construction for these queries,
# and verifies the mask argument.
return sdo_op(geo_annot.value[1]).as_sql(geo_col)
else:
# Otherwise, just call the `as_sql` method on the SDOOperation instance.
return sdo_op.as_sql(geo_col)
else:
# Lookup info is a SDOOperation instance, whose `as_sql` method returns
# the SQL necessary for the geometry function call. For example:
# SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE'
return lookup_info.as_sql(geo_col)
elif lookup_type == 'isnull':
# Handling 'isnull' lookup type
return "%s IS %sNULL" % (geo_col, (not geo_annot.value and 'NOT ' or ''))
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))

View File

@ -0,0 +1,42 @@
__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
from django.contrib.gis.db.backend.base import BaseSpatialBackend
from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor
from django.contrib.gis.db.backend.postgis.creation import create_spatial_db
from django.contrib.gis.db.backend.postgis.field import PostGISField
from django.contrib.gis.db.backend.postgis.query import *
SpatialBackend = BaseSpatialBackend(name='postgis', postgis=True,
area=AREA,
centroid=CENTROID,
difference=DIFFERENCE,
distance=DISTANCE,
distance_functions=DISTANCE_FUNCTIONS,
distance_sphere=DISTANCE_SPHERE,
distance_spheroid=DISTANCE_SPHEROID,
envelope=ENVELOPE,
extent=EXTENT,
gis_terms=POSTGIS_TERMS,
gml=ASGML,
intersection=INTERSECTION,
kml=ASKML,
length=LENGTH,
length_spheroid=LENGTH_SPHEROID,
make_line=MAKE_LINE,
mem_size=MEM_SIZE,
num_geom=NUM_GEOM,
num_points=NUM_POINTS,
perimeter=PERIMETER,
point_on_surface=POINT_ON_SURFACE,
scale=SCALE,
select=GEOM_SELECT,
svg=ASSVG,
sym_difference=SYM_DIFFERENCE,
transform=TRANSFORM,
translate=TRANSLATE,
union=UNION,
unionagg=UNIONAGG,
version=(MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2),
Adaptor=PostGISAdaptor,
Field=PostGISField,
)

View File

@ -0,0 +1,33 @@
"""
This object provides quoting for GEOS geometries into PostgreSQL/PostGIS.
"""
from django.contrib.gis.db.backend.postgis.query import GEOM_FROM_WKB
from psycopg2 import Binary
from psycopg2.extensions import ISQLQuote
class PostGISAdaptor(object):
def __init__(self, geom):
"Initializes on the geometry."
# Getting the WKB (in string form, to allow easy pickling of
# the adaptor) and the SRID from the geometry.
self.wkb = str(geom.wkb)
self.srid = geom.srid
def __conform__(self, proto):
# Does the given protocol conform to what Psycopg2 expects?
if proto == ISQLQuote:
return self
else:
raise Exception('Error implementing psycopg2 protocol. Is psycopg2 installed?')
def __eq__(self, other):
return (self.wkb == other.wkb) and (self.srid == other.srid)
def __str__(self):
return self.getquoted()
def getquoted(self):
"Returns a properly quoted string for use in PostgreSQL/PostGIS."
# Want to use WKB, so wrap with psycopg2 Binary() to quote properly.
return "%s(%s, %s)" % (GEOM_FROM_WKB, Binary(self.wkb), self.srid or -1)

View File

@ -0,0 +1,224 @@
from django.conf import settings
from django.core.management import call_command
from django.db import connection
from django.test.utils import _set_autocommit, TEST_DATABASE_PREFIX
import os, re, sys
def getstatusoutput(cmd):
"A simpler version of getstatusoutput that works on win32 platforms."
stdin, stdout, stderr = os.popen3(cmd)
output = stdout.read()
if output.endswith('\n'): output = output[:-1]
status = stdin.close()
return status, output
def create_lang(db_name, verbosity=1):
"Sets up the pl/pgsql language on the given database."
# Getting the command-line options for the shell command
options = get_cmd_options(db_name)
# Constructing the 'createlang' command.
createlang_cmd = 'createlang %splpgsql' % options
if verbosity >= 1: print createlang_cmd
# Must have database super-user privileges to execute createlang -- it must
# also be in your path.
status, output = getstatusoutput(createlang_cmd)
# Checking the status of the command, 0 => execution successful
if status:
raise Exception("Error executing 'plpgsql' command: %s\n" % output)
def _create_with_cursor(db_name, verbosity=1, autoclobber=False):
"Creates database with psycopg2 cursor."
# Constructing the necessary SQL to create the database (the DATABASE_USER
# must possess the privileges to create a database)
create_sql = 'CREATE DATABASE %s' % connection.ops.quote_name(db_name)
if settings.DATABASE_USER:
create_sql += ' OWNER %s' % settings.DATABASE_USER
cursor = connection.cursor()
_set_autocommit(connection)
try:
# Trying to create the database first.
cursor.execute(create_sql)
#print create_sql
except Exception, e:
# Drop and recreate, if necessary.
if not autoclobber:
confirm = raw_input("\nIt appears the database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % db_name)
if autoclobber or confirm == 'yes':
if verbosity >= 1: print 'Destroying old spatial database...'
drop_db(db_name)
if verbosity >= 1: print 'Creating new spatial database...'
cursor.execute(create_sql)
else:
raise Exception('Spatial Database Creation canceled.')
foo = _create_with_cursor
created_regex = re.compile(r'^createdb: database creation failed: ERROR: database ".+" already exists')
def _create_with_shell(db_name, verbosity=1, autoclobber=False):
"""
If no spatial database already exists, then using a cursor will not work.
Thus, a `createdb` command will be issued through the shell to bootstrap
creation of the spatial database.
"""
# Getting the command-line options for the shell command
options = get_cmd_options(False)
create_cmd = 'createdb -O %s %s%s' % (settings.DATABASE_USER, options, db_name)
if verbosity >= 1: print create_cmd
# Attempting to create the database.
status, output = getstatusoutput(create_cmd)
if status:
if created_regex.match(output):
if not autoclobber:
confirm = raw_input("\nIt appears the database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % db_name)
if autoclobber or confirm == 'yes':
if verbosity >= 1: print 'Destroying old spatial database...'
drop_cmd = 'dropdb %s%s' % (options, db_name)
status, output = getstatusoutput(drop_cmd)
if status != 0:
raise Exception('Could not drop database %s: %s' % (db_name, output))
if verbosity >= 1: print 'Creating new spatial database...'
status, output = getstatusoutput(create_cmd)
if status != 0:
raise Exception('Could not create database after dropping: %s' % output)
else:
raise Exception('Spatial Database Creation canceled.')
else:
raise Exception('Unknown error occurred in creating database: %s' % output)
def create_spatial_db(test=False, verbosity=1, autoclobber=False, interactive=False):
"Creates a spatial database based on the settings."
# Making sure we're using PostgreSQL and psycopg2
if settings.DATABASE_ENGINE != 'postgresql_psycopg2':
raise Exception('Spatial database creation only supported postgresql_psycopg2 platform.')
# Getting the spatial database name
if test:
db_name = get_spatial_db(test=True)
_create_with_cursor(db_name, verbosity=verbosity, autoclobber=autoclobber)
else:
db_name = get_spatial_db()
_create_with_shell(db_name, verbosity=verbosity, autoclobber=autoclobber)
# Creating the db language, does not need to be done on NT platforms
# since the PostGIS installer enables this capability.
if os.name != 'nt':
create_lang(db_name, verbosity=verbosity)
# Now adding in the PostGIS routines.
load_postgis_sql(db_name, verbosity=verbosity)
if verbosity >= 1: print 'Creation of spatial database %s successful.' % db_name
# Closing the connection
connection.close()
settings.DATABASE_NAME = db_name
# Syncing the database
call_command('syncdb', verbosity=verbosity, interactive=interactive)
def drop_db(db_name=False, test=False):
"""
Drops the given database (defaults to what is returned from
get_spatial_db()). All exceptions are propagated up to the caller.
"""
if not db_name: db_name = get_spatial_db(test=test)
cursor = connection.cursor()
cursor.execute('DROP DATABASE %s' % connection.ops.quote_name(db_name))
def get_cmd_options(db_name):
"Obtains the command-line PostgreSQL connection options for shell commands."
# The db_name parameter is optional
options = ''
if db_name:
options += '-d %s ' % db_name
if settings.DATABASE_USER:
options += '-U %s ' % settings.DATABASE_USER
if settings.DATABASE_HOST:
options += '-h %s ' % settings.DATABASE_HOST
if settings.DATABASE_PORT:
options += '-p %s ' % settings.DATABASE_PORT
return options
def get_spatial_db(test=False):
"""
Returns the name of the spatial database. The 'test' keyword may be set
to return the test spatial database name.
"""
if test:
if settings.TEST_DATABASE_NAME:
test_db_name = settings.TEST_DATABASE_NAME
else:
test_db_name = TEST_DATABASE_PREFIX + settings.DATABASE_NAME
return test_db_name
else:
if not settings.DATABASE_NAME:
raise Exception('must configure DATABASE_NAME in settings.py')
return settings.DATABASE_NAME
def load_postgis_sql(db_name, verbosity=1):
"""
This routine loads up the PostGIS SQL files lwpostgis.sql and
spatial_ref_sys.sql.
"""
# Getting the path to the PostGIS SQL
try:
# POSTGIS_SQL_PATH may be placed in settings to tell GeoDjango where the
# PostGIS SQL files are located. This is especially useful on Win32
# platforms since the output of pg_config looks like "C:/PROGRA~1/..".
sql_path = settings.POSTGIS_SQL_PATH
except AttributeError:
status, sql_path = getstatusoutput('pg_config --sharedir')
if status:
sql_path = '/usr/local/share'
# The PostGIS SQL post-creation files.
lwpostgis_file = os.path.join(sql_path, 'lwpostgis.sql')
srefsys_file = os.path.join(sql_path, 'spatial_ref_sys.sql')
if not os.path.isfile(lwpostgis_file):
raise Exception('Could not find PostGIS function definitions in %s' % lwpostgis_file)
if not os.path.isfile(srefsys_file):
raise Exception('Could not find PostGIS spatial reference system definitions in %s' % srefsys_file)
# Getting the psql command-line options, and command format.
options = get_cmd_options(db_name)
cmd_fmt = 'psql %s-f "%%s"' % options
# Now trying to load up the PostGIS functions
cmd = cmd_fmt % lwpostgis_file
if verbosity >= 1: print cmd
status, output = getstatusoutput(cmd)
if status:
raise Exception('Error in loading PostGIS lwgeometry routines.')
# Now trying to load up the Spatial Reference System table
cmd = cmd_fmt % srefsys_file
if verbosity >= 1: print cmd
status, output = getstatusoutput(cmd)
if status:
raise Exception('Error in loading PostGIS spatial_ref_sys table.')
# Setting the permissions because on Windows platforms the owner
# of the spatial_ref_sys and geometry_columns tables is always
# the postgres user, regardless of how the db is created.
if os.name == 'nt': set_permissions(db_name)
def set_permissions(db_name):
"""
Sets the permissions on the given database to that of the user specified
in the settings. Needed specifically for PostGIS on Win32 platforms.
"""
cursor = connection.cursor()
user = settings.DATABASE_USER
cursor.execute('ALTER TABLE geometry_columns OWNER TO %s' % user)
cursor.execute('ALTER TABLE spatial_ref_sys OWNER TO %s' % user)

View File

@ -0,0 +1,95 @@
from django.db import connection
from django.db.models.fields import Field # Django base Field class
from django.contrib.gis.db.backend.util import gqn
from django.contrib.gis.db.backend.postgis.query import TRANSFORM
# Quotename & geographic quotename, respectively
qn = connection.ops.quote_name
class PostGISField(Field):
"""
The backend-specific geographic field for PostGIS.
"""
def _add_geom(self, style, db_table):
"""
Constructs the addition of the geometry to the table using the
AddGeometryColumn(...) PostGIS (and OGC standard) stored procedure.
Takes the style object (provides syntax highlighting) and the
database table as parameters.
"""
sql = style.SQL_KEYWORD('SELECT ') + \
style.SQL_TABLE('AddGeometryColumn') + '(' + \
style.SQL_TABLE(gqn(db_table)) + ', ' + \
style.SQL_FIELD(gqn(self.column)) + ', ' + \
style.SQL_FIELD(str(self._srid)) + ', ' + \
style.SQL_COLTYPE(gqn(self._geom)) + ', ' + \
style.SQL_KEYWORD(str(self._dim)) + ');'
if not self.null:
# Add a NOT NULL constraint to the field
sql += '\n' + \
style.SQL_KEYWORD('ALTER TABLE ') + \
style.SQL_TABLE(qn(db_table)) + \
style.SQL_KEYWORD(' ALTER ') + \
style.SQL_FIELD(qn(self.column)) + \
style.SQL_KEYWORD(' SET NOT NULL') + ';'
return sql
def _geom_index(self, style, db_table,
index_type='GIST', index_opts='GIST_GEOMETRY_OPS'):
"Creates a GiST index for this geometry field."
sql = style.SQL_KEYWORD('CREATE INDEX ') + \
style.SQL_TABLE(qn('%s_%s_id' % (db_table, self.column))) + \
style.SQL_KEYWORD(' ON ') + \
style.SQL_TABLE(qn(db_table)) + \
style.SQL_KEYWORD(' USING ') + \
style.SQL_COLTYPE(index_type) + ' ( ' + \
style.SQL_FIELD(qn(self.column)) + ' ' + \
style.SQL_KEYWORD(index_opts) + ' );'
return sql
def post_create_sql(self, style, db_table):
"""
Returns SQL that will be executed after the model has been
created. Geometry columns must be added after creation with the
PostGIS AddGeometryColumn() function.
"""
# Getting the AddGeometryColumn() SQL necessary to create a PostGIS
# geometry field.
post_sql = self._add_geom(style, db_table)
# If the user wants to index this data, then get the indexing SQL as well.
if self._index:
return (post_sql, self._geom_index(style, db_table))
else:
return (post_sql,)
def _post_delete_sql(self, style, db_table):
"Drops the geometry column."
sql = style.SQL_KEYWORD('SELECT ') + \
style.SQL_KEYWORD('DropGeometryColumn') + '(' + \
style.SQL_TABLE(gqn(db_table)) + ', ' + \
style.SQL_FIELD(gqn(self.column)) + ');'
return sql
def db_type(self):
"""
PostGIS geometry columns are added by stored procedures, should be
None.
"""
return None
def get_placeholder(self, value):
"""
Provides a proper substitution value for Geometries that are not in the
SRID of the field. Specifically, this routine will substitute in the
ST_Transform() function call.
"""
if value is None or value.srid == self._srid:
return '%s'
else:
# Adding Transform() to the SQL placeholder.
return '%s(%%s, %s)' % (TRANSFORM, self._srid)

View File

@ -0,0 +1,54 @@
"""
This utility module is for obtaining information about the PostGIS
installation.
See PostGIS docs at Ch. 6.2.1 for more information on these functions.
"""
import re
def _get_postgis_func(func):
"Helper routine for calling PostGIS functions and returning their result."
from django.db import connection
cursor = connection.cursor()
cursor.execute('SELECT %s()' % func)
row = cursor.fetchone()
cursor.close()
return row[0]
### PostGIS management functions ###
def postgis_geos_version():
"Returns the version of the GEOS library used with PostGIS."
return _get_postgis_func('postgis_geos_version')
def postgis_lib_version():
"Returns the version number of the PostGIS library used with PostgreSQL."
return _get_postgis_func('postgis_lib_version')
def postgis_proj_version():
"Returns the version of the PROJ.4 library used with PostGIS."
return _get_postgis_func('postgis_proj_version')
def postgis_version():
"Returns PostGIS version number and compile-time options."
return _get_postgis_func('postgis_version')
def postgis_full_version():
"Returns PostGIS version number and compile-time options."
return _get_postgis_func('postgis_full_version')
### Routines for parsing output of management functions. ###
version_regex = re.compile('^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)')
def postgis_version_tuple():
"Returns the PostGIS version as a tuple."
# Getting the PostGIS version
version = postgis_lib_version()
m = version_regex.match(version)
if m:
major = int(m.group('major'))
minor1 = int(m.group('minor1'))
minor2 = int(m.group('minor2'))
else:
raise Exception('Could not parse PostGIS version string: %s' % version)
return (version, major, minor1, minor2)

View File

@ -0,0 +1,58 @@
"""
The GeometryColumns and SpatialRefSys models for the PostGIS backend.
"""
from django.db import models
from django.contrib.gis.models import SpatialRefSysMixin
# Checking for the presence of GDAL (needed for the SpatialReference object)
from django.contrib.gis.gdal import HAS_GDAL
if HAS_GDAL:
from django.contrib.gis.gdal import SpatialReference
class GeometryColumns(models.Model):
"""
The 'geometry_columns' table from the PostGIS. See the PostGIS
documentation at Ch. 4.2.2.
"""
f_table_catalog = models.CharField(max_length=256)
f_table_schema = models.CharField(max_length=256)
f_table_name = models.CharField(max_length=256)
f_geometry_column = models.CharField(max_length=256)
coord_dimension = models.IntegerField()
srid = models.IntegerField(primary_key=True)
type = models.CharField(max_length=30)
class Meta:
db_table = 'geometry_columns'
@classmethod
def table_name_col(cls):
"Class method for returning the table name column for this model."
return 'f_table_name'
def __unicode__(self):
return "%s.%s - %dD %s field (SRID: %d)" % \
(self.f_table_name, self.f_geometry_column,
self.coord_dimension, self.type, self.srid)
class SpatialRefSys(models.Model, SpatialRefSysMixin):
"""
The 'spatial_ref_sys' table from PostGIS. See the PostGIS
documentaiton at Ch. 4.2.1.
"""
srid = models.IntegerField(primary_key=True)
auth_name = models.CharField(max_length=256)
auth_srid = models.IntegerField()
srtext = models.CharField(max_length=2048)
proj4text = models.CharField(max_length=2048)
class Meta:
db_table = 'spatial_ref_sys'
@property
def wkt(self):
return self.srtext
@classmethod
def wkt_col(cls):
return 'srtext'

View File

@ -0,0 +1,287 @@
"""
This module contains the spatial lookup types, and the get_geo_where_clause()
routine for PostGIS.
"""
import re
from decimal import Decimal
from django.db import connection
from django.contrib.gis.measure import Distance
from django.contrib.gis.db.backend.postgis.management import postgis_version_tuple
from django.contrib.gis.db.backend.util import SpatialOperation, SpatialFunction
qn = connection.ops.quote_name
# Getting the PostGIS version information
POSTGIS_VERSION, MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 = postgis_version_tuple()
# The supported PostGIS versions.
# TODO: Confirm tests with PostGIS versions 1.1.x -- should work.
# Versions <= 1.0.x do not use GEOS C API, and will not be supported.
if MAJOR_VERSION != 1 or (MAJOR_VERSION == 1 and MINOR_VERSION1 < 1):
raise Exception('PostGIS version %s not supported.' % POSTGIS_VERSION)
# Versions of PostGIS >= 1.2.2 changed their naming convention to be
# 'SQL-MM-centric' to conform with the ISO standard. Practically, this
# means that 'ST_' prefixes geometry function names.
GEOM_FUNC_PREFIX = ''
if MAJOR_VERSION >= 1:
if (MINOR_VERSION1 > 2 or
(MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2)):
GEOM_FUNC_PREFIX = 'ST_'
def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func)
# Custom selection not needed for PostGIS because GEOS geometries are
# instantiated directly from the HEXEWKB returned by default. If
# WKT is needed for some reason in the future, this value may be changed,
# e.g,, 'AsText(%s)'.
GEOM_SELECT = None
# Functions used by the GeoManager & GeoQuerySet
AREA = get_func('Area')
ASKML = get_func('AsKML')
ASGML = get_func('AsGML')
ASSVG = get_func('AsSVG')
CENTROID = get_func('Centroid')
DIFFERENCE = get_func('Difference')
DISTANCE = get_func('Distance')
DISTANCE_SPHERE = get_func('distance_sphere')
DISTANCE_SPHEROID = get_func('distance_spheroid')
ENVELOPE = get_func('Envelope')
EXTENT = get_func('extent')
GEOM_FROM_TEXT = get_func('GeomFromText')
GEOM_FROM_WKB = get_func('GeomFromWKB')
INTERSECTION = get_func('Intersection')
LENGTH = get_func('Length')
LENGTH_SPHEROID = get_func('length_spheroid')
MAKE_LINE = get_func('MakeLine')
MEM_SIZE = get_func('mem_size')
NUM_GEOM = get_func('NumGeometries')
NUM_POINTS = get_func('npoints')
PERIMETER = get_func('Perimeter')
POINT_ON_SURFACE = get_func('PointOnSurface')
SCALE = get_func('Scale')
SYM_DIFFERENCE = get_func('SymDifference')
TRANSFORM = get_func('Transform')
TRANSLATE = get_func('Translate')
# Special cases for union and KML methods.
if MINOR_VERSION1 < 3:
UNIONAGG = 'GeomUnion'
UNION = 'Union'
else:
UNIONAGG = 'ST_Union'
UNION = 'ST_Union'
if MINOR_VERSION1 == 1:
ASKML = False
else:
raise NotImplementedError('PostGIS versions < 1.0 are not supported.')
#### Classes used in constructing PostGIS spatial SQL ####
class PostGISOperator(SpatialOperation):
"For PostGIS operators (e.g. `&&`, `~`)."
def __init__(self, operator):
super(PostGISOperator, self).__init__(operator=operator, beg_subst='%s %s %%s')
class PostGISFunction(SpatialFunction):
"For PostGIS function calls (e.g., `ST_Contains(table, geom)`)."
def __init__(self, function, **kwargs):
super(PostGISFunction, self).__init__(get_func(function), **kwargs)
class PostGISFunctionParam(PostGISFunction):
"For PostGIS functions that take another parameter (e.g. DWithin, Relate)."
def __init__(self, func):
super(PostGISFunctionParam, self).__init__(func, end_subst=', %%s)')
class PostGISDistance(PostGISFunction):
"For PostGIS distance operations."
dist_func = 'Distance'
def __init__(self, operator):
super(PostGISDistance, self).__init__(self.dist_func, end_subst=') %s %s',
operator=operator, result='%%s')
class PostGISSpheroidDistance(PostGISFunction):
"For PostGIS spherical distance operations (using the spheroid)."
dist_func = 'distance_spheroid'
def __init__(self, operator):
# An extra parameter in `end_subst` is needed for the spheroid string.
super(PostGISSpheroidDistance, self).__init__(self.dist_func,
beg_subst='%s(%s, %%s, %%s',
end_subst=') %s %s',
operator=operator, result='%%s')
class PostGISSphereDistance(PostGISFunction):
"For PostGIS spherical distance operations."
dist_func = 'distance_sphere'
def __init__(self, operator):
super(PostGISSphereDistance, self).__init__(self.dist_func, end_subst=') %s %s',
operator=operator, result='%%s')
class PostGISRelate(PostGISFunctionParam):
"For PostGIS Relate(<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(PostGISRelate, self).__init__('Relate')
#### Lookup type mapping dictionaries of PostGIS operations. ####
# PostGIS-specific operators. The commented descriptions of these
# operators come from Section 6.2.2 of the official PostGIS documentation.
POSTGIS_OPERATORS = {
# The "&<" operator returns true if A's bounding box overlaps or
# is to the left of B's bounding box.
'overlaps_left' : PostGISOperator('&<'),
# The "&>" operator returns true if A's bounding box overlaps or
# is to the right of B's bounding box.
'overlaps_right' : PostGISOperator('&>'),
# The "<<" operator returns true if A's bounding box is strictly
# to the left of B's bounding box.
'left' : PostGISOperator('<<'),
# The ">>" operator returns true if A's bounding box is strictly
# to the right of B's bounding box.
'right' : PostGISOperator('>>'),
# The "&<|" operator returns true if A's bounding box overlaps or
# is below B's bounding box.
'overlaps_below' : PostGISOperator('&<|'),
# The "|&>" operator returns true if A's bounding box overlaps or
# is above B's bounding box.
'overlaps_above' : PostGISOperator('|&>'),
# The "<<|" operator returns true if A's bounding box is strictly
# below B's bounding box.
'strictly_below' : PostGISOperator('<<|'),
# The "|>>" operator returns true if A's bounding box is strictly
# above B's bounding box.
'strictly_above' : PostGISOperator('|>>'),
# The "~=" operator is the "same as" operator. It tests actual
# geometric equality of two features. So if A and B are the same feature,
# vertex-by-vertex, the operator returns true.
'same_as' : PostGISOperator('~='),
'exact' : PostGISOperator('~='),
# The "@" operator returns true if A's bounding box is completely contained
# by B's bounding box.
'contained' : PostGISOperator('@'),
# The "~" operator returns true if A's bounding box completely contains
# by B's bounding box.
'bbcontains' : PostGISOperator('~'),
# The "&&" operator returns true if A's bounding box overlaps
# B's bounding box.
'bboverlaps' : PostGISOperator('&&'),
}
# For PostGIS >= 1.2.2 the following lookup types will do a bounding box query
# first before calling the more computationally expensive GEOS routines (called
# "inline index magic"):
# 'touches', 'crosses', 'contains', 'intersects', 'within', 'overlaps', and
# 'covers'.
POSTGIS_GEOMETRY_FUNCTIONS = {
'equals' : PostGISFunction('Equals'),
'disjoint' : PostGISFunction('Disjoint'),
'touches' : PostGISFunction('Touches'),
'crosses' : PostGISFunction('Crosses'),
'within' : PostGISFunction('Within'),
'overlaps' : PostGISFunction('Overlaps'),
'contains' : PostGISFunction('Contains'),
'intersects' : PostGISFunction('Intersects'),
'relate' : (PostGISRelate, basestring),
}
# Valid distance types and substitutions
dtypes = (Decimal, Distance, float, int, long)
def get_dist_ops(operator):
"Returns operations for both regular and spherical distances."
return (PostGISDistance(operator), PostGISSphereDistance(operator), PostGISSpheroidDistance(operator))
DISTANCE_FUNCTIONS = {
'distance_gt' : (get_dist_ops('>'), dtypes),
'distance_gte' : (get_dist_ops('>='), dtypes),
'distance_lt' : (get_dist_ops('<'), dtypes),
'distance_lte' : (get_dist_ops('<='), dtypes),
}
if GEOM_FUNC_PREFIX == 'ST_':
# The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+
POSTGIS_GEOMETRY_FUNCTIONS.update(
{'coveredby' : PostGISFunction('CoveredBy'),
'covers' : PostGISFunction('Covers'),
})
DISTANCE_FUNCTIONS['dwithin'] = (PostGISFunctionParam('DWithin'), dtypes)
# Distance functions are a part of PostGIS geometry functions.
POSTGIS_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS)
# Any other lookup types that do not require a mapping.
MISC_TERMS = ['isnull']
# These are the PostGIS-customized QUERY_TERMS -- a list of the lookup types
# allowed for geographic queries.
POSTGIS_TERMS = POSTGIS_OPERATORS.keys() # Getting the operators first
POSTGIS_TERMS += POSTGIS_GEOMETRY_FUNCTIONS.keys() # Adding on the Geometry Functions
POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull')
POSTGIS_TERMS = dict((term, None) for term in POSTGIS_TERMS) # Making a dictionary for fast lookups
# For checking tuple parameters -- not very pretty but gets job done.
def exactly_two(val): return val == 2
def two_to_three(val): return val >= 2 and val <=3
def num_params(lookup_type, val):
if lookup_type in DISTANCE_FUNCTIONS and lookup_type != 'dwithin': return two_to_three(val)
else: return exactly_two(val)
#### The `get_geo_where_clause` function for PostGIS. ####
def get_geo_where_clause(table_alias, name, lookup_type, geo_annot):
"Returns the SQL WHERE clause for use in PostGIS SQL construction."
# Getting the quoted field as `geo_col`.
geo_col = '%s.%s' % (qn(table_alias), qn(name))
if lookup_type in POSTGIS_OPERATORS:
# See if a PostGIS operator matches the lookup type.
return POSTGIS_OPERATORS[lookup_type].as_sql(geo_col)
elif lookup_type in POSTGIS_GEOMETRY_FUNCTIONS:
# See if a PostGIS geometry function matches the lookup type.
tmp = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type]
# Lookup types that are tuples take tuple arguments, e.g., 'relate' and
# distance lookups.
if isinstance(tmp, tuple):
# First element of tuple is the PostGISOperation instance, and the
# second element is either the type or a tuple of acceptable types
# that may passed in as further parameters for the lookup type.
op, arg_type = tmp
# Ensuring that a tuple _value_ was passed in from the user
if not isinstance(geo_annot.value, (tuple, list)):
raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
# Number of valid tuple parameters depends on the lookup type.
nparams = len(geo_annot.value)
if not num_params(lookup_type, nparams):
raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type)
# Ensuring the argument type matches what we expect.
if not isinstance(geo_annot.value[1], arg_type):
raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(geo_annot.value[1])))
# For lookup type `relate`, the op instance is not yet created (has
# to be instantiated here to check the pattern parameter).
if lookup_type == 'relate':
op = op(geo_annot.value[1])
elif lookup_type in DISTANCE_FUNCTIONS and lookup_type != 'dwithin':
if geo_annot.geodetic:
# Geodetic distances are only availble from Points to PointFields.
if geo_annot.geom_type != 'POINT':
raise TypeError('PostGIS spherical operations are only valid on PointFields.')
if geo_annot.value[0].geom_typeid != 0:
raise TypeError('PostGIS geometry distance parameter is required to be of type Point.')
# Setting up the geodetic operation appropriately.
if nparams == 3 and geo_annot.value[2] == 'spheroid': op = op[2]
else: op = op[1]
else:
op = op[0]
else:
op = tmp
# Calling the `as_sql` function on the operation instance.
return op.as_sql(geo_col)
elif lookup_type == 'isnull':
# Handling 'isnull' lookup type
return "%s IS %sNULL" % (geo_col, (not geo_annot.value and 'NOT ' or ''))
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))

View File

@ -0,0 +1,52 @@
from types import UnicodeType
def gqn(val):
"""
The geographic quote name function; used for quoting tables and
geometries (they use single rather than the double quotes of the
backend quotename function).
"""
if isinstance(val, basestring):
if isinstance(val, UnicodeType): val = val.encode('ascii')
return "'%s'" % val
else:
return str(val)
class SpatialOperation(object):
"""
Base class for generating spatial SQL.
"""
def __init__(self, function='', operator='', result='', beg_subst='', end_subst=''):
self.function = function
self.operator = operator
self.result = result
self.beg_subst = beg_subst
try:
# Try and put the operator and result into to the
# end substitution.
self.end_subst = end_subst % (operator, result)
except TypeError:
self.end_subst = end_subst
@property
def sql_subst(self):
return ''.join([self.beg_subst, self.end_subst])
def as_sql(self, geo_col):
return self.sql_subst % self.params(geo_col)
def params(self, geo_col):
return (geo_col, self.operator)
class SpatialFunction(SpatialOperation):
"""
Base class for generating spatial SQL related to a function.
"""
def __init__(self, func, beg_subst='%s(%s, %%s', end_subst=')', result='', operator=''):
# Getting the function prefix.
kwargs = {'function' : func, 'operator' : operator, 'result' : result,
'beg_subst' : beg_subst, 'end_subst' : end_subst,}
super(SpatialFunction, self).__init__(**kwargs)
def params(self, geo_col):
return (self.function, geo_col)

View File

@ -0,0 +1,17 @@
# Want to get everything from the 'normal' models package.
from django.db.models import *
# The GeoManager
from django.contrib.gis.db.models.manager import GeoManager
# The GeoQ object
from django.contrib.gis.db.models.query import GeoQ
# The geographic-enabled fields.
from django.contrib.gis.db.models.fields import \
GeometryField, PointField, LineStringField, PolygonField, \
MultiPointField, MultiLineStringField, MultiPolygonField, \
GeometryCollectionField
# The geographic mixin class.
from mixin import GeoMixin

View File

@ -0,0 +1,214 @@
from django.contrib.gis import forms
from django.db import connection
# Getting the SpatialBackend container and the geographic quoting method.
from django.contrib.gis.db.backend import SpatialBackend, gqn
# GeometryProxy, GEOS, Distance, and oldforms imports.
from django.contrib.gis.db.models.proxy import GeometryProxy
from django.contrib.gis.measure import Distance
from django.contrib.gis.oldforms import WKTField
# The `get_srid_info` function gets SRID information from the spatial
# reference system table w/o using the ORM.
from django.contrib.gis.models import get_srid_info
#TODO: Flesh out widgets; consider adding support for OGR Geometry proxies.
class GeometryField(SpatialBackend.Field):
"The base GIS field -- maps to the OpenGIS Specification Geometry type."
# The OpenGIS Geometry name.
_geom = 'GEOMETRY'
# Geodetic units.
geodetic_units = ('Decimal Degree', 'degree')
def __init__(self, srid=4326, spatial_index=True, dim=2, **kwargs):
"""
The initialization function for geometry fields. Takes the following
as keyword arguments:
srid:
The spatial reference system identifier, an OGC standard.
Defaults to 4326 (WGS84).
spatial_index:
Indicates whether to create a spatial index. Defaults to True.
Set this instead of 'db_index' for geographic fields since index
creation is different for geometry columns.
dim:
The number of dimensions for this geometry. Defaults to 2.
"""
# Setting the index flag with the value of the `spatial_index` keyword.
self._index = spatial_index
# Setting the SRID and getting the units. Unit information must be
# easily available in the field instance for distance queries.
self._srid = srid
self._unit, self._unit_name, self._spheroid = get_srid_info(srid)
# Setting the dimension of the geometry field.
self._dim = dim
super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function
### Routines specific to GeometryField ###
@property
def geodetic(self):
"""
Returns true if this field's SRID corresponds with a coordinate
system that uses non-projected units (e.g., latitude/longitude).
"""
return self._unit_name in self.geodetic_units
def get_distance(self, dist_val, lookup_type):
"""
Returns a distance number in units of the field. For example, if
`D(km=1)` was passed in and the units of the field were in meters,
then 1000 would be returned.
"""
# Getting the distance parameter and any options.
if len(dist_val) == 1: dist, option = dist_val[0], None
else: dist, option = dist_val
if isinstance(dist, Distance):
if self.geodetic:
# Won't allow Distance objects w/DWithin lookups on PostGIS.
if SpatialBackend.postgis and lookup_type == 'dwithin':
raise TypeError('Only numeric values of degree units are allowed on geographic DWithin queries.')
# Spherical distance calculation parameter should be in meters.
dist_param = dist.m
else:
dist_param = getattr(dist, Distance.unit_attname(self._unit_name))
else:
# Assuming the distance is in the units of the field.
dist_param = dist
if SpatialBackend.postgis and self.geodetic and lookup_type != 'dwithin' and option == 'spheroid':
# On PostGIS, by default `ST_distance_sphere` is used; but if the
# accuracy of `ST_distance_spheroid` is needed than the spheroid
# needs to be passed to the SQL stored procedure.
return [gqn(self._spheroid), dist_param]
else:
return [dist_param]
def get_geometry(self, value):
"""
Retrieves the geometry, setting the default SRID from the given
lookup parameters.
"""
if isinstance(value, (tuple, list)):
geom = value[0]
else:
geom = value
# When the input is not a GEOS geometry, attempt to construct one
# from the given string input.
if isinstance(geom, SpatialBackend.Geometry):
pass
elif isinstance(geom, basestring):
try:
geom = SpatialBackend.Geometry(geom)
except SpatialBackend.GeometryException:
raise ValueError('Could not create geometry from lookup value: %s' % str(value))
else:
raise TypeError('Cannot use parameter of `%s` type as lookup parameter.' % type(value))
# Assigning the SRID value.
geom.srid = self.get_srid(geom)
return geom
def get_srid(self, geom):
"""
Returns the default SRID for the given geometry, taking into account
the SRID set for the field. For example, if the input geometry
has no SRID, then that of the field will be returned.
"""
gsrid = geom.srid # SRID of given geometry.
if gsrid is None or self._srid == -1 or (gsrid == -1 and self._srid != -1):
return self._srid
else:
return gsrid
### Routines overloaded from Field ###
def contribute_to_class(self, cls, name):
super(GeometryField, self).contribute_to_class(cls, name)
# Setup for lazy-instantiated Geometry object.
setattr(cls, self.attname, GeometryProxy(SpatialBackend.Geometry, self))
def formfield(self, **kwargs):
defaults = {'form_class' : forms.GeometryField,
'geom_type' : self._geom,
'null' : self.null,
}
defaults.update(kwargs)
return super(GeometryField, self).formfield(**defaults)
def get_db_prep_lookup(self, lookup_type, value):
"""
Returns the spatial WHERE clause and associated parameters for the
given lookup type and value. The value will be prepared for database
lookup (e.g., spatial transformation SQL will be added if necessary).
"""
if lookup_type in SpatialBackend.gis_terms:
# special case for isnull lookup
if lookup_type == 'isnull': return [], []
# Get the geometry with SRID; defaults SRID to that of the field
# if it is None.
geom = self.get_geometry(value)
# Getting the WHERE clause list and the associated params list. The params
# list is populated with the Adaptor wrapping the Geometry for the
# backend. The WHERE clause list contains the placeholder for the adaptor
# (e.g. any transformation SQL).
where = [self.get_placeholder(geom)]
params = [SpatialBackend.Adaptor(geom)]
if isinstance(value, (tuple, list)):
if lookup_type in SpatialBackend.distance_functions:
# Getting the distance parameter in the units of the field.
where += self.get_distance(value[1:], lookup_type)
elif lookup_type in SpatialBackend.limited_where:
pass
else:
# Otherwise, making sure any other parameters are properly quoted.
where += map(gqn, value[1:])
return where, params
else:
raise TypeError("Field has invalid lookup: %s" % lookup_type)
def get_db_prep_save(self, value):
"Prepares the value for saving in the database."
if value is None:
return None
else:
return SpatialBackend.Adaptor(self.get_geometry(value))
def get_manipulator_field_objs(self):
"Using the WKTField (oldforms) to be our manipulator."
return [WKTField]
# The OpenGIS Geometry Type Fields
class PointField(GeometryField):
_geom = 'POINT'
class LineStringField(GeometryField):
_geom = 'LINESTRING'
class PolygonField(GeometryField):
_geom = 'POLYGON'
class MultiPointField(GeometryField):
_geom = 'MULTIPOINT'
class MultiLineStringField(GeometryField):
_geom = 'MULTILINESTRING'
class MultiPolygonField(GeometryField):
_geom = 'MULTIPOLYGON'
class GeometryCollectionField(GeometryField):
_geom = 'GEOMETRYCOLLECTION'

View File

@ -0,0 +1,82 @@
from django.db.models.manager import Manager
from django.contrib.gis.db.models.query import GeoQuerySet
class GeoManager(Manager):
"Overrides Manager to return Geographic QuerySets."
# This manager should be used for queries on related fields
# so that geometry columns on Oracle and MySQL are selected
# properly.
use_for_related_fields = True
def get_query_set(self):
return GeoQuerySet(model=self.model)
def area(self, *args, **kwargs):
return self.get_query_set().area(*args, **kwargs)
def centroid(self, *args, **kwargs):
return self.get_query_set().centroid(*args, **kwargs)
def difference(self, *args, **kwargs):
return self.get_query_set().difference(*args, **kwargs)
def distance(self, *args, **kwargs):
return self.get_query_set().distance(*args, **kwargs)
def envelope(self, *args, **kwargs):
return self.get_query_set().envelope(*args, **kwargs)
def extent(self, *args, **kwargs):
return self.get_query_set().extent(*args, **kwargs)
def gml(self, *args, **kwargs):
return self.get_query_set().gml(*args, **kwargs)
def intersection(self, *args, **kwargs):
return self.get_query_set().intersection(*args, **kwargs)
def kml(self, *args, **kwargs):
return self.get_query_set().kml(*args, **kwargs)
def length(self, *args, **kwargs):
return self.get_query_set().length(*args, **kwargs)
def make_line(self, *args, **kwargs):
return self.get_query_set().make_line(*args, **kwargs)
def mem_size(self, *args, **kwargs):
return self.get_query_set().mem_size(*args, **kwargs)
def num_geom(self, *args, **kwargs):
return self.get_query_set().num_geom(*args, **kwargs)
def num_points(self, *args, **kwargs):
return self.get_query_set().num_points(*args, **kwargs)
def perimeter(self, *args, **kwargs):
return self.get_query_set().perimeter(*args, **kwargs)
def point_on_surface(self, *args, **kwargs):
return self.get_query_set().point_on_surface(*args, **kwargs)
def scale(self, *args, **kwargs):
return self.get_query_set().scale(*args, **kwargs)
def svg(self, *args, **kwargs):
return self.get_query_set().svg(*args, **kwargs)
def sym_difference(self, *args, **kwargs):
return self.get_query_set().sym_difference(*args, **kwargs)
def transform(self, *args, **kwargs):
return self.get_query_set().transform(*args, **kwargs)
def translate(self, *args, **kwargs):
return self.get_query_set().translate(*args, **kwargs)
def union(self, *args, **kwargs):
return self.get_query_set().union(*args, **kwargs)
def unionagg(self, *args, **kwargs):
return self.get_query_set().unionagg(*args, **kwargs)

View File

@ -0,0 +1,11 @@
# Until model subclassing is a possibility, a mixin class is used to add
# the necessary functions that may be contributed for geographic objects.
class GeoMixin:
"""
The Geographic Mixin class provides routines for geographic objects,
however, it is no longer necessary, since all of its previous functions
may now be accessed via the GeometryProxy. This mixin is only provided
for backwards-compatibility purposes, and will be eventually removed
(unless the need arises again).
"""
pass

View File

@ -0,0 +1,62 @@
"""
The GeometryProxy object, allows for lazy-geometries. The proxy uses
Python descriptors for instantiating and setting Geometry objects
corresponding to geographic model fields.
Thanks to Robert Coup for providing this functionality (see #4322).
"""
from types import NoneType, StringType, UnicodeType
class GeometryProxy(object):
def __init__(self, klass, field):
"""
Proxy initializes on the given Geometry class (not an instance) and
the GeometryField.
"""
self._field = field
self._klass = klass
def __get__(self, obj, type=None):
"""
This accessor retrieves the geometry, initializing it using the geometry
class specified during initialization and the HEXEWKB value of the field.
Currently, only GEOS or OGR geometries are supported.
"""
# Getting the value of the field.
geom_value = obj.__dict__[self._field.attname]
if isinstance(geom_value, self._klass):
geom = geom_value
elif (geom_value is None) or (geom_value==''):
geom = None
else:
# Otherwise, a Geometry object is built using the field's contents,
# and the model's corresponding attribute is set.
geom = self._klass(geom_value)
setattr(obj, self._field.attname, geom)
return geom
def __set__(self, obj, value):
"""
This accessor sets the proxied geometry with the geometry class
specified during initialization. Values of None, HEXEWKB, or WKT may
be used to set the geometry as well.
"""
# The OGC Geometry type of the field.
gtype = self._field._geom
# The geometry type must match that of the field -- unless the
# general GeometryField is used.
if isinstance(value, self._klass) and (str(value.geom_type).upper() == gtype or gtype == 'GEOMETRY'):
# Assigning the SRID to the geometry.
if value.srid is None: value.srid = self._field._srid
elif isinstance(value, (NoneType, StringType, UnicodeType)):
# Set with None, WKT, or HEX
pass
else:
raise TypeError('cannot set %s GeometryProxy with value of type: %s' % (obj.__class__.__name__, type(value)))
# Setting the objects dictionary with the value, and returning.
obj.__dict__[self._field.attname] = value
return value

View File

@ -0,0 +1,617 @@
from django.core.exceptions import ImproperlyConfigured
from django.db import connection
from django.db.models.query import sql, QuerySet, Q
from django.contrib.gis.db.backend import SpatialBackend
from django.contrib.gis.db.models.fields import GeometryField, PointField
from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery, GeoWhereNode
from django.contrib.gis.measure import Area, Distance
from django.contrib.gis.models import get_srid_info
qn = connection.ops.quote_name
# For backwards-compatibility; Q object should work just fine
# after queryset-refactor.
class GeoQ(Q): pass
class GeomSQL(object):
"Simple wrapper object for geometric SQL."
def __init__(self, geo_sql):
self.sql = geo_sql
def as_sql(self, *args, **kwargs):
return self.sql
class GeoQuerySet(QuerySet):
"The Geographic QuerySet."
def __init__(self, model=None, query=None):
super(GeoQuerySet, self).__init__(model=model, query=query)
self.query = query or GeoQuery(self.model, connection)
def area(self, tolerance=0.05, **kwargs):
"""
Returns the area of the geographic field in an `area` attribute on
each element of this GeoQuerySet.
"""
# Peforming setup here rather than in `_spatial_attribute` so that
# we can get the units for `AreaField`.
procedure_args, geo_field = self._spatial_setup('area', field_name=kwargs.get('field_name', None))
s = {'procedure_args' : procedure_args,
'geo_field' : geo_field,
'setup' : False,
}
if SpatialBackend.oracle:
s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s'
s['procedure_args']['tolerance'] = tolerance
s['select_field'] = AreaField('sq_m') # Oracle returns area in units of meters.
elif SpatialBackend.postgis:
if not geo_field.geodetic:
# Getting the area units of the geographic field.
s['select_field'] = AreaField(Area.unit_attname(geo_field._unit_name))
else:
# TODO: Do we want to support raw number areas for geodetic fields?
raise Exception('Area on geodetic coordinate systems not supported.')
return self._spatial_attribute('area', s, **kwargs)
def centroid(self, **kwargs):
"""
Returns the centroid of the geographic field in a `centroid`
attribute on each element of this GeoQuerySet.
"""
return self._geom_attribute('centroid', **kwargs)
def difference(self, geom, **kwargs):
"""
Returns the spatial difference of the geographic field in a `difference`
attribute on each element of this GeoQuerySet.
"""
return self._geomset_attribute('difference', geom, **kwargs)
def distance(self, geom, **kwargs):
"""
Returns the distance from the given geographic field name to the
given geometry in a `distance` attribute on each element of the
GeoQuerySet.
Keyword Arguments:
`spheroid` => If the geometry field is geodetic and PostGIS is
the spatial database, then the more accurate
spheroid calculation will be used instead of the
quicker sphere calculation.
`tolerance` => Used only for Oracle. The tolerance is
in meters -- a default of 5 centimeters (0.05)
is used.
"""
return self._distance_attribute('distance', geom, **kwargs)
def envelope(self, **kwargs):
"""
Returns a Geometry representing the bounding box of the
Geometry field in an `envelope` attribute on each element of
the GeoQuerySet.
"""
return self._geom_attribute('envelope', **kwargs)
def extent(self, **kwargs):
"""
Returns the extent (aggregate) of the features in the GeoQuerySet. The
extent will be returned as a 4-tuple, consisting of (xmin, ymin, xmax, ymax).
"""
convert_extent = None
if SpatialBackend.postgis:
def convert_extent(box, geo_field):
# TODO: Parsing of BOX3D, Oracle support (patches welcome!)
# Box text will be something like "BOX(-90.0 30.0, -85.0 40.0)";
# parsing out and returning as a 4-tuple.
ll, ur = box[4:-1].split(',')
xmin, ymin = map(float, ll.split())
xmax, ymax = map(float, ur.split())
return (xmin, ymin, xmax, ymax)
elif SpatialBackend.oracle:
def convert_extent(wkt, geo_field):
raise NotImplementedError
return self._spatial_aggregate('extent', convert_func=convert_extent, **kwargs)
def gml(self, precision=8, version=2, **kwargs):
"""
Returns GML representation of the given field in a `gml` attribute
on each element of the GeoQuerySet.
"""
s = {'desc' : 'GML', 'procedure_args' : {'precision' : precision}}
if SpatialBackend.postgis:
# PostGIS AsGML() aggregate function parameter order depends on the
# version -- uggh.
major, minor1, minor2 = SpatialBackend.version
if major >= 1 and (minor1 > 3 or (minor1 == 3 and minor2 > 1)):
procedure_fmt = '%(version)s,%(geo_col)s,%(precision)s'
else:
procedure_fmt = '%(geo_col)s,%(precision)s,%(version)s'
s['procedure_args'] = {'precision' : precision, 'version' : version}
return self._spatial_attribute('gml', s, **kwargs)
def intersection(self, geom, **kwargs):
"""
Returns the spatial intersection of the Geometry field in
an `intersection` attribute on each element of this
GeoQuerySet.
"""
return self._geomset_attribute('intersection', geom, **kwargs)
def kml(self, **kwargs):
"""
Returns KML representation of the geometry field in a `kml`
attribute on each element of this GeoQuerySet.
"""
s = {'desc' : 'KML',
'procedure_fmt' : '%(geo_col)s,%(precision)s',
'procedure_args' : {'precision' : kwargs.pop('precision', 8)},
}
return self._spatial_attribute('kml', s, **kwargs)
def length(self, **kwargs):
"""
Returns the length of the geometry field as a `Distance` object
stored in a `length` attribute on each element of this GeoQuerySet.
"""
return self._distance_attribute('length', None, **kwargs)
def make_line(self, **kwargs):
"""
Creates a linestring from all of the PointField geometries in the
this GeoQuerySet and returns it. This is a spatial aggregate
method, and thus returns a geometry rather than a GeoQuerySet.
"""
kwargs['geo_field_type'] = PointField
kwargs['agg_field'] = GeometryField
return self._spatial_aggregate('make_line', **kwargs)
def mem_size(self, **kwargs):
"""
Returns the memory size (number of bytes) that the geometry field takes
in a `mem_size` attribute on each element of this GeoQuerySet.
"""
return self._spatial_attribute('mem_size', {}, **kwargs)
def num_geom(self, **kwargs):
"""
Returns the number of geometries if the field is a
GeometryCollection or Multi* Field in a `num_geom`
attribute on each element of this GeoQuerySet; otherwise
the sets with None.
"""
return self._spatial_attribute('num_geom', {}, **kwargs)
def num_points(self, **kwargs):
"""
Returns the number of points in the first linestring in the
Geometry field in a `num_points` attribute on each element of
this GeoQuerySet; otherwise sets with None.
"""
return self._spatial_attribute('num_points', {}, **kwargs)
def perimeter(self, **kwargs):
"""
Returns the perimeter of the geometry field as a `Distance` object
stored in a `perimeter` attribute on each element of this GeoQuerySet.
"""
return self._distance_attribute('perimeter', None, **kwargs)
def point_on_surface(self, **kwargs):
"""
Returns a Point geometry guaranteed to lie on the surface of the
Geometry field in a `point_on_surface` attribute on each element
of this GeoQuerySet; otherwise sets with None.
"""
return self._geom_attribute('point_on_surface', **kwargs)
def scale(self, x, y, z=0.0, **kwargs):
"""
Scales the geometry to a new size by multiplying the ordinates
with the given x,y,z scale factors.
"""
s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s',
'procedure_args' : {'x' : x, 'y' : y, 'z' : z},
'select_field' : GeomField(),
}
return self._spatial_attribute('scale', s, **kwargs)
def svg(self, **kwargs):
"""
Returns SVG representation of the geographic field in a `svg`
attribute on each element of this GeoQuerySet.
"""
s = {'desc' : 'SVG',
'procedure_fmt' : '%(geo_col)s,%(rel)s,%(precision)s',
'procedure_args' : {'rel' : int(kwargs.pop('relative', 0)),
'precision' : kwargs.pop('precision', 8)},
}
return self._spatial_attribute('svg', s, **kwargs)
def sym_difference(self, geom, **kwargs):
"""
Returns the symmetric difference of the geographic field in a
`sym_difference` attribute on each element of this GeoQuerySet.
"""
return self._geomset_attribute('sym_difference', geom, **kwargs)
def translate(self, x, y, z=0.0, **kwargs):
"""
Translates the geometry to a new location using the given numeric
parameters as offsets.
"""
s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s',
'procedure_args' : {'x' : x, 'y' : y, 'z' : z},
'select_field' : GeomField(),
}
return self._spatial_attribute('translate', s, **kwargs)
def transform(self, srid=4326, **kwargs):
"""
Transforms the given geometry field to the given SRID. If no SRID is
provided, the transformation will default to using 4326 (WGS84).
"""
if not isinstance(srid, (int, long)):
raise TypeError('An integer SRID must be provided.')
field_name = kwargs.get('field_name', None)
tmp, geo_field = self._spatial_setup('transform', field_name=field_name)
# Getting the selection SQL for the given geographic field.
field_col = self._geocol_select(geo_field, field_name)
# Why cascading substitutions? Because spatial backends like
# Oracle and MySQL already require a function call to convert to text, thus
# when there's also a transformation we need to cascade the substitutions.
# For example, 'SDO_UTIL.TO_WKTGEOMETRY(SDO_CS.TRANSFORM( ... )'
geo_col = self.query.custom_select.get(geo_field, field_col)
# Setting the key for the field's column with the custom SELECT SQL to
# override the geometry column returned from the database.
custom_sel = '%s(%s, %s)' % (SpatialBackend.transform, geo_col, srid)
# TODO: Should we have this as an alias?
# custom_sel = '(%s(%s, %s)) AS %s' % (SpatialBackend.transform, geo_col, srid, qn(geo_field.name))
self.query.transformed_srid = srid # So other GeoQuerySet methods
self.query.custom_select[geo_field] = custom_sel
return self._clone()
def union(self, geom, **kwargs):
"""
Returns the union of the geographic field with the given
Geometry in a `union` attribute on each element of this GeoQuerySet.
"""
return self._geomset_attribute('union', geom, **kwargs)
def unionagg(self, **kwargs):
"""
Performs an aggregate union on the given geometry field. Returns
None if the GeoQuerySet is empty. The `tolerance` keyword is for
Oracle backends only.
"""
kwargs['agg_field'] = GeometryField
return self._spatial_aggregate('unionagg', **kwargs)
### Private API -- Abstracted DRY routines. ###
def _spatial_setup(self, att, aggregate=False, desc=None, field_name=None, geo_field_type=None):
"""
Performs set up for executing the spatial function.
"""
# Does the spatial backend support this?
func = getattr(SpatialBackend, att, False)
if desc is None: desc = att
if not func: raise ImproperlyConfigured('%s stored procedure not available.' % desc)
# Initializing the procedure arguments.
procedure_args = {'function' : func}
# Is there a geographic field in the model to perform this
# operation on?
geo_field = self.query._geo_field(field_name)
if not geo_field:
raise TypeError('%s output only available on GeometryFields.' % func)
# If the `geo_field_type` keyword was used, then enforce that
# type limitation.
if not geo_field_type is None and not isinstance(geo_field, geo_field_type):
raise TypeError('"%s" stored procedures may only be called on %ss.' % (func, geo_field_type.__name__))
# Setting the procedure args.
procedure_args['geo_col'] = self._geocol_select(geo_field, field_name, aggregate)
return procedure_args, geo_field
def _spatial_aggregate(self, att, field_name=None,
agg_field=None, convert_func=None,
geo_field_type=None, tolerance=0.0005):
"""
DRY routine for calling aggregate spatial stored procedures and
returning their result to the caller of the function.
"""
# Constructing the setup keyword arguments.
setup_kwargs = {'aggregate' : True,
'field_name' : field_name,
'geo_field_type' : geo_field_type,
}
procedure_args, geo_field = self._spatial_setup(att, **setup_kwargs)
if SpatialBackend.oracle:
procedure_args['tolerance'] = tolerance
# Adding in selection SQL for Oracle geometry columns.
if agg_field is GeometryField:
agg_sql = '%s' % SpatialBackend.select
else:
agg_sql = '%s'
agg_sql = agg_sql % ('%(function)s(SDOAGGRTYPE(%(geo_col)s,%(tolerance)s))' % procedure_args)
else:
agg_sql = '%(function)s(%(geo_col)s)' % procedure_args
# Wrapping our selection SQL in `GeomSQL` to bypass quoting, and
# specifying the type of the aggregate field.
self.query.select = [GeomSQL(agg_sql)]
self.query.select_fields = [agg_field]
try:
# `asql` => not overriding `sql` module.
asql, params = self.query.as_sql()
except sql.datastructures.EmptyResultSet:
return None
# Getting a cursor, executing the query, and extracting the returned
# value from the aggregate function.
cursor = connection.cursor()
cursor.execute(asql, params)
result = cursor.fetchone()[0]
# If the `agg_field` is specified as a GeometryField, then autmatically
# set up the conversion function.
if agg_field is GeometryField and not callable(convert_func):
if SpatialBackend.postgis:
def convert_geom(hex, geo_field):
if hex: return SpatialBackend.Geometry(hex)
else: return None
elif SpatialBackend.oracle:
def convert_geom(clob, geo_field):
if clob: return SpatialBackend.Geometry(clob.read(), geo_field._srid)
else: return None
convert_func = convert_geom
# Returning the callback function evaluated on the result culled
# from the executed cursor.
if callable(convert_func):
return convert_func(result, geo_field)
else:
return result
def _spatial_attribute(self, att, settings, field_name=None, model_att=None):
"""
DRY routine for calling a spatial stored procedure on a geometry column
and attaching its output as an attribute of the model.
Arguments:
att:
The name of the spatial attribute that holds the spatial
SQL function to call.
settings:
Dictonary of internal settings to customize for the spatial procedure.
Public Keyword Arguments:
field_name:
The name of the geographic field to call the spatial
function on. May also be a lookup to a geometry field
as part of a foreign key relation.
model_att:
The name of the model attribute to attach the output of
the spatial function to.
"""
# Default settings.
settings.setdefault('desc', None)
settings.setdefault('geom_args', ())
settings.setdefault('geom_field', None)
settings.setdefault('procedure_args', {})
settings.setdefault('procedure_fmt', '%(geo_col)s')
settings.setdefault('select_params', [])
# Performing setup for the spatial column, unless told not to.
if settings.get('setup', True):
default_args, geo_field = self._spatial_setup(att, desc=settings['desc'], field_name=field_name)
for k, v in default_args.iteritems(): settings['procedure_args'].setdefault(k, v)
else:
geo_field = settings['geo_field']
# The attribute to attach to the model.
if not isinstance(model_att, basestring): model_att = att
# Special handling for any argument that is a geometry.
for name in settings['geom_args']:
# Using the field's get_db_prep_lookup() to get any needed
# transformation SQL -- we pass in a 'dummy' `contains` lookup.
where, params = geo_field.get_db_prep_lookup('contains', settings['procedure_args'][name])
# Replacing the procedure format with that of any needed
# transformation SQL.
old_fmt = '%%(%s)s' % name
new_fmt = where[0] % '%%s'
settings['procedure_fmt'] = settings['procedure_fmt'].replace(old_fmt, new_fmt)
settings['select_params'].extend(params)
# Getting the format for the stored procedure.
fmt = '%%(function)s(%s)' % settings['procedure_fmt']
# If the result of this function needs to be converted.
if settings.get('select_field', False):
sel_fld = settings['select_field']
if isinstance(sel_fld, GeomField) and SpatialBackend.select:
self.query.custom_select[model_att] = SpatialBackend.select
self.query.extra_select_fields[model_att] = sel_fld
# Finally, setting the extra selection attribute with
# the format string expanded with the stored procedure
# arguments.
return self.extra(select={model_att : fmt % settings['procedure_args']},
select_params=settings['select_params'])
def _distance_attribute(self, func, geom=None, tolerance=0.05, spheroid=False, **kwargs):
"""
DRY routine for GeoQuerySet distance attribute routines.
"""
# Setting up the distance procedure arguments.
procedure_args, geo_field = self._spatial_setup(func, field_name=kwargs.get('field_name', None))
# If geodetic defaulting distance attribute to meters (Oracle and
# PostGIS spherical distances return meters). Otherwise, use the
# units of the geometry field.
if geo_field.geodetic:
dist_att = 'm'
else:
dist_att = Distance.unit_attname(geo_field._unit_name)
# Shortcut booleans for what distance function we're using.
distance = func == 'distance'
length = func == 'length'
perimeter = func == 'perimeter'
if not (distance or length or perimeter):
raise ValueError('Unknown distance function: %s' % func)
# The field's get_db_prep_lookup() is used to get any
# extra distance parameters. Here we set up the
# parameters that will be passed in to field's function.
lookup_params = [geom or 'POINT (0 0)', 0]
# If the spheroid calculation is desired, either by the `spheroid`
# keyword or wehn calculating the length of geodetic field, make
# sure the 'spheroid' distance setting string is passed in so we
# get the correct spatial stored procedure.
if spheroid or (SpatialBackend.postgis and geo_field.geodetic and length):
lookup_params.append('spheroid')
where, params = geo_field.get_db_prep_lookup('distance_lte', lookup_params)
# The `geom_args` flag is set to true if a geometry parameter was
# passed in.
geom_args = bool(geom)
if SpatialBackend.oracle:
if distance:
procedure_fmt = '%(geo_col)s,%(geom)s,%(tolerance)s'
elif length or perimeter:
procedure_fmt = '%(geo_col)s,%(tolerance)s'
procedure_args['tolerance'] = tolerance
else:
# Getting whether this field is in units of degrees since the field may have
# been transformed via the `transform` GeoQuerySet method.
if self.query.transformed_srid:
u, unit_name, s = get_srid_info(self.query.transformed_srid)
geodetic = unit_name in geo_field.geodetic_units
else:
geodetic = geo_field.geodetic
if distance:
if self.query.transformed_srid:
# Setting the `geom_args` flag to false because we want to handle
# transformation SQL here, rather than the way done by default
# (which will transform to the original SRID of the field rather
# than to what was transformed to).
geom_args = False
procedure_fmt = '%s(%%(geo_col)s, %s)' % (SpatialBackend.transform, self.query.transformed_srid)
if geom.srid is None or geom.srid == self.query.transformed_srid:
# If the geom parameter srid is None, it is assumed the coordinates
# are in the transformed units. A placeholder is used for the
# geometry parameter.
procedure_fmt += ', %%s'
else:
# We need to transform the geom to the srid specified in `transform()`,
# so wrapping the geometry placeholder in transformation SQL.
procedure_fmt += ', %s(%%%%s, %s)' % (SpatialBackend.transform, self.query.transformed_srid)
else:
# `transform()` was not used on this GeoQuerySet.
procedure_fmt = '%(geo_col)s,%(geom)s'
if geodetic:
# Spherical distance calculation is needed (because the geographic
# field is geodetic). However, the PostGIS ST_distance_sphere/spheroid()
# procedures may only do queries from point columns to point geometries
# some error checking is required.
if not isinstance(geo_field, PointField):
raise TypeError('Spherical distance calculation only supported on PointFields.')
if not str(SpatialBackend.Geometry(buffer(params[0].wkb)).geom_type) == 'Point':
raise TypeError('Spherical distance calculation only supported with Point Geometry parameters')
# The `function` procedure argument needs to be set differently for
# geodetic distance calculations.
if spheroid:
# Call to distance_spheroid() requires spheroid param as well.
procedure_fmt += ',%(spheroid)s'
procedure_args.update({'function' : SpatialBackend.distance_spheroid, 'spheroid' : where[1]})
else:
procedure_args.update({'function' : SpatialBackend.distance_sphere})
elif length or perimeter:
procedure_fmt = '%(geo_col)s'
if geodetic and length:
# There's no `length_sphere`
procedure_fmt += ',%(spheroid)s'
procedure_args.update({'function' : SpatialBackend.length_spheroid, 'spheroid' : where[1]})
# Setting up the settings for `_spatial_attribute`.
s = {'select_field' : DistanceField(dist_att),
'setup' : False,
'geo_field' : geo_field,
'procedure_args' : procedure_args,
'procedure_fmt' : procedure_fmt,
}
if geom_args:
s['geom_args'] = ('geom',)
s['procedure_args']['geom'] = geom
elif geom:
# The geometry is passed in as a parameter because we handled
# transformation conditions in this routine.
s['select_params'] = [SpatialBackend.Adaptor(geom)]
return self._spatial_attribute(func, s, **kwargs)
def _geom_attribute(self, func, tolerance=0.05, **kwargs):
"""
DRY routine for setting up a GeoQuerySet method that attaches a
Geometry attribute (e.g., `centroid`, `point_on_surface`).
"""
s = {'select_field' : GeomField(),}
if SpatialBackend.oracle:
s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s'
s['procedure_args'] = {'tolerance' : tolerance}
return self._spatial_attribute(func, s, **kwargs)
def _geomset_attribute(self, func, geom, tolerance=0.05, **kwargs):
"""
DRY routine for setting up a GeoQuerySet method that attaches a
Geometry attribute and takes a Geoemtry parameter. This is used
for geometry set-like operations (e.g., intersection, difference,
union, sym_difference).
"""
s = {'geom_args' : ('geom',),
'select_field' : GeomField(),
'procedure_fmt' : '%(geo_col)s,%(geom)s',
'procedure_args' : {'geom' : geom},
}
if SpatialBackend.oracle:
s['procedure_fmt'] += ',%(tolerance)s'
s['procedure_args']['tolerance'] = tolerance
return self._spatial_attribute(func, s, **kwargs)
def _geocol_select(self, geo_field, field_name, aggregate=False):
"""
Helper routine for constructing the SQL to select the geographic
column. Takes into account if the geographic field is in a
ForeignKey relation to the current model.
"""
# If this is an aggregate spatial query, the flag needs to be
# set on the `GeoQuery` object of this queryset.
if aggregate: self.query.aggregate = True
# Is this operation going to be on a related geographic field?
if not geo_field in self.model._meta.fields:
# If so, it'll have to be added to the select related information
# (e.g., if 'location__point' was given as the field name).
self.query.add_select_related([field_name])
self.query.pre_sql_setup()
rel_table, rel_col = self.query.related_select_cols[self.query.related_select_fields.index(geo_field)]
return self.query._field_column(geo_field, rel_table)
else:
return self.query._field_column(geo_field)

View File

@ -0,0 +1,2 @@
from django.contrib.gis.db.models.sql.query import AreaField, DistanceField, GeomField, GeoQuery
from django.contrib.gis.db.models.sql.where import GeoWhereNode

View File

@ -0,0 +1,327 @@
from itertools import izip
from django.db.models.query import sql
from django.db.models.fields import FieldDoesNotExist
from django.db.models.fields.related import ForeignKey
from django.contrib.gis.db.backend import SpatialBackend
from django.contrib.gis.db.models.fields import GeometryField
from django.contrib.gis.db.models.sql.where import GeoWhereNode
from django.contrib.gis.measure import Area, Distance
# Valid GIS query types.
ALL_TERMS = sql.constants.QUERY_TERMS.copy()
ALL_TERMS.update(SpatialBackend.gis_terms)
class GeoQuery(sql.Query):
"""
A single spatial SQL query.
"""
# Overridding the valid query terms.
query_terms = ALL_TERMS
#### Methods overridden from the base Query class ####
def __init__(self, model, conn):
super(GeoQuery, self).__init__(model, conn, where=GeoWhereNode)
# The following attributes are customized for the GeoQuerySet.
# The GeoWhereNode and SpatialBackend classes contain backend-specific
# routines and functions.
self.aggregate = False
self.custom_select = {}
self.transformed_srid = None
self.extra_select_fields = {}
def clone(self, *args, **kwargs):
obj = super(GeoQuery, self).clone(*args, **kwargs)
# Customized selection dictionary and transformed srid flag have
# to also be added to obj.
obj.aggregate = self.aggregate
obj.custom_select = self.custom_select.copy()
obj.transformed_srid = self.transformed_srid
obj.extra_select_fields = self.extra_select_fields.copy()
return obj
def get_columns(self, with_aliases=False):
"""
Return the list of columns to use in the select statement. If no
columns have been specified, returns all columns relating to fields in
the model.
If 'with_aliases' is true, any column names that are duplicated
(without the table names) are given unique aliases. This is needed in
some cases to avoid ambiguitity with nested queries.
This routine is overridden from Query to handle customized selection of
geometry columns.
"""
qn = self.quote_name_unless_alias
qn2 = self.connection.ops.quote_name
result = ['(%s) AS %s' % (self.get_extra_select_format(alias) % col, qn2(alias))
for alias, col in self.extra_select.iteritems()]
aliases = set(self.extra_select.keys())
if with_aliases:
col_aliases = aliases.copy()
else:
col_aliases = set()
if self.select:
# This loop customized for GeoQuery.
for col, field in izip(self.select, self.select_fields):
if isinstance(col, (list, tuple)):
r = self.get_field_select(field, col[0])
if with_aliases and col[1] in col_aliases:
c_alias = 'Col%d' % len(col_aliases)
result.append('%s AS %s' % (r, c_alias))
aliases.add(c_alias)
col_aliases.add(c_alias)
else:
result.append(r)
aliases.add(r)
col_aliases.add(col[1])
else:
result.append(col.as_sql(quote_func=qn))
if hasattr(col, 'alias'):
aliases.add(col.alias)
col_aliases.add(col.alias)
elif self.default_cols:
cols, new_aliases = self.get_default_columns(with_aliases,
col_aliases)
result.extend(cols)
aliases.update(new_aliases)
# This loop customized for GeoQuery.
if not self.aggregate:
for (table, col), field in izip(self.related_select_cols, self.related_select_fields):
r = self.get_field_select(field, table)
if with_aliases and col in col_aliases:
c_alias = 'Col%d' % len(col_aliases)
result.append('%s AS %s' % (r, c_alias))
aliases.add(c_alias)
col_aliases.add(c_alias)
else:
result.append(r)
aliases.add(r)
col_aliases.add(col)
self._select_aliases = aliases
return result
def get_default_columns(self, with_aliases=False, col_aliases=None,
start_alias=None, opts=None, as_pairs=False):
"""
Computes the default columns for selecting every field in the base
model.
Returns a list of strings, quoted appropriately for use in SQL
directly, as well as a set of aliases used in the select statement.
This routine is overridden from Query to handle customized selection of
geometry columns.
"""
result = []
if opts is None:
opts = self.model._meta
if start_alias:
table_alias = start_alias
else:
table_alias = self.tables[0]
root_pk = self.model._meta.pk.column
seen = {None: table_alias}
aliases = set()
for field, model in opts.get_fields_with_model():
try:
alias = seen[model]
except KeyError:
alias = self.join((table_alias, model._meta.db_table,
root_pk, model._meta.pk.column))
seen[model] = alias
if as_pairs:
result.append((alias, field.column))
continue
# This part of the function is customized for GeoQuery. We
# see if there was any custom selection specified in the
# dictionary, and set up the selection format appropriately.
field_sel = self.get_field_select(field, alias)
if with_aliases and field.column in col_aliases:
c_alias = 'Col%d' % len(col_aliases)
result.append('%s AS %s' % (field_sel, c_alias))
col_aliases.add(c_alias)
aliases.add(c_alias)
else:
r = field_sel
result.append(r)
aliases.add(r)
if with_aliases:
col_aliases.add(field.column)
if as_pairs:
return result, None
return result, aliases
def get_ordering(self):
"""
This routine is overridden to disable ordering for aggregate
spatial queries.
"""
if not self.aggregate:
return super(GeoQuery, self).get_ordering()
else:
return ()
def resolve_columns(self, row, fields=()):
"""
This routine is necessary so that distances and geometries returned
from extra selection SQL get resolved appropriately into Python
objects.
"""
values = []
aliases = self.extra_select.keys()
index_start = len(aliases)
values = [self.convert_values(v, self.extra_select_fields.get(a, None))
for v, a in izip(row[:index_start], aliases)]
if SpatialBackend.oracle:
# This is what happens normally in Oracle's `resolve_columns`.
for value, field in izip(row[index_start:], fields):
values.append(self.convert_values(value, field))
else:
values.extend(row[index_start:])
return values
def convert_values(self, value, field):
"""
Using the same routines that Oracle does we can convert our
extra selection objects into Geometry and Distance objects.
TODO: Laziness.
"""
if SpatialBackend.oracle:
# Running through Oracle's first.
value = super(GeoQuery, self).convert_values(value, field)
if isinstance(field, DistanceField):
# Using the field's distance attribute, can instantiate
# `Distance` with the right context.
value = Distance(**{field.distance_att : value})
elif isinstance(field, AreaField):
value = Area(**{field.area_att : value})
elif isinstance(field, GeomField):
value = SpatialBackend.Geometry(value)
return value
#### Routines unique to GeoQuery ####
def get_extra_select_format(self, alias):
sel_fmt = '%s'
if alias in self.custom_select:
sel_fmt = sel_fmt % self.custom_select[alias]
return sel_fmt
def get_field_select(self, fld, alias=None):
"""
Returns the SELECT SQL string for the given field. Figures out
if any custom selection SQL is needed for the column The `alias`
keyword may be used to manually specify the database table where
the column exists, if not in the model associated with this
`GeoQuery`.
"""
sel_fmt = self.get_select_format(fld)
if fld in self.custom_select:
field_sel = sel_fmt % self.custom_select[fld]
else:
field_sel = sel_fmt % self._field_column(fld, alias)
return field_sel
def get_select_format(self, fld):
"""
Returns the selection format string, depending on the requirements
of the spatial backend. For example, Oracle and MySQL require custom
selection formats in order to retrieve geometries in OGC WKT. For all
other fields a simple '%s' format string is returned.
"""
if SpatialBackend.select and hasattr(fld, '_geom'):
# This allows operations to be done on fields in the SELECT,
# overriding their values -- used by the Oracle and MySQL
# spatial backends to get database values as WKT, and by the
# `transform` method.
sel_fmt = SpatialBackend.select
# Because WKT doesn't contain spatial reference information,
# the SRID is prefixed to the returned WKT to ensure that the
# transformed geometries have an SRID different than that of the
# field -- this is only used by `transform` for Oracle backends.
if self.transformed_srid and SpatialBackend.oracle:
sel_fmt = "'SRID=%d;'||%s" % (self.transformed_srid, sel_fmt)
else:
sel_fmt = '%s'
return sel_fmt
# Private API utilities, subject to change.
def _check_geo_field(self, model, name_param):
"""
Recursive utility routine for checking the given name parameter
on the given model. Initially, the name parameter is a string,
of the field on the given model e.g., 'point', 'the_geom'.
Related model field strings like 'address__point', may also be
used.
If a GeometryField exists according to the given name parameter
it will be returned, otherwise returns False.
"""
if isinstance(name_param, basestring):
# This takes into account the situation where the name is a
# lookup to a related geographic field, e.g., 'address__point'.
name_param = name_param.split(sql.constants.LOOKUP_SEP)
name_param.reverse() # Reversing so list operates like a queue of related lookups.
elif not isinstance(name_param, list):
raise TypeError
try:
# Getting the name of the field for the model (by popping the first
# name from the `name_param` list created above).
fld, mod, direct, m2m = model._meta.get_field_by_name(name_param.pop())
except (FieldDoesNotExist, IndexError):
return False
# TODO: ManyToManyField?
if isinstance(fld, GeometryField):
return fld # A-OK.
elif isinstance(fld, ForeignKey):
# ForeignKey encountered, return the output of this utility called
# on the _related_ model with the remaining name parameters.
return self._check_geo_field(fld.rel.to, name_param) # Recurse to check ForeignKey relation.
else:
return False
def _field_column(self, field, table_alias=None):
"""
Helper function that returns the database column for the given field.
The table and column are returned (quoted) in the proper format, e.g.,
`"geoapp_city"."point"`. If `table_alias` is not specified, the
database table associated with the model of this `GeoQuery` will be
used.
"""
if table_alias is None: table_alias = self.model._meta.db_table
return "%s.%s" % (self.quote_name_unless_alias(table_alias),
self.connection.ops.quote_name(field.column))
def _geo_field(self, field_name=None):
"""
Returns the first Geometry field encountered; or specified via the
`field_name` keyword. The `field_name` may be a string specifying
the geometry field on this GeoQuery's model, or a lookup string
to a geometry field via a ForeignKey relation.
"""
if field_name is None:
# Incrementing until the first geographic field is found.
for fld in self.model._meta.fields:
if isinstance(fld, GeometryField): return fld
return False
else:
# Otherwise, check by the given field name -- which may be
# a lookup to a _related_ geographic field.
return self._check_geo_field(self.model, field_name)
### Field Classes for `convert_values` ####
class AreaField(object):
def __init__(self, area_att):
self.area_att = area_att
class DistanceField(object):
def __init__(self, distance_att):
self.distance_att = distance_att
# Rather than use GeometryField (which requires a SQL query
# upon instantiation), use this lighter weight class.
class GeomField(object):
pass

View File

@ -0,0 +1,64 @@
import datetime
from django.db.models.fields import Field
from django.db.models.sql.where import WhereNode
from django.contrib.gis.db.backend import get_geo_where_clause, SpatialBackend
class GeoAnnotation(object):
"""
The annotation used for GeometryFields; basically a placeholder
for metadata needed by the `get_geo_where_clause` of the spatial
backend.
"""
def __init__(self, field, value, where):
self.geodetic = field.geodetic
self.geom_type = field._geom
self.value = value
self.where = tuple(where)
class GeoWhereNode(WhereNode):
"""
Used to represent the SQL where-clause for spatial databases --
these are tied to the GeoQuery class that created it.
"""
def add(self, data, connector):
"""
This is overridden from the regular WhereNode to handle the
peculiarties of GeometryFields, because they need a special
annotation object that contains the spatial metadata from the
field to generate the spatial SQL.
"""
if not isinstance(data, (list, tuple)):
return super(WhereNode, self).add(data, connector)
alias, col, field, lookup_type, value = data
if not hasattr(field, "_geom"):
# Not a geographic field, so call `WhereNode.add`.
return super(GeoWhereNode, self).add(data, connector)
else:
# `GeometryField.get_db_prep_lookup` returns a where clause
# substitution array in addition to the parameters.
where, params = field.get_db_prep_lookup(lookup_type, value)
# The annotation will be a `GeoAnnotation` object that
# will contain the necessary geometry field metadata for
# the `get_geo_where_clause` to construct the appropriate
# spatial SQL when `make_atom` is called.
annotation = GeoAnnotation(field, value, where)
return super(WhereNode, self).add((alias, col, field.db_type(), lookup_type,
annotation, params), connector)
def make_atom(self, child, qn):
table_alias, name, db_type, lookup_type, value_annot, params = child
if isinstance(value_annot, GeoAnnotation):
if lookup_type in SpatialBackend.gis_terms:
# Getting the geographic where clause; substitution parameters
# will be populated in the GeoFieldSQL object returned by the
# GeometryField.
gwc = get_geo_where_clause(table_alias, name, lookup_type, value_annot)
return gwc % value_annot.where, params
else:
raise TypeError('Invalid lookup type: %r' % lookup_type)
else:
# If not a GeometryField, call the `make_atom` from the
# base class.
return super(GeoWhereNode, self).make_atom(child, qn)

View File

@ -0,0 +1 @@
from django.contrib.gis.forms.fields import GeometryField

View File

@ -0,0 +1,37 @@
from django import forms
from django.contrib.gis.geos import GEOSGeometry, GEOSException
from django.utils.translation import ugettext_lazy as _
class GeometryField(forms.Field):
# By default a Textarea widget is used.
widget = forms.Textarea
default_error_messages = {
'no_geom' : _(u'No geometry value provided.'),
'invalid_geom' : _(u'Invalid Geometry value.'),
'invalid_geom_type' : _(u'Invalid Geometry type.'),
}
def __init__(self, **kwargs):
self.null = kwargs.pop('null')
self.geom_type = kwargs.pop('geom_type')
super(GeometryField, self).__init__(**kwargs)
def clean(self, value):
"""
Validates that the input value can be converted to a Geometry
object (which is returned). A ValidationError is raised if
the value cannot be instantiated as a Geometry.
"""
if not value:
if self.null:
# The geometry column allows NULL, return None.
return None
else:
raise forms.ValidationError(self.error_messages['no_geom'])
try:
geom = GEOSGeometry(value)
if geom.geom_type.upper() != self.geom_type:
raise forms.ValidationError(self.error_messages['invalid_geom_type'])
return geom
except GEOSException:
raise forms.ValidationError(self.error_messages['invalid_geom'])

View File

@ -0,0 +1,28 @@
Copyright (c) 2007, Justin Bronn
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of OGRGeometry nor the names of its contributors may be used
to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,51 @@
"""
This module houses ctypes interfaces for GDAL objects. The following GDAL
objects are supported:
CoordTransform: Used for coordinate transformations from one spatial
reference system to another.
Driver: Wraps an OGR data source driver.
DataSource: Wrapper for the OGR data source object, supports
OGR-supported data sources.
Envelope: A ctypes structure for bounding boxes (GDAL library
not required).
OGRGeometry: Layer for accessing OGR Geometry objects.
OGRGeomType: A class for representing the different OGR Geometry
types (GDAL library not required).
SpatialReference: Represents OSR Spatial Reference objects.
The GDAL library will be imported from the system path using the default
library name for the current OS. The default library path may be overridden
by setting `GDAL_LIBRARY_PATH` in your settings with the path to the GDAL C
library on your system.
GDAL links to a large number of external libraries that consume RAM when
loaded. Thus, it may desirable to disable GDAL on systems with limited
RAM resources -- this may be accomplished by setting `GDAL_LIBRARY_PATH`
to a non-existant file location (e.g., `GDAL_LIBRARY_PATH='/null/path'`;
setting to None/False/'' will not work as a string must be given).
"""
# Attempting to import objects that depend on the GDAL library. The
# HAS_GDAL flag will be set to True if the library is present on
# the system.
try:
from django.contrib.gis.gdal.driver import Driver
from django.contrib.gis.gdal.datasource import DataSource
from django.contrib.gis.gdal.libgdal import gdal_version, gdal_full_version, gdal_release_date
from django.contrib.gis.gdal.srs import SpatialReference, CoordTransform
from django.contrib.gis.gdal.geometries import OGRGeometry, GEOJSON
HAS_GDAL = True
except:
HAS_GDAL, GEOJSON = False, False
# The envelope, error, and geomtype modules do not actually require the
# GDAL library.
from django.contrib.gis.gdal.envelope import Envelope
from django.contrib.gis.gdal.error import check_err, OGRException, OGRIndexError, SRSException
from django.contrib.gis.gdal.geomtype import OGRGeomType

View File

@ -0,0 +1,138 @@
"""
DataSource is a wrapper for the OGR Data Source object, which provides
an interface for reading vector geometry data from many different file
formats (including ESRI shapefiles).
When instantiating a DataSource object, use the filename of a
GDAL-supported data source. For example, a SHP file or a
TIGER/Line file from the government.
The ds_driver keyword is used internally when a ctypes pointer
is passed in directly.
Example:
ds = DataSource('/home/foo/bar.shp')
for layer in ds:
for feature in layer:
# Getting the geometry for the feature.
g = feature.geom
# Getting the 'description' field for the feature.
desc = feature['description']
# We can also increment through all of the fields
# attached to this feature.
for field in feature:
# Get the name of the field (e.g. 'description')
nm = field.name
# Get the type (integer) of the field, e.g. 0 => OFTInteger
t = field.type
# Returns the value the field; OFTIntegers return ints,
# OFTReal returns floats, all else returns string.
val = field.value
"""
# ctypes prerequisites.
from ctypes import byref, c_void_p
# The GDAL C library, OGR exceptions, and the Layer object.
from django.contrib.gis.gdal.driver import Driver
from django.contrib.gis.gdal.error import OGRException, OGRIndexError
from django.contrib.gis.gdal.layer import Layer
# Getting the ctypes prototypes for the DataSource.
from django.contrib.gis.gdal.prototypes.ds import \
destroy_ds, get_driver_count, register_all, open_ds, release_ds, \
get_ds_name, get_layer, get_layer_count, get_layer_by_name
# For more information, see the OGR C API source code:
# http://www.gdal.org/ogr/ogr__api_8h.html
#
# The OGR_DS_* routines are relevant here.
class DataSource(object):
"Wraps an OGR Data Source object."
#### Python 'magic' routines ####
def __init__(self, ds_input, ds_driver=False, write=False):
# DataSource pointer is initially NULL.
self._ptr = None
# The write flag.
if write:
self._write = 1
else:
self._write = 0
# Registering all the drivers, this needs to be done
# _before_ we try to open up a data source.
if not get_driver_count(): register_all()
if isinstance(ds_input, basestring):
# The data source driver is a void pointer.
ds_driver = c_void_p()
try:
# OGROpen will auto-detect the data source type.
ds = open_ds(ds_input, self._write, byref(ds_driver))
except OGRException:
# Making the error message more clear rather than something
# like "Invalid pointer returned from OGROpen".
raise OGRException('Could not open the datasource at "%s"' % ds_input)
elif isinstance(ds_input, c_void_p) and isinstance(ds_driver, c_void_p):
ds = ds_input
else:
raise OGRException('Invalid data source input type: %s' % type(ds_input))
if bool(ds):
self._ptr = ds
self._driver = Driver(ds_driver)
else:
# Raise an exception if the returned pointer is NULL
raise OGRException('Invalid data source file "%s"' % ds_input)
def __del__(self):
"Destroys this DataStructure object."
if self._ptr: destroy_ds(self._ptr)
def __iter__(self):
"Allows for iteration over the layers in a data source."
for i in xrange(self.layer_count):
yield self[i]
def __getitem__(self, index):
"Allows use of the index [] operator to get a layer at the index."
if isinstance(index, basestring):
l = get_layer_by_name(self._ptr, index)
if not l: raise OGRIndexError('invalid OGR Layer name given: "%s"' % index)
elif isinstance(index, int):
if index < 0 or index >= self.layer_count:
raise OGRIndexError('index out of range')
l = get_layer(self._ptr, index)
else:
raise TypeError('Invalid index type: %s' % type(index))
return Layer(l)
def __len__(self):
"Returns the number of layers within the data source."
return self.layer_count
def __str__(self):
"Returns OGR GetName and Driver for the Data Source."
return '%s (%s)' % (self.name, str(self.driver))
#### DataSource Properties ####
@property
def driver(self):
"Returns the Driver object for this Data Source."
return self._driver
@property
def layer_count(self):
"Returns the number of layers in the data source."
return get_layer_count(self._ptr)
@property
def name(self):
"Returns the name of the data source."
return get_ds_name(self._ptr)

View File

@ -0,0 +1,66 @@
# prerequisites imports
from ctypes import c_void_p
from django.contrib.gis.gdal.error import OGRException
from django.contrib.gis.gdal.prototypes.ds import \
get_driver, get_driver_by_name, get_driver_count, get_driver_name, register_all
# For more information, see the OGR C API source code:
# http://www.gdal.org/ogr/ogr__api_8h.html
#
# The OGR_Dr_* routines are relevant here.
class Driver(object):
"Wraps an OGR Data Source Driver."
# Case-insensitive aliases for OGR Drivers.
_alias = {'esri' : 'ESRI Shapefile',
'shp' : 'ESRI Shapefile',
'shape' : 'ESRI Shapefile',
'tiger' : 'TIGER',
'tiger/line' : 'TIGER',
}
def __init__(self, dr_input):
"Initializes an OGR driver on either a string or integer input."
if isinstance(dr_input, basestring):
# If a string name of the driver was passed in
self._ptr = None # Initially NULL
self._register()
# Checking the alias dictionary (case-insensitive) to see if an alias
# exists for the given driver.
if dr_input.lower() in self._alias:
name = self._alias[dr_input.lower()]
else:
name = dr_input
# Attempting to get the OGR driver by the string name.
dr = get_driver_by_name(name)
elif isinstance(dr_input, int):
self._register()
dr = get_driver(dr_input)
elif isinstance(dr_input, c_void_p):
dr = dr_input
else:
raise OGRException('Unrecognized input type for OGR Driver: %s' % str(type(dr_input)))
# Making sure we get a valid pointer to the OGR Driver
if not dr:
raise OGRException('Could not initialize OGR Driver on input: %s' % str(dr_input))
self._ptr = dr
def __str__(self):
"Returns the string name of the OGR Driver."
return get_driver_name(self._ptr)
def _register(self):
"Attempts to register all the data source drivers."
# Only register all if the driver count is 0 (or else all drivers
# will be registered over and over again)
if not self.driver_count: register_all()
# Driver properties
@property
def driver_count(self):
"Returns the number of OGR data source drivers registered."
return get_driver_count()

View File

@ -0,0 +1,134 @@
"""
The GDAL/OGR library uses an Envelope structure to hold the bounding
box information for a geometry. The envelope (bounding box) contains
two pairs of coordinates, one for the lower left coordinate and one
for the upper right coordinate:
+----------o Upper right; (max_x, max_y)
| |
| |
| |
Lower left (min_x, min_y) o----------+
"""
from ctypes import Structure, c_double
from types import TupleType, ListType
from django.contrib.gis.gdal.error import OGRException
# The OGR definition of an Envelope is a C structure containing four doubles.
# See the 'ogr_core.h' source file for more information:
# http://www.gdal.org/ogr/ogr__core_8h-source.html
class OGREnvelope(Structure):
"Represents the OGREnvelope C Structure."
_fields_ = [("MinX", c_double),
("MaxX", c_double),
("MinY", c_double),
("MaxY", c_double),
]
class Envelope(object):
"""
The Envelope object is a C structure that contains the minimum and
maximum X, Y coordinates for a rectangle bounding box. The naming
of the variables is compatible with the OGR Envelope structure.
"""
def __init__(self, *args):
"""
The initialization function may take an OGREnvelope structure, 4-element
tuple or list, or 4 individual arguments.
"""
if len(args) == 1:
if isinstance(args[0], OGREnvelope):
# OGREnvelope (a ctypes Structure) was passed in.
self._envelope = args[0]
elif isinstance(args[0], (TupleType, ListType)):
# A tuple was passed in.
if len(args[0]) != 4:
raise OGRException('Incorrect number of tuple elements (%d).' % len(args[0]))
else:
self._from_sequence(args[0])
else:
raise TypeError('Incorrect type of argument: %s' % str(type(args[0])))
elif len(args) == 4:
# Individiual parameters passed in.
# Thanks to ww for the help
self._from_sequence(map(float, args))
else:
raise OGRException('Incorrect number (%d) of arguments.' % len(args))
# Checking the x,y coordinates
if self.min_x >= self.max_x:
raise OGRException('Envelope minimum X >= maximum X.')
if self.min_y >= self.max_y:
raise OGRException('Envelope minimum Y >= maximum Y.')
def __eq__(self, other):
"""
Returns True if the envelopes are equivalent; can compare against
other Envelopes and 4-tuples.
"""
if isinstance(other, Envelope):
return (self.min_x == other.min_x) and (self.min_y == other.min_y) and \
(self.max_x == other.max_x) and (self.max_y == other.max_y)
elif isinstance(other, TupleType) and len(other) == 4:
return (self.min_x == other[0]) and (self.min_y == other[1]) and \
(self.max_x == other[2]) and (self.max_y == other[3])
else:
raise OGRException('Equivalence testing only works with other Envelopes.')
def __str__(self):
"Returns a string representation of the tuple."
return str(self.tuple)
def _from_sequence(self, seq):
"Initializes the C OGR Envelope structure from the given sequence."
self._envelope = OGREnvelope()
self._envelope.MinX = seq[0]
self._envelope.MinY = seq[1]
self._envelope.MaxX = seq[2]
self._envelope.MaxY = seq[3]
@property
def min_x(self):
"Returns the value of the minimum X coordinate."
return self._envelope.MinX
@property
def min_y(self):
"Returns the value of the minimum Y coordinate."
return self._envelope.MinY
@property
def max_x(self):
"Returns the value of the maximum X coordinate."
return self._envelope.MaxX
@property
def max_y(self):
"Returns the value of the maximum Y coordinate."
return self._envelope.MaxY
@property
def ur(self):
"Returns the upper-right coordinate."
return (self.max_x, self.max_y)
@property
def ll(self):
"Returns the lower-left coordinate."
return (self.min_x, self.min_y)
@property
def tuple(self):
"Returns a tuple representing the envelope."
return (self.min_x, self.min_y, self.max_x, self.max_y)
@property
def wkt(self):
"Returns WKT representing a Polygon for this envelope."
# TODO: Fix significant figures.
return 'POLYGON((%s %s,%s %s,%s %s,%s %s,%s %s))' % \
(self.min_x, self.min_y, self.min_x, self.max_y,
self.max_x, self.max_y, self.max_x, self.min_y,
self.min_x, self.min_y)

View File

@ -0,0 +1,41 @@
"""
This module houses the OGR & SRS Exception objects, and the
check_err() routine which checks the status code returned by
OGR methods.
"""
#### OGR & SRS Exceptions ####
class OGRException(Exception): pass
class SRSException(Exception): pass
class OGRIndexError(OGRException, KeyError):
"""
This exception is raised when an invalid index is encountered, and has
the 'silent_variable_feature' attribute set to true. This ensures that
django's templates proceed to use the next lookup type gracefully when
an Exception is raised. Fixes ticket #4740.
"""
silent_variable_failure = True
#### OGR error checking codes and routine ####
# OGR Error Codes
OGRERR_DICT = { 1 : (OGRException, 'Not enough data.'),
2 : (OGRException, 'Not enough memory.'),
3 : (OGRException, 'Unsupported geometry type.'),
4 : (OGRException, 'Unsupported operation.'),
5 : (OGRException, 'Corrupt data.'),
6 : (OGRException, 'OGR failure.'),
7 : (SRSException, 'Unsupported SRS.'),
8 : (OGRException, 'Invalid handle.'),
}
OGRERR_NONE = 0
def check_err(code):
"Checks the given OGRERR, and raises an exception where appropriate."
if code == OGRERR_NONE:
return
elif code in OGRERR_DICT:
e, msg = OGRERR_DICT[code]
raise e, msg
else:
raise OGRException('Unknown error code: "%s"' % code)

View File

@ -0,0 +1,115 @@
# The GDAL C library, OGR exception, and the Field object
from django.contrib.gis.gdal.error import OGRException, OGRIndexError
from django.contrib.gis.gdal.field import Field
from django.contrib.gis.gdal.geometries import OGRGeometry, OGRGeomType
from django.contrib.gis.gdal.srs import SpatialReference
# ctypes function prototypes
from django.contrib.gis.gdal.prototypes.ds import \
destroy_feature, feature_equal, get_fd_geom_type, get_feat_geom_ref, \
get_feat_name, get_feat_field_count, get_fid, get_field_defn, \
get_field_index, get_field_name
from django.contrib.gis.gdal.prototypes.geom import clone_geom, get_geom_srs
from django.contrib.gis.gdal.prototypes.srs import clone_srs
# For more information, see the OGR C API source code:
# http://www.gdal.org/ogr/ogr__api_8h.html
#
# The OGR_F_* routines are relevant here.
class Feature(object):
"A class that wraps an OGR Feature, needs to be instantiated from a Layer object."
#### Python 'magic' routines ####
def __init__(self, feat, fdefn):
"Initializes on the pointers for the feature and the layer definition."
self._ptr = None # Initially NULL
if not feat or not fdefn:
raise OGRException('Cannot create OGR Feature, invalid pointer given.')
self._ptr = feat
self._fdefn = fdefn
def __del__(self):
"Releases a reference to this object."
if self._ptr: destroy_feature(self._ptr)
def __getitem__(self, index):
"""
Gets the Field object at the specified index, which may be either
an integer or the Field's string label. Note that the Field object
is not the field's _value_ -- use the `get` method instead to
retrieve the value (e.g. an integer) instead of a Field instance.
"""
if isinstance(index, basestring):
i = self.index(index)
else:
if index < 0 or index > self.num_fields:
raise OGRIndexError('index out of range')
i = index
return Field(self._ptr, i)
def __iter__(self):
"Iterates over each field in the Feature."
for i in xrange(self.num_fields):
yield self[i]
def __len__(self):
"Returns the count of fields in this feature."
return self.num_fields
def __str__(self):
"The string name of the feature."
return 'Feature FID %d in Layer<%s>' % (self.fid, self.layer_name)
def __eq__(self, other):
"Does equivalence testing on the features."
return bool(feature_equal(self._ptr, other._ptr))
#### Feature Properties ####
@property
def fid(self):
"Returns the feature identifier."
return get_fid(self._ptr)
@property
def layer_name(self):
"Returns the name of the layer for the feature."
return get_feat_name(self._fdefn)
@property
def num_fields(self):
"Returns the number of fields in the Feature."
return get_feat_field_count(self._ptr)
@property
def fields(self):
"Returns a list of fields in the Feature."
return [get_field_name(get_field_defn(self._fdefn, i))
for i in xrange(self.num_fields)]
@property
def geom(self):
"Returns the OGR Geometry for this Feature."
# Retrieving the geometry pointer for the feature.
geom_ptr = get_feat_geom_ref(self._ptr)
return OGRGeometry(clone_geom(geom_ptr))
@property
def geom_type(self):
"Returns the OGR Geometry Type for this Feture."
return OGRGeomType(get_fd_geom_type(self._fdefn))
#### Feature Methods ####
def get(self, field):
"""
Returns the value of the field, instead of an instance of the Field
object. May take a string of the field name or a Field object as
parameters.
"""
field_name = getattr(field, 'name', field)
return self[field_name].value
def index(self, field_name):
"Returns the index of the given field name."
i = get_field_index(self._ptr, field_name)
if i < 0: raise OGRIndexError('invalid OFT field name given: "%s"' % field_name)
return i

View File

@ -0,0 +1,179 @@
from ctypes import byref, c_int
from datetime import date, datetime, time
from django.contrib.gis.gdal.error import OGRException
from django.contrib.gis.gdal.prototypes.ds import \
get_feat_field_defn, get_field_as_datetime, get_field_as_double, \
get_field_as_integer, get_field_as_string, get_field_name, get_field_precision, \
get_field_type, get_field_type_name, get_field_width
# For more information, see the OGR C API source code:
# http://www.gdal.org/ogr/ogr__api_8h.html
#
# The OGR_Fld_* routines are relevant here.
class Field(object):
"A class that wraps an OGR Field, needs to be instantiated from a Feature object."
#### Python 'magic' routines ####
def __init__(self, feat, index):
"""
Initializes on the feature pointer and the integer index of
the field within the feature.
"""
# Setting the feature pointer and index.
self._feat = feat
self._index = index
# Getting the pointer for this field.
fld = get_feat_field_defn(feat, index)
if not fld:
raise OGRException('Cannot create OGR Field, invalid pointer given.')
self._ptr = fld
# Setting the class depending upon the OGR Field Type (OFT)
self.__class__ = FIELD_CLASSES[self.type]
# OFTReal with no precision should be an OFTInteger.
if isinstance(self, OFTReal) and self.precision == 0:
self.__class__ = OFTInteger
def __str__(self):
"Returns the string representation of the Field."
return str(self.value).strip()
#### Field Methods ####
def as_double(self):
"Retrieves the Field's value as a double (float)."
return get_field_as_double(self._feat, self._index)
def as_int(self):
"Retrieves the Field's value as an integer."
return get_field_as_integer(self._feat, self._index)
def as_string(self):
"Retrieves the Field's value as a string."
return get_field_as_string(self._feat, self._index)
def as_datetime(self):
"Retrieves the Field's value as a tuple of date & time components."
yy, mm, dd, hh, mn, ss, tz = [c_int() for i in range(7)]
status = get_field_as_datetime(self._feat, self._index, byref(yy), byref(mm), byref(dd),
byref(hh), byref(mn), byref(ss), byref(tz))
if status:
return (yy, mm, dd, hh, mn, ss, tz)
else:
raise OGRException('Unable to retrieve date & time information from the field.')
#### Field Properties ####
@property
def name(self):
"Returns the name of this Field."
return get_field_name(self._ptr)
@property
def precision(self):
"Returns the precision of this Field."
return get_field_precision(self._ptr)
@property
def type(self):
"Returns the OGR type of this Field."
return get_field_type(self._ptr)
@property
def type_name(self):
"Return the OGR field type name for this Field."
return get_field_type_name(self.type)
@property
def value(self):
"Returns the value of this Field."
# Default is to get the field as a string.
return self.as_string()
@property
def width(self):
"Returns the width of this Field."
return get_field_width(self._ptr)
### The Field sub-classes for each OGR Field type. ###
class OFTInteger(Field):
@property
def value(self):
"Returns an integer contained in this field."
return self.as_int()
@property
def type(self):
"""
GDAL uses OFTReals to represent OFTIntegers in created
shapefiles -- forcing the type here since the underlying field
type may actually be OFTReal.
"""
return 0
class OFTReal(Field):
@property
def value(self):
"Returns a float contained in this field."
return self.as_double()
# String & Binary fields, just subclasses
class OFTString(Field): pass
class OFTWideString(Field): pass
class OFTBinary(Field): pass
# OFTDate, OFTTime, OFTDateTime fields.
class OFTDate(Field):
@property
def value(self):
"Returns a Python `date` object for the OFTDate field."
yy, mm, dd, hh, mn, ss, tz = self.as_datetime()
try:
return date(yy.value, mm.value, dd.value)
except ValueError:
return None
class OFTDateTime(Field):
@property
def value(self):
"Returns a Python `datetime` object for this OFTDateTime field."
yy, mm, dd, hh, mn, ss, tz = self.as_datetime()
# TODO: Adapt timezone information.
# See http://lists.maptools.org/pipermail/gdal-dev/2006-February/007990.html
# The `tz` variable has values of: 0=unknown, 1=localtime (ambiguous),
# 100=GMT, 104=GMT+1, 80=GMT-5, etc.
try:
return datetime(yy.value, mm.value, dd.value, hh.value, mn.value, ss.value)
except ValueError:
return None
class OFTTime(Field):
@property
def value(self):
"Returns a Python `time` object for this OFTTime field."
yy, mm, dd, hh, mn, ss, tz = self.as_datetime()
try:
return time(hh.value, mn.value, ss.value)
except ValueError:
return None
# List fields are also just subclasses
class OFTIntegerList(Field): pass
class OFTRealList(Field): pass
class OFTStringList(Field): pass
class OFTWideStringList(Field): pass
# Class mapping dictionary for OFT Types
FIELD_CLASSES = { 0 : OFTInteger,
1 : OFTIntegerList,
2 : OFTReal,
3 : OFTRealList,
4 : OFTString,
5 : OFTStringList,
6 : OFTWideString,
7 : OFTWideStringList,
8 : OFTBinary,
9 : OFTDate,
10 : OFTTime,
11 : OFTDateTime,
}

View File

@ -0,0 +1,643 @@
"""
The OGRGeometry is a wrapper for using the OGR Geometry class
(see http://www.gdal.org/ogr/classOGRGeometry.html). OGRGeometry
may be instantiated when reading geometries from OGR Data Sources
(e.g. SHP files), or when given OGC WKT (a string).
While the 'full' API is not present yet, the API is "pythonic" unlike
the traditional and "next-generation" OGR Python bindings. One major
advantage OGR Geometries have over their GEOS counterparts is support
for spatial reference systems and their transformation.
Example:
>>> from django.contrib.gis.gdal import OGRGeometry, OGRGeomType, SpatialReference
>>> wkt1, wkt2 = 'POINT(-90 30)', 'POLYGON((0 0, 5 0, 5 5, 0 5)'
>>> pnt = OGRGeometry(wkt1)
>>> print pnt
POINT (-90 30)
>>> mpnt = OGRGeometry(OGRGeomType('MultiPoint'), SpatialReference('WGS84'))
>>> mpnt.add(wkt1)
>>> mpnt.add(wkt1)
>>> print mpnt
MULTIPOINT (-90 30,-90 30)
>>> print mpnt.srs.name
WGS 84
>>> print mpnt.srs.proj
+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs
>>> mpnt.transform_to(SpatialReference('NAD27'))
>>> print mpnt.proj
+proj=longlat +ellps=clrk66 +datum=NAD27 +no_defs
>>> print mpnt
MULTIPOINT (-89.999930378602485 29.999797886557641,-89.999930378602485 29.999797886557641)
The OGRGeomType class is to make it easy to specify an OGR geometry type:
>>> from django.contrib.gis.gdal import OGRGeomType
>>> gt1 = OGRGeomType(3) # Using an integer for the type
>>> gt2 = OGRGeomType('Polygon') # Using a string
>>> gt3 = OGRGeomType('POLYGON') # It's case-insensitive
>>> print gt1 == 3, gt1 == 'Polygon' # Equivalence works w/non-OGRGeomType objects
True
"""
# Python library requisites.
import re, sys
from binascii import a2b_hex
from ctypes import byref, string_at, c_char_p, c_double, c_ubyte, c_void_p
from types import UnicodeType
# Getting GDAL prerequisites
from django.contrib.gis.gdal.envelope import Envelope, OGREnvelope
from django.contrib.gis.gdal.error import OGRException, OGRIndexError, SRSException
from django.contrib.gis.gdal.geomtype import OGRGeomType
from django.contrib.gis.gdal.srs import SpatialReference, CoordTransform
# Getting the ctypes prototype functions that interface w/the GDAL C library.
from django.contrib.gis.gdal.prototypes.geom import *
from django.contrib.gis.gdal.prototypes.srs import clone_srs
# For more information, see the OGR C API source code:
# http://www.gdal.org/ogr/ogr__api_8h.html
#
# The OGR_G_* routines are relevant here.
# Regular expressions for recognizing HEXEWKB and WKT.
hex_regex = re.compile(r'^[0-9A-F]+$', re.I)
wkt_regex = re.compile(r'^(?P<type>POINT|LINESTRING|LINEARRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON|GEOMETRYCOLLECTION)[ACEGIMLONPSRUTY\d,\.\-\(\) ]+$', re.I)
json_regex = re.compile(r'^\{[\s\w,\-\.\"\'\:\[\]]+\}$')
#### OGRGeometry Class ####
class OGRGeometry(object):
"Generally encapsulates an OGR geometry."
def __init__(self, geom_input, srs=None):
"Initializes Geometry on either WKT or an OGR pointer as input."
self._ptr = c_void_p(None) # Initially NULL
str_instance = isinstance(geom_input, basestring)
# If HEX, unpack input to to a binary buffer.
if str_instance and hex_regex.match(geom_input):
geom_input = buffer(a2b_hex(geom_input.upper()))
str_instance = False
# Constructing the geometry,
if str_instance:
# Checking if unicode
if isinstance(geom_input, UnicodeType):
# Encoding to ASCII, WKT or HEX doesn't need any more.
geo_input = geo_input.encode('ascii')
wkt_m = wkt_regex.match(geom_input)
json_m = json_regex.match(geom_input)
if wkt_m:
if wkt_m.group('type').upper() == 'LINEARRING':
# OGR_G_CreateFromWkt doesn't work with LINEARRING WKT.
# See http://trac.osgeo.org/gdal/ticket/1992.
g = create_geom(OGRGeomType(wkt_m.group('type')).num)
import_wkt(g, byref(c_char_p(geom_input)))
else:
g = from_wkt(byref(c_char_p(geom_input)), None, byref(c_void_p()))
elif json_m:
if GEOJSON:
g = from_json(geom_input)
else:
raise NotImplementedError('GeoJSON input only supported on GDAL 1.5+.')
else:
# Seeing if the input is a valid short-hand string
# (e.g., 'Point', 'POLYGON').
ogr_t = OGRGeomType(geom_input)
g = create_geom(OGRGeomType(geom_input).num)
elif isinstance(geom_input, buffer):
# WKB was passed in
g = from_wkb(str(geom_input), None, byref(c_void_p()), len(geom_input))
elif isinstance(geom_input, OGRGeomType):
# OGRGeomType was passed in, an empty geometry will be created.
g = create_geom(geom_input.num)
elif isinstance(geom_input, c_void_p):
# OGR pointer (c_void_p) was the input.
g = geom_input
else:
raise OGRException('Invalid input type for OGR Geometry construction: %s' % type(geom_input))
# Now checking the Geometry pointer before finishing initialization
# by setting the pointer for the object.
if not g:
raise OGRException('Cannot create OGR Geometry from input: %s' % str(geom_input))
self._ptr = g
# Assigning the SpatialReference object to the geometry, if valid.
if bool(srs): self.srs = srs
# Setting the class depending upon the OGR Geometry Type
self.__class__ = GEO_CLASSES[self.geom_type.num]
def __del__(self):
"Deletes this Geometry."
if self._ptr: destroy_geom(self._ptr)
### Geometry set-like operations ###
# g = g1 | g2
def __or__(self, other):
"Returns the union of the two geometries."
return self.union(other)
# g = g1 & g2
def __and__(self, other):
"Returns the intersection of this Geometry and the other."
return self.intersection(other)
# g = g1 - g2
def __sub__(self, other):
"Return the difference this Geometry and the other."
return self.difference(other)
# g = g1 ^ g2
def __xor__(self, other):
"Return the symmetric difference of this Geometry and the other."
return self.sym_difference(other)
def __eq__(self, other):
"Is this Geometry equal to the other?"
return self.equals(other)
def __ne__(self, other):
"Tests for inequality."
return not self.equals(other)
def __str__(self):
"WKT is used for the string representation."
return self.wkt
#### Geometry Properties ####
@property
def dimension(self):
"Returns 0 for points, 1 for lines, and 2 for surfaces."
return get_dims(self._ptr)
@property
def coord_dim(self):
"Returns the coordinate dimension of the Geometry."
return get_coord_dims(self._ptr)
@property
def geom_count(self):
"The number of elements in this Geometry."
return get_geom_count(self._ptr)
@property
def point_count(self):
"Returns the number of Points in this Geometry."
return get_point_count(self._ptr)
@property
def num_points(self):
"Alias for `point_count` (same name method in GEOS API.)"
return self.point_count
@property
def num_coords(self):
"Alais for `point_count`."
return self.point_count
@property
def geom_type(self):
"Returns the Type for this Geometry."
try:
return OGRGeomType(get_geom_type(self._ptr))
except OGRException:
# VRT datasources return an invalid geometry type
# number, but a valid name -- we'll try that instead.
# See: http://trac.osgeo.org/gdal/ticket/2491
return OGRGeomType(get_geom_name(self._ptr))
@property
def geom_name(self):
"Returns the Name of this Geometry."
return get_geom_name(self._ptr)
@property
def area(self):
"Returns the area for a LinearRing, Polygon, or MultiPolygon; 0 otherwise."
return get_area(self._ptr)
@property
def envelope(self):
"Returns the envelope for this Geometry."
# TODO: Fix Envelope() for Point geometries.
return Envelope(get_envelope(self._ptr, byref(OGREnvelope())))
@property
def extent(self):
"Returns the envelope as a 4-tuple, instead of as an Envelope object."
return self.envelope.tuple
#### SpatialReference-related Properties ####
# The SRS property
def get_srs(self):
"Returns the Spatial Reference for this Geometry."
try:
srs_ptr = get_geom_srs(self._ptr)
return SpatialReference(clone_srs(srs_ptr))
except SRSException:
return None
def set_srs(self, srs):
"Sets the SpatialReference for this geometry."
if isinstance(srs, SpatialReference):
srs_ptr = clone_srs(srs._ptr)
elif isinstance(srs, (int, long, basestring)):
sr = SpatialReference(srs)
srs_ptr = clone_srs(sr._ptr)
else:
raise TypeError('Cannot assign spatial reference with object of type: %s' % type(srs))
assign_srs(self._ptr, srs_ptr)
srs = property(get_srs, set_srs)
# The SRID property
def get_srid(self):
if self.srs: return self.srs.srid
else: return None
def set_srid(self, srid):
if isinstance(srid, (int, long)):
self.srs = srid
else:
raise TypeError('SRID must be set with an integer.')
srid = property(get_srid, set_srid)
#### Output Methods ####
@property
def geos(self):
"Returns a GEOSGeometry object from this OGRGeometry."
from django.contrib.gis.geos import GEOSGeometry
return GEOSGeometry(self.wkb, self.srid)
@property
def gml(self):
"Returns the GML representation of the Geometry."
return to_gml(self._ptr)
@property
def hex(self):
"Returns the hexadecimal representation of the WKB (a string)."
return str(self.wkb).encode('hex').upper()
#return b2a_hex(self.wkb).upper()
@property
def json(self):
if GEOJSON:
return to_json(self._ptr)
else:
raise NotImplementedError('GeoJSON output only supported on GDAL 1.5+.')
geojson = json
@property
def wkb_size(self):
"Returns the size of the WKB buffer."
return get_wkbsize(self._ptr)
@property
def wkb(self):
"Returns the WKB representation of the Geometry."
if sys.byteorder == 'little':
byteorder = 1 # wkbNDR (from ogr_core.h)
else:
byteorder = 0 # wkbXDR
sz = self.wkb_size
# Creating the unsigned character buffer, and passing it in by reference.
buf = (c_ubyte * sz)()
wkb = to_wkb(self._ptr, byteorder, byref(buf))
# Returning a buffer of the string at the pointer.
return buffer(string_at(buf, sz))
@property
def wkt(self):
"Returns the WKT representation of the Geometry."
return to_wkt(self._ptr, byref(c_char_p()))
#### Geometry Methods ####
def clone(self):
"Clones this OGR Geometry."
return OGRGeometry(clone_geom(self._ptr), self.srs)
def close_rings(self):
"""
If there are any rings within this geometry that have not been
closed, this routine will do so by adding the starting point at the
end.
"""
# Closing the open rings.
geom_close_rings(self._ptr)
def transform(self, coord_trans, clone=False):
"""
Transforms this geometry to a different spatial reference system.
May take a CoordTransform object, a SpatialReference object, string
WKT or PROJ.4, and/or an integer SRID. By default nothing is returned
and the geometry is transformed in-place. However, if the `clone`
keyword is set, then a transformed clone of this geometry will be
returned.
"""
if clone:
klone = self.clone()
klone.transform(coord_trans)
return klone
if isinstance(coord_trans, CoordTransform):
geom_transform(self._ptr, coord_trans._ptr)
elif isinstance(coord_trans, SpatialReference):
geom_transform_to(self._ptr, coord_trans._ptr)
elif isinstance(coord_trans, (int, long, basestring)):
sr = SpatialReference(coord_trans)
geom_transform_to(self._ptr, sr._ptr)
else:
raise TypeError('Transform only accepts CoordTransform, SpatialReference, string, and integer objects.')
def transform_to(self, srs):
"For backwards-compatibility."
self.transform(srs)
#### Topology Methods ####
def _topology(self, func, other):
"""A generalized function for topology operations, takes a GDAL function and
the other geometry to perform the operation on."""
if not isinstance(other, OGRGeometry):
raise TypeError('Must use another OGRGeometry object for topology operations!')
# Returning the output of the given function with the other geometry's
# pointer.
return func(self._ptr, other._ptr)
def intersects(self, other):
"Returns True if this geometry intersects with the other."
return self._topology(ogr_intersects, other)
def equals(self, other):
"Returns True if this geometry is equivalent to the other."
return self._topology(ogr_equals, other)
def disjoint(self, other):
"Returns True if this geometry and the other are spatially disjoint."
return self._topology(ogr_disjoint, other)
def touches(self, other):
"Returns True if this geometry touches the other."
return self._topology(ogr_touches, other)
def crosses(self, other):
"Returns True if this geometry crosses the other."
return self._topology(ogr_crosses, other)
def within(self, other):
"Returns True if this geometry is within the other."
return self._topology(ogr_within, other)
def contains(self, other):
"Returns True if this geometry contains the other."
return self._topology(ogr_contains, other)
def overlaps(self, other):
"Returns True if this geometry overlaps the other."
return self._topology(ogr_overlaps, other)
#### Geometry-generation Methods ####
def _geomgen(self, gen_func, other=None):
"A helper routine for the OGR routines that generate geometries."
if isinstance(other, OGRGeometry):
return OGRGeometry(gen_func(self._ptr, other._ptr), self.srs)
else:
return OGRGeometry(gen_func(self._ptr), self.srs)
@property
def boundary(self):
"Returns the boundary of this geometry."
return self._geomgen(get_boundary)
@property
def convex_hull(self):
"""
Returns the smallest convex Polygon that contains all the points in
this Geometry.
"""
return self._geomgen(geom_convex_hull)
def difference(self, other):
"""
Returns a new geometry consisting of the region which is the difference
of this geometry and the other.
"""
return self._geomgen(geom_diff, other)
def intersection(self, other):
"""
Returns a new geometry consisting of the region of intersection of this
geometry and the other.
"""
return self._geomgen(geom_intersection, other)
def sym_difference(self, other):
"""
Returns a new geometry which is the symmetric difference of this
geometry and the other.
"""
return self._geomgen(geom_sym_diff, other)
def union(self, other):
"""
Returns a new geometry consisting of the region which is the union of
this geometry and the other.
"""
return self._geomgen(geom_union, other)
# The subclasses for OGR Geometry.
class Point(OGRGeometry):
@property
def x(self):
"Returns the X coordinate for this Point."
return getx(self._ptr, 0)
@property
def y(self):
"Returns the Y coordinate for this Point."
return gety(self._ptr, 0)
@property
def z(self):
"Returns the Z coordinate for this Point."
if self.coord_dim == 3:
return getz(self._ptr, 0)
@property
def tuple(self):
"Returns the tuple of this point."
if self.coord_dim == 2:
return (self.x, self.y)
elif self.coord_dim == 3:
return (self.x, self.y, self.z)
coords = tuple
class LineString(OGRGeometry):
def __getitem__(self, index):
"Returns the Point at the given index."
if index >= 0 and index < self.point_count:
x, y, z = c_double(), c_double(), c_double()
get_point(self._ptr, index, byref(x), byref(y), byref(z))
dim = self.coord_dim
if dim == 1:
return (x.value,)
elif dim == 2:
return (x.value, y.value)
elif dim == 3:
return (x.value, y.value, z.value)
else:
raise OGRIndexError('index out of range: %s' % str(index))
def __iter__(self):
"Iterates over each point in the LineString."
for i in xrange(self.point_count):
yield self[i]
def __len__(self):
"The length returns the number of points in the LineString."
return self.point_count
@property
def tuple(self):
"Returns the tuple representation of this LineString."
return tuple([self[i] for i in xrange(len(self))])
coords = tuple
def _listarr(self, func):
"""
Internal routine that returns a sequence (list) corresponding with
the given function.
"""
return [func(self._ptr, i) for i in xrange(len(self))]
@property
def x(self):
"Returns the X coordinates in a list."
return self._listarr(getx)
@property
def y(self):
"Returns the Y coordinates in a list."
return self._listarr(gety)
@property
def z(self):
"Returns the Z coordinates in a list."
if self.coord_dim == 3:
return self._listarr(getz)
# LinearRings are used in Polygons.
class LinearRing(LineString): pass
class Polygon(OGRGeometry):
def __len__(self):
"The number of interior rings in this Polygon."
return self.geom_count
def __iter__(self):
"Iterates through each ring in the Polygon."
for i in xrange(self.geom_count):
yield self[i]
def __getitem__(self, index):
"Gets the ring at the specified index."
if index < 0 or index >= self.geom_count:
raise OGRIndexError('index out of range: %s' % index)
else:
return OGRGeometry(clone_geom(get_geom_ref(self._ptr, index)), self.srs)
# Polygon Properties
@property
def shell(self):
"Returns the shell of this Polygon."
return self[0] # First ring is the shell
exterior_ring = shell
@property
def tuple(self):
"Returns a tuple of LinearRing coordinate tuples."
return tuple([self[i].tuple for i in xrange(self.geom_count)])
coords = tuple
@property
def point_count(self):
"The number of Points in this Polygon."
# Summing up the number of points in each ring of the Polygon.
return sum([self[i].point_count for i in xrange(self.geom_count)])
@property
def centroid(self):
"Returns the centroid (a Point) of this Polygon."
# The centroid is a Point, create a geometry for this.
p = OGRGeometry(OGRGeomType('Point'))
get_centroid(self._ptr, p._ptr)
return p
# Geometry Collection base class.
class GeometryCollection(OGRGeometry):
"The Geometry Collection class."
def __getitem__(self, index):
"Gets the Geometry at the specified index."
if index < 0 or index >= self.geom_count:
raise OGRIndexError('index out of range: %s' % index)
else:
return OGRGeometry(clone_geom(get_geom_ref(self._ptr, index)), self.srs)
def __iter__(self):
"Iterates over each Geometry."
for i in xrange(self.geom_count):
yield self[i]
def __len__(self):
"The number of geometries in this Geometry Collection."
return self.geom_count
def add(self, geom):
"Add the geometry to this Geometry Collection."
if isinstance(geom, OGRGeometry):
if isinstance(geom, self.__class__):
for g in geom: add_geom(self._ptr, g._ptr)
else:
add_geom(self._ptr, geom._ptr)
elif isinstance(geom, basestring):
tmp = OGRGeometry(geom)
add_geom(self._ptr, tmp._ptr)
else:
raise OGRException('Must add an OGRGeometry.')
@property
def point_count(self):
"The number of Points in this Geometry Collection."
# Summing up the number of points in each geometry in this collection
return sum([self[i].point_count for i in xrange(self.geom_count)])
@property
def tuple(self):
"Returns a tuple representation of this Geometry Collection."
return tuple([self[i].tuple for i in xrange(self.geom_count)])
coords = tuple
# Multiple Geometry types.
class MultiPoint(GeometryCollection): pass
class MultiLineString(GeometryCollection): pass
class MultiPolygon(GeometryCollection): pass
# Class mapping dictionary (using the OGRwkbGeometryType as the key)
GEO_CLASSES = {1 : Point,
2 : LineString,
3 : Polygon,
4 : MultiPoint,
5 : MultiLineString,
6 : MultiPolygon,
7 : GeometryCollection,
101: LinearRing,
}

View File

@ -0,0 +1,73 @@
from django.contrib.gis.gdal.error import OGRException
#### OGRGeomType ####
class OGRGeomType(object):
"Encapulates OGR Geometry Types."
# Dictionary of acceptable OGRwkbGeometryType s and their string names.
_types = {0 : 'Unknown',
1 : 'Point',
2 : 'LineString',
3 : 'Polygon',
4 : 'MultiPoint',
5 : 'MultiLineString',
6 : 'MultiPolygon',
7 : 'GeometryCollection',
100 : 'None',
101 : 'LinearRing',
}
# Reverse type dictionary, keyed by lower-case of the name.
_str_types = dict([(v.lower(), k) for k, v in _types.items()])
def __init__(self, type_input):
"Figures out the correct OGR Type based upon the input."
if isinstance(type_input, OGRGeomType):
num = type_input.num
elif isinstance(type_input, basestring):
num = self._str_types.get(type_input.lower(), None)
if num is None:
raise OGRException('Invalid OGR String Type "%s"' % type_input)
elif isinstance(type_input, int):
if not type_input in self._types:
raise OGRException('Invalid OGR Integer Type: %d' % type_input)
num = type_input
else:
raise TypeError('Invalid OGR input type given.')
# Setting the OGR geometry type number.
self.num = num
def __str__(self):
"Returns the value of the name property."
return self.name
def __eq__(self, other):
"""
Does an equivalence test on the OGR type with the given
other OGRGeomType, the short-hand string, or the integer.
"""
if isinstance(other, OGRGeomType):
return self.num == other.num
elif isinstance(other, basestring):
return self.name.lower() == other.lower()
elif isinstance(other, int):
return self.num == other
else:
return False
def __ne__(self, other):
return not (self == other)
@property
def name(self):
"Returns a short-hand string form of the OGR Geometry type."
return self._types[self.num]
@property
def django(self):
"Returns the Django GeometryField for this OGR Type."
s = self.name
if s in ('Unknown', 'LinearRing', 'None'):
return None
else:
return s + 'Field'

View File

@ -0,0 +1,187 @@
# Needed ctypes routines
from ctypes import byref
# Other GDAL imports.
from django.contrib.gis.gdal.envelope import Envelope, OGREnvelope
from django.contrib.gis.gdal.error import OGRException, OGRIndexError, SRSException
from django.contrib.gis.gdal.feature import Feature
from django.contrib.gis.gdal.field import FIELD_CLASSES
from django.contrib.gis.gdal.geometries import OGRGeomType
from django.contrib.gis.gdal.srs import SpatialReference
# GDAL ctypes function prototypes.
from django.contrib.gis.gdal.prototypes.ds import \
get_extent, get_fd_geom_type, get_fd_name, get_feature, get_feature_count, \
get_field_count, get_field_defn, get_field_name, get_field_precision, \
get_field_width, get_field_type, get_layer_defn, get_layer_srs, \
get_next_feature, reset_reading, test_capability
from django.contrib.gis.gdal.prototypes.srs import clone_srs
# For more information, see the OGR C API source code:
# http://www.gdal.org/ogr/ogr__api_8h.html
#
# The OGR_L_* routines are relevant here.
class Layer(object):
"A class that wraps an OGR Layer, needs to be instantiated from a DataSource object."
#### Python 'magic' routines ####
def __init__(self, layer_ptr):
"Needs a C pointer (Python/ctypes integer) in order to initialize."
self._ptr = None # Initially NULL
if not layer_ptr:
raise OGRException('Cannot create Layer, invalid pointer given')
self._ptr = layer_ptr
self._ldefn = get_layer_defn(self._ptr)
# Does the Layer support random reading?
self._random_read = self.test_capability('RandomRead')
def __getitem__(self, index):
"Gets the Feature at the specified index."
if isinstance(index, (int, long)):
# An integer index was given -- we cannot do a check based on the
# number of features because the beginning and ending feature IDs
# are not guaranteed to be 0 and len(layer)-1, respectively.
if index < 0: raise OGRIndexError('Negative indices are not allowed on OGR Layers.')
return self._make_feature(index)
elif isinstance(index, slice):
# A slice was given
start, stop, stride = index.indices(self.num_feat)
return [self._make_feature(fid) for fid in xrange(start, stop, stride)]
else:
raise TypeError('Integers and slices may only be used when indexing OGR Layers.')
def __iter__(self):
"Iterates over each Feature in the Layer."
# ResetReading() must be called before iteration is to begin.
reset_reading(self._ptr)
for i in xrange(self.num_feat):
yield Feature(get_next_feature(self._ptr), self._ldefn)
def __len__(self):
"The length is the number of features."
return self.num_feat
def __str__(self):
"The string name of the layer."
return self.name
def _make_feature(self, feat_id):
"""
Helper routine for __getitem__ that constructs a Feature from the given
Feature ID. If the OGR Layer does not support random-access reading,
then each feature of the layer will be incremented through until the
a Feature is found matching the given feature ID.
"""
if self._random_read:
# If the Layer supports random reading, return.
try:
return Feature(get_feature(self._ptr, feat_id), self._ldefn)
except OGRException:
pass
else:
# Random access isn't supported, have to increment through
# each feature until the given feature ID is encountered.
for feat in self:
if feat.fid == feat_id: return feat
# Should have returned a Feature, raise an OGRIndexError.
raise OGRIndexError('Invalid feature id: %s.' % feat_id)
#### Layer properties ####
@property
def extent(self):
"Returns the extent (an Envelope) of this layer."
env = OGREnvelope()
get_extent(self._ptr, byref(env), 1)
return Envelope(env)
@property
def name(self):
"Returns the name of this layer in the Data Source."
return get_fd_name(self._ldefn)
@property
def num_feat(self, force=1):
"Returns the number of features in the Layer."
return get_feature_count(self._ptr, force)
@property
def num_fields(self):
"Returns the number of fields in the Layer."
return get_field_count(self._ldefn)
@property
def geom_type(self):
"Returns the geometry type (OGRGeomType) of the Layer."
return OGRGeomType(get_fd_geom_type(self._ldefn))
@property
def srs(self):
"Returns the Spatial Reference used in this Layer."
try:
ptr = get_layer_srs(self._ptr)
return SpatialReference(clone_srs(ptr))
except SRSException:
return None
@property
def fields(self):
"""
Returns a list of string names corresponding to each of the Fields
available in this Layer.
"""
return [get_field_name(get_field_defn(self._ldefn, i))
for i in xrange(self.num_fields) ]
@property
def field_types(self):
"""
Returns a list of the types of fields in this Layer. For example,
the list [OFTInteger, OFTReal, OFTString] would be returned for
an OGR layer that had an integer, a floating-point, and string
fields.
"""
return [FIELD_CLASSES[get_field_type(get_field_defn(self._ldefn, i))]
for i in xrange(self.num_fields)]
@property
def field_widths(self):
"Returns a list of the maximum field widths for the features."
return [get_field_width(get_field_defn(self._ldefn, i))
for i in xrange(self.num_fields)]
@property
def field_precisions(self):
"Returns the field precisions for the features."
return [get_field_precision(get_field_defn(self._ldefn, i))
for i in xrange(self.num_fields)]
#### Layer Methods ####
def get_fields(self, field_name):
"""
Returns a list containing the given field name for every Feature
in the Layer.
"""
if not field_name in self.fields:
raise OGRException('invalid field name: %s' % field_name)
return [feat.get(field_name) for feat in self]
def get_geoms(self, geos=False):
"""
Returns a list containing the OGRGeometry for every Feature in
the Layer.
"""
if geos:
from django.contrib.gis.geos import GEOSGeometry
return [GEOSGeometry(feat.geom.wkb) for feat in self]
else:
return [feat.geom for feat in self]
def test_capability(self, capability):
"""
Returns a bool indicating whether the this Layer supports the given
capability (a string). Valid capability strings include:
'RandomRead', 'SequentialWrite', 'RandomWrite', 'FastSpatialFilter',
'FastFeatureCount', 'FastGetExtent', 'CreateField', 'Transactions',
'DeleteFeature', and 'FastSetNextByIndex'.
"""
return bool(test_capability(self._ptr, capability))

View File

@ -0,0 +1,83 @@
import os, sys
from ctypes import c_char_p, CDLL
from ctypes.util import find_library
from django.contrib.gis.gdal.error import OGRException
# Custom library path set?
try:
from django.conf import settings
lib_path = settings.GDAL_LIBRARY_PATH
except (AttributeError, EnvironmentError, ImportError):
lib_path = None
if lib_path:
lib_names = None
elif os.name == 'nt':
# Windows NT shared library
lib_names = ['gdal15']
elif os.name == 'posix':
# *NIX library names.
lib_names = ['gdal', 'gdal1.5.0']
else:
raise OGRException('Unsupported OS "%s"' % os.name)
# Using the ctypes `find_library` utility to find the
# path to the GDAL library from the list of library names.
if lib_names:
for lib_name in lib_names:
lib_path = find_library(lib_name)
if not lib_path is None: break
if lib_path is None:
raise OGRException('Could not find the GDAL library (tried "%s"). '
'Try setting GDAL_LIBRARY_PATH in your settings.' %
'", "'.join(lib_names))
# This loads the GDAL/OGR C library
lgdal = CDLL(lib_path)
# On Windows, the GDAL binaries have some OSR routines exported with
# STDCALL, while others are not. Thus, the library will also need to
# be loaded up as WinDLL for said OSR functions that require the
# different calling convention.
if os.name == 'nt':
from ctypes import WinDLL
lwingdal = WinDLL(lib_name)
def std_call(func):
"""
Returns the correct STDCALL function for certain OSR routines on Win32
platforms.
"""
if os.name == 'nt':
return lwingdal[func]
else:
return lgdal[func]
#### Version-information functions. ####
# Returns GDAL library version information with the given key.
_version_info = std_call('GDALVersionInfo')
_version_info.argtypes = [c_char_p]
_version_info.restype = c_char_p
def gdal_version():
"Returns only the GDAL version number information."
return _version_info('RELEASE_NAME')
def gdal_full_version():
"Returns the full GDAL version information."
return _version_info('')
def gdal_release_date(date=False):
"""
Returns the release date in a string format, e.g, "2007/06/27".
If the date keyword argument is set to True, a Python datetime object
will be returned instead.
"""
from datetime import date as date_type
rel = _version_info('RELEASE_DATE')
yy, mm, dd = map(int, (rel[0:4], rel[4:6], rel[6:8]))
d = date_type(yy, mm, dd)
if date: return d
else: return d.strftime('%Y/%m/%d')

View File

@ -0,0 +1,68 @@
"""
This module houses the ctypes function prototypes for OGR DataSource
related data structures. OGR_Dr_*, OGR_DS_*, OGR_L_*, OGR_F_*,
OGR_Fld_* routines are relevant here.
"""
from ctypes import c_char_p, c_int, c_long, c_void_p, POINTER
from django.contrib.gis.gdal.envelope import OGREnvelope
from django.contrib.gis.gdal.libgdal import lgdal
from django.contrib.gis.gdal.prototypes.generation import \
const_string_output, double_output, geom_output, int_output, \
srs_output, void_output, voidptr_output
c_int_p = POINTER(c_int) # shortcut type
### Driver Routines ###
register_all = void_output(lgdal.OGRRegisterAll, [], errcheck=False)
cleanup_all = void_output(lgdal.OGRCleanupAll, [], errcheck=False)
get_driver = voidptr_output(lgdal.OGRGetDriver, [c_int])
get_driver_by_name = voidptr_output(lgdal.OGRGetDriverByName, [c_char_p])
get_driver_count = int_output(lgdal.OGRGetDriverCount, [])
get_driver_name = const_string_output(lgdal.OGR_Dr_GetName, [c_void_p])
### DataSource ###
open_ds = voidptr_output(lgdal.OGROpen, [c_char_p, c_int, POINTER(c_void_p)])
destroy_ds = void_output(lgdal.OGR_DS_Destroy, [c_void_p], errcheck=False)
release_ds = void_output(lgdal.OGRReleaseDataSource, [c_void_p])
get_ds_name = const_string_output(lgdal.OGR_DS_GetName, [c_void_p])
get_layer = voidptr_output(lgdal.OGR_DS_GetLayer, [c_void_p, c_int])
get_layer_by_name = voidptr_output(lgdal.OGR_DS_GetLayerByName, [c_void_p, c_char_p])
get_layer_count = int_output(lgdal.OGR_DS_GetLayerCount, [c_void_p])
### Layer Routines ###
get_extent = void_output(lgdal.OGR_L_GetExtent, [c_void_p, POINTER(OGREnvelope), c_int])
get_feature = voidptr_output(lgdal.OGR_L_GetFeature, [c_void_p, c_long])
get_feature_count = int_output(lgdal.OGR_L_GetFeatureCount, [c_void_p, c_int])
get_layer_defn = voidptr_output(lgdal.OGR_L_GetLayerDefn, [c_void_p])
get_layer_srs = srs_output(lgdal.OGR_L_GetSpatialRef, [c_void_p])
get_next_feature = voidptr_output(lgdal.OGR_L_GetNextFeature, [c_void_p])
reset_reading = void_output(lgdal.OGR_L_ResetReading, [c_void_p], errcheck=False)
test_capability = int_output(lgdal.OGR_L_TestCapability, [c_void_p, c_char_p])
### Feature Definition Routines ###
get_fd_geom_type = int_output(lgdal.OGR_FD_GetGeomType, [c_void_p])
get_fd_name = const_string_output(lgdal.OGR_FD_GetName, [c_void_p])
get_feat_name = const_string_output(lgdal.OGR_FD_GetName, [c_void_p])
get_field_count = int_output(lgdal.OGR_FD_GetFieldCount, [c_void_p])
get_field_defn = voidptr_output(lgdal.OGR_FD_GetFieldDefn, [c_void_p, c_int])
### Feature Routines ###
clone_feature = voidptr_output(lgdal.OGR_F_Clone, [c_void_p])
destroy_feature = void_output(lgdal.OGR_F_Destroy, [c_void_p], errcheck=False)
feature_equal = int_output(lgdal.OGR_F_Equal, [c_void_p, c_void_p])
get_feat_geom_ref = geom_output(lgdal.OGR_F_GetGeometryRef, [c_void_p])
get_feat_field_count = int_output(lgdal.OGR_F_GetFieldCount, [c_void_p])
get_feat_field_defn = voidptr_output(lgdal.OGR_F_GetFieldDefnRef, [c_void_p, c_int])
get_fid = int_output(lgdal.OGR_F_GetFID, [c_void_p])
get_field_as_datetime = int_output(lgdal.OGR_F_GetFieldAsDateTime, [c_void_p, c_int, c_int_p, c_int_p, c_int_p, c_int_p, c_int_p, c_int_p])
get_field_as_double = double_output(lgdal.OGR_F_GetFieldAsDouble, [c_void_p, c_int])
get_field_as_integer = int_output(lgdal.OGR_F_GetFieldAsInteger, [c_void_p, c_int])
get_field_as_string = const_string_output(lgdal.OGR_F_GetFieldAsString, [c_void_p, c_int])
get_field_index = int_output(lgdal.OGR_F_GetFieldIndex, [c_void_p, c_char_p])
### Field Routines ###
get_field_name = const_string_output(lgdal.OGR_Fld_GetNameRef, [c_void_p])
get_field_precision = int_output(lgdal.OGR_Fld_GetPrecision, [c_void_p])
get_field_type = int_output(lgdal.OGR_Fld_GetType, [c_void_p])
get_field_type_name = const_string_output(lgdal.OGR_GetFieldTypeName, [c_int])
get_field_width = int_output(lgdal.OGR_Fld_GetWidth, [c_void_p])

View File

@ -0,0 +1,125 @@
"""
This module houses the error-checking routines used by the GDAL
ctypes prototypes.
"""
from ctypes import c_void_p, string_at
from django.contrib.gis.gdal.error import check_err, OGRException, SRSException
from django.contrib.gis.gdal.libgdal import lgdal
# Helper routines for retrieving pointers and/or values from
# arguments passed in by reference.
def arg_byref(args, offset=-1):
"Returns the pointer argument's by-refernece value."
return args[offset]._obj.value
def ptr_byref(args, offset=-1):
"Returns the pointer argument passed in by-reference."
return args[offset]._obj
def check_bool(result, func, cargs):
"Returns the boolean evaluation of the value."
if bool(result): return True
else: return False
### String checking Routines ###
def check_const_string(result, func, cargs, offset=None):
"""
Similar functionality to `check_string`, but does not free the pointer.
"""
if offset:
check_err(result)
ptr = ptr_byref(cargs, offset)
return ptr.value
else:
return result
def check_string(result, func, cargs, offset=-1, str_result=False):
"""
Checks the string output returned from the given function, and frees
the string pointer allocated by OGR. The `str_result` keyword
may be used when the result is the string pointer, otherwise
the OGR error code is assumed. The `offset` keyword may be used
to extract the string pointer passed in by-reference at the given
slice offset in the function arguments.
"""
if str_result:
# For routines that return a string.
ptr = result
if not ptr: s = None
else: s = string_at(result)
else:
# Error-code return specified.
check_err(result)
ptr = ptr_byref(cargs, offset)
# Getting the string value
s = ptr.value
# Correctly freeing the allocated memory beind GDAL pointer
# w/the VSIFree routine.
if ptr: lgdal.VSIFree(ptr)
return s
### DataSource, Layer error-checking ###
### Envelope checking ###
def check_envelope(result, func, cargs, offset=-1):
"Checks a function that returns an OGR Envelope by reference."
env = ptr_byref(cargs, offset)
return env
### Geometry error-checking routines ###
def check_geom(result, func, cargs):
"Checks a function that returns a geometry."
# OGR_G_Clone may return an integer, even though the
# restype is set to c_void_p
if isinstance(result, int):
result = c_void_p(result)
if not result:
raise OGRException('Invalid geometry pointer returned from "%s".' % func.__name__)
return result
def check_geom_offset(result, func, cargs, offset=-1):
"Chcks the geometry at the given offset in the C parameter list."
check_err(result)
geom = ptr_byref(cargs, offset=offset)
return check_geom(geom, func, cargs)
### Spatial Reference error-checking routines ###
def check_srs(result, func, cargs):
if isinstance(result, int):
result = c_void_p(result)
if not result:
raise SRSException('Invalid spatial reference pointer returned from "%s".' % func.__name__)
return result
### Other error-checking routines ###
def check_arg_errcode(result, func, cargs):
"""
The error code is returned in the last argument, by reference.
Check its value with `check_err` before returning the result.
"""
check_err(arg_byref(cargs))
return result
def check_errcode(result, func, cargs):
"""
Check the error code returned (c_int).
"""
check_err(result)
return
def check_pointer(result, func, cargs):
"Makes sure the result pointer is valid."
if bool(result):
return result
else:
raise OGRException('Invalid pointer returned from "%s"' % func.__name__)
def check_str_arg(result, func, cargs):
"""
This is for the OSRGet[Angular|Linear]Units functions, which
require that the returned string pointer not be freed. This
returns both the double and tring values.
"""
dbl = result
ptr = cargs[-1]._obj
return dbl, ptr.value

View File

@ -0,0 +1,116 @@
"""
This module contains functions that generate ctypes prototypes for the
GDAL routines.
"""
from ctypes import c_char_p, c_double, c_int, c_void_p
from django.contrib.gis.gdal.prototypes.errcheck import \
check_arg_errcode, check_errcode, check_geom, check_geom_offset, \
check_pointer, check_srs, check_str_arg, check_string, check_const_string
def double_output(func, argtypes, errcheck=False, strarg=False):
"Generates a ctypes function that returns a double value."
func.argtypes = argtypes
func.restype = c_double
if errcheck: func.errcheck = check_arg_errcode
if strarg: func.errcheck = check_str_arg
return func
def geom_output(func, argtypes, offset=None):
"""
Generates a function that returns a Geometry either by reference
or directly (if the return_geom keyword is set to True).
"""
# Setting the argument types
func.argtypes = argtypes
if not offset:
# When a geometry pointer is directly returned.
func.restype = c_void_p
func.errcheck = check_geom
else:
# Error code returned, geometry is returned by-reference.
func.restype = c_int
def geomerrcheck(result, func, cargs):
return check_geom_offset(result, func, cargs, offset)
func.errcheck = geomerrcheck
return func
def int_output(func, argtypes):
"Generates a ctypes function that returns an integer value."
func.argtypes = argtypes
func.restype = c_int
return func
def srs_output(func, argtypes):
"""
Generates a ctypes prototype for the given function with
the given C arguments that returns a pointer to an OGR
Spatial Reference System.
"""
func.argtypes = argtypes
func.restype = c_void_p
func.errcheck = check_srs
return func
def const_string_output(func, argtypes, offset=None):
func.argtypes = argtypes
if offset:
func.restype = c_int
else:
func.restype = c_char_p
def _check_const(result, func, cargs):
return check_const_string(result, func, cargs, offset=offset)
func.errcheck = _check_const
return func
def string_output(func, argtypes, offset=-1, str_result=False):
"""
Generates a ctypes prototype for the given function with the
given argument types that returns a string from a GDAL pointer.
The `const` flag indicates whether the allocated pointer should
be freed via the GDAL library routine VSIFree -- but only applies
only when `str_result` is True.
"""
func.argtypes = argtypes
if str_result:
# String is the result, don't explicitly define
# the argument type so we can get the pointer.
pass
else:
# Error code is returned
func.restype = c_int
# Dynamically defining our error-checking function with the
# given offset.
def _check_str(result, func, cargs):
return check_string(result, func, cargs,
offset=offset, str_result=str_result)
func.errcheck = _check_str
return func
def void_output(func, argtypes, errcheck=True):
"""
For functions that don't only return an error code that needs to
be examined.
"""
if argtypes: func.argtypes = argtypes
if errcheck:
# `errcheck` keyword may be set to False for routines that
# return void, rather than a status code.
func.restype = c_int
func.errcheck = check_errcode
else:
func.restype = None
return func
def voidptr_output(func, argtypes):
"For functions that return c_void_p."
func.argtypes = argtypes
func.restype = c_void_p
func.errcheck = check_pointer
return func

View File

@ -0,0 +1,109 @@
from datetime import date
from ctypes import c_char, c_char_p, c_double, c_int, c_ubyte, c_void_p, POINTER
from django.contrib.gis.gdal.envelope import OGREnvelope
from django.contrib.gis.gdal.libgdal import lgdal, gdal_version
from django.contrib.gis.gdal.prototypes.errcheck import check_bool, check_envelope
from django.contrib.gis.gdal.prototypes.generation import \
const_string_output, double_output, geom_output, int_output, \
srs_output, string_output, void_output
# Some prototypes need to be aware of what version GDAL we have.
major, minor = map(int, gdal_version().split('.')[:2])
if major <= 1 and minor <= 4:
GEOJSON = False
else:
GEOJSON = True
### Generation routines specific to this module ###
def env_func(f, argtypes):
"For getting OGREnvelopes."
f.argtypes = argtypes
f.restype = None
f.errcheck = check_envelope
return f
def pnt_func(f):
"For accessing point information."
return double_output(f, [c_void_p, c_int])
def topology_func(f):
f.argtypes = [c_void_p, c_void_p]
f.restype = c_int
f.errchck = check_bool
return f
### OGR_G ctypes function prototypes ###
# GeoJSON routines, if supported.
if GEOJSON:
from_json = geom_output(lgdal.OGR_G_CreateGeometryFromJson, [c_char_p])
to_json = string_output(lgdal.OGR_G_ExportToJson, [c_void_p], str_result=True)
else:
from_json = False
to_json = False
# GetX, GetY, GetZ all return doubles.
getx = pnt_func(lgdal.OGR_G_GetX)
gety = pnt_func(lgdal.OGR_G_GetY)
getz = pnt_func(lgdal.OGR_G_GetZ)
# Geometry creation routines.
from_wkb = geom_output(lgdal.OGR_G_CreateFromWkb, [c_char_p, c_void_p, POINTER(c_void_p), c_int], offset=-2)
from_wkt = geom_output(lgdal.OGR_G_CreateFromWkt, [POINTER(c_char_p), c_void_p, POINTER(c_void_p)], offset=-1)
create_geom = geom_output(lgdal.OGR_G_CreateGeometry, [c_int])
clone_geom = geom_output(lgdal.OGR_G_Clone, [c_void_p])
get_geom_ref = geom_output(lgdal.OGR_G_GetGeometryRef, [c_void_p, c_int])
get_boundary = geom_output(lgdal.OGR_G_GetBoundary, [c_void_p])
geom_convex_hull = geom_output(lgdal.OGR_G_ConvexHull, [c_void_p])
geom_diff = geom_output(lgdal.OGR_G_Difference, [c_void_p, c_void_p])
geom_intersection = geom_output(lgdal.OGR_G_Intersection, [c_void_p, c_void_p])
geom_sym_diff = geom_output(lgdal.OGR_G_SymmetricDifference, [c_void_p, c_void_p])
geom_union = geom_output(lgdal.OGR_G_Union, [c_void_p, c_void_p])
# Geometry modification routines.
add_geom = void_output(lgdal.OGR_G_AddGeometry, [c_void_p, c_void_p])
import_wkt = void_output(lgdal.OGR_G_ImportFromWkt, [c_void_p, POINTER(c_char_p)])
# Destroys a geometry
destroy_geom = void_output(lgdal.OGR_G_DestroyGeometry, [c_void_p], errcheck=False)
# Geometry export routines.
to_wkb = void_output(lgdal.OGR_G_ExportToWkb, None, errcheck=True) # special handling for WKB.
to_wkt = string_output(lgdal.OGR_G_ExportToWkt, [c_void_p, POINTER(c_char_p)])
to_gml = string_output(lgdal.OGR_G_ExportToGML, [c_void_p], str_result=True)
get_wkbsize = int_output(lgdal.OGR_G_WkbSize, [c_void_p])
# Geometry spatial-reference related routines.
assign_srs = void_output(lgdal.OGR_G_AssignSpatialReference, [c_void_p, c_void_p], errcheck=False)
get_geom_srs = srs_output(lgdal.OGR_G_GetSpatialReference, [c_void_p])
# Geometry properties
get_area = double_output(lgdal.OGR_G_GetArea, [c_void_p])
get_centroid = void_output(lgdal.OGR_G_Centroid, [c_void_p, c_void_p])
get_dims = int_output(lgdal.OGR_G_GetDimension, [c_void_p])
get_coord_dims = int_output(lgdal.OGR_G_GetCoordinateDimension, [c_void_p])
get_geom_count = int_output(lgdal.OGR_G_GetGeometryCount, [c_void_p])
get_geom_name = const_string_output(lgdal.OGR_G_GetGeometryName, [c_void_p])
get_geom_type = int_output(lgdal.OGR_G_GetGeometryType, [c_void_p])
get_point_count = int_output(lgdal.OGR_G_GetPointCount, [c_void_p])
get_point = void_output(lgdal.OGR_G_GetPoint, [c_void_p, c_int, POINTER(c_double), POINTER(c_double), POINTER(c_double)], errcheck=False)
geom_close_rings = void_output(lgdal.OGR_G_CloseRings, [c_void_p], errcheck=False)
# Topology routines.
ogr_contains = topology_func(lgdal.OGR_G_Contains)
ogr_crosses = topology_func(lgdal.OGR_G_Crosses)
ogr_disjoint = topology_func(lgdal.OGR_G_Disjoint)
ogr_equals = topology_func(lgdal.OGR_G_Equals)
ogr_intersects = topology_func(lgdal.OGR_G_Intersects)
ogr_overlaps = topology_func(lgdal.OGR_G_Overlaps)
ogr_touches = topology_func(lgdal.OGR_G_Touches)
ogr_within = topology_func(lgdal.OGR_G_Within)
# Transformation routines.
geom_transform = void_output(lgdal.OGR_G_Transform, [c_void_p, c_void_p])
geom_transform_to = void_output(lgdal.OGR_G_TransformTo, [c_void_p, c_void_p])
# For retrieving the envelope of the geometry.
get_envelope = env_func(lgdal.OGR_G_GetEnvelope, [c_void_p, POINTER(OGREnvelope)])

View File

@ -0,0 +1,71 @@
from ctypes import c_char_p, c_int, c_void_p, POINTER
from django.contrib.gis.gdal.libgdal import lgdal, std_call
from django.contrib.gis.gdal.prototypes.generation import \
const_string_output, double_output, int_output, \
srs_output, string_output, void_output
## Shortcut generation for routines with known parameters.
def srs_double(f):
"""
Creates a function prototype for the OSR routines that take
the OSRSpatialReference object and
"""
return double_output(f, [c_void_p, POINTER(c_int)], errcheck=True)
def units_func(f):
"""
Creates a ctypes function prototype for OSR units functions, e.g.,
OSRGetAngularUnits, OSRGetLinearUnits.
"""
return double_output(f, [c_void_p, POINTER(c_char_p)], strarg=True)
# Creation & destruction.
clone_srs = srs_output(std_call('OSRClone'), [c_void_p])
new_srs = srs_output(std_call('OSRNewSpatialReference'), [c_char_p])
release_srs = void_output(lgdal.OSRRelease, [c_void_p], errcheck=False)
destroy_srs = void_output(std_call('OSRDestroySpatialReference'), [c_void_p], errcheck=False)
srs_validate = void_output(lgdal.OSRValidate, [c_void_p])
# Getting the semi_major, semi_minor, and flattening functions.
semi_major = srs_double(lgdal.OSRGetSemiMajor)
semi_minor = srs_double(lgdal.OSRGetSemiMinor)
invflattening = srs_double(lgdal.OSRGetInvFlattening)
# WKT, PROJ, EPSG, XML importation routines.
from_wkt = void_output(lgdal.OSRImportFromWkt, [c_void_p, POINTER(c_char_p)])
from_proj = void_output(lgdal.OSRImportFromProj4, [c_void_p, c_char_p])
from_epsg = void_output(std_call('OSRImportFromEPSG'), [c_void_p, c_int])
from_xml = void_output(lgdal.OSRImportFromXML, [c_void_p, c_char_p])
# Morphing to/from ESRI WKT.
morph_to_esri = void_output(lgdal.OSRMorphToESRI, [c_void_p])
morph_from_esri = void_output(lgdal.OSRMorphFromESRI, [c_void_p])
# Identifying the EPSG
identify_epsg = void_output(lgdal.OSRAutoIdentifyEPSG, [c_void_p])
# Getting the angular_units, linear_units functions
linear_units = units_func(lgdal.OSRGetLinearUnits)
angular_units = units_func(lgdal.OSRGetAngularUnits)
# For exporting to WKT, PROJ.4, "Pretty" WKT, and XML.
to_wkt = string_output(std_call('OSRExportToWkt'), [c_void_p, POINTER(c_char_p)])
to_proj = string_output(std_call('OSRExportToProj4'), [c_void_p, POINTER(c_char_p)])
to_pretty_wkt = string_output(std_call('OSRExportToPrettyWkt'), [c_void_p, POINTER(c_char_p), c_int], offset=-2)
# Memory leak fixed in GDAL 1.5; still exists in 1.4.
to_xml = string_output(lgdal.OSRExportToXML, [c_void_p, POINTER(c_char_p), c_char_p], offset=-2)
# String attribute retrival routines.
get_attr_value = const_string_output(std_call('OSRGetAttrValue'), [c_void_p, c_char_p, c_int])
get_auth_name = const_string_output(lgdal.OSRGetAuthorityName, [c_void_p, c_char_p])
get_auth_code = const_string_output(lgdal.OSRGetAuthorityCode, [c_void_p, c_char_p])
# SRS Properties
isgeographic = int_output(lgdal.OSRIsGeographic, [c_void_p])
islocal = int_output(lgdal.OSRIsLocal, [c_void_p])
isprojected = int_output(lgdal.OSRIsProjected, [c_void_p])
# Coordinate transformation
new_ct= srs_output(std_call('OCTNewCoordinateTransformation'), [c_void_p, c_void_p])
destroy_ct = void_output(std_call('OCTDestroyCoordinateTransformation'), [c_void_p], errcheck=False)

View File

@ -0,0 +1,360 @@
"""
The Spatial Reference class, represensents OGR Spatial Reference objects.
Example:
>>> from django.contrib.gis.gdal import SpatialReference
>>> srs = SpatialReference('WGS84')
>>> print srs
GEOGCS["WGS 84",
DATUM["WGS_1984",
SPHEROID["WGS 84",6378137,298.257223563,
AUTHORITY["EPSG","7030"]],
TOWGS84[0,0,0,0,0,0,0],
AUTHORITY["EPSG","6326"]],
PRIMEM["Greenwich",0,
AUTHORITY["EPSG","8901"]],
UNIT["degree",0.01745329251994328,
AUTHORITY["EPSG","9122"]],
AUTHORITY["EPSG","4326"]]
>>> print srs.proj
+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs
>>> print srs.ellipsoid
(6378137.0, 6356752.3142451793, 298.25722356300003)
>>> print srs.projected, srs.geographic
False True
>>> srs.import_epsg(32140)
>>> print srs.name
NAD83 / Texas South Central
"""
import re
from types import UnicodeType, TupleType
from ctypes import byref, c_char_p, c_int, c_void_p
# Getting the error checking routine and exceptions
from django.contrib.gis.gdal.error import OGRException, SRSException
from django.contrib.gis.gdal.prototypes.srs import *
#### Spatial Reference class. ####
class SpatialReference(object):
"""
A wrapper for the OGRSpatialReference object. According to the GDAL website,
the SpatialReference object "provide[s] services to represent coordinate
systems (projections and datums) and to transform between them."
"""
# Well-Known Geographical Coordinate System Name
_well_known = {'WGS84':4326, 'WGS72':4322, 'NAD27':4267, 'NAD83':4269}
_epsg_regex = re.compile('^(EPSG:)?(?P<epsg>\d+)$', re.I)
_proj_regex = re.compile(r'^\+proj')
#### Python 'magic' routines ####
def __init__(self, srs_input='', srs_type='wkt'):
"""
Creates a GDAL OSR Spatial Reference object from the given input.
The input may be string of OGC Well Known Text (WKT), an integer
EPSG code, a PROJ.4 string, and/or a projection "well known" shorthand
string (one of 'WGS84', 'WGS72', 'NAD27', 'NAD83').
"""
# Intializing pointer and string buffer.
self._ptr = None
buf = c_char_p('')
if isinstance(srs_input, basestring):
# Encoding to ASCII if unicode passed in.
if isinstance(srs_input, UnicodeType):
srs_input = srs_input.encode('ascii')
epsg_m = self._epsg_regex.match(srs_input)
proj_m = self._proj_regex.match(srs_input)
if epsg_m:
# Is this an EPSG well known name?
srs_type = 'epsg'
srs_input = int(epsg_m.group('epsg'))
elif proj_m:
# Is the string a PROJ.4 string?
srs_type = 'proj'
elif srs_input in self._well_known:
# Is this a short-hand well known name?
srs_type = 'epsg'
srs_input = self._well_known[srs_input]
elif srs_type == 'proj':
pass
else:
# Setting the buffer with WKT, PROJ.4 string, etc.
buf = c_char_p(srs_input)
elif isinstance(srs_input, int):
# EPSG integer code was input.
if srs_type != 'epsg': srs_type = 'epsg'
elif isinstance(srs_input, c_void_p):
srs_type = 'ogr'
else:
raise TypeError('Invalid SRS type "%s"' % srs_type)
if srs_type == 'ogr':
# SRS input is OGR pointer
srs = srs_input
else:
# Creating a new pointer, using the string buffer.
srs = new_srs(buf)
# If the pointer is NULL, throw an exception.
if not srs:
raise SRSException('Could not create spatial reference from: %s' % srs_input)
else:
self._ptr = srs
# Post-processing if in PROJ.4 or EPSG formats.
if srs_type == 'proj': self.import_proj(srs_input)
elif srs_type == 'epsg': self.import_epsg(srs_input)
def __del__(self):
"Destroys this spatial reference."
if self._ptr: release_srs(self._ptr)
def __getitem__(self, target):
"""
Returns the value of the given string attribute node, None if the node
doesn't exist. Can also take a tuple as a parameter, (target, child),
where child is the index of the attribute in the WKT. For example:
>>> wkt = 'GEOGCS["WGS 84", DATUM["WGS_1984, ... AUTHORITY["EPSG","4326"]]')
>>> srs = SpatialReference(wkt) # could also use 'WGS84', or 4326
>>> print srs['GEOGCS']
WGS 84
>>> print srs['DATUM']
WGS_1984
>>> print srs['AUTHORITY']
EPSG
>>> print srs['AUTHORITY', 1] # The authority value
4326
>>> print srs['TOWGS84', 4] # the fourth value in this wkt
0
>>> print srs['UNIT|AUTHORITY'] # For the units authority, have to use the pipe symbole.
EPSG
>>> print srs['UNIT|AUTHORITY', 1] # The authority value for the untis
9122
"""
if isinstance(target, TupleType):
return self.attr_value(*target)
else:
return self.attr_value(target)
def __str__(self):
"The string representation uses 'pretty' WKT."
return self.pretty_wkt
#### SpatialReference Methods ####
def attr_value(self, target, index=0):
"""
The attribute value for the given target node (e.g. 'PROJCS'). The index
keyword specifies an index of the child node to return.
"""
if not isinstance(target, str) or not isinstance(index, int):
raise TypeError
return get_attr_value(self._ptr, target, index)
def auth_name(self, target):
"Returns the authority name for the given string target node."
return get_auth_name(self._ptr, target)
def auth_code(self, target):
"Returns the authority code for the given string target node."
return get_auth_code(self._ptr, target)
def clone(self):
"Returns a clone of this SpatialReference object."
return SpatialReference(clone_srs(self._ptr))
def from_esri(self):
"Morphs this SpatialReference from ESRI's format to EPSG."
morph_from_esri(self._ptr)
def identify_epsg(self):
"""
This method inspects the WKT of this SpatialReference, and will
add EPSG authority nodes where an EPSG identifier is applicable.
"""
identify_epsg(self._ptr)
def to_esri(self):
"Morphs this SpatialReference to ESRI's format."
morph_to_esri(self._ptr)
def validate(self):
"Checks to see if the given spatial reference is valid."
srs_validate(self._ptr)
#### Name & SRID properties ####
@property
def name(self):
"Returns the name of this Spatial Reference."
if self.projected: return self.attr_value('PROJCS')
elif self.geographic: return self.attr_value('GEOGCS')
elif self.local: return self.attr_value('LOCAL_CS')
else: return None
@property
def srid(self):
"Returns the SRID of top-level authority, or None if undefined."
try:
return int(self.attr_value('AUTHORITY', 1))
except (TypeError, ValueError):
return None
#### Unit Properties ####
@property
def linear_name(self):
"Returns the name of the linear units."
units, name = linear_units(self._ptr, byref(c_char_p()))
return name
@property
def linear_units(self):
"Returns the value of the linear units."
units, name = linear_units(self._ptr, byref(c_char_p()))
return units
@property
def angular_name(self):
"Returns the name of the angular units."
units, name = angular_units(self._ptr, byref(c_char_p()))
return name
@property
def angular_units(self):
"Returns the value of the angular units."
units, name = angular_units(self._ptr, byref(c_char_p()))
return units
@property
def units(self):
"""
Returns a 2-tuple of the units value and the units name,
and will automatically determines whether to return the linear
or angular units.
"""
if self.projected or self.local:
return linear_units(self._ptr, byref(c_char_p()))
elif self.geographic:
return angular_units(self._ptr, byref(c_char_p()))
else:
return (None, None)
#### Spheroid/Ellipsoid Properties ####
@property
def ellipsoid(self):
"""
Returns a tuple of the ellipsoid parameters:
(semimajor axis, semiminor axis, and inverse flattening)
"""
return (self.semi_major, self.semi_minor, self.inverse_flattening)
@property
def semi_major(self):
"Returns the Semi Major Axis for this Spatial Reference."
return semi_major(self._ptr, byref(c_int()))
@property
def semi_minor(self):
"Returns the Semi Minor Axis for this Spatial Reference."
return semi_minor(self._ptr, byref(c_int()))
@property
def inverse_flattening(self):
"Returns the Inverse Flattening for this Spatial Reference."
return invflattening(self._ptr, byref(c_int()))
#### Boolean Properties ####
@property
def geographic(self):
"""
Returns True if this SpatialReference is geographic
(root node is GEOGCS).
"""
return bool(isgeographic(self._ptr))
@property
def local(self):
"Returns True if this SpatialReference is local (root node is LOCAL_CS)."
return bool(islocal(self._ptr))
@property
def projected(self):
"""
Returns True if this SpatialReference is a projected coordinate system
(root node is PROJCS).
"""
return bool(isprojected(self._ptr))
#### Import Routines #####
def import_wkt(self, wkt):
"Imports the Spatial Reference from OGC WKT (string)"
from_wkt(self._ptr, byref(c_char_p(wkt)))
def import_proj(self, proj):
"Imports the Spatial Reference from a PROJ.4 string."
from_proj(self._ptr, proj)
def import_epsg(self, epsg):
"Imports the Spatial Reference from the EPSG code (an integer)."
from_epsg(self._ptr, epsg)
def import_xml(self, xml):
"Imports the Spatial Reference from an XML string."
from_xml(self._ptr, xml)
#### Export Properties ####
@property
def wkt(self):
"Returns the WKT representation of this Spatial Reference."
return to_wkt(self._ptr, byref(c_char_p()))
@property
def pretty_wkt(self, simplify=0):
"Returns the 'pretty' representation of the WKT."
return to_pretty_wkt(self._ptr, byref(c_char_p()), simplify)
@property
def proj(self):
"Returns the PROJ.4 representation for this Spatial Reference."
return to_proj(self._ptr, byref(c_char_p()))
@property
def proj4(self):
"Alias for proj()."
return self.proj
@property
def xml(self, dialect=''):
"Returns the XML representation of this Spatial Reference."
# FIXME: This leaks memory, have to figure out why.
return to_xml(self._ptr, byref(c_char_p()), dialect)
def to_esri(self):
"Morphs this SpatialReference to ESRI's format."
morph_to_esri(self._ptr)
def from_esri(self):
"Morphs this SpatialReference from ESRI's format to EPSG."
morph_from_esri(self._ptr)
class CoordTransform(object):
"The coordinate system transformation object."
def __init__(self, source, target):
"Initializes on a source and target SpatialReference objects."
self._ptr = None # Initially NULL
if not isinstance(source, SpatialReference) or not isinstance(target, SpatialReference):
raise SRSException('source and target must be of type SpatialReference')
self._ptr = new_ct(source._ptr, target._ptr)
if not self._ptr:
raise SRSException('could not intialize CoordTransform object')
self._srs1_name = source.name
self._srs2_name = target.name
def __del__(self):
"Deletes this Coordinate Transformation object."
if self._ptr: destroy_ct(self._ptr)
def __str__(self):
return 'Transform from "%s" to "%s"' % (self._srs1_name, self._srs2_name)

View File

@ -0,0 +1,27 @@
Copyright (c) 2007, Justin Bronn
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of GEOSGeometry nor the names of its contributors may be used
to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,69 @@
"""
The goal of this module is to be a ctypes wrapper around the GEOS library
that will work on both *NIX and Windows systems. Specifically, this uses
the GEOS C api.
I have several motivations for doing this:
(1) The GEOS SWIG wrapper is no longer maintained, and requires the
installation of SWIG.
(2) The PCL implementation is over 2K+ lines of C and would make
PCL a requisite package for the GeoDjango application stack.
(3) Windows and Mac compatibility becomes substantially easier, and does not
require the additional compilation of PCL or GEOS and SWIG -- all that
is needed is a Win32 or Mac compiled GEOS C library (dll or dylib)
in a location that Python can read (e.g. 'C:\Python25').
In summary, I wanted to wrap GEOS in a more maintainable and portable way using
only Python and the excellent ctypes library (now standard in Python 2.5).
In the spirit of loose coupling, this library does not require Django or
GeoDjango. Only the GEOS C library and ctypes are needed for the platform
of your choice.
For more information about GEOS:
http://geos.refractions.net
For more info about PCL and the discontinuation of the Python GEOS
library see Sean Gillies' writeup (and subsequent update) at:
http://zcologia.com/news/150/geometries-for-python/
http://zcologia.com/news/429/geometries-for-python-update/
"""
from django.contrib.gis.geos.base import GEOSGeometry, wkt_regex, hex_regex
from django.contrib.gis.geos.geometries import Point, LineString, LinearRing, Polygon, HAS_NUMPY
from django.contrib.gis.geos.collections import GeometryCollection, MultiPoint, MultiLineString, MultiPolygon
from django.contrib.gis.geos.error import GEOSException, GEOSIndexError
from django.contrib.gis.geos.libgeos import geos_version, geos_version_info
def fromfile(file_name):
"""
Given a string file name, returns a GEOSGeometry. The file may contain WKB,
WKT, or HEX.
"""
fh = open(file_name, 'rb')
buf = fh.read()
fh.close()
if wkt_regex.match(buf) or hex_regex.match(buf):
return GEOSGeometry(buf)
else:
return GEOSGeometry(buffer(buf))
def fromstr(wkt_or_hex, **kwargs):
"Given a string value (wkt or hex), returns a GEOSGeometry object."
return GEOSGeometry(wkt_or_hex, **kwargs)
def hex_to_wkt(hex):
"Converts HEXEWKB into WKT."
return GEOSGeometry(hex).wkt
def wkt_to_hex(wkt):
"Converts WKT into HEXEWKB."
return GEOSGeometry(wkt).hex
def centroid(input):
"Returns the centroid of the geometry (given in HEXEWKB)."
return GEOSGeometry(input).centroid.wkt
def area(input):
"Returns the area of the geometry (given in HEXEWKB)."
return GEOSGeometry(input).area

View File

@ -0,0 +1,608 @@
"""
This module contains the 'base' GEOSGeometry object -- all GEOS Geometries
inherit from this object.
"""
# Python, ctypes and types dependencies.
import re
from ctypes import addressof, byref, c_double, c_size_t
from types import UnicodeType
# GEOS-related dependencies.
from django.contrib.gis.geos.coordseq import GEOSCoordSeq
from django.contrib.gis.geos.error import GEOSException
from django.contrib.gis.geos.libgeos import GEOM_PTR
# All other functions in this module come from the ctypes
# prototypes module -- which handles all interaction with
# the underlying GEOS library.
from django.contrib.gis.geos.prototypes import *
# Trying to import GDAL libraries, if available. Have to place in
# try/except since this package may be used outside GeoDjango.
try:
from django.contrib.gis.gdal import OGRGeometry, SpatialReference, GEOJSON
HAS_GDAL = True
except:
HAS_GDAL, GEOJSON = False, False
# Regular expression for recognizing HEXEWKB and WKT. A prophylactic measure
# to prevent potentially malicious input from reaching the underlying C
# library. Not a substitute for good web security programming practices.
hex_regex = re.compile(r'^[0-9A-F]+$', re.I)
wkt_regex = re.compile(r'^(SRID=(?P<srid>\d+);)?(?P<wkt>(POINT|LINESTRING|LINEARRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON|GEOMETRYCOLLECTION)[ACEGIMLONPSRUTY\d,\.\-\(\) ]+)$', re.I)
json_regex = re.compile(r'^\{.+\}$')
class GEOSGeometry(object):
"A class that, generally, encapsulates a GEOS geometry."
# Initially, the geometry pointer is NULL
_ptr = None
#### Python 'magic' routines ####
def __init__(self, geo_input, srid=None):
"""
The base constructor for GEOS geometry objects, and may take the
following inputs:
* string: WKT
* string: HEXEWKB (a PostGIS-specific canonical form)
* buffer: WKB
The `srid` keyword is used to specify the Source Reference Identifier
(SRID) number for this Geometry. If not set, the SRID will be None.
"""
if isinstance(geo_input, basestring):
if isinstance(geo_input, UnicodeType):
# Encoding to ASCII, WKT or HEXEWKB doesn't need any more.
geo_input = geo_input.encode('ascii')
wkt_m = wkt_regex.match(geo_input)
if wkt_m:
# Handling WKT input.
if wkt_m.group('srid'): srid = int(wkt_m.group('srid'))
g = from_wkt(wkt_m.group('wkt'))
elif hex_regex.match(geo_input):
# Handling HEXEWKB input.
g = from_hex(geo_input, len(geo_input))
elif GEOJSON and json_regex.match(geo_input):
# Handling GeoJSON input.
wkb_input = str(OGRGeometry(geo_input).wkb)
g = from_wkb(wkb_input, len(wkb_input))
else:
raise ValueError('String or unicode input unrecognized as WKT EWKT, and HEXEWKB.')
elif isinstance(geo_input, GEOM_PTR):
# When the input is a pointer to a geomtry (GEOM_PTR).
g = geo_input
elif isinstance(geo_input, buffer):
# When the input is a buffer (WKB).
wkb_input = str(geo_input)
g = from_wkb(wkb_input, len(wkb_input))
else:
# Invalid geometry type.
raise TypeError('Improper geometry input type: %s' % str(type(geo_input)))
if bool(g):
# Setting the pointer object with a valid pointer.
self._ptr = g
else:
raise GEOSException('Could not initialize GEOS Geometry with given input.')
# Post-initialization setup.
self._post_init(srid)
def _post_init(self, srid):
"Helper routine for performing post-initialization setup."
# Setting the SRID, if given.
if srid and isinstance(srid, int): self.srid = srid
# Setting the class type (e.g., Point, Polygon, etc.)
self.__class__ = GEOS_CLASSES[self.geom_typeid]
# Setting the coordinate sequence for the geometry (will be None on
# geometries that do not have coordinate sequences)
self._set_cs()
@property
def ptr(self):
"""
Property for controlling access to the GEOS geometry pointer. Using
this raises an exception when the pointer is NULL, thus preventing
the C library from attempting to access an invalid memory location.
"""
if self._ptr:
return self._ptr
else:
raise GEOSException('NULL GEOS pointer encountered; was this geometry modified?')
def __del__(self):
"""
Destroys this Geometry; in other words, frees the memory used by the
GEOS C++ object.
"""
if self._ptr: destroy_geom(self._ptr)
def __copy__(self):
"""
Returns a clone because the copy of a GEOSGeometry may contain an
invalid pointer location if the original is garbage collected.
"""
return self.clone()
def __deepcopy__(self, memodict):
"""
The `deepcopy` routine is used by the `Node` class of django.utils.tree;
thus, the protocol routine needs to be implemented to return correct
copies (clones) of these GEOS objects, which use C pointers.
"""
return self.clone()
def __str__(self):
"WKT is used for the string representation."
return self.wkt
def __repr__(self):
"Short-hand representation because WKT may be very large."
return '<%s object at %s>' % (self.geom_type, hex(addressof(self.ptr)))
# Pickling support
def __getstate__(self):
# The pickled state is simply a tuple of the WKB (in string form)
# and the SRID.
return str(self.wkb), self.srid
def __setstate__(self, state):
# Instantiating from the tuple state that was pickled.
wkb, srid = state
ptr = from_wkb(wkb, len(wkb))
if not ptr: raise GEOSException('Invalid Geometry loaded from pickled state.')
self._ptr = ptr
self._post_init(srid)
# Comparison operators
def __eq__(self, other):
"""
Equivalence testing, a Geometry may be compared with another Geometry
or a WKT representation.
"""
if isinstance(other, basestring):
return self.wkt == other
elif isinstance(other, GEOSGeometry):
return self.equals_exact(other)
else:
return False
def __ne__(self, other):
"The not equals operator."
return not (self == other)
### Geometry set-like operations ###
# Thanks to Sean Gillies for inspiration:
# http://lists.gispython.org/pipermail/community/2007-July/001034.html
# g = g1 | g2
def __or__(self, other):
"Returns the union of this Geometry and the other."
return self.union(other)
# g = g1 & g2
def __and__(self, other):
"Returns the intersection of this Geometry and the other."
return self.intersection(other)
# g = g1 - g2
def __sub__(self, other):
"Return the difference this Geometry and the other."
return self.difference(other)
# g = g1 ^ g2
def __xor__(self, other):
"Return the symmetric difference of this Geometry and the other."
return self.sym_difference(other)
#### Coordinate Sequence Routines ####
@property
def has_cs(self):
"Returns True if this Geometry has a coordinate sequence, False if not."
# Only these geometries are allowed to have coordinate sequences.
if isinstance(self, (Point, LineString, LinearRing)):
return True
else:
return False
def _set_cs(self):
"Sets the coordinate sequence for this Geometry."
if self.has_cs:
self._cs = GEOSCoordSeq(get_cs(self.ptr), self.hasz)
else:
self._cs = None
@property
def coord_seq(self):
"Returns a clone of the coordinate sequence for this Geometry."
if self.has_cs:
return self._cs.clone()
#### Geometry Info ####
@property
def geom_type(self):
"Returns a string representing the Geometry type, e.g. 'Polygon'"
return geos_type(self.ptr)
@property
def geom_typeid(self):
"Returns an integer representing the Geometry type."
return geos_typeid(self.ptr)
@property
def num_geom(self):
"Returns the number of geometries in the Geometry."
return get_num_geoms(self.ptr)
@property
def num_coords(self):
"Returns the number of coordinates in the Geometry."
return get_num_coords(self.ptr)
@property
def num_points(self):
"Returns the number points, or coordinates, in the Geometry."
return self.num_coords
@property
def dims(self):
"Returns the dimension of this Geometry (0=point, 1=line, 2=surface)."
return get_dims(self.ptr)
def normalize(self):
"Converts this Geometry to normal form (or canonical form)."
return geos_normalize(self.ptr)
#### Unary predicates ####
@property
def empty(self):
"""
Returns a boolean indicating whether the set of points in this Geometry
are empty.
"""
return geos_isempty(self.ptr)
@property
def hasz(self):
"Returns whether the geometry has a 3D dimension."
return geos_hasz(self.ptr)
@property
def ring(self):
"Returns whether or not the geometry is a ring."
return geos_isring(self.ptr)
@property
def simple(self):
"Returns false if the Geometry not simple."
return geos_issimple(self.ptr)
@property
def valid(self):
"This property tests the validity of this Geometry."
return geos_isvalid(self.ptr)
#### Binary predicates. ####
def contains(self, other):
"Returns true if other.within(this) returns true."
return geos_contains(self.ptr, other.ptr)
def crosses(self, other):
"""
Returns true if the DE-9IM intersection matrix for the two Geometries
is T*T****** (for a point and a curve,a point and an area or a line and
an area) 0******** (for two curves).
"""
return geos_crosses(self.ptr, other.ptr)
def disjoint(self, other):
"""
Returns true if the DE-9IM intersection matrix for the two Geometries
is FF*FF****.
"""
return geos_disjoint(self.ptr, other.ptr)
def equals(self, other):
"""
Returns true if the DE-9IM intersection matrix for the two Geometries
is T*F**FFF*.
"""
return geos_equals(self.ptr, other.ptr)
def equals_exact(self, other, tolerance=0):
"""
Returns true if the two Geometries are exactly equal, up to a
specified tolerance.
"""
return geos_equalsexact(self.ptr, other.ptr, float(tolerance))
def intersects(self, other):
"Returns true if disjoint returns false."
return geos_intersects(self.ptr, other.ptr)
def overlaps(self, other):
"""
Returns true if the DE-9IM intersection matrix for the two Geometries
is T*T***T** (for two points or two surfaces) 1*T***T** (for two curves).
"""
return geos_overlaps(self.ptr, other.ptr)
def relate_pattern(self, other, pattern):
"""
Returns true if the elements in the DE-9IM intersection matrix for the
two Geometries match the elements in pattern.
"""
if not isinstance(pattern, str) or len(pattern) > 9:
raise GEOSException('invalid intersection matrix pattern')
return geos_relatepattern(self.ptr, other.ptr, pattern)
def touches(self, other):
"""
Returns true if the DE-9IM intersection matrix for the two Geometries
is FT*******, F**T***** or F***T****.
"""
return geos_touches(self.ptr, other.ptr)
def within(self, other):
"""
Returns true if the DE-9IM intersection matrix for the two Geometries
is T*F**F***.
"""
return geos_within(self.ptr, other.ptr)
#### SRID Routines ####
def get_srid(self):
"Gets the SRID for the geometry, returns None if no SRID is set."
s = geos_get_srid(self.ptr)
if s == 0: return None
else: return s
def set_srid(self, srid):
"Sets the SRID for the geometry."
geos_set_srid(self.ptr, srid)
srid = property(get_srid, set_srid)
#### Output Routines ####
@property
def ewkt(self):
"Returns the EWKT (WKT + SRID) of the Geometry."
if self.get_srid(): return 'SRID=%s;%s' % (self.srid, self.wkt)
else: return self.wkt
@property
def wkt(self):
"Returns the WKT (Well-Known Text) of the Geometry."
return to_wkt(self.ptr)
@property
def hex(self):
"""
Returns the HEX of the Geometry -- please note that the SRID is not
included in this representation, because the GEOS C library uses
-1 by default, even if the SRID is set.
"""
# A possible faster, all-python, implementation:
# str(self.wkb).encode('hex')
return to_hex(self.ptr, byref(c_size_t()))
@property
def json(self):
"""
Returns GeoJSON representation of this Geometry if GDAL 1.5+
is installed.
"""
if GEOJSON: return self.ogr.json
geojson = json
@property
def wkb(self):
"Returns the WKB of the Geometry as a buffer."
bin = to_wkb(self.ptr, byref(c_size_t()))
return buffer(bin)
@property
def kml(self):
"Returns the KML representation of this Geometry."
gtype = self.geom_type
return '<%s>%s</%s>' % (gtype, self.coord_seq.kml, gtype)
#### GDAL-specific output routines ####
@property
def ogr(self):
"Returns the OGR Geometry for this Geometry."
if HAS_GDAL:
if self.srid:
return OGRGeometry(self.wkb, self.srid)
else:
return OGRGeometry(self.wkb)
else:
return None
@property
def srs(self):
"Returns the OSR SpatialReference for SRID of this Geometry."
if HAS_GDAL and self.srid:
return SpatialReference(self.srid)
else:
return None
@property
def crs(self):
"Alias for `srs` property."
return self.srs
def transform(self, ct, clone=False):
"""
Requires GDAL. Transforms the geometry according to the given
transformation object, which may be an integer SRID, and WKT or
PROJ.4 string. By default, the geometry is transformed in-place and
nothing is returned. However if the `clone` keyword is set, then this
geometry will not be modified and a transformed clone will be returned
instead.
"""
srid = self.srid
if HAS_GDAL and srid:
g = OGRGeometry(self.wkb, srid)
g.transform(ct)
wkb = str(g.wkb)
ptr = from_wkb(wkb, len(wkb))
if clone:
# User wants a cloned transformed geometry returned.
return GEOSGeometry(ptr, srid=g.srid)
if ptr:
# Reassigning pointer, and performing post-initialization setup
# again due to the reassignment.
destroy_geom(self.ptr)
self._ptr = ptr
self._post_init(g.srid)
else:
raise GEOSException('Transformed WKB was invalid.')
#### Topology Routines ####
def _topology(self, gptr):
"Helper routine to return Geometry from the given pointer."
return GEOSGeometry(gptr, srid=self.srid)
@property
def boundary(self):
"Returns the boundary as a newly allocated Geometry object."
return self._topology(geos_boundary(self.ptr))
def buffer(self, width, quadsegs=8):
"""
Returns a geometry that represents all points whose distance from this
Geometry is less than or equal to distance. Calculations are in the
Spatial Reference System of this Geometry. The optional third parameter sets
the number of segment used to approximate a quarter circle (defaults to 8).
(Text from PostGIS documentation at ch. 6.1.3)
"""
return self._topology(geos_buffer(self.ptr, width, quadsegs))
@property
def centroid(self):
"""
The centroid is equal to the centroid of the set of component Geometries
of highest dimension (since the lower-dimension geometries contribute zero
"weight" to the centroid).
"""
return self._topology(geos_centroid(self.ptr))
@property
def convex_hull(self):
"""
Returns the smallest convex Polygon that contains all the points
in the Geometry.
"""
return self._topology(geos_convexhull(self.ptr))
def difference(self, other):
"""
Returns a Geometry representing the points making up this Geometry
that do not make up other.
"""
return self._topology(geos_difference(self.ptr, other.ptr))
@property
def envelope(self):
"Return the envelope for this geometry (a polygon)."
return self._topology(geos_envelope(self.ptr))
def intersection(self, other):
"Returns a Geometry representing the points shared by this Geometry and other."
return self._topology(geos_intersection(self.ptr, other.ptr))
@property
def point_on_surface(self):
"Computes an interior point of this Geometry."
return self._topology(geos_pointonsurface(self.ptr))
def relate(self, other):
"Returns the DE-9IM intersection matrix for this Geometry and the other."
return geos_relate(self.ptr, other.ptr)
def simplify(self, tolerance=0.0, preserve_topology=False):
"""
Returns the Geometry, simplified using the Douglas-Peucker algorithm
to the specified tolerance (higher tolerance => less points). If no
tolerance provided, defaults to 0.
By default, this function does not preserve topology - e.g. polygons can
be split, collapse to lines or disappear holes can be created or
disappear, and lines can cross. By specifying preserve_topology=True,
the result will have the same dimension and number of components as the
input. This is significantly slower.
"""
if preserve_topology:
return self._topology(geos_preservesimplify(self.ptr, tolerance))
else:
return self._topology(geos_simplify(self.ptr, tolerance))
def sym_difference(self, other):
"""
Returns a set combining the points in this Geometry not in other,
and the points in other not in this Geometry.
"""
return self._topology(geos_symdifference(self.ptr, other.ptr))
def union(self, other):
"Returns a Geometry representing all the points in this Geometry and other."
return self._topology(geos_union(self.ptr, other.ptr))
#### Other Routines ####
@property
def area(self):
"Returns the area of the Geometry."
return geos_area(self.ptr, byref(c_double()))
def distance(self, other):
"""
Returns the distance between the closest points on this Geometry
and the other. Units will be in those of the coordinate system of
the Geometry.
"""
if not isinstance(other, GEOSGeometry):
raise TypeError('distance() works only on other GEOS Geometries.')
return geos_distance(self.ptr, other.ptr, byref(c_double()))
@property
def extent(self):
"""
Returns the extent of this geometry as a 4-tuple, consisting of
(xmin, ymin, xmax, ymax).
"""
env = self.envelope
if isinstance(env, Point):
xmin, ymin = env.tuple
xmax, ymax = xmin, ymin
else:
xmin, ymin = env[0][0]
xmax, ymax = env[0][2]
return (xmin, ymin, xmax, ymax)
@property
def length(self):
"""
Returns the length of this Geometry (e.g., 0 for point, or the
circumfrence of a Polygon).
"""
return geos_length(self.ptr, byref(c_double()))
def clone(self):
"Clones this Geometry."
return GEOSGeometry(geom_clone(self.ptr), srid=self.srid)
# Class mapping dictionary
from django.contrib.gis.geos.geometries import Point, Polygon, LineString, LinearRing
from django.contrib.gis.geos.collections import GeometryCollection, MultiPoint, MultiLineString, MultiPolygon
GEOS_CLASSES = {0 : Point,
1 : LineString,
2 : LinearRing,
3 : Polygon,
4 : MultiPoint,
5 : MultiLineString,
6 : MultiPolygon,
7 : GeometryCollection,
}

View File

@ -0,0 +1,105 @@
"""
This module houses the Geometry Collection objects:
GeometryCollection, MultiPoint, MultiLineString, and MultiPolygon
"""
from ctypes import c_int, c_uint, byref
from types import TupleType, ListType
from django.contrib.gis.geos.base import GEOSGeometry
from django.contrib.gis.geos.error import GEOSException, GEOSIndexError
from django.contrib.gis.geos.geometries import Point, LineString, LinearRing, Polygon
from django.contrib.gis.geos.libgeos import get_pointer_arr, GEOM_PTR
from django.contrib.gis.geos.prototypes import create_collection, destroy_geom, geom_clone, geos_typeid, get_cs, get_geomn
class GeometryCollection(GEOSGeometry):
_allowed = (Point, LineString, LinearRing, Polygon)
_typeid = 7
def __init__(self, *args, **kwargs):
"Initializes a Geometry Collection from a sequence of Geometry objects."
# Checking the arguments
if not args:
raise TypeError, 'Must provide at least one Geometry to initialize %s.' % self.__class__.__name__
if len(args) == 1:
# If only one geometry provided or a list of geometries is provided
# in the first argument.
if isinstance(args[0], (TupleType, ListType)):
init_geoms = args[0]
else:
init_geoms = args
else:
init_geoms = args
# Ensuring that only the permitted geometries are allowed in this collection
if False in [isinstance(geom, self._allowed) for geom in init_geoms]:
raise TypeError('Invalid Geometry type encountered in the arguments.')
# Creating the geometry pointer array.
ngeoms = len(init_geoms)
geoms = get_pointer_arr(ngeoms)
for i in xrange(ngeoms): geoms[i] = geom_clone(init_geoms[i].ptr)
super(GeometryCollection, self).__init__(create_collection(c_int(self._typeid), byref(geoms), c_uint(ngeoms)), **kwargs)
def __getitem__(self, index):
"Returns the Geometry from this Collection at the given index (0-based)."
# Checking the index and returning the corresponding GEOS geometry.
self._checkindex(index)
return GEOSGeometry(geom_clone(get_geomn(self.ptr, index)), srid=self.srid)
def __setitem__(self, index, geom):
"Sets the Geometry at the specified index."
self._checkindex(index)
if not isinstance(geom, self._allowed):
raise TypeError('Incompatible Geometry for collection.')
ngeoms = len(self)
geoms = get_pointer_arr(ngeoms)
for i in xrange(ngeoms):
if i == index:
geoms[i] = geom_clone(geom.ptr)
else:
geoms[i] = geom_clone(get_geomn(self.ptr, i))
# Creating a new collection, and destroying the contents of the previous poiner.
prev_ptr = self.ptr
srid = self.srid
self._ptr = create_collection(c_int(self._typeid), byref(geoms), c_uint(ngeoms))
if srid: self.srid = srid
destroy_geom(prev_ptr)
def __iter__(self):
"Iterates over each Geometry in the Collection."
for i in xrange(len(self)):
yield self.__getitem__(i)
def __len__(self):
"Returns the number of geometries in this Collection."
return self.num_geom
def _checkindex(self, index):
"Checks the given geometry index."
if index < 0 or index >= self.num_geom:
raise GEOSIndexError('invalid GEOS Geometry index: %s' % str(index))
@property
def kml(self):
"Returns the KML for this Geometry Collection."
return '<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
class MultiPolygon(GeometryCollection):
_allowed = Polygon
_typeid = 6

View File

@ -0,0 +1,164 @@
"""
This module houses the GEOSCoordSeq object, which is used internally
by GEOSGeometry to house the actual coordinates of the Point,
LineString, and LinearRing geometries.
"""
from ctypes import c_double, c_uint, byref
from types import ListType, TupleType
from django.contrib.gis.geos.error import GEOSException, GEOSIndexError
from django.contrib.gis.geos.libgeos import CS_PTR, HAS_NUMPY
from django.contrib.gis.geos.prototypes import cs_clone, cs_getdims, cs_getordinate, cs_getsize, cs_setordinate
if HAS_NUMPY: from numpy import ndarray
class GEOSCoordSeq(object):
"The internal representation of a list of coordinates inside a Geometry."
#### Python 'magic' routines ####
def __init__(self, ptr, z=False):
"Initializes from a GEOS pointer."
if not isinstance(ptr, CS_PTR):
raise TypeError('Coordinate sequence should initialize with a CS_PTR.')
self._ptr = ptr
self._z = z
def __iter__(self):
"Iterates over each point in the coordinate sequence."
for i in xrange(self.size):
yield self[i]
def __len__(self):
"Returns the number of points in the coordinate sequence."
return int(self.size)
def __str__(self):
"Returns the string representation of the coordinate sequence."
return str(self.tuple)
def __getitem__(self, index):
"Returns the coordinate sequence value at the given index."
coords = [self.getX(index), self.getY(index)]
if self.dims == 3 and self._z:
coords.append(self.getZ(index))
return tuple(coords)
def __setitem__(self, index, value):
"Sets the coordinate sequence value at the given index."
# Checking the input value
if isinstance(value, (ListType, TupleType)):
pass
elif HAS_NUMPY and isinstance(value, ndarray):
pass
else:
raise TypeError('Must set coordinate with a sequence (list, tuple, or numpy array).')
# Checking the dims of the input
if self.dims == 3 and self._z:
n_args = 3
set_3d = True
else:
n_args = 2
set_3d = False
if len(value) != n_args:
raise TypeError('Dimension of value does not match.')
# Setting the X, Y, Z
self.setX(index, value[0])
self.setY(index, value[1])
if set_3d: self.setZ(index, value[2])
#### Internal Routines ####
def _checkindex(self, index):
"Checks the given index."
sz = self.size
if (sz < 1) or (index < 0) or (index >= sz):
raise GEOSIndexError('invalid GEOS Geometry index: %s' % str(index))
def _checkdim(self, dim):
"Checks the given dimension."
if dim < 0 or dim > 2:
raise GEOSException('invalid ordinate dimension "%d"' % dim)
@property
def ptr(self):
"""
Property for controlling access to coordinate sequence pointer,
preventing attempted access to a NULL memory location.
"""
if self._ptr: return self._ptr
else: raise GEOSException('NULL coordinate sequence pointer encountered.')
#### Ordinate getting and setting routines ####
def getOrdinate(self, dimension, index):
"Returns the value for the given dimension and index."
self._checkindex(index)
self._checkdim(dimension)
return cs_getordinate(self.ptr, index, dimension, byref(c_double()))
def setOrdinate(self, dimension, index, value):
"Sets the value for the given dimension and index."
self._checkindex(index)
self._checkdim(dimension)
cs_setordinate(self.ptr, index, dimension, value)
def getX(self, index):
"Get the X value at the index."
return self.getOrdinate(0, index)
def setX(self, index, value):
"Set X with the value at the given index."
self.setOrdinate(0, index, value)
def getY(self, index):
"Get the Y value at the given index."
return self.getOrdinate(1, index)
def setY(self, index, value):
"Set Y with the value at the given index."
self.setOrdinate(1, index, value)
def getZ(self, index):
"Get Z with the value at the given index."
return self.getOrdinate(2, index)
def setZ(self, index, value):
"Set Z with the value at the given index."
self.setOrdinate(2, index, value)
### Dimensions ###
@property
def size(self):
"Returns the size of this coordinate sequence."
return cs_getsize(self.ptr, byref(c_uint()))
@property
def dims(self):
"Returns the dimensions of this coordinate sequence."
return cs_getdims(self.ptr, byref(c_uint()))
@property
def hasz(self):
"""
Returns whether this coordinate sequence is 3D. This property value is
inherited from the parent Geometry.
"""
return self._z
### Other Methods ###
def clone(self):
"Clones this coordinate sequence."
return GEOSCoordSeq(cs_clone(self.ptr), self.hasz)
@property
def kml(self):
"Returns the KML representation for the coordinates."
# Getting the substitution string depending on whether the coordinates have
# a Z dimension.
if self.hasz: substr = '%s,%s,%s '
else: substr = '%s,%s,0 '
return '<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)])

View File

@ -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

View File

@ -0,0 +1,391 @@
"""
This module houses the Point, LineString, LinearRing, and Polygon OGC
geometry classes. All geometry classes in this module inherit from
GEOSGeometry.
"""
from ctypes import c_uint, byref
from django.contrib.gis.geos.base import GEOSGeometry
from django.contrib.gis.geos.coordseq import GEOSCoordSeq
from django.contrib.gis.geos.error import GEOSException, GEOSIndexError
from django.contrib.gis.geos.libgeos import get_pointer_arr, GEOM_PTR, HAS_NUMPY
from django.contrib.gis.geos.prototypes import *
if HAS_NUMPY: from numpy import ndarray, array
class Point(GEOSGeometry):
def __init__(self, x, y=None, z=None, srid=None):
"""
The Point object may be initialized with either a tuple, or individual
parameters.
For Example:
>>> p = Point((5, 23)) # 2D point, passed in as a tuple
>>> p = Point(5, 23, 8) # 3D point, passed in with individual parameters
"""
if isinstance(x, (tuple, list)):
# Here a tuple or list was passed in under the `x` parameter.
ndim = len(x)
if ndim < 2 or ndim > 3:
raise TypeError('Invalid sequence parameter: %s' % str(x))
coords = x
elif isinstance(x, (int, float, long)) and isinstance(y, (int, float, long)):
# Here X, Y, and (optionally) Z were passed in individually, as parameters.
if isinstance(z, (int, float, long)):
ndim = 3
coords = [x, y, z]
else:
ndim = 2
coords = [x, y]
else:
raise TypeError('Invalid parameters given for Point initialization.')
# Creating the coordinate sequence, and setting X, Y, [Z]
cs = create_cs(c_uint(1), c_uint(ndim))
cs_setx(cs, 0, coords[0])
cs_sety(cs, 0, coords[1])
if ndim == 3: cs_setz(cs, 0, coords[2])
# Initializing using the address returned from the GEOS
# createPoint factory.
super(Point, self).__init__(create_point(cs), srid=srid)
def __len__(self):
"Returns the number of dimensions for this Point (either 0, 2 or 3)."
if self.empty: return 0
if self.hasz: return 3
else: return 2
def get_x(self):
"Returns the X component of the Point."
return self._cs.getOrdinate(0, 0)
def set_x(self, value):
"Sets the X component of the Point."
self._cs.setOrdinate(0, 0, value)
def get_y(self):
"Returns the Y component of the Point."
return self._cs.getOrdinate(1, 0)
def set_y(self, value):
"Sets the Y component of the Point."
self._cs.setOrdinate(1, 0, value)
def get_z(self):
"Returns the Z component of the Point."
if self.hasz:
return self._cs.getOrdinate(2, 0)
else:
return None
def set_z(self, value):
"Sets the Z component of the Point."
if self.hasz:
self._cs.setOrdinate(2, 0, value)
else:
raise GEOSException('Cannot set Z on 2D Point.')
# X, Y, Z properties
x = property(get_x, set_x)
y = property(get_y, set_y)
z = property(get_z, set_z)
### Tuple setting and retrieval routines. ###
def get_coords(self):
"Returns a tuple of the point."
return self._cs.tuple
def set_coords(self, tup):
"Sets the coordinates of the point with the given tuple."
self._cs[0] = tup
# The tuple and coords properties
tuple = property(get_coords, set_coords)
coords = tuple
class LineString(GEOSGeometry):
#### Python 'magic' routines ####
def __init__(self, *args, **kwargs):
"""
Initializes on the given sequence -- may take lists, tuples, NumPy arrays
of X,Y pairs, or Point objects. If Point objects are used, ownership is
_not_ transferred to the LineString object.
Examples:
ls = LineString((1, 1), (2, 2))
ls = LineString([(1, 1), (2, 2)])
ls = LineString(array([(1, 1), (2, 2)]))
ls = LineString(Point(1, 1), Point(2, 2))
"""
# If only one argument provided, set the coords array appropriately
if len(args) == 1: coords = args[0]
else: coords = args
if isinstance(coords, (tuple, list)):
# Getting the number of coords and the number of dimensions -- which
# must stay the same, e.g., no LineString((1, 2), (1, 2, 3)).
ncoords = len(coords)
if coords: ndim = len(coords[0])
else: raise TypeError('Cannot initialize on empty sequence.')
self._checkdim(ndim)
# Incrementing through each of the coordinates and verifying
for i in xrange(1, ncoords):
if not isinstance(coords[i], (tuple, list, Point)):
raise TypeError('each coordinate should be a sequence (list or tuple)')
if len(coords[i]) != ndim: raise TypeError('Dimension mismatch.')
numpy_coords = False
elif HAS_NUMPY and isinstance(coords, ndarray):
shape = coords.shape # Using numpy's shape.
if len(shape) != 2: raise TypeError('Too many dimensions.')
self._checkdim(shape[1])
ncoords = shape[0]
ndim = shape[1]
numpy_coords = True
else:
raise TypeError('Invalid initialization input for LineStrings.')
# Creating a coordinate sequence object because it is easier to
# set the points using GEOSCoordSeq.__setitem__().
cs = GEOSCoordSeq(create_cs(ncoords, ndim), z=bool(ndim==3))
for i in xrange(ncoords):
if numpy_coords: cs[i] = coords[i,:]
elif isinstance(coords[i], Point): cs[i] = coords[i].tuple
else: cs[i] = coords[i]
# Getting the correct initialization function
if kwargs.get('ring', False):
func = create_linearring
else:
func = create_linestring
# If SRID was passed in with the keyword arguments
srid = kwargs.get('srid', None)
# Calling the base geometry initialization with the returned pointer
# from the function.
super(LineString, self).__init__(func(cs.ptr), srid=srid)
def __getitem__(self, index):
"Gets the point at the specified index."
return self._cs[index]
def __setitem__(self, index, value):
"Sets the point at the specified index, e.g., line_str[0] = (1, 2)."
self._cs[index] = value
def __iter__(self):
"Allows iteration over this LineString."
for i in xrange(len(self)):
yield self[i]
def __len__(self):
"Returns the number of points in this LineString."
return len(self._cs)
def _checkdim(self, dim):
if dim not in (2, 3): raise TypeError('Dimension mismatch.')
#### Sequence Properties ####
@property
def tuple(self):
"Returns a tuple version of the geometry from the coordinate sequence."
return self._cs.tuple
coords = tuple
def _listarr(self, func):
"""
Internal routine that returns a sequence (list) corresponding with
the given function. Will return a numpy array if possible.
"""
lst = [func(i) for i in xrange(len(self))]
if HAS_NUMPY: return array(lst) # ARRRR!
else: return lst
@property
def array(self):
"Returns a numpy array for the LineString."
return self._listarr(self._cs.__getitem__)
@property
def x(self):
"Returns a list or numpy array of the X variable."
return self._listarr(self._cs.getX)
@property
def y(self):
"Returns a list or numpy array of the Y variable."
return self._listarr(self._cs.getY)
@property
def z(self):
"Returns a list or numpy array of the Z variable."
if not self.hasz: return None
else: return self._listarr(self._cs.getZ)
# LinearRings are LineStrings used within Polygons.
class LinearRing(LineString):
def __init__(self, *args, **kwargs):
"Overriding the initialization function to set the ring keyword."
kwargs['ring'] = True # Setting the ring keyword argument to True
super(LinearRing, self).__init__(*args, **kwargs)
class Polygon(GEOSGeometry):
def __init__(self, *args, **kwargs):
"""
Initializes on an exterior ring and a sequence of holes (both
instances may be either LinearRing instances, or a tuple/list
that may be constructed into a LinearRing).
Examples of initialization, where shell, hole1, and hole2 are
valid LinearRing geometries:
>>> poly = Polygon(shell, hole1, hole2)
>>> poly = Polygon(shell, (hole1, hole2))
Example where a tuple parameters are used:
>>> poly = Polygon(((0, 0), (0, 10), (10, 10), (0, 10), (0, 0)),
((4, 4), (4, 6), (6, 6), (6, 4), (4, 4)))
"""
if not args:
raise TypeError('Must provide at list one LinearRing instance to initialize Polygon.')
# Getting the ext_ring and init_holes parameters from the argument list
ext_ring = args[0]
init_holes = args[1:]
n_holes = len(init_holes)
# If initialized as Polygon(shell, (LinearRing, LinearRing)) [for backward-compatibility]
if n_holes == 1 and isinstance(init_holes[0], (tuple, list)) and \
(len(init_holes[0]) == 0 or isinstance(init_holes[0][0], LinearRing)):
init_holes = init_holes[0]
n_holes = len(init_holes)
# Ensuring the exterior ring and holes parameters are LinearRing objects
# or may be instantiated into LinearRings.
ext_ring = self._construct_ring(ext_ring, 'Exterior parameter must be a LinearRing or an object that can initialize a LinearRing.')
holes_list = [] # Create new list, cause init_holes is a tuple.
for i in xrange(n_holes):
holes_list.append(self._construct_ring(init_holes[i], 'Holes parameter must be a sequence of LinearRings or objects that can initialize to LinearRings'))
# Why another loop? Because if a TypeError is raised, cloned pointers will
# be around that can't be cleaned up.
holes = get_pointer_arr(n_holes)
for i in xrange(n_holes): holes[i] = geom_clone(holes_list[i].ptr)
# Getting the shell pointer address.
shell = geom_clone(ext_ring.ptr)
# Calling with the GEOS createPolygon factory.
super(Polygon, self).__init__(create_polygon(shell, byref(holes), c_uint(n_holes)), **kwargs)
def __getitem__(self, index):
"""
Returns the ring at the specified index. The first index, 0, will
always return the exterior ring. Indices > 0 will return the
interior ring at the given index (e.g., poly[1] and poly[2] would
return the first and second interior ring, respectively).
"""
if index == 0:
return self.exterior_ring
else:
# Getting the interior ring, have to subtract 1 from the index.
return self.get_interior_ring(index-1)
def __setitem__(self, index, ring):
"Sets the ring at the specified index with the given ring."
# Checking the index and ring parameters.
self._checkindex(index)
if not isinstance(ring, LinearRing):
raise TypeError('must set Polygon index with a LinearRing object')
# Getting the shell
if index == 0:
shell = geom_clone(ring.ptr)
else:
shell = geom_clone(get_extring(self.ptr))
# Getting the interior rings (holes)
nholes = len(self)-1
if nholes > 0:
holes = get_pointer_arr(nholes)
for i in xrange(nholes):
if i == (index-1):
holes[i] = geom_clone(ring.ptr)
else:
holes[i] = geom_clone(get_intring(self.ptr, i))
holes_param = byref(holes)
else:
holes_param = None
# Getting the current pointer, replacing with the newly constructed
# geometry, and destroying the old geometry.
prev_ptr = self.ptr
srid = self.srid
self._ptr = create_polygon(shell, holes_param, c_uint(nholes))
if srid: self.srid = srid
destroy_geom(prev_ptr)
def __iter__(self):
"Iterates over each ring in the polygon."
for i in xrange(len(self)):
yield self[i]
def __len__(self):
"Returns the number of rings in this Polygon."
return self.num_interior_rings + 1
def _checkindex(self, index):
"Internal routine for checking the given ring index."
if index < 0 or index >= len(self):
raise GEOSIndexError('invalid Polygon ring index: %s' % index)
def _construct_ring(self, param, msg=''):
"Helper routine for trying to construct a ring from the given parameter."
if isinstance(param, LinearRing): return param
try:
ring = LinearRing(param)
return ring
except TypeError:
raise TypeError(msg)
def get_interior_ring(self, ring_i):
"""
Gets the interior ring at the specified index, 0 is for the first
interior ring, not the exterior ring.
"""
self._checkindex(ring_i+1)
return GEOSGeometry(geom_clone(get_intring(self.ptr, ring_i)), srid=self.srid)
#### Polygon Properties ####
@property
def num_interior_rings(self):
"Returns the number of interior rings."
# Getting the number of rings
return get_nrings(self.ptr)
def get_ext_ring(self):
"Gets the exterior ring of the Polygon."
return GEOSGeometry(geom_clone(get_extring(self.ptr)), srid=self.srid)
def set_ext_ring(self, ring):
"Sets the exterior ring of the Polygon."
self[0] = ring
# properties for the exterior ring/shell
exterior_ring = property(get_ext_ring, set_ext_ring)
shell = exterior_ring
@property
def tuple(self):
"Gets the tuple for each ring in this Polygon."
return tuple([self[i].tuple for i in xrange(len(self))])
coords = tuple
@property
def kml(self):
"Returns the KML representation of this Polygon."
inner_kml = ''.join(["<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)

View File

@ -0,0 +1,126 @@
"""
This module houses the ctypes initialization procedures, as well
as the notice and error handler function callbacks (get called
when an error occurs in GEOS).
This module also houses GEOS Pointer utilities, including
get_pointer_arr(), and GEOM_PTR.
"""
import atexit, os, re, sys
from ctypes import c_char_p, Structure, CDLL, CFUNCTYPE, POINTER
from ctypes.util import find_library
from django.contrib.gis.geos.error import GEOSException
# NumPy supported?
try:
from numpy import array, ndarray
HAS_NUMPY = True
except ImportError:
HAS_NUMPY = False
# Custom library path set?
try:
from django.conf import settings
lib_path = settings.GEOS_LIBRARY_PATH
except (AttributeError, EnvironmentError, ImportError):
lib_path = None
# Setting the appropriate names for the GEOS-C library.
if lib_path:
lib_names = None
elif os.name == 'nt':
# Windows NT libraries
lib_names = ['libgeos_c-1']
elif os.name == 'posix':
# *NIX libraries
lib_names = ['geos_c']
else:
raise GEOSException('Unsupported OS "%s"' % os.name)
# Using the ctypes `find_library` utility to find the the path to the GEOS
# shared library. This is better than manually specifiying each library name
# and extension (e.g., libgeos_c.[so|so.1|dylib].).
if lib_names:
for lib_name in lib_names:
lib_path = find_library(lib_name)
if not lib_path is None: break
# No GEOS library could be found.
if lib_path is None:
raise GEOSException('Could not find the GEOS library (tried "%s"). '
'Try setting GEOS_LIBRARY_PATH in your settings.' %
'", "'.join(lib_names))
# Getting the GEOS C library. The C interface (CDLL) is used for
# both *NIX and Windows.
# See the GEOS C API source code for more details on the library function calls:
# http://geos.refractions.net/ro/doxygen_docs/html/geos__c_8h-source.html
lgeos = CDLL(lib_path)
# The notice and error handler C function callback definitions.
# Supposed to mimic the GEOS message handler (C below):
# "typedef void (*GEOSMessageHandler)(const char *fmt, ...);"
NOTICEFUNC = CFUNCTYPE(None, c_char_p, c_char_p)
def notice_h(fmt, lst, output_h=sys.stdout):
try:
warn_msg = fmt % lst
except:
warn_msg = fmt
output_h.write('GEOS_NOTICE: %s\n' % warn_msg)
notice_h = NOTICEFUNC(notice_h)
ERRORFUNC = CFUNCTYPE(None, c_char_p, c_char_p)
def error_h(fmt, lst, output_h=sys.stderr):
try:
err_msg = fmt % lst
except:
err_msg = fmt
output_h.write('GEOS_ERROR: %s\n' % err_msg)
error_h = ERRORFUNC(error_h)
# The initGEOS routine should be called first, however, that routine takes
# the notice and error functions as parameters. Here is the C code that
# is wrapped:
# "extern void GEOS_DLL initGEOS(GEOSMessageHandler notice_function, GEOSMessageHandler error_function);"
lgeos.initGEOS(notice_h, error_h)
#### GEOS Geometry C data structures, and utility functions. ####
# Opaque GEOS geometry structures, used for GEOM_PTR and CS_PTR
class GEOSGeom_t(Structure): pass
class GEOSCoordSeq_t(Structure): pass
# Pointers to opaque GEOS geometry structures.
GEOM_PTR = POINTER(GEOSGeom_t)
CS_PTR = POINTER(GEOSCoordSeq_t)
# Used specifically by the GEOSGeom_createPolygon and GEOSGeom_createCollection
# GEOS routines
def get_pointer_arr(n):
"Gets a ctypes pointer array (of length `n`) for GEOSGeom_t opaque pointer."
GeomArr = GEOM_PTR * n
return GeomArr()
# Returns the string version of the GEOS library. Have to set the restype
# explicitly to c_char_p to ensure compatibility accross 32 and 64-bit platforms.
geos_version = lgeos.GEOSversion
geos_version.argtypes = None
geos_version.restype = c_char_p
# Regular expression should be able to parse version strings such as
# '3.0.0rc4-CAPI-1.3.3', or '3.0.0-CAPI-1.4.1'
version_regex = re.compile(r'^(?P<version>\d+\.\d+\.\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'))
# Calling the finishGEOS() upon exit of the interpreter.
atexit.register(lgeos.finishGEOS)

View File

@ -0,0 +1,33 @@
"""
This module contains all of the GEOS ctypes function prototypes. Each
prototype handles the interaction between the GEOS library and Python
via ctypes.
"""
# Coordinate sequence routines.
from django.contrib.gis.geos.prototypes.coordseq import create_cs, get_cs, \
cs_clone, cs_getordinate, cs_setordinate, cs_getx, cs_gety, cs_getz, \
cs_setx, cs_sety, cs_setz, cs_getsize, cs_getdims
# Geometry routines.
from django.contrib.gis.geos.prototypes.geom import from_hex, from_wkb, from_wkt, \
create_point, create_linestring, create_linearring, create_polygon, create_collection, \
destroy_geom, get_extring, get_intring, get_nrings, get_geomn, geom_clone, \
geos_normalize, geos_type, geos_typeid, geos_get_srid, geos_set_srid, \
get_dims, get_num_coords, get_num_geoms, \
to_hex, to_wkb, to_wkt
# Miscellaneous routines.
from django.contrib.gis.geos.prototypes.misc import geos_area, geos_distance, geos_length
# Predicates
from django.contrib.gis.geos.prototypes.predicates import geos_hasz, geos_isempty, \
geos_isring, geos_issimple, geos_isvalid, geos_contains, geos_crosses, \
geos_disjoint, geos_equals, geos_equalsexact, geos_intersects, \
geos_intersects, geos_overlaps, geos_relatepattern, geos_touches, geos_within
# Topology routines
from django.contrib.gis.geos.prototypes.topology import \
geos_boundary, geos_buffer, geos_centroid, geos_convexhull, geos_difference, \
geos_envelope, geos_intersection, geos_pointonsurface, geos_preservesimplify, \
geos_simplify, geos_symdifference, geos_union, geos_relate

View File

@ -0,0 +1,82 @@
from ctypes import c_double, c_int, c_uint, POINTER
from django.contrib.gis.geos.libgeos import lgeos, GEOM_PTR, CS_PTR
from django.contrib.gis.geos.prototypes.errcheck import last_arg_byref, GEOSException
## Error-checking routines specific to coordinate sequences. ##
def check_cs_ptr(result, func, cargs):
"Error checking on routines that return Geometries."
if not result:
raise GEOSException('Error encountered checking Coordinate Sequence returned from GEOS C function "%s".' % func.__name__)
return result
def check_cs_op(result, func, cargs):
"Checks the status code of a coordinate sequence operation."
if result == 0:
raise GEOSException('Could not set value on coordinate sequence')
else:
return result
def check_cs_get(result, func, cargs):
"Checking the coordinate sequence retrieval."
check_cs_op(result, func, cargs)
# Object in by reference, return its value.
return last_arg_byref(cargs)
## Coordinate sequence prototype generation functions. ##
def cs_int(func):
"For coordinate sequence routines that return an integer."
func.argtypes = [CS_PTR, POINTER(c_uint)]
func.restype = c_int
func.errcheck = check_cs_get
return func
def cs_operation(func, ordinate=False, get=False):
"For coordinate sequence operations."
if get:
# Get routines get double parameter passed-in by reference.
func.errcheck = check_cs_get
dbl_param = POINTER(c_double)
else:
func.errcheck = check_cs_op
dbl_param = c_double
if ordinate:
# Get/Set ordinate routines have an extra uint parameter.
func.argtypes = [CS_PTR, c_uint, c_uint, dbl_param]
else:
func.argtypes = [CS_PTR, c_uint, dbl_param]
func.restype = c_int
return func
def cs_output(func, argtypes):
"For routines that return a coordinate sequence."
func.argtypes = argtypes
func.restype = CS_PTR
func.errcheck = check_cs_ptr
return func
## Coordinate Sequence ctypes prototypes ##
# Coordinate Sequence constructors & cloning.
cs_clone = cs_output(lgeos.GEOSCoordSeq_clone, [CS_PTR])
create_cs = cs_output(lgeos.GEOSCoordSeq_create, [c_uint, c_uint])
get_cs = cs_output(lgeos.GEOSGeom_getCoordSeq, [GEOM_PTR])
# Getting, setting ordinate
cs_getordinate = cs_operation(lgeos.GEOSCoordSeq_getOrdinate, ordinate=True, get=True)
cs_setordinate = cs_operation(lgeos.GEOSCoordSeq_setOrdinate, ordinate=True)
# For getting, x, y, z
cs_getx = cs_operation(lgeos.GEOSCoordSeq_getX, get=True)
cs_gety = cs_operation(lgeos.GEOSCoordSeq_getY, get=True)
cs_getz = cs_operation(lgeos.GEOSCoordSeq_getZ, get=True)
# For setting, x, y, z
cs_setx = cs_operation(lgeos.GEOSCoordSeq_setX)
cs_sety = cs_operation(lgeos.GEOSCoordSeq_setY)
cs_setz = cs_operation(lgeos.GEOSCoordSeq_setZ)
# These routines return size & dimensions.
cs_getsize = cs_int(lgeos.GEOSCoordSeq_getSize)
cs_getdims = cs_int(lgeos.GEOSCoordSeq_getDimensions)

View File

@ -0,0 +1,76 @@
"""
Error checking functions for GEOS ctypes prototype functions.
"""
import os
from ctypes import string_at, CDLL
from ctypes.util import find_library
from django.contrib.gis.geos.error import GEOSException
# Getting the C library, needed to free the string pointers
# returned from GEOS.
if os.name == 'nt':
libc_name = 'msvcrt'
else:
libc_name = 'libc'
libc = CDLL(find_library(libc_name))
### ctypes error checking routines ###
def last_arg_byref(args):
"Returns the last C argument's by reference value."
return args[-1]._obj.value
def check_dbl(result, func, cargs):
"Checks the status code and returns the double value passed in by reference."
# Checking the status code
if result != 1: return None
# Double passed in by reference, return its value.
return last_arg_byref(cargs)
def check_geom(result, func, cargs):
"Error checking on routines that return Geometries."
if not result:
raise GEOSException('Error encountered checking Geometry returned from GEOS C function "%s".' % func.__name__)
return result
def check_minus_one(result, func, cargs):
"Error checking on routines that should not return -1."
if result == -1:
raise GEOSException('Error encountered in GEOS C function "%s".' % func.__name__)
else:
return result
def check_predicate(result, func, cargs):
"Error checking for unary/binary predicate functions."
val = ord(result) # getting the ordinal from the character
if val == 1: return True
elif val == 0: return False
else:
raise GEOSException('Error encountered on GEOS C predicate function "%s".' % func.__name__)
def check_sized_string(result, func, cargs):
"Error checking for routines that return explicitly sized strings."
if not result:
raise GEOSException('Invalid string pointer returned by GEOS C function "%s"' % func.__name__)
# A c_size_t object is passed in by reference for the second
# argument on these routines, and its needed to determine the
# correct size.
s = string_at(result, last_arg_byref(cargs))
libc.free(result)
return s
def check_string(result, func, cargs):
"Error checking for routines that return strings."
if not result: raise GEOSException('Error encountered checking string return value in GEOS C function "%s".' % func.__name__)
# Getting the string value at the pointer address.
s = string_at(result)
# Freeing the memory allocated by the GEOS library.
libc.free(result)
return s
def check_zero(result, func, cargs):
"Error checking on routines that should not return 0."
if result == 0:
raise GEOSException('Error encountered in GEOS C function "%s".' % func.__name__)
else:
return result

View File

@ -0,0 +1,111 @@
from ctypes import c_char_p, c_int, c_size_t, c_uint, POINTER
from django.contrib.gis.geos.libgeos import lgeos, CS_PTR, GEOM_PTR
from django.contrib.gis.geos.prototypes.errcheck import \
check_geom, check_minus_one, check_sized_string, check_string, check_zero
### ctypes generation functions ###
def bin_constructor(func):
"Generates a prototype for binary construction (HEX, WKB) GEOS routines."
func.argtypes = [c_char_p, c_size_t]
func.restype = GEOM_PTR
func.errcheck = check_geom
return func
# HEX & WKB output
def bin_output(func):
"Generates a prototype for the routines that return a a sized string."
func.argtypes = [GEOM_PTR, POINTER(c_size_t)]
func.errcheck = check_sized_string
return func
def geom_output(func, argtypes):
"For GEOS routines that return a geometry."
if argtypes: func.argtypes = argtypes
func.restype = GEOM_PTR
func.errcheck = check_geom
return func
def geom_index(func):
"For GEOS routines that return geometries from an index."
return geom_output(func, [GEOM_PTR, c_int])
def int_from_geom(func, zero=False):
"Argument is a geometry, return type is an integer."
func.argtypes = [GEOM_PTR]
func.restype = c_int
if zero:
func.errcheck = check_zero
else:
func.errcheck = check_minus_one
return func
def string_from_geom(func):
"Argument is a Geometry, return type is a string."
# We do _not_ specify an argument type because we want just an
# address returned from the function.
func.argtypes = [GEOM_PTR]
func.errcheck = check_string
return func
### ctypes prototypes ###
# TODO: Tell all users to use GEOS 3.0.0, instead of the release
# candidates, and use the new Reader and Writer APIs (e.g.,
# GEOSWKT[Reader|Writer], GEOSWKB[Reader|Writer]). A good time
# to do this will be when Refractions releases a Windows PostGIS
# installer using GEOS 3.0.0.
# Creation routines from WKB, HEX, WKT
from_hex = bin_constructor(lgeos.GEOSGeomFromHEX_buf)
from_wkb = bin_constructor(lgeos.GEOSGeomFromWKB_buf)
from_wkt = geom_output(lgeos.GEOSGeomFromWKT, [c_char_p])
# Output routines
to_hex = bin_output(lgeos.GEOSGeomToHEX_buf)
to_wkb = bin_output(lgeos.GEOSGeomToWKB_buf)
to_wkt = string_from_geom(lgeos.GEOSGeomToWKT)
# The GEOS geometry type, typeid, num_coordites and number of geometries
geos_normalize = int_from_geom(lgeos.GEOSNormalize)
geos_type = string_from_geom(lgeos.GEOSGeomType)
geos_typeid = int_from_geom(lgeos.GEOSGeomTypeId)
get_dims = int_from_geom(lgeos.GEOSGeom_getDimensions, zero=True)
get_num_coords = int_from_geom(lgeos.GEOSGetNumCoordinates)
get_num_geoms = int_from_geom(lgeos.GEOSGetNumGeometries)
# Geometry creation factories
create_point = geom_output(lgeos.GEOSGeom_createPoint, [CS_PTR])
create_linestring = geom_output(lgeos.GEOSGeom_createLineString, [CS_PTR])
create_linearring = geom_output(lgeos.GEOSGeom_createLinearRing, [CS_PTR])
# Polygon and collection creation routines are special and will not
# have their argument types defined.
create_polygon = geom_output(lgeos.GEOSGeom_createPolygon, None)
create_collection = geom_output(lgeos.GEOSGeom_createCollection, None)
# Ring routines
get_extring = geom_output(lgeos.GEOSGetExteriorRing, [GEOM_PTR])
get_intring = geom_index(lgeos.GEOSGetInteriorRingN)
get_nrings = int_from_geom(lgeos.GEOSGetNumInteriorRings)
# Collection Routines
get_geomn = geom_index(lgeos.GEOSGetGeometryN)
# Cloning
geom_clone = lgeos.GEOSGeom_clone
geom_clone.argtypes = [GEOM_PTR]
geom_clone.restype = GEOM_PTR
# Destruction routine.
destroy_geom = lgeos.GEOSGeom_destroy
destroy_geom.argtypes = [GEOM_PTR]
destroy_geom.restype = None
# SRID routines
geos_get_srid = lgeos.GEOSGetSRID
geos_get_srid.argtypes = [GEOM_PTR]
geos_get_srid.restype = c_int
geos_set_srid = lgeos.GEOSSetSRID
geos_set_srid.argtypes = [GEOM_PTR, c_int]
geos_set_srid.restype = None

View File

@ -0,0 +1,27 @@
"""
This module is for the miscellaneous GEOS routines, particularly the
ones that return the area, distance, and length.
"""
from ctypes import c_int, c_double, POINTER
from django.contrib.gis.geos.libgeos import lgeos, GEOM_PTR
from django.contrib.gis.geos.prototypes.errcheck import check_dbl
### ctypes generator function ###
def dbl_from_geom(func, num_geom=1):
"""
Argument is a Geometry, return type is double that is passed
in by reference as the last argument.
"""
argtypes = [GEOM_PTR for i in xrange(num_geom)]
argtypes += [POINTER(c_double)]
func.argtypes = argtypes
func.restype = c_int # Status code returned
func.errcheck = check_dbl
return func
### ctypes prototypes ###
# Area, distance, and length prototypes.
geos_area = dbl_from_geom(lgeos.GEOSArea)
geos_distance = dbl_from_geom(lgeos.GEOSDistance, num_geom=2)
geos_length = dbl_from_geom(lgeos.GEOSLength)

View File

@ -0,0 +1,43 @@
"""
This module houses the GEOS ctypes prototype functions for the
unary and binary predicate operations on geometries.
"""
from ctypes import c_char, c_char_p, c_double
from django.contrib.gis.geos.libgeos import lgeos, GEOM_PTR
from django.contrib.gis.geos.prototypes.errcheck import check_predicate
## Binary & unary predicate functions ##
def binary_predicate(func, *args):
"For GEOS binary predicate functions."
argtypes = [GEOM_PTR, GEOM_PTR]
if args: argtypes += args
func.argtypes = argtypes
func.restype = c_char
func.errcheck = check_predicate
return func
def unary_predicate(func):
"For GEOS unary predicate functions."
func.argtypes = [GEOM_PTR]
func.restype = c_char
func.errcheck = check_predicate
return func
## Unary Predicates ##
geos_hasz = unary_predicate(lgeos.GEOSHasZ)
geos_isempty = unary_predicate(lgeos.GEOSisEmpty)
geos_isring = unary_predicate(lgeos.GEOSisRing)
geos_issimple = unary_predicate(lgeos.GEOSisSimple)
geos_isvalid = unary_predicate(lgeos.GEOSisValid)
## Binary Predicates ##
geos_contains = binary_predicate(lgeos.GEOSContains)
geos_crosses = binary_predicate(lgeos.GEOSCrosses)
geos_disjoint = binary_predicate(lgeos.GEOSDisjoint)
geos_equals = binary_predicate(lgeos.GEOSEquals)
geos_equalsexact = binary_predicate(lgeos.GEOSEqualsExact, c_double)
geos_intersects = binary_predicate(lgeos.GEOSIntersects)
geos_overlaps = binary_predicate(lgeos.GEOSOverlaps)
geos_relatepattern = binary_predicate(lgeos.GEOSRelatePattern, c_char_p)
geos_touches = binary_predicate(lgeos.GEOSTouches)
geos_within = binary_predicate(lgeos.GEOSWithin)

View File

@ -0,0 +1,35 @@
"""
This module houses the GEOS ctypes prototype functions for the
topological operations on geometries.
"""
from ctypes import c_char_p, c_double, c_int
from django.contrib.gis.geos.libgeos import lgeos, GEOM_PTR
from django.contrib.gis.geos.prototypes.errcheck import check_geom, check_string
def topology(func, *args):
"For GEOS unary topology functions."
argtypes = [GEOM_PTR]
if args: argtypes += args
func.argtypes = argtypes
func.restype = GEOM_PTR
func.errcheck = check_geom
return func
### Topology Routines ###
geos_boundary = topology(lgeos.GEOSBoundary)
geos_buffer = topology(lgeos.GEOSBuffer, c_double, c_int)
geos_centroid = topology(lgeos.GEOSGetCentroid)
geos_convexhull = topology(lgeos.GEOSConvexHull)
geos_difference = topology(lgeos.GEOSDifference, GEOM_PTR)
geos_envelope = topology(lgeos.GEOSEnvelope)
geos_intersection = topology(lgeos.GEOSIntersection, GEOM_PTR)
geos_pointonsurface = topology(lgeos.GEOSPointOnSurface)
geos_preservesimplify = topology(lgeos.GEOSTopologyPreserveSimplify, c_double)
geos_simplify = topology(lgeos.GEOSSimplify, c_double)
geos_symdifference = topology(lgeos.GEOSSymDifference, GEOM_PTR)
geos_union = topology(lgeos.GEOSUnion, GEOM_PTR)
# GEOSRelate returns a string, not a geometry.
geos_relate = lgeos.GEOSRelate
geos_relate.argtypes = [GEOM_PTR, GEOM_PTR]
geos_relate.errcheck = check_string

View File

@ -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()

View File

@ -0,0 +1,190 @@
"""
This overrides the traditional `inspectdb` command so that geographic databases
may be introspected.
"""
from django.core.management.commands.inspectdb import Command as InspectCommand
from django.contrib.gis.db.backend import SpatialBackend
class Command(InspectCommand):
# Mapping from lower-case OGC type to the corresponding GeoDjango field.
geofield_mapping = {'point' : 'PointField',
'linestring' : 'LineStringField',
'polygon' : 'PolygonField',
'multipoint' : 'MultiPointField',
'multilinestring' : 'MultiLineStringField',
'multipolygon' : 'MultiPolygonField',
'geometrycollection' : 'GeometryCollectionField',
'geometry' : 'GeometryField',
}
def geometry_columns(self):
"""
Returns a datastructure of metadata information associated with the
`geometry_columns` (or equivalent) table.
"""
# The `geo_cols` is a dictionary data structure that holds information
# about any geographic columns in the database.
geo_cols = {}
def add_col(table, column, coldata):
if table in geo_cols:
# If table already has a geometry column.
geo_cols[table][column] = coldata
else:
# Otherwise, create a dictionary indexed by column.
geo_cols[table] = { column : coldata }
if SpatialBackend.name == 'postgis':
# PostGIS holds all geographic column information in the `geometry_columns` table.
from django.contrib.gis.models import GeometryColumns
for geo_col in GeometryColumns.objects.all():
table = geo_col.f_table_name
column = geo_col.f_geometry_column
coldata = {'type' : geo_col.type, 'srid' : geo_col.srid, 'dim' : geo_col.coord_dimension}
add_col(table, column, coldata)
return geo_cols
elif SpatialBackend.name == 'mysql':
# On MySQL have to get all table metadata before hand; this means walking through
# each table and seeing if any column types are spatial. Can't detect this with
# `cursor.description` (what the introspection module does) because all spatial types
# have the same integer type (255 for GEOMETRY).
from django.db import connection
cursor = connection.cursor()
cursor.execute('SHOW TABLES')
tables = cursor.fetchall();
for table_tup in tables:
table = table_tup[0]
table_desc = cursor.execute('DESCRIBE `%s`' % table)
col_info = cursor.fetchall()
for column, typ, null, key, default, extra in col_info:
if typ in self.geofield_mapping: add_col(table, column, {'type' : typ})
return geo_cols
else:
# TODO: Oracle (has incomplete `geometry_columns` -- have to parse
# SDO SQL to get specific type, SRID, and other information).
raise NotImplementedError('Geographic database inspection not available.')
def handle_inspection(self):
"Overloaded from Django's version to handle geographic database tables."
from django.db import connection, get_introspection_module
import keyword
introspection_module = get_introspection_module()
geo_cols = self.geometry_columns()
table2model = lambda table_name: table_name.title().replace('_', '')
cursor = connection.cursor()
yield "# This is an auto-generated Django model module."
yield "# You'll have to do the following manually to clean this up:"
yield "# * Rearrange models' order"
yield "# * Make sure each model has one field with primary_key=True"
yield "# Feel free to rename the models, but don't rename db_table values or field names."
yield "#"
yield "# Also note: You'll have to insert the output of 'django-admin.py sqlcustom [appname]'"
yield "# into your database."
yield ''
yield 'from django.contrib.gis.db import models'
yield ''
for table_name in introspection_module.get_table_list(cursor):
# Getting the geographic table dictionary.
geo_table = geo_cols.get(table_name, {})
yield 'class %s(models.Model):' % table2model(table_name)
try:
relations = introspection_module.get_relations(cursor, table_name)
except NotImplementedError:
relations = {}
try:
indexes = introspection_module.get_indexes(cursor, table_name)
except NotImplementedError:
indexes = {}
for i, row in enumerate(introspection_module.get_table_description(cursor, table_name)):
att_name, iatt_name = row[0].lower(), row[0]
comment_notes = [] # Holds Field notes, to be displayed in a Python comment.
extra_params = {} # Holds Field parameters such as 'db_column'.
if ' ' in att_name:
extra_params['db_column'] = att_name
att_name = att_name.replace(' ', '')
comment_notes.append('Field renamed to remove spaces.')
if keyword.iskeyword(att_name):
extra_params['db_column'] = att_name
att_name += '_field'
comment_notes.append('Field renamed because it was a Python reserved word.')
if i in relations:
rel_to = relations[i][1] == table_name and "'self'" or table2model(relations[i][1])
field_type = 'ForeignKey(%s' % rel_to
if att_name.endswith('_id'):
att_name = att_name[:-3]
else:
extra_params['db_column'] = att_name
else:
if iatt_name in geo_table:
## Customization for Geographic Columns ##
geo_col = geo_table[iatt_name]
field_type = self.geofield_mapping[geo_col['type'].lower()]
# Adding extra keyword arguments for the SRID and dimension (if not defaults).
dim, srid = geo_col.get('dim', 2), geo_col.get('srid', 4326)
if dim != 2: extra_params['dim'] = dim
if srid != 4326: extra_params['srid'] = srid
else:
try:
field_type = introspection_module.DATA_TYPES_REVERSE[row[1]]
except KeyError:
field_type = 'TextField'
comment_notes.append('This field type is a guess.')
# This is a hook for DATA_TYPES_REVERSE to return a tuple of
# (field_type, extra_params_dict).
if type(field_type) is tuple:
field_type, new_params = field_type
extra_params.update(new_params)
# Add max_length for all CharFields.
if field_type == 'CharField' and row[3]:
extra_params['max_length'] = row[3]
if field_type == 'DecimalField':
extra_params['max_digits'] = row[4]
extra_params['decimal_places'] = row[5]
# Add primary_key and unique, if necessary.
column_name = extra_params.get('db_column', att_name)
if column_name in indexes:
if indexes[column_name]['primary_key']:
extra_params['primary_key'] = True
elif indexes[column_name]['unique']:
extra_params['unique'] = True
field_type += '('
# Don't output 'id = meta.AutoField(primary_key=True)', because
# that's assumed if it doesn't exist.
if att_name == 'id' and field_type == 'AutoField(' and extra_params == {'primary_key': True}:
continue
# Add 'null' and 'blank', if the 'null_ok' flag was present in the
# table description.
if row[6]: # If it's NULL...
extra_params['blank'] = True
if not field_type in ('TextField(', 'CharField('):
extra_params['null'] = True
field_desc = '%s = models.%s' % (att_name, field_type)
if extra_params:
if not field_desc.endswith('('):
field_desc += ', '
field_desc += ', '.join(['%s=%r' % (k, v) for k, v in extra_params.items()])
field_desc += ')'
if comment_notes:
field_desc += ' # ' + ' '.join(comment_notes)
yield ' %s' % field_desc
if table_name in geo_cols:
yield ' objects = models.GeoManager()'
yield ' class Meta:'
yield ' db_table = %r' % table_name
yield ''

View File

@ -0,0 +1,119 @@
import os, sys
from optparse import make_option
from django.contrib.gis import gdal
from django.contrib.gis.management.base import ArgsCommand, CommandError
def layer_option(option, opt, value, parser):
"""
Callback for `make_option` for the `ogrinspect` `layer_key`
keyword option which may be an integer or a string.
"""
try:
dest = int(value)
except ValueError:
dest = value
setattr(parser.values, option.dest, dest)
def list_option(option, opt, value, parser):
"""
Callback for `make_option` for `ogrinspect` keywords that require
a string list. If the string is 'True'/'true' then the option
value will be a boolean instead.
"""
if value.lower() == 'true':
dest = True
else:
dest = [s for s in value.split(',')]
setattr(parser.values, option.dest, dest)
class Command(ArgsCommand):
help = ('Inspects the given OGR-compatible data source (e.g., a shapefile) and outputs\n'
'a GeoDjango model with the given model name. For example:\n'
' ./manage.py ogrinspect zipcode.shp Zipcode')
args = '[data_source] [model_name]'
option_list = ArgsCommand.option_list + (
make_option('--blank', dest='blank', type='string', action='callback',
callback=list_option, default=False,
help='Use a comma separated list of OGR field names to add '
'the `blank=True` option to the field definition. Set with'
'`true` to apply to all applicable fields.'),
make_option('--decimal', dest='decimal', type='string', action='callback',
callback=list_option, default=False,
help='Use a comma separated list of OGR float fields to '
'generate `DecimalField` instead of the default '
'`FloatField`. Set to `true` to apply to all OGR float fields.'),
make_option('--geom-name', dest='geom_name', type='string', default='geom',
help='Specifies the model name for the Geometry Field '
'(defaults to `geom`)'),
make_option('--layer', dest='layer_key', type='string', action='callback',
callback=layer_option, default=0,
help='The key for specifying which layer in the OGR data '
'source to use. Defaults to 0 (the first layer). May be '
'an integer or a string identifier for the layer.'),
make_option('--multi-geom', action='store_true', dest='multi_geom', default=False,
help='Treat the geometry in the data source as a geometry collection.'),
make_option('--name-field', dest='name_field',
help='Specifies a field name to return for the `__unicode__` function.'),
make_option('--no-imports', action='store_false', dest='imports', default=True,
help='Do not include `from django.contrib.gis.db import models` '
'statement.'),
make_option('--null', dest='null', type='string', action='callback',
callback=list_option, default=False,
help='Use a comma separated list of OGR field names to add '
'the `null=True` option to the field definition. Set with'
'`true` to apply to all applicable fields.'),
make_option('--srid', dest='srid',
help='The SRID to use for the Geometry Field. If it can be '
'determined, the SRID of the data source is used.'),
make_option('--mapping', action='store_true', dest='mapping',
help='Generate mapping dictionary for use with `LayerMapping`.')
)
requires_model_validation = False
def handle_args(self, *args, **options):
try:
data_source, model_name = args
except ValueError:
raise CommandError('Invalid arguments, must provide: %s' % self.args)
if not gdal.HAS_GDAL:
raise CommandError('GDAL is required to inspect geospatial data sources.')
# TODO: Support non file-based OGR datasources.
if not os.path.isfile(data_source):
raise CommandError('The given data source cannot be found: "%s"' % data_source)
# Removing options with `None` values.
options = dict([(k, v) for k, v in options.items() if not v is None])
# Getting the OGR DataSource from the string parameter.
try:
ds = gdal.DataSource(data_source)
except gdal.OGRException, msg:
raise CommandError(msg)
# Whether the user wants to generate the LayerMapping dictionary as well.
show_mapping = options.pop('mapping', False)
# Returning the output of ogrinspect with the given arguments
# and options.
from django.contrib.gis.utils.ogrinspect import _ogrinspect, mapping
output = [s for s in _ogrinspect(ds, model_name, **options)]
if show_mapping:
# Constructing the keyword arguments for `mapping`, and
# calling it on the data source.
kwargs = {'geom_name' : options['geom_name'],
'layer_key' : options['layer_key'],
'multi_geom' : options['multi_geom'],
}
mapping_dict = mapping(ds, **kwargs)
# This extra legwork is so that the dictionary definition comes
# out in the same order as the fields in the model definition.
rev_mapping = dict([(v, k) for k, v in mapping_dict.items()])
output.extend(['', '# Auto-generated `LayerMapping` dictionary for %s model' % model_name,
'%s_mapping = {' % model_name.lower()])
output.extend([" '%s' : '%s'," % (rev_mapping[ogr_fld], ogr_fld) for ogr_fld in ds[options['layer_key']].fields])
output.extend([" '%s' : '%s'," % (options['geom_name'], mapping_dict[options['geom_name']]), '}'])
return '\n'.join(output)

View File

View File

@ -0,0 +1,61 @@
"""
This module houses the GoogleMap object, used for generating
the needed javascript to embed Google Maps in a webpage.
Google(R) is a registered trademark of Google, Inc. of Mountain View, California.
Example:
* In the view:
return render_to_response('template.html', {'google' : GoogleMap(key="abcdefg")})
* In the template:
<!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
from django.contrib.gis.maps.google.overlays import GEvent, GMarker, GPolygon, GPolyline
from django.contrib.gis.maps.google.zoom import GoogleZoom

View File

@ -0,0 +1,138 @@
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
# 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&amp;v=%s&amp;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', load_func='gmap_load',
kml_urls=[], polygons=[], polylines=[], markers=[],
template='gis/google/js/google-map.js',
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.load_func = load_func
self.template = template
self.kml_urls = kml_urls
# Does the user want any GMarker, GPolygon, and/or GPolyline overlays?
self.polygons, self.polylines, self.markers = [], [], []
if markers:
for point in markers:
if isinstance(point, GMarker):
self.markers.append(point)
else:
self.markers.append(GMarker(point))
if polygons:
for poly in polygons:
if isinstance(poly, GPolygon):
self.polygons.append(poly)
else:
self.polygons.append(GPolygon(poly))
if polylines:
for pline in polylines:
if isinstance(pline, GPolyline):
self.polylines.append(pline)
else:
self.polylines.append(GPolyline(pline))
# 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
# Setting the parameters for the javascript template.
params = {'calc_zoom' : self.calc_zoom,
'center' : self.center,
'dom_id' : self.dom_id,
'kml_urls' : self.kml_urls,
'load_func' : self.load_func,
'zoom' : self.zoom,
'polygons' : self.polygons,
'polylines' : self.polylines,
'markers' : self.markers,
}
params.update(extra_context)
self.js = 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()"' % self.load_func)
@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 scripts(self):
"Returns all <script></script> tags required for 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)

View File

@ -0,0 +1,220 @@
from django.utils.safestring import mark_safe
from django.contrib.gis.geos import fromstr, Point, LineString, LinearRing, Polygon
class GEvent(object):
"""
A Python wrapper for the Google GEvent object.
Events can be attached to any object derived from GOverlayBase with the
add_event() call.
For more information please see the Google Maps API Reference:
http://code.google.com/apis/maps/documentation/reference.html#GEvent
Example:
from django.shortcuts import render_to_response
from django.contrib.gis.maps.google import GoogleMap, GEvent, GPolyline
def sample_request(request):
polyline = GPolyline('LINESTRING(101 26, 112 26, 102 31)')
event = GEvent('click',
'function() { location.href = "http://www.google.com"}')
polyline.add_event(event)
return render_to_response('mytemplate.html',
{'google' : GoogleMap(polylines=[polyline])})
"""
def __init__(self, event, action):
"""
Initializes a GEvent object.
Parameters:
event:
string for the event, such as 'click'. The event must be a valid
event for the object in the Google Maps API.
There is no validation of the event type within Django.
action:
string containing a Javascript function, such as
'function() { location.href = "newurl";}'
The string must be a valid Javascript function. Again there is no
validation fo the function within Django.
"""
self.event = event
self.action = action
def __unicode__(self):
"Returns the parameter part of a GEvent."
return mark_safe('"%s", %s' %(self.event, self.action))
class GOverlayBase(object):
def __init__(self):
self.events = []
def latlng_from_coords(self, coords):
"Generates a JavaScript array of GLatLng objects for the given coordinates."
return '[%s]' % ','.join(['new GLatLng(%s,%s)' % (y, x) for x, y in coords])
def add_event(self, event):
"Attaches a GEvent to the overlay object."
self.events.append(event)
def __unicode__(self):
"The string representation is the JavaScript API call."
return mark_safe('%s(%s)' % (self.__class__.__name__, self.js_params))
class GPolygon(GOverlayBase):
"""
A Python wrapper for the Google GPolygon object. For more information
please see the Google Maps API Reference:
http://code.google.com/apis/maps/documentation/reference.html#GPolygon
"""
def __init__(self, poly,
stroke_color='#0000ff', stroke_weight=2, stroke_opacity=1,
fill_color='#0000ff', fill_opacity=0.4):
"""
The GPolygon object initializes on a GEOS Polygon or a parameter that
may be instantiated into GEOS Polygon. Please note that this will not
depict a Polygon's internal rings.
Keyword Options:
stroke_color:
The color of the polygon outline. Defaults to '#0000ff' (blue).
stroke_weight:
The width of the polygon outline, in pixels. Defaults to 2.
stroke_opacity:
The opacity of the polygon outline, between 0 and 1. Defaults to 1.
fill_color:
The color of the polygon fill. Defaults to '#0000ff' (blue).
fill_opacity:
The opacity of the polygon fill. Defaults to 0.4.
"""
if isinstance(poly, basestring): poly = fromstr(poly)
if isinstance(poly, (tuple, list)): poly = Polygon(poly)
if not isinstance(poly, Polygon):
raise TypeError('GPolygon may only initialize on GEOS Polygons.')
# Getting the envelope of the input polygon (used for automatically
# determining the zoom level).
self.envelope = poly.envelope
# Translating the coordinates into a JavaScript array of
# Google `GLatLng` objects.
self.points = self.latlng_from_coords(poly.shell.coords)
# Stroke settings.
self.stroke_color, self.stroke_opacity, self.stroke_weight = stroke_color, stroke_opacity, stroke_weight
# Fill settings.
self.fill_color, self.fill_opacity = fill_color, fill_opacity
super(GPolygon, self).__init__()
@property
def js_params(self):
return '%s, "%s", %s, %s, "%s", %s' % (self.points, self.stroke_color, self.stroke_weight, self.stroke_opacity,
self.fill_color, self.fill_opacity)
class GPolyline(GOverlayBase):
"""
A Python wrapper for the Google GPolyline object. For more information
please see the Google Maps API Reference:
http://code.google.com/apis/maps/documentation/reference.html#GPolyline
"""
def __init__(self, geom, color='#0000ff', weight=2, opacity=1):
"""
The GPolyline object may be initialized on GEOS LineStirng, LinearRing,
and Polygon objects (internal rings not supported) or a parameter that
may instantiated into one of the above geometries.
Keyword Options:
color:
The color to use for the polyline. Defaults to '#0000ff' (blue).
weight:
The width of the polyline, in pixels. Defaults to 2.
opacity:
The opacity of the polyline, between 0 and 1. Defaults to 1.
"""
# If a GEOS geometry isn't passed in, try to contsruct one.
if isinstance(geom, basestring): geom = fromstr(geom)
if isinstance(geom, (tuple, list)): geom = Polygon(geom)
# Generating the lat/lng coordinate pairs.
if isinstance(geom, (LineString, LinearRing)):
self.latlngs = self.latlng_from_coords(geom.coords)
elif isinstance(geom, Polygon):
self.latlngs = self.latlng_from_coords(geom.shell.coords)
else:
raise TypeError('GPolyline may only initialize on GEOS LineString, LinearRing, and/or Polygon geometries.')
# Getting the envelope for automatic zoom determination.
self.envelope = geom.envelope
self.color, self.weight, self.opacity = color, weight, opacity
super(GPolyline, self).__init__()
@property
def js_params(self):
return '%s, "%s", %s, %s' % (self.latlngs, self.color, self.weight, self.opacity)
class GMarker(GOverlayBase):
"""
A Python wrapper for the Google GMarker object. For more information
please see the Google Maps API Reference:
http://code.google.com/apis/maps/documentation/reference.html#GMarker
Example:
from django.shortcuts import render_to_response
from django.contrib.gis.maps.google.overlays import GMarker, GEvent
def sample_request(request):
marker = GMarker('POINT(101 26)')
event = GEvent('click',
'function() { location.href = "http://www.google.com"}')
marker.add_event(event)
return render_to_response('mytemplate.html',
{'google' : GoogleMap(markers=[marker])})
"""
def __init__(self, geom, title=None):
"""
The GMarker object may initialize on GEOS Points or a parameter
that may be instantiated into a GEOS point. Keyword options map to
GMarkerOptions -- so far only the title option is supported.
Keyword Options:
title:
Title option for GMarker, will be displayed as a tooltip.
"""
# If a GEOS geometry isn't passed in, try to construct one.
if isinstance(geom, basestring): geom = fromstr(geom)
if isinstance(geom, (tuple, list)): geom = Point(geom)
if isinstance(geom, Point):
self.latlng = self.latlng_from_coords(geom.coords)
else:
raise TypeError('GMarker may only initialize on GEOS Point geometry.')
# Getting the envelope for automatic zoom determination.
self.envelope = geom.envelope
# TODO: Add support for more GMarkerOptions
self.title = title
super(GMarker, self).__init__()
def latlng_from_coords(self, coords):
return 'new GLatLng(%s,%s)' %(coords[1], coords[0])
def options(self):
result = []
if self.title: result.append('title: "%s"' % self.title)
return '{%s}' % ','.join(result)
@property
def js_params(self):
return '%s, %s' % (self.latlng, self.options())

View File

@ -0,0 +1,164 @@
from django.contrib.gis.geos import GEOSGeometry, LinearRing, Polygon, Point
from django.contrib.gis.maps.google.gmap import GoogleMapException
from math import pi, sin, cos, log, exp, atan
# Constants used for degree to radian conversion, and vice-versa.
DTOR = pi / 180.
RTOD = 180. / pi
def get_width_height(envelope):
# Getting the lower-left, upper-left, and upper-right
# coordinates of the envelope.
ll = Point(envelope[0][0])
ul = Point(envelope[0][1])
ur = Point(envelope[0][2])
height = ll.distance(ul)
width = ul.distance(ur)
return width, height
class GoogleZoom(object):
"""
GoogleZoom is a utility for performing operations related to the zoom
levels on Google Maps.
This class is inspired by the OpenStreetMap Mapnik tile generation routine
`generate_tiles.py`, and the article "How Big Is the World" (Hack #16) in
"Google Maps Hacks" by Rich Gibson and Schuyler Erle.
`generate_tiles.py` may be found at:
http://trac.openstreetmap.org/browser/applications/rendering/mapnik/generate_tiles.py
"Google Maps Hacks" may be found at http://safari.oreilly.com/0596101619
"""
def __init__(self, num_zoom=19, tilesize=256):
"Initializes the Google Zoom object."
# Google's tilesize is 256x256, square tiles are assumed.
self._tilesize = tilesize
# The number of zoom levels
self._nzoom = num_zoom
# Initializing arrays to hold the parameters for each
# one of the zoom levels.
self._degpp = [] # Degrees per pixel
self._radpp = [] # Radians per pixel
self._npix = [] # 1/2 the number of pixels for a tile at the given zoom level
# Incrementing through the zoom levels and populating the
# parameter arrays.
z = tilesize # The number of pixels per zoom level.
for i in xrange(num_zoom):
# Getting the degrees and radians per pixel, and the 1/2 the number of
# for every zoom level.
self._degpp.append(z / 360.) # degrees per pixel
self._radpp.append(z / (2 * pi)) # radians per pixl
self._npix.append(z / 2) # number of pixels to center of tile
# Multiplying `z` by 2 for the next iteration.
z *= 2
def __len__(self):
"Returns the number of zoom levels."
return self._nzoom
def get_lon_lat(self, lonlat):
"Unpacks longitude, latitude from GEOS Points and 2-tuples."
if isinstance(lonlat, Point):
lon, lat = lonlat.coords
else:
lon, lat = lonlat
return lon, lat
def lonlat_to_pixel(self, lonlat, zoom):
"Converts a longitude, latitude coordinate pair for the given zoom level."
# Setting up, unpacking the longitude, latitude values and getting the
# number of pixels for the given zoom level.
lon, lat = self.get_lon_lat(lonlat)
npix = self._npix[zoom]
# Calculating the pixel x coordinate by multiplying the longitude
# value with with the number of degrees/pixel at the given
# zoom level.
px_x = round(npix + (lon * self._degpp[zoom]))
# Creating the factor, and ensuring that 1 or -1 is not passed in as the
# base to the logarithm. Here's why:
# if fac = -1, we'll get log(0) which is undefined;
# if fac = 1, our logarithm base will be divided by 0, also undefined.
fac = min(max(sin(DTOR * lat), -0.9999), 0.9999)
# Calculating the pixel y coordinate.
px_y = round(npix + (0.5 * log((1 + fac)/(1 - fac)) * (-1.0 * self._radpp[zoom])))
# Returning the pixel x, y to the caller of the function.
return (px_x, px_y)
def pixel_to_lonlat(self, px, zoom):
"Converts a pixel to a longitude, latitude pair at the given zoom level."
if len(px) != 2:
raise TypeError('Pixel should be a sequence of two elements.')
# Getting the number of pixels for the given zoom level.
npix = self._npix[zoom]
# Calculating the longitude value, using the degrees per pixel.
lon = (px[0] - npix) / self._degpp[zoom]
# Calculating the latitude value.
lat = RTOD * ( 2 * atan(exp((px[1] - npix)/ (-1.0 * self._radpp[zoom]))) - 0.5 * pi)
# Returning the longitude, latitude coordinate pair.
return (lon, lat)
def tile(self, lonlat, zoom):
"""
Returns a Polygon corresponding to the region represented by a fictional
Google Tile for the given longitude/latitude pair and zoom level. This
tile is used to determine the size of a tile at the given point.
"""
# The given lonlat is the center of the tile.
delta = self._tilesize / 2
# Getting the pixel coordinates corresponding to the
# the longitude/latitude.
px = self.lonlat_to_pixel(lonlat, zoom)
# Getting the lower-left and upper-right lat/lon coordinates
# for the bounding box of the tile.
ll = self.pixel_to_lonlat((px[0]-delta, px[1]-delta), zoom)
ur = self.pixel_to_lonlat((px[0]+delta, px[1]+delta), zoom)
# Constructing the Polygon, representing the tile and returning.
return Polygon(LinearRing(ll, (ll[0], ur[1]), ur, (ur[0], ll[1]), ll), srid=4326)
def get_zoom(self, geom):
"Returns the optimal Zoom level for the given geometry."
# Checking the input type.
if not isinstance(geom, GEOSGeometry) or geom.srid != 4326:
raise TypeError('get_zoom() expects a GEOS Geometry with an SRID of 4326.')
# Getting the envelope for the geometry, and its associated width, height
# and centroid.
env = geom.envelope
env_w, env_h = get_width_height(env)
center = env.centroid
for z in xrange(self._nzoom):
# Getting the tile at the zoom level.
tile = self.tile(center, z)
tile_w, tile_h = get_width_height(tile)
# When we span more than one tile, this is an approximately good
# zoom level.
if (env_w > tile_w) or (env_h > tile_h):
if z == 0:
raise GoogleMapException('Geometry width and height should not exceed that of the Earth.')
return z-1
# Otherwise, we've zoomed in to the max.
return self._nzoom-1

View File

@ -0,0 +1,329 @@
# 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 the default units specified
from the given keyword arguments dictionary.
"""
val = 0.0
for unit, value in kwargs.iteritems():
if unit in self.UNITS:
val += self.UNITS[unit] * value
default_unit = unit
elif unit in self.ALIAS:
u = self.ALIAS[unit]
val += self.UNITS[u] * value
default_unit = u
else:
lower = unit.lower()
if lower in self.UNITS:
val += self.UNITS[lower] * value
default_unit = lower
elif lower in self.LALIAS:
u = self.LALIAS[lower]
val += self.UNITS[u] * value
default_unit = u
else:
raise AttributeError('Unknown unit type: %s' % unit)
return val, default_unit
@classmethod
def unit_attname(cls, unit_str):
"""
Retrieves the unit attribute name for the given unit string.
For example, if the given unit string is 'metre', 'm' would be returned.
An exception is raised if an attribute cannot be found.
"""
lower = unit_str.lower()
if unit_str in cls.UNITS:
return unit_str
elif lower in cls.UNITS:
return lower
elif lower in cls.LALIAS:
return cls.LALIAS[lower]
else:
raise Exception('Could not find a unit keyword associated with "%s"' % unit_str)
class Distance(MeasureBase):
UNITS = {
'chain' : 20.1168,
'chain_benoit' : 20.116782,
'chain_sears' : 20.1167645,
'british_chain_benoit' : 20.1167824944,
'british_chain_sears' : 20.1167651216,
'british_chain_sears_truncated' : 20.116756,
'cm' : 0.01,
'british_ft' : 0.304799471539,
'british_yd' : 0.914398414616,
'clarke_ft' : 0.3047972654,
'clarke_link' : 0.201166195164,
'fathom' : 1.8288,
'ft': 0.3048,
'german_m' : 1.0000135965,
'gold_coast_ft' : 0.304799710181508,
'indian_yd' : 0.914398530744,
'inch' : 0.0254,
'km': 1000.0,
'link' : 0.201168,
'link_benoit' : 0.20116782,
'link_sears' : 0.20116765,
'm': 1.0,
'mi': 1609.344,
'mm' : 0.001,
'nm': 1852.0,
'nm_uk' : 1853.184,
'rod' : 5.0292,
'sears_yd' : 0.91439841,
'survey_ft' : 0.304800609601,
'um' : 0.000001,
'yd': 0.9144,
}
# Unit aliases for `UNIT` terms encountered in Spatial Reference WKT.
ALIAS = {
'centimeter' : 'cm',
'foot' : 'ft',
'inches' : 'inch',
'kilometer' : 'km',
'kilometre' : 'km',
'meter' : 'm',
'metre' : 'm',
'micrometer' : 'um',
'micrometre' : 'um',
'millimeter' : 'mm',
'millimetre' : 'mm',
'mile' : 'mi',
'yard' : 'yd',
'British chain (Benoit 1895 B)' : 'british_chain_benoit',
'British chain (Sears 1922)' : 'british_chain_sears',
'British chain (Sears 1922 truncated)' : 'british_chain_sears_truncated',
'British foot (Sears 1922)' : 'british_ft',
'British foot' : 'british_ft',
'British yard (Sears 1922)' : 'british_yd',
'British yard' : 'british_yd',
"Clarke's Foot" : 'clarke_ft',
"Clarke's link" : 'clarke_link',
'Chain (Benoit)' : 'chain_benoit',
'Chain (Sears)' : 'chain_sears',
'Foot (International)' : 'ft',
'German legal metre' : 'german_m',
'Gold Coast foot' : 'gold_coast_ft',
'Indian yard' : 'indian_yd',
'Link (Benoit)': 'link_benoit',
'Link (Sears)': 'link_sears',
'Nautical Mile' : 'nm',
'Nautical Mile (UK)' : 'nm_uk',
'US survey foot' : 'survey_ft',
'U.S. Foot' : 'survey_ft',
'Yard (Indian)' : 'indian_yd',
'Yard (Sears)' : 'sears_yd'
}
LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])
def __init__(self, default_unit=None, **kwargs):
# The base unit is in meters.
self.m, self._default_unit = self.default_units(kwargs)
if default_unit and isinstance(default_unit, str):
self._default_unit = default_unit
def __getattr__(self, name):
if name in self.UNITS:
return self.m / self.UNITS[name]
else:
raise AttributeError('Unknown unit type: %s' % name)
def __repr__(self):
return 'Distance(%s=%s)' % (self._default_unit, getattr(self, self._default_unit))
def __str__(self):
return '%s %s' % (getattr(self, self._default_unit), self._default_unit)
def __cmp__(self, other):
if isinstance(other, Distance):
return cmp(self.m, other.m)
else:
return NotImplemented
def __add__(self, other):
if isinstance(other, Distance):
return Distance(default_unit=self._default_unit, m=(self.m + other.m))
else:
raise TypeError('Distance must be added with Distance')
def __iadd__(self, other):
if isinstance(other, Distance):
self.m += other.m
return self
else:
raise TypeError('Distance must be added with Distance')
def __sub__(self, other):
if isinstance(other, Distance):
return Distance(default_unit=self._default_unit, m=(self.m - other.m))
else:
raise TypeError('Distance must be subtracted from Distance')
def __isub__(self, other):
if isinstance(other, Distance):
self.m -= other.m
return self
else:
raise TypeError('Distance must be subtracted from Distance')
def __mul__(self, other):
if isinstance(other, (int, float, long, Decimal)):
return Distance(default_unit=self._default_unit, m=(self.m * float(other)))
elif isinstance(other, Distance):
return Area(default_unit='sq_' + self._default_unit, sq_m=(self.m * other.m))
else:
raise TypeError('Distance must be multiplied with number or Distance')
def __imul__(self, other):
if isinstance(other, (int, float, long, Decimal)):
self.m *= float(other)
return self
else:
raise TypeError('Distance must be multiplied with number')
def __div__(self, other):
if isinstance(other, (int, float, long, Decimal)):
return Distance(default_unit=self._default_unit, m=(self.m / float(other)))
else:
raise TypeError('Distance must be divided with number')
def __idiv__(self, other):
if isinstance(other, (int, float, long, Decimal)):
self.m /= float(other)
return self
else:
raise TypeError('Distance must be divided with number')
def __nonzero__(self):
return bool(self.m)
class Area(MeasureBase):
# Getting the square units values and the alias dictionary.
UNITS = dict([('sq_%s' % k, v ** 2) for k, v in Distance.UNITS.items()])
ALIAS = dict([(k, 'sq_%s' % v) for k, v in Distance.ALIAS.items()])
LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])
def __init__(self, default_unit=None, **kwargs):
self.sq_m, self._default_unit = self.default_units(kwargs)
if default_unit and isinstance(default_unit, str):
self._default_unit = default_unit
def __getattr__(self, name):
if name in self.UNITS:
return self.sq_m / self.UNITS[name]
else:
raise AttributeError('Unknown unit type: ' + name)
def __repr__(self):
return 'Area(%s=%s)' % (self._default_unit, getattr(self, self._default_unit))
def __str__(self):
return '%s %s' % (getattr(self, self._default_unit), self._default_unit)
def __cmp__(self, other):
if isinstance(other, Area):
return cmp(self.sq_m, other.sq_m)
else:
return NotImplemented
def __add__(self, other):
if isinstance(other, Area):
return Area(default_unit=self._default_unit, sq_m=(self.sq_m + other.sq_m))
else:
raise TypeError('Area must be added with Area')
def __iadd__(self, other):
if isinstance(other, Area):
self.sq_m += other.sq_m
return self
else:
raise TypeError('Area must be added with Area')
def __sub__(self, other):
if isinstance(other, Area):
return Area(default_unit=self._default_unit, sq_m=(self.sq_m - other.sq_m))
else:
raise TypeError('Area must be subtracted from Area')
def __isub__(self, other):
if isinstance(other, Area):
self.sq_m -= other.sq_m
return self
else:
raise TypeError('Area must be subtracted from Area')
def __mul__(self, other):
if isinstance(other, (int, float, long, Decimal)):
return Area(default_unit=self._default_unit, sq_m=(self.sq_m * float(other)))
else:
raise TypeError('Area must be multiplied with number')
def __imul__(self, other):
if isinstance(other, (int, float, long, Decimal)):
self.sq_m *= float(other)
return self
else:
raise TypeError('Area must be multiplied with number')
def __div__(self, other):
if isinstance(other, (int, float, long, Decimal)):
return Area(default_unit=self._default_unit, sq_m=(self.sq_m / float(other)))
else:
raise TypeError('Area must be divided with number')
def __idiv__(self, other):
if isinstance(other, (int, float, long, Decimal)):
self.sq_m /= float(other)
return self
else:
raise TypeError('Area must be divided with number')
def __nonzero__(self):
return bool(self.sq_m)
# Shortcuts
D = Distance
A = Area

View File

@ -0,0 +1,284 @@
"""
Imports the SpatialRefSys and GeometryColumns models dependent on the
spatial database backend.
"""
import re
from django.conf import settings
# Checking for the presence of GDAL (needed for the SpatialReference object)
from django.contrib.gis.gdal import HAS_GDAL
if HAS_GDAL:
from django.contrib.gis.gdal import SpatialReference
class SpatialRefSysMixin(object):
"""
The SpatialRefSysMixin is a class used by the database-dependent
SpatialRefSys objects to reduce redundnant code.
"""
# For pulling out the spheroid from the spatial reference string. This
# regular expression is used only if the user does not have GDAL installed.
# TODO: Flattening not used in all ellipsoids, could also be a minor axis, or 'b'
# parameter.
spheroid_regex = re.compile(r'.+SPHEROID\[\"(?P<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 HAS_GDAL:
if hasattr(self, '_srs'):
# Returning a clone of the cached SpatialReference object.
return self._srs.clone()
else:
# Attempting to cache a SpatialReference object.
# Trying to get from WKT first.
try:
self._srs = SpatialReference(self.wkt)
return self.srs
except Exception, msg:
pass
raise Exception('Could not get OSR SpatialReference from WKT: %s\nError:\n%s' % (self.wkt, msg))
else:
raise Exception('GDAL is not installed.')
@property
def ellipsoid(self):
"""
Returns a tuple of the ellipsoid parameters:
(semimajor axis, semiminor axis, and inverse flattening).
"""
if HAS_GDAL:
return self.srs.ellipsoid
else:
m = self.spheroid_regex.match(self.wkt)
if m: return (float(m.group('major')), float(m.group('flattening')))
else: return None
@property
def name(self):
"Returns the projection name."
return self.srs.name
@property
def spheroid(self):
"Returns the spheroid name for this spatial reference."
return self.srs['spheroid']
@property
def datum(self):
"Returns the datum for this spatial reference."
return self.srs['datum']
@property
def projected(self):
"Is this Spatial Reference projected?"
if HAS_GDAL:
return self.srs.projected
else:
return self.wkt.startswith('PROJCS')
@property
def local(self):
"Is this Spatial Reference local?"
if HAS_GDAL:
return self.srs.local
else:
return self.wkt.startswith('LOCAL_CS')
@property
def geographic(self):
"Is this Spatial Reference geographic?"
if HAS_GDAL:
return self.srs.geographic
else:
return self.wkt.startswith('GEOGCS')
@property
def linear_name(self):
"Returns the linear units name."
if HAS_GDAL:
return self.srs.linear_name
elif self.geographic:
return None
else:
m = self.units_regex.match(self.wkt)
return m.group('unit_name')
@property
def linear_units(self):
"Returns the linear units."
if HAS_GDAL:
return self.srs.linear_units
elif self.geographic:
return None
else:
m = self.units_regex.match(self.wkt)
return m.group('unit')
@property
def angular_name(self):
"Returns the name of the angular units."
if HAS_GDAL:
return self.srs.angular_name
elif self.projected:
return None
else:
m = self.units_regex.match(self.wkt)
return m.group('unit_name')
@property
def angular_units(self):
"Returns the angular units."
if HAS_GDAL:
return self.srs.angular_units
elif self.projected:
return None
else:
m = self.units_regex.match(self.wkt)
return m.group('unit')
@property
def units(self):
"Returns a tuple of the units and the name."
if self.projected or self.local:
return (self.linear_units, self.linear_name)
elif self.geographic:
return (self.angular_units, self.angular_name)
else:
return (None, None)
@classmethod
def get_units(cls, wkt):
"""
Class method used by GeometryField on initialization to
retrive the units on the given WKT, without having to use
any of the database fields.
"""
if HAS_GDAL:
return SpatialReference(wkt).units
else:
m = cls.units_regex.match(wkt)
return m.group('unit'), m.group('unit_name')
@classmethod
def get_spheroid(cls, wkt, string=True):
"""
Class method used by GeometryField on initialization to
retrieve the `SPHEROID[..]` parameters from the given WKT.
"""
if HAS_GDAL:
srs = SpatialReference(wkt)
sphere_params = srs.ellipsoid
sphere_name = srs['spheroid']
else:
m = cls.spheroid_regex.match(wkt)
if m:
sphere_params = (float(m.group('major')), float(m.group('flattening')))
sphere_name = m.group('name')
else:
return None
if not string:
return sphere_name, sphere_params
else:
# `string` parameter used to place in format acceptable by PostGIS
if len(sphere_params) == 3:
radius, flattening = sphere_params[0], sphere_params[2]
else:
radius, flattening = sphere_params
return 'SPHEROID["%s",%s,%s]' % (sphere_name, radius, flattening)
def __unicode__(self):
"""
Returns the string representation. If GDAL is installed,
it will be 'pretty' OGC WKT.
"""
try:
return unicode(self.srs)
except:
return unicode(self.wkt)
# The SpatialRefSys and GeometryColumns models
_srid_info = True
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
# Because the PostGIS version is checked when initializing the spatial
# backend a `ProgrammingError` will be raised if the PostGIS tables
# and functions are not installed. We catch here so it won't be raised when
# running the Django test suite.
from psycopg2 import ProgrammingError
try:
from django.contrib.gis.db.backend.postgis.models import GeometryColumns, SpatialRefSys
except ProgrammingError:
_srid_info = False
elif settings.DATABASE_ENGINE == 'oracle':
# Same thing as above, except the GEOS library is attempted to be loaded for
# `BaseSpatialBackend`, and an exception will be raised during the
# Django test suite if it doesn't exist.
try:
from django.contrib.gis.db.backend.oracle.models import GeometryColumns, SpatialRefSys
except:
_srid_info = False
else:
_srid_info = False
if _srid_info:
def get_srid_info(srid):
"""
Returns the units, unit name, and spheroid WKT associated with the
given SRID from the `spatial_ref_sys` (or equivalent) spatial database
table. We use a database cursor to execute the query because this
function is used when it is not possible to use the ORM (for example,
during field initialization).
"""
# SRID=-1 is a common convention for indicating the geometry has no
# spatial reference information associated with it. Thus, we will
# return all None values without raising an exception.
if srid == -1: return None, None, None
# Getting the spatial reference WKT associated with the SRID from the
# `spatial_ref_sys` (or equivalent) spatial database table. This query
# cannot be executed using the ORM because this information is needed
# when the ORM cannot be used (e.g., during the initialization of
# `GeometryField`).
from django.db import connection
cur = connection.cursor()
qn = connection.ops.quote_name
stmt = 'SELECT %(table)s.%(wkt_col)s FROM %(table)s WHERE (%(table)s.%(srid_col)s = %(srid)s)'
stmt = stmt % {'table' : qn(SpatialRefSys._meta.db_table),
'wkt_col' : qn(SpatialRefSys.wkt_col()),
'srid_col' : qn('srid'),
'srid' : srid,
}
cur.execute(stmt)
# Fetching the WKT from the cursor; if the query failed raise an Exception.
fetched = cur.fetchone()
if not fetched:
raise ValueError('Failed to find spatial reference entry in "%s" corresponding to SRID=%s.' %
(SpatialRefSys._meta.db_table, srid))
srs_wkt = fetched[0]
# Getting metadata associated with the spatial reference system identifier.
# Specifically, getting the unit information and spheroid information
# (both required for distance queries).
unit, unit_name = SpatialRefSys.get_units(srs_wkt)
spheroid = SpatialRefSys.get_spheroid(srs_wkt)
return unit, unit_name, spheroid
else:
def get_srid_info(srid):
"""
Dummy routine for the backends that do not have the OGC required
spatial metadata tables (like MySQL).
"""
return None, None, None

View File

@ -0,0 +1,29 @@
from django.core.validators import ValidationError
from django.oldforms import LargeTextField
from django.contrib.gis.geos import GEOSException, GEOSGeometry
class WKTField(LargeTextField):
"An oldforms LargeTextField for editing WKT text in the admin."
def __init__(self, *args, **kwargs):
super(WKTField, self).__init__(*args, **kwargs)
# Overridding the validator list.
self.validator_list = [self.isValidGeom]
def render(self, data):
# Returns the WKT value for the geometry field. When no such data
# is present, return None to LargeTextField's render.
if isinstance(data, GEOSGeometry):
return super(WKTField, self).render(data.wkt)
elif isinstance(data, basestring):
return super(WKTField, self).render(data)
else:
return super(WKTField, self).render(None)
def isValidGeom(self, field_data, all_data):
try:
g = GEOSGeometry(field_data)
except GEOSException:
raise ValidationError('Valid WKT or HEXEWKB is required for Geometry Fields.')

View File

@ -0,0 +1,12 @@
from django.http import HttpResponse
from django.template import loader
def render_to_kml(*args, **kwargs):
"Renders the response using the MIME type for KML."
return HttpResponse(loader.render_to_string(*args, **kwargs),
mimetype='application/vnd.google-earth.kml+xml kml')
def render_to_text(*args, **kwargs):
"Renders the response using the MIME type for plain text."
return HttpResponse(loader.render_to_string(*args, **kwargs),
mimetype='text/plain')

View File

@ -0,0 +1,55 @@
from django.core import urlresolvers
from django.contrib.sitemaps import Sitemap
from django.contrib.gis.db.models.fields import GeometryField
from django.contrib.gis.shortcuts import render_to_kml
from django.db.models import get_model, get_models
from django.http import HttpResponse
class KMLSitemap(Sitemap):
"""
A minimal hook to produce KML sitemaps.
"""
def __init__(self, locations=None):
if locations is None:
self.locations = _build_kml_sources()
else:
self.locations = locations
def items(self):
return self.locations
def location(self, obj):
return urlresolvers.reverse('django.contrib.gis.sitemaps.kml',
kwargs={'label':obj[0],
'field_name':obj[1]})
def _build_kml_sources():
"Make a mapping of all available KML sources."
ret = []
for klass in get_models():
for field in klass._meta.fields:
if isinstance(field, GeometryField):
label = "%s.%s" % (klass._meta.app_label,
klass._meta.module_name)
ret.append((label, field.name))
return ret
class KMLNotFound(Exception):
pass
def kml(request, label, field_name):
placemarks = []
klass = get_model(*label.split('.'))
if not klass:
raise KMLNotFound("You must supply a valid app.model label. Got %s" % label)
#FIXME: GMaps apparently has a limit on size of displayed kml files
# check if paginating w/ external refs (i.e. linked list) helps.
placemarks.extend(list(klass._default_manager.kml(field_name)[:100]))
#FIXME: other KML features?
return render_to_kml('gis/kml/placemarks.kml', {'places' : placemarks})

View File

@ -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: 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 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"></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="{{ field_name }}">{{ wkt }}</textarea>
<script type="text/javascript">{% block init_function %}{{ module }}.init();{% endblock %}</script>
</span>

View File

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

View File

@ -0,0 +1,2 @@
{% extends "gis/admin/openlayers.html" %}
{% block openlayers %}{% include "gis/admin/osm.js" %}{% endblock %}

View File

@ -0,0 +1,2 @@
{% extends "gis/admin/openlayers.js" %}
{% block base_layer %}new OpenLayers.Layer.OSM.Mapnik("OpenStreetMap (Mapnik)");{% endblock %}

View File

@ -0,0 +1,34 @@
{% autoescape off %}{% block vars %}var map;{% endblock %}
{% block functions %}{% endblock %}
{% block load %}function {{ load_func }}(){
if (GBrowserIsCompatible()) {
map = new GMap2(document.getElementById("{{ dom_id }}"));
map.setCenter(new GLatLng({{ center.1 }}, {{ center.0 }}), {{ zoom }});
{% block controls %}map.addControl(new GSmallMapControl());
map.addControl(new GMapTypeControl());{% endblock %}
{% if calc_zoom %}var bounds = new GLatLngBounds(); var tmp_bounds = new GLatLngBounds();{% endif %}
{% for kml_url in kml_urls %}var kml{{ forloop.counter }} = new GGeoXml("{{ kml_url }}");
map.addOverlay(kml{{ forloop.counter }});{% endfor %}
{% for polygon in polygons %}var poly{{ forloop.counter }} = new {{ polygon }};
map.addOverlay(poly{{ forloop.counter }});
{% for event in polygon.events %}GEvent.addListener(poly{{ forloop.parentloop.counter }}, {{ event }});{% endfor %}
{% if calc_zoom %}tmp_bounds = poly{{ forloop.counter }}.getBounds(); bounds.extend(tmp_bounds.getSouthWest()); bounds.extend(tmp_bounds.getNorthEast());{% endif %}{% endfor %}
{% for polyline in polylines %}var polyline{{ forloop.counter }} = new {{ polyline }};
map.addOverlay(polyline{{ forloop.counter }});
{% for event in polyline.events %}GEvent.addListener(polyline{{ forloop.parentloop.counter }}, {{ event }}); {% endfor %}
{% if calc_zoom %}tmp_bounds = polyline{{ forloop.counter }}.getBounds(); bounds.extend(tmp_bounds.getSouthWest()); bounds.extend(tmp_bounds.getNorthEast());{% endif %}{% endfor %}
{% for marker in markers %}var marker{{ forloop.counter }} = new {{ marker }};
map.addOverlay(marker{{ forloop.counter }});
{% for event in marker.events %}GEvent.addListener(marker{{ forloop.parentloop.counter }}, {{ event }}); {% endfor %}
{% if calc_zoom %}bounds.extend(marker{{ forloop.counter }}.getLatLng()); {% endif %}{% endfor %}
{% if calc_zoom %}map.setCenter(bounds.getCenter(), map.getBoundsZoomLevel(bounds));{% endif %}
{% block load_extra %}{% endblock %}
}else {
alert("Sorry, the Google Maps API is not compatible with this browser.");
}
}
{% endblock %}{% endautoescape %}

View File

@ -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>

View File

@ -0,0 +1,8 @@
{% extends "gis/kml/base.kml" %}
{% block placemarks %}{% for place in places %}
<Placemark>
<name>{{ place.name|escape }}</name>
<description>{{ place.description|escape }}</description>
{{ place.kml }}
</Placemark>{% endfor %}{% endblock %}

View File

@ -0,0 +1,148 @@
import sys
from copy import copy
from unittest import TestSuite, TextTestRunner
from django.contrib.gis.gdal import HAS_GDAL
try:
from django.contrib.gis.tests.utils import mysql, oracle, postgis
except:
mysql, oracle, postgis = (False, False, False)
from django.contrib.gis.utils import HAS_GEOIP
from django.conf import settings
if not settings._target: settings.configure()
# Tests that require use of a spatial database (e.g., creation of models)
test_models = ['geoapp',]
# Tests that do not require setting up and tearing down a spatial database.
test_suite_names = [
'test_geos',
'test_measure',
]
if HAS_GDAL:
if oracle:
# TODO: There's a problem with `select_related` and GeoQuerySet on
# Oracle -- e.g., GeoModel.objects.distance(geom, field_name='fk__point')
# doesn't work so we don't test `relatedapp`.
test_models += ['distapp', 'layermap']
elif postgis:
test_models += ['distapp', 'layermap', 'relatedapp']
elif mysql:
test_models += ['relatedapp']
test_suite_names += [
'test_gdal_driver',
'test_gdal_ds',
'test_gdal_envelope',
'test_gdal_geom',
'test_gdal_srs',
'test_spatialrefsys',
]
else:
print >>sys.stderr, "GDAL not available - no GDAL tests will be run."
if HAS_GEOIP:
if hasattr(settings, 'GEOIP_PATH'):
test_suite_names.append('test_geoip')
def geo_suite():
"""
Builds a test suite for the GIS package. This is not named
`suite` so it will not interfere with the Django test suite (since
spatial database tables are required to execute these tests on
some backends).
"""
s = TestSuite()
for test_suite in test_suite_names:
tsuite = getattr(__import__('django.contrib.gis.tests', globals(), locals(), [test_suite]),test_suite)
s.addTest(tsuite.suite())
return s
def run(verbosity=1):
"Runs the tests that do not require geographic (GEOS, GDAL, etc.) models."
TextTestRunner(verbosity=verbosity).run(geo_suite())
def run_tests(module_list, verbosity=1, interactive=True):
"""
Run the tests that require creation of a spatial database.
In order to run geographic model tests the DATABASE_USER will require
superuser priviliges. To accomplish this outside the `postgres` user,
create your own PostgreSQL database as a user:
(1) Initialize database: `initdb -D /path/to/user/db`
(2) If there's already a Postgres instance on the machine, it will need
to use a different TCP port than 5432. Edit postgresql.conf (in
/path/to/user/db) to change the database port (e.g. `port = 5433`).
(3) Start this database `pg_ctl -D /path/to/user/db start`
On Windows platforms simply use the pgAdmin III utility to add superuser
privileges to your database user.
Make sure your settings.py matches the settings of the user database.
For example, set the same port number (`DATABASE_PORT=5433`).
DATABASE_NAME or TEST_DATABSE_NAME must be set, along with DATABASE_USER.
In settings.py set TEST_RUNNER='django.contrib.gis.tests.run_tests'.
Finally, this assumes that the PostGIS SQL files (lwpostgis.sql and
spatial_ref_sys.sql) are installed in the directory specified by
`pg_config --sharedir` (and defaults to /usr/local/share if that fails).
This behavior is overridden if `POSTGIS_SQL_PATH` is in your settings.
Windows users should set POSTGIS_SQL_PATH manually because the output
of `pg_config` uses paths like 'C:/PROGRA~1/POSTGR~1/..'.
Finally, the tests may be run by invoking `./manage.py test`.
"""
from django.contrib.gis.db.backend import create_spatial_db
from django.db import connection
from django.test.utils import destroy_test_db
# Getting initial values.
old_debug = settings.DEBUG
old_name = copy(settings.DATABASE_NAME)
old_installed = copy(settings.INSTALLED_APPS)
new_installed = copy(settings.INSTALLED_APPS)
# Want DEBUG to be set to False.
settings.DEBUG = False
from django.db.models import loading
# Creating the test suite, adding the test models to INSTALLED_APPS, and
# adding the model test suites to our suite package.
test_suite = geo_suite()
for test_model in test_models:
module_name = 'django.contrib.gis.tests.%s' % test_model
if mysql:
test_module_name = 'tests_mysql'
else:
test_module_name = 'tests'
new_installed.append(module_name)
# Getting the test suite
tsuite = getattr(__import__('django.contrib.gis.tests.%s' % test_model, globals(), locals(), [test_module_name]), test_module_name)
test_suite.addTest(tsuite.suite())
# Resetting the loaded flag to take into account what we appended to
# the INSTALLED_APPS (since this routine is invoked through
# django/core/management, it caches the apps; this ensures that syncdb
# will see our appended models)
settings.INSTALLED_APPS = new_installed
loading.cache.loaded = False
# Creating the test spatial database.
create_spatial_db(test=True, verbosity=verbosity)
# Executing the tests (including the model tests)
result = TextTestRunner(verbosity=verbosity).run(test_suite)
# Cleaning up, destroying the test spatial database and resetting the INSTALLED_APPS.
destroy_test_db(old_name, verbosity)
settings.DEBUG = old_debug
settings.INSTALLED_APPS = old_installed
# Returning the total failures and errors
return len(result.failures) + len(result.errors)
if __name__ == '__main__':
run()

View File

@ -0,0 +1 @@
GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]

Some files were not shown because too many files have changed in this diff Show More