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 Address
Reverse Geocode
Location Change
UI
User Address
Reverse Geocode
Location Change
User Address
Reverse 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.
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!!!!
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.