1st amf_controller tests, and some basic sanity check fixes
[rails.git] / app / controllers / amf_controller.rb
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).
5 #
6 # Helper functions are in /lib/potlatch.
7 #
8 # Author::      editions Systeme D / Richard Fairhurst 2004-2008
9 # Licence::     public domain.
10 #
11 # == General structure
12 #
13 # Apart from the amf_read and amf_write methods (which distribute the requests
14 # from the AMF message), each method generally takes arguments in the order 
15 # they were sent by the Potlatch SWF. Do not assume typing has been preserved. 
16 # Methods all return an array to the SWF.
17
18 # == Debugging
19
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
23
24 # To write to the Rails log, use RAILS_DEFAULT_LOGGER.info("message").
25
26 class AmfController < ApplicationController
27   require 'stringio'
28
29   include Potlatch
30
31   # Help methods for checking boundary sanity and area size
32   include MapBoundary
33
34   session :off
35   before_filter :check_write_availability
36
37   # Main AMF handlers: process the raw AMF string (using AMF library) and
38   # calls each action (private method) accordingly.
39   # ** FIXME: refactor to reduce duplication of code across read/write
40   
41   def amf_read
42         req=StringIO.new(request.raw_post+0.chr)# Get POST data as request
43                                                                                         # (cf http://www.ruby-forum.com/topic/122163)
44         req.read(2)                                                             # Skip version indicator and client ID
45         results={}                                                              # Results of each body
46
47         # Parse request
48
49         headers=AMF.getint(req)                                 # Read number of headers
50
51         headers.times do                                                # Read each header
52           name=AMF.getstring(req)                               #  |
53           req.getc                                                              #  | skip boolean
54           value=AMF.getvalue(req)                               #  |
55           header["name"]=value                                  #  |
56         end
57
58         bodies=AMF.getint(req)                                  # Read number of bodies
59         bodies.times do                                                 # Read each body
60           message=AMF.getstring(req)                    #  | get message name
61           index=AMF.getstring(req)                              #  | get index in response sequence
62           bytes=AMF.getlong(req)                                #  | get total size in bytes
63           args=AMF.getvalue(req)                                #  | get response (probably an array)
64
65           case message
66                 when 'getpresets';                      results[index]=AMF.putdata(index,getpresets())
67                 when 'whichways';                       results[index]=AMF.putdata(index,whichways(*args))
68                 when 'whichways_deleted';       results[index]=AMF.putdata(index,whichways_deleted(*args))
69                 when 'getway';                          results[index]=AMF.putdata(index,getway(args[0].to_i))
70                 when 'getrelation';                     results[index]=AMF.putdata(index,getrelation(args[0].to_i))
71                 when 'getway_old';                      results[index]=AMF.putdata(index,getway_old(args[0].to_i,args[1].to_i))
72                 when 'getway_history';          results[index]=AMF.putdata(index,getway_history(args[0].to_i))
73                 when 'getnode_history';         results[index]=AMF.putdata(index,getnode_history(args[0].to_i))
74                 when 'findrelations';           results[index]=AMF.putdata(index,findrelations(*args))
75                 when 'getpoi';                          results[index]=AMF.putdata(index,getpoi(*args))
76           end
77         end
78     sendresponse(results)
79   end
80
81   def amf_write
82         req=StringIO.new(request.raw_post+0.chr)
83         req.read(2)
84         results={}
85         renumberednodes={}                                              # Shared across repeated putways
86         renumberedways={}                                               # Shared across repeated putways
87
88         headers=AMF.getint(req)                                 # Read number of headers
89         headers.times do                                                # Read each header
90           name=AMF.getstring(req)                               #  |
91           req.getc                                                              #  | skip boolean
92           value=AMF.getvalue(req)                               #  |
93           header["name"]=value                                  #  |
94         end
95
96         bodies=AMF.getint(req)                                  # Read number of bodies
97         bodies.times do                                                 # Read each body
98           message=AMF.getstring(req)                    #  | get message name
99           index=AMF.getstring(req)                              #  | get index in response sequence
100           bytes=AMF.getlong(req)                                #  | get total size in bytes
101           args=AMF.getvalue(req)                                #  | get response (probably an array)
102
103           case message
104                 when 'putway';                          r=putway(renumberednodes,*args)
105                                                                         renumberednodes=r[3]
106                                                                         if r[1] != r[2]
107                                                                           renumberedways[r[1]] = r[2]
108                                                                         end
109                                                                         results[index]=AMF.putdata(index,r)
110                 when 'putrelation';                     results[index]=AMF.putdata(index,putrelation(renumberednodes, renumberedways, *args))
111                 when 'deleteway';                       results[index]=AMF.putdata(index,deleteway(args[0],args[1].to_i))
112                 when 'putpoi';                          results[index]=AMF.putdata(index,putpoi(*args))
113           end
114         end
115     sendresponse(results)
116   end
117
118   private
119
120   # Return presets (default tags, localisation etc.):
121   # uses POTLATCH_PRESETS global, set up in OSM::Potlatch.
122
123   def getpresets() #:doc:
124         return POTLATCH_PRESETS
125   end
126
127   # Find all the ways, POI nodes (i.e. not part of ways), and relations
128   # in a given bounding box. Nodes are returned in full; ways and relations 
129   # are IDs only. 
130
131   def whichways(xmin, ymin, xmax, ymax) #:doc:
132         xmin -= 0.01; ymin -= 0.01
133         xmax += 0.01; ymax += 0.01
134     
135     # check boundary is sane and area within defined
136     # see /config/application.yml
137     begin
138       check_boundaries(xmin, ymin, xmax, ymax)
139     rescue Exception => err
140       # FIXME: report an error rather than just return an empty result
141       return [[],[],[]]
142     end
143
144         if POTLATCH_USE_SQL then
145           way_ids = sql_find_way_ids_in_area(xmin, ymin, xmax, ymax)
146           points = sql_find_pois_in_area(xmin, ymin, xmax, ymax)
147           relation_ids = sql_find_relations_in_area_and_ways(xmin, ymin, xmax, ymax, way_ids)
148         else
149           # find the way ids in an area
150           nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => ["current_nodes.visible = ?", true], :include => :ways)
151           way_ids = nodes_in_area.collect { |node| node.way_ids }.flatten.uniq
152
153           # find the node ids in an area that aren't part of ways
154           nodes_not_used_in_area = nodes_in_area.select { |node| node.ways.empty? }
155           points = nodes_not_used_in_area.collect { |n| [n.id, n.lon, n.lat, n.tags_as_hash] }
156
157           # find the relations used by those nodes and ways
158           relations = Relation.find_for_nodes(nodes_in_area.collect { |n| n.id }, :conditions => {:visible => true}) +
159                   Relation.find_for_ways(way_ids, :conditions => {:visible => true})
160           relation_ids = relations.collect { |relation| relation.id }.uniq
161         end
162
163         [way_ids, points, relation_ids]
164   end
165
166   # Find deleted ways in current bounding box (similar to whichways, but ways
167   # with a deleted node only - not POIs or relations).
168
169   def whichways_deleted(xmin, ymin, xmax, ymax) #:doc:
170         xmin -= 0.01; ymin -= 0.01
171         xmax += 0.01; ymax += 0.01
172
173     # check boundary is sane and area within defined
174     # see /config/application.yml
175     begin
176       check_boundaries(xmin, ymin, xmax, ymax)
177     rescue Exception => err
178       # FIXME: report an error rather than just return an empty result
179       return [[]]
180     end
181
182         nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => ["current_ways.visible = ?", false], :include => :ways_via_history)
183         way_ids = nodes_in_area.collect { |node| node.ways_via_history_ids }.flatten.uniq
184
185         [way_ids]
186   end
187
188   # Get a way including nodes and tags.
189   # Returns 0 (success), a Potlatch-style array of points, and a hash of tags.
190
191   def getway(wayid) #:doc:
192         if POTLATCH_USE_SQL then
193           points = sql_get_nodes_in_way(wayid)
194           tags = sql_get_tags_in_way(wayid)
195         else
196           # Ideally we would do ":include => :nodes" here but if we do that
197           # then rails only seems to return the first copy of a node when a
198           # way includes a node more than once
199       begin
200             way = Way.find(wayid)
201       rescue ActiveRecord::RecordNotFound
202         return [wayid,[],{}]
203       end
204
205       # check case where way has been deleted or doesn't exist
206       return [wayid,[],{}] if way.nil? or !way.visible
207
208           points = way.nodes.collect do |node|
209                 nodetags=node.tags_as_hash
210                 nodetags.delete('created_by')
211                 [node.lon, node.lat, node.id, nodetags]
212           end
213           tags = way.tags
214         end
215
216         [wayid, points, tags]
217   end
218
219   # Get an old version of a way, and all constituent nodes.
220   #
221   # For undelete (version=0), always uses the most recent version of each node, 
222   # even if it's moved.  For revert (version=1+), uses the node in existence 
223   # at the time, generating a new id if it's still visible and has been moved/
224   # retagged.
225
226   def getway_old(id, version) #:doc:
227         if version < 0
228           old_way = OldWay.find(:first, :conditions => ['visible = ? AND id = ?', true, id], :order => 'version DESC')
229           points = old_way.get_nodes_undelete
230         else
231           old_way = OldWay.find(:first, :conditions => ['id = ? AND version = ?', id, version])
232           points = old_way.get_nodes_revert
233         end
234
235         old_way.tags['history'] = "Retrieved from v#{old_way.version}"
236
237         [0, id, points, old_way.tags, old_way.version]
238   end
239   
240   # Find history of a way. Returns 'way', id, and 
241   # an array of previous versions.
242
243   def getway_history(wayid) #:doc:
244         history = Way.find(wayid).old_ways.reverse.collect do |old_way|
245           user = old_way.user.data_public? ? old_way.user.display_name : 'anonymous'
246           uid  = old_way.user.data_public? ? old_way.user.id : 0
247           [old_way.version, old_way.timestamp.strftime("%d %b %Y, %H:%M"), old_way.visible ? 1 : 0, user, uid]
248         end
249
250         ['way',wayid,history]
251   end
252
253   # Find history of a node. Returns 'node', id, and 
254   # an array of previous versions.
255
256   def getnode_history(nodeid) #:doc:
257         history = Node.find(nodeid).old_nodes.reverse.collect do |old_node|
258           user = old_node.user.data_public? ? old_node.user.display_name : 'anonymous'
259           uid  = old_node.user.data_public? ? old_node.user.id : 0
260           [old_node.timestamp.to_i, old_node.timestamp.strftime("%d %b %Y, %H:%M"), old_node.visible ? 1 : 0, user, uid]
261         end
262
263         ['node',nodeid,history]
264   end
265
266   # Get a relation with all tags and members.
267   # Returns:
268   # 0. relation id,
269   # 1. hash of tags,
270   # 2. list of members.
271   
272   def getrelation(relid) #:doc:
273     begin
274           rel = Relation.find(relid)
275     rescue ActiveRecord::RecordNotFound
276       return [relid, {}, []]
277     end
278
279     return [relid, {}, []] if rel.nil? or !rel.visible
280
281         [relid, rel.tags, rel.members]
282   end
283
284   # Find relations with specified name/id.
285   # Returns array of relations, each in same form as getrelation.
286   
287   def findrelations(searchterm)
288         rels = []
289         if searchterm.to_i>0 then
290           rel = Relation.find(searchterm.to_i)
291           if rel and rel.visible then
292             rels.push([rel.id, rel.tags, rel.members])
293           end
294         else
295           RelationTag.find(:all, :limit => 11, :conditions => ["match(v) against (?)", searchterm] ).each do |t|
296                 if t.relation.visible then
297               rels.push([t.relation.id, t.relation.tags, t.relation.members])
298             end
299           end
300         end
301         rels
302   end
303
304   # Save a relation.
305   # Returns
306   # 0. 0 (success),
307   # 1. original relation id (unchanged),
308   # 2. new relation id.
309
310   def putrelation(renumberednodes, renumberedways, usertoken, relid, tags, members, visible) #:doc:
311         uid = getuserid(usertoken)
312         if !uid then return -1,"You are not logged in, so the relation could not be saved." end
313
314         relid = relid.to_i
315         visible = (visible.to_i != 0)
316
317         # create a new relation, or find the existing one
318         if relid <= 0
319           rel = Relation.new
320         else
321           rel = Relation.find(relid)
322         end
323
324         # check the members are all positive, and correctly type
325         typedmembers = []
326         members.each do |m|
327           mid = m[1].to_i
328           if mid < 0
329                 mid = renumberednodes[mid] if m[0] == 'node'
330                 mid = renumberedways[mid] if m[0] == 'way'
331           end
332       if mid
333             typedmembers << [m[0], mid, m[2]]
334           end
335         end
336
337         # assign new contents
338         rel.members = typedmembers
339         rel.tags = tags
340         rel.visible = visible
341         rel.user_id = uid
342
343         # check it then save it
344         # BUG: the following is commented out because it always fails on my
345         #  install. I think it's a Rails bug.
346
347         #if !rel.preconditions_ok?
348         #  return -2, "Relation preconditions failed"
349         #else
350           rel.save_with_history!
351         #end
352
353         [0, relid, rel.id]
354   end
355
356   # Save a way to the database, including all nodes. Any nodes in the previous
357   # version and no longer used are deleted.
358   # 
359   # Returns:
360   # 0. '0' (code for success),
361   # 1. original way id (unchanged),
362   # 2. new way id,
363   # 3. hash of renumbered nodes (old id=>new id)
364
365   def putway(renumberednodes, usertoken, originalway, points, attributes) #:doc:
366
367         # -- Initialise and carry out checks
368         
369         uid = getuserid(usertoken)
370         if !uid then return -1,"You are not logged in, so the way could not be saved." end
371
372         originalway = originalway.to_i
373
374         points.each do |a|
375           if a[2] == 0 or a[2].nil? then return -2,"Server error - node with id 0 found in way #{originalway}." end
376           if a[1] == 90 then return -2,"Server error - node with lat -90 found in way #{originalway}." end
377         end
378
379         if points.length < 2 then return -2,"Server error - way is only #{points.length} points long." end
380
381         # -- Get unique nodes
382
383         if originalway < 0
384           way = Way.new
385           uniques = []
386         else
387           way = Way.find(originalway)
388           uniques = way.unshared_node_ids
389         end
390
391         # -- Compare nodes and save changes to any that have changed
392
393         nodes = []
394
395         points.each do |n|
396           lon = n[0].to_f
397           lat = n[1].to_f
398           id = n[2].to_i
399           savenode = false
400
401           if renumberednodes[id]
402             id = renumberednodes[id]
403           elsif id < 0
404                 # Create new node
405                 node = Node.new
406                 savenode = true
407           else
408                 node = Node.find(id)
409                 nodetags=node.tags_as_hash
410                 nodetags.delete('created_by')
411                 if !fpcomp(lat, node.lat) or !fpcomp(lon, node.lon) or
412                    n[4] != nodetags or !node.visible?
413                   savenode = true
414                 end
415           end
416
417           if savenode
418                 node.user_id = uid
419             node.lat = lat
420         node.lon = lon
421             node.tags = Tags.join(n[4])
422             node.visible = true
423             node.save_with_history!
424
425                 if id != node.id
426                   renumberednodes[id] = node.id
427                   id = node.id
428             end
429           end
430
431           uniques = uniques - [id]
432           nodes.push(id)
433         end
434
435         # -- Delete any unique nodes
436         
437         uniques.each do |n|
438           deleteitemrelations(n, 'node')
439
440           node = Node.find(n)
441           node.user_id = uid
442           node.visible = false
443           node.save_with_history!
444         end
445
446         # -- Save revised way
447
448         way.tags = attributes
449         way.nds = nodes
450         way.user_id = uid
451         way.visible = true
452         way.save_with_history!
453
454         [0, originalway, way.id, renumberednodes]
455   end
456
457   # Save POI to the database.
458   # Refuses save if the node has since become part of a way.
459   # Returns:
460   # 0. 0 (success),
461   # 1. original node id (unchanged),
462   # 2. new node id.
463
464   def putpoi(usertoken, id, lon, lat, tags, visible) #:doc:
465         uid = getuserid(usertoken)
466         if !uid then return -1,"You are not logged in, so the point could not be saved." end
467
468         id = id.to_i
469         visible = (visible.to_i == 1)
470
471         if id > 0 then
472           node = Node.find(id)
473
474           if !visible then
475             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
476             deleteitemrelations(id, 'node')
477           end
478         else
479           node = Node.new
480         end
481
482         node.user_id = uid
483         node.lat = lat
484         node.lon = lon
485         node.tags = Tags.join(tags)
486         node.visible = visible
487         node.save_with_history!
488
489         [0, id, node.id]
490   end
491
492   # Read POI from database
493   # (only called on revert: POIs are usually read by whichways).
494   #
495   # Returns array of id, long, lat, hash of tags.
496
497   def getpoi(id,timestamp) #:doc:
498         if timestamp>0 then
499           n = OldNode.find(id, :conditions=>['UNIX_TIMESTAMP(timestamp)=?',timestamp])
500         else
501           n = Node.find(id)
502         end
503
504         if n
505           return [n.id, n.lon, n.lat, n.tags_as_hash]
506         else
507           return [nil, nil, nil, '']
508         end
509   end
510
511   # Delete way and all constituent nodes. Also removes from any relations.
512   # Returns 0 (success), unchanged way id.
513
514   def deleteway(usertoken, way_id) #:doc:
515         uid = getuserid(usertoken)
516         if !uid then return -1,"You are not logged in, so the way could not be deleted." end
517
518         # FIXME: would be good not to make two history entries when removing
519         #                two nodes from the same relation
520         user = User.find(uid)
521         way = Way.find(way_id)
522         way.unshared_node_ids.each do |n|
523           deleteitemrelations(n, 'node')
524         end
525         deleteitemrelations(way_id, 'way')
526
527         way.delete_with_relations_and_nodes_and_history(user)  
528
529         [0, way_id]
530   end
531
532
533   # ====================================================================
534   # Support functions
535
536   # Remove a node or way from all relations
537
538   def deleteitemrelations(objid, type) #:doc:
539         relations = RelationMember.find(:all, 
540                                                                         :conditions => ['member_type = ? and member_id = ?', type, objid], 
541                                                                         :include => :relation).collect { |rm| rm.relation }.uniq
542
543         relations.each do |rel|
544           rel.members.delete_if { |x| x[0] == type and x[1] == objid }
545           rel.save_with_history!
546         end
547   end
548
549   # Break out node tags into a hash
550   # (should become obsolete as of API 0.6)
551
552   def tagstring_to_hash(a) #:doc:
553         tags={}
554         Tags.split(a) do |k, v|
555           tags[k]=v
556         end
557         tags
558   end
559
560   # Authenticate token
561   # (could be removed if no-one uses the username+password form)
562
563   def getuserid(token) #:doc:
564         if (token =~ /^(.+)\+(.+)$/) then
565           user = User.authenticate(:username => $1, :password => $2)
566         else
567           user = User.authenticate(:token => token)
568         end
569
570         return user ? user.id : nil;
571   end
572
573   # Compare two floating-point numbers to within 0.0000001
574
575   def fpcomp(a,b) #:doc:
576         return ((a/0.0000001).round==(b/0.0000001).round)
577   end
578
579   # Send AMF response
580   
581   def sendresponse(results)
582         a,b=results.length.divmod(256)
583         render :content_type => "application/x-amf", :text => proc { |response, output| 
584           output.write 0.chr+0.chr+0.chr+0.chr+a.chr+b.chr
585           results.each do |k,v|
586                 output.write(v)
587           end
588         }
589   end
590
591
592   # ====================================================================
593   # Alternative SQL queries for getway/whichways
594
595   def sql_find_way_ids_in_area(xmin,ymin,xmax,ymax)
596         sql=<<-EOF
597   SELECT DISTINCT current_way_nodes.id AS wayid
598                 FROM current_way_nodes
599   INNER JOIN current_nodes ON current_nodes.id=current_way_nodes.node_id
600   INNER JOIN current_ways  ON current_ways.id =current_way_nodes.id
601            WHERE current_nodes.visible=TRUE 
602                  AND current_ways.visible=TRUE 
603                  AND #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")}
604         EOF
605         return ActiveRecord::Base.connection.select_all(sql).collect { |a| a['wayid'].to_i }
606   end
607         
608   def sql_find_pois_in_area(xmin,ymin,xmax,ymax)
609         sql=<<-EOF
610                   SELECT current_nodes.id,current_nodes.latitude*0.0000001 AS lat,current_nodes.longitude*0.0000001 AS lon,current_nodes.tags 
611                         FROM current_nodes 
612  LEFT OUTER JOIN current_way_nodes cwn ON cwn.node_id=current_nodes.id 
613                    WHERE current_nodes.visible=TRUE
614                          AND cwn.id IS NULL
615                          AND #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")}
616         EOF
617         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'])] }
618   end
619         
620   def sql_find_relations_in_area_and_ways(xmin,ymin,xmax,ymax,way_ids)
621         # ** It would be more Potlatchy to get relations for nodes within ways
622         #    during 'getway', not here
623         sql=<<-EOF
624           SELECT DISTINCT cr.id AS relid 
625                 FROM current_relations cr
626   INNER JOIN current_relation_members crm ON crm.id=cr.id 
627   INNER JOIN current_nodes cn ON crm.member_id=cn.id AND crm.member_type='node' 
628            WHERE #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "cn.")}
629         EOF
630         unless way_ids.empty?
631           sql+=<<-EOF
632            UNION
633           SELECT DISTINCT cr.id AS relid
634                 FROM current_relations cr
635   INNER JOIN current_relation_members crm ON crm.id=cr.id
636            WHERE crm.member_type='way' 
637                  AND crm.member_id IN (#{way_ids.join(',')})
638           EOF
639         end
640         return ActiveRecord::Base.connection.select_all(sql).collect { |a| a['relid'].to_i }.uniq
641   end
642         
643   def sql_get_nodes_in_way(wayid)
644         points=[]
645         sql=<<-EOF
646                 SELECT latitude*0.0000001 AS lat,longitude*0.0000001 AS lon,current_nodes.id,tags 
647                   FROM current_way_nodes,current_nodes 
648                  WHERE current_way_nodes.id=#{wayid.to_i} 
649                    AND current_way_nodes.node_id=current_nodes.id 
650                    AND current_nodes.visible=TRUE
651           ORDER BY sequence_id
652           EOF
653         ActiveRecord::Base.connection.select_all(sql).each do |row|
654           nodetags=tagstring_to_hash(row['tags'])
655           nodetags.delete('created_by')
656           points << [row['lon'].to_f,row['lat'].to_f,row['id'].to_i,nodetags]
657         end
658         points
659   end
660         
661   def sql_get_tags_in_way(wayid)
662         tags={}
663         ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_way_tags WHERE id=#{wayid.to_i}").each do |row|
664           tags[row['k']]=row['v']
665         end
666         tags
667   end
668
669 end
670
671 # Local Variables:
672 # indent-tabs-mode: t
673 # tab-width: 4
674 # End: