GNSS: Places, kept as GPX waypoints

A Place is a named position the user marked (issue #16). The list is
read from and written to a GPX file, so a map program opens it and
waypoints made by one are read here. Host-tested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
This commit is contained in:
2026-10-11 17:01:57 +02:00
co-authored by Claude Opus 5.5
parent da311a11b6
commit a87c3016cc
3 changed files with 201 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
#include "places.h"
#include <cstdio>
#include <cstdlib>
namespace roro::gnss {
namespace {
struct Entity {
const char* text;
char ch;
};
constexpr Entity kEntities[] = {{"&amp;", '&'}, {"&lt;", '<'}, {"&gt;", '>'}, {"&quot;", '"'}, {"&apos;", '\''}};
std::string escaped(const std::string& s) {
std::string out;
for (char c : s) {
const char* entity = nullptr;
for (auto& e : kEntities)
if (e.ch == c) entity = e.text;
if (entity) out += entity;
else out += c;
}
return out;
}
std::string unescaped(const std::string& s) {
std::string out;
for (size_t i = 0; i < s.size();) {
bool found = false;
if (s[i] == '&')
for (auto& e : kEntities) {
std::string text = e.text;
if (s.compare(i, text.size(), text) == 0) {
out += e.ch;
i += text.size();
found = true;
break;
}
}
if (!found) out += s[i++];
}
return out;
}
// The value of ` name="..."` in a tag; false if it isn't there or isn't a number.
bool attribute(const std::string& tag, const char* name, double& value) {
std::string key = std::string(" ") + name + "=";
size_t at = tag.find(key);
if (at == std::string::npos || at + key.size() >= tag.size()) return false;
at += key.size();
char quote = tag[at];
if (quote != '"' && quote != '\'') return false;
size_t end = tag.find(quote, at + 1);
if (end == std::string::npos) return false;
std::string text = tag.substr(at + 1, end - at - 1);
char* rest = nullptr;
value = std::strtod(text.c_str(), &rest);
return !text.empty() && rest && *rest == 0;
}
} // namespace
std::vector<Place> parsePlaces(const std::string& gpx) {
std::vector<Place> places;
size_t at = 0;
while (places.size() < kMaxPlaces && (at = gpx.find("<wpt", at)) != std::string::npos) {
size_t tagEnd = gpx.find('>', at);
if (tagEnd == std::string::npos) break;
std::string tag = gpx.substr(at, tagEnd - at);
bool alone = !tag.empty() && tag.back() == '/'; // <wpt .../>: no name
size_t close = alone ? tagEnd : gpx.find("</wpt>", tagEnd);
if (close == std::string::npos) break;
Place p;
if (attribute(tag, "lat", p.latitude) && attribute(tag, "lon", p.longitude) && p.latitude >= -90 &&
p.latitude <= 90 && p.longitude >= -180 && p.longitude <= 180) {
size_t name = alone ? std::string::npos : gpx.find("<name>", tagEnd);
if (name != std::string::npos && name < close) {
size_t nameEnd = gpx.find("</name>", name);
if (nameEnd != std::string::npos && nameEnd < close) p.name = unescaped(gpx.substr(name + 6, nameEnd - name - 6));
}
if (p.name.empty()) p.name = "Place " + std::to_string(places.size() + 1);
places.push_back(p);
}
at = close + 1;
}
return places;
}
std::string placesGpx(const std::vector<Place>& places) {
std::string out = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<gpx version=\"1.1\" creator=\"roro9stack\" xmlns=\"http://www.topografix.com/GPX/1/1\">\n";
for (auto& p : places) {
char buf[80];
std::snprintf(buf, sizeof buf, "<wpt lat=\"%.7f\" lon=\"%.7f\"><name>", p.latitude, p.longitude);
out += buf + escaped(p.name) + "</name></wpt>\n";
}
return out + "</gpx>\n";
}
} // namespace roro::gnss
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <cstddef>
#include <string>
#include <vector>
namespace roro::gnss {
// A Place (issue #16): somewhere the user marked, to be led back to by the Compass.
struct Place {
std::string name;
double latitude = 0, longitude = 0;
};
constexpr const char* kPlacesPath = "/gnss/places.gpx";
constexpr size_t kMaxPlaces = 50;
constexpr size_t kPlaceNameBytes = 24;
// The Places are kept as GPX waypoints, so a map program opens the file, and a file made by one
// is read here: every <wpt> with a latitude and a longitude, named by its <name>.
std::vector<Place> parsePlaces(const std::string& gpx);
std::string placesGpx(const std::vector<Place>& places);
} // namespace roro::gnss
+75
View File
@@ -0,0 +1,75 @@
#include <unity.h>
#include <cmath>
#include <string>
#include "places.h"
using namespace roro::gnss;
void setUp() {}
void tearDown() {}
void test_places_round_trip() {
std::vector<Place> places = {{"Car", 50.8503123, 4.3517456}, {"Tent <A&B>", -33.9, -151.2}};
std::string gpx = placesGpx(places);
TEST_ASSERT_NOT_EQUAL(std::string::npos, gpx.find("<wpt lat=\"50.8503123\" lon=\"4.3517456\"><name>Car</name></wpt>"));
TEST_ASSERT_NOT_EQUAL(std::string::npos, gpx.find("<name>Tent &lt;A&amp;B&gt;</name>"));
TEST_ASSERT_NOT_EQUAL(std::string::npos, gpx.find("</gpx>"));
std::vector<Place> back = parsePlaces(gpx);
TEST_ASSERT_EQUAL(2, back.size());
TEST_ASSERT_EQUAL_STRING("Car", back[0].name.c_str());
TEST_ASSERT_TRUE(std::fabs(back[0].latitude - (50.8503123)) < 1e-7);
TEST_ASSERT_TRUE(std::fabs(back[0].longitude - (4.3517456)) < 1e-7);
TEST_ASSERT_EQUAL_STRING("Tent <A&B>", back[1].name.c_str());
TEST_ASSERT_TRUE(std::fabs(back[1].longitude - (-151.2)) < 1e-7);
}
void test_an_empty_list_is_still_gpx() {
std::string gpx = placesGpx({});
TEST_ASSERT_NOT_EQUAL(std::string::npos, gpx.find("<gpx version=\"1.1\""));
TEST_ASSERT_EQUAL(0, parsePlaces(gpx).size());
TEST_ASSERT_EQUAL(0, parsePlaces("").size());
}
void test_waypoints_from_a_map_program() {
std::string gpx =
"<?xml version='1.0'?>\n<gpx>\n"
" <wpt lon='4.40' lat='51.22'>\n <ele>7</ele>\n <name>Antwerp</name>\n <sym>City</sym>\n </wpt>\n"
" <wpt lat=\"50.0\" lon=\"5.0\"/>\n"
" <wpt lat=\"95.0\" lon=\"5.0\"><name>Nowhere</name></wpt>\n"
" <wpt lat=\"x\" lon=\"5.0\"><name>Broken</name></wpt>\n"
" <wpt lat=\"50.5\" lon=\"5.5\"><desc>no name</desc></wpt>\n"
" <trk><trkseg><trkpt lat=\"1\" lon=\"2\"><name>not a place</name></trkpt></trkseg></trk>\n"
"</gpx>\n";
std::vector<Place> p = parsePlaces(gpx);
TEST_ASSERT_EQUAL(3, p.size());
TEST_ASSERT_EQUAL_STRING("Antwerp", p[0].name.c_str());
TEST_ASSERT_TRUE(std::fabs(p[0].latitude - (51.22)) < 1e-7);
TEST_ASSERT_TRUE(std::fabs(p[0].longitude - (4.40)) < 1e-7);
TEST_ASSERT_EQUAL_STRING("Place 2", p[1].name.c_str());
TEST_ASSERT_EQUAL_STRING("Place 3", p[2].name.c_str());
}
void test_a_cut_file_gives_what_is_whole() {
std::string gpx = placesGpx({{"A", 1, 2}, {"B", 3, 4}});
gpx.resize(gpx.find("<name>B"));
std::vector<Place> p = parsePlaces(gpx);
TEST_ASSERT_EQUAL(1, p.size());
TEST_ASSERT_EQUAL_STRING("A", p[0].name.c_str());
}
void test_no_more_than_the_limit() {
std::vector<Place> many(kMaxPlaces + 10, Place{"P", 1, 2});
TEST_ASSERT_EQUAL(kMaxPlaces, parsePlaces(placesGpx(many)).size());
}
int main() {
UNITY_BEGIN();
RUN_TEST(test_places_round_trip);
RUN_TEST(test_an_empty_list_is_still_gpx);
RUN_TEST(test_waypoints_from_a_map_program);
RUN_TEST(test_a_cut_file_gives_what_is_whole);
RUN_TEST(test_no_more_than_the_limit);
return UNITY_END();
}