如何在Django中计算Frechet距离?

时间:2019-06-06 10:38:13

标签: python django postgresql postgis geodjango

这基本上是有关在Django代码中运行自定义PostGIS函数的问题。这个站点上有许多相关的答案,最接近我的情况的是this one。建议使用Func()或什至GeoFunc()类,但是那里没有地理空间函数的示例。后者('GeoFunc')甚至对我抛出st_geofunc does not exist异常(Django 2.1.5)都不起作用。

我要完成的任务是根据LineStrings到给定几何形状的Frechet距离进行过滤。 Frechet距离应该使用PostGIS提供的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的项目中,线串最初位于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_annotation QuerySet引发异常:

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

1 个答案:

答案 0 :(得分:1)

在您的SQLAlchemy示例中,您正在执行在GeoDjango中未执行的操作,即将WKT字符串强制转换为Geometry
本质上,这里发生的是您正在尝试使用PostGIS函数,但是要向其传递字符串而不是Geometry。

修复第一个问题后,我们会偶然发现的另一个问题是以下异常:

django.core.exceptions.FieldError: Cannot resolve expression type, unknown output_field

,这就是为什么我们需要基于GeoFunc创建自定义数据库功能的原因。但这本身就带来了一些问题,我们将需要考虑以下因素:

  • 我们的数据库函数将接收2个几何作为参数。

    这有点令人费解,但是如果我们看一下GeoFunc的代码,我们将看到该类继承了一个名为GeoFuncMixin的mixin,它具有属性geom_param_pos = (0,)并指定了函数参数的位置将是几何形状。 (Yeaahhh框架很有趣:P)

  • 我们的函数将输出FloatField

因此,我们的自定义数据库功能应如下所示:

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