Skip to content

Commit

Permalink
Add difficulty slider to search filters (#41)
Browse files Browse the repository at this point in the history
* Add difficulty slider to search filters

* Remove logs

* Refactor to have values internal to models lib
  • Loading branch information
greghart authored Dec 30, 2024
1 parent 4ac23ae commit e991de3
Show file tree
Hide file tree
Showing 10 changed files with 207 additions and 118 deletions.
3 changes: 3 additions & 0 deletions packages/models/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,8 @@
"power-putty-io": "^1.0.1",
"power-putty-test": "^1.0.0",
"typescript": "5.x.x"
},
"devDependencies": {
"mocha": "^11.0.1"
}
}
213 changes: 112 additions & 101 deletions packages/models/src/Grade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,106 +45,11 @@ class Grade {
}
}

export default Grade;

/**
* GRADE DATA BELOW
* Grade parsers
*/

type TupleToRecord<T extends readonly string[], V> = {
[K in T[number]]: V; // or any other type
};

// We are focusing on bouldering, so start with v scale and will adapt others
// into this value system. To start with, normalize as base (V+1) * 10
// Plus/minus grades can add/subtract 1
// Slash grades can average the two grades.
const VGrade = [
"VB",
"V0",
"V1",
"V2",
"V3",
"V4",
"V5",
"V6",
"V7",
"V8",
"V9",
"V10",
"V11",
"V12",
"V13",
"V14",
"V15",
"V16",
"V17",
] as const;
const vGrades: TupleToRecord<typeof VGrade, number> = {
VB: 0,
V0: 10,
V1: 20,
V2: 30,
V3: 40,
V4: 50,
V5: 60,
V6: 70,
V7: 80,
V8: 90,
V9: 100,
V10: 110,
V11: 120,
V12: 130,
V13: 140,
V14: 150,
V15: 160,
V16: 170,
V17: 180,
};

export const grades: Record<GradingSystemType, Record<string, number>> = {
[GradingSystemType.V]: vGrades,
// Sourced from https://en.wikipedia.org/wiki/Grade_(climbing)#Comparison_bouldering
[GradingSystemType.YDS]: {
"5.1": -6,
"5.2": -5,
"5.3": -4,
"5.4": -3,
"5.5": -2,
"5.6": -1,
"5.7": vGrades.VB,
"5.8": vGrades.V0 - 1,
"5.9": vGrades.V0,
"5.10": vGrades.V0 + 1,
"5.10a": vGrades.V0 + 1, // 5.10a == 5.10
"5.10b": vGrades.V0 + 2,
"5.10c": vGrades.V1,
"5.10d": (vGrades.V1 + vGrades.V2) / 2, // V1-2
"5.11": vGrades.V2,
"5.11a": vGrades.V2, // 5.11a == 5.11
"5.11b": vGrades.V2 + 1,
"5.11c": vGrades.V3,
"5.11d": vGrades.V3 + 1,
"5.12": vGrades.V4,
"5.12a": vGrades.V4, // 5.12a == 5.12
"5.12b": vGrades.V5,
"5.12c": vGrades.V6,
"5.12d": vGrades.V7,
"5.13": vGrades.V8,
"5.13a": vGrades.V8, // 5.13a == 5.13
"5.13b": vGrades.V8 + 1,
"5.13c": vGrades.V9,
"5.13d": vGrades.V10,
"5.14": vGrades.V11,
"5.14a": vGrades.V11, // 5.14a == 5.14
"5.14b": vGrades.V12,
"5.14c": vGrades.V13,
"5.14d": vGrades.V14,
"5.15": vGrades.V15,
"5.15a": vGrades.V15, // 5.15a == 5.15
"5.15b": vGrades.V16,
"5.15c": vGrades.V17,
},
};

export function parseRaw(raw: string): IGrade {
if (raw.length === 0) {
throw new RangeError("Invalid grade ''");
Expand Down Expand Up @@ -236,7 +141,7 @@ export function parseRawVScore(raw: string, flexible: boolean = false): number {
plus = true;
}
// Should be a basic grade now
let score = grades[GradingSystemType.V]![raw];
let score = gradeValues[GradingSystemType.V]![raw];
if (score === undefined) {
throw new RangeError(`Invalid V grade '${raw}'`);
}
Expand Down Expand Up @@ -276,7 +181,7 @@ export function parseRawYDSScore(
plus = true;
}
// Should be a basic grade now
let score = grades[GradingSystemType.YDS]![raw];
let score = gradeValues[GradingSystemType.YDS]![raw];
if (score === undefined) {
throw new RangeError(`Invalid YDS grade '${raw}'`);
}
Expand All @@ -290,4 +195,110 @@ export function parseRawYDSScore(
}
export const parseRawYDS = splitter(GradingSystemType.YDS, parseRawYDSScore);

export default Grade;

/** Grade data */
type TupleToRecord<T extends readonly string[], V> = {
[K in T[number]]: V; // or any other type
};

// We are focusing on bouldering, so start with v scale and will adapt others
// into this value system. To start with, normalize as base (V+1) * 10
// Plus/minus grades can add/subtract 1
// Slash grades can average the two grades.
const VGrade = [
"VB",
"V0",
"V1",
"V2",
"V3",
"V4",
"V5",
"V6",
"V7",
"V8",
"V9",
"V10",
"V11",
"V12",
"V13",
"V14",
"V15",
"V16",
"V17",
] as const;
const vGradeValues: TupleToRecord<typeof VGrade, number> = {
VB: 0,
V0: 10,
V1: 20,
V2: 30,
V3: 40,
V4: 50,
V5: 60,
V6: 70,
V7: 80,
V8: 90,
V9: 100,
V10: 110,
V11: 120,
V12: 130,
V13: 140,
V14: 150,
V15: 160,
V16: 170,
V17: 180,
};

export const gradeValues: Record<GradingSystemType, Record<string, number>> = {
[GradingSystemType.V]: vGradeValues,
// Sourced from https://en.wikipedia.org/wiki/Grade_(climbing)#Comparison_bouldering
[GradingSystemType.YDS]: {
"5.1": -6,
"5.2": -5,
"5.3": -4,
"5.4": -3,
"5.5": -2,
"5.6": -1,
"5.7": vGradeValues.VB,
"5.8": vGradeValues.V0 - 1,
"5.9": vGradeValues.V0,
"5.10": vGradeValues.V0 + 1,
"5.10a": vGradeValues.V0 + 1, // 5.10a == 5.10
"5.10b": vGradeValues.V0 + 2,
"5.10c": vGradeValues.V1,
"5.10d": (vGradeValues.V1 + vGradeValues.V2) / 2, // V1-2
"5.11": vGradeValues.V2,
"5.11a": vGradeValues.V2, // 5.11a == 5.11
"5.11b": vGradeValues.V2 + 1,
"5.11c": vGradeValues.V3,
"5.11d": vGradeValues.V3 + 1,
"5.12": vGradeValues.V4,
"5.12a": vGradeValues.V4, // 5.12a == 5.12
"5.12b": vGradeValues.V5,
"5.12c": vGradeValues.V6,
"5.12d": vGradeValues.V7,
"5.13": vGradeValues.V8,
"5.13a": vGradeValues.V8, // 5.13a == 5.13
"5.13b": vGradeValues.V8 + 1,
"5.13c": vGradeValues.V9,
"5.13d": vGradeValues.V10,
"5.14": vGradeValues.V11,
"5.14a": vGradeValues.V11, // 5.14a == 5.14
"5.14b": vGradeValues.V12,
"5.14c": vGradeValues.V13,
"5.14d": vGradeValues.V14,
"5.15": vGradeValues.V15,
"5.15a": vGradeValues.V15, // 5.15a == 5.15
"5.15b": vGradeValues.V16,
"5.15c": vGradeValues.V17,
},
};
export const grades: Record<GradingSystemType, Record<string, Grade>> = Object.keys(GradingSystemType).reduce(
(bySystem, system) => ({
...bySystem,
[system]: Object.keys(gradeValues[system as GradingSystemType]).reduce((agg, v) => {
agg[v] = Grade.build(v);
return agg;
}, {} as Record<string, Grade>),
}),
{} as Record<GradingSystemType, Record<string, Grade>>,
);
19 changes: 19 additions & 0 deletions packages/models/src/Topo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* A Topo represents drawn diagrams on top of photo, intending
* to show route lines, sections of a wall, or a topo of a crag
* showing areas from afar.
*
* Features:
* * Vector based canvas editor to generate topos
* * A Topo can be associated with any Photoable, and also have
* additional relationships with crags, areas, boulders, or routes.
* * Crag -- topos annotate areas in the crag
* * Area -- topos annotate boulders in an area
* * Boulder -- topos annotate the routes on a boulder.
* * Route -- topo annotates a single route on a single image.
* * A Topo will have various tools for generating diagrams
* * Path -- basic paths, or even a closed polygon
* * Icons -- signal hold types, or mark flexing holds, etc.
* * Labels -- while associations will help us relate which path applies to what,
* labels can bake that into the image.
*/
10 changes: 4 additions & 6 deletions packages/models/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,15 @@ export {
isCoordinateLiteral,
type ICoordinate,
type ICoordinateLiteral,
type ICoordinateTuple,
type ICoordinateTuple
} from "./Coordinate.js";
export {
default as CoordinateOptional,
type ICoordinateOptional,
type ICoordinateOptional
} from "./CoordinateOptional.js";
export { default as Crag, type ICrag } from "./Crag.js";
export {
default as Grade,
GradingSystemType,
grades,
type IGrade,
default as Grade, grades, GradingSystemType, type IGrade
} from "./Grade.js";
export { default as Line, type ILine } from "./Line.js";
export { default as Photo, type IPhoto } from "./Photo.js";
Expand All @@ -28,3 +25,4 @@ export { default as Polygon, type IPolygon } from "./Polygon.js";
export { default as Route, type IRoute } from "./Route.js";
export { default as Trail, type ITrail } from "./Trail.js";
export { default as Upload, type IUpload } from "./Upload.js";

37 changes: 37 additions & 0 deletions packages/ui/app/_components/search/DifficultySlider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";
import { searchParamsParsers } from "@/app/_components/search/searchParams";
import useQueryState from "@/app/_util/useQueryState";
import { Slider } from "@mui/material";
import { grades } from "models";

export default function DifficultySlider() {
const [difficultyMin, setDifficultyMin] = useQueryState(
"vMin",
searchParamsParsers.vMin
);
const [difficultyMax, setDifficultyMax] = useQueryState(
"vMax",
searchParamsParsers.vMax
);
// VB - V17
const marks = Object.values(grades.V).map((g) => ({
value: g.value,
label: g.raw,
}));
return (
<Slider
aria-label="v scale"
value={[difficultyMin, difficultyMax]}
max={marks[marks.length - 1].value}
step={null}
marks={marks}
onChange={(_, _values: unknown) => {
const values = _values as [number, number];
setDifficultyMin(values[0]);
setDifficultyMax(values[1]);
}}
valueLabelDisplay="off"
valueLabelFormat={(_, i) => marks[i].label}
/>
);
}
2 changes: 2 additions & 0 deletions packages/ui/app/_components/search/SearchFilters.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import DifficultySlider from "@/app/_components/search/DifficultySlider";
import SearchShadeCheck from "@/app/_components/search/SearchShadeCheck";
import SearchTypeSelect from "@/app/_components/search/SearchTypeSelect";
import SunHoursField from "@/app/_components/search/SunHoursField";
Expand Down Expand Up @@ -28,6 +29,7 @@ export default function SearchFilters(props: Props) {
<FormGroup>
<SearchTypeSelect />
<SearchShadeCheck />
<DifficultySlider />
<FormControl fullWidth sx={{ p: 1 }}>
<SunHoursField coordinate={props.shadeLocation} />
</FormControl>
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/app/_components/search/searchParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export const searchParamsParsers = {
shade: parseAsBoolean.withDefault(false).withOptions(defaultOptions),
// TODO: Nicety, get users' current hour as default
shadeHour: parseAsInteger.withDefault(12).withOptions(defaultOptions),
vMin: parseAsInteger.withDefault(0).withOptions(defaultOptions),
vMax: parseAsInteger.withDefault(0).withOptions(defaultOptions),
};

export const searchParamsCache = createSearchParamsCache(searchParamsParsers);
Loading

0 comments on commit e991de3

Please sign in to comment.