require "open3"
require "ftools"
require "matrix"
require 'erb'
include ErrorCalculator
class Map < ActiveRecord::Base
has_many :my_maps, :dependent => :destroy
has_many :users, :through => :my_maps
belongs_to :owner, :class_name => "User"
has_one :user, :through => :my_maps
has_many :gcps, :dependent => :destroy
has_many :layers_maps, :dependent => :destroy
has_many :layers, :through => :layers_maps # ,:after_add, :after_remove
acts_as_taggable
has_attached_file :upload, :styles => {:thumb => ["100x100>", :png]} ,
:url => '/:attachment/:id/:style/:basename.:extension'
attr_protected :upload_file_name, :upload_content_type, :upload_size
#maximum dimension size for height or width. it will resize if one of the dimensions is over this value
#define MAX_ATTACHMENT_SIZE in your config/environments/whatever.rb file
validates_attachment_size(:upload, :less_than => MAX_ATTACHMENT_SIZE) if defined?(MAX_ATTACHMENT_SIZE)
acts_as_audited :except => [:filename]
named_scope :public, :conditions => ['public = ?', true]
named_scope :warped, :conditions => ['status = ?', 4]
acts_as_enum :status, [:unloaded, :loading, :available, :warping, :warped, :published]
acts_as_enum :mask_status, [:unmasked, :masking, :masked]
default_values :status => :unloaded, :mask_status => :unmasked
attr_accessor :error
validates_presence_of :title
before_create :save_dimensions
after_create :setup_image
after_destroy :delete_images
after_destroy :delete_map
#############################################
#CUSTOM VALIDATIONS
#############################################
def validate_on_create
errors.add(:filename, "is already being used") if Map.find_by_filename(upload.original_filename)
end
#############################################
#FILTERS
#############################################
def save_dimensions
if ["image/jpeg", "image/tiff", "image/png", "image/gif", "image/bmp"].include?(upload.content_type.to_s)
self.width = upload.width
self.height = upload.height
end
self.status = :available
end
#this gets the upload, detects what it is, and converts to a tif, if necessary.
#Although an uploaded tif with existing geo fields may confuse things
def setup_image
logger.info "setup_image "
self.filename = upload.original_filename
save!
if self.upload?
if defined?(MAX_DIMENSION) && (width > MAX_DIMENSION || height > MAX_DIMENSION)
logger.info "Image is too big, so going to resize "
if width > height
dest_width = MAX_DIMENSION
dest_height = (dest_width.to_f / width.to_f) * height.to_f
else
dest_height = MAX_DIMENSION
dest_width = (dest_height.to_f / height.to_f) * width.to_f
end
self.width = dest_width
self.height = dest_height
save!
outsize = "-outsize #{dest_width.to_i} #{dest_height.to_i}"
else
outsize = ""
end
orig_ext = File.extname(self.upload_file_name).to_s.downcase
tiffed_filename = (orig_ext == ".tif" || orig_ext == ".tiff")? self.upload_file_name : self.upload_file_name + ".tif"
tiffed_file_path = File.join(maps_dir , tiffed_filename)
logger.info "We convert to tiff"
# -co compress=DEFLATE for compression?
command = "#{GDAL_PATH}gdal_translate #{self.upload.path} #{outsize} -co PHOTOMETRIC=RGB -co PROFILE=BASELINE #{tiffed_file_path}"
logger.info command
ti_stdin, ti_stdout, ti_stderr = Open3::popen3( command )
logger.info ti_stdout.readlines.to_s
logger.info ti_stderr.readlines.to_s
command = "gdaladdo -r average #{tiffed_file_path} 2 4 8 16 32 64"
o_stdin, o_stdout, o_stderr = Open3::popen3(command)
logger.info command
o_out = o_stdout.readlines.to_s
o_err = o_stderr.readlines.to_s
if o_err.size > 0
logger.error "Error gdal overview script" + o_err
logger.error "output = "+o_out
end
self.filename = tiffed_filename
end
save!
save_mapfile
end
#paperclip plugin deletes the images when model is destroyed
def delete_images
logger.info "Deleting map images"
if File.exists?(temp_filename)
logger.info "deleted temp"
File.delete(temp_filename)
end
if File.exists?(warped_filename)
logger.info "Deleted Map warped"
File.delete(warped_filename)
end
if File.exists?(warped_png)
logger.info "deleted warped png"
File.delete(warped_png)
end
if File.exists?(unwarped_filename)
logger.info "deleting unwarped"
File.delete unwarped_filename
end
end
def delete_map
logger.info "Deleting mapfile"
end
#############################################
#ACCESSOR METHODS
#############################################
def maps_dir
defined?(SRC_MAPS_DIR) ? SRC_MAPS_DIR : File.join(RAILS_ROOT, "/public/mapimages/src/")
end
def dest_dir
defined?(DEST_MAPS_DIR) ? DEST_MAPS_DIR : File.join(RAILS_ROOT, "/public/mapimages/dst/")
end
def mapfile
self.class.mapfile_path(self.id)
# RAILS_ROOT+"/db/mapfiles/"+self.id.to_s+".map"
end
def warped_dir
dest_dir
end
def unwarped_filename
File.join(maps_dir, self.filename)
end
def warped_filename
File.join(warped_dir, id.to_s) + ".tif"
end
def warped_png
warped_filename + ".png"
end
def public_warped_tif_url
"mapimages/dst/"+id.to_s + ".tif"
end
def public_warped_png_url
public_warped_tif_url + ".png"
end
def mask_file_format
"gml"
end
def temp_filename
# self.full_filename + "_temp"
File.join(warped_dir, id.to_s) + "_temp"
end
def masking_file_gml
File.join(RAILS_ROOT, "/public/mapimages/", self.id.to_s) + ".gml"
end
#file made when rasterizing
def masking_file_gfs
File.join(RAILS_ROOT, "/public/mapimages/", self.id.to_s) + ".gfs"
end
def masked_src_filename
self.unwarped_filename + "_masked";
end
#############################################
#CLASS METHODS
#############################################
def self.mapfile_path(mapid)
File.join(RAILS_ROOT,"/db/mapfiles/", mapid.to_s + ".map")
end
def self.max_attachment_size
max_attachment_size = defined?(MAX_ATTACHMENT_SIZE)? MAX_ATTACHMENT_SIZE : nil
end
def self.max_dimension
max_dimension = defined?(MAX_DIMENSION)? MAX_DIMENSION : nil
end
#saves tilecache's config file
def self.save_tilecache_config
@maps = Map.all(:conditions => "status = 4")
cfg = File.open(RAILS_ROOT+"/public/cgi/tilecache.cfg", File::CREAT|File::TRUNC|File::RDWR, 0666)
template = File.open(RAILS_ROOT + "/db/maptemplates/tilecache.text.erb").read
cfg.puts ERB.new(template).result( binding )
cfg.close
end
#############################################
#INSTANCE METHODS
#############################################
def warped?
status == :warped
end
def available?
return [:available,:warping,:warped].include?(status)
end
def last_changed
if self.gcps.size > 0
self.gcps.last.created_at
elsif !self.updated_at.nil?
self.updated_at
elsif !self.created_at.nil?
self.created_at
else
Time.now
end
end
def save_bbox
stdin, stdout, stderr = Open3::popen3("#{GDAL_PATH}gdalinfo #{warped_filename}")
unless stderr.readlines.to_s.size > 0
info = stdout.readlines.to_s
string,west,south = info.match(/Lower Left\s+\(\s*([-.\d]+),\s+([-.\d]+)/).to_a
string,east,north = info.match(/Upper Right\s+\(\s*([-.\d]+),\s+([-.\d]+)/).to_a
self.bbox = [west,south,east,north].join(",")
else
logger.debug "Save bbox error "+ stderr.readlines.to_s
end
end
def bounds
if bbox.nil?
x_array = []
y_array = []
self.gcps.each do |gcp|
#logger.info "GCP lat #{gcp[:lat]} , lon #{gcp[:lon]} "
x_array << gcp[:lat]
y_array << gcp[:lon]
end
#south, west, north, east
our_bounds = [y_array.min ,x_array.min ,y_array.max, x_array.max].join ','
else
bbox
end
end
def converted_bbox
bnds = self.bounds.split(",")
cbounds = []
c_in, c_out, c_err =
Open3::popen3("echo #{bnds[0]} #{bnds[1]} | #{GDAL_PATH}cs2cs +proj=latlong +datum=WGS84 +to +proj=merc +ellps=sphere +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0")
info = c_out.readlines.to_s
string,cbounds[0], cbounds[1] = info.match(/([-.\d]+)\s*([-.\d]+).*/).to_a
c_in, c_out, c_err =
Open3::popen3("echo #{bnds[2]} #{bnds[3]} | #{GDAL_PATH}cs2cs +proj=latlong +datum=WGS84 +to +proj=merc +ellps=sphere +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0")
info = c_out.readlines.to_s
string,cbounds[2], cbounds[3] = info.match(/([-.\d]+)\s*([-.\d]+).*/).to_a
cbounds.join(",")
end
#method to publish the map to cluster
def publish
#self.status = :published
#save!
end
#attempts to align based on the extent and offset of the
#reference map's warped image
#results it nicer gpcs to edit with later
def align_with_warped (srcmap, align = nil, append = false)
srcmap = Map.find(srcmap)
origgcps = srcmap.gcps
#clear out original gcps, unless we want to append the copied gcps to the existing ones
self.gcps.destroy_all unless append == true
#extent of source from gdalinfo
stdin, stdout, sterr = Open3::popen3("#{GDAL_PATH}gdalinfo #{srcmap.warped_filename}")
info = stdout.readlines.to_s
string_lw,west,south = info.match(/Lower Left\s+\(\s*([-.\d]+), \s+([-.\d]+)/).to_a
string_ur,east,north = info.match(/Upper Right\s+\(\s*([-.\d]+), \s+([-.\d]+)/).to_a
lon_shift = west.to_f - east.to_f
lat_shift = south.to_f - north.to_f
origgcps.each do |gcp|
a = Gcp.new()
a = gcp.clone
if align == "east"
a.lon -= lon_shift
elsif align == "west"
a.lon += lon_shift
elsif align == "north"
a.lat -= lat_shift
elsif align == "south"
a.lat += lat_shift
else
#if no align, then dont change the gcps
end
a.map = self
a.save
end
newgcps = self.gcps
end
#attempts to align based on the width and height of
#reference map's un warped image
#results it potential better fit than align_with_warped
#but with less accessible gpcs to edit
def align_with_original(srcmap, align = nil, append = false)
srcmap = Map.find(srcmap)
origgcps = srcmap.gcps
#clear out original gcps, unless we want to append the copied gcps to the existing ones
self.gcps.destroy_all unless append == true
origgcps.each do |gcp|
new_gcp = Gcp.new()
new_gcp = gcp.clone
if align == "east"
new_gcp.x -= srcmap.width
elsif align == "west"
new_gcp.x += srcmap.width
elsif align == "north"
new_gcp.y += srcmap.height
elsif align == "south"
new_gcp.y -= srcmap.height
else
#if no align, then dont change the gcps
end
new_gcp.map = self
new_gcp.save
end
newgcps = self.gcps
end
# map gets error attibute set and gcps get error attribute set
def gcps_with_error
gcps = Gcp.find(:all, :conditions =>["map_id = ?", self.id], :order => 'created_at')
gcps, map_error = ErrorCalculator::calc_error(gcps)
@error = map_error
#send back the gpcs with error calculation
gcps
end
#Main warp method
def warp!(resample_option, transform_option, use_mask="false")
self.status = :warping
save!
gcp_array = self.gcps
gcp_string = ""
gcp_array.each do |gcp|
gcp_string = gcp_string + gcp.gdal_string
end
mask_options = ""
if use_mask == "true" && self.mask_status == :masked
src_filename = self.masked_src_filename
mask_options = " -srcnodata '17 17 17' "
else
src_filename = self.unwarped_filename
end
dest_filename = self.warped_filename
temp_filename = self.temp_filename
#delete existing temp images @map.delete_images
if File.exists?(dest_filename)
File.delete(dest_filename)
end
logger.info "gdal translate"
t_stdin, t_stdout, t_stderr = Open3::popen3(
"#{GDAL_PATH}gdal_translate -a_srs '+init=epsg:4326' -of VRT #{src_filename} #{temp_filename}.vrt #{gcp_string}"
)
logger.info "gdal_translate -a_srs '+init=epsg:4326' -of VRT #{src_filename} #{temp_filename}.vrt #{gcp_string}"
t_out = t_stdout.readlines.to_s
t_err = t_stderr.readlines.to_s
if t_err.size > 0
logger.error "ERROR gdal translate script: "+ t_err
logger.error "Output = " +t_out
t_out = "ERROR with gdal translate script: " + t_err + "
You may want to try it again?
" + t_out
else
t_out = "Okay, translate command ran fine!