diff --git a/src/departure.zig b/src/departure.zig index be30b93..6bd9c70 100644 --- a/src/departure.zig +++ b/src/departure.zig @@ -1,13 +1,101 @@ +const std = @import("std"); const raylib = @import("raylib.zig"); const rl = raylib.rl; const AppState = @import("state.zig"); +const Curl = @import("curl.zig"); + +fn fetchThread(state: *AppState) !void { + std.debug.print("[departure/fetchThread] Started\n", .{}); + defer std.debug.print("[departure/fetchThread] Ended\n", .{}); + defer state.departure_screen_state.fetch_thread = null; + const allocator = state.allocator; + var station_id_buf = std.BoundedArray(u8, 10){}; + var include_tram = false; + var curl = Curl.init() orelse return; + defer curl.deinit(); + + while (state.departure_screen_state.fetch_thread != null) { + const fetch_anyway = state.departure_screen_state.should_refresh; + if (!fetch_anyway and std.mem.eql(u8, station_id_buf.slice(), state.departure_screen_state.station_id.items) and include_tram == state.departure_screen_state.include_tram) { + std.time.sleep(100 * 1000); + continue; + } + + station_id_buf.resize(state.departure_screen_state.station_id.items.len) catch continue; + std.mem.copyForwards(u8, station_id_buf.slice(), state.departure_screen_state.station_id.items); + include_tram = state.departure_screen_state.include_tram; + std.debug.print("[departure/fetchThread] Detected update: {s}\n", .{station_id_buf.slice()}); + + curl.reset(); + + const departures_base = std.fmt.allocPrintZ( + allocator, + "https://v6.db.transport.rest/stops/{s}/departures", + .{state.departure_screen_state.station_id.items}, + ) catch continue; + defer allocator.free(departures_base); + var departures_uri = std.Uri.parse(departures_base) catch unreachable; + const query = std.fmt.allocPrint(allocator, "duration=300&bus=false&ferry=false&taxi=false&pretty=false{s}", .{if (include_tram) "" else "&tram=false&subway=false"}) catch continue; + defer allocator.free(query); + departures_uri.query = query; + defer departures_uri.query = null; + std.debug.print("[departure/fetchThread] Making request to: {}\n", .{departures_uri}); + + const url = try std.fmt.allocPrintZ(allocator, "{}", .{departures_uri}); + defer allocator.free(url); + _ = curl.setopt(.url, .{url.ptr}); + + var result = std.ArrayList(u8).init(allocator); + defer result.deinit(); + _ = curl.setopt(.write_function, .{Curl.Utils.array_list_append}); + _ = curl.setopt(.write_data, .{&result}); + + const code = curl.perform(); + std.debug.print("[departure/fetchThread] cURL Code: {}\n", .{code}); + if (code != 0) continue; + + std.debug.print("[departure/fetchThread] Fetched data: (len: {})\n", .{result.items.len}); + const parsed = std.json.parseFromSlice(std.json.Value, allocator, result.items, .{}) catch |err| { + std.debug.print("[departure/fetchThread] JSON parse error: {}\n", .{err}); + continue; + }; + if (state.departure_screen_state.fetch_result) |old_result| { + old_result.deinit(); + } + state.departure_screen_state.fetch_result = parsed; + state.departure_screen_state.should_refresh = false; + } + if (state.departure_screen_state.fetch_result) |old_result| { + old_result.deinit(); + state.departure_screen_state.fetch_result = null; + } +} pub fn render(state: *AppState) !void { + const allocator = state.allocator; + var ds = &state.departure_screen_state; + + if (ds.fetch_thread == null) { + ds.fetch_thread = std.Thread.spawn(.{}, fetchThread, .{state}) catch null; + } + while (raylib.GetKeyPressed()) |key| { switch (key) { rl.KEY_LEFT => { state.screen = .home; }, + rl.KEY_R => { + ds.should_refresh = true; + }, + rl.KEY_MINUS, rl.KEY_KP_SUBTRACT => { + ds.max_next_trains = @max(1, ds.max_next_trains - 1); + }, + rl.KEY_EQUAL, rl.KEY_KP_EQUAL => { + ds.max_next_trains = @min(ds.max_next_trains + 1, if (ds.fetch_result) |fr| @as(c_int, @intCast(fr.value.object.get("departures").?.array.items.len)) else 5); + }, + rl.KEY_T => { + ds.include_tram = !ds.include_tram; + }, else => {}, } } @@ -15,8 +103,186 @@ pub fn render(state: *AppState) !void { rl.BeginDrawing(); defer rl.EndDrawing(); - rl.ClearBackground(raylib.ColorInt(0x18226f)); - rl.DrawText(state.departure_screen_state.station_id.items.ptr, 16, 16, 32, rl.WHITE); + const db_blue = raylib.ColorInt(0x18226f); + rl.ClearBackground(if (ds.should_refresh) rl.ORANGE else db_blue); + if (ds.fetch_result) |data| { + if (data.value.object.get("departures")) |departures_raw| { + const departures = departures_raw.array.items; + var not_cancelled = std.ArrayList(std.json.Value).init(allocator); + defer not_cancelled.deinit(); + for (departures) |d| { + if (d.object.get("cancelled")) |c| { + switch (c) { + .bool => |b| { + if (b) { + continue; + } + }, + else => {}, + } + } + not_cancelled.append(d) catch continue; + } + if (not_cancelled.items.len > 0) { + var y: c_int = 16; + // Info area + y += 32 + 16; + const first = not_cancelled.items[0].object; + + station_name_blk: { + const station_name = std.fmt.allocPrintZ(allocator, "{s}", .{first.get("stop").?.object.get("name").?.string}) catch break :station_name_blk; + defer allocator.free(station_name); + rl.SetWindowTitle(station_name.ptr); + raylib.DrawRightAlignedText(station_name.ptr, rl.GetScreenWidth() - 4, 4, 14, rl.WHITE); + } + + const line = try std.fmt.allocPrintZ(allocator, "{s}", .{first.get("line").?.object.get("name").?.string}); + defer allocator.free(line); + const destination = try std.fmt.allocPrintZ(allocator, "{s}", .{first.get("direction").?.string}); + defer allocator.free(destination); + var next_y = y; + if (state.db_font) |db_font| { + next_y += @intFromFloat(raylib.DrawAndMeasureTextEx(db_font, line.ptr, 16, @floatFromInt(y), 32, 1, rl.WHITE).y); + } else { + rl.DrawText(line.ptr, 16, y, 32, rl.WHITE); + next_y += 32; + } + next_y += 16; + if (ds.platform.items.len == 0) blk: { + if (first.get("platform")) |platform_raw| { + switch (platform_raw) { + .string => |p| { + const platform = std.fmt.allocPrintZ(allocator, "{s}", .{p}) catch break :blk; + defer allocator.free(platform); + if (state.db_font) |db_font| { + raylib.DrawRightAlignedTextEx(db_font, platform.ptr, @floatFromInt(rl.GetScreenWidth() - 16), @floatFromInt(y), 40, 1, rl.WHITE); + } else { + raylib.DrawRightAlignedText(platform.ptr, rl.GetScreenWidth() - 16, y, 40, rl.WHITE); + } + }, + else => {}, + } + } + } + y = next_y; + if (state.db_font) |db_font| { + y += @intFromFloat(raylib.DrawAndMeasureTextEx( + db_font, + destination.ptr, + 16, + @floatFromInt(y), + 56, + 1, + rl.WHITE, + ).y); + } else { + rl.DrawText(destination.ptr, 16, y, 56, rl.WHITE); + y += 56; + } + y += 16; + } + if (not_cancelled.items.len > 1) { + var max_trains: c_int = @intCast(not_cancelled.items.len - 1); + if (max_trains > ds.max_next_trains) max_trains = ds.max_next_trains; + const font_size: c_int = 32; + var x: c_int = 16; + var y = rl.GetScreenHeight() - (font_size + 8) * max_trains - 4; + rl.DrawRectangle(0, y, rl.GetScreenWidth(), rl.GetScreenHeight(), rl.WHITE); + y += 8; + const label_measurement_width = if (state.db_font) |db_font| @as(c_int, @intFromFloat(raylib.DrawAndMeasureTextEx( + db_font, + if (max_trains == 1) "Next train: " else "Next trains: ", + @floatFromInt(x), + @floatFromInt(y), + @floatFromInt(font_size), + 1, + db_blue, + ).x)) else raylib.DrawAndMeasureText( + if (max_trains == 1) "Next train: " else "Next trains: ", + x, + y, + font_size, + db_blue, + ).width; + x += label_measurement_width; + + // Compute line name width + var line_name_width: c_int = 0; + for (not_cancelled.items, 0..) |dep_raw, idx| { + if (idx == 0) continue; + if (idx > max_trains) break; + const second = dep_raw.object; + const next_train_line = try std.fmt.allocPrintZ( + allocator, + "{s} ", + .{ + second.get("line").?.object.get("name").?.string, + }, + ); + defer allocator.free(next_train_line); + if (state.db_font) |db_font| { + line_name_width = @max( + line_name_width, + @as(c_int, @intFromFloat(rl.MeasureTextEx(db_font, next_train_line.ptr, @floatFromInt(font_size), 1).x)), + ); + } else { + line_name_width = @max(line_name_width, rl.MeasureText(next_train_line.ptr, font_size)); + } + } + const destionation_x = x + line_name_width; + + for (not_cancelled.items, 0..) |dep_raw, idx| { + if (idx == 0) continue; + if (idx > max_trains) break; + const second = dep_raw.object; + const next_train_line = try std.fmt.allocPrintZ( + allocator, + "{s} ", + .{ + second.get("line").?.object.get("name").?.string, + }, + ); + defer allocator.free(next_train_line); + const next_train_direction = try std.fmt.allocPrintZ( + allocator, + "{s}", + .{ + second.get("direction").?.string, + }, + ); + defer allocator.free(next_train_direction); + if (state.db_font) |db_font| { + rl.DrawTextEx(db_font, next_train_line.ptr, .{ .x = @floatFromInt(x), .y = @floatFromInt(y) }, font_size, 1, db_blue); + rl.DrawTextEx(db_font, next_train_direction.ptr, .{ .x = @floatFromInt(destionation_x), .y = @floatFromInt(y) }, font_size, 1, db_blue); + } else { + rl.DrawText(next_train_line.ptr, x, y, font_size, db_blue); + rl.DrawText(next_train_direction.ptr, destionation_x, y, font_size, db_blue); + } + if (ds.platform.items.len == 0) blk: { + if (second.get("platform")) |platform_raw| { + switch (platform_raw) { + .string => |p| { + const platform = std.fmt.allocPrintZ(allocator, "{s}", .{p}) catch break :blk; + defer allocator.free(platform); + if (state.db_font) |db_font| { + raylib.DrawRightAlignedTextEx(db_font, platform.ptr, @floatFromInt(rl.GetScreenWidth() - 16), @floatFromInt(y), @floatFromInt(font_size), 1, db_blue); + } else { + raylib.DrawRightAlignedText(platform.ptr, rl.GetScreenWidth() - 16, y, font_size, db_blue); + } + }, + else => {}, + } + } + } + y += font_size + 4; + rl.DrawLine(x, y, rl.GetScreenWidth() - 8, y, db_blue); + y += 4; + } + } + } + } else { + rl.DrawText(state.departure_screen_state.station_id.items.ptr, 16, 16, 32, rl.WHITE); + } state.close_app = rl.WindowShouldClose(); } diff --git a/src/state.zig b/src/state.zig index 8892e13..eb33ca5 100644 --- a/src/state.zig +++ b/src/state.zig @@ -23,7 +23,12 @@ pub const DepartureScreenState = struct { station_id: std.ArrayListUnmanaged(u8), platform: std.ArrayListUnmanaged(u8), departure_date: std.time.Instant, - loading: bool = false, + fetch_thread: ?std.Thread = null, + last_refresh_time: std.time.Instant = std.mem.zeroInit(std.time.Instant, .{}), + fetch_result: ?std.json.Parsed(std.json.Value) = null, + should_refresh: bool = false, + max_next_trains: c_int = 5, + include_tram: bool = false, }; allocator: std.mem.Allocator,