Created
February 17, 2021 01:19
-
-
Save joyanujoy/19831b8849848f999e71333f15b94d40 to your computer and use it in GitHub Desktop.
Scriptable.app widget for autotrip - background mileage logger
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
| "use strict" | |
| /** | |
| Queue file to track current trip is stored locally instead of iCloud to avoid mobile data sync issues when you are out and about. | |
| As it's stored in local doc directory, you can't view it in Files app. So use logging for debugging. | |
| Daily trips summary is stored in iCloud and can be viewed in Files app. | |
| */ | |
| async function getAddress(lat, lon) { | |
| try { | |
| const latitude = Number.parseFloat(lat) | |
| const longitude = Number.parseFloat(lon) | |
| const addr = await Location.reverseGeocode(latitude, longitude) | |
| const street = addr?.[0].postalAddress?.street ?? addr?.[0]?.name ?? addr?.[0]?.thoroughfare ?? addr?.[0]?.subLocality ?? addr?.[0]?.locality ?? '' | |
| const postalCode = addr?.[0].postalAddress?.postalCode ?? '' | |
| if (street && postalCode) { | |
| return `${street}, ${postalCode}` | |
| } | |
| return street ?? postalCode ?? `${lat}, ${lon}` | |
| } catch(error) { | |
| console.log(error) | |
| return `${lat}, ${lon}` | |
| } | |
| } | |
| function getCurrentTrip() { | |
| const fm = FileManager.local() | |
| const queueFile = fm.joinPath(fm.documentsDirectory(), 'autotrip_queue.json') | |
| if (!fm.fileExists(queueFile)) { | |
| return undefined | |
| } | |
| return JSON.parse(fm.readString(queueFile)) | |
| } | |
| function getFormattedTripDate(trip) { | |
| const d = new Date(trip.created) | |
| return `${d.getDate()}-${d.getMonth()+1}-${d.getFullYear()}` | |
| } | |
| async function appendTrip(trip) { | |
| const fileDate = getFormattedTripDate(trip) | |
| const fm = FileManager.iCloud() | |
| const tripsFile = fm.joinPath(fm.documentsDirectory(), `autotrips_${fileDate}.json`) | |
| var trips = [] | |
| let savedData = null | |
| if (fm.fileExists(tripsFile)) { | |
| await fm.downloadFileFromiCloud(tripsFile) | |
| savedData = fm.readString(tripsFile) | |
| } | |
| try { | |
| trips = JSON.parse(savedData) ?? [] | |
| } catch (error) { | |
| console.log(error) | |
| } | |
| trips.push(trip) | |
| fm.writeString(tripsFile, JSON.stringify(trips)) | |
| } | |
| function updateCurrentTrip(trip) { | |
| const fm = FileManager.local() | |
| const queueFile = fm.joinPath(fm.documentsDirectory(), 'autotrip_queue.json') | |
| try { | |
| fm.writeString(queueFile, JSON.stringify(trip)) | |
| } catch(error) { | |
| console.log(error) | |
| } | |
| } | |
| function isValidTrip(trip) { | |
| /** | |
| * Check if trip record has all the required fields. | |
| * You could get a corrupt file due to iCloud / file write issues. | |
| */ | |
| return trip?.created && trip?.lastUpd && trip?.startLoc && trip?.lastLoc && trip?.distance >= 0 | |
| } | |
| function newTrip(lat, lon, timestamp) { | |
| return { | |
| created: timestamp, | |
| lastUpd: timestamp, | |
| startLoc: [lat, lon], | |
| lastLoc: [lat, lon], | |
| distance: 0, | |
| path: [[lat, lon]] | |
| } | |
| } | |
| function equiRectangularDistance(a, b){ | |
| const R = 6371000 | |
| const toRadians = (n) => (n * Math.PI) / 180.0 | |
| const latA = toRadians(a[0]) | |
| const lonA = toRadians(a[1]) | |
| const latB = toRadians(b[0]) | |
| const lonB = toRadians(b[1]) | |
| const x = (lonA - lonB) * Math.cos((latA + latB) / 2) | |
| const y = latA - latB | |
| const distance = R * Math.sqrt(x*x + y*y) | |
| return distance | |
| } | |
| async function closeTrip(trip) { | |
| if (trip.distance <= 250) { | |
| return false | |
| } | |
| trip.startAddr = await getAddress(...trip.startLoc) | |
| trip.endAddr = await getAddress(...trip.lastLoc) | |
| trip.duration = (trip.lastUpd - trip.created) / (1000 * 60) | |
| trip.speed = trip.distance / (trip.duration / 60) | |
| return true | |
| } | |
| function addSymbol({ | |
| symbol = 'applelogo', | |
| stack, | |
| color = Color.white(), | |
| size = 20, | |
| }) { | |
| const _sym = SFSymbol.named(symbol) | |
| const wImg = stack.addImage(_sym.image) | |
| wImg.tintColor = color | |
| wImg.imageSize = new Size(size, size) | |
| return wImg | |
| } | |
| async function createWidget(trips) { | |
| const widget = new ListWidget() | |
| widget.setPadding(10, 10, 10, 10) | |
| const greyA = new Color("#232526",1) | |
| const greyB = new Color("#414345",1) | |
| const galleryGrey = new Color('#ececec', 1) | |
| const darkGrey = new Color('#484848', 1) | |
| const gradient = new LinearGradient() | |
| gradient.colors = [Color.dynamic(galleryGrey, greyA), Color.dynamic(galleryGrey, greyB)] | |
| gradient.locations = [0, 1] | |
| gradient.startPoint = new Point(0, 1) | |
| gradient.endPoint = new Point(1, 0) | |
| widget.backgroundGradient = gradient | |
| const header = widget.addStack() | |
| header.centerAlignContent() | |
| const titleText = header.addText('autotrip') | |
| titleText.textColor = Color.dynamic(darkGrey,Color.white()) | |
| titleText.font = new Font("TimesNewRoman", 29) | |
| titleText.leftAlignText() | |
| header.addSpacer(null) | |
| addSymbol({ | |
| symbol: 'calendar', | |
| stack: header, | |
| color: Color.dynamic(darkGrey,Color.white()), | |
| size: 11, | |
| }) | |
| header.addSpacer(4) | |
| const headerDate = header.addDate(new Date()) | |
| headerDate.textColor = Color.dynamic(darkGrey, Color.white()) | |
| headerDate.font = new Font("AvenirNext-DemiBold", 11) | |
| headerDate.leftAlignText() | |
| widget.addSpacer(8) | |
| trips.forEach(trip => { | |
| const row = widget.addStack() | |
| row.centerAlignContent() | |
| const startTimeStack = row.addStack() | |
| startTimeStack.layoutVertically() | |
| const tstack1 = startTimeStack.addStack() | |
| tstack1.centerAlignContent() | |
| tstack1.setPadding(2, 2, 2, 2) | |
| tstack1.borderColor = Color.dynamic(darkGrey, Color.white()) | |
| tstack1.borderWidth = 1 | |
| tstack1.cornerRadius = 6 | |
| const startTime = new Date(trip.created) | |
| const st = tstack1.addText(`${startTime.getHours()}:${startTime.getMinutes()}`) | |
| st.font = new Font("AvenirNext-DemiBold", 13) | |
| st.textColor = Color.dynamic(darkGrey, Color.white()) | |
| startTimeStack.addSpacer(2) | |
| startTimeStack.addSpacer(2) | |
| const tstack2 = startTimeStack.addStack() | |
| tstack2.centerAlignContent() | |
| tstack2.setPadding(2, 2, 2, 2) | |
| tstack2.borderColor = Color.dynamic(darkGrey, Color.white()) | |
| tstack2.borderWidth = 1 | |
| tstack2.cornerRadius = 6 | |
| const endTime = new Date(trip.lastUpd) | |
| const et = tstack2.addText(`${endTime.getHours()}:${endTime.getMinutes()}`) | |
| et.font = new Font("AvenirNext-DemiBold", 13) | |
| et.textColor = Color.dynamic(darkGrey, Color.white()) | |
| row.addSpacer(4) | |
| const addrStack = row.addStack() | |
| addrStack.layoutVertically() | |
| addrStack.centerAlignContent() | |
| addrStack.setPadding(2, 2, 2, 2) | |
| const stAddr = addrStack.addText(trip.startAddr) | |
| stAddr.font = new Font("AvenirNext-DemiBold", 13) | |
| stAddr.textColor = Color.dynamic(darkGrey, Color.white()) | |
| addrStack.addSpacer(8) | |
| const endAddr = addrStack.addText(trip.endAddr) | |
| endAddr.font = new Font("AvenirNext-DemiBold", 13) | |
| endAddr.textColor = Color.dynamic(darkGrey, Color.white()) | |
| row.addSpacer() | |
| const mStack = row.addStack() | |
| mStack.setPadding(4, 4, 4, 4) | |
| mStack.borderColor = Color.dynamic(darkGrey, Color.white()) | |
| mStack.borderWidth = 1 | |
| mStack.cornerRadius = 6 | |
| mStack.centerAlignContent() | |
| const mText = mStack.addText(Math.round(trip.distance / 1000).toString()) | |
| mText.font = new Font("AvenirNext-DemiBold", 26) | |
| mText.textColor = Color.dynamic(darkGrey, Color.white()) | |
| widget.addSpacer(16) | |
| }) | |
| return widget | |
| } | |
| function syntheticTrips() { | |
| // create a few synthetic trips for testing. | |
| // this is based on real data recorded from this scriptale widget | |
| return [ | |
| { | |
| created: 1612966437000, | |
| lastUpd: 1612883810106, | |
| startLoc: [ | |
| 51.41196988656488, | |
| -0.23478093822075533 | |
| ], | |
| lastLoc: [ | |
| 51.426643671611274, | |
| -0.17576353836542444 | |
| ], | |
| distance: 45021.962908309607, | |
| path: [ | |
| [ | |
| 51.41196988656488, | |
| -0.23478093822075533 | |
| ], | |
| [ | |
| 51.41195867382313, | |
| -0.23478537273809932 | |
| ], | |
| [ | |
| 51.411953107488905, | |
| -0.2347855524772907 | |
| ], | |
| [ | |
| 51.41192474766314, | |
| -0.23478994397209724 | |
| ], | |
| [ | |
| 51.426732660465575, | |
| -0.175616795177332 | |
| ] | |
| ], | |
| startAddr: "Spencer Court, Spencer Road, SW20 0QW", | |
| endAddr: "St. Georges Hospital, Blackshaw Road, SW17 0QT", | |
| duration: 6785.423466666667, | |
| speed: 39.81737850641487 | |
| }, | |
| { | |
| created: 1612975077000, | |
| lastUpd: 1612980717000, | |
| startLoc: [ | |
| 51.41196988656488, | |
| -0.23478093822075533 | |
| ], | |
| lastLoc: [ | |
| 51.426643671611274, | |
| -0.17576353836542444 | |
| ], | |
| distance: 12021.962908309607, | |
| path: [ | |
| [ | |
| 51.41196988656488, | |
| -0.23478093822075533 | |
| ], | |
| [ | |
| 51.41195867382313, | |
| -0.23478537273809932 | |
| ], | |
| [ | |
| 51.411953107488905, | |
| -0.2347855524772907 | |
| ], | |
| [ | |
| 51.41192474766314, | |
| -0.23478994397209724 | |
| ], | |
| [ | |
| 51.426732660465575, | |
| -0.175616795177332 | |
| ] | |
| ], | |
| startAddr: "Spencer Court, Spencer Road, SW20 0QW", | |
| endAddr: "St. Georges Hospital, Blackshaw Road, SW17 0QT", | |
| duration: 6785.423466666667, | |
| speed: 39.81737850641487 | |
| }, | |
| { | |
| created: 1612987017000, | |
| lastUpd: 1612991577000, | |
| startLoc: [ | |
| 51.41196988656488, | |
| -0.23478093822075533 | |
| ], | |
| lastLoc: [ | |
| 51.426643671611274, | |
| -0.17576353836542444 | |
| ], | |
| distance: 74021.962908309607, | |
| path: [ | |
| [ | |
| 51.41196988656488, | |
| -0.23478093822075533 | |
| ], | |
| [ | |
| 51.41195867382313, | |
| -0.23478537273809932 | |
| ], | |
| [ | |
| 51.411953107488905, | |
| -0.2347855524772907 | |
| ], | |
| [ | |
| 51.41192474766314, | |
| -0.23478994397209724 | |
| ], | |
| [ | |
| 51.426732660465575, | |
| -0.175616795177332 | |
| ] | |
| ], | |
| startAddr: "Spencer Court, Spencer Road, SW20 0QW", | |
| endAddr: "St. Georges Hospital, Blackshaw Road, SW17 0QT", | |
| duration: 6785.423466666667, | |
| speed: 39.81737850641487 | |
| }, | |
| { | |
| created: 1612476684698, | |
| lastUpd: 1612999797000, | |
| startLoc: [ | |
| 51.41196988656488, | |
| -0.23478093822075533 | |
| ], | |
| lastLoc: [ | |
| 51.426643671611274, | |
| -0.17576353836542444 | |
| ], | |
| distance: 37021.962908309607, | |
| path: [ | |
| [ | |
| 51.41196988656488, | |
| -0.23478093822075533 | |
| ], | |
| [ | |
| 51.41195867382313, | |
| -0.23478537273809932 | |
| ], | |
| [ | |
| 51.411953107488905, | |
| -0.2347855524772907 | |
| ], | |
| [ | |
| 51.41192474766314, | |
| -0.23478994397209724 | |
| ], | |
| [ | |
| 51.426732660465575, | |
| -0.175616795177332 | |
| ] | |
| ], | |
| startAddr: "Spencer Court, Spencer Road, SW20 0QW", | |
| endAddr: "St. Georges Hospital, Blackshaw Road, SW17 0QT", | |
| duration: 6785.423466666667, | |
| speed: 39.81737850641487 | |
| } | |
| ] | |
| } | |
| async function run() { | |
| Location.setAccuracyToHundredMeters() | |
| const {latitude, longitude} = await Location.current() | |
| const now = Date.now() | |
| var currentTrip = getCurrentTrip() | |
| console.log(currentTrip) | |
| if (!isValidTrip(currentTrip)) { | |
| currentTrip = newTrip(latitude, longitude, now) | |
| } | |
| const elapsed = now - currentTrip.lastUpd | |
| if (elapsed < 6 * 60 * 1000) { | |
| // trip in progress, update it. | |
| const distanceDelta = equiRectangularDistance(currentTrip.lastLoc, [latitude, longitude]) | |
| if (distanceDelta > 25) { | |
| currentTrip.distance += distanceDelta | |
| currentTrip.path.push([latitude, longitude]) | |
| } | |
| currentTrip.lastUpd = now | |
| currentTrip.lastLoc = [latitude, longitude] | |
| } else { | |
| // prev trip ended. This is part of new trip. | |
| if (await closeTrip(currentTrip)) { | |
| try { | |
| await appendTrip(currentTrip) | |
| } catch(error) { | |
| console.log(error) | |
| } | |
| } | |
| currentTrip = newTrip(latitude, longitude, now) | |
| } | |
| updateCurrentTrip(currentTrip) | |
| // loading some test data for quick UI build. Later modify this to read from autotrip_<today's date>.json | |
| const todaysTrips = syntheticTrips() | |
| let widget = await createWidget(todaysTrips) | |
| if (config.runsInWidget) { | |
| Script.setWidget(widget) | |
| } else { | |
| widget.presentLarge() | |
| } | |
| Script.complete() | |
| } | |
| await run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment