PageRenderTime 52ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/app/models/map.rb

https://github.com/jywarren/mapknitter
Ruby | 399 lines | 356 code | 31 blank | 12 comment | 49 complexity | f0b3eb288b9a1be47d7a328b41f072bb MD5 | raw file
  1. require 'open3'
  2. class NotAtOriginValidator < ActiveModel::Validator
  3. def validate(record)
  4. if record.lat == 0 || record.lon == 0
  5. record.errors[:base] << "Your location at 0,0 is unlikely."
  6. end
  7. end
  8. end
  9. class Map < ActiveRecord::Base
  10. extend FriendlyId
  11. friendly_id :name, :use => [:slugged, :static]
  12. attr_accessible :author, :name, :slug, :lat, :lon, :location, :description, :zoom, :license
  13. validates_presence_of :name, :slug, :author, :lat, :lon
  14. validates_uniqueness_of :slug
  15. validates_presence_of :location, :message => ' cannot be found. Try entering a latitude and longitude if this problem persists.'
  16. validates_format_of :slug,
  17. :with => /^[\w-]*$/,
  18. :message => " must not include spaces and must be alphanumeric, as it'll be used in the URL of your map, like: https://mapknitter.org/maps/your-map-name. You may use dashes and underscores.",
  19. :on => :create
  20. # validates_format_of :tile_url, :with => /^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/ix
  21. validates_with NotAtOriginValidator
  22. has_many :exports, :dependent => :destroy
  23. has_many :tags, :dependent => :destroy
  24. has_many :comments, :dependent => :destroy
  25. has_many :annotations, :dependent => :destroy
  26. belongs_to :user
  27. has_many :warpables do
  28. def public_filenames
  29. filenames = {}
  30. self.each do |warpable|
  31. filenames[warpable.id] = {}
  32. sizes = Array.new(Warpable::THUMBNAILS.keys).push(nil)
  33. sizes.each do |size|
  34. key = size != nil ? size : "original"
  35. filenames[warpable.id][key] = warpable.public_filename(size)
  36. end
  37. end
  38. filenames
  39. end
  40. end
  41. def validate
  42. self.name != 'untitled'
  43. self.lat >= -90 && self.lat <= 90 && self.lon >= -180 && self.lat <= 180
  44. end
  45. # Hash the password before saving the record
  46. def before_create
  47. self.password = Password::update(self.password) if self.password != ""
  48. end
  49. def placed_warpables
  50. self.warpables.where('width > 0 AND nodes <> ""')
  51. end
  52. def private
  53. self.password != ""
  54. end
  55. def anonymous?
  56. self.author == "" || self.user_id == 0
  57. end
  58. def self.bbox(minlat,minlon,maxlat,maxlon)
  59. Map.find :all, :conditions => ['lat > ? AND lat < ? AND lon > ? AND lon < ?',minlat,maxlat,minlon,maxlon]
  60. end
  61. def exporting?
  62. self.export && self.export.running?
  63. end
  64. def export
  65. self.latest_export
  66. end
  67. def latest_export
  68. self.exports.last
  69. end
  70. def self.authors(limit = 50)
  71. Map.find(:all, :limit => limit, :group => "maps.author", :order => "id DESC", :conditions => ['password = "" AND archived = "false"']).collect(&:author)
  72. end
  73. def self.new_maps
  74. self.find(:all, :order => "created_at DESC", :limit => 12, :conditions => ['password = "" AND archived = "false"'])
  75. end
  76. def nodes
  77. nodes = {}
  78. self.warpables.each do |warpable|
  79. if warpable.nodes != ''
  80. w_nodes = []
  81. warpable.nodes.split(',').each do |node|
  82. node_obj = Node.find(node)
  83. w_nodes << [node_obj.lon,node_obj.lat]
  84. end
  85. nodes[warpable.id.to_s] = w_nodes
  86. end
  87. nodes[warpable.id.to_s] ||= 'none'
  88. end
  89. nodes
  90. end
  91. # find all other maps within <dist> degrees lat or lon
  92. def nearby_maps(dist)
  93. if self.lat.to_f == 0.0 || self.lon.to_f == 0.0
  94. return []
  95. else
  96. return Map.find(:all,:conditions => ['id != ? AND lat > ? AND lat < ? AND lon > ? AND lon < ?',self.id,self.lat-dist,self.lat+dist,self.lon-dist,self.lon+dist], :limit => 10)
  97. end
  98. end
  99. def average_scale
  100. # determine optimal zoom level
  101. puts '> calculating scale'
  102. pxperms = []
  103. self.placed_warpables.each do |warpable|
  104. pxperms << 100.00/warpable.cm_per_pixel if warpable.placed?
  105. end
  106. average = (pxperms.inject {|sum, n| sum + n })/pxperms.length
  107. average
  108. end
  109. def best_cm_per_pixel
  110. hist = self.images_histogram
  111. scores = []
  112. (0..(hist.length-1)).each do |i|
  113. scores[i] = 0
  114. scores[i] += hist[i-3] if i > 3
  115. scores[i] += hist[i-2] if i > 2
  116. scores[i] += hist[i-1] if i > 1
  117. scores[i] += hist[i]
  118. scores[i] += hist[i+1] if i < hist.length - 2
  119. scores[i] += hist[i+2] if i < hist.length - 3
  120. scores[i] += hist[i+3] if i < hist.length - 4
  121. end
  122. highest = 0
  123. scores.each_with_index do |s,i|
  124. highest = i if s > scores[highest]
  125. end
  126. highest
  127. end
  128. def average_cm_per_pixel
  129. if self.warpables.length > 0
  130. scales = []
  131. count = 0
  132. average = 0
  133. self.placed_warpables.each do |warpable|
  134. count += 1
  135. res = warpable.cm_per_pixel
  136. res = 1 if res == 0 # let's not ever try to go for infinite resolution
  137. scales << res unless res == nil
  138. end
  139. sum = (scales.inject {|sum, n| sum + n }) if scales
  140. average = sum/count if sum
  141. average
  142. else
  143. 0
  144. end
  145. end
  146. # for sparklines graph display
  147. def images_histogram
  148. hist = []
  149. self.warpables.each do |warpable|
  150. res = warpable.cm_per_pixel.to_i
  151. hist[res] = 0 if hist[res] == nil
  152. hist[res] += 1
  153. end
  154. (0..hist.length-1).each do |bin|
  155. hist[bin] = 0 if hist[bin] == nil
  156. end
  157. hist
  158. end
  159. # for sparklines graph display
  160. def grouped_images_histogram(binsize)
  161. hist = []
  162. self.warpables.each do |warpable|
  163. res = warpable.cm_per_pixel
  164. if res != nil
  165. res = (warpable.cm_per_pixel/(0.001+binsize)).to_i
  166. hist[res] = 0 if hist[res] == nil
  167. hist[res] += 1
  168. end
  169. end
  170. (0..hist.length-1).each do |bin|
  171. hist[bin] = 0 if hist[bin] == nil
  172. end
  173. hist
  174. end
  175. def run_export(user,resolution)
  176. begin
  177. unless export = self.export
  178. export = Export.new({
  179. :map_id => self.id
  180. })
  181. end
  182. export.user_id = user.id if user
  183. export.status = 'starting'
  184. export.tms = false
  185. export.geotiff = false
  186. export.zip = false
  187. export.jpg = false
  188. export.save
  189. directory = "#{Rails.root}/public/warps/"+self.slug+"/"
  190. stdin, stdout, stderr = Open3.popen3('rm -r '+directory.to_s)
  191. puts stdout.readlines
  192. puts stderr.readlines
  193. stdin, stdout, stderr = Open3.popen3("rm -r #{Rails.root}/public/tms/#{self.slug}")
  194. puts stdout.readlines
  195. puts stderr.readlines
  196. puts '> averaging scales'
  197. pxperm = 100/(resolution).to_f || self.average_scale # pixels per meter
  198. puts '> distorting warpables'
  199. origin = self.distort_warpables(pxperm)
  200. warpable_coords = origin.pop
  201. export = self.export
  202. export.status = 'compositing'
  203. export.save
  204. puts '> generating composite tiff'
  205. composite_location = self.generate_composite_tiff(warpable_coords,origin)
  206. info = (`identify -quiet -format '%b,%w,%h' #{composite_location}`).split(',')
  207. puts info
  208. export = self.export
  209. if info[0] != ''
  210. export.geotiff = true
  211. export.size = info[0]
  212. export.width = info[1]
  213. export.height = info[2]
  214. export.cm_per_pixel = 100.0000/pxperm
  215. export.status = 'tiling'
  216. export.save
  217. end
  218. puts '> generating tiles'
  219. export = self.export
  220. export.tms = true if self.generate_tiles
  221. export.status = 'zipping tiles'
  222. export.save
  223. puts '> zipping tiles'
  224. export = self.export
  225. export.zip = true if self.zip_tiles
  226. export.status = 'creating jpg'
  227. export.save
  228. puts '> generating jpg'
  229. export = self.export
  230. export.jpg = true if self.generate_jpg
  231. export.status = 'complete'
  232. export.save
  233. rescue SystemCallError
  234. export = self.export
  235. export.status = 'failed'
  236. export.save
  237. end
  238. return export.status
  239. end
  240. # distort all warpables, returns upper left corner coords in x,y
  241. def distort_warpables(scale)
  242. export = self.latest_export
  243. puts '> generating geotiffs of each warpable in GDAL'
  244. lowest_x=0
  245. lowest_y=0
  246. warpable_coords = []
  247. warpables = self.placed_warpables
  248. current = 0
  249. warpables.each do |warpable|
  250. current += 1
  251. export.status = 'warping '+current.to_s+' of '+warpables.length.to_s
  252. puts 'warping '+current.to_s+' of '+warpables.length.to_s
  253. export.save
  254. my_warpable_coords = warpable.generate_perspectival_distort(scale,self.slug)
  255. puts '- '+my_warpable_coords.to_s
  256. warpable_coords << my_warpable_coords
  257. lowest_x = my_warpable_coords.first if (my_warpable_coords.first < lowest_x || lowest_x == 0)
  258. lowest_y = my_warpable_coords.last if (my_warpable_coords.last < lowest_y || lowest_y == 0)
  259. end
  260. [lowest_x,lowest_y,warpable_coords]
  261. end
  262. # generate a tiff from all warpable images in this set
  263. def generate_composite_tiff(coords,origin)
  264. directory = "public/warps/"+self.slug+"/"
  265. composite_location = directory+self.slug+'-geo.tif'
  266. geotiffs = ''
  267. minlat = nil
  268. minlon = nil
  269. maxlat = nil
  270. maxlon = nil
  271. self.placed_warpables.each do |warpable|
  272. warpable.nodes_array.each do |n|
  273. minlat = n.lat if minlat == nil || n.lat < minlat
  274. minlon = n.lon if minlon == nil || n.lon < minlon
  275. maxlat = n.lat if maxlat == nil || n.lat > maxlat
  276. maxlon = n.lon if maxlon == nil || n.lon > maxlon
  277. end
  278. end
  279. first = true
  280. # sort by area; this would be overridden by a provided order
  281. warpables = self.placed_warpables.sort{|a,b|b.poly_area <=> a.poly_area}
  282. warpables.each do |warpable|
  283. geotiffs += ' '+directory+warpable.id.to_s+'-geo.tif'
  284. if first
  285. gdalwarp = "gdalwarp -te "+minlon.to_s+" "+minlat.to_s+" "+maxlon.to_s+" "+maxlat.to_s+" "+directory+warpable.id.to_s+'-geo.tif '+directory+self.slug+'-geo.tif'
  286. first = false
  287. else
  288. gdalwarp = "gdalwarp "+directory+warpable.id.to_s+'-geo.tif '+directory+self.slug+'-geo.tif'
  289. end
  290. puts gdalwarp
  291. system(Gdal.ulimit+gdalwarp)
  292. end
  293. composite_location
  294. end
  295. # generates a tileset at Rails.root.to_s/public/tms/<map_name>/
  296. def generate_tiles
  297. google_api_key = APP_CONFIG["google_maps_api_key"]
  298. gdal2tiles = 'gdal2tiles.py -k -t "'+self.slug+'" -g "'+google_api_key+'" '+Rails.root.to_s+'/public/warps/'+self.slug+'/'+self.slug+'-geo.tif '+Rails.root.to_s+'/public/tms/'+self.slug+"/"
  299. # puts gdal2tiles
  300. # puts system('which gdal2tiles.py')
  301. system(Gdal.ulimit+gdal2tiles)
  302. end
  303. # zips up tiles at Rails.root/public/tms/<map_name>.zip
  304. def zip_tiles
  305. rmzip = 'cd public/tms/ && rm '+self.slug+'.zip && cd ../../'
  306. system(Gdal.ulimit+rmzip)
  307. zip = 'cd public/tms/ && zip -rq '+self.slug+'.zip '+self.slug+'/ && cd ../../'
  308. # puts zip
  309. # puts system('which gdal2tiles.py')
  310. system(Gdal.ulimit+zip)
  311. end
  312. def generate_jpg
  313. imageMagick = 'convert -background white -flatten '+Rails.root.to_s+'/public/warps/'+self.slug+'/'+self.slug+'-geo.tif '+Rails.root.to_s+'/public/warps/'+self.slug+'/'+self.slug+'.jpg'
  314. system(Gdal.ulimit+imageMagick)
  315. end
  316. def after_create
  317. puts 'saving Map'
  318. if last = Map.find_by_name(self.slug,:order => "version DESC")
  319. self.version = last.version + 1
  320. end
  321. end
  322. def license_link
  323. if self.license == "cc-by"
  324. "<a href='http://creativecommons.org/licenses/by/3.0/'>Creative Commons Attribution 3.0 Unported License</a>"
  325. elsif self.license == "publicdomain"
  326. "<a href='http://creativecommons.org/publicdomain/zero/1.0/'>Public Domain</a>"
  327. end
  328. end
  329. def polygons(dist)
  330. nodes = Node.find(:all,:conditions => ['lat > ? AND lat < ? AND lon > ? AND lon < ? AND way_id != 0 AND map_id != 0',self.lat-dist,self.lat+dist,self.lon-dist,self.lon+dist], :limit => 50, :order => "way_order DESC")
  331. Way.where('id IN (?)',nodes.collect(&:way_id).uniq)
  332. end
  333. def legacy_annotations(dist)
  334. Node.find(:all,:conditions => ['lat > ? AND lat < ? AND lon > ? AND lon < ? AND way_id = 0 AND map_id != 0',self.lat-dist,self.lat+dist,self.lon-dist,self.lon+dist], :limit => 50, :order => "id DESC")
  335. end
  336. #--------------------
  337. def has_tag(tagname)
  338. Tag.find(:all, :conditions => { :map_id => self.id, :name => tagname }).length > 0
  339. end
  340. def add_tag(tagname, user)
  341. tagname = tagname.downcase
  342. unless self.has_tag(tagname)
  343. tag = self.tags.create({
  344. :name => tagname,
  345. :user_id => user.id,
  346. :map_id => self.id
  347. })
  348. end
  349. end
  350. end