diff --git a/Project17-C-Map/InteractiveClusteringMap.xcodeproj/project.pbxproj b/Project17-C-Map/InteractiveClusteringMap.xcodeproj/project.pbxproj index 38aeb38..d0faf96 100644 --- a/Project17-C-Map/InteractiveClusteringMap.xcodeproj/project.pbxproj +++ b/Project17-C-Map/InteractiveClusteringMap.xcodeproj/project.pbxproj @@ -22,16 +22,13 @@ 372216C12569325600231245 /* DataManagable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372216C02569325600231245 /* DataManagable.swift */; }; 372216CD256943D300231245 /* POI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372216CC256943D300231245 /* POI.swift */; }; 37279B03256EBC5200EA689A /* KMeansTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37279B02256EBC5200EA689A /* KMeansTests.swift */; }; - 37279B07256EBC6300EA689A /* BoundingBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A12E22256CD4DB007BDF3E /* BoundingBox.swift */; }; - 37279B0B256EBC6E00EA689A /* KMeansClustering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DC4C5256E1EC60044DA6C /* KMeansClustering.swift */; }; - 37311088256F641400350D55 /* MockPOIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37311087256F641400350D55 /* MockPOIService.swift */; }; + 37279B0B256EBC6E00EA689A /* KMeans.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DC4C5256E1EC60044DA6C /* KMeans.swift */; }; 3731108C256F641F00350D55 /* POIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3753DCEF256B4C5B006D26A9 /* POIService.swift */; }; 37352CD125695B8A00CAFDFD /* restuarant-list-for-test.json in Resources */ = {isa = PBXBuildFile; fileRef = 37352CD025695B8A00CAFDFD /* restuarant-list-for-test.json */; }; 37352CDB25695E7400CAFDFD /* JSONReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37352CDA25695E7400CAFDFD /* JSONReader.swift */; }; 37352CE025695EE000CAFDFD /* Place.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37352CDF25695EE000CAFDFD /* Place.swift */; }; 3753DCF0256B4C5B006D26A9 /* POIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3753DCEF256B4C5B006D26A9 /* POIService.swift */; }; - 376E43BA256F9811002D3EE9 /* MockPOIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37311087256F641400350D55 /* MockPOIService.swift */; }; - 379DC4C6256E1EC60044DA6C /* KMeansClustering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DC4C5256E1EC60044DA6C /* KMeansClustering.swift */; }; + 379DC4C6256E1EC60044DA6C /* KMeans.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DC4C5256E1EC60044DA6C /* KMeans.swift */; }; 37EB73292562592700AC44D6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EB73282562592700AC44D6 /* AppDelegate.swift */; }; 37EB732B2562592700AC44D6 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EB732A2562592700AC44D6 /* SceneDelegate.swift */; }; 37EB732D2562592700AC44D6 /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EB732C2562592700AC44D6 /* MapViewController.swift */; }; @@ -52,10 +49,18 @@ 46B444582568F095001B79D4 /* InteractiveMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46B444572568F095001B79D4 /* InteractiveMapView.swift */; }; C9FF187A077822AA8BD00410 /* Pods_InteractiveClusteringMap.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A551109FA7E11823B10D084F /* Pods_InteractiveClusteringMap.framework */; }; FA81D67825690F360023E123 /* CoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA81D67725690F360023E123 /* CoreDataStack.swift */; }; - FA84D560256F796700ECDD44 /* BoundingBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A12E22256CD4DB007BDF3E /* BoundingBox.swift */; }; FAC9FD05256E2107009FBB41 /* KCoefficient.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC9FD04256E2107009FBB41 /* KCoefficient.swift */; }; FAC9FD0A256E23B1009FBB41 /* KDefineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC9FD09256E23B1009FBB41 /* KDefineTests.swift */; }; FAC9FD0E256E2495009FBB41 /* KCoefficient.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC9FD04256E2107009FBB41 /* KCoefficient.swift */; }; + FCE422B02570194B0017416F /* Coordinate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE422AF2570194B0017416F /* Coordinate.swift */; }; + FCE422B8257019650017416F /* Cluster.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE422B7257019650017416F /* Cluster.swift */; }; + FCE422BD25701A200017416F /* Quadrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE422BC25701A200017416F /* Quadrant.swift */; }; + FCE422C225701A390017416F /* Degree.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE422C125701A390017416F /* Degree.swift */; }; + FCE422C725701B8A0017416F /* KMeansCentroidsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE422C625701B8A0017416F /* KMeansCentroidsTest.swift */; }; + FCE422CE25701C760017416F /* Coordinate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE422AF2570194B0017416F /* Coordinate.swift */; }; + FCE422D225701C790017416F /* Cluster.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE422B7257019650017416F /* Cluster.swift */; }; + FCE422D625701CBD0017416F /* Quadrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE422BC25701A200017416F /* Quadrant.swift */; }; + FCE422DA25701CC00017416F /* Degree.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE422C125701A390017416F /* Degree.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -84,12 +89,11 @@ 372216C02569325600231245 /* DataManagable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataManagable.swift; sourceTree = ""; }; 372216CC256943D300231245 /* POI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POI.swift; sourceTree = ""; }; 37279B02256EBC5200EA689A /* KMeansTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KMeansTests.swift; sourceTree = ""; }; - 37311087256F641400350D55 /* MockPOIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPOIService.swift; sourceTree = ""; }; 37352CD025695B8A00CAFDFD /* restuarant-list-for-test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "restuarant-list-for-test.json"; sourceTree = ""; }; 37352CDA25695E7400CAFDFD /* JSONReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONReader.swift; sourceTree = ""; }; 37352CDF25695EE000CAFDFD /* Place.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Place.swift; sourceTree = ""; }; 3753DCEF256B4C5B006D26A9 /* POIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POIService.swift; sourceTree = ""; }; - 379DC4C5256E1EC60044DA6C /* KMeansClustering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KMeansClustering.swift; sourceTree = ""; }; + 379DC4C5256E1EC60044DA6C /* KMeans.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KMeans.swift; sourceTree = ""; }; 37EB73252562592700AC44D6 /* InteractiveClusteringMap.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InteractiveClusteringMap.app; sourceTree = BUILT_PRODUCTS_DIR; }; 37EB73282562592700AC44D6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37EB732A2562592700AC44D6 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -116,6 +120,11 @@ FA81D67725690F360023E123 /* CoreDataStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStack.swift; sourceTree = ""; }; FAC9FD04256E2107009FBB41 /* KCoefficient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KCoefficient.swift; sourceTree = ""; }; FAC9FD09256E23B1009FBB41 /* KDefineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KDefineTests.swift; sourceTree = ""; }; + FCE422AF2570194B0017416F /* Coordinate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinate.swift; sourceTree = ""; }; + FCE422B7257019650017416F /* Cluster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cluster.swift; sourceTree = ""; }; + FCE422BC25701A200017416F /* Quadrant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quadrant.swift; sourceTree = ""; }; + FCE422C125701A390017416F /* Degree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Degree.swift; sourceTree = ""; }; + FCE422C625701B8A0017416F /* KMeansCentroidsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KMeansCentroidsTest.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -149,7 +158,8 @@ children = ( 37352CDF25695EE000CAFDFD /* Place.swift */, 372216CC256943D300231245 /* POI.swift */, - 379DC4C5256E1EC60044DA6C /* KMeansClustering.swift */, + FCE422AF2570194B0017416F /* Coordinate.swift */, + FCE422B7257019650017416F /* Cluster.swift */, ); path = VO; sourceTree = ""; @@ -196,13 +206,13 @@ 37EB73272562592700AC44D6 /* InteractiveClusteringMap */ = { isa = PBXGroup; children = ( + FCE422AD257018E30017416F /* Clustering */, 37352CD925695E4D00CAFDFD /* Util */, 37352CCF25695AA500CAFDFD /* Resource */, 372216CB256943C800231245 /* VO */, FA81D66E2568FEC50023E123 /* CoreData */, 37EB73282562592700AC44D6 /* AppDelegate.swift */, 37EB732A2562592700AC44D6 /* SceneDelegate.swift */, - FAC9FD04256E2107009FBB41 /* KCoefficient.swift */, 37EB732C2562592700AC44D6 /* MapViewController.swift */, 4623BD1225694A1400940B12 /* MapController.swift */, 46153145256BDDD700BAE674 /* QuadTree */, @@ -222,10 +232,10 @@ 46A12E3E256CEDD3007BDF3E /* QuadTreeTests */, 37EB73422562592900AC44D6 /* InteractiveClusteringMapTests.swift */, 37279B02256EBC5200EA689A /* KMeansTests.swift */, - 37311087256F641400350D55 /* MockPOIService.swift */, 3713D3DB25694C3E00E703EC /* CoreDataStackTests.swift */, FAC9FD09256E23B1009FBB41 /* KDefineTests.swift */, 37EB73442562592900AC44D6 /* Info.plist */, + FCE422C625701B8A0017416F /* KMeansCentroidsTest.swift */, ); path = InteractiveClusteringMapTests; sourceTree = ""; @@ -294,6 +304,25 @@ path = POI; sourceTree = ""; }; + FCE422AD257018E30017416F /* Clustering */ = { + isa = PBXGroup; + children = ( + FCE422AE257018EA0017416F /* KMeans */, + ); + path = Clustering; + sourceTree = ""; + }; + FCE422AE257018EA0017416F /* KMeans */ = { + isa = PBXGroup; + children = ( + FAC9FD04256E2107009FBB41 /* KCoefficient.swift */, + 379DC4C5256E1EC60044DA6C /* KMeans.swift */, + FCE422BC25701A200017416F /* Quadrant.swift */, + FCE422C125701A390017416F /* Degree.swift */, + ); + path = KMeans; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -495,18 +524,21 @@ 37EB73292562592700AC44D6 /* AppDelegate.swift in Sources */, 4623BD1325694A1400940B12 /* MapController.swift in Sources */, 372216CD256943D300231245 /* POI.swift in Sources */, - 379DC4C6256E1EC60044DA6C /* KMeansClustering.swift in Sources */, + 379DC4C6256E1EC60044DA6C /* KMeans.swift in Sources */, 3753DCF0256B4C5B006D26A9 /* POIService.swift in Sources */, FA81D67825690F360023E123 /* CoreDataStack.swift in Sources */, + FCE422B02570194B0017416F /* Coordinate.swift in Sources */, 37EB732D2562592700AC44D6 /* MapViewController.swift in Sources */, 37EB73332562592700AC44D6 /* Model.xcdatamodeld in Sources */, + FCE422B8257019650017416F /* Cluster.swift in Sources */, 192D34AD256E355500861703 /* QuadTree.swift in Sources */, 46A12E1B256CD099007BDF3E /* Extension.swift in Sources */, + FCE422BD25701A200017416F /* Quadrant.swift in Sources */, 37352CDB25695E7400CAFDFD /* JSONReader.swift in Sources */, 46A12E23256CD4DB007BDF3E /* BoundingBox.swift in Sources */, FAC9FD05256E2107009FBB41 /* KCoefficient.swift in Sources */, 37EB73292562592700AC44D6 /* AppDelegate.swift in Sources */, - 376E43BA256F9811002D3EE9 /* MockPOIService.swift in Sources */, + FCE422C225701A390017416F /* Degree.swift in Sources */, 372216C12569325600231245 /* DataManagable.swift in Sources */, 37352CE025695EE000CAFDFD /* Place.swift in Sources */, 37EB732B2562592700AC44D6 /* SceneDelegate.swift in Sources */, @@ -517,24 +549,27 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + FCE422D225701C790017416F /* Cluster.swift in Sources */, FAC9FD0E256E2495009FBB41 /* KCoefficient.swift in Sources */, 192D34BE256E36B900861703 /* Extension.swift in Sources */, 3713D3E425694CD300E703EC /* POI.swift in Sources */, 46A12E2B256CED1B007BDF3E /* POIEntity+CoreDataProperties.swift in Sources */, - 37279B0B256EBC6E00EA689A /* KMeansClustering.swift in Sources */, + 37279B0B256EBC6E00EA689A /* KMeans.swift in Sources */, + FCE422DA25701CC00017416F /* Degree.swift in Sources */, + FCE422CE25701C760017416F /* Coordinate.swift in Sources */, 3713D3E025694C6B00E703EC /* CoreDataStack.swift in Sources */, 3731108C256F641F00350D55 /* POIService.swift in Sources */, + FCE422D625701CBD0017416F /* Quadrant.swift in Sources */, 46A12E40256CEDF2007BDF3E /* BoundingBoxTests.swift in Sources */, 192D34B6256E36A000861703 /* QuadTree.swift in Sources */, 46A12E2F256CED3A007BDF3E /* AppDelegate.swift in Sources */, 46A12E27256CED18007BDF3E /* POIEntity+CoreDataClass.swift in Sources */, 37EB73432562592900AC44D6 /* InteractiveClusteringMapTests.swift in Sources */, - 37311088256F641400350D55 /* MockPOIService.swift in Sources */, 46A12E3A256CED57007BDF3E /* JSONReader.swift in Sources */, - 192D34BA256E36A200861703 /* BoundingBox.swift in Sources */, 192D34B2256E368D00861703 /* QuadTreeTests.swift in Sources */, 199DDFF6256B9C8E0065B94E /* Place.swift in Sources */, + FCE422C725701B8A0017416F /* KMeansCentroidsTest.swift in Sources */, 3713D3FA25694D9C00E703EC /* DataManagable.swift in Sources */, 37279B03256EBC5200EA689A /* KMeansTests.swift in Sources */, FAC9FD0A256E23B1009FBB41 /* KDefineTests.swift in Sources */, diff --git a/Project17-C-Map/InteractiveClusteringMap/Clustering/KMeans/Degree.swift b/Project17-C-Map/InteractiveClusteringMap/Clustering/KMeans/Degree.swift new file mode 100644 index 0000000..42f5470 --- /dev/null +++ b/Project17-C-Map/InteractiveClusteringMap/Clustering/KMeans/Degree.swift @@ -0,0 +1,14 @@ +// +// Degree.swift +// InteractiveClusteringMap +// +// Created by Seungeon Kim on 2020/11/27. +// + +import Foundation + +enum Degree { + static let right = 90.0 + static let straight = 180.0 + static let turn = 360.0 +} diff --git a/Project17-C-Map/InteractiveClusteringMap/KCoefficient.swift b/Project17-C-Map/InteractiveClusteringMap/Clustering/KMeans/KCoefficient.swift similarity index 100% rename from Project17-C-Map/InteractiveClusteringMap/KCoefficient.swift rename to Project17-C-Map/InteractiveClusteringMap/Clustering/KMeans/KCoefficient.swift diff --git a/Project17-C-Map/InteractiveClusteringMap/VO/KMeansClustering.swift b/Project17-C-Map/InteractiveClusteringMap/Clustering/KMeans/KMeans.swift similarity index 65% rename from Project17-C-Map/InteractiveClusteringMap/VO/KMeansClustering.swift rename to Project17-C-Map/InteractiveClusteringMap/Clustering/KMeans/KMeans.swift index 28fd86f..ca0ff7e 100644 --- a/Project17-C-Map/InteractiveClusteringMap/VO/KMeansClustering.swift +++ b/Project17-C-Map/InteractiveClusteringMap/Clustering/KMeans/KMeans.swift @@ -1,5 +1,5 @@ // -// KMeansClustering.swift +// KMeans.swift // InteractiveClusteringMap // // Created by Oh Donggeon on 2020/11/25. @@ -7,7 +7,7 @@ import Foundation -class KMeansClustering { +class KMeans { private let k: Int @@ -15,6 +15,59 @@ class KMeansClustering { self.k = k } + func randomCentroids(rangeOfLat lats: ClosedRange, + rangeOfLng lngs: ClosedRange) -> [Coordinate] { + var centroids: [Coordinate] = [] + for _ in 0.. [Coordinate] { + let center = (topLeft + bottomRight) / 2.0 + let boundary = Coordinate(x: bottomRight.x - center.x, y: topLeft.y - center.y) + let pivot = center.findTheta(vertex: Coordinate(x: bottomRight.x, y: topLeft.y)) + + let increase = Degree.turn / Double(k) + var angle = increase + var coords: [Coordinate] = [] + + for _ in 0.. pivot { + y = boundary.y + x = y / tan(radian) + } else { + x = boundary.x + y = tan(radian) * x + } + let distance = Coordinate(x: x, y: y) / 2 + let coord = quadrant.convertToCoordinate(center: center, distance: distance) + coords.append(coord) + } + angle += increase + } + + return coords + } + /// points에 대한 centroid를 계속 계산하여 centroid가 이동한 총 거리가 /// convergeDistance 이하로 움직일 경우 cluster를 반환합니다. /// diff --git a/Project17-C-Map/InteractiveClusteringMap/Clustering/KMeans/Quadrant.swift b/Project17-C-Map/InteractiveClusteringMap/Clustering/KMeans/Quadrant.swift new file mode 100644 index 0000000..eb27185 --- /dev/null +++ b/Project17-C-Map/InteractiveClusteringMap/Clustering/KMeans/Quadrant.swift @@ -0,0 +1,57 @@ +// +// Quadrant.swift +// InteractiveClusteringMap +// +// Created by Seungeon Kim on 2020/11/27. +// + +import Foundation + +enum Quadrant: Int { + case first = 0, second = 1, third = 2, fourth = 3 + + static func findQuadrant(angle: Double) -> Quadrant { + guard angle != .zero else { return Quadrant.first } + + let number = Int(angle / Degree.right) + if angle.truncatingRemainder(dividingBy: Degree.right) == 0 { + return Quadrant(rawValue: number - 1) ?? Quadrant.first + } else { + return Quadrant(rawValue: number) ?? Quadrant.first + } + } + + func theta(angle: Double) -> Double { + if self == .first || self == .third { + return Degree.right - angle + } else { + return angle + } + } + + func degree(center: Coordinate, boundary: Coordinate) -> Coordinate { + switch self { + case .first: + return Coordinate(x: center.x + boundary.x, y: center.y) + case .second: + return Coordinate(x: center.x, y: center.y - boundary.y) + case .third: + return Coordinate(x: center.x - boundary.x, y: center.y) + case .fourth: + return Coordinate(x: center.x, y: center.y + boundary.y) + } + } + + func convertToCoordinate(center: Coordinate, distance: Coordinate) -> Coordinate { + switch self { + case .first: + return Coordinate(x: center.x + distance.x, y: center.y + distance.y) + case .second: + return Coordinate(x: center.x + distance.x, y: center.y - distance.y) + case .third: + return Coordinate(x: center.x - distance.x, y: center.y - distance.y) + case .fourth: + return Coordinate(x: center.x - distance.x, y: center.y + distance.y) + } + } +} diff --git a/Project17-C-Map/InteractiveClusteringMap/QuadTree/BoundingBox.swift b/Project17-C-Map/InteractiveClusteringMap/QuadTree/BoundingBox.swift index e64faed..473d06c 100644 --- a/Project17-C-Map/InteractiveClusteringMap/QuadTree/BoundingBox.swift +++ b/Project17-C-Map/InteractiveClusteringMap/QuadTree/BoundingBox.swift @@ -39,74 +39,3 @@ struct BoundingBox { extension BoundingBox: Equatable {} -struct Coordinate { - - let x: Double - let y: Double - - func distanceTo(_ other: Coordinate) -> Double { - let powX = pow(self.x - other.x, 2.0) - let powY = pow(self.y - other.y, 2.0) - - return sqrt(powX + powY) - } - - func center(other: Coordinate) -> Coordinate { - let centerX: Double = (self.x + other.x) / 2.0 - let centerY: Double = (self.y + other.y) / 2.0 - - return Coordinate(x: centerX, y: centerY) - } - -} - -extension Coordinate: Hashable { - - static let zero = Coordinate(x: 0, y: 0) - - static func + (left: Coordinate, right: Coordinate) -> Coordinate { - let x = left.x + right.x - let y = left.y + right.y - - return Coordinate(x: x, y: y) - } - - static func - (left: Coordinate, right: Coordinate) -> Coordinate { - let x = left.x - right.x - let y = left.y - right.y - - return Coordinate(x: x, y: y) - } - - static func / (left: Coordinate, right: Double) -> Coordinate { - let x = left.x / right - let y = left.y / right - - return Coordinate(x: x, y: y) - } - - static func <= (left: Coordinate, right: Coordinate) -> Bool { - left.x <= right.x && left.y <= right.y - } - -} - -struct Cluster { - var coordinates: [Coordinate] - var center: Coordinate { - coordinates.reduce(.zero, +) / Double(coordinates.count) - } -} - -extension Cluster: Hashable { - - static func == (lhs: Cluster, rhs: Cluster) -> Bool { - return lhs.center == rhs.center && - lhs.coordinates == rhs.coordinates - } - - func hash(into hasher: inout Hasher) { - hasher.combine(coordinates) - } - -} diff --git a/Project17-C-Map/InteractiveClusteringMap/VO/Cluster.swift b/Project17-C-Map/InteractiveClusteringMap/VO/Cluster.swift new file mode 100644 index 0000000..d66f050 --- /dev/null +++ b/Project17-C-Map/InteractiveClusteringMap/VO/Cluster.swift @@ -0,0 +1,28 @@ +// +// Cluster.swift +// InteractiveClusteringMap +// +// Created by Seungeon Kim on 2020/11/27. +// + +import Foundation + +struct Cluster { + var coordinates: [Coordinate] + var center: Coordinate { + coordinates.reduce(.zero, +) / Double(coordinates.count) + } +} + +extension Cluster: Hashable { + + static func == (lhs: Cluster, rhs: Cluster) -> Bool { + return lhs.center == rhs.center && + lhs.coordinates == rhs.coordinates + } + + func hash(into hasher: inout Hasher) { + hasher.combine(coordinates) + } + +} diff --git a/Project17-C-Map/InteractiveClusteringMap/VO/Coordinate.swift b/Project17-C-Map/InteractiveClusteringMap/VO/Coordinate.swift new file mode 100644 index 0000000..b7935d6 --- /dev/null +++ b/Project17-C-Map/InteractiveClusteringMap/VO/Coordinate.swift @@ -0,0 +1,66 @@ +// +// Coordinate.swift +// InteractiveClusteringMap +// +// Created by Seungeon Kim on 2020/11/27. +// + +import Foundation + +struct Coordinate { + + let x: Double + let y: Double + + func distanceTo(_ other: Coordinate) -> Double { + let powX = pow(self.x - other.x, 2.0) + let powY = pow(self.y - other.y, 2.0) + + return sqrt(powX + powY) + } + + func center(other: Coordinate) -> Coordinate { + let centerX: Double = (self.x + other.x) / 2.0 + let centerY: Double = (self.y + other.y) / 2.0 + + return Coordinate(x: centerX, y: centerY) + } + + func findTheta(vertex: Coordinate) -> Double { + let gradient = (vertex.y - self.y) / (vertex.x - self.x) + + return atan(gradient) * 180 / .pi + } + +} + +extension Coordinate: Hashable { + + static let zero = Coordinate(x: 0, y: 0) + + static func + (left: Coordinate, right: Coordinate) -> Coordinate { + let x = left.x + right.x + let y = left.y + right.y + + return Coordinate(x: x, y: y) + } + + static func - (left: Coordinate, right: Coordinate) -> Coordinate { + let x = left.x - right.x + let y = left.y - right.y + + return Coordinate(x: x, y: y) + } + + static func / (left: Coordinate, right: Double) -> Coordinate { + let x = left.x / right + let y = left.y / right + + return Coordinate(x: x, y: y) + } + + static func <= (left: Coordinate, right: Coordinate) -> Bool { + left.x <= right.x && left.y <= right.y + } + +} diff --git a/Project17-C-Map/InteractiveClusteringMapTests/KMeansCentroidsTest.swift b/Project17-C-Map/InteractiveClusteringMapTests/KMeansCentroidsTest.swift new file mode 100644 index 0000000..b0fce60 --- /dev/null +++ b/Project17-C-Map/InteractiveClusteringMapTests/KMeansCentroidsTest.swift @@ -0,0 +1,99 @@ +// +// KMeansCentroidsTest.swift +// InteractiveClusteringMapTests +// +// Created by Seungeon Kim on 2020/11/27. +// + +import XCTest + +class KMeansCentroidsTest: XCTestCase { + + func test_2split_coordinates_by_screen() throws { + let centroids = mockCentoidsByScreen(number: 2) + let expected = [ + Coordinate(x: 128.0000, y: 35.5000), + Coordinate(x: 128.0000, y: 40.5000) + ] + + validateCendroids(centroids: centroids, expected: expected) + } + + func test_4split_coordinates_by_screen() throws { + let centroids = mockCentoidsByScreen(number: 4) + let expected = [ + Coordinate(x: 130.0000, y: 38.0000), + Coordinate(x: 128.0000, y: 35.5000), + Coordinate(x: 126.0000, y: 38.0000), + Coordinate(x: 128.0000, y: 40.5000) + ] + + validateCendroids(centroids: centroids, expected: expected) + } + + func test_6split_coordinates_by_screen() throws { + let centroids = mockCentoidsByScreen(number: 6) + let expected = [ + Coordinate(x: 130.0000, y: 39.1547), + Coordinate(x: 130.0000, y: 36.8452), + Coordinate(x: 128.0000, y: 35.5000), + Coordinate(x: 126.0000, y: 36.8452), + Coordinate(x: 126.0000, y: 39.1547), + Coordinate(x: 128.0000, y: 40.5000) + ] + + validateCendroids(centroids: centroids, expected: expected) + } + + func test_7split_coordinates_by_screen() throws { + let centroids = mockCentoidsByScreen(number: 7) + let expected = [ + Coordinate(x: 130.0000, y: 39.5949), + Coordinate(x: 130.0000, y: 37.5435), + Coordinate(x: 129.2039, y: 35.5000), + Coordinate(x: 126.7960, y: 35.5000), + Coordinate(x: 126.0000, y: 37.5435), + Coordinate(x: 126.0000, y: 39.5949), + Coordinate(x: 128.0000, y: 40.5000) + ] + + validateCendroids(centroids: centroids, expected: expected) + } + + func test_coordinates_by_random() throws { + let cendroids = mockCentoidsByRandom(number: 1000) + + cendroids.forEach { cendroid in + validateCendtoid(cendroid: cendroid) + } + } + + private func validateCendroids(centroids: [Coordinate], expected: [Coordinate]) { + XCTAssertEqual(centroids.count, expected.count) + + let count = centroids.count + for i in 0.. [Coordinate] { + let kmm = KMeans(k: number) + let topLeft = Coordinate(x: 124.0, y: 43.0) + let bottomRight = Coordinate(x: 132.0, y: 33.0) + return kmm.screenCentroids(topLeft: topLeft, bottomRight: bottomRight) + } + + private func mockCentoidsByRandom(number: Int) -> [Coordinate] { + let kmm = KMeans(k: number) + + return kmm.randomCentroids(rangeOfLat: 33.0...43.0, rangeOfLng: 123.0...132.0) + } + +} diff --git a/Project17-C-Map/InteractiveClusteringMapTests/KMeansTests.swift b/Project17-C-Map/InteractiveClusteringMapTests/KMeansTests.swift index d0b708a..a19195c 100644 --- a/Project17-C-Map/InteractiveClusteringMapTests/KMeansTests.swift +++ b/Project17-C-Map/InteractiveClusteringMapTests/KMeansTests.swift @@ -66,7 +66,7 @@ class KMeansTests: XCTestCase { } func test_kmeans_clustering() throws { - let kmeans = KMeansClustering(k: 4) + let kmeans = KMeans(k: 4) let clusters = kmeans.trainCenters(points, initialCentroids: coordinates) let expectPoints: [[Coordinate]] = [[Coordinate(x: 1, y: 1), Coordinate(x: 2, y: 2)], [Coordinate(x: 8, y: 2), Coordinate(x: 9, y: 1)],