mirror of
https://github.com/vicliu624/trail-mate.git
synced 2026-11-11 14:47:39 +00:00
307 lines
13 KiB
C++
307 lines
13 KiB
C++
#include "app/input_event.h"
|
|
#include "platform/ui/tmap_creation_runtime.h"
|
|
#include "ui/app_runtime.h"
|
|
#include "ui/shell_ui_runner.h"
|
|
#include <cassert>
|
|
#include <chrono>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <thread>
|
|
#include <unistd.h>
|
|
|
|
using namespace std::chrono_literals;
|
|
using Key = trailmate::cardputer_zero::app::InputKey;
|
|
namespace tc = platform::ui::tmap_creation;
|
|
struct Service final : tc::Service
|
|
{
|
|
tc::Snapshot state{};
|
|
tc::Plan received{};
|
|
tc::AreaSearchSnapshot search_state{};
|
|
bool ready = false, reject = false;
|
|
unsigned starts = 0;
|
|
Service()
|
|
{
|
|
state.producer_ready = true;
|
|
state.downloader_ready = true;
|
|
state.source_directory = "/home/pi/.trailmate_cardputer_zero/sdcard/map-sources";
|
|
}
|
|
std::vector<tc::Source> sources() override
|
|
{
|
|
return ready ? std::vector<tc::Source>{{"/test.osm.pbf", "test.osm.pbf", 300}} : std::vector<tc::Source>{};
|
|
}
|
|
tc::Estimate estimate(const tc::Plan&) override
|
|
{
|
|
tc::Estimate result;
|
|
result.valid = true;
|
|
result.tiles = 4;
|
|
result.raster_bytes = 524288;
|
|
result.working_bytes = 67108864;
|
|
result.available_bytes = 1024ULL * 1024 * 1024;
|
|
return result;
|
|
}
|
|
bool start(const tc::Plan& plan, std::string& error) override
|
|
{
|
|
if (reject)
|
|
{
|
|
error = "Test producer error";
|
|
return false;
|
|
}
|
|
++starts;
|
|
received = plan;
|
|
state.busy = true;
|
|
state.phase = tc::Phase::Building;
|
|
state.stage = "Rendering map tiles";
|
|
return true;
|
|
}
|
|
tc::Snapshot snapshot() override { return state; }
|
|
bool searchAreas(const std::string& query) override
|
|
{
|
|
if (query.empty()) return false;
|
|
search_state = {search_state.revision + 1, true, false, query, {}, {}};
|
|
return true;
|
|
}
|
|
tc::AreaSearchSnapshot areaSearch() override { return search_state; }
|
|
void cancel() override
|
|
{
|
|
state.busy = false;
|
|
state.phase = tc::Phase::Cancelled;
|
|
state.stage = "Creation cancelled";
|
|
}
|
|
};
|
|
lv_obj_t* label(lv_obj_t* object, const char* text)
|
|
{
|
|
if (lv_obj_has_flag(object, LV_OBJ_FLAG_HIDDEN)) return nullptr;
|
|
if (lv_obj_check_type(object, &lv_label_class) && !std::strcmp(lv_label_get_text(object), text)) return object;
|
|
for (uint32_t i = 0; i < lv_obj_get_child_count(object); ++i)
|
|
if (auto* found = label(lv_obj_get_child(object, i), text)) return found;
|
|
return nullptr;
|
|
}
|
|
void objects(lv_obj_t* object, const lv_obj_class_t* type, std::vector<lv_obj_t*>& output)
|
|
{
|
|
if (lv_obj_check_type(object, type)) output.push_back(object);
|
|
for (uint32_t i = 0; i < lv_obj_get_child_count(object); ++i) objects(lv_obj_get_child(object, i), type, output);
|
|
}
|
|
lv_obj_t* button(const char* text)
|
|
{
|
|
auto* object = label(lv_screen_active(), text);
|
|
assert(object);
|
|
while (object && !lv_obj_check_type(object, &lv_button_class)) object = lv_obj_get_parent(object);
|
|
assert(object);
|
|
return object;
|
|
}
|
|
int main(int argc, char** argv)
|
|
{
|
|
const auto root = std::filesystem::temp_directory_path() / ("trailmate-tmap-keyboard-" + std::to_string(getpid()));
|
|
std::filesystem::create_directories(root);
|
|
setenv("TRAIL_MATE_SETTINGS_ROOT", root.c_str(), 1);
|
|
setenv("TRAIL_MATE_CACHE_ROOT", root.c_str(), 1);
|
|
setenv("TRAIL_MATE_SD_ROOT", (root / "sd").c_str(), 1);
|
|
setenv("TRAIL_MATE_PACK_CATALOG_URL", "http://127.0.0.1:1/catalog.json", 1);
|
|
setenv("TRAIL_MATE_RUNTIME_MODE", "local", 1);
|
|
setenv("TRAIL_MATE_LORA_DISABLE", "1", 1);
|
|
setenv("SDL_AUDIODRIVER", "dummy", 1);
|
|
{
|
|
trailmate::cardputer_zero::linux_ui::ShellSession shell;
|
|
trailmate::cardputer_zero::linux_ui::CanvasLvglHost host(shell);
|
|
assert(shell.begin());
|
|
const auto tick = [&](auto duration)
|
|
{
|
|
const auto end = std::chrono::steady_clock::now() + duration;
|
|
do
|
|
{
|
|
host.tick();
|
|
std::this_thread::sleep_for(5ms);
|
|
} while (std::chrono::steady_clock::now() < end);
|
|
};
|
|
const auto key = [&](Key code, char value = 0)
|
|
{
|
|
shell.enqueueInputs({{code, value ? std::string(1, value) : std::string{}, value}});
|
|
tick(80ms);
|
|
};
|
|
const auto snapshot = [&](const char* name)
|
|
{
|
|
if (argc < 2) return;
|
|
std::filesystem::create_directories(argv[1]);
|
|
host.tick();
|
|
const auto& canvas = host.canvas();
|
|
std::ofstream out(std::filesystem::path(argv[1]) / (std::string(name) + ".ppm"), std::ios::binary);
|
|
out << "P6\n320 170\n255\n";
|
|
for (const auto pixel : canvas.pixels())
|
|
{
|
|
const char rgb[]{char(pixel.r), char(pixel.g), char(pixel.b)};
|
|
out.write(rgb, sizeof(rgb));
|
|
}
|
|
};
|
|
tick(3300ms);
|
|
Service service;
|
|
auto* previous_service = tc::service();
|
|
tc::bind(&service);
|
|
for (unsigned i = 0; i < 10; ++i) key(Key::Next);
|
|
key(Key::Enter);
|
|
tick(300ms);
|
|
key(Key::Character, 't');
|
|
tick(300ms);
|
|
assert(ui_get_active_app() && !std::strcmp(ui_get_active_app()->stable_id(), "extensions"));
|
|
assert(lv_obj_has_state(button("Next"), LV_STATE_DISABLED));
|
|
assert(lv_obj_get_height(button("Search")) == 20);
|
|
const auto activate = [&](const char* caption)
|
|
{
|
|
auto* target = button(caption);
|
|
assert(!lv_obj_has_state(target, LV_STATE_DISABLED));
|
|
unsigned tabs = 0;
|
|
while (lv_group_get_focused(app_g) != target && tabs++ < 24) key(Key::Tab);
|
|
assert(lv_group_get_focused(app_g) == target);
|
|
lv_obj_update_layout(lv_screen_active());
|
|
lv_area_t area{};
|
|
lv_obj_get_coords(target, &area);
|
|
assert(area.x1 >= 0 && area.x2 < 320 && area.y1 >= 0 && area.y2 < 170);
|
|
assert(lv_obj_get_height(target) == 20);
|
|
key(Key::Enter);
|
|
tick(100ms);
|
|
};
|
|
const auto get = [&](const lv_obj_class_t* type)
|
|
{
|
|
std::vector<lv_obj_t*> result;
|
|
// Fields live in the wizard body, alongside the footer.
|
|
objects(lv_obj_get_parent(lv_obj_get_parent(label(lv_screen_active(), "Tab / Enter"))), type, result);
|
|
return result;
|
|
};
|
|
const auto fill = [&](std::initializer_list<const char*> values)
|
|
{
|
|
auto editors = get(&lv_textarea_class);
|
|
assert(editors.size() == values.size());
|
|
size_t index = 0;
|
|
for (const auto* value : values)
|
|
{
|
|
auto* editor = editors[index++];
|
|
unsigned tabs = 0;
|
|
while (lv_group_get_focused(app_g) != editor && tabs++ < 24) key(Key::Tab);
|
|
assert(lv_group_get_focused(app_g) == editor);
|
|
lv_textarea_set_text(editor, "");
|
|
for (const char* character = value; *character; ++character) key(Key::Character, *character);
|
|
assert(!std::strcmp(lv_textarea_get_text(editor), value));
|
|
}
|
|
};
|
|
fill({"test district"});
|
|
key(Key::Enter);
|
|
assert(service.search_state.busy && service.search_state.query == "test district");
|
|
service.search_state.busy = false;
|
|
service.search_state.error = "Test network failure; retry search.";
|
|
++service.search_state.revision;
|
|
tick(350ms);
|
|
assert(label(lv_screen_active(), "Test network failure; retry search."));
|
|
assert(lv_obj_has_state(button("Next"), LV_STATE_DISABLED));
|
|
activate("Search");
|
|
service.search_state.busy = false;
|
|
service.search_state.results = {
|
|
{"R10", "Test district", "Test district, Yunnan, China", "CN", 102.70, 25, 102.71, 25.01},
|
|
{"R11", "Test district", "Test district, California, United States", "US", -122.5, 37.7, -122.4, 37.8}};
|
|
++service.search_state.revision;
|
|
tick(350ms);
|
|
assert(!lv_obj_has_state(button("Next"), LV_STATE_DISABLED));
|
|
snapshot("area-search");
|
|
activate("Next");
|
|
activate("Back");
|
|
activate("Coordinates");
|
|
fill({"", "", "", ""});
|
|
activate("Next"); // Validation stays on Area, with a visible error.
|
|
assert(label(lv_screen_active(), "Enter all four coordinates as decimal degrees."));
|
|
fill({"102.70", "25", "102.71", "25.01"});
|
|
assert(!label(lv_screen_active(), "Enter all four coordinates as decimal degrees."));
|
|
snapshot("area");
|
|
activate("Next");
|
|
auto zooms = get(&lv_dropdown_class);
|
|
assert(zooms.size() == 3);
|
|
lv_dropdown_set_selected(zooms[2], 15);
|
|
lv_obj_send_event(zooms[2], LV_EVENT_VALUE_CHANGED, nullptr);
|
|
activate("Back");
|
|
activate("Next");
|
|
zooms = get(&lv_dropdown_class);
|
|
assert(lv_dropdown_get_selected(zooms[2]) == 15);
|
|
lv_dropdown_set_selected(zooms[2], 14);
|
|
lv_obj_send_event(zooms[2], LV_EVENT_VALUE_CHANGED, nullptr);
|
|
snapshot("layers");
|
|
activate("Next");
|
|
fill({"hwst map", "keyboard-area.tmap"});
|
|
service.ready = true;
|
|
activate("Advanced");
|
|
fill({"hwst map", "keyboard-area.tmap", "keyboard-area", "2"});
|
|
auto budgets = get(&lv_dropdown_class);
|
|
assert(budgets.size() == 2);
|
|
// Exercise actual keyboard selection, including the shared Pager router.
|
|
// A popup's Down key must select an option rather than focus Back.
|
|
unsigned source_tabs = 0;
|
|
while (lv_group_get_focused(app_g) != budgets[1] && source_tabs++ < 24) key(Key::Tab);
|
|
assert(lv_group_get_focused(app_g) == budgets[1]);
|
|
key(Key::Enter);
|
|
assert(lv_dropdown_is_open(budgets[1]));
|
|
key(Key::Down);
|
|
assert(lv_group_get_focused(app_g) == budgets[1]);
|
|
assert(lv_dropdown_get_selected(budgets[1]) == 1);
|
|
key(Key::Enter);
|
|
assert(!lv_dropdown_is_open(budgets[1]));
|
|
key(Key::Enter);
|
|
key(Key::Up);
|
|
key(Key::Enter);
|
|
assert(lv_dropdown_get_selected(budgets[1]) == 0);
|
|
lv_dropdown_set_selected(budgets[0], 0);
|
|
activate("Back");
|
|
activate("Next");
|
|
auto editors = get(&lv_textarea_class);
|
|
assert(!std::strcmp(lv_textarea_get_text(editors[3]), "2"));
|
|
assert(lv_dropdown_get_selected(get(&lv_dropdown_class)[0]) == 0);
|
|
activate("Next");
|
|
snapshot("review");
|
|
service.reject = true;
|
|
activate("Create");
|
|
assert(label(lv_screen_active(), "Test producer error"));
|
|
service.reject = false;
|
|
activate("Create");
|
|
assert(service.starts == 1 && service.received.download_source && service.received.source.empty() && service.received.name == "hwst map" && service.received.revision == 2 &&
|
|
service.received.maximum_bytes == 128ULL * 1024 * 1024 && service.received.max_zoom == 14);
|
|
assert(lv_obj_has_state(button("New map"), LV_STATE_DISABLED));
|
|
activate("Close");
|
|
key(Key::Character, 't');
|
|
tick(300ms);
|
|
assert(label(lv_screen_active(), "Rendering map tiles"));
|
|
service.state.busy = false;
|
|
service.state.phase = tc::Phase::Succeeded;
|
|
service.state.stage = "Map created and available in Map layers";
|
|
service.state.output = "maps/tmap/osm/keyboard-area-osm.tmap";
|
|
tick(350ms);
|
|
snapshot("result");
|
|
const auto bars = get(&lv_bar_class);
|
|
assert(bars.size() == 1 && lv_bar_get_value(bars.front()) == 100);
|
|
assert(lv_obj_has_state(button("Cancel"), LV_STATE_DISABLED));
|
|
activate("View map");
|
|
tick(300ms);
|
|
assert(label(lv_screen_active(), "Map"));
|
|
lv_area_t map_title{};
|
|
lv_obj_get_coords(label(lv_screen_active(), "Map"), &map_title);
|
|
assert(map_title.x1 >= 0 && map_title.x2 < 320 && map_title.y1 >= 0 && map_title.y2 < 170);
|
|
snapshot("view-map");
|
|
assert(!label(lv_screen_active(), "Tab / Enter"));
|
|
key(Key::Home);
|
|
tick(350ms);
|
|
assert(button("View map") && button("New map"));
|
|
activate("Close");
|
|
key(Key::Character, 't');
|
|
tick(300ms);
|
|
assert(!lv_obj_has_state(button("New map"), LV_STATE_DISABLED));
|
|
activate("New map");
|
|
activate("Close");
|
|
service.state.busy = true;
|
|
service.state.phase = tc::Phase::Building;
|
|
key(Key::Character, 't');
|
|
tick(300ms);
|
|
activate("Cancel");
|
|
assert(!service.state.busy && service.state.phase == tc::Phase::Cancelled);
|
|
activate("Close");
|
|
ui_exit_active_app(lv_screen_active());
|
|
tc::bind(previous_service);
|
|
}
|
|
std::filesystem::remove_all(root);
|
|
}
|