+#include <stdio.h>
+#include <vector>
+#include "ceres/ceres.h"
+#include "gflags/gflags.h"
+#include "glog/logging.h"
+#include "LatLong-UTMconversion.h"
+#include "constants.h"
+
+#define GPS_SIGMA 20.0
+#define GPS_EXTRA_SIGMA_PER_SECOND 1.0
+
+#define KD_LEAF_SIZE 5
+
+using namespace ceres;
+using std::vector;
+
+struct Coord {
+ double x, y;
+};
+struct TCXPoint {
+ int time;
+ double lat, lon, alt;
+ int hr;
+
+ // computed
+ double utm_north, utm_east;
+ char utm_zone[16];
+};
+struct FootPoint {
+ int time;
+ int hr;
+ int cadence;
+ double speed;
+ double distance;
+};
+
+struct LineSegment {
+ Coord from, to;
+
+ // computed
+ double wv_x, wv_y;
+ double len_sq, inv_len_sq;
+};
+vector<LineSegment> roads;
+
+struct KDTreeNode {
+ enum Axis { LEAF, X_AXIS, Y_AXIS } split_axis;
+ double split_pos;
+ vector<int> roads_this_node;
+ KDTreeNode *left, *right;
+};
+KDTreeNode *root = NULL;
+
+double signed_distance(KDTreeNode::Axis split_axis, double split_pos, Coord c)
+{
+ if (split_axis == KDTreeNode::X_AXIS) {
+ return c.x - split_pos;
+ } else {
+ return c.y - split_pos;
+ }
+}
+
+double split_cost(const vector<int>& remaining_roads, KDTreeNode::Axis split_axis, double split_pos)
+{
+ unsigned in_left = 0, in_right = 0, in_both = 0;
+ for (int i = 0; i < remaining_roads.size(); ++i) {
+ double dist_from = signed_distance(split_axis, split_pos, roads[remaining_roads[i]].from);
+ double dist_to = signed_distance(split_axis, split_pos, roads[remaining_roads[i]].to);
+ if (dist_from > 0.0 && dist_to > 0.0) {
+ ++in_right;
+ } else if (dist_from < 0.0 && dist_to < 0.0) {
+ ++in_left;
+ } else {
+ ++in_both;
+ }
+ }
+
+ return max(in_left, in_right) + 1.1 * in_both;
+}
+
+KDTreeNode* make_kd_tree(const vector<int>& remaining_roads, KDTreeNode::Axis split_axis)
+{
+ KDTreeNode *node = new KDTreeNode;
+ if (remaining_roads.size() <= KD_LEAF_SIZE) {
+ node->split_axis = KDTreeNode::LEAF;
+ node->left = node->right = NULL;
+ node->roads_this_node = remaining_roads;
+ return node;
+ }
+
+ node->split_axis = split_axis;
+
+ // collect all points
+ vector<double> all_points;
+ KDTreeNode::Axis next_axis;
+ if (split_axis == KDTreeNode::X_AXIS) {
+ for (unsigned i = 0; i < remaining_roads.size(); ++i) {
+ all_points.push_back(roads[remaining_roads[i]].from.x - 1e-6);
+ all_points.push_back(roads[remaining_roads[i]].to.x - 1e-6);
+ all_points.push_back(roads[remaining_roads[i]].from.x + 1e-6);
+ all_points.push_back(roads[remaining_roads[i]].to.x + 1e-6);
+ }
+ next_axis = KDTreeNode::Y_AXIS;
+ } else {
+ for (unsigned i = 0; i < remaining_roads.size(); ++i) {
+ all_points.push_back(roads[remaining_roads[i]].from.y - 1e-6);
+ all_points.push_back(roads[remaining_roads[i]].to.y - 1e-6);
+ all_points.push_back(roads[remaining_roads[i]].from.y + 1e-6);
+ all_points.push_back(roads[remaining_roads[i]].to.y + 1e-6);
+ }
+ next_axis = KDTreeNode::X_AXIS;
+ }
+ sort(all_points.begin(), all_points.end());
+
+ vector<double>::iterator new_end = unique(all_points.begin(), all_points.end());
+ all_points.resize(new_end - all_points.begin());
+
+ // now try all split points
+ double best_pos = 0.0;
+ double best_cost = HUGE_VAL;
+
+ for (unsigned i = 0; i < all_points.size(); ++i) {
+ double cost = split_cost(remaining_roads, split_axis, all_points[i]);
+ if (cost < best_cost) {
+ best_pos = all_points[i];
+ best_cost = cost;
+ }
+ }
+ node->split_pos = best_pos;
+
+ vector<int> left, right;
+ for (unsigned i = 0; i < remaining_roads.size(); ++i) {
+ double dist_from = signed_distance(split_axis, best_pos, roads[remaining_roads[i]].from);
+ double dist_to = signed_distance(split_axis, best_pos, roads[remaining_roads[i]].to);
+ if (dist_from > 0.0 && dist_to > 0.0) {
+ right.push_back(remaining_roads[i]);
+ } else if (dist_from < 0.0 && dist_to < 0.0) {
+ left.push_back(remaining_roads[i]);
+ } else {
+ node->roads_this_node.push_back(remaining_roads[i]);
+ }
+ }
+
+ if (left.size() == 0) {
+ node->left = NULL;
+ } else {
+ node->left = make_kd_tree(left, next_axis);
+ }
+ if (right.size() == 0) {
+ node->right = NULL;
+ } else {
+ node->right = make_kd_tree(right, next_axis);
+ }
+ return node;
+}
+
+void plot_kd_tree(FILE *fp, KDTreeNode *node, double min_x, double min_y, double max_x, double max_y) {
+ if (node->split_axis == KDTreeNode::X_AXIS) {
+ // vertical line
+ fprintf(fp, "%f %f %f %f\n", node->split_pos, min_y, node->split_pos, max_y);
+ if (node->left != NULL) {
+ plot_kd_tree(fp, node->left, min_x, min_y, node->split_pos, max_y);
+ }
+ if (node->right != NULL) {
+ plot_kd_tree(fp, node->right, node->split_pos, min_y, max_x, max_y);
+ }
+ }
+ if (node->split_axis == KDTreeNode::Y_AXIS) {
+ // horizontal line
+ fprintf(fp, "%f %f %f %f\n", min_x, node->split_pos, max_x, node->split_pos);
+ if (node->left != NULL) {
+ plot_kd_tree(fp, node->left, min_x, min_y, max_x, node->split_pos);
+ }
+ if (node->right != NULL) {
+ plot_kd_tree(fp, node->right, min_x, node->split_pos, max_x, max_y);
+ }
+ }
+}
+
+char* write_time(time_t t)
+{
+ static char buf[256];
+ struct tm *foo = gmtime(&t);
+ strftime(buf, 256, "%Y-%m-%dT%H:%M:%SZ", foo);
+ return buf;
+}
+
+class MinimizeAccelerationCostFunction {
+ public:
+ MinimizeAccelerationCostFunction() {}
+
+ template<class T>
+ bool operator() (const T * const p0, const T * const p1, const T * const p2, T* e) const {
+ e[0] = p0[0] - T(2.0) * p1[0] + p2[0];
+ e[1] = p0[1] - T(2.0) * p1[1] + p2[1];
+ return true;
+ }
+};
+
+double sq_distance_point_point(const Coord& a, const Coord& b)
+{
+ //return hypot(b.x - a.x, b.y - a.y);
+ return (b.x - a.x) * (b.x - a.x) + (b.y - a.y) * (b.y - a.y);
+}
+
+double sq_distance_point_line_segment(const Coord& c, LineSegment &ls)
+{
+ if (ls.len_sq < 1e-6) {
+ return sq_distance_point_point(c, ls.from);
+ }
+ const double pv_x = c.x - ls.from.x;
+ const double pv_y = c.y - ls.from.y;
+ const double t = (pv_x * ls.wv_x + pv_y * ls.wv_y) * ls.inv_len_sq;
+ if (t <= 0.0) {
+ return sq_distance_point_point(c, ls.from);
+ }
+ if (t >= 1.0) {
+ return sq_distance_point_point(c, ls.to);
+ }
+
+ Coord r;
+ r.x = ls.from.x + t * ls.wv_x;
+ r.y = ls.from.y + t * ls.wv_y;
+ return sq_distance_point_point(c, r);
+}
+
+pair<int, double> find_closest_road_nonkd(Coord c)
+{
+ int best_idx = -1;
+ double sq_best_distance = HUGE_VAL;
+ for (unsigned i = 0; i < roads.size(); ++i) {
+ double sq_distance = sq_distance_point_line_segment(c, roads[i]);
+ if (sq_distance < sq_best_distance) {
+ best_idx = i;
+ sq_best_distance = sq_distance;
+ }
+ }
+ return make_pair(best_idx, std::sqrt(sq_best_distance));
+}
+
+struct KDTreeJob {
+ KDTreeJob(KDTreeNode* node, KDTreeNode* parent) : node(node), parent(parent) {}
+ KDTreeJob(const KDTreeJob& other) : node(other.node), parent(other.parent) {}
+
+ KDTreeNode* node;
+ KDTreeNode* parent;
+};
+vector<KDTreeJob> kd_todo;
+//vector<KDTreeNode*> kd_todo;
+
+pair<int, double> find_closest_road(Coord c)
+{
+ int best_idx = -1;
+ double sq_best_distance = HUGE_VAL;
+
+ kd_todo.clear();
+ kd_todo.push_back(KDTreeJob(root, NULL));
+ int kd_pos = 0;
+
+ while (kd_todo.size() > kd_pos) {
+ KDTreeJob job = kd_todo[kd_pos++];
+ KDTreeNode* node = job.node;
+
+ // Re-verify versus sq_best_distance here, since it might have changed.
+ if (job.parent != NULL) {
+ double dist = signed_distance(job.parent->split_axis, job.parent->split_pos, c);
+ if (dist*dist > sq_best_distance) {
+ // OK, later changes to best_distance has rendered this job moot.
+ continue;
+ }
+ }
+shortcut:
+
+ // Check all nodes that are interior to this.
+ for (unsigned i = 0; i < node->roads_this_node.size(); ++i) {
+ int idx = node->roads_this_node[i];
+ double sq_distance = sq_distance_point_line_segment(c, roads[idx]);
+ if (sq_distance < sq_best_distance || (sq_distance == sq_best_distance && idx < best_idx)) {
+ best_idx = idx;
+ sq_best_distance = sq_distance;
+ }
+ }
+
+ if (node->split_axis == KDTreeNode::LEAF) {
+ continue;
+ }
+
+ double dist = signed_distance(node->split_axis, node->split_pos, c);
+ if (dist * dist <= sq_best_distance) {
+ // gah! we have to put both in there.
+ // try at least the correct side first.
+ if (dist > 0.0) {
+ if (node->left != NULL) {
+ kd_todo.push_back(KDTreeJob(node->left, node));
+ }
+ node = node->right;
+ if (node != NULL) {
+ goto shortcut;
+ }
+ } else {
+ if (node->right != NULL) {
+ kd_todo.push_back(KDTreeJob(node->right, node));
+ }
+ node = node->left;
+ if (node != NULL) {
+ goto shortcut;
+ }
+ }
+ } else {
+ // woo, we can do with going on only one side
+ if (dist > 0.0) {
+ // go on to the right side
+ node = node->right;
+ if (node != NULL) {
+ goto shortcut;
+ }
+ continue;
+ }
+ if (dist < 0.0) {
+ // go on to the left side
+ node = node->left;
+ if (node != NULL) {
+ goto shortcut;
+ }
+ continue;
+ }
+ }
+ }
+
+ return make_pair(best_idx, std::sqrt(sq_best_distance));
+}
+
+class DistanceFromRoadCostFunction : public SizedCostFunction<1, 2, 2> {
+ public:
+ virtual bool Evaluate(double const* const* p,
+ double* e,
+ double** jacobians) const {
+ (void) jacobians; // Ignored; filled in by numeric differentiation.
+
+ Coord c;
+ c.x = p[0][0];
+ c.y = p[0][1];
+
+ Coord prev_c;
+ prev_c.x = p[1][0];
+ prev_c.y = p[1][1];
+
+ pair<int, double> closest_road = find_closest_road(c);
+#if 0
+ // verify kd behavior
+ pair<int, double> closest_road_nonkd = find_closest_road_nonkd(c);
+ if (closest_road.first != closest_road_nonkd.first) {
+ printf("oops! for <%f,%f> kd=(%d,%.10f) and nonkd=(%d,%.10f)\n",
+ c.x, c.y, closest_road.first, closest_road.second,
+ closest_road_nonkd.first, closest_road_nonkd.second);
+ printf("kd:\n%f %f\n%f %f\n",
+ roads[closest_road.first].from.x,
+ roads[closest_road.first].from.y,
+ roads[closest_road.first].to.x,
+ roads[closest_road.first].to.y);
+ printf("Nd:\n%f %f\n%f %f\n",
+ roads[closest_road_nonkd.first].from.x,
+ roads[closest_road_nonkd.first].from.y,
+ roads[closest_road_nonkd.first].to.x,
+ roads[closest_road_nonkd.first].to.y);
+ exit(1);
+ }
+#endif
+ double prev_distance = std::sqrt(sq_distance_point_line_segment(prev_c, roads[closest_road.first]));
+
+ // hack
+ //e[0] = 2.0 - std::exp(-(best_distance * best_distance)) - std::exp(-(prev_distance * prev_distance));
+ //e[0] = 0.2 * (best_distance + 0.5 * prev_distance);
+ //e[0] = ceres::log(best_distance);
+ e[0] = closest_road.second + 0.5 * prev_distance;
+ return true;
+ }
+};
+
+class FootDiffCostFunction {
+ public:
+ FootDiffCostFunction(double speed) : speed_(speed) {}
+
+ template<class T>
+ bool operator() (const T * const p0, const T * const p1, T* e) const {
+ T ex = p1[0] - p0[0];
+ T ey = p1[1] - p0[1];
+ T speed = 0.5 * ceres::sqrt(ex * ex + ey * ey);
+ e[0] = speed - speed_;
+ return true;
+ }
+
+ private:
+ double speed_;
+};
+
+class GPSDiffCostFunction {
+ public:
+ GPSDiffCostFunction(double x, double y) : x_(x), y_(y) {}
+
+ template<class T>
+ bool operator() (const T * const p, T* e) const {
+ T x_dist = p[0] - T(x_);
+ T y_dist = p[1] - T(y_);
+ T sq_dist = x_dist * x_dist + y_dist * y_dist;
+ e[0] = T(10.0) * (T(1.0) - ceres::exp(-sq_dist / T(GPS_SIGMA * GPS_SIGMA)));
+ //e[0] = sq_dist / GPS_SIGMA;
+ // e[0] = sq_dist;
+ return true;
+ }
+
+ private:
+ double x_, y_;
+};
+
+class MyIterationCallback : public IterationCallback {
+ public:
+ MyIterationCallback(Coord *c, int num_points) : c_(c), num_points_(num_points) {}
+
+ virtual CallbackReturnType operator()(const IterationSummary& summary)
+ {
+ char buf[256];
+ sprintf(buf, "temp%04d.plot", summary.iteration);
+ FILE *fp = fopen(buf, "w");
+ for (int i = 0; i < num_points_; ++i) {
+ fprintf(fp, "%f %f\n", c_[i].x, c_[i].y);
+ }
+ fclose(fp);
+ return SOLVER_CONTINUE;
+ }
+
+ private:
+ Coord *c_;
+ int num_points_;
+};
+
+
+int main(int argc, char** argv) {
+ google::ParseCommandLineFlags(&argc, &argv, true);
+ google::InitGoogleLogging(argv[0]);
+
+ int max_time = INT_MIN, min_time = INT_MAX;
+
+ vector<TCXPoint> tcx_points;
+ vector<FootPoint> foot_points;
+
+ FILE *fp = fopen(argv[1], "r");
+ if (fp == NULL) {
+ perror(argv[1]);
+ exit(1);
+ }
+
+ while (!feof(fp)) {
+ char buf[256];
+ if (fgets(buf, 256, fp) == NULL) {
+ break;
+ }
+ if (strncmp(buf, "tcx ", 4) == 0) {
+ TCXPoint p;
+ if (sscanf(buf, "tcx %d %lf %lf %lf %d", &p.time, &p.lat, &p.lon, &p.alt, &p.hr) != 5) {
+ fprintf(stderr, "parse error: %s\n", buf);
+ exit(1);
+ }
+
+ LLtoUTM(23, p.lat * rad2deg, p.lon * rad2deg, p.utm_north, p.utm_east, p.utm_zone);
+
+ tcx_points.push_back(p);
+ max_time = max(max_time, p.time);
+ min_time = min(min_time, p.time);
+ continue;
+ }
+ if (strncmp(buf, "footpod ", 8) == 0) {
+ FootPoint p;
+ if (sscanf(buf, "footpod %d %d %d %lf %lf", &p.time, &p.hr, &p.cadence, &p.speed, &p.distance) != 5) {
+ fprintf(stderr, "parse error: %s\n", buf);
+ exit(1);
+ }
+ foot_points.push_back(p);
+ max_time = max(max_time, p.time);
+ min_time = min(min_time, p.time);
+ continue;
+ }
+ }
+ fclose(fp);
+
+ // normalize the utm guys a bit
+ double utm_north_sum = 0.0, utm_east_sum = 0.0;
+ for (unsigned i = 0; i < tcx_points.size(); ++i) {
+ utm_north_sum += tcx_points[i].utm_north;
+ utm_east_sum += tcx_points[i].utm_east;
+ }
+ utm_north_sum /= tcx_points.size();
+ utm_east_sum /= tcx_points.size();
+ for (unsigned i = 0; i < tcx_points.size(); ++i) {
+ tcx_points[i].utm_north -= utm_north_sum;
+ tcx_points[i].utm_east -= utm_east_sum;
+ }
+
+ // Read the OSM file
+ fp = fopen(argv[2], "r");
+ if (fp == NULL) {
+ perror(argv[2]);
+ exit(1);
+ }
+
+ while (!feof(fp)) {
+ double lat1, lon1, lat2, lon2;
+ if (fscanf(fp, "%lf %lf %lf %lf\n", &lat1, &lon1, &lat2, &lon2) != 4) {
+ break;
+ }
+
+ char utm_zone[16];
+ LineSegment seg;
+ LLtoUTM(23, lat1, lon1, seg.from.y, seg.from.x, utm_zone);
+ LLtoUTM(23, lat2, lon2, seg.to.y, seg.to.x, utm_zone);
+
+ seg.from.y -= utm_north_sum;
+ seg.from.x -= utm_east_sum;
+ seg.to.y -= utm_north_sum;
+ seg.to.x -= utm_east_sum;
+
+ seg.len_sq = (seg.to.y - seg.from.y) * (seg.to.y - seg.from.y) + (seg.to.x - seg.from.x) * (seg.to.x - seg.from.x);
+ seg.inv_len_sq = 1.0 / seg.len_sq;
+ seg.wv_x = seg.to.x - seg.from.x;
+ seg.wv_y = seg.to.y - seg.from.y;
+
+ roads.push_back(seg);
+ }
+ fclose(fp);
+
+ vector<int> all_road_indexes;
+ for (unsigned i = 0; i < roads.size(); ++i) {
+ all_road_indexes.push_back(i);
+ }
+ root = make_kd_tree(all_road_indexes, KDTreeNode::X_AXIS);
+ //root = make_kd_tree(all_road_indexes, KDTreeNode::Y_AXIS);
+
+ fp = fopen("kdtree.plot", "w");
+ plot_kd_tree(fp, root, -700.0, -2100.0, 800.0, 1800.0);
+ fclose(fp);
+
+ fp = fopen("map.plot", "w");
+ for (unsigned i = 0; i < roads.size(); ++i) {
+ fprintf(fp, "%f %f %f %f\n", roads[i].from.x, roads[i].from.y, roads[i].to.x, roads[i].to.y);
+ }
+ fclose(fp);
+
+ int num_points = max_time - min_time + 1;
+ Coord c[num_points];
+
+ Problem problem;
+
+ // Seed the initial path with some guesses.
+ for (int t = min_time; t <= max_time; ++t) {
+ // find the closest TCX point (yeah, yeah, linear search...)
+ int best_idx = -1;
+ int best_distance = INT_MAX;
+ for (unsigned i = 0; i < tcx_points.size(); ++i) {
+ int distance = abs(t - tcx_points[i].time);
+ if (distance < best_distance) {
+ best_idx = i;
+ best_distance = distance;
+ }
+ }
+
+ c[t - min_time].x = tcx_points[best_idx].utm_east + (rand() % 1000) / 1000.0;
+ c[t - min_time].y = tcx_points[best_idx].utm_north + (rand() % 1000) / 1000.0;
+ }
+
+ // Create regularization constraints.
+ for (int t = min_time + 1; t <= max_time - 1; ++t) {
+ problem.AddResidualBlock(
+ new AutoDiffCostFunction<MinimizeAccelerationCostFunction, 2, 2, 2, 2>(
+ new MinimizeAccelerationCostFunction),
+ NULL,
+ reinterpret_cast<double *>(&c[(t - min_time) - 1]),
+ reinterpret_cast<double *>(&c[(t - min_time) + 0]),
+ reinterpret_cast<double *>(&c[(t - min_time) + 1]));
+ }
+
+ // Create close-to-road constraints.
+ for (int t = min_time + 1; t <= max_time; ++t) {
+ problem.AddResidualBlock(
+ new NumericDiffCostFunction<DistanceFromRoadCostFunction, FORWARD, 1, 2, 2>(
+ new DistanceFromRoadCostFunction,
+ ceres::TAKE_OWNERSHIP),
+ NULL,
+ reinterpret_cast<double *>(&c[t - min_time]),
+ reinterpret_cast<double *>(&c[t - min_time - 1]));
+ }
+
+ // Now create one contraint per GPS point.
+ for (unsigned i = 0; i < tcx_points.size(); ++i) {
+ int t = tcx_points[i].time;
+ problem.AddResidualBlock(
+ new AutoDiffCostFunction<GPSDiffCostFunction, 1, 2>(
+ new GPSDiffCostFunction(tcx_points[i].utm_east, tcx_points[i].utm_north)),
+ NULL,
+ reinterpret_cast<double *>(&c[t - min_time]));
+ }
+
+ // And per foot point.
+ for (unsigned i = 0; i < foot_points.size(); ++i) {
+ int t = foot_points[i].time;
+ if (t - 1 < min_time || t + 1 >= max_time) {
+ continue;
+ }
+ problem.AddResidualBlock(
+ new AutoDiffCostFunction<FootDiffCostFunction, 1, 2, 2>(
+ new FootDiffCostFunction(foot_points[i].speed)),
+ NULL,
+ reinterpret_cast<double *>(&c[(t - min_time) - 1]),
+ reinterpret_cast<double *>(&c[(t - min_time) + 1]));
+ }
+
+ printf("pushed %d+%d points\n", int(tcx_points.size()), int(foot_points.size()));
+ printf("time from %d to %d\n", min_time, max_time);
+
+ // Run the solver!
+ Solver::Options options;
+ options.max_num_iterations = 1000000;
+ options.linear_solver_type = ceres::SPARSE_NORMAL_CHOLESKY;
+ //options.linear_solver_type = ceres::DENSE_QR;
+ options.minimizer_progress_to_stdout = true;
+
+ // callback
+ options.update_state_every_iteration = true;
+ options.callbacks.push_back(new MyIterationCallback(c, num_points));
+
+ Solver::Summary summary;
+ Solve(options, &problem, &summary);
+ // if (summary.termination_type == NUMERICAL_FAILURE) {
+ // return 1;
+ // }
+
+ fp = fopen("out.plot", "w");
+ for (int t = min_time; t <= max_time; ++t) {
+ fprintf(fp, "%d %f %f\n", t, c[t - min_time].x, c[t - min_time].y);
+ }
+ fclose(fp);
+
+ fp = fopen("out.tcx", "w");
+ fprintf(fp, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
+ fprintf(fp, "<TrainingCenterDatabase xmlns=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd\">\n");
+ fprintf(fp, " <Activities>\n");
+ fprintf(fp, " <Activity Sport=\"Running\">\n");
+ fprintf(fp, " <Id>smoothed</Id>\n");
+ fprintf(fp, " <Lap StartTime=\"%s\">\n", write_time(min_time));
+ fprintf(fp, " <TotalTimeSeconds>%f</TotalTimeSeconds>\n", float(max_time - min_time + 1));
+ fprintf(fp, " <DistanceMeters>3720.0</DistanceMeters>\n"); // FIXME
+ fprintf(fp, " <Intensity>Active</Intensity>\n");
+ fprintf(fp, " <TriggerMethod>Manual</TriggerMethod>\n");
+ fprintf(fp, " <Track>\n");
+
+ for (int t = min_time; t <= max_time; ++t) {
+ double lat, lon;
+ double north = c[t - min_time].y + utm_north_sum;
+ double east = c[t - min_time].x + utm_east_sum;
+ UTMtoLL(23,
+ north,
+ east,
+ tcx_points[0].utm_zone,
+ lat, lon);
+ fprintf(fp, " <TrackPoint>\n");
+ fprintf(fp, " <Time>%s</Time>\n", write_time(t));
+ fprintf(fp, " <Position>\n");
+ fprintf(fp, " <LatitudeDegrees>%.10f</LatitudeDegrees>\n", lat);
+ fprintf(fp, " <LongitudeDegrees>%.10f</LongitudeDegrees>\n", lon);
+ fprintf(fp, " </Position>\n");
+ fprintf(fp, " </TrackPoint>\n");
+ }
+ fprintf(fp, " </Track>\n");
+ fprintf(fp, " </Lap>\n");
+ fprintf(fp, " <Notes>Created by auto-smoothing points</Notes>\n");
+ fprintf(fp, " </Activity>\n");
+ fprintf(fp, " </Activities>\n");
+ fprintf(fp, "</TrainingCenterDatabase>\n");
+ fclose(fp);
+
+ std::cout << summary.BriefReport() << "\n";
+
+ return 0;
+}