├── .gitignore
├── google-location-history-to-gpx
├── .gitignore
├── package.json
├── enum-devices.js
├── package-lock.json
└── index.js
├── README.org
└── my-location.el
/.gitignore:
--------------------------------------------------------------------------------
1 | ~
2 | *.elc
3 |
--------------------------------------------------------------------------------
/google-location-history-to-gpx/.gitignore:
--------------------------------------------------------------------------------
1 | .log
2 | node_modules
3 | output
4 | devices.txt
5 |
--------------------------------------------------------------------------------
/google-location-history-to-gpx/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module",
3 | "dependencies": {
4 | "JSONStream": "^1.3.5"
5 | },
6 | "name": "google-location-history-to-gpx",
7 | "version": "1.0.0",
8 | "main": "index.js",
9 | "scripts": {
10 | "test": "echo \"Error: no test specified\" && exit 1"
11 | },
12 | "author": "",
13 | "license": "ISC",
14 | "description": ""
15 | }
16 |
--------------------------------------------------------------------------------
/google-location-history-to-gpx/enum-devices.js:
--------------------------------------------------------------------------------
1 | // 履歴の中に登場するデバイスの一覧を作成するスクリプト
2 |
3 | // deviceTag(数値)と現れる時間の範囲や緯度経度の範囲を求める。
4 |
5 | // node enum-devices.js > devices.txt
6 |
7 | import fs from 'fs';
8 | import path from 'path';
9 | import JSONStream from 'JSONStream';
10 |
11 | const inputFile = "./takeout-20241212T072446Z-001/Takeout/ロケーション履歴/ロケーション履歴(タイムライン)/Records.json"; //[修正してください]
12 |
13 | const devices = [];
14 |
15 | function recordDevice(data){
16 | const device = devices.find((device)=>device.tag == data.deviceTag);
17 |
18 | // 以前は整数値で入っていたが、今回はISO文字列になっていた
19 | // const time = (new Date(parseInt(data.timestampMs))).toISOString();
20 | const time = (new Date(Date.parse(data.timestamp))).toISOString();
21 |
22 | if(device){
23 | device.lastTime = time;
24 | device.count++;
25 | if(data.latitudeE7 < device.latMin){
26 | device.latMin = data.latitudeE7;
27 | }
28 | if(data.latitudeE7 > device.latMax){
29 | device.latMax = data.latitudeE7;
30 | }
31 | if(data.longitudeE7 < device.lngMin){
32 | device.lngMin = data.longitudeE7;
33 | }
34 | if(data.longitudeE7 > device.lngMax){
35 | device.lngMax = data.longitudeE7;
36 | }
37 | }
38 | else{
39 | devices.push({
40 | tag:data.deviceTag,
41 | firstTime: time,
42 | lastTime: time,
43 | latMin: data.latitudeE7,
44 | latMax: data.latitudeE7,
45 | lngMin: data.longitudeE7,
46 | lngMax: data.longitudeE7,
47 | count: 1
48 | });
49 | }
50 | }
51 | function showDevices(){
52 | console.log(devices);
53 | }
54 |
55 | const inputStream = fs.createReadStream(inputFile)
56 | .pipe(JSONStream.parse('locations.*'));
57 |
58 | inputStream.on('data', (data)=>{
59 | recordDevice(data);
60 | }).on('end', ()=>{
61 | showDevices();
62 | });
63 |
--------------------------------------------------------------------------------
/google-location-history-to-gpx/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "google-location-history-to-gpx",
3 | "version": "1.0.0",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "version": "1.0.0",
9 | "license": "ISC",
10 | "dependencies": {
11 | "JSONStream": "^1.3.5"
12 | }
13 | },
14 | "node_modules/jsonparse": {
15 | "version": "1.3.1",
16 | "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
17 | "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=",
18 | "engines": [
19 | "node >= 0.2.0"
20 | ]
21 | },
22 | "node_modules/JSONStream": {
23 | "version": "1.3.5",
24 | "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz",
25 | "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==",
26 | "dependencies": {
27 | "jsonparse": "^1.2.0",
28 | "through": ">=2.2.7 <3"
29 | },
30 | "bin": {
31 | "JSONStream": "bin.js"
32 | },
33 | "engines": {
34 | "node": "*"
35 | }
36 | },
37 | "node_modules/through": {
38 | "version": "2.3.8",
39 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
40 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
41 | }
42 | },
43 | "dependencies": {
44 | "jsonparse": {
45 | "version": "1.3.1",
46 | "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
47 | "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA="
48 | },
49 | "JSONStream": {
50 | "version": "1.3.5",
51 | "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz",
52 | "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==",
53 | "requires": {
54 | "jsonparse": "^1.2.0",
55 | "through": ">=2.2.7 <3"
56 | }
57 | },
58 | "through": {
59 | "version": "2.3.8",
60 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
61 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/google-location-history-to-gpx/index.js:
--------------------------------------------------------------------------------
1 | // ロケーション履歴のjsonを日付毎にGPXへ変換するスクリプト
2 |
3 | // node index.js
4 |
5 | import fs from 'fs';
6 | import path from 'path';
7 | import JSONStream from 'JSONStream';
8 |
9 | const inputFile = "./takeout-20241212T072446Z-001/Takeout/ロケーション履歴/ロケーション履歴(タイムライン)/Records.json"; //[修正してください]
10 | const outputDir = "output";
11 |
12 | // 無視するデバイスの deviceTag を指定する。
13 | // 自宅に置きっぱなしのデバイスがあるとその位置も出力されてしまうので。
14 | // 先に enum-devices.js で調べておく。
15 | const ignoreDevices = [
16 | //[修正してください]
17 | // 例:
18 | // 12345678, //端末A
19 | // -23232323, //端末B
20 | // 35353535 //端末C
21 | ];
22 |
23 | let lastDate = null;
24 | const dayPoints = [];
25 |
26 | function addPoint(timeMs, lat, lng){
27 | const date = new Date(timeMs);
28 |
29 | // date change?
30 | if(!lastDate ||
31 | // ↓ローカル時間での日付区切りになる(UTC区切りにしたければ要変更)
32 | lastDate.getFullYear() != date.getFullYear() ||
33 | lastDate.getMonth() != date.getMonth() ||
34 | lastDate.getDate() != date.getDate()){
35 |
36 | const lastPoint = dayPoints[dayPoints.length-1];//point or undefined
37 | // Include the first point of the next day.
38 | if(lastDate){
39 | dayPoints.push({date, lat, lng});
40 | }
41 |
42 | flushGPX();
43 |
44 | // Include the last point of the previous day.
45 | if(lastPoint){
46 | dayPoints.push(lastPoint);
47 | }
48 | }
49 |
50 | lastDate = date;
51 | dayPoints.push({date, lat, lng});
52 | }
53 |
54 | function flushGPX(){
55 | if(lastDate){
56 | if(dayPoints.length > 0){
57 | writeGPX(lastDate, dayPoints);
58 | }
59 | lastDate = null;
60 | dayPoints.splice(0);
61 | }
62 | }
63 |
64 | function writeGPX(date, points){
65 | const yyyymm = "" + date.getFullYear() +
66 | ("0" + (date.getMonth() + 1)).substr(-2);
67 | const directory = path.join(outputDir, yyyymm);
68 | const dd = ("0" + (date.getDate())).substr(-2);
69 | const filename = "" + yyyymm + dd + ".gpx";
70 | fs.mkdirSync(directory, {recursive:true});
71 | const stream = fs.createWriteStream(path.join(directory, filename));
72 |
73 | const gpx = `
74 |
75 |
76 | ${date.toISOString().split("T")[0]}
77 |
78 | ${
79 | points.map((point)=>
80 | `
81 |
82 |
83 | `).join("")}`;
84 | stream.end(gpx);
85 | }
86 |
87 | const inputStream = fs.createReadStream(inputFile)
88 | .pipe(JSONStream.parse('locations.*'));
89 |
90 | inputStream.on('data', (data)=>{
91 | if(!ignoreDevices.includes(data.deviceTag)){
92 | addPoint(
93 | Date.parse(data.timestamp), // parseInt(data.timestampMs), 以前は整数値だった
94 | parseInt(data.latitudeE7) / 10000000,
95 | parseInt(data.longitudeE7) / 10000000);
96 | }
97 | }).on('end', ()=>{
98 | flushGPX();
99 | });
100 |
--------------------------------------------------------------------------------
/README.org:
--------------------------------------------------------------------------------
1 | #+TITLE: 過去に自分がいた位置をGPXファイルから求めるEmacs Lisp
2 |
3 | * 何が出来る
4 |
5 | ~M-x my-location-at-time~ の後に日時を入力するとその時刻にいた場所の緯度・経度を表示し、マップサービスのURLを生成して開きます。また、Emacs Lispから ~(my-location-latlng-at-time time)~ を評価すると緯度と経度が入ったconsセルが返ってきます。
6 |
7 | 写真の撮影日時情報と組み合わせてGPSを搭載していないカメラでも撮影場所を取得するために作りましたが、他にもアイデア次第で様々な活用方法があると思います。
8 |
9 | * 事前の準備
10 | ** GPXファイルの用意
11 |
12 | 過去の位置を記録したGPXファイル(GPSログ)が必要です。
13 |
14 | *** すでにGPXファイルを沢山持っている場合
15 |
16 | もしハイキングやサイクリングなど何らかの活動を趣味にしているならすでに沢山のGPXファイルを持っているかもしれません。もしそうならカスタマイズ変数 ~my-location-sources~ にそのパスを指定してください。
17 |
18 | ~my-location-sources~ のデフォルト値は次のようになっています。
19 |
20 | #+begin_src elisp
21 | (setq my-location-sources
22 | '((:dir "~/my-location/%Y%m" :file-pattern "\\`%Y%m%d.*\\.gpx")))
23 | #+end_src
24 |
25 | ~:dir~ や ~:file-pattern~ の文字列は ~format-time-string~ 関数を通した後使われます。%Yや%mの部分は求めたい日時の年や月等に置き換わります。
26 |
27 | ~:file-pattern~ はファイルを探すための正規表現です。 ~:dir~ で指定したディレクトリ下にあるファイルの内、正規表現にマッチするファイルを全て読み込みます。例えば "\\`%Y%m%d.*\\.gpx" であれば、 "20211218_稲荷山.gpx" のようなファイルにマッチします。
28 |
29 | *** Googleロケーション履歴のデータからGPXファイルを生成する
30 |
31 | Android端末を持ち歩いていてロケーション履歴を有効にしているなら、既に沢山の位置情報がGoogle社のサーバに記録されているかもしれません。[[https://timeline.google.com/][タイムライン]]にアクセスして確認してみましょう。もし過去の沢山の地点が表示されたなら、そのデータからGPXファイルを生成することができるかもしれません。
32 |
33 | まずはロケーション履歴に関する全てのデータをjson形式でダウンロードする必要があります。指定した日のkmlでは時刻とポイントの正確な対応関係が分からないので面倒でも全てのデータをダウンロードする必要があります。タイムラインから歯車マークをクリックし、「全てのデータのコピーをダウンロード」を選択するとGoogleの個人情報を一括してダウンロードできるページにジャンプします。そこから他のサービスの選択を解除して、ロケーション履歴のデータだけをダウンロードしてください。やり方は今後変わるかもしれないのでそのときは調べてみてください。また、ダウンロードできるようになるまでにしばらくかかる可能性もあります。私の場合、夜寝る前にリクエストして起きたときにはダウンロードできるようになっていました。1時間程度で用意できたようです。
34 |
35 | 私の場合、 ~takeout-20211217T164337Z-001.zip~ というファイル名で80MB程度のzipファイルがダウンロードできました。
36 |
37 | 展開すると ~takeout-20211217T164337Z-001/Takeout/ロケーション履歴/ロケーション履歴.json~ というファイルができました。これがお目当てのファイルでAndroid端末から送られた全ての位置情報、とりわけ日時と緯度経度が入っています。
38 |
39 | ただ、このファイルは 1.2GB というとんでもないサイズなのでEmacsで開くのもためらわれます。
40 |
41 | そこでNode.jsで変換するスクリプトを作成しました。本リポジトリの ~google-location-history-to-gpx/~ ディレクトリに入っているので、Node.jsを使えるようにして、そのディレクトリ下で ~npm install~ を実行して依存するパッケージをインストールしてください。 ~JSONStream~ というパッケージを使用します。
42 |
43 | まずは ~enum-devices.js~ を使って除外する端末のデバイスタグを求めます。複数のAndroid端末を使用している場合、同時に別々の場所の位置情報が記録されている場合があります。私の場合自宅にタブレットを置きっぱなしにしているのでその位置情報も一緒に記録されています。それを除外しないと正しい位置が求められません。
44 |
45 | 1. ~enum-devices.js~ 内の const inputFile = "..." の部分を書き替えて =ロケーション履歴.json= を指すようにする。
46 | 2. コマンドラインから ~node enum-devices.js>devices.txt~ を実行してデバイス一覧をテキストファイルに保存する。
47 | 3. devices.txtの内容を見て除外するタグ番号を集める。
48 |
49 | タグ番号(tag:)の他に最初(firstTime:)や最後(lastTime:)に現れる日時、緯度経度の範囲(latMin:, latMax:, lngMin:, lngMax:)、記録回数(count:)も出力しているので、それを元にどのタグ番号がどの端末なのかを特定し除外するタグ番号を決めてください。
50 |
51 | 次に実際の変換作業に入ります。
52 |
53 | 1. ~index.js~ 内の const inputFile = "..." の部分を書き替えて =ロケーション履歴.json= を指すようにする。
54 | 2. ~index.js~ 内の ~const ignoreDevices = [ ... ]~ の部分に除外したい端末のタグ番号(整数値)をカンマ区切りで書く。
55 | 3. コマンドラインから ~node index.js~ を実行する。
56 |
57 | ~output/YYYYMM/YYYYMMDD.gpx~ という形式のファイル名で日毎のGPXファイルが出力されます。
58 |
59 | GPXファイルが用意できたら、カスタマイズ変数 ~my-location-sources~ がGPXファイルを参照できるように修正してください。 ~output~ の中身をデフォルトの場所 =~/my-location/= へ移動するのでも構いません。
60 |
61 | ** マップサービスのURLを設定する
62 |
63 | 必要ならカスタマイズ変数 ~my-location-map-url~ に使いたいマップサービスへのURLを設定してください。デフォルトはOpen Street Mapsを使うようになっています。
64 |
65 | #+begin_src elisp
66 | (setq my-location-map-url "https://www.openstreetmap.org/#map=17/{{{lat:%.6f}}}/{{{lng:%.6f}}}")
67 | #+end_src
68 |
69 | ** Emacs Lispコードを読み込む
70 |
71 | ~my-location.el~ をEmacsが読み込める場所に置いてください。 ~(require 'my-location)~ で読み込みます。
72 |
73 | * 使ってみる
74 | ** インタラクティブなコマンド
75 | ~M-x my-location-at-time~ と打つと過去の日時を聞いてきます。例えば ~8/7 12:35~ と入力すると、過去の一番近い 8月7日 12:35 の場所を探します。該当するGPXファイルをロードし、その中にその時刻の位置情報があるなら結果をエコーラインに表示します。同時にマップサービスのURLをブラウザで開きます。コマンドプレフィックスを付けると緯度経度をバッファに挿入します(マップは開きません)。
76 |
77 | ** キャッシュのクリア方法
78 | 一度読み込んだGPXファイル内の情報はEmacs内の変数にキャッシュされます。クリアして再度読み込みたい場合は ~M-x my-location-clear~ を実行してください。
79 |
80 | ** Emacs Lispからの利用
81 |
82 | #+begin_src elisp
83 | (my-location-latlng-at-time (encode-time (parse-time-string "2021-01-02 12:34:56"))) ;;=> (xx.xxxxxxxxxx . xxx.xxxxxxxx)
84 | #+end_src
85 |
86 | ** 利用例
87 |
88 | 次のコードは写真(JPEG画像)の撮影場所を緯度経度で表示し、ブラウザでその場所を開きます。JPEG画像のExif情報から撮影日時とGPS情報を取得します。GPS情報が取得できなかった場合は ~my-location-latlng-at-time~ を使用して撮影日時から位置を割り出します。
89 |
90 | #+begin_src elisp
91 | (require 'my-location)
92 | ;; require code at end of http://misohena.jp/blog/2021-12-16-how-to-get-shooting-date-and-location-of-jpg-with-elisp.html
93 | ;; and exif.el
94 | (defun my-photo-location (file)
95 | (interactive "fJPEG File: ")
96 | (let* ((exif (or (my-exif-parse-file file)
97 | (error "No Exif Data")))
98 | (time (or (my-exif-date-time-original exif)
99 | (error "No Exif.DateTimeOriginal")))
100 | (latlng (or (my-exif-latlng exif) ;;From GPS Info
101 | (my-location-latlng-at-time time)))) ;From GPX File
102 |
103 | (when latlng
104 | (my-location-browse-map latlng)
105 | (message "%.6f,%.6f" (car latlng) (cdr latlng)))))
106 | #+end_src
107 |
108 | Emacs27以降に標準で入っているexif.elを使用していますが、それだけでは機能が十分ではないので[[http://misohena.jp/blog/2021-12-16-how-to-get-shooting-date-and-location-of-jpg-with-elisp.html][jpgファイルの撮影日時と撮影場所をEmacs Lispで取得する方法(exif.el) | Misohena Blog]]の最後に書かれているコード(my-exif-*)も使用しています。将来的にexif.elが改善されると良いのですが……。
109 |
--------------------------------------------------------------------------------
/my-location.el:
--------------------------------------------------------------------------------
1 | ;;; my-location.el --- My Location DB -*- lexical-binding: t; -*-
2 |
3 | ;; Copyright (C) 2021 AKIYAMA Kouhei
4 |
5 | ;; Author: AKIYAMA Kouhei
6 | ;; Keywords:
7 |
8 | ;; This program is free software; you can redistribute it and/or modify
9 | ;; it under the terms of the GNU General Public License as published by
10 | ;; the Free Software Foundation, either version 3 of the License, or
11 | ;; (at your option) any later version.
12 |
13 | ;; This program is distributed in the hope that it will be useful,
14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | ;; GNU General Public License for more details.
17 |
18 | ;; You should have received a copy of the GNU General Public License
19 | ;; along with this program. If not, see .
20 |
21 | ;;; Commentary:
22 |
23 | ;; Find out where you were at the specified time.
24 |
25 | ;;; Code:
26 |
27 | (require 'dom)
28 | (require 'cl-lib)
29 | (require 'subr-x)
30 | (require 'parse-time)
31 |
32 | (defgroup my-location nil
33 | "Find out where you were at the specified time"
34 | :prefix "my-location-"
35 | :group 'data)
36 |
37 | ;;;; Math
38 |
39 | (defun my-location-latlng-to-vec3 (ll)
40 | (let* ((lat (degrees-to-radians (car ll)))
41 | (lng (degrees-to-radians (cdr ll)))
42 | (cos-lat (cos lat)))
43 | (vector (* (cos lng) cos-lat) ;;x=lng0
44 | (* (sin lng) cos-lat) ;;y=lng90E
45 | (sin lat)))) ;;z=North
46 |
47 | (defun my-location-vec3-to-latlng (v)
48 | (cons
49 | (radians-to-degrees
50 | (atan (aref v 2)
51 | (sqrt (+ (* (aref v 0) (aref v 0))
52 | (* (aref v 1) (aref v 1))))))
53 | (radians-to-degrees
54 | (atan (aref v 1)
55 | (aref v 0)))))
56 |
57 | (defun my-location-vec3-cross (a b)
58 | (vector (- (* (aref a 1) (aref b 2)) (* (aref a 2) (aref b 1)))
59 | (- (* (aref a 2) (aref b 0)) (* (aref a 0) (aref b 2)))
60 | (- (* (aref a 0) (aref b 1)) (* (aref a 1) (aref b 0)))))
61 |
62 | (defun my-location-vec3-dot (a b)
63 | (+ (* (aref a 0) (aref b 0))
64 | (* (aref a 1) (aref b 1))
65 | (* (aref a 2) (aref b 2))))
66 |
67 | (defun my-location-vec3-length-sq (v)
68 | (let ((x (aref v 0))
69 | (y (aref v 1))
70 | (z (aref v 2)))
71 | (+ (* x x) (* y y) (* z z))))
72 |
73 | (defun my-location-vec3-length (v)
74 | (sqrt (my-location-vec3-length-sq v)))
75 |
76 | (defun my-location-vec3-angle (a b)
77 | (atan (my-location-vec3-length (my-location-vec3-cross a b))
78 | (my-location-vec3-dot a b)))
79 |
80 | (defun my-location-vec3-mul-scalar (v s)
81 | (vector (* (aref v 0) s)
82 | (* (aref v 1) s)
83 | (* (aref v 2) s)))
84 |
85 | (defun my-location-vec3-add (v1 v2)
86 | (vector (+ (aref v1 0) (aref v2 0))
87 | (+ (aref v1 1) (aref v2 1))
88 | (+ (aref v1 2) (aref v2 2))))
89 |
90 | (defun my-location-vec3-normalize (v)
91 | (my-location-vec3-mul-scalar
92 | v
93 | (/ 1.0 (my-location-vec3-length v))))
94 |
95 | (defun my-location-vec3-rotate-z-90 (v)
96 | (vector (- (aref v 1))
97 | (aref v 0)
98 | (aref v 2)))
99 |
100 | (defun my-location-vec3-slerp (v1 v2 T)
101 | (let* ((angle (* T (my-location-vec3-angle v1 v2)))
102 | (axis (my-location-vec3-cross v1 v2))
103 | (err 10e-16)
104 | (axis (if (< (my-location-vec3-length-sq axis) (* err err))
105 | (my-location-vec3-rotate-z-90 v1)
106 | axis))
107 | (dir (my-location-vec3-normalize (my-location-vec3-cross axis v1))))
108 | (my-location-vec3-add
109 | (my-location-vec3-mul-scalar v1 (cos angle))
110 | (my-location-vec3-mul-scalar dir (sin angle)))))
111 |
112 | (defun my-location-interpolate (ll1 ll2 T)
113 | (my-location-vec3-to-latlng
114 | (my-location-vec3-slerp
115 | (my-location-latlng-to-vec3 ll1)
116 | (my-location-latlng-to-vec3 ll2)
117 | T)))
118 |
119 | ;;;; Date/Time
120 |
121 | (defun my-location-string-to-number (str)
122 | (when str
123 | (string-to-number str)))
124 |
125 | (defun my-location-string-to-time (str &optional curr-time)
126 | (unless (string-match "\\(?:\\(?:\\(?:\\([0-9]+\\)[/-]\\)?\\([0-9]+\\)[/-]\\)?\\([0-9]+\\) +\\)?\\([0-9]+\\):\\([0-9]+\\)\\(?::\\([0-9]+\\)\\)?" str)
127 | (error "Invalid date/time format"))
128 | (let* ((curr-time (or curr-time (decode-time (current-time))))
129 | (curr-second (decoded-time-second curr-time))
130 | (curr-minute (decoded-time-minute curr-time))
131 | (curr-hour (decoded-time-hour curr-time))
132 | (curr-day (decoded-time-day curr-time))
133 | (curr-month (decoded-time-month curr-time))
134 | (curr-year (decoded-time-year curr-time))
135 | (input-second (or (my-location-string-to-number (match-string 6 str))
136 | 0))
137 | (input-minute (my-location-string-to-number (match-string 5 str)))
138 | (input-hour (my-location-string-to-number (match-string 4 str)))
139 | (input-day (or (my-location-string-to-number (match-string 3 str))
140 | (if (<= (+ (* 3600 input-hour)
141 | (* 60 input-minute)
142 | input-second)
143 | (+ (* 3600 curr-hour)
144 | (* 60 curr-minute)
145 | curr-second))
146 | curr-day
147 | (1- curr-day))))
148 | (input-month (or (my-location-string-to-number (match-string 2 str))
149 | (if (<= input-day curr-day)
150 | curr-month
151 | (1- curr-month))))
152 | (input-year (or (my-location-string-to-number (match-string 1 str))
153 | (if (or (and (= input-month curr-month)
154 | (<= input-day curr-day))
155 | (< input-month curr-month))
156 | curr-year
157 | (1- curr-year)))))
158 | (encode-time
159 | (make-decoded-time :second input-second
160 | :minute input-minute
161 | :hour input-hour
162 | :day input-day
163 | :month input-month
164 | :year input-year))))
165 |
166 | (defun my-location-guess-time-from-file-name (file)
167 | (when (and (stringp file)
168 | (string-match "\\(20[0-9][0-9]\\|19[0-9][0-9]\\)-?\\(0[1-9]\\|1[0-2]\\)-?\\([0-3][0-9]\\)[ _]?\\([01][0-9]\\|2[0-3]\\)\\([0-5][0-9]\\)\\([0-5][0-9]\\)?" file))
169 | (encode-time
170 | (make-decoded-time
171 | :year (string-to-number (match-string 1 file))
172 | :month (string-to-number (match-string 2 file))
173 | :day (string-to-number (match-string 3 file))
174 | :hour (string-to-number (match-string 4 file))
175 | :minute (string-to-number (match-string 5 file))
176 | :second (string-to-number (or (match-string 6 file) "0"))))))
177 |
178 | (defun my-location-read-date-time (prompt)
179 | (let* ((str (read-string prompt))
180 | (time (or (ignore-errors (my-location-string-to-time str))
181 | (my-location-guess-time-from-file-name str))))
182 | (unless time
183 | (error "Invalid date/time format"))
184 | time))
185 |
186 | ;;;; Track Point
187 |
188 | (cl-defstruct (my-location-point
189 | (:constructor my-location-point-create (lat lng time)))
190 | lat
191 | lng
192 | time)
193 |
194 | (defun my-location-point-latlng (point)
195 | (when point
196 | (cons
197 | (my-location-point-lat point)
198 | (my-location-point-lng point))))
199 |
200 | ;;;; Track Segment
201 |
202 | (cl-defstruct (my-location-segment
203 | (:constructor my-location-segment-create (points track)))
204 | points
205 | track)
206 |
207 | (defun my-location-segment-min-time (segment)
208 | "SEGMENTに記録されている点の最も早い時刻を返します。"
209 | (when segment
210 | (my-location-point-time (car (my-location-segment-points segment)))))
211 |
212 | (defun my-location-segment-max-time (segment)
213 | "SEGMENTに記録されている点の最も遅い時刻を返します。"
214 | (when segment
215 | (my-location-point-time (car (last (my-location-segment-points segment))))))
216 |
217 | (defun my-location-segment-find-point-by-time (segment time)
218 | "SEGMENTに記録されている点のリストのTIMEで指定されている時刻以降を返します。
219 |
220 | 返すリストの要素が一つだけの場合、SEGMENTが持つ一番最後の点とTIME
221 | の時刻は一致しています。
222 |
223 | 返すリストの要素が二つ以上の場合、先頭と二番目の要素の間に時刻
224 | TIMEがあります。二つ目の要素がある場合、二つ目の時刻は必ずTIMEよ
225 | り大きくなります(等しいということはありません)。一つ目の要素は
226 | TIMEと同じか小さい時刻になります。
227 |
228 | TIMEがSEGMENTが持つ範囲の外である場合、nilを返します。
229 | "
230 | (let ((points (my-location-segment-points segment)))
231 |
232 | (unless (time-less-p time (my-location-point-time (car points)))
233 | (while (and (cdr points)
234 | (not
235 | (time-less-p time (my-location-point-time (cadr points)))))
236 | (setq points (cdr points)))
237 | (if (or (cdr points)
238 | (time-equal-p (my-location-point-time (car points)) time))
239 | points))))
240 |
241 | (defun my-location-segment-latlng-at-time (segment time)
242 | "SEGMENTに記録されている情報を使って、TIMEで指定した時刻の位置を推定し緯度経度で返します。
243 |
244 | 記録されていない時刻の場合、前後の記録から補間して求めます。
245 |
246 | TIMEがSEGMENTが持つ範囲の外である場合、nilを返します。"
247 | (let ((points (my-location-segment-find-point-by-time segment time)))
248 | (cond
249 | ((null points)
250 | nil)
251 | ((null (cdr points))
252 | (my-location-point-latlng (car points)))
253 | (t
254 | (let* ((p0 (car points))
255 | (p1 (cadr points))
256 | (time0 (my-location-point-time p0))
257 | (time1 (my-location-point-time p1))
258 | (dt01 (float-time (time-subtract time1 time0)))
259 | (dt0t (float-time (time-subtract time time0))))
260 | (if (= dt01 0)
261 | (my-location-point-latlng p1)
262 | (my-location-interpolate
263 | (my-location-point-latlng p0)
264 | (my-location-point-latlng p1)
265 | (/ dt0t dt01))))))))
266 |
267 | ;;;; Track
268 |
269 | (cl-defstruct (my-location-track
270 | (:constructor my-location-track-create (name)))
271 | name
272 | segments)
273 |
274 | ;;;; Global Location Data
275 |
276 | (defun my-location-clear ()
277 | (interactive)
278 | (my-location-loaded-files-clear)
279 | (my-location-tracks-clear)
280 | (my-location-segments-clear))
281 |
282 | ;;;;; Global Segment List
283 |
284 | (defvar my-location-segments nil) ;; a list of segment
285 |
286 | (defun my-location-segments-clear ()
287 | (setq my-location-segments nil))
288 |
289 | (defun my-location-segment-on-time (time-lower &optional time-upper)
290 | (unless time-upper
291 | (setq time-upper time-lower))
292 | (seq-find
293 | (lambda (segment)
294 | (and
295 | (not (time-less-p time-upper (my-location-segment-min-time segment)))
296 | (not (time-less-p (my-location-segment-max-time segment) time-lower))))
297 | my-location-segments))
298 |
299 | (defun my-location-add-segment (segment)
300 | ;; @todo Prohibit duplication of ranges?
301 | ;; (unless (my-location-segment-on-time
302 | ;; (my-location-segment-min-time segment)
303 | ;; (my-location-segment-max-time segment)) )
304 | (push segment my-location-segments))
305 |
306 | (defun my-location-points-on-time (time &optional no-auto-load-p)
307 | (unless no-auto-load-p
308 | (my-location-load-files-on-time time))
309 | (when-let ((segment (my-location-segment-on-time time)))
310 | (my-location-segment-find-point-by-time segment time)))
311 |
312 | (defun my-location-latlng-at-time (time &optional no-auto-load-p)
313 | (unless no-auto-load-p
314 | (my-location-load-files-on-time time))
315 | (when-let ((segment (my-location-segment-on-time time)))
316 | (my-location-segment-latlng-at-time segment time)))
317 |
318 | ;;;;; Global Track List
319 |
320 | (defvar my-location-tracks nil) ;;a list of track
321 |
322 | (defun my-location-tracks-clear ()
323 | (setq my-location-tracks nil))
324 |
325 | (defun my-location-add-track (track)
326 | (push track my-location-tracks)
327 | (mapc
328 | #'my-location-add-segment
329 | (my-location-track-segments track)))
330 |
331 | (defun my-location-add-tracks (tracks)
332 | (mapc
333 | #'my-location-add-track
334 | tracks))
335 |
336 | ;;;;; Global File Management
337 |
338 | (defvar my-location-loaded-files nil)
339 |
340 | (defun my-location-loaded-files-clear ()
341 | (setq my-location-loaded-files nil))
342 |
343 | (defcustom my-location-sources
344 | '((:dir "~/my-location/%Y%m" :file-pattern "\\`%Y%m%d.*\\.gpx"))
345 | "gpx file locations."
346 | :group 'my-location
347 | :type '(repeat
348 | (list
349 | (const :format "" :dir)
350 | (string :format "Directory: %v" "~/my-location/%Y%m")
351 | (const :format "" :file-pattern)
352 | (string :format "File Regexp: %v" "\\`%Y%m%d.*\\.gpx"))))
353 |
354 | (defun my-location-source-files-on-time (time)
355 | (apply
356 | #'nconc
357 | (mapcar
358 | (lambda (source)
359 | (when-let ((dir-format (plist-get source :dir))
360 | (file-pattern-format (plist-get source :file-pattern)))
361 | (let ((dir (format-time-string dir-format time))
362 | (file-pattern (format-time-string file-pattern-format time)))
363 | (when (and (file-exists-p dir)
364 | (directory-name-p dir))
365 | (directory-files dir t file-pattern)))))
366 | my-location-sources)))
367 |
368 | (defun my-location-load-files-on-time (time)
369 | (mapc #'my-location-load-file (my-location-source-files-on-time time)))
370 |
371 | (defun my-location-load-file (file)
372 | (let ((abs-file (expand-file-name file)))
373 | (unless (assoc abs-file my-location-loaded-files #'string=)
374 | (message "loading %s..." abs-file)
375 | (let ((tracks (my-location-load-gpx abs-file)))
376 | (push (cons abs-file tracks) my-location-loaded-files)))))
377 |
378 | (defun my-location-unload-file (file)
379 | (let* ((abs-file (expand-file-name file))
380 | (file-tracks (assoc abs-file my-location-loaded-files #'string=)))
381 | (when file-tracks
382 | ;; Remove track & segments
383 | ;;@todo
384 |
385 | ;; Remove the entry
386 | (setf
387 | (alist-get abs-file my-location-loaded-files nil 'remove #'string=)
388 | nil))))
389 |
390 | ;;;; GPX File
391 |
392 | ;; References:
393 | ;; - https://www.topografix.com/gpx.asp
394 |
395 | (defun my-location-load-gpx (file)
396 | "GPXファイルを読み取ります。読み取ったデータはグローバルなトラックリストやセグメントリストに格納して問い合わせに備えます。"
397 | (let* ((gpx (with-temp-buffer
398 | (insert-file-contents file)
399 | (libxml-parse-xml-region (point-min) (point-max))))
400 | (tracks
401 | (cl-loop
402 | for trk in (dom-children gpx)
403 | when (eq (dom-tag trk) 'trk)
404 | collect
405 | (let* ((track (my-location-track-create
406 | (dom-text (car (dom-by-tag trk 'name)))))
407 | (segments (cl-loop
408 | for trkseg in (dom-children trk)
409 | when (eq (dom-tag trkseg) 'trkseg)
410 | collect
411 | (my-location-segment-create
412 | (cl-loop
413 | for trkpt in (dom-children trkseg)
414 | when (eq (dom-tag trkpt) 'trkpt)
415 | collect
416 | (when-let ((lat-str (dom-attr trkpt 'lat))
417 | (lng-str (dom-attr trkpt 'lon))
418 | (time-elem (car (dom-by-tag trkpt 'time))))
419 | (my-location-point-create
420 | (string-to-number lat-str)
421 | (string-to-number lng-str)
422 | (parse-iso8601-time-string
423 | (dom-text time-elem)))))
424 | track))))
425 | (setf (my-location-track-segments track) segments)
426 | track))))
427 | (my-location-add-tracks tracks)
428 | tracks))
429 |
430 |
431 | ;;;; Map
432 |
433 | (defun my-location-expand-template (template alist)
434 | (let ((regexp "{{{\\([^:}]+\\)\\(?::\\([^:}]+\\)\\)?}}}"))
435 | (replace-regexp-in-string
436 | regexp
437 | (lambda (str)
438 | (save-match-data
439 | (if (string-match regexp str)
440 | (let ((key (match-string 1 str))
441 | (fmt (or (match-string 2 str) "%s")))
442 | (format fmt (alist-get (intern key) alist "")))
443 | "")))
444 | template
445 | t)))
446 |
447 | (defcustom my-location-map-url
448 | "https://www.openstreetmap.org/#map=17/{{{lat:%.6f}}}/{{{lng:%.6f}}}"
449 | "Map Service URL."
450 | :group 'my-location
451 | :type '(choice
452 | (const nil)
453 | (const :tag "Open Street Maps" "https://www.openstreetmap.org/#map=17/{{{lat:%.6f}}}/{{{lng:%.6f}}}")
454 | (const :tag "Apple Maps" "http://maps.apple.com/?ll={{{lat:%.6f}}},{{{lng:%.6f}}}&z=17")
455 | (const :tag "Google Maps" "https://www.google.com/maps?ll={{{lat:%.6f}}},{{{lng:%.6f}}}&z=17")
456 | (const :tag "Japan GSI" "https://maps.gsi.go.jp/#17/{{{lat:%.6f}}}/{{{lng:%.6f}}}/")
457 | string))
458 |
459 | (defun my-location-browse-map (ll)
460 | (message "url=%s" (my-location-expand-template
461 | my-location-map-url
462 | (list (cons 'lat (car ll))
463 | (cons 'lng (cdr ll)))))
464 | (browse-url (my-location-expand-template
465 | my-location-map-url
466 | (list (cons 'lat (car ll))
467 | (cons 'lng (cdr ll))))))
468 |
469 | (defun my-location-at-time (time &optional arg)
470 | "TIMEで指定した時刻の位置を表示します。
471 |
472 | ARGを指定した場合結果をバッファに挿入します。指定しなかった場合マップを開きます。
473 | "
474 | (interactive
475 | (list
476 | (my-location-read-date-time
477 | "Past date and time ([[[YYYY-]MM-]DD ]HH:MM[:SS]): ")
478 | current-prefix-arg))
479 | (let ((ll (my-location-latlng-at-time time)))
480 | (if ll
481 | (let ((str (format "%.6f,%.6f" (car ll) (cdr ll))))
482 | (if arg
483 | (insert str)
484 | (when my-location-map-url
485 | (my-location-browse-map ll)))
486 | (message "%s" str)
487 | ll)
488 | (message "No data")
489 | nil)))
490 |
491 |
492 | (provide 'my-location)
493 | ;;; my-location.el ends here
494 |
--------------------------------------------------------------------------------