const std = @import("std"); const temp = @import("temp"); const zdt = @import("zdt"); const Dimensions = @import("../../Dimensions.zig"); const renderer = @import("../../renderer.zig"); pub const api = @import("../../api/root.zig"); const template = @import("template/root.zig"); const Self = @This(); allocator: std.mem.Allocator, dimensions: Dimensions, pub fn init(allocator: std.mem.Allocator, dimensions: Dimensions) Self { return .{ .allocator = allocator, .dimensions = dimensions, }; } pub const RenderOptions = struct { now: *const zdt.Datetime, tz: *const zdt.Timezone, max_items: usize, show_operator: bool, }; pub fn render(self: *const Self, efa_dm_resp: *const api.efa.models.DmResponse, options: RenderOptions) ![]const u8 { const now_local = try options.now.tzConvert(.{ .tz = options.tz }); var arena = std.heap.ArenaAllocator.init(self.allocator); defer arena.deinit(); const departures = try arena.allocator().alloc(template.Departure, efa_dm_resp.stopEvents.len); var dep_i: usize = 0; for (efa_dm_resp.stopEvents) |dep| { const dp_timetable = try zdt.Datetime.fromISO8601(dep.departureTimeBaseTimetable); const dp_timetable_local = try dp_timetable.tzConvert(.{ .tz = options.tz }); const dp_planned = try zdt.Datetime.fromISO8601( dep.departureTimeEstimated orelse dep.departureTimePlanned, ); const dp_planned_local = try dp_planned.tzConvert(.{ .tz = options.tz }); const diff_min: i32 = @intFromFloat(dp_planned_local.diff(dp_timetable_local).totalMinutes()); const on_time: ?u32 = if (diff_min < 0) @abs(diff_min) else null; const delayed: ?u32 = if (diff_min > 0) @abs(diff_min) else null; const date: ?[]const u8 = if (dp_planned_local.dayOfYear() != now_local.dayOfYear()) try std.fmt.allocPrint(arena.allocator(), "{d}.{d}.{d}", .{ dp_planned_local.day, dp_planned_local.month, dp_planned_local.year, }) else null; var infos: ?[]const []const u8 = null; if (dep.hints) |hints| { const arr = try arena.allocator().alloc([]const u8, hints.len + 1); var i: usize = 0; for (hints) |hint| { if (std.mem.eql(u8, hint.type, "Timetable")) continue; arr[i] = hint.content; i += 1; } infos = arr[0..i]; } departures[dep_i] = template.Departure{ .time = try std.fmt.allocPrint( arena.allocator(), "{d}:{d:0>2}", .{ dp_timetable_local.hour, dp_timetable_local.minute }, ), .time_unix = dp_timetable_local.toUnix(.second), .line = dep.transportation.name, .transportation_product_class = dep.transportation.product.class, .operator = if (dep.transportation.operator) |op| op.name else null, .destination = dep.transportation.destination.name, .platform = dep.location.properties.platform, .date = date, .on_time = on_time, .delayed = delayed, .cancelled = dep.isCancelled, .information = infos, }; dep_i += 1; } std.mem.sort( template.Departure, departures, {}, template.Departure.cmpByTimeUnix, ); const context = template.Context{ .station = efa_dm_resp.locations[0].name, .time = try std.fmt.allocPrint( arena.allocator(), "{d}:{d:0>2}", .{ now_local.hour, now_local.minute }, ), .departures = departures[0..@min(departures.len, options.max_items)], .show_operator = options.show_operator, }; const escaped_context = try context.escape(arena.allocator()); var tmp_file = try temp.TempFile.create(arena.allocator(), .{ .pattern = "render-departures-*.html", }); defer tmp_file.deinit(); const path = try tmp_file.parent_dir.realpathAlloc(arena.allocator(), tmp_file.basename); { const file = try tmp_file.open(.{ .mode = .write_only }); defer file.close(); const writer = file.writer(); try template.render(&escaped_context, writer.any()); } return try renderer.render(self.allocator, path, self.dimensions); }