Keep vanetza-idf in obu-firmware, so a plain clone builds the firmware
obu-firmware builds against the vanetza-idf C-ITS library, which until now came from the colleague's microbu-esp32c5 tree beside the repository and was not tracked here, so a clone of this repository could not build the firmware it ships. The library alone is now part of obu-firmware, as obu-firmware/external/vanetza-idf: their external/vanetza-idf at commit cf4b99f, unchanged (9775 files; see its PROVENANCE.md). CMake takes it from there by default; -DVANETZA_IDF_DIR still points the build elsewhere. The rest of the colleague's tree (their own VAM firmware, PKI tooling, station-link Python tools, the V2X2MAP bridge) stays out of this repository and gitignored; nothing is pushed to their repository. NOTES.md, docs/06, TODO.md and the pcap verifier's usage line point at the new location.
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
add_vanetza_component(geodesy
|
||||
country_data_reader.cpp
|
||||
country_database.cpp
|
||||
geodesy.cpp
|
||||
haversine.cpp
|
||||
)
|
||||
|
||||
if(VANETZA_WITH_GEOGRAPHICLIB)
|
||||
target_sources(geodesy PRIVATE geographiclib.cpp)
|
||||
target_link_libraries(geodesy PRIVATE GeographicLib::GeographicLib)
|
||||
target_compile_definitions(geodesy PUBLIC VANETZA_WITH_GEOGRAPHICLIB)
|
||||
endif()
|
||||
|
||||
if(VANETZA_EMBED_COUNTRY_DATA)
|
||||
find_program(UV_EXECUTABLE uv REQUIRED)
|
||||
set(VANETZA_COUNTRY_DATA_RESOLUTION "50m" CACHE STRING
|
||||
"Natural Earth resolution for country data (110m, 50m, 10m)")
|
||||
set_property(CACHE VANETZA_COUNTRY_DATA_RESOLUTION PROPERTY STRINGS 110m 50m 10m)
|
||||
|
||||
set(_tool_dir "${PROJECT_SOURCE_DIR}/tools/country-data")
|
||||
set(_country_bin "${CMAKE_CURRENT_BINARY_DIR}/country_data.bin")
|
||||
set(_embedded_cpp "${CMAKE_CURRENT_BINARY_DIR}/country_data_embedded.cpp")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${_country_bin}"
|
||||
COMMAND "${UV_EXECUTABLE}" run "${_tool_dir}/convert.py"
|
||||
--resolution "${VANETZA_COUNTRY_DATA_RESOLUTION}"
|
||||
--output "${_country_bin}"
|
||||
DEPENDS "${_tool_dir}/convert.py"
|
||||
COMMENT "Converting Natural Earth ${VANETZA_COUNTRY_DATA_RESOLUTION} to country data binary"
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_command(
|
||||
OUTPUT "${_embedded_cpp}"
|
||||
COMMAND "${UV_EXECUTABLE}" run "${_tool_dir}/embed.py"
|
||||
--input "${_country_bin}"
|
||||
--output "${_embedded_cpp}"
|
||||
DEPENDS "${_country_bin}" "${_tool_dir}/embed.py"
|
||||
COMMENT "Generating embedded country data source"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
target_sources(geodesy PRIVATE "${_embedded_cpp}")
|
||||
target_link_libraries(geodesy PRIVATE common)
|
||||
target_compile_definitions(geodesy PUBLIC VANETZA_WITH_EMBEDDED_COUNTRY_DATA)
|
||||
endif()
|
||||
|
||||
add_test_subdirectory(tests)
|
||||
@@ -0,0 +1,155 @@
|
||||
#include <vanetza/common/byte_order.hpp>
|
||||
#include <vanetza/geodesy/country_data_reader.hpp>
|
||||
#include <cstring>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace geodesy
|
||||
{
|
||||
namespace detail
|
||||
{
|
||||
|
||||
uint16_t read_u16le(const uint8_t* p)
|
||||
{
|
||||
uint16_t v;
|
||||
std::memcpy(&v, p, sizeof(v));
|
||||
return endian_cast<ByteOrder::LittleEndian>(v).host();
|
||||
}
|
||||
|
||||
uint32_t read_u32le(const uint8_t* p)
|
||||
{
|
||||
uint32_t v;
|
||||
std::memcpy(&v, p, sizeof(v));
|
||||
return endian_cast<ByteOrder::LittleEndian>(v).host();
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
static constexpr std::size_t wkb_header_length = 5; /*< byte order (1) and type (4) */
|
||||
static constexpr std::size_t wkb_count_length = 4; /*< counters are uint32_t (4) */
|
||||
static constexpr std::uint32_t wkb_polygon_type = 3;
|
||||
static constexpr std::uint32_t wkb_multipolygon_type = 6;
|
||||
|
||||
uint32_t read_u32(const uint8_t* p, bool little_endian)
|
||||
{
|
||||
uint32_t v;
|
||||
std::memcpy(&v, p, sizeof(v));
|
||||
return little_endian
|
||||
? endian_cast<ByteOrder::LittleEndian>(v).host()
|
||||
: endian_cast<ByteOrder::BigEndian>(v).host();
|
||||
}
|
||||
|
||||
double read_f64(const uint8_t* p, bool little_endian)
|
||||
{
|
||||
uint64_t bits;
|
||||
std::memcpy(&bits, p, sizeof(bits));
|
||||
bits = little_endian
|
||||
? endian_cast<ByteOrder::LittleEndian>(bits).host()
|
||||
: endian_cast<ByteOrder::BigEndian>(bits).host();
|
||||
double v;
|
||||
std::memcpy(&v, &bits, sizeof(v));
|
||||
return v;
|
||||
}
|
||||
|
||||
// Parse a WKB Polygon body (after byte-order and type fields).
|
||||
// On success, `consumed` receives the number of bytes consumed from `data`.
|
||||
// Positions in the returned result are relative to `data` (body start).
|
||||
CountryReaderResult parse_polygon_body(const uint8_t* data, std::size_t length, bool le, country::Polygon& out)
|
||||
{
|
||||
if (length < wkb_count_length) {
|
||||
return CountryReaderResult::failure("truncated polygon: missing ring count", 0);
|
||||
}
|
||||
|
||||
uint32_t num_rings = read_u32(data, le);
|
||||
std::size_t offset = wkb_count_length;
|
||||
|
||||
for (uint32_t r = 0; r < num_rings; ++r) {
|
||||
if (length - offset < wkb_count_length) {
|
||||
return CountryReaderResult::failure("truncated polygon: missing point count", offset);
|
||||
}
|
||||
|
||||
uint32_t num_points = read_u32(data + offset, le);
|
||||
offset += wkb_count_length;
|
||||
|
||||
std::size_t points_bytes = static_cast<std::size_t>(num_points) * 2 * sizeof(double);
|
||||
if (length - offset < points_bytes) {
|
||||
return CountryReaderResult::failure("truncated polygon: missinsg point data", offset);
|
||||
}
|
||||
|
||||
country::Ring ring;
|
||||
ring.reserve(num_points);
|
||||
for (uint32_t i = 0; i < num_points; ++i) {
|
||||
double lon = read_f64(data + offset, le);
|
||||
offset += sizeof(double);
|
||||
double lat = read_f64(data + offset, le);
|
||||
offset += sizeof(double);
|
||||
ring.push_back(country::Point(lon, lat));
|
||||
}
|
||||
|
||||
if (r == 0) {
|
||||
out.outer() = std::move(ring);
|
||||
} else {
|
||||
out.inners().push_back(std::move(ring));
|
||||
}
|
||||
}
|
||||
|
||||
return CountryReaderResult::success(offset);
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
CountryReaderResult parse_wkb(const uint8_t* data, std::size_t length, CountryPolygon& out)
|
||||
{
|
||||
if (length < wkb_header_length) {
|
||||
return CountryReaderResult::failure("WKB too short for header", 0);
|
||||
}
|
||||
|
||||
bool le = (data[0] == 0x01);
|
||||
uint32_t type = read_u32(data + 1, le);
|
||||
|
||||
if (type == wkb_polygon_type) {
|
||||
country::Polygon polygon;
|
||||
auto inner = parse_polygon_body(data + wkb_header_length, length - wkb_header_length, le, polygon);
|
||||
if (inner.ok()) {
|
||||
out.push_back(std::move(polygon));
|
||||
}
|
||||
return inner.add_offset(wkb_header_length);
|
||||
} else if (type == wkb_multipolygon_type) {
|
||||
if (length < wkb_header_length + wkb_count_length) {
|
||||
return CountryReaderResult::failure("truncated multi-polygon: missing polygon count", wkb_header_length);
|
||||
}
|
||||
uint32_t num_polygons = read_u32(data + wkb_header_length, le);
|
||||
std::size_t offset = wkb_header_length + wkb_count_length;
|
||||
|
||||
for (uint32_t i = 0; i < num_polygons; ++i) {
|
||||
if (length - offset < wkb_header_length) {
|
||||
return CountryReaderResult::failure("truncated multi-polygon: missing polygon header", offset);
|
||||
}
|
||||
|
||||
bool poly_le = (data[offset] == 0x01);
|
||||
uint32_t poly_type = read_u32(data + offset + 1, poly_le);
|
||||
if (poly_type != wkb_polygon_type) {
|
||||
return CountryReaderResult::failure("unexpected geometry type inside multi-polygon", offset + 1);
|
||||
}
|
||||
offset += wkb_header_length;
|
||||
|
||||
country::Polygon polygon;
|
||||
auto inner = parse_polygon_body(data + offset, length - offset, poly_le, polygon);
|
||||
if (inner.ok()) {
|
||||
out.push_back(std::move(polygon));
|
||||
offset += inner.position();
|
||||
} else {
|
||||
return inner.add_offset(offset);
|
||||
}
|
||||
}
|
||||
|
||||
return CountryReaderResult::success(offset);
|
||||
} else {
|
||||
return CountryReaderResult::failure("unsupported WKB geometry type" , 1);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
} // namespace geodesy
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,128 @@
|
||||
#pragma once
|
||||
#include <vanetza/geodesy/country_polygon.hpp>
|
||||
#include <vanetza/geodesy/m49_code.hpp>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace geodesy
|
||||
{
|
||||
|
||||
class CountryReaderResult
|
||||
{
|
||||
public:
|
||||
static CountryReaderResult success(std::size_t pos)
|
||||
{
|
||||
return CountryReaderResult { pos };
|
||||
}
|
||||
|
||||
static CountryReaderResult failure(std::string msg, std::size_t pos)
|
||||
{
|
||||
return CountryReaderResult { std::move(msg), pos };
|
||||
}
|
||||
|
||||
bool ok() const { return m_success; }
|
||||
bool failed() const { return !ok(); }
|
||||
const std::string& message() const { return m_detail; }
|
||||
std::size_t position() const { return m_position; }
|
||||
|
||||
CountryReaderResult& add_offset(std::size_t offset)
|
||||
{
|
||||
m_position += offset;
|
||||
return *this;
|
||||
}
|
||||
|
||||
private:
|
||||
CountryReaderResult(std::size_t pos) : m_position(pos) {}
|
||||
CountryReaderResult(std::string msg, std::size_t pos) :
|
||||
m_success(false), m_position(pos), m_detail(std::move(msg)) {}
|
||||
|
||||
bool m_success = true;
|
||||
std::size_t m_position = 0;
|
||||
std::string m_detail;
|
||||
};
|
||||
|
||||
namespace detail
|
||||
{
|
||||
|
||||
/**
|
||||
* Version of the country data binary file format recognised by the reader.
|
||||
*/
|
||||
static constexpr uint16_t country_data_format_version = 1;
|
||||
|
||||
/**
|
||||
* Parse a single WKB geometry blob (Polygon or MultiPolygon) into a CountryPolygon.
|
||||
* Positions in the returned result are relative to the WKB data blob.
|
||||
* \param data pointer to WKB data
|
||||
* \param length size of WKB data in bytes
|
||||
* \param out parsed polygon (output)
|
||||
* \return ok on success, failure(msg, pos) on parse error
|
||||
*/
|
||||
CountryReaderResult parse_wkb(const uint8_t* data, std::size_t length, CountryPolygon& out);
|
||||
|
||||
uint16_t read_u16le(const uint8_t*);
|
||||
uint32_t read_u32le(const uint8_t*);
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/**
|
||||
* Parse a country data binary file (custom framing around OGC WKB payloads).
|
||||
*
|
||||
* File format: version (uint16 LE) | sequence of entries until EOF
|
||||
* Each entry: m49_code (uint16 LE) | wkb_size (uint32 LE) | wkb_data (wkb_size bytes, OGC WKB).
|
||||
*
|
||||
* Positions in the returned result are relative to the start of the input buffer.
|
||||
*
|
||||
* \param data pointer to binary data (may be nullptr if length is 0)
|
||||
* \param length size of data in bytes
|
||||
* \param fn callback invoked for each parsed entry: void(M49Code, CountryPolygon&&)
|
||||
* \return ok on success, failure(msg, pos) on parse error
|
||||
*/
|
||||
template<typename CallbackFn>
|
||||
CountryReaderResult read_country_data(const uint8_t* data, std::size_t length, CallbackFn fn)
|
||||
{
|
||||
if (length < sizeof(uint16_t)) {
|
||||
return CountryReaderResult::failure("truncated file header: missing version field", 0);
|
||||
}
|
||||
|
||||
uint16_t version = detail::read_u16le(data);
|
||||
if (version != detail::country_data_format_version) {
|
||||
return CountryReaderResult::failure("unsupported country data format version", 0);
|
||||
}
|
||||
|
||||
std::size_t offset = sizeof(uint16_t);
|
||||
const std::size_t entry_header_size = sizeof(uint16_t) + sizeof(uint32_t);
|
||||
|
||||
while (offset < length) {
|
||||
if (length - offset < entry_header_size) {
|
||||
return CountryReaderResult::failure("truncated entry header", offset);
|
||||
}
|
||||
|
||||
uint16_t m49 = detail::read_u16le(data + offset);
|
||||
offset += sizeof(uint16_t);
|
||||
|
||||
uint32_t wkb_size = detail::read_u32le(data + offset);
|
||||
offset += sizeof(uint32_t);
|
||||
|
||||
if (length - offset < wkb_size) {
|
||||
return CountryReaderResult::failure("invalid WKB payload size exceeding remaining data", offset);
|
||||
}
|
||||
|
||||
CountryPolygon polygon;
|
||||
auto inner = detail::parse_wkb(data + offset, wkb_size, polygon);
|
||||
if (inner.failed()) {
|
||||
inner.add_offset(offset);
|
||||
return inner;
|
||||
}
|
||||
|
||||
fn(M49Code(m49), std::move(polygon));
|
||||
offset += wkb_size;
|
||||
}
|
||||
|
||||
return CountryReaderResult::success(offset);
|
||||
}
|
||||
|
||||
} // namespace geodesy
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,84 @@
|
||||
#include <vanetza/geodesy/country_database.hpp>
|
||||
#include <vanetza/geodesy/country_data_reader.hpp>
|
||||
#include <boost/geometry/algorithms/within.hpp>
|
||||
#include <boost/units/quantity.hpp>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <vector>
|
||||
|
||||
#ifdef VANETZA_WITH_EMBEDDED_COUNTRY_DATA
|
||||
#include <vanetza/common/byte_view.hpp>
|
||||
namespace vanetza { namespace geodesy { namespace country { vanetza::byte_view_range embedded(); } } }
|
||||
#endif
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace geodesy
|
||||
{
|
||||
|
||||
CountryDatabase CountryDatabase::embedded(std::string* error)
|
||||
{
|
||||
CountryDatabase db;
|
||||
#ifdef VANETZA_WITH_EMBEDDED_COUNTRY_DATA
|
||||
auto view = country::embedded();
|
||||
db.load(view.data(), view.size(), error);
|
||||
#else
|
||||
if (error) {
|
||||
*error = "embedded country data not available";
|
||||
}
|
||||
#endif
|
||||
return db;
|
||||
}
|
||||
|
||||
bool CountryDatabase::load(const std::string& path, std::string* error)
|
||||
{
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
if (error) {
|
||||
*error = "cannot open file: " + path;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> data { std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>() };
|
||||
return load(data.data(), data.size(), error);
|
||||
}
|
||||
|
||||
bool CountryDatabase::load(const uint8_t* data, std::size_t length, std::string* error)
|
||||
{
|
||||
m_countries.clear();
|
||||
|
||||
auto result = read_country_data(data, length,
|
||||
[this](M49Code code, CountryPolygon&& polygon) {
|
||||
m_countries.emplace(code, std::move(polygon));
|
||||
});
|
||||
|
||||
if (result.failed()) {
|
||||
if (error) {
|
||||
*error = result.message() + " at offset " + std::to_string(result.position());
|
||||
}
|
||||
m_countries.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CountryDatabase::is_inside(M49Code country, const GeodeticPosition& position) const
|
||||
{
|
||||
auto it = m_countries.find(country);
|
||||
if (it == m_countries.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
country::Point point(position.longitude / units::degree, position.latitude / units::degree);
|
||||
return boost::geometry::within(point, it->second);
|
||||
}
|
||||
|
||||
bool CountryDatabase::empty() const
|
||||
{
|
||||
return m_countries.empty();
|
||||
}
|
||||
|
||||
} // namespace geodesy
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
#include <vanetza/geodesy/country_polygon.hpp>
|
||||
#include <vanetza/geodesy/m49_code.hpp>
|
||||
#include <vanetza/geodesy/position.hpp>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace geodesy
|
||||
{
|
||||
|
||||
class CountryDatabase
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Create a CountryDatabase from embedded country data.
|
||||
* Only available when built with VANETZA_WITH_EMBEDDED_COUNTRY_DATA.
|
||||
* \param[out] error optional error message
|
||||
* \return loaded database, or empty database on failure
|
||||
*/
|
||||
static CountryDatabase embedded(std::string* error = nullptr);
|
||||
|
||||
/**
|
||||
* Load country data from a file.
|
||||
* \param[in] path path to the binary country data file
|
||||
* \param[out] error optional error message
|
||||
* \return true on success
|
||||
*/
|
||||
bool load(const std::string& path, std::string* error = nullptr);
|
||||
|
||||
/**
|
||||
* Load country data from a memory buffer.
|
||||
* \param[in] data pointer to binary data
|
||||
* \param[in] length size of data in bytes
|
||||
* \param[out] error optional error message
|
||||
* \return true on success
|
||||
*/
|
||||
bool load(const uint8_t* data, std::size_t length, std::string* error = nullptr);
|
||||
|
||||
/**
|
||||
* Check if a geodetic position lies within a country.
|
||||
* \param country M.49 country code
|
||||
* \param position geodetic position to check
|
||||
* \return true if position is inside the country's boundaries
|
||||
*/
|
||||
bool is_inside(M49Code country, const GeodeticPosition& position) const;
|
||||
|
||||
/**
|
||||
* Check if the database contains any country data.
|
||||
* \return true if no countries are loaded
|
||||
*/
|
||||
bool empty() const;
|
||||
|
||||
private:
|
||||
std::unordered_map<M49Code, CountryPolygon> m_countries;
|
||||
};
|
||||
|
||||
} // namespace geodesy
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
#include <boost/geometry/core/cs.hpp>
|
||||
#include <boost/geometry/geometries/multi_polygon.hpp>
|
||||
#include <boost/geometry/geometries/point.hpp>
|
||||
#include <boost/geometry/geometries/polygon.hpp>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace geodesy
|
||||
{
|
||||
namespace country
|
||||
{
|
||||
using CoordinateSystem = boost::geometry::cs::geographic<boost::geometry::degree>;
|
||||
using Point = boost::geometry::model::point<double, 2, CoordinateSystem>;
|
||||
using Ring = boost::geometry::model::ring<Point>;
|
||||
using Polygon = boost::geometry::model::polygon<Point>;
|
||||
using MultiPolygon = boost::geometry::model::multi_polygon<Polygon>;
|
||||
} // namespace country
|
||||
|
||||
using CountryPolygon = country::MultiPolygon;
|
||||
|
||||
} // namespace geodesy
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,39 @@
|
||||
#include <vanetza/geodesy/geodesy.hpp>
|
||||
#ifdef VANETZA_WITH_GEOGRAPHICLIB
|
||||
# include <vanetza/geodesy/geographiclib.hpp>
|
||||
#else
|
||||
# include <vanetza/geodesy/haversine.hpp>
|
||||
#endif
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace geodesy
|
||||
{
|
||||
|
||||
CartesianPosition operator-(const CartesianPosition& a, const CartesianPosition& b)
|
||||
{
|
||||
return CartesianPosition { a.x - b.x, a.y - b.y };
|
||||
}
|
||||
|
||||
units::Length distance(const GeodeticPosition& a, const GeodeticPosition& b)
|
||||
{
|
||||
#ifdef VANETZA_WITH_GEOGRAPHICLIB
|
||||
return geographiclib::distance(a, b);
|
||||
#else
|
||||
return haversine::distance(a, b);
|
||||
#endif
|
||||
}
|
||||
|
||||
CartesianPosition local_cartesian(
|
||||
const GeodeticPosition& origin,
|
||||
const GeodeticPosition& position)
|
||||
{
|
||||
#ifdef VANETZA_WITH_GEOGRAPHICLIB
|
||||
return geographiclib::local_cartesian(origin, position);
|
||||
#else
|
||||
return haversine::local_cartesian(origin, position);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace geodesy
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <vanetza/geodesy/position.hpp>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace geodesy
|
||||
{
|
||||
|
||||
/**
|
||||
* Get distance between two geodetic positions.
|
||||
* Delegates to the best available backend (GeographicLib if available, else haversine).
|
||||
* \param a first position
|
||||
* \param b second position
|
||||
* \return distance in meters (always positive) or NaN for invalid input
|
||||
*/
|
||||
units::Length distance(const GeodeticPosition& a, const GeodeticPosition& b);
|
||||
|
||||
/**
|
||||
* Derive cartesian position ENU from geodetic WGS84 coordinates
|
||||
* and a WGS84 reference point which becomes the cartesian origin.
|
||||
* Delegates to the best available backend.
|
||||
* \param origin WGS84 reference point becoming origin
|
||||
* \param position Calculate cartesian coordinates for this point
|
||||
* \return Cartesian coordinates of position relative to origin
|
||||
*/
|
||||
CartesianPosition local_cartesian(
|
||||
const GeodeticPosition& origin,
|
||||
const GeodeticPosition& position);
|
||||
|
||||
} // namespace geodesy
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,43 @@
|
||||
#include <vanetza/geodesy/geographiclib.hpp>
|
||||
#include <GeographicLib/Geocentric.hpp>
|
||||
#include <GeographicLib/Geodesic.hpp>
|
||||
#include <GeographicLib/LocalCartesian.hpp>
|
||||
#include <limits>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace geodesy
|
||||
{
|
||||
namespace geographiclib
|
||||
{
|
||||
|
||||
units::Length distance(const GeodeticPosition& a, const GeodeticPosition& b)
|
||||
{
|
||||
const auto& geod = GeographicLib::Geodesic::WGS84();
|
||||
double distance_m = 0.0;
|
||||
geod.Inverse(a.latitude / units::degree, a.longitude / units::degree,
|
||||
b.latitude / units::degree, b.longitude / units::degree,
|
||||
distance_m);
|
||||
return (distance_m >= 0.0 ? distance_m : std::numeric_limits<double>::quiet_NaN()) * units::si::meter;
|
||||
}
|
||||
|
||||
CartesianPosition local_cartesian(
|
||||
const GeodeticPosition& origin,
|
||||
const GeodeticPosition& position)
|
||||
{
|
||||
const auto& earth = GeographicLib::Geocentric::WGS84();
|
||||
GeographicLib::LocalCartesian proj {
|
||||
origin.latitude / units::degree,
|
||||
origin.longitude / units::degree,
|
||||
0.0, earth
|
||||
};
|
||||
double result_x, result_y, unused_z = 0.0;
|
||||
proj.Forward(position.latitude / units::degree,
|
||||
position.longitude / units::degree, 0.0,
|
||||
result_x, result_y, unused_z);
|
||||
return CartesianPosition(result_x * units::si::meter, result_y * units::si::meter);
|
||||
}
|
||||
|
||||
} // namespace geographiclib
|
||||
} // namespace geodesy
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <vanetza/geodesy/position.hpp>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace geodesy
|
||||
{
|
||||
namespace geographiclib
|
||||
{
|
||||
|
||||
/**
|
||||
* Get distance between two geodetic positions on WGS84 ellipsoid using GeographicLib.
|
||||
* \param a first position
|
||||
* \param b second position
|
||||
* \return distance in meters (always positive) or NaN for invalid input
|
||||
*/
|
||||
units::Length distance(const GeodeticPosition& a, const GeodeticPosition& b);
|
||||
|
||||
/**
|
||||
* Derive cartesian position ENU from geodetic WGS84 coordinates using GeographicLib.
|
||||
* Uses proper ellipsoidal LocalCartesian projection.
|
||||
* \param origin WGS84 reference point becoming origin
|
||||
* \param position Calculate cartesian coordinates for this point
|
||||
* \return Cartesian coordinates of position relative to origin
|
||||
*/
|
||||
CartesianPosition local_cartesian(
|
||||
const GeodeticPosition& origin,
|
||||
const GeodeticPosition& position);
|
||||
|
||||
} // namespace geographiclib
|
||||
} // namespace geodesy
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,49 @@
|
||||
#include <vanetza/geodesy/haversine.hpp>
|
||||
#include <boost/units/cmath.hpp>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace geodesy
|
||||
{
|
||||
namespace haversine
|
||||
{
|
||||
|
||||
// arithmetic mean radius of WGS84 ellipsoid
|
||||
static const units::Length earth_radius = 6371008.8 * units::si::meter;
|
||||
|
||||
units::Length distance(const GeodeticPosition& a, const GeodeticPosition& b)
|
||||
{
|
||||
using boost::units::sin;
|
||||
using boost::units::cos;
|
||||
|
||||
const units::Angle delta_phi { b.latitude - a.latitude };
|
||||
const units::Angle delta_lambda { b.longitude - a.longitude };
|
||||
|
||||
const auto sin_dphi = sin(delta_phi / 2.0);
|
||||
const auto sin_dlambda = sin(delta_lambda / 2.0);
|
||||
const auto h = sin_dphi * sin_dphi + cos(a.latitude) * cos(b.latitude) * sin_dlambda * sin_dlambda;
|
||||
const auto c = 2.0 * std::atan2(std::sqrt(h), std::sqrt(1.0 - h));
|
||||
|
||||
return earth_radius * c;
|
||||
}
|
||||
|
||||
CartesianPosition local_cartesian(
|
||||
const GeodeticPosition& origin,
|
||||
const GeodeticPosition& position)
|
||||
{
|
||||
using boost::units::cos;
|
||||
|
||||
const double dlat = units::Angle {position.latitude - origin.latitude }.value();
|
||||
const double dlon = units::Angle {position.longitude - origin.longitude }.value();
|
||||
|
||||
const units::Length x = dlon * cos(origin.latitude) * earth_radius;
|
||||
const units::Length y = dlat * earth_radius;
|
||||
|
||||
return CartesianPosition(x, y);
|
||||
}
|
||||
|
||||
} // namespace haversine
|
||||
} // namespace geodesy
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <vanetza/geodesy/position.hpp>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace geodesy
|
||||
{
|
||||
namespace haversine
|
||||
{
|
||||
|
||||
/**
|
||||
* Get distance between two geodetic positions using the Haversine formula.
|
||||
* Uses a spherical Earth model (radius 6371 km).
|
||||
* \param a first position
|
||||
* \param b second position
|
||||
* \return distance in meters (always positive)
|
||||
*/
|
||||
units::Length distance(const GeodeticPosition& a, const GeodeticPosition& b);
|
||||
|
||||
/**
|
||||
* Derive cartesian position ENU from geodetic coordinates using equirectangular projection.
|
||||
* Uses a spherical Earth model (radius 6371 km).
|
||||
* Suitable for short distances (< ~10 km) typical in GeoNetworking.
|
||||
* \param origin Reference point becoming origin
|
||||
* \param position Calculate cartesian coordinates for this point
|
||||
* \return Cartesian coordinates of position relative to origin
|
||||
*/
|
||||
CartesianPosition local_cartesian(
|
||||
const GeodeticPosition& origin,
|
||||
const GeodeticPosition& position);
|
||||
|
||||
} // namespace haversine
|
||||
} // namespace geodesy
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace geodesy
|
||||
{
|
||||
|
||||
/**
|
||||
* Standard country or area codes for statistical use by M49 standard.
|
||||
* \see https://unstats.un.org/unsd/methodology/m49/
|
||||
*/
|
||||
class M49Code
|
||||
{
|
||||
public:
|
||||
explicit constexpr M49Code(uint16_t value) : m_value(value) {}
|
||||
constexpr uint16_t value() const { return m_value; }
|
||||
|
||||
bool operator==(M49Code other) const { return m_value == other.m_value; }
|
||||
bool operator!=(M49Code other) const { return m_value != other.m_value; }
|
||||
|
||||
private:
|
||||
uint16_t m_value;
|
||||
};
|
||||
|
||||
} // namespace geodesy
|
||||
} // namespace vanetza
|
||||
|
||||
namespace std
|
||||
{
|
||||
template<>
|
||||
struct hash<vanetza::geodesy::M49Code>
|
||||
{
|
||||
std::size_t operator()(vanetza::geodesy::M49Code code) const
|
||||
{
|
||||
return std::hash<uint16_t>{}(code.value());
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct less<vanetza::geodesy::M49Code>
|
||||
{
|
||||
bool operator()(vanetza::geodesy::M49Code a, vanetza::geodesy::M49Code b) const
|
||||
{
|
||||
return a.value() < b.value();
|
||||
}
|
||||
};
|
||||
} // namespace std
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <vanetza/units/angle.hpp>
|
||||
#include <vanetza/units/length.hpp>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace geodesy
|
||||
{
|
||||
|
||||
/**
|
||||
* Cartesian position.
|
||||
* Point in an ENU (East-North-Up) coordinate system, units in meters.
|
||||
*/
|
||||
struct CartesianPosition
|
||||
{
|
||||
CartesianPosition() : x(0.0 * units::si::meter), y(0.0 * units::si::meter) {}
|
||||
CartesianPosition(units::Length x_, units::Length y_) : x(x_), y(y_) {}
|
||||
units::Length x;
|
||||
units::Length y;
|
||||
};
|
||||
|
||||
CartesianPosition operator-(const CartesianPosition&, const CartesianPosition&);
|
||||
|
||||
struct GeodeticPosition
|
||||
{
|
||||
GeodeticPosition() :
|
||||
latitude(0.0 * units::degree), longitude(0.0 * units::degree) {}
|
||||
GeodeticPosition(units::GeoAngle lat, units::GeoAngle lon) :
|
||||
latitude(lat), longitude(lon) {}
|
||||
units::GeoAngle latitude;
|
||||
units::GeoAngle longitude;
|
||||
};
|
||||
|
||||
} // namespace geodesy
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,7 @@
|
||||
include(UseGTest)
|
||||
configure_gtest_directory(LINK_LIBRARIES geodesy)
|
||||
|
||||
add_gtest(Geodesy geodesy.cpp)
|
||||
add_gtest(M49Code m49_code.cpp)
|
||||
add_gtest(CountryDataReader country_data_reader.cpp)
|
||||
add_gtest(CountryDatabase country_database.cpp)
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
#include <vanetza/common/byte_order.hpp>
|
||||
#include <vanetza/geodesy/country_data_reader.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
using namespace vanetza;
|
||||
using namespace vanetza::geodesy;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
template<ByteOrder Order, typename T>
|
||||
void append(std::vector<uint8_t>& buf, T v)
|
||||
{
|
||||
EndianType<T, Order> e = host_cast(v);
|
||||
auto raw = e.get();
|
||||
const auto* p = reinterpret_cast<const uint8_t*>(&raw);
|
||||
buf.insert(buf.end(), p, p + sizeof(raw));
|
||||
}
|
||||
|
||||
void append_u16le(std::vector<uint8_t>& buf, uint16_t v)
|
||||
{
|
||||
append<ByteOrder::LittleEndian>(buf, v);
|
||||
}
|
||||
|
||||
void append_u32le(std::vector<uint8_t>& buf, uint32_t v)
|
||||
{
|
||||
append<ByteOrder::LittleEndian>(buf, v);
|
||||
}
|
||||
|
||||
void append_u32be(std::vector<uint8_t>& buf, uint32_t v)
|
||||
{
|
||||
append<ByteOrder::BigEndian>(buf, v);
|
||||
}
|
||||
|
||||
void append_f64le(std::vector<uint8_t>& buf, double v)
|
||||
{
|
||||
uint64_t bits;
|
||||
std::memcpy(&bits, &v, sizeof(bits));
|
||||
append<ByteOrder::LittleEndian>(buf, bits);
|
||||
}
|
||||
|
||||
void append_f64be(std::vector<uint8_t>& buf, double v)
|
||||
{
|
||||
uint64_t bits;
|
||||
std::memcpy(&bits, &v, sizeof(bits));
|
||||
append<ByteOrder::BigEndian>(buf, bits);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> make_wkb_polygon_le(const std::vector<std::pair<double, double>>& ring)
|
||||
{
|
||||
std::vector<uint8_t> wkb;
|
||||
wkb.push_back(0x01); // little-endian
|
||||
append_u32le(wkb, 3); // type = Polygon
|
||||
append_u32le(wkb, 1); // num_rings = 1
|
||||
uint32_t num_points = static_cast<uint32_t>(ring.size()) + 1; // +1 for closing point
|
||||
append_u32le(wkb, num_points);
|
||||
for (const auto& p : ring) {
|
||||
append_f64le(wkb, p.first); // lon
|
||||
append_f64le(wkb, p.second); // lat
|
||||
}
|
||||
append_f64le(wkb, ring.front().first);
|
||||
append_f64le(wkb, ring.front().second);
|
||||
return wkb;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> make_wkb_polygon_be(const std::vector<std::pair<double, double>>& ring)
|
||||
{
|
||||
std::vector<uint8_t> wkb;
|
||||
wkb.push_back(0x00); // big-endian
|
||||
append_u32be(wkb, 3); // type = Polygon
|
||||
append_u32be(wkb, 1); // num_rings = 1
|
||||
uint32_t num_points = static_cast<uint32_t>(ring.size()) + 1;
|
||||
append_u32be(wkb, num_points);
|
||||
for (const auto& p : ring) {
|
||||
append_f64be(wkb, p.first);
|
||||
append_f64be(wkb, p.second);
|
||||
}
|
||||
append_f64be(wkb, ring.front().first);
|
||||
append_f64be(wkb, ring.front().second);
|
||||
return wkb;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> make_wkb_multipolygon_le(const std::vector<std::pair<double, double>>& ring)
|
||||
{
|
||||
auto poly_wkb = make_wkb_polygon_le(ring);
|
||||
std::vector<uint8_t> wkb;
|
||||
wkb.push_back(0x01); // little-endian
|
||||
append_u32le(wkb, 6); // type = MultiPolygon
|
||||
append_u32le(wkb, 1); // num_polygons = 1
|
||||
wkb.insert(wkb.end(), poly_wkb.begin(), poly_wkb.end());
|
||||
return wkb;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> make_entry(uint16_t m49, const std::vector<uint8_t>& wkb)
|
||||
{
|
||||
std::vector<uint8_t> entry;
|
||||
append_u16le(entry, m49);
|
||||
append_u32le(entry, static_cast<uint32_t>(wkb.size()));
|
||||
entry.insert(entry.end(), wkb.begin(), wkb.end());
|
||||
return entry;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> make_file(std::initializer_list<std::vector<uint8_t>> entries)
|
||||
{
|
||||
std::vector<uint8_t> buf;
|
||||
append_u16le(buf, vanetza::geodesy::detail::country_data_format_version);
|
||||
for (const auto& e : entries) {
|
||||
buf.insert(buf.end(), e.begin(), e.end());
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
// A simple square polygon approximating Europe: (5,45) to (15,55)
|
||||
const std::vector<std::pair<double, double>> square_europe = {
|
||||
{5.0, 45.0}, {15.0, 45.0}, {15.0, 55.0}, {5.0, 55.0}
|
||||
};
|
||||
|
||||
// A simple square polygon approximating France: (-5,42) to (10,51)
|
||||
const std::vector<std::pair<double, double>> square_france = {
|
||||
{-5.0, 42.0}, {10.0, 42.0}, {10.0, 51.0}, {-5.0, 51.0}
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
TEST(CountryDataReader, empty_input)
|
||||
{
|
||||
auto result = read_country_data(nullptr, 0, [](M49Code, CountryPolygon&&) {});
|
||||
EXPECT_TRUE(result.failed());
|
||||
}
|
||||
|
||||
TEST(CountryDataReader, header_only)
|
||||
{
|
||||
auto data = make_file({});
|
||||
|
||||
std::map<uint16_t, CountryPolygon> result;
|
||||
auto r = read_country_data(data.data(), data.size(), [&](M49Code code, CountryPolygon&& poly) {
|
||||
result[code.value()] = std::move(poly);
|
||||
});
|
||||
EXPECT_TRUE(r.ok());
|
||||
}
|
||||
|
||||
TEST(CountryDataReader, single_polygon_entry)
|
||||
{
|
||||
auto wkb = make_wkb_polygon_le(square_europe);
|
||||
auto data = make_file({make_entry(276, wkb)});
|
||||
|
||||
std::map<uint16_t, CountryPolygon> result;
|
||||
auto r = read_country_data(data.data(), data.size(), [&](M49Code code, CountryPolygon&& poly) {
|
||||
result[code.value()] = std::move(poly);
|
||||
});
|
||||
EXPECT_TRUE(r.ok());
|
||||
ASSERT_EQ(1u, result.size());
|
||||
EXPECT_EQ(1u, result.count(276));
|
||||
}
|
||||
|
||||
TEST(CountryDataReader, multipolygon_entry)
|
||||
{
|
||||
auto wkb = make_wkb_multipolygon_le(square_europe);
|
||||
auto data = make_file({make_entry(276, wkb)});
|
||||
|
||||
std::map<uint16_t, CountryPolygon> result;
|
||||
auto r = read_country_data(data.data(), data.size(), [&](M49Code code, CountryPolygon&& poly) {
|
||||
result[code.value()] = std::move(poly);
|
||||
});
|
||||
EXPECT_TRUE(r.ok());
|
||||
ASSERT_EQ(1u, result.size());
|
||||
EXPECT_EQ(1u, result.count(276));
|
||||
}
|
||||
|
||||
TEST(CountryDataReader, multiple_entries)
|
||||
{
|
||||
auto wkb1 = make_wkb_polygon_le(square_europe);
|
||||
auto wkb2 = make_wkb_polygon_le(square_france);
|
||||
auto data = make_file({make_entry(276, wkb1), make_entry(250, wkb2)});
|
||||
|
||||
std::map<uint16_t, CountryPolygon> result;
|
||||
auto r = read_country_data(data.data(), data.size(), [&](M49Code code, CountryPolygon&& poly) {
|
||||
result[code.value()] = std::move(poly);
|
||||
});
|
||||
EXPECT_TRUE(r.ok());
|
||||
EXPECT_EQ(2u, result.size());
|
||||
EXPECT_EQ(1u, result.count(276));
|
||||
EXPECT_EQ(1u, result.count(250));
|
||||
}
|
||||
|
||||
TEST(CountryDataReader, big_endian_wkb_payload)
|
||||
{
|
||||
auto wkb = make_wkb_polygon_be(square_europe);
|
||||
auto data = make_file({make_entry(276, wkb)});
|
||||
|
||||
std::map<uint16_t, CountryPolygon> result;
|
||||
auto r = read_country_data(data.data(), data.size(), [&](M49Code code, CountryPolygon&& poly) {
|
||||
result[code.value()] = std::move(poly);
|
||||
});
|
||||
EXPECT_TRUE(r.ok());
|
||||
ASSERT_EQ(1u, result.size());
|
||||
EXPECT_EQ(1u, result.count(276));
|
||||
}
|
||||
|
||||
TEST(CountryDataReader, truncated_version_header)
|
||||
{
|
||||
std::vector<uint8_t> data = {0x14}; // only 1 byte, not enough for version field
|
||||
auto result = read_country_data(data.data(), data.size(), [](M49Code, CountryPolygon&&) {});
|
||||
EXPECT_TRUE(result.failed());
|
||||
}
|
||||
|
||||
TEST(CountryDataReader, unsupported_version)
|
||||
{
|
||||
std::vector<uint8_t> data;
|
||||
append_u16le(data, vanetza::geodesy::detail::country_data_format_version + 1);
|
||||
|
||||
auto result = read_country_data(data.data(), data.size(), [](M49Code, CountryPolygon&&) {});
|
||||
EXPECT_TRUE(result.failed());
|
||||
}
|
||||
|
||||
TEST(CountryDataReader, truncated_entry_header)
|
||||
{
|
||||
std::vector<uint8_t> data;
|
||||
append_u16le(data, vanetza::geodesy::detail::country_data_format_version);
|
||||
data.push_back(0x14); // one byte of an entry header, not enough
|
||||
|
||||
auto result = read_country_data(data.data(), data.size(), [](M49Code, CountryPolygon&&) {});
|
||||
EXPECT_TRUE(result.failed());
|
||||
}
|
||||
|
||||
TEST(CountryDataReader, truncated_wkb_payload)
|
||||
{
|
||||
auto wkb = make_wkb_polygon_le(square_europe);
|
||||
auto data = make_file({make_entry(276, wkb)});
|
||||
// keep the version header + entry header intact but truncate halfway through the WKB
|
||||
data.resize(data.size() - wkb.size() / 2);
|
||||
|
||||
auto result = read_country_data(data.data(), data.size(), [](M49Code, CountryPolygon&&) {});
|
||||
EXPECT_TRUE(result.failed());
|
||||
EXPECT_FALSE(result.message().empty());
|
||||
}
|
||||
|
||||
TEST(CountryDataReader, wkb_size_exceeds_remaining)
|
||||
{
|
||||
std::vector<uint8_t> data;
|
||||
append_u16le(data, vanetza::geodesy::detail::country_data_format_version);
|
||||
append_u16le(data, 276);
|
||||
append_u32le(data, 9999); // claims 9999 bytes of WKB
|
||||
data.push_back(0x01); // just 1 byte
|
||||
|
||||
auto result = read_country_data(data.data(), data.size(), [](M49Code, CountryPolygon&&) {});
|
||||
EXPECT_TRUE(result.failed());
|
||||
}
|
||||
|
||||
TEST(CountryDataReader, unsupported_geometry_type)
|
||||
{
|
||||
std::vector<uint8_t> wkb;
|
||||
wkb.push_back(0x01); // little-endian
|
||||
append_u32le(wkb, 1); // type = Point (unsupported)
|
||||
append_f64le(wkb, 10.0);
|
||||
append_f64le(wkb, 50.0);
|
||||
auto data = make_file({make_entry(276, wkb)});
|
||||
|
||||
auto result = read_country_data(data.data(), data.size(), [](M49Code, CountryPolygon&&) {});
|
||||
EXPECT_TRUE(result.failed());
|
||||
}
|
||||
|
||||
TEST(CountryDataReader, failure_reports_position)
|
||||
{
|
||||
// "truncated entry header" — position is the offset of the partial entry (right after version).
|
||||
std::vector<uint8_t> data;
|
||||
append_u16le(data, vanetza::geodesy::detail::country_data_format_version);
|
||||
data.push_back(0x14); // partial entry header at offset 2
|
||||
|
||||
auto result = read_country_data(data.data(), data.size(), [](M49Code, CountryPolygon&&) {});
|
||||
ASSERT_TRUE(result.failed());
|
||||
EXPECT_EQ(sizeof(uint16_t), result.position());
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
#include <vanetza/common/byte_order.hpp>
|
||||
#include <vanetza/geodesy/country_data_reader.hpp>
|
||||
#include <vanetza/geodesy/country_database.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
using namespace vanetza;
|
||||
using namespace vanetza::geodesy;
|
||||
using vanetza::units::degree;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
template<ByteOrder Order, typename T>
|
||||
void append(std::vector<uint8_t>& buf, T v)
|
||||
{
|
||||
EndianType<T, Order> e;
|
||||
e = host_cast(v);
|
||||
auto raw = e.get();
|
||||
const auto* p = reinterpret_cast<const uint8_t*>(&raw);
|
||||
buf.insert(buf.end(), p, p + sizeof(raw));
|
||||
}
|
||||
|
||||
void append_u16le(std::vector<uint8_t>& buf, uint16_t v)
|
||||
{
|
||||
append<ByteOrder::LittleEndian>(buf, v);
|
||||
}
|
||||
|
||||
void append_u32le(std::vector<uint8_t>& buf, uint32_t v)
|
||||
{
|
||||
append<ByteOrder::LittleEndian>(buf, v);
|
||||
}
|
||||
|
||||
void append_f64le(std::vector<uint8_t>& buf, double v)
|
||||
{
|
||||
uint64_t bits;
|
||||
std::memcpy(&bits, &v, sizeof(bits));
|
||||
append<ByteOrder::LittleEndian>(buf, bits);
|
||||
}
|
||||
|
||||
// Build WKB Polygon (LE) for a rectangle
|
||||
std::vector<uint8_t> make_rect_wkb(double lon_min, double lat_min, double lon_max, double lat_max)
|
||||
{
|
||||
std::vector<uint8_t> wkb;
|
||||
wkb.push_back(0x01); // LE
|
||||
append_u32le(wkb, 3); // Polygon
|
||||
append_u32le(wkb, 1); // 1 ring
|
||||
append_u32le(wkb, 5); // 5 points (closed)
|
||||
append_f64le(wkb, lon_min); append_f64le(wkb, lat_min);
|
||||
append_f64le(wkb, lon_max); append_f64le(wkb, lat_min);
|
||||
append_f64le(wkb, lon_max); append_f64le(wkb, lat_max);
|
||||
append_f64le(wkb, lon_min); append_f64le(wkb, lat_max);
|
||||
append_f64le(wkb, lon_min); append_f64le(wkb, lat_min);
|
||||
return wkb;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> make_entry(uint16_t m49, const std::vector<uint8_t>& wkb)
|
||||
{
|
||||
std::vector<uint8_t> entry;
|
||||
append_u16le(entry, m49);
|
||||
append_u32le(entry, static_cast<uint32_t>(wkb.size()));
|
||||
entry.insert(entry.end(), wkb.begin(), wkb.end());
|
||||
return entry;
|
||||
}
|
||||
|
||||
const uint16_t germany_m49 = 276;
|
||||
const uint16_t france_m49 = 250;
|
||||
|
||||
std::vector<uint8_t> make_test_data()
|
||||
{
|
||||
// Germany bounding box (rough): lon 5.9-15.0, lat 47.3-55.1
|
||||
auto de_wkb = make_rect_wkb(5.9, 47.3, 15.0, 55.1);
|
||||
// France bounding box (rough): lon -5.1-9.6, lat 42.3-51.1
|
||||
auto fr_wkb = make_rect_wkb(-5.1, 42.3, 9.6, 51.1);
|
||||
auto de_entry = make_entry(germany_m49, de_wkb);
|
||||
auto fr_entry = make_entry(france_m49, fr_wkb);
|
||||
std::vector<uint8_t> data;
|
||||
append_u16le(data, vanetza::geodesy::detail::country_data_format_version);
|
||||
data.insert(data.end(), de_entry.begin(), de_entry.end());
|
||||
data.insert(data.end(), fr_entry.begin(), fr_entry.end());
|
||||
return data;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
TEST(CountryDatabase, initially_empty)
|
||||
{
|
||||
CountryDatabase db;
|
||||
EXPECT_TRUE(db.empty());
|
||||
}
|
||||
|
||||
TEST(CountryDatabase, load_from_buffer)
|
||||
{
|
||||
auto data = make_test_data();
|
||||
CountryDatabase db;
|
||||
EXPECT_TRUE(db.load(data.data(), data.size()));
|
||||
EXPECT_FALSE(db.empty());
|
||||
}
|
||||
|
||||
TEST(CountryDatabase, berlin_inside_germany)
|
||||
{
|
||||
auto data = make_test_data();
|
||||
CountryDatabase db;
|
||||
ASSERT_TRUE(db.load(data.data(), data.size()));
|
||||
|
||||
GeodeticPosition berlin(52.52 * degree, 13.405 * degree);
|
||||
EXPECT_TRUE(db.is_inside(M49Code(germany_m49), berlin));
|
||||
}
|
||||
|
||||
TEST(CountryDatabase, paris_inside_france)
|
||||
{
|
||||
auto data = make_test_data();
|
||||
CountryDatabase db;
|
||||
ASSERT_TRUE(db.load(data.data(), data.size()));
|
||||
|
||||
GeodeticPosition paris(48.8566 * degree, 2.3522 * degree);
|
||||
EXPECT_TRUE(db.is_inside(M49Code(france_m49), paris));
|
||||
}
|
||||
|
||||
TEST(CountryDatabase, berlin_not_in_france)
|
||||
{
|
||||
auto data = make_test_data();
|
||||
CountryDatabase db;
|
||||
ASSERT_TRUE(db.load(data.data(), data.size()));
|
||||
|
||||
GeodeticPosition berlin(52.52 * degree, 13.405 * degree);
|
||||
EXPECT_FALSE(db.is_inside(M49Code(france_m49), berlin));
|
||||
}
|
||||
|
||||
TEST(CountryDatabase, mid_atlantic_in_no_country)
|
||||
{
|
||||
auto data = make_test_data();
|
||||
CountryDatabase db;
|
||||
ASSERT_TRUE(db.load(data.data(), data.size()));
|
||||
|
||||
GeodeticPosition ocean(40.0 * degree, -30.0 * degree);
|
||||
EXPECT_FALSE(db.is_inside(M49Code(germany_m49), ocean));
|
||||
EXPECT_FALSE(db.is_inside(M49Code(france_m49), ocean));
|
||||
}
|
||||
|
||||
TEST(CountryDatabase, unknown_country_code)
|
||||
{
|
||||
auto data = make_test_data();
|
||||
CountryDatabase db;
|
||||
ASSERT_TRUE(db.load(data.data(), data.size()));
|
||||
|
||||
GeodeticPosition berlin(52.52 * degree, 13.405 * degree);
|
||||
EXPECT_FALSE(db.is_inside(M49Code(999), berlin));
|
||||
}
|
||||
|
||||
TEST(CountryDatabase, load_invalid_data)
|
||||
{
|
||||
std::vector<uint8_t> bad_data = {0x14};
|
||||
CountryDatabase db;
|
||||
std::string error;
|
||||
EXPECT_FALSE(db.load(bad_data.data(), bad_data.size(), &error));
|
||||
EXPECT_FALSE(error.empty());
|
||||
EXPECT_TRUE(db.empty());
|
||||
}
|
||||
|
||||
#ifdef VANETZA_WITH_EMBEDDED_COUNTRY_DATA
|
||||
|
||||
TEST(CountryDatabase, embedded_not_empty)
|
||||
{
|
||||
auto db = CountryDatabase::embedded();
|
||||
EXPECT_FALSE(db.empty());
|
||||
}
|
||||
|
||||
TEST(CountryDatabase, embedded_ingolstadt_in_germany)
|
||||
{
|
||||
auto db = CountryDatabase::embedded();
|
||||
// Ingolstadt, Germany
|
||||
GeodeticPosition ingolstadt(48.7665 * degree, 11.4258 * degree);
|
||||
EXPECT_TRUE(db.is_inside(M49Code(germany_m49), ingolstadt));
|
||||
}
|
||||
|
||||
TEST(CountryDatabase, embedded_etsi_hq_in_france)
|
||||
{
|
||||
auto db = CountryDatabase::embedded();
|
||||
// ETSI headquarters, Sophia Antipolis, France
|
||||
GeodeticPosition etsi_hq(43.6244 * degree, 7.0494 * degree);
|
||||
EXPECT_TRUE(db.is_inside(M49Code(france_m49), etsi_hq));
|
||||
}
|
||||
|
||||
#endif // VANETZA_WITH_EMBEDDED_COUNTRY_DATA
|
||||
@@ -0,0 +1,110 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <vanetza/geodesy/geodesy.hpp>
|
||||
#include <vanetza/geodesy/haversine.hpp>
|
||||
#ifdef VANETZA_WITH_GEOGRAPHICLIB
|
||||
#include <vanetza/geodesy/geographiclib.hpp>
|
||||
#endif
|
||||
|
||||
using namespace vanetza::geodesy;
|
||||
namespace units = vanetza::units;
|
||||
using units::si::meter;
|
||||
using units::degree;
|
||||
|
||||
// Technische Hochschule Ingolstadt
|
||||
static const GeodeticPosition thi(48.76714 * degree, 11.43263 * degree);
|
||||
// Zentrum fuer Angewandte Forschung
|
||||
static const GeodeticPosition zaf(48.7656 * degree, 11.4296 * degree);
|
||||
// Munich (short range ~70 km)
|
||||
static const GeodeticPosition munich(48.1351 * degree, 11.5820 * degree);
|
||||
// Frankfurt (medium range ~304 km)
|
||||
static const GeodeticPosition frankfurt(50.1109 * degree, 8.6821 * degree);
|
||||
// Sao Paulo (long range ~10000 km)
|
||||
static const GeodeticPosition sao_paulo(-25.41272 * degree, -49.24815 * degree);
|
||||
|
||||
|
||||
TEST(GeodesyHaversine, distance_zero)
|
||||
{
|
||||
auto d = haversine::distance(thi, thi);
|
||||
EXPECT_DOUBLE_EQ(0.0, d / meter);
|
||||
}
|
||||
|
||||
TEST(GeodesyHaversine, distance_short_range)
|
||||
{
|
||||
auto d = haversine::distance(thi, zaf);
|
||||
EXPECT_NEAR(d / meter, 280.0, 10.0);
|
||||
}
|
||||
|
||||
TEST(GeodesyHaversine, distance_medium_range)
|
||||
{
|
||||
auto d = haversine::distance(frankfurt, munich);
|
||||
EXPECT_NEAR(d / meter, 304000.0, 3000.0);
|
||||
}
|
||||
|
||||
TEST(GeodesyHaversine, local_cartesian_short_range)
|
||||
{
|
||||
auto cart = haversine::local_cartesian(thi, zaf);
|
||||
// ZAF is roughly south-west of THI
|
||||
EXPECT_NEAR(cart.x / meter, -222.0, 5.0);
|
||||
EXPECT_NEAR(cart.y / meter, -171.0, 5.0);
|
||||
}
|
||||
|
||||
TEST(GeodesyHaversine, local_cartesian_zero)
|
||||
{
|
||||
auto cart = haversine::local_cartesian(thi, thi);
|
||||
EXPECT_DOUBLE_EQ(cart.x / meter, 0.0);
|
||||
EXPECT_DOUBLE_EQ(cart.y / meter, 0.0);
|
||||
}
|
||||
|
||||
#ifdef VANETZA_WITH_GEOGRAPHICLIB
|
||||
|
||||
TEST(GeodesyGeographicLib, distance_matches_known_value)
|
||||
{
|
||||
auto d = geographiclib::distance(thi, sao_paulo);
|
||||
EXPECT_NEAR(d / meter, 10185367.442, 0.5);
|
||||
}
|
||||
|
||||
TEST(GeodesyGeographicLib, local_cartesian_known_value)
|
||||
{
|
||||
auto cart = geographiclib::local_cartesian(thi, zaf);
|
||||
EXPECT_NEAR(cart.x / meter, -222.74, 0.01);
|
||||
EXPECT_NEAR(cart.y / meter, -171.25, 0.01);
|
||||
}
|
||||
|
||||
TEST(GeodesyComparison, distance_short_range)
|
||||
{
|
||||
auto h = haversine::distance(thi, zaf);
|
||||
auto g = geographiclib::distance(thi, zaf);
|
||||
EXPECT_NEAR(h / meter, g / meter, std::abs(g / meter * 0.005)); // <0.5%
|
||||
}
|
||||
|
||||
TEST(GeodesyComparison, distance_medium_range)
|
||||
{
|
||||
auto h = haversine::distance(frankfurt, munich);
|
||||
auto g = geographiclib::distance(frankfurt, munich);
|
||||
EXPECT_NEAR(h / meter, g / meter, std::abs(g / meter * 0.005)); // <0.5%
|
||||
}
|
||||
|
||||
TEST(GeodesyComparison, distance_long_range)
|
||||
{
|
||||
auto h = haversine::distance(thi, sao_paulo);
|
||||
auto g = geographiclib::distance(thi, sao_paulo);
|
||||
EXPECT_NEAR(h / meter, g / meter, std::abs(g / meter * 0.005)); // <0.5%
|
||||
}
|
||||
|
||||
TEST(GeodesyComparison, local_cartesian_short_range)
|
||||
{
|
||||
auto h = haversine::local_cartesian(thi, zaf);
|
||||
auto g = geographiclib::local_cartesian(thi, zaf);
|
||||
EXPECT_NEAR(h.x / meter, g.x / meter, 1.0); // <1m
|
||||
EXPECT_NEAR(h.y / meter, g.y / meter, 1.0);
|
||||
}
|
||||
|
||||
TEST(GeodesyComparison, local_cartesian_medium_range)
|
||||
{
|
||||
auto h = haversine::local_cartesian(thi, munich);
|
||||
auto g = geographiclib::local_cartesian(thi, munich);
|
||||
EXPECT_NEAR(h.x / meter, g.x / meter, std::abs(g.x / meter * 0.02)); // <2%
|
||||
EXPECT_NEAR(h.y / meter, g.y / meter, std::abs(g.y / meter * 0.02));
|
||||
}
|
||||
|
||||
#endif // VANETZA_WITH_GEOGRAPHICLIB
|
||||
@@ -0,0 +1,39 @@
|
||||
#include <vanetza/geodesy/m49_code.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
|
||||
using vanetza::geodesy::M49Code;
|
||||
|
||||
TEST(M49Code, value)
|
||||
{
|
||||
M49Code code(276);
|
||||
EXPECT_EQ(276, code.value());
|
||||
}
|
||||
|
||||
TEST(M49Code, equality)
|
||||
{
|
||||
EXPECT_EQ(M49Code(276), M49Code(276));
|
||||
EXPECT_NE(M49Code(276), M49Code(250));
|
||||
}
|
||||
|
||||
TEST(M49Code, std_hash)
|
||||
{
|
||||
std::unordered_set<M49Code> codes;
|
||||
codes.insert(M49Code(276));
|
||||
codes.insert(M49Code(250));
|
||||
codes.insert(M49Code(276)); // duplicate
|
||||
EXPECT_EQ(2u, codes.size());
|
||||
EXPECT_EQ(1u, codes.count(M49Code(276)));
|
||||
EXPECT_EQ(1u, codes.count(M49Code(250)));
|
||||
}
|
||||
|
||||
TEST(M49Code, std_less)
|
||||
{
|
||||
std::map<M49Code, std::string> names;
|
||||
names[M49Code(276)] = "Germany";
|
||||
names[M49Code(250)] = "France";
|
||||
EXPECT_EQ("Germany", names.at(M49Code(276)));
|
||||
EXPECT_EQ("France", names.at(M49Code(250)));
|
||||
}
|
||||
Reference in New Issue
Block a user