かの邪智暴虐なAlphabet Inc.を除こう

まえがき

生活に不便を感じたらその不便さをwrapするためにラッパライブラリ等を作ることが私のプログラミングの動機となることが多く、

  • 写真を写真.appで管理していたがPythonなどのスクリプトを使った一括編集などが難しいためDjangoを用いて自作
  • 画像の変換が右クリメニューからできるソフトがあるのだが.avif.webpなど次世代形式に対応していないため自作
  • SNSの交換の際にQRコードそのものが送られてくることが多々あったので画面上のQRコードを読み取りクリップボードにコピーするソフトを自作
  • マイクラ鯖のrconが生では使いにくすぎたのでDjangoでラッパを自作

などのツール系の自作が多くくも、去年はPC98でマインスイーパを自作しその続きをやろうと思っていたが、文化祭片付け後にPC98が死亡していたためジョークRFCのパロディでも書こうと思っていたのだが面白さがイマイチ伸びなかった上PC98を秋葉原最終処分場。で二台購入し動作確認もしたためPONGでも作る気概であったけれども夏休みが多忙を極めていた挙句外が暑すぎて部活に行きたくなかったので簡単に作れそうな

  • Google Mapのlocation-history.jsonを解析して地図に表示するツール

を自作することとした。

献立

実行されたらオプションを解析、不足しているなら対話シェルを呼び出しユーザに情報を求める。その後はデータを解析しブラウザに表示し待機、ブラウザのタブが閉じられたらプログラム終了となる。また、急いで製作したものであるので、大半が「なんか知らんけど動いた」ゾーンに属していることを留意してもらいたい。

util.py, logger.py

ベクトル計算の支援を主とするライブラリとロギング用のライブラリを自作して使用している。util.pyにて定義されているVectorついて解説すると、Vectorは四則演算が大体行列計算と同じものとなる。ただし、乗算は横ベクトルと縦ベクトルの積として計算される。つまりVector(a,b)*Vector(x,y)=Vector(ax,by)となり、除算もこれに倣う。logger.pyは、ほぼ想像できるような内容しか書かれていないので深くは解説しないが、StreamHandlerFileHandlerで標準入出力とファイルにログを流している。

解析

location-history.jsonactivity,timelinePath,visitの三種類うちどれか一つを持つデータの配列であり、activityは移動手段と移動元/先の座標、timelinePathは移動時のパス、visitは訪れた定点と訪れた時間帯が記録してある。今回移動手段はあまり気にしていない上Googleの推測が精度を欠いており、海上を車で移動していたりするので移動はtimelinePathを採用した。

座標は全て"geo:<lat>,<lng>"という形式で記録されていたので、geo_resolver関数を作成し効率化を図った。

main.py
28
29
30
31
32
33
def geo_resolver(text: str) -> Vector[float, float]:
    """
        "geo:lat,lng" -> (lat, lng)
    """

    return Vector(map(float, text[4:].split(",")))

また、データの特性上(datetime, (lat, lng))という形式のオブジェクトを扱う機会が多いのでNamedTupleを利用してリーダブルなコードにした。

main.py
19
20
21
class Visit(NamedTuple):
    dt: datetime.datetime
    pos: Vector[float, float]

markers辞書は同じ座標のmarkerが重なって読めないという事態を防ぐために座標をキーにした辞書型で格納しその後forを回してfolium.Mapに載せている。座標をキーにする際に処理を簡潔に.append()で済ませるためにdefaultdictを使用した。

main.py
155
    markers = defaultdict(list)
169
                markers[",".join(list(map(str,p)))].append(tooltip)
187
188
        for pos in markers:
            folium.Marker(tuple(map(float, pos.split(","))), tooltip=",".join(markers[pos]), popup=pos).add_to(fmap)

また、データ構造の記述時に<float>などが記述されているが全て値はダブルクオーテーションで括られていることは記載していない。

timelinePath

データ構造

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
    "endTime": <ISO 8601 EDTF>,
    "startTime": <ISO 8601 EDTF>,
    "timelinePath": [
        {
            "point": "geo:<lat>,<lng>",
            "durationMinutesOffsetFromStartTime": <int>
        },...
    ]
}

timelinePathは現地時間で記録されているのでdatetime.timedeltaを加算して時差補正をする必要がない。startTimeからdurationMinutesOffsetFromStartTime分後のlatlngが格納されている。

解析

main.py
1
2
3
4
5
for pts in d["timelinePath"]:
    p = geo_resolver(pts["point"])
    t = sdtm+datetime.timedelta(minutes=int(pts["durationMinutesOffsetFromStartTime"]))
    # markers[",".join(list(map(str,p)))].append(f"{t.strftime("%H:%M")}") # for debug
    points.append(Visit(normalize(t), p))

forループを回してpointsに格納。以上。

visit

データ構造

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
    "endTime": <ISO 8601 EDTF>,
    "startTime": <ISO 8601 EDTF>,
    "visit": {
        "hierarchyLevel": <bool>,
        "topCandidate": {
            "probability": <float>,
            "semanticType": <str>,
            "placeID": <str>,
            "placeLocation": "geo:<lat>,<lng>"
        },
        "probability": <float>
    }
}

解析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
if "visit" in d:
    t = datetime.datetime.fromisoformat(d["endTime"])
    if t.date() > day:
        t = datetime.datetime.combine(day, datetime.time(23, 59, 59))
    elif sdtm.date() < day:
        sdtm = datetime.datetime.combine(day, datetime.time(0, 0, 0))
    p = geo_resolver(d["visit"]["topCandidate"]["placeLocation"])
    tooltip = f"{sdtm.strftime("%H:%M")}-{t.strftime("%H:%M")}"
    markers[",".join(list(map(str,p)))].append(tooltip)
    points.append(Visit(normalize(t), p))

前述のtimelinePathと比べ僅かに複雑になっている。visitは長時間を記録するため日付をまたぐことが多いのでその日付を補正する部分と、マップ表示用のデータの作成の部分が増えている。

表示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
try:
    fmap.location = points[0].pos
    fmap.options["zoom"] = zoom
    folium.PolyLine(locations=[x[1] for x in points]).add_to(fmap)
    for pos in markers:
        folium.Marker(tuple(map(float, pos.split(","))), tooltip=",".join(markers[pos]), popup=pos).add_to(fmap)
except:
    exception(f"Error while creating map data. Following is points data:\npoints=\n{pformat(points)}\nmarkers=\n{pformat(markers)}")
else:
    try:
        fname = f"{day}.html"
        fmap.save(fname)
        browser = webbrowser.get(""""C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" %s""")
        browser.open(f"file://{os.getcwd()}/{fname}")
    except:
        exception("Error while show html file in browser.")

データがイカれていると表示中にエラーを吐くことが多いのでtry~except文を使っている。また、Chrome以外のブラウザではエラーで何も表示されないバグがあるそうなのでChromeを名指ししている。defaultdictを回している部分が少し引っかかると思うがそれぐらいだろう。

おわりに

締切の日の深夜にこの文を書いているため、最後のほうが駆け足になったことを許してもらいたい。これだけでは「Googleがサ終しなかったら意味ないじゃん」と思うかもしれないが、移動経路のグラフから撮影時間をクエリにして座標を返すプログラムに発展させることができ、これができるとGPSモジュールがついていない一眼の写真にGPS情報をつけることができる。ソースコードの全文は追ってWeb版で公開される思うので、よかったらWeb版も見てもらっていきたい。会場の部屋の何処かでQRコードが配布されていることだろう。

次へ猫でもわかる電工班>
前へ418 I'm a Teapot>