※GAE/GOの入門記事も書いてみたので、はじめての方は以下を参考にしていただければ幸いです。
http://www.apps-gcp.com/gae-go-gettingstart-01/
はじめに
少し前の話ですが、大学の仲間と1泊2日の開発合宿をおこないました。そのときにつくった「水族館マップ」というアプリの作成話を書かせていただきます。1日じゃなくて2日では?と突っ込まれそうですが開発時間的には24時間未満なので1日・・・ということにさせていただきます。ちなみに水族館をテーマに選んだのは、水族館巡りが個人的な趣味だからです。行った水族館にチェックを入れて、自分だけの水族館制覇マップをつくるのが目的でした。
GAE/Go + Titaniumを開発言語に選んだ理由
それぞれに理由があります。
GAE/Goを選んだ理由
1日という短い期間内でアプリをつくれ、と言われたらGAE以上に最適なものは他にないと考えているのでプラットフォームはGAE以外選択肢はありませんでした。言語にGOを選んだ理由は、ちょうどそのときに@ttyokoyamaさんのGAE/GO本を読んでいて使ってみたかったからです。
Titaniumを選んだ理由
ただ、GOを利用して何かをつくるのは初めてだったこともあり時間的コストを考慮した結果、JavaScriptだけでスピーディに開発がおこなえるTitaniumを選びました。
まずはデータを集めたいが・・・
当たり前ですが、まずはデータが必要でした。宿泊するホテルに向かう途中、車に揺られながら以下の方法で水族館情報(水族館名称・住所・緯度経度等)を得られないか考えました。
1. RSSで全国の水族館名のリストを取得
2. GoogleMapsAPIを利用して水族館名から住所・緯度経度を取得
しかし・・・調べた結果都合よく(1)のようなRSSは見つかりませんでした。
結局手動で集める
結局以下の手段で水族館データを集めました。あまり手間はかからなかったので最初
からこのやり方で十分でした。
1. wikipediaの「日本の水族館」ページから水族館リストをコピペ
2. エディタに貼り付け
3. 正規表現でカンマ区切りのデータリストに変換
水族館名からGPS情報を得る
データの収集が終わったら今度はGoogle Maps APIのジオコーディングサービスを利用して水族館のGPS情報を取得します。以下は水族館名からGPS情報を取得するためのコードとなります。GPSの取得後、後述するGOプログラムが動作するURLにパラメータをPOSTしています。
/**
* ジオコーディング
*/
function getGeoInfo(name) {
geocoder.geocode({
'address' : name
}, function(results, status) {
if (status == google.maps.GeocoderStatus.OK) {
var location = String(results[0].geometry.location);
var gps = location.substring(1,
location.length - 1);
var latlng = gps.split(',');
var address = results[0].formatted_address.split(',')[1];
save('name=' + name + '&lat=' + latlng[0] + '&lng='
+ latlng[1] + '&address=' + address);
} else {
console.error('Geocode was not successful for the following reason: '
+ status);
alert('「' + name + '」という名の水族館は存在しません。');
}
});
}
/**
* GPS保存
*/
function save(param) {
var xmlhttp = createHttpRequest();
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == 4) {
setTimeout("showPins()", 500);
}
}
xmlhttp.open('POST', '/save', true);
xmlhttp.setRequestHeader("Content-Type",
"application/x-www-form-urlencoded");
xmlhttp.send(param);
}
GPS情報を保存する
ここからがGOの出番です。クライアント側からのリクエストパラメータをデータストアに保存します。以下がそのコードとなります。
package main
import (
"appengine"
"appengine/user"
"fmt"
"html/template"
"net/http"
"pkg/db"
)
func init() {
http.HandleFunc("/", index)
http.HandleFunc("/save", save)
http.HandleFunc("/list", list)
}
func index(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
u := user.Current(c)
if u == nil {
url, err := user.LoginURL(c, r.URL.String())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Location", url)
w.WriteHeader(http.StatusFound)
return
}
var mapTemplate = template.Must(template.ParseFiles("template/map.html"))
if err := mapTemplate.Execute(w, nil); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func save(w http.ResponseWriter, r *http.Request) {
var name = r.FormValue("name")
var lat = r.FormValue("lat")
var lng = r.FormValue("lng")
var address = r.FormValue("address")
c := appengine.NewContext(r)
err := db.Save(name, address, lat, lng, c)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func list(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
gpsList, err := db.List(c)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "%s", gpsList)
}
package db
import (
"appengine"
"appengine/datastore"
"time"
"encoding/json"
)
type Aquarium struct {
Key *datastore.Key `json:"key"`
Name string `json:"name"`
Address string `json:"address"`
Lat string `json:"lat"`
Lng string `json:"lng"`
Date time.Time `json:"date"`
}
func Save(name string, address string, lat string, lng string, c appengine.Context)(err error) {
aquarium := Get(name,c)
if(aquarium==nil) {
aquarium := Aquarium{
Name: name,
Lat: lat,
Lng: lng,
Address: address,
Date: time.Now(),
}
_, err = datastore.Put(c, datastore.NewIncompleteKey(c, "Aquarium", nil), &aquarium)
if err != nil {
c.Infof("insert error %v", err.Error())
return err
}
return nil
}
aquarium.Date = time.Now()
aquarium.Address = address
aquarium.Lat = lat
aquarium.Lng = lng
aquarium.Name = name
_, err = datastore.Put(c, aquarium.Key, &aquarium)
if err != nil {
c.Infof("update error %v", err.Error())
return err
}
return nil
}
func Get(name string, c appengine.Context) *Aquarium {
q := datastore.NewQuery("Aquarium").Filter("Name =", name).Limit(1)
var aquarium []*Aquarium
if _, err := q.GetAll(c, &aquarium); err != nil {
c.Infof("error %v", err.Error())
return nil
}
if len(aquarium)>0 {
return aquarium[0]
}
return nil
}
func List(c appengine.Context)(resultJson []byte, err error) {
q := datastore.NewQuery("Aquarium").Order("-Date")
gpsList := make([]Aquarium, 0, 200)
if _, err := q.GetAll(c, &gpsList); err != nil {
return nil, err
}
resultJson, err = json.Marshal(gpsList)
if err != nil {
return nil, err
}
return resultJson, nil
}
デモ
以下はデモページとなります。水族館名を入力し「GPS保存」ボタンを押下するとデータストアに水族館のGPS情報が保存されます。
iPhoneアプリをつくる
↓な感じのUIをつくりました。インポートされた水族館情報は以下のように表示されるようになります。今回はGAE/Gメインの説明なのでTitanium側のコードの説明は省かせていただきますが、もう少ししたらgithubに公開しようと思います。
まとめ
(後半はかなりいいかげんな説明になってしまいましたが)以上で話は終了です。今回のAdvent Calendarでとにかく訴えたかったことはGCP(GAE)は最高である、ということです。この「水族館マップ」はお酒を飲みながらグダグダ作成しましたが数時間程度で作ることができました。これも全てGCPというエンジニア指向のプラットフォームのおかげだと思います。
ちなみにこの「水族館マップ」は今月初旬にAppStoreにリリースしたのですがクラッシュするバグを見つけてしまい、ストアから削除しました・・・こちらのバグの修正が終わったら再度リリースしたいと思っています。