Something went wrong. Try again.
Live STIB-MIVB departures on the Pebble Time 2. It covers the bus, tram, and metro network of Brussels
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570#include "line_window.h"#include "alarm_target.h"#include "line_state.h"#include "anim.h"#include "badge.h"#include "detail_surface.h"#include "labels.h"#include "model.h"#include "msg.h"#include "disruption_window.h"#include "touch.h"
#define HEADER_H 54#define BANNER_H 40#define CHEVRON_W 14#define LINE_X 30#define RIGHT_COL_W 58#define BADGE_W 48#define BADGE_H 32#define SHAPE_MAX_POINTS 24#define LINE_TIMEOUT_MS 8000#define COUNTDOWN_H 76#define MINI_MAX_H 64#define MINI_MIN_H 24
static Window *s_window;static Layer *s_content;static Layer *s_countdown;static GRect s_countdown_rest;static char s_stop_id[ID_LEN];static char s_line[LINE_LEN];static char s_dest[NAME_LEN];static bool s_flash;static bool s_was_loading;static int16_t s_shown_min1;static LineFsm s_fsm;static AppTimer *s_timeout_timer;static GPoint s_shape_points[SHAPE_MAX_POINTS];static LineShapePoint s_shape_scaled[SHAPE_MAX_POINTS];static LineDetail s_backup;static GBitmap *s_alarm_icon;static Label s_reason;static bool s_reason_on;static AppTimer *s_reason_timer;
#define REASON_MS 1500
static LineDetail *active_detail(void) { return s_fsm.state == LINE_FSM_REFRESHING ? &s_backup : model_line_detail();}
static bool showing_content(void) { return s_fsm.state == LINE_FSM_COMPLETE || s_fsm.state == LINE_FSM_REFRESHING;}
static GColor line_accent(void) { LineDetail *detail = active_detail(); if (detail->header_received) { return detail->color; } Stop *stop = model_stop_by_id(PAGE_TRANSIT, s_stop_id); if (stop) { for (uint8_t j = 0; j < stop->row_count; j++) { if (strcmp(stop->rows[j].line, s_line) == 0) { return stop->rows[j].color; } } } return GColorLightGray;}
static void sync_loading(void) { bool loading = s_content && s_fsm.state == LINE_FSM_LOADING; if (loading == s_was_loading) { return; } s_was_loading = loading; if (loading) { anim_loading_start(s_content); } else { anim_loading_stop(); }}
static void maybe_slide(void) { if (!s_countdown || !showing_content()) { return; } int16_t cur = line_countdown_minutes(active_detail()->min1, s_fsm.completed_at, time(NULL)); if (cur >= 0 && s_shown_min1 >= 0 && cur != s_shown_min1) { anim_slide_start(s_countdown, s_countdown_rest); } s_shown_min1 = cur;}
static void cancel_timeout(void) { if (s_timeout_timer) { app_timer_cancel(s_timeout_timer); s_timeout_timer = NULL; }}
static void timeout_handler(void *data);
static void apply(LineFsmActions a) { if (a.cancel_timer) { cancel_timeout(); } if (a.restore_backup) { *model_line_detail() = s_backup; } if (a.save_backup) { s_backup = *model_line_detail(); model_line_begin(); } if (a.send_request) { msg_request_line(s_stop_id, s_line, s_dest); } if (a.arm_timer) { s_timeout_timer = app_timer_register(LINE_TIMEOUT_MS, timeout_handler, NULL); } if (a.redraw && s_content) { layer_mark_dirty(s_content); layer_mark_dirty(s_countdown); }}
static void timeout_handler(void *data) { s_timeout_timer = NULL; apply(line_fsm_timeout(&s_fsm));}
static void draw_header(GContext *ctx, GRect bounds, LineDetail *detail) { graphics_context_set_fill_color(ctx, detail->color); graphics_fill_rect(ctx, bounds, 0, GCornerNone);
GRect badge = GRect(bounds.origin.x + 8, bounds.origin.y + (bounds.size.h - BADGE_H) / 2, BADGE_W, BADGE_H); draw_line_badge(ctx, badge, detail->line, detail->color, detail->text_color); graphics_context_set_stroke_color(ctx, detail->text_color); graphics_context_set_stroke_width(ctx, 2); graphics_draw_round_rect(ctx, badge, 6);
int16_t dest_x = badge.origin.x + BADGE_W + 8; graphics_context_set_text_color(ctx, detail->text_color); graphics_draw_text(ctx, detail->destination, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD), GRect(dest_x, bounds.origin.y + (bounds.size.h - 22) / 2, bounds.origin.x + bounds.size.w - dest_x - 4, 22), GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL); draw_clock(ctx, bounds, detail->text_color);}
static void draw_shape(GContext *ctx, GRect body, LineDetail *detail) { int16_t top = body.origin.y + COUNTDOWN_H + 2; int16_t bottom = body.origin.y + body.size.h - 4; int16_t box_h = bottom - top; if (box_h < MINI_MIN_H) { return; } if (box_h > MINI_MAX_H) { top = bottom - MINI_MAX_H; box_h = MINI_MAX_H; } GRect box = GRect(body.size.w - RIGHT_COL_W + 4, top, RIGHT_COL_W - 12, box_h); uint8_t point_count = line_shape_project(detail->shape, detail->shape_len, box.size.w - 4, box.size.h - 4, s_shape_scaled, SHAPE_MAX_POINTS); if (point_count < 2) { return; } for (uint8_t i = 0; i < point_count; i++) { s_shape_points[i] = GPoint(box.origin.x + 2 + s_shape_scaled[i].x, box.origin.y + 2 + s_shape_scaled[i].y); } GPath path = { .num_points = point_count, .points = s_shape_points, .rotation = 0, .offset = GPointZero }; graphics_context_set_stroke_color(ctx, GColorDarkGray); graphics_context_set_stroke_width(ctx, 1); gpath_draw_outline_open(ctx, &path);}
static void draw_strip(GContext *ctx, GRect body, LineDetail *detail) { int16_t strip_w = body.size.w - RIGHT_COL_W; if (detail->strip_count == 0) { detail_surface_draw_empty(ctx, GRect(0, body.origin.y, strip_w, body.size.h), LABEL_NO_LINE, GTextAlignmentCenter); return; } uint8_t start = 0; uint8_t visible = detail->strip_count; int16_t row_h = body.size.h / visible; if (row_h < 14) { visible = body.size.h / 14; if (visible == 0) { visible = 1; } if (visible > detail->strip_count) { visible = detail->strip_count; } start = detail->strip_count - visible; row_h = body.size.h / visible; } if (row_h > 34) { row_h = 34; }
graphics_context_set_stroke_color(ctx, GColorDarkGray); graphics_context_set_stroke_width(ctx, 1); graphics_draw_line(ctx, GPoint(LINE_X, body.origin.y + 4), GPoint(LINE_X, body.origin.y + row_h * visible - 4));
for (uint8_t i = 0; i < visible; i++) { StripEntry *entry = &detail->strip[start + i]; int16_t y = body.origin.y + i * row_h; int16_t cy = y + row_h / 2; GColor name_color = GColorLightGray; GFont name_font = fonts_get_system_font(FONT_KEY_GOTHIC_14);
if (entry->marker == 2) { GPoint center = GPoint(LINE_X, cy); graphics_context_set_fill_color(ctx, detail->color); graphics_fill_circle(ctx, center, 9); graphics_context_set_fill_color(ctx, GColorWhite); graphics_fill_circle(ctx, center, 6); name_color = GColorWhite; name_font = fonts_get_system_font(FONT_KEY_GOTHIC_14_BOLD); } else if (entry->marker == 1) { graphics_context_set_fill_color(ctx, detail->color); graphics_fill_circle(ctx, GPoint(LINE_X, cy), 6 + anim_pulse_delta()); name_color = GColorWhite; } else { GPoint center = GPoint(LINE_X, cy); graphics_context_set_fill_color(ctx, GColorBlack); graphics_fill_circle(ctx, center, 4); graphics_context_set_stroke_color(ctx, GColorLightGray); graphics_context_set_stroke_width(ctx, 1); graphics_draw_circle(ctx, center, 4); }
graphics_context_set_text_color(ctx, name_color); graphics_draw_text(ctx, entry->name, name_font, GRect(LINE_X + 12, y + 1, strip_w - LINE_X - 16, 16), GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL);
if (entry->marker == 1 && row_h >= 30) { graphics_context_set_text_color(ctx, detail->color); graphics_draw_text(ctx, "vehicle", fonts_get_system_font(FONT_KEY_GOTHIC_14), GRect(LINE_X + 12, y + 16, strip_w - LINE_X - 16, 14), GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL); } }}
static GFont countdown_pick_font(const char *buf, GRect *box) { GFont primary = fonts_get_system_font(FONT_KEY_BITHAM_42_BOLD); *box = GRect(20, 8, RIGHT_COL_W - 4, 46); GSize sz = graphics_text_layout_get_content_size(buf, primary, GRect(0, 0, 200, 60), GTextOverflowModeFill, GTextAlignmentLeft); if (sz.w <= box->size.w) { return primary; } int16_t right = box->origin.x + box->size.w; int16_t wide_w = RIGHT_COL_W + 12; *box = GRect(right - wide_w, 12, wide_w, 42); return fonts_get_system_font(FONT_KEY_BITHAM_30_BLACK);}
static void countdown_update(Layer *layer, GContext *ctx) { if (!showing_content() || s_flash) { return; } LineDetail *detail = active_detail(); GRect bounds = layer_get_bounds(layer); char buf[16]; bool stale = model_stale(PAGE_TRANSIT, time(NULL)); int16_t min1 = line_countdown_minutes(detail->min1, s_fsm.completed_at, time(NULL)); int16_t min2 = line_countdown_minutes(detail->min2, s_fsm.completed_at, time(NULL)); bool armed = alarm_target_armed(s_stop_id, s_line) && s_alarm_icon; GRect bell = GRect(2, 24, 0, 0); if (armed) { bell.size = gbitmap_get_bounds(s_alarm_icon).size; } if (min1 >= 0) { graphics_context_set_text_color(ctx, stale ? GColorLightGray : GColorWhite); snprintf(buf, sizeof(buf), "%d'", min1); GRect box; GFont font = countdown_pick_font(buf, &box); graphics_draw_text(ctx, buf, font, box, GTextOverflowModeTrailingEllipsis, GTextAlignmentRight, NULL); if (armed) { GSize sz = graphics_text_layout_get_content_size(buf, font, GRect(0, 0, 200, 60), GTextOverflowModeFill, GTextAlignmentLeft); bell.origin.x = box.origin.x + box.size.w - sz.w - 4 - bell.size.w; bell.origin.y = box.origin.y + (sz.h - bell.size.h) / 2 + sz.h / 5; } } if (armed) { graphics_context_set_compositing_mode(ctx, GCompOpSet); graphics_draw_bitmap_in_rect(ctx, s_alarm_icon, bell); } if (s_reason_on) { detail_surface_draw_reason(ctx, GRect(bounds.size.w - 4 - 150, 56, 150, 16), s_reason); } else if (min2 >= 0) { graphics_context_set_text_color(ctx, stale ? GColorDarkGray : GColorLightGray); snprintf(buf, sizeof(buf), "then %d'", min2); graphics_draw_text(ctx, buf, fonts_get_system_font(FONT_KEY_GOTHIC_14), GRect(0, 56, bounds.size.w - 4, 16), GTextOverflowModeTrailingEllipsis, GTextAlignmentRight, NULL); }}
static void draw_chevron(GContext *ctx, GPoint center) { graphics_context_set_stroke_color(ctx, GColorBlack); graphics_context_set_stroke_width(ctx, 2); graphics_draw_line(ctx, GPoint(center.x - 4, center.y - 2), GPoint(center.x, center.y + 2)); graphics_draw_line(ctx, GPoint(center.x, center.y + 2), GPoint(center.x + 4, center.y - 2));}
static void draw_banner(GContext *ctx, GRect bounds, LineDetail *detail) { GRect banner = GRect(0, bounds.size.h - BANNER_H, bounds.size.w, BANNER_H); graphics_context_set_fill_color(ctx, GColorChromeYellow); graphics_fill_rect(ctx, banner, 0, GCornerNone); graphics_context_set_text_color(ctx, GColorBlack); GFont font = fonts_get_system_font(FONT_KEY_GOTHIC_14); int16_t full_w = bounds.size.w - 12; GSize natural = graphics_text_layout_get_content_size(detail->disruption, font, GRect(0, 0, full_w * 4, BANNER_H), GTextOverflowModeFill, GTextAlignmentLeft); bool overflow = natural.w > full_w; int16_t text_w = overflow ? full_w - CHEVRON_W : full_w; graphics_draw_text(ctx, detail->disruption, font, GRect(6, banner.origin.y + 2, text_w, BANNER_H - 4), GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL); if (overflow) { draw_chevron(ctx, GPoint(bounds.size.w - 10, banner.origin.y + BANNER_H - 10)); }}
static void content_update(Layer *layer, GContext *ctx) { GRect bounds = layer_get_bounds(layer); graphics_context_set_fill_color(ctx, GColorBlack); graphics_fill_rect(ctx, bounds, 0, GCornerNone);
if (!showing_content() || s_flash) { Label label = s_fsm.state == LINE_FSM_FAILED ? LABEL_NO_NETWORK : s_fsm.state == LINE_FSM_UNKNOWN ? LABEL_NO_LINE : LABEL_LOADING; if (!s_flash && label == LABEL_LOADING) { anim_draw_loading(ctx, bounds, line_accent()); return; } detail_surface_draw_empty(ctx, bounds, label, GTextAlignmentCenter); return; }
LineDetail *detail = active_detail(); draw_header(ctx, GRect(0, 0, bounds.size.w, HEADER_H), detail); bool has_banner = detail->disruption[0] != '\0'; int16_t body_h = bounds.size.h - HEADER_H - (has_banner ? BANNER_H : 0); GRect body = GRect(0, HEADER_H, bounds.size.w, body_h); draw_shape(ctx, body, detail); draw_strip(ctx, body, detail); if (has_banner) { draw_banner(ctx, bounds, detail); }}
static uint16_t stop_distance(void) { Stop *stop = model_stop_by_id(PAGE_TRANSIT, s_stop_id); return stop ? stop->distance_m : 0;}
static void flash_timeout(void *data) { s_flash = false; if (s_content) { layer_mark_dirty(s_content); }}
static void flash_cannot(void) { s_flash = true; if (s_content) { layer_mark_dirty(s_content); } app_timer_register(700, flash_timeout, NULL);}
static void reason_timeout(void *data) { s_reason_timer = NULL; s_reason_on = false; if (s_countdown) { layer_mark_dirty(s_countdown); }}
static void show_reason(Label label) { s_reason = label; s_reason_on = true; if (s_reason_timer) { app_timer_cancel(s_reason_timer); } s_reason_timer = app_timer_register(REASON_MS, reason_timeout, NULL); if (s_countdown) { layer_mark_dirty(s_countdown); }}
static void toggle_alarm(void) { LineDetail *detail = model_line_detail(); time_t now = time(NULL); AlarmTarget target = { .base = s_fsm.completed_at ? s_fsm.completed_at : now, .minutes = detail->min1, .walk_seconds = stop_distance(), .stop_id = s_stop_id, .line = s_line, .kind = ALARM_WAKE_LINE, }; switch (alarm_target_toggle(&target, s_fsm.state == LINE_FSM_COMPLETE, now)) { case ALARM_ARM: case ALARM_DISARM: layer_mark_dirty(s_countdown); layer_mark_dirty(s_content); break; case ALARM_BLOCKED_LOADING: case ALARM_BLOCKED_CANNOT_SCHEDULE: flash_cannot(); break; case ALARM_BLOCKED_NO_DEPARTURE: show_reason(LABEL_NO_DEPARTURE); break; case ALARM_BLOCKED_TOO_LATE: show_reason(LABEL_TOO_LATE); break; }}
static void select_long_click(ClickRecognizerRef ref, void *ctx) { toggle_alarm();}
static void touch_tap(GPoint p, void *ctx) { LineDetail *detail = active_detail(); GRect bounds = layer_get_bounds(s_content); if (detail->disruption[0] && p.y >= bounds.size.h - BANNER_H) { disruption_window_push(detail); return; } line_window_poll();}
static void touch_hold(GPoint p, void *ctx) { toggle_alarm();}
static void touch_swipe(TouchSwipeDir dir, void *ctx) { if (dir == TOUCH_SWIPE_RIGHT) { window_stack_pop(true); }}
static void select_click(ClickRecognizerRef ref, void *ctx) { line_window_poll();}
static void down_click(ClickRecognizerRef ref, void *ctx) { LineDetail *detail = active_detail(); if (detail->disruption[0] == '\0') { return; } disruption_window_push(detail);}
static void click_config(void *ctx) { window_long_click_subscribe(BUTTON_ID_SELECT, 500, select_long_click, NULL); window_single_click_subscribe(BUTTON_ID_SELECT, select_click); window_single_click_subscribe(BUTTON_ID_DOWN, down_click);}
static void window_load(Window *window) { Layer *root = window_get_root_layer(window); GRect bounds = layer_get_bounds(root); s_alarm_icon = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_ICON_ALARM); s_content = layer_create(bounds); layer_set_update_proc(s_content, content_update); layer_add_child(root, s_content); s_countdown_rest = GRect(bounds.size.w - RIGHT_COL_W - 20, HEADER_H, RIGHT_COL_W + 20, COUNTDOWN_H); s_countdown = layer_create(s_countdown_rest); layer_set_update_proc(s_countdown, countdown_update); layer_set_clips(s_countdown, false); layer_add_child(root, s_countdown); touch_attach(window, &(TouchHandlers){ .on_tap = touch_tap, .on_hold = touch_hold, .on_swipe = touch_swipe, }); sync_loading();}
static void window_unload(Window *window) { apply(line_fsm_unload(&s_fsm)); anim_layer_gone(s_content); anim_layer_gone(s_countdown); s_was_loading = false; if (s_reason_timer) { app_timer_cancel(s_reason_timer); s_reason_timer = NULL; } s_reason_on = false; gbitmap_destroy(s_alarm_icon); s_alarm_icon = NULL; layer_destroy(s_countdown); s_countdown = NULL; layer_destroy(s_content); s_content = NULL; touch_detach(window); window_destroy(window); s_window = NULL;}
void line_window_push_label(const char *stop_id, const char *line, const char *destination) { detail_surface_pin(s_stop_id, stop_id, ID_LEN); detail_surface_pin(s_line, line, LINE_LEN); detail_surface_pin(s_dest, destination ? destination : "", NAME_LEN); s_flash = false; s_was_loading = false; s_shown_min1 = -1; model_line_begin(); apply(line_fsm_push(&s_fsm));
s_window = window_create(); window_set_window_handlers(s_window, (WindowHandlers){ .load = window_load, .unload = window_unload, }); window_set_click_config_provider(s_window, click_config); window_stack_push(s_window, true);}
void line_window_refresh(void) { if (!s_window || !s_content) { return; } LineDetail *detail = model_line_detail(); time_t prev_completed = s_fsm.completed_at; apply(line_fsm_refresh(&s_fsm, detail->status == 1, model_line_complete(), time(NULL))); if (s_fsm.state == LINE_FSM_COMPLETE && s_fsm.completed_at != prev_completed) { anim_pulse_start(s_content); } sync_loading(); maybe_slide(); layer_mark_dirty(s_countdown);}
void line_window_poll(void) { if (!s_window) { return; } apply(line_fsm_poll(&s_fsm, time(NULL))); sync_loading();}
void line_window_fail(void) { if (!s_window) { return; } apply(line_fsm_fail(&s_fsm)); sync_loading();}