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