如何计算 Django 中的 Frechet 距离?
How to calculate Frechet Distance in Django?
这基本上是一个关于在 Django 代码中 运行ning 自定义 PostGIS 函数的问题。这个网站上有很多相关的答案,最接近我的情况是。建议使用 Func()
甚至 GeoFunc()
类 但那里没有地理空间功能的示例。后者 ('GeoFunc') 甚至对我抛出 st_geofunc does not exist
异常都不起作用 (Django 2.1.5)。
我必须完成的任务是根据它们到给定几何体的 Frechet 距离来过滤 LineStrings
。应该使用 PostGIS 提供的 ST_FrechetDistance
函数计算 Frechet 距离。
在另一个基于 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 转换,因为在 SQL 基于 ite 的项目中,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_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
?
在您的 SQLAlchemy 示例中,您正在做一些在 GeoDjango 中没有做的事情,即将 WKT
字符串转换为 Geometry
.
本质上,这里发生的是您尝试使用 PostGIS
函数,而不是几何图形,而是向它传递一个字符串。
我们在修复第一个问题后偶然发现的另一个问题是以下异常:
django.core.exceptions.FieldError: Cannot resolve expression type, unknown output_field
这就是为什么我们需要基于 GeoFunc
创建自定义数据库函数的原因。不过,这会带来一些问题,我们需要考虑以下几点:
我们的数据库函数将接收 2 个几何图形作为参数。
这有点令人费解,但是如果我们看一下 GeoFunc
we will see that the class inherits a mixin called: GeoFuncMixin
的代码,它具有属性 geom_param_pos = (0,)
并指定函数参数的位置将是几何图形。 (是啊框架很有趣 :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
这基本上是一个关于在 Django 代码中 运行ning 自定义 PostGIS 函数的问题。这个网站上有很多相关的答案,最接近我的情况是Func()
甚至 GeoFunc()
类 但那里没有地理空间功能的示例。后者 ('GeoFunc') 甚至对我抛出 st_geofunc does not exist
异常都不起作用 (Django 2.1.5)。
我必须完成的任务是根据它们到给定几何体的 Frechet 距离来过滤 LineStrings
。应该使用 PostGIS 提供的 ST_FrechetDistance
函数计算 Frechet 距离。
在另一个基于 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 转换,因为在 SQL 基于 ite 的项目中,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_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
?
在您的 SQLAlchemy 示例中,您正在做一些在 GeoDjango 中没有做的事情,即将 WKT
字符串转换为 Geometry
.
本质上,这里发生的是您尝试使用 PostGIS
函数,而不是几何图形,而是向它传递一个字符串。
我们在修复第一个问题后偶然发现的另一个问题是以下异常:
django.core.exceptions.FieldError: Cannot resolve expression type, unknown output_field
这就是为什么我们需要基于 GeoFunc
创建自定义数据库函数的原因。不过,这会带来一些问题,我们需要考虑以下几点:
我们的数据库函数将接收 2 个几何图形作为参数。
这有点令人费解,但是如果我们看一下
GeoFunc
we will see that the class inherits a mixin called:GeoFuncMixin
的代码,它具有属性geom_param_pos = (0,)
并指定函数参数的位置将是几何图形。 (是啊框架很有趣 :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