Skip to content

Commit 96a5240

Browse files
committed
Add Google Maps Phone Takeout parser
1 parent 5adc96b commit 96a5240

File tree

8 files changed

+193
-3
lines changed

8 files changed

+193
-3
lines changed

.app_version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.5.1
1+
0.5.2

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
## [0.5.2] — 2024-06-08
9+
10+
### Added
11+
12+
- Test version of google takeout importing service for exports from users' phones
13+
14+
---
15+
816
## [0.5.1] — 2024-06-07
917

1018
### Added

app/jobs/import_job.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def parser(source)
2222
case source
2323
when 'google_semantic_history' then GoogleMaps::SemanticHistoryParser
2424
when 'google_records' then GoogleMaps::RecordsParser
25+
when 'google_phone_takeout' then GoogleMaps::PhoneTakeoutParser
2526
when 'owntracks' then OwnTracks::ExportParser
2627
end
2728
end

app/models/import.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ class Import < ApplicationRecord
88

99
include ImportUploader::Attachment(:raw)
1010

11-
enum source: { google_semantic_history: 0, owntracks: 1, google_records: 2 }
11+
enum source: { google_semantic_history: 0, owntracks: 1, google_records: 2, google_phone_takeout: 3 }
1212
end
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# frozen_string_literal: true
2+
3+
class GoogleMaps::PhoneTakeoutParser
4+
attr_reader :import, :user_id
5+
6+
def initialize(import, user_id)
7+
@import = import
8+
@user_id = user_id
9+
end
10+
11+
def call
12+
points_data = parse_json
13+
14+
points = 0
15+
16+
points_data.each do |point_data|
17+
next if Point.exists?(timestamp: point_data[:timestamp])
18+
19+
Point.create(
20+
latitude: point_data[:latitude],
21+
longitude: point_data[:longitude],
22+
timestamp: point_data[:timestamp],
23+
raw_data: point_data[:raw_data],
24+
topic: 'Google Maps Phone Timeline Export',
25+
tracker_id: 'google-maps-phone-timeline-export',
26+
import_id: import.id,
27+
user_id:
28+
)
29+
30+
points += 1
31+
end
32+
33+
doubles = points_data.size - points
34+
processed = points + doubles
35+
36+
{ raw_points: points_data.size, points:, doubles:, processed: }
37+
end
38+
39+
private
40+
41+
def parse_json
42+
import.raw_data['semanticSegments'].flat_map do |segment|
43+
if segment.key?('timelinePath')
44+
segment['timelinePath'].map do |point|
45+
lat, lon = parse_coordinates(point['point'])
46+
timestamp = DateTime.parse(point['time']).to_i
47+
48+
point_hash(lat, lon, timestamp, segment)
49+
end
50+
elsif segment.key?('visit')
51+
lat, lon = parse_coordinates(segment['visit']['topCandidate']['placeLocation']['latLng'])
52+
timestamp = DateTime.parse(segment['startTime']).to_i
53+
54+
point_hash(lat, lon, timestamp, segment)
55+
end
56+
end
57+
end
58+
59+
def parse_coordinates(coordinates)
60+
coordinates.split(', ').map { _1.chomp('°') }
61+
end
62+
63+
def point_hash(lat, lon, timestamp, raw_data)
64+
{
65+
latitude: lat.to_f,
66+
longitude: lon.to_f,
67+
timestamp:,
68+
raw_data:
69+
}
70+
end
71+
end
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
{
2+
"semanticSegments": [
3+
{
4+
"startTime": "2019-04-03T08:00:00.000+02:00",
5+
"endTime": "2019-04-03T10:00:00.000+02:00",
6+
"timelinePath": [
7+
{
8+
"point": "50.0506312°, 14.3439906°",
9+
"time": "2019-04-03T08:14:00.000+02:00"
10+
},
11+
{
12+
"point": "50.0506312°, 14.3439906°",
13+
"time": "2019-04-03T08:46:00.000+02:00"
14+
}
15+
]
16+
},
17+
{
18+
"startTime": "2019-04-03T08:13:57.000+02:00",
19+
"endTime": "2019-04-03T20:10:18.000+02:00",
20+
"startTimeTimezoneUtcOffsetMinutes": 120,
21+
"endTimeTimezoneUtcOffsetMinutes": 120,
22+
"visit": {
23+
"hierarchyLevel": 0,
24+
"probability": 0.8500000238418579,
25+
"topCandidate": {
26+
"placeId": "some random id",
27+
"semanticType": "UNKNOWN",
28+
"probability": 0.44970497488975525,
29+
"placeLocation": {
30+
"latLng": "50.0506312°, 14.3439906°"
31+
}
32+
}
33+
}
34+
}
35+
],
36+
"rawSignals": [
37+
{
38+
"activityRecord": {
39+
"probableActivities": [
40+
{
41+
"type": "STILL",
42+
"confidence": 0.9599999785423279
43+
},
44+
{
45+
"type": "IN_VEHICLE",
46+
"confidence": 0.009999999776482582
47+
},
48+
{
49+
"type": "ON_FOOT",
50+
"confidence": 0.009999999776482582
51+
},
52+
{
53+
"type": "WALKING",
54+
"confidence": 0.009999999776482582
55+
},
56+
{
57+
"type": "UNKNOWN",
58+
"confidence": 0.009999999776482582
59+
},
60+
{
61+
"type": "IN_ROAD_VEHICLE",
62+
"confidence": 0.009999999776482582
63+
},
64+
{
65+
"type": "IN_RAIL_VEHICLE",
66+
"confidence": 0.009999999776482582
67+
},
68+
{
69+
"type": "IN_ROAD_VEHICLE",
70+
"confidence": 0.009999999776482582
71+
}
72+
],
73+
"timestamp": "2024-04-26T20:54:38.000+02:00"
74+
}
75+
},
76+
{
77+
"activityRecord": {
78+
"probableActivities": [
79+
{
80+
"type": "STILL",
81+
"confidence": 0.9900000095367432
82+
},
83+
{
84+
"type": "UNKNOWN",
85+
"confidence": 0.009999999776482582
86+
}
87+
],
88+
"timestamp": "2024-04-26T20:55:45.000+02:00"
89+
}
90+
}
91+
],
92+
"userLocationProfile": {
93+
"frequentPlaces": [
94+
{
95+
"placeId": "some random id",
96+
"placeLocation": "50.0506312°, 14.3439906°",
97+
"label": "WORK"
98+
},
99+
{
100+
"placeId": "some random id",
101+
"placeLocation": "50.0506312°, 14.3439906°",
102+
"label": "HOME"
103+
}
104+
]
105+
}
106+
}

spec/models/import_spec.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,8 @@
55
it { is_expected.to have_many(:points).dependent(:destroy) }
66
it { is_expected.to belong_to(:user) }
77
end
8+
9+
describe 'enums' do
10+
it { is_expected.to define_enum_for(:source).with_values(google_semantic_history: 0, owntracks: 1, google_records: 2, google_phone_takeout: 3) }
11+
end
812
end

swagger/v1/swagger.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ paths:
180180
lat: 52.502397
181181
lon: 13.356718
182182
tid: Swagger
183-
tst: 1717786543
183+
tst: 1717877268
184184
servers:
185185
- url: http://{defaultHost}
186186
variables:

0 commit comments

Comments
 (0)