mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-05-27 02:44:13 +00:00
ae234e81af
- Fix Bytes({key_int}) bug in LXMessage::unpack_from_bytes() that caused
incoming field keys to be empty, making fields_get() always miss
- Telemetry-only messages (fields present, no body) now skip chat bubble,
notification sound, and message store entirely
- Implement position_peer_markers() with proper lat/lon to screen coords
- Add display name resolution for map markers via recall_app_data()
- Store peer locations for repositioning on pan/zoom
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
577 lines
20 KiB
C++
577 lines
20 KiB
C++
// Copyright (c) 2024 microReticulum contributors
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#include "MapScreen.h"
|
|
|
|
#ifdef ARDUINO
|
|
|
|
#include "Theme.h"
|
|
#include "TileMath.h"
|
|
#include "Log.h"
|
|
#include "../LVGL/LVGLInit.h"
|
|
#include "../LVGL/LVGLLock.h"
|
|
#include "../../Hardware/TDeck/TileDownloader.h"
|
|
#include <TinyGPSPlus.h>
|
|
|
|
using namespace RNS;
|
|
|
|
namespace UI {
|
|
namespace LXMF {
|
|
|
|
MapScreen::MapScreen(lv_obj_t* parent)
|
|
: _screen(nullptr), _header(nullptr), _viewport(nullptr),
|
|
_gps_marker(nullptr), _zoom_label(nullptr),
|
|
_peer_marker_count(0), _gps(nullptr),
|
|
_center_lat(0.0), _center_lon(0.0), _zoom(14),
|
|
_follow_gps(true), _has_gps_fix(false), _loaded_zoom(-1) {
|
|
|
|
memset(_tile_imgs, 0, sizeof(_tile_imgs));
|
|
memset(_peer_markers, 0, sizeof(_peer_markers));
|
|
memset(_peer_labels, 0, sizeof(_peer_labels));
|
|
memset(_loaded_tile_x, -1, sizeof(_loaded_tile_x));
|
|
memset(_loaded_tile_y, -1, sizeof(_loaded_tile_y));
|
|
memset(_pending_tiles, 0, sizeof(_pending_tiles));
|
|
_pending_count = 0;
|
|
_last_touch_x = 0;
|
|
_last_touch_y = 0;
|
|
|
|
// Download queue created once; task started/stopped on show/hide
|
|
_download_queue = xQueueCreate(8, sizeof(TileRequest));
|
|
_download_complete = false;
|
|
_download_task = nullptr;
|
|
_task_should_stop = false;
|
|
|
|
LVGL_LOCK();
|
|
|
|
if (parent) {
|
|
_screen = lv_obj_create(parent);
|
|
} else {
|
|
_screen = lv_obj_create(lv_scr_act());
|
|
}
|
|
|
|
lv_obj_set_size(_screen, LV_PCT(100), LV_PCT(100));
|
|
lv_obj_clear_flag(_screen, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_set_style_bg_color(_screen, Theme::surface(), 0);
|
|
lv_obj_set_style_bg_opa(_screen, LV_OPA_COVER, 0);
|
|
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);
|
|
|
|
create_header();
|
|
create_viewport();
|
|
|
|
hide();
|
|
|
|
TRACE("MapScreen created");
|
|
}
|
|
|
|
MapScreen::~MapScreen() {
|
|
// Stop download task if running
|
|
if (_download_task) {
|
|
_task_should_stop = true;
|
|
TileRequest dummy = {0, 0, 0};
|
|
xQueueSend(_download_queue, &dummy, 0);
|
|
vTaskDelay(pdMS_TO_TICKS(100));
|
|
}
|
|
if (_download_queue) {
|
|
vQueueDelete(_download_queue);
|
|
}
|
|
|
|
LVGL_LOCK();
|
|
if (_screen) {
|
|
lv_obj_del(_screen);
|
|
}
|
|
}
|
|
|
|
void MapScreen::create_header() {
|
|
_header = lv_obj_create(_screen);
|
|
lv_obj_set_size(_header, LV_PCT(100), 36);
|
|
lv_obj_align(_header, LV_ALIGN_TOP_MID, 0, 0);
|
|
lv_obj_set_style_bg_color(_header, Theme::surfaceHeader(), 0);
|
|
lv_obj_set_style_border_width(_header, 0, 0);
|
|
lv_obj_set_style_radius(_header, 0, 0);
|
|
lv_obj_set_style_pad_all(_header, 0, 0);
|
|
|
|
// Back button
|
|
lv_obj_t* btn_back = lv_btn_create(_header);
|
|
lv_obj_set_size(btn_back, 50, 28);
|
|
lv_obj_align(btn_back, LV_ALIGN_LEFT_MID, 4, 0);
|
|
lv_obj_set_style_bg_color(btn_back, Theme::btnSecondary(), 0);
|
|
lv_obj_set_style_bg_color(btn_back, Theme::btnSecondaryPressed(), LV_STATE_PRESSED);
|
|
lv_obj_add_event_cb(btn_back, on_back_clicked, LV_EVENT_CLICKED, this);
|
|
|
|
lv_obj_t* label_back = lv_label_create(btn_back);
|
|
lv_label_set_text(label_back, LV_SYMBOL_LEFT);
|
|
lv_obj_center(label_back);
|
|
lv_obj_set_style_text_color(label_back, Theme::textSecondary(), 0);
|
|
|
|
// Zoom label
|
|
_zoom_label = lv_label_create(_header);
|
|
lv_label_set_text(_zoom_label, "z:14");
|
|
lv_obj_align(_zoom_label, LV_ALIGN_LEFT_MID, 62, 0);
|
|
lv_obj_set_style_text_color(_zoom_label, Theme::textPrimary(), 0);
|
|
lv_obj_set_style_text_font(_zoom_label, &lv_font_montserrat_14, 0);
|
|
|
|
// Zoom in button
|
|
lv_obj_t* btn_zin = lv_btn_create(_header);
|
|
lv_obj_set_size(btn_zin, 40, 28);
|
|
lv_obj_align(btn_zin, LV_ALIGN_RIGHT_MID, -48, 0);
|
|
lv_obj_set_style_bg_color(btn_zin, Theme::btnSecondary(), 0);
|
|
lv_obj_set_style_bg_color(btn_zin, Theme::btnSecondaryPressed(), LV_STATE_PRESSED);
|
|
lv_obj_add_event_cb(btn_zin, on_zoom_in_clicked, LV_EVENT_CLICKED, this);
|
|
|
|
lv_obj_t* label_zin = lv_label_create(btn_zin);
|
|
lv_label_set_text(label_zin, "+");
|
|
lv_obj_center(label_zin);
|
|
lv_obj_set_style_text_color(label_zin, Theme::textPrimary(), 0);
|
|
|
|
// Zoom out button
|
|
lv_obj_t* btn_zout = lv_btn_create(_header);
|
|
lv_obj_set_size(btn_zout, 40, 28);
|
|
lv_obj_align(btn_zout, LV_ALIGN_RIGHT_MID, -4, 0);
|
|
lv_obj_set_style_bg_color(btn_zout, Theme::btnSecondary(), 0);
|
|
lv_obj_set_style_bg_color(btn_zout, Theme::btnSecondaryPressed(), LV_STATE_PRESSED);
|
|
lv_obj_add_event_cb(btn_zout, on_zoom_out_clicked, LV_EVENT_CLICKED, this);
|
|
|
|
lv_obj_t* label_zout = lv_label_create(btn_zout);
|
|
lv_label_set_text(label_zout, "-");
|
|
lv_obj_center(label_zout);
|
|
lv_obj_set_style_text_color(label_zout, Theme::textPrimary(), 0);
|
|
}
|
|
|
|
void MapScreen::create_viewport() {
|
|
_viewport = lv_obj_create(_screen);
|
|
lv_obj_set_size(_viewport, VIEWPORT_W, VIEWPORT_H);
|
|
lv_obj_align(_viewport, LV_ALIGN_BOTTOM_MID, 0, 0);
|
|
lv_obj_set_style_bg_color(_viewport, lv_color_hex(0x1a1a2e), 0); // Dark blue for missing tiles
|
|
lv_obj_set_style_border_width(_viewport, 0, 0);
|
|
lv_obj_set_style_radius(_viewport, 0, 0);
|
|
lv_obj_set_style_pad_all(_viewport, 0, 0);
|
|
lv_obj_clear_flag(_viewport, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_set_style_clip_corner(_viewport, true, 0);
|
|
|
|
// Create 4 tile image objects (2x2 grid)
|
|
for (int i = 0; i < 4; i++) {
|
|
_tile_imgs[i] = lv_img_create(_viewport);
|
|
lv_obj_set_size(_tile_imgs[i], TILE_SIZE, TILE_SIZE);
|
|
// Position will be set in update_tiles()
|
|
}
|
|
|
|
// GPS marker: colored circle on top of tiles
|
|
_gps_marker = lv_obj_create(_viewport);
|
|
lv_obj_set_size(_gps_marker, 12, 12);
|
|
lv_obj_set_style_radius(_gps_marker, LV_RADIUS_CIRCLE, 0);
|
|
lv_obj_set_style_bg_color(_gps_marker, Theme::primary(), 0);
|
|
lv_obj_set_style_bg_opa(_gps_marker, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_border_width(_gps_marker, 2, 0);
|
|
lv_obj_set_style_border_color(_gps_marker, lv_color_white(), 0);
|
|
lv_obj_set_style_pad_all(_gps_marker, 0, 0);
|
|
lv_obj_add_flag(_gps_marker, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
// Register key event on viewport for pan/zoom
|
|
lv_obj_add_flag(_viewport, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_add_event_cb(_viewport, on_key_event, LV_EVENT_KEY, this);
|
|
|
|
// Touch drag for panning
|
|
lv_obj_add_event_cb(_viewport, on_touch_event, LV_EVENT_PRESSED, this);
|
|
lv_obj_add_event_cb(_viewport, on_touch_event, LV_EVENT_PRESSING, this);
|
|
}
|
|
|
|
void MapScreen::show() {
|
|
// Start download task (16KB stack only while map is visible)
|
|
if (!_download_task) {
|
|
_task_should_stop = false;
|
|
xQueueReset(_download_queue);
|
|
xTaskCreatePinnedToCore(download_task_func, "tile_dl", 16384, this, 1, &_download_task, 0);
|
|
}
|
|
|
|
LVGL_LOCK();
|
|
lv_obj_clear_flag(_screen, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_move_foreground(_screen);
|
|
|
|
// Register viewport in focus group for key events
|
|
lv_group_t* group = LVGL::LVGLInit::get_default_group();
|
|
if (group) {
|
|
lv_group_add_obj(group, _viewport);
|
|
lv_group_focus_obj(_viewport);
|
|
}
|
|
|
|
// If we have GPS fix and follow mode, center on GPS
|
|
if (_follow_gps && _gps && _gps->location.isValid()) {
|
|
_center_lat = _gps->location.lat();
|
|
_center_lon = _gps->location.lng();
|
|
_has_gps_fix = true;
|
|
}
|
|
|
|
update_tiles();
|
|
position_gps_marker();
|
|
position_peer_markers();
|
|
}
|
|
|
|
void MapScreen::hide() {
|
|
// Stop download task to free 16KB internal RAM
|
|
if (_download_task) {
|
|
_task_should_stop = true;
|
|
// Send dummy request to wake the task from xQueueReceive
|
|
TileRequest dummy = {0, 0, 0};
|
|
xQueueSend(_download_queue, &dummy, 0);
|
|
// Wait for task to exit (up to 2s)
|
|
for (int i = 0; i < 200 && _download_task; i++) {
|
|
vTaskDelay(pdMS_TO_TICKS(10));
|
|
}
|
|
_download_task = nullptr;
|
|
}
|
|
|
|
LVGL_LOCK();
|
|
lv_group_t* group = LVGL::LVGLInit::get_default_group();
|
|
if (group) {
|
|
lv_group_remove_obj(_viewport);
|
|
}
|
|
lv_obj_add_flag(_screen, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
|
|
void MapScreen::update_gps_position() {
|
|
// Reload tiles after background downloads complete
|
|
if (_download_complete) {
|
|
_download_complete = false;
|
|
_loaded_zoom = -1; // Force tile reload
|
|
update_tiles();
|
|
}
|
|
|
|
// Incremental tile loading: process one pending tile per cycle
|
|
// This prevents LVGL mutex timeout from decoding multiple PNGs at once
|
|
if (_pending_count > 0) {
|
|
PendingTile& pt = _pending_tiles[_pending_count - 1];
|
|
load_tile(pt.slot, pt.x, pt.y, pt.z);
|
|
_loaded_tile_x[pt.slot] = pt.x;
|
|
_loaded_tile_y[pt.slot] = pt.y;
|
|
_pending_count--;
|
|
}
|
|
|
|
if (!_gps) return;
|
|
|
|
if (_gps->location.isValid()) {
|
|
_has_gps_fix = true;
|
|
if (_follow_gps) {
|
|
_center_lat = _gps->location.lat();
|
|
_center_lon = _gps->location.lng();
|
|
update_tiles();
|
|
}
|
|
position_gps_marker();
|
|
}
|
|
}
|
|
|
|
void MapScreen::update_peer_locations(const PeerLocation* locations, size_t count) {
|
|
LVGL_LOCK();
|
|
|
|
// Clean up old markers
|
|
for (int i = 0; i < _peer_marker_count; i++) {
|
|
if (_peer_markers[i]) lv_obj_del(_peer_markers[i]);
|
|
if (_peer_labels[i]) lv_obj_del(_peer_labels[i]);
|
|
_peer_markers[i] = nullptr;
|
|
_peer_labels[i] = nullptr;
|
|
}
|
|
|
|
_peer_marker_count = (count > MAX_PEER_MARKERS) ? MAX_PEER_MARKERS : (int)count;
|
|
|
|
// Store locations for repositioning on pan/zoom
|
|
_peer_locations.clear();
|
|
_peer_locations.reserve(_peer_marker_count);
|
|
|
|
for (int i = 0; i < _peer_marker_count; i++) {
|
|
_peer_locations.push_back(locations[i]);
|
|
|
|
// Create marker dot
|
|
_peer_markers[i] = lv_obj_create(_viewport);
|
|
lv_obj_set_size(_peer_markers[i], 10, 10);
|
|
lv_obj_set_style_radius(_peer_markers[i], LV_RADIUS_CIRCLE, 0);
|
|
lv_obj_set_style_bg_color(_peer_markers[i], Theme::success(), 0);
|
|
lv_obj_set_style_bg_opa(_peer_markers[i], LV_OPA_COVER, 0);
|
|
lv_obj_set_style_border_width(_peer_markers[i], 1, 0);
|
|
lv_obj_set_style_border_color(_peer_markers[i], lv_color_white(), 0);
|
|
lv_obj_set_style_pad_all(_peer_markers[i], 0, 0);
|
|
|
|
// Create name label — use display name if available, else truncated hash
|
|
_peer_labels[i] = lv_label_create(_viewport);
|
|
if (!locations[i].name.empty()) {
|
|
lv_label_set_text(_peer_labels[i], locations[i].name.c_str());
|
|
} else {
|
|
char hash_str[12];
|
|
snprintf(hash_str, sizeof(hash_str), "%.6s", locations[i].peer_hash.toHex().c_str());
|
|
lv_label_set_text(_peer_labels[i], hash_str);
|
|
}
|
|
lv_obj_set_style_text_color(_peer_labels[i], Theme::textPrimary(), 0);
|
|
lv_obj_set_style_text_font(_peer_labels[i], &lv_font_montserrat_12, 0);
|
|
}
|
|
|
|
position_peer_markers();
|
|
}
|
|
|
|
void MapScreen::update_tiles() {
|
|
// Convert center lat/lon to global pixel coordinates
|
|
double center_px, center_py;
|
|
TileMath::latlon_to_pixel(_center_lat, _center_lon, _zoom, center_px, center_py);
|
|
|
|
// Top-left corner of viewport in global pixels
|
|
double vp_left = center_px - VIEWPORT_W / 2.0;
|
|
double vp_top = center_py - VIEWPORT_H / 2.0;
|
|
|
|
// Which tile contains the top-left corner
|
|
int base_tile_x = (int)floor(vp_left / TILE_SIZE);
|
|
int base_tile_y = (int)floor(vp_top / TILE_SIZE);
|
|
|
|
// Pixel offset of base tile relative to viewport
|
|
int offset_x = (int)(base_tile_x * TILE_SIZE - vp_left);
|
|
int offset_y = (int)(base_tile_y * TILE_SIZE - vp_top);
|
|
|
|
// Position the 2x2 tile grid
|
|
// Slot layout: [0]=top-left, [1]=top-right, [2]=bottom-left, [3]=bottom-right
|
|
int tile_coords[4][2] = {
|
|
{base_tile_x, base_tile_y},
|
|
{base_tile_x + 1, base_tile_y},
|
|
{base_tile_x, base_tile_y + 1},
|
|
{base_tile_x + 1, base_tile_y + 1}
|
|
};
|
|
|
|
_pending_count = 0;
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
int col = i % 2;
|
|
int row = i / 2;
|
|
int px = offset_x + col * TILE_SIZE;
|
|
int py = offset_y + row * TILE_SIZE;
|
|
lv_obj_set_pos(_tile_imgs[i], px, py);
|
|
|
|
// Only reload if tile changed
|
|
if (_loaded_zoom != _zoom ||
|
|
_loaded_tile_x[i] != tile_coords[i][0] ||
|
|
_loaded_tile_y[i] != tile_coords[i][1]) {
|
|
|
|
int tx = tile_coords[i][0];
|
|
int ty = tile_coords[i][1];
|
|
|
|
// Always queue for incremental loading (one per update cycle)
|
|
// to prevent multiple simultaneous PNG decodes from crashing
|
|
_pending_tiles[_pending_count++] = {i, _zoom, tx, ty};
|
|
|
|
if (!Hardware::TDeck::TileDownloader::tile_exists(_zoom, tx, ty)) {
|
|
// Queue background download for missing tiles
|
|
TileRequest req = {_zoom, tx, ty};
|
|
xQueueSend(_download_queue, &req, 0);
|
|
}
|
|
}
|
|
}
|
|
_loaded_zoom = _zoom;
|
|
}
|
|
|
|
void MapScreen::position_gps_marker() {
|
|
if (!_gps || !_gps->location.isValid()) {
|
|
lv_obj_add_flag(_gps_marker, LV_OBJ_FLAG_HIDDEN);
|
|
return;
|
|
}
|
|
|
|
double gps_px, gps_py;
|
|
TileMath::latlon_to_pixel(_gps->location.lat(), _gps->location.lng(), _zoom, gps_px, gps_py);
|
|
|
|
double center_px, center_py;
|
|
TileMath::latlon_to_pixel(_center_lat, _center_lon, _zoom, center_px, center_py);
|
|
|
|
// Position relative to viewport center
|
|
int screen_x = (int)(gps_px - center_px) + VIEWPORT_W / 2 - 6; // -6 for marker center
|
|
int screen_y = (int)(gps_py - center_py) + VIEWPORT_H / 2 - 6;
|
|
|
|
// Only show if within viewport
|
|
if (screen_x >= -12 && screen_x <= VIEWPORT_W &&
|
|
screen_y >= -12 && screen_y <= VIEWPORT_H) {
|
|
lv_obj_set_pos(_gps_marker, screen_x, screen_y);
|
|
lv_obj_clear_flag(_gps_marker, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_move_foreground(_gps_marker);
|
|
} else {
|
|
lv_obj_add_flag(_gps_marker, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
}
|
|
|
|
void MapScreen::position_peer_markers() {
|
|
double center_px, center_py;
|
|
TileMath::latlon_to_pixel(_center_lat, _center_lon, _zoom, center_px, center_py);
|
|
|
|
for (int i = 0; i < _peer_marker_count && i < (int)_peer_locations.size(); i++) {
|
|
double peer_px, peer_py;
|
|
TileMath::latlon_to_pixel(_peer_locations[i].lat, _peer_locations[i].lon, _zoom, peer_px, peer_py);
|
|
|
|
int screen_x = (int)(peer_px - center_px) + VIEWPORT_W / 2 - 5; // -5 for marker center (10px dot)
|
|
int screen_y = (int)(peer_py - center_py) + VIEWPORT_H / 2 - 5;
|
|
|
|
if (screen_x < -50 || screen_x > VIEWPORT_W + 50 ||
|
|
screen_y < -50 || screen_y > VIEWPORT_H + 50) {
|
|
lv_obj_add_flag(_peer_markers[i], LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_add_flag(_peer_labels[i], LV_OBJ_FLAG_HIDDEN);
|
|
} else {
|
|
lv_obj_clear_flag(_peer_markers[i], LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_clear_flag(_peer_labels[i], LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_set_pos(_peer_markers[i], screen_x, screen_y);
|
|
lv_obj_set_pos(_peer_labels[i], screen_x + 12, screen_y - 2); // label right of dot
|
|
}
|
|
}
|
|
}
|
|
|
|
void MapScreen::update_zoom_label() {
|
|
char buf[8];
|
|
snprintf(buf, sizeof(buf), "z:%d", _zoom);
|
|
lv_label_set_text(_zoom_label, buf);
|
|
}
|
|
|
|
void MapScreen::pan(int dx, int dy) {
|
|
_follow_gps = false; // Manual pan disables follow mode
|
|
|
|
double center_px, center_py;
|
|
TileMath::latlon_to_pixel(_center_lat, _center_lon, _zoom, center_px, center_py);
|
|
|
|
center_px += dx;
|
|
center_py += dy;
|
|
|
|
TileMath::pixel_to_latlon(center_px, center_py, _zoom, _center_lat, _center_lon);
|
|
|
|
update_tiles();
|
|
position_gps_marker();
|
|
position_peer_markers();
|
|
}
|
|
|
|
void MapScreen::load_tile(int slot, int tile_x, int tile_y, int z) {
|
|
char path[64];
|
|
snprintf(path, sizeof(path), "S:tiles/%d/%d/%d.png", z, tile_x, tile_y);
|
|
lv_img_set_src(_tile_imgs[slot], path);
|
|
}
|
|
|
|
void MapScreen::download_task_func(void* param) {
|
|
MapScreen* self = (MapScreen*)param;
|
|
TileRequest req;
|
|
while (!self->_task_should_stop) {
|
|
if (xQueueReceive(self->_download_queue, &req, pdMS_TO_TICKS(500)) == pdTRUE) {
|
|
if (self->_task_should_stop) break;
|
|
if (Hardware::TDeck::TileDownloader::download_tile(req.z, req.x, req.y)) {
|
|
self->_download_complete = true;
|
|
}
|
|
}
|
|
}
|
|
self->_download_task = nullptr;
|
|
vTaskDelete(NULL);
|
|
}
|
|
|
|
void MapScreen::on_touch_event(lv_event_t* event) {
|
|
MapScreen* screen = (MapScreen*)lv_event_get_user_data(event);
|
|
lv_event_code_t code = lv_event_get_code(event);
|
|
lv_indev_t* indev = lv_indev_get_act();
|
|
if (!indev) return;
|
|
|
|
lv_point_t point;
|
|
lv_indev_get_point(indev, &point);
|
|
|
|
if (code == LV_EVENT_PRESSED) {
|
|
screen->_last_touch_x = point.x;
|
|
screen->_last_touch_y = point.y;
|
|
} else if (code == LV_EVENT_PRESSING) {
|
|
int dx = screen->_last_touch_x - point.x;
|
|
int dy = screen->_last_touch_y - point.y;
|
|
if (dx != 0 || dy != 0) {
|
|
screen->pan(dx, dy);
|
|
screen->_last_touch_x = point.x;
|
|
screen->_last_touch_y = point.y;
|
|
}
|
|
}
|
|
}
|
|
|
|
void MapScreen::on_back_clicked(lv_event_t* event) {
|
|
MapScreen* screen = (MapScreen*)lv_event_get_user_data(event);
|
|
if (screen->_back_callback) {
|
|
screen->_back_callback();
|
|
}
|
|
}
|
|
|
|
void MapScreen::on_zoom_in_clicked(lv_event_t* event) {
|
|
MapScreen* screen = (MapScreen*)lv_event_get_user_data(event);
|
|
if (screen->_zoom < 19) {
|
|
screen->_zoom++;
|
|
screen->update_zoom_label();
|
|
// Invalidate loaded tiles to force reload
|
|
screen->_loaded_zoom = -1;
|
|
screen->update_tiles();
|
|
screen->position_gps_marker();
|
|
screen->position_peer_markers();
|
|
}
|
|
}
|
|
|
|
void MapScreen::on_zoom_out_clicked(lv_event_t* event) {
|
|
MapScreen* screen = (MapScreen*)lv_event_get_user_data(event);
|
|
if (screen->_zoom > 1) {
|
|
screen->_zoom--;
|
|
screen->update_zoom_label();
|
|
screen->_loaded_zoom = -1;
|
|
screen->update_tiles();
|
|
screen->position_gps_marker();
|
|
screen->position_peer_markers();
|
|
}
|
|
}
|
|
|
|
void MapScreen::on_key_event(lv_event_t* event) {
|
|
MapScreen* screen = (MapScreen*)lv_event_get_user_data(event);
|
|
uint32_t key = lv_event_get_key(event);
|
|
|
|
const int PAN_STEP = 32; // pixels per key press
|
|
|
|
switch (key) {
|
|
case LV_KEY_UP:
|
|
screen->pan(0, -PAN_STEP);
|
|
break;
|
|
case LV_KEY_DOWN:
|
|
screen->pan(0, PAN_STEP);
|
|
break;
|
|
case LV_KEY_LEFT:
|
|
screen->pan(-PAN_STEP, 0);
|
|
break;
|
|
case LV_KEY_RIGHT:
|
|
screen->pan(PAN_STEP, 0);
|
|
break;
|
|
case '+':
|
|
case '=':
|
|
if (screen->_zoom < 19) {
|
|
screen->_zoom++;
|
|
screen->update_zoom_label();
|
|
screen->_loaded_zoom = -1;
|
|
screen->update_tiles();
|
|
screen->position_gps_marker();
|
|
screen->position_peer_markers();
|
|
}
|
|
break;
|
|
case '-':
|
|
if (screen->_zoom > 1) {
|
|
screen->_zoom--;
|
|
screen->update_zoom_label();
|
|
screen->_loaded_zoom = -1;
|
|
screen->update_tiles();
|
|
screen->position_gps_marker();
|
|
screen->position_peer_markers();
|
|
}
|
|
break;
|
|
case 'c':
|
|
case 'C':
|
|
// Re-center on GPS
|
|
screen->_follow_gps = true;
|
|
if (screen->_gps && screen->_gps->location.isValid()) {
|
|
screen->_center_lat = screen->_gps->location.lat();
|
|
screen->_center_lon = screen->_gps->location.lng();
|
|
screen->update_tiles();
|
|
screen->position_gps_marker();
|
|
screen->position_peer_markers();
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
} // namespace LXMF
|
|
} // namespace UI
|
|
|
|
#endif // ARDUINO
|