mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-08-24 11:39:49 +00:00
972 lines
42 KiB
C++
972 lines
42 KiB
C++
// Copyright (c) 2026 Pyxis contributors
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#include "MapScreen.h"
|
|
#include "MapTileStreamReader.h"
|
|
|
|
#ifdef ARDUINO
|
|
|
|
#include "Theme.h"
|
|
#include "../LVGL/LVGLInit.h"
|
|
#include "../LVGL/LVGLLock.h"
|
|
|
|
#include <esp_heap_caps.h>
|
|
#include <esp_system.h>
|
|
#define LODEPNG_NO_COMPILE_CPP
|
|
extern "C" {
|
|
#include <src/extra/libs/png/lodepng.h>
|
|
}
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
|
|
namespace UI {
|
|
namespace LXMF {
|
|
namespace {
|
|
|
|
constexpr std::size_t TILE_PIXEL_COUNT = 256U * 256U;
|
|
|
|
class PackReadStream final : public Pyxis::MapTileReadStream {
|
|
public:
|
|
explicit PackReadStream(Hardware::TDeck::MapTilePack& pack) : pack_(pack) {}
|
|
Pyxis::MapTileStreamResult begin(const Hardware::TDeck::TileKey& key,
|
|
std::uint32_t& size) override {
|
|
const Hardware::TDeck::MapTilePackResult result = pack_.beginGet(key, size);
|
|
if (result == Hardware::TDeck::MapTilePackResult::OK) return Pyxis::MapTileStreamResult::OK;
|
|
if (result == Hardware::TDeck::MapTilePackResult::UNCOVERED ||
|
|
result == Hardware::TDeck::MapTilePackResult::TILE_MISSING ||
|
|
result == Hardware::TDeck::MapTilePackResult::NOT_INITIALIZED) {
|
|
return Pyxis::MapTileStreamResult::MISS;
|
|
}
|
|
if (result == Hardware::TDeck::MapTilePackResult::STORAGE_UNAVAILABLE) {
|
|
return Pyxis::MapTileStreamResult::STORAGE_UNAVAILABLE;
|
|
}
|
|
return Pyxis::MapTileStreamResult::IO_ERROR;
|
|
}
|
|
Pyxis::MapTileStreamResult read(std::uint8_t* output, std::size_t capacity,
|
|
std::size_t& count) override {
|
|
const Hardware::TDeck::MapTilePackResult result =
|
|
pack_.readGetChunk(output, capacity, count);
|
|
if (result == Hardware::TDeck::MapTilePackResult::OK) return Pyxis::MapTileStreamResult::OK;
|
|
if (result == Hardware::TDeck::MapTilePackResult::STORAGE_UNAVAILABLE) {
|
|
return Pyxis::MapTileStreamResult::STORAGE_UNAVAILABLE;
|
|
}
|
|
return Pyxis::MapTileStreamResult::IO_ERROR;
|
|
}
|
|
void end() override { pack_.endGet(); }
|
|
private:
|
|
Hardware::TDeck::MapTilePack& pack_;
|
|
};
|
|
|
|
class AtomicStopSource final : public Pyxis::MapTileStopSource {
|
|
public:
|
|
explicit AtomicStopSource(const std::atomic<bool>& stop) : stop_(stop) {}
|
|
bool stopRequested() const override {
|
|
return stop_.load(std::memory_order_acquire);
|
|
}
|
|
private:
|
|
const std::atomic<bool>& stop_;
|
|
};
|
|
|
|
lv_obj_t* createToolbarButton(lv_obj_t* parent, const char* text,
|
|
lv_event_cb_t callback, void* context,
|
|
lv_coord_t width, lv_obj_t** label_output = nullptr) {
|
|
lv_obj_t* button = lv_btn_create(parent);
|
|
lv_obj_set_size(button, width, 26);
|
|
lv_obj_set_style_pad_all(button, 0, 0);
|
|
lv_obj_set_style_bg_color(button, Theme::surfaceInput(), 0);
|
|
lv_obj_set_style_bg_color(button, Theme::primaryPressed(), LV_STATE_PRESSED);
|
|
lv_obj_add_event_cb(button, callback, LV_EVENT_CLICKED, context);
|
|
lv_obj_t* label = lv_label_create(button);
|
|
lv_label_set_text(label, text);
|
|
lv_obj_center(label);
|
|
if (label_output) *label_output = label;
|
|
return button;
|
|
}
|
|
|
|
const char* styleLabel(const char* style_id) {
|
|
if (style_id == nullptr) return "Style";
|
|
if (std::strcmp(style_id, "osm-bright") == 0) return "Bright";
|
|
if (std::strcmp(style_id, "dark-matter") == 0) return "Dark";
|
|
if (std::strcmp(style_id, "positron") == 0) return "Positron";
|
|
if (std::strcmp(style_id, "toner") == 0) return "Toner";
|
|
return "Style";
|
|
}
|
|
|
|
} // namespace
|
|
|
|
static_assert(sizeof(lv_color_t) == 2U,
|
|
"offline tile buffers assume LVGL RGB565 true color");
|
|
|
|
MapScreen::MapScreen(lv_obj_t* parent)
|
|
: screen_(nullptr), toolbar_(nullptr), viewport_(nullptr),
|
|
status_label_(nullptr), attribution_label_(nullptr), zoom_label_(nullptr),
|
|
zoom_out_button_(nullptr), zoom_in_button_(nullptr),
|
|
recenter_button_(nullptr), style_button_(nullptr), style_label_(nullptr),
|
|
pan_buttons_{}, tile_images_{},
|
|
tile_descriptors_{}, tile_pixels_{}, decoded_tile_cache_(TILE_PIXEL_COUNT),
|
|
decoded_cache_pixels_{}, approximation_halos_{}, markers_{}, marker_labels_{},
|
|
presenter_(), style_selector_(), storage_(), pack_(storage_),
|
|
style_catalog_(storage_), style_request_{}, style_request_pending_(false),
|
|
style_request_lifecycle_epoch_(0U),
|
|
pack_attribution_{},
|
|
screen_visible_(false), style_lifecycle_epoch_(0U),
|
|
style_lifecycle_exhausted_(false), pack_refresh_epoch_(0U),
|
|
compressed_staging_(nullptr),
|
|
state_mutex_(nullptr), worker_task_(nullptr),
|
|
worker_exited_(true), worker_started_(false),
|
|
requests_released_(false),
|
|
has_location_fix_(false), center_initialized_(false), current_location_{}, dragging_(false),
|
|
last_drag_point_{0, 0}, back_callback_() {
|
|
LVGL_LOCK();
|
|
state_mutex_ = xSemaphoreCreateMutex();
|
|
// Reserve the mandatory SD/PNG staging buffer before optional decoded
|
|
// cache entries. A partial cache must never prevent the worker starting.
|
|
compressed_staging_ = static_cast<std::uint8_t*>(heap_caps_malloc(
|
|
MAX_COMPRESSED_TILE_BYTES, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
|
|
|
|
screen_ = lv_obj_create(parent ? parent : lv_scr_act());
|
|
lv_obj_set_size(screen_, 320, 240);
|
|
lv_obj_set_style_pad_all(screen_, 0, 0);
|
|
lv_obj_set_style_border_width(screen_, 0, 0);
|
|
lv_obj_set_style_radius(screen_, 0, 0);
|
|
lv_obj_set_style_bg_color(screen_, Theme::surface(), 0);
|
|
lv_obj_clear_flag(screen_, LV_OBJ_FLAG_SCROLLABLE);
|
|
|
|
toolbar_ = lv_obj_create(screen_);
|
|
lv_obj_set_size(toolbar_, 320, 32);
|
|
lv_obj_align(toolbar_, LV_ALIGN_TOP_MID, 0, 0);
|
|
lv_obj_set_style_pad_all(toolbar_, 2, 0);
|
|
lv_obj_set_style_pad_gap(toolbar_, 3, 0);
|
|
lv_obj_set_style_border_width(toolbar_, 0, 0);
|
|
lv_obj_set_style_radius(toolbar_, 0, 0);
|
|
lv_obj_set_style_bg_color(toolbar_, Theme::surfaceHeader(), 0);
|
|
lv_obj_set_flex_flow(toolbar_, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(toolbar_, LV_FLEX_ALIGN_START,
|
|
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
|
|
back_button_ = createToolbarButton(toolbar_, LV_SYMBOL_LEFT, onBack, this, 36);
|
|
zoom_out_button_ = createToolbarButton(toolbar_, LV_SYMBOL_MINUS,
|
|
onZoomOut, this, 36);
|
|
zoom_label_ = lv_label_create(toolbar_);
|
|
lv_obj_set_width(zoom_label_, 62);
|
|
lv_obj_set_style_text_align(zoom_label_, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_label_set_text(zoom_label_, "z2");
|
|
zoom_in_button_ = createToolbarButton(toolbar_, LV_SYMBOL_PLUS,
|
|
onZoomIn, this, 36);
|
|
recenter_button_ = createToolbarButton(toolbar_, LV_SYMBOL_GPS,
|
|
onRecenter, this, 44);
|
|
style_button_ = createToolbarButton(toolbar_, "Style", onStyle, this, 80,
|
|
&style_label_);
|
|
lv_obj_add_state(style_button_, LV_STATE_DISABLED);
|
|
lv_obj_set_width(style_label_, 74);
|
|
lv_label_set_long_mode(style_label_, LV_LABEL_LONG_DOT);
|
|
lv_obj_set_style_text_align(style_label_, LV_TEXT_ALIGN_CENTER, 0);
|
|
|
|
viewport_ = lv_obj_create(screen_);
|
|
lv_obj_set_size(viewport_, Pyxis::MapScreenPresenter::VIEWPORT_WIDTH,
|
|
Pyxis::MapScreenPresenter::VIEWPORT_HEIGHT);
|
|
lv_obj_align(viewport_, LV_ALIGN_TOP_MID, 0, 32);
|
|
lv_obj_set_style_pad_all(viewport_, 0, 0);
|
|
lv_obj_set_style_border_width(viewport_, 0, 0);
|
|
lv_obj_set_style_radius(viewport_, 0, 0);
|
|
lv_obj_set_style_bg_color(viewport_, lv_color_hex(0x25282b), 0);
|
|
lv_obj_set_style_bg_opa(viewport_, LV_OPA_COVER, 0);
|
|
lv_obj_add_flag(viewport_, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_clear_flag(viewport_, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_add_event_cb(viewport_, onMapPressed, LV_EVENT_PRESSED, this);
|
|
lv_obj_add_event_cb(viewport_, onMapPressing, LV_EVENT_PRESSING, this);
|
|
lv_obj_add_event_cb(viewport_, onMapReleased, LV_EVENT_RELEASED, this);
|
|
lv_obj_add_event_cb(viewport_, onMapReleased, LV_EVENT_PRESS_LOST, this);
|
|
|
|
for (std::size_t index = 0; index < TILE_COUNT; ++index) {
|
|
tile_pixels_[index] = static_cast<lv_color_t*>(heap_caps_malloc(
|
|
TILE_PIXEL_COUNT * sizeof(lv_color_t),
|
|
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
|
|
if (tile_pixels_[index]) {
|
|
for (std::size_t pixel = 0; pixel < TILE_PIXEL_COUNT; ++pixel) {
|
|
const bool light = (((pixel % 256U) / 32U) +
|
|
((pixel / 256U) / 32U)) % 2U == 0U;
|
|
tile_pixels_[index][pixel] =
|
|
lv_color_hex(light ? 0x303438 : 0x272a2e);
|
|
}
|
|
}
|
|
lv_img_dsc_t& descriptor = tile_descriptors_[index];
|
|
std::memset(&descriptor, 0, sizeof(descriptor));
|
|
descriptor.header.always_zero = 0;
|
|
descriptor.header.w = 256;
|
|
descriptor.header.h = 256;
|
|
descriptor.header.cf = LV_IMG_CF_TRUE_COLOR;
|
|
descriptor.data_size = TILE_PIXEL_COUNT * sizeof(lv_color_t);
|
|
descriptor.data = reinterpret_cast<const std::uint8_t*>(
|
|
tile_pixels_[index]);
|
|
tile_images_[index] = lv_img_create(viewport_);
|
|
lv_obj_add_flag(tile_images_[index], LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
if (compressed_staging_) {
|
|
for (std::size_t index = 0U;
|
|
index < Pyxis::DecodedTileCache::CAPACITY; ++index) {
|
|
decoded_cache_pixels_[index] = static_cast<std::uint16_t*>(heap_caps_malloc(
|
|
TILE_PIXEL_COUNT * sizeof(std::uint16_t),
|
|
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
|
|
if (decoded_cache_pixels_[index]) {
|
|
(void)decoded_tile_cache_.attach(index, decoded_cache_pixels_[index]);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (std::size_t index = 0; index < MARKER_COUNT; ++index) {
|
|
approximation_halos_[index] = lv_obj_create(viewport_);
|
|
lv_obj_set_style_radius(approximation_halos_[index], LV_RADIUS_CIRCLE, 0);
|
|
lv_obj_set_style_bg_color(approximation_halos_[index], Theme::info(), 0);
|
|
lv_obj_set_style_bg_opa(approximation_halos_[index], LV_OPA_20, 0);
|
|
lv_obj_set_style_border_width(approximation_halos_[index], 1, 0);
|
|
lv_obj_set_style_border_color(approximation_halos_[index], Theme::info(), 0);
|
|
lv_obj_set_style_pad_all(approximation_halos_[index], 0, 0);
|
|
lv_obj_clear_flag(approximation_halos_[index], LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_add_flag(approximation_halos_[index], LV_OBJ_FLAG_HIDDEN);
|
|
markers_[index] = lv_obj_create(viewport_);
|
|
lv_obj_set_size(markers_[index], 10, 10);
|
|
lv_obj_set_style_radius(markers_[index], LV_RADIUS_CIRCLE, 0);
|
|
lv_obj_set_style_border_width(markers_[index], 2, 0);
|
|
lv_obj_set_style_border_color(markers_[index], lv_color_white(), 0);
|
|
lv_obj_set_style_pad_all(markers_[index], 0, 0);
|
|
lv_obj_clear_flag(markers_[index], LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_add_flag(markers_[index], LV_OBJ_FLAG_HIDDEN);
|
|
marker_labels_[index] = lv_label_create(viewport_);
|
|
lv_label_set_text(marker_labels_[index], "");
|
|
lv_obj_set_style_text_font(marker_labels_[index],
|
|
&lv_font_montserrat_12, 0);
|
|
lv_obj_add_flag(marker_labels_[index], LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
|
|
static const char* pan_symbols[4] = {
|
|
LV_SYMBOL_UP, LV_SYMBOL_DOWN, LV_SYMBOL_LEFT, LV_SYMBOL_RIGHT};
|
|
static const lv_align_t pan_alignments[4] = {
|
|
LV_ALIGN_TOP_MID, LV_ALIGN_BOTTOM_MID,
|
|
LV_ALIGN_LEFT_MID, LV_ALIGN_RIGHT_MID};
|
|
static const lv_coord_t pan_x[4] = {0, 0, 2, -2};
|
|
static const lv_coord_t pan_y[4] = {2, -2, 0, 0};
|
|
for (std::size_t index = 0; index < 4U; ++index) {
|
|
pan_buttons_[index] = createToolbarButton(
|
|
viewport_, pan_symbols[index], onPan, this, 30);
|
|
lv_obj_set_user_data(pan_buttons_[index],
|
|
reinterpret_cast<void*>(index));
|
|
lv_obj_align(pan_buttons_[index], pan_alignments[index],
|
|
pan_x[index], pan_y[index]);
|
|
lv_obj_set_style_bg_opa(pan_buttons_[index], LV_OPA_60, 0);
|
|
}
|
|
|
|
status_label_ = lv_label_create(viewport_);
|
|
lv_label_set_text(status_label_, "Offline tiles");
|
|
lv_obj_set_style_text_color(status_label_, Theme::textTertiary(), 0);
|
|
lv_obj_set_style_bg_color(status_label_, Theme::surfaceElevated(), 0);
|
|
lv_obj_set_style_bg_opa(status_label_, LV_OPA_70, 0);
|
|
lv_obj_set_style_pad_all(status_label_, 3, 0);
|
|
lv_obj_align(status_label_, LV_ALIGN_BOTTOM_LEFT, 4, -4);
|
|
|
|
attribution_label_ = lv_label_create(viewport_);
|
|
lv_label_set_text(attribution_label_, "No active map pack");
|
|
lv_obj_set_style_text_color(attribution_label_, Theme::textTertiary(), 0);
|
|
lv_obj_set_style_text_font(attribution_label_, &lv_font_montserrat_12, 0);
|
|
lv_obj_set_style_bg_color(attribution_label_, Theme::surfaceElevated(), 0);
|
|
lv_obj_set_style_bg_opa(attribution_label_, LV_OPA_70, 0);
|
|
lv_obj_set_style_pad_all(attribution_label_, 2, 0);
|
|
lv_obj_align(attribution_label_, LV_ALIGN_BOTTOM_RIGHT, -3, -3);
|
|
|
|
lv_obj_add_flag(screen_, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
|
|
MapScreen::~MapScreen() {
|
|
stopWorker();
|
|
LVGL_LOCK();
|
|
if (screen_) lv_obj_del(screen_);
|
|
screen_ = nullptr;
|
|
for (std::size_t index = 0; index < TILE_COUNT; ++index) {
|
|
if (tile_pixels_[index]) heap_caps_free(tile_pixels_[index]);
|
|
tile_pixels_[index] = nullptr;
|
|
}
|
|
for (std::size_t index = 0U; index < Pyxis::DecodedTileCache::CAPACITY; ++index) {
|
|
if (decoded_cache_pixels_[index]) heap_caps_free(decoded_cache_pixels_[index]);
|
|
decoded_cache_pixels_[index] = nullptr;
|
|
}
|
|
if (compressed_staging_) heap_caps_free(compressed_staging_);
|
|
compressed_staging_ = nullptr;
|
|
if (state_mutex_) vSemaphoreDelete(state_mutex_);
|
|
state_mutex_ = nullptr;
|
|
}
|
|
|
|
bool MapScreen::lockState(TickType_t ticks) {
|
|
return state_mutex_ && xSemaphoreTake(state_mutex_, ticks) == pdTRUE;
|
|
}
|
|
|
|
void MapScreen::unlockState() {
|
|
xSemaphoreGive(state_mutex_);
|
|
}
|
|
|
|
bool MapScreen::startWorker() {
|
|
if (worker_started_) return true;
|
|
if (!state_mutex_ || !compressed_staging_) return false;
|
|
stop_requested_.store(false, std::memory_order_release);
|
|
worker_exited_.store(false, std::memory_order_release);
|
|
const BaseType_t created = xTaskCreatePinnedToCore(
|
|
workerEntry, "map-tile-worker", 16384, this, 1, &worker_task_, 0);
|
|
if (created != pdPASS) {
|
|
worker_exited_.store(true, std::memory_order_release);
|
|
worker_task_ = nullptr;
|
|
return false;
|
|
}
|
|
worker_started_ = true;
|
|
return true;
|
|
}
|
|
|
|
void MapScreen::stopWorker() {
|
|
if (!worker_started_) return;
|
|
stop_requested_.store(true, std::memory_order_release);
|
|
if (worker_task_) xTaskNotifyGive(worker_task_);
|
|
const TickType_t started = xTaskGetTickCount();
|
|
const TickType_t shutdown_limit = pdMS_TO_TICKS(20000);
|
|
while (!worker_exited_.load(std::memory_order_acquire)) {
|
|
if ((xTaskGetTickCount() - started) >= shutdown_limit) {
|
|
// Do not destroy storage or decoder state while the worker
|
|
// may still own it. A controlled restart is safer than UAF or an
|
|
// unbounded UI teardown after a wedged hardware operation.
|
|
esp_restart();
|
|
for (;;) vTaskDelay(pdMS_TO_TICKS(1000));
|
|
}
|
|
vTaskDelay(pdMS_TO_TICKS(1));
|
|
}
|
|
worker_task_ = nullptr;
|
|
worker_started_ = false;
|
|
}
|
|
|
|
void MapScreen::workerEntry(void* context) {
|
|
static_cast<MapScreen*>(context)->workerLoop();
|
|
}
|
|
|
|
void MapScreen::publishPackAttribution() {
|
|
const char* attribution = pack_.hasSelection()
|
|
? pack_.metadata().attribution : "No active map pack";
|
|
if (!lockState(portMAX_DELAY)) return;
|
|
std::strncpy(pack_attribution_, attribution, sizeof(pack_attribution_) - 1U);
|
|
pack_attribution_[sizeof(pack_attribution_) - 1U] = '\0';
|
|
unlockState();
|
|
}
|
|
|
|
void MapScreen::publishStyleCatalog(std::uint32_t activation_token,
|
|
std::uint32_t request_lifecycle_epoch,
|
|
bool activation_failed) {
|
|
Pyxis::MapStyleSummary summaries[Pyxis::MapStyleSelector::MAX_STYLES] = {};
|
|
char active_id[Pyxis::MapPackManifest::PACK_ID_CAPACITY] = {};
|
|
const std::size_t count = style_catalog_.count();
|
|
for (std::size_t index = 0U; index < count; ++index) {
|
|
const Hardware::TDeck::MapStyleSummary* source = style_catalog_.style(index);
|
|
if (source == nullptr) continue;
|
|
std::strncpy(summaries[index].id, source->id,
|
|
sizeof(summaries[index].id) - 1U);
|
|
std::strncpy(summaries[index].label, styleLabel(source->id),
|
|
sizeof(summaries[index].label) - 1U);
|
|
if (source->active) {
|
|
std::strncpy(active_id, source->id, sizeof(active_id) - 1U);
|
|
}
|
|
}
|
|
if (!lockState(portMAX_DELAY)) return;
|
|
const bool retain_error = activation_failed &&
|
|
screen_visible_.load(std::memory_order_acquire) &&
|
|
!style_lifecycle_exhausted_ &&
|
|
request_lifecycle_epoch == style_lifecycle_epoch_;
|
|
if (activation_token == 0U) {
|
|
(void)style_selector_.setCatalog(style_catalog_.generation(), summaries, count,
|
|
active_id[0] == '\0' ? nullptr : active_id);
|
|
} else {
|
|
(void)style_selector_.reconcileActivation(
|
|
activation_token, style_catalog_.generation(), summaries, count,
|
|
active_id[0] == '\0' ? nullptr : active_id, retain_error);
|
|
}
|
|
unlockState();
|
|
}
|
|
|
|
bool MapScreen::beginStyleActivationCommit(void* raw_context) {
|
|
StyleActivationCommitContext* context =
|
|
static_cast<StyleActivationCommitContext*>(raw_context);
|
|
if (context == NULL || context->screen == NULL) return false;
|
|
MapScreen* screen = context->screen;
|
|
if (!screen->lockState(portMAX_DELAY)) return false;
|
|
const bool admitted =
|
|
screen->screen_visible_.load(std::memory_order_acquire) &&
|
|
!screen->style_lifecycle_exhausted_ &&
|
|
context->lifecycle_epoch == screen->style_lifecycle_epoch_ &&
|
|
screen->style_selector_.activationOwnedBy(context->token);
|
|
// This is the lifecycle linearization point. Release before commitWrite()
|
|
// so LVGL navigation never waits on SD I/O. A hide that wins this lock
|
|
// rejects publication; a commit admitted first represents visible intent.
|
|
screen->unlockState();
|
|
return admitted;
|
|
}
|
|
|
|
void MapScreen::workerLoop() {
|
|
std::uint32_t handled_pack_refresh_epoch =
|
|
pack_refresh_epoch_.load(std::memory_order_acquire);
|
|
(void)pack_.initialize();
|
|
publishPackAttribution();
|
|
if (style_catalog_.discover() == Hardware::TDeck::MapStyleCatalogResult::OK) {
|
|
publishStyleCatalog();
|
|
}
|
|
while (!stop_requested_.load(std::memory_order_acquire)) {
|
|
const bool screen_visible =
|
|
screen_visible_.load(std::memory_order_acquire);
|
|
const std::uint32_t pack_refresh_epoch =
|
|
pack_refresh_epoch_.load(std::memory_order_acquire);
|
|
if (pack_refresh_epoch != handled_pack_refresh_epoch) {
|
|
const bool had_selection = pack_.hasSelection();
|
|
const std::uint32_t previous_generation = pack_.selectionGeneration();
|
|
char previous_pack_id[Pyxis::MapPackManifest::PACK_ID_CAPACITY] = {};
|
|
if (had_selection) {
|
|
std::memcpy(previous_pack_id, pack_.metadata().pack_id,
|
|
sizeof(previous_pack_id));
|
|
}
|
|
(void)pack_.initialize();
|
|
const bool has_selection = pack_.hasSelection();
|
|
if (had_selection != has_selection ||
|
|
(had_selection && has_selection &&
|
|
(std::strcmp(previous_pack_id, pack_.metadata().pack_id) != 0 ||
|
|
previous_generation != pack_.selectionGeneration()))) {
|
|
decoded_tile_cache_.clear();
|
|
}
|
|
publishPackAttribution();
|
|
if (style_catalog_.discover() == Hardware::TDeck::MapStyleCatalogResult::OK) {
|
|
publishStyleCatalog();
|
|
}
|
|
handled_pack_refresh_epoch = pack_refresh_epoch;
|
|
}
|
|
|
|
Pyxis::MapStyleRequest style_request{};
|
|
std::uint32_t style_request_lifecycle_epoch = 0U;
|
|
bool have_style_request = false;
|
|
if (lockState(pdMS_TO_TICKS(20))) {
|
|
if (style_request_pending_) {
|
|
const bool style_screen_visible =
|
|
screen_visible_.load(std::memory_order_acquire);
|
|
if (!style_screen_visible || style_lifecycle_exhausted_ ||
|
|
style_request_lifecycle_epoch_ != style_lifecycle_epoch_) {
|
|
(void)style_selector_.cancelPending(style_request_.token);
|
|
style_request_pending_ = false;
|
|
style_request_ = Pyxis::MapStyleRequest();
|
|
} else if (style_selector_.activationOwnedBy(style_request_.token)) {
|
|
style_request = style_request_;
|
|
style_request_lifecycle_epoch = style_request_lifecycle_epoch_;
|
|
style_request_pending_ = false;
|
|
style_request_ = Pyxis::MapStyleRequest();
|
|
have_style_request = true;
|
|
} else {
|
|
style_request_pending_ = false;
|
|
style_request_ = Pyxis::MapStyleRequest();
|
|
}
|
|
}
|
|
unlockState();
|
|
}
|
|
if (have_style_request) {
|
|
bool activation_admitted = false;
|
|
if (lockState(portMAX_DELAY)) {
|
|
activation_admitted =
|
|
screen_visible_.load(std::memory_order_acquire) &&
|
|
!style_lifecycle_exhausted_ &&
|
|
style_request_lifecycle_epoch == style_lifecycle_epoch_ &&
|
|
style_selector_.activationOwnedBy(style_request.token);
|
|
if (!activation_admitted) {
|
|
(void)style_selector_.cancelPending(style_request.token);
|
|
}
|
|
unlockState();
|
|
}
|
|
if (!activation_admitted) continue;
|
|
StyleActivationCommitContext commit_context = {
|
|
this, style_request.token, style_request_lifecycle_epoch};
|
|
const Hardware::TDeck::MapStyleCatalogResult activation =
|
|
style_catalog_.activate(style_request.catalog_generation,
|
|
style_request.style_id,
|
|
&MapScreen::beginStyleActivationCommit,
|
|
&commit_context);
|
|
if (activation == Hardware::TDeck::MapStyleCatalogResult::CANCELLED) {
|
|
if (lockState(portMAX_DELAY)) {
|
|
(void)style_selector_.cancelPending(style_request.token);
|
|
unlockState();
|
|
}
|
|
continue;
|
|
}
|
|
bool success = activation == Hardware::TDeck::MapStyleCatalogResult::OK;
|
|
if (success) {
|
|
const Hardware::TDeck::MapTilePackResult initialized = pack_.initialize();
|
|
success = initialized == Hardware::TDeck::MapTilePackResult::OK &&
|
|
pack_.hasSelection() &&
|
|
std::strcmp(pack_.metadata().pack_id, style_request.style_id) == 0;
|
|
}
|
|
if (success) {
|
|
decoded_tile_cache_.clear();
|
|
publishPackAttribution();
|
|
}
|
|
Pyxis::MapStyleCompletion style_completion{};
|
|
style_completion.token = style_request.token;
|
|
style_completion.catalog_generation = style_request.catalog_generation;
|
|
std::strncpy(style_completion.style_id, style_request.style_id,
|
|
sizeof(style_completion.style_id) - 1U);
|
|
style_completion.success = success;
|
|
if (lockState(portMAX_DELAY)) {
|
|
const bool same_visible_lifecycle =
|
|
screen_visible_.load(std::memory_order_acquire) &&
|
|
!style_lifecycle_exhausted_ &&
|
|
style_request_lifecycle_epoch == style_lifecycle_epoch_;
|
|
const bool completed = style_selector_.complete(style_completion);
|
|
if (completed && !same_visible_lifecycle && !success) {
|
|
(void)style_selector_.clearError();
|
|
}
|
|
if (success) {
|
|
presenter_.invalidateTiles();
|
|
requests_released_ = false;
|
|
}
|
|
unlockState();
|
|
}
|
|
// Reconcile the reservation owner's catalog on every outcome. A
|
|
// durable activate() may have advanced its generation even when the
|
|
// subsequent pack reload failed; stale-generation failures can also
|
|
// follow a refresh that raced the queued request. Keep the lease until
|
|
// both the in-memory truth and any successful rediscovery are published.
|
|
publishStyleCatalog(style_request.token,
|
|
style_request_lifecycle_epoch, !success);
|
|
if (style_catalog_.discover() == Hardware::TDeck::MapStyleCatalogResult::OK) {
|
|
publishStyleCatalog(style_request.token,
|
|
style_request_lifecycle_epoch, !success);
|
|
}
|
|
if (lockState(portMAX_DELAY)) {
|
|
(void)style_selector_.releaseActivation(style_request.token);
|
|
unlockState();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
Pyxis::MapTileRequest request{};
|
|
bool have_request = false;
|
|
if (lockState(pdMS_TO_TICKS(20))) {
|
|
if (requests_released_ && screen_visible) {
|
|
have_request = presenter_.takeRequest(request);
|
|
}
|
|
unlockState();
|
|
}
|
|
if (!have_request) {
|
|
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(100));
|
|
continue;
|
|
}
|
|
Pyxis::MapTileCompletion completion{};
|
|
completion.generation = request.generation;
|
|
completion.frame_epoch = request.frame_epoch;
|
|
completion.slot_token = request.slot_token;
|
|
completion.slot_index = request.slot_index;
|
|
completion.key = request.key;
|
|
completion.result = loadTile(request);
|
|
if (lockState(portMAX_DELAY)) {
|
|
(void)presenter_.publishCompletion(completion);
|
|
unlockState();
|
|
}
|
|
}
|
|
worker_exited_.store(true, std::memory_order_release);
|
|
vTaskDelete(nullptr);
|
|
}
|
|
|
|
Pyxis::MapTileLoadResult MapScreen::loadTile(
|
|
const Pyxis::MapTileRequest& request) {
|
|
return readTile(request);
|
|
}
|
|
|
|
Pyxis::MapTileLoadResult MapScreen::readTile(
|
|
const Pyxis::MapTileRequest& request) {
|
|
if (request.slot_index < TILE_COUNT && tile_pixels_[request.slot_index] &&
|
|
decoded_tile_cache_.get(
|
|
request.key,
|
|
reinterpret_cast<std::uint16_t*>(tile_pixels_[request.slot_index]),
|
|
TILE_PIXEL_COUNT)) {
|
|
return Pyxis::MapTileLoadResult::READY;
|
|
}
|
|
|
|
return readCompressedTile(request);
|
|
}
|
|
|
|
Pyxis::MapTileLoadResult MapScreen::readCompressedTile(
|
|
const Pyxis::MapTileRequest& request) {
|
|
PackReadStream pack_stream(pack_);
|
|
AtomicStopSource stop(stop_requested_);
|
|
std::size_t total = 0U;
|
|
const Pyxis::MapTileStreamResult read = Pyxis::MapTileStreamReader::readExact(
|
|
pack_stream, stop, request.key, compressed_staging_, MAX_COMPRESSED_TILE_BYTES,
|
|
READ_CHUNK_BYTES, total);
|
|
if (read == Pyxis::MapTileStreamResult::MISS) {
|
|
return Pyxis::MapTileLoadResult::MISS;
|
|
}
|
|
if (read == Pyxis::MapTileStreamResult::STORAGE_UNAVAILABLE) {
|
|
return Pyxis::MapTileLoadResult::STORAGE_UNAVAILABLE;
|
|
}
|
|
if (read == Pyxis::MapTileStreamResult::TOO_LARGE) {
|
|
return Pyxis::MapTileLoadResult::TOO_LARGE;
|
|
}
|
|
if (read != Pyxis::MapTileStreamResult::OK) {
|
|
return Pyxis::MapTileLoadResult::IO_ERROR;
|
|
}
|
|
|
|
unsigned char* rgb = nullptr;
|
|
unsigned width = 0U;
|
|
unsigned height = 0U;
|
|
LodePNGState decode_state;
|
|
lodepng_state_init(&decode_state);
|
|
// Bound the largest legal 256x256 source scanline stream (RGBA16 plus
|
|
// filter bytes/interlace overhead) independently of compressed size.
|
|
decode_state.decoder.zlibsettings.max_output_size = 528U * 1024U;
|
|
unsigned decode_error = lodepng_inspect(
|
|
&width, &height, &decode_state, compressed_staging_, total);
|
|
if (decode_error != 0U || width != 256U || height != 256U) {
|
|
lodepng_state_cleanup(&decode_state);
|
|
return Pyxis::MapTileLoadResult::INVALID_PNG;
|
|
}
|
|
decode_state.info_raw.colortype = LCT_RGB;
|
|
decode_state.info_raw.bitdepth = 8U;
|
|
decode_error = lodepng_decode(
|
|
&rgb, &width, &height, &decode_state, compressed_staging_, total);
|
|
lodepng_state_cleanup(&decode_state);
|
|
if (decode_error != 0U || rgb == nullptr || width != 256U || height != 256U) {
|
|
if (rgb) lv_mem_free(rgb);
|
|
return Pyxis::MapTileLoadResult::INVALID_PNG;
|
|
}
|
|
if (request.slot_index >= TILE_COUNT || !tile_pixels_[request.slot_index]) {
|
|
lv_mem_free(rgb);
|
|
return Pyxis::MapTileLoadResult::IO_ERROR;
|
|
}
|
|
lv_color_t* pixels = tile_pixels_[request.slot_index];
|
|
for (std::size_t index = 0; index < TILE_PIXEL_COUNT; ++index) {
|
|
const std::size_t source = index * 3U;
|
|
pixels[index] = lv_color_make(rgb[source], rgb[source + 1U],
|
|
rgb[source + 2U]);
|
|
}
|
|
lv_mem_free(rgb);
|
|
(void)decoded_tile_cache_.put(
|
|
request.key, reinterpret_cast<const std::uint16_t*>(pixels),
|
|
TILE_PIXEL_COUNT);
|
|
return Pyxis::MapTileLoadResult::READY;
|
|
}
|
|
|
|
void MapScreen::serviceIo() {
|
|
if (!worker_started_ && !startWorker()) return;
|
|
if (worker_task_) xTaskNotifyGive(worker_task_);
|
|
}
|
|
|
|
void MapScreen::updateModel(const Pyxis::MapView::Request& request) {
|
|
if (!lockState(pdMS_TO_TICKS(100))) return;
|
|
has_location_fix_ = request.has_local_location;
|
|
current_location_ = request.local_location;
|
|
if (!center_initialized_ && request.has_local_location &&
|
|
presenter_.recenter(true, request.local_location)) {
|
|
center_initialized_ = true;
|
|
}
|
|
if (!presenter_.frameBuiltForCurrentEpoch()) {
|
|
requests_released_ = false;
|
|
}
|
|
(void)presenter_.buildFrame(request);
|
|
unlockState();
|
|
}
|
|
|
|
void MapScreen::setPlaceholder(std::size_t index) {
|
|
lv_obj_add_flag(tile_images_[index], LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
|
|
void MapScreen::applyFrame() {
|
|
if (!lockState(pdMS_TO_TICKS(100))) return;
|
|
const Pyxis::MapView::Frame& frame = presenter_.frame();
|
|
for (std::size_t index = 0; index < TILE_COUNT; ++index) {
|
|
const Pyxis::MapTileSlot& slot = presenter_.slot(index);
|
|
if (slot.state == Pyxis::MapTileSlot::READY && tile_pixels_[index]) {
|
|
lv_img_set_src(tile_images_[index], &tile_descriptors_[index]);
|
|
lv_obj_set_pos(tile_images_[index],
|
|
static_cast<lv_coord_t>(std::lround(slot.screen_x)),
|
|
static_cast<lv_coord_t>(std::lround(slot.screen_y)));
|
|
lv_obj_clear_flag(tile_images_[index], LV_OBJ_FLAG_HIDDEN);
|
|
} else {
|
|
setPlaceholder(index);
|
|
}
|
|
}
|
|
for (std::size_t index = 0; index < MARKER_COUNT; ++index) {
|
|
if (index >= frame.marker_count) {
|
|
lv_obj_add_flag(approximation_halos_[index], LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_add_flag(markers_[index], LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_add_flag(marker_labels_[index], LV_OBJ_FLAG_HIDDEN);
|
|
continue;
|
|
}
|
|
const Pyxis::MapView::Marker& marker = frame.markers[index];
|
|
const lv_coord_t x = static_cast<lv_coord_t>(std::lround(marker.screen_x));
|
|
const lv_coord_t y = static_cast<lv_coord_t>(std::lround(marker.screen_y));
|
|
if (marker.has_approx_radius && marker.approx_radius_pixels > 0.0) {
|
|
const double bounded_radius = marker.approx_radius_pixels > 320.0
|
|
? 320.0 : (marker.approx_radius_pixels < 2.0
|
|
? 2.0 : marker.approx_radius_pixels);
|
|
const lv_coord_t radius = static_cast<lv_coord_t>(std::lround(bounded_radius));
|
|
lv_obj_set_size(approximation_halos_[index], radius * 2, radius * 2);
|
|
lv_obj_set_pos(approximation_halos_[index], x - radius, y - radius);
|
|
lv_obj_clear_flag(approximation_halos_[index], LV_OBJ_FLAG_HIDDEN);
|
|
} else {
|
|
lv_obj_add_flag(approximation_halos_[index], LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
lv_obj_set_pos(markers_[index], x - 5, y - 5);
|
|
lv_obj_set_style_bg_color(
|
|
markers_[index],
|
|
marker.kind == Pyxis::MapView::MarkerKind::LOCAL
|
|
? Theme::info() : Theme::success(), 0);
|
|
lv_obj_clear_flag(markers_[index], LV_OBJ_FLAG_HIDDEN);
|
|
char label[8] = {};
|
|
if (marker.kind == Pyxis::MapView::MarkerKind::LOCAL) {
|
|
std::snprintf(label, sizeof(label), "me");
|
|
} else {
|
|
std::snprintf(label, sizeof(label), "%02x%02x",
|
|
marker.peer.bytes[Telemetry::PEER_ID_SIZE - 2U],
|
|
marker.peer.bytes[Telemetry::PEER_ID_SIZE - 1U]);
|
|
}
|
|
lv_label_set_text(marker_labels_[index], label);
|
|
lv_obj_set_pos(marker_labels_[index], x + 6, y - 7);
|
|
lv_obj_clear_flag(marker_labels_[index], LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
char zoom_text[12] = {};
|
|
std::snprintf(zoom_text, sizeof(zoom_text), "z%lu",
|
|
static_cast<unsigned long>(presenter_.zoom()));
|
|
lv_label_set_text(zoom_label_, zoom_text);
|
|
if (style_selector_.state() == Pyxis::MapStyleSelector::State::APPLYING) {
|
|
lv_label_set_text(style_label_, "Switching");
|
|
} else {
|
|
lv_label_set_text(style_label_, style_selector_.activeLabel());
|
|
}
|
|
if (style_selector_.canCycle()) {
|
|
lv_obj_clear_state(style_button_, LV_STATE_DISABLED);
|
|
} else {
|
|
lv_obj_add_state(style_button_, LV_STATE_DISABLED);
|
|
}
|
|
lv_label_set_text(attribution_label_, pack_attribution_[0] != '\0'
|
|
? pack_attribution_ : "No active map pack");
|
|
refreshVisibleStatus();
|
|
if (style_selector_.state() == Pyxis::MapStyleSelector::State::APPLYING) {
|
|
lv_label_set_text(status_label_, "Switching style...");
|
|
} else if (style_selector_.state() == Pyxis::MapStyleSelector::State::ERROR) {
|
|
lv_label_set_text(status_label_, "Style switch failed");
|
|
}
|
|
// Non-ready images are now detached under LVGL_LOCK, so the worker may
|
|
// safely decode into their permanent buffers without a draw race.
|
|
requests_released_ = true;
|
|
unlockState();
|
|
if (worker_task_) xTaskNotifyGive(worker_task_);
|
|
}
|
|
|
|
void MapScreen::setStatusFor(Pyxis::MapTileLoadResult result) {
|
|
switch (result) {
|
|
case Pyxis::MapTileLoadResult::READY:
|
|
lv_label_set_text(status_label_, "Tile ready");
|
|
break;
|
|
case Pyxis::MapTileLoadResult::MISS:
|
|
lv_label_set_text(status_label_, "Tile unavailable");
|
|
break;
|
|
case Pyxis::MapTileLoadResult::STORAGE_UNAVAILABLE:
|
|
lv_label_set_text(status_label_, "SD unavailable");
|
|
break;
|
|
case Pyxis::MapTileLoadResult::INVALID_PNG:
|
|
lv_label_set_text(status_label_, "Bad tile");
|
|
break;
|
|
case Pyxis::MapTileLoadResult::TOO_LARGE:
|
|
lv_label_set_text(status_label_, "Tile too large");
|
|
break;
|
|
case Pyxis::MapTileLoadResult::DOWNLOAD_FAILED:
|
|
lv_label_set_text(status_label_, "Tile unavailable");
|
|
break;
|
|
case Pyxis::MapTileLoadResult::IO_ERROR:
|
|
lv_label_set_text(status_label_, "Tile I/O error");
|
|
break;
|
|
}
|
|
}
|
|
|
|
void MapScreen::refreshVisibleStatus() {
|
|
Pyxis::MapTileLoadResult visible_status{};
|
|
if (presenter_.visibleTileStatus(visible_status)) {
|
|
setStatusFor(visible_status);
|
|
} else {
|
|
lv_label_set_text(status_label_, "Loading tiles...");
|
|
}
|
|
}
|
|
|
|
bool MapScreen::applyOneCompletion() {
|
|
if (!lockState(pdMS_TO_TICKS(100))) return false;
|
|
Pyxis::MapTileCompletion completion{};
|
|
const bool applied = presenter_.takeApplicableCompletion(completion);
|
|
if (applied) {
|
|
refreshVisibleStatus();
|
|
if (completion.result == Pyxis::MapTileLoadResult::READY &&
|
|
completion.slot_index < TILE_COUNT) {
|
|
const std::size_t index = completion.slot_index;
|
|
lv_img_set_src(tile_images_[index], &tile_descriptors_[index]);
|
|
const Pyxis::MapTileSlot& slot = presenter_.slot(index);
|
|
lv_obj_set_pos(tile_images_[index],
|
|
static_cast<lv_coord_t>(std::lround(slot.screen_x)),
|
|
static_cast<lv_coord_t>(std::lround(slot.screen_y)));
|
|
lv_obj_clear_flag(tile_images_[index], LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
}
|
|
unlockState();
|
|
return applied;
|
|
}
|
|
|
|
void MapScreen::show() {
|
|
pack_refresh_epoch_.fetch_add(1U, std::memory_order_acq_rel);
|
|
screen_visible_.store(true, std::memory_order_release);
|
|
if (worker_task_) xTaskNotifyGive(worker_task_);
|
|
if (lockState(pdMS_TO_TICKS(100))) {
|
|
presenter_.show();
|
|
unlockState();
|
|
}
|
|
lv_obj_clear_flag(screen_, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_move_foreground(screen_);
|
|
lv_group_t* group = LVGL::LVGLInit::get_default_group();
|
|
if (group) {
|
|
lv_group_add_obj(group, back_button_);
|
|
lv_group_add_obj(group, zoom_out_button_);
|
|
lv_group_add_obj(group, zoom_in_button_);
|
|
lv_group_add_obj(group, recenter_button_);
|
|
lv_group_add_obj(group, style_button_);
|
|
for (std::size_t i = 0; i < 4U; ++i) lv_group_add_obj(group, pan_buttons_[i]);
|
|
lv_group_focus_obj(back_button_);
|
|
}
|
|
}
|
|
|
|
void MapScreen::hide() {
|
|
if (lockState(portMAX_DELAY)) {
|
|
screen_visible_.store(false, std::memory_order_release);
|
|
if (style_lifecycle_epoch_ == UINT32_MAX) {
|
|
style_lifecycle_exhausted_ = true;
|
|
} else {
|
|
++style_lifecycle_epoch_;
|
|
}
|
|
if (style_request_pending_) {
|
|
(void)style_selector_.cancelPending(style_request_.token);
|
|
style_request_pending_ = false;
|
|
style_request_ = Pyxis::MapStyleRequest();
|
|
}
|
|
(void)style_selector_.clearError();
|
|
presenter_.hide();
|
|
unlockState();
|
|
}
|
|
if (worker_task_) xTaskNotifyGive(worker_task_);
|
|
lv_group_t* group = LVGL::LVGLInit::get_default_group();
|
|
if (group) {
|
|
lv_group_remove_obj(back_button_);
|
|
lv_group_remove_obj(zoom_out_button_);
|
|
lv_group_remove_obj(zoom_in_button_);
|
|
lv_group_remove_obj(recenter_button_);
|
|
lv_group_remove_obj(style_button_);
|
|
for (std::size_t i = 0; i < 4U; ++i) lv_group_remove_obj(pan_buttons_[i]);
|
|
}
|
|
lv_obj_add_flag(screen_, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
|
|
MapScreen* MapScreen::fromEvent(lv_event_t* event) {
|
|
return static_cast<MapScreen*>(lv_event_get_user_data(event));
|
|
}
|
|
|
|
void MapScreen::pan(double dx, double dy) {
|
|
if (!lockState(pdMS_TO_TICKS(100))) return;
|
|
center_initialized_ = true;
|
|
(void)presenter_.panPixels(dx, dy);
|
|
unlockState();
|
|
}
|
|
|
|
void MapScreen::onBack(lv_event_t* event) {
|
|
MapScreen* screen = fromEvent(event);
|
|
if (screen && screen->back_callback_) screen->back_callback_();
|
|
}
|
|
|
|
void MapScreen::onZoomIn(lv_event_t* event) {
|
|
MapScreen* screen = fromEvent(event);
|
|
if (screen && screen->lockState(pdMS_TO_TICKS(100))) {
|
|
screen->center_initialized_ = true;
|
|
(void)screen->presenter_.zoomBy(1);
|
|
screen->unlockState();
|
|
}
|
|
}
|
|
|
|
void MapScreen::onZoomOut(lv_event_t* event) {
|
|
MapScreen* screen = fromEvent(event);
|
|
if (screen && screen->lockState(pdMS_TO_TICKS(100))) {
|
|
screen->center_initialized_ = true;
|
|
(void)screen->presenter_.zoomBy(-1);
|
|
screen->unlockState();
|
|
}
|
|
}
|
|
|
|
void MapScreen::onRecenter(lv_event_t* event) {
|
|
MapScreen* screen = fromEvent(event);
|
|
if (!screen || !screen->lockState(pdMS_TO_TICKS(100))) return;
|
|
const bool centered = screen->presenter_.recenter(
|
|
screen->has_location_fix_, screen->current_location_);
|
|
if (centered) screen->center_initialized_ = true;
|
|
screen->unlockState();
|
|
if (!centered) lv_label_set_text(screen->status_label_, "No GPS fix");
|
|
}
|
|
|
|
void MapScreen::onStyle(lv_event_t* event) {
|
|
MapScreen* screen = fromEvent(event);
|
|
bool queued = false;
|
|
if (screen && screen->lockState(pdMS_TO_TICKS(100))) {
|
|
if (!screen->style_lifecycle_exhausted_ &&
|
|
screen->style_selector_.requestNext(screen->style_request_)) {
|
|
screen->style_request_lifecycle_epoch_ =
|
|
screen->style_lifecycle_epoch_;
|
|
screen->style_request_pending_ = true;
|
|
queued = true;
|
|
}
|
|
screen->unlockState();
|
|
}
|
|
if (queued && screen->worker_task_) xTaskNotifyGive(screen->worker_task_);
|
|
}
|
|
|
|
void MapScreen::onPan(lv_event_t* event) {
|
|
MapScreen* screen = fromEvent(event);
|
|
lv_obj_t* target = lv_event_get_current_target(event);
|
|
if (!screen || !target) return;
|
|
const std::size_t direction = reinterpret_cast<std::size_t>(
|
|
lv_obj_get_user_data(target));
|
|
switch (direction) {
|
|
case 0U: screen->pan(0.0, -64.0); break;
|
|
case 1U: screen->pan(0.0, 64.0); break;
|
|
case 2U: screen->pan(-64.0, 0.0); break;
|
|
case 3U: screen->pan(64.0, 0.0); break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
void MapScreen::onMapPressed(lv_event_t* event) {
|
|
MapScreen* screen = fromEvent(event);
|
|
lv_indev_t* input = lv_indev_get_act();
|
|
if (!screen || !input) return;
|
|
screen->dragging_ = true;
|
|
lv_indev_get_point(input, &screen->last_drag_point_);
|
|
}
|
|
|
|
void MapScreen::onMapPressing(lv_event_t* event) {
|
|
MapScreen* screen = fromEvent(event);
|
|
lv_indev_t* input = lv_indev_get_act();
|
|
if (!screen || !input || !screen->dragging_) return;
|
|
lv_point_t point{};
|
|
lv_indev_get_point(input, &point);
|
|
const double dx = static_cast<double>(screen->last_drag_point_.x - point.x);
|
|
const double dy = static_cast<double>(screen->last_drag_point_.y - point.y);
|
|
screen->last_drag_point_ = point;
|
|
if (dx != 0.0 || dy != 0.0) screen->pan(dx, dy);
|
|
}
|
|
|
|
void MapScreen::onMapReleased(lv_event_t* event) {
|
|
MapScreen* screen = fromEvent(event);
|
|
if (screen) screen->dragging_ = false;
|
|
}
|
|
|
|
} // namespace LXMF
|
|
} // namespace UI
|
|
|
|
#endif // ARDUINO
|