Как сделать эффективный поиск диапазона + подсчет с помощью данных широты/долготы?

Я работаю с большим набором точек, представленных парами широты/долготы (точки не обязательно уникальны, в наборе может быть несколько точек, находящихся в одном и том же месте). Точки хранятся в базе данных.

Что мне нужно сделать, так это выяснить способ эффективного выполнения поиска, чтобы получить количество точек, лежащих в пределах заданного радиуса (скажем, 25 миль) произвольной точки. Счет не должен быть на 100% точным - что более важно, он просто должен быть быстрым и разумно приближенным к правильному счету. Выполнение этого с помощью SQL возможно, используя запрос с некоторой тригонометрией в предложении WHERE для фильтрации точек по их расстоянию до контрольной точки. К сожалению, этот запрос очень, очень дорог, и кэширование вряд ли поможет вам, поскольку местоположения будут очень распространены.

В конечном итоге я собираюсь создать какую-то структуру памяти, которая сможет эффективно обрабатывать этот вид операции - избавляясь от некоторой точности и долговечности данных (возможно, перестраивая ее только один раз в день) в возвращение к скорости. Я занимаюсь некоторыми исследованиями на kd-деревьях, но пока неясно, насколько это можно применить к данным широты/долготы (в отличие от данных x, y в плоскости 2d).

Если у кого-нибудь есть какие-то идеи или решения, которые я должен изучить, я бы очень благодарен за это - так спасибо заранее.

Ответ 1

Я не думаю, что вы должны использовать это решение. Случайно подумав об этом несколько дней назад, я думаю, что, измеряя расстояние от конкретной точки, места сетки квадратов будут основаны на кругах, а не на сетке. Чем дальше от 0,0, тем менее точно это будет!

То, что я сделал, состояло в том, чтобы иметь 2 дополнительных значения в моем классе PostalCode. Всякий раз, когда я обновляю Long/Lat на PostalCode, я вычисляю расстояние X, Y от Long 0, Lat 0.

public static class MathExtender
{
    public static double GetDistanceBetweenPoints(double sourceLatitude, double sourceLongitude, double destLatitude, double destLongitude)
    {
        double theta = sourceLongitude - destLongitude;
        double distance =
            Math.Sin(DegToRad(sourceLatitude))
            * Math.Sin(DegToRad(destLatitude))
            + Math.Cos(DegToRad(sourceLatitude))
            * Math.Cos(DegToRad(destLatitude))
            * Math.Cos(DegToRad(theta));
        distance = Math.Acos(distance);
        distance = RadToDeg(distance);
        distance = distance * 60 * 1.1515;
        return (distance);
    }


    public static double DegToRad(double degrees)
    {
        return (degrees * Math.PI / 180.0);
    }

    public static double RadToDeg(double radians)
    {
        return (radians / Math.PI * 180.0);
    }
}

Затем я обновляю свой класс следующим образом:

private void CalculateGridReference()
{
    GridReferenceX = MathExtender.GetDistanceBetweenPoints(0, 0, 0, Longitude);
    GridReferenceY = MathExtender.GetDistanceBetweenPoints(0, 0, Latitude, 0);
}

Итак, теперь у меня есть х, у расстояния сетки (в милях) от координатной сетки 0,0 для каждой строки в моей БД. Если я хочу найти все места с 5 милями длинного/лата, я бы сначала получил ссылку на X, Y (скажем, 25,75), тогда я бы искал 20..30, 70..80 в БД и далее фильтровать результаты в памяти с помощью

MathExtensder.GetDistanceBetweenPoints(candidate.Lat, candidate.Long, search.Lat, search.Long) < TheRadiusOfInterest

В части DB очень быстро, а часть в памяти работает на меньшем наборе, чтобы сделать ее более точной.

Ответ 2

Используйте R-Trees.

В Oracle, используя Oracle Spatial, вы можете создать индекс:

CREATE INDEX ix_spatial ON spatial_table (locations) INDEXTYPE IS MDSYS.SPATIAL_INDEX;

который создаст для вас R-Tree и выполнит поиск по нему.

Вы можете использовать любой Earth Model, который вам нравится: WGS84, PZ-90 и т.д.

Ответ 3

Используйте какое-то дерево поиска для пространственных данных, например. a quad tree. Более такие структуры данных упоминаются в разделе "См. Также".

Ответ 5

Не могли бы вы предоставить образец существующего дорогостоящего запроса?

Если вы делаете правильное вычисление большого круга, основанное на принятие синуса() и косинус() опорной точки и других точек данных, то весьма существенная оптимизация может быть сделана на самом деле хранить этот грех/созом значений в базе данных в дополнение к значениям lat/long.

В качестве альтернативы просто используйте свою базу данных, чтобы извлечь прямоугольник диапазонов lat/long, которые соответствуют, и только потом отфильтровывать те, которые находятся за пределами истинного кругового радиуса.

Но помните, что одна градус долготы - это несколько более короткое расстояние в высоких широтах, чем на экваторе. Однако должно быть легко определить правильное соотношение сторон для этого прямоугольника. У вас также были бы ошибки, если вам нужно было рассмотреть области, очень близкие к полюсам, поскольку выбор прямоугольника не справился бы с кругом, который перекрывал бы полюс.

Ответ 6

Этот UDF (SQL Server) предоставит вам расстояние между двумя точками lat/lon:

CREATE FUNCTION [dbo].[zipDistance] (
    @Lat1 decimal(11, 6),
    @Lon1 decimal(11, 6),
    @Lat2 decimal(11, 6),
    @Lon2 decimal(11, 6)
)
RETURNS
    decimal(11, 6) AS
BEGIN

    IF @Lat1 = @Lat2 AND @Lon1 = @Lon2
        RETURN 0 /* same lat/long points, 0 distance = */

    DECLARE @x decimal(18,13)
    SET @x = 0.0

    /* degrees -> radians */
    SET @Lat1 = @Lat1 * PI() / 180
    SET @Lon1 = @Lon1 * PI() / 180
    SET @Lat2 = @Lat2 * PI() / 180
    SET @Lon2 = @Lon2 * PI() / 180

    /* accurate to +/- 30 feet */
    SET @x = Sin(@Lat1) * Sin(@Lat2) + Cos(@Lat1) * Cos(@Lat2) * Cos(@Lon2 - @Lon1)
    IF 1 = @x
        RETURN 0

    DECLARE @EarthRad decimal(5,1)
    SET @EarthRad = 3963.1

    RETURN @EarthRadius * (-1 * ATAN(@x / SQRT(1 - @x * @x)) + PI() / 2)

END

И, очевидно, вы можете использовать это в отдельном запросе, например:

SELECT * FROM table WHERE [dbo].[zipDistance] < 25.0