Как рассчитать расстояние Фреше в Джанго? - PullRequest
1 голос
/ 06 июня 2019

Это в основном вопрос о запуске пользовательских функций PostGIS внутри кода Django. На этом сайте есть несколько связанных ответов, наиболее близким к моему случаю является этот . Предлагается использовать классы Func() или даже GeoFunc(), но там нет примеров для геопространственных функций. Последний ('GeoFunc') даже не работал для меня, выбрасывая исключение st_geofunc does not exist (Django 2.1.5).

Задача, которую я должен выполнить, - отфильтровать LineStrings по их расстоянию Фреше до заданной геометрии. Предполагается, что расстояние Фреше рассчитывается с использованием функции ST_FrechetDistance, предоставляемой PostGIS.

В другом проекте, основанном на 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_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. Как мне это исправить?


UPDATE

Изучил SQL, который написал Django. Это в целом правильно, но попытка привести результат от FrecheDistance к bytea портит его ST_FrechetDistance(...)::bytea. Когда я вручную запускаю запрос без bytea cast, SQL работает. Так что вопрос в том, как избежать этого броска на bytea?

1 Ответ

1 голос
/ 06 июня 2019

В вашем примере с SQLAlchemy вы делаете что-то, чего не делали в GeoDjango, и это приводит к приведению строки WKT к Geometry.
По сути, здесь происходит то, что вы пытаетесьиспользуйте функцию PostGIS, но вместо Geometry вы передаете ей строку.

Другая проблема, с которой мы столкнемся после исправления первой, будет следующим исключением:

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

, и именно поэтому нам нужно создать пользовательскую функцию базы данных на основе GeoFunc.Это создает некоторые проблемы само по себе, и нам нужно учитывать следующее:

  • Наша функция БД получит 2 геометрии в качестве аргументов.

    Это немного запутанно, но если мы посмотрим на код GeoFunc, то увидим, что класс наследует миксин с именем: 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   
...