Skip to content

Instantly share code, notes, and snippets.

@modestman
Created April 8, 2017 09:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save modestman/c5c86a741003b3994fc6fdcaadc75b9e to your computer and use it in GitHub Desktop.
Save modestman/c5c86a741003b3994fc6fdcaadc75b9e to your computer and use it in GitHub Desktop.
//
// 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