skandragon/thing

View on GitHub
sched.rb

Summary

Maintainability
F
5 days
Test Coverage
require 'rubygems'

require 'prawn'
require 'json'
require 'pp'

include GriffinMarkdown

@schedule = ARGV[0] || "Pennsic University"
puts "Rendering schedule for #{@schedule}"

@render_notes_and_doodles = @schedule == "Pennsic University"
@render_topic_list = @schedule == "Pennsic University"
@draftit = false

@location_label_width = 6
@header_height = 3
@row_height = 2
@column_width = 8

@title_font_size = 15

@grey = 'eeeeee'
@grey_dark = 'dddddd'
@grey_50 = '888888'
@grey_40 = '444444'
@grey_20 = '222222'
@black = '000000'
@white = 'ffffff'
@red = 'ff0000'

ADDITIONAL_SPACING = 0.5

entries = {}

# 24-hour times to display
@morning_hours =   [  9, 10, 11, 12, 13 ]
@afternoon_hours = [ 14, 15, 16, 17, 18 ]

ANS_ROOMS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 19].map { |x| "A&S #{x}"}

if @schedule == "Pennsic University"
  @locs1 = ANS_ROOMS
  @locs1 << 'University-Battlefield'
  @locs1 << 'Dance'
  @locs1 << 'Games'

  @locs2 = [
    'Battlefield Loud 2',
    'Performing Arts',
    'Amphitheater',
    'Middle Eastern',
    'Æthelmearc 1',
    'Æthelmearc 2',
    'Performing Arts Rehearsal',
    'Livonia Smithery',
    'Pine Box Traders',
    'Bog U',
  ].sort
elsif @schedule == "Battlefield"
  @locs1 = [
    'North Battlefield',
    'East Battlefield',
    'West Battlefield',
    'South Battlefield',
    'White List',
    'Blue List',
    'Fort',
    'Youth Combat List',
    'Thrown Weapons Range',
    'Thrown Weapons Tent',
    'Rapier List 1',
    'Rapier List 2',
    'Rapier List 3',
    'Rapier List 4',
  ].sort

  @locs2 = []
elsif @schedule == "Artisan's Row"
  @locs1 = [
    "Artisan's Row A",
    "Artisan's Row B",
    "Artisan's Row C",
  ].sort

  @locs2 = []
end

@loc_count = {}
@locs1.each { |loc| @loc_count[loc] = 0 }
@locs2.each { |loc| @loc_count[loc] = 0 }

def generate_magic_tokens(instructables)
  items = instructables.map { |x| [x.formatted_topic, x.name.gsub('*', ''), x.id] }.sort

  last_topic = nil
  magic_token = 0

  @instructable_magic_tokens = {}
  items.each do |item|
    if last_topic != item[0].split(':').first.strip
      magic_token += 100 - (magic_token % 100)
      last_topic = item[0].split(':').first.strip
    end
    @instructable_magic_tokens[item[2]] = magic_token
    magic_token += 1
  end
end

instructables = Instructable.where(schedule: @schedule)
generate_magic_tokens(instructables)

ids = instructables.map { |x| x.id }
instances = Instance.where(instructable_id: ids).includes(:instructable).select { |x| x.scheduled? }

instances.each do |instance|
  next if instance.instructable.name =~ /^Empty Tent/
  if instance.start_time.nil?
    pp instance
    next
  end

  date = instance.start_time.to_date

  entries[date] ||= {
    morning: { loc1: [], loc2: [] },
    afternoon: { loc1: [], loc2: [] },
    other_location: [],
    other_time: [],
    other_morning: [],
    other_afternoon: []
  }

  hour = instance.start_time.hour
  morning_check = @morning_hours.index(hour)
  afternoon_check = @afternoon_hours.index(hour)
  if @schedule == "Pennsic University"
    actual_loc = instance.location.gsub(/ Tent$/, '')
  else
    actual_loc = instance.location
  end
  loc = instance.instructable.location_nontrack? ? instance.instructable.camp_name : actual_loc
  if @schedule == "Pennsic University"
    loc.gsub!(/ Tent$/, '')
  end
  loc1_check = @locs1.index(loc) || @locs1.index(actual_loc)
  loc2_check = @locs2.index(loc) || @locs2.index(actual_loc)

  @loc_count[loc] ||= 0
  @loc_count[loc] += 1

  subsection = nil
  in_loc = false
  if !(morning_check || afternoon_check) || !(loc1_check || loc2_check)
    if hour <= @morning_hours.sort.last
      section = :other_morning
    else
      section = :other_afternoon
    end
    if (loc1_check || loc2_check) && hour < @morning_hours.first
      in_loc = true
    end
  elsif (loc1_check || loc2_check) && (morning_check)
    section = :morning
    subsection = loc1_check ? :loc1 : :loc2
  elsif (loc1_check || loc2_check) && (afternoon_check)
    section = :afternoon
    subsection = loc1_check ? :loc1 : :loc2
  else
    puts "OTHER:"
    pp instance
    section = :other_afternoon
  end

  locindex = (loc1_check || loc2_check)
  hourindex = (morning_check || afternoon_check)

  duration = instance.instructable.duration
  minute = instance.start_time.strftime('%M').to_i

  if hourindex
    hourindex += (minute / 60.0)
    if (hourindex + duration) > 5
      display_duration = 5 - hourindex
      extended_right = true
    else
      display_duration = duration
      extended_right = false
    end
  else
    display_duration = duration
    extended_right = false
  end

  extended_left = false

  if instance.instructable.name == 'Bellatrix: Individual Session'
    start_time = "By appointment"
    section = :other_morning
    subsection = nil
  else
    start_time = instance.start_time
  end

  data = {
    name: instance.instructable.name,
    start_time: start_time,
    hourindex: hourindex,
    locindex: locindex,
    duration: duration,
    display_duration: display_duration,
    id: @instructable_magic_tokens[instance.instructable.id],
    extended_right: extended_right,
    extended_left: extended_left,
    instance: instance,
  }

  spot = subsection ? entries[date][section][subsection] : entries[date][section]
  spot << data

  if in_loc and (hour + minute / 60.0 + duration > @morning_hours.first)
    display_duration = duration - (@morning_hours.first - (hour + minute / 60.0))
    name = instance.instructable.name
    name = 'Yoga' if name =~ /^Yoga/
    data = {
        name: name,
        start_time: start_time,
        hourindex: 0,
        locindex: locindex,
        duration: duration,
        display_duration: display_duration,
        id: @instructable_magic_tokens[instance.instructable.id],
        extended_right: extended_right,
        extended_left: true,
        instance: instance,
    }

    spot = entries[date][:morning][loc1_check ? :loc1 : :loc2]
    spot << data
  end

  if extended_right and section == :morning
    display_duration = duration - display_duration
    if display_duration > 5
      display_duration = 5
      extended_right = true
    else
      extended_right = false
    end

    data = {
      name: instance.instructable.name,
      start_time: instance.start_time,
      hourindex: 0,
      locindex: locindex,
      duration: duration,
      display_duration: display_duration,
      id: @instructable_magic_tokens[instance.instructable.id],
      extended_right: extended_right,
      extended_left: true,
      instance: instance,
    }
    spot = entries[date][:afternoon][subsection]
    spot << data
  end
end

def draw_hour_labels(pdf, opts)
  opts[:hour_labels].count.times do |timeindex|
    opts[:location_labels].count.times do |locindex|
      y1 = @header_height + locindex * @row_height
      x1 = @location_label_width + timeindex * @column_width
      y2 = y1 + @row_height - 1
      x2 = x1 - 1 + @column_width
      box = pdf.grid([y1, x1], [y2, x2])
      box.bounding_box {
        pdf.stroke_color @grey_dark
        pdf.fill_color @grey
        pdf.fill {
          pdf.rectangle [0, box.height], box.width, box.height
        }
        pdf.stroke_bounds
        pdf.stroke_color @black
        pdf.fill_color @black
      }
    end
  end

  opts[:hour_labels].each_with_index do |label, labelindex|
    y1 = @header_height - 1
    x1 = @location_label_width + labelindex * @column_width
    y2 = y1
    x2 = x1 + @column_width - 1
    box = pdf.grid([y1, x1], [y2, x2])

    box.bounding_box {
      pdf.fill_color @grey
      pdf.fill {
        pdf.rectangle [0, box.height], box.width, box.height
      }
      pdf.fill_color @black
      pdf.stroke {
        #pdf.line [box.width, 0], box.width, box.height
        pdf.line [0, 0], 0, box.height
        pdf.line [0, 0], box.width, 0
      }
    }
    box_opts = {
        at: [box.top_left[0] + 2, box.top_left[1] - 2],
        width: box.width - 4,
        height: box.height - 4,
        size: 10,
        overflow: :shrink_to_fit,
        min_font_size: 6,
        align: :center,
        valign: :center,
        style: :bold,
    }
    if label < 12
      pdf.text_box "#{label}:00 AM", box_opts
    else
      pm = label > 12 ? label - 12 : 12
      pdf.text_box "#{pm}:00 PM", box_opts
    end
  end
end

def draw_location_labels(pdf, opts)
  opts[:location_labels].each_with_index do |label, labelindex|
    y1 = @header_height + labelindex * @row_height
    x1 = 0
    y2 = y1 + @row_height - 1
    x2 = @location_label_width - 1
    box = pdf.grid([y1, x1], [y2, x2])
    box.bounding_box {
      pdf.fill_color @grey
      pdf.fill {
        pdf.rectangle [0, box.height], box.width, box.height
      }
      pdf.fill_color @black
      pdf.stroke {
        pdf.line [0, box.height], box.width, box.height
        pdf.line [0, 0], box.width, 0
        pdf.line [box.width, 0], box.width, box.height
      }
    }
    box_opts = {
        at: [box.top_left[0] + 2, box.top_left[1] - 2],
        width: box.width - 4,
        height: box.height - 4,
        size: 9,
        overflow: :shrink_to_fit,
        min_font_size: 8,
        align: :center,
        valign: :center,
        style: :bold,
    }
    pdf.text_box label, box_opts
  end
end

def render(pdf, opts)
  debug = false

  if debug
    pdf.stroke_axis
  end

  pdf.line_width 0.25
  pdf.stroke_color @black

  hour_slots = opts[:hour_labels].count
  location_slots = opts[:location_labels].count
  location_slots = 18 if location_slots < 18

  pdf.define_grid(columns: hour_slots * 8 + 6, rows: location_slots * 2 + 2 + 1, gutter: 0)

  if debug
    (pdf.grid.rows - 1).times do |row|
      (pdf.grid.columns - 1).times do |column|
        pdf.grid([row, column], [row + 1, column + 1]).bounding_box do
          pdf.stroke_color @grey
          pdf.stroke_bounds
        end
      end
    end
    pdf.stroke_color @black
  end

  box = pdf.grid([0, 0], [@header_height - 1, pdf.grid.columns - 1])
  box.bounding_box do
    pdf.fill_color @grey
    pdf.stroke_color @grey
    pdf.fill { pdf.rectangle [0, box.height], box.width, box.height }
    pdf.stroke_bounds
  end

  box = pdf.grid([0, 0], [0.5, pdf.grid.columns - 1])
  msg = opts[:title]
  box_opts = {
    align: :center,
    valign: :center,
    size: @title_font_size,
    at: [box.top_left[0], box.top_left[1]],
    width: box.width,
    height: box.height,
  }
  pdf.text_rendering_mode(:fill_stroke) do
    pdf.fill_color @grey_50
    pdf.stroke_color @grey_40
    pdf.font("TitleFont") do
      pdf.text_box msg, box_opts
    end
  end

  pdf.fill_color @black
  pdf.stroke_color @black

  draw_hour_labels(pdf, opts)
  draw_location_labels(pdf, opts)

  pdf.font_size 10
  opts[:entries].each do |data|
    locindex = data[:locindex]
    hourindex = data[:hourindex]
    display_duration = data[:display_duration]
    duration = data[:duration]

    extended_right = data[:extended_right]
    extended_left = data[:extended_left]

    y1 = @header_height + locindex * @row_height
    x1 = @location_label_width + hourindex.to_f * @column_width
    y2 = y1 + @row_height - 1
    x2 = x1 - 1 + display_duration * @column_width
    box = pdf.grid([y1, x1], [y2, x2])

    if extended_left
      at = [box.top_left[0] + 6, box.top_left[1] - 2]
    else
      at = [box.top_left[0] + 2, box.top_left[1] - 2]
    end

    box_opts = {
      at: at,
      width: box.width - 4,
      height: box.height - 4,
      size: 9,
      overflow: :shrink_to_fit,
      min_font_size: 8,
      leading: 0,
      inline_format: true,
    }

    box.bounding_box {
      pdf.fill_color @white
      pdf.fill {
        pdf.rectangle [0, box.height], box.width, box.height
      }
      pdf.fill_color @black
      pdf.stroke_bounds
    }
    msg = "#{markdown_pdf data[:name]} <i>(#{data[:id]})</i>"
    if duration != display_duration
      msg += "<br/><b>(#{duration} hours)</b>"
    end
    pdf.text_box msg, box_opts

    if extended_right
      coords = [
        [box.top_right[0], box.top_right[1]],
        [box.top_right[0] + 5, (box.top_right[1] + box.bottom_right[1]) / 2],
        [box.bottom_right[0], box.bottom_right[1]]
      ]
      pdf.fill_polygon *coords
    end

    if extended_left
      coords = [
          [box.top_left[0], box.top_left[1]],
          [box.top_left[0] + 5, (box.top_left[1] + box.bottom_left[1]) / 2],
          [box.bottom_left[0], box.bottom_left[1]]
      ]
      pdf.fill_polygon *coords
    end
  end
end

def render_extra(pdf, opts)
  rowoffset = opts[:rowoffset]

  #box = pdf.grid([rowoffset, 0], [pdf.grid.rows - 1, pdf.grid.columns - 1])
  box = pdf.grid([rowoffset, 0], [rowoffset + 0.2, pdf.grid.columns - 1])
  box.bounding_box do
    pdf.fill_color @grey
    pdf.stroke_color @grey
    pdf.fill { pdf.rectangle [0, box.height], box.width, box.height }
    pdf.stroke_bounds
  end

  msg = opts[:title]
  box_opts = {
      align: :center,
      valign: :center,
      size: @title_font_size,
      at: [box.top_left[0], box.top_left[1]],
      width: box.width,
      height: box.height,
  }
  pdf.text_rendering_mode(:fill_stroke) do
    pdf.fill_color @grey_50
    pdf.stroke_color @grey_40
    pdf.font("TitleFont") do
      pdf.text_box msg, box_opts
    end
  end

  pdf.stroke_color @black
  pdf.fill_color @black
  rowoffset += 1.3

  entries_count = opts[:entries].count
  if entries_count < 25
    font_size = 7.8 + (25 - entries_count) / 10.0
  else
    font_size = 8.5 - (entries_count / 30.0)
  end
  columns = 3

  first = true
  box = pdf.grid([rowoffset, 0], [pdf.grid.rows - 1, pdf.grid.columns - 1])
  pdf.column_box([box.top_left[0], box.top_left[1] - 3], columns: columns, width: box.width) do
    opts[:entries].each do |entry|
      pdf.move_down 2 unless first
      first = false
      if entry[:start_time].is_a?(String)
        start_time = entry[:instance].formatted_location
        start_time += " (#{entry[:start_time]})"
      else
        start_time = entry[:instance].formatted_location_and_time(:pennsic_time_only).gsub(':00', '').gsub('12 PM', 'noon')
      end
      msg = "<b>#{entry[:id]}</b>: #{markdown_pdf(entry[:name])}, #{start_time}"
      duration = entry[:duration]
      if duration != 1
        msg += ", #{duration} hours"
      end
      pdf.text msg, size: font_size, inline_format: true
    end
  end
end

def render_notes(pdf, opts)
  rowoffset = opts[:rowoffset]
  draw_lines = opts[:mode] == :notes
  draw_box = opts[:mode] == :doodles

  line_box = pdf.grid([rowoffset + 2, 0], [pdf.grid.rows - 1, pdf.grid.columns - 1])

  if draw_box
    line_box.bounding_box do
      pdf.stroke_color @grey_40
      pdf.fill_color @grey_40
      pdf.stroke_bounds
    end
  end

  box = pdf.grid([rowoffset, 0], [rowoffset + 1, pdf.grid.columns - 1])
  box.bounding_box do
    pdf.fill_color @grey
    pdf.stroke_color @grey
    pdf.fill { pdf.rectangle [0, box.height], box.width, box.height }
    pdf.stroke_bounds
  end

  msg = opts[:title]
  box_opts = {
      align: :center,
      valign: :center,
      size: @title_font_size,
      at: [box.top_left[0], box.top_left[1] - 2],
      width: box.width,
      height: box.height,
  }
  pdf.text_rendering_mode(:fill_stroke) do
    pdf.fill_color @grey_50
    pdf.stroke_color @grey_40
    pdf.font("TitleFont") do
      pdf.text_box msg, box_opts
    end
  end

  if draw_lines
    rowoffset += 2

    pdf.stroke_color @grey_40
    pdf.fill_color @grey_40

    spacing = 25
    y = spacing
    pdf.move_down spacing

    while y < line_box.height
      pdf.stroke_horizontal_rule
      pdf.move_down spacing
      y += spacing
    end
  end

  pdf.stroke_color @black
  pdf.fill_color @black
end

def draftit(pdf)
  return unless @draftit
  pdf.save_graphics_state do
    pdf.soft_mask do
      pdf.rotate(45, origin: [0, 0]) do
        pdf.fill_color @grey_50
        pdf.draw_text "Draft", size: 200, at: [250, 0]
        pdf.fill_color @black
      end
    end

    pdf.rotate(45, origin: [0, 0]) do
      pdf.fill_color 'bbbbbb'
      pdf.draw_text "Draft", size: 200, at: [250, 0]
      pdf.fill_color @black
    end
  end
end

def materials_and_handout_content(instructable)
  materials = []
  handout = []
  handout << "limit: #{instructable.handout_limit}" if instructable.handout_limit
  materials << "limit: #{instructable.material_limit}" if instructable.material_limit

  handout << "fee: $#{'%.2f' % instructable.handout_fee}" if instructable.handout_fee
  materials << "fee: $#{'%.2f' % instructable.material_fee}" if instructable.material_fee

  handout_content = nil
  handout_content = 'Handout ' + handout.join(', ') + '. ' if handout.size > 0

  materials_content = nil
  materials_content = 'Materials ' + materials.join(', ') + '. ' if materials.size > 0

  [ handout_content, materials_content ].compact
end

def render_topic_list(pdf, instructables)
  previous_topic = ''

  instructables.sort { |a, b|
    [a.formatted_topic, a.name.gsub('*', '')] <=> [b.formatted_topic, b.name.gsub('*', '')]
  }.each do |instructable|
    next if instructable.name =~ /^Empty Tent/
    if instructable.topic != previous_topic
      pdf.move_down 7 unless pdf.cursor == pdf.bounds.top
      pdf.font_size @title_font_size
      pdf.text_rendering_mode(:fill_stroke) do
        pdf.fill_color @grey_50
        pdf.stroke_color @grey_40
        pdf.font("TitleFont") do
          pdf.text instructable.topic
        end
      end

      pdf.font_size 7.4
      pdf.fill_color @black
      pdf.stroke_color @black
    end
    previous_topic = instructable.topic

    pdf.move_down 4.6 unless pdf.cursor == pdf.bounds.top
    name = markdown_pdf(instructable.name, tags_remove: 'strong')
    token = @instructable_magic_tokens[instructable.id]
    topic = instructable.formatted_topic
    culture = instructable.culture.present? ? instructable.culture : nil

    heading = "<strong>#{token}</strong>: <strong>#{name}</strong>"

    lines = [
        heading,
        [topic, culture].compact.join(', '),
        "Instructor: #{instructable.user.titled_sca_name}",
    ]

    scheduled_instances = instructable.instances.select { |x| x.scheduled? }
    if (name == 'Bellatrix: Individual Session')
      lines << 'Scheduled by appointment'
      lines << 'Location: ' + scheduled_instances.first.formatted_location
    elsif scheduled_instances.count > 1 and scheduled_instances.map(&:formatted_location).uniq.count == 1
      lines << scheduled_instances.map { |x| "#{x.start_time.strftime('%a %b %e %I:%M %p')}" }.join(', ')
      lines << 'Location: ' + scheduled_instances.first.formatted_location
    else
      lines << scheduled_instances.map { |x| x.scheduled? ? "#{x.start_time.strftime('%a %b %e %I:%M %p')} #{x.formatted_location}" : nil }.compact.join(",\n")
    end

    lines << materials_and_handout_content(instructable).join(' ')

    pdf.text lines.join("\n"), inline_format: true

    pdf.move_down 2 unless pdf.cursor == pdf.bounds.top
    pdf.text markdown_pdf(instructable.description_web.present? ? instructable.description_web : instructable.description_book), inline_format: true, align: :justify
  end
end

pdf = Prawn::Document.new(page_size: [ 7.75 * 72, 10.25 * 72],
                          margin: 0.125 * 72,
                          page_layout: :portrait,
                          compress: true,
                          optimize_objects: true,
                          info: {
                            Title: "Pennsic Schedule",
                            Author: "thing.pennsicuniversity.org",
                            Subject: "Pennsic Schedule",
                            Keywords: "Pennsic Schedule",
                            Creator: "sched.rb",
                            CreationDate: Time.now
                          })

pdf.font_families.update(
  'TitleFont' => {
    normal: { file: Rails.root.join('app', 'assets', 'fonts', 'Arial.ttf') },
  },
  'BodyFont' => {
    normal: Rails.root.join('app', 'assets', 'fonts', 'Arial.ttf'),
    bold: Rails.root.join('app', 'assets', 'fonts', 'Arial Bold.ttf'),
    italic: Rails.root.join('app', 'assets', 'fonts', 'Arial Italic.ttf'),
    bold_italic: Rails.root.join('app', 'assets', 'fonts', 'Arial Bold Italic.ttf'),
  },
)
pdf.font 'BodyFont'
pdf.text "Spacer page"
draftit(pdf)
pdf.start_new_page

@note_counter = 1
def next_note_type
  ret = [:notes, :doodles][@note_counter % 2]
  @note_counter += 1
  ret
end

entries.keys.sort.each do |key|
  day = key.strftime("%d").to_i
  date = key.strftime("%A, %B #{day.ordinalize}")
  render(pdf,
         location_labels: @locs1,
         hour_labels: @morning_hours,
         entries: entries[key][:morning][:loc1],
         title: "#{date} ~ Morning")
  draftit(pdf)
  pdf.start_new_page

  need_new_page = false
  if @locs2.size > 0
    need_new_page = true
    render(pdf,
           location_labels: @locs2,
           hour_labels: @morning_hours,
           entries: entries[key][:morning][:loc2],
           title: "#{date} ~ Morning")
  end
  subentries = entries[key][:other_morning]
  if subentries.count > 0
    need_new_page = true
    subentries.sort! { |a, b| a[:start_time].to_i <=> b[:start_time].to_i }
    render_extra(pdf,
                 entries: subentries,
                 title: "#{date} ~ Additional Morning Classes",
                 rowoffset: @locs2.count * @row_height + @header_height + ADDITIONAL_SPACING)
  elsif @render_notes_and_doodles
    need_new_page = true
    render_notes(pdf,
                 mode: :notes,
                 title: "Notes",
                 rowoffset: @locs2.count * @row_height + @header_height + ADDITIONAL_SPACING)
  end

  if (need_new_page)
    draftit(pdf)
    pdf.start_new_page
  end

  render(pdf,
         location_labels: @locs1,
         hour_labels: @afternoon_hours,
         entries: entries[key][:afternoon][:loc1],
         title: "#{date} ~ Afternoon")
  draftit(pdf)
  pdf.start_new_page

  need_new_page = false
  if @locs2.size > 0
    need_new_page = true
    render(pdf,
           location_labels: @locs2,
           hour_labels: @afternoon_hours,
           entries: entries[key][:afternoon][:loc2],
           title: "#{date} ~ Afternoon")
  end
  subentries = entries[key][:other_afternoon]
  if subentries.count > 0
    need_new_page = true
    subentries.sort! { |a, b| a[:start_time].to_i <=> b[:start_time].to_i }
    render_extra(pdf,
                 entries: subentries,
                 title: "#{date} ~ Additional Afternoon Classes",
                 rowoffset: @locs2.count * @row_height + @header_height + ADDITIONAL_SPACING)
  elsif @render_notes_and_doodles
    need_new_page = true
    render_notes(pdf,
                 mode: :notes,
                 title: "Notes",
                 rowoffset: @locs2.count * @row_height + @header_height + ADDITIONAL_SPACING)
  end

  if need_new_page
    draftit(pdf)
    pdf.start_new_page
  end
end

pdf.font 'BodyFont'

if @render_topic_list
  pdf.column_box([0, pdf.cursor ], columns: 3, spacer: 6, width: pdf.bounds.width) do
    render_topic_list(pdf, instructables)
  end
end

timestamp = Time.now.strftime("%y%m%d-%H%M%S")
filename = "sched-" + @schedule.downcase.gsub(/[^a-z]/, '-') + "-#{timestamp}.pdf"
pdf.render_file filename

@loc_count.keys.sort.each { |x|
  puts (' %2d %s' % [@loc_count[x], x])
}