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:
parent
d0f57e7c73
commit
79e68c225b
Binary file not shown.
After Width: | Height: | Size: 711 B |
Binary file not shown.
After Width: | Height: | Size: 506 B |
|
@ -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
|
|
@ -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'
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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))
|
|
@ -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,
|
||||||
|
)
|
|
@ -0,0 +1,5 @@
|
||||||
|
from cx_Oracle import CLOB
|
||||||
|
from django.contrib.gis.db.backend.adaptor import WKTAdaptor
|
||||||
|
|
||||||
|
class OracleSpatialAdaptor(WKTAdaptor):
|
||||||
|
input_size = CLOB
|
|
@ -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)
|
|
@ -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
|
|
@ -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'
|
|
@ -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))
|
|
@ -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,
|
||||||
|
)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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'
|
|
@ -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))
|
|
@ -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)
|
|
@ -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
|
|
@ -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'
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -0,0 +1 @@
|
||||||
|
from django.contrib.gis.forms.fields import GeometryField
|
|
@ -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'])
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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()
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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,
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
|
@ -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'
|
|
@ -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))
|
|
@ -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')
|
|
@ -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])
|
|
@ -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
|
|
@ -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
|
|
@ -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)])
|
||||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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.
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
|
@ -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
|
|
@ -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)])
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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()
|
|
@ -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 ''
|
|
@ -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)
|
|
@ -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
|
|
@ -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&v=%s&key='
|
||||||
|
|
||||||
|
class GoogleMap(object):
|
||||||
|
"A class for generating Google Maps JavaScript."
|
||||||
|
|
||||||
|
# String constants
|
||||||
|
onunload = mark_safe('onunload="GUnload()"') # Cleans up after Google Maps
|
||||||
|
vml_css = mark_safe('v\:* {behavior:url(#default#VML);}') # CSS for IE VML
|
||||||
|
xmlns = mark_safe('xmlns:v="urn:schemas-microsoft-com:vml"') # XML Namespace (for IE VML).
|
||||||
|
|
||||||
|
def __init__(self, key=None, api_url=None, version=None,
|
||||||
|
center=None, zoom=None, dom_id='map', 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)
|
|
@ -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())
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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.')
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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')
|
|
@ -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})
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% extends "gis/admin/openlayers.html" %}
|
||||||
|
{% block openlayers %}{% include "gis/admin/osm.js" %}{% endblock %}
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% extends "gis/admin/openlayers.js" %}
|
||||||
|
{% block base_layer %}new OpenLayers.Layer.OSM.Mapnik("OpenStreetMap (Mapnik)");{% endblock %}
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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 %}
|
||||||
|
|
|
@ -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()
|
Binary file not shown.
|
@ -0,0 +1 @@
|
||||||
|
GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]
|
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue