3df6da6faf97e1e32d58a1dc5a730b843a04e447
[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 log:
17   # RAILS_DEFAULT_LOGGER.error("Args: #{args[0]}, #{args[1]}, #{args[2]}, #{args[3]}")
18
19   # ====================================================================
20   # Main AMF handler
21
22   # ---- talk   process AMF request
23
24   def talk
25     req=StringIO.new(request.raw_post+0.chr)    # Get POST data as request
26                                                                                         # (cf http://www.ruby-forum.com/topic/122163)
27     req.read(2)                                                                 # Skip version indicator and client ID
28     results={}                                                                  # Results of each body
29
30     # -------------
31     # Parse request
32
33     headers=getint(req)                                 # Read number of headers
34
35     headers.times do                                # Read each header
36       name=getstring(req)                               #  |
37       req.getc                                  #  | skip boolean
38       value=getvalue(req)                               #  |
39       header["name"]=value                              #  |
40     end
41
42     bodies=getint(req)                                  # Read number of bodies
43     bodies.times do                                     # Read each body
44       message=getstring(req)                    #  | get message name
45       index=getstring(req)                              #  | get index in response sequence
46       bytes=getlong(req)                                #  | get total size in bytes
47       args=getvalue(req)                                #  | get response (probably an array)
48
49       case message
50                   when 'getpresets';            results[index]=putdata(index,getpresets)
51                   when 'whichways';                     results[index]=putdata(index,whichways(args))
52                   when 'whichways_deleted';     results[index]=putdata(index,whichways_deleted(args))
53                   when 'getway';                        results[index]=putdata(index,getway(args))
54                   when 'getway_old';            results[index]=putdata(index,getway_old(args))
55                   when 'getway_history';        results[index]=putdata(index,getway_history(args))
56                   when 'putway';                        results[index]=putdata(index,putway(args))
57                   when 'deleteway';                     results[index]=putdata(index,deleteway(args))
58                   when 'putpoi';                        results[index]=putdata(index,putpoi(args))
59                   when 'getpoi';                        results[index]=putdata(index,getpoi(args))
60       end
61     end
62
63     # ------------------
64     # Write out response
65
66     RAILS_DEFAULT_LOGGER.info("  Response: start")
67     a,b=results.length.divmod(256)
68         render :content_type => "application/x-amf", :text => proc { |response, output| 
69         output.write 0.chr+0.chr+0.chr+0.chr+a.chr+b.chr
70                 results.each do |k,v|
71                   output.write(v)
72                 end
73         }
74     RAILS_DEFAULT_LOGGER.info("  Response: end")
75
76   end
77
78   private
79
80   # ====================================================================
81   # Remote calls
82
83   # ----- getpresets
84   #           return presets,presetmenus and presetnames arrays
85
86   def getpresets
87     presets={}
88     presetmenus={}; presetmenus['point']=[]; presetmenus['way']=[]; presetmenus['POI']=[]
89     presetnames={}; presetnames['point']={}; presetnames['way']={}; presetnames['POI']={}
90     presettype=''
91     presetcategory=''
92
93     RAILS_DEFAULT_LOGGER.info("  Message: getpresets")
94
95     #           File.open("config/potlatch/presets.txt") do |file|
96
97     # Temporary patch to get around filepath problem
98     # To remove this patch and make the code nice again:
99     # 1. uncomment above line
100     # 2. fix the path in the above line
101     # 3. delete this here document, and the following line (StringIO....)
102
103     txt=<<-EOF
104 way/road
105 motorway: highway=motorway,ref=(type road number)
106 trunk road: highway=trunk,ref=(type road number),name=(type road name)
107 primary road: highway=primary,ref=(type road number),name=(type road name)
108 secondary road: highway=secondary,ref=(type road number),name=(type road name)
109 tertiary road: highway=tertiary,ref=,name=(type road name)
110 residential road: highway=residential,ref=,name=(type road name)
111 unclassified road: highway=unclassified,ref=,name=(type road name)
112
113 way/footway
114 footpath: highway=footway,foot=yes
115 bridleway: highway=bridleway,foot=yes
116 byway: highway=unsurfaced,foot=yes
117 permissive path: highway=footway,foot=permissive
118
119 way/cycleway
120 cycle lane: highway=cycleway,cycleway=lane,ncn_ref=
121 cycle track: highway=cycleway,cycleway=track,ncn_ref=
122 cycle lane (NCN): highway=cycleway,cycleway=lane,name=(type name here),ncn_ref=(type route number)
123 cycle track (NCN): highway=cycleway,cycleway=track,name=(type name here),ncn_ref=(type route number)
124
125 way/waterway
126 canal: waterway=canal,name=(type name here)
127 navigable river: waterway=river,boat=yes,name=(type name here)
128 navigable drain: waterway=drain,boat=yes,name=(type name here)
129 derelict canal: waterway=derelict_canal,name=(type name here)
130 unnavigable river: waterway=river,boat=no,name=(type name here)
131 unnavigable drain: waterway=drain,boat=no,name=(type name here)
132
133 way/railway
134 railway: railway=rail
135 tramway: railway=tram
136 light railway: railway=light_rail
137 preserved railway: railway=preserved
138 disused railway tracks: railway=disused
139 course of old railway: railway=abandoned
140
141 way/natural
142 lake: natural=water,landuse=
143 forest: landuse=forest,natural=
144
145 point/road
146 mini roundabout: highway=mini_roundabout
147 traffic lights: highway=traffic_signals
148
149 point/footway
150 bridge: highway=bridge
151 gate: highway=gate
152 stile: highway=stile
153 cattle grid: highway=cattle_grid
154
155 point/cycleway
156 gate: highway=gate
157
158 point/waterway
159 lock gate: waterway=lock_gate
160 weir: waterway=weir
161 aqueduct: waterway=aqueduct
162 winding hole: waterway=turning_point
163 mooring: waterway=mooring
164
165 point/railway
166 station: railway=station
167 viaduct: railway=viaduct
168 level crossing: railway=crossing
169
170 point/natural
171 peak: natural=peak
172
173 POI/road
174 car park: amenity=parking
175 petrol station: amenity=fuel
176
177 POI/cycleway
178 bike park: amenity=bicycle_parking
179
180 POI/place
181 city: place=city,name=(type name here),is_in=(type region or county)
182 town: place=town,name=(type name here),is_in=(type region or county)
183 suburb: place=suburb,name=(type name here),is_in=(type region or county)
184 village: place=village,name=(type name here),is_in=(type region or county)
185 hamlet: place=hamlet,name=(type name here),is_in=(type region or county)
186
187 POI/tourism
188 attraction: tourism=attraction,amenity=,religion=,denomination=
189 church: tourism=,amenity=place_of_worship,name=(type name here),religion=christian,denomination=(type denomination here)
190 hotel: tourism=hotel,amenity=,religion=,denomination=
191 other religious: tourism=,amenity=place_of_worship,name=(type name here),religion=(type religion),denomination=
192 post box: amenity=post_box,tourism=,name=,religion=,denomination=
193 post office: amenity=post_office,tourism=,name=,religion=,denomination=
194 pub: tourism=,amenity=pub,name=(type name here),religion=,denomination=
195
196 POI/natural
197 peak: point=peak
198 EOF
199
200     StringIO.open(txt) do |file|
201       file.each_line {|line|
202         t=line.chomp
203         if (t=~/(\w+)\/(\w+)/) then
204           presettype=$1
205           presetcategory=$2
206           presetmenus[presettype].push(presetcategory)
207           presetnames[presettype][presetcategory]=["(no preset)"]
208         elsif (t=~/^(.+):\s?(.+)$/) then
209           pre=$1; kv=$2
210           presetnames[presettype][presetcategory].push(pre)
211           presets[pre]={}
212           kv.split(',').each {|a|
213             if (a=~/^(.+)=(.*)$/) then presets[pre][$1]=$2 end
214           }
215         end
216       }
217     end
218     return [presets,presetmenus,presetnames]
219   end
220
221   # ----- whichways(left,bottom,right,top)
222   #               return array of ways in current bounding box
223   #               at present, instead of using correct (=more complex) SQL to find
224   #               corner-crossing ways, it simply enlarges the bounding box by +/- 0.01
225
226   def whichways(args)
227     xmin = args[0].to_f-0.01
228     ymin = args[1].to_f-0.01
229     xmax = args[2].to_f+0.01
230     ymax = args[3].to_f+0.01
231     baselong    = args[4]
232     basey       = args[5]
233     masterscale = args[6]
234
235     RAILS_DEFAULT_LOGGER.info("  Message: whichways, bbox=#{xmin},#{ymin},#{xmax},#{ymax}")
236
237     waylist = ActiveRecord::Base.connection.select_all("SELECT DISTINCT current_way_nodes.id AS wayid"+
238        "  FROM current_way_nodes,current_nodes,current_ways "+
239        " WHERE current_nodes.id=current_way_nodes.node_id "+
240        "   AND current_nodes.visible=1 "+
241        "   AND current_ways.id=current_way_nodes.id "+
242        "   AND current_ways.visible=1 "+
243        "   AND "+OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes."))
244
245     ways = waylist.collect {|a| a['wayid'].to_i } # get an array of way IDs
246
247     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 "+
248        "  FROM current_nodes "+
249        "  LEFT OUTER JOIN current_way_nodes cwn ON cwn.node_id=current_nodes.id "+
250        " WHERE "+OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")+
251        "   AND cwn.id IS NULL "+
252        "   AND current_nodes.visible=1")
253
254     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
255
256     return [ways,points]
257   end
258
259   # ----- whichways_deleted(left,bottom,right,top)
260   #               return array of deleted ways in current bounding box
261   
262   def whichways_deleted(args)
263     xmin = args[0].to_f-0.01
264     ymin = args[1].to_f-0.01
265     xmax = args[2].to_f+0.01
266     ymax = args[3].to_f+0.01
267     baselong    = args[4]
268     basey       = args[5]
269     masterscale = args[6]
270
271         sql=<<-EOF
272                  SELECT DISTINCT current_ways.id 
273                    FROM current_nodes,way_nodes,current_ways 
274                   WHERE #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")} 
275                         AND way_nodes.node_id=current_nodes.id 
276                         AND way_nodes.id=current_ways.id 
277                         AND current_nodes.visible=0 
278                         AND current_ways.visible=0 
279         EOF
280     waylist = ActiveRecord::Base.connection.select_all(sql)
281     ways = waylist.collect {|a| a['id'].to_i }
282         [ways]
283   end
284   
285   # ----- getway (objectname, way, baselong, basey, masterscale)
286   #               returns objectname, array of co-ordinates, attributes,
287   #                               xmin,xmax,ymin,ymax
288
289   def getway(args)
290     objname,wayid,baselong,basey,masterscale=args
291     wayid = wayid.to_i
292     points = []
293     xmin = ymin =  999999
294     xmax = ymax = -999999
295
296     RAILS_DEFAULT_LOGGER.info("  Message: getway, id=#{wayid}")
297
298     readwayquery(wayid).each {|row|
299       points<<[long2coord(row['longitude'].to_f,baselong,masterscale),lat2coord(row['latitude'].to_f,basey,masterscale),row['id'].to_i,nil,tag2array(row['tags'])]
300       xmin = [xmin,row['longitude'].to_f].min
301       xmax = [xmax,row['longitude'].to_f].max
302       ymin = [ymin,row['latitude'].to_f].min
303       ymax = [ymax,row['latitude'].to_f].max
304     }
305
306     attributes={}
307     attrlist=ActiveRecord::Base.connection.select_all "SELECT k,v FROM current_way_tags WHERE id=#{wayid}"
308     attrlist.each {|a| attributes[a['k'].gsub(':','|')]=a['v'] }
309
310     [objname,points,attributes,xmin,xmax,ymin,ymax]
311   end
312   
313   # -----       getway_old (objectname, way, version, baselong, basey, masterscale)
314   #                     returns old version of way
315
316   def getway_old(args)
317     RAILS_DEFAULT_LOGGER.info("  Message: getway_old (server is #{SERVER_URL})")
318         return if SERVER_URL=='www.openstreetmap.org'
319         
320     objname,wayid,version,baselong,basey,masterscale=args
321     wayid = wayid.to_i
322     version = version.to_i
323     xmin = ymin =  999999
324     xmax = ymax = -999999
325         points=[]
326         if version<0
327           historic=false
328           version=getlastversion(wayid,version)
329         else
330           historic=true
331         end
332         readwayquery_old(wayid,version,historic).each { |row|
333       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)]
334       xmin=[xmin,row['longitude'].to_f].min
335       xmax=[xmax,row['longitude'].to_f].max
336       ymin=[ymin,row['latitude' ].to_f].min
337       ymax=[ymax,row['latitude' ].to_f].max
338         }
339
340         # get tags from this version
341     attributes={}
342     attrlist=ActiveRecord::Base.connection.select_all "SELECT k,v FROM way_tags WHERE id=#{wayid} AND version=#{version}"
343     attrlist.each {|a| attributes[a['k'].gsub(':','|')]=a['v'] }
344         attributes['history']="Retrieved from v"+version.to_s
345
346     [objname,points,attributes,xmin,xmax,ymin,ymax,version]
347   end
348
349   # -----       getway_history (way)
350   #                     returns array of previous versions (version,timestamp,visible,user)
351   #                     should also show 'created_by'
352
353   def getway_history(wayid)
354         history=[]
355         sql=<<-EOF
356         SELECT version,timestamp,visible,display_name,data_public
357           FROM ways,users
358          WHERE ways.id=#{wayid}
359            AND ways.user_id=users.id
360          ORDER BY version DESC
361         EOF
362         histlist=ActiveRecord::Base.connection.select_all(sql)
363         histlist.each { |row|
364                 if row['data_public'] then user=row['display_name'] else user='anonymous' end
365                 history<<[row['version'],row['timestamp'],row['visible'],user]
366         }
367         [history]
368   end
369
370   # -----       putway (user token, way, array of co-ordinates, array of attributes,
371   #                                     baselong, basey, masterscale)
372   #                     returns current way ID, new way ID, hash of renumbered nodes,
373   #                                     xmin,xmax,ymin,ymax
374
375   # ** needs to be updated so that nodes with visible=0 (i.e. undeleted)
376   #     no longer cause a problem
377   #    best way to do this would be to know which version it was recovered from;
378   #             then replace readwayquery with historic query. Line 506 would also need
379   #             to check if visible had changed, and update if so
380   #        (a side-effect is that merging/splitting ways will need to be forbidden 
381   #             for undeleted ways, I think)
382
383   def putway(args)
384     RAILS_DEFAULT_LOGGER.info("  putway started")
385     usertoken,originalway,points,attributes,oldversion,baselong,basey,masterscale=args
386     uid=getuserid(usertoken)
387     return if !uid
388     RAILS_DEFAULT_LOGGER.info("  putway authenticated happily")
389     db_uqn='unin'+uid.to_s+originalway.to_i.abs.to_s+Time.new.to_i.to_s # temp uniquenodes table name, typically 51 chars
390     db_now='@now'+uid.to_s+originalway.to_i.abs.to_s+Time.new.to_i.to_s # 'now' variable name, typically 51 chars
391     ActiveRecord::Base.connection.execute("SET #{db_now}=NOW()")
392     originalway=originalway.to_i
393         oldversion=oldversion.to_i
394         
395     RAILS_DEFAULT_LOGGER.info("  Message: putway, id=#{originalway}")
396
397     # -- 3.     read original way into memory
398
399     xc={}; yc={}; tagc={}; vc={}
400     if originalway>0
401       way=originalway
402           if oldversion==0
403             readwayquery(way).each { |row|
404                   id=row['id'].to_i
405                   xc[id]=row['longitude'].to_f
406                   yc[id]=row['latitude' ].to_f
407                   tagc[id]=row['tags']
408                   vc[id]=1
409                 }
410           else
411             readwayquery_old(way,oldversion,true).each { |row|
412                   id=row['id'].to_i
413                   if (id>0) then
414                         xc[id]=row['longitude'].to_f
415                         yc[id]=row['latitude' ].to_f
416                         tagc[id]=row['tags']
417                         vc[id]=row['visible'].to_i
418                   end
419                 }
420           end
421       ActiveRecord::Base.connection.update("UPDATE current_ways SET timestamp=#{db_now},user_id=#{uid},visible=1 WHERE id=#{way}")
422     else
423       way=ActiveRecord::Base.connection.insert("INSERT INTO current_ways (user_id,timestamp,visible) VALUES (#{uid},#{db_now},1)")
424     end
425
426     # -- 4.     get version by inserting new row into ways
427
428     version=ActiveRecord::Base.connection.insert("INSERT INTO ways (id,user_id,timestamp,visible) VALUES (#{way},#{uid},#{db_now},1)")
429
430     # -- 5. compare nodes and update xmin,xmax,ymin,ymax
431
432     xmin=ymin= 999999
433     xmax=ymax=-999999
434     insertsql=''
435     renumberednodes={}
436         nodelist=[]
437
438     points.each_index do |i|
439       xs=coord2long(points[i][0],masterscale,baselong)
440       ys=coord2lat(points[i][1],masterscale,basey)
441       xmin=[xs,xmin].min; xmax=[xs,xmax].max
442       ymin=[ys,ymin].min; ymax=[ys,ymax].max
443       node=points[i][2].to_i
444           tagstr=array2tag(points[i][4])
445       tagsql="'"+sqlescape(tagstr)+"'"
446       lat=(ys * 10000000).round
447       long=(xs * 10000000).round
448       tile=QuadTile.tile_for_point(ys, xs)
449
450       # compare node
451       if node<0
452         # new node - create
453                 if renumberednodes[node.to_s].nil?
454           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})")
455                   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})")
456           points[i][2]=newnode
457           nodelist.push(newnode)
458           renumberednodes[node.to_s]=newnode.to_s
459                 else
460           points[i][2]=renumberednodes[node.to_s].to_i
461                 end
462
463       elsif xc.has_key?(node)
464                 nodelist.push(node)
465         # old node from original way - update
466         if (xs!=xc[node] or (ys/0.0000001).round!=(yc[node]/0.0000001).round or tagstr!=tagc[node] or vc[node]==0)
467           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})")
468           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}")
469         end
470       else
471         # old node, created in another way and now added to this way
472       end
473     end
474
475
476         # -- 6a. delete any nodes not in modified way
477
478     createuniquenodes(way,db_uqn,nodelist)      # nodes which appear in this way but no other
479
480     sql=<<-EOF
481         INSERT INTO nodes (id,latitude,longitude,timestamp,user_id,visible,tile)  
482         SELECT DISTINCT cn.id,cn.latitude,cn.longitude,#{db_now},#{uid},0,cn.tile
483           FROM current_nodes AS cn,#{db_uqn}
484          WHERE cn.id=node_id
485     EOF
486     ActiveRecord::Base.connection.insert(sql)
487
488     sql=<<-EOF
489       UPDATE current_nodes AS cn, #{db_uqn}
490          SET cn.timestamp=#{db_now},cn.visible=0,cn.user_id=#{uid} 
491        WHERE cn.id=node_id
492     EOF
493     ActiveRecord::Base.connection.update(sql)
494
495         deleteuniquenoderelations(db_uqn,uid,db_now)
496     ActiveRecord::Base.connection.execute("DROP TABLE #{db_uqn}")
497
498         #       6b. insert new version of route into way_nodes
499
500     insertsql =''
501     currentsql=''
502     sequence  =1
503     points.each do |p|
504       if insertsql !='' then insertsql +=',' end
505       if currentsql!='' then currentsql+=',' end
506       insertsql +="(#{way},#{p[2]},#{sequence},#{version})"
507       currentsql+="(#{way},#{p[2]},#{sequence})"
508       sequence  +=1
509     end
510
511     ActiveRecord::Base.connection.execute("DELETE FROM current_way_nodes WHERE id=#{way}");
512     ActiveRecord::Base.connection.insert( "INSERT INTO         way_nodes (id,node_id,sequence_id,version) VALUES #{insertsql}");
513     ActiveRecord::Base.connection.insert( "INSERT INTO current_way_nodes (id,node_id,sequence_id        ) VALUES #{currentsql}");
514
515     # -- 7. insert new way tags
516
517     insertsql =''
518     currentsql=''
519     attributes.each do |k,v|
520       if v=='' or v.nil? then next end
521       if v[0,6]=='(type ' then next end
522       if insertsql !='' then insertsql +=',' end
523       if currentsql!='' then currentsql+=',' end
524       insertsql +="(#{way},'"+sqlescape(k.gsub('|',':'))+"','"+sqlescape(v)+"',#{version})"
525       currentsql+="(#{way},'"+sqlescape(k.gsub('|',':'))+"','"+sqlescape(v)+"')"
526     end
527
528     ActiveRecord::Base.connection.execute("DELETE FROM current_way_tags WHERE id=#{way}")
529     if (insertsql !='') then ActiveRecord::Base.connection.insert("INSERT INTO way_tags (id,k,v,version) VALUES #{insertsql}" ) end
530     if (currentsql!='') then ActiveRecord::Base.connection.insert("INSERT INTO current_way_tags (id,k,v) VALUES #{currentsql}") end
531
532     [originalway,way,renumberednodes,xmin,xmax,ymin,ymax]
533   end
534
535   # -----       putpoi (user token, id, x,y,tag array,visible,baselong,basey,masterscale)
536   #                     returns current id, new id
537   #                     if new: add new row to current_nodes and nodes
538   #                     if old: add new row to nodes, update current_nodes
539
540   def putpoi(args)
541     usertoken,id,x,y,tags,visible,baselong,basey,masterscale=args
542     uid=getuserid(usertoken)
543     return if !uid
544     db_now='@now'+uid.to_s+id.to_i.abs.to_s+Time.new.to_i.to_s  # 'now' variable name, typically 51 chars
545     ActiveRecord::Base.connection.execute("SET #{db_now}=NOW()")
546
547     id=id.to_i
548     visible=visible.to_i
549         if visible==0 then
550                 # if deleting, check node hasn't become part of a way 
551                 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")
552                 unless inway.nil? then return [id,id] end       # should really return an error
553                 deleteitemrelations(id,'node',uid,db_now)
554         end
555
556     x=coord2long(x.to_f,masterscale,baselong)
557     y=coord2lat(y.to_f,masterscale,basey)
558     tagsql="'"+sqlescape(array2tag(tags))+"'"
559     lat=(y * 10000000).round
560     long=(x * 10000000).round
561     tile=QuadTile.tile_for_point(y, x)
562         
563     if (id>0) then
564         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})");
565         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}");
566         newid=id
567     else
568         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})");
569               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})");
570     end
571     [id,newid]
572   end
573
574   # -----       getpoi (id,baselong,basey,masterscale)
575   #                     returns id,x,y,tag array
576   
577   def getpoi(args)
578         id,baselong,basey,masterscale=args; id=id.to_i
579         poi=ActiveRecord::Base.connection.select_one("SELECT latitude*0.0000001 AS lat,longitude*0.0000001 AS lng,tags "+
580                 "FROM current_nodes WHERE visible=1 AND id=#{id}")
581         if poi.nil? then return [nil,nil,nil,''] end
582         [id,
583          long2coord(poi['lng'].to_f,baselong,masterscale),
584          lat2coord(poi['lat'].to_f,basey,masterscale),
585          tag2array(poi['tags'])]
586   end
587
588   # -----       deleteway (user token, way, nodes to keep)
589   #                     returns way ID only
590
591   def deleteway(args)
592     usertoken,way,preserve=args
593
594     RAILS_DEFAULT_LOGGER.info("  Message: deleteway, id=#{way}")
595
596     uid=getuserid(usertoken); if !uid then return end
597     way=way.to_i
598
599     db_uqn='unin'+uid.to_s+way.to_i.abs.to_s+Time.new.to_i.to_s # temp uniquenodes table name, typically 51 chars
600     db_now='@now'+uid.to_s+way.to_i.abs.to_s+Time.new.to_i.to_s # 'now' variable name, typically 51 chars
601     ActiveRecord::Base.connection.execute("SET #{db_now}=NOW()")
602
603     # - delete any otherwise unused nodes
604   
605     createuniquenodes(way,db_uqn,[])
606
607         unless (preserve.empty?) then
608                 ActiveRecord::Base.connection.execute("DELETE FROM #{db_uqn} WHERE node_id IN ("+preserve.join(',')+")")
609         end
610
611     sql=<<-EOF
612         INSERT INTO nodes (id,latitude,longitude,timestamp,user_id,visible,tile)
613         SELECT DISTINCT cn.id,cn.latitude,cn.longitude,#{db_now},#{uid},0,cn.tile
614           FROM current_nodes AS cn,#{db_uqn}
615          WHERE cn.id=node_id
616     EOF
617     ActiveRecord::Base.connection.insert(sql)
618
619     sql=<<-EOF
620       UPDATE current_nodes AS cn, #{db_uqn}
621          SET cn.timestamp=#{db_now},cn.visible=0,cn.user_id=#{uid} 
622        WHERE cn.id=node_id
623     EOF
624     ActiveRecord::Base.connection.update(sql)
625
626         deleteuniquenoderelations(db_uqn,uid,db_now)
627     ActiveRecord::Base.connection.execute("DROP TABLE #{db_uqn}")
628
629     # - delete way
630         
631     ActiveRecord::Base.connection.insert("INSERT INTO ways (id,user_id,timestamp,visible) VALUES (#{way},#{uid},#{db_now},0)")
632     ActiveRecord::Base.connection.update("UPDATE current_ways SET user_id=#{uid},timestamp=#{db_now},visible=0 WHERE id=#{way}")
633     ActiveRecord::Base.connection.execute("DELETE FROM current_way_nodes WHERE id=#{way}")
634     ActiveRecord::Base.connection.execute("DELETE FROM current_way_tags WHERE id=#{way}")
635         deleteitemrelations(way,'way',uid,db_now)
636     way
637 end
638
639
640
641 # ====================================================================
642 # Support functions for remote calls
643
644 def readwayquery(id)
645   ActiveRecord::Base.connection.select_all "SELECT latitude*0.0000001 AS latitude,longitude*0.0000001 AS longitude,current_nodes.id,tags "+
646       "    FROM current_way_nodes,current_nodes "+
647       "   WHERE current_way_nodes.id=#{id} "+
648       "     AND current_way_nodes.node_id=current_nodes.id "+
649       "     AND current_nodes.visible=1 "+
650       "   ORDER BY sequence_id"
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   # and use this (though with a new id) if it differs from the current one
685   if historic then
686         rows.each_index do |i|
687           sql=<<-EOF
688           SELECT latitude*0.0000001 AS latitude,longitude*0.0000001 AS longitude,tags 
689             FROM nodes 
690            WHERE id=#{rows[i]['id']} 
691              AND timestamp<="#{waytime}" 
692            ORDER BY timestamp DESC 
693            LIMIT 1
694           EOF
695           row=ActiveRecord::Base.connection.select_one(sql)
696           unless row.nil? then
697             nx=row['longitude'].to_f
698             ny=row['latitude'].to_f
699             if (nx!=rows[i]['longitude'].to_f or ny!=rows[i]['latitude'].to_f or row['tags']!=rows[i]['tags']) then
700                   rows[i]['id']=-1
701                   # This generates a new node id if x/y/tags differ from current node.
702                   # Strictly speaking, it need only do this for uniquenodes, but we're
703                   # not generating uniquenodes for historic ways (yet!).
704             end
705                 rows[i]['longitude']=nx
706                 rows[i]['latitude' ]=ny
707                 rows[i]['tags'     ]=row['tags']
708           end
709     end
710   end
711   rows
712 end
713
714 def createuniquenodes(way,uqn_name,nodelist)
715         # Find nodes which appear in this way but no others
716         sql=<<-EOF
717         CREATE TEMPORARY TABLE #{uqn_name}
718                                         SELECT a.node_id
719                                           FROM (SELECT DISTINCT node_id FROM current_way_nodes
720                                                         WHERE id=#{way}) a
721                                  LEFT JOIN current_way_nodes b
722                                                 ON b.node_id=a.node_id
723                                            AND b.id!=#{way}
724                                          WHERE b.node_id IS NULL
725         EOF
726         unless nodelist.empty? then
727           sql+="AND a.node_id NOT IN ("+nodelist.join(',')+")"
728         end
729         ActiveRecord::Base.connection.execute(sql)
730 end
731
732
733
734 # ====================================================================
735 # Relations handling
736 # deleteuniquenoderelations(uqn_name,uid,db_now)
737 # deleteitemrelations(way|node,'way'|'node',uid,db_now)
738
739 def deleteuniquenoderelations(uqn_name,uid,db_now)
740         sql=<<-EOF
741         SELECT node_id,cr.id FROM #{uqn_name},current_relation_members crm,current_relations cr 
742          WHERE crm.member_id=node_id 
743            AND crm.member_type='node' 
744            AND crm.id=cr.id 
745            AND cr.visible=1
746         EOF
747
748         relnodes=ActiveRecord::Base.connection.select_all(sql)
749         relnodes.each do |a|
750                 removefromrelation(a['node_id'],'node',a['id'],uid,db_now)
751         end
752 end
753
754 def deleteitemrelations(objid,type,uid,db_now)
755         sql=<<-EOF
756         SELECT cr.id FROM current_relation_members crm,current_relations cr 
757          WHERE crm.member_id=#{objid} 
758            AND crm.member_type='#{type}' 
759            AND crm.id=cr.id 
760            AND cr.visible=1
761         EOF
762         
763         relways=ActiveRecord::Base.connection.select_all(sql)
764         relways.each do |a|
765                 removefromrelation(objid,type,a['id'],uid,db_now)
766         end
767 end
768
769 def removefromrelation(objid,type,relation,uid,db_now)
770         rver=ActiveRecord::Base.connection.insert("INSERT INTO relations (id,user_id,timestamp,visible) VALUES (#{relation},#{uid},#{db_now},1)")
771
772         tagsql=<<-EOF
773         INSERT INTO relation_tags (id,k,v,version) 
774         SELECT id,k,v,#{rver} FROM current_relation_tags 
775          WHERE id=#{relation} 
776         EOF
777         ActiveRecord::Base.connection.insert(tagsql)
778
779         membersql=<<-EOF
780         INSERT INTO relation_members (id,member_type,member_id,member_role,version) 
781         SELECT id,member_type,member_id,member_role,#{rver} FROM current_relation_members 
782          WHERE id=#{relation} 
783            AND (member_id!=#{objid} OR member_type!='#{type}')
784         EOF
785         ActiveRecord::Base.connection.insert(membersql)
786         
787         ActiveRecord::Base.connection.update("UPDATE current_relations SET user_id=#{uid},timestamp=#{db_now} WHERE id=#{relation}")
788         ActiveRecord::Base.connection.execute("DELETE FROM current_relation_members WHERE id=#{relation} AND member_type='#{type}' AND member_id=#{objid}")
789 end
790
791
792 def sqlescape(a)
793   a.gsub(/[\000-\037]/,"").gsub("'","''").gsub(92.chr,92.chr+92.chr)
794 end
795
796 def tag2array(a)
797   tags={}
798   a.gsub(';;;','#%').split(';').each do |b|
799     b.gsub!('#%',';;;')
800     b.gsub!('===','#%')
801     k,v=b.split('=')
802     if k.nil? then k='' end
803     if v.nil? then v='' end
804     tags[k.gsub('#%','=').gsub(':','|')]=v.gsub('#%','=')
805   end
806   tags
807 end
808
809 def array2tag(a)
810   str=''
811   a.each do |k,v|
812     if v=='' then next end
813     if v[0,6]=='(type ' then next end
814     if str!='' then str+=';' end
815     str+=k.gsub(';',';;;').gsub('=','===').gsub('|',':')+'='+v.gsub(';',';;;').gsub('=','===')
816   end
817   str
818 end
819
820 def getuserid(token)
821   if (token =~ /^(.+)\+(.+)$/) then
822     user = User.authenticate(:username => $1, :password => $2)
823   else
824     user = User.authenticate(:token => token)
825   end
826
827   return user ? user.id : nil;
828 end
829
830
831
832 # ====================================================================
833 # AMF read subroutines
834
835 # ----- getint          return two-byte integer
836 # ----- getlong         return four-byte long
837 # ----- getstring       return string with two-byte length
838 # ----- getdouble       return eight-byte double-precision float
839 # ----- getobject       return object/hash
840 # ----- getarray        return numeric array
841
842 def getint(s)
843   s.getc*256+s.getc
844 end
845
846 def getlong(s)
847   ((s.getc*256+s.getc)*256+s.getc)*256+s.getc
848 end
849
850 def getstring(s)
851   len=s.getc*256+s.getc
852   s.read(len)
853 end
854
855 def getdouble(s)
856   a=s.read(8).unpack('G')                       # G big-endian, E little-endian
857   a[0]
858 end
859
860 def getarray(s)
861   len=getlong(s)
862   arr=[]
863   for i in (0..len-1)
864     arr[i]=getvalue(s)
865   end
866   arr
867 end
868
869 def getobject(s)
870   arr={}
871   while (key=getstring(s))
872     if (key=='') then break end
873     arr[key]=getvalue(s)
874   end
875   s.getc                # skip the 9 'end of object' value
876   arr
877 end
878
879 # ----- getvalue        parse and get value
880
881 def getvalue(s)
882   case s.getc
883         when 0; return getdouble(s)                     # number
884         when 1; return s.getc                           # boolean
885         when 2; return getstring(s)                     # string
886         when 3; return getobject(s)                     # object/hash
887         when 5; return nil                                      # null
888         when 6; return nil                                      # undefined
889         when 8; s.read(4)                                       # mixedArray
890                         return getobject(s)                     #  |
891         when 10;return getarray(s)                      # array
892         else;   return nil                                      # error
893   end
894 end
895
896 # ====================================================================
897 # AMF write subroutines
898
899 # ----- putdata         envelope data into AMF writeable form
900 # ----- encodevalue     pack variables as AMF
901
902 def putdata(index,n)
903   d =encodestring(index+"/onResult")
904   d+=encodestring("null")
905   d+=[-1].pack("N")
906   d+=encodevalue(n)
907 end
908
909 def encodevalue(n)
910   case n.class.to_s
911   when 'Array'
912     a=10.chr+encodelong(n.length)
913     n.each do |b|
914       a+=encodevalue(b)
915     end
916     a
917   when 'Hash'
918     a=3.chr
919     n.each do |k,v|
920       a+=encodestring(k)+encodevalue(v)
921     end
922     a+0.chr+0.chr+9.chr
923   when 'String'
924     2.chr+encodestring(n)
925   when 'Bignum','Fixnum','Float'
926     0.chr+encodedouble(n)
927   when 'NilClass'
928     5.chr
929   else
930     RAILS_DEFAULT_LOGGER.error("Unexpected Ruby type for AMF conversion: "+n.class.to_s)
931   end
932 end
933
934 # ----- encodestring    encode string with two-byte length
935 # ----- encodedouble    encode number as eight-byte double precision float
936 # ----- encodelong              encode number as four-byte long
937
938 def encodestring(n)
939   a,b=n.size.divmod(256)
940   a.chr+b.chr+n
941 end
942
943 def encodedouble(n)
944   [n].pack('G')
945 end
946
947 def encodelong(n)
948   [n].pack('N')
949 end
950
951 # ====================================================================
952 # Co-ordinate conversion
953
954 def lat2coord(a,basey,masterscale)
955   -(lat2y(a)-basey)*masterscale+250
956 end
957
958 def long2coord(a,baselong,masterscale)
959   (a-baselong)*masterscale+350
960 end
961
962 def lat2y(a)
963   180/Math::PI * Math.log(Math.tan(Math::PI/4+a*(Math::PI/180)/2))
964 end
965
966 def coord2lat(a,masterscale,basey)
967   y2lat((a-250)/-masterscale+basey)
968 end
969
970 def coord2long(a,masterscale,baselong)
971   (a-350)/masterscale+baselong
972 end
973
974 def y2lat(a)
975   180/Math::PI * (2*Math.atan(Math.exp(a*Math::PI/180))-Math::PI/2)
976 end
977
978 end