Skip to content

Commit 9fc0a6c

Browse files
feat: Merge feature/cluster into feature/Cluster-HeatMap-swift branch. (#523)
* refactor: Translate & Modernised `GMUCluster` & `GMUClusterItem` class of `Clustering` module. (#491) * refactor: Translate & Modernised `GMUClusterAlgorithm` protocol of `Clustering` module. (#492) * refactor: Translate & Modernise `GMUSimpleClusterAlgorithm` & `GMUStaticCluster` class of `Clustering` module. (#494) * refactor: Translate & Modernise `GMUSimpleClusterAlgorithm` class of `Clustering` module. * refactor: Translate & Modernise `GMUStaticCluster` class of `Clustering` module. * refactor: Update code comments. * refactor: Translate & Modernise `GMUGridBasedClusterAlgorithm`, `GMUStaticClusterTest`(UT) & `GMUTestClusterItem`(Test) class of `Clustering` module. (#495) * refactor: Translate & Modernise `GMUGridBasedClusterAlgorithm` & `GMUTestClusterItem`(Test) class of `Clustering` module. * refactor: Translate & Modernise `GMUStaticClusterTest`(UT) class of `Clustering` module. * refactor: Code comment. * refactor: Translate & Modernised `QuadTree` module. (#489) (#496) * refactor: Refactor & Modernised `QuadTree` module. * refactor: Added Doc descriptions for all the Quad Tree module. * refactor: Code Review comments fix. * refactor: Translate & Modernise `GMUWrappingDictionaryKey` class, `GMUWrappingDictionaryKeyTest`(UT) of `Clustering` module. (#497) * refactor: Translate & Modernise `Clustering` Algorithm module. (#498) * refactor: Translate & Modernise protocols for `Clustering` view module. (#500) * refactor: Translate & Modernise protocols for `Clustering` view module. * refactor `GMUClusterRenderer1` class name. * refactor: Translate & Modernise for `Clustering` view & test module. (#501) * refactor: Translate & Modernise protocols for `Clustering` view module. * refactor `GMUClusterRenderer1` class name. * refactor: Translate & Modernise for `Clustering` view & test module. * refactor: added `NSEC_PER_SEC`. * fix: Add final to `GMUDefaultClusterIconGenerator` class. * refactor: Translate & Modernise `GMUClusterManager` class for `Clustering` module. (#503) * refactor: Translate & Modernise protocols for `Clustering` view module. * refactor `GMUClusterRenderer1` class name. * refactor: Translate & Modernise for `Clustering` view & test module. * refactor: added `NSEC_PER_SEC`. * fix: Add final to `GMUDefaultClusterIconGenerator` class. * refactor: Translate & Modernise `GMUClusterManager` class for `Clustering` module.
1 parent 2891ad4 commit 9fc0a6c

33 files changed

+3451
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import GoogleMaps
16+
17+
/// TO-DO: Rename the class to `GMUGridBasedClusterAlgorithm` once the linking is done and remove the objective c class.
18+
/// A simple algorithm which devides the map into a grid where a cell has fixed dimension in screen space.
19+
///
20+
final class GMUGridBasedClusterAlgorithm1: GMUClusterAlgorithm1 {
21+
22+
// MARK: - Properties
23+
/// Internal array to store cluster items.
24+
private var clusterItems: [GMUClusterItem1]
25+
/// Grid cell dimension in pixels to keep clusters about 100 pixels apart on screen.
26+
private let gmuGridCellSizePoints: Float = 100.0
27+
28+
// MARK: - Initializers
29+
init() {
30+
clusterItems = []
31+
}
32+
33+
// MARK: - Methods
34+
/// Adds an array of items to the grid based cluster algorithm.
35+
///
36+
/// - Parameter items: Array of items conforming to `GMUClusterItem` protocol.
37+
func addItems(_ items: [GMUClusterItem1]) {
38+
clusterItems.append(contentsOf: items)
39+
}
40+
41+
/// Removes a specific item from the grid based cluster algorithm.
42+
///
43+
/// - Parameter item: The item conforming to `GMUClusterItem` protocol to be removed.
44+
func removeItem(_ item: GMUClusterItem1) {
45+
clusterItems.removeAll { $0 === item }
46+
}
47+
48+
/// Clears all items from the grid based cluster algorithm.
49+
func clearItems() {
50+
clusterItems.removeAll()
51+
}
52+
53+
/// Returns an array of clusters at the specified zoom level.
54+
///
55+
/// - Parameter zoom: The zoom level at which to compute clusters.
56+
/// - Returns: An array of clusters conforming to `GMUCluster` protocol.
57+
func clusters(atZoom zoom: Float) -> [GMUCluster1] {
58+
var clusters: [Int : GMUCluster1] = [:]
59+
60+
// Divide the whole map into a numCells x numCells grid and assign items to them.
61+
let numCells: Int = Int(ceil(256 * pow(2, zoom) / gmuGridCellSizePoints))
62+
63+
for item in clusterItems {
64+
let point: GMSMapPoint = GMSProject(item.position)
65+
/// point.x is in [-1, 1] range
66+
let col: Int = Int(Double(numCells) * (1.0 + point.x) / 2.0)
67+
/// point.y is in [-1, 1] range
68+
let row: Int = Int(Double(numCells) * (1.0 + point.y) / 2.0)
69+
let index: Int = numCells * row + col
70+
var cluster: GMUStaticCluster1? = clusters[index] as? GMUStaticCluster1
71+
if cluster == nil {
72+
// Normalize cluster's centroid to center of the cell.
73+
let newNumCells = Double(numCells - 1)
74+
let xCoordinate = Double((Double(col) + 0.5) * 2.0 / newNumCells)
75+
let yCoordinate = Double((Double(row) + 0.5) * 2.0 / newNumCells)
76+
let mapPoint: GMSMapPoint = GMSMapPoint(x: xCoordinate, y: yCoordinate)
77+
let position: CLLocationCoordinate2D = GMSUnproject(mapPoint)
78+
cluster = GMUStaticCluster1(position: position)
79+
clusters[index] = cluster
80+
}
81+
cluster?.addItem(item)
82+
}
83+
return Array(clusters.values)
84+
}
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import GoogleMaps
16+
17+
/// TO-DO: Rename the class to `GMUNonHierarchicalDistanceBasedAlgorithm` once the linking is done and remove the objective c class.
18+
/// A simple clustering algorithm with O(nlog n) performance.
19+
/// Resulting clusters are not hierarchical.
20+
/// High level algorithm:
21+
/// 1. Iterate over items in the order they were added (candidate clusters).
22+
/// 2. Create a cluster with the center of the item.
23+
/// 3. Add all items that are within a certain distance to the cluster.
24+
/// 4. Move any items out of an existing cluster if they are closer to another cluster.
25+
/// 5. Remove those items from the list of candidate clusters.
26+
/// Clusters have the center of the first element (not the centroid of the items within it).
27+
///
28+
final class GMUNonHierarchicalDistanceBasedAlgorithm1: GMUClusterAlgorithm1 {
29+
30+
// MARK: - Properties
31+
/// MapPoint is in a [-1,1]x[-1,1] space.
32+
private let mapPointWidth: Double = 2.0
33+
private var clusterItems: [GMUClusterItem1]
34+
private var quadTree: GQTPointQuadTree1
35+
private var clusterDistancePoints: Int
36+
37+
// MARK: - Initializers
38+
/// Initializes this GMUNonHierarchicalDistanceBasedAlgorithm with clusterDistancePoints
39+
/// for the distance it uses to cluster items (default is 100).
40+
///
41+
/// - Parameter clusterDistancePoints: Int
42+
init(clusterDistancePoints: Int) {
43+
self.clusterItems = []
44+
let bounds = GQTBounds1(minX: -1, minY: -1, maxX: 1, maxY: 1)
45+
self.quadTree = GQTPointQuadTree1(bounds: bounds)
46+
self.clusterDistancePoints = clusterDistancePoints
47+
}
48+
49+
/// Convenience init with default(100) `clusterDistancePoints`
50+
///
51+
convenience init() {
52+
self.init(clusterDistancePoints: 100)
53+
}
54+
55+
// MARK: - Protocol Method's
56+
/// Adds an array of items to the non-hierarchical distance based cluster algorithm and quad tree.
57+
///
58+
/// - Parameter items: Array of items conforming to `GMUClusterItem` protocol.
59+
func addItems(_ items: [GMUClusterItem1]) {
60+
clusterItems.append(contentsOf: items)
61+
for item in items {
62+
let quadItem = GMUClusterItemQuadItem1(clusterItem: item)
63+
_ = quadTree.add(item: quadItem)
64+
}
65+
}
66+
67+
/// Removes a specific item from the non-hierarchical distance based cluster algorithm and quad tree.
68+
///
69+
/// - Parameter item: The item conforming to `GMUClusterItem` protocol to be removed.
70+
func removeItem(_ item: GMUClusterItem1) {
71+
clusterItems.removeAll { $0 === item }
72+
let quadItem = GMUClusterItemQuadItem1(clusterItem: item)
73+
_ = quadTree.remove(item: quadItem)
74+
}
75+
76+
/// Clears all items from the non-hierarchical distance based cluster algorithm and quad tree.
77+
func clearItems() {
78+
clusterItems.removeAll()
79+
quadTree.clear()
80+
}
81+
82+
/// Returns an array of clusters at the specified zoom level.
83+
///
84+
/// - Parameter zoom: The zoom level at which to compute clusters.
85+
/// - Returns: An array of clusters conforming to `GMUCluster` protocol.
86+
func clusters(atZoom zoom: Float) -> [GMUCluster1] {
87+
var clusters: [GMUCluster1] = []
88+
var itemToClusterMap: [GMUWrappingDictionaryKey1: GMUCluster1] = [:]
89+
var itemToClusterDistanceMap: [GMUWrappingDictionaryKey1: Double] = [:]
90+
var processedItems: [GMUClusterItem1] = []
91+
92+
for item in clusterItems {
93+
if processedItems.contains(where: { $0 === item }) {
94+
continue
95+
}
96+
97+
let cluster: GMUStaticCluster1 = GMUStaticCluster1(position: item.position)
98+
let point: GMSMapPoint = GMSProject(item.position)
99+
// Query items within a fixed point distance to form a cluster.
100+
let radius: Double = Double(clusterDistancePoints) * mapPointWidth / pow(2.0, Double(zoom) + 8.0)
101+
let bounds: GQTBounds1 = GQTBounds1(minX: point.x - radius, minY: point.y - radius, maxX: point.x + radius, maxY: point.y + radius)
102+
let nearbyItems: [GQTPointQuadTreeItem1] = quadTree.search(withBounds: bounds)
103+
104+
for quadItem in nearbyItems {
105+
guard let quadItem = quadItem as? GMUClusterItemQuadItem1 else {
106+
continue
107+
}
108+
let nearbyItem: GMUClusterItem1 = quadItem.gmuClusterItem
109+
processedItems.append(nearbyItem)
110+
let nearbyItemPoint: GMSMapPoint = GMSProject(nearbyItem.position)
111+
let key: GMUWrappingDictionaryKey1 = GMUWrappingDictionaryKey1(object: nearbyItem)
112+
let distanceSquared: Double = distanceSquared(between: point, and: nearbyItemPoint)
113+
if let existingDistance = itemToClusterDistanceMap[key] {
114+
if existingDistance < distanceSquared {
115+
/// Already belongs to a closer cluster.
116+
continue
117+
}
118+
if let existingCluster: GMUStaticCluster1 = itemToClusterMap[key] as? GMUStaticCluster1 {
119+
existingCluster.removeItem(nearbyItem)
120+
}
121+
}
122+
itemToClusterDistanceMap[key] = distanceSquared
123+
itemToClusterMap[key] = cluster
124+
cluster.addItem(nearbyItem)
125+
}
126+
clusters.append(cluster)
127+
}
128+
129+
assert(itemToClusterDistanceMap.count == clusterItems.count, "All items should be mapped to a distance")
130+
assert(itemToClusterMap.count == clusterItems.count, "All items should be mapped to a cluster")
131+
132+
#if DEBUG
133+
let totalCount = clusters.reduce(0) { $0 + $1.count }
134+
assert(clusterItems.count == totalCount, "All clusters combined should make up original item set")
135+
#endif
136+
137+
return clusters
138+
}
139+
140+
// MARK: - Private method
141+
/// Calculates squared distance between two GMSMapPoint's.
142+
///
143+
private func distanceSquared(between pointA: GMSMapPoint, and pointB: GMSMapPoint) -> Double {
144+
let deltaX: Double = pointA.x - pointB.x
145+
let deltaY: Double = pointA.y - pointB.y
146+
return deltaX * deltaX + deltaY * deltaY
147+
}
148+
}
149+
150+
// MARK: - `GMUClusterItemQuadItem`
151+
/// TO-DO: Rename the class to `GMUClusterItemQuadItem` once the linking is done and remove the objective c class.
152+
/// A class to represent the cluster Quad item and its projected point.
153+
///
154+
final private class GMUClusterItemQuadItem1: NSObject, GQTPointQuadTreeItem1 {
155+
156+
// MARK: - Properties
157+
let gmuClusterItem: GMUClusterItem1
158+
private var clusterItemPoint: GQTPoint1
159+
160+
// MARK: - Initializers
161+
init(clusterItem: GMUClusterItem1) {
162+
self.gmuClusterItem = clusterItem
163+
let point = GMSProject(clusterItem.position)
164+
self.clusterItemPoint = GQTPoint1(x: point.x, y: point.y)
165+
}
166+
167+
// MARK: - Method
168+
/// Method to retrieve the GQTPoint of the cluster item
169+
/// - Returns: `GQTPoint`
170+
func point() -> GQTPoint1 {
171+
return clusterItemPoint
172+
}
173+
174+
// MARK: - Override's
175+
/// Forward the hash value to the underlying object.
176+
override var hash: Int {
177+
// Use the `hash` property of the wrapped object to provide the hash value.
178+
return (gmuClusterItem as AnyObject).hash
179+
}
180+
181+
/// Forward the equality check to the underlying object.
182+
override func isEqual(_ object: Any?) -> Bool {
183+
// If both instances are the same, return true.
184+
if self === object as AnyObject {
185+
return true
186+
}
187+
188+
// Check if the object is of the same type, and then compare the underlying objects.
189+
guard let other = object as? GMUClusterItemQuadItem1 else {
190+
return false
191+
}
192+
return (self.gmuClusterItem as AnyObject).isEqual(other.gmuClusterItem)
193+
}
194+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
16+
// MARK: - GMUClusterAlgorithm Protocol
17+
/// TO-DO: Rename the class to `GMUClusterAlgorithm` once the linking is done and remove the objective c class.
18+
/// Generic protocol for arranging cluster items into groups.
19+
///
20+
protocol GMUClusterAlgorithm1 {
21+
22+
/// Adds an array of items to the cluster algorithm.
23+
///
24+
/// - Parameter items: An array of items conforming to `GMUClusterItem` protocol.
25+
func addItems(_ items: [GMUClusterItem1])
26+
27+
/// Removes a specific item from the cluster algorithm.
28+
///
29+
/// - Parameter item: The item conforming to `GMUClusterItem` protocol to be removed.
30+
func removeItem(_ item: GMUClusterItem1)
31+
32+
/// Removes an item.
33+
func clearItems()
34+
35+
/// Returns the set of clusters of the added items.
36+
///
37+
/// - Parameter zoom: The zoom level at which to compute clusters.
38+
/// - Returns: An array of clusters conforming to `GMUCluster` protocol.
39+
func clusters(atZoom zoom: Float) -> [GMUCluster1]
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
16+
/// TO-DO: Rename the class to `GMUSimpleClusterAlgorithm` once the linking is done and remove the objective c class.
17+
/// A basic clustering algorithm that groups a set of `GMUClusterItem` objects into a fixed number of clusters (default 10).
18+
/// Not for production: used for experimenting with new clustering algorithms only.
19+
///
20+
final class GMUSimpleClusterAlgorithm1: GMUClusterAlgorithm1 {
21+
22+
// MARK: - Properties
23+
/// Number of clusters to form.
24+
private let clusterCount: Int = 10
25+
/// Internal array to store cluster items.
26+
private var clusterItems: [GMUClusterItem1] = []
27+
28+
// MARK: - `GMUClusterAlgorithm` Methods
29+
/// Adds an array of items to the cluster algorithm.
30+
///
31+
/// - Parameter items: Array of items conforming to `GMUClusterItem` protocol.
32+
func addItems(_ items: [GMUClusterItem1]) {
33+
clusterItems.append(contentsOf: items)
34+
}
35+
36+
/// Removes a specific item from the cluster algorithm.
37+
///
38+
/// - Parameter item: The item conforming to `GMUClusterItem` protocol to be removed.
39+
func removeItem(_ item: GMUClusterItem1) {
40+
clusterItems.removeAll { $0 === item }
41+
}
42+
43+
/// Clears all items from the cluster algorithm.
44+
func clearItems() {
45+
clusterItems.removeAll()
46+
}
47+
48+
/// Returns an array of clusters at the specified zoom level.
49+
///
50+
/// - Parameter zoom: The zoom level at which to compute clusters.
51+
/// - Returns: An array of clusters conforming to `GMUCluster` protocol.
52+
func clusters(atZoom zoom: Float) -> [GMUCluster1] {
53+
var clusters: [GMUCluster1] = []
54+
55+
for i in 0..<clusterCount {
56+
if i >= clusterItems.count {
57+
break
58+
}
59+
let item: GMUClusterItem1 = clusterItems[i]
60+
clusters.append(GMUStaticCluster1(position: item.position))
61+
}
62+
63+
var clusterIndex: Int = 0
64+
for i in clusterCount..<clusterItems.count {
65+
let item = clusterItems[i]
66+
if let cluster = clusters[clusterIndex % clusterCount] as? GMUStaticCluster1 {
67+
cluster.addItem(item)
68+
}
69+
clusterIndex += 1
70+
}
71+
72+
return clusters
73+
}
74+
}

0 commit comments

Comments
 (0)