1 # amf_controller is a semi-standalone API for Flash clients, particularly
2 # Potlatch. All interaction between Potlatch (as a .SWF application) and the
3 # OSM database takes place using this controller. Messages are
4 # encoded in the Actionscript Message Format (AMF).
6 # Helper functions are in /lib/potlatch.
8 # Author:: editions Systeme D / Richard Fairhurst 2004-2008
9 # Licence:: public domain.
11 # == General structure
13 # Apart from the talk method (which distributes the requests from the
14 # AMF message), each method generally takes arguments in the order they were
15 # sent by the Potlatch SWF. Do not assume typing has been preserved. Methods
16 # all return an array to the SWF.
20 # Any method that returns a status code (0 for ok) can also send:
21 # return(-1,"message") <-- just puts up a dialogue
22 # return(-2,"message") <-- also asks the user to e-mail me
24 # To write to the Rails log, use RAILS_DEFAULT_LOGGER.info("message").
28 # - Check authentication
29 # - Check the right things are being written to the database!
31 class AmfController < ApplicationController
37 before_filter :check_write_availability
39 # Main AMF handler: processes the raw AMF string (using AMF library) and
40 # calls each action (private method) accordingly.
43 req=StringIO.new(request.raw_post+0.chr) # Get POST data as request
44 # (cf http://www.ruby-forum.com/topic/122163)
45 req.read(2) # Skip version indicator and client ID
46 results={} # Results of each body
47 renumberednodes={} # Shared across repeated putways
48 renumberedways={} # Shared across repeated putways
52 headers=AMF.getint(req) # Read number of headers
54 headers.times do # Read each header
55 name=AMF.getstring(req) # |
56 req.getc # | skip boolean
57 value=AMF.getvalue(req) # |
58 header["name"]=value # |
61 bodies=AMF.getint(req) # Read number of bodies
62 bodies.times do # Read each body
63 message=AMF.getstring(req) # | get message name
64 index=AMF.getstring(req) # | get index in response sequence
65 bytes=AMF.getlong(req) # | get total size in bytes
66 args=AMF.getvalue(req) # | get response (probably an array)
69 when 'getpresets'; results[index]=AMF.putdata(index,getpresets())
70 when 'whichways'; results[index]=AMF.putdata(index,whichways(*args))
71 when 'whichways_deleted'; results[index]=AMF.putdata(index,whichways_deleted(*args))
72 when 'getway'; results[index]=AMF.putdata(index,getway(args[0].to_i))
73 when 'getrelation'; results[index]=AMF.putdata(index,getrelation(args[0].to_i))
74 when 'getway_old'; results[index]=AMF.putdata(index,getway_old(args[0].to_i,args[1].to_i))
75 when 'getway_history'; results[index]=AMF.putdata(index,getway_history(args[0].to_i))
76 when 'putway'; r=putway(renumberednodes,*args)
79 renumberedways[r[1]] = r[2]
81 results[index]=AMF.putdata(index,r)
82 when 'putrelation'; results[index]=AMF.putdata(index,putrelation(renumberednodes, renumberedways, *args))
83 when 'deleteway'; results[index]=AMF.putdata(index,deleteway(args[0],args[1].to_i))
84 when 'putpoi'; results[index]=AMF.putdata(index,putpoi(*args))
85 when 'getpoi'; results[index]=AMF.putdata(index,getpoi(args[0].to_i))
91 a,b=results.length.divmod(256)
92 render :content_type => "application/x-amf", :text => proc { |response, output|
93 output.write 0.chr+0.chr+0.chr+0.chr+a.chr+b.chr
102 # Return presets (default tags, localisation etc.):
103 # uses POTLATCH_PRESETS global, set up in OSM::Potlatch.
105 def getpresets() #:doc:
106 return POTLATCH_PRESETS
109 # Find all the ways, POI nodes (i.e. not part of ways), and relations
110 # in a given bounding box. Nodes are returned in full; ways and relations
113 def whichways(xmin,ymin,xmax,ymax) #:doc:
114 xmin-=0.01; ymin-=0.01
115 xmax+=0.01; ymax+=0.01
117 if POTLATCH_USE_SQL then
118 way_ids=sql_find_way_ids_in_area(xmin,ymin,xmax,ymax)
119 points=sql_find_pois_in_area(xmin,ymin,xmax,ymax)
120 relation_ids=sql_find_relations_in_area_and_ways(xmin,ymin,xmax,ymax,way_ids)
122 # find the way ids in an area
123 nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => "current_nodes.visible = 1", :include => :ways)
124 way_ids = nodes_in_area.collect { |node| node.way_ids }.flatten.uniq
126 # find the node ids in an area that aren't part of ways
127 nodes_not_used_in_area = nodes_in_area.select { |node| node.ways.empty? }
128 points = nodes_not_used_in_area.collect { |n| [n.id, n.lon, n.lat, n.tags_as_hash] }
130 # find the relations used by those nodes and ways
131 relations = nodes_in_area.collect { |node| node.containing_relations.visible }.flatten +
132 way_ids.collect { |id| Way.find(id).containing_relations.visible }.flatten
133 relation_ids = relations.collect { |relation| relation.id }.uniq
136 [way_ids,points,relation_ids]
139 # Find deleted ways in current bounding box (similar to whichways, but ways
140 # with a deleted node only - not POIs or relations).
142 def whichways_deleted(xmin,ymin,xmax,ymax) #:doc:
143 xmin-=0.01; ymin-=0.01
144 xmax+=0.01; ymax+=0.01
146 nodes_in_area = Node.find_by_area(ymin, xmin, ymax,xmax, :conditions => "current_nodes.visible=0 AND current_ways.visible=0", :include => :ways_via_history )
147 way_ids = nodes_in_area.collect { |node| node.ways_via_history_ids }.flatten.uniq
151 # Get a way including nodes and tags.
152 # Returns 0 (success), a Potlatch-style array of points, and a hash of tags.
154 def getway(wayid) #:doc:
155 if POTLATCH_USE_SQL then
156 points=sql_get_nodes_in_way(wayid)
157 tags=sql_get_tags_in_way(wayid)
159 # Ideally we would do ":include => :nodes" here but if we do that
160 # then rails only seems to return the first copy of a node when a
161 # way includes a node more than once
163 way = Way.find(wayid)
164 way.nodes.each do |node|
165 points << [node.lon, node.lat, node.id, nil, node.tags_as_hash]
172 # Get an old version of a way, and all constituent nodes.
174 # For undelete (version=0), always uses the most recent version of each node,
175 # even if it's moved. For revert (version=1+), uses the node in existence
176 # at the time, generating a new id if it's still visible and has been moved/
179 def getway_old(id,version) #:doc:
181 old_way = OldWay.find(:first, :conditions => ['visible=1 AND id=?', id], :order => 'version DESC')
182 points = old_way.get_nodes_undelete
184 old_way = OldWay.find(:first, :conditions => ['id=? AND version=?', id, version])
185 points = old_way.get_nodes_revert
187 old_way.tags['history']="Retrieved from v#{old_way.version}"
188 [0,id,points,old_way.tags,old_way.version]
191 # Find history of a way. Returns an array of previous versions.
193 def getway_history(wayid) #:doc:
196 way.old_ways.each do |old_way|
197 if old_way.user.data_public then user=old_way.user.display_name else user='anonymous' end
198 history<<[old_way.version,old_way.timestamp.strftime("%d %b %Y, %H:%M"),old_way.visible ? 1 : 0,user]
203 # Get a relation with all tags and members.
207 # 2. list of members.
209 def getrelation(relid) #:doc:
210 rel = Relation.find(relid)
211 [relid,rel.tags,rel.members]
217 # 1. original relation id (unchanged),
218 # 2. new relation id.
220 def putrelation(renumberednodes, renumberedways, usertoken,relid,tags,members,visible) #:doc:
221 uid=getuserid(usertoken)
222 if !uid then return -1,"You are not logged in, so the point could not be saved." end
225 visible = visible.to_i
227 # create a new relation, or find the existing one
231 rel = Relation.find(relid)
234 # check the members are all positive, and correctly type
239 mid = renumberednodes[mid] if m[0] == 'node'
240 mid = renumberedways[mid] if m[0] == 'way'
242 return -2, "Negative ID unresolved"
245 typedmembers << [m[0], mid, m[2]]
248 # assign new contents
249 rel.members = typedmembers
251 rel.visible = visible
254 # check it then save it
255 # BUG: the following is commented out because it always fails on my
256 # install. I think it's a Rails bug.
258 #if !rel.preconditions_ok?
259 # return -2, "Relation preconditions failed"
261 rel.save_with_history!
267 # Save a way to the database, including all nodes. Any nodes in the previous
268 # version and no longer used are deleted.
271 # 0. '0' (code for success),
272 # 1. original way id (unchanged),
274 # 3. hash of renumbered nodes (old id=>new id)
276 def putway(renumberednodes,usertoken,originalway,points,attributes) #:doc:
278 # -- Initialise and carry out checks
280 uid=getuserid(usertoken)
281 if !uid then return -1,"You are not logged in, so the way could not be saved." end
282 originalway=originalway.to_i
285 if a[2]==0 or a[2].nil? then return -2,"Server error - node with id 0 found in way #{originalway}." end
286 if a[1]==90 then return -2,"Server error - node with lat -90 found in way #{originalway}." end
289 if points.length<2 then return -2,"Server error - way is only #{points.length} points long." end
291 # -- Get unique nodes
297 way=Way.find(originalway)
298 uniques=way.unique_nodes
301 # -- Compare nodes and save changes to any that have changed
310 if renumberednodes[id]
311 id=renumberednodes[id]
318 if (!fpcomp(lat,node.lat) or !fpcomp(lon,node.lon) or \
319 Tags.join(n[4])!=node.tags or node.visible==0)
325 node.lat=lat; node.lon=lon
326 node.tags=Tags.join(n[4])
328 node.save_with_history!
330 renumberednodes[id]=node.id
338 # -- Delete any unique nodes
341 deleteitemrelations(n,'node')
345 node.save_with_history!
348 # -- Save revised way
354 way.save_with_history!
356 [0,originalway,way.id,renumberednodes]
359 # Save POI to the database.
360 # Refuses save if the node has since become part of a way.
363 # 1. original node id (unchanged),
366 def putpoi(usertoken,id,lon,lat,tags,visible) #:doc:
367 uid=getuserid(usertoken)
368 if !uid then return -1,"You are not logged in, so the point could not be saved." end
371 visible=(visible.to_i==1)
376 unless node.ways.empty? then return -1,"The point has since become part of a way, so you cannot save it as a POI." end
377 deleteitemrelations(id,'node')
384 node.latitude = (lat*10000000).round
385 node.longitude = (lon*10000000).round
386 node.tags = Tags.join(tags)
388 node.save_with_history!
393 # Read POI from database
394 # (only called on revert: POIs are usually read by whichways).
396 # Returns array of id, long, lat, hash of tags.
398 def getpoi(id) #:doc:
401 return [n.id, n.lon, n.lat, n.tags_as_hash]
403 return [nil,nil,nil,'']
407 # Delete way and all constituent nodes. Also removes from any relations.
408 # Returns 0 (success), unchanged way id.
410 def deleteway(usertoken,way_id) #:doc:
411 uid=getuserid(usertoken)
412 if !uid then return -1,"You are not logged in, so the way could not be deleted." end
414 # FIXME: would be good not to make two history entries when removing
415 # two nodes from the same relation
416 user = User.find(uid)
417 way = Way.find(way_id)
418 way.unique_nodes.each do |n|
419 deleteitemrelations(n,'node')
422 way.delete_with_relations_and_nodes_and_history(user)
427 # ====================================================================
430 # Remove a node or way from all relations
432 def deleteitemrelations(objid,type) #:doc:
433 relationids = RelationMember.find(:all, :conditions => ['member_type=? and member_id=?', type, objid]).collect { |ws| ws.id }.uniq
434 relationids.each do |relid|
435 rel=Relation.find(relid)
436 rel.members.delete_if {|x| x[0]==type and x[1]==objid}
437 rel.save_with_history!
441 # Break out node tags into a hash
442 # (should become obsolete as of API 0.6)
444 def tagstring_to_hash(a) #:doc:
446 Tags.split(a) do |k, v|
453 # (could be removed if no-one uses the username+password form)
455 def getuserid(token) #:doc:
456 if (token =~ /^(.+)\+(.+)$/) then
457 user = User.authenticate(:username => $1, :password => $2)
459 user = User.authenticate(:token => token)
462 return user ? user.id : nil;
465 # Compare two floating-point numbers to within 0.0000001
467 def fpcomp(a,b) #:doc:
468 return ((a/0.0000001).round==(b/0.0000001).round)
472 # ====================================================================
473 # Alternative SQL queries for getway/whichways
475 def sql_find_way_ids_in_area(xmin,ymin,xmax,ymax)
477 SELECT DISTINCT current_way_nodes.id AS wayid
478 FROM current_way_nodes
479 INNER JOIN current_nodes ON current_nodes.id=current_way_nodes.node_id
480 INNER JOIN current_ways ON current_ways.id =current_way_nodes.id
481 WHERE current_nodes.visible=1
482 AND current_ways.visible=1
483 AND #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")}
485 return ActiveRecord::Base.connection.select_all(sql).collect { |a| a['wayid'].to_i }
488 def sql_find_pois_in_area(xmin,ymin,xmax,ymax)
490 SELECT current_nodes.id,current_nodes.latitude*0.0000001 AS lat,current_nodes.longitude*0.0000001 AS lon,current_nodes.tags
492 LEFT OUTER JOIN current_way_nodes cwn ON cwn.node_id=current_nodes.id
493 WHERE current_nodes.visible=1
495 AND #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")}
497 return ActiveRecord::Base.connection.select_all(sql).collect { |n| [n['id'].to_i,n['lon'].to_f,n['lat'].to_f,tagstring_to_hash(n['tags'])] }
500 def sql_find_relations_in_area_and_ways(xmin,ymin,xmax,ymax,way_ids)
501 # ** It would be more Potlatchy to get relations for nodes within ways
502 # during 'getway', not here
504 SELECT DISTINCT cr.id AS relid
505 FROM current_relations cr
506 INNER JOIN current_relation_members crm ON crm.id=cr.id
507 INNER JOIN current_nodes cn ON crm.member_id=cn.id AND crm.member_type='node'
508 WHERE #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "cn.")}
510 unless way_ids.empty?
513 SELECT DISTINCT cr.id AS relid
514 FROM current_relations cr
515 INNER JOIN current_relation_members crm ON crm.id=cr.id
516 WHERE crm.member_type='way'
517 AND crm.member_id IN (#{way_ids.join(',')})
520 return ActiveRecord::Base.connection.select_all(sql).collect { |a| a['relid'].to_i }.uniq
523 def sql_get_nodes_in_way(wayid)
526 SELECT latitude*0.0000001 AS lat,longitude*0.0000001 AS lon,current_nodes.id,tags
527 FROM current_way_nodes,current_nodes
528 WHERE current_way_nodes.id=#{wayid.to_i}
529 AND current_way_nodes.node_id=current_nodes.id
530 AND current_nodes.visible=1
533 ActiveRecord::Base.connection.select_all(sql).each do |row|
534 points << [row['lon'].to_f,row['lat'].to_f,row['id'].to_i,nil,tagstring_to_hash(row['tags'])]
539 def sql_get_tags_in_way(wayid)
541 ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_way_tags WHERE id=#{wayid.to_i}").each do |row|
542 tags[row['k']]=row['v']