这基本上是一个关于在 Django 代码中运行自定义 PostGIS 函数的问题。 我必须完成的任务是LineStrings根据它们与给定几何图形的 Frechet 距离进行过滤。Frechet 距离应该使用ST_FrechetDistancePostGIS 提供的函数来计算。
LineStrings
ST_FrechetDistance
在另一个基于 SQLAlchemy 的项目中,我使用以下功能完成完全相同的任务(它正在运行):
from geoalchemy2 import Geography, Geometry from sqlalchemy import func, cast def get_matched_segments(wkt: str, freche_threshold: float = 0.002): matched_segments = db_session.query(RoadElement).filter( func.ST_Dwithin( RoadElement.geom, cast(wkt, Geography), 10 ) ).filter( (func.ST_FrechetDistance( cast(RoadElement.geom, Geometry), cast(wkt, Geometry), 0.1 ) < freche_threshold) | # Frechet Distance is sensitive to geometry direction (func.ST_FrechetDistance( cast(RoadElement.geom, Geometry), func.ST_Reverse(cast(wkt, Geometry)), 0.1 ) < freche_threshold) ) return matched_segments
正如我所说,上面的函数正在运行,我想在 Django 中重新实现它。我必须添加额外的几何 SRS 转换,因为在基于 SQLite 的项目中,LineStrings 最初位于 EPSG:4326 中,而在 Django 中它们位于 EPSG:3857 中。以下是我想出的办法:
from django.db.models import Func, Value, Q, QuerySet, F from django.contrib.gis.geos import GEOSGeometry class HighwayOnlyMotor(models.Model): geom = LineStringField(srid=3857) def get_matched_segments(wkt: str, freche_threshold: float = 0.002) -> QuerySet: linestring = GEOSGeometry(wkt, srid=4326) transform_ls = linestring.transform(3857, clone=True) linestring.reverse() frechet_annotation = HighwayOnlyMotor.objects.filter( geom__dwithin=(transform_ls, D(m=20)) ).annotate( fre_forward=Func( Func(F('geom'), Value(4326), function='ST_Transform'), Value(wkt), Value(0.1), function='ST_FrechetDistance' ), fre_backward=Func( Func(F('geom'), Value(4326), function='ST_Transform'), Value(linestring.wkt), Value(0.1), function='ST_FrechetDistance' ) ) matched_segments = frechet_annotation.filter( Q(fre_forward__lte=freche_threshold) | Q(fre_backward__lte=freche_threshold) ) return matched_segments
它不起作用,因为frechet_annotationQuerySet 引发了异常:
frechet_annotation
django.db.utils.ProgrammingError: cannot cast type double precision to bytea LINE 1: ...548 55.717805109,36.825235998 55.717761246)', 0.1)::bytea AS... ^
似乎我错误地定义了“ST_FrechetDistance”计算。我该如何修复它?
更新
检查了 Django 编写的 SQL。它总体上是正确的,但尝试将结果转换为 会FrecheDistance破坏bytea它ST_FrechetDistance(...)::bytea。当我手动运行未进行转换的查询时bytea,SQL 可以正常工作。那么问题是如何避免将结果转换为bytea?
FrecheDistance
bytea
ST_FrechetDistance(...)::bytea
在 SQLAlchemy 示例中,您正在执行在 GeoDjango 示例中未执行的操作,即将WKT字符串转换为Geometry。 这里发生的事情本质上是您正在尝试使用一个PostGIS函数,但不是 Geometry,而是向它传递一个字符串。
WKT
Geometry
PostGIS
修复第一个问题后我们会遇到的另一个问题是以下异常:
django.core.exceptions.FieldError: Cannot resolve expression type, unknown output_field
这就是为什么我们需要基于 创建一个自定义数据库函数GeoFunc。但这本身也带来了一些问题,我们需要考虑以下几点:
GeoFunc
这有点复杂,但如果我们看一下代码,GeoFunc我们会看到该类继承了一个名为的混合类:GeoFuncMixin它具有属性geom_param_pos = (0,)并指定将是几何体的函数参数的位置。(是的,框架很有趣:P)
GeoFuncMixin
geom_param_pos = (0,)
FloatField
因此我们的自定义 DB 函数应该是这样的:
from django.contrib.gis.db.models.functions import GeoFunc from django.db.models.fields import FloatField class FrechetDistance(GeoFunc): function='ST_FrechetDistance' geom_param_pos = (0, 1,) output_field = FloatField()
现在我们可以在查询中使用此函数来计算ST_FrechetDistance。 我们还需要解决将几何图形(而不仅仅是WKT字符串)传递给函数的原始问题:
def get_matched_segments(wkt: str, freche_threshold: float = 0.002) -> QuerySet: forward_linestring = GEOSGeometry(wkt, srid=4326) backward_linestring = GEOSGeometry(wkt, srid=4326) backward_linestring.reverse() backward_linestring.srid = 4326 # On Django 2.1.5 `srid` is lost after `reverse()` transform_ls = linestring.transform(3857, clone=True) frechet_annotation = HighwayOnlyMotor.objects.filter( geom__dwithin=(transform_ls, D(m=20)) ).annotate( fre_forward=FrechetDistance( Func(F('geom'), Value(4326), function='ST_Transform'), Value(forward_linestring), Value(0.1) ), fre_backward=FrechetDistance( Func(F('geom'), Value(4326), function='ST_Transform'), Value(backward_linestring), Value(0.1) ) ) matched_segments = frechet_annotation.filter( Q(fre_forward__lte=freche_threshold) | Q(fre_backward__lte=freche_threshold) ) return matched_segments