zenly - reverse geocoding
TRANSCRIPT
An architecture to handle reverse [email protected]
Zenly is a location app that lets you know what friends and family are
up to in real-time.
The app is focused on location, and therefore relies a lot on adresses.
Our users do millions of reverse geocoding's everyday and it has to
work :)
The operation to transform a latitude/longitude into an address.
Handled by CoreLocation : ReverseGeocoder one simple API.
public typealias CLGeocodeCompletionHandler = ([CLPlacemark]?, NSError?) -> Void
// reverse geocode requests public func reverseGeocodeLocation(location: CLLocation, completionHandler: CLGeocodeCompletionHandler)
May return multiple placemarks.
Reverse geocoding
In practice
Take one, use it, forget it….
Usually data comes from two providers NAVTEQ and TELEATLAS.
Other APIs available : Google (do not mix and match), open data…
Simply search for “Free reverse geocoding”.
CoreLocation hides everything.
When problems start
Some applications may want to display multiple adresses for multiple people, possibly moving … … and we bump into a limit.
Apple writes: “avoid more than one per minute and do it when UI needs it”. Problem of count and rate.
Empirically it can handle one every few seconds.
If too many: longer answer time and then errors.
“Boss, we need to pay Google”
Architecture leading to a problem
Being purely model driven leads to problems.
User AddressReverse Geocode
Location Change
UI
User AddressReverse Geocode
Location Change
User AddressReverse Geocode
Location Change
Thinking bias
Get UI components ready ASAP to avoid spinning wheel.
Optimize in a time based manner.
Optimize in the context of one user only.
Improvements
Global.
Cache.
Scheduling.
Priority and UI driven.
Track identifier.
Do only what is necessary!
Global operation
Avoid to use “reverse geocoding on the go”.
One available through a unique object (Singleton).
Handle both time based and position based consideration and optimization.
Make sure an operation can be redistributed through multiple places.
Only one object is responsible.
Data structure
func reverseGeocode<T:ReverseGeocodable>(geocodableObject:T?, identifier:String?, priority:ReverseGeocodingPriority, completionHandler: CLGeocodeCompletionHandler?) -> ReverseGeocodeResult
struct GeopositioningRunningInfo:CustomStringConvertible { var priority:ReverseGeocodingPriority = ReverseGeocodingPriority.Normal var context:Any? = nil var handlers:[Any] = [] var identifier:String? }
Cache
Based on coordinates….but how to encode this ?
Geohash: a division of the planet and many nice things (neighbors…). See Wikipedia !.
Must have a resolution: level. May depend on where you are!
Implementation through an NSCache where key is geohash and value the originally received CLPlacemarks.
Scheduled operations should be considered: multiple handlers.
Multiple people in the same place.
Geohash samples
//Paris: Zenly office var aPos:Pos = Pos(latitude:48.868117, longitude:2.355915) var geoHash:String = (ServiceReverseGeocoder.sharedGeocoder.geoHashString(aPos, level: 8))! XCTAssert(geoHash == "u09wj85t") geoHash = (ServiceReverseGeocoder.sharedGeocoder.geoHashString(aPos, level: 3))! XCTAssert(geoHash == "u09") geoHash = (ServiceReverseGeocoder.sharedGeocoder.geoHashString(aPos, level: 1))! XCTAssert(geoHash == "u")
Downscaled information in no network.
Scheduling
Avoid flooding the Apple Reverse Geocoder.
Schedule operation on a heartbeat (timer).
Do nothing in the background.
Stop when all is done.
If error: next operation can be progressively delayed.
Don’t trap yourself!
Scheduling Principle
func reverseGeocode<T:ReverseGeocodable>(geocodableObject:T?, identifier:String?, priority:ReverseGeocodingPriority, completionHandler: CLGeocodeCompletionHandler?) -> ReverseGeocodeResult
{ var result = ReverseGeocodeResult.DefaultLaunch
//Do pre check //Keep where we are in order to start heartbeat if necessary let countBefore:Int = self.reverseGeocodingCount()
//More stuff
//Start heartbeat if needed let countAfter:Int = self.reverseGeocodingCount() if 0 == countBefore && countAfter > 0 { self.heartbeat = Foundation.NSTimer.scheduledTimerWithTimeInterval(self.GeocodingServiceHeartbeatValue,
target: self, selector:#selector(_doExecuteHeartBeat(_:)) , userInfo: nil, repeats: true) self.heartbeat?.tolerance = self.GeocodingServiceHeartbeatTolerance } return result }
Priority
Arbitrary number: 3 sounds good for normal usage.
Very simple FIFO model : 3 FIFOs as queues.
Heartbeat is unstacking the queues.
Operations can be moved between queues.
Should be settable on a more permanent basis: use cautiously!
Priorities are to be used!!!!
Priority queues
HighMedium
u09wj85t
u09wj86d
ke7fynh8
6gyqdy5u
6gyqdy5v
drt2xp2mu09wj84f
Normal
Heart beat
UI driven
Medium High Medium
Track identifier
Follow someone moving
T1
T2
T3
T4T5
T6
Implementation details
Follow someone moving.
Identifier can be a user UUID but we can think of other cases (e.g UI).
We do not want to encode position of the past.
An operation with an identifier replaces the previous one.
Should handle both identifier and priority.
One more improvement: still keep a delivery ratio.
Thank You!https://zen.ly/join