After saving, show "Saved! Add another from this activity?" with two buttons: "Add another" (resets name/handles, keeps map loaded) and "Done" (navigates to /segments/).
9.2 KiB
Bincio Segments — Design Plan
Overview
Segments are named GPS stretches that can be defined by any user. Every time an activity passes through a segment, an "effort" is recorded automatically. Users can track their personal history and PRs on each segment.
Data Model
Segment (global, shared)
File: /var/bincio/segments/{segment-id}.json
{
"id": "climb-to-passo-rolle-a3f2",
"name": "Climb to Passo Rolle",
"sport": "cycling",
"polyline": [[46.123, 11.456], [46.124, 11.457], "..."],
"distance_m": 4320.5,
"bbox": [11.456, 46.120, 11.502, 46.135],
"created_by": "brut",
"created_at": "2026-05-13T10:00:00Z"
}
Segment ID: slugified name + 4-char hash to avoid collisions.
SegmentEffort (per user)
File: /var/bincio/data/{handle}/segment_efforts/{segment-id}.json
Array of efforts, newest first:
[
{
"activity_id": "2026-04-26T053013Z-ccbas-...",
"started_at": "2026-04-26T06:12:33Z",
"elapsed_s": 1842,
"avg_speed_kmh": 12.4,
"avg_hr_bpm": 158,
"avg_power_w": 245,
"np_power_w": 261,
"detected_at": "2026-05-13T10:05:00Z"
}
]
Detection Algorithm
Input: activity GPS track (lat/lon, ~1 Hz), segment polyline
Steps:
-
Bbox pre-filter — skip if activity bbox doesn't overlap segment bbox (padded by ~50 m). O(1) per segment.
-
Start proximity — find all activity points within 25 m of
polyline[0]. Each is a candidate entry point. -
End proximity — from each candidate entry, scan forward (never backward) for an activity point within 25 m of
polyline[-1]. Enforces correct direction. -
Path conformance — between entry and exit indices, project each activity point onto the nearest segment polyline segment and compute perpendicular distance. If more than 30% of points exceed 50 m deviation, reject the match. Configurable thresholds.
-
Effort extraction — from the matched entry/exit indices, slice the timeseries and compute
elapsed_sand avg metrics. -
Multiple efforts — after a successful match, continue scanning from the exit point to detect repeat efforts in the same activity (e.g. repeat climbs).
Configurable constants (easy to tune):
MATCH_RADIUS_M = 25— proximity to start/end pointsCONFORMANCE_MAX_DEVIATION_M = 50— max perpendicular distanceCONFORMANCE_MAX_FRACTION = 0.30— fraction of points allowed to exceed max deviation
Server-side Code Structure
bincio/segments/
__init__.py
models.py — Segment, SegmentEffort dataclasses
store.py — read/write segments and efforts to/from /var/bincio
detect.py — matching algorithm
cli.py — bincio segments detect [--activity X] [--segment Y] [--user U] [--all]
Detection is called:
- At ingest time — after each new activity is processed, reads all segment definitions (bbox pre-filter makes this fast), writes matches to the user's
segment_efforts/ - Retroactively — via CLI or API endpoint, for one user re-running all activities against all (or one) segment(s)
API Endpoints
All new, added to server.py:
GET /api/segments?bbox=lon_min,lat_min,lon_max,lat_max list segments in viewport
POST /api/segments create segment (auth required)
DELETE /api/segments/{id} delete (created_by or admin only)
GET /api/segments/{id}/efforts current user's efforts (auth)
POST /api/segments/{id}/detect retrigger detection for current user
Bbox filtering is done in memory at request time. All segment definitions are read from disk on startup (or lazily cached). Works fine for hundreds of segments.
Frontend
New pages / routes
| Route | Purpose |
|---|---|
/segments/ |
Map + list of all segments; entry point for browsing and creating |
/segments/new/ |
Segment creation flow |
/segments/{id}/ |
Segment detail: polyline on map + user's effort history |
/segments/ page
- Full-width map showing segment polylines for segments whose bbox overlaps the current map viewport
- Segment list below (or sidebar): name, sport, distance, creator
- "New segment" button →
/segments/new/ - Clicking a segment →
/segments/{id}/
/segments/new/ — creation flow
- Searchable list of the user's own activities with GPS → select one
- Activity track shown on map
- Dual-handle range slider below the map, one position per GPS point in the track
- Moving handles highlights the selected portion on the map in a contrasting colour
- Start and end coordinates are the GPS points at the slider handle positions
- Name input + optional sport selector
- Confirm →
POST /api/segments
/segments/{id}/ — segment detail
- Segment polyline on map
- Metadata: name, sport, distance, created by
- Effort history table (sorted by date, newest first):
- Date, elapsed time, avg speed, avg HR, avg power, NP
- Delta vs. personal best highlighted
- "Retrigger detection" button →
POST /api/segments/{id}/detect
Activity detail page (existing)
Below the stats table: if any segment efforts were detected for this activity, show a compact list — segment name + elapsed time + Δ vs. PR.
Athlete page (existing)
New "Segments" section: list of segments the user has efforts on, showing best time and effort count for each.
Segment Creation UX (refined)
Standalone /segments/new/ page with an activity picker. Additionally, the activity detail page gets a "Create segment from this activity" button that links to /segments/new/?activity={id}, pre-selecting the activity and skipping the picker step.
Rationale: users will typically think "I want to turn this climb I just did into a segment" — the shortcut from activity detail makes that fast, while the standalone page remains the canonical entry point for discovery.
Implementation Strategy
UI-first, so users can start populating the segment collection immediately. Detection comes after.
Phase 1 — Minimal backend to unblock UI ✅
bincio/segments/models.py— Segment and SegmentEffort dataclasses ✅bincio/segments/store.py— read/write segments to/from disk ✅POST /api/segments+GET /api/segments?bbox=...+DELETE /api/segments/{id}✅
Phase 2 — Creation UI ✅
4. /segments/new/ page — activity picker → map + dual-handle slider → name/save ✅
5. /segments/ page — map + list of existing segments, bbox-filtered ✅
6. Activity detail — "Create segment from this activity" shortcut button ✅
Phase 3 — Detection ✅
7. bincio/segments/detect.py — matching algorithm, tested in isolation ✅
8. bincio/segments/cli.py — retroactive detection CLI (bincio segments detect) ✅
9. Ingest hook — run detection at end of each ingest ✅
10. POST /api/segments/{id}/detect + GET /api/segments/{id}/efforts ✅
Phase 4 — Display (next)
11. /segments/{id}/ page — segment detail + effort history table
12. Activity detail — show matched segment efforts below stats
13. Athlete page — segments section
Phase 4 Detail
New API endpoints
| Endpoint | Notes |
|---|---|
GET /api/segments/{id} |
Single segment metadata |
GET /api/activities/{activity_id}/segment_efforts |
Scans user's effort files; returns [{segment_id, segment_name, elapsed_s, started_at, ...}] for hits on this activity. Auth required. |
GET /api/users/{handle}/segment_summary |
Public. Returns [{segment: {...}, best_elapsed_s, effort_count}] — all segments where this user has efforts, best time + count each. |
SegmentDetail page (/segments/{id}/)
- Map with segment polyline (reuse MapLibre, same style as
SegmentsView) - Metadata row: name, sport, distance, created by
- Effort history table sorted newest-first; columns: Date, Time, Avg speed, Avg HR, Avg power, NP, Δ vs PR
- PR row highlighted; Δ column shows
+N s/-N sin red/green vs best effort
- PR row highlighted; Δ column shows
- "Retrigger detection" button with loading state (blocks, shows count when done)
- Auth-gated: effort table only rendered if logged in
Activity detail additions
Below the laps table: if the user is logged in and this activity has any segment effort hits, show a compact block — segment name (linked to /segments/{id}/) + elapsed time + Δ vs PR.
Fetch: GET /api/activities/{activity_id}/segment_efforts (gracefully hidden if 401 or empty).
Athlete page additions
New "Segments" tab in AthleteView.svelte. Fetches GET /api/users/{handle}/segment_summary (public, no auth needed). Shows a table: Segment name (linked), Sport, Distance, Best time, # efforts. Empty state if no efforts yet.
Effort time display
Use compact m:ss format (e.g. "5:23") for segment effort tables — add formatElapsed(s: number): string to format.ts.
Post-save UX in SegmentCreate (updated)
After a successful save, instead of immediately redirecting to /segments/, show an inline prompt:
Segment saved! Add another from this activity? [Yes, add another] [Done]
- "Yes, add another" — clears
segName,segSport; resets handles to full range; keeps the map and activity loaded - "Done" — navigates to
/segments/