Public Access
GNSS App: a Compass, with Places and a level #98
@@ -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