Files
bincio-activity/segments-plan.md
T
Davide Scaini c7f0013e57 SegmentCreate: prompt after save instead of immediate redirect; update plan
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/).
2026-05-13 01:03:34 +02:00

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:

  1. Bbox pre-filter — skip if activity bbox doesn't overlap segment bbox (padded by ~50 m). O(1) per segment.

  2. Start proximity — find all activity points within 25 m of polyline[0]. Each is a candidate entry point.

  3. End proximity — from each candidate entry, scan forward (never backward) for an activity point within 25 m of polyline[-1]. Enforces correct direction.

  4. 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.

  5. Effort extraction — from the matched entry/exit indices, slice the timeseries and compute elapsed_s and avg metrics.

  6. 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 points
  • CONFORMANCE_MAX_DEVIATION_M = 50 — max perpendicular distance
  • CONFORMANCE_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

  1. Searchable list of the user's own activities with GPS → select one
  2. Activity track shown on map
  3. 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
  4. Name input + optional sport selector
  5. 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

  1. bincio/segments/models.py — Segment and SegmentEffort dataclasses
  2. bincio/segments/store.py — read/write segments to/from disk
  3. 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 s in red/green vs best effort
  • "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/