마커 클러스터링
한 화면에 대량의 마커가 노출되면 성능이 저하될 뿐만 아니라 여러 마커가 겹쳐 나타나므로 시인성이 떨어집니다. 마커의 겹침 처리 기능을 사용하면 시인성을 일부 향상시킬 수 있으나 겹침 처리로 인해 가려진 마커의 정보를 알 수 없으며, 성능도 여전히 저하됩니다. 마커 클러스터링 기능을 이용하면 카메라의 줌 레벨에 따라 근접한 마커를 클러스터링해 성능과 시인성을 모두 향상시킬 수 있습니다.
기본적인 사용법
키 정의
마커 클러스터링 기능을 이용하려면 먼저 데이터의 키를 의미하는 NMCClusteringKey
인터페이스를 구현한 클래스를 정의해야 합니다. NMCClusteringKey
인터페이스는 데이터의 좌표뿐만 아니라 두 데이터가 동일한지를 정의합니다. 따라서 이 인터페이스를 구현하는 클래스는 isEqual:
, hash
및 copyWithZone:
도 구현하는 것이 권장됩니다.
다음은 NMCClusteringKey
를 구현하는 클래스를 정의하는 예제입니다.
class ItemKey: NSObject, NMCClusteringKey {
let identifier: Int
let position: NMGLatLng
init(identifier: Int, position: NMGLatLng) {
self.identifier = identifier
self.position = position
}
static func markerKey(withIdentifier identifier: Int, position: NMGLatLng) -> ItemKey {
return ItemKey(identifier: identifier, position: position)
}
override func isEqual(_ o: Any?) -> Bool {
guard let o = o as? ItemKey else {
return false
}
if self === o {
return true
}
return o.identifier == self.identifier
}
override var hash: Int {
return self.identifier
}
func copy(with zone: NSZone? = nil) -> Any {
return ItemKey(identifier: self.identifier, position: self.position)
}
}
Swift
class ItemKey: NSObject, NMCClusteringKey {
let identifier: Int
let position: NMGLatLng
init(identifier: Int, position: NMGLatLng) {
self.identifier = identifier
self.position = position
}
static func markerKey(withIdentifier identifier: Int, position: NMGLatLng) -> ItemKey {
return ItemKey(identifier: identifier, position: position)
}
override func isEqual(_ o: Any?) -> Bool {
guard let o = o as? ItemKey else {
return false
}
if self === o {
return true
}
return o.identifier == self.identifier
}
override var hash: Int {
return self.identifier
}
func copy(with zone: NSZone? = nil) -> Any {
return ItemKey(identifier: self.identifier, position: self.position)
}
}
Objective-C
@interface ItemKey: NSObject<NMCClusteringKey>
@property (nonatomic) NSInteger identifier;
@property (nonatomic) NMGLatLng *position;
+ (instancetype)markerKeyWithIdentifier:(NSInteger)identifier Position:(NMGLatLng *)position;
@end
@implementation ItemKey
+ (instancetype)markerKeyWithIdentifier:(NSInteger)identifier Position:(NMGLatLng *)position {
return [[ItemKey alloc] initWithIdentifier:identifier Position:position];
}
- (instancetype)initWithIdentifier:(NSInteger)identifier Position:(NMGLatLng *)position {
self = [super init];
if (self) {
_identifier = identifier;
_position = position;
}
return self;
}
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (object == nil || [self class] != [object class]) {
return NO;
}
ItemKey *key = object;
return key.identifier == self.identifier;
}
- (NSUInteger)hash {
return self.identifier;
}
- (nonnil id)copyWithZone:(nilable NSZone *)zone {
return [[[self class] alloc] initWithIdentifier:self.identifier Position:self.position];
}
클러스터러 생성
실제 클러스터링 동작은 NMCClusterer
객체가 수행합니다. NMCClusterer
클래스의 인스턴스를 생성하려면 NMCBuilder
를 사용해야 합니다. NMCBuilder
는 데이터의 키를 의미하는 타입 파라미터를 요구하므로 앞서 정의한 클래스를 지정해야 합니다. 이후 -build
메서드를 호출하면 NMCClusterer
객체가 만들어집니다.
다음은 NMCClusterer
객체를 생성하는 예제입니다.
let builder = NMCBuilder<ItemKey>()
let clusterer = builder.build()
Swift
let builder = NMCBuilder<ItemKey>()
let clusterer = builder.build()
Objective-C
NMCBuilder *builder = [[NMCBuilder alloc] init];
NMCClusterer *clusterer = [builder build];
데이터 추가
마커 데이터를 추가하려면 생성한 NMCClusterer
객체의 -add::
메서드를 호출해야 합니다. 데이터에는 키와 별도로 태그를 지정할 수 있으며, 필요치 않을 경우 nil
을 지정할 수 있습니다.
다음은 클러스터러에 데이터를 추가하는 예제입니다.
clusterer.add(ItemKey(identifier: 1, position: NMGLatLng(lat: 37.372, lng: 127.113)), nil)
clusterer.add(ItemKey(identifier: 2, position: NMGLatLng(lat: 37.366, lng: 127.106)), nil)
clusterer.add(ItemKey(identifier: 3, position: NMGLatLng(lat: 37.365, lng: 127.157)), nil)
clusterer.add(ItemKey(identifier: 4, position: NMGLatLng(lat: 37.361, lng: 127.105)), nil)
clusterer.add(ItemKey(identifier: 5, position: NMGLatLng(lat: 37.368, lng: 127.110)), nil)
clusterer.add(ItemKey(identifier: 6, position: NMGLatLng(lat: 37.360, lng: 127.106)), nil)
clusterer.add(ItemKey(identifier: 7, position: NMGLatLng(lat: 37.363, lng: 127.111)), nil)
Swift
clusterer.add(ItemKey(identifier: 1, position: NMGLatLng(lat: 37.372, lng: 127.113)), nil)
clusterer.add(ItemKey(identifier: 2, position: NMGLatLng(lat: 37.366, lng: 127.106)), nil)
clusterer.add(ItemKey(identifier: 3, position: NMGLatLng(lat: 37.365, lng: 127.157)), nil)
clusterer.add(ItemKey(identifier: 4, position: NMGLatLng(lat: 37.361, lng: 127.105)), nil)
clusterer.add(ItemKey(identifier: 5, position: NMGLatLng(lat: 37.368, lng: 127.110)), nil)
clusterer.add(ItemKey(identifier: 6, position: NMGLatLng(lat: 37.360, lng: 127.106)), nil)
clusterer.add(ItemKey(identifier: 7, position: NMGLatLng(lat: 37.363, lng: 127.111)), nil)
Objective-C
[clusterer add:[[ItemKey alloc] initWithIdentifier:1 Position:NMGLatLngMake(37.372, 127.113)] :nil];
[clusterer add:[[ItemKey alloc] initWithIdentifier:2 Position:NMGLatLngMake(37.366, 127.106)] :nil];
[clusterer add:[[ItemKey alloc] initWithIdentifier:3 Position:NMGLatLngMake(37.365, 127.157)] :nil];
[clusterer add:[[ItemKey alloc] initWithIdentifier:4 Position:NMGLatLngMake(37.361, 127.105)] :nil];
[clusterer add:[[ItemKey alloc] initWithIdentifier:5 Position:NMGLatLngMake(37.368, 127.110)] :nil];
[clusterer add:[[ItemKey alloc] initWithIdentifier:6 Position:NMGLatLngMake(37.360, 127.106)] :nil];
[clusterer add:[[ItemKey alloc] initWithIdentifier:7 Position:NMGLatLngMake(37.363, 127.111)] :nil];
데이터를 추가할 때는 -add::
메서드를 여러 번 호출하기보다 -addAll:
메서드를 한 번만 호출하면 성능이 향상됩니다.
다음은 -addAll:
로 데이터를 추가하는 예제입니다.
let keyTagMap = [
ItemKey(identifier: 1, position: NMGLatLng(lat: 37.372, lng: 127.113)): NSNull(),
ItemKey(identifier: 2, position: NMGLatLng(lat: 37.366, lng: 127.106)): NSNull(),
ItemKey(identifier: 3, position: NMGLatLng(lat: 37.365, lng: 127.157)): NSNull(),
ItemKey(identifier: 4, position: NMGLatLng(lat: 37.361, lng: 127.105)): NSNull(),
ItemKey(identifier: 5, position: NMGLatLng(lat: 37.368, lng: 127.110)): NSNull(),
ItemKey(identifier: 6, position: NMGLatLng(lat: 37.360, lng: 127.106)): NSNull(),
ItemKey(identifier: 7, position: NMGLatLng(lat: 37.363, lng: 127.111)): NSNull()
]
clusterer.addAll(keyTagMap);
Swift
let keyTagMap = [
ItemKey(identifier: 1, position: NMGLatLng(lat: 37.372, lng: 127.113)): NSNull(),
ItemKey(identifier: 2, position: NMGLatLng(lat: 37.366, lng: 127.106)): NSNull(),
ItemKey(identifier: 3, position: NMGLatLng(lat: 37.365, lng: 127.157)): NSNull(),
ItemKey(identifier: 4, position: NMGLatLng(lat: 37.361, lng: 127.105)): NSNull(),
ItemKey(identifier: 5, position: NMGLatLng(lat: 37.368, lng: 127.110)): NSNull(),
ItemKey(identifier: 6, position: NMGLatLng(lat: 37.360, lng: 127.106)): NSNull(),
ItemKey(identifier: 7, position: NMGLatLng(lat: 37.363, lng: 127.111)): NSNull()
]
clusterer.addAll(keyTagMap);
Objective-C
NSDictionary *keyTagMap = @{
[[ItemKey alloc] initWithIdentifier:1 Position:NMGLatLngMake(37.372, 127.113)]: [NSNull null],
[[ItemKey alloc] initWithIdentifier:2 Position:NMGLatLngMake(37.366, 127.106)]: [NSNull null],
[[ItemKey alloc] initWithIdentifier:3 Position:NMGLatLngMake(37.365, 127.157)]: [NSNull null],
[[ItemKey alloc] initWithIdentifier:4 Position:NMGLatLngMake(37.361, 127.105)]: [NSNull null],
[[ItemKey alloc] initWithIdentifier:5 Position:NMGLatLngMake(37.368, 127.110)]: [NSNull null],
[[ItemKey alloc] initWithIdentifier:6 Position:NMGLatLngMake(37.360, 127.106)]: [NSNull null],
[[ItemKey alloc] initWithIdentifier:7 Position:NMGLatLngMake(37.363, 127.111)]: [NSNull null]
};
[clusterer addAll:keyTagMap];
지도 객체 지정
NMCClusterer
객체의 mapView
속성에 지도 객체를 지정하면 카메라의 줌 레벨에 따라 자동으로 클러스터링된 마커가 나타나며, 클러스터링된 마커를 클릭하면 클러스터가 펼쳐지는 최소 줌 레벨로 확대됩니다.
다음은 클러스터러에 지도 객체를 지정하는 예제입니다.
clusterer.mapView = mapView
Swift
clusterer.mapView = mapView
Objective-C
clusterer.mapView = mapView;
옵션 사용
NMCBuilder
의 프로퍼티를 사용해 클러스터링할 거리, 최소/최대 줌 레벨, 애니메이션 여부, 클러스터/단말 마커 커스터마이징 등 다양한 클러스터링 옵션을 지정할 수 있습니다.
클러스터링 거리
screenDistance
프로퍼티를 사용해 클러스터링할 기준 거리를 pt 단위로 지정할 수 있습니다. 클러스터에 추가된 두 데이터의 화면상 거리가 기준 거리보다 가깝다면 클러스터링되어 하나의 마커로 나타납니다.
다음은 클러스터러의 기준 거리를 20DP로 지정하는 예제입니다.
builder.screenDistance = 20
Swift
builder.screenDistance = 20
Objective-C
builder.screenDistance = 20;
최소 및 최대 줌 레벨
minZoom
및 maxZoom
프로퍼티를 사용해 클러스터링할 최소 및 최대 줌 레벨을 제한할 수 있습니다. 카메라의 줌 레벨이 최소 줌 레벨보다 낮거나 최대 줌 레벨보다 높다면 두 데이터가 화면상 기준 거리보다 가깝더라도 클러스터링되지 않습니다. 예를 들어, 클러스터링할 최소 줌 레벨이 4이고 최대 줌 레벨이 16이라면, 카메라의 줌 레벨을 3 이하로 축소하더라도 4레벨의 클러스터가 더 이상 클러스터링되지 않고 그대로 유지되며, 17 이상으로 확대하면 모든 데이터가 클러스터링되지 않고 낱개로 나타납니다.
다음은 클러스터러의 최소 레벨을 4, 최대 레벨을 16으로 지정하는 예제입니다.
builder.minZoom = 4
builder.maxZoom = 16
Swift
builder.minZoom = 4
builder.maxZoom = 16
Objective-C
builder.minZoom = 4;
builder.maxZoom = 16;
확대/축소 애니메이션
animate
프로퍼티를 사용해 카메라 확대/축소시 클러스터가 펼쳐지는/합쳐지는 애니메이션을 적용할지 여부를 지정할 수 있습니다.
다음은 애니메이션을 적용하지 않도록 지정하는 예제입니다.
builder.animate = false
Swift
builder.animate = false
Objective-C
builder.animate = NO;
마커 커스터마이징
clusterMarkerUpdater
및 leafMarkerUpdater
프로퍼티를 사용해 클러스터와 단말(leaf) 마커를 커스터마이징할 수 있습니다.
클러스터 마커를 커스터마이징하려면 clusterMarkerUpdater
프로퍼티를 사용해 NMCClusterMarkerUpdater
인스턴스를 지정해야 합니다. 클러스터 마커가 지도에 나타날 때마다 -updateClusterMarker::
메서드가 호출되며, 파라미터로 NMCClusterMarkerInfo
와 이를 표현할 마커 객체가 전달됩니다. NMCClusterMarkerInfo
에는 클러스터의 크기, 노출되는 최소/최대 줌 레벨, 태그 등 클러스터 마커의 정보가 포함되어 있으므로 필요한 정보를 마커 객체의 속성에 반영하면 지도 화면에도 반영됩니다. 기본값인 NMCDefaultClusterMarkerUpdater
를 사용하면 클러스터의 크기를 캡션으로 노출하고 클러스터 클릭 시 클러스터가 펼쳐지는 최소 줌 레벨로 확대하는 기능 등을 이용할 수 있으므로 이를 상속받아 필요한 부분만 커스터마이징할 수도 있습니다.
단말 마커를 커스터마이징하려면 leafMarkerUpdater
프로퍼티를 사용해 NMCLeafMarkerUpdater
인스턴스를 지정해야 합니다. 단말 마커가 지도에 나타날 때마다 -updateLeafMarker::
메서드가 호출되며, 파라미터로 NMCLeafMarkerInfo
와 이를 표현할 마커 객체가 전달됩니다. NMCLeafMarkerInfo
에는 데이터가 노출되는 최소/최대 줌 레벨, 태그 등 단말 마커의 정보가 포함되어 있으므로 필요한 정보를 마커 객체의 속성에 반영하면 지도 화면에도 반영됩니다. 기본값인 NMCDefaultLeafMarkerUpdater
를 상속받아 필요한 부분만 커스터마이징할 수도 있습니다.
한편 클러스터러는 화면에 나타나야 하는 클러스터 및 단말 데이터만을 마커로 만들어 동적으로 지도에 추가/제거하며, 제거된 마커 객체는 기본적으로 재사용됩니다. 때문에 -updateClusterMarker::
, -updateLeafMarker::
메서드로 전달되는 마커 객체에는 이전 데이터의 속성이 여전히 반영되어 있을 수 있습니다. 따라서 마커를 커스터마이징하고자 하는 경우 필요한 속성을 빠짐없이 덮어씌워야 합니다.
다음은 클러스터의 크기 및 데이터의 ID에 따라 마커 아이콘을 변경하고, 단말 마커를 클릭했을 때 클러스터러에서 제거하도록 지정하는 예제입니다.
let icons: [NMFOverlayImage] = [ NMF_MARKER_IMAGE_BLUE, NMF_MARKER_IMAGE_GREEN, NMF_MARKER_IMAGE_RED, NMF_MARKER_IMAGE_YELLOW ]
class ClusterMarkerUpdater: NMCDefaultClusterMarkerUpdater {
override func updateClusterMarker(_ info: NMCClusterMarkerInfo, _ marker: NMFMarker) {
super.updateClusterMarker(info, marker)
if info.size < 3 {
marker.iconImage = NMF_MARKER_IMAGE_CLUSTER_LOW_DENSITY
} else {
marker.iconImage = NMF_MARKER_IMAGE_CLUSTER_MEDIUM_DENSITY
}
}
}
class LeafMarkerUpdater: NMCDefaultLeafMarkerUpdater {
var clusterer: NMCClusterer<ItemKey>?
override func updateLeafMarker(_ info: NMCLeafMarkerInfo, _ marker: NMFMarker) {
super.updateLeafMarker(info, marker)
if let key = info.key as? ItemKey {
marker.iconImage = icons[key.identifier % icons.count]
marker.touchHandler = { [weak self] (o: NMFOverlay) -> Bool in
self?.clusterer?.remove(key)
return true
}
}
}
}
builder.clusterMarkerUpdater = ClusterMarkerUpdater()
builder.leafMarkerUpdater = LeafMarkerUpdater()
Swift
let icons: [NMFOverlayImage] = [ NMF_MARKER_IMAGE_BLUE, NMF_MARKER_IMAGE_GREEN, NMF_MARKER_IMAGE_RED, NMF_MARKER_IMAGE_YELLOW ]
class ClusterMarkerUpdater: NMCDefaultClusterMarkerUpdater {
override func updateClusterMarker(_ info: NMCClusterMarkerInfo, _ marker: NMFMarker) {
super.updateClusterMarker(info, marker)
if info.size < 3 {
marker.iconImage = NMF_MARKER_IMAGE_CLUSTER_LOW_DENSITY
} else {
marker.iconImage = NMF_MARKER_IMAGE_CLUSTER_MEDIUM_DENSITY
}
}
}
class LeafMarkerUpdater: NMCDefaultLeafMarkerUpdater {
var clusterer: NMCClusterer<ItemKey>?
override func updateLeafMarker(_ info: NMCLeafMarkerInfo, _ marker: NMFMarker) {
super.updateLeafMarker(info, marker)
if let key = info.key as? ItemKey {
marker.iconImage = icons[key.identifier % icons.count]
marker.touchHandler = { [weak self] (o: NMFOverlay) -> Bool in
self?.clusterer?.remove(key)
return true
}
}
}
}
builder.clusterMarkerUpdater = ClusterMarkerUpdater()
builder.leafMarkerUpdater = LeafMarkerUpdater()
Objective-C
@interface ClusterMarkerUpdater : NMCDefaultClusterMarkerUpdater
@end
@implementation ClusterMarkerUpdater
- (void)updateClusterMarker:(NMCClusterMarkerInfo *)info :(NMFMarker *)marker {
[super updateClusterMarker:info :marker];
if (info.size < 3) {
marker.iconImage = NMF_MARKER_IMAGE_CLUSTER_LOW_DENSITY;
} else {
marker.iconImage = NMF_MARKER_IMAGE_CLUSTER_MEDIUM_DENSITY;
}
}
@end
@interface LeafMarkerUpdater : NMCDefaultLeafMarkerUpdater
@property (nonatomic, weak, nullable) NMCClusterer *clusterer;
@end
@implementation LeafMarkerUpdater
- (void)updateLeafMarker:(NMCLeafMarkerInfo * _Nonnull)info :(NMFMarker * _Nonnull)marker {
[super updateLeafMarker:info :marker];
ItemKey *key = (ItemKey *)info.key;
NSArray<NMFOverlayImage *> *icons = @[
NMF_MARKER_IMAGE_BLUE,
NMF_MARKER_IMAGE_GREEN,
NMF_MARKER_IMAGE_RED,
NMF_MARKER_IMAGE_YELLOW
];
marker.iconImage = icons[key.identifier % icons.count];
__block typeof(self) weakSelf = self;
marker.touchHandler = ^BOOL(NMFOverlay * _Nonnull __weak overlay) {
[weakSelf.clusterer remove:(ItemKey *)info.key];
return YES;
};
}
@end
builder.leafMarkerUpdater = [[LeafMarkerUpdater alloc] init];
builder.clusterMarkerUpdater = [[ClusterMarkerUpdater alloc] init];
복잡한 전략 사용
NMCBuilder
대신 NMCComplexBuilder
를 사용하면 화면상 거리가 아닌 다른 기준으로 데이터를 클러스터링하는 등 복잡한 전략과 기능을 사용할 수 있습니다.
노드
네이버 지도 SDK의 마커 클러스터링 기능은 hierarchical agglomerative clustering 알고리즘에 기반합니다. 즉, 클러스터러의 최대 줌 레벨부터 인접 데이터를 클러스터링하여, 최소 줌 레벨까지 바텀-업 방식으로 트리를 구성하며 클러스터링합니다. 따라서 트리에서 자식이 있는 노드는 클러스터, 단말 노드는 데이터가 됩니다.
각 노드는 NMCNode
클래스로 표현되며, 클러스터와 단말 노드는 각각 NMCNode
의 하위 클래스인 NMCCluster
와 NMCLeaf
로 표현됩니다.
노드를 이용해 공통적으로 좌표, 태그, 자식 노드의 개수, 노출되어야 하는 최소/최대 줌 레벨 속성을 가져올 수 있으며, 이에 더해 클러스터 노드는 자식 노드의 목록에, 단말 노드는 데이터의 키에 접근할 수 있습니다.
기준 거리 전략 및 거리 측정 전략
thresholdStrategy
및 distanceStrategy
프로퍼티를 사용해 특정 줌 레벨에서 두 노드를 클러스터링할지에 대한 전략을 지정할 수 있습니다.
thresholdStrategy
로 지정하는 NMCThresholdStrategy
는 특정 줌 레벨에서 두 노드를 클러스터링할 기준 거리를 구하는 전략에 대한 인터페이스이며, distanceStrategy
로 지정하는 NMCDistanceStrategy
는 특정 줌 레벨에서 두 노드간의 거리를 측정하는 전략에 대한 인터페이스입니다.
즉, 두 노드 node1
과 node2
가 있을 때, zoom
줌 레벨에서 NMCDistanceStrategy.getDistance:Node1:Node2:
가 NMCThresholdStrategy.getThreshold:
보다 작거나 같다면 node1
과 node2
는 클러스터링됩니다. 이를 이용하면 줌 레벨에 따라 거리 기준을 달리하거나 화면상 거리와 무관하게 클러스터링 전략을 지정할 수 있습니다.
다음은 9레벨 이하에서는 무조건 클러스터링, 10~13레벨에서는 태그가 동일할 때 클러스터링, 14레벨 이상에서는 화면상 거리가 70 미만일 때 클러스터링하도록 전략을 지정하는 예제입니다.
extension ClusteringViewController: NMCThresholdStrategy, NMCDistanceStrategy {
func getThreshold(_ zoom: Int) -> Double {
if zoom <= 11 {
return 0
} else {
return 70
}
}
func getDistance(_ zoom: Int, node1: NMCNode, node2: NMCNode) -> Double {
if zoom <= 9 {
return -1
} else if zoom <= 13, let tag1 = node1.tag, let tag2 = node2.tag {
if tag1.isEqual(tag2) {
return -1
} else {
return 1
}
} else {
return NMCDefaultDistanceStrategy().getDistance(zoom, node1: node1, node2: node2)
}
}
}
complexBuilder.thresholdStrategy = self
complexBuilder.distanceStrategy = self
Swift
extension ClusteringViewController: NMCThresholdStrategy, NMCDistanceStrategy {
func getThreshold(_ zoom: Int) -> Double {
if zoom <= 11 {
return 0
} else {
return 70
}
}
func getDistance(_ zoom: Int, node1: NMCNode, node2: NMCNode) -> Double {
if zoom <= 9 {
return -1
} else if zoom <= 13, let tag1 = node1.tag, let tag2 = node2.tag {
if tag1.isEqual(tag2) {
return -1
} else {
return 1
}
} else {
return NMCDefaultDistanceStrategy().getDistance(zoom, node1: node1, node2: node2)
}
}
}
complexBuilder.thresholdStrategy = self
complexBuilder.distanceStrategy = self
Objective-C
@interface ClusteringViewController () <NMCThresholdStrategy, NMCDistanceStrategy>
@end
@implementation MarkerClusteringStrategyViewController
- (double)getThreshold:(NSInteger)zoom {
if (zoom <= 11) {
return 0;
} else {
return 70;
}
}
- (double)getDistance:(NSInteger)zoom Node1:(NMCNode * _Nonnull)node1 Node2:(NMCNode * _Nonnull)node2 {
if (zoom <= 9) {
return -1;
} else if (zoom <= 13) {
if ([node1.tag isEqual:node2.tag]) {
return -1;
} else {
return 1;
}
} else {
return [[[NMCDefaultDistanceStrategy alloc] init] getDistance:zoom Node1:node1 Node2:node2];
}
}
@end
complexBuilder.thresholdStrategy = self;
complexBuilder.distanceStrategy = self;
최대 화면 거리 제한
모든 노드에 대해 모든 인접 노드를 대상으로 클러스터링을 시도하면 시간복잡도가 기하급수적으로 증가하여 성능에 큰 악영향을 미칩니다. 때문에 화면상 거리를 기준으로 삼아 후보 노드에 대한 탐색 공간을 제한할 수 있습니다.
maxScreenDistance
프로퍼티를 사용해 클러스터링할 최대 화면 거리를 지정할 수 있습니다. 즉, 두 노드 node1
과 node2
가 있을 때, zoom
줌 레벨에서 두 노드의 화면상 거리가 이 값보다 크다면, NMCDistanceStrategy.getDistance:Node1:Node2:
가 NMCThresholdStrategy.getThreshold:
보다 작거나 같더라도 node1
과 node2
는 클러스터링되지 않습니다.
따라서 NMCThresholdStrategy
와 NMCDistanceStrategy
를 지정했다면 전략을 고려해 최적의 거리를 지정해야 합니다. 값을 너무 작게 지정하면 클러스터링되어야 하는 노드가 클러스터링되지 않을 수 있고, 너무 크게 지정하면 성능이 저하됩니다.
다음은 최대 화면 거리 제한을 100DP로 변경하는 예제입니다.
complexBuilder.maxScreenDistance = 100
Swift
complexBuilder.maxScreenDistance = 100
Objective-C
complexBuilder.maxScreenDistance = 100
태그 병합 전략
단말 노드에는 NMCClusterer.add::
로 지정한 키와 태그가 유지됩니다. 하지만 클러스터 노드는 여러 노드가 클러스터링되어 만들어진 것이므로 키를 가질 수 없으며, 태그 역시 자동으로 생성할 수 없습니다.
대신 tagMergeStrategy
프로퍼티를 사용해 자식 노드들의 태그를 병합해 클러스터 노드의 태그로 삼는 전략을 지정할 수 있습니다. 여러 노드가 하나의 클러스터로 클러스터링될 때마다 -mergeTag:
메서드가 호출되며, 파라미터로 전달되는 NMCCluster
의 children
을 이용해 자식 노드를 순회할 수 있습니다. 자식 노드의 태그를 병합해 반환하면 반환한 값이 NMCCluster
의 태그가 됩니다.
다음은 데이터의 태그가 정수일 때, 자식 노드의 정수를 모두 더해 부모 클러스터의 태그로 삼는 전략을 지정하는 예제입니다.
extension ClusteringViewController: NMCTagMergeStrategy {
func mergeTag(_ cluster: NMCCluster) -> NSObject? {
var sum = 0
for node in cluster.children {
if let tag = node.tag as? NSNumber {
sum += tag.intValue
}
}
return NSNumber(value: sum)
}
}
complexBuilder.tagMergeStrategy = self
Swift
extension ClusteringViewController: NMCTagMergeStrategy {
func mergeTag(_ cluster: NMCCluster) -> NSObject? {
var sum = 0
for node in cluster.children {
if let tag = node.tag as? NSNumber {
sum += tag.intValue
}
}
return NSNumber(value: sum)
}
}
complexBuilder.tagMergeStrategy = self
Objective-C
@interface ClusteringViewController () <NMCTagMergeStrategy>
@end
@implementation MarkerClusteringStrategyViewController
- (NSObject * _Nullable)mergeTag:(NMCCluster * _Nonnull)cluster {
int sum = 0;
for (NMCNode *node in cluster.children) {
sum += ((NSNumber *)node.tag).intValue;
}
return [NSNumber numberWithInt:sum];
}
@end
complexBuilder.tagMergeStrategy = self;
좌표 전략
단말 노드의 좌표는 NMCClusterer.add::
로 지정한 키의 좌표입니다. 하지만 클러스터 노드는 여러 노드가 클러스터링되어 만들어진 것이므로 좌표를 계산해서 지정해야 합니다.
positioningStrategy
프로퍼티를 사용해 클러스터 노드의 좌표를 정하는 전략을 지정할 수 있습니다. 여러 노드가 하나의 클러스터로 클러스터링될 때마다 -getPosition:
메서드가 호출되며, 파라미터로 전달되는 NMCCluster
의 children
을 이용해 자식 노드를 순회할 수 있습니다. 좌표 등 자식 노드의 정보를 이용해 NMGWebMercatorCoord
좌표를 반환하면 반환한 값이 NMCCluster
의 좌표가 됩니다.
다음은 무조건 첫 번째 자식 노드의 좌표를 클러스터 노드의 좌표로 삼는 전략을 지정하는 예제입니다.
extension ClusteringViewController: NMCPositioningStrategy {
func getPosition(_ cluster: NMCCluster) -> NMGWebMercatorCoord {
return cluster.children.first!.coord
}
}
complexBuilder.positioningStrategy = self
Swift
extension ClusteringViewController: NMCPositioningStrategy {
func getPosition(_ cluster: NMCCluster) -> NMGWebMercatorCoord {
return cluster.children.first!.coord
}
}
complexBuilder.positioningStrategy = self
Objective-C
@interface ClusteringViewController () <NMCPositioningStrategy>
@end
@implementation MarkerClusteringStrategyViewController
- (NMGWebMercatorCoord * _Nonnull)getPosition:(NMCCluster * _Nonnull)cluster {
return cluster.children.firstObject.coord;
}
@end
complexBuilder.positioningStrategy = self;
마커 관리
클러스터러는 화면에 포함되는 노드만을 마커로 변환해 노출합니다. 때문에 화면에 동시에 나타나는 마커의 개수를 효과적으로 제한할 수 있으며 재사용에도 유리합니다. 하지만 마커의 속성 중 변하지 않는 것이 있더라도 매번 덮어씌워줘야 하는 비효율이 발생합니다.
markerManager
프로퍼티를 사용해 마커를 관리하는 객체를 지정하면 마커를 직접 관리하여 비효율을 최소화할 수 있습니다. 마커 객체가 필요해지면 -retainMarker:
메서드가, 불필요해지면 -releaseMarker::
메서드가 호출되므로 NMCClusterMarkerUpdater
와 NMCLeafMarkerUpdater
에서 관리하지 않는 속성은 여기서 공통 관리할 수 있습니다.
또한 기본 NMCMarkerManager
구현체인 NMCDefaultMarkerManager
를 상속하여 -createMarker
메서드만 오버라이드할 수도 있습니다. 이렇게 하면 효율적인 마커 풀 및 객체 재사용 기능을 그대로 활용하면서 공통 속성만을 간편하게 관리할 수 있습니다.
다음은 NMCDefaultMarkerManager
를 상속하여 새로 생성되는 마커의 서브캡션 관련 속성만을 지정하는 예제입니다.
class MarkerManager: NMCDefaultMarkerManager {
override func createMarker() -> NMFMarker {
let marker = super.createMarker()
marker.subCaptionTextSize = 10
marker.subCaptionColor = UIColor.white
marker.subCaptionHaloColor = UIColor.clear
return marker
}
}
complexBuilder.markerManager = MarkerManager()
Swift
class MarkerManager: NMCDefaultMarkerManager {
override func createMarker() -> NMFMarker {
let marker = super.createMarker()
marker.subCaptionTextSize = 10
marker.subCaptionColor = UIColor.white
marker.subCaptionHaloColor = UIColor.clear
return marker
}
}
complexBuilder.markerManager = MarkerManager()
Objective-C
@interface MarkerManager : NMCDefaultMarkerManager
@end
@implementation MarkerManager
- (NMFMarker *)createMarker {
NMFMarker *marker = [super createMarker];
marker.subCaptionTextSize = 10;
marker.subCaptionColor = UIColor.whiteColor;
marker.subCaptionHaloColor = UIColor.clearColor;
return marker;
}
@end
complexBuilder.markerManager = [[MarkerManager alloc] init];