마커 클러스터링

한 화면에 대량의 마커가 노출되면 성능이 저하될 뿐만 아니라 여러 마커가 겹쳐 나타나므로 시인성이 떨어집니다. 마커의 겹침 처리 기능을 사용하면 시인성을 일부 향상시킬 수 있으나 겹침 처리로 인해 가려진 마커의 정보를 알 수 없으며, 성능도 여전히 저하됩니다. 마커 클러스터링 기능을 이용하면 카메라의 줌 레벨에 따라 근접한 마커를 클러스터링해 성능과 시인성을 모두 향상시킬 수 있습니다.

기본적인 사용법

키 정의

마커 클러스터링 기능을 이용하려면 먼저 데이터의 키를 의미하는 NMCClusteringKey 인터페이스를 구현한 클래스를 정의해야 합니다. NMCClusteringKey 인터페이스는 데이터의 좌표뿐만 아니라 두 데이터가 동일한지를 정의합니다. 따라서 이 인터페이스를 구현하는 클래스는 isEqual:, hashcopyWithZone:도 구현하는 것이 권장됩니다.

다음은 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;

클러스터링 1 클러스터링 2 클러스터링 3 클러스터링 4

옵션 사용

NMCBuilder의 프로퍼티를 사용해 클러스터링할 거리, 최소/최대 줌 레벨, 애니메이션 여부, 클러스터/단말 마커 커스터마이징 등 다양한 클러스터링 옵션을 지정할 수 있습니다.

클러스터링 거리

screenDistance 프로퍼티를 사용해 클러스터링할 기준 거리를 pt 단위로 지정할 수 있습니다. 클러스터에 추가된 두 데이터의 화면상 거리가 기준 거리보다 가깝다면 클러스터링되어 하나의 마커로 나타납니다.

다음은 클러스터러의 기준 거리를 20DP로 지정하는 예제입니다.

builder.screenDistance = 20

Swift

builder.screenDistance = 20

Objective-C

builder.screenDistance = 20;

클러스터링 거리 1 클러스터링 거리 2

최소 및 최대 줌 레벨

minZoommaxZoom 프로퍼티를 사용해 클러스터링할 최소 및 최대 줌 레벨을 제한할 수 있습니다. 카메라의 줌 레벨이 최소 줌 레벨보다 낮거나 최대 줌 레벨보다 높다면 두 데이터가 화면상 기준 거리보다 가깝더라도 클러스터링되지 않습니다. 예를 들어, 클러스터링할 최소 줌 레벨이 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;

마커 커스터마이징

clusterMarkerUpdaterleafMarkerUpdater 프로퍼티를 사용해 클러스터와 단말(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];

마커 커스터마이징 1 마커 커스터마이징 2

복잡한 전략 사용

NMCBuilder 대신 NMCComplexBuilder를 사용하면 화면상 거리가 아닌 다른 기준으로 데이터를 클러스터링하는 등 복잡한 전략과 기능을 사용할 수 있습니다.

노드

네이버 지도 SDK의 마커 클러스터링 기능은 hierarchical agglomerative clustering 알고리즘에 기반합니다. 즉, 클러스터러의 최대 줌 레벨부터 인접 데이터를 클러스터링하여, 최소 줌 레벨까지 바텀-업 방식으로 트리를 구성하며 클러스터링합니다. 따라서 트리에서 자식이 있는 노드는 클러스터, 단말 노드는 데이터가 됩니다.

각 노드는 NMCNode 클래스로 표현되며, 클러스터와 단말 노드는 각각 NMCNode의 하위 클래스인 NMCClusterNMCLeaf로 표현됩니다.

노드를 이용해 공통적으로 좌표, 태그, 자식 노드의 개수, 노출되어야 하는 최소/최대 줌 레벨 속성을 가져올 수 있으며, 이에 더해 클러스터 노드는 자식 노드의 목록에, 단말 노드는 데이터의 키에 접근할 수 있습니다.

기준 거리 전략 및 거리 측정 전략

thresholdStrategydistanceStrategy 프로퍼티를 사용해 특정 줌 레벨에서 두 노드를 클러스터링할지에 대한 전략을 지정할 수 있습니다.

thresholdStrategy로 지정하는 NMCThresholdStrategy는 특정 줌 레벨에서 두 노드를 클러스터링할 기준 거리를 구하는 전략에 대한 인터페이스이며, distanceStrategy로 지정하는 NMCDistanceStrategy는 특정 줌 레벨에서 두 노드간의 거리를 측정하는 전략에 대한 인터페이스입니다.

즉, 두 노드 node1node2가 있을 때, zoom 줌 레벨에서 NMCDistanceStrategy.getDistance:Node1:Node2:NMCThresholdStrategy.getThreshold:보다 작거나 같다면 node1node2는 클러스터링됩니다. 이를 이용하면 줌 레벨에 따라 거리 기준을 달리하거나 화면상 거리와 무관하게 클러스터링 전략을 지정할 수 있습니다.

다음은 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 프로퍼티를 사용해 클러스터링할 최대 화면 거리를 지정할 수 있습니다. 즉, 두 노드 node1node2가 있을 때, zoom 줌 레벨에서 두 노드의 화면상 거리가 이 값보다 크다면, NMCDistanceStrategy.getDistance:Node1:Node2:NMCThresholdStrategy.getThreshold:보다 작거나 같더라도 node1node2는 클러스터링되지 않습니다.

따라서 NMCThresholdStrategyNMCDistanceStrategy를 지정했다면 전략을 고려해 최적의 거리를 지정해야 합니다. 값을 너무 작게 지정하면 클러스터링되어야 하는 노드가 클러스터링되지 않을 수 있고, 너무 크게 지정하면 성능이 저하됩니다.

다음은 최대 화면 거리 제한을 100DP로 변경하는 예제입니다.

complexBuilder.maxScreenDistance = 100

Swift

complexBuilder.maxScreenDistance = 100

Objective-C

complexBuilder.maxScreenDistance = 100

태그 병합 전략

단말 노드에는 NMCClusterer.add::로 지정한 키와 태그가 유지됩니다. 하지만 클러스터 노드는 여러 노드가 클러스터링되어 만들어진 것이므로 키를 가질 수 없으며, 태그 역시 자동으로 생성할 수 없습니다.

대신 tagMergeStrategy 프로퍼티를 사용해 자식 노드들의 태그를 병합해 클러스터 노드의 태그로 삼는 전략을 지정할 수 있습니다. 여러 노드가 하나의 클러스터로 클러스터링될 때마다 -mergeTag: 메서드가 호출되며, 파라미터로 전달되는 NMCClusterchildren을 이용해 자식 노드를 순회할 수 있습니다. 자식 노드의 태그를 병합해 반환하면 반환한 값이 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: 메서드가 호출되며, 파라미터로 전달되는 NMCClusterchildren을 이용해 자식 노드를 순회할 수 있습니다. 좌표 등 자식 노드의 정보를 이용해 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:: 메서드가 호출되므로 NMCClusterMarkerUpdaterNMCLeafMarkerUpdater에서 관리하지 않는 속성은 여기서 공통 관리할 수 있습니다.

또한 기본 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];

results matching ""

    No results matching ""