Created
February 10, 2018 19:18
-
-
Save JKalash/3954966bdb1f362c443b5a2ca97f25f4 to your computer and use it in GitHub Desktop.
JKHeatMapEngine
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// JKHeatMapEngine.swift | |
// JKTinder | |
// Created by Joseph Kalash on 2/10/18. | |
// Copyright © 2018 Joseph Kalash. All rights reserved. | |
import Alamofire | |
import DeepDiff | |
// Note: This manager only deals with profile IDs and does not store any other info. Once user selectes a node in the | |
// cluster data, the array of profileIDs are passed to the next VC where the profile details are fetched and displayed to the user | |
struct DistanceEstimate : Hashable { | |
var distance : JKTrilateration.Circle //The central point estimate | |
var weight : Double //Estimated error on the distance | |
// JKTrilateration.Circle already conforms to Hashable | |
var hashValue: Int { | |
return distance.hashValue | |
} | |
// JKTrilateration.Circle already conforms to Hashable thus conforms to Equatable as well | |
static func == (lhs: DistanceEstimate, rhs: DistanceEstimate) -> Bool { | |
return lhs.distance == rhs.distance && lhs.weight == rhs.weight | |
} | |
} | |
struct LocationEstimate : Hashable { | |
var estimatedDistances : Set<DistanceEstimate> //The set of estimated distances | |
var estimatedLocation : Vector2? | |
// JKTrilateration.Circle already conforms to Hashable | |
var hashValue: Int { | |
return estimatedDistances.hashValue | |
} | |
// JKTrilateration.Circle already conforms to Hashable thus conforms to Equatable as well | |
static func == (lhs: LocationEstimate, rhs: LocationEstimate) -> Bool { | |
return lhs.estimatedDistances == rhs.estimatedDistances && lhs.estimatedLocation == rhs.estimatedLocation | |
} | |
mutating func locate() { | |
var knownPos : [[Double]] = [] | |
var dist : [Double] = [] | |
let distances : [JKTrilateration.Circle] = self.estimatedDistances.flatMap({ $0.distance }) | |
for d in distances { | |
knownPos.append([d.center.x, d.center.y]) | |
dist.append(d.radius) | |
} | |
//Convert distance to km | |
dist = dist.map({ $0 / 1000 }) | |
let weights = self.estimatedDistances.flatMap({ $0.weight }) | |
let optimizer = NonLinearLeastSquareOptimizer(knownPos: knownPos, dist: &dist, weights: weights) | |
self.estimatedLocation = optimizer.getLocation() | |
} | |
} | |
//Continuously run : | |
//1. Fetch random point in the boundary defined by circle at center of mapView / radius zoomlevel | |
//2. Request nearby profiles and store all distance estimates in userLocations data structure | |
//3. Run Levmarq least square optimization on all profiles that have at least 3 entry points | |
//4. Refresh locatedUsers with new/updated estimated locations | |
//5. Refresh MapView with the new points | |
class JKHeatMapEngine { | |
// Stores all points found for given profile IDs. Maps profile IDs to a set of estimates | |
// Each estimated point distance has a weight; the error on the computation (used to regularize in ML training) | |
// Also holds that have been located, alongside their location. Maps a profile id to a location. | |
internal var userLocations : [String : LocationEstimate] = [:] | |
public var runner : DataRequest? { | |
didSet { | |
oldValue?.cancel() //Cancel any existing sequest when being reassigned | |
} | |
} | |
//Repeatedly call the runner until either method called again or runner canceled by the controller | |
public func fetchUsers(mapCenter : Vector2, mapZoomRadius : Double, callback : @escaping (_ diff: Array<Change<[String : Vector2]>>) -> Void) { | |
let randomPoint = Utils.randomPointInCircle(JKTrilateration.Circle(center: mapCenter, radius: mapZoomRadius)) | |
runner = APIManager.current.fetchNearbyAt(loc: randomPoint, callback: { (json, error) in | |
guard let dict = json, let profiles = dict["profiles"] as? [NSDictionary] else { | |
self.fetchUsers(mapCenter: mapCenter, mapZoomRadius: mapZoomRadius, callback: callback) | |
return | |
} | |
//Store to compute DeepDiff | |
var profileIdsToLocate : [String] = [] | |
//A user either has a distance shared --> Append a point | |
for i in 0 ..< profiles.count { | |
if let profileId = profiles[i]["profileId"] as? String { | |
var estimate : DistanceEstimate? = nil | |
if let distance = profiles[i]["distance"] as? Double { | |
estimate = DistanceEstimate(distance: JKTrilateration.Circle(center: randomPoint, radius: distance), weight: 5.0) | |
} | |
else { | |
//Find the approximate distance | |
if let (distance, w) = Utils.estimateDistance(of: i, profiles: profiles) { | |
estimate = DistanceEstimate(distance: JKTrilateration.Circle(center: randomPoint, radius: distance), weight: w) | |
} | |
} | |
//If we somehow found no estimate, skip | |
if estimate == nil { continue } | |
//Check if user location already a record | |
if self.userLocations[profileId] != nil { | |
self.userLocations[profileId]!.estimatedDistances.insert(estimate!) //Appent to existing set | |
//Locates profiles whose distance found is anywhere between 3rd and 6th | |
if self.userLocations[profileId]!.estimatedDistances.count == 3 { | |
profileIdsToLocate.append(profileId) | |
} | |
} | |
else { | |
//Create initial set | |
self.userLocations[profileId] = LocationEstimate(estimatedDistances: [estimate!], estimatedLocation: nil) | |
} | |
} | |
} | |
//Compute distances | |
for id in profileIdsToLocate { | |
self.userLocations[id]!.locate() | |
} | |
//Repeat as long as we were able to locate users | |
if profileIdsToLocate.count > 0 { | |
self.fetchUsers(mapCenter: mapCenter, mapZoomRadius: mapZoomRadius, callback: callback) | |
} | |
}, applyFilterParams: false) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment