こんばんは。はじめまして。tarokamikazeです。
前回に引き続き、位置情報を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でも位置情報検索できるところを見せてやる!
成果物
こちらです。
「陛下が発見したタヌキを見られるモバイルアプリ」みたいなやーつ(誰得)を想定して、バックエンドAPIを書きました。
以下で起動します。
# ローカルサーバー起動
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 にコピペした結果が以下です。
- いびつな四角: リクエストデータの範囲
- 格子状の正方形: 検索に使用されたQuadkey範囲
- ピン: ヒットしたデータの位置情報
です。いいかんじに検索できてますね。
ポイント
モバイルアプリを想定すると、指定範囲が長方形とは限らない
GoogleMap等だと、画面上が北とはかぎりません。
そう、 自分が向いてる方を画面上にする モードとか、 Google Earthみたいな斜め上から見た画面 ね。
それを見越して、四隅の情報をGETパラメータに含むようにしています。
画面上が北だと決め打ちできる場合は、北東と南西の緯度経度だけでも大丈夫でしょう。
指定範囲から検索に使うべきQuadkeyを推定してみよう
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を検索
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検索はできない。が、前方一致検索だけなら可能 なのだ。
これがQuadkeyの検索と相性最高。
あらかじめ必要な限り長いQuadkeyをDBに保存しておき(この例ではlv20)、検索したいQuadkeyのおしりに0
を足した上で前方一致検索するのだ!
まとめ
- 平成狸合戦ぽんぽこはジブリ最高傑作。異論は認める。
- GAE/Go + Datastoreだけでも位置情報検索は楽勝で実装できるよ!
- Quadkey実装とか無理せず巨人の方に乗ろう!
調子に乗ってアドベントカレンダーやるとか言わなきゃよかった
たぶん後日改定します。
Happy GeoCoding!