Files
bincio-activity/segments-plan.md
T
Davide Scaini 4d2df860ce Segments Phase 3: detection algorithm, CLI, ingest hook, and efforts API
- detect.py: ActivityTrack + detect_one/detect_all (bbox pre-filter →
  start/end proximity 25m → path conformance 50m/30% → effort extraction
  with avg speed/HR/power and Coggan NP)
- cli.py: `bincio segments detect` for retroactive detection over stored
  timeseries JSONs, with optional --activity-id / --segment-id filters
- ingest.py: non-fatal hook at end of ingest_parsed runs detect_all
- server.py: GET /api/segments/{id}/efforts and POST /api/segments/{id}/detect
2026-05-13 00:50:39 +02:00

7.1 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