mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-27 15:28:11 +00:00
feat(units): imperial min-distance gate + imperial map grid
Follow the global Units preference in the two remaining Trail distance spots that were still metric-only: - Min dist gate: the index is now a unit-agnostic level (0=finest..3). Metric stays 5/10/25/100 m; imperial uses round 15/30/75/300 ft (applied as their metre equivalents in the sampling gate). - Map scale grid: imperial mode uses round ft/mi steps (10 ft … 100 mi) instead of metric steps with a converted label. Step storage moved to float metres so sub-metre imperial steps keep their geometry; labels come from a parallel table so the scale bar reads an exact round value. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -102,7 +102,7 @@ struct NodePrefs { // persisted to file
|
||||
// GPS trail cadence. Logging on/off is a runtime state (Tools › Trail),
|
||||
// not a persisted preference.
|
||||
uint8_t trail_interval_idx; // reserved — sampling cadence is now fixed at TrailStore::SAMPLING_SECS
|
||||
uint8_t trail_min_delta_idx; // indexes TrailStore::minDeltaMeters (5/10/25/100 m)
|
||||
uint8_t trail_min_delta_idx; // min-distance gate level (0=finest..3); metres or feet per units_imperial
|
||||
uint8_t trail_units_idx; // legacy: old combined speed/pace+unit index (km/h, mph, min/km, min/mi)
|
||||
|
||||
uint64_t ch_fav_bitmask; // bit i = channel i is marked as favourite
|
||||
|
||||
@@ -31,14 +31,20 @@ public:
|
||||
|
||||
// Min-delta (metres) gates samples too close to the previous one.
|
||||
// Default 5 m: keeps walking jitter out, dense enough for a visible trail.
|
||||
// The index is a unit-agnostic "level" (0=finest … 3=coarsest); the actual
|
||||
// gate distance and its label follow the global metric/imperial preference.
|
||||
static const uint8_t MIN_DELTA_COUNT = 4;
|
||||
static uint16_t minDeltaMeters(uint8_t idx) {
|
||||
static const uint16_t OPTS[MIN_DELTA_COUNT] = { 5, 10, 25, 100 };
|
||||
return OPTS[idx < MIN_DELTA_COUNT ? idx : 0];
|
||||
static uint16_t minDeltaMeters(uint8_t idx, bool imperial) {
|
||||
static const uint16_t MET[MIN_DELTA_COUNT] = { 5, 10, 25, 100 };
|
||||
static const uint16_t IMP[MIN_DELTA_COUNT] = { 5, 9, 23, 91 }; // ≈ 15/30/75/300 ft
|
||||
if (idx >= MIN_DELTA_COUNT) idx = 0;
|
||||
return imperial ? IMP[idx] : MET[idx];
|
||||
}
|
||||
static const char* minDeltaLabel(uint8_t idx) {
|
||||
static const char* L[MIN_DELTA_COUNT] = { "5 m", "10 m", "25 m", "100 m" };
|
||||
return L[idx < MIN_DELTA_COUNT ? idx : 0];
|
||||
static const char* minDeltaLabel(uint8_t idx, bool imperial) {
|
||||
static const char* MET[MIN_DELTA_COUNT] = { "5 m", "10 m", "25 m", "100 m" };
|
||||
static const char* IMP[MIN_DELTA_COUNT] = { "15 ft", "30 ft", "75 ft", "300 ft" };
|
||||
if (idx >= MIN_DELTA_COUNT) idx = 0;
|
||||
return imperial ? IMP[idx] : MET[idx];
|
||||
}
|
||||
|
||||
// Speed / pace display units. UNITS_KMH / UNITS_MPH show speed; UNITS_PACE_KM
|
||||
|
||||
@@ -288,7 +288,8 @@ private:
|
||||
void refreshActionLabels() {
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
snprintf(_act_min_dist_label, sizeof(_act_min_dist_label),
|
||||
"Min dist: %s", TrailStore::minDeltaLabel(p ? p->trail_min_delta_idx : 0));
|
||||
"Min dist: %s", TrailStore::minDeltaLabel(p ? p->trail_min_delta_idx : 0,
|
||||
p && p->units_imperial));
|
||||
// Unit system (km/mi) is the global Settings choice; here we only toggle
|
||||
// what the Summary readout shows — speed or pace.
|
||||
snprintf(_act_units_label, sizeof(_act_units_label),
|
||||
@@ -775,26 +776,45 @@ private:
|
||||
float shorter_m = (float)(area_w < area_h ? area_w : area_h) / ppm;
|
||||
float target_m = shorter_m / 3.0f;
|
||||
|
||||
static const uint32_t STEPS[] = {
|
||||
// Round scale steps with matching labels. The grid geometry works in metres,
|
||||
// so imperial steps are stored as their metre equivalents but labelled with
|
||||
// their exact round ft/mi value. Both tables are parallel (step ↔ label).
|
||||
static const float METRIC_STEPS[] = {
|
||||
1, 2, 5, 10, 20, 50, 100, 200, 500,
|
||||
1000, 2000, 5000, 10000, 20000, 50000, 100000
|
||||
};
|
||||
static const int N_STEPS = (int)(sizeof(STEPS)/sizeof(STEPS[0]));
|
||||
static const char* METRIC_LABELS[] = {
|
||||
"1m", "2m", "5m", "10m", "20m", "50m", "100m", "200m", "500m",
|
||||
"1km", "2km", "5km", "10km", "20km", "50km", "100km"
|
||||
};
|
||||
static const float IMPERIAL_STEPS[] = { // 10..2000 ft, then 1..100 mi (metres)
|
||||
3.048f, 7.62f, 15.24f, 30.48f, 76.2f, 152.4f, 304.8f, 609.6f,
|
||||
1609.344f, 3218.69f, 8046.72f, 16093.44f, 32186.88f, 80467.2f, 160934.4f
|
||||
};
|
||||
static const char* IMPERIAL_LABELS[] = {
|
||||
"10ft", "25ft", "50ft", "100ft", "250ft", "500ft", "1000ft", "2000ft",
|
||||
"1mi", "2mi", "5mi", "10mi", "20mi", "50mi", "100mi"
|
||||
};
|
||||
const bool imp = useImperial();
|
||||
const float* STEPS = imp ? IMPERIAL_STEPS : METRIC_STEPS;
|
||||
const char* const* LABELS = imp ? IMPERIAL_LABELS : METRIC_LABELS;
|
||||
const int N_STEPS = imp ? (int)(sizeof(IMPERIAL_STEPS)/sizeof(IMPERIAL_STEPS[0]))
|
||||
: (int)(sizeof(METRIC_STEPS)/sizeof(METRIC_STEPS[0]));
|
||||
|
||||
// Pick largest step ≤ target_m (gives ~3 intervals across shorter dim).
|
||||
int sel = 0;
|
||||
for (int si = N_STEPS - 1; si >= 0; si--) {
|
||||
if ((float)STEPS[si] <= target_m) { sel = si; break; }
|
||||
if (STEPS[si] <= target_m) { sel = si; break; }
|
||||
}
|
||||
// Bump up until pixel spacing between intersections is at least MIN_GRID_PX.
|
||||
// This keeps OLED grids sparse even when target_m selects a fine step.
|
||||
static const float MIN_GRID_PX = 22.0f;
|
||||
while (sel < N_STEPS - 1 && (float)STEPS[sel] / M_PER_1E6 * scale < MIN_GRID_PX)
|
||||
while (sel < N_STEPS - 1 && STEPS[sel] / M_PER_1E6 * scale < MIN_GRID_PX)
|
||||
sel++;
|
||||
// Clamp back down: ensure at least 2 intervals (3 visible lines) across
|
||||
// the shorter dimension. MIN_GRID_PX must not leave only 1 interval.
|
||||
float shorter_px = (float)(area_w < area_h ? area_w : area_h);
|
||||
while (sel > 0 && (float)STEPS[sel] / M_PER_1E6 * scale > shorter_px / 2.0f)
|
||||
while (sel > 0 && STEPS[sel] / M_PER_1E6 * scale > shorter_px / 2.0f)
|
||||
sel--;
|
||||
|
||||
float vis_lat_max = (float)max_lat + (float)(off_y - area_y) / scale;
|
||||
@@ -808,16 +828,16 @@ private:
|
||||
// line count fits the static buffers (40×40 = ~1600 intersections, plenty
|
||||
// for any sane display).
|
||||
static const int MAX_GRID_LINES = 40;
|
||||
uint32_t grid_m = STEPS[sel];
|
||||
float gs_lat = (float)grid_m / M_PER_1E6;
|
||||
float gs_lon = (float)grid_m / (M_PER_1E6 * lon_scale_geo);
|
||||
float grid_m = STEPS[sel];
|
||||
float gs_lat = grid_m / M_PER_1E6;
|
||||
float gs_lon = grid_m / (M_PER_1E6 * lon_scale_geo);
|
||||
int lat_n = (int)((vis_lat_max - vis_lat_min) / gs_lat) + 2;
|
||||
int lon_n = (int)((vis_lon_max - vis_lon_min) / gs_lon) + 2;
|
||||
while ((lat_n > MAX_GRID_LINES || lon_n > MAX_GRID_LINES) && sel < N_STEPS - 1) {
|
||||
sel++;
|
||||
grid_m = STEPS[sel];
|
||||
gs_lat = (float)grid_m / M_PER_1E6;
|
||||
gs_lon = (float)grid_m / (M_PER_1E6 * lon_scale_geo);
|
||||
gs_lat = grid_m / M_PER_1E6;
|
||||
gs_lon = grid_m / (M_PER_1E6 * lon_scale_geo);
|
||||
lat_n = (int)((vis_lat_max - vis_lat_min) / gs_lat) + 2;
|
||||
lon_n = (int)((vis_lon_max - vis_lon_min) / gs_lon) + 2;
|
||||
}
|
||||
@@ -835,18 +855,9 @@ private:
|
||||
display.fillRect(x, y, 1, 1);
|
||||
};
|
||||
|
||||
// Grid spacing stays metric (the STEPS are round metres); the label is
|
||||
// converted to the user's unit so the scale bar matches the rest of the UI.
|
||||
// Label is the exact round step value for the selected unit system.
|
||||
char lbl[12];
|
||||
if (useImperial()) {
|
||||
float ft = grid_m * 3.28084f;
|
||||
if (ft < 5280.0f) snprintf(lbl, sizeof(lbl), "%uft", (unsigned)(ft + 0.5f));
|
||||
else snprintf(lbl, sizeof(lbl), "%umi", (unsigned)(grid_m / 1609.344f + 0.5f));
|
||||
} else if (grid_m < 1000) {
|
||||
snprintf(lbl, sizeof(lbl), "%um", (unsigned)grid_m);
|
||||
} else {
|
||||
snprintf(lbl, sizeof(lbl), "%ukm", (unsigned)(grid_m / 1000));
|
||||
}
|
||||
snprintf(lbl, sizeof(lbl), "%s", LABELS[sel]);
|
||||
int lh = display.getLineHeight();
|
||||
int cw = display.getCharWidth();
|
||||
|
||||
|
||||
@@ -1921,7 +1921,8 @@ void UITask::loop() {
|
||||
_next_trail_sample_ms = millis() + (uint32_t)TrailStore::SAMPLING_SECS * 1000UL;
|
||||
LocationProvider* loc = _sensors ? _sensors->getLocationProvider() : nullptr;
|
||||
if (loc && loc->isValid()) {
|
||||
uint16_t md = TrailStore::minDeltaMeters(_node_prefs->trail_min_delta_idx);
|
||||
uint16_t md = TrailStore::minDeltaMeters(_node_prefs->trail_min_delta_idx,
|
||||
_node_prefs->units_imperial);
|
||||
_trail.addPoint((int32_t)loc->getLatitude(),
|
||||
(int32_t)loc->getLongitude(),
|
||||
(uint32_t)rtc_clock.getCurrentTime(), md);
|
||||
|
||||
Reference in New Issue
Block a user