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.
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
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:
| Type | Notable fields |
|---|---|
RawPoint | timestamp, coordinate, horizontalAccuracy, speed, source, collectorMode, timeZone |
MotionSample | timestamp, activity (.walking, .automotive…), confidence |
Place | name, coordinate, radius, category, code, favorite, mergedInto |
Visit | placeID, arrival, departure?, purpose, userStatus, deleted; duration(now:) |
Trip | originVisitID, destinationVisitID, departure, arrival, distance, modeAuto, modeUser, segments; mode = final mode |
TripSegment | tripID, sequence, start, end, modeAuto, modeUser; mode |
Flight, LifePhase, AuditEntry | Mirror 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
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, placesCreatedEvery 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:
| Type | Key methods |
|---|---|
StayDetector | clean(_:), detect(_:motion:now:) -> [DetectedStay], mergeStays(_:points:) |
ModeClassifier | segment(path:motion:) -> [SegmentResult], static mainMode(_:) |
MotionTimeline | activity(at:), movingSeconds(from:to:), hasCoverage(from:to:) |
Editing
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:) -> PlaceupdatePlace(_:) / 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(…) -> VisitExport and import
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:) -> DatafullJSON(range:) -> DataqueryCSV(_ sql: String, _ args: [SQLBindable]) -> StringAny SQL query as CSV.importTrips(csv:, defaultTimeZone:) -> ResultPast trips. Format on Using the app.importFlights(csv:, defaultTimeZone:) -> ResultimportGPX(_ 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
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]) -> FlightStatsMobilityMetrics 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
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.
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
| Command | What it does |
|---|---|
make help | List all targets |
make test | Run the HHTCore test suite (swift test) |
make project | Regenerate HHT.xcodeproj from project.yml |
make build | Compile the app for the simulator, unsigned |
make sim | Build, install and launch in the simulator with demo data |
make phone | Build, sign and install on the connected iPhone |
make analysis-env | Create analysis/.venv |
make export-check | Swift writes a real export; Python loads and validates it |
make ci | Everything CI runs |