mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-08-03 10:46:12 +00:00
feat(companion): stream-simplify GPS trail points on straight runs
addPoint() now holds the latest accepted sample as a pending candidate instead of committing it immediately. It only becomes a stored vertex once a later sample proves it was a real bend (cross-track deviation from the last vertex exceeds the min-delta tolerance) — so a straight stretch costs just its two endpoints no matter how long it is, while turns still get a vertex roughly every min-delta of deviation. This stretches the 1024-point buffer over much longer routes without losing shape on curves. The pending candidate is flushed before any segment break (setActive(false), setPaused(true), writeTo()) so the last stretch of a run is never silently dropped just because no bend forced a commit; readFrom()/clear() drop a leftover candidate that belonged to the session being replaced. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
4f175bda3c
commit
0f475969c7
@@ -12,6 +12,11 @@
|
|||||||
// always resident (UITask::_trail member). The trail survives auto-off (only
|
// always resident (UITask::_trail member). The trail survives auto-off (only
|
||||||
// the display blanks) but is lost on reboot — user explicitly snapshots to a
|
// the display blanks) but is lost on reboot — user explicitly snapshots to a
|
||||||
// LittleFS slot before powering down to keep it.
|
// LittleFS slot before powering down to keep it.
|
||||||
|
//
|
||||||
|
// Samples are simplified in-stream as they arrive (see addPoint()): a straight
|
||||||
|
// stretch is stored as just its two endpoints, no matter how long, while a
|
||||||
|
// turn still gets a vertex roughly every min-delta of deviation — so CAPACITY
|
||||||
|
// covers a much longer route than CAPACITY × min-delta would suggest.
|
||||||
|
|
||||||
struct TrailPoint {
|
struct TrailPoint {
|
||||||
int32_t lat_1e6;
|
int32_t lat_1e6;
|
||||||
@@ -81,6 +86,7 @@ public:
|
|||||||
_accumulated_ms += millis() - _session_start_ms;
|
_accumulated_ms += millis() - _session_start_ms;
|
||||||
_session_start_ms = 0;
|
_session_start_ms = 0;
|
||||||
}
|
}
|
||||||
|
flushPending();
|
||||||
_pending_seg_break = true;
|
_pending_seg_break = true;
|
||||||
_paused = false;
|
_paused = false;
|
||||||
}
|
}
|
||||||
@@ -100,6 +106,7 @@ public:
|
|||||||
_accumulated_ms += millis() - _session_start_ms;
|
_accumulated_ms += millis() - _session_start_ms;
|
||||||
_session_start_ms = 0;
|
_session_start_ms = 0;
|
||||||
}
|
}
|
||||||
|
flushPending();
|
||||||
_pending_seg_break = true;
|
_pending_seg_break = true;
|
||||||
} else {
|
} else {
|
||||||
_session_start_ms = millis(); // resume timing from now
|
_session_start_ms = millis(); // resume timing from now
|
||||||
@@ -121,31 +128,52 @@ public:
|
|||||||
_accumulated_ms = 0;
|
_accumulated_ms = 0;
|
||||||
_session_start_ms = 0;
|
_session_start_ms = 0;
|
||||||
_paused = false;
|
_paused = false;
|
||||||
|
_has_pending = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns true if the point was stored (passed the min-delta gate).
|
// Returns true if the sample was accepted (passed the min-delta gate) —
|
||||||
// First point of the ring and the first point after a stop/start cycle
|
// whether that landed it as a new committed vertex right away, or just
|
||||||
// get flagged TRAIL_FLAG_SEG_START so the map renderer breaks the line.
|
// extended the pending candidate (see below). First point of the ring and
|
||||||
|
// the first point after a stop/start cycle get flagged TRAIL_FLAG_SEG_START
|
||||||
|
// so the map renderer breaks the line.
|
||||||
|
//
|
||||||
|
// Straight-run simplification: a sample is only ever a *candidate* vertex
|
||||||
|
// (_pending) until the next sample proves it was a real bend. On each new
|
||||||
|
// sample, check whether the previous candidate still lies within
|
||||||
|
// min_delta_m of the straight line from the last committed vertex to this
|
||||||
|
// new sample — if so it was redundant (the run is still straight), drop it
|
||||||
|
// and extend the candidate; if not, the candidate was a genuine corner, so
|
||||||
|
// commit it as a vertex and start a fresh candidate run from there. This
|
||||||
|
// reuses min_delta_m as the simplification tolerance too, so a straight
|
||||||
|
// road costs two stored points (its endpoints) no matter how long it is,
|
||||||
|
// while a curve still gets a vertex every ~min_delta_m of deviation.
|
||||||
bool addPoint(int32_t lat_1e6, int32_t lon_1e6, uint32_t ts, uint16_t min_delta_m) {
|
bool addPoint(int32_t lat_1e6, int32_t lon_1e6, uint32_t ts, uint16_t min_delta_m) {
|
||||||
if (_count > 0 && !_pending_seg_break) {
|
const TrailPoint* ref = _has_pending ? &_pending : (_count > 0 ? &last() : nullptr);
|
||||||
float d = haversineMeters(last().lat_1e6, last().lon_1e6, lat_1e6, lon_1e6);
|
if (ref && !_pending_seg_break) {
|
||||||
|
float d = haversineMeters(ref->lat_1e6, ref->lon_1e6, lat_1e6, lon_1e6);
|
||||||
if (d < (float)min_delta_m) return false;
|
if (d < (float)min_delta_m) return false;
|
||||||
}
|
}
|
||||||
uint8_t flags = (_count == 0 || _pending_seg_break) ? TRAIL_FLAG_SEG_START : 0;
|
|
||||||
_pending_seg_break = false;
|
|
||||||
|
|
||||||
int pos;
|
if (_count == 0 || _pending_seg_break) {
|
||||||
if (_count < CAPACITY) {
|
flushPending(); // shouldn't normally have one here, but never lose real distance
|
||||||
pos = (_head + _count) % CAPACITY;
|
commitPoint(lat_1e6, lon_1e6, ts, TRAIL_FLAG_SEG_START);
|
||||||
_count++;
|
_pending_seg_break = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TrailPoint sample{ lat_1e6, lon_1e6, ts, 0 };
|
||||||
|
if (!_has_pending) {
|
||||||
|
_pending = sample;
|
||||||
|
_has_pending = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (crossTrackMeters(last(), sample, _pending) <= (float)min_delta_m) {
|
||||||
|
_pending = sample; // still straight — extend
|
||||||
} else {
|
} else {
|
||||||
pos = _head;
|
commitPoint(_pending.lat_1e6, _pending.lon_1e6, _pending.ts, 0); // real bend — keep it
|
||||||
_head = (_head + 1) % CAPACITY;
|
_pending = sample;
|
||||||
}
|
}
|
||||||
_buf[pos].lat_1e6 = lat_1e6;
|
|
||||||
_buf[pos].lon_1e6 = lon_1e6;
|
|
||||||
_buf[pos].ts = ts;
|
|
||||||
_buf[pos].flags = flags;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,6 +233,7 @@ public:
|
|||||||
// cleanly. The file is left open for the caller to close.
|
// cleanly. The file is left open for the caller to close.
|
||||||
template <typename F>
|
template <typename F>
|
||||||
bool writeTo(F& file) {
|
bool writeTo(F& file) {
|
||||||
|
flushPending(); // the snapshot should include the latest position, not lag behind it
|
||||||
if (!persist::writeHeader(file, SAVE_MAGIC, SAVE_VERSION, (uint16_t)_count)) return false;
|
if (!persist::writeHeader(file, SAVE_MAGIC, SAVE_VERSION, (uint16_t)_count)) return false;
|
||||||
uint32_t accum = currentAccumulatedMs();
|
uint32_t accum = currentAccumulatedMs();
|
||||||
if (file.write((uint8_t*)&accum, sizeof(accum)) != sizeof(accum)) return false;
|
if (file.write((uint8_t*)&accum, sizeof(accum)) != sizeof(accum)) return false;
|
||||||
@@ -226,6 +255,7 @@ public:
|
|||||||
_session_start_ms = 0;
|
_session_start_ms = 0;
|
||||||
}
|
}
|
||||||
_paused = false;
|
_paused = false;
|
||||||
|
_has_pending = false; // any candidate belonged to the session being replaced
|
||||||
_head = 0;
|
_head = 0;
|
||||||
_count = 0;
|
_count = 0;
|
||||||
for (int i = 0; i < cnt; i++) {
|
for (int i = 0; i < cnt; i++) {
|
||||||
@@ -395,4 +425,51 @@ private:
|
|||||||
bool _pending_seg_break = false; // next addPoint flags itself SEG_START
|
bool _pending_seg_break = false; // next addPoint flags itself SEG_START
|
||||||
uint32_t _accumulated_ms = 0; // banked active time across previous sessions
|
uint32_t _accumulated_ms = 0; // banked active time across previous sessions
|
||||||
uint32_t _session_start_ms = 0; // millis() of the current active session, 0 if none
|
uint32_t _session_start_ms = 0; // millis() of the current active session, 0 if none
|
||||||
|
|
||||||
|
// Streaming simplification: a sample that passed the min-delta gate but
|
||||||
|
// hasn't been committed as a real vertex yet — see addPoint().
|
||||||
|
bool _has_pending = false;
|
||||||
|
TrailPoint _pending;
|
||||||
|
|
||||||
|
void commitPoint(int32_t lat_1e6, int32_t lon_1e6, uint32_t ts, uint8_t flags) {
|
||||||
|
int pos;
|
||||||
|
if (_count < CAPACITY) {
|
||||||
|
pos = (_head + _count) % CAPACITY;
|
||||||
|
_count++;
|
||||||
|
} else {
|
||||||
|
pos = _head;
|
||||||
|
_head = (_head + 1) % CAPACITY;
|
||||||
|
}
|
||||||
|
_buf[pos].lat_1e6 = lat_1e6;
|
||||||
|
_buf[pos].lon_1e6 = lon_1e6;
|
||||||
|
_buf[pos].ts = ts;
|
||||||
|
_buf[pos].flags = flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the pending candidate (if any) as a real vertex. Called before a
|
||||||
|
// segment break (stop/pause/save) so the last stretch of a straight run is
|
||||||
|
// never silently dropped just because no bend came along to force a commit.
|
||||||
|
void flushPending() {
|
||||||
|
if (!_has_pending) return;
|
||||||
|
commitPoint(_pending.lat_1e6, _pending.lon_1e6, _pending.ts, 0);
|
||||||
|
_has_pending = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perpendicular ("cross-track") distance from q to the line through a→b, in
|
||||||
|
// metres. Planar approximation (equirectangular, centred at a) — accurate
|
||||||
|
// enough at trail scale, where consecutive points are metres to a few km
|
||||||
|
// apart. Falls back to a straight distance to a if a and b coincide.
|
||||||
|
static float crossTrackMeters(const TrailPoint& a, const TrailPoint& b, const TrailPoint& q) {
|
||||||
|
static const float M_PER_DEG = 111320.0f; // metres per degree of latitude
|
||||||
|
float lat_rad = (a.lat_1e6 * 1e-6f) * ((float)M_PI / 180.0f);
|
||||||
|
float lon_scale = cosf(lat_rad);
|
||||||
|
float bx = (b.lon_1e6 - a.lon_1e6) * 1e-6f * M_PER_DEG * lon_scale;
|
||||||
|
float by = (b.lat_1e6 - a.lat_1e6) * 1e-6f * M_PER_DEG;
|
||||||
|
float ab_len = sqrtf(bx * bx + by * by);
|
||||||
|
if (ab_len < 0.01f) return haversineMeters(a.lat_1e6, a.lon_1e6, q.lat_1e6, q.lon_1e6);
|
||||||
|
float qx = (q.lon_1e6 - a.lon_1e6) * 1e-6f * M_PER_DEG * lon_scale;
|
||||||
|
float qy = (q.lat_1e6 - a.lat_1e6) * 1e-6f * M_PER_DEG;
|
||||||
|
float cross = bx * qy - by * qx;
|
||||||
|
return fabsf(cross) / ab_len;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user