Sergio B.
Sergio B.

Reputation: 990

Filter core data entities by distance from a dynamic position

I have a NSManagedObject derived class (entity) whose instances are persisted in a local SQL-lite storage. The class has also longitude and latitude properties and I need to fetch the entities based on the distance from specific coordinates. I tried to used NSPredicate with custom functions, but I couldn't find documentation on how to implement the function (... if it does support that). Has anyone an idea on how to perform that kind of dynamic filtering on Core Data entities ? I tried with NSPredicate withBlock, but it doesn't work on objects persisted on SQL-Lite databases. Please help.

Upvotes: 1

Views: 1217

Answers (4)

iamgsiva
iamgsiva

Reputation: 41

Here's the Swift version of above answer

        let D: Double = 80 * 1.1
        let R: Double = 6371009
        let meanLatitidue = pointOfInterest.coordinate.latitude * .pi / 180
        let deltaLatitude = D / R * 180 / .pi
        let deltaLongitude = D / (R * cos(meanLatitidue)) * 180 / .pi
        let minLatitude: Double = pointOfInterest.coordinate.latitude - deltaLatitude
        let maxLatitude: Double = pointOfInterest.coordinate.latitude + deltaLatitude
        let minLongitude: Double = pointOfInterest.coordinate.longitude - deltaLongitude
        let maxLongitude: Double = pointOfInterest.coordinate.longitude + deltaLongitude
        let predicate = NSPredicate(format: "(%lf <= longitude) AND (longitude <= %lf) AND (%lf <= latitude) AND (latitude <= %lf)", minLongitude, maxLongitude,minLatitude, maxLatitude)

Upvotes: 0

Ilya
Ilya

Reputation: 141

Here is the answer https://www.objc.io/issues/4-core-data/core-data-fetch-requests/

static double const D = 80. * 1.1;
double const R = 6371009.; // Earth readius in meters
double meanLatitidue = pointOfInterest.latitude * M_PI / 180.;
double deltaLatitude = D / R * 180. / M_PI;
double deltaLongitude = D / (R * cos(meanLatitidue)) * 180. / M_PI;
double minLatitude = pointOfInterest.latitude - deltaLatitude;
double maxLatitude = pointOfInterest.latitude + deltaLatitude;
double minLongitude = pointOfInterest.longitude - deltaLongitude;
double maxLongitude = pointOfInterest.longitude + deltaLongitude;
request.result = [NSPredicate predicateWithFormat:
                  @"(%@ <= longitude) AND (longitude <= %@)"
                  @"AND (%@ <= latitude) AND (latitude <= %@)",
                  @(minLongitude), @(maxLongitude), @(minLatitude), @(maxLatitude)];

Upvotes: 1

Chris Garrett
Chris Garrett

Reputation: 4924

I store my locations in the data model as latitude/longitude coordinates. Then I wrote some helper extensions to find a semirectangular region of lat/lon coordinates and query by that. This greatly limits the result set, so if you need to sort by location you can sort the resulting objects using the CLLocation distance calculator.

I create a CLCircularRegion and you can see my buildPredicate() function that creates the query.

Here's the code I'm using:

Swift 3

    extension CLLocationDegrees {
        static var north: CLLocationDegrees {
            return 90.0
        }
        static var south: CLLocationDegrees {
            return -90.0
        }
        static var east: CLLocationDegrees {
            return 180.0
        }
        static var west: CLLocationDegrees {
            return -180.0
        }

        var radians: Double {
            return Double.pi * self / 180.0
        }
    }

    extension CLLocationCoordinate2D {
        var metersPerDegreeLatitude: CLLocationDistance {
            return 111319.4907932736
        }
        var metersPerDegreeLongitude: CLLocationDistance {
            return max(0.0, cos(self.latitude.radians) * self.metersPerDegreeLatitude)
        }
    }

    extension CLCircularRegion {
        var northernmostLatitude: CLLocationDegrees {
            let longitude = self.center.latitude + self.radius / self.center.metersPerDegreeLatitude
            return min(longitude, .north)
        }

        var southernmostLatitude: CLLocationDegrees {
            let longitude = self.center.latitude - self.radius / self.center.metersPerDegreeLatitude
            return max(longitude, .south)
        }

        var easternmostLongitude: CLLocationDegrees {
            guard self.northernmostLatitude <= .north else {
                return .east
            }
            guard self.southernmostLatitude >= .south else {
                return .east
            }
            return min(.east, self.center.longitude + self.radius / (self.center.metersPerDegreeLongitude + 0.0001))
        }

        var westernmostLongitude: CLLocationDegrees {
            guard self.northernmostLatitude <= .north else {
                return .west
            }
            guard self.southernmostLatitude >= .south else {
                return .west
            }
            return max(.west, self.center.longitude - self.radius / (self.center.metersPerDegreeLongitude + 0.0001))
        }

        func buildPredicate(latitudeName: String = "latitude", longitudeName: String = "longitude") -> NSPredicate {
            let args = [self.southernmostLatitude, self.northernmostLatitude, self.westernmostLongitude, self.easternmostLongitude]
            return NSPredicate(format: "\(latitudeName) >= %@ && \(latitudeName) <= %@ && \(longitudeName) >= %@ && \(longitudeName) <= %@", argumentArray: args)
        }
    }

Upvotes: 1

Hal Mueller
Hal Mueller

Reputation: 7655

You can't do that with Core Data. You could use a Transient property to model the distance from your dynamic location, after the entities have been fetched, and even order the items based on that Transient property. But you can only fetch properties from the persistent store if they are persistent properties.

In practice, I've found it's very fast to query points in a rectangular window, if the coordinates are indexed. Build the window by taking the dynamic position's latitude/longitude, plus/minus the search radius, throwing in a cosine(latitude) adjustment for the longitude window if it's unprojected data (pure lat/lon, not UTM or similar grid).

If that's really not fast enough, you could store a geohash on your entities, which gives you a string you can do a prefix search on. See https://en.wikipedia.org/wiki/Geohash for a discussion. Or you could implement a true spatial index. But I've never had a Core Data performance problem that made it worth implementing either of those approaches.

Upvotes: 3

Related Questions