c0b642edaeb4faf313b473c53ea8b30230d50f2a
[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-2007
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).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
333             readwayquery(way).each { |row|
334                   id=row['id'].to_i
335                   xc[id]=row['longitude'].to_f
336                   yc[id]=row['latitude' ].to_f
337                   tagc[id]=row['tags']
338                   vc[id]=1
339                 }
340           else
341             readwayquery_old(way,oldversion,true).each { |row|
342                   id=row['id'].to_i
343                   if (id>0) then
344                         xc[id]=row['longitude'].to_f
345                         yc[id]=row['latitude' ].to_f
346                         tagc[id]=row['tags']
347                         vc[id]=row['visible'].to_i
348                   end
349                 }
350           end
351       ActiveRecord::Base.connection.update("UPDATE current_ways SET timestamp=#{db_now},user_id=#{uid},visible=1 WHERE id=#{way}")
352     else
353       way=ActiveRecord::Base.connection.insert("INSERT INTO current_ways (user_id,timestamp,visible) VALUES (#{uid},#{db_now},1)")
354     end
355
356     # -- 4.     get version by inserting new row into ways
357
358     version=ActiveRecord::Base.connection.insert("INSERT INTO ways (id,user_id,timestamp,visible) VALUES (#{way},#{uid},#{db_now},1)")
359
360     # -- 5. compare nodes and update xmin,xmax,ymin,ymax
361
362     xmin=ymin= 999999
363     xmax=ymax=-999999
364     insertsql=''
365         nodelist=[]
366
367     points.each_index do |i|
368       xs=coord2long(points[i][0],masterscale,baselong)
369       ys=coord2lat(points[i][1],masterscale,basey)
370       xmin=[xs,xmin].min; xmax=[xs,xmax].max
371       ymin=[ys,ymin].min; ymax=[ys,ymax].max
372       node=points[i][2].to_i
373           tagstr=array2tag(points[i][4])
374       tagsql="'"+sqlescape(tagstr)+"'"
375       lat=(ys * 10000000).round
376       long=(xs * 10000000).round
377       tile=QuadTile.tile_for_point(ys, xs)
378
379       # compare node
380       if node<0
381         # new node - create
382                 if renumberednodes[node.to_s].nil?
383           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})")
384                   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})")
385           points[i][2]=newnode
386           nodelist.push(newnode)
387           renumberednodes[node.to_s]=newnode.to_s
388                 else
389           points[i][2]=renumberednodes[node.to_s].to_i
390                 end
391
392       elsif xc.has_key?(node)
393                 nodelist.push(node)
394         # old node from original way - update
395         if (xs!=xc[node] or (ys/0.0000001).round!=(yc[node]/0.0000001).round or tagstr!=tagc[node] or vc[node]==0)
396           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})")
397           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}")
398         end
399       else
400         # old node, created in another way and now added to this way
401       end
402     end
403
404
405         # -- 6a. delete any nodes not in modified way
406
407     createuniquenodes(way,db_uqn,nodelist)      # nodes which appear in this way but no other
408
409     sql=<<-EOF
410         INSERT INTO nodes (id,latitude,longitude,timestamp,user_id,visible,tile)  
411         SELECT DISTINCT cn.id,cn.latitude,cn.longitude,#{db_now},#{uid},0,cn.tile
412           FROM current_nodes AS cn,#{db_uqn}
413          WHERE cn.id=node_id
414     EOF
415     ActiveRecord::Base.connection.insert(sql)
416
417     sql=<<-EOF
418       UPDATE current_nodes AS cn, #{db_uqn}
419          SET cn.timestamp=#{db_now},cn.visible=0,cn.user_id=#{uid} 
420        WHERE cn.id=node_id
421     EOF
422     ActiveRecord::Base.connection.update(sql)
423
424         deleteuniquenoderelations(db_uqn,uid,db_now)
425     ActiveRecord::Base.connection.execute("DROP TEMPORARY TABLE #{db_uqn}")
426
427         #       6b. insert new version of route into way_nodes
428
429     insertsql =''
430     currentsql=''
431     sequence  =1
432     points.each do |p|
433       if insertsql !='' then insertsql +=',' end
434       if currentsql!='' then currentsql+=',' end
435       insertsql +="(#{way},#{p[2]},#{sequence},#{version})"
436       currentsql+="(#{way},#{p[2]},#{sequence})"
437       sequence  +=1
438     end
439
440     ActiveRecord::Base.connection.execute("DELETE FROM current_way_nodes WHERE id=#{way}");
441     ActiveRecord::Base.connection.insert( "INSERT INTO         way_nodes (id,node_id,sequence_id,version) VALUES #{insertsql}");
442     ActiveRecord::Base.connection.insert( "INSERT INTO current_way_nodes (id,node_id,sequence_id        ) VALUES #{currentsql}");
443
444     # -- 7. insert new way tags
445
446     insertsql =''
447     currentsql=''
448     attributes.each do |k,v|
449       if v=='' or v.nil? then next end
450       if v[0,6]=='(type ' then next end
451       if insertsql !='' then insertsql +=',' end
452       if currentsql!='' then currentsql+=',' end
453       insertsql +="(#{way},'"+sqlescape(k.gsub('|',':'))+"','"+sqlescape(v)+"',#{version})"
454       currentsql+="(#{way},'"+sqlescape(k.gsub('|',':'))+"','"+sqlescape(v)+"')"
455     end
456
457     ActiveRecord::Base.connection.execute("DELETE FROM current_way_tags WHERE id=#{way}")
458     if (insertsql !='') then ActiveRecord::Base.connection.insert("INSERT INTO way_tags (id,k,v,version) VALUES #{insertsql}" ) end
459     if (currentsql!='') then ActiveRecord::Base.connection.insert("INSERT INTO current_way_tags (id,k,v) VALUES #{currentsql}") end
460
461     [0,originalway,way,renumberednodes,xmin,xmax,ymin,ymax]
462   end
463
464   # -----       putpoi (user token, id, x,y,tag array,visible,baselong,basey,masterscale)
465   #                     returns current id, new id
466   #                     if new: add new row to current_nodes and nodes
467   #                     if old: add new row to nodes, update current_nodes
468
469   def putpoi(args)
470     usertoken,id,x,y,tags,visible,baselong,basey,masterscale=args
471     uid=getuserid(usertoken)
472     if !uid then return -1,"You are not logged in, so the point could not be saved." end
473
474     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
475     ActiveRecord::Base.connection.execute("SET #{db_now}=NOW()")
476
477     id=id.to_i
478     visible=visible.to_i
479         if visible==0 then
480                 # if deleting, check node hasn't become part of a way 
481                 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")
482                 unless inway.nil? then return -1,"The point has since become part of a way, so you cannot save it as a POI." end
483                 deleteitemrelations(id,'node',uid,db_now)
484         end
485
486     x=coord2long(x.to_f,masterscale,baselong)
487     y=coord2lat(y.to_f,masterscale,basey)
488     tagsql="'"+sqlescape(array2tag(tags))+"'"
489     lat=(y * 10000000).round
490     long=(x * 10000000).round
491     tile=QuadTile.tile_for_point(y, x)
492         
493     if (id>0) then
494         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})");
495         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}");
496         newid=id
497     else
498         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})");
499               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})");
500     end
501     [0,id,newid]
502   end
503
504   # -----       getpoi (id,baselong,basey,masterscale)
505   #                     returns id,x,y,tag array
506   
507   def getpoi(args)
508         id,baselong,basey,masterscale=args; id=id.to_i
509         poi=ActiveRecord::Base.connection.select_one("SELECT latitude*0.0000001 AS lat,longitude*0.0000001 AS lng,tags "+
510                 "FROM current_nodes WHERE visible=1 AND id=#{id}")
511         if poi.nil? then return [nil,nil,nil,''] end
512         [id,
513          long2coord(poi['lng'].to_f,baselong,masterscale),
514          lat2coord(poi['lat'].to_f,basey,masterscale),
515          tag2array(poi['tags'])]
516   end
517
518   # -----       deleteway (user token, way, nodes to keep)
519   #                     returns way ID only
520
521   def deleteway(args)
522     usertoken,way=args
523
524     RAILS_DEFAULT_LOGGER.info("  Message: deleteway, id=#{way}")
525     uid=getuserid(usertoken)
526     if !uid then return -1,"You are not logged in, so the way could not be deleted." end
527
528     way=way.to_i
529     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
530     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
531     ActiveRecord::Base.connection.execute("SET #{db_now}=NOW()")
532
533     # - delete any otherwise unused nodes
534   
535     createuniquenodes(way,db_uqn,[])
536
537 #       unless (preserve.empty?) then
538 #               ActiveRecord::Base.connection.execute("DELETE FROM #{db_uqn} WHERE node_id IN ("+preserve.join(',')+")")
539 #       end
540
541     sql=<<-EOF
542         INSERT INTO nodes (id,latitude,longitude,timestamp,user_id,visible,tile)
543         SELECT DISTINCT cn.id,cn.latitude,cn.longitude,#{db_now},#{uid},0,cn.tile
544           FROM current_nodes AS cn,#{db_uqn}
545          WHERE cn.id=node_id
546     EOF
547     ActiveRecord::Base.connection.insert(sql)
548
549     sql=<<-EOF
550       UPDATE current_nodes AS cn, #{db_uqn}
551          SET cn.timestamp=#{db_now},cn.visible=0,cn.user_id=#{uid} 
552        WHERE cn.id=node_id
553     EOF
554     ActiveRecord::Base.connection.update(sql)
555
556         deleteuniquenoderelations(db_uqn,uid,db_now)
557     ActiveRecord::Base.connection.execute("DROP TEMPORARY TABLE #{db_uqn}")
558
559     # - delete way
560         
561     ActiveRecord::Base.connection.insert("INSERT INTO ways (id,user_id,timestamp,visible) VALUES (#{way},#{uid},#{db_now},0)")
562     ActiveRecord::Base.connection.update("UPDATE current_ways SET user_id=#{uid},timestamp=#{db_now},visible=0 WHERE id=#{way}")
563     ActiveRecord::Base.connection.execute("DELETE FROM current_way_nodes WHERE id=#{way}")
564     ActiveRecord::Base.connection.execute("DELETE FROM current_way_tags WHERE id=#{way}")
565         deleteitemrelations(way,'way',uid,db_now)
566     [0,way]
567 end
568
569
570
571 # ====================================================================
572 # Support functions for remote calls
573
574 def readwayquery(id)
575   ActiveRecord::Base.connection.select_all "SELECT latitude*0.0000001 AS latitude,longitude*0.0000001 AS longitude,current_nodes.id,tags "+
576       "    FROM current_way_nodes,current_nodes "+
577       "   WHERE current_way_nodes.id=#{id} "+
578       "     AND current_way_nodes.node_id=current_nodes.id "+
579       "     AND current_nodes.visible=1 "+
580       "   ORDER BY sequence_id"
581 end
582
583 def getlastversion(id,version)
584   row=ActiveRecord::Base.connection.select_one("SELECT version FROM ways WHERE id=#{id} AND visible=1 ORDER BY version DESC LIMIT 1")
585   row['version']
586 end
587
588 def readwayquery_old(id,version,historic)
589   # Node handling on undelete (historic=false):
590   # - always use the node specified, even if it's moved
591   
592   # Node handling on revert (historic=true):
593   # - if it's a visible node, use a new node id (i.e. not mucking up the old one)
594   #   which means the SWF needs to allocate new ids
595   # - if it's an invisible node, we can reuse the old node id
596
597   # get node list from specified version of way,
598   # and the _current_ lat/long/tags of each node
599
600   row=ActiveRecord::Base.connection.select_one("SELECT timestamp FROM ways WHERE version=#{version} AND id=#{id}")
601   waytime=row['timestamp']
602
603   sql=<<-EOF
604         SELECT cn.id,visible,latitude*0.0000001 AS latitude,longitude*0.0000001 AS longitude,tags 
605           FROM way_nodes wn,current_nodes cn 
606          WHERE wn.version=#{version} 
607            AND wn.id=#{id} 
608            AND wn.node_id=cn.id 
609          ORDER BY sequence_id
610   EOF
611   rows=ActiveRecord::Base.connection.select_all(sql)
612
613   # if historic (full revert), get the old version of each node
614   # - if it's in another way now, generate a new id
615   # - if it's not in another way, use the old ID
616   if historic then
617         rows.each_index do |i|
618           sql=<<-EOF
619           SELECT latitude*0.0000001 AS latitude,longitude*0.0000001 AS longitude,tags,cwn.id AS currentway 
620             FROM nodes n
621    LEFT JOIN current_way_nodes cwn
622                   ON cwn.node_id=n.id
623            WHERE n.id=#{rows[i]['id']} 
624              AND n.timestamp<="#{waytime}" 
625                  AND cwn.id!=#{id} 
626            ORDER BY n.timestamp DESC 
627            LIMIT 1
628           EOF
629           row=ActiveRecord::Base.connection.select_one(sql)
630           unless row.nil? then
631             nx=row['longitude'].to_f
632             ny=row['latitude'].to_f
633             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
634                 rows[i]['longitude']=nx
635                 rows[i]['latitude' ]=ny
636                 rows[i]['tags'     ]=row['tags']
637           end
638     end
639   end
640   rows
641 end
642
643 def createuniquenodes(way,uqn_name,nodelist)
644         # Find nodes which appear in this way but no others
645         sql=<<-EOF
646         CREATE TEMPORARY TABLE #{uqn_name}
647                                         SELECT a.node_id
648                                           FROM (SELECT DISTINCT node_id FROM current_way_nodes
649                                                         WHERE id=#{way}) a
650                                  LEFT JOIN current_way_nodes b
651                                                 ON b.node_id=a.node_id
652                                            AND b.id!=#{way}
653                                          WHERE b.node_id IS NULL
654         EOF
655         unless nodelist.empty? then
656           sql+="AND a.node_id NOT IN ("+nodelist.join(',')+")"
657         end
658         ActiveRecord::Base.connection.execute(sql)
659 end
660
661
662
663 # ====================================================================
664 # Relations handling
665 # deleteuniquenoderelations(uqn_name,uid,db_now)
666 # deleteitemrelations(way|node,'way'|'node',uid,db_now)
667
668 def deleteuniquenoderelations(uqn_name,uid,db_now)
669         sql=<<-EOF
670         SELECT node_id,cr.id FROM #{uqn_name},current_relation_members crm,current_relations cr 
671          WHERE crm.member_id=node_id 
672            AND crm.member_type='node' 
673            AND crm.id=cr.id 
674            AND cr.visible=1
675         EOF
676
677         relnodes=ActiveRecord::Base.connection.select_all(sql)
678         relnodes.each do |a|
679                 removefromrelation(a['node_id'],'node',a['id'],uid,db_now)
680         end
681 end
682
683 def deleteitemrelations(objid,type,uid,db_now)
684         sql=<<-EOF
685         SELECT cr.id FROM current_relation_members crm,current_relations cr 
686          WHERE crm.member_id=#{objid} 
687            AND crm.member_type='#{type}' 
688            AND crm.id=cr.id 
689            AND cr.visible=1
690         EOF
691         
692         relways=ActiveRecord::Base.connection.select_all(sql)
693         relways.each do |a|
694                 removefromrelation(objid,type,a['id'],uid,db_now)
695         end
696 end
697
698 def removefromrelation(objid,type,relation,uid,db_now)
699         rver=ActiveRecord::Base.connection.insert("INSERT INTO relations (id,user_id,timestamp,visible) VALUES (#{relation},#{uid},#{db_now},1)")
700
701         tagsql=<<-EOF
702         INSERT INTO relation_tags (id,k,v,version) 
703         SELECT id,k,v,#{rver} FROM current_relation_tags 
704          WHERE id=#{relation} 
705         EOF
706         ActiveRecord::Base.connection.insert(tagsql)
707
708         membersql=<<-EOF
709         INSERT INTO relation_members (id,member_type,member_id,member_role,version) 
710         SELECT id,member_type,member_id,member_role,#{rver} FROM current_relation_members 
711          WHERE id=#{relation} 
712            AND (member_id!=#{objid} OR member_type!='#{type}')
713         EOF
714         ActiveRecord::Base.connection.insert(membersql)
715         
716         ActiveRecord::Base.connection.update("UPDATE current_relations SET user_id=#{uid},timestamp=#{db_now} WHERE id=#{relation}")
717         ActiveRecord::Base.connection.execute("DELETE FROM current_relation_members WHERE id=#{relation} AND member_type='#{type}' AND member_id=#{objid}")
718 end
719
720
721 def sqlescape(a)
722   a.gsub(/[\000-\037]/,"").gsub("'","''").gsub(92.chr) {92.chr+92.chr}
723 end
724
725 def tag2array(a)
726   tags={}
727   Tags.split(a) do |k, v|
728     tags[k.gsub(':','|')]=v
729   end
730   tags
731 end
732
733 def array2tag(a)
734   tags = []
735   a.each do |k,v|
736     if v=='' then next end
737     if v[0,6]=='(type ' then next end
738     tags << [k.gsub('|',':'), v]
739   end
740   return Tags.join(tags)
741 end
742
743 def getuserid(token)
744   if (token =~ /^(.+)\+(.+)$/) then
745     user = User.authenticate(:username => $1, :password => $2)
746   else
747     user = User.authenticate(:token => token)
748   end
749
750   return user ? user.id : nil;
751 end
752
753
754
755 # ====================================================================
756 # AMF read subroutines
757
758 # ----- getint          return two-byte integer
759 # ----- getlong         return four-byte long
760 # ----- getstring       return string with two-byte length
761 # ----- getdouble       return eight-byte double-precision float
762 # ----- getobject       return object/hash
763 # ----- getarray        return numeric array
764
765 def getint(s)
766   s.getc*256+s.getc
767 end
768
769 def getlong(s)
770   ((s.getc*256+s.getc)*256+s.getc)*256+s.getc
771 end
772
773 def getstring(s)
774   len=s.getc*256+s.getc
775   s.read(len)
776 end
777
778 def getdouble(s)
779   a=s.read(8).unpack('G')                       # G big-endian, E little-endian
780   a[0]
781 end
782
783 def getarray(s)
784   len=getlong(s)
785   arr=[]
786   for i in (0..len-1)
787     arr[i]=getvalue(s)
788   end
789   arr
790 end
791
792 def getobject(s)
793   arr={}
794   while (key=getstring(s))
795     if (key=='') then break end
796     arr[key]=getvalue(s)
797   end
798   s.getc                # skip the 9 'end of object' value
799   arr
800 end
801
802 # ----- getvalue        parse and get value
803
804 def getvalue(s)
805   case s.getc
806         when 0; return getdouble(s)                     # number
807         when 1; return s.getc                           # boolean
808         when 2; return getstring(s)                     # string
809         when 3; return getobject(s)                     # object/hash
810         when 5; return nil                                      # null
811         when 6; return nil                                      # undefined
812         when 8; s.read(4)                                       # mixedArray
813                         return getobject(s)                     #  |
814         when 10;return getarray(s)                      # array
815         else;   return nil                                      # error
816   end
817 end
818
819 # ====================================================================
820 # AMF write subroutines
821
822 # ----- putdata         envelope data into AMF writeable form
823 # ----- encodevalue     pack variables as AMF
824
825 def putdata(index,n)
826   d =encodestring(index+"/onResult")
827   d+=encodestring("null")
828   d+=[-1].pack("N")
829   d+=encodevalue(n)
830 end
831
832 def encodevalue(n)
833   case n.class.to_s
834   when 'Array'
835     a=10.chr+encodelong(n.length)
836     n.each do |b|
837       a+=encodevalue(b)
838     end
839     a
840   when 'Hash'
841     a=3.chr
842     n.each do |k,v|
843       a+=encodestring(k)+encodevalue(v)
844     end
845     a+0.chr+0.chr+9.chr
846   when 'String'
847     2.chr+encodestring(n)
848   when 'Bignum','Fixnum','Float'
849     0.chr+encodedouble(n)
850   when 'NilClass'
851     5.chr
852   else
853     RAILS_DEFAULT_LOGGER.error("Unexpected Ruby type for AMF conversion: "+n.class.to_s)
854   end
855 end
856
857 # ----- encodestring    encode string with two-byte length
858 # ----- encodedouble    encode number as eight-byte double precision float
859 # ----- encodelong              encode number as four-byte long
860
861 def encodestring(n)
862   a,b=n.size.divmod(256)
863   a.chr+b.chr+n
864 end
865
866 def encodedouble(n)
867   [n].pack('G')
868 end
869
870 def encodelong(n)
871   [n].pack('N')
872 end
873
874 # ====================================================================
875 # Co-ordinate conversion
876
877 def lat2coord(a,basey,masterscale)
878   -(lat2y(a)-basey)*masterscale+250
879 end
880
881 def long2coord(a,baselong,masterscale)
882   (a-baselong)*masterscale+350
883 end
884
885 def lat2y(a)
886   180/Math::PI * Math.log(Math.tan(Math::PI/4+a*(Math::PI/180)/2))
887 end
888
889 def coord2lat(a,masterscale,basey)
890   y2lat((a-250)/-masterscale+basey)
891 end
892
893 def coord2long(a,masterscale,baselong)
894   (a-350)/masterscale+baselong
895 end
896
897 def y2lat(a)
898   180/Math::PI * (2*Math.atan(Math.exp(a*Math::PI/180))-Math::PI/2)
899 end
900
901 end