123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324 |
- //
- // CCHMapClusterControllerUtils.m
- // CCHMapClusterController
- //
- // Copyright (C) 2013 Claus Höfele
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to deal
- // in the Software without restriction, including without limitation the rights
- // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- // copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in
- // all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- // THE SOFTWARE.
- //
- #import "CCHMapClusterControllerUtils.h"
- #import "CCHMapClusterAnnotation.h"
- #import <float.h>
- #define fequal(a, b) (fabs((a) - (b)) < __FLT_EPSILON__)
- #define GEOHASH_LENGTH 9
- MKMapRect CCHMapClusterControllerAlignMapRectToCellSize(MKMapRect mapRect, double cellSize)
- {
- if (cellSize == 0) {
- return MKMapRectNull;
- }
- double startX = floor(MKMapRectGetMinX(mapRect) / cellSize) * cellSize;
- double startY = floor(MKMapRectGetMinY(mapRect) / cellSize) * cellSize;
- double endX = ceil(MKMapRectGetMaxX(mapRect) / cellSize) * cellSize;
- double endY = ceil(MKMapRectGetMaxY(mapRect) / cellSize) * cellSize;
-
- return MKMapRectMake(startX, startY, endX - startX, endY - startY);
- }
- CCHMapClusterAnnotation *CCHMapClusterControllerFindVisibleAnnotation(NSSet *annotations, NSSet *visibleAnnotations)
- {
- for (id<MKAnnotation> annotation in annotations) {
- for (CCHMapClusterAnnotation *visibleAnnotation in visibleAnnotations) {
- if ([visibleAnnotation.annotations containsObject:annotation]) {
- return visibleAnnotation;
- }
- }
- }
-
- return nil;
- }
- #if TARGET_OS_IPHONE
- double CCHMapClusterControllerMapLengthForLength(MKMapView *mapView, UIView *view, double length)
- #else
- double CCHMapClusterControllerMapLengthForLength(MKMapView *mapView, NSView *view, double length)
- #endif
- {
- // Convert points to coordinates
- CLLocationCoordinate2D leftCoordinate = [mapView convertPoint:CGPointZero toCoordinateFromView:view];
- CLLocationCoordinate2D rightCoordinate = [mapView convertPoint:CGPointMake(length, 0) toCoordinateFromView:view];
-
- // Convert coordinates to map points
- MKMapPoint leftMapPoint = MKMapPointForCoordinate(leftCoordinate);
- MKMapPoint rightMapPoint = MKMapPointForCoordinate(rightCoordinate);
- // Calculate distance between map points
- double xd = leftMapPoint.x - rightMapPoint.x;
- double yd = leftMapPoint.y - rightMapPoint.y;
- double mapLength = sqrt(xd*xd + yd*yd);
-
- // For very large lengths, we assume that we measured the other way around the world
- if (mapLength > (MKMapSizeWorld.width * 0.5)) {
- mapLength = MKMapSizeWorld.width - mapLength;
- }
-
- return mapLength;
- }
- double CCHMapClusterControllerAlignMapLengthToWorldWidth(double mapLength)
- {
- if (mapLength == 0) {
- return 0;
- }
- mapLength = MKMapSizeWorld.width / floor(MKMapSizeWorld.width / mapLength);
- return mapLength;
- }
- BOOL CCHMapClusterControllerCoordinateEqualToCoordinate(CLLocationCoordinate2D coordinate0, CLLocationCoordinate2D coordinate1)
- {
- BOOL isCoordinateUpToDate = fequal(coordinate0.latitude, coordinate1.latitude) && fequal(coordinate0.longitude, coordinate1.longitude);
- return isCoordinateUpToDate;
- }
- CCHMapClusterAnnotation *CCHMapClusterControllerClusterAnnotationForAnnotation(MKMapView *mapView, id<MKAnnotation> annotation, MKMapRect mapRect)
- {
- CCHMapClusterAnnotation *annotationResult;
-
- NSSet *mapAnnotations = [mapView annotationsInMapRect:mapRect];
- for (id<MKAnnotation> mapAnnotation in mapAnnotations) {
- if ([mapAnnotation isKindOfClass:CCHMapClusterAnnotation.class]) {
- CCHMapClusterAnnotation *mapClusterAnnotation = (CCHMapClusterAnnotation *)mapAnnotation;
- if (mapClusterAnnotation.annotations) {
- if ([mapClusterAnnotation.annotations containsObject:annotation]) {
- annotationResult = mapClusterAnnotation;
- break;
- }
- }
- }
- }
-
- return annotationResult;
- }
- void CCHMapClusterControllerEnumerateCells(MKMapRect mapRect, double cellSize, void (^block)(MKMapRect cellMapRect))
- {
- NSCAssert(block != NULL, @"Block argument can't be NULL");
- if (block == nil) {
- return;
- }
-
- MKMapRect cellRect = MKMapRectMake(0, MKMapRectGetMinY(mapRect), cellSize, cellSize);
- while (MKMapRectGetMinY(cellRect) < MKMapRectGetMaxY(mapRect)) {
- cellRect.origin.x = MKMapRectGetMinX(mapRect);
-
- while (MKMapRectGetMinX(cellRect) < MKMapRectGetMaxX(mapRect)) {
- // Wrap around the origin's longitude
- MKMapRect rect = MKMapRectMake(fmod(cellRect.origin.x, MKMapSizeWorld.width), cellRect.origin.y, cellRect.size.width, cellRect.size.height);
- block(rect);
-
- cellRect.origin.x += MKMapRectGetWidth(cellRect);
- }
- cellRect.origin.y += MKMapRectGetWidth(cellRect);
- }
- }
- MKMapRect CCHMapClusterControllerMapRectForCoordinateRegion(MKCoordinateRegion coordinateRegion)
- {
- CLLocationCoordinate2D topLeftCoordinate = CLLocationCoordinate2DMake(coordinateRegion.center.latitude + (coordinateRegion.span.latitudeDelta / 2.0), coordinateRegion.center.longitude - (coordinateRegion.span.longitudeDelta / 2.0));
- MKMapPoint topLeftMapPoint = MKMapPointForCoordinate(topLeftCoordinate);
-
- CLLocationCoordinate2D bottomRightCoordinate = CLLocationCoordinate2DMake(coordinateRegion.center.latitude - (coordinateRegion.span.latitudeDelta / 2.0), coordinateRegion.center.longitude + (coordinateRegion.span.longitudeDelta / 2.0));
- MKMapPoint bottomRightMapPoint = MKMapPointForCoordinate(bottomRightCoordinate);
-
- MKMapRect mapRect = MKMapRectMake(topLeftMapPoint.x, topLeftMapPoint.y, fabs(bottomRightMapPoint.x - topLeftMapPoint.x), fabs(bottomRightMapPoint.y - topLeftMapPoint.y));
-
- return mapRect;
- }
- NSSet *CCHMapClusterControllerClusterAnnotationsForAnnotations(NSArray *annotations, CCHMapClusterController *mapClusterController)
- {
- NSSet *filteredAnnotations = [NSMutableSet setWithArray:annotations];
- filteredAnnotations = [filteredAnnotations filteredSetUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
- BOOL evaluation = NO;
- if ([evaluatedObject isKindOfClass:CCHMapClusterAnnotation.class]) {
- CCHMapClusterAnnotation *clusterAnnotation = (CCHMapClusterAnnotation *)evaluatedObject;
- evaluation = (clusterAnnotation.mapClusterController == mapClusterController);
- }
- return evaluation;
- }]];
-
- return filteredAnnotations;
- }
- NS_INLINE double originXForLongitudeAtZoomLevel22(CLLocationDegrees longitude)
- {
- const double MERCATOR_OFFSET = 536870912; // (width in points at zoom level 22) / 2
- const double MERCATOR_RADIUS_SCALE = MERCATOR_OFFSET / 180.0;
-
- return MERCATOR_OFFSET + MERCATOR_RADIUS_SCALE * longitude;
- }
- double CCHMapClusterControllerZoomLevelForRegion(CLLocationDegrees longitudeCenter, CLLocationDegrees longitudeDelta, CGFloat width)
- {
- // Based on http://troybrant.net/blog/2010/01/mkmapview-and-zoom-levels-a-visual-guide/
- // Adjusted so that at zoom level 0, the entire world fits into a single 256 point tile.
- const double LOG_2 = 0.69314718055994529; // log(2)
-
- double centerPointX = originXForLongitudeAtZoomLevel22(longitudeCenter);
- double topLeftPointX = originXForLongitudeAtZoomLevel22(longitudeCenter - longitudeDelta / 2);
-
- double scaledMapWidth = (centerPointX - topLeftPointX) * 2;
- double zoomScale = scaledMapWidth / width;
- double zoomExponent = log(zoomScale) / LOG_2;
- double zoomLevel = 22 - zoomExponent;
-
- return zoomLevel;
- }
- #define MAX_HASH_LENGTH 22
- #define SET_BIT(bits, mid, range, value, offset) \
- mid = ((range)->max + (range)->min) / 2.0; \
- if ((value) >= mid) { \
- (range)->min = mid; \
- (bits) |= (0x1 << (offset)); \
- } else { \
- (range)->max = mid; \
- (bits) |= (0x0 << (offset)); \
- }
- static const char BASE32_ENCODE_TABLE[33] = "0123456789bcdefghjkmnpqrstuvwxyz";
- typedef struct {
- double max;
- double min;
- } GEOHASH_range;
- static char *GEOHASH_encode(double lat, double lon, unsigned long len)
- {
- unsigned long i;
- char *hash;
- unsigned char bits = 0;
- double mid;
- GEOHASH_range lat_range = { 90, -90 };
- GEOHASH_range lon_range = { 180, -180 };
-
- double val1, val2, val_tmp;
- GEOHASH_range *range1, *range2, *range_tmp;
-
- assert(lat >= -90.0);
- assert(lat <= 90.0);
- assert(lon >= -180.0);
- assert(lon <= 180.0);
- assert(len <= MAX_HASH_LENGTH);
-
- hash = (char *)malloc(sizeof(char) * (len + 1));
- if (hash == NULL)
- return NULL;
-
- val1 = lon; range1 = &lon_range;
- val2 = lat; range2 = &lat_range;
-
- for (i=0; i < len; i++) {
-
- bits = 0;
-
- SET_BIT(bits, mid, range1, val1, 4);
- SET_BIT(bits, mid, range2, val2, 3);
- SET_BIT(bits, mid, range1, val1, 2);
- SET_BIT(bits, mid, range2, val2, 1);
- SET_BIT(bits, mid, range1, val1, 0);
-
- hash[i] = BASE32_ENCODE_TABLE[bits];
-
- val_tmp = val1;
- val1 = val2;
- val2 = val_tmp;
- range_tmp = range1;
- range1 = range2;
- range2 = range_tmp;
- }
-
- hash[len] = '\0';
- return hash;
- }
- static NSString *hashForCoordinate(CLLocationCoordinate2D coordinate, NSUInteger length)
- {
- // Based on https://github.com/lyokato/objc-geohash
- NSString *geohashAsString;
-
- char *geohash = GEOHASH_encode(coordinate.latitude, coordinate.longitude, length);
- if (geohash) {
- geohashAsString = [NSString stringWithCString:geohash encoding:NSASCIIStringEncoding];
- }
- free(geohash);
-
- return geohashAsString;
- }
- NSArray *CCHMapClusterControllerAnnotationSetsByUniqueLocations(NSSet *annotations, NSUInteger maxUniqueLocations)
- {
- NSMutableDictionary *annotationsByGeohash;
-
- if (maxUniqueLocations > 0) {
- annotationsByGeohash = [NSMutableDictionary dictionary];
-
- for (id<MKAnnotation> annotation in annotations) {
- // Add annotation to unique locations
- NSString *geohash = hashForCoordinate(annotation.coordinate, GEOHASH_LENGTH);
- NSMutableSet *annotationsAtLocation = [annotationsByGeohash objectForKey:geohash];
- if (!annotationsAtLocation) {
- annotationsAtLocation = [NSMutableSet set];
- }
- [annotationsAtLocation addObject:annotation];
- [annotationsByGeohash setObject:annotationsAtLocation forKey:geohash];
-
- // Return nil if max has been reached
- if (annotationsByGeohash.count > maxUniqueLocations) {
- annotationsByGeohash = nil;
- break;
- }
- }
- }
-
- return [annotationsByGeohash allValues];
- }
- BOOL CCHMapClusterControllerIsUniqueLocation(NSSet *annotations)
- {
- NSString *geohash;
- for (id<MKAnnotation> annotation in annotations) {
- NSString *updatedGeohash = hashForCoordinate(annotation.coordinate, GEOHASH_LENGTH);
- if (geohash == nil) {
- geohash = updatedGeohash;
- } else if (![geohash isEqualToString:updatedGeohash]) {
- geohash = nil;
- break;
- }
- }
-
- return (geohash != nil);
- }
|