按与动态位置的距离过滤核心数据实体

Posted

技术标签:

【中文标题】按与动态位置的距离过滤核心数据实体【英文标题】:Filter core data entities by distance from a dynamic position 【发布时间】:2017-02-10 18:24:09 【问题描述】:

我有一个 NSManagedObject 派生类(实体),其实例保存在本地 SQL-lite 存储中。该类还具有经度和纬度属性,我需要根据与特定坐标的距离来获取实体。我尝试将 NSPredicate 与自定义函数一起使用,但我找不到有关如何实现该函数的文档(...如果它确实支持)。有没有人知道如何对核心数据实体执行这种动态过滤?我尝试使用 NSPredicate withBlock,但它不适用于 SQL-Lite 数据库上持久化的对象。请帮忙。

【问题讨论】:

【参考方案1】:

Core Data 无法做到这一点。在获取实体后,您可以使用 Transient 属性对与动态位置的距离进行建模,甚至可以根据该 Transient 属性对项目进行排序。但是,如果它们是持久属性,则只能从持久存储中获取属性。

在实践中,如果坐标被索引,我发现在矩形窗口中查询点非常快。通过获取动态位置的纬度/经度,加上/减去搜索半径来构建窗口,如果它是非投影数据(纯纬度/经度,而不是 UTM 或类似网格),则对经度窗口进行余弦(纬度)调整。

如果这真的不够快,您可以在您的实体上存储一个 geohash,它会为您提供一个可以进行前缀搜索的字符串。请参阅https://en.wikipedia.org/wiki/Geohash 进行讨论。或者你可以实现一个真正的空间索引。但我从来没有遇到过值得实施这两种方法的 Core Data 性能问题。

【讨论】:

【参考方案2】:

我将我的位置作为纬度/经度坐标存储在数据模型中。然后我写了一些辅助扩展来找到纬度/经度坐标的半矩形区域并通过它进行查询。这极大地限制了结果集,因此如果您需要按位置排序,您可以使用 CLLocation 距离计算器对结果对象进行排序。

我创建了一个 CLCircularRegion,你可以看到我的 buildPredicate() 函数创建了查询。

这是我正在使用的代码:

斯威夫特 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)
        
    

【讨论】:

不正是我需要的... tx【参考方案3】:

这是答案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)];

【讨论】:

上面有没有快速的实现?【参考方案4】:

这是上述答案的 Swift 版本

        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)

【讨论】:

以上是关于按与动态位置的距离过滤核心数据实体的主要内容,如果未能解决你的问题,请参考以下文章

如何根据与已知参考轨迹的距离过滤掉位置数据?

按与当前位置的距离对 POI 进行排序

对许多实体和实体关系进行过滤的核心数据提取

按祖父母实体关系过滤核心数据

如何按不相关的实体过滤核心数据

从出现在 NSPopUpButton 列表中的核心数据实体中过滤条目