Public Access
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:
@@ -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[] = {{"&", '&'}, {"<", '<'}, {">", '>'}, {""", '"'}, {"'", '\''}};
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -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 <A&B></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();
|
||||
}
|
||||
Reference in New Issue
Block a user