diff --git a/CMakeLists.txt b/CMakeLists.txt index d05ab05..4b9b8af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,7 @@ endif() option(ENABLE_DOCTESTS "Include tests in the library. Setting this to OFF will remove all doctest related code. Tests in tests/*.cpp will still be enabled." ${MAIN_PROJECT}) +option(ENABLE_DEBUG_LOG "Enable debug log" OFF) set_property(GLOBAL PROPERTY USE_FOLDERS ON) set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON) # with newer cmake versions put all find_package in global scope @@ -33,10 +34,6 @@ add_compile_definitions(ARDUINO) add_compile_definitions(VIEW_320x240) add_compile_definitions(USE_X11=1) -if(ENABLE_DOCTESTS) - add_compile_definitions(UNIT_TEST) -endif() - include(FetchContent) include(Portduino) include(LovyanGFX) @@ -45,17 +42,12 @@ include(protobuf) include(nanopb) include(Doctest) -# Manually set the Doctest include directory if not set by the Doctest module -if(NOT DEFINED DOCTEST_INCLUDE_DIR) - set(DOCTEST_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/path/to/doctest) # Adjust the path as necessary -endif() - file(GLOB_RECURSE sources source/* generated/* portduino/* locale/* generated/${GENERATED_VIEW}/*) file(GLOB_RECURSE sources_test tests/*.cpp) add_library(DeviceUI ${sources}) target_link_libraries(DeviceUI PRIVATE lvgl::lvgl LovyanGFX Portduino Protobufs) -target_compile_options(DeviceUI PUBLIC -Wall -Wfloat-conversion) +target_compile_options(DeviceUI PUBLIC -Wall -Wno-format -Wfloat-conversion) target_include_directories(DeviceUI PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(DeviceUI PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) @@ -66,6 +58,10 @@ target_include_directories(DeviceUI PRIVATE ${GENERATED_FILES_DIR} ${CMAKE_CURRE if(ENABLE_DOCTESTS) target_include_directories(DeviceUI PRIVATE ${doctest_SOURCE_DIR}) + target_compile_definitions(DeviceUI PRIVATE UNIT_TEST) + if(ENABLE_DEBUG_LOG) + target_compile_definitions(DeviceUI PRIVATE DEBUG_UNIT_TEST) + endif() endif() # diff --git a/include/graphics/map/GeoPoint.h b/include/graphics/map/GeoPoint.h index ec66d8f..0240ef0 100644 --- a/include/graphics/map/GeoPoint.h +++ b/include/graphics/map/GeoPoint.h @@ -28,7 +28,7 @@ class GeoPoint GeoPoint(uint32_t xtile, uint32_t ytile, uint8_t zoom) : xPos(0), yPos(0), xTile(xtile), yTile(ytile), zoomLevel(zoom) { // not used in any scenario yet; so comment out for now - // reverse calculate from tile x/y/z back to lat/long (upper left corner 0/0) + // reverse calculate from tile x/y/z back to lat/lon (upper left corner 0/0) // auto n = 1 << zoom; // longitude = FLOATING_POINT(xTile) / n * FLOATING_POINT(360.0) - FLOATING_POINT(180.0); // latitude = FLOATING_POINT(180.0) / FLOATING_POINT(PI) * diff --git a/include/graphics/map/MapPanel.h b/include/graphics/map/MapPanel.h index 8fff115..94c62c3 100644 --- a/include/graphics/map/MapPanel.h +++ b/include/graphics/map/MapPanel.h @@ -39,7 +39,7 @@ class MapPanel // set new home position void setHomeLocation(float lat, float lon); void setGpsPosition(float lat, float lon); - void scroll(int16_t deltaX, int16_t deltaY, uint16_t fraction = 3); // -1, 0, +1, 1/3 + bool scroll(int16_t deltaX, int16_t deltaY, uint16_t fraction = 3); // -1, 0, +1, 1/3 void moveHome(bool zoomDefault = true); void moveCurrent(void); // placing objects (uses *user_data as internal reference!) @@ -52,6 +52,9 @@ class MapPanel void setGpsPositionImage(lv_obj_t *img); void setNoTileImage(const lv_image_dsc_t *img_src); void forceRedraw(void) { needsRedraw = true; } + bool redrawComplete(void) { return redrawCompleted; } + // for debugging + void printTiles(void); // must be called for incremental drawing of all changes void task_handler(void); ~MapPanel(void); @@ -70,6 +73,7 @@ class MapPanel void drawObject(const MapObject &obj); bool needsRedraw = false; + bool redrawCompleted = true; bool locked = false; // map follows GPS location int16_t widthPixel; // visible panel width diff --git a/include/graphics/map/TileService.h b/include/graphics/map/TileService.h index b23d8c5..9410d41 100644 --- a/include/graphics/map/TileService.h +++ b/include/graphics/map/TileService.h @@ -26,8 +26,8 @@ class TileService : public ITileService { public: TileService(ITileService *s) : ITileService(""), service(s) {} - void setService(ITileService *s); - void setBackupService(ITileService *s); + virtual void setService(ITileService *s); + virtual void setBackupService(ITileService *s); bool load(const char *name, void *img) override { diff --git a/include/graphics/view/TFT/TFTView_320x240.h b/include/graphics/view/TFT/TFTView_320x240.h index 883109a..83fb777 100644 --- a/include/graphics/view/TFT/TFTView_320x240.h +++ b/include/graphics/view/TFT/TFTView_320x240.h @@ -356,6 +356,7 @@ class TFTView_320x240 : public MeshtasticView static void ui_event_backup_restore_radio_button(lv_event_t *e); // map navigation + static void ui_screen_event_cb(lv_event_t *e); static void ui_event_arrow(lv_event_t *e); static void ui_event_navHome(lv_event_t *e); static void ui_event_zoomSlider(lv_event_t *e); diff --git a/include/util/ILog.h b/include/util/ILog.h index 5cde8f2..4efb109 100644 --- a/include/util/ILog.h +++ b/include/util/ILog.h @@ -42,6 +42,39 @@ class ILog #define ILOG_CRIT(...) LOG_CRIT("[DeviceUI] " __VA_ARGS__) #define ILOG_TRACE(...) LOG_TRACE("[DeviceUI] " __VA_ARGS__) +#elif defined(DEBUG_UNIT_TEST) +#include +#define ILOG_DEBUG(...) \ + { \ + printf("DEBUG " __VA_ARGS__); \ + printf("\n"); \ + } +#define ILOG_INFO(...) \ + { \ + printf("INFO " __VA_ARGS__); \ + printf("\n"); \ + } +#define ILOG_WARN(...) \ + { \ + printf("WARN " __VA_ARGS__); \ + printf("\n"); \ + } +#define ILOG_ERROR(...) \ + { \ + printf("ERROR " __VA_ARGS__); \ + printf("\n"); \ + } +#define ILOG_CRIT(...) \ + { \ + printf("CRIT " __VA_ARGS__); \ + printf("\n"); \ + } +#define ILOG_TRACE(...) \ + { \ + printf("TRACE " __VA_ARGS__); \ + printf("\n"); \ + } + #else // no logging #define ILOG_DEBUG(...) diff --git a/source/graphics/TFT/TFTView_320x240.cpp b/source/graphics/TFT/TFTView_320x240.cpp index 1ee8196..8c97848 100644 --- a/source/graphics/TFT/TFTView_320x240.cpp +++ b/source/graphics/TFT/TFTView_320x240.cpp @@ -796,11 +796,12 @@ void TFTView_320x240::ui_events_init(void) lv_obj_add_event_cb(objects.settings_restore_checkbox, ui_event_backup_restore_radio_button, LV_EVENT_ALL, NULL); // map settings and navigation - lv_obj_add_event_cb(objects.arrow_up_button, this->ui_event_arrow, LV_EVENT_CLICKED, (void *)8); - lv_obj_add_event_cb(objects.arrow_left_button, this->ui_event_arrow, LV_EVENT_CLICKED, (void *)4); - lv_obj_add_event_cb(objects.arrow_right_button, this->ui_event_arrow, LV_EVENT_CLICKED, (void *)6); - lv_obj_add_event_cb(objects.arrow_down_button, this->ui_event_arrow, LV_EVENT_CLICKED, (void *)2); - lv_obj_add_event_cb(objects.nav_button, this->ui_event_navHome, LV_EVENT_ALL, NULL); + lv_obj_add_event_cb(objects.main_screen, ui_screen_event_cb, LV_EVENT_GESTURE, NULL); + lv_obj_add_event_cb(objects.arrow_up_button, ui_event_arrow, LV_EVENT_CLICKED, (void *)8); + lv_obj_add_event_cb(objects.arrow_left_button, ui_event_arrow, LV_EVENT_CLICKED, (void *)4); + lv_obj_add_event_cb(objects.arrow_right_button, ui_event_arrow, LV_EVENT_CLICKED, (void *)6); + lv_obj_add_event_cb(objects.arrow_down_button, ui_event_arrow, LV_EVENT_CLICKED, (void *)2); + lv_obj_add_event_cb(objects.nav_button, ui_event_navHome, LV_EVENT_ALL, NULL); lv_obj_add_event_cb(objects.zoom_slider, ui_event_zoomSlider, LV_EVENT_VALUE_CHANGED, NULL); lv_obj_add_event_cb(objects.zoom_in_button, ui_event_zoomIn, LV_EVENT_CLICKED, NULL); lv_obj_add_event_cb(objects.zoom_out_button, ui_event_zoomOut, LV_EVENT_CLICKED, NULL); @@ -2147,9 +2148,34 @@ void TFTView_320x240::ui_event_mapNodeButton(lv_event_t *e) lv_obj_scroll_to_view(panel, LV_ANIM_ON); } +void TFTView_320x240::ui_screen_event_cb(lv_event_t *e) +{ + if (THIS->activePanel == objects.map_panel) { + lv_dir_t dir = lv_indev_get_gesture_dir(lv_indev_active()); + switch (dir) { + case LV_DIR_LEFT: + e->user_data = (void *)6; + break; + case LV_DIR_RIGHT: + e->user_data = (void *)4; + break; + case LV_DIR_TOP: + e->user_data = (void *)2; + break; + case LV_DIR_BOTTOM: + e->user_data = (void *)8; + break; + default: + break; + } + ILOG_DEBUG("gesture: %d", (uint16_t)dir); + THIS->ui_event_arrow(e); + } +} + void TFTView_320x240::ui_event_arrow(lv_event_t *e) { - if (THIS->map) { + if (THIS->map && THIS->map->redrawComplete()) { uint16_t deltaX = 0; uint16_t deltaY = 0; ScrollDirection direction = (ScrollDirection)(unsigned long)e->user_data; @@ -2189,7 +2215,8 @@ void TFTView_320x240::ui_event_arrow(lv_event_t *e) default: break; }; - THIS->map->scroll(deltaX, deltaY); + if (!THIS->map->scroll(deltaX, deltaY)) + THIS->map->forceRedraw(); } THIS->updateLocationMap(THIS->map->getObjectsOnMap()); } @@ -4420,7 +4447,8 @@ void TFTView_320x240::updateNode(uint32_t nodeNum, uint8_t ch, const char *userS lv_label_set_text(objects.basic_settings_user_label, buf); char buf1[30], buf2[40]; - lv_dropdown_set_selected(objects.settings_device_role_dropdown, role2val(meshtastic_Config_DeviceConfig_Role(role)), LV_ANIM_OFF); + lv_dropdown_set_selected(objects.settings_device_role_dropdown, role2val(meshtastic_Config_DeviceConfig_Role(role)), + LV_ANIM_OFF); lv_dropdown_get_selected_str(objects.settings_device_role_dropdown, buf1, sizeof(buf1)); lv_snprintf(buf2, sizeof(buf2), _("Device Role: %s"), buf1); lv_label_set_text(objects.basic_settings_role_label, buf2); diff --git a/source/graphics/map/MapPanel.cpp b/source/graphics/map/MapPanel.cpp index e414fc0..7115c9d 100644 --- a/source/graphics/map/MapPanel.cpp +++ b/source/graphics/map/MapPanel.cpp @@ -1,5 +1,6 @@ #include "graphics/map/MapPanel.h" #include "graphics/map/MapTileSettings.h" +#include "graphics/map/TileService.h" #include "screens.h" #include "util/ILog.h" #include @@ -9,11 +10,15 @@ MapPanel::MapPanel(lv_obj_t *p, ITileService *s) current(home), scrolled(home), panel(p), homeLocationImage(nullptr), gpsPositionImage(nullptr), noTileImage(nullptr), service(new TileService(s)), objectsOnMap(0) { - assert(lv_obj_is_valid(p)); - lv_obj_update_layout(panel); - widthPixel = lv_obj_get_width(panel); - heightPixel = lv_obj_get_height(panel); - ILOG_DEBUG("panel size: %dx%d", widthPixel, heightPixel); + if (p) { + lv_obj_update_layout(panel); + widthPixel = lv_obj_get_width(panel); + heightPixel = lv_obj_get_height(panel); + ILOG_DEBUG("panel size: %dx%d", widthPixel, heightPixel); + } else { + widthPixel = 320; + heightPixel = 240; + } extern OSMTiles *osm; osm = OSMTiles::create([this](const char *name, void *img) -> bool { return service->load(name, img); }); @@ -26,24 +31,35 @@ MapPanel::MapPanel(lv_obj_t *p, ITileService *s) */ void MapPanel::redraw(void) { - static bool done = false; static int16_t x = INT16_MAX; static int16_t y = INT16_MAX; if (needsRedraw) { needsRedraw = false; - done = false; + redrawCompleted = false; x = 0; y = 0; tiles.clear(); } - if (done) + if (redrawCompleted) return; + int16_t size = MapTileSettings::getTileSize(); +#if defined(MAP_FULL_REDRAW) + for (int x = 0; x < tilesX; x++) { + for (int y = 0; y < tilesY; y++) { + uint32_t hash = ((xStart + x) << 16) | (yStart + y); + tiles[hash] = std::move(std::unique_ptr(new MapTile(xStart + x, yStart + y))); + tiles[hash]->load(panel, x * size + xOffset, y * size + yOffset, noTileImage); + } + } + redrawCompleted = true; + drawLocation(); + drawObjects(); +#else // incremental redraw for (int i = 0; i < tilesY; i++) { if (x < tilesX && y < tilesY) { - int16_t size = MapTileSettings::getTileSize(); uint32_t hash = ((xStart + x) << 16) | (yStart + y); tiles[hash] = std::move(std::unique_ptr(new MapTile(xStart + x, yStart + y))); tiles[hash]->load(panel, x * size + xOffset, y * size + yOffset, noTileImage); @@ -53,13 +69,14 @@ void MapPanel::redraw(void) x = 0; y++; if (y >= tilesY) { - done = true; + redrawCompleted = true; drawLocation(); drawObjects(); } } } } +#endif } /** @@ -89,10 +106,12 @@ void MapPanel::drawLocation(void) lv_obj_add_flag(homeLocationImage, LV_OBJ_FLAG_HIDDEN); } } - char buf[30]; - sprintf(buf, "%0.4f %0.4f", scrolled.latitude, scrolled.longitude); - lv_label_set_text(objects.map_location_label, buf); - lv_obj_move_foreground(objects.map_location_label); + if (objects.map_location_label) { + char buf[30]; + sprintf(buf, "%0.4f %0.4f", scrolled.latitude, scrolled.longitude); + lv_label_set_text(objects.map_location_label, buf); + lv_obj_move_foreground(objects.map_location_label); + } } /** @@ -176,8 +195,7 @@ void MapPanel::setGpsPosition(float lat, float lon) { current = GeoPoint(lat, lon, MapTileSettings::getZoomLevel()); if (locked) { - scrolled = current; - center(); + moveCurrent(); } else { drawLocation(); } @@ -220,7 +238,7 @@ void MapPanel::setLocked(bool lock) /** * move map in direction x/y -1, 0, 1 by fraction of panel width but not more than tile size */ -void MapPanel::scroll(int16_t deltaX, int16_t deltaY, uint16_t fraction) +bool MapPanel::scroll(int16_t deltaX, int16_t deltaY, uint16_t fraction) { int16_t size = MapTileSettings::getTileSize(); int16_t scrollX, scrollY; @@ -241,7 +259,7 @@ void MapPanel::scroll(int16_t deltaX, int16_t deltaY, uint16_t fraction) if ((xStart == 0 && scrollX > 0) || (yStart == 0 && scrollY > 0) || (xStart + tilesX > (uint32_t)pow(2, MapTileSettings::getZoomLevel()) && scrollX < 0) || (yStart + tilesY > (uint32_t)pow(2, MapTileSettings::getZoomLevel()) && scrollY < 0)) { - return; + return false; } // check if the scrolling requires new tiles at the beginning row or column @@ -250,19 +268,19 @@ void MapPanel::scroll(int16_t deltaX, int16_t deltaY, uint16_t fraction) MapTile &tile00 = *sit->second; if (tile00.getX() + scrollX > 0) { if (xStart == 0) - return; + return false; xStart--; tilesX++; } if (tile00.getY() + scrollY > 0) { if (yStart == 0) - return; + return false; yStart--; tilesY++; } } else { ILOG_ERROR("scroll: start tile %d/%d missing", xStart, yStart); - return; + return false; } // check if scrolling requires new tiles at the ending row or column auto eit = tiles.find(((xStart + tilesX - 1) << 16) | (yStart + tilesY - 1)); @@ -276,7 +294,7 @@ void MapPanel::scroll(int16_t deltaX, int16_t deltaY, uint16_t fraction) } } else { ILOG_ERROR("scroll: end tile %d/%d missing", xStart + tilesX - 1, yStart + tilesY - 1); - return; + return false; } // calculate new x/y offset of the first entirely visible tile @@ -309,11 +327,11 @@ void MapPanel::scroll(int16_t deltaX, int16_t deltaY, uint16_t fraction) // create new tiles at panel pos x/y int16_t xpos = x * size + xOffset; int16_t ypos = y * size + yOffset; - if ((x == 0 && xpos >= 0) || (x == tilesX - 1 && xpos >= widthPixel)) { + if ((x == 0 && xpos > 0) || (x == tilesX - 1 && xpos >= widthPixel)) { xpos -= size; xOffset -= size; } - if ((y == 0 && ypos >= 0) || (y == tilesY - 1 && ypos >= heightPixel)) { + if ((y == 0 && ypos > 0) || (y == tilesY - 1 && ypos >= heightPixel)) { ypos -= size; yOffset -= size; } @@ -364,12 +382,15 @@ void MapPanel::scroll(int16_t deltaX, int16_t deltaY, uint16_t fraction) if (changeYtiles) tilesY--; - if (tilesX * tilesY != tiles.size()) - ILOG_ERROR("tile size mismatch: %d*%d != %d", tilesX, tilesY, tiles.size()); - scrolled.move(scrollX, scrollY); drawLocation(); drawObjects(); + + if (tilesX * tilesY != tiles.size()) { + ILOG_ERROR("tile size mismatch: %d*%d != %d", tilesX, tilesY, tiles.size()); + return false; + } + return true; } void MapPanel::add(uint32_t id, float lat, float lon, DrawCallback drawCB) @@ -425,3 +446,21 @@ MapPanel::~MapPanel(void) { delete service; } + +#ifdef UNIT_TEST +#include "sstream" +void MapPanel::printTiles(void) +{ + std::stringstream ss; + for (int x = 0; x < tilesX; x++) { + for (int y = 0; y < tilesY; y++) { + uint32_t hash = ((xStart + x) << 16) | (yStart + y); + ss << x << "/" << y << ": " + << "(" << (uint32_t)MapTileSettings::getZoomLevel() << "/" << tiles[hash].get()->xTile << "/" + << tiles[hash].get()->yTile << ") - " << tiles[hash].get()->xPos << "/" << tiles[hash].get()->yPos << " ==> " + << tiles[hash].get()->getX() << "/" << tiles[hash].get()->getY() << std::endl; + } + } + ILOG_DEBUG("tiles: %d\n%s", tiles.size(), ss.str().c_str()); +} +#endif \ No newline at end of file diff --git a/source/graphics/map/MapTile.cpp b/source/graphics/map/MapTile.cpp index 00d99ea..2f1e2da 100644 --- a/source/graphics/map/MapTile.cpp +++ b/source/graphics/map/MapTile.cpp @@ -24,6 +24,8 @@ bool MapTile::load(lv_obj_t *p, int16_t posx, int16_t posy, const lv_image_dsc_t { x = posx; y = posy; + if (!p) + return false; removeImage(); img = lv_image_create(p); lv_obj_set_pos(img, posx, posy); @@ -76,7 +78,8 @@ bool MapTile::move(int16_t posx, int16_t posy) { x += posx; y += posy; - lv_obj_set_pos(img, x, y); + if (img) + lv_obj_set_pos(img, x, y); if (MapTileSettings::getDebug()) { lv_label_set_text_fmt(lbl, "(%d/%d/%d) -> %d,%d", MapTileSettings::getZoomLevel(), xTile, yTile, x, y); } diff --git a/tests/test_MapPanel.cpp b/tests/test_MapPanel.cpp new file mode 100644 index 0000000..490154f --- /dev/null +++ b/tests/test_MapPanel.cpp @@ -0,0 +1,74 @@ +#include "graphics/map/MapPanel.h" +#include + +class TestMapPanel : public MapPanel +{ + public: + TestMapPanel(lv_obj_t *p, ITileService *s = nullptr) : MapPanel(p, s) {} + void setHome(GeoPoint &p) { home = p; } + void setCurrent(GeoPoint &p) { current = p; } + void setScrolled(GeoPoint &p) { scrolled = p; } + void redraw(void) { MapPanel::redraw(); } + void center(void) { MapPanel::center(); } + void setWidthPixel(int16_t width) { widthPixel = width; } + void setHeightPixel(int16_t height) { heightPixel = height; } + void setXStart(uint32_t x) { xStart = x; } + void setYStart(uint32_t y) { yStart = y; } + void setTilesX(uint32_t x) { tilesX = x; } + void setTilesY(uint32_t y) { tilesY = y; } + void setXOffset(int16_t x) { xOffset = x; } + void setYOffset(int16_t y) { yOffset = y; } + uint32_t getXStart() const { return xStart; } + uint32_t getYStart() const { return yStart; } + uint8_t getTilesX() const { return tilesX; } + uint8_t getTilesY() const { return tilesY; } + int16_t getXOffset() const { return xOffset; } + int16_t getYOffset() const { return yOffset; } + + void redrawAll(void) + { + for (int i = 0; i <= tilesX; i++) { + MapPanel::redraw(); + } + } +}; + +TEST_CASE("MapPanel::scroll") +{ + TestMapPanel mapPanel(nullptr, nullptr); + + mapPanel.redrawAll(); + mapPanel.printTiles(); + + SUBCASE("Scroll left by one pixel") + { + for (int i = 0; i < 256; i++) { + CHECK(mapPanel.scroll(-1, 0, 256)); + mapPanel.printTiles(); + } + } + + SUBCASE("Scroll right by one pixel") + { + for (int i = 0; i < 256; i++) { + CHECK(mapPanel.scroll(1, 0, 256)); + mapPanel.printTiles(); + } + } + + SUBCASE("Scroll up by one pixel") + { + for (int i = 0; i < 256; i++) { + CHECK(mapPanel.scroll(0, -1, 256)); + mapPanel.printTiles(); + } + } + + SUBCASE("Scroll down by one pixel") + { + for (int i = 0; i < 256; i++) { + CHECK(mapPanel.scroll(0, 1, 256)); + mapPanel.printTiles(); + } + } +}