2011年03月02日

ソーシャル携帯電波マップを作ろう2 〜そのログデータに用がある〜

はてなブックマークに登録

前回はiPhoneにデータ取得用の簡易アプリを作って送り込み、実測したところまででひとまず終わりとした。データは取ったからには使わなければ。そのログデータに用がある。まず既に作成した手元のログを確認してみたいわけですよ。

このPING結果を地図上に表示したい。もう一旦ファイル保存したものだし、PC上でGoogle Mapsを使用するのが最も簡単そうなのでそれで出すことにした。あとはこのログデータをどのように保持するかだが、Webアプリに模してAJAX的にクライアントに送られるようにしたいと考えている。本来ならばデータはDBにしまって、Webアプリによる処理を経た出力にしたいが、ひとまずJSONを静的ファイルにして対処するという段階的な実装で対応する。

そんなわけで、ログファイルの加工に入ろう。"LocationPinger[1785:307]"や、"latitude"、"longitude"といった冗長な語句をまずは除去する。一方でこのログには位置情報とPINGの2種類が入っているので、それぞれを区別しやすくする。そしてCSV形式に準じてプログラム上取り込みやすい形式にしよう。

% sed -e 's/ LocationPinger[[^]]*]/,/' 
          -e 's/latitude /latlng,/' 
          -e 's/longitude //' 
          -e 's/, #/,ping,/' 
          -e 's/ sent/, sent/' 
          -e 's/ received/, received/' 
          -e 's/ send/ , send/' 
          -e 's/, /,/g' < (log file)

CSVの2カラム目に、PING=pingと位置情報=laglngというそれぞれの区別を示すカラムを追加した。

また、第1カラムが時刻のままになっているが、これをデータの記録開始時点からの相対時間に置き換える。なぜそんなことをするかと言うと、位置情報の線形補間をやるためだ。

def at_location(t):
    i = 0
    while t > locations[i + 1][0] and i + 1 < len(locations):
        i += 1
    (t0, p0, q0) = locations[i]
    (t1, p1, q1) = locations[i + 1]

    dt = t1 - t0
    dp = p1 - p0
    dq = q1 - q0

    return (p0 + dp * (t - t0) /  dt, q0 + dq * (t - t0) / dt)

何しろ今回新幹線という高速移動しながらのデータ取得となるため、0コンマ何秒の間隔であってもかなりの距離を移動している計算になる。最も近いlatlngのデータで間に合わせてもいいが、すべてのlatlngデータにはタイムスタンプがあることを考えれば、そのタイムスタンプを入力にした関数 x = f(t), y = g(t) を定義すれば、任意の時点での位置をそこそこの手間で計算できることになる。それをやるのが(線形)補間だ。

import datetime
import sys

f = sys.stdin
dt0 = None

def __read_datetime(s):
    dt0 = s.split(' ')
    dt1 = dt0[1].split('.')
    fd = dt0[0].split('-')
    ft = dt1[0].split(':')

    da = [int(x) for x in fd + ft + [dt1[1] + '000']]
    dt = datetime.datetime(*da)
    return dt

for line in f.readlines():
    line = line.rstrip()
    fs = line.split(',', 2)
    dt = __read_datetime(fs[0])
    if not dt0:
        dt0 = dt
    d = dt - dt0
    print "%d,%s,%s" % (d.seconds * 1000 + d.microseconds / 1000, fs[1], fs[2])

ここまででログデータの整形は大分進んだ。さらにこれをプログラム内で扱う為のデータ構造に合わせて再編成する。最初のログ形式から1行分は単位として保持して来たが、PINGは送信と受信がそれぞれ紐づいているので、対応するログ同士をまとめて新たな一個のデータ単位となるようにしたい。

for line in sys.stdin.readlines():
    line = line.rstrip()

    fs = line.split(',')
    fs[0] = long(fs[0])

    if fs[1] == 'ping':
        d = None
        fs[2] = int(fs[2])
        if fs[2] in pings:
            d = pings[fs[2]]
        else:
            d = {}
            pings[fs[2]] = d
        if fs[3] in ('sent', 'received'):
            d[fs[3]] = fs[0]
    elif fs[1] == 'latlng':
        locations += [(fs[0], float(fs[2]), float(fs[3]))]

"ping"の行の第3カラムはPINGの送信シーケンスの番号を示している。"if fs[1] == 'ping':"の行以降で、同一のシーケンス番号を持つ"sent"と"received"の組を作り、一個の連想配列としてまとめている。まとめた連想配列は、さらにpings配列に逐次追加している。

さらに、位置情報のエントリも時間順にlocations配列に入れて、前述の線形補間のためのデータとして使った。PING送受信のそれぞれタイムスタンプを使用して各時点での位置情報を推定して取り込んだ。

for k in pings:
    p = pings[k]
    if 'sent' in p:
        p['sent'] = (p['sent'],) + at_location(p['sent'])
    if 'received' in p:
        p['received'] = (p['received'],) + at_location(p['received'])

あとは、これらを一個のJSON出力にして出来上がりである。

result = {"locations":locations, "events":list(pings[k] for k in sorted(pings.keys())) }

print json.dumps(result)
{"locations": [
  [0, 39.158541999999997, 141.18367799999999],
  [606, 39.149102999999997, 141.187062],
  [1641, 39.149863000000003, 141.18681799999999],
  [2629, 39.150553000000002, 141.186599],
  [4189, 39.151201, 141.18638999999999],
  [4668, 39.151829999999997, 141.18618599999999],
  ...
], "pings": [
  {"received": [734, 39.149196990338162, 141.18703182415459], "sent": [581, 39.149492397689762, 141.18692239603959]},
  {"received": [1689, 39.149896522267213, 141.18680736032388], "sent": [1584, 39.149821144927536, 141.18683143768115]},
  {"received": [2696, 39.150580830769236, 141.18659002371794], "sent": [2584, 39.150521572874496, 141.18660897469636]},
  {"received": [3674, 39.15098707692308, 141.18645899679487], "sent": [3584, 39.150949692307691, 141.18647105448719]},
  {"received": [4757, 39.151889238247861, 141.18616726816239], "sent": [4584, 39.151719695198324, 141.18622177453025]},
  {"received": [5766, 39.152556356000005, 141.18595579000001], "sent": [5584, 39.152439688034192, 141.18599320940172]},
  ...
]}

さあ、いよいよこれを使ってGoogle Maps上に表示してみようじゃないですか。

<html>
<head>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
<script type="text/javascript" src="jquery-1.5.1.min.js"></script>
<script type="text/javascript">

var map = null;
function initialize() {
  var y = (39.158542 + 39.701141) / 2;
  var x = (141.183678 + 141.136431) / 2;
  var latlng = new google.maps.LatLng(y, x);
  var myOptions = {
    zoom: 10,
    center: latlng,
    mapTypeId: google.maps.MapTypeId.ROADMAP
  };
  map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
}

$(document).ready(function() {
    initialize();

    $.ajax({
        url: 'pinglog.json',
        type: 'GET',
        dataType: 'json',
        success: function(data) {
            $.each(data['events'], function(i, ev) {
                var loc0 = null, loc1 = null, t0 = null, t1 = null, thickness = 1;
                if ('sent' in ev) {
                    loc0 = new google.maps.LatLng(ev['sent'][1], ev['sent'][2]);
                    t0 = ev['sent'][0];
                }
                if ('received' in ev) {
                    loc1 = new google.maps.LatLng(ev['received'][1], ev['received'][2]);
                    t1 = ev['received'][0];
                }

                if (loc0 != null && loc1 != null) {
                    var dt = t1 - t0;
                    if (dt <= 100) {
                        thickness = 20;
                    } else if (dt <= 200) {
                        thickness = 12;
                    } else if (dt <= 400) {
                        thickness = 7;
                    } else if (dt <= 1000) {
                        thickness = 4;
                    } else if (dt <= 2000) {
                        thickness = 2;
                    }

                    var path = new google.maps.Polyline({
                        path: [loc0, loc1],
                        strokeColor: "#FF0000",
                        strokeOpacity: 1.0,
                        strokeWeight: thickness 
                    });
                    path.setMap(map);
                    google.maps.event.addListener(path, 'click', function() {
                        alert('clicked')
                    });
                }
            });
        }
    });
});
</script>
</head>
<body>
  <div id="map_canvas"></div>
</body>
</html>

やりました!やりましたよ皆さん!

疎通が確認できた箇所ではPINGを送信した地点から受信した地点までの間に赤線を入れており、RTTの時間が短ければ短い程太い線にしている。これで見ると、水沢江刺駅から盛岡駅までの間、概ねどこでもSoftbank 3Gの電波を拾えていたことが分かる。しかし、ちょうど花巻市のあたりではっきり空白ができている。これは、新花巻駅南側にある花巻トンネルと高松トンネルという二つのトンネルのせいで電波が届かなくなったためであり、またこの両トンネル間の地上を走る間にはネットワークの到達性が復元できなかったことを意味している。おそらく、徒歩や車移動などで計測すれば異なる結果が出るだろうことが推測される。

また、他にもトンネルとは関係なく所々PING結果がプロットされていない箇所があることも分かると思う。航空写真と重ね合わせて見ると、人家が途切れ気味になったり森林地帯だったりしていて、基地局から遠かったのかとか局間のハンドオーバーに失敗したのだろうかとか、いろいろ推測してみるのも面白いだろう。ちなみに、水沢江刺から一関・仙台方面に向かうともっとトンネルが多いので、より変動の多い拾い甲斐のあるデータが取れることだろう。こんどやるときはもっと用意を進めてこちらでもデータが取れるようにしておきたいところ。

というわけで、ここまででデータの記録と出力はできるようになった。クライアントサイドの動作検証モデルが出来たことになる。残るはサーバへのデータ集積。ソーシャルって言うくらいなんですからさあ、皆さん、皆さんと僕らは、ずっと、ずっと、こうしてつながってたいわけですよおおお!!!…次回はいよいよそのへんの機能を実装します。どのへん?

(こうら ひろのぶ)

klab_gijutsu2 at 10:01│Comments(0)TrackBack(0)Python 

トラックバックURL

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント