class TileUp::Tiler

Public Class Methods

new(image_filename, options) click to toggle source
# File lib/tileup/tiler.rb, line 10
def initialize(image_filename, options)
  default_options = {
    auto_zoom_levels: nil,
    tile_width: 256,
    tile_height: 256,
    filename_prefix: "map_tile",
    output_dir: ".",
    extend_incomplete_tiles: true,
    verbose: false
  }
  @options = OpenStruct.new(default_options.merge(options))
  @options.tile_width = @options.tile_width.to_i unless !@options.tile_width.respond_to? :to_i
  @options.tile_height = @options.tile_height.to_i unless !@options.tile_height.respond_to? :to_i
  @extension = image_filename.split(".").last
  @filename_prefix = @options.filename_prefix
  @logger = ConsoleLogger.new(:info, {verbose: @options.verbose})

  begin
    @image = Magick::Image::read(image_filename).first
  rescue Magick::ImageMagickError => e
    @logger.error "Could not open image #{image_filename}."
    exit
  end

  if @options.auto_zoom_levels && @options.auto_zoom_levels > 20
    @logger.warn "Warning: auto zoom levels hard limited to 20."
    @options.auto_zoom_levels = 20
  end
  if @options.auto_zoom_levels && @options.auto_zoom_levels <= 0
    @options.auto_zoom_levels = nil
  end

  @logger.info "Opened #{image_filename}, #{@image.columns} x #{@image.rows}"

  # pre-process our inputs to work out what we're supposed to do
  tasks = []

  if @options.auto_zoom_levels.nil?
    # if we have no auto zoom request, then
    # we dont shrink or scale, and save directly to the output
    # dir.
    tasks << {
      output_dir: @options.output_dir, # normal output dir
      scale: 1.0 # dont scale
    }
  else
    # do have zoom levels, so construct those tasks
    zoom_name = 20
    scale = 1.0
    tasks << {
      output_dir: File.join(@options.output_dir, zoom_name.to_s),
      scale: scale
    }
    (@options.auto_zoom_levels-1).times do |level|
      scale = scale / 2.0
      zoom_name = zoom_name - 1
      tasks << {
        output_dir: File.join(@options.output_dir, zoom_name.to_s),
        scale: scale
      }
    end
  end

  # run through tasks list
  tasks.each do |task|
    image = @image
    image_path = File.join(task[:output_dir], @filename_prefix)
    if task[:scale] != 1.0
      # if scale required, scale image
      begin
        image = @image.scale(task[:scale])
      rescue RuntimeError => e
        message = "Failed to scale image, are you sure the original image "\
                  "is large enough to scale down this far (#{scale}) with this "\
                  "tilesize (#{@options.tile_width}x#{@options.tile_height})?"
        @logger.error message
        exit
      end
    end
    # make output dir
    make_path(task[:output_dir])
    self.make_tiles(image, image_path, @options.tile_width, @options.tile_height)
    image = nil
  end

  @logger.info "Finished."

end

Public Instance Methods

make_path(directory_path) click to toggle source
# File lib/tileup/tiler.rb, line 99
def make_path(directory_path)
  FileUtils.mkdir_p directory_path
end
make_tiles(image, filename_prefix, tile_width, tile_height) click to toggle source
# File lib/tileup/tiler.rb, line 103
def make_tiles(image, filename_prefix, tile_width, tile_height)
  # find image width and height
  # then find out how many tiles we'll get out of
  # the image, then use that for the xy offset in crop.
  num_columns = (image.columns/tile_width.to_f).ceil
  num_rows = (image.rows/tile_height.to_f).ceil
  x,y,column,row = 0,0,0,0
  crops = []

  @logger.info "Tiling image into columns: #{num_columns}, rows: #{num_rows}"

  while true
    x = column * tile_width
    y = row * tile_height
    crops << {
      x: x,
      y: y,
      row: row,
      column: column
    }
    column = column + 1
    if column >= num_columns
      column = 0
      row = row + 1
    end
    if row >= num_rows
      break
    end
  end

  crops.each do |c|
    @logger.verbose "Crop: x: #{c[:x]}, y: #{c[:y]}, w: #{tile_width}, h: #{tile_height}"
    ci = image.crop(c[:x], c[:y], tile_width, tile_height, true);

    # unless told to do otherwise, extend tiles in the last row and column
    # if they do not fill an entire tile width and height.
    
    is_edge = (c[:row] == num_rows - 1 || c[:column] == num_columns - 1)
    needs_extension = ci.rows != tile_height || ci.columns != tile_width
    if @options.extend_incomplete_tiles && is_edge && needs_extension
      # defaults to white background color, but transparent is probably
      # a better default for our purposes.
      ci.background_color = "none"
      # fill to width height, start from top left corner.
      ci = ci.extent(tile_width, tile_height, 0, 0)
    end

    @logger.verbose "Saving tile: #{c[:column]}, #{c[:row]}..."
    ci.write("#{filename_prefix}_#{c[:column]}_#{c[:row]}.#{@extension}")
    @logger.verbose "Saving tile: #{c[:column]}, #{c[:row]}... saved"

    ci = nil
  end
end