few bits of tidying
[rails.git] / app / controllers / amf_controller.rb
1 class AmfController < ApplicationController
2   require 'stringio'
3
4   session :off
5   before_filter :check_write_availability
6
7   # AMF controller for Potlatch
8   # ---------------------------
9   # All interaction between Potlatch (as a .SWF application) and the 
10   # OSM database takes place using this controller. Messages are 
11   # encoded in the Actionscript Message Format (AMF).
12   #
13   # Public domain. Set your tab width to 4 to read this document. :)
14   # editions Systeme D / Richard Fairhurst 2004-2008
15   #
16   # All in/out parameters are floats unless explicitly stated.
17   # 
18   # to trap errors (getway_old,putway,putpoi,deleteway only):
19   #   return(-1,"message")              <-- just puts up a dialogue
20   #   return(-2,"message")              <-- also asks the user to e-mail me
21   # to log:
22   #   RAILS_DEFAULT_LOGGER.error("Args: #{args[0]}, #{args[1]}, #{args[2]}, #{args[3]}")
23
24   # ====================================================================
25   # Main AMF handler
26
27   # ---- talk   process AMF request
28
29   def talk
30     req=StringIO.new(request.raw_post+0.chr)    # Get POST data as request
31     # (cf http://www.ruby-forum.com/topic/122163)
32     req.read(2)                                                                 # Skip version indicator and client ID
33     results={}                                                                  # Results of each body
34     renumberednodes={}                                                  # Shared across repeated putways
35
36     # -------------
37     # Parse request
38
39     headers=getint(req)                                 # Read number of headers
40
41     headers.times do                                # Read each header
42       name=getstring(req)                               #  |
43       req.getc                                  #  | skip boolean
44       value=getvalue(req)                               #  |
45       header["name"]=value                              #  |
46     end
47
48     bodies=getint(req)                                  # Read number of bodies
49     bodies.times do                                     # Read each body
50       message=getstring(req)                    #  | get message name
51       index=getstring(req)                              #  | get index in response sequence
52       bytes=getlong(req)                                #  | get total size in bytes
53       args=getvalue(req)                                #  | get response (probably an array)
54
55       case message
56       when 'getpresets';                results[index]=putdata(index,getpresets)
57       when 'whichways';                 results[index]=putdata(index,whichways(args))
58       when 'whichways_deleted'; results[index]=putdata(index,whichways_deleted(args))
59       when 'getway';                    results[index]=putdata(index,getway(args))
60       when 'getway_old';                results[index]=putdata(index,getway_old(args))
61       when 'getway_history';    results[index]=putdata(index,getway_history(args))
62       when 'putway';                    r=putway(args,renumberednodes)
63                                                                 renumberednodes=r[3]
64                                                                 results[index]=putdata(index,r)
65       when 'deleteway';                 results[index]=putdata(index,deleteway(args))
66       when 'putpoi';                    results[index]=putdata(index,putpoi(args))
67       when 'getpoi';                    results[index]=putdata(index,getpoi(args))
68       end
69     end
70
71     # ------------------
72     # Write out response
73
74     RAILS_DEFAULT_LOGGER.info("  Response: start")
75     a,b=results.length.divmod(256)
76     render :content_type => "application/x-amf", :text => proc { |response, output| 
77       output.write 0.chr+0.chr+0.chr+0.chr+a.chr+b.chr
78       results.each do |k,v|
79         output.write(v)
80       end
81     }
82     RAILS_DEFAULT_LOGGER.info("  Response: end")
83
84   end
85
86   private
87
88
89   # ====================================================================
90   # Remote calls
91
92   # ----- getpresets
93   #               in:   none
94   #               does: reads tag preset menus, colours, and autocomplete config files
95   #           out:  [0] presets, [1] presetmenus, [2] presetnames,
96   #                             [3] colours, [4] casing, [5] areas, [6] autotags
97   #                             (all hashes)
98
99   def getpresets
100     RAILS_DEFAULT_LOGGER.info("  Message: getpresets")
101
102     # Read preset menus
103     presets={}
104     presetmenus={}; presetmenus['point']=[]; presetmenus['way']=[]; presetmenus['POI']=[]
105     presetnames={}; presetnames['point']={}; presetnames['way']={}; presetnames['POI']={}
106     presettype=''
107     presetcategory=''
108     #   StringIO.open(txt) do |file|
109     File.open("#{RAILS_ROOT}/config/potlatch/presets.txt") do |file|
110       file.each_line {|line|
111         t=line.chomp
112         if (t=~/(\w+)\/(\w+)/) then
113           presettype=$1
114           presetcategory=$2
115           presetmenus[presettype].push(presetcategory)
116           presetnames[presettype][presetcategory]=["(no preset)"]
117         elsif (t=~/^(.+):\s?(.+)$/) then
118           pre=$1; kv=$2
119           presetnames[presettype][presetcategory].push(pre)
120           presets[pre]={}
121           kv.split(',').each {|a|
122             if (a=~/^(.+)=(.*)$/) then presets[pre][$1]=$2 end
123           }
124         end
125       }
126     end
127
128     # Read colours/styling
129     colours={}; casing={}; areas={}
130     File.open("#{RAILS_ROOT}/config/potlatch/colours.txt") do |file|
131       file.each_line {|line|
132         t=line.chomp
133         if (t=~/(\w+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)/) then
134           tag=$1
135           if ($2!='-') then colours[tag]=$2.hex end
136           if ($3!='-') then casing[tag]=$3.hex end
137           if ($4!='-') then areas[tag]=$4.hex end
138         end
139       }
140     end
141
142     # Read auto-complete
143     autotags={}; autotags['point']={}; autotags['way']={}; autotags['POI']={};
144     File.open("#{RAILS_ROOT}/config/potlatch/autocomplete.txt") do |file|
145       file.each_line {|line|
146         t=line.chomp
147         if (t=~/^(\w+)\/(\w+)\s+(.+)$/) then
148           tag=$1; type=$2; values=$3
149           if values=='-' then autotags[type][tag]=[]
150           else autotags[type][tag]=values.split(',').sort.reverse end
151         end
152       }
153     end
154
155     [presets,presetmenus,presetnames,colours,casing,areas,autotags]
156   end
157
158
159   # ----- whichways
160   
161   # Find all the way ids and nodes (including tags and projected lat/lng) which aren't part of those ways in an are
162   # 
163   # The argument is an array containing the following, in order:
164   # 0. minimum longitude
165   # 1. minimum latitude
166   # 2. maximum longitude
167   # 3. maximum latitude
168   # 4. baselong, 5. basey, 6. masterscale as above
169
170   def whichways(args)
171     xmin = args[0].to_f-0.01
172     ymin = args[1].to_f-0.01
173     xmax = args[2].to_f+0.01
174     ymax = args[3].to_f+0.01
175     baselong    = args[4]
176     basey       = args[5]
177     masterscale = args[6]
178
179     RAILS_DEFAULT_LOGGER.info("  Message: whichways, bbox=#{xmin},#{ymin},#{xmax},#{ymax}")
180
181     # find the way ids in an area
182     nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax,:conditions => "visible = 1", :include => :way_nodes)
183     waynodes_in_area = nodes_in_area.collect {|node| node.way_nodes }.flatten
184     ways = waynodes_in_area.collect {|way_node| way_node.id[0]}.uniq
185
186     # find the node ids in an area that aren't part of ways
187     node_ids_in_area = nodes_in_area.collect {|node| node.id}.uniq
188     node_ids_used_in_ways = waynodes_in_area.collect {|way_node| way_node.node_id}.uniq
189     node_ids_not_used_in_area = node_ids_in_area - node_ids_used_in_ways
190     nodes_not_used_in_area = Node.find(node_ids_not_used_in_area)
191     points = nodes_not_used_in_area.collect {|n| [n.id, n.lon_potlatch(baselong,masterscale), n.lat_potlatch(basey,masterscale), n.tags_as_hash] }
192
193     [ways,points]
194   end
195
196   # ----- whichways_deleted
197   #               return array of deleted ways in current bounding box
198   #               in:   as whichways
199   #               does: finds all deleted ways with a deleted node in bounding box
200   #               out:  [0] array of way ids
201   
202   def whichways_deleted(args)
203     xmin = args[0].to_f-0.01
204     ymin = args[1].to_f-0.01
205     xmax = args[2].to_f+0.01
206     ymax = args[3].to_f+0.01
207     baselong    = args[4]
208     basey       = args[5]
209     masterscale = args[6]
210
211     sql=<<-EOF
212      SELECT DISTINCT current_ways.id 
213        FROM current_nodes,way_nodes,current_ways 
214       WHERE #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")} 
215       AND way_nodes.node_id=current_nodes.id 
216       AND way_nodes.id=current_ways.id 
217       AND current_nodes.visible=0 
218       AND current_ways.visible=0 
219   EOF
220     waylist = ActiveRecord::Base.connection.select_all(sql)
221     ways = waylist.collect {|a| a['id'].to_i }
222     [ways]
223   end
224
225
226   # ----- getway
227
228   # Get a way with all of it's nodes and tags
229   # The input is an array with the following components, in order:
230   # 0. wayid - the ID of the way to get
231   # 1. baselong - origin of SWF map (longitude)
232   # 2. basey - origin of SWF map (latitude)
233   # 3. masterscale - SWF map scale
234   #
235   # The output is an array which contains all the nodes (with projected 
236   # latitude and longitude) and tags for a way (and all the nodes tags). 
237   # It also has the way's unprojected (WGS84) bbox.
238   #
239   # FIXME: The server really shouldn't be figuring out a ways bounding box and doing projection for potlatch
240   # FIXME: the argument splitting should be done in the 'talk' method, not here
241
242   def getway(args)
243     wayid,baselong,basey,masterscale = args
244     wayid = wayid.to_i
245
246     RAILS_DEFAULT_LOGGER.info("  Message: getway, id=#{wayid}")
247
248     way = Way.find_eager(wayid)
249     long_array = []
250     lat_array = []
251     points = []
252
253     way.way_nodes.each do |way_node|
254       node = way_node.node # get the node record
255       projected_longitude = node.lon_potlatch(baselong,masterscale) # do projection for potlatch
256       projected_latitude = node.lat_potlatch(basey,masterscale)
257       id = node.id
258       tags_hash = node.tags_as_hash
259       
260       points << [projected_longitude, projected_latitude, id, nil, tags_hash]
261       long_array << projected_longitude
262       lat_array << projected_latitude
263     end
264
265     [wayid,points,way.tags,long_array.min,long_array.max,lat_array.min,lat_array.max]
266   end
267
268   # ----- getway_old
269   #               returns old version of way
270
271   #               in:   [0] way id,
272   #                             [1] way version to get (or -1 for "last deleted version")
273   #                             [2] baselong, [3] basey, [4] masterscale
274   #               does: gets old version of way and all constituent nodes
275   #                             for undelete, always uses the most recent version of each node
276   #                               (even if it's moved)
277   #                             for revert, uses the historic version of each node, but if that node is
278   #                               still visible and has been changed since, generates a new node id
279   #               out:  [0] 0 (code for success), [1] SWF object name,
280   #                             [2] array of points (as getway _except_ [3] is node.visible?, 0 or 1),
281   #                             [4] xmin, [5] xmax, [6] ymin, [7] ymax (unprojected bbox),
282   #                             [8] way version
283
284   def getway_old(args)
285     RAILS_DEFAULT_LOGGER.info("  Message: getway_old (server is #{SERVER_URL})")
286     #   if SERVER_URL=="www.openstreetmap.org" then return -1,"Revert is not currently enabled on the OpenStreetMap server." end
287
288     wayid,version,baselong,basey,masterscale=args
289     wayid = wayid.to_i
290     version = version.to_i
291     xmin = ymin =  999999
292     xmax = ymax = -999999
293     points=[]
294     if version<0
295       historic=false
296       version=getlastversion(wayid,version)
297     else
298       historic=true
299     end
300     readwayquery_old(wayid,version,historic).each { |row|
301       points<<[long2coord(row['longitude'].to_f,baselong,masterscale),lat2coord(row['latitude'].to_f,basey,masterscale),row['id'].to_i,row['visible'].to_i,tag2array(row['tags'].to_s)]
302       xmin=[xmin,row['longitude'].to_f].min
303       xmax=[xmax,row['longitude'].to_f].max
304       ymin=[ymin,row['latitude' ].to_f].min
305       ymax=[ymax,row['latitude' ].to_f].max
306     }
307
308     # get tags from this version
309     attributes={}
310     attrlist=ActiveRecord::Base.connection.select_all "SELECT k,v FROM way_tags WHERE id=#{wayid} AND version=#{version}"
311     attrlist.each {|a| attributes[a['k'].gsub(':','|')]=a['v'] }
312     attributes['history']="Retrieved from v"+version.to_s
313
314     [0,wayid,points,attributes,xmin,xmax,ymin,ymax,version]
315   end
316
317   # ----- getway_history
318   #               find history of a way
319
320   #               in:   [0] way id
321   #               does: finds history of a way
322   #               out:  [0] array of previous versions (where each is
323   #                                     [0] version, [1] db timestamp (string),
324   #                                     [2] visible 0 or 1,
325   #                                     [3] username or 'anonymous' (string))
326
327   def getway_history(args)
328     wayid=args[0]
329     history=[]
330     sql=<<-EOF
331   SELECT version,timestamp,visible,display_name,data_public
332     FROM ways,users
333    WHERE ways.id=#{wayid}
334      AND ways.user_id=users.id
335      AND ways.visible=1
336    ORDER BY version DESC
337   EOF
338     histlist=ActiveRecord::Base.connection.select_all(sql)
339     histlist.each { |row|
340       if row['data_public'].to_i==1 then user=row['display_name'] else user='anonymous' end
341       history<<[row['version'],row['timestamp'],row['visible'],user]
342     }
343     [history]
344   end
345
346   # ----- putway
347   #               saves a way to the database
348
349   #               in:   [0] user token (string),
350   #                             [1] original way id (may be negative), 
351   #                             [2] array of points (as getway/getway_old),
352   #                             [3] hash of way tags,
353   #                             [4] original way version (0 if not a reverted/undeleted way),
354   #                             [5] baselong, [6] basey, [7] masterscale
355   #               does: saves way to the database
356   #                             all constituent nodes are created/updated as necessary
357   #                             (or deleted if they were in the old version and are otherwise unused)
358   #               out:  [0] 0 (code for success), [1] original way id (unchanged),
359   #                             [2] new way id, [3] hash of renumbered nodes (old id=>new id),
360   #                             [4] xmin, [5] xmax, [6] ymin, [7] ymax (unprojected bbox)
361
362   def putway(args,renumberednodes)
363     RAILS_DEFAULT_LOGGER.info("  putway started")
364     usertoken,originalway,points,attributes,oldversion,baselong,basey,masterscale=args
365     uid=getuserid(usertoken)
366     if !uid then return -1,"You are not logged in, so the way could not be saved." end
367
368     RAILS_DEFAULT_LOGGER.info("  putway authenticated happily")
369     db_uqn='unin'+(rand*100).to_i.to_s+uid.to_s+originalway.to_i.abs.to_s+Time.new.to_i.to_s    # temp uniquenodes table name, typically 51 chars
370     db_now='@now'+(rand*100).to_i.to_s+uid.to_s+originalway.to_i.abs.to_s+Time.new.to_i.to_s    # 'now' variable name, typically 51 chars
371     ActiveRecord::Base.connection.execute("SET #{db_now}=NOW()")
372     originalway=originalway.to_i
373     oldversion=oldversion.to_i
374
375     RAILS_DEFAULT_LOGGER.info("  Message: putway, id=#{originalway}")
376
377     # -- Temporary check for null IDs
378
379     points.each do |a|
380       if a[2]==0 or a[2].nil? then return -2,"Server error - node with id 0 found in way #{originalway}." end
381     end
382
383     # -- 3.     read original way into memory
384
385     xc={}; yc={}; tagc={}; vc={}
386     if originalway>0
387       way=originalway
388       if oldversion==0 then r=readwayquery(way,false)
389       else r=readwayquery_old(way,oldversion,true) end
390       r.each { |row|
391         id=row['id'].to_i
392         if (id>0) then
393           xc[id]=row['longitude'].to_f
394           yc[id]=row['latitude' ].to_f
395           tagc[id]=row['tags']
396           vc[id]=row['visible'].to_i
397         end
398       }
399       ActiveRecord::Base.connection.update("UPDATE current_ways SET timestamp=#{db_now},user_id=#{uid},visible=1 WHERE id=#{way}")
400     else
401       way=ActiveRecord::Base.connection.insert("INSERT INTO current_ways (user_id,timestamp,visible) VALUES (#{uid},#{db_now},1)")
402     end
403
404     # -- 4.     get version by inserting new row into ways
405
406     version=ActiveRecord::Base.connection.insert("INSERT INTO ways (id,user_id,timestamp,visible) VALUES (#{way},#{uid},#{db_now},1)")
407
408     # -- 5. compare nodes and update xmin,xmax,ymin,ymax
409
410     xmin=ymin= 999999
411     xmax=ymax=-999999
412     insertsql=''
413     nodelist=[]
414
415     points.each_index do |i|
416       xs=coord2long(points[i][0],masterscale,baselong)
417       ys=coord2lat(points[i][1],masterscale,basey)
418       xmin=[xs,xmin].min; xmax=[xs,xmax].max
419       ymin=[ys,ymin].min; ymax=[ys,ymax].max
420       node=points[i][2].to_i
421       tagstr=array2tag(points[i][4])
422       tagsql="'"+sqlescape(tagstr)+"'"
423       lat=(ys * 10000000).round
424       long=(xs * 10000000).round
425       tile=QuadTile.tile_for_point(ys, xs)
426
427       # compare node
428       if node<0
429         # new node - create
430         if renumberednodes[node.to_s].nil?
431           newnode=ActiveRecord::Base.connection.insert("INSERT INTO current_nodes (   latitude,longitude,timestamp,user_id,visible,tags,tile) VALUES (           #{lat},#{long},#{db_now},#{uid},1,#{tagsql},#{tile})")
432           ActiveRecord::Base.connection.insert("INSERT INTO nodes         (id,latitude,longitude,timestamp,user_id,visible,tags,tile) VALUES (#{newnode},#{lat},#{long},#{db_now},#{uid},1,#{tagsql},#{tile})")
433           points[i][2]=newnode
434           nodelist.push(newnode)
435           renumberednodes[node.to_s]=newnode.to_s
436         else
437           points[i][2]=renumberednodes[node.to_s].to_i
438         end
439
440       elsif xc.has_key?(node)
441         nodelist.push(node)
442         # old node from original way - update
443         if ((xs/0.0000001).round!=(xc[node]/0.0000001).round or (ys/0.0000001).round!=(yc[node]/0.0000001).round or tagstr!=tagc[node] or vc[node]==0)
444           ActiveRecord::Base.connection.insert("INSERT INTO nodes (id,latitude,longitude,timestamp,user_id,visible,tags,tile) VALUES (#{node},#{lat},#{long},#{db_now},#{uid},1,#{tagsql},#{tile})")
445           ActiveRecord::Base.connection.update("UPDATE current_nodes SET latitude=#{lat},longitude=#{long},timestamp=#{db_now},user_id=#{uid},tags=#{tagsql},visible=1,tile=#{tile} WHERE id=#{node}")
446         end
447       else
448         # old node, created in another way and now added to this way
449       end
450     end
451
452
453     # -- 6a. delete any nodes not in modified way
454
455     createuniquenodes(way,db_uqn,nodelist)      # nodes which appear in this way but no other
456
457     sql=<<-EOF
458   INSERT INTO nodes (id,latitude,longitude,timestamp,user_id,visible,tile)  
459   SELECT DISTINCT cn.id,cn.latitude,cn.longitude,#{db_now},#{uid},0,cn.tile
460     FROM current_nodes AS cn,#{db_uqn}
461    WHERE cn.id=node_id
462     EOF
463     ActiveRecord::Base.connection.insert(sql)
464
465     sql=<<-EOF
466       UPDATE current_nodes AS cn, #{db_uqn}
467          SET cn.timestamp=#{db_now},cn.visible=0,cn.user_id=#{uid} 
468        WHERE cn.id=node_id
469     EOF
470     ActiveRecord::Base.connection.update(sql)
471
472     deleteuniquenoderelations(db_uqn,uid,db_now)
473     ActiveRecord::Base.connection.execute("DROP TEMPORARY TABLE #{db_uqn}")
474
475     #   6b. insert new version of route into way_nodes
476
477     insertsql =''
478     currentsql=''
479     sequence  =1
480     points.each do |p|
481       if insertsql !='' then insertsql +=',' end
482       if currentsql!='' then currentsql+=',' end
483       insertsql +="(#{way},#{p[2]},#{sequence},#{version})"
484       currentsql+="(#{way},#{p[2]},#{sequence})"
485       sequence  +=1
486     end
487
488     ActiveRecord::Base.connection.execute("DELETE FROM current_way_nodes WHERE id=#{way}");
489     ActiveRecord::Base.connection.insert( "INSERT INTO         way_nodes (id,node_id,sequence_id,version) VALUES #{insertsql}");
490     ActiveRecord::Base.connection.insert( "INSERT INTO current_way_nodes (id,node_id,sequence_id        ) VALUES #{currentsql}");
491
492     # -- 7. insert new way tags
493
494     insertsql =''
495     currentsql=''
496     attributes.each do |k,v|
497       if v=='' or v.nil? then next end
498       if v[0,6]=='(type ' then next end
499       if insertsql !='' then insertsql +=',' end
500       if currentsql!='' then currentsql+=',' end
501       insertsql +="(#{way},'"+sqlescape(k.gsub('|',':'))+"','"+sqlescape(v)+"',#{version})"
502       currentsql+="(#{way},'"+sqlescape(k.gsub('|',':'))+"','"+sqlescape(v)+"')"
503     end
504
505     ActiveRecord::Base.connection.execute("DELETE FROM current_way_tags WHERE id=#{way}")
506     if (insertsql !='') then ActiveRecord::Base.connection.insert("INSERT INTO way_tags (id,k,v,version) VALUES #{insertsql}" ) end
507     if (currentsql!='') then ActiveRecord::Base.connection.insert("INSERT INTO current_way_tags (id,k,v) VALUES #{currentsql}") end
508
509     [0,originalway,way,renumberednodes,xmin,xmax,ymin,ymax]
510   end
511
512   # ----- putpoi
513   #               save POI to the database
514
515   #               in:   [0] user token (string),
516   #                             [1] original node id (may be negative),
517   #                             [2] projected longitude, [3] projected latitude,
518   #                             [4] hash of tags, [5] visible (0 to delete, 1 otherwise), 
519   #                             [6] baselong, [7] basey, [8] masterscale
520   #               does: saves POI node to the database
521   #                             refuses save if the node has since become part of a way
522   #               out:  [0] 0 (success), [1] original node id (unchanged),
523   #                             [2] new node id
524
525   def putpoi(args)
526     usertoken,id,x,y,tags,visible,baselong,basey,masterscale=args
527     uid=getuserid(usertoken)
528     if !uid then return -1,"You are not logged in, so the point could not be saved." end
529
530     db_now='@now'+(rand*100).to_i.to_s+uid.to_s+id.to_i.abs.to_s+Time.new.to_i.to_s     # 'now' variable name, typically 51 chars
531     ActiveRecord::Base.connection.execute("SET #{db_now}=NOW()")
532
533     id=id.to_i
534     visible=visible.to_i
535     if visible==0 then
536       # if deleting, check node hasn't become part of a way 
537       inway=ActiveRecord::Base.connection.select_one("SELECT cw.id FROM current_ways cw,current_way_nodes cwn WHERE cw.id=cwn.id AND cw.visible=1 AND cwn.node_id=#{id} LIMIT 1")
538       unless inway.nil? then return -1,"The point has since become part of a way, so you cannot save it as a POI." end
539       deleteitemrelations(id,'node',uid,db_now)
540     end
541
542     x=coord2long(x.to_f,masterscale,baselong)
543     y=coord2lat(y.to_f,masterscale,basey)
544     tagsql="'"+sqlescape(array2tag(tags))+"'"
545     lat=(y * 10000000).round
546     long=(x * 10000000).round
547     tile=QuadTile.tile_for_point(y, x)
548
549     if (id>0) then
550       ActiveRecord::Base.connection.insert("INSERT INTO nodes (id,latitude,longitude,timestamp,user_id,visible,tags,tile) VALUES (#{id},#{lat},#{long},#{db_now},#{uid},#{visible},#{tagsql},#{tile})");
551       ActiveRecord::Base.connection.update("UPDATE current_nodes SET latitude=#{lat},longitude=#{long},timestamp=#{db_now},user_id=#{uid},visible=#{visible},tags=#{tagsql},tile=#{tile} WHERE id=#{id}");
552       newid=id
553     else
554       newid=ActiveRecord::Base.connection.insert("INSERT INTO current_nodes (latitude,longitude,timestamp,user_id,visible,tags,tile) VALUES (#{lat},#{long},#{db_now},#{uid},#{visible},#{tagsql},#{tile})");
555       ActiveRecord::Base.connection.update("INSERT INTO nodes (id,latitude,longitude,timestamp,user_id,visible,tags,tile) VALUES (#{newid},#{lat},#{long},#{db_now},#{uid},#{visible},#{tagsql},#{tile})");
556     end
557     [0,id,newid]
558   end
559
560   # ----- getpoi
561   #               read POI from database
562   #               (only called on revert: POIs are usually read by whichways)
563
564   #               in:   [0] node id, [1] baselong, [2] basey, [3] masterscale
565   #               does: reads POI
566   #               out:  [0] id (unchanged), [1] projected long, [2] projected lat,
567   #                             [3] hash of tags
568
569   def getpoi(args)
570     id,baselong,basey,masterscale=args; id=id.to_i
571     poi=ActiveRecord::Base.connection.select_one("SELECT latitude*0.0000001 AS lat,longitude*0.0000001 AS lng,tags "+
572     "FROM current_nodes WHERE visible=1 AND id=#{id}")
573     if poi.nil? then return [nil,nil,nil,''] end
574     [id,
575       long2coord(poi['lng'].to_f,baselong,masterscale),
576       lat2coord(poi['lat'].to_f,basey,masterscale),
577       tag2array(poi['tags'])]
578   end
579
580   # ----- deleteway
581   #               delete way and constituent nodes from database
582
583   #               in:   [0] user token (string), [1] way id
584   #               does: deletes way from db and any constituent nodes not used elsewhere
585   #                             also removes ways/nodes from any relations they're in
586   #               out:  [0] 0 (success), [1] way id (unchanged)
587
588   def deleteway(args)
589     usertoken,way=args
590
591     RAILS_DEFAULT_LOGGER.info("  Message: deleteway, id=#{way}")
592     uid=getuserid(usertoken)
593     if !uid then return -1,"You are not logged in, so the way could not be deleted." end
594
595     way=way.to_i
596     db_uqn='unin'+(rand*100).to_i.to_s+uid.to_s+way.to_i.abs.to_s+Time.new.to_i.to_s    # temp uniquenodes table name, typically 51 chars
597     db_now='@now'+(rand*100).to_i.to_s+uid.to_s+way.to_i.abs.to_s+Time.new.to_i.to_s    # 'now' variable name, typically 51 chars
598     ActiveRecord::Base.connection.execute("SET #{db_now}=NOW()")
599
600     # - delete any otherwise unused nodes
601
602     createuniquenodes(way,db_uqn,[])
603
604     #   unless (preserve.empty?) then
605     #           ActiveRecord::Base.connection.execute("DELETE FROM #{db_uqn} WHERE node_id IN ("+preserve.join(',')+")")
606     #   end
607
608     sql=<<-EOF
609   INSERT INTO nodes (id,latitude,longitude,timestamp,user_id,visible,tile)
610   SELECT DISTINCT cn.id,cn.latitude,cn.longitude,#{db_now},#{uid},0,cn.tile
611     FROM current_nodes AS cn,#{db_uqn}
612    WHERE cn.id=node_id
613     EOF
614     ActiveRecord::Base.connection.insert(sql)
615
616     sql=<<-EOF
617       UPDATE current_nodes AS cn, #{db_uqn}
618          SET cn.timestamp=#{db_now},cn.visible=0,cn.user_id=#{uid} 
619        WHERE cn.id=node_id
620     EOF
621     ActiveRecord::Base.connection.update(sql)
622
623     deleteuniquenoderelations(db_uqn,uid,db_now)
624     ActiveRecord::Base.connection.execute("DROP TEMPORARY TABLE #{db_uqn}")
625
626     # - delete way
627
628     ActiveRecord::Base.connection.insert("INSERT INTO ways (id,user_id,timestamp,visible) VALUES (#{way},#{uid},#{db_now},0)")
629     ActiveRecord::Base.connection.update("UPDATE current_ways SET user_id=#{uid},timestamp=#{db_now},visible=0 WHERE id=#{way}")
630     ActiveRecord::Base.connection.execute("DELETE FROM current_way_nodes WHERE id=#{way}")
631     ActiveRecord::Base.connection.execute("DELETE FROM current_way_tags WHERE id=#{way}")
632     deleteitemrelations(way,'way',uid,db_now)
633     [0,way]
634   end
635
636
637
638   # ====================================================================
639   # Support functions for remote calls
640
641   def readwayquery(id,insistonvisible)
642     sql=<<-EOF
643     SELECT latitude*0.0000001 AS latitude,longitude*0.0000001 AS longitude,current_nodes.id,tags,visible 
644       FROM current_way_nodes,current_nodes 
645      WHERE current_way_nodes.id=#{id} 
646        AND current_way_nodes.node_id=current_nodes.id 
647   EOF
648     if insistonvisible then sql+=" AND current_nodes.visible=1 " end
649     sql+=" ORDER BY sequence_id"
650     ActiveRecord::Base.connection.select_all(sql)
651   end
652
653   def getlastversion(id,version)
654     row=ActiveRecord::Base.connection.select_one("SELECT version FROM ways WHERE id=#{id} AND visible=1 ORDER BY version DESC LIMIT 1")
655     row['version']
656   end
657
658   def readwayquery_old(id,version,historic)
659     # Node handling on undelete (historic=false):
660     # - always use the node specified, even if it's moved
661
662     # Node handling on revert (historic=true):
663     # - if it's a visible node, use a new node id (i.e. not mucking up the old one)
664     #   which means the SWF needs to allocate new ids
665     # - if it's an invisible node, we can reuse the old node id
666
667     # get node list from specified version of way,
668     # and the _current_ lat/long/tags of each node
669
670     row=ActiveRecord::Base.connection.select_one("SELECT timestamp FROM ways WHERE version=#{version} AND id=#{id}")
671     waytime=row['timestamp']
672
673     sql=<<-EOF
674   SELECT cn.id,visible,latitude*0.0000001 AS latitude,longitude*0.0000001 AS longitude,tags 
675     FROM way_nodes wn,current_nodes cn 
676    WHERE wn.version=#{version} 
677      AND wn.id=#{id} 
678      AND wn.node_id=cn.id 
679    ORDER BY sequence_id
680   EOF
681     rows=ActiveRecord::Base.connection.select_all(sql)
682
683     # if historic (full revert), get the old version of each node
684     # - if it's in another way now, generate a new id
685     # - if it's not in another way, use the old ID
686     if historic then
687       rows.each_index do |i|
688         sql=<<-EOF
689     SELECT latitude*0.0000001 AS latitude,longitude*0.0000001 AS longitude,tags,cwn.id AS currentway 
690       FROM nodes n
691    LEFT JOIN current_way_nodes cwn
692       ON cwn.node_id=n.id
693      WHERE n.id=#{rows[i]['id']} 
694        AND n.timestamp<="#{waytime}" 
695      AND cwn.id!=#{id} 
696      ORDER BY n.timestamp DESC 
697      LIMIT 1
698     EOF
699         row=ActiveRecord::Base.connection.select_one(sql)
700         unless row.nil? then
701           nx=row['longitude'].to_f
702           ny=row['latitude'].to_f
703           if (row['currentway'] && (nx!=rows[i]['longitude'].to_f or ny!=rows[i]['latitude'].to_f or row['tags']!=rows[i]['tags'])) then rows[i]['id']=-1 end
704           rows[i]['longitude']=nx
705           rows[i]['latitude' ]=ny
706           rows[i]['tags'     ]=row['tags']
707         end
708       end
709     end
710     rows
711   end
712
713   def createuniquenodes(way,uqn_name,nodelist)
714     # Find nodes which appear in this way but no others
715     sql=<<-EOF
716   CREATE TEMPORARY TABLE #{uqn_name}
717           SELECT a.node_id
718             FROM (SELECT DISTINCT node_id FROM current_way_nodes
719               WHERE id=#{way}) a
720          LEFT JOIN current_way_nodes b
721             ON b.node_id=a.node_id
722              AND b.id!=#{way}
723            WHERE b.node_id IS NULL
724   EOF
725     unless nodelist.empty? then
726       sql+="AND a.node_id NOT IN ("+nodelist.join(',')+")"
727     end
728     ActiveRecord::Base.connection.execute(sql)
729   end
730
731
732
733   # ====================================================================
734   # Relations handling
735   # deleteuniquenoderelations(uqn_name,uid,db_now)
736   # deleteitemrelations(way|node,'way'|'node',uid,db_now)
737
738   def deleteuniquenoderelations(uqn_name,uid,db_now)
739     sql=<<-EOF
740   SELECT node_id,cr.id FROM #{uqn_name},current_relation_members crm,current_relations cr 
741    WHERE crm.member_id=node_id 
742      AND crm.member_type='node' 
743      AND crm.id=cr.id 
744      AND cr.visible=1
745   EOF
746
747     relnodes=ActiveRecord::Base.connection.select_all(sql)
748     relnodes.each do |a|
749       removefromrelation(a['node_id'],'node',a['id'],uid,db_now)
750     end
751   end
752
753   def deleteitemrelations(objid,type,uid,db_now)
754     sql=<<-EOF
755   SELECT cr.id FROM current_relation_members crm,current_relations cr 
756    WHERE crm.member_id=#{objid} 
757      AND crm.member_type='#{type}' 
758      AND crm.id=cr.id 
759      AND cr.visible=1
760   EOF
761
762     relways=ActiveRecord::Base.connection.select_all(sql)
763     relways.each do |a|
764       removefromrelation(objid,type,a['id'],uid,db_now)
765     end
766   end
767
768   def removefromrelation(objid,type,relation,uid,db_now)
769     rver=ActiveRecord::Base.connection.insert("INSERT INTO relations (id,user_id,timestamp,visible) VALUES (#{relation},#{uid},#{db_now},1)")
770
771     tagsql=<<-EOF
772   INSERT INTO relation_tags (id,k,v,version) 
773   SELECT id,k,v,#{rver} FROM current_relation_tags 
774    WHERE id=#{relation} 
775   EOF
776     ActiveRecord::Base.connection.insert(tagsql)
777
778     membersql=<<-EOF
779   INSERT INTO relation_members (id,member_type,member_id,member_role,version) 
780   SELECT id,member_type,member_id,member_role,#{rver} FROM current_relation_members 
781    WHERE id=#{relation} 
782      AND (member_id!=#{objid} OR member_type!='#{type}')
783   EOF
784     ActiveRecord::Base.connection.insert(membersql)
785
786     ActiveRecord::Base.connection.update("UPDATE current_relations SET user_id=#{uid},timestamp=#{db_now} WHERE id=#{relation}")
787     ActiveRecord::Base.connection.execute("DELETE FROM current_relation_members WHERE id=#{relation} AND member_type='#{type}' AND member_id=#{objid}")
788   end
789
790   def sqlescape(a)
791     a.gsub(/[\000-\037]/,"").gsub("'","''").gsub(92.chr) {92.chr+92.chr}
792   end
793
794   def tag2array(a)
795     tags={}
796     Tags.split(a) do |k, v|
797       tags[k.gsub(':','|')]=v
798     end
799     tags
800   end
801
802   def array2tag(a)
803     tags = []
804     a.each do |k,v|
805       if v=='' then next end
806       if v[0,6]=='(type ' then next end
807       tags << [k.gsub('|',':'), v]
808     end
809     return Tags.join(tags)
810   end
811
812   def getuserid(token)
813     if (token =~ /^(.+)\+(.+)$/) then
814       user = User.authenticate(:username => $1, :password => $2)
815     else
816       user = User.authenticate(:token => token)
817     end
818
819     return user ? user.id : nil;
820   end
821
822
823
824   # ====================================================================
825   # AMF read subroutines
826
827   # -----       getint          return two-byte integer
828   # -----       getlong         return four-byte long
829   # -----       getstring       return string with two-byte length
830   # ----- getdouble     return eight-byte double-precision float
831   # ----- getobject     return object/hash
832   # ----- getarray      return numeric array
833
834   def getint(s)
835     s.getc*256+s.getc
836   end
837
838   def getlong(s)
839     ((s.getc*256+s.getc)*256+s.getc)*256+s.getc
840   end
841
842   def getstring(s)
843     len=s.getc*256+s.getc
844     s.read(len)
845   end
846
847   def getdouble(s)
848     a=s.read(8).unpack('G')                     # G big-endian, E little-endian
849     a[0]
850   end
851
852   def getarray(s)
853     len=getlong(s)
854     arr=[]
855     for i in (0..len-1)
856       arr[i]=getvalue(s)
857     end
858     arr
859   end
860
861   def getobject(s)
862     arr={}
863     while (key=getstring(s))
864       if (key=='') then break end
865       arr[key]=getvalue(s)
866     end
867     s.getc              # skip the 9 'end of object' value
868     arr
869   end
870
871   # -----       getvalue        parse and get value
872
873   def getvalue(s)
874     case s.getc
875     when 0;     return getdouble(s)                     # number
876     when 1;     return s.getc                           # boolean
877     when 2;     return getstring(s)                     # string
878     when 3;     return getobject(s)                     # object/hash
879     when 5;     return nil                                      # null
880     when 6;     return nil                                      # undefined
881     when 8;     s.read(4)                                       # mixedArray
882       return getobject(s)                       #  |
883     when 10;return getarray(s)                  # array
884     else;       return nil                                      # error
885     end
886   end
887
888   # ====================================================================
889   # AMF write subroutines
890
891   # -----       putdata         envelope data into AMF writeable form
892   # -----       encodevalue     pack variables as AMF
893
894   def putdata(index,n)
895     d =encodestring(index+"/onResult")
896     d+=encodestring("null")
897     d+=[-1].pack("N")
898     d+=encodevalue(n)
899   end
900
901   def encodevalue(n)
902     case n.class.to_s
903     when 'Array'
904       a=10.chr+encodelong(n.length)
905       n.each do |b|
906         a+=encodevalue(b)
907       end
908       a
909     when 'Hash'
910       a=3.chr
911       n.each do |k,v|
912         a+=encodestring(k)+encodevalue(v)
913       end
914       a+0.chr+0.chr+9.chr
915     when 'String'
916       2.chr+encodestring(n)
917     when 'Bignum','Fixnum','Float'
918       0.chr+encodedouble(n)
919     when 'NilClass'
920       5.chr
921     else
922       RAILS_DEFAULT_LOGGER.error("Unexpected Ruby type for AMF conversion: "+n.class.to_s)
923     end
924   end
925
926   # -----       encodestring    encode string with two-byte length
927   # -----       encodedouble    encode number as eight-byte double precision float
928   # -----       encodelong              encode number as four-byte long
929
930   def encodestring(n)
931     a,b=n.size.divmod(256)
932     a.chr+b.chr+n
933   end
934
935   def encodedouble(n)
936     [n].pack('G')
937   end
938
939   def encodelong(n)
940     [n].pack('N')
941   end
942
943   # ====================================================================
944   # Co-ordinate conversion
945
946   def lat2coord(a,basey,masterscale)
947     -(lat2y(a)-basey)*masterscale+250
948   end
949
950   def long2coord(a,baselong,masterscale)
951     (a-baselong)*masterscale+350
952   end
953
954   def lat2y(a)
955     180/Math::PI * Math.log(Math.tan(Math::PI/4+a*(Math::PI/180)/2))
956   end
957
958   def coord2lat(a,masterscale,basey)
959     y2lat((a-250)/-masterscale+basey)
960   end
961
962   def coord2long(a,masterscale,baselong)
963     (a-350)/masterscale+baselong
964   end
965
966   def y2lat(a)
967     180/Math::PI * (2*Math.atan(Math.exp(a*Math::PI/180))-Math::PI/2)
968   end
969
970 end