• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

paulmthompson / WhiskerToolbox / 18477247352

13 Oct 2025 08:18PM UTC coverage: 72.391% (+0.4%) from 71.943%
18477247352

push

github

web-flow
Merge pull request #140 from paulmthompson/kdtree

Jules PR

164 of 287 new or added lines in 3 files covered. (57.14%)

350 existing lines in 9 files now uncovered.

51889 of 71679 relevant lines covered (72.39%)

63071.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.66
/src/DataManager/transforms/Lines/Line_Angle/line_angle.cpp
1
#include "line_angle.hpp"
2

3
#include "AnalogTimeSeries/Analog_Time_Series.hpp"
4
#include "Lines/Line_Data.hpp"
5
#include "CoreGeometry/line_geometry.hpp"
6
#include "utils/polynomial/polynomial_fit.hpp"
7

8
#include <armadillo>
9

10
#include <cmath>
11
#include <map>
12
#include <numbers>
13
#include <vector>
14

15

16
float normalize_angle(float raw_angle, float reference_x, float reference_y) {
37✔
17
    // Calculate the angle of the reference vector (if not the default x-axis)
18
    float reference_angle = 0.0f;
37✔
19
    if (!(reference_x == 1.0f && reference_y == 0.0f)) {
37✔
20
        reference_angle = std::atan2(reference_y, reference_x);
12✔
21
        // Convert to degrees
22
        reference_angle *= 180.0f / static_cast<float>(std::numbers::pi);
12✔
23
    }
24

25
    // Adjust the raw angle by subtracting the reference angle
26
    float normalized_angle = raw_angle - reference_angle;
37✔
27

28
    // Normalize to range [-180, 180]
29
    while (normalized_angle > 180.0f) normalized_angle -= 360.0f;
37✔
30
    while (normalized_angle <= -180.0f) normalized_angle += 360.0f;
37✔
31

32
    return normalized_angle;
37✔
33
}
34

35
// Calculate angle using direct point comparison
36
float calculate_direct_angle(Line2D const & line, float position, float reference_x, float reference_y) {
26✔
37
    if (line.size() < 2) {
26✔
38
        return 0.0f;
×
39
    }
40

41
    // Calculate the index of the position point, ensuring we never select the base point
42
    // when computing the direction from the first point. This avoids a zero-length vector
43
    // for 2-point lines at position 1.0.
44
    auto idx = static_cast<size_t>(position * static_cast<float>((line.size() - 1)));
26✔
45
    if (idx == 0) {
26✔
46
        idx = 1; // always pick at least the second point
7✔
47
    } else if (idx >= line.size()) {
19✔
UNCOV
48
        idx = line.size() - 1; // clamp to last valid index
×
49
    } else if (idx >= line.size() - 1) {
19✔
50
        idx = line.size() - 1; // for end positions, use the last point
3✔
51
    }
52

53
    Point2D<float> const base = line[0];
26✔
54
    Point2D<float> const pos = line[idx];
26✔
55

56
    // Calculate angle in radians (atan2 returns value in range [-π, π])
57
    float raw_angle = std::atan2(pos.y - base.y, pos.x - base.x);
26✔
58

59
    // Convert to degrees
60
    float angle_degrees = raw_angle * 180.0f / static_cast<float>(std::numbers::pi);
26✔
61

62
    // Normalize with respect to the reference vector
63
    return normalize_angle(angle_degrees, reference_x, reference_y);
26✔
64
}
65

66
// Calculate angle using polynomial parameterization
67
float calculate_polynomial_angle(Line2D const & line,
12✔
68
                                 float position,
69
                                 int polynomial_order,
70
                                 float reference_x,
71
                                 float reference_y) {
72
    if (line.size() < static_cast<size_t>(polynomial_order + 1)) {
12✔
73
        // Fall back to direct method if not enough points
74
        return calculate_direct_angle(line, position, reference_x, reference_y);
1✔
75
    }
76

77
    // Extract x and y coordinates
78
    std::vector<double> x_coords;
11✔
79
    std::vector<double> y_coords;
11✔
80

81
    auto length = calc_length(line);
11✔
82

83
    x_coords.reserve(line.size());
11✔
84
    y_coords.reserve(line.size());
11✔
85
    auto t_values_f = calc_cumulative_length_vector(line);
11✔
86
    for (size_t i = 0; i < line.size(); ++i) {
1,064✔
87
        // Normalize t_values to [0, 1]
88
        t_values_f[i] /= length;
1,053✔
89
    }
90

91
    std::vector<double> t_values(t_values_f.begin(), t_values_f.end());
33✔
92

93
    for (size_t i = 0; i < line.size(); ++i) {
1,064✔
94
        x_coords.push_back(static_cast<double>(line[i].x));
1,053✔
95
        y_coords.push_back(static_cast<double>(line[i].y));
1,053✔
96
    }
97

98
    // Fit polynomials to x(t) and y(t)
99
    std::vector<double> x_coeffs = fit_polynomial(t_values, x_coords, polynomial_order);
11✔
100
    std::vector<double> y_coeffs = fit_polynomial(t_values, y_coords, polynomial_order);
11✔
101

102
    if (x_coeffs.empty() || y_coeffs.empty()) {
11✔
103
        // Fall back to direct method if fitting failed
UNCOV
104
        return calculate_direct_angle(line, position, reference_x, reference_y);
×
105
    }
106

107
    // Evaluate derivatives at the specified position
108
    double t = static_cast<double>(position);
11✔
109
    double dx_dt = evaluate_polynomial_derivative(x_coeffs, t);
11✔
110
    double dy_dt = evaluate_polynomial_derivative(y_coeffs, t);
11✔
111

112
    // Calculate angle using the derivatives
113
    double raw_angle = std::atan2(dy_dt, dx_dt);
11✔
114

115
    // Convert to degrees
116
    float angle_degrees = static_cast<float>(raw_angle * 180.0 / std::numbers::pi);
11✔
117

118
    // Normalize with respect to the reference vector
119
    return normalize_angle(angle_degrees, reference_x, reference_y);
11✔
120
}
11✔
121

122
///////////////////////////////////////////////////////////////////////////////
123

124
std::shared_ptr<AnalogTimeSeries> line_angle(LineData const * line_data, LineAngleParameters const * params) {
30✔
125
    // Call the version with progress reporting but ignore progress
126
    return line_angle(line_data, params, [](int) {});
30✔
127
}
128

129
std::shared_ptr<AnalogTimeSeries> line_angle(LineData const * line_data,
34✔
130
                                             LineAngleParameters const * params,
131
                                             ProgressCallback progressCallback) {
132

133
    static_cast<void>(progressCallback);
134

135
    std::map<int, float> angles;
34✔
136

137
    // Use default parameters if none provided
138
    float position = params ? params->position : 0.2f;
34✔
139
    AngleCalculationMethod method = params ? params->method : AngleCalculationMethod::DirectPoints;
34✔
140
    int polynomial_order = params ? params->polynomial_order : 3;
34✔
141
    float reference_x = params ? params->reference_x : 1.0f;
34✔
142
    float reference_y = params ? params->reference_y : 0.0f;
34✔
143

144
    // Ensure position is within valid range
145
    position = std::max(0.0f, std::min(position, 1.0f));
34✔
146

147
    // Normalize reference vector if needed
148
    if (reference_x != 0.0f || reference_y != 0.0f) {
34✔
149
        float length = std::sqrt(reference_x * reference_x + reference_y * reference_y);
33✔
150
        if (length > 0.0f) {
33✔
151
            reference_x /= length;
33✔
152
            reference_y /= length;
33✔
153
        } else {
154
            // Default to x-axis if invalid reference
UNCOV
155
            reference_x = 1.0f;
×
156
            reference_y = 0.0f;
×
157
        }
158
    } else {
33✔
159
        // Default to x-axis if zero reference
160
        reference_x = 1.0f;
1✔
161
        reference_y = 0.0f;
1✔
162
    }
163

164
    for (auto const & line_and_time: line_data->GetAllLinesAsRange()) {
72✔
165
        if (line_and_time.lines.empty()) {
38✔
UNCOV
166
            continue;
×
167
        }
168

169
        Line2D const & line = line_and_time.lines[0];
38✔
170

171
        if (line.size() < 2) {
38✔
172
            continue;
1✔
173
        }
174

175
        float angle = 0.0f;
37✔
176

177
        // Calculate angle using the selected method
178
        if (method == AngleCalculationMethod::DirectPoints) {
37✔
179
            angle = calculate_direct_angle(line, position, reference_x, reference_y);
25✔
180
        } else if (method == AngleCalculationMethod::PolynomialFit) {
12✔
181
            angle = calculate_polynomial_angle(line, position, polynomial_order, reference_x, reference_y);
12✔
182
        }
183

184
        angles[static_cast<int>(line_and_time.time.getValue())] = angle;
37✔
185
    }
38✔
186

187
    return std::make_shared<AnalogTimeSeries>(angles);
68✔
188
}
34✔
189

190
///////////////////////////////////////////////////////////////////////////////
191

192
std::string LineAngleOperation::getName() const {
148✔
193
    return "Calculate Line Angle";
444✔
194
}
195

196
std::type_index LineAngleOperation::getTargetInputTypeIndex() const {
148✔
197
    return typeid(std::shared_ptr<LineData>);
148✔
198
}
199

200
bool LineAngleOperation::canApply(DataTypeVariant const & dataVariant) const {
4✔
201
    if (!std::holds_alternative<std::shared_ptr<LineData>>(dataVariant)) {
4✔
UNCOV
202
        return false;
×
203
    }
204

205
    auto const * ptr_ptr = std::get_if<std::shared_ptr<LineData>>(&dataVariant);
4✔
206

207
    return ptr_ptr && *ptr_ptr;
4✔
208
}
209

210
std::unique_ptr<TransformParametersBase> LineAngleOperation::getDefaultParameters() const {
4✔
211
    return std::make_unique<LineAngleParameters>();
4✔
212
}
213

214
DataTypeVariant LineAngleOperation::execute(DataTypeVariant const & dataVariant,
4✔
215
                                            TransformParametersBase const * transformParameters,
216
                                            ProgressCallback progressCallback) {
217
    auto const * ptr_ptr = std::get_if<std::shared_ptr<LineData>>(&dataVariant);
4✔
218

219
    if (!ptr_ptr || !(*ptr_ptr)) {
4✔
UNCOV
220
        std::cerr << "LineAngleOperation::execute called with incompatible variant type or null data." << std::endl;
×
221
        return {};// Return empty variant
×
222
    }
223

224
    LineData const * line_raw_ptr = (*ptr_ptr).get();
4✔
225

226
    LineAngleParameters const * typed_params = nullptr;
4✔
227
    if (transformParameters) {
4✔
228
        typed_params = dynamic_cast<LineAngleParameters const *>(transformParameters);
4✔
229
        if (!typed_params) {
4✔
UNCOV
230
            std::cerr << "LineAngleOperation::execute: Invalid parameter type" << std::endl;
×
231
        }
232
    }
233

234
    std::shared_ptr<AnalogTimeSeries> result_ts = line_angle(line_raw_ptr, typed_params, progressCallback);
4✔
235

236
    // Handle potential failure from the calculation function
237
    if (!result_ts) {
4✔
UNCOV
238
        std::cerr << "LineAngleOperation::execute: 'line_angle' failed to produce a result." << std::endl;
×
239
        return {};// Return empty variant
×
240
    }
241

242
    std::cout << "LineAngleOperation executed successfully using variant input." << std::endl;
4✔
243
    return result_ts;
4✔
244
}
4✔
245

246
DataTypeVariant LineAngleOperation::execute(DataTypeVariant const & dataVariant,
1✔
247
                                            TransformParametersBase const * transformParameters) {
248

249
    return execute(dataVariant, transformParameters, [](int){});
1✔
250
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc