Reference

Developer API

TripLibre has no web API: there's no server to call. Its "API" is HHTCore, the Swift package that holds the database, inference, editing, export and analytics, plus a small Python module for analysing exports. This page is a brief tour of both.

i

HHTCore targets iOS 17 / macOS 14, uses Swift tools 5.9, and has no third-party dependencies. All paths below are relative to Packages/HHTCore/Sources/HHTCore/.

Quick start

A complete round trip: write some raw fixes, run inference, correct a trip, export.

import HHTCore

let store  = try TravelStore.inMemory()          // or TravelStore(path: ".../travel.sqlite")
let engine = InferenceEngine(store: store)       // uses InferenceConfig.default
let edit   = EditService(store: store)

// 1. Raw observations (the app's LocationCollector does this for real)
try store.insertPoints(myFixes)                  // [RawPoint]

// 2. Rebuild visits and trips
let report = try engine.process(from: start, to: end)
print(report.visitsCreated, report.tripsCreated)

// 3. Correct the first trip's mode (locks it against future rebuilds)
let trips = try store.trips(overlapping: start, end)
if let first = trips.first { try edit.setTripMode(first.id, .subway) }

// 4. Metrics and export
let metrics = try Analytics.compute(store: store, range: DateRange(start: start, end: end))
let folder  = try Exporter(store: store).exportBundle(to: outputDirectory)

Storage

classTravelStoreDatabase/TravelStore.swift

Typed access to the SQLite database. Opening it runs any pending migrations. Every other service takes a store.

init(path: String) throwsOpen or create the database at a path.
static func inMemory() throws -> TravelStoreFor tests and scripts.
func insertPoints(_ ps: [RawPoint]) throwsAppend raw GPS fixes. Also insertPoint, points(from:to:), lastPoint().
func insertMotion(_ samples: [MotionSample]) throwsAppend CoreMotion episodes; read back with motion(from:to:).
func places() throws -> [Place]Also place(_:), upsertPlace, matchPlace(_:slack:), placesNear(_:within:), placeStats().
func visits(overlapping: Date, _: Date, includeDeleted: Bool = false) throws -> [Visit]Also visit(_:), visits(atPlace:), allVisits(), latestVisit().
func trips(overlapping: Date, _: Date, includeDeleted: Bool = false, withSegments: Bool = true) throws -> [Trip]Also trip(_:), segments(tripID:), allTrips().
func flights() / lifePhases() throwsWith matching upsert… and delete….
func auditLog(entityID: String) throws -> [AuditEntry]History of corrections for one record.
func backup(to url: URL) throwsConsistent snapshot of the database file.
func eraseAll() throwsDelete all contents. Irreversible.

Records

Plain value types in Models/Records.swift, each Identifiable with a UUID string id:

TypeNotable fields
RawPointtimestamp, coordinate, horizontalAccuracy, speed, source, collectorMode, timeZone
MotionSampletimestamp, activity (.walking, .automotive…), confidence
Placename, coordinate, radius, category, code, favorite, mergedInto
VisitplaceID, arrival, departure?, purpose, userStatus, deleted; duration(now:)
TriporiginVisitID, destinationVisitID, departure, arrival, distance, modeAuto, modeUser, segments; mode = final mode
TripSegmenttripID, sequence, start, end, modeAuto, modeUser; mode
Flight, LifePhase, AuditEntryMirror their tables

Enums in Models/Enums.swift: TravelMode, ModeGroup, TripPurpose, PlaceCategory, RecordSource, UserStatus. Their raw values are the codes written to the database and exports.

Inference

classInferenceEngineInference/InferenceEngine.swift

Turns raw points into visits, places, trips and segments, never touching user-corrected records.

init(store: TravelStore, config: InferenceConfig = .default)
func processNew(now: Date = Date()) throws -> ReportIncremental: from the latest visit to now. What the app calls on launch and in the background.
func process(from: Date, to: Date, now: Date = Date()) throws -> ReportRebuild a range (widened to whole visits). "Rebuild from raw data" in Settings.
func unresolvedTail(now: Date = Date()) throws -> UnresolvedTail?Movement since the last visit that isn't a trip yet.
struct ReportwindowStart, windowEnd, pointsConsidered, visitsCreated/Updated/Removed, tripsCreated/Removed, placesCreated
structInferenceConfigInference/InferenceConfig.swift

Every threshold in one Codable struct. Nothing else hard-codes them. Tune a copy and pass it to the engine:

var config = InferenceConfig()
config.stayRadius = 80            // m (default 100)
config.minStayDuration = 3 * 60   // s (default 5 min)
let engine = InferenceEngine(store: store, config: config)

The building blocks are public too, for experiments outside the engine:

TypeKey methods
StayDetectorclean(_:), detect(_:motion:now:) -> [DetectedStay], mergeStays(_:points:)
ModeClassifiersegment(path:motion:) -> [SegmentResult], static mainMode(_:)
MotionTimelineactivity(at:), movingSeconds(from:to:), hasCoverage(from:to:)

Editing

classEditServiceEditing/EditService.swift

Every user correction. Each call runs in a transaction, writes audit_log, and marks the record edited / confirmed / manual so inference won't overwrite it. Throws EditError on invalid input.

setTripMode(_ tripID:, _ mode: TravelMode?)nil resets to automatic. Also setSegmentMode.
setTripPurpose / setVisitPurpose(_ id:, _ purpose: TripPurpose?)
confirm(tripID:) / confirm(visitID:)"Looks right."
setNotes(tripID:, _:) / setNotes(visitID:, _:)
setVisitPlace(_ visitID:, placeID:)Reassign a stop to another place.
createPlace(forVisit:, name:, category:, coordinate:, radius:) -> Place
updatePlace(_:) / mergePlaces(_ sourceID:, into targetID:)
setVisitTimes(_ visitID:, arrival:, departure:)
deleteVisit(_:) / deleteTrip(_:)Soft delete; joins neighbouring trips / visits.
splitTrip(_:, stopStart:, stopEnd:, placeID:, at:) -> Visit"I stopped somewhere."
splitVisit(_:, leave:, back:, stop:, mode:) -> [Trip]"I left and came back."
addManualTrip(…) -> Trip / addManualVisit(…) -> Visit

Export and import

classExporterExport/Exporter.swift
exportBundle(to directory: URL, options: Options = Options()) throws -> URLWrites hht-export-…/. Options: range, includeRawPoints, includeSQLite, csv, json, geojson.
tripsCSV(range:) -> StringThe canonical HHTS trip table. tripRows(range:) gives typed rows.
placesGeoJSON() / tripsGeoJSON(range:) / tripEndsGeoJSON(range:) -> Data
fullJSON(range:) -> Data
queryCSV(_ sql: String, _ args: [SQLBindable]) -> StringAny SQL query as CSV.
classImporterExport/Importer.swift
importTrips(csv:, defaultTimeZone:) -> ResultPast trips. Format on Using the app.
importFlights(csv:, defaultTimeZone:) -> Result
importGPX(_ data: Data) -> (count, start, end)Adds raw points; run process(from:to:) on the returned range.
importAirports(csv:) -> IntOurAirports CSV. Then airport(_ code:), airportPlace(_ code:).

Analytics

enumAnalyticsAnalytics/MobilityMetrics.swift
static compute(store:, range: DateRange, now:) throws -> MobilityMetricsAll metrics for a period.
static compute(trips:, visits:, places:, firstVisitByPlace:, range:, now:) -> MobilityMetricsPure version, no database. Handy in tests.
static flightStats(_ flights: [Flight]) -> FlightStats

MobilityMetrics has fields such as personDays, tripsPerDay, distancePerDay, modeShare, departuresByHour, radiusOfGyrationDwell, activitySpaceHullArea, destinationEntropy, tours, complexTourShare, weekdayTripsPerDay. DateRange has helpers .day(_:), .lastDays(_:) and .phase(_:).

Geometry

Geo (Geo/Geo.swift): distance (haversine), pathLength, centroid, sphericalCentroid, radiusOfGyration, convexHull, simplify (Douglas–Peucker), project / unproject. Polyline.encode / decode for Google polylines.

Widget snapshot

structWidgetSnapshotWidget/WidgetSnapshot.swift
static build(store:, tracking:, diaryUpdatedAt:, usesMetric:, now:, calendar:) throws -> WidgetSnapshotEverything the widgets show, from the same queries as Today and Stats › 7 days.
write(to: URL) throwsAtomic JSON write (file protection until first unlock).
static read(from: URL) -> WidgetSnapshot?nil when missing, unreadable or from another currentVersion.
sameContent(as:) -> BoolEqual apart from generatedAt; the app skips widget reloads when true.

Fields: tracking (recording, paused, permissionNeeded, backgroundLimited), lastPointAt, diaryUpdatedAt, reviewPending (last 7 days, same as Today's banner), today (trips, places, distance, travelTime, unconfirmedTrips, current stay, timeline blocks) and week (tripsPerDay, totals, modeDistance by ModeGroup, topPlace). See Architecture › Home Screen widgets.

Python analysis helpers

analysis/hht.py loads an export folder or a travel.sqlite file into pandas. Install with pip install -r analysis/requirements.txt.

modulehhtanalysis/hht.py
load(path) -> HHTExport folder or .sqlite file. Times are tz-aware; *_local columns use each record's tz.
HHT.trips / segments / visits / places / flights / life_phases / points / motionDataFrames (points, motion only if exported).
HHT.phase(name) -> HHTSubset to a life phase.
HHT.between(start, end) -> HHTSubset by person-day.
summary(d) -> pd.SeriesPerson-days, trips/day, km/day, travel min/day, unique places, r_g, entropies.
mode_share(trips, by="trips") -> DataFrameby: "trips", "distance_m" or "travel_time_min".
radius_of_gyration(lat, lon, w=None), shannon(counts), haversine(…)
trips_geodataframe(trips)LineString GeoDataFrame (needs geopandas, shapely, polyline).

Make targets

CommandWhat it does
make helpList all targets
make testRun the HHTCore test suite (swift test)
make projectRegenerate HHT.xcodeproj from project.yml
make buildCompile the app for the simulator, unsigned
make simBuild, install and launch in the simulator with demo data
make phoneBuild, sign and install on the connected iPhone
make analysis-envCreate analysis/.venv
make export-checkSwift writes a real export; Python loads and validates it
make ciEverything CI runs