TILE38紹介（4） ：Goプログラムからのアクセス2

|TILE38紹介|
|:---|
|[TILE38紹介（1） ：基本コマンド](https://qiita.com/tesujiro/items/69f8970bd69d501cb0c8)|
|[TILE38紹介（2） ：空間系コマンド](https://qiita.com/tesujiro/items/0a998f2c6e3dd49d7429)|
|[TILE38紹介（3） ：Goプログラムからのアクセス1](https://qiita.com/tesujiro/items/fa7dfc079a94824cc77e)|
|[TILE38紹介（4） ：Goプログラムからのアクセス2](https://qiita.com/tesujiro/items/d9bf26233ce96bc3596f)|
|[TILE38紹介（5） ：Geofencing](https://qiita.com/tesujiro/items/e4b5a3df174e69516553)|

# TL;DR
- TILE38を使用したアプリケーションの簡単な実装例として、検索の実装を説明する。
- ここではSCAN関数を使用したが、これまでに説明した他の検索コマンド（NEARBY,WITHIN,INTERSECTSなど)も基本的には実装方法は同じである。
- サンプルとしては若干プログラムが長いが、検索結果の編集処理が２段階（redigoでコマンド実行結果からJSON文字列の抽出、JSON文字列から構造体への変換）となっているためである。
- GeoJsonとしてはFeatureや各種Collectionといったタイプがあるが、ここではGeometryタイプのオブジェクト（Point,LineString,Polygon）のみに対応している。（拡張できる。）

# データ登録
- tile38データベースへのレコード登録については[前回](https://qiita.com/tesujiro/items/fa7dfc079a94824cc77e)と変わらない。

```go:main.go
        // db connect
        c, err := redis.Dial("tcp", ":9851")
        if err != nil {
                log.Fatalf("Could not connect: %v\n", err)
        }
        defer c.Close()

        // SET fleet
        for _, data := range [][]interface{}{
                {"fleet", "id1", "FIELD", "start", "123456", "FIELD", "end", "789012", "POINT", 35.6581, 139.6975},
                {"fleet", "id2", "OBJECT", `{"type":"Point","coordinates":[139.6975,35.6581]}`},
                {"fleet", "id3", "OBJECT", `{"type":"LineString","coordinates":[[139.6975,35.6581],[1,1],[2,2]]}`},
                {"fleet", "id4", "POINT", 35.6581, 139.6975},
        } {
                ret, err := c.Do("SET", data...)
                if err != nil {
                        log.Fatalf("Could not SET: %v\n", err)
                }
                fmt.Printf("SET ret:%#s\n", ret)
        }
```
- SETコマンドによって４個のオブジェクトを登録した。
- 単純なTILE38のPOINT（点）オブジェクト以外に、GeoJsonオブジェクトとしての点や線やGeoJson以外の付加的情報としてFIELD情報を持ったレコードを登録した。

# 検索実行〜結果の抽出
- 検索コマンドの実行はSET,GETと同様にConnection.Do関数を使用する。
- Connection.Doの結果はinterface{}型として定義されており、redigoでは結果集合を扱うための各種ヘルパー関数が提供されている。(タイプアサーションによって変換しながらオブジェクト集合を辿っていくことは可能ですが、ここでは少しでも可読性を上げるためヘルパー関数を使用します。）

```go:main.go
        // SCAN fleet
        results, err := redis.Values(c.Do("SCAN", "fleet"))
        if err != nil {
                log.Fatalf("Could not SCAN: %v\n", err)
        }

        var cursor int
        var members []interface{}
        _, err = redis.Scan(results, &cursor, &members)
        if err != nil {
                fmt.Printf("scan result error: %v", err)
                return
        }

        for len(members) > 0 {
                // pick up one record from results as []interface{}
                var object []interface{}
                members, err = redis.Scan(members, &object)
                if err != nil {
                        fmt.Printf("scan record error: %v", err)
                        return
                }
                // scan columns from one record -> [id,json],fields
                var id []byte
                var json []byte
                others, err := redis.Scan(object, &id, &json)
                if err != nil {
                        fmt.Printf("scan columns error: %v", err)
                        return
                }

                // unmarshal geojson string to struct
                gjm, err := unmarshalSingleResult(json)
                if err != nil {
                        fmt.Printf("unmarshal json error: %v", err)
                        return
                }
                fmt.Printf("id:%s  json:%s  others:%s\n", id, gjm, others)
        }
```
- まず最初にDO関数が返却した結果を配列に変換する（）ためにValues関数で変換する。（結果配列）
- 結果配列の第一要素は「開始位置のカーソル番号」、第二要素は「結果レコードの集合」であるためScan関数を使用してそれぞれの要素に分けて格納する。（カーソル番号は大量データの検索の場合には使用するが、今回は件数が少ないので固定値のゼロが入る。）
- 結果レコードの集合について以下を繰り返す。

1. 結果集合からの１件の取得。（ループ内の最初のScan関数呼び出し）
1. １レコードからの必須項目(idおよびJSONオブジェクト）の抽出。（ループ内の２番目のScan関数呼び出し）

- Scan関数は戻り値として、読み込み終わった次の要素から始まるスライスを返す。これによってループ内の次のレコードの取得や必須項目以外の任意の項目などを扱うことができる。
- ループ内でJSONを[]byte型で取得することができたので、次に構造体への変換を説明する。

# JSON文字列から構造体への変換
- JSON文字列から変換するための構造体を定義する。
- 実際の変換ロジックはgoのjsonライブラリを使用するため以下のような定義とした。

```go:main.go
type GeoJsonMember struct {
        Type           string          `json:"type"`
        CoordinatesRaw json.RawMessage `json:"coordinates,omitempty"`
        CoordinatesObj interface{}     `json:"-"`
}

func (member *GeoJsonMember) String() string {
        return fmt.Sprintf("%s %v", member.Type, member.CoordinatesObj)
}

type Point [2]float64

type LineString []Point

type Polygon []LineString
```
- GeoJsonのフォーマットとしてはGeometryオブジェクトの場合には"type"と"coordinates"と言う属性を持つ。
- type属性は文字列で"Point""LineString""Polygon"のいずれかを持つことにする。（単純化するためにその他のタイプは想定しないが、Featureや各種Collectionを想定した場合には構造体のメンバーも追加することになる。）
- Coordinates属性に持つ値はオブジェクトのタイプ（Point,LineString,Polygon）によってフォーマット（配列の次元）が変わる。このためにjson.RawMessage型([]byte)で一旦格納し、実態のオブジェクトはinterface{}型として別メンバーとして持つ。
- Coordinatesの実態のオブジェクトとしては、それぞれPoint,LineString,Polygonの型を用意した。
- []byte型のJSONオブジェクトをこの構造体に変換するために以下の二つの関数を定義した。

```go:main.go
func (member *GeoJsonMember) setCoordinates() error {
        var coordinates interface{}
        switch member.Type {
        case "Point":
                coordinates = new(Point)
        case "LineString":
                coordinates = new(LineString)
        case "Polygon":
                coordinates = new(Polygon)
        default:
                return fmt.Errorf("Unknown type: %v", member.Type)
        }
        err := json.Unmarshal(member.CoordinatesRaw, &coordinates)
        if err != nil {
                return fmt.Errorf("json.Unmarshal error: %v", err)
        }
        member.CoordinatesObj = coordinates
        return nil
}

func unmarshalSingleResult(shapes []byte) (*GeoJsonMember, error) {
        var member GeoJsonMember
        err := json.Unmarshal(shapes, &member)
        if err != nil {
                return nil, fmt.Errorf("Unmarshal error: %v", err)
        }

        err = member.setCoordinates()
        if err != nil {
                return nil, fmt.Errorf("type:%v coordinates:%v err:%v\n", member.Type, member.CoordinatesRaw, err)
        }
        return &member, nil
}
```

- unmarshalSingleResult関数は単体のGeoJsonバイト列からGeoJsonMemberに変換し、構造体を返却する関数である。
- go標準のjson.Unmarshalによって構造体への変換を行うが、RawMessage型で定義されたCoordinatesRawには該当部分のJSONバイト配列がそのまま設定される。このため、setCoordinates関数によってRawMessageからオブジェクトに変換して、CoordinatesObjに設定している。

# 実行結果

```bash
$ go run main.go
SET ret:OK
SET ret:OK
SET ret:OK
SET ret:OK
id:id1  json:Point &[139.6975 35.6581]  others:[[end 789012 start 123456 time 123456]]
id:id2  json:Point &[139.6975 35.6581]  others:[]
id:id3  json:LineString &[[139.6975 35.6581] [1 1] [2 2]]  others:[]
id:id4  json:Point &[139.6975 35.6581]  others:[]
$ 
```
- SCAN結果のオブジェクトが変換され表示される。
- Point型、LineString型、Polygon型に対応している。（Polygonはサンプルに含まれないが。）
- TILE38でFIELDとして設定した値も扱うことができる。

# 全体ソース
```go:main.go
package main

import (
        "encoding/json"
        "fmt"
        "log"

        "github.com/garyburd/redigo/redis"
)

type GeoJsonMember struct {
        Type           string          `json:"type"`
        CoordinatesRaw json.RawMessage `json:"coordinates,omitempty"`
        CoordinatesObj interface{}     `json:"-"`
}

type Point [2]float64

type LineString []Point

type Polygon []LineString

func (member *GeoJsonMember) String() string {
        return fmt.Sprintf("%s %v", member.Type, member.CoordinatesObj)
}

func (member *GeoJsonMember) setCoordinates() error {
        var coordinates interface{}
        switch member.Type {
        case "Point":
                coordinates = new(Point)
        case "LineString":
                coordinates = new(LineString)
        case "Polygon":
                coordinates = new(Polygon)
        default:
                return fmt.Errorf("Unknown type: %v", member.Type)
        }
        err := json.Unmarshal(member.CoordinatesRaw, &coordinates)
        if err != nil {
                return fmt.Errorf("json.Unmarshal error: %v", err)
        }
        member.CoordinatesObj = coordinates
        return nil
}

func unmarshalMultiResults(shapes []byte) ([]*GeoJsonMember, error) {
        var members []*GeoJsonMember
        err := json.Unmarshal(shapes, &members)
        if err != nil {
                return nil, fmt.Errorf("Unmarshal error: %v", err)
        }

        for i, member := range members {
                err := member.setCoordinates()
                if err != nil {
                        return nil, fmt.Errorf("member[%v]:type:%v coordinates:%v err:%v\n", i, member.Type, member.CoordinatesRaw, err)
                }
        }
        return members, nil
}

func unmarshalSingleResult(shapes []byte) (*GeoJsonMember, error) {
        var member GeoJsonMember
        err := json.Unmarshal(shapes, &member)
        if err != nil {
                return nil, fmt.Errorf("Unmarshal error: %v", err)
        }

        err = member.setCoordinates()
        if err != nil {
                return nil, fmt.Errorf("type:%v coordinates:%v err:%v\n", member.Type, member.CoordinatesRaw, err)
        }
        return &member, nil
}

func main() {
        // db connect
        c, err := redis.Dial("tcp", ":9851")
        if err != nil {
                log.Fatalf("Could not connect: %v\n", err)
        }
        defer c.Close()

        // SET fleet
        for _, data := range [][]interface{}{
                {"fleet", "id1", "FIELD", "start", "123456", "FIELD", "end", "789012", "POINT", 35.6581, 139.6975},
                {"fleet", "id2", "OBJECT", `{"type":"Point","coordinates":[139.6975,35.6581]}`},
                {"fleet", "id3", "OBJECT", `{"type":"LineString","coordinates":[[139.6975,35.6581],[1,1],[2,2]]}`},
                {"fleet", "id4", "POINT", 35.6581, 139.6975},
        } {
                ret, err := c.Do("SET", data...)
                if err != nil {
                        log.Fatalf("Could not SET: %v\n", err)
                }
                fmt.Printf("SET ret:%#s\n", ret)
        }

        // SCAN fleet
        results, err := redis.Values(c.Do("SCAN", "fleet"))
        if err != nil {
                log.Fatalf("Could not SCAN: %v\n", err)
        }

        var cursor int
        var members []interface{}
        _, err = redis.Scan(results, &cursor, &members)
        if err != nil {
                fmt.Printf("scan result error: %v", err)
                return
        }

        for len(members) > 0 {
                // pick up one record from results as []interface{}
                var object []interface{}
                members, err = redis.Scan(members, &object)
                if err != nil {
                        fmt.Printf("scan record error: %v", err)
                        return
                }
                // scan columns from one record -> [id,json],fields
                var id []byte
                var json []byte
                others, err := redis.Scan(object, &id, &json)
                if err != nil {
                        fmt.Printf("scan columns error: %v", err)
                        return
                }

                // unmarshal geojson string to struct
                gjm, err := unmarshalSingleResult(json)
                if err != nil {
                        fmt.Printf("unmarshal json error: %v", err)
                        return
                }
                fmt.Printf("id:%s  json:%s  others:%s\n", id, gjm, others)
        }
}
```