| 224 | | |
| 225 | | |
| 226 | | class within_distance(BaseFunction): |
| 227 | | """This class is used as SQLAlchemy function to query features that are |
| 228 | | within a certain distance of a geometry. |
| 229 | | When it is used inside a query, the SQLAlchemy compiler calls the |
| 230 | | method __compile_within_distance. |
| 231 | | """ |
| 232 | | pass |
| 233 | | |
| 234 | | @compiles(within_distance) |
| 235 | | def __compile_within_distance(element, compiler, **kw): |
| 236 | | if isinstance(compiler.dialect, PGDialect): |
| 237 | | function = __within_distance_pg |
| 238 | | elif isinstance(compiler.dialect, MySQLDialect): |
| 239 | | function = __within_distance_mysql |
| 240 | | elif isinstance(compiler.dialect, SQLiteDialect): |
| 241 | | function = __within_distance_spatialite |
| 242 | | elif isinstance(compiler.dialect, OracleDialect): |
| 243 | | function = __within_distance_oracle |
| 244 | | else: |
| 245 | | raise NotImplementedError("Operation 'within_distance' is not supported by '%s'" % (compiler.dialect)) |
| 246 | | |
| 247 | | arguments = list(element.arguments) |
| 248 | | return compiler.process(function(compiler, parse_clause(arguments.pop(0), compiler), |
| 249 | | parse_clause(arguments.pop(0), compiler), arguments.pop(0), *arguments)) |
| 250 | | |
| 251 | | def __within_distance_pg(compiler, geom1, geom2, distance): |
| 252 | | """Implementation of within_distance for PostGIS |
| 253 | | |
| 254 | | ST_DWithin in early versions of PostGIS 1.3 does not work when |
| 255 | | distance = 0. So we are directly using the (correct) internal definition. |
| 256 | | Note that the definition changed in version 1.3.4, see also: |
| 257 | | http://postgis.refractions.net/docs/ST_DWithin.html |
| 258 | | """ |
| 259 | | return and_(func.ST_Expand(geom2, distance).op('&&')(geom1), |
| 260 | | func.ST_Expand(geom1, distance).op('&&')(geom2), |
| 261 | | func.ST_Distance(geom1, geom2) <= distance) |
| 262 | | |
| 263 | | def __within_distance_mysql(compiler, geom1, geom2, distance): |
| 264 | | """Implementation of within_distance for MySQL |
| 265 | | |
| 266 | | MySQL does not support the function distance, so we are doing |
| 267 | | a kind of "mbr_within_distance". |
| 268 | | The MBR of 'geom2' is expanded with the amount of 'distance' by |
| 269 | | manually changing the coordinates. Then we test if 'geom1' intersects |
| 270 | | this expanded MBR. |
| 271 | | """ |
| 272 | | mbr = func.ExteriorRing(func.Envelope(geom2)) |
| 273 | | |
| 274 | | lower_left = func.StartPoint(mbr) |
| 275 | | upper_right = func.PointN(mbr, 3) |
| 276 | | |
| 277 | | xmin = func.X(lower_left) |
| 278 | | ymin = func.Y(lower_left) |
| 279 | | xmax = func.X(upper_right) |
| 280 | | ymax = func.Y(upper_right) |
| 281 | | |
| 282 | | return func.Intersects( |
| 283 | | geom1, |
| 284 | | func.GeomFromText( |
| 285 | | func.Concat('Polygon((', |
| 286 | | xmin - distance, ' ', ymin - distance, ',', |
| 287 | | xmax + distance, ' ', ymin - distance, ',', |
| 288 | | xmax + distance, ' ', ymax + distance, ',', |
| 289 | | xmin - distance, ' ', ymax + distance, ',', |
| 290 | | xmin - distance, ' ', ymin - distance, '))'), func.srid(geom2) |
| 291 | | ) |
| 292 | | ) |
| 293 | | |
| 294 | | def __within_distance_spatialite(compiler, geom1, geom2, distance): |
| 295 | | """Implementation of within_distance for Spatialite |
| 296 | | """ |
| 297 | | if isinstance(geom1, GeometryExtensionColumn) and geom1.type.spatial_index and SQLiteSpatialDialect.supports_rtree(compiler.dialect): |
| 298 | | """If querying on a geometry column that also has a spatial index, |
| 299 | | then make use of this index. |
| 300 | | |
| 301 | | see: http://www.gaia-gis.it/spatialite/spatialite-tutorial-2.3.1.html#t8 and |
| 302 | | http://groups.google.com/group/spatialite-users/browse_thread/thread/34609c7a711ac92d/7688ced3f909039c?lnk=gst&q=index#f6dbc235471574db |
| 303 | | """ |
| 304 | | return and_( |
| 305 | | func.Distance(geom1, geom2) <= distance, |
| 306 | | table(geom1.table.fullname, column("rowid")).c.rowid.in_( |
| 307 | | select([table("idx_%s_%s" % (geom1.table.fullname, geom1.key), column("pkid")).c.pkid]).where( |
| 308 | | and_(text('xmin') >= func.MbrMinX(geom2) - distance, |
| 309 | | and_(text('xmax') <= func.MbrMaxX(geom2) + distance, |
| 310 | | and_(text('ymin') >= func.MbrMinY(geom2) - distance, |
| 311 | | text('ymax') <= func.MbrMaxY(geom2) + distance))) |
| 312 | | ) |
| 313 | | ) |
| 314 | | ) |
| 315 | | |
| 316 | | else: |
| 317 | | return func.Distance(geom1, geom2) <= distance |
| 318 | | |
| 319 | | |
| 320 | | |
| 321 | | def __within_distance_oracle(compiler, geom1, geom2, distance, additional_params={}): |
| 322 | | """Implementation of within_distance for Oracle |
| 323 | | |
| 324 | | If the first parameter is a geometry column, then the Oracle operator SDO_WITHIN_DISTANCE |
| 325 | | is called and Oracle makes use of the spatial index of this column. |
| 326 | | |
| 327 | | If the first parameter is not a geometry column but a function, which is the case when a coordinate |
| 328 | | transformation had to be added by the spatial filter, then the function SDO_GEOM.WITHIN_DISTANCE |
| 329 | | is called. SDO_GEOM.WITHIN_DISTANCE does not make use of a spatial index and requires |
| 330 | | additional parameters: either a tolerance value or a dimension information array (DIMINFO) |
| 331 | | for both geometries. These parameters can be specified when defining the spatial filter, e.g.:: |
| 332 | | |
| 333 | | additional_params={'tol': '0.005'} |
| 334 | | |
| 335 | | or |
| 336 | | |
| 337 | | from sqlalchemy.sql.expression import text |
| 338 | | diminfo = text("MDSYS.SDO_DIM_ARRAY("\ |
| 339 | | "MDSYS.SDO_DIM_ELEMENT('LONGITUDE', -180, 180, 0.000000005),"\ |
| 340 | | "MDSYS.SDO_DIM_ELEMENT('LATITUDE', -90, 90, 0.000000005)"\ |
| 341 | | ")") |
| 342 | | additional_params={'dim1': diminfo, 'dim2': diminfo} |
| 343 | | |
| 344 | | filter = create_default_filter(request, Spot, additional_params=additional_params) |
| 345 | | proto.count(request, filter=filter) |
| 346 | | |
| 347 | | For its distance calculation Oracle by default uses meter as unit for geodetic data (like EPSG:4326) |
| 348 | | and otherwise the 'unit of measurement associated with the data'. The unit used for the 'distance' value |
| 349 | | can be changed by adding an entry to 'additional_params'. Valid units are defined in the |
| 350 | | view 'sdo_dist_units':: |
| 351 | | |
| 352 | | additional_params={'params': 'unit=km'} |
| 353 | | |
| 354 | | SDO_WITHIN_DISTANCE accepts further parameters, which can also be set using the name 'params' |
| 355 | | together with the unit:: |
| 356 | | |
| 357 | | additional_params={'params': 'unit=km max_resolution=10'} |
| 358 | | |
| 359 | | |
| 360 | | Valid options for 'additional_params' are: |
| 361 | | |
| 362 | | params |
| 363 | | A String containing additional parameters, for example the unit. |
| 364 | | |
| 365 | | tol |
| 366 | | The tolerance value used for the SDO_GEOM.WITHIN_DISTANCE function call. |
| 367 | | |
| 368 | | dim1 and dim2 |
| 369 | | If the parameter 'tol' is not set, these two parameters have to be set. 'dim1' is the DIMINFO |
| 370 | | for the first geometry (the reprojected geometry column) and 'dim2' is the DIMINFO for |
| 371 | | the second geometry (the input geometry from the request). Values for 'dim1' and 'dim2' |
| 372 | | have to be SQLAlchemy expressions, either literal text (text(..)) or a select query. |
| 373 | | |
| 374 | | Note that 'tol' or 'dim1'/'dim2' only have to be set when the input geometry from the request |
| 375 | | uses a different CRS than the geometry column! |
| 376 | | |
| 377 | | |
| 378 | | SDO_WITHIN_DISTANCE: http://download.oracle.com/docs/cd/E11882_01/appdev.112/e11830/sdo_operat.htm#i77653 |
| 379 | | SDO_GEOM.WITHIN_DISTANCE: http://download.oracle.com/docs/cd/E11882_01/appdev.112/e11830/sdo_objgeom.htm#i856373 |
| 380 | | DIMINFO: http://download.oracle.com/docs/cd/E11882_01/appdev.112/e11830/sdo_objrelschema.htm#i1010905 |
| 381 | | TOLERANCE: http://download.oracle.com/docs/cd/E11882_01/appdev.112/e11830/sdo_intro.htm#i884589 |
| 382 | | |
| 383 | | """ |
| 384 | | params = additional_params.get('params', '') |
| 385 | | |
| 386 | | if isinstance(geom1, Column): |
| 387 | | return (func.SDO_WITHIN_DISTANCE(geom1, geom2, |
| 388 | | 'distance=%s %s' % (distance, params)) == 'TRUE') |
| 389 | | else: |
| 390 | | dim1 = additional_params.get('dim1', None) |
| 391 | | dim2 = additional_params.get('dim2', None) |
| 392 | | |
| 393 | | if dim1 is not None and dim2 is not None: |
| 394 | | return (func.SDO_GEOM.WITHIN_DISTANCE(geom1, dim1, |
| 395 | | distance, |
| 396 | | geom2, dim2, |
| 397 | | params) == 'TRUE') |
| 398 | | else: |
| 399 | | tol = additional_params.get('tol', None) |
| 400 | | |
| 401 | | if tol is not None: |
| 402 | | return (func.SDO_GEOM.WITHIN_DISTANCE(geom1, |
| 403 | | distance, |
| 404 | | geom2, |
| 405 | | tol, |
| 406 | | params) == 'TRUE') |
| 407 | | else: |
| 408 | | raise Exception('No dimension information ("dim1" and "dim2") or '\ |
| 409 | | 'tolerance value ("tol") specified for calling '\ |
| 410 | | 'SDO_GEOM.WITHIN_DISTANCE on Oracle, which is '\ |
| 411 | | 'required when reprojecting.') |