route-surface-breakdown (Phase 1): Planner-path surface/waytype bars
Synchronous path + rendering for the surface/waytype breakdown: - map-core `computeSurfaceBreakdown(coords, surfaces, highways)` → distance- weighted metres per surface + waytype category (unit-tested); - `SurfaceBreakdownSchema` in @trails-cool/api; - nullable `surfaceBreakdown` jsonb on routes + activities; - Planner `SaveToJournalButton` computes it from the BRouter waytags already in routeData and sends it; `api.save-to-journal` forwards it; the journal route callback validates + persists (journal is the authoritative validator); - `SurfaceBreakdown` component (stacked bars per dimension, map-core palettes, legend category · % · km largest-first, unknown → "other", hidden when empty) on route + activity detail; journal gains a @trails-cool/map-core dep; - i18n journal.surface.* (en + de). Phase 2 (async Overpass backfill + SSE for imports/uploads, and the e2e that seeds a breakdown) follows in a separate PR. Tests: computeSurfaceBreakdown unit (map-core 36); SurfaceBreakdown component (jsdom, journal 326). typecheck + lint green; verified in the browser. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e1e3dc5c81
commit
3a1c34317d
21 changed files with 388 additions and 18 deletions
|
|
@ -91,6 +91,15 @@ export const magicTokens = journalSchema.table("magic_tokens", {
|
|||
*/
|
||||
export type Visibility = "private" | "unlisted" | "public";
|
||||
|
||||
/**
|
||||
* Distance-weighted surface/waytype breakdown (metres per category), derived
|
||||
* from BRouter waytags at Planner save or backfilled via Overpass.
|
||||
*/
|
||||
export type SurfaceBreakdown = {
|
||||
surface: Record<string, number>;
|
||||
highway: Record<string, number>;
|
||||
};
|
||||
|
||||
export const routes = journalSchema.table("routes", {
|
||||
id: text("id").primaryKey(),
|
||||
ownerId: text("owner_id")
|
||||
|
|
@ -106,6 +115,7 @@ export const routes = journalSchema.table("routes", {
|
|||
elevationLoss: real("elevation_loss"),
|
||||
dayBreaks: jsonb("day_breaks").$type<number[]>(),
|
||||
tags: jsonb("tags").$type<string[]>(),
|
||||
surfaceBreakdown: jsonb("surface_breakdown").$type<SurfaceBreakdown>(),
|
||||
plannerState: bytea("planner_state"),
|
||||
visibility: text("visibility").$type<Visibility>().notNull().default("private"),
|
||||
/**
|
||||
|
|
@ -173,6 +183,9 @@ export const activities = journalSchema.table("activities", {
|
|||
description: text("description").default(""),
|
||||
// Sport / activity type (NULL = unspecified). See SPORT_TYPES above.
|
||||
sportType: text("sport_type").$type<SportType>(),
|
||||
// Distance-weighted surface/waytype breakdown (backfilled via Overpass for
|
||||
// imported/uploaded activities). NULL until derived.
|
||||
surfaceBreakdown: jsonb("surface_breakdown").$type<SurfaceBreakdown>(),
|
||||
gpx: text("gpx"),
|
||||
geom: lineString("geom"),
|
||||
startedAt: timestamp("started_at", { withTimezone: true }),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue