LoginSignup
5
4

More than 5 years have passed since last update.

こんばんは。はじめまして。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 にコピペした結果が以下です。

スクリーンショット 2018-12-17 20.34.13.png

  • いびつな四角: リクエストデータの範囲
  • 格子状の正方形: 検索に使用された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!

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4