Консолидация аннотаций на увеличенном MKMapView

Существует множество плохих способов сделать то, что я хочу сделать, но это похоже на один из таких случаев: "должен быть лучший способ".

Я использую MKMapView в приложении iPhone, который отображает несколько аннотаций. Представьте себе, что в каждом городе в штате США есть аннотация, поэтому на экране есть довольно плотная куча аннотаций. Когда пользователь масштабирует карту, эти аннотации начинают хрустеть друг на друга, пока они не перекрываются и становятся трудно выделяться индивидуально.

То, что я хотел бы сделать, заключается в том, что при определенной плотности аннотаций (например, при перекрытии любых аннотаций) объедините эти аннотации в одну аннотацию, которая указывает на то, что она включает в себя ряд суб-аннотаций (некоторый визуальный индикатор, "увеличьте масштаб, и вы увидите больше аннотаций" ).

Я мог бы вызвать CGRectIntersectsRect в представлениях аннотаций, но использование этого будет проблемой N ^ 2 - мне нужно было бы перебирать каждую аннотацию для каждой аннотации. Рассмотрим этот псевдокод:

FOR firstAnnotationView IN allAnnotationViews
   FOR secondAnnotationView in allAnnotationViews
       IF CGRectIntersectsRect(firstAnnotationView.frame, secondAnnotationView.frame)
           // found two overlapping annotations, consolidate them
       ENDIF
   ENDFOR
ENDFOR

Вы можете понять, почему это будет медленным, и он должен запускаться каждый раз, когда карта будет увеличена или уменьшена!

Итак, как бы вы все обнаруживали перекрывающиеся аннотации на карте и с точки зрения производительности, консолидировать их разумно?

Ответы

Ответ 1

Я бы выложил ваши аннотации на основе долготы/широты, а затем объединил их с помощью этих ящиков. Основная идея будет выглядеть примерно так:

#include <vector>

float minLongitude = 180.0f;
float maxLongitude = -180.0f;
float longitudeBinSize = 0.1; // Degrees
float minLatitude = -90.0f;
float maxLatitude = 90.0f;
float latitudeBinSize = 0.1; // Degrees
int numBinColumns = int((maxLongitude - minLongitude) / longitudeBinSize);
int numBinRows = int((maxLatitude - minLatitude) / latitudeBinSize);

void calcBinCoords(float longitude, float latitude, int &column, int &row) {
    column = int((latitude - minLatitude) / latitudeBinSize);
    row = int((longitude - minLongitude) / longitudeBinSize);
}

typedef std::vector<AnnotationView *> AnnotationViews;

void binAnnotations(NSArray *annotationViews, std::vector<AnnotationViews> &binnedAnnotations) {
    binnedAnnotations.clear();
    binnedAnnotations.resize(numBinColumns * numBinRows);
    for (AnnotationView *annotationView in annotationViews) {
        int column, row;
        calcBinCoords(annotationView.longitude, annotationView.latitude, column, row);
        binnedAnnotations[row * numBinColumns + column].push_back(annotationView);
    }
}

Значения longitudeBinSize и latitudeBinSize будут максимальным расстоянием, которое вы собираетесь искать при консолидации. После того, как все находится в корзине, ваша проблема поиска включает поиск в списке значений в соседних ячейках для кандидатов. Кроме того, поскольку вы будете сканировать массив во время консолидации, вам действительно нужно только проверить три соседних бункера для каждого загружаемого вами мусорного ящика: bin at (column + 1, row), bin at (column, row + 1), а бит в (столбец + 1, строка + 1).

Вы можете использовать NSMutableArrays вместо std::vector для бункеров, но похоже, что у вас есть большое количество элементов для обработки, и я подозреваю, что std::vector будет быстрее. Это только мое предпочтение, хотя, возможно, это не имеет большого значения, чтобы даже заботиться. Если вы используете ObjC вместо ObjС++, вы не можете использовать std::vector, конечно.

Ответ 3

Вы можете использовать Geohash для разделения аннотаций. Это уменьшит пространство поиска при попытке "консолидировать" ваши аннотации.