package climb import ( "math" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // stretch describes one [lengthM, slopePercent] stretch of the buildProfile trace. type stretch struct { lengthM float64 slopePercent float64 } // buildProfile lays out one recorded point every 50 m along the given stretches. func buildProfile(stretches ...stretch) Route { route := Route{Distances: []float64{0}, Elevations: []float64{0}} distanceM, elevationM := 0.0, 0.0 for _, stretch := range stretches { pointCount := int(math.Round(stretch.lengthM / 50)) for index := 1; index <= pointCount; index++ { stepM := stretch.lengthM * float64(index) / float64(pointCount) route.Distances = append(route.Distances, distanceM+stepM) route.Elevations = append(route.Elevations, elevationM+stepM*stretch.slopePercent/100) } distanceM += stretch.lengthM elevationM += stretch.lengthM * stretch.slopePercent / 100 } return route } func TestProfileSectionsKeepsShortRamp(t *testing.T) { route := buildProfile(stretch{300, 4}, stretch{300, 12}, stretch{300, 4}) sections := route.ProfileSections(DefaultProfilePolicy()) require.Len(t, sections, 3) assert.InDelta(t, 4, sections[0].SlopePercent, 0.01) assert.InDelta(t, 12, sections[1].SlopePercent, 0.01) assert.InDelta(t, 4, sections[2].SlopePercent, 0.01) assert.InDelta(t, 300, sections[1].EndDistanceM-sections[1].StartDistanceM, 0.01) } func TestProfileSectionsCollapsesConstantSlope(t *testing.T) { route := buildProfile(stretch{5000, 6}) sections := route.ProfileSections(DefaultProfilePolicy()) require.Len(t, sections, 1) assert.InDelta(t, 6, sections[0].SlopePercent, 0.01) assert.Zero(t, sections[0].StartDistanceM) assert.InDelta(t, 5000, sections[0].EndDistanceM, 0.01) } func TestProfileSectionsReportsDescent(t *testing.T) { route := buildProfile(stretch{2000, -5}) sections := route.ProfileSections(DefaultProfilePolicy()) require.Len(t, sections, 1) assert.InDelta(t, -5, sections[0].SlopePercent, 0.01) } func TestProfileSectionsHonoursBudget(t *testing.T) { route := Route{} for index := 0; index <= 200; index++ { distanceM := float64(index) * 50 route.Distances = append(route.Distances, distanceM) route.Elevations = append(route.Elevations, distanceM*0.05+math.Sin(float64(index))*4) } sections := route.ProfileSections(DefaultProfilePolicy()) assert.Greater(t, len(sections), 10) assert.LessOrEqual(t, len(sections), DefaultProfilePolicy().MaxSections) budgeted := route.ProfileSections(ProfilePolicy{MaxSections: 5, MinBendM: 1, BendPerBaseStep: 0.01}) assert.LessOrEqual(t, len(budgeted), 5) } func TestProfileSectionsKeepsAbsoluteDistances(t *testing.T) { route := buildProfile(stretch{2000, 8}) for index := range route.Distances { route.Distances[index] += 5000 } sections := route.ProfileSections(DefaultProfilePolicy()) require.Len(t, sections, 1) assert.InDelta(t, 5000, sections[0].StartDistanceM, 0.01) assert.InDelta(t, 7000, sections[0].EndDistanceM, 0.01) } func TestProfileSectionsReadsRouteSlice(t *testing.T) { route := buildProfile(stretch{1000, 20}, stretch{1000, 8}, stretch{1000, 20}) start := 20 end := 40 sections := route.Slice(start, end).ProfileSections(DefaultProfilePolicy()) require.Len(t, sections, 1) assert.InDelta(t, 8, sections[0].SlopePercent, 0.01) assert.InDelta(t, route.Distances[start], sections[0].StartDistanceM, 0.01) assert.InDelta(t, route.Distances[end], sections[0].EndDistanceM, 0.01) } func TestProfileSectionsRejectsShortRoute(t *testing.T) { assert.Nil(t, Route{}.ProfileSections(DefaultProfilePolicy())) assert.Nil(t, Route{Distances: []float64{0}, Elevations: []float64{0}}.ProfileSections(DefaultProfilePolicy())) assert.Nil(t, Route{Distances: []float64{0, 10}, Elevations: []float64{0}}.ProfileSections(DefaultProfilePolicy())) }