Add Google Maps Phone Takeout parser

This commit is contained in:
Eugene Burmakin 2024-06-08 22:10:10 +02:00
parent 5adc96bdb9
commit 96a5240729
8 changed files with 193 additions and 3 deletions

View file

@ -1 +1 @@
0.5.1
0.5.2

View file

@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [0.5.2] — 2024-06-08
### Added
- Test version of google takeout importing service for exports from users' phones
---
## [0.5.1] — 2024-06-07
### Added

View file

@ -22,6 +22,7 @@ class ImportJob < ApplicationJob
case source
when 'google_semantic_history' then GoogleMaps::SemanticHistoryParser
when 'google_records' then GoogleMaps::RecordsParser
when 'google_phone_takeout' then GoogleMaps::PhoneTakeoutParser
when 'owntracks' then OwnTracks::ExportParser
end
end

View file

@ -8,5 +8,5 @@ class Import < ApplicationRecord
include ImportUploader::Attachment(:raw)
enum source: { google_semantic_history: 0, owntracks: 1, google_records: 2 }
enum source: { google_semantic_history: 0, owntracks: 1, google_records: 2, google_phone_takeout: 3 }
end

View file

@ -0,0 +1,71 @@
# frozen_string_literal: true
class GoogleMaps::PhoneTakeoutParser
attr_reader :import, :user_id
def initialize(import, user_id)
@import = import
@user_id = user_id
end
def call
points_data = parse_json
points = 0
points_data.each do |point_data|
next if Point.exists?(timestamp: point_data[:timestamp])
Point.create(
latitude: point_data[:latitude],
longitude: point_data[:longitude],
timestamp: point_data[:timestamp],
raw_data: point_data[:raw_data],
topic: 'Google Maps Phone Timeline Export',
tracker_id: 'google-maps-phone-timeline-export',
import_id: import.id,
user_id:
)
points += 1
end
doubles = points_data.size - points
processed = points + doubles
{ raw_points: points_data.size, points:, doubles:, processed: }
end
private
def parse_json
import.raw_data['semanticSegments'].flat_map do |segment|
if segment.key?('timelinePath')
segment['timelinePath'].map do |point|
lat, lon = parse_coordinates(point['point'])
timestamp = DateTime.parse(point['time']).to_i
point_hash(lat, lon, timestamp, segment)
end
elsif segment.key?('visit')
lat, lon = parse_coordinates(segment['visit']['topCandidate']['placeLocation']['latLng'])
timestamp = DateTime.parse(segment['startTime']).to_i
point_hash(lat, lon, timestamp, segment)
end
end
end
def parse_coordinates(coordinates)
coordinates.split(', ').map { _1.chomp('°') }
end
def point_hash(lat, lon, timestamp, raw_data)
{
latitude: lat.to_f,
longitude: lon.to_f,
timestamp:,
raw_data:
}
end
end

View file

@ -0,0 +1,106 @@
{
"semanticSegments": [
{
"startTime": "2019-04-03T08:00:00.000+02:00",
"endTime": "2019-04-03T10:00:00.000+02:00",
"timelinePath": [
{
"point": "50.0506312°, 14.3439906°",
"time": "2019-04-03T08:14:00.000+02:00"
},
{
"point": "50.0506312°, 14.3439906°",
"time": "2019-04-03T08:46:00.000+02:00"
}
]
},
{
"startTime": "2019-04-03T08:13:57.000+02:00",
"endTime": "2019-04-03T20:10:18.000+02:00",
"startTimeTimezoneUtcOffsetMinutes": 120,
"endTimeTimezoneUtcOffsetMinutes": 120,
"visit": {
"hierarchyLevel": 0,
"probability": 0.8500000238418579,
"topCandidate": {
"placeId": "some random id",
"semanticType": "UNKNOWN",
"probability": 0.44970497488975525,
"placeLocation": {
"latLng": "50.0506312°, 14.3439906°"
}
}
}
}
],
"rawSignals": [
{
"activityRecord": {
"probableActivities": [
{
"type": "STILL",
"confidence": 0.9599999785423279
},
{
"type": "IN_VEHICLE",
"confidence": 0.009999999776482582
},
{
"type": "ON_FOOT",
"confidence": 0.009999999776482582
},
{
"type": "WALKING",
"confidence": 0.009999999776482582
},
{
"type": "UNKNOWN",
"confidence": 0.009999999776482582
},
{
"type": "IN_ROAD_VEHICLE",
"confidence": 0.009999999776482582
},
{
"type": "IN_RAIL_VEHICLE",
"confidence": 0.009999999776482582
},
{
"type": "IN_ROAD_VEHICLE",
"confidence": 0.009999999776482582
}
],
"timestamp": "2024-04-26T20:54:38.000+02:00"
}
},
{
"activityRecord": {
"probableActivities": [
{
"type": "STILL",
"confidence": 0.9900000095367432
},
{
"type": "UNKNOWN",
"confidence": 0.009999999776482582
}
],
"timestamp": "2024-04-26T20:55:45.000+02:00"
}
}
],
"userLocationProfile": {
"frequentPlaces": [
{
"placeId": "some random id",
"placeLocation": "50.0506312°, 14.3439906°",
"label": "WORK"
},
{
"placeId": "some random id",
"placeLocation": "50.0506312°, 14.3439906°",
"label": "HOME"
}
]
}
}

View file

@ -5,4 +5,8 @@ RSpec.describe Import, type: :model do
it { is_expected.to have_many(:points).dependent(:destroy) }
it { is_expected.to belong_to(:user) }
end
describe 'enums' do
it { is_expected.to define_enum_for(:source).with_values(google_semantic_history: 0, owntracks: 1, google_records: 2, google_phone_takeout: 3) }
end
end

View file

@ -180,7 +180,7 @@ paths:
lat: 52.502397
lon: 13.356718
tid: Swagger
tst: 1717786543
tst: 1717877268
servers:
- url: http://{defaultHost}
variables: