過去にアイドルが居た場所に近づくとスマホが震えるアプリ

#概要
都市の幽霊をコンセプトにしたアイドルのグループの[・・・・・・・・・](https://twitter.com/dotstokyo)は、androidアプリ「都市の幽霊観測装置」をリリースしました。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Androidアプリ、都市の幽霊観測装置ができました🎉<br>このアプリを入れて過去に・ちゃんが居た場所に近づくと、スマホが震えるよ😧<br><br>アプリはここから入手❗️<a href="https://t.co/OL0xXHxyYQ">https://t.co/OL0xXHxyYQ</a><br><br>解説記事<a href="https://t.co/HAKXNW9jdQ">https://t.co/HAKXNW9jdQ</a> <a href="https://t.co/1xbGV548qN">pic.twitter.com/1xbGV548qN</a></p>&mdash; ・・・・・・・・・運営 (@tokyo_tsukurou) <a href="https://twitter.com/tokyo_tsukurou/status/918473976757628935?ref_src=twsrc%5Etfw">2017年10月12日</a></blockquote> <script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

ダウンロードはこちらから
[都市の幽霊観測装置 - Google Play](https://play.google.com/store/apps/details?id=dotstokyo.ghostinthecity)

このアプリをインストールして「過去にアイドルが居た場所」に近づくと、振動と通知で「そこにアイドルが居たよ」と教えてくれます。

![コンセプトイメージ.001.jpeg](https://qiita-image-store.s3.amazonaws.com/0/133133/aa86335c-d10a-b31d-8dc3-ede808259bce.jpeg)

実際の通知画面はこちら。「・ちゃん」とは・・・・・・・・・メンバーの呼称です。

![通知.png](https://qiita-image-store.s3.amazonaws.com/0/133133/9cca2eee-b44a-d1ce-b3c3-74f8782f261c.png)

#なぜこのアプリを作ったのか

・・・・・・・・・は「都市の幽霊」「東京そのものになる」というコンセプトを実現するために、これまでもビーコンや位置情報を使ったイベントを行ってきましたが、いずれも「モニターをガン見しながら」「こちらから探しにいく」プレイスタイルで、「都市に・ちゃんがぷかぷか幽霊のように浮いている」「アイドルをまとう」「日常に・ちゃんが混じる」といった世界観を、完全には表現できていませんでした。

>LINE Beaconを持ったアイドルを探し回るイベント
http://qiita.com/a-r-i/items/8987a808f60a294e894b

>アイドルが居る方角を指し示すコンパス
http://qiita.com/a-r-i/items/1389397ceeeff19b4e02

いっぽう都市の幽霊観測装置では**ただ日常生活を送っているだけで、・ちゃんを観測できます**。
必要なのはアプリのインストールと起動だけ。
あとは受動的に、これまで通り生きる。
モニターを見る必要はありません。
**土地に染み付いた震え**で・ちゃんを感じる。
地表に「・ちゃんのGPSログという新しいレイヤー」が重なった世界で、いつもとかわらず過ごすだけ。

同じジオ・アプリのポケモンGO!と都市の幽霊観測装置を比較してみると、その特徴が際立ちます。

|       |       ユーザーの意識|    プレイスタイル    |
|:-----------------|------------------:|:------------------:|
| ポケモンGO!             |      モニター内 |        能動的にポケモンを探しに行く        |
| 都市の幽霊観測装置         |            現実世界 |       受動的に・ちゃんと出会うのを待つ     |

このアプリは「ポスト・ポケモンGO!時代のアイドル」を標榜してきた・・・・・・・・・の、ポスト・ポケモンGO!時代に向けたひとつの回答です。

[“ポスト・ポケモンGO!”時代のアイドル・グループ!? 「・・・・・・・・・」デビュー直前初インタヴュー](http://ototoy.jp/feature/2016082303)

>僕たちはモニター抜きでアイドルと遠隔でつながることができるんじゃないかと考えていて。メディア環境の変化に応じてアイドルも変化してきた歴史があって、僕たちも動画サイトとSNS以降の対応すべき情報環境、つまりIoT時代や「ポケモンGO!」=都市の風景を拡張するAR時代の先に行きたいと思っています。言い方が難しいんですけど「今会えるアイドル」じゃなくて「常に纏える(まとえる)アイドル」を作りたいんです。

目に見えない都市の幽霊が街を徘徊し、気づかないうちに、彼女たちとすれ違っている。
東京に住むひとりひとりが、いつの間にか・ちゃんを身に纏っている。

![名称未設定.001.jpeg](https://qiita-image-store.s3.amazonaws.com/0/133133/3224a949-bd19-919e-0337-39f67c1c082f.jpeg)

都市の幽霊観測装置で**世界に・ちゃんをインストールしませんか。**

![Imgur](http://i.imgur.com/7iyXIo4.jpg)


#仕組み
①GPSロガーで・ちゃんの位置情報を計測・記録
②ユーザーがスマートフォンで都市の幽霊観測装置を起動
③ユーザーが50m移動するごとに、・ちゃんのGPSログとユーザーの現在位置を比較・照合
④・ちゃんのGPSログのなかに、ユーザーの現在位置と近いもの(10m以内)があれば、通知を出し、スマホを振動させる。

![ghostintecity.001.jpeg](https://qiita-image-store.s3.amazonaws.com/0/133133/d754db68-7dde-73e8-95cd-bb59c949e355.jpeg)

##位置情報の記録
・ちゃんの位置情報ログはGPSロガーのHolux M-241を用いて取得しました。

![](http://shopping-all.up.seesaa.net/image/Holux20m-241.jpg)

##定期的にユーザーの位置情報を取得する
後述するように「日常生活で**いつの間にか**すれ違っている」ことを重視しているので、できるだけユーザーにアプリを意識させたくありませんでした。

androidのrequestLocationUpdatesメソッドは、アプリがバックグラウンド状態になっても、定期的に実行され、位置情報を取得します。
今回はデバイスが50m移動するごとに位置情報を取得する設定にしました。

##GPSログを検索する
ユーザーの位置情報を取得したら、「過去の・ちゃんの位置情報が記録されたデータベース」から「ユーザーの現在位置に近い・ちゃんのログがあるか」検索します。

具体的には、androidクライアントが、APIサーバーにユーザーの位置情報をPOSTします。
APIサーバーは、「ユーザーの現在位置±10mの範囲内にある、過去の・ちゃんの位置情報」をデータベースから検索し、その結果をクライアントに返します。

##通知を出す
ユーザーがいま居る場所から10m以内にある・ちゃんの位置情報が見つかったら、通知を出します。
その際、バイブレーションを作動させ、スマートフォンを10秒震わせます。

**観測されると、・ちゃんがピョコッと跳び出てくるイメージです**。

![通知.png](https://qiita-image-store.s3.amazonaws.com/0/133133/9cca2eee-b44a-d1ce-b3c3-74f8782f261c.png)

##クライアントサイドのコード

```java:MainActivity.java
    @Override
    public void onLocationChanged(Location location) {

　　　　　//ユーザーの位置情報
        final Double observerLatitude = location.getLatitude();
        final Double observerLongitude = location.getLongitude();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    JSONObject paramJSON = new JSONObject();
                    paramJSON.put("latitude", observerLatitude);
                    paramJSON.put("longitude", observerLongitude);

                    final String USERNAME   = "";
                    final String PASSWORD   = "";
                    final String userPassword = USERNAME + ":" + PASSWORD;
                    final String encodeAuthorization = Base64.encodeToString(userPassword.getBytes(), Base64.NO_WRAP);

                    try {
                        String buffer = "";
                        HttpURLConnection con = null;
                        URL url = new URL(""); //APIのURL
                        con = (HttpURLConnection) url.openConnection();
                        con.setRequestMethod("POST");
                        con.setInstanceFollowRedirects(false);
                        con.setRequestProperty("Accept-Language", "jp");
                        con.setDoOutput(true);
                        con.setRequestProperty("Authorization", "Basic " + encodeAuthorization);
                        OutputStream os = con.getOutputStream();
                        PrintStream ps = new PrintStream(os);
                        ps.print(paramJSON); //ユーザーの位置情報を、APIにPOST
                        ps.close();

                        BufferedReader reader =
                                new BufferedReader(new InputStreamReader(con.getInputStream(), "UTF-8"));
                        buffer = reader.readLine();

                        JSONObject jsonObject = new JSONObject(buffer);

                        //APIからtrue(・ちゃんがこの近くに居た)と帰ってくれば、通知処理に進む。
                        if(jsonObject.getBoolean("result")){

                            Log.d("結果", "・ちゃんを観測した");

                            String ghostDate = jsonObject.getString("timestamp");

                            //通知処理
                            Intent notificationIntent = new Intent(context, MainActivity.class);
                            Notification.Builder builder = new Notification.Builder(context);
                            NotificationManager manager= (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
                            builder.setSmallIcon(R.mipmap.ic_launcher);
                            builder.setContentTitle("・ちゃんを観測");
                            builder.setContentText(ghostDate + "の・ちゃんを観測");
                            builder.setDefaults(Notification.PRIORITY_DEFAULT);
                            manager.notify(1,builder.build());

                            //バイブレーションを10秒震わせる
                            vib = (Vibrator)getSystemService(VIBRATOR_SERVICE);
                            vib.vibrate(10000);

                        }else{
                            Log.d("結果", "・ちゃんが観測できない");
                        }
                        con.disconnect();
                    } catch (MalformedURLException e) {
                        e.printStackTrace();
                    } catch (ProtocolException e) {
                        e.printStackTrace();
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                } catch(Exception ex) {
                    System.out.println(ex);
                }
            }
        }).start();
    }
```

##サーバーサイドのコード
```ruby:search_controller.rb
def search_location
    observer_location_json_key_array = []

    params.each_key{|key|
      observer_location_json_key_array.push(key)
    }

    observer_location_raw = observer_location_json_key_array[0]
    observer_latitude = observer_location_raw.split(",")[0].split(":")[1].to_f
    observer_longitude = observer_location_raw.split(",")[1].split(":")[1].delete("}").to_f

    #観測通知を出す範囲。ユーザーが、過去の・ちゃんの位置に、どれくらい近ければ通知を出すか。約10m以内に設定。
    range = 0.00005
    min_latitude = observer_latitude - range
    max_latitude = observer_latitude + range
    min_longitude = observer_longitude - range
    max_longitude = observer_longitude + range

    #「ユーザーの位置情報+調整レンジ」の範囲内にある位置情報が存在するか、検索。
    result = Location.where(latitude: min_latitude..max_latitude,longitude:min_longitude..max_longitude)

    #存在すれば、trueとその位置情報が記録された日時をjsonに格納。
    if result[0] == nil
      result_json = { result: false}
    else
      result_json = { result:true, timestamp: result[0].timestamp}
    end

    #jsonを返す。
    render json: result_json
end
```

#参考文献
GPSロギング
[GPSロガー「HOLUX M-241」をMacで利用する](http://www.kajinet.info/?p=3895)

位置情報の取得
[[Android] GPSで位置情報を取得するアプリを作る](https://akira-watson.com/android/gps.html)

POSTリクエスト
https://qiita.com/kaikusakari/items/b1dd02803bb16956010a
http://wicachi.com/archives/151021【6.0動作確認済】手軽にAndroidでJSON形式POSTする方法(非推奨API有り).html
    
Basic認証
http://www.programing-style.com/android/android-api/android-basic-authentication/

通知
[Androidアプリでステータスバーに通知を出すやりかた。](https://qiita.com/steroid66/items/27f5ce27eb32eae49732)

バイブレーション
[Androidでバイブレーター動作](https://qiita.com/shuheni/items/8a0d29c105657a5dbb43)