Skip to content

Instantly share code, notes, and snippets.

@xioustic
Last active February 19, 2018 04:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xioustic/5c451a3c3805489385a726387cfb3c0e to your computer and use it in GitHub Desktop.
Save xioustic/5c451a3c3805489385a726387cfb3c0e to your computer and use it in GitHub Desktop.
think it's good to go...
// hello
// i need to do taxes and i have to log my mileage retroactively
// my accountant said google timeline is fine
// google timeline is slow to iterate through by hand
// it's also slow to log by hand
// this script handles enough that i can put it in a csv file and work on it from there
var STOP_DATE = '2017-01-01'
var DEBUG = false
function qSA(arg) {
return Array.prototype.slice.call(document.querySelectorAll(arg))
}
var qS = document.querySelector.bind(document)
var prevButton = document.querySelector('.previous-date-range-button')
var nextButton = document.querySelector('.next-date-range-button')
var monthToNumMap = {'January':'01','February':'02','March':'03','April':'04','May':'05','June':'06','July':'07','August':'08','September':'09','October':'10','November':'11','December':'12'}
function getCurrentMonth() { return qS('.month-picker .goog-flat-menu-button-caption').textContent }
function getCurrentYear() { return qS('.year-picker .goog-flat-menu-button-caption').textContent }
function getCurrentDay() { return qS('.day-picker .goog-flat-menu-button-caption').textContent }
function getISODate() { return [getCurrentYear(), monthToNumMap[getCurrentMonth()], getCurrentDay().padStart(2, '0')].join('-') }
function getEvents() {
var timeline_items = qSA('.timeline-item-content')
timeline_items = timeline_items.map(itm => {
var item = {}
item['dom'] = itm
item['date'] = getISODate()
item['duration'] = itm.querySelector('.duration-text') ? itm.querySelector('.duration-text').textContent.trim() : ''
item['distance'] = itm.querySelector('.distance-text') ? itm.querySelector('.distance-text').textContent.trim().split('- ')[1] : ''
item['type'] = itm.querySelector('.activity-type') ? itm.querySelector('.activity-type').textContent.trim() : ''
item['place-title'] = itm.querySelector('.place-visit-title') ? itm.querySelector('.place-visit-title').textContent.trim() : ''
item['address'] = itm.querySelector('.timeline-item-text') ? itm.querySelector('.timeline-item-text').textContent.trim() : ''
return item
}).filter(item => {
var success = false
var csvMap = ['duration','distance','type','place-title','address']
csvMap.forEach(attr => { if (item[attr]) success = true })
if (item['dom'].querySelector('.travel-segment-summary-item')) success = false
var bannedStrings = ['manually', 'dd a stop in', ' delete']
bannedStrings.forEach(bannedString => { if (item['address'].indexOf(bannedString) !== -1) success = false })
return success
})
return timeline_items
}
function waitForNewDate(callback, origDate) {
origDate = origDate ? origDate : getISODate()
if (origDate !== getISODate()) { callback(getISODate()) }
else { setTimeout(() => waitForNewDate(callback, origDate), 1000) }
}
// must use globals to 'generate' with each call since an origin might exist from a previous day
var csvHeaders = ['origin-date','origin-time','origin-place-title','origin-address','distance','duration','destination-date','destination-time','destination-place-title','destination-address']
var eventBuffer = {'distance': 0}
var locationMode = 'origin'
// returns a csvEntry, which may be false (no event produced from current+prev input)
// or a list of csv columns per csvHeaders (event produced from current+prev input)
function processEvent(evnt) {
var retval = false
evnt['type'] = evnt['type'] ? evnt['type'] : 'location'
// TODO: there's some weird mechanic where there's "stops" without any address or time
// just a duration of the stop; we skip them for now...
if (evnt['duration'].indexOf('mins') !== -1 && !evnt['distance']) {
if (DEBUG) { console.log('aids:'); console.log(evnt) }
return retval
}
// handle an event, and only if we already have an origin in the pipeline
if (evnt['type'] !== 'location' && eventBuffer['origin-place-title'] || eventBuffer['origin-address']) {
// we only care about Driving events
if (evnt['type'] === 'Driving') {
// this is stored in tenths of a mile
var thisDistance = parseInt(evnt['distance'].split(' mi')[0].replace('.',''),10)
// if there was no decimal, then we need to correct it into tenths
if (evnt['distance'].indexOf('.') === -1) thisDistance = thisDistance * 10
if (DEBUG) console.log(thisDistance)
if (DEBUG) console.log(eventBuffer['distance'])
eventBuffer['distance'] = eventBuffer['distance'] + thisDistance
if (DEBUG) console.log(eventBuffer['distance'])
// TODO: handle adding up multiple consecutive destinations duration
// eg [7 hours 2 mins, 1 hour 9 mins, 1 hour 1 min, 2 mins, 1 min]
eventBuffer['duration'] = evnt['duration']
locationMode = 'destination'
return retval
}
}
if (locationMode === 'destination') {
eventBuffer['destination-time'] = evnt['duration'].split(' - ')[0]
eventBuffer['destination-place-title'] = evnt['place-title']
eventBuffer['destination-address'] = evnt['address']
eventBuffer['destination-date'] = evnt['date']
locationMode = 'origin'
// finalize the entry
// convert distance to xx.x mi
var distStr = eventBuffer['distance']+''
eventBuffer['distance'] = distStr.slice(0,-1) + '.' + distStr.slice(-1)
retval = []
csvHeaders.forEach(attr => {
retval.push(eventBuffer[attr])
})
// must clear distance, don't want it to carry to next event entry
eventBuffer['distance'] = 0
// treat our event as an origin in the next event also
locationMode = 'origin'
}
if (locationMode === 'origin') {
eventBuffer['origin-time'] = evnt['duration'].split(' - ').pop().split(' ').pop().trim()
eventBuffer['origin-place-title'] = evnt['place-title']
eventBuffer['origin-address'] = evnt['address']
eventBuffer['origin-date'] = evnt['date']
}
return retval
}
var totalCsv = []
var csvLineBuffer = []
var stopLoop = 0
var debugCsvHeaders = ['date','duration','distance','type','place-title','address']
function mainLoop(calledRecursively) {
// if false, we're on the first call
if (!calledRecursively) {
stopLoop = 0
totalCsv.push(csvHeaders)
if (DEBUG) console.log(debugCsvHeaders.join('\t'))
if (DEBUG) totalCsv[0] = debugCsvHeaders.concat(csvHeaders)
}
// if true, we're done and should output & reset
if (stopLoop) {
// output results
if (DEBUG) console.log(totalCsv)
var totalCsvStrings = totalCsv.map(csvEntry => csvEntry.join('\t'))
if (DEBUG) console.log(totalCsvStrings)
var totalCsvString = totalCsvStrings.join('\n')
console.log(totalCsvString)
console.log('stopped due to stopLoop')
// reset globals
totalCsv = []
csvLineBuffer = []
return
}
// expand collapsed activities
qSA('.activity-expand-toggle').forEach((itm) => itm.click())
var events = getEvents()
events.forEach((evnt) => {
if (DEBUG) {
debugCsvHeaders.forEach((attr) => {
csvLineBuffer.push(evnt[attr])
})
}
var maybeEvent = processEvent(evnt)
// if we got an event, we should add it to the csvLineBuffer
if (maybeEvent) {
if (DEBUG) console.log('got maybeEvent')
if (DEBUG) console.log(maybeEvent)
if (DEBUG) console.log(csvLineBuffer)
csvLineBuffer = csvLineBuffer.concat(maybeEvent)
if (DEBUG) console.log(csvLineBuffer)
}
// if we have anything on the csvLineBuffer at this point, flush to totalCsv
if (csvLineBuffer.length) {
console.log(csvLineBuffer)
totalCsv.push(csvLineBuffer); csvLineBuffer = []
}
})
if (getISODate() === STOP_DATE) { stopLoop = 1 }
waitForNewDate(() => mainLoop(true), getISODate())
prevButton.click()
}
mainLoop()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment