Firestoreで一定距離内にいるユーザーetc..をMap上に表示させたい



位置情報を用いたサービスをFirestoreで作りたい時、
「一定距離内にいるユーザーetc..をMap上に表示させたい」
ということがあります。

今回Firestoreで実現する際に少し手間取ったのでまとめます

# 1. 【事前知識】Firestoreの位置情報
Firestoreの位置情報はGeopointという形で保管されています
緯度経度の位置情報が保管される値です。普通にFirestoreに位置情報を保管する場合こちらを使うことになります。

https://developers.google.com/android/reference/com/google/firebase/firestore/GeoPoint

# 2. Geopointの問題点
緯度と経度を複合条件にした、検　索　が　で　き　な　い...


下記はjsSDKのissueですが、Geopointについてfirebaseのメンバーからこんなコメントが
https://github.com/firebase/firebase-js-sdk/issues/826

```
Sorry, we have nothing to announce at this time.

I don't have a good written summary of this, but we did announce a few things at Cloud Next '18--these things are just around the corner. Nearly all of our effort lately has been focused on transitioning from a limited beta to something much more broadly useful. I realize this is no consolation for this specific feature, but at least you can see we've been busy :-).
```

雑訳
”今それより優先することあるから時間ないよ！”

ということで「FireStore上で位置情報を元に検索するロジック」を別で作ることに

# 3. 【解決策】ライブラリGeoFirestoreでFireStore上で位置情報を元にドキュメントを検索する
 
    
一旦コードを書きます

宣言周り

```swift
import Geofirestore

// Firestore上で位置情報とドキュメントの組み合わせを保管したい場所
let geoFirestore:GeoFirestore = GeoFirestore(collectionRef: Firestore.UserLocation.reference)
```


位置情報とドキュメントの組み合わせを保存

```swift
          geoFirestore.setLocation(location: location, forDocumentWithID: user.id) { (err) in
                if let error = err {
                    print("ERROR CREATING DOCUMENT")
                    print(error)
                }
          }
```

ドキュメントの取得処理

```swift
        geoFirestore.query(withCenter: location, radius: radius).observe(.documentEntered, with: { (key, location) in
            // keyには該当する位置情報のドキュメントIDが入ってくる                                                                                   
            if let key = key, let _ = location {
                Firestore.User.get(key, block: { (user, error) in
                    if let user = user {
                       let annotation = MKPointAnnotation()
                            annotation.coordinate = location.coordinate
                            annotation.title = key
                        debugPrint("[documentEntered] Added userId : \(key)!")
                    }
                })

            }
        })

        geoFirestore.query(withCenter: location, radius: radius).observe(.documentMoved, with: { (key, location) in
            if let key = key, let _ = location {
                Firestore.User.get(key, block: { (user, error) in
                    if let user = user {
                        if let annotation =
                            self.mapView.annotations.first(where: { (annotation) -> Bool in
                                return annotation.title == key
                            })
                        {
                            self.mapView.removeAnnotation(annotation)
                            debugPrint("[documentMoved] Removed userId : \(key)!")
                            self.mapView.addAnnotation(annotation)
                            debugPrint("[documentMoved] Added userId : \(key)!")
                        }
                    }
                })

            }
        })

        //
        geoFirestore.query(withCenter: location, radius: radius).observe(.documentExited, with: { (key, location) in
            if let key = key {

                if key == Auth.auth().currentUser?.uid { return }
                
                Firestore.User.get(key, block: { (user, error) in
                    if let user = user {
                        if let annotation =
                            self.mapView.annotations.first(where: { (annotation) -> Bool in
                                return annotation.title == key
                            })
                        {
                            self.mapView.removeAnnotation(annotation)
                            debugPrint("[documentExited] Removed userId \(key)!")
                        }
                    }
                })
            }
        })
        
```


    
    
    
## 4. 【解決策】GeoFirestore　宣言編 

宣言編です
importと
位置情報とドキュメントの組み合わせを保管しておくFirestoreのドキュメントを宣言する必要があります。


```swift
import Geofirestore

// FireStore上で位置情報とドキュメントの組み合わせを保管したい場所
let geoFirestore:GeoFirestore = GeoFirestore(collectionRef: Firestore.UserLocation.reference)
```


## 5. 【解決策】GeoFirestore　位置情報保存編

位置情報とドキュメントの組み合わせを保存しています。
上手くいくとFireStore上で下記のように保存されます

```swift
          geoFirestore.setLocation(location: location, forDocumentWithID: user.id) { (err) in
                if let error = err {
                    print("ERROR CREATING DOCUMENT")
                    print(error)
                }
          }
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/55864/2a875474-7e13-d49a-ece5-0ed3a145b4c9.png)


## 6. 【解決策】GeoFirestore　ドキュメント取得編

ドキュメントの取得処理　は
* 新たに取得データが増えた時  --> .documentEntered
* 取得データが更新された時  ---> .documentMoved
* 取得できていたデータが見えなくなった時  ---> .documentExited
の3パターンの処理が必要なようです。

下記のSampleではannotationのtitleにドキュメントIDを保存して、それを元に削除の処理を行なっています。


ドキュメントの取得処理

```swift
        geoFirestore.query(withCenter: location, radius: radius).observe(.documentEntered, with: { (key, location) in
            // keyには該当する位置情報のドキュメントIDが入ってくる                                                                                   
            if let key = key, let location = location {
                Firestore.User.get(key, block: { (user, error) in
                    if let user = user {
                       let annotation = MKPointAnnotation()
                            annotation.coordinate = location.coordinate
                            annotation.title = key
                        debugPrint("[documentEntered] Added userId : \(key)!")
                    }
                })

            }
        })

        geoFirestore.query(withCenter: location, radius: radius).observe(.documentMoved, with: { (key, location) in
            if let key = key, let location = location {
                Firestore.User.get(key, block: { (user, error) in
                    if let user = user {
                        if let annotation =
                            self.mapView.annotations.first(where: { (annotation) -> Bool in
                                return annotation.title == key
                            })
                        {
                            self.mapView.removeAnnotation(annotation)
                            debugPrint("[documentMoved] Removed userId : \(key)!")
                          　annotation.coordinate = location.coordinate
                            self.mapView.addAnnotation(annotation)
                            debugPrint("[documentMoved] Added userId : \(key)!")
                        }
                    }
                })

            }
        })

        //
        geoFirestore.query(withCenter: location, radius: radius).observe(.documentExited, with: { (key, location) in
            if let key = key {

                if key == Auth.auth().currentUser?.uid { return }
                
                Firestore.User.get(key, block: { (user, error) in
                    if let user = user {
                        if let annotation =
                            self.mapView.annotations.first(where: { (annotation) -> Bool in
                                return annotation.title == key
                            })
                        {
                            self.mapView.removeAnnotation(annotation)
                            debugPrint("[documentExited] Removed userId \(key)!")
                        }
                    }
                })
            }
        })
        
```


以上で「Firestore上で位置情報を元に検索するロジック」ができました。

# 7. 備考
   GeoFirestoreはiosだけでなく、Androidやjs版もあるので、同じ考え方でできると思います。
　　　　　　(Firestore標準機能にならないかなぁ...)
   https://github.com/imperiumlabs/GeoFirestore-IOS

　　　　　　また今回はPringというFirestoreを扱う際のライブラリを使っています。
   https://github.com/1amageek/Pring

# 8. サンプル

`余裕があればどっかで作ります...`


# 9. 参考

Firestore を扱う際の便利なライブラリ
https://qiita.com/1amageek/items/ca7574698c50cc076347
GeoFirestore ios版
https://github.com/imperiumlabs/GeoFirestore-IOS
firebase時間無い宣言issue
https://github.com/firebase/firebase-js-sdk/issues/826