2017-10-08 56 views
9

Firebase的新Firestore数据库是否本地支持基于位置的地理查询?即在10英里内查找帖子,或查找50个最近的帖子?如何使用firestore运行地理“附近”查询?

我看到有一些针对实时Firebase数据库的现有项目,诸如geofire之类的项目也可以将这些项目改编为firestore?

+4

[如何在Firebase Cloud Firestore中的集合中查询最近的GeoPoints?]可能的重复?(https://stackoverflow.com/questions/46607760/how-to-query-closest-geopoints-in-a-collection -in-火力-云公司的FireStore) –

回答

3

截至今日,没有办法做这样的查询。有在SO相关的其他问题吧:

Is there a way to use GeoFire with Firestore?

How to query closest GeoPoints in a collection in Firebase Cloud Firestore?

Is there a way to use GeoFire with Firestore?

在我目前的Android项目,我可以使用https://github.com/drfonfon/android-geohash添加地理散列领域,而火力地堡的团队正在开发的原生支持。

使用Firebase实时数据库就像在其他问题中建议的一样意味着您无法同时过滤位置和其他字段的结果集,这是我首先想要切换到Firestore的主要原因。

14

这可以通过创建小于大于查询的边界框来完成。至于效率,我无法对它说话。

注意,精度纬度/长偏移〜1英里应加以审查,但这里是一个快速的方法来做到这一点:

SWIFT 3.0版本

func getDocumentNearBy(latitude: Double, longitude: Double, distance: Double) { 

    // ~1 mile of lat and lon in degrees 
    let lat = 0.0144927536231884 
    let lon = 0.0181818181818182 

    let lowerLat = latitude - (lat * distance) 
    let lowerLon = longitude - (lon * distance) 

    let greaterLat = latitude + (lat * distance) 
    let greaterLon = longitude + (lon * distance) 

    let lesserGeopoint = GeoPoint(latitude: lowerLat, longitude: lowerLon) 
    let greaterGeopoint = GeoPoint(latitude: greaterLat, longitude: greaterLon) 

    let docRef = Firestore.firestore().collection("locations") 
    let query = docRef.whereField("location", isGreaterThan: lesserGeopoint).whereField("location", isLessThan: greaterGeopoint) 

    query.getDocuments { snapshot, error in 
     if let error = error { 
      print("Error getting documents: \(error)") 
     } else { 
      for document in snapshot!.documents { 
       print("\(document.documentID) => \(document.data())") 
      } 
     } 
    } 

} 

func run() { 
    // Get all locations within 10 miles of Google Headquarters 
    getDocumentNearBy(latitude: 37.422000, longitude: -122.084057, distance: 10) 
} 
6

(第一让我为这篇文章中的所有代码道歉,我只是希望任何读这个答案的人都可以轻松地再现这个功能。)

为了解决OP的问题,起初我改编了GeoFire library与Firestore合作(您可以通过查看该库来了解更多有关地理信息的内容)。然后我意识到我并不介意地点是否以一个确切的圆圈返回。我只是想通过某种方式获得“附近”的位置。

我无法相信我花了多长时间才意识到这一点,但是您可以使用SW角和NE角在GeoPoint字段上执行双重不等式查询,以获取围绕中心点的边界框内的位置。

所以我做了一个JavaScript函数,就像下面的函数一样(这基本上是Ryan Lee答案的JS版本)。

/** 
* Get locations within a bounding box defined by a center point and distance from from the center point to the side of the box; 
* 
* @param {Object} area an object that represents the bounding box 
* around a point in which locations should be retrieved 
* @param {Object} area.center an object containing the latitude and 
* longitude of the center point of the bounding box 
* @param {number} area.center.latitude the latitude of the center point 
* @param {number} area.center.longitude the longitude of the center point 
* @param {number} area.radius (in kilometers) the radius of a circle 
* that is inscribed in the bounding box; 
* This could also be described as half of the bounding box's side length. 
* @return {Promise} a Promise that fulfills with an array of all the 
* retrieved locations 
*/ 
function getLocations(area) { 
    // calculate the SW and NE corners of the bounding box to query for 
    const box = utils.boundingBoxCoordinates(area.center, area.radius); 

    // construct the GeoPoints 
    const lesserGeopoint = new GeoPoint(box.swCorner.latitude, box.swCorner.longitude); 
    const greaterGeopoint = new GeoPoint(box.neCorner.latitude, box.neCorner.longitude); 

    // construct the Firestore query 
    let query = firebase.firestore().collection('myCollection').where('location', '>', lesserGeopoint).where('location', '<', greaterGeopoint); 

    // return a Promise that fulfills with the locations 
    return query.get() 
    .then((snapshot) => { 
     const allLocs = []; // used to hold all the loc data 
     snapshot.forEach((loc) => { 
     // get the data 
     const data = loc.data(); 
     // calculate a distance from the center 
     data.distanceFromCenter = utils.distance(area.center, data.location); 
     // add to the array 
     allLocs.push(data); 
     }); 
     return allLocs; 
    }) 
    .catch((err) => { 
     return new Error('Error while retrieving events'); 
    }); 
} 

上面的功能还增加了一个.distanceFromCenter属性,每一块的位置数据的所返回,这样你可以通过只检查,如果这个距离是你想要的范围内获得圆的行为。

我在上面的函数中使用了两个util函数,所以这里也是这些代码。 (下面所有的util函数实际上都是从GeoFire库改编而来的。)

距离():

/** 
* Calculates the distance, in kilometers, between two locations, via the 
* Haversine formula. Note that this is approximate due to the fact that 
* the Earth's radius varies between 6356.752 km and 6378.137 km. 
* 
* @param {Object} location1 The first location given as .latitude and .longitude 
* @param {Object} location2 The second location given as .latitude and .longitude 
* @return {number} The distance, in kilometers, between the inputted locations. 
*/ 
distance(location1, location2) { 
    const radius = 6371; // Earth's radius in kilometers 
    const latDelta = degreesToRadians(location2.latitude - location1.latitude); 
    const lonDelta = degreesToRadians(location2.longitude - location1.longitude); 

    const a = (Math.sin(latDelta/2) * Math.sin(latDelta/2)) + 
      (Math.cos(degreesToRadians(location1.latitude)) * Math.cos(degreesToRadians(location2.latitude)) * 
      Math.sin(lonDelta/2) * Math.sin(lonDelta/2)); 

    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 

    return radius * c; 
} 

boundingBoxCoordinates():(有在这里也使用更utils的,我已经粘贴下面)

/** 
* Calculates the SW and NE corners of a bounding box around a center point for a given radius; 
* 
* @param {Object} center The center given as .latitude and .longitude 
* @param {number} radius The radius of the box (in kilometers) 
* @return {Object} The SW and NE corners given as .swCorner and .neCorner 
*/ 
boundingBoxCoordinates(center, radius) { 
    const KM_PER_DEGREE_LATITUDE = 110.574; 
    const latDegrees = radius/KM_PER_DEGREE_LATITUDE; 
    const latitudeNorth = Math.min(90, center.latitude + latDegrees); 
    const latitudeSouth = Math.max(-90, center.latitude - latDegrees); 
    // calculate longitude based on current latitude 
    const longDegsNorth = metersToLongitudeDegrees(radius, latitudeNorth); 
    const longDegsSouth = metersToLongitudeDegrees(radius, latitudeSouth); 
    const longDegs = Math.max(longDegsNorth, longDegsSouth); 
    return { 
    swCorner: { // bottom-left (SW corner) 
     latitude: latitudeSouth, 
     longitude: wrapLongitude(center.longitude - longDegs), 
    }, 
    neCorner: { // top-right (NE corner) 
     latitude: latitudeNorth, 
     longitude: wrapLongitude(center.longitude + longDegs), 
    }, 
    }; 
} 

metersToLongitudeDegrees():

/** 
* Calculates the number of degrees a given distance is at a given latitude. 
* 
* @param {number} distance The distance to convert. 
* @param {number} latitude The latitude at which to calculate. 
* @return {number} The number of degrees the distance corresponds to. 
*/ 
function metersToLongitudeDegrees(distance, latitude) { 
    const EARTH_EQ_RADIUS = 6378137.0; 
    // this is a super, fancy magic number that the GeoFire lib can explain (maybe) 
    const E2 = 0.00669447819799; 
    const EPSILON = 1e-12; 
    const radians = degreesToRadians(latitude); 
    const num = Math.cos(radians) * EARTH_EQ_RADIUS * Math.PI/180; 
    const denom = 1/Math.sqrt(1 - E2 * Math.sin(radians) * Math.sin(radians)); 
    const deltaDeg = num * denom; 
    if (deltaDeg < EPSILON) { 
    return distance > 0 ? 360 : 0; 
    } 
    // else 
    return Math.min(360, distance/deltaDeg); 
} 

wrapLongitude():

/** 
* Wraps the longitude to [-180,180]. 
* 
* @param {number} longitude The longitude to wrap. 
* @return {number} longitude The resulting longitude. 
*/ 
function wrapLongitude(longitude) { 
    if (longitude <= 180 && longitude >= -180) { 
    return longitude; 
    } 
    const adjusted = longitude + 180; 
    if (adjusted > 0) { 
    return (adjusted % 360) - 180; 
    } 
    // else 
    return 180 - (-adjusted % 360); 
}