Is there a good solution to reverse geocode considering the horizontal accuracy of the location?
For instance this is the result for the location (and is regardless of horizontal accuracy):
{
City = "San Francisco";
Country = "United States";
CountryCode = US;
FormattedAddressLines = (
"246 Powell St",
"San Francisco, CA 94102-2206",
"United States"
);
Name = "246 Powell St";
PostCodeExtension = 2206;
State = CA;
Street = "246 Powell St";
SubAdministrativeArea = "San Francisco";
SubLocality = "Union Square";
SubThoroughfare = 246;
Thoroughfare = "Powell St";
ZIP = 94102;
}
I would like to get the result considering accuracy. E.g.:
accuracy: 10m; result: 246 Powell St
accuracy: 100m; result: Union Square
...
accuracy: 100km; result: San Francisco
I have figured I could probably do this by requesting reverse geocoding for multiple coordinates that are roughly on the edges of the provided horizontal accuracy and then intersecting the results. But is there a more clean way?
Did you mean CLGeocoder?
I think that requesting multiple reverse geocoding will probably do more harm then good.
From Apple's CLGeocoder documentation (which can be found here):
Applications should be conscious of how they use geocoding.
Geocoding requests are rate-limited for each app,
so making too many requests in a short period of time
may cause some of the requests to fail.
So I suggest not going this way.
Could be better ways, but just at the top of my head, you could probably use some sort of if-else method that uses the horizontalAccuracy property.
It's been a while since I've used MapKit so could be that my code below isn't 100% accurate but I can't test its functionality at the moment (it should compile though), but it'll give you an idea of how this can be done.
For example:
// here geocoder is your CLGeocoder object
// and location is the CLLocation you try to reverse geocode
[geocoder reverseGeocodeLocation:location completionHandler:^(NSArray *placemarks, NSError *error) {
if(error) {
NSLog(#"Error occurred: %#", [error localizedDescription]);
} else { // No error has occurred
if([placemarks count]) { // Just another step of precaution
CLPlacemark *placemark = placemarks[0]; // assume the first object is our correct place mark
if(location.horizontalAccuracy <= 10) { // equal or less than 10 meters
NSLog(#"Result: %# %#", placemark.subThoroughfare, placemark.thoroughfare); // 246 Powell St
} else if (location.horizontalAccuracy <= 100) { // equal or less than 100 meters
NSLog(#"Result: %#", placemark.subLocality); // Union Square
} else if (location.horizontalAccuracy <= 100000) { // equal or less than 100km
NSLog(#"Result: %#", placemark.subAdministrativeArea); // San Francisco
}
}
}
}];
When I would be able to test its functionality myself (could be a couple of hours, or a couple of days) I would edit if changes needed to be made.
Let me know if you are having issues or you have more questions.
Good luck mate.
Related
Here's an interesting one:
I have a UIPickerView that contains the values "10 km", "25 km", "50 km" and "100 km". What ever value is selected will be the value [queryLocation ... withinKilometers: value here ]; is equal too.
Since the value for 'withinKilometers' needs to be a double value, I have the following code to convert the X km to just X as a double value:
if ([self.selectedData isEqualToString:#"10 km"]) {
self.selectedDataConverted = #"10";
self.stringToDouble = [self.selectedDataConverted doubleValue];
NSLog(#"Double Value: %f", self.stringToDouble);
}
... and so on...
and here's the code for the query:
PFQuery *queryLocation = [PFUser query];
[queryLocation whereKey:#"location" nearGeoPoint:self.userLocation withinKilometers:self.stringToDouble];
if (!error) {
NSLog(#"Great Success");
//Do the stuff here
}
else {
NSLog(#"Error");
}
}];
Now when I run this, I get the error [Error]: geo query within or is not supported (Code: 102, Version: 1.7.4). I'm stumped and am not sure how to fix this. Can't find anything similar to my problem either online.
EDIT: I'm starting to think maybe it's because I have connected 2 queries into 1:
PFQuery *finalQuery = [PFQuery orQueryWithSubqueries:#[queryMale, queryLocation]];
[finalQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
NSLog(#"Great Success");
}
else {
NSLog(#"Error");
}
}];
This is to find accounts with the gender 'male' within the desired kilometres at the same time.
You're right. The error geo query within or is not supported means you can't use this query with orQueryWithSubqueries. You will need to run these as 2 separate queries and then combine the result sets yourself.
I am using the NSLengthFormatter class to format the distance between the user and some destination.
CLLocation *userLocation; //<- Coordinates fetched from CLLocationManager
CLLocation *targetLocation; //<- Some location retrieved from server data
CLLocationDistance distance = [userLocation distanceFromLocation:targetLocation];
NSLengthFormatter *lengthFormatter = [NSLengthFormatter new];
NSString *formattedLength = [lengthFormatter stringFromMeters:distance];
Now, if the length is less than 1000 meters, the formatted distance is always shown in yards or meters (depending on the locale).
Eg. if distance = 450.0, the formatted string will be 492.7 yd or 450 m.
How can I tweak NSLengthFormatter to return the distance strings in miles/kilometers only?
This is what I have ended up using:
-(NSString *)formattedDistanceForMeters:(CLLocationDistance)distance
{
NSLengthFormatter *lengthFormatter = [NSLengthFormatter new];
[lengthFormatter.numberFormatter setMaximumFractionDigits:1];
if ([[[NSLocale currentLocale] objectForKey:NSLocaleUsesMetricSystem] boolValue])
{
return [lengthFormatter stringFromValue:distance / 1000 unit:NSLengthFormatterUnitKilometer];
}
else
{
return [lengthFormatter stringFromValue:distance / 1609.34 unit:NSLengthFormatterUnitMile];
}
}
EDIT:
The same in Swift would look like:
func formattedDistanceForMeters(distance:CLLocationDistance) -> String {
let lengthFormatter:NSLengthFormatter! = NSLengthFormatter()
lengthFormatter.numberFormatter.maximumFractionDigits = 1
if NSLocale.currentLocale().objectForKey(NSLocaleUsesMetricSystem).boolValue()
{
return lengthFormatter.stringFromValue(distance / 1000, unit:NSLengthFormatterUnitKilometer)
}
else
{
return lengthFormatter.stringFromValue(distance / 1609.34, unit:NSLengthFormatterUnitMile)
}
}
There doesn't seem a way to opt out of this behaviour. To be honest, your requirement is not very common from UX perspective.
Note that meter is the base unit, not a kilometer (a thousand of meters). Usually, displaying 10 meters is preferred over displaying 0.01 kilometers. It's just more friendly for the users.
It would be actually very hard to design an API that would enforce a specific unit considering that the base unit depends on current locale.
You can enforce a specific unit using:
- (NSString *)unitStringFromValue:(double)value unit:(NSLengthFormatterUnit)unit;
but you will have to handle the locale and scaling & unit conversion by yourself (see Objective c string formatter for distances)
It's actually a very common requirement for people not using metric system (Yes I know...).
In metric system it just makes sense to go from Kilometers to Meters, etc. If you follow the same logic with the imperial system you'll go from Miles to Yards to Foot.
Usually you don't want to use Yards for road distances for example and you don't want to display 5,000 ft but 0.9 mi (Actually Google Maps display in feet up to 0.1 miles or 528 feet, and then in miles).
let distanceAsDouble = 415.0
let lengthFormatter = LengthFormatter()
if Locale.current.usesMetricSystem {
return distanceFormatter.string(fromMeters: distanceAsDouble)
} else {
let metersInOneMile = Measurement<UnitLength>(value: 1.0, unit: .miles).converted(to: .meters).value
if distanceAsDouble < metersInOneMile / 10 { // Display feets from 0 to 0.1 mi, then miles
let distanceInFeets = Measurement<UnitLength>(value: distanceAsDouble, unit: .meters).converted(to: .feet).value
return distanceFormatter.string(fromValue: distanceInFeets, unit: .foot)
} else {
return distanceFormatter.string(fromValue: distanceAsDouble / metersInOneMile, unit: .mile)
}
}
-(NSString *)formattedDistance {
CLLocationDistance distance = _distance;
NSLengthFormatter *lengthFormatter = [NSLengthFormatter new];
[lengthFormatter.numberFormatter setMaximumFractionDigits:1];
if ([[[NSLocale currentLocale] objectForKey:NSLocaleUsesMetricSystem] boolValue])
{
return [lengthFormatter stringFromValue:distance / 1000 unit:NSLengthFormatterUnitKilometer];
}
else
{
return [lengthFormatter stringFromValue:distance / 1609.34 unit:NSLengthFormatterUnitMile];
}
}
I'm doing some tutorials about how to send and receive data to and from Apple HealthKit app.
Part of the tutorial I'm doing is how to get the height from the healthKitStore.
I want to do the same thing but to retrieve the glucose data instead of the height, I did all the steps but got stuck at this piece of code:
var heightLocalizedString = self.kUnknownString;
self.height = mostRecentHeight as? HKQuantitySample;
// 3. Format the height to display it on the screen
if let meters = self.height?.quantity.doubleValueForUnit(HKUnit.meterUnit()) {
let heightFormatter = NSLengthFormatter()
heightFormatter.forPersonHeightUse = true;
heightLocalizedString = heightFormatter.stringFromMeters(meters);
}
As shown, the meters var is being assigned a double value from the meterUnit, and then creating a constant formatter to format the meters var and assign it to the pre-declared var (heightLocalizedString)
My question is, when I use this method for the glucose reading, I face a couple of issues, the first problem is I can't figure out what glucose units are available, the only one I get is
HKUnitMolarMassBloodGlucose
and when I use it an error appears says "'NSNumber' is not a subtype of 'HKUnit'", it's clear from the error this parameter is not a subtype of the HKUnit class.
Another issue is, as shown in the previous code there is a formatter for the height (NSLengthFormatter()), but I can't see a such formatter for the Glucose.
Actually I'm not sure if have to follow exactly the tutorial to get the Glucose data, but also I don't see another manner to do so.
Any ideas please?
Here is the code I'm using to retrieve the glucose data:
func updateGluco(){
let sampleType = HKSampleType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBloodGlucose)
self.healthManager?.readMostRecentSample(sampleType, completion: {(mostRecentGluco, error) -> Void in
if (error != nil){
println("Error reading blood glucose from HealthKit store: \(error.localizedDescription)")
return;
}
var glucoLocalizedString = self.kUnknownString;
self.gluco = mostRecentGluco as? HKQuantitySample
println("\(self.gluco?.quantity.doubleValueForUnit(HKUnit.moleUnitWithMolarMass(HKUnitMolarMassBloodGlucose)))")
self.gluco?.quantity.doubleValueForUnit(HKUnit.moleUnitWithMolarMass(HKUnitMolarMassBloodGlucose))
if let mmol = self.gluco?.quantity.doubleValueForUnit(HKUnit.moleUnitWithMolarMass(HKUnitMolarMassBloodGlucose)) {
glucoLocalizedString = "\(mmol)"
} else {
println("error reading gluco data!")
}
dispatch_async(dispatch_get_main_queue(), {() -> Void in
self.glucoLabel.text = glucoLocalizedString})
})
}
The "mmol" variable returns a nil value.
I don't know if this relates to my problem, but I've just read an article said that Apple brought back blood glucose tracking in iOS 8.2 and my application deployment target is 8.1. (I can't upgrade the deployment target to the latest iOS despite that the XCode is updated to the last release!)
Looking in the header, blood glucose requires units of the type (Mass / Volume), which means you need to specify a compound unit with a Mass unit divided by a Volume unit:
HK_EXTERN NSString * const HKQuantityTypeIdentifierBloodGlucose NS_AVAILABLE_IOS(8_0); // Mass/Volume, Discrete
Typically, the units that people measure blood glucose in are mg/dL, and mmol/L. You can construct these by using:
HKUnit *mgPerdL = [HKUnit unitFromString:#"mg/dL"];
HKUnit *mmolPerL = [[HKUnit moleUnitWithMetricPrefix:HKMetricPrefixMilli molarMass:HKUnitMolarMassBloodGlucose] unitDividedByUnit:[HKUnit literUnit]];
Note that doubleValueForUnit: requires an HKUnit, not an NSNumber. See for more information
I figured out the solution
To get the actual value of the sample gulco, I have only to use this property: self.gluco?.quantity, that's exactly what I wanted before.
The default unit for the Glucose in HealthKit is mg\dL, in order to change the unit simply I extract the number value from the result and then divide it by 18.
Here is the code I'm using to retrieve the glucose data:
NSInteger limit = 0;
NSPredicate* predicate = [HKQuery predicateForSamplesWithStartDate:[NSDate date] endDate:[NSDate date] options:HKQueryOptionStrictStartDate];;
NSString *endKey = HKSampleSortIdentifierEndDate;
NSSortDescriptor *endDate = [NSSortDescriptor sortDescriptorWithKey: endKey ascending: NO];
HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType: [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierBloodGlucose]
predicate: predicate
limit: limit
sortDescriptors: #[endDate]
resultsHandler:^(HKSampleQuery *query, NSArray* results, NSError *error)
{
dispatch_async(dispatch_get_main_queue(), ^{
// sends the data using HTTP
// Determine the Blood Glucose.
NSLog(#"BloodGlucose=%#",results);
if([results count]>0){
NSMutableArray *arrBGL=[NSMutableArray new];
for (HKQuantitySample *quantitySample in results) {
HKQuantity *quantity = [quantitySample quantity];
double bloodGlucosegmgPerdL = [quantity doubleValueForUnit:[HKUnit bloodGlucosegmgPerdLUnit]];
NSLog(#"%#",[NSString stringWithFormat:#"%.f g",bloodGlucosegmgPerdL]);
}
}
});
}];
[self.healthStore executeQuery:query];
// And Units :
#implementation HKUnit (HKManager)
+ (HKUnit *)heartBeatsPerMinuteUnit {
return [[HKUnit countUnit] unitDividedByUnit:[HKUnit minuteUnit]];
}
+ (HKUnit *)bloodGlucosegmgPerdLUnit{
return [[HKUnit gramUnit] unitDividedByUnit:[HKUnit literUnit]];
}
I'm calling geocodeAddressString with a city name (e.g. Seattle, Los Angeles, etc.). In the Xcode simulator, this returns a single correct placemark value, but no postalCode:
[geocoder geocodeAddressString:placename completionHandler:^(NSArray *placemarks, NSError *error) {
//Error checking
NSLog(#"placemarks %i",[placemarks count]);
if ([placemarks count] == 1) { // one city returned. set location accordingly
CLPlacemark *placemark = [placemarks objectAtIndex:0];
NSLog(#"one location found %#",placemark);
[self setUserLocation:placemark];
}
}];
Output placemark values:
- (void)setUserLocation:(CLPlacemark *)placemark {
NSLog(#"found zip %#",placemark.postalCode);
NSLog(#"found city %#",placemark.locality);
NSLog(#"found state %#",placemark.administrativeArea);
NSLog(#"found country %#",placemark.ISOcountryCode);
NSLog(#"found lat %f",placemark.region.center.latitude);
NSLog(#"found long %f",placemark.region.center.longitude);
}
Shows the following:
2014-05-21 07:28:12.378 jobagent[5157:60b] entered location seattle
2014-05-21 07:28:12.378 jobagent[5157:60b] placename seattle
2014-05-21 07:28:12.992 jobagent[5157:60b] placemarks 1
2014-05-21 07:28:12.993 jobagent[5157:60b] one location found Seattle, WA, United States # <+47.60620950,-122.33207080> +/- 100.00m, region CLCircularRegion (identifier:'<+47.56346943,-122.32853603> radius 26395.84', center:<+47.56346943,-122.32853603>, radius:26395.84m)
2014-05-21 07:28:12.993 jobagent[5157:60b] found zip (null)
2014-05-21 07:28:12.994 jobagent[5157:60b] found city Seattle
2014-05-21 07:28:12.994 jobagent[5157:60b] found state WA
2014-05-21 07:28:12.995 jobagent[5157:60b] found country US
2014-05-21 07:28:12.995 jobagent[5157:60b] found lat 47.563469
2014-05-21 07:28:12.996 jobagent[5157:60b] found long -122.328536
CLGeocoder doesn't return the postalCode for just a City, because there are multiple postalCode.
To obtain the postalCode, you need to be more specific like for example writing the street.
I have an application that I am working on for iOS that is to display trails on a map based on the bounding box of the current view. So as the user moves around the map it will load the appropriate set of trails.
The SQLite table is LARGE, 260k rows, where each row is a point on a trail. The table has a primary key (int), latitude (float), longitude (float) and a name (varchar). The latitude and longitude columns are indexed. When I query I am looking for where the latitude is between the lower right and upper left latitude and the longitude is between the upper left and lower right longitude. The query works flawlessly on desktop as well as on the phones as it returns the expected results. The issue is that on my Mac the query returns instantaniously where as on the phone it may take as little as 4 seconds before returning anything. The bottle neck does appear to be the database query and I'm starting to think its a limitation of the hardware.
I have tried using CoreData which is where I noticed the issue first. Then I moved to using FMDB to access the database and am still having issues.
I have done no tweaking to the database or to the connection.
The guts of the queryForTrails method
if( ![db open] ) {
[db release];
NSLog(#"Error opening DB: %#", dbPath);
}
FMResultSet *trails = [db executeQueryWithFormat:#"SELECT zname, zlatitude, zlongitude FROM ztrail WHERE ztype = 1 and zlatitude BETWEEN %# and %# AND zlongitude BETWEEN %# and %# order by zname", lrLat, ulLat, ulLon,lrLon];
//Start to load the map with data
NSString *lastTrailName=#"";
int idx = 0;
CLLocationCoordinate2D *trailCoords = nil;
NSUInteger coordSize = 20;
trailCoords = malloc(sizeof(CLLocationCoordinate2D)*coordSize);
while( [trails next] ) {
NSString *trailName = [trails stringForColumnIndex:0];
NSString *lat = [trails stringForColumnIndex:1];
NSString *lon = [trails stringForColumnIndex:2];
if( [lastTrailName compare:trailName] != NSOrderedSame ) {
if(idx > 0) {
[trailLines addObject:[MKPolyline polylineWithCoordinates:trailCoords count:idx]];
free(trailCoords);
idx = 0;
coordSize = 20;
}
lastTrailName = trailName;
trailCoords = malloc(sizeof(CLLocationCoordinate2D)*coordSize);
}
if(idx == coordSize) {
coordSize *= 2;
trailCoords = realloc(trailCoords, sizeof(CLLocationCoordinate2D) * coordSize);
}
trailCoords[idx++] = CLLocationCoordinate2DMake([lat doubleValue], [lon doubleValue]);
}
//Build the new polyline
[trailLines addObject:[MKPolyline polylineWithCoordinates:trailCoords count:idx]];
//NSLog(#"Num Trails: %d", [trailLines count]);
free(trailCoords);
//NSLog(#"Num of Points %d for %#",idx, lastTrailName);
if( [trailLines count] > 0 ) {
dispatch_async(dispatch_get_main_queue(),^{
[mapView addOverlays:trailLines];
});
}
I can provide some NSLog data if needed. I will also be doing the same application for Android so I'd like to try and work out the performance issues now.