mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 07:53:09 +00:00
feat(tours): show the known SAC grades along a hike (MAP-03)
A hike in the planner, being edited or viewed as an import, now has an "SAC grades" card. Switched on, it asks the router which OpenStreetMap ways lie under the line (matched on foot, every grade of path open) and the server reads their sac_scale through the Overpass mirrors, the same trail-grades read the mountain bike grades use. The grades are drawn along the route on the planner map in one ramp of a single hue, T1 to T6, and listed by share and length; a row draws its class alone. Each row names its source in a tooltip: the SAC hiking scale and the sac_scale value OpenStreetMap wrote. What is not known stays unknown and says why, never T1: a way without a grade, a grade that names no single step (handed on as written), a stretch with no way clearly under the tour's line, or a way that could not be asked about. For imported tracks and stretches drawn by hand a matched way only counts where it keeps within 25 m of the tour's own line, so the original line is kept and nothing is guessed. The router's own reading of the scale is not asked for, since it may be normalised. The routing limit of a planned hike sits apart as a setting with its own badge, and a known step above it is marked. Nothing about the tour, its places or its days changes; switched off, the map is as it was. No regional colour categories and no grade derived from slope, height or trail markings.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
SAC_SCALE_UNCLEAR_MAX,
|
||||
SAC_SCALE_VALUES,
|
||||
sacStepOf,
|
||||
tourTrailGradeRequestSchema,
|
||||
tourTrailGradeResponseSchema,
|
||||
tourTrailGradeSchema,
|
||||
} from './tour-trail-grade.schema';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const grade = {
|
||||
id: 1,
|
||||
mtb_scale: null,
|
||||
mtb_scale_uphill: null,
|
||||
mtb_scale_imba: null,
|
||||
sac_scale: null,
|
||||
sac_scale_unclear: null,
|
||||
};
|
||||
|
||||
describe('tour trail grades (BIK-05, MAP-03)', () => {
|
||||
it('TRAIL-GRADE-SCHEMA-001: reads the six sac_scale values to T1 to T6 in order', () => {
|
||||
expect(SAC_SCALE_VALUES.map(sacStepOf)).toEqual([1, 2, 3, 4, 5, 6]);
|
||||
expect(sacStepOf('hiking')).toBe(1);
|
||||
expect(sacStepOf('difficult_alpine_hiking')).toBe(6);
|
||||
});
|
||||
|
||||
it('TRAIL-GRADE-SCHEMA-002: takes a way with an SAC grade, or with one written unclearly, and nothing else', () => {
|
||||
expect(tourTrailGradeSchema.parse({ ...grade, sac_scale: 'alpine_hiking' }).sac_scale).toBe('alpine_hiking');
|
||||
expect(tourTrailGradeSchema.parse({ ...grade, sac_scale_unclear: 'T3;T4' }).sac_scale_unclear).toBe('T3;T4');
|
||||
expect(tourTrailGradeSchema.safeParse({ ...grade, sac_scale: 'T3' }).success).toBe(false);
|
||||
expect(tourTrailGradeSchema.safeParse({ ...grade, sac_scale_unclear: '' }).success).toBe(false);
|
||||
expect(
|
||||
tourTrailGradeSchema.safeParse({ ...grade, sac_scale_unclear: 'x'.repeat(SAC_SCALE_UNCLEAR_MAX + 1) }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('TRAIL-GRADE-SCHEMA-003: an answer lists graded ways and the ways it could not ask about', () => {
|
||||
const answer = { ways: [{ ...grade, mtb_scale: '2+' }], unanswered: [7] };
|
||||
expect(tourTrailGradeResponseSchema.parse(answer)).toEqual(answer);
|
||||
});
|
||||
|
||||
it('TRAIL-GRADE-SCHEMA-004: a question names at least one way, by a positive id, and nothing more', () => {
|
||||
expect(tourTrailGradeRequestSchema.safeParse({ way_ids: [1, 2] }).success).toBe(true);
|
||||
expect(tourTrailGradeRequestSchema.safeParse({ way_ids: [] }).success).toBe(false);
|
||||
expect(tourTrailGradeRequestSchema.safeParse({ way_ids: [0] }).success).toBe(false);
|
||||
expect(tourTrailGradeRequestSchema.safeParse({ way_ids: [1], extra: true }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,9 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* BIK-05: the trail grades OpenStreetMap carries for the ways a mountain bike
|
||||
* tour runs on, asked for by the ways' ids. A read: nothing about the tour
|
||||
* BIK-05 and MAP-03: the trail grades OpenStreetMap carries for the ways a
|
||||
* tour runs on, asked for by the ways' ids: the mountain bike scales for a
|
||||
* bike tour, the SAC hiking scale for a hike. A read: nothing about the tour
|
||||
* changes. A way without a grade is unknown, never easy; a way that could not
|
||||
* be asked about is named, so the planner can say so.
|
||||
*/
|
||||
@@ -17,11 +18,38 @@ export const tourTrailGradeRequestSchema = z
|
||||
.strict();
|
||||
export type TourTrailGradeRequest = z.infer<typeof tourTrailGradeRequestSchema>;
|
||||
|
||||
/**
|
||||
* MAP-03: the six steps of the SAC hiking scale as OpenStreetMap writes them
|
||||
* in `sac_scale`, from T1 to T6 in this order.
|
||||
*/
|
||||
export const SAC_SCALE_VALUES = [
|
||||
'hiking',
|
||||
'mountain_hiking',
|
||||
'demanding_mountain_hiking',
|
||||
'alpine_hiking',
|
||||
'demanding_alpine_hiking',
|
||||
'difficult_alpine_hiking',
|
||||
] as const;
|
||||
export const sacScaleSchema = z.enum(SAC_SCALE_VALUES);
|
||||
export type SacScale = z.infer<typeof sacScaleSchema>;
|
||||
export type SacStep = 1 | 2 | 3 | 4 | 5 | 6;
|
||||
|
||||
/** The step of the scale a `sac_scale` value names: `hiking` is T1, `difficult_alpine_hiking` T6. */
|
||||
export function sacStepOf(value: SacScale): SacStep {
|
||||
return (SAC_SCALE_VALUES.indexOf(value) + 1) as SacStep;
|
||||
}
|
||||
|
||||
/** The most of an unclear `sac_scale` tag that is handed on, so a stray long value cannot bloat the answer. */
|
||||
export const SAC_SCALE_UNCLEAR_MAX = 64;
|
||||
|
||||
/**
|
||||
* One way's grades as OpenStreetMap tags them, read to the scale's own steps:
|
||||
* `mtb:scale` 0 to 6 (a + or - kept), `mtb:scale:uphill` 0 to 5,
|
||||
* `mtb:scale:imba` 0 to 4. Null where the way has no such tag or one that is
|
||||
* no step of the scale.
|
||||
* `mtb:scale:imba` 0 to 4, and `sac_scale` as one of its six values. Null
|
||||
* where the way has no such tag or one that is no step of the scale. A
|
||||
* `sac_scale` that is there but names no single step (`T3`, two values joined
|
||||
* by `;`) is handed on as written in `sac_scale_unclear`, so the planner can
|
||||
* say the grade is unclear rather than missing; it is never read to a step.
|
||||
*/
|
||||
export const tourTrailGradeSchema = z.object({
|
||||
id: z.number().int().positive(),
|
||||
@@ -37,6 +65,8 @@ export const tourTrailGradeSchema = z.object({
|
||||
.string()
|
||||
.regex(/^[0-4]$/)
|
||||
.nullable(),
|
||||
sac_scale: sacScaleSchema.nullable(),
|
||||
sac_scale_unclear: z.string().min(1).max(SAC_SCALE_UNCLEAR_MAX).nullable(),
|
||||
});
|
||||
export type TourTrailGrade = z.infer<typeof tourTrailGradeSchema>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user