feat(ui): converge shared map viewport and localization contracts

This commit is contained in:
liu weikai
2026-04-22 22:10:45 +08:00
parent c4b763d2ea
commit c4e300a57f
41 changed files with 5776 additions and 1340 deletions
@@ -1,10 +1,18 @@
#include "ui/components/two_pane_styles.h"
#if defined(ESP_PLATFORM)
#include "esp_log.h"
#endif
namespace ui::components::two_pane_styles
{
namespace
{
#if defined(ESP_PLATFORM)
constexpr const char* kLogTag = "two-pane-style";
#endif
bool s_inited = false;
lv_style_t s_panel_side;
@@ -29,6 +37,10 @@ void init_once()
if (s_inited) return;
s_inited = true;
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag, "init_once");
#endif
lv_style_init(&s_panel_side);
lv_style_set_bg_opa(&s_panel_side, LV_OPA_COVER);
lv_style_set_bg_color(&s_panel_side, lv_color_hex(kSidePanelBg));
@@ -28,7 +28,7 @@
#ifdef INADDR_NONE
#undef INADDR_NONE
#endif
#include "rom/miniz.h"
#include <rom/miniz.h>
#define UI_PACKS_HAVE_CRT_BUNDLE 1
@@ -1271,9 +1271,9 @@ std::string sha256_hex_of_bytes(const std::uint8_t* data, std::size_t len)
unsigned char hash[32];
mbedtls_sha256_context ctx;
mbedtls_sha256_init(&ctx);
mbedtls_sha256_starts_ret(&ctx, 0);
mbedtls_sha256_update_ret(&ctx, data, len);
mbedtls_sha256_finish_ret(&ctx, hash);
mbedtls_sha256_starts(&ctx, 0);
mbedtls_sha256_update(&ctx, data, len);
mbedtls_sha256_finish(&ctx, hash);
mbedtls_sha256_free(&ctx);
char hex[65];
@@ -48,6 +48,8 @@
#define CONTACTS_LOG(...)
#endif
#define CONTACTS_NODE_INFO_LOG(...) std::printf("[Contacts][NodeInfo] " __VA_ARGS__)
using namespace contacts::ui;
namespace chat_support = chat::ui::support;
@@ -1136,8 +1138,14 @@ static void open_delete_confirm_modal()
static void open_node_info_screen_for_node(uint32_t node_id)
{
CONTACTS_NODE_INFO_LOG("open request node=%08lX existing_root=%p contacts_root=%p active=%p\n",
static_cast<unsigned long>(node_id),
g_contacts_state.node_info_root,
g_contacts_state.root,
lv_screen_active());
if (g_contacts_state.node_info_root)
{
CONTACTS_NODE_INFO_LOG("open ignored: node_info_root already exists\n");
return;
}
@@ -1148,19 +1156,39 @@ static void open_node_info_screen_for_node(uint32_t node_id)
}
if (!node)
{
CONTACTS_NODE_INFO_LOG("open aborted: node not found\n");
return;
}
CONTACTS_NODE_INFO_LOG("resolved node=%08lX display='%s' long='%s' short='%s' pos_valid=%d\n",
static_cast<unsigned long>(node->node_id),
node->display_name.c_str(),
node->long_name,
node->short_name,
node->position.valid ? 1 : 0);
lv_obj_t* parent = g_contacts_state.root
? lv_obj_get_parent(g_contacts_state.root)
: lv_screen_active();
if (!parent)
{
CONTACTS_NODE_INFO_LOG("open aborted: parent missing\n");
return;
}
lv_obj_update_layout(parent);
CONTACTS_NODE_INFO_LOG("parent=%p size=%dx%d hidden=%d\n",
parent,
static_cast<int>(lv_obj_get_width(parent)),
static_cast<int>(lv_obj_get_height(parent)),
lv_obj_has_flag(parent, LV_OBJ_FLAG_HIDDEN) ? 1 : 0);
node_info::ui::NodeInfoWidgets widgets = node_info::ui::create(parent);
g_contacts_state.node_info_root = widgets.root;
CONTACTS_NODE_INFO_LOG("node_info created root=%p header=%p content=%p back_btn=%p\n",
widgets.root,
widgets.header,
widgets.content,
widgets.back_btn);
const chat::contacts::NodeInfo* info = node;
if (g_contacts_state.contact_service)
@@ -1169,68 +1197,106 @@ static void open_node_info_screen_for_node(uint32_t node_id)
if (latest)
{
info = latest;
CONTACTS_NODE_INFO_LOG("using latest contact_service snapshot node=%08lX pos_valid=%d\n",
static_cast<unsigned long>(latest->node_id),
latest->position.valid ? 1 : 0);
}
}
node_info::ui::set_node_info(*info);
CONTACTS_NODE_INFO_LOG("set_node_info done root=%p hidden=%d size=%dx%d\n",
widgets.root,
widgets.root ? (lv_obj_has_flag(widgets.root, LV_OBJ_FLAG_HIDDEN) ? 1 : 0) : -1,
widgets.root ? static_cast<int>(lv_obj_get_width(widgets.root)) : -1,
widgets.root ? static_cast<int>(lv_obj_get_height(widgets.root)) : -1);
if (!g_contacts_state.node_info_group)
{
g_contacts_state.node_info_group = lv_group_create();
CONTACTS_NODE_INFO_LOG("created node_info_group=%p\n", g_contacts_state.node_info_group);
}
lv_group_remove_all_objs(g_contacts_state.node_info_group);
g_contacts_state.node_info_prev_group = lv_group_get_default();
set_default_group(g_contacts_state.node_info_group);
CONTACTS_NODE_INFO_LOG("focus group switched prev=%p current=%p\n",
g_contacts_state.node_info_prev_group,
g_contacts_state.node_info_group);
if (widgets.back_btn)
{
lv_group_add_obj(g_contacts_state.node_info_group, widgets.back_btn);
lv_group_focus_obj(widgets.back_btn);
lv_obj_add_event_cb(widgets.back_btn, on_node_info_back_clicked, LV_EVENT_CLICKED, nullptr);
CONTACTS_NODE_INFO_LOG("back button wired and focused back_btn=%p\n", widgets.back_btn);
}
if (widgets.layer_btn)
{
lv_group_add_obj(g_contacts_state.node_info_group, widgets.layer_btn);
CONTACTS_NODE_INFO_LOG("layer button added layer_btn=%p\n", widgets.layer_btn);
}
if (g_contacts_state.root)
{
lv_obj_add_flag(g_contacts_state.root, LV_OBJ_FLAG_HIDDEN);
CONTACTS_NODE_INFO_LOG("contacts root hidden root=%p hidden=%d\n",
g_contacts_state.root,
lv_obj_has_flag(g_contacts_state.root, LV_OBJ_FLAG_HIDDEN) ? 1 : 0);
}
if (g_contacts_state.refresh_timer)
{
lv_timer_pause(g_contacts_state.refresh_timer);
CONTACTS_NODE_INFO_LOG("refresh timer paused timer=%p\n", g_contacts_state.refresh_timer);
}
CONTACTS_NODE_INFO_LOG("open complete\n");
}
static void close_node_info_screen()
{
CONTACTS_NODE_INFO_LOG("close request root=%p group=%p prev_group=%p\n",
g_contacts_state.node_info_root,
g_contacts_state.node_info_group,
g_contacts_state.node_info_prev_group);
if (!g_contacts_state.node_info_root)
{
CONTACTS_NODE_INFO_LOG("close ignored: no node_info_root\n");
return;
}
node_info::ui::destroy();
g_contacts_state.node_info_root = nullptr;
CONTACTS_NODE_INFO_LOG("node_info destroyed\n");
if (g_contacts_state.node_info_group)
{
lv_group_remove_all_objs(g_contacts_state.node_info_group);
CONTACTS_NODE_INFO_LOG("node_info_group cleared group=%p\n", g_contacts_state.node_info_group);
}
lv_group_t* restore = contacts_input_get_group();
if (restore)
{
set_default_group(restore);
CONTACTS_NODE_INFO_LOG("restored default group=%p\n", restore);
}
g_contacts_state.node_info_prev_group = nullptr;
if (g_contacts_state.root)
{
lv_obj_clear_flag(g_contacts_state.root, LV_OBJ_FLAG_HIDDEN);
CONTACTS_NODE_INFO_LOG("contacts root shown root=%p hidden=%d\n",
g_contacts_state.root,
lv_obj_has_flag(g_contacts_state.root, LV_OBJ_FLAG_HIDDEN) ? 1 : 0);
}
if (g_contacts_state.refresh_timer)
{
lv_timer_resume(g_contacts_state.refresh_timer);
CONTACTS_NODE_INFO_LOG("refresh timer resumed timer=%p\n", g_contacts_state.refresh_timer);
}
CONTACTS_NODE_INFO_LOG("close refresh_ui\n");
refresh_ui();
CONTACTS_NODE_INFO_LOG("close contacts_focus_to_list\n");
contacts_focus_to_list();
CONTACTS_NODE_INFO_LOG("close complete\n");
}
static void open_chat_compose()
@@ -1993,6 +2059,7 @@ static void on_del_cancel_clicked(lv_event_t* /*e*/)
static void on_node_info_back_clicked(lv_event_t* /*e*/)
{
CONTACTS_NODE_INFO_LOG("back button clicked\n");
close_node_info_screen();
}
@@ -320,8 +320,8 @@ static void gps_initial_tiles_async(void* /*user_data*/)
g_gps_state.has_fix,
sanitize_map_source(app::configFacade().getConfig().map_source),
app::configFacade().getConfig().map_contour_enabled,
lv_obj_get_width(g_gps_state.map),
lv_obj_get_height(g_gps_state.map),
static_cast<int>(lv_obj_get_width(g_gps_state.map)),
static_cast<int>(lv_obj_get_height(g_gps_state.map)),
g_gps_state.lat,
g_gps_state.lng);
update_map_tiles(false);
File diff suppressed because it is too large Load Diff
@@ -77,8 +77,9 @@ lv_obj_t* create_root(lv_obj_t* parent)
lv_obj_t* create_header(lv_obj_t* root)
{
const lv_coord_t root_w = resolve_parent_width(root);
lv_obj_t* header = lv_obj_create(root);
lv_obj_set_size(header, lv_obj_get_width(root), top_bar_height());
lv_obj_set_size(header, root_w, top_bar_height());
lv_obj_set_pos(header, 0, 0);
make_plain(header);
return header;
@@ -86,9 +87,11 @@ lv_obj_t* create_header(lv_obj_t* root)
lv_obj_t* create_content(lv_obj_t* root)
{
const lv_coord_t root_w = resolve_parent_width(root);
const lv_coord_t root_h = resolve_parent_height(root);
const lv_coord_t header_h = top_bar_height();
lv_obj_t* content = lv_obj_create(root);
lv_obj_set_size(content, lv_obj_get_width(root), lv_obj_get_height(root) - header_h);
lv_obj_set_size(content, root_w, root_h - header_h);
lv_obj_set_pos(content, 0, header_h);
make_plain(content);
return content;
@@ -42,12 +42,20 @@
#include "ui/widgets/system_notification.h"
#include "ui/widgets/top_bar.h"
#if defined(ESP_PLATFORM)
#include "esp_log.h"
#endif
namespace settings::ui::components
{
namespace
{
#if defined(ESP_PLATFORM)
constexpr const char* kLogTag = "settings-page";
#endif
namespace device_runtime = ::platform::ui::device;
namespace firmware_update_runtime = ::platform::ui::firmware_update;
namespace gps_runtime = ::platform::ui::gps;
@@ -794,6 +802,9 @@ static void perform_factory_reset()
static void settings_load()
{
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag, "settings_load begin");
#endif
app::IAppFacade& app_ctx = app::appFacade();
g_settings.chat_protocol = static_cast<int>(app_ctx.getConfig().mesh_protocol);
@@ -1036,6 +1047,14 @@ static void settings_load()
snprintf(g_settings.gauge_full_mah, sizeof(g_settings.gauge_full_mah), "%lu",
static_cast<unsigned long>(f));
}
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag,
"settings_load complete locales=%u tx_power_options=%u wifi_supported=%d wifi_networks=%u",
static_cast<unsigned>(kLocaleOptionCount),
static_cast<unsigned>(kTxPowerOptionCount),
wifi_runtime::is_supported() ? 1 : 0,
static_cast<unsigned>(kWifiNetworkOptionCount));
#endif
}
static void format_value(const settings::ui::SettingItem& item, char* out, size_t out_len)
@@ -2863,12 +2882,23 @@ static void build_item_list()
return;
}
s_building_list = true;
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag,
"build_item_list begin category=%d",
g_state.current_category);
#endif
g_state.list_back_btn = nullptr;
lv_obj_clean(g_state.list_panel);
g_state.item_count = 0;
lv_obj_clear_flag(g_state.list_panel, LV_OBJ_FLAG_SCROLLABLE);
const CategoryDef& cat = kCategories[g_state.current_category];
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag,
"build_item_list category_label=%s item_count=%u",
cat.label ? cat.label : "<null>",
static_cast<unsigned>(cat.item_count));
#endif
for (size_t i = 0; i < cat.item_count && g_state.item_count < kMaxItems; ++i)
{
settings::ui::ItemWidget& widget = g_state.item_widgets[g_state.item_count];
@@ -2898,9 +2928,22 @@ static void build_item_list()
}
if (!should_show_item(*widget.def))
{
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag,
"build_item_list skip index=%u key=%s",
static_cast<unsigned>(i),
widget.def->pref_key ? widget.def->pref_key : "<none>");
#endif
continue;
}
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag,
"build_item_list item index=%u key=%s type=%d",
static_cast<unsigned>(i),
widget.def->pref_key ? widget.def->pref_key : "<none>",
static_cast<int>(widget.def->type));
#endif
lv_obj_t* btn = lv_btn_create(g_state.list_panel);
configure_list_item_button(btn);
style::apply_list_item(btn);
@@ -2937,6 +2980,12 @@ static void build_item_list()
lv_obj_add_flag(g_state.list_panel, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_scrollbar_mode(g_state.list_panel, LV_SCROLLBAR_MODE_AUTO);
s_building_list = false;
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag,
"build_item_list complete visible_items=%u has_back=%d",
static_cast<unsigned>(g_state.item_count),
g_state.list_back_btn ? 1 : 0);
#endif
}
static bool activate_item_widget(settings::ui::ItemWidget& widget)
@@ -3242,6 +3291,9 @@ static void settings_back_cb(void* /*user_data*/)
void create(lv_obj_t* parent)
{
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag, "create begin");
#endif
settings_load();
// Avoid auto-adding widgets to the current default group during creation.
@@ -3249,16 +3301,42 @@ void create(lv_obj_t* parent)
set_default_group(nullptr);
g_state.parent = parent;
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag, "create root");
#endif
g_state.root = layout::create_root(parent);
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag, "create header");
#endif
layout::create_header(g_state.root, settings_back_cb, nullptr);
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag, "create content");
#endif
g_state.content = layout::create_content(g_state.root);
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag, "create filter panel");
#endif
layout::create_filter_panel(g_state.content);
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag, "create list panel");
#endif
layout::create_list_panel(g_state.content);
g_state.filter_count = sizeof(kCategories) / sizeof(kCategories[0]);
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag,
"create filter buttons count=%u",
static_cast<unsigned>(g_state.filter_count));
#endif
for (size_t i = 0; i < g_state.filter_count; ++i)
{
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag,
"create filter button index=%u label=%s",
static_cast<unsigned>(i),
kCategories[i].label ? kCategories[i].label : "<null>");
#endif
lv_obj_t* btn = lv_btn_create(g_state.filter_panel);
lv_obj_set_size(btn, LV_PCT(100), ::ui::page_profile::current().filter_button_height);
style::apply_btn_filter(btn);
@@ -3274,8 +3352,17 @@ void create(lv_obj_t* parent)
g_state.filter_buttons[i] = btn;
}
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag, "create update_filter_styles");
#endif
update_filter_styles();
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag, "create build_item_list");
#endif
build_item_list();
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag, "create sync_firmware_update_ui");
#endif
sync_firmware_update_ui(false);
if (s_firmware_update_timer)
{
@@ -3295,7 +3382,13 @@ void create(lv_obj_t* parent)
// Restore previous default group before initializing input.
set_default_group(prev_group);
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag, "create input init");
#endif
settings::ui::input::init();
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag, "create complete");
#endif
}
void destroy()
@@ -7,11 +7,19 @@
#include "ui/components/info_card.h"
#include "ui/components/two_pane_styles.h"
#if defined(ESP_PLATFORM)
#include "esp_log.h"
#endif
namespace settings::ui::style
{
namespace
{
#if defined(ESP_PLATFORM)
constexpr const char* kLogTag = "settings-style";
#endif
bool s_inited = false;
lv_style_t s_modal_bg;
lv_style_t s_modal_panel;
@@ -26,6 +34,10 @@ void init_once()
if (s_inited) return;
s_inited = true;
#if defined(ESP_PLATFORM)
ESP_LOGI(kLogTag, "init_once");
#endif
lv_style_init(&s_modal_bg);
lv_style_set_bg_opa(&s_modal_bg, LV_OPA_COVER);
lv_style_set_bg_color(&s_modal_bg,
@@ -0,0 +1,911 @@
/**
* @file map_viewport.cpp
* @brief Shared map viewport facade backed by the platform tile runtime.
*/
#include "ui/widgets/map/map_viewport.h"
#include "app/app_config.h"
#include "app/app_facade_access.h"
#include "platform/ui/device_runtime.h"
#include "ui/localization.h"
#include "ui/widgets/map/map_tiles.h"
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
namespace ui::widgets::map
{
struct RuntimeImpl
{
Widgets widgets{};
Model model{};
MapAnchor anchor{};
std::vector<MapTile> tiles{};
TileContext tile_ctx{};
lv_timer_t* loader_timer = nullptr;
uint32_t loader_interval_ms = 200;
bool alive = false;
bool has_map_data = false;
bool has_visible_map_data = false;
GestureCallback gesture_callback = nullptr;
void* gesture_user_data = nullptr;
bool gesture_enabled = false;
bool gesture_pressed = false;
bool gesture_dragging = false;
lv_point_t gesture_start{};
lv_point_t gesture_last{};
};
namespace
{
#define MAP_VIEWPORT_LOG(...) std::printf("[MapViewport] " __VA_ARGS__)
constexpr double kCoordPi = 3.14159265358979323846;
constexpr double kCoordA = 6378245.0;
constexpr double kCoordEe = 0.00669342162296594323;
constexpr int kGestureDragStartPx = 6;
bool coord_out_of_china(double lat, double lon)
{
return (lon < 72.004 || lon > 137.8347 || lat < 0.8293 || lat > 55.8271);
}
double coord_transform_lat(double x, double y)
{
double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y +
0.2 * std::sqrt(std::fabs(x));
ret += (20.0 * std::sin(6.0 * x * kCoordPi) + 20.0 * std::sin(2.0 * x * kCoordPi)) * 2.0 / 3.0;
ret += (20.0 * std::sin(y * kCoordPi) + 40.0 * std::sin(y / 3.0 * kCoordPi)) * 2.0 / 3.0;
ret += (160.0 * std::sin(y / 12.0 * kCoordPi) + 320 * std::sin(y * kCoordPi / 30.0)) * 2.0 / 3.0;
return ret;
}
double coord_transform_lon(double x, double y)
{
double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y +
0.1 * std::sqrt(std::fabs(x));
ret += (20.0 * std::sin(6.0 * x * kCoordPi) + 20.0 * std::sin(2.0 * x * kCoordPi)) * 2.0 / 3.0;
ret += (20.0 * std::sin(x * kCoordPi) + 40.0 * std::sin(x / 3.0 * kCoordPi)) * 2.0 / 3.0;
ret += (150.0 * std::sin(x / 12.0 * kCoordPi) + 300.0 * std::sin(x / 30.0 * kCoordPi)) * 2.0 / 3.0;
return ret;
}
void wgs84_to_gcj02(double lat, double lon, double& out_lat, double& out_lon)
{
if (coord_out_of_china(lat, lon))
{
out_lat = lat;
out_lon = lon;
return;
}
double dlat = coord_transform_lat(lon - 105.0, lat - 35.0);
double dlon = coord_transform_lon(lon - 105.0, lat - 35.0);
double radlat = lat / 180.0 * kCoordPi;
double magic = std::sin(radlat);
magic = 1 - kCoordEe * magic * magic;
double sqrt_magic = std::sqrt(magic);
dlat = (dlat * 180.0) / ((kCoordA * (1 - kCoordEe)) / (magic * sqrt_magic) * kCoordPi);
dlon = (dlon * 180.0) / (kCoordA / sqrt_magic * std::cos(radlat) * kCoordPi);
out_lat = lat + dlat;
out_lon = lon + dlon;
}
void gcj02_to_bd09(double lat, double lon, double& out_lat, double& out_lon)
{
double z = std::sqrt(lon * lon + lat * lat) + 0.00002 * std::sin(lat * kCoordPi);
double theta = std::atan2(lat, lon) + 0.000003 * std::cos(lon * kCoordPi);
out_lon = z * std::cos(theta) + 0.0065;
out_lat = z * std::sin(theta) + 0.006;
}
void make_plain(lv_obj_t* obj)
{
if (!obj)
{
return;
}
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_scrollbar_mode(obj, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_style_pad_all(obj, 0, 0);
lv_obj_set_style_border_width(obj, 0, 0);
lv_obj_set_style_radius(obj, 0, 0);
lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0);
}
bool is_runtime_alive(const RuntimeImpl& impl)
{
return impl.alive &&
impl.widgets.root &&
lv_obj_is_valid(impl.widgets.root) &&
impl.widgets.tile_layer &&
lv_obj_is_valid(impl.widgets.tile_layer);
}
void reset_gesture_state(RuntimeImpl& impl)
{
impl.gesture_pressed = false;
impl.gesture_dragging = false;
impl.gesture_start = lv_point_t{};
impl.gesture_last = lv_point_t{};
}
void emit_gesture_event(RuntimeImpl& impl, GesturePhase phase, const lv_point_t& point)
{
if (!impl.gesture_callback)
{
return;
}
GestureEvent event{};
event.phase = phase;
event.point = point;
event.total_dx = static_cast<int>(point.x - impl.gesture_start.x);
event.total_dy = static_cast<int>(point.y - impl.gesture_start.y);
event.dragging = impl.gesture_dragging;
impl.gesture_callback(event, impl.gesture_user_data);
}
void update_gesture_surface_visibility(RuntimeImpl& impl)
{
if (!impl.widgets.gesture_surface || !lv_obj_is_valid(impl.widgets.gesture_surface))
{
return;
}
const bool visible = impl.gesture_enabled && impl.gesture_callback != nullptr;
if (visible)
{
lv_obj_clear_flag(impl.widgets.gesture_surface, LV_OBJ_FLAG_HIDDEN);
}
else
{
lv_obj_add_flag(impl.widgets.gesture_surface, LV_OBJ_FLAG_HIDDEN);
reset_gesture_state(impl);
}
MAP_VIEWPORT_LOG("gesture_surface enabled=%d callback=%d visible=%d obj=%p\n",
impl.gesture_enabled ? 1 : 0,
impl.gesture_callback ? 1 : 0,
visible ? 1 : 0,
impl.widgets.gesture_surface);
}
lv_indev_t* resolve_event_indev(lv_event_t* e)
{
if (e)
{
if (lv_indev_t* indev = lv_event_get_indev(e))
{
return indev;
}
}
return lv_indev_active();
}
bool resolve_event_point(lv_event_t* e, lv_point_t& out_point)
{
lv_indev_t* indev = resolve_event_indev(e);
if (!indev)
{
out_point = {};
return false;
}
lv_indev_get_point(indev, &out_point);
return true;
}
void gesture_surface_event_cb(lv_event_t* e)
{
auto* impl = static_cast<RuntimeImpl*>(lv_event_get_user_data(e));
if (!impl || !impl->gesture_enabled || !impl->gesture_callback || !is_runtime_alive(*impl))
{
return;
}
lv_point_t point{};
const bool has_point = resolve_event_point(e, point);
const lv_event_code_t code = lv_event_get_code(e);
switch (code)
{
case LV_EVENT_PRESSED:
if (!has_point)
{
return;
}
impl->gesture_pressed = true;
impl->gesture_dragging = false;
impl->gesture_start = point;
impl->gesture_last = point;
emit_gesture_event(*impl, GesturePhase::Pressed, point);
break;
case LV_EVENT_PRESSING:
if (!impl->gesture_pressed || !has_point)
{
return;
}
impl->gesture_last = point;
if (!impl->gesture_dragging)
{
const int dx = static_cast<int>(point.x - impl->gesture_start.x);
const int dy = static_cast<int>(point.y - impl->gesture_start.y);
if (std::abs(dx) < kGestureDragStartPx && std::abs(dy) < kGestureDragStartPx)
{
return;
}
impl->gesture_dragging = true;
MAP_VIEWPORT_LOG("drag_begin root=%p dx=%d dy=%d\n",
impl->widgets.root,
dx,
dy);
emit_gesture_event(*impl, GesturePhase::DragBegin, point);
}
if (impl->gesture_dragging)
{
if (lv_indev_t* indev = resolve_event_indev(e))
{
lv_indev_stop_processing(indev);
}
MAP_VIEWPORT_LOG("drag_update root=%p dx=%d dy=%d\n",
impl->widgets.root,
static_cast<int>(point.x - impl->gesture_start.x),
static_cast<int>(point.y - impl->gesture_start.y));
emit_gesture_event(*impl, GesturePhase::DragUpdate, point);
}
break;
case LV_EVENT_RELEASED:
case LV_EVENT_PRESS_LOST:
if (!impl->gesture_pressed)
{
return;
}
if (!has_point)
{
point = impl->gesture_last;
}
if (impl->gesture_dragging)
{
if (lv_indev_t* indev = resolve_event_indev(e))
{
lv_indev_stop_processing(indev);
}
MAP_VIEWPORT_LOG("drag_end root=%p dx=%d dy=%d code=%d\n",
impl->widgets.root,
static_cast<int>(point.x - impl->gesture_start.x),
static_cast<int>(point.y - impl->gesture_start.y),
static_cast<int>(code));
emit_gesture_event(*impl,
code == LV_EVENT_PRESS_LOST ? GesturePhase::Cancel : GesturePhase::DragEnd,
point);
}
reset_gesture_state(*impl);
break;
default:
break;
}
}
void prime_visible_tiles(RuntimeImpl& impl, const char* reason)
{
if (!is_runtime_alive(impl) || !impl.model.focus_point.valid)
{
MAP_VIEWPORT_LOG("prime_visible_tiles skipped reason=%s alive=%d focus=%d\n",
reason ? reason : "<none>",
impl.alive ? 1 : 0,
impl.model.focus_point.valid ? 1 : 0);
return;
}
MAP_VIEWPORT_LOG("prime_visible_tiles begin reason=%s map_data=%d visible_map=%d\n",
reason ? reason : "<none>",
impl.has_map_data ? 1 : 0,
impl.has_visible_map_data ? 1 : 0);
for (int attempt = 0; attempt < 2 && !impl.has_visible_map_data; ++attempt)
{
tile_loader_step(impl.tile_ctx);
MAP_VIEWPORT_LOG("prime_visible_tiles step=%d visible_map=%d map_data=%d\n",
attempt + 1,
impl.has_visible_map_data ? 1 : 0,
impl.has_map_data ? 1 : 0);
}
MAP_VIEWPORT_LOG("prime_visible_tiles end reason=%s map_data=%d visible_map=%d\n",
reason ? reason : "<none>",
impl.has_map_data ? 1 : 0,
impl.has_visible_map_data ? 1 : 0);
}
GeoPoint transformed_focus(const Model& model)
{
GeoPoint out{};
transform_geo_point(model.focus_point, model.coord_system, out);
return out;
}
void refresh_tiles(RuntimeImpl& impl, const char* reason)
{
if (!is_runtime_alive(impl))
{
MAP_VIEWPORT_LOG("refresh_tiles skipped reason=%s alive=%d root=%p tile=%p\n",
reason ? reason : "<none>",
impl.alive ? 1 : 0,
impl.widgets.root,
impl.widgets.tile_layer);
return;
}
if (!impl.model.focus_point.valid)
{
MAP_VIEWPORT_LOG("refresh_tiles skipped reason=%s focus=invalid\n",
reason ? reason : "<none>");
cleanup_tiles(impl.tile_ctx);
impl.anchor.valid = false;
return;
}
GeoPoint focus = transformed_focus(impl.model);
if (!focus.valid)
{
MAP_VIEWPORT_LOG("refresh_tiles skipped reason=%s transformed_focus=invalid\n",
reason ? reason : "<none>");
cleanup_tiles(impl.tile_ctx);
impl.anchor.valid = false;
return;
}
lv_obj_update_layout(impl.widgets.root);
set_map_render_options(impl.model.map_source, impl.model.contour_enabled);
calculate_required_tiles(impl.tile_ctx,
focus.lat,
focus.lon,
impl.model.zoom,
impl.model.pan_x,
impl.model.pan_y,
true);
prime_visible_tiles(impl, reason);
MAP_VIEWPORT_LOG("refresh_tiles reason=%s zoom=%d pan=%d,%d src=%u contour=%d anchor=%d size=%dx%d map_data=%d visible_map=%d\n",
reason ? reason : "<none>",
impl.model.zoom,
impl.model.pan_x,
impl.model.pan_y,
sanitize_map_source(impl.model.map_source),
impl.model.contour_enabled ? 1 : 0,
impl.anchor.valid ? 1 : 0,
static_cast<int>(lv_obj_get_width(impl.widgets.tile_layer)),
static_cast<int>(lv_obj_get_height(impl.widgets.tile_layer)),
impl.has_map_data ? 1 : 0,
impl.has_visible_map_data ? 1 : 0);
}
void loader_timer_cb(lv_timer_t* timer)
{
auto* impl = static_cast<RuntimeImpl*>(lv_timer_get_user_data(timer));
if (!impl || !is_runtime_alive(*impl) || !impl->model.focus_point.valid)
{
return;
}
tile_loader_step(impl->tile_ctx);
uint8_t missing_source = 0;
if (::take_missing_tile_notice(&missing_source))
{
MAP_VIEWPORT_LOG("missing_tiles source=%u label=%s\n",
static_cast<unsigned>(missing_source),
map_source_label(missing_source));
}
}
const Widgets& empty_widgets()
{
static Widgets kEmpty{};
return kEmpty;
}
void clear_layer_notice(LayerNotice* notice)
{
if (!notice)
{
return;
}
notice->has_message = false;
notice->message[0] = '\0';
notice->duration_ms = 0;
}
void set_layer_notice(LayerNotice* notice, const char* message, uint32_t duration_ms)
{
clear_layer_notice(notice);
if (!notice || !message || message[0] == '\0')
{
return;
}
notice->has_message = true;
std::snprintf(notice->message, sizeof(notice->message), "%s", message);
notice->duration_ms = duration_ms;
}
} // namespace
Runtime::~Runtime()
{
destroy(*this);
}
Runtime::Runtime(Runtime&& other) noexcept
: impl_(other.impl_)
{
other.impl_ = nullptr;
}
Runtime& Runtime::operator=(Runtime&& other) noexcept
{
if (this != &other)
{
destroy(*this);
impl_ = other.impl_;
other.impl_ = nullptr;
}
return *this;
}
Widgets create(Runtime& runtime, lv_obj_t* parent, uint32_t loader_interval_ms)
{
destroy(runtime);
if (!runtime.impl_)
{
runtime.impl_ = new RuntimeImpl();
runtime.impl_->tiles.reserve(TILE_RECORD_LIMIT);
}
RuntimeImpl* impl = runtime.impl_;
impl->loader_interval_ms = loader_interval_ms;
impl->widgets.root = lv_obj_create(parent);
lv_obj_set_size(impl->widgets.root, LV_PCT(100), LV_PCT(100));
lv_obj_set_pos(impl->widgets.root, 0, 0);
make_plain(impl->widgets.root);
#ifdef LV_OBJ_FLAG_CLIP_CHILDREN
lv_obj_add_flag(impl->widgets.root, LV_OBJ_FLAG_CLIP_CHILDREN);
#endif
impl->widgets.tile_layer = lv_obj_create(impl->widgets.root);
lv_obj_set_size(impl->widgets.tile_layer, LV_PCT(100), LV_PCT(100));
lv_obj_set_pos(impl->widgets.tile_layer, 0, 0);
make_plain(impl->widgets.tile_layer);
impl->widgets.overlay_layer = lv_obj_create(impl->widgets.root);
lv_obj_set_size(impl->widgets.overlay_layer, LV_PCT(100), LV_PCT(100));
lv_obj_set_pos(impl->widgets.overlay_layer, 0, 0);
make_plain(impl->widgets.overlay_layer);
impl->widgets.gesture_surface = lv_obj_create(impl->widgets.root);
lv_obj_set_size(impl->widgets.gesture_surface, LV_PCT(100), LV_PCT(100));
lv_obj_set_pos(impl->widgets.gesture_surface, 0, 0);
make_plain(impl->widgets.gesture_surface);
lv_obj_add_flag(impl->widgets.gesture_surface, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_flag(impl->widgets.gesture_surface, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_event_cb(impl->widgets.gesture_surface, gesture_surface_event_cb, LV_EVENT_ALL, impl);
init_tile_context(impl->tile_ctx,
impl->widgets.tile_layer,
&impl->anchor,
&impl->tiles,
&impl->has_map_data,
&impl->has_visible_map_data);
impl->loader_timer = lv_timer_create(loader_timer_cb, loader_interval_ms, impl);
impl->alive = true;
MAP_VIEWPORT_LOG("create root=%p tile=%p overlay=%p loader_timer=%p interval=%u\n",
impl->widgets.root,
impl->widgets.tile_layer,
impl->widgets.overlay_layer,
impl->loader_timer,
static_cast<unsigned>(loader_interval_ms));
return impl->widgets;
}
void destroy(Runtime& runtime)
{
RuntimeImpl* impl = runtime.impl_;
if (!impl)
{
return;
}
MAP_VIEWPORT_LOG("destroy begin root=%p timer=%p alive=%d\n",
impl->widgets.root,
impl->loader_timer,
impl->alive ? 1 : 0);
impl->alive = false;
if (impl->loader_timer)
{
lv_timer_del(impl->loader_timer);
impl->loader_timer = nullptr;
}
cleanup_tiles(impl->tile_ctx);
if (impl->widgets.root && lv_obj_is_valid(impl->widgets.root))
{
lv_obj_del(impl->widgets.root);
}
delete impl;
runtime.impl_ = nullptr;
MAP_VIEWPORT_LOG("destroy end\n");
}
const Widgets& widgets(const Runtime& runtime)
{
const RuntimeImpl* impl = runtime.impl_;
return impl ? impl->widgets : empty_widgets();
}
void set_size(Runtime& runtime, lv_coord_t width, lv_coord_t height)
{
if (!runtime.impl_)
{
runtime.impl_ = new RuntimeImpl();
runtime.impl_->tiles.reserve(TILE_RECORD_LIMIT);
}
RuntimeImpl* impl = runtime.impl_;
if (!impl->widgets.root)
{
return;
}
lv_obj_set_size(impl->widgets.root, width, height);
lv_obj_set_size(impl->widgets.tile_layer, width, height);
lv_obj_set_size(impl->widgets.overlay_layer, width, height);
lv_obj_set_size(impl->widgets.gesture_surface, width, height);
MAP_VIEWPORT_LOG("set_size root=%p size=%dx%d\n",
impl->widgets.root,
static_cast<int>(width),
static_cast<int>(height));
}
void apply_model(Runtime& runtime, const Model& model)
{
if (!runtime.impl_)
{
runtime.impl_ = new RuntimeImpl();
runtime.impl_->tiles.reserve(TILE_RECORD_LIMIT);
}
RuntimeImpl* impl = runtime.impl_;
impl->model = model;
impl->model.map_source = sanitize_map_source(impl->model.map_source);
MAP_VIEWPORT_LOG("apply_model focus_valid=%d lat=%.7f lon=%.7f zoom=%d pan=%d,%d src=%u contour=%d coord=%u\n",
impl->model.focus_point.valid ? 1 : 0,
impl->model.focus_point.lat,
impl->model.focus_point.lon,
impl->model.zoom,
impl->model.pan_x,
impl->model.pan_y,
static_cast<unsigned>(impl->model.map_source),
impl->model.contour_enabled ? 1 : 0,
static_cast<unsigned>(impl->model.coord_system));
refresh_tiles(*impl, "apply_model");
}
void clear(Runtime& runtime)
{
RuntimeImpl* impl = runtime.impl_;
if (!impl)
{
return;
}
impl->model.focus_point = GeoPoint{};
cleanup_tiles(impl->tile_ctx);
impl->anchor.valid = false;
reset_gesture_state(*impl);
MAP_VIEWPORT_LOG("clear root=%p\n", impl->widgets.root);
}
bool project_point(const Runtime& runtime, const GeoPoint& point, lv_point_t& out_screen_point)
{
const RuntimeImpl* impl = runtime.impl_;
if (!impl || !is_runtime_alive(*impl) || !impl->anchor.valid || !point.valid)
{
return false;
}
GeoPoint transformed{};
if (!transform_geo_point(point, impl->model.coord_system, transformed) || !transformed.valid)
{
return false;
}
int screen_x = 0;
int screen_y = 0;
if (!gps_screen_pos(impl->tile_ctx, transformed.lat, transformed.lon, screen_x, screen_y))
{
return false;
}
out_screen_point.x = static_cast<lv_coord_t>(screen_x);
out_screen_point.y = static_cast<lv_coord_t>(screen_y);
return true;
}
bool preview_project_point(lv_obj_t* viewport_root, const Model& model, const GeoPoint& point, lv_point_t& out_screen_point)
{
if (!viewport_root || !lv_obj_is_valid(viewport_root) || !model.focus_point.valid || !point.valid)
{
return false;
}
GeoPoint transformed_focus{};
GeoPoint transformed_point{};
if (!transform_geo_point(model.focus_point, model.coord_system, transformed_focus) || !transformed_focus.valid)
{
return false;
}
if (!transform_geo_point(point, model.coord_system, transformed_point) || !transformed_point.valid)
{
return false;
}
MapAnchor anchor{};
TileContext ctx{};
ctx.map_container = viewport_root;
ctx.anchor = &anchor;
update_map_anchor(ctx,
transformed_focus.lat,
transformed_focus.lon,
model.zoom,
model.pan_x,
model.pan_y,
true);
if (!anchor.valid)
{
return false;
}
int screen_x = 0;
int screen_y = 0;
if (!gps_screen_pos(ctx, transformed_point.lat, transformed_point.lon, screen_x, screen_y))
{
return false;
}
out_screen_point.x = static_cast<lv_coord_t>(screen_x);
out_screen_point.y = static_cast<lv_coord_t>(screen_y);
return true;
}
bool focus_tile_available(const Model& model)
{
if (!model.focus_point.valid)
{
return false;
}
GeoPoint transformed{};
if (!transform_geo_point(model.focus_point, model.coord_system, transformed) || !transformed.valid)
{
return false;
}
int tile_x = 0;
int tile_y = 0;
latLngToTile(transformed.lat, transformed.lon, model.zoom, tile_x, tile_y);
normalize_tile(model.zoom, tile_x, tile_y);
char path[96];
if (!build_base_tile_path(model.zoom,
tile_x,
tile_y,
sanitize_map_source(model.map_source),
path,
sizeof(path)))
{
return false;
}
lv_fs_file_t file;
const lv_fs_res_t res = lv_fs_open(&file, path, LV_FS_MODE_RD);
if (res != LV_FS_RES_OK)
{
return false;
}
lv_fs_close(&file);
return true;
}
Status status(const Runtime& runtime)
{
Status out{};
const RuntimeImpl* impl = runtime.impl_;
if (!impl)
{
return out;
}
out.alive = impl->alive;
out.has_focus = impl->model.focus_point.valid;
out.anchor_valid = impl->anchor.valid;
out.has_map_data = impl->has_map_data;
out.has_visible_map_data = impl->has_visible_map_data;
out.zoom = impl->model.zoom;
out.pan_x = impl->model.pan_x;
out.pan_y = impl->model.pan_y;
return out;
}
bool take_missing_tile_notice(Runtime& runtime, uint8_t* out_map_source)
{
(void)runtime;
return ::take_missing_tile_notice(out_map_source);
}
void set_gesture_enabled(Runtime& runtime, bool enabled)
{
RuntimeImpl* impl = runtime.impl_;
if (!impl)
{
return;
}
impl->gesture_enabled = enabled;
update_gesture_surface_visibility(*impl);
}
void set_gesture_callback(Runtime& runtime, GestureCallback callback, void* user_data)
{
RuntimeImpl* impl = runtime.impl_;
if (!impl)
{
return;
}
impl->gesture_callback = callback;
impl->gesture_user_data = user_data;
update_gesture_surface_visibility(*impl);
}
bool transform_geo_point(const GeoPoint& point, uint8_t coord_system, GeoPoint& out_point)
{
out_point = {};
if (!point.valid)
{
return false;
}
out_point.valid = true;
if (coord_system == 1)
{
wgs84_to_gcj02(point.lat, point.lon, out_point.lat, out_point.lon);
return true;
}
if (coord_system == 2)
{
double gcj_lat = 0.0;
double gcj_lon = 0.0;
wgs84_to_gcj02(point.lat, point.lon, gcj_lat, gcj_lon);
gcj02_to_bd09(gcj_lat, gcj_lon, out_point.lat, out_point.lon);
return true;
}
out_point.lat = point.lat;
out_point.lon = point.lon;
return true;
}
LayerState current_layer_state()
{
const auto& cfg = app::configFacade().getConfig();
LayerState state{};
state.map_source = sanitize_map_source(cfg.map_source);
state.contour_enabled = cfg.map_contour_enabled;
return state;
}
const char* layer_map_source_label_key(uint8_t map_source)
{
return map_source_label(sanitize_map_source(map_source));
}
const char* layer_contour_status_key(bool contour_enabled)
{
return contour_enabled ? "Contour: ON" : "Contour: OFF";
}
std::string layer_base_summary_text(uint8_t map_source)
{
return ::ui::i18n::format("Base: %s",
::ui::i18n::tr(layer_map_source_label_key(map_source)));
}
bool set_layer_map_source(uint8_t map_source, LayerNotice* out_notice)
{
clear_layer_notice(out_notice);
app::IAppConfigFacade& config_api = app::configFacade();
const uint8_t previous = sanitize_map_source(config_api.getConfig().map_source);
const uint8_t normalized = sanitize_map_source(map_source);
const bool changed = previous != normalized;
MAP_VIEWPORT_LOG("set_layer_map_source from=%u to=%u contour=%d changed=%d\n",
static_cast<unsigned>(previous),
static_cast<unsigned>(normalized),
config_api.getConfig().map_contour_enabled ? 1 : 0,
changed ? 1 : 0);
if (changed)
{
config_api.getConfig().map_source = normalized;
config_api.saveConfig();
}
if (!platform::ui::device::sd_ready())
{
set_layer_notice(out_notice, ::ui::i18n::tr("No SD Card"), 1200);
}
else if (!map_source_directory_available(normalized))
{
const std::string message = ::ui::i18n::format(
"%s layer missing",
::ui::i18n::tr(layer_map_source_label_key(normalized)));
set_layer_notice(out_notice, message.c_str(), 1600);
}
return changed;
}
bool toggle_layer_contour(LayerNotice* out_notice)
{
clear_layer_notice(out_notice);
app::IAppConfigFacade& config_api = app::configFacade();
const bool previous = config_api.getConfig().map_contour_enabled;
const bool enabled = !previous;
MAP_VIEWPORT_LOG("toggle_layer_contour from=%d to=%d src=%u\n",
previous ? 1 : 0,
enabled ? 1 : 0,
static_cast<unsigned>(sanitize_map_source(config_api.getConfig().map_source)));
config_api.getConfig().map_contour_enabled = enabled;
config_api.saveConfig();
if (enabled)
{
if (!platform::ui::device::sd_ready())
{
set_layer_notice(out_notice, ::ui::i18n::tr("No SD Card"), 1200);
}
else if (!contour_directory_available())
{
set_layer_notice(out_notice, ::ui::i18n::tr("Contour data missing"), 1600);
}
}
return true;
}
} // namespace ui::widgets::map