Подтвердить что ты не робот

Алгоритм кластеризации карт

Мой текущий код довольно быстрый, но мне нужно сделать его еще быстрее, чтобы мы могли разместить еще больше маркеров. Любые предложения?

Примечания:

  • Код выполняется быстрее всего, когда оператор SQL упорядочен по имени маркера - который сам выполняет очень мелкую работу по кластеризации маркеров (имена маркеров в одном месте часто, но не всегда аналогичны).
  • Я не могу предварительно кластеризовать маркеры, потому что их можно динамически искать и фильтровать.
  • Я пробовал кластеризацию на основе сетки, но результаты часто не очень приятны.
  • Я знаю, что кластеры слегка перекошены на проекции Меркатора.
  • Мне не нужна коммерческая служба кластеризации.

Код:

$singleMarkers = array();
$clusterMarkers = array();

while (count($markers)) {
    $marker  = array_pop($markers);
    $cluster = array();

    // Compare marker against all remaining markers.
    foreach ($markers as $key => $compareMarker) {
        // This function returns the distance between two markers, at a defined zoom level.
        $pixels = pixelDistance($marker['lat'], $marker['lng'], $compareMarker['lat'], $compareMarker['lng'], $zoomLevel);
        // If two markers are closer than defined distance, remove compareMarker from array and add to cluster.
        if ($pixels < $distance) {
            unset($markers[$key]);
            $cluster[] = $compareMarker;
        }
    }

    // If a marker was added to cluster, also add the marker we were comparing to.
    if (count($cluster) > 0) {
        $cluster[] = $marker;
        $clusterMarkers[] = $cluster;
    } else {
        $singleMarkers[] = $marker;
    }
}

function pixelDistance($lat1, $lon1, $lat2, $lon2, $zoom) {
    $x1 = $lon1*10000000; //This is what I did to compensate for using lat/lon values instead of pixels.
    $y1 = $lat1*10000000;
    $x2 = $lon2*10000000;
    $y2 = $lat2*10000000;

    return sqrt(pow(($x1-$x2),2) + pow(($y1-$y2),2)) >> (21 - $zoom); //21 is the max zoom level
}

UPDATE

Здесь текущий код:

$singleMarkers = array();
$clusterMarkers = array();

// Minimum distance between markers to be included in a cluster, at diff. zoom levels
$DISTANCE = (10000000 >> $ZOOM) / 100000;

// Loop until all markers have been compared.
while (count($markers)) {
    $marker  = array_pop($markers);
    $cluster = array();

    // Compare against all markers which are left.
    foreach ($markers as $key => $target) {
        $pixels = abs($marker['lat']-$target['lat']) + abs($marker['lng']-$target['lng']);

        // If the two markers are closer than given distance remove target marker from array and add it to cluster.
        if ($pixels < $DISTANCE) {
            unset($markers[$key]);
            $cluster[] = $target;
        }
    }

    // If a marker has been added to cluster, add also the one we were comparing to.
    if (count($cluster) > 0) {
        $cluster[] = $marker;
        $clusterMarkers[] = $cluster;
    } else {
        $singleMarkers[] = $marker;
    }
}
4b9b3361

Ответ 1

Вам действительно нужно рассчитать Евклидово расстояние? Если вы просто сравниваете относительные величины расстояний, вы, вероятно, можете уйти с помощью Manhattan distance, что избавит вас от двух вызовов pow() и один к sqrt():

function pixelDistance($lat1, $lon1, $lat2, $lon2, $zoom) {
    $x1 = $lon1*10000000; //This is what I did to compensate for using lat/lon values instead of pixels.
    $y1 = $lat1*10000000;
    $x2 = $lon2*10000000;
    $y2 = $lat2*10000000;

    return ($x1-$x2) + ($y1-$y2) >> (21 - $zoom);
}

Не уверен, нужен ли вам бит >> (21 - $zoom) для ваших вычислений, поэтому я его оставил. Но если вам действительно не нужно использовать рассчитанные значения расстояния в другом месте, вам, вероятно, удастся просто использовать широту/долготу напрямую ( нет необходимости умножаться на что-либо) и принимая расстояние Манхэттена, предполагая, что вы предварительно вычислите $distance, чтобы соответствовать этой мере, что будет намного дешевле в вычислительных терминах, чем принуждение всех расстояний, чтобы они соответствовали единицам и величине $distance.

EDIT: Когда я изучал эту проблему в прошлом году, я нашел полезный материал в Википедии - да, это может произойти; -)

Я также могу настоятельно рекомендовать книгу Программирование коллективного интеллекта: создание приложений Smart Web 2.0, которое идет в кластеризацию на большой глубине, как применимо не только к географическим данным, а также к другим областям анализа данных.

Ответ 2

Развернувшись на том, что сказал Джон, я думаю, вы должны попробовать включить эту функцию. Функциональные вызовы в PHP очень медленные, поэтому вы должны получить приличное ускорение от этого.

Ответ 3

Итак, вот что я сделал - я добавил два дополнительных столбца в таблицу маркеров (точек) с преобразованными меркатором значениями для широты и долготы, используя следующие функции:

public static $offset = 268435456;
public static $radius = 85445659.44705395; /* $offset / pi(); */

function LonToX($lon)
{
    return round(self::$offset + self::$radius * $lon * pi() / 180);        
}

function LatToY($lat)
{
    return round(self::$offset - self::$radius * log((1 + sin($lat * pi() / 180)) / (1 - sin($lat * pi() / 180))) / 2);
}

Таким образом, я мог бы получить точно размещенные кластеры. Я все еще пытаюсь решить, как избежать использования array_pop и циклического перехода каждый раз. Пока что он довольно эффективен с маркерами под 1000. Я опубликую результаты для + 5K и + 10K маркеров позже.

Избегая функции pixelDistance и имея ее встроенный, производительность почти наполовину!

Ответ 4

Кажется, что ускорение функции pixelDistance() может быть частью вашего решения, поскольку оно выполняется внутри цикла. Это хорошее место для поиска в первую очередь, но вы не включили этот код, поэтому я не могу быть уверен.

Ответ 5

Здесь я вижу два возможных улучшения:

  • Можете ли вы просто пропустить маркеры $ с циклом for вместо popping их от массива? Всплывающее вскрытие массива полностью не требуется - вы должны использовать только массивы в виде очередей, если вы одновременно добавляете и удаляете элементы (что вы не делаете, вы просто обрабатываете их, а затем отбрасываете)

  • Вы должны попытаться вычислить count() массивов на начало, а затем вручную увеличить или уменьшить переменную счетчика $. Пересчет размера массива каждый цикл является расточительным.

Ответ 6

Ниже перечислены некоторые идеи, которые вы можете реализовать в случае, если производительность представляет собой большую проблему:

  • Уменьшить размерность данные: у вас есть 2d данных long/lat, возможно, вы можете попробовать проецировать его до 1D, используя что-то вроде Многомерное масштабирование (MDS), которое пытается уменьшить количество размеров при сохранении расстояния в исходном пространстве, таким образом, функция расстояния будет иметь дело только с одной функцией вместо двух. Альтернативный способ - использовать Анализ основных компонентов (PCA)
  • Быстрый поиск: шаг вычисления расстояние до каждого маркера может быть улучшено с помощью KD-tree.

Оба эти метода применяются в автономном режиме, поэтому они обычно вычисляются один раз, а затем используются много раз.

Ответ 7

Простая оптимизация заключалась бы в том, чтобы воспользоваться тем, что sqrt (x) < sqrt (y) истинно, если x < y, поэтому вы можете опустить sqrt в pixelDistance и вычислить $distance в квадрате вне цикла. Вы также можете вычислить 21 - $zoomLevel вне цикла, вам придется умножить его на 2, если вы сравниваете квадрат значений. Еще одна небольшая оптимизация заключалась бы в том, чтобы сохранить 2 умножения, выполнив $x1- $x2 до масштабирования на 10000000. И еще немного, хранение дельта в var и его умножение, вероятно, быстрее, чем функция pow. И еще для того, чтобы вы могли встроить функцию сравнения пикселей. Такая оптимизация даст только постоянный коэффициент ускорения.

Для большего ускорения вам понадобится какая-то структура данных ускорения. Легко было бы поместить маркеры на квадраты расстояния. Затем вы можете запускать над бункерами поиск маркеров для кластера с одним и тем же бункером и тремя другими, выбранными в зависимости от того, какая сторона центра бункера помечена маркером. Это приведет к линейной кластеризации времени, которая будет бить любую оптимизацию квадратичного алгоритма для больших наборов результатов.

Ответ 8

Если вы можете, сортируйте маркеры по долготе при первоначальном поиске; то, как только маркер будет больше ширины маркера от следующего маркера в отсортированном списке, вы определенно знаете, что оставшиеся маркеры не будут перекрываться, поэтому вы можете разбить цикл foreach и сэкономить массу времени обработки. Я реализовал это на своем собственном сайте и работает очень эффективно.

У меня есть исходный код в Python здесь.