Authored by Thomas Rasch

o Automatic annotation clustering

... ... @@ -152,6 +152,8 @@ enum {
NSMutableArray *annotations;
NSMutableSet *visibleAnnotations;
RMQuadTree *quadTree;
BOOL enableClustering, positionClusterMarkersAtTheGravityCenter;
CGSize clusterMarkerSize;
id <RMTileSource> tileSource;
RMTileCache *tileCache; // Generic tile cache
... ... @@ -221,6 +223,9 @@ enum {
@property (nonatomic, readonly) RMMarkerManager *markerManager;
@property (nonatomic, readonly) RMMapLayer *overlay;
@property (nonatomic, retain) RMQuadTree *quadTree;
@property (nonatomic, assign) BOOL enableClustering;
@property (nonatomic, assign) BOOL positionClusterMarkersAtTheGravityCenter;
@property (nonatomic, assign) CGSize clusterMarkerSize;
@property (nonatomic, readonly) RMTileImageSet *imagesOnScreen;
@property (nonatomic, readonly) RMTileLoader *tileLoader;
... ...
... ... @@ -95,21 +95,19 @@
@implementation RMMapView
@synthesize decelerationFactor;
@synthesize deceleration;
@synthesize decelerationFactor, deceleration;
@synthesize enableDragging;
@synthesize enableZoom;
@synthesize enableDragging, enableZoom;
@synthesize lastGesture;
@synthesize boundingMask;
@synthesize minZoom;
@synthesize maxZoom;
@synthesize minZoom, maxZoom;
@synthesize screenScale;
@synthesize markerManager;
@synthesize imagesOnScreen;
@synthesize tileCache;
@synthesize quadTree;
@synthesize enableClustering, positionClusterMarkersAtTheGravityCenter, clusterMarkerSize;
#pragma mark -
#pragma mark Initialization
... ... @@ -150,6 +148,12 @@
mercatorToScreenProjection = [[RMMercatorToScreenProjection alloc] initFromProjection:[newTilesource projection] toScreenBounds:[self bounds]];
annotations = [NSMutableArray new];
visibleAnnotations = [NSMutableSet new];
[self setQuadTree:[[[RMQuadTree alloc] initWithMapView:self] autorelease]];
enableClustering = positionClusterMarkersAtTheGravityCenter = NO;
clusterMarkerSize = CGSizeMake(100.0, 100.0);
[self setTileCache:[[[RMTileCache alloc] init] autorelease]];
[self setTileSource:newTilesource];
[self setRenderer:[[[RMCoreAnimationRenderer alloc] initWithView:self] autorelease]];
... ... @@ -172,10 +176,6 @@
[self setBackground:[[[CALayer alloc] init] autorelease]];
[self setOverlay:[[[RMMapLayer alloc] init] autorelease]];
annotations = [NSMutableArray new];
visibleAnnotations = [NSMutableSet new];
[self setQuadTree:[[[RMQuadTree alloc] init] autorelease]];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleMemoryWarningNotification:)
name:UIApplicationDidReceiveMemoryWarningNotification
... ... @@ -253,8 +253,8 @@
CGRect r = self.frame;
[super setFrame:frame];
// only change if the frame changes
if (!CGRectEqualToRect(r, frame)) {
// only change if the frame changes and not during initialization
if (tileLoader && !CGRectEqualToRect(r, frame)) {
CGRect bounds = CGRectMake(0, 0, frame.size.width, frame.size.height);
[mercatorToScreenProjection setScreenBounds:bounds];
background.frame = bounds;
... ... @@ -1353,9 +1353,20 @@
if (self.quadTree)
{
if (!correctAllAnnotations) {
for (RMAnnotation *annotation in visibleAnnotations)
{
[self correctScreenPosition:annotation];
}
// RMLog(@"%d annotations corrected", [visibleAnnotations count]);
return;
}
RMProjectedRect boundingBox = [[self mercatorToScreenProjection] projectedBounds];
float metersPerPixel = self.metersPerPixel;
NSArray *annotationsToCorrect = [quadTree annotationsInProjectedRect:boundingBox];
NSArray *annotationsToCorrect = [quadTree annotationsInProjectedRect:boundingBox createClusterAnnotations:self.enableClustering withClusterSize:RMMakeProjectedSize(self.clusterMarkerSize.width * metersPerPixel, self.clusterMarkerSize.height * metersPerPixel) findGravityCenter:self.positionClusterMarkersAtTheGravityCenter];
NSMutableSet *previousVisibleAnnotations = [NSMutableSet setWithSet:visibleAnnotations];
for (RMAnnotation *annotation in annotationsToCorrect)
... ... @@ -1385,7 +1396,7 @@
[visibleAnnotations removeObject:annotation];
}
// RMLog(@"%d annotations on screen, %d total", [[overlay sublayers] count], [annotations count]);
// RMLog(@"%d annotations on screen, %d total", [[overlay sublayers] count], [annotations count]);
} else
{
... ... @@ -1418,13 +1429,13 @@
[visibleAnnotations removeObject:annotation];
}
}
// RMLog(@"%d annotations on screen, %d total", [[overlay sublayers] count], [annotations count]);
// RMLog(@"%d annotations on screen, %d total", [[overlay sublayers] count], [annotations count]);
} else {
for (RMAnnotation *annotation in visibleAnnotations)
{
[self correctScreenPosition:annotation];
}
// RMLog(@"%d annotations corrected", [visibleAnnotations count]);
// RMLog(@"%d annotations corrected", [visibleAnnotations count]);
}
}
}
... ... @@ -1446,16 +1457,22 @@
{
@synchronized (annotations) {
[annotations addObject:annotation];
[self.quadTree addAnnotation:annotation];
}
[self.quadTree addAnnotation:annotation];
[self correctScreenPosition:annotation];
if ([annotation isAnnotationOnScreen] && [delegate respondsToSelector:@selector(mapView:layerForAnnotation:)])
{
annotation.layer = [delegate mapView:self layerForAnnotation:annotation];
if (annotation.layer) {
[overlay addSublayer:annotation.layer];
[visibleAnnotations addObject:annotation];
if (enableClustering) {
[self correctPositionOfAllAnnotations];
} else {
[self correctScreenPosition:annotation];
if ([annotation isAnnotationOnScreen] && [delegate respondsToSelector:@selector(mapView:layerForAnnotation:)])
{
annotation.layer = [delegate mapView:self layerForAnnotation:annotation];
if (annotation.layer) {
[overlay addSublayer:annotation.layer];
[visibleAnnotations addObject:annotation];
}
}
}
}
... ... @@ -1484,12 +1501,19 @@
annotation.layer = nil;
}
- (void)removeAnnotations:(NSArray *)newAnnotations
- (void)removeAnnotations:(NSArray *)annotationsToRemove
{
for (RMAnnotation *annotation in newAnnotations)
{
[self removeAnnotation:annotation];
@synchronized (annotations) {
for (RMAnnotation *annotation in annotationsToRemove)
{
[annotations removeObject:annotation];
[visibleAnnotations removeObject:annotation];
[self.quadTree removeAnnotation:annotation];
annotation.layer = nil;
}
}
[self correctPositionOfAllAnnotations];
}
- (void)removeAllAnnotations
... ... @@ -1505,6 +1529,7 @@
[annotations removeAllObjects];
[visibleAnnotations removeAllObjects];
[quadTree removeAllObjects];
[self correctPositionOfAllAnnotations];
}
- (CGPoint)screenCoordinatesForAnnotation:(RMAnnotation *)annotation
... ...
... ... @@ -8,7 +8,7 @@
#import "RMFoundation.h"
@class RMAnnotation;
@class RMAnnotation, RMMapView;
typedef enum {
nodeTypeLeaf,
... ... @@ -24,6 +24,8 @@ typedef enum {
NSMutableArray *annotations;
RMQuadTreeNode *parentNode, *northWest, *northEast, *southWest, *southEast;
RMQuadTreeNodeType nodeType;
RMMapView *mapView;
RMAnnotation *cachedClusterAnnotation;
}
@property (nonatomic, readonly) NSArray *annotations;
... ... @@ -41,6 +43,10 @@ typedef enum {
@property (nonatomic, readonly) RMQuadTreeNode *southWest;
@property (nonatomic, readonly) RMQuadTreeNode *southEast;
// Operations on this node and all subnodes
@property (nonatomic, readonly) NSArray *enclosedAnnotations;
@property (nonatomic, readonly) NSUInteger countEnclosedAnnotations;
@end
#pragma mark -
... ... @@ -49,8 +55,11 @@ typedef enum {
@interface RMQuadTree : NSObject
{
RMQuadTreeNode *rootNode;
RMMapView *mapView;
}
- (id)initWithMapView:(RMMapView *)aMapView;
- (void)addAnnotation:(RMAnnotation *)annotation;
- (void)removeAnnotation:(RMAnnotation *)annotation;
... ... @@ -58,5 +67,6 @@ typedef enum {
// Returns all annotations that are either inside of or intersect with boundingBox
- (NSArray *)annotationsInProjectedRect:(RMProjectedRect)boundingBox;
- (NSArray *)annotationsInProjectedRect:(RMProjectedRect)boundingBox createClusterAnnotations:(BOOL)createClusterAnnotations withClusterSize:(RMProjectedSize)clusterSize findGravityCenter:(BOOL)findGravityCenter;
@end
... ...
... ... @@ -9,21 +9,24 @@
#import "RMQuadTree.h"
#import "RMAnnotation.h"
#import "RMProjection.h"
#import "RMMapView.h"
#pragma mark -
#pragma mark RMQuadTreeElement implementation
#define kMinimumQuadTreeElementWidth 200.0 // projected meters
#define kMaxAnnotationsPerLeaf 8
#define kMaxAnnotationsPerLeaf 4
@interface RMQuadTreeNode ()
- (id)initWithParent:(RMQuadTreeNode *)aParent forBoundingBox:(RMProjectedRect)aBoundingBox;
- (id)initWithMapView:(RMMapView *)aMapView forParent:(RMQuadTreeNode *)aParentNode inBoundingBox:(RMProjectedRect)aBoundingBox;
- (void)addAnnotation:(RMAnnotation *)annotation;
- (void)removeAnnotation:(RMAnnotation *)annotation;
- (void)addAnnotationsInBoundingBox:(RMProjectedRect)aBoundingBox toMutableArray:(NSMutableArray *)someArray;
- (void)addAnnotationsInBoundingBox:(RMProjectedRect)aBoundingBox toMutableArray:(NSMutableArray *)someArray createClusterAnnotations:(BOOL)createClusterAnnotations withClusterSize:(RMProjectedSize)clusterSize findGravityCenter:(BOOL)findGravityCenter;
- (void)removeUpwardsAllCachedClusterAnnotations;
@end
... ... @@ -33,17 +36,19 @@
@synthesize boundingBox, northWestBoundingBox, northEastBoundingBox, southWestBoundingBox, southEastBoundingBox;
@synthesize parentNode, northWest, northEast, southWest, southEast;
- (id)initWithParent:(RMQuadTreeNode *)aParentNode forBoundingBox:(RMProjectedRect)aBoundingBox
- (id)initWithMapView:(RMMapView *)aMapView forParent:(RMQuadTreeNode *)aParentNode inBoundingBox:(RMProjectedRect)aBoundingBox
{
if (!(self = [super init]))
return nil;
// RMLog(@"New quadtree node at {(%.0f,%.0f),(%.0f,%.0f)}", aBoundingBox.origin.easting, aBoundingBox.origin.northing, aBoundingBox.size.width, aBoundingBox.size.height);
mapView = aMapView;
parentNode = aParentNode;
northWest = northEast = southWest = southEast = nil;
annotations = [NSMutableArray new];
boundingBox = aBoundingBox;
cachedClusterAnnotation = nil;
double halfWidth = boundingBox.size.width / 2.0, halfHeight = boundingBox.size.height / 2.0;
northWestBoundingBox = RMMakeProjectedRect(boundingBox.origin.easting, boundingBox.origin.northing + halfHeight, halfWidth, halfHeight);
... ... @@ -65,6 +70,7 @@
}
}
[annotations release]; annotations = nil;
cachedClusterAnnotation.layer = nil; [cachedClusterAnnotation release]; cachedClusterAnnotation = nil;
[northWest release]; northWest = nil;
[northEast release]; northEast = nil;
... ... @@ -90,19 +96,19 @@
{
RMProjectedRect projectedRect = annotation.projectedBoundingBox;
if (RMProjectedRectContainsProjectedRect(northWestBoundingBox, projectedRect)) {
if (!northWest) northWest = [[RMQuadTreeNode alloc] initWithParent:self forBoundingBox:northWestBoundingBox];
if (!northWest) northWest = [[RMQuadTreeNode alloc] initWithMapView:mapView forParent:self inBoundingBox:northWestBoundingBox];
[northWest addAnnotation:annotation];
} else if (RMProjectedRectContainsProjectedRect(northEastBoundingBox, projectedRect)) {
if (!northEast) northEast = [[RMQuadTreeNode alloc] initWithParent:self forBoundingBox:northEastBoundingBox];
if (!northEast) northEast = [[RMQuadTreeNode alloc] initWithMapView:mapView forParent:self inBoundingBox:northEastBoundingBox];
[northEast addAnnotation:annotation];
} else if (RMProjectedRectContainsProjectedRect(southWestBoundingBox, projectedRect)) {
if (!southWest) southWest = [[RMQuadTreeNode alloc] initWithParent:self forBoundingBox:southWestBoundingBox];
if (!southWest) southWest = [[RMQuadTreeNode alloc] initWithMapView:mapView forParent:self inBoundingBox:southWestBoundingBox];
[southWest addAnnotation:annotation];
} else if (RMProjectedRectContainsProjectedRect(southEastBoundingBox, projectedRect)) {
if (!southEast) southEast = [[RMQuadTreeNode alloc] initWithParent:self forBoundingBox:southEastBoundingBox];
if (!southEast) southEast = [[RMQuadTreeNode alloc] initWithMapView:mapView forParent:self inBoundingBox:southEastBoundingBox];
[southEast addAnnotation:annotation];
} else {
... ... @@ -110,6 +116,7 @@
[annotations addObject:annotation];
}
annotation.quadTreeNode = self;
[self removeUpwardsAllCachedClusterAnnotations];
}
}
... ... @@ -122,13 +129,15 @@
}
annotation.quadTreeNode = self;
if ([annotations count] <= kMaxAnnotationsPerLeaf || boundingBox.size.width < (kMinimumQuadTreeElementWidth * 2.0))
if ([annotations count] <= kMaxAnnotationsPerLeaf || boundingBox.size.width < (kMinimumQuadTreeElementWidth * 2.0)) {
[self removeUpwardsAllCachedClusterAnnotations];
return;
}
nodeType = nodeTypeNode;
// problem: all annotations that cross two quadrants will always be readded here, which
// will be a problem depending on kMaxAnnotationsPerLeaf
// problem: all annotations that cross two quadrants will always be re-added here, which
// might be a problem depending on kMaxAnnotationsPerLeaf
NSArray *immutableAnnotations = nil;
@synchronized (annotations) {
... ... @@ -155,6 +164,7 @@
[annotations removeObject:annotation];
}
[self removeUpwardsAllCachedClusterAnnotations];
annotation.quadTreeNode = nil;
}
... ... @@ -163,6 +173,8 @@
if (RMProjectedRectContainsProjectedRect(boundingBox, annotation.projectedBoundingBox))
return;
[annotation retain];
[self removeAnnotation:annotation];
RMQuadTreeNode *nextParentNode = self;
... ... @@ -173,25 +185,109 @@
break;
}
}
[annotation release];
}
- (void)addAnnotationsInBoundingBox:(RMProjectedRect)aBoundingBox toMutableArray:(NSMutableArray *)someArray
- (NSUInteger)countEnclosedAnnotations
{
if (nodeType == nodeTypeLeaf) {
@synchronized (annotations) {
[someArray addObjectsFromArray:annotations];
NSUInteger count = [annotations count];
count += [northWest countEnclosedAnnotations];
count += [northEast countEnclosedAnnotations];
count += [southWest countEnclosedAnnotations];
count += [southEast countEnclosedAnnotations];
return count;
}
- (NSArray *)enclosedAnnotations
{
NSMutableArray *enclosedAnnotations = [NSMutableArray arrayWithArray:self.annotations];
if (northWest) [enclosedAnnotations addObjectsFromArray:northWest.enclosedAnnotations];
if (northEast) [enclosedAnnotations addObjectsFromArray:northEast.enclosedAnnotations];
if (southWest) [enclosedAnnotations addObjectsFromArray:southWest.enclosedAnnotations];
if (southEast) [enclosedAnnotations addObjectsFromArray:southEast.enclosedAnnotations];
return enclosedAnnotations;
}
- (void)addAnnotationsInBoundingBox:(RMProjectedRect)aBoundingBox toMutableArray:(NSMutableArray *)someArray createClusterAnnotations:(BOOL)createClusterAnnotations withClusterSize:(RMProjectedSize)clusterSize findGravityCenter:(BOOL)findGravityCenter
{
if (createClusterAnnotations)
{
double halfWidth = boundingBox.size.width / 2.0;
if (boundingBox.size.width >= clusterSize.width && halfWidth < clusterSize.width)
{
if (!cachedClusterAnnotation)
{
NSArray *enclosedAnnotations = self.enclosedAnnotations;
NSUInteger enclosedAnnotationsCount = [enclosedAnnotations count];
if (enclosedAnnotationsCount < 2) {
[someArray addObjectsFromArray:enclosedAnnotations];
return;
}
RMProjectedPoint clusterMarkerPosition;
if (findGravityCenter)
{
double averageEasting = 0.0, averageNorthing = 0.0;
for (RMAnnotation *annotation in enclosedAnnotations)
{
averageEasting += annotation.projectedLocation.easting;
averageNorthing += annotation.projectedLocation.northing;
}
averageEasting /= (double)enclosedAnnotationsCount;
averageNorthing /= (double) enclosedAnnotationsCount;
double halfClusterWidth = clusterSize.width / 2.0, halfClusterHeight = clusterSize.height / 2.0;
if (averageEasting - halfClusterWidth < boundingBox.origin.easting) averageEasting = boundingBox.origin.easting + halfClusterWidth;
if (averageEasting + halfClusterWidth > boundingBox.origin.easting + boundingBox.size.width) averageEasting = boundingBox.origin.easting + boundingBox.size.width - halfClusterWidth;
if (averageNorthing - halfClusterHeight < boundingBox.origin.northing) averageNorthing = boundingBox.origin.northing + halfClusterHeight;
if (averageNorthing + halfClusterHeight > boundingBox.origin.northing + boundingBox.size.height) averageNorthing = boundingBox.origin.northing + boundingBox.size.height - halfClusterHeight;
// TODO: anchorPoint
clusterMarkerPosition = RMMakeProjectedPoint(averageEasting, averageNorthing);
} else
{
clusterMarkerPosition = RMMakeProjectedPoint(boundingBox.origin.easting + halfWidth, boundingBox.origin.northing + (boundingBox.size.height / 2.0));
}
cachedClusterAnnotation = [[RMAnnotation alloc] initWithMapView:mapView coordinate:[[mapView projection] projectedPointToCoordinate:clusterMarkerPosition] andTitle:[NSString stringWithFormat:@"%d", enclosedAnnotationsCount]];
cachedClusterAnnotation.annotationType = @"RMClusterMarker";
cachedClusterAnnotation.userInfo = self;
}
[someArray addObject:cachedClusterAnnotation];
return;
}
// TODO: leaf clustering (necessary?)
if (nodeType == nodeTypeLeaf) {
@synchronized (annotations) {
[someArray addObjectsFromArray:annotations];
}
return;
}
} else {
if (nodeType == nodeTypeLeaf) {
@synchronized (annotations) {
[someArray addObjectsFromArray:annotations];
}
return;
}
return;
}
if (RMProjectedRectInterectsProjectedRect(aBoundingBox, northWestBoundingBox))
[northWest addAnnotationsInBoundingBox:aBoundingBox toMutableArray:someArray];
[northWest addAnnotationsInBoundingBox:aBoundingBox toMutableArray:someArray createClusterAnnotations:createClusterAnnotations withClusterSize:clusterSize findGravityCenter:findGravityCenter];
if (RMProjectedRectInterectsProjectedRect(aBoundingBox, northEastBoundingBox))
[northEast addAnnotationsInBoundingBox:aBoundingBox toMutableArray:someArray];
[northEast addAnnotationsInBoundingBox:aBoundingBox toMutableArray:someArray createClusterAnnotations:createClusterAnnotations withClusterSize:clusterSize findGravityCenter:findGravityCenter];
if (RMProjectedRectInterectsProjectedRect(aBoundingBox, southWestBoundingBox))
[southWest addAnnotationsInBoundingBox:aBoundingBox toMutableArray:someArray];
[southWest addAnnotationsInBoundingBox:aBoundingBox toMutableArray:someArray createClusterAnnotations:createClusterAnnotations withClusterSize:clusterSize findGravityCenter:findGravityCenter];
if (RMProjectedRectInterectsProjectedRect(aBoundingBox, southEastBoundingBox))
[southEast addAnnotationsInBoundingBox:aBoundingBox toMutableArray:someArray];
[southEast addAnnotationsInBoundingBox:aBoundingBox toMutableArray:someArray createClusterAnnotations:createClusterAnnotations withClusterSize:clusterSize findGravityCenter:findGravityCenter];
@synchronized (annotations) {
for (RMAnnotation *annotation in annotations)
... ... @@ -202,6 +298,12 @@
}
}
- (void)removeUpwardsAllCachedClusterAnnotations
{
if (parentNode) [parentNode removeUpwardsAllCachedClusterAnnotations];
cachedClusterAnnotation.layer = nil; [cachedClusterAnnotation release]; cachedClusterAnnotation = nil;
}
@end
#pragma mark -
... ... @@ -209,12 +311,13 @@
@implementation RMQuadTree
- (id)init
- (id)initWithMapView:(RMMapView *)aMapView
{
if (!(self = [super init]))
return nil;
rootNode = [[RMQuadTreeNode alloc] initWithParent:nil forBoundingBox:[[RMProjection googleProjection] planetBounds]];
mapView = aMapView;
rootNode = [[RMQuadTreeNode alloc] initWithMapView:mapView forParent:nil inBoundingBox:[[RMProjection googleProjection] planetBounds]];
return self;
}
... ... @@ -232,15 +335,20 @@
- (void)removeAllObjects
{
[rootNode release];
rootNode = [[RMQuadTreeNode alloc] initWithParent:nil forBoundingBox:[[RMProjection googleProjection] planetBounds]];
rootNode = [[RMQuadTreeNode alloc] initWithMapView:mapView forParent:nil inBoundingBox:[[RMProjection googleProjection] planetBounds]];
}
#pragma mark -
- (NSArray *)annotationsInProjectedRect:(RMProjectedRect)boundingBox
{
return [self annotationsInProjectedRect:boundingBox createClusterAnnotations:NO withClusterSize:RMMakeProjectedSize(0.0, 0.0) findGravityCenter:NO];
}
- (NSArray *)annotationsInProjectedRect:(RMProjectedRect)boundingBox createClusterAnnotations:(BOOL)createClusterAnnotations withClusterSize:(RMProjectedSize)clusterSize findGravityCenter:(BOOL)findGravityCenter
{
NSMutableArray *annotations = [NSMutableArray array];
[rootNode addAnnotationsInBoundingBox:boundingBox toMutableArray:annotations];
[rootNode addAnnotationsInBoundingBox:boundingBox toMutableArray:annotations createClusterAnnotations:createClusterAnnotations withClusterSize:clusterSize findGravityCenter:findGravityCenter];
return annotations;
}
... ...
... ... @@ -68,4 +68,6 @@ Changes in this clone (Alpstein/route-me)
* Markers have been refactored into a MKMapView-like system, with annotations and on-demand markers
* Automatic annotation clustering
* Requires at least iOS 4.0
... ...