位置情報はこわくない(GAE/Go 実践編)

こんばんは。はじめまして。tarokamikazeです。
[前回](https://qiita.com/tarokamikaze/items/c40f1cfe07ee3d5282ba)に引き続き、位置情報をGAE/Goでやってみたいと思います。

## Why GAE/Go?

**わたしがすきだからです。**
というのは半分冗談で。Googleの営業のひと曰く

> GAE/Go を使いたくてGCPやGolangを使おうという案件が実際増えてます。
> 安くて早くてうまいので、実際。

というわけで、Golang使いの嗜みとしては、GAE/Go使えなきゃだめよねということです。

## 方針

* Server: GAE/Go1.11(standard)
* DB: Datastore
* Search API などという ~~ググラビリティが最悪な~~ メインDBと二重登録せにゃならんようなものは使わない
* Quadkeyを使って、クエリが貧弱なDatastoreでも位置情報検索できるところを見せてやる！

## 成果物

[こちら](https://github.com/tarokamikaze/happygeocoding)です。
「[陛下が発見したタヌキ](http://www.kunaicho.go.jp/page/ronbun/show/2)を見られるモバイルアプリ」みたいなやーつ（誰得）を想定して、バックエンドAPIを書きました。
以下で起動します。

```shell
# ローカルサーバー起動
dev_appserver.py --port=8081 --admin_port=8001  server/app.yaml 

# テストデータ投入(初回のみ)
go run tool/post.go 20

```

### 想定する使い方

```
# 結果を見る
curl http://localhost:8081/?lt=35.6949464269382,139.7493553161621&rt=35.688299,139.759852&rb=35.676834,139.758911&lb=35.682482,139.745092&lv=16
```

* lt(left-top),rt,rb,lb にモバイル画面の四隅の緯度経度をカンマ区切りで
* lvは検索するquadkeyのレベル。モバイルの場合、地図のzoom level +2 が経験上いい感じ

結果はGeoJsonを返します。それをgeojson.io にコピペした結果が以下です。

<img width="755" alt="スクリーンショット 2018-12-17 20.34.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/103900/64e5c251-289a-bb78-1271-70adcd3bc697.png">

* いびつな四角: リクエストデータの範囲
* 格子状の正方形: 検索に使用されたQuadkey範囲
* ピン: ヒットしたデータの位置情報

です。いいかんじに検索できてますね。

## ポイント

### モバイルアプリを想定すると、指定範囲が長方形とは限らない

GoogleMap等だと、画面上が北とはかぎりません。
そう、 **自分が向いてる方を画面上にする** モードとか、 **Google Earthみたいな斜め上から見た画面** ね。

それを見越して、四隅の情報をGETパラメータに含むようにしています。
画面上が北だと決め打ちできる場合は、北東と南西の緯度経度だけでも大丈夫でしょう。

### 指定範囲から検索に使うべきQuadkeyを推定してみよう

```golang
func EstimateTiles(r orb.Ring, ilv int) []QuadkeyTile {
	b := r.Bound()
	lv := maptile.Zoom(ilv)
	minTile := maptile.At(b.Min, lv)
	maxTile := maptile.At(b.Max, lv)

	// 指定レベル内でtileがひとつだけ(指定レベルが範囲に対して比較的小さい)場合は
	// 範囲内のタイルを総なめする価値がないのですぐ返す
	if reflect.DeepEqual(minTile, maxTile) {
		return []QuadkeyTile{{Tile: minTile}}
	}

	res := []QuadkeyTile{}
	minX := float64(minTile.X)
	minY := float64(minTile.Y)
	maxX := float64(maxTile.X)
	maxY := float64(maxTile.Y)
	// 範囲内のタイルを総なめしてQuadkey文字列を取り出す
	for x := math.Min(minX, maxX); x <= math.Max(minX, maxX); x++ {
		for y := math.Min(minY, maxY); y <= math.Max(minY, maxY); y++ {
			tile := QuadkeyTile{Tile: maptile.New(uint32(x), uint32(y), lv)}
			// 四隅が指定範囲に含まれているかチェック
			if !tile.IsContained(r) {
				continue
			}
			res = append(res, tile)
		}
	}
	return res
}

```

`github.com/paulmach/orb`　で Quadkeyを出力するために必要な `maptile.Tile` は、所詮地図を区切ってX,Y座標で表現した代物です。
指定範囲がわかれば、 `foreach` でぐるぐるまわすこともカンタン！

### 推定したQuadkeyでDatastoreを検索

```golang
func fetchTanukis(ctx context.Context, tiles []QuadkeyTile, r orb.Ring) ([]*entity.Tanuki, error) {
	g := goon.FromContext(ctx)
	eg := errgroup.Group{}
	mu := new(sync.Mutex)
	res := []*entity.Tanuki{}
	resMap := map[string]bool{}

	f := func(t QuadkeyTile) func() error {
		return func() error {
			qk1, qk2 := t.FixQuadkey()

			q := datastore.NewQuery(g.Kind(entity.Tanuki{})).
				Filter("Quadkey20 >=", qk1).
				Filter("Quadkey20 <", qk2)
			entities := []*entity.Tanuki{}
			if _, err := g.GetAll(q, &entities); err != nil {
				return err
			}
			mu.Lock()
			defer mu.Unlock()
			for _, e := range entities {
				if resMap[e.ID] {
					continue
				}
				//指定範囲外のたぬきは結果に含めない
				if !planar.RingContains(r, orb.Point{e.Geo.Lng, e.Geo.Lat}) {
					continue
				}
				res = append(res, e)
			}
			return nil
		}
	}

	for _, t := range tiles {
		eg.Go(f(t))
	}
	err := eg.Wait()
	return res, err
}
func (t QuadkeyTile) FixQuadkey() (string, string) {
	org := t.QuadkeyString()
	if 20 < len(org) {
		return "", ""
	}
	lv := len(org)
	// Quadkey Query Filter のMax値に利用するため、最後の数字に+1する。
	lastString := org[lv-1 : lv]
	lastInt, _ := strconv.Atoi(lastString)
	lastInt++
	res2 := org[0:lv-1] + strconv.Itoa(lastInt)

	diff := 20 - lv
	for i := 0; i < diff; i++ {
		org += "0"
		res2 += "0"
	}
	return org, res2
}
```

ポイントは `datastore.NewQuery(g.Kind(entity.Tanuki{})).Filter("Quadkey20 >=", qk1).Filter("Quadkey20 <", qk2)`。
DatastoreはLike検索はできない。が、[前方一致検索だけなら可能](https://qiita.com/ryutah/items/3235144ab822c2621b57#%E5%89%8D%E6%96%B9%E4%B8%80%E8%87%B4) なのだ。
これがQuadkeyの検索と相性最高。
あらかじめ必要な限り長いQuadkeyをDBに保存しておき(この例ではlv20)、検索したいQuadkeyのおしりに`0`を足した上で前方一致検索するのだ！

## まとめ

* [平成狸合戦ぽんぽこ](https://github.com/tarokamikaze/happygeocoding/blob/master/tool/post.go#L21)はジブリ最高傑作。異論は認める。
* GAE/Go + Datastoreだけでも位置情報検索は楽勝で実装できるよ！
* Quadkey実装とか無理せず巨人の方に乗ろう！
* ~~調子に乗ってアドベントカレンダーやるとか言わなきゃよかった~~

たぶん後日改定します。
Happy GeoCoding!