Как запустить гео-"ближайший" запрос с firestore?
Создает ли новая база данных firestore из firebase исходные геологические запросы на основе местоположения? т.е. найти сообщения в пределах 10 миль или найти 50 ближайших сообщений?
Я вижу, что есть некоторые существующие проекты для базы данных firebase в реальном времени, такие проекты, как geofire-, могут ли они быть адаптированы к firestore?
Ответы
Ответ 1
UPDATE: Firestore в настоящий момент не поддерживает фактические запросы GeoPoint, поэтому, пока выполняемый ниже запрос выполняется успешно, он фильтрует только по широте, а не по долготе, и таким образом возвратит много результатов, которые не находятся поблизости. Лучшим решением было бы использовать geohash. Чтобы узнать, как сделать что-то подобное себе, посмотрите на это видео.
Это можно сделать, создав ограничивающий прямоугольник меньше, чем запрос. Что касается эффективности, я не могу с этим поговорить.
Обратите внимание, что точность сдвига lat/long на ~ 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)
}
Ответ 2
UPDATE: Firestore в настоящий момент не поддерживает фактические запросы GeoPoint, поэтому, пока выполняемый ниже запрос выполняется успешно, он фильтрует только по широте, а не по долготе, и таким образом возвратит много результатов, которые не находятся поблизости. Лучшим решением было бы использовать geohash. Чтобы узнать, как сделать что-то подобное себе, посмотрите на это видео.
(Сначала позвольте мне извиниться за весь код в этом сообщении, я просто хотел, чтобы кто-нибудь читал этот ответ, чтобы легко воспроизвести функциональность.)
Для решения той же проблемы, которую имел ОП, я сначала адаптировал библиотеку GeoFire для работы с Firestore (вы можете много узнать о геоматериалах, посмотрев на эту библиотеку). Тогда я понял, что на самом деле я не возражал, если бы места были возвращены в точном кругу. Я просто хотел, чтобы можно было найти "близлежащие" места.
Я не могу поверить, сколько времени мне потребовалось, чтобы понять это, но вы можете просто выполнить запрос двойного неравенства в поле GeoPoint, используя угол SW и угол NE, чтобы получить места в ограничивающей рамке вокруг центральной точки.
Поэтому я сделал функцию JavaScript, подобную приведенной ниже (это в основном версия 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 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 к каждому возвращенному фрагменту данных местоположения, чтобы вы могли получить поведение, подобное кругу, просто проверяя, находится ли это расстояние в пределах диапазона, который вы хотите.
Я использую две функции функции в вышеприведенной функции, поэтому здесь и код для них. (Все функции утилиты, приведенные ниже, фактически адаптированы из библиотеки 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 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 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);
}
Ответ 3
На сегодняшний день нет никакого способа сделать такой запрос. В SO есть другие вопросы, связанные с ним:
Есть ли способ использовать GeoFire с Firestore?
Как запросить ближайшие GeoPoints в коллекции Firebase Cloud Firestore?
Есть ли способ использовать GeoFire с Firestore?
В моем текущем проекте Android я могу использовать https://github.com/drfonfon/android-geohash, чтобы добавить поле geohash, в то время как команда Firebase разрабатывает собственную поддержку.
Использование Firebase Realtime Database, как это предлагается в других вопросах, означает, что вы не можете фильтровать результаты, заданные по местоположению и другим полям одновременно, основной причиной, по которой я хочу переключиться на Firestore в первую очередь.
Ответ 4
Новый проект был введен с тех пор, как @monkeybonkey впервые задал этот вопрос. Проект называется GEOFirestore.
С помощью этой библиотеки вы можете выполнять запросы, такие как документы документов в круге:
const geoQuery = geoFirestore.query({
center: new firebase.firestore.GeoPoint(10.38, 2.41),
radius: 10.5
});
Вы можете установить GeoFirestore через npm. Вам придется установить Firebase отдельно (потому что это одноранговая зависимость от GeoFirestore):
$ npm install geofirestore firebase --save
Ответ 5
В настоящее время существует новая библиотека для iOS и Android, которая позволяет разработчикам выполнять геологические запросы на основе местоположения. Библиотека называется GeoFirestore. Я уже реализовал эту библиотеку и нашел много документации и ошибок. Он кажется проверенным и хорошим вариантом использования.
Ответ 6
Для дартс
///
/// Checks if these coordinates are valid geo coordinates.
/// [latitude] The latitude must be in the range [-90, 90]
/// [longitude] The longitude must be in the range [-180, 180]
/// returns [true] if these are valid geo coordinates
///
bool coordinatesValid(double latitude, double longitude) {
return (latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180);
}
///
/// Checks if the coordinates of a GeopPoint are valid geo coordinates.
/// [latitude] The latitude must be in the range [-90, 90]
/// [longitude] The longitude must be in the range [-180, 180]
/// returns [true] if these are valid geo coordinates
///
bool geoPointValid(GeoPoint point) {
return (point.latitude >= -90 &&
point.latitude <= 90 &&
point.longitude >= -180 &&
point.longitude <= 180);
}
///
/// Wraps the longitude to [-180,180].
///
/// [longitude] The longitude to wrap.
/// returns The resulting longitude.
///
double wrapLongitude(double longitude) {
if (longitude <= 180 && longitude >= -180) {
return longitude;
}
final adjusted = longitude + 180;
if (adjusted > 0) {
return (adjusted % 360) - 180;
}
// else
return 180 - (-adjusted % 360);
}
double degreesToRadians(double degrees) {
return (degrees * math.pi) / 180;
}
///
///Calculates the number of degrees a given distance is at a given latitude.
/// [distance] The distance to convert.
/// [latitude] The latitude at which to calculate.
/// returns the number of degrees the distance corresponds to.
double kilometersToLongitudeDegrees(double distance, double 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;
final radians = degreesToRadians(latitude);
final numerator = math.cos(radians) * EARTH_EQ_RADIUS * math.pi / 180;
final denom = 1 / math.sqrt(1 - E2 * math.sin(radians) * math.sin(radians));
final deltaDeg = numerator * denom;
if (deltaDeg < EPSILON) {
return distance > 0 ? 360.0 : 0.0;
}
// else
return math.min(360.0, distance / deltaDeg);
}
///
/// Defines the boundingbox for the query based
/// on its south-west and north-east corners
class GeoBoundingBox {
final GeoPoint swCorner;
final GeoPoint neCorner;
GeoBoundingBox({this.swCorner, this.neCorner});
}
///
/// Defines the search area by a circle [center] / [radiusInKilometers]
/// Based on the limitations of FireStore we can only search in rectangles
/// which means that from this definition a final search square is calculated
/// that contains the circle
class Area {
final GeoPoint center;
final double radiusInKilometers;
Area(this.center, this.radiusInKilometers):
assert(geoPointValid(center)), assert(radiusInKilometers >= 0);
factory Area.inMeters(GeoPoint gp, int radiusInMeters) {
return new Area(gp, radiusInMeters / 1000.0);
}
factory Area.inMiles(GeoPoint gp, int radiusMiles) {
return new Area(gp, radiusMiles * 1.60934);
}
/// returns the distance in km of [point] to center
double distanceToCenter(GeoPoint point) {
return distanceInKilometers(center, point);
}
}
///
///Calculates the SW and NE corners of a bounding box around a center point for a given radius;
/// [area] with the center given as .latitude and .longitude
/// and the radius of the box (in kilometers)
GeoBoundingBox boundingBoxCoordinates(Area area) {
const KM_PER_DEGREE_LATITUDE = 110.574;
final latDegrees = area.radiusInKilometers / KM_PER_DEGREE_LATITUDE;
final latitudeNorth = math.min(90.0, area.center.latitude + latDegrees);
final latitudeSouth = math.max(-90.0, area.center.latitude - latDegrees);
// calculate longitude based on current latitude
final longDegsNorth = kilometersToLongitudeDegrees(area.radiusInKilometers, latitudeNorth);
final longDegsSouth = kilometersToLongitudeDegrees(area.radiusInKilometers, latitudeSouth);
final longDegs = math.max(longDegsNorth, longDegsSouth);
return new GeoBoundingBox(
swCorner: new GeoPoint(latitudeSouth, wrapLongitude(area.center.longitude - longDegs)),
neCorner: new GeoPoint(latitudeNorth, wrapLongitude(area.center.longitude + longDegs)));
}
///
/// Calculates the distance, in kilometers, between two locations, via the
/// Haversine formula. Note that this is approximate due to the fact that
/// the Earth radius varies between 6356.752 km and 6378.137 km.
/// [location1] The first location given
/// [location2] The second location given
/// sreturn the distance, in kilometers, between the two locations.
///
double distanceInKilometers(GeoPoint location1, GeoPoint location2) {
const radius = 6371; // Earth radius in kilometers
final latDelta = degreesToRadians(location2.latitude - location1.latitude);
final lonDelta = degreesToRadians(location2.longitude - location1.longitude);
final 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));
final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
return radius * c;
}
Я только что опубликовал пакет Flutter на основе кода JS выше https://pub.dartlang.org/packages/firestore_helpers
Ответ 7
Угон этой ветки, надеюсь, поможет всем, кто еще ищет. Firestore по-прежнему не поддерживает запросы, основанные на географии, и использование библиотеки GeoFirestore от Google также не является идеальным, поскольку позволяет выполнять поиск только по местоположению, и ничего больше.
Я собрал это вместе: https://github.com/mbramwell1/GeoFire-Android
Это в основном позволяет вам делать поиск поблизости, используя местоположение и расстояние:
QueryLocation queryLocation = QueryLocation.fromDegrees(latitude, longitude);
Distance searchDistance = new Distance(1.0, DistanceUnit.KILOMETERS);
geoFire.query()
.whereNearTo(queryLocation, distance)
.build()
.get();
На репо больше документов. Это работает для меня, так что попробуйте, надеюсь, он сделает то, что вам нужно.
Ответ 8
Это еще не полностью проверено, но это должно быть немного улучшительным для ответа Райана Ли
Мой расчет более точен, и затем я фильтрую ответы на удаление ударов, попадающих в ограничительную рамку, но вне радиуса
Swift 4
func getDocumentNearBy(latitude: Double, longitude: Double, meters: Double) {
let myGeopoint = GeoPoint(latitude:latitude, longitude:longitude )
let r_earth : Double = 6378137 // Radius of earth in Meters
// 1 degree lat in m
let kLat = (2 * Double.pi / 360) * r_earth
let kLon = (2 * Double.pi / 360) * r_earth * __cospi(latitude/180.0)
let deltaLat = meters / kLat
let deltaLon = meters / kLon
let swGeopoint = GeoPoint(latitude: latitude - deltaLat, longitude: longitude - deltaLon)
let neGeopoint = GeoPoint(latitude: latitude + deltaLat, longitude: longitude + deltaLon)
let docRef : CollectionReference = appDelegate.db.collection("restos")
let query = docRef.whereField("location", isGreaterThan: swGeopoint).whereField("location", isLessThan: neGeopoint)
query.getDocuments { snapshot, error in
guard let snapshot = snapshot else {
print("Error fetching snapshot results: \(error!)")
return
}
self.documents = snapshot.documents.filter { (document) in
if let location = document.get("location") as? GeoPoint {
let myDistance = self.distanceBetween(geoPoint1:myGeopoint,geoPoint2:location)
print("myDistance:\(myDistance) distance:\(meters)")
return myDistance <= meters
}
return false
}
}
}
Функции, которые точно измеряют расстояние в метрах между 2 точками для фильтрации
func distanceBetween(geoPoint1:GeoPoint, geoPoint2:GeoPoint) -> Double{
return distanceBetween(lat1: geoPoint1.latitude,
lon1: geoPoint1.longitude,
lat2: geoPoint2.latitude,
lon2: geoPoint2.longitude)
}
func distanceBetween(lat1:Double, lon1:Double, lat2:Double, lon2:Double) -> Double{ // generally used geo measurement function
let R : Double = 6378.137; // Radius of earth in KM
let dLat = lat2 * Double.pi / 180 - lat1 * Double.pi / 180;
let dLon = lon2 * Double.pi / 180 - lon1 * Double.pi / 180;
let a = sin(dLat/2) * sin(dLat/2) +
cos(lat1 * Double.pi / 180) * cos(lat2 * Double.pi / 180) *
sin(dLon/2) * sin(dLon/2);
let c = 2 * atan2(sqrt(a), sqrt(1-a));
let d = R * c;
return d * 1000; // meters
}
Ответ 9
Да, это старая тема, но я хочу помочь только по Java-коду. Как я решил проблему с долготой? Я использовал код от Райана Ли и Майкла Тепера.
Код:
@Override
public void getUsersForTwentyMiles() {
FirebaseFirestore db = FirebaseFirestore.getInstance();
double latitude = 33.0076665;
double longitude = 35.1011336;
int distance = 20; //20 milles
GeoPoint lg = new GeoPoint(latitude, longitude);
// ~1 mile of lat and lon in degrees
double lat = 0.0144927536231884;
double lon = 0.0181818181818182;
final double lowerLat = latitude - (lat * distance);
final double lowerLon = longitude - (lon * distance);
double greaterLat = latitude + (lat * distance);
final double greaterLon = longitude + (lon * distance);
final GeoPoint lesserGeopoint = new GeoPoint(lowerLat, lowerLon);
final GeoPoint greaterGeopoint = new GeoPoint(greaterLat, greaterLon);
Log.d(LOG_TAG, "local general lovation " + lg);
Log.d(LOG_TAG, "local lesserGeopoint " + lesserGeopoint);
Log.d(LOG_TAG, "local greaterGeopoint " + greaterGeopoint);
//get users for twenty miles by only a latitude
db.collection("users")
.whereGreaterThan("location", lesserGeopoint)
.whereLessThan("location", greaterGeopoint)
.get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
@Override
public void onComplete(@NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : task.getResult()) {
UserData user = document.toObject(UserData.class);
//here a longitude condition (myLocation - 20 <= myLocation <= myLocation +20)
if (lowerLon <= user.getUserGeoPoint().getLongitude() && user.getUserGeoPoint().getLongitude() <= greaterLon) {
Log.d(LOG_TAG, "location: " + document.getId());
}
}
} else {
Log.d(LOG_TAG, "Error getting documents: ", task.getException());
}
}
});
}
Сразу после выдачи результата установите фильтр на долготу:
if (lowerLon <= user.getUserGeoPoint().getLongitude() && user.getUserGeoPoint().getLongitude() <= greaterLon) {
Log.d(LOG_TAG, "location: " + document.getId());
}
Надеюсь, это кому-нибудь поможет. Хорошего дня!
Ответ 10
Есть библиотека GeoFire для Firestore, которая называется Geofirestore: https://github.com/imperiumlabs/GeoFirestore (Отказ от ответственности: я помогал в ее разработке). Он очень прост в использовании и предлагает те же функции для Firestore, что и Geofire для Firebase Realtime DB)
Ответ 11
Самый простой способ - вычислить "гео-хеш" при сохранении местоположения в базе данных.
Географический хеш - это строка, которая представляет местоположение с определенной точностью. Чем длиннее гео-хеш, тем ближе должны быть местоположения с указанным гео-хешем. Два местоположения, например, на расстоянии 100 м друг от друга, могут иметь один и тот же 6-символьный гео-хэш, но при вычислении 7-символьного гео-хэша последний символ может отличаться.
Существует множество библиотек, позволяющих вычислять гео-хэши для любого языка. Просто сохраните его рядом с местоположением и используйте запрос ==, чтобы найти местоположения с таким же гео-хешем.