feat(travel): Refactor route handling to use direction instead of destination

This commit is contained in:
GW_MC
2026-02-03 20:11:16 +08:00
parent c4635948e4
commit 3617a206ff
9 changed files with 137 additions and 57 deletions

View File

@@ -1,7 +1,5 @@
#pragma once #pragma once
#include "external/mtr/arrival.h"
#include "cJSON.h" #include "cJSON.h"
#include "external/mtr/mtr.h"
#include <string> #include <string>
#include <vector> #include <vector>

View File

@@ -51,3 +51,12 @@ LineInfo::LineInfo(cJSON* line_json) {
ESP_LOGW(LINE_INFO_TAG, "Missing or invalid 'stations' array"); ESP_LOGW(LINE_INFO_TAG, "Missing or invalid 'stations' array");
} }
} }
const char* LineInfo::get_station_name(const std::string& station_code) const {
for (const auto& station : _stations) {
if (std::string(station.code()) == station_code) {
return station.name();
}
}
return "";
}

View File

@@ -35,6 +35,8 @@ public:
return &_stations; return &_stations;
} }
const char* get_station_name(const std::string& station_code) const;
friend class MTRNextTrainHandler; friend class MTRNextTrainHandler;
private: private:

View File

@@ -28,7 +28,8 @@ void SettingHandler::load_settings() {
} }
} }
// Load routes // Load routes - clear existing settings as per new format
routes_.clear();
std::string routes_json = storage_->get(NVS_KEY_ROUTES); std::string routes_json = storage_->get(NVS_KEY_ROUTES);
if (!routes_json.empty()) { if (!routes_json.empty()) {
routes_from_json(routes_json); routes_from_json(routes_json);
@@ -65,7 +66,7 @@ void SettingHandler::add_route(const RoutePair& route) {
} }
routes_.push_back(route); routes_.push_back(route);
ESP_LOGI(TAG, "Added route: %s -> %s", route.station_name.c_str(), route.dest_name.c_str()); ESP_LOGI(TAG, "Added route: %s -> %s", route.station_name.c_str(), route.direction_name.c_str());
} }
void SettingHandler::remove_route(size_t index) { void SettingHandler::remove_route(size_t index) {
@@ -100,8 +101,8 @@ std::string SettingHandler::routes_to_json() const {
cJSON_AddStringToObject(route_obj, "line_color", route.line_color.c_str()); cJSON_AddStringToObject(route_obj, "line_color", route.line_color.c_str());
cJSON_AddStringToObject(route_obj, "station_code", route.station_code.c_str()); cJSON_AddStringToObject(route_obj, "station_code", route.station_code.c_str());
cJSON_AddStringToObject(route_obj, "station_name", route.station_name.c_str()); cJSON_AddStringToObject(route_obj, "station_name", route.station_name.c_str());
cJSON_AddStringToObject(route_obj, "dest_code", route.dest_code.c_str()); cJSON_AddStringToObject(route_obj, "direction", route.direction.c_str());
cJSON_AddStringToObject(route_obj, "dest_name", route.dest_name.c_str()); cJSON_AddStringToObject(route_obj, "direction_name", route.direction_name.c_str());
cJSON_AddItemToArray(root, route_obj); cJSON_AddItemToArray(root, route_obj);
} }
@@ -152,13 +153,13 @@ void SettingHandler::routes_from_json(const std::string& json) {
item = cJSON_GetObjectItem(route_obj, "station_name"); item = cJSON_GetObjectItem(route_obj, "station_name");
if (item && cJSON_IsString(item)) route.station_name = item->valuestring; if (item && cJSON_IsString(item)) route.station_name = item->valuestring;
item = cJSON_GetObjectItem(route_obj, "dest_code"); item = cJSON_GetObjectItem(route_obj, "direction");
if (item && cJSON_IsString(item)) route.dest_code = item->valuestring; if (item && cJSON_IsString(item)) route.direction = item->valuestring;
item = cJSON_GetObjectItem(route_obj, "dest_name"); item = cJSON_GetObjectItem(route_obj, "direction_name");
if (item && cJSON_IsString(item)) route.dest_name = item->valuestring; if (item && cJSON_IsString(item)) route.direction_name = item->valuestring;
if (!route.line_code.empty() && !route.station_code.empty() && !route.dest_code.empty()) { if (!route.line_code.empty() && !route.station_code.empty() && !route.direction.empty()) {
routes_.push_back(route); routes_.push_back(route);
} }
} }

View File

@@ -6,7 +6,7 @@
namespace travel { namespace travel {
/** /**
* @brief Structure representing a monitored route (station -> destination pair) * @brief Structure representing a monitored route (station -> direction pair)
*/ */
struct RoutePair { struct RoutePair {
std::string line_code; // Line code (e.g., "ISL", "TWL") std::string line_code; // Line code (e.g., "ISL", "TWL")
@@ -14,13 +14,13 @@ struct RoutePair {
std::string line_color; // Hex color code (e.g., "#007DC5") std::string line_color; // Hex color code (e.g., "#007DC5")
std::string station_code; // Station code (e.g., "CEN") std::string station_code; // Station code (e.g., "CEN")
std::string station_name; // Station name in TC (e.g., "中環") std::string station_name; // Station name in TC (e.g., "中環")
std::string dest_code; // Destination station code (e.g., "CHW") std::string direction; // Direction terminal code (e.g., "CHW" for "柴灣方向")
std::string dest_name; // Destination station name in TC (e.g., "柴灣") std::string direction_name; // Direction name in TC (e.g., "柴灣方向")
bool operator==(const RoutePair& other) const { bool operator==(const RoutePair& other) const {
return line_code == other.line_code && return line_code == other.line_code &&
station_code == other.station_code && station_code == other.station_code &&
dest_code == other.dest_code; direction == other.direction;
} }
}; };
@@ -28,8 +28,9 @@ struct RoutePair {
* @brief Structure representing a single arrival display entry * @brief Structure representing a single arrival display entry
*/ */
struct ArrivalDisplay { struct ArrivalDisplay {
std::string arrival_time; // Formatted arrival time (e.g., "2分鐘", "14:32") std::string arrival_time; // Formatted arrival time (e.g., "2分鐘")
std::string destination; // Destination station name std::string arrival_time_full; // Full time (e.g., "14:32") if available
std::string direction; // Direction terminal name (e.g., "柴灣方向")
std::string platform; // Platform number if available std::string platform; // Platform number if available
}; };
@@ -38,7 +39,7 @@ struct ArrivalDisplay {
*/ */
struct RouteArrivalData { struct RouteArrivalData {
RoutePair route; RoutePair route;
std::vector<ArrivalDisplay> arrivals; // List of upcoming trains to destination std::vector<ArrivalDisplay> arrivals; // List of upcoming trains in direction
bool is_valid = false; bool is_valid = false;
std::string error_message; std::string error_message;
}; };

View File

@@ -179,7 +179,7 @@ void MainUI::update_route_display_(RouteDisplay& display, const RouteArrivalData
lv_obj_clear_flag(display.container, LV_OBJ_FLAG_HIDDEN); lv_obj_clear_flag(display.container, LV_OBJ_FLAG_HIDDEN);
// Update header with line color // Update header with line color
std::string header_text = data.route.station_name + "" + data.route.dest_name; std::string header_text = data.route.station_name + "" + data.route.direction_name;
lv_label_set_text(display.header, header_text.c_str()); lv_label_set_text(display.header, header_text.c_str());
if (!data.route.line_color.empty()) { if (!data.route.line_color.empty()) {
@@ -191,7 +191,11 @@ void MainUI::update_route_display_(RouteDisplay& display, const RouteArrivalData
for (int i = 0; i < MAX_ARRIVALS_PER_ROUTE; i++) { for (int i = 0; i < MAX_ARRIVALS_PER_ROUTE; i++) {
if (i < static_cast<int>(data.arrivals.size())) { if (i < static_cast<int>(data.arrivals.size())) {
const auto& arrival = data.arrivals[i]; const auto& arrival = data.arrivals[i];
std::string arrival_text = " " + arrival.arrival_time + "" + arrival.destination; std::string arrival_text = " " + arrival.arrival_time;
if (!arrival.arrival_time_full.empty()) {
arrival_text += " (" + arrival.arrival_time_full + ")";
}
arrival_text += " " + arrival.direction;
lv_label_set_text(display.arrival_labels[i], arrival_text.c_str()); lv_label_set_text(display.arrival_labels[i], arrival_text.c_str());
lv_obj_clear_flag(display.arrival_labels[i], LV_OBJ_FLAG_HIDDEN); lv_obj_clear_flag(display.arrival_labels[i], LV_OBJ_FLAG_HIDDEN);
} else { } else {

View File

@@ -1,6 +1,6 @@
#include "ui/apps/travel/ui/main_handler.h" #include "ui/apps/travel/ui/main_handler.h"
#include "display/lvgl_handler.h" #include "display/lvgl_handler.h"
#include "external/mtr/arrival.h" #include "external/mtr/line_info.h"
#include "esp_log.h" #include "esp_log.h"
#include <ctime> #include <ctime>
#include <iomanip> #include <iomanip>
@@ -153,28 +153,65 @@ void MainUIHandler::fetch_and_update_arrivals_() {
); );
if (error == MtrArrivalErrorCode::NONE && arrival_info) { if (error == MtrArrivalErrorCode::NONE && arrival_info) {
// Filter arrivals going to our destination // Determine which direction (UP or DOWN) to filter based on terminal station
const auto* up_arrivals = arrival_info->up_arrivals(); const auto* up_arrivals = arrival_info->up_arrivals();
const auto* down_arrivals = arrival_info->down_arrivals(); const auto* down_arrivals = arrival_info->down_arrivals();
// Check both UP and DOWN directions for trains to our destination // Get all lines to find station positions
auto filter_arrivals = [&](const std::vector<ArrivalInfo>* arrivals) { std::vector<LineInfo> lines = mtr_handler_->get_lines();
// Find current line and determine direction
bool filter_up = false;
bool filter_down = false;
for (const auto& line : lines) {
if (std::string(line.code()) == line_code) {
const auto* stations = line.stations();
if (stations) {
// Find index of current station and terminal station
size_t current_idx = SIZE_MAX;
size_t terminal_idx = SIZE_MAX;
for (size_t i = 0; i < stations->size(); i++) {
if (std::string(stations->at(i).code()) == station_code) {
current_idx = i;
}
if (std::string(stations->at(i).code()) == route.direction) {
terminal_idx = i;
}
}
// Determine direction: if terminal is at higher index, it's DOWN
if (current_idx != SIZE_MAX && terminal_idx != SIZE_MAX) {
if (terminal_idx > current_idx) {
filter_down = true;
} else {
filter_up = true;
}
}
}
break;
}
}
// Filter arrivals based on direction
auto process_arrivals = [&](const std::vector<ArrivalInfo>* arrivals) {
if (!arrivals) return; if (!arrivals) return;
for (const auto& arrival : *arrivals) { for (const auto& arrival : *arrivals) {
// Check if this train goes to our destination
std::string dest = arrival.destination();
if (dest.find(route.dest_name) != std::string::npos ||
dest.find(route.dest_code) != std::string::npos) {
ArrivalDisplay display; ArrivalDisplay display;
display.arrival_time = format_arrival_time_(arrival.arrival_time()); display.arrival_time = format_arrival_time_(arrival.arrival_time());
display.destination = dest; display.arrival_time_full = format_arrival_time_full_(arrival.arrival_time());
display.direction = route.direction_name;
data.arrivals.push_back(display); data.arrivals.push_back(display);
} }
}
}; };
filter_arrivals(up_arrivals); if (filter_up) {
filter_arrivals(down_arrivals); process_arrivals(up_arrivals);
}
if (filter_down) {
process_arrivals(down_arrivals);
}
data.is_valid = true; data.is_valid = true;
@@ -224,6 +261,21 @@ std::string MainUIHandler::format_arrival_time_(const std::string& api_time) {
return api_time; return api_time;
} }
std::string MainUIHandler::format_arrival_time_full_(const std::string& api_time) {
// Returns absolute time for display (e.g., "14:32")
// Returns empty string for relative times
if (api_time.length() <= 2) {
return "";
}
size_t t_pos = api_time.find('T');
if (t_pos != std::string::npos && api_time.length() > t_pos + 5) {
return api_time.substr(t_pos + 1, 5);
}
return "";
}
std::string MainUIHandler::get_current_time_string_() { std::string MainUIHandler::get_current_time_string_() {
auto now = std::time(nullptr); auto now = std::time(nullptr);
auto tm = *std::localtime(&now); auto tm = *std::localtime(&now);

View File

@@ -3,6 +3,7 @@
#include "ui/apps/travel/ui/main.h" #include "ui/apps/travel/ui/main.h"
#include "ui/apps/travel/settings/settings_handler.h" #include "ui/apps/travel/settings/settings_handler.h"
#include "ui/interaction_handler.h" #include "ui/interaction_handler.h"
#include "external/mtr/arrival.h"
#include "external/mtr/mtr.h" #include "external/mtr/mtr.h"
#include "network/network.h" #include "network/network.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
@@ -47,6 +48,7 @@ private:
void on_settings_button_clicked_(); void on_settings_button_clicked_();
void fetch_and_update_arrivals_(); void fetch_and_update_arrivals_();
std::string format_arrival_time_(const std::string& api_time); std::string format_arrival_time_(const std::string& api_time);
std::string format_arrival_time_full_(const std::string& api_time);
std::string get_current_time_string_(); std::string get_current_time_string_();
std::unique_ptr<MainUI> main_ui_; std::unique_ptr<MainUI> main_ui_;

View File

@@ -256,8 +256,8 @@ esp_err_t WebHandler::settings_page_handler_(httpd_req_t* req) {
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label></label> <label></label>
<select id="dest_select"> <select id="direction_select">
<option value=""></option> <option value=""></option>
</select> </select>
</div> </div>
@@ -327,11 +327,11 @@ esp_err_t WebHandler::settings_page_handler_(httpd_req_t* req) {
function updateStations() { function updateStations() {
const lineCode = document.getElementById('line_select').value; const lineCode = document.getElementById('line_select').value;
const stationSelect = document.getElementById('station_select'); const stationSelect = document.getElementById('station_select');
const destSelect = document.getElementById('dest_select'); const directionSelect = document.getElementById('direction_select');
if (!lineCode) { if (!lineCode) {
stationSelect.innerHTML = '<option value=""></option>'; stationSelect.innerHTML = '<option value=""></option>';
destSelect.innerHTML = '<option value=""></option>'; directionSelect.innerHTML = '<option value=""></option>';
return; return;
} }
@@ -343,7 +343,18 @@ esp_err_t WebHandler::settings_page_handler_(httpd_req_t* req) {
).join(''); ).join('');
stationSelect.innerHTML = '<option value=""></option>' + stationsHtml; stationSelect.innerHTML = '<option value=""></option>' + stationsHtml;
destSelect.innerHTML = '<option value=""></option>' + stationsHtml;
const stations = line.stations;
if (stations.length >= 2) {
const firstStation = stations[0];
const lastStation = stations[stations.length - 1];
const directionsHtml =
`<option value="${firstStation.code}">${firstStation.name}</option>` +
`<option value="${lastStation.code}">${lastStation.name}</option>`;
directionSelect.innerHTML = '<option value=""></option>' + directionsHtml;
} else {
directionSelect.innerHTML = '<option value=""></option>';
}
} }
function renderRoutes() { function renderRoutes() {
@@ -357,7 +368,7 @@ esp_err_t WebHandler::settings_page_handler_(httpd_req_t* req) {
<div class="route-item"> <div class="route-item">
<div class="route-info"> <div class="route-info">
<span class="route-line" style="background: ${route.line_color}">${route.line_name}</span> <span class="route-line" style="background: ${route.line_color}">${route.line_name}</span>
${route.station_name} ${route.dest_name} ${route.station_name} ${route.direction_name}
</div> </div>
<button class="danger" style="width: auto; padding: 5px 10px;" <button class="danger" style="width: auto; padding: 5px 10px;"
onclick="removeRoute(${index})"></button> onclick="removeRoute(${index})"></button>
@@ -368,21 +379,21 @@ esp_err_t WebHandler::settings_page_handler_(httpd_req_t* req) {
async function addRoute() { async function addRoute() {
const lineCode = document.getElementById('line_select').value; const lineCode = document.getElementById('line_select').value;
const stationCode = document.getElementById('station_select').value; const stationCode = document.getElementById('station_select').value;
const destCode = document.getElementById('dest_select').value; const directionCode = document.getElementById('direction_select').value;
if (!lineCode || !stationCode || !destCode) { if (!lineCode || !stationCode || !directionCode) {
showStatus('', 'error'); showStatus('', 'error');
return; return;
} }
if (stationCode === destCode) { if (stationCode === directionCode) {
showStatus('', 'error'); showStatus('', 'error');
return; return;
} }
const line = linesData.find(l => l.code === lineCode); const line = linesData.find(l => l.code === lineCode);
const station = line.stations.find(s => s.code === stationCode); const station = line.stations.find(s => s.code === stationCode);
const dest = line.stations.find(s => s.code === destCode); const direction = line.stations.find(s => s.code === directionCode);
const route = { const route = {
line_code: lineCode, line_code: lineCode,
@@ -390,8 +401,8 @@ esp_err_t WebHandler::settings_page_handler_(httpd_req_t* req) {
line_color: line.color, line_color: line.color,
station_code: stationCode, station_code: stationCode,
station_name: station.name, station_name: station.name,
dest_code: destCode, direction: directionCode,
dest_name: dest.name direction_name: direction.name + ''
}; };
try { try {
@@ -536,8 +547,8 @@ esp_err_t WebHandler::get_routes_handler_(httpd_req_t* req) {
cJSON_AddStringToObject(route_obj, "line_color", route.line_color.c_str()); cJSON_AddStringToObject(route_obj, "line_color", route.line_color.c_str());
cJSON_AddStringToObject(route_obj, "station_code", route.station_code.c_str()); cJSON_AddStringToObject(route_obj, "station_code", route.station_code.c_str());
cJSON_AddStringToObject(route_obj, "station_name", route.station_name.c_str()); cJSON_AddStringToObject(route_obj, "station_name", route.station_name.c_str());
cJSON_AddStringToObject(route_obj, "dest_code", route.dest_code.c_str()); cJSON_AddStringToObject(route_obj, "direction", route.direction.c_str());
cJSON_AddStringToObject(route_obj, "dest_name", route.dest_name.c_str()); cJSON_AddStringToObject(route_obj, "direction_name", route.direction_name.c_str());
cJSON_AddItemToArray(routes_arr, route_obj); cJSON_AddItemToArray(routes_arr, route_obj);
} }
cJSON_AddItemToObject(root, "routes", routes_arr); cJSON_AddItemToObject(root, "routes", routes_arr);
@@ -603,15 +614,15 @@ esp_err_t WebHandler::add_route_handler_(httpd_req_t* req) {
item = cJSON_GetObjectItem(root, "station_name"); item = cJSON_GetObjectItem(root, "station_name");
if (item && cJSON_IsString(item)) route.station_name = item->valuestring; if (item && cJSON_IsString(item)) route.station_name = item->valuestring;
item = cJSON_GetObjectItem(root, "dest_code"); item = cJSON_GetObjectItem(root, "direction");
if (item && cJSON_IsString(item)) route.dest_code = item->valuestring; if (item && cJSON_IsString(item)) route.direction = item->valuestring;
item = cJSON_GetObjectItem(root, "dest_name"); item = cJSON_GetObjectItem(root, "direction_name");
if (item && cJSON_IsString(item)) route.dest_name = item->valuestring; if (item && cJSON_IsString(item)) route.direction_name = item->valuestring;
cJSON_Delete(root); cJSON_Delete(root);
if (route.line_code.empty() || route.station_code.empty() || route.dest_code.empty()) { if (route.line_code.empty() || route.station_code.empty() || route.direction.empty()) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing required fields"); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing required fields");
return ESP_FAIL; return ESP_FAIL;
} }