Skip to content

Instantly share code, notes, and snippets.

@JKalash
Created February 10, 2018 19:18
Show Gist options
  • Save JKalash/3954966bdb1f362c443b5a2ca97f25f4 to your computer and use it in GitHub Desktop.
Save JKalash/3954966bdb1f362c443b5a2ca97f25f4 to your computer and use it in GitHub Desktop.
JKHeatMapEngine
// 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