جستارهای جغرافیایی

بسیاری از برنامه‌ها اسنادی دارند که بر اساس مکان‌های فیزیکی ایندکس می‌شوند. به عنوان مثال، برنامه شما ممکن است به کاربران اجازه دهد فروشگاه های نزدیک به مکان فعلی خود را مرور کنند.

Cloud Firestore فقط به ازای هر جستار مرکب اجازه می دهد تا یک بند محدوده واحد را انجام دهیم، به این معنی که ما نمی توانیم به سادگی با ذخیره طول و عرض جغرافیایی به عنوان فیلدهای جداگانه و جست و جوی یک کادر محدود، جستارهای جغرافیایی را انجام دهیم.

راه حل: Geohashes

Geohash سیستمی برای رمزگذاری یک جفت (latitude, longitude) در یک رشته Base32 است. در سیستم Geohash جهان به یک شبکه مستطیلی تقسیم شده است. هر کاراکتر یک رشته Geohash یکی از 32 زیربخش هش پیشوند را مشخص می کند. برای مثال Geohash abcd یکی از 32 هش چهار کاراکتری است که به طور کامل در Geohash abc بزرگتر موجود است.

هرچه پیشوند مشترک بین دو هش طولانی تر باشد، آنها به یکدیگر نزدیک تر هستند. برای مثال abcdef به abcdeg نزدیکتر از abcdff است. با این حال برعکس آن درست نیست! دو ناحیه ممکن است بسیار نزدیک به یکدیگر باشند در حالی که دارای Geohashes بسیار متفاوت هستند:

ژئوهش های دور از هم

ما می توانیم از Geohashes برای ذخیره و پرس و جو اسناد بر اساس موقعیت در Cloud Firestore با کارایی معقول استفاده کنیم در حالی که فقط به یک فیلد نمایه شده نیاز داریم.

کتابخانه کمکی را نصب کنید

ایجاد و تجزیه Geohashes شامل برخی ریاضیات پیچیده است، بنابراین ما کتابخانه های کمکی برای انتزاع دشوارترین بخش ها در Android، Apple و Web ایجاد کردیم:

وب

// Install from NPM. If you prefer to use a static .js file visit
// https://github.com/firebase/geofire-js/releases and download
// geofire-common.min.js from the latest version
npm install --save geofire-common

سریع

توجه: این محصول در اهداف watchOS و App Clip موجود نیست.
// این را به Podfile pod 'GeoFire/Utils' خود اضافه کنید

Java

// Add this to your app/build.gradle
implementation 'com.firebase:geofire-android-common:3.1.0'

Geohashes را ذخیره کنید

برای هر سندی که می خواهید بر اساس مکان فهرست کنید، باید یک فیلد Geohash ذخیره کنید:

وب

// Compute the GeoHash for a lat/lng point
const lat = 51.5074;
const lng = 0.1278;
const hash = geofire.geohashForLocation([lat, lng]);

// Add the hash and the lat/lng to the document. We will use the hash
// for queries and the lat/lng for distance comparisons.
const londonRef = db.collection('cities').doc('LON');
londonRef.update({
  geohash: hash,
  lat: lat,
  lng: lng
}).then(() => {
  // ...
});

سریع

توجه: این محصول در اهداف watchOS و App Clip موجود نیست.
// Compute the GeoHash for a lat/lng point
let latitude = 51.5074
let longitude = 0.12780
let location = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)

let hash = GFUtils.geoHash(forLocation: location)

// Add the hash and the lat/lng to the document. We will use the hash
// for queries and the lat/lng for distance comparisons.
let documentData: [String: Any] = [
    "geohash": hash,
    "lat": latitude,
    "lng": longitude
]

let londonRef = db.collection("cities").document("LON")
londonRef.updateData(documentData) { error in
    // ...
}

Java

// Compute the GeoHash for a lat/lng point
double lat = 51.5074;
double lng = 0.1278;
String hash = GeoFireUtils.getGeoHashForLocation(new GeoLocation(lat, lng));

// Add the hash and the lat/lng to the document. We will use the hash
// for queries and the lat/lng for distance comparisons.
Map<String, Object> updates = new HashMap<>();
updates.put("geohash", hash);
updates.put("lat", lat);
updates.put("lng", lng);

DocumentReference londonRef = db.collection("cities").document("LON");
londonRef.update(updates)
        .addOnCompleteListener(new OnCompleteListener<Void>() {
            @Override
            public void onComplete(@NonNull Task<Void> task) {
                // ...
            }
        });

Geohashes را جویا شوید

ژئوهش‌ها به ما اجازه می‌دهند با پیوستن به مجموعه‌ای از پرس‌و‌جوها در فیلد Geohash و سپس فیلتر کردن برخی موارد مثبت کاذب، پرس‌وجوهای ناحیه را تقریبی کنیم:

وب

// Find cities within 50km of London
const center = [51.5074, 0.1278];
const radiusInM = 50 * 1000;

// Each item in 'bounds' represents a startAt/endAt pair. We have to issue
// a separate query for each pair. There can be up to 9 pairs of bounds
// depending on overlap, but in most cases there are 4.
const bounds = geofire.geohashQueryBounds(center, radiusInM);
const promises = [];
for (const b of bounds) {
  const q = db.collection('cities')
    .orderBy('geohash')
    .startAt(b[0])
    .endAt(b[1]);

  promises.push(q.get());
}

// Collect all the query results together into a single list
Promise.all(promises).then((snapshots) => {
  const matchingDocs = [];

  for (const snap of snapshots) {
    for (const doc of snap.docs) {
      const lat = doc.get('lat');
      const lng = doc.get('lng');

      // We have to filter out a few false positives due to GeoHash
      // accuracy, but most will match
      const distanceInKm = geofire.distanceBetween([lat, lng], center);
      const distanceInM = distanceInKm * 1000;
      if (distanceInM <= radiusInM) {
        matchingDocs.push(doc);
      }
    }
  }

  return matchingDocs;
}).then((matchingDocs) => {
  // Process the matching documents
  // ...
});

سریع

توجه: این محصول در اهداف watchOS و App Clip موجود نیست.
// Find cities within 50km of London
let center = CLLocationCoordinate2D(latitude: 51.5074, longitude: 0.1278)
let radiusInM: Double = 50 * 1000

// Each item in 'bounds' represents a startAt/endAt pair. We have to issue
// a separate query for each pair. There can be up to 9 pairs of bounds
// depending on overlap, but in most cases there are 4.
let queryBounds = GFUtils.queryBounds(forLocation: center,
                                      withRadius: radiusInM)
let queries = queryBounds.map { bound -> Query in
    return db.collection("cities")
        .order(by: "geohash")
        .start(at: [bound.startValue])
        .end(at: [bound.endValue])
}

var matchingDocs = [QueryDocumentSnapshot]()
// Collect all the query results together into a single list
func getDocumentsCompletion(snapshot: QuerySnapshot?, error: Error?) -> () {
    guard let documents = snapshot?.documents else {
        print("Unable to fetch snapshot data. \(String(describing: error))")
        return
    }

    for document in documents {
        let lat = document.data()["lat"] as? Double ?? 0
        let lng = document.data()["lng"] as? Double ?? 0
        let coordinates = CLLocation(latitude: lat, longitude: lng)
        let centerPoint = CLLocation(latitude: center.latitude, longitude: center.longitude)

        // We have to filter out a few false positives due to GeoHash accuracy, but
        // most will match
        let distance = GFUtils.distance(from: centerPoint, to: coordinates)
        if distance <= radiusInM {
            matchingDocs.append(document)
        }
    }
}

// After all callbacks have executed, matchingDocs contains the result. Note that this
// sample does not demonstrate how to wait on all callbacks to complete.
for query in queries {
    query.getDocuments(completion: getDocumentsCompletion)
}

Java

// Find cities within 50km of London
final GeoLocation center = new GeoLocation(51.5074, 0.1278);
final double radiusInM = 50 * 1000;

// Each item in 'bounds' represents a startAt/endAt pair. We have to issue
// a separate query for each pair. There can be up to 9 pairs of bounds
// depending on overlap, but in most cases there are 4.
List<GeoQueryBounds> bounds = GeoFireUtils.getGeoHashQueryBounds(center, radiusInM);
final List<Task<QuerySnapshot>> tasks = new ArrayList<>();
for (GeoQueryBounds b : bounds) {
    Query q = db.collection("cities")
            .orderBy("geohash")
            .startAt(b.startHash)
            .endAt(b.endHash);

    tasks.add(q.get());
}

// Collect all the query results together into a single list
Tasks.whenAllComplete(tasks)
        .addOnCompleteListener(new OnCompleteListener<List<Task<?>>>() {
            @Override
            public void onComplete(@NonNull Task<List<Task<?>>> t) {
                List<DocumentSnapshot> matchingDocs = new ArrayList<>();

                for (Task<QuerySnapshot> task : tasks) {
                    QuerySnapshot snap = task.getResult();
                    for (DocumentSnapshot doc : snap.getDocuments()) {
                        double lat = doc.getDouble("lat");
                        double lng = doc.getDouble("lng");

                        // We have to filter out a few false positives due to GeoHash
                        // accuracy, but most will match
                        GeoLocation docLocation = new GeoLocation(lat, lng);
                        double distanceInM = GeoFireUtils.getDistanceBetween(docLocation, center);
                        if (distanceInM <= radiusInM) {
                            matchingDocs.add(doc);
                        }
                    }
                }

                // matchingDocs contains the results
                // ...
            }
        });

محدودیت ها

استفاده از Geohashes برای جستجوی مکان‌ها قابلیت‌های جدیدی به ما می‌دهد، اما مجموعه‌ای از محدودیت‌های خاص خود را دارد:

  • موارد مثبت کاذب - پرس و جو توسط Geohash دقیق نیست، و شما باید نتایج مثبت کاذب را در سمت مشتری فیلتر کنید. این خواندن های اضافی هزینه و تاخیر را به برنامه شما اضافه می کند.
  • موارد لبه - این روش پرس و جو بر تخمین فاصله بین خطوط طول و عرض جغرافیایی متکی است. دقت این تخمین با نزدیک‌تر شدن نقاط به قطب شمال یا جنوب کاهش می‌یابد که به این معنی است که جستارهای Geohash در عرض‌های جغرافیایی شدید، مثبت کاذب بیشتری دارند.