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