Created
April 8, 2017 09:40
-
-
Save modestman/c5c86a741003b3994fc6fdcaadc75b9e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// GTMapDatasource.m | |
// GallopTravel | |
// | |
// Created by Anton Glezman on 02.06.16. | |
// Copyright © 2016 Globus It. All rights reserved. | |
// | |
@interface GTMapDatasource() <GTMapDataSourceDelegate, GTPoiDataSourceDelegate> | |
{ | |
GTBoundingBox *_currentRegion; | |
float currentZoom; | |
GTRequestTask* apiRequestTask; | |
NSMutableArray<GTRequestTask*> *poiRequestTasks; | |
NSInteger maxCountObjectsOnMap; | |
GTMapFilters *previousFilters; | |
GTPoiFilters *previousPoiFilters; | |
} | |
@property (nonatomic, strong) GTMapGridDataSource *gridDatasource; | |
@property (nonatomic, strong) GTMapMyPlacesDataSource *myPlacesDatasource; | |
@property (nonatomic, strong) GTPoiDataSource *poiDatasource; | |
@end | |
@implementation GTMapDatasource | |
@synthesize currentRegion = _currentRegion; | |
-(id)init | |
{ | |
self = [super init]; | |
if (self) | |
{ | |
self.objectsOnMap = [NSMutableDictionary new]; | |
self.poisOnMap = [NSMutableDictionary new]; | |
poiRequestTasks = [NSMutableArray new]; | |
} | |
return self; | |
} | |
-(void)setMapFrame:(CGRect)mapFrame | |
{ | |
// увеличиваем на 25% с каждой стороны | |
_mapFrame = CGRectInset(mapFrame, -(mapFrame.size.width/4), -(mapFrame.size.height/4)); | |
maxCountObjectsOnMap = floorf(_mapFrame.size.width / BigIconSize) * floorf(_mapFrame.size.height / BigIconSize); | |
self.gridDatasource.mapFrame = _mapFrame; | |
} | |
#pragma mark - | |
/* Алгоритм получения объектов для отображения на карте: | |
* 1) определить видимый регион (getObjectsForRegion) | |
* 2) удалить лишние объекты с карты (clusteringFilterExistingObjects) | |
* 3) Выбор дочернего datasource в зависимости от filtersModel (избранное, в поездке, грид) | |
* 4) получение объектов из дочернего datasource | |
* 5) фильтрация объектов с пересекающимися иконками (clusteringFilterNewObjects) | |
* 6) отображение на карте | |
*/ | |
-(void)getObjectsForRegion:(GTBoundingBox*)bbox zoomLevel:(float)zoom | |
{ | |
GTBoundingBox *_bbox = [bbox copy]; | |
[_bbox expandBBoxByPercent:0.5]; | |
_currentRegion = _bbox; | |
currentZoom = zoom; | |
if (![previousFilters isEqual:self.filtersDatasource.filtersModel]) | |
{ | |
// если предыдущий фильтр не соответсвует текущему, то удаляем все объекты с карты | |
[self clearAllObjects]; | |
[self.routesDatasource setPreviousFilter:nil]; | |
} | |
else | |
{ | |
// если фильтр не изменился, но поменялась видимая область карты, фильтруем существующие точки | |
[self clusteringFilterExistingObjectsInRegion:_bbox zoomLevel:zoom]; | |
} | |
id<GTMapDataSourceProtocol> specifiedDataSource = [self datasourceForCurrentFilter]; | |
if (specifiedDataSource) | |
{ | |
apiRequestTask = [specifiedDataSource requestObjectsForRegion:_bbox | |
withFilters:self.filtersDatasource.filtersModel zoomLevel:zoom]; | |
} | |
previousFilters = [self.filtersDatasource.filtersModel copy]; | |
} | |
#pragma mark - Clustering | |
-(void)clearAllObjects | |
{ | |
@synchronized (self) | |
{ | |
NSMutableSet *toRemove = [NSMutableSet setWithArray:[self.objectsOnMap allKeys]]; | |
[self.objectsOnMap removeAllObjects]; | |
if (self.delegate && [toRemove count] > 0) | |
{ | |
[self.delegate datasource:self addingObjects:@[] removedObjectIds:toRemove forRegion:nil]; | |
} | |
} | |
} | |
// фильтрация уже размещенных на карте точек при изменении масштаба или сдвиге карты | |
// убираем точки которые оказываются за границей видимой области и точки у которых пересекаются иконки | |
-(void)clusteringFilterExistingObjectsInRegion:(GTBoundingBox*)bbox zoomLevel:(float)zoom | |
{ | |
// WARNING: need to optimize! | |
@synchronized (self) | |
{ | |
// сортировка по рейтингу точек расположенных на карте | |
NSArray<GTMapAnnotationObject*>* sortedObjects = [[self.objectsOnMap allValues] sortedArrayUsingDescriptors: | |
@[[NSSortDescriptor sortDescriptorWithKey:@"object.rating" ascending:NO]]]; | |
// коллекция точек которые остаются на карте без изменения | |
NSMutableDictionary *updatedObjects = [NSMutableDictionary new]; | |
// коллекция точек которые необходимо удалить | |
NSMutableSet *toRemove = [NSMutableSet new]; | |
// цикл по всем точкам на карте | |
for (GTMapAnnotationObject *obj in sortedObjects) | |
{ | |
// экранные координаты точки (центр иконки) | |
CGPoint center = [GTSKMapCoordinateHelper pointForCoordinate:obj.object.coordinate inBoundingBox:bbox zoomLevel:zoom]; | |
CGRect iconFrame; | |
// вычисляем размер иконки (маленькая, средняя, большая) | |
iconFrame.size = iconSizeForType(obj.iconType); | |
iconFrame.origin = CGPointMake(center.x - (iconFrame.size.width / 2.0), center.y - (iconFrame.size.height / 2.0)); | |
obj.iconFrame = iconFrame; | |
// убираем иконки городов на масштабе > cityZoomLevel | |
NSNumber *key = [NSNumber numberWithInteger:obj.object.identifier]; | |
if (zoom > cityZoomLevel && obj.object.type != GeoObjectTypePlace) | |
{ | |
[toRemove addObject:key]; | |
continue; | |
} | |
if ([bbox containsCoordinate:obj.object.coordinate] && // точка не входит в видимый регион | |
![self isObject:obj intersectsWithOthers:[updatedObjects allValues]]) // проверяем пересечения иконок | |
{ | |
[updatedObjects setObject:obj forKey:key]; | |
} | |
else | |
{ | |
[toRemove addObject:key]; | |
} | |
} | |
self.objectsOnMap = updatedObjects; | |
if (self.delegate && [toRemove count] > 0) | |
{ | |
[self.delegate datasource:self addingObjects:@[] removedObjectIds:toRemove forRegion:bbox]; | |
} | |
} | |
} | |
// Фильтрация новых объектов которые получены из api или из кэша | |
// здесь проверяем пересечения с уже размещенными на карте иконками | |
-(void)clusteringFilterNewObjects:(NSArray<GTGeoObject*>*)objects | |
forRegion:(GTBoundingBox*)bbox zoomLevel:(float)zoom filters:(GTMapFilters*)filters | |
{ | |
@synchronized (self) { | |
// проверяем, что текущий регион пересекается с тем, который пришел из api ответа | |
// может быть ситуация, когда api отвечает долго и пользователь уже прокрутил карту в другое место, | |
// тогда нет необходимости обрабатывать пришедшие точки | |
GTBoundingBox *intersectRegion = [_currentRegion intersectionWithRegion:bbox]; | |
if (intersectRegion == nil) return; | |
// коллекция идентификаторов новых объектов | |
NSMutableSet *newObjectsIds = [NSMutableSet new]; | |
// коллекция идентификаторов для удаления | |
NSMutableSet *toRemove = [NSMutableSet new]; | |
int i = 0; | |
for (GTGeoObject *object in objects) | |
{ | |
// проверяем, размещена ли уже эта точка на карте | |
// если нет - то создаем новую (класс обертку) | |
GTMapAnnotationObject *obj = self.objectsOnMap[[NSNumber numberWithInteger:object.identifier]]; | |
BOOL alreadyExists = obj != nil; | |
if (!obj) | |
{ | |
obj = [GTMapAnnotationObject anntotationObjectWithGeoObject:[object copy]]; | |
obj.isFavorite = filters.favorites && object.favorite; | |
obj.isInRoute = filters.route != nil || [self.objectIdsInRoute containsObject:@(object.identifier)]; | |
} | |
// вычисляем положение и фрейм иконки на карте | |
CGPoint center = [GTSKMapCoordinateHelper pointForCoordinate:object.coordinate inBoundingBox:bbox zoomLevel:zoom]; | |
CGRect iconFrame; | |
iconFrame.size = iconSizeForType(obj.iconType); | |
iconFrame.origin = CGPointMake(center.x - (iconFrame.size.width / 2.0), center.y - (iconFrame.size.height / 2.0)); | |
obj.iconFrame = iconFrame; | |
if (!alreadyExists) | |
{ | |
// проверяем, есть ли у добавляемого объекта пересечения с уже расположенными на карте объектами | |
NSArray *intersects = [self findIntersectionsForObject:obj withOthers:[self.objectsOnMap allValues]]; | |
if ([intersects count] > 0) | |
{ | |
// если у нового объекта рейтинг больше чем у существующих, | |
// то удаляем старые объекты и добавляем новый | |
BOOL highRating = YES; | |
NSMutableArray *otherIds = [NSMutableArray new]; | |
for (GTMapAnnotationObject *otherObj in intersects) | |
{ | |
highRating = highRating && obj.object.rating > otherObj.object.rating; | |
[otherIds addObject:[NSNumber numberWithInteger:otherObj.object.identifier]]; | |
} | |
if (highRating) | |
{ | |
[self.objectsOnMap setObject:obj forKey:[NSNumber numberWithInteger:object.identifier]]; | |
[newObjectsIds addObject:[NSNumber numberWithInteger:object.identifier]]; | |
[toRemove addObjectsFromArray:otherIds]; | |
} | |
} | |
else | |
{ | |
// новый объект ни с чем не пересекается, добавляем его на карту | |
[self.objectsOnMap setObject:obj forKey:[NSNumber numberWithInteger:object.identifier]]; | |
[newObjectsIds addObject:[NSNumber numberWithInteger:object.identifier]]; | |
} | |
} | |
i++; | |
// ограничиваем максимальное количество точек на карте | |
// maxCountObjectsOnMap = mapFrame.width / bigIcon.width * mapFrame.height / bigIcon.height | |
if (self.objectsOnMap.count > maxCountObjectsOnMap) | |
break; | |
} | |
NSArray *addingObjects = [self.objectsOnMap objectsForKeys:[newObjectsIds allObjects] notFoundMarker:(id)[NSNull null]]; | |
if (self.delegate && ([addingObjects count] > 0 || [toRemove count] > 0)) | |
{ | |
[self.delegate datasource:self addingObjects:addingObjects removedObjectIds:toRemove forRegion:bbox]; | |
} | |
} | |
} | |
-(BOOL)isObject:(GTMapAnnotationObject*)object intersectsWithOthers:(NSArray<GTMapAnnotationObject*>*)otherObjects | |
{ | |
BOOL result = NO; | |
for (GTMapAnnotationObject *otherObj in otherObjects) | |
{ | |
result = CGRectIntersectsRect(object.iconFrame, otherObj.iconFrame); | |
if (result) | |
break; | |
} | |
return result; | |
} | |
-(NSArray*)findIntersectionsForObject:(GTMapAnnotationObject*)object withOthers:(NSArray<GTMapAnnotationObject*>*)otherObjects | |
{ | |
NSMutableArray<GTMapAnnotationObject*>* result = [NSMutableArray new]; | |
for (GTMapAnnotationObject *otherObj in otherObjects) | |
{ | |
BOOL isIntersects = CGRectIntersectsRect(object.iconFrame, otherObj.iconFrame); | |
if (isIntersects) | |
[result addObject:otherObj]; | |
} | |
return result; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment