Last active
February 19, 2018 04:52
-
-
Save xioustic/5c451a3c3805489385a726387cfb3c0e to your computer and use it in GitHub Desktop.
think it's good to go...
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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