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