Merge api06 branch to trunk.
authorTom Hughes <tom@compton.nu>
Mon, 20 Apr 2009 09:12:03 +0000 (09:12 +0000)
committerTom Hughes <tom@compton.nu>
Mon, 20 Apr 2009 09:12:03 +0000 (09:12 +0000)
1  2 
app/controllers/amf_controller.rb
app/controllers/export_controller.rb
app/views/user/view.rhtml

index 7f85280b79d29d5b320460d01f028f14727a3fd0,de3c7583b42d9977f255a581afbc5fe9265121b2..b0b3f13cf6c1947914caa7583e3fef58ff4bc392
@@@ -3,7 -3,7 +3,7 @@@
  # OSM database takes place using this controller. Messages are 
  # encoded in the Actionscript Message Format (AMF).
  #
- # Helper functions are in /lib/potlatch.
+ # Helper functions are in /lib/potlatch.rb
  #
  # Author::    editions Systeme D / Richard Fairhurst 2004-2008
  # Licence::   public domain.
  # from the AMF message), each method generally takes arguments in the order 
  # they were sent by the Potlatch SWF. Do not assume typing has been preserved. 
  # Methods all return an array to the SWF.
+ #
+ # == API 0.6
+ #
+ # Note that this requires a patched version of composite_primary_keys 1.1.0
+ # (see http://groups.google.com/group/compositekeys/t/a00e7562b677e193) 
+ # if you are to run with POTLATCH_USE_SQL=false .
  # 
  # == Debugging
  # 
  #     return(-1,"message")            <-- just puts up a dialogue
  #     return(-2,"message")            <-- also asks the user to e-mail me
  # 
- # To write to the Rails log, use RAILS_DEFAULT_LOGGER.info("message").
+ # To write to the Rails log, use logger.info("message").
+ # Remaining issues:
+ # * version conflict when POIs and ways are reverted
  
  class AmfController < ApplicationController
    require 'stringio'
  
    include Potlatch
  
+   # Help methods for checking boundary sanity and area size
+   include MapBoundary
    session :off
    before_filter :check_api_writable
  
    # ** FIXME: refactor to reduce duplication of code across read/write
    
    def amf_read
-       req=StringIO.new(request.raw_post+0.chr)# Get POST data as request
-                                                                                       # (cf http://www.ruby-forum.com/topic/122163)
-       req.read(2)                                                             # Skip version indicator and client ID
-       results={}                                                              # Results of each body
+     req=StringIO.new(request.raw_post+0.chr)# Get POST data as request
+                               # (cf http://www.ruby-forum.com/topic/122163)
+     req.read(2)                                                               # Skip version indicator and client ID
+     results={}                                                                # Results of each body
  
-       # Parse request
+     # Parse request
  
 -    headers=AMF.getint(req)                                   # Read number of headers
 +      headers=AMF.getint(req)                                 # Read number of headers
  
-       headers.times do                                                # Read each header
-         name=AMF.getstring(req)                               #  |
-         req.getc                                                              #  | skip boolean
-         value=AMF.getvalue(req)                               #  |
-         header["name"]=value                                  #  |
-       end
-       bodies=AMF.getint(req)                                  # Read number of bodies
-       bodies.times do                                                 # Read each body
-         message=AMF.getstring(req)                    #  | get message name
-         index=AMF.getstring(req)                              #  | get index in response sequence
-         bytes=AMF.getlong(req)                                #  | get total size in bytes
-         args=AMF.getvalue(req)                                #  | get response (probably an array)
-         case message
-               when 'getpresets';                      results[index]=AMF.putdata(index,getpresets())
-               when 'whichways';                       results[index]=AMF.putdata(index,whichways(*args))
-               when 'whichways_deleted';       results[index]=AMF.putdata(index,whichways_deleted(*args))
-               when 'getway';                          results[index]=AMF.putdata(index,getway(args[0].to_i))
-               when 'getrelation';                     results[index]=AMF.putdata(index,getrelation(args[0].to_i))
-               when 'getway_old';                      results[index]=AMF.putdata(index,getway_old(args[0].to_i,args[1].to_i))
-               when 'getway_history';          results[index]=AMF.putdata(index,getway_history(args[0].to_i))
-               when 'getnode_history';         results[index]=AMF.putdata(index,getnode_history(args[0].to_i))
-               when 'findrelations';           results[index]=AMF.putdata(index,findrelations(*args))
-               when 'getpoi';                          results[index]=AMF.putdata(index,getpoi(*args))
-         end
-       end
+     headers.times do                                          # Read each header
+       name=AMF.getstring(req)                         #  |
+       req.getc                                                                #  | skip boolean
+       value=AMF.getvalue(req)                         #  |
+       header["name"]=value                                    #  |
+     end
+     bodies=AMF.getint(req)                                    # Read number of bodies
+     bodies.times do                                                   # Read each body
+       message=AMF.getstring(req)                      #  | get message name
+       index=AMF.getstring(req)                                #  | get index in response sequence
+       bytes=AMF.getlong(req)                          #  | get total size in bytes
+       args=AMF.getvalue(req)                          #  | get response (probably an array)
+       logger.info("Executing AMF #{message}:#{index}")
+       case message
+         when 'getpresets';                    results[index]=AMF.putdata(index,getpresets())
+         when 'whichways';                     results[index]=AMF.putdata(index,whichways(*args))
+         when 'whichways_deleted';     results[index]=AMF.putdata(index,whichways_deleted(*args))
+         when 'getway';                                results[index]=AMF.putdata(index,getway(args[0].to_i))
+         when 'getrelation';                   results[index]=AMF.putdata(index,getrelation(args[0].to_i))
+         when 'getway_old';                    results[index]=AMF.putdata(index,getway_old(args[0].to_i,args[1]))
+         when 'getway_history';                results[index]=AMF.putdata(index,getway_history(args[0].to_i))
+         when 'getnode_history';               results[index]=AMF.putdata(index,getnode_history(args[0].to_i))
+         when 'findgpx';                               results[index]=AMF.putdata(index,findgpx(*args))
+         when 'findrelations';         results[index]=AMF.putdata(index,findrelations(*args))
+         when 'getpoi';                                results[index]=AMF.putdata(index,getpoi(*args))
+       end
+     end
+     logger.info("encoding AMF results")
      sendresponse(results)
    end
  
    def amf_write
-       req=StringIO.new(request.raw_post+0.chr)
-       req.read(2)
-       results={}
-       renumberednodes={}                                              # Shared across repeated putways
-       renumberedways={}                                               # Shared across repeated putways
-       headers=AMF.getint(req)                                 # Read number of headers
-       headers.times do                                                # Read each header
-         name=AMF.getstring(req)                               #  |
-         req.getc                                                              #  | skip boolean
-         value=AMF.getvalue(req)                               #  |
-         header["name"]=value                                  #  |
-       end
-       bodies=AMF.getint(req)                                  # Read number of bodies
-       bodies.times do                                                 # Read each body
-         message=AMF.getstring(req)                    #  | get message name
-         index=AMF.getstring(req)                              #  | get index in response sequence
-         bytes=AMF.getlong(req)                                #  | get total size in bytes
-         args=AMF.getvalue(req)                                #  | get response (probably an array)
-         case message
-               when 'putway';                          r=putway(renumberednodes,*args)
+     req=StringIO.new(request.raw_post+0.chr)
+     req.read(2)
+     results={}
+     renumberednodes={}                                                # Shared across repeated putways
+     renumberedways={}                                         # Shared across repeated putways
+     headers=AMF.getint(req)                                   # Read number of headers
+     headers.times do                                          # Read each header
+       name=AMF.getstring(req)                         #  |
+       req.getc                                                                #  | skip boolean
+       value=AMF.getvalue(req)                         #  |
+       header["name"]=value                                    #  |
+     end
+     bodies=AMF.getint(req)                                    # Read number of bodies
+     bodies.times do                                                   # Read each body
+       message=AMF.getstring(req)                      #  | get message name
+       index=AMF.getstring(req)                                #  | get index in response sequence
+       bytes=AMF.getlong(req)                          #  | get total size in bytes
+       args=AMF.getvalue(req)                          #  | get response (probably an array)
+       logger.info("Executing AMF #{message}:#{index}")
+       case message
+         when 'putway';                                r=putway(renumberednodes,*args)
                                                                        renumberednodes=r[3]
-                                                                       if r[1] != r[2]
-                                                                         renumberedways[r[1]] = r[2]
-                                                                       end
+                                                                       if r[1] != r[2] then renumberedways[r[1]] = r[2] end
                                                                        results[index]=AMF.putdata(index,r)
-               when 'putrelation';                     results[index]=AMF.putdata(index,putrelation(renumberednodes, renumberedways, *args))
-               when 'deleteway';                       results[index]=AMF.putdata(index,deleteway(args[0],args[1].to_i))
-               when 'putpoi';                          results[index]=AMF.putdata(index,putpoi(*args))
-         end
-       end
+         when 'putrelation';                   results[index]=AMF.putdata(index,putrelation(renumberednodes, renumberedways, *args))
+         when 'deleteway';                     results[index]=AMF.putdata(index,deleteway(*args))
+         when 'putpoi';                                r=putpoi(*args)
+                                                                       if r[1] != r[2] then renumberednodes[r[1]] = r[2] end
+                                                               results[index]=AMF.putdata(index,r)
+         when 'startchangeset';                results[index]=AMF.putdata(index,startchangeset(*args))
+       end
+     end
+     logger.info("encoding AMF results")
      sendresponse(results)
    end
  
    private
  
+   # Start new changeset
+   
+   def startchangeset(usertoken, cstags, closeid, closecomment)
+     user = getuser(usertoken)
+     if !user then return -1,"You are not logged in, so Potlatch can't write any changes to the database." end
+     # close previous changeset and add comment
+     if closeid
+       cs = Changeset.find(closeid)
+       cs.set_closed_time_now
+       if cs.user_id!=user.id
+         return -2,"You cannot close that changeset because you're not the person who opened it."
+       elsif closecomment.empty?
+         cs.save!
+       else
+         cs.tags['comment']=closecomment
+         cs.save_with_tags!
+       end
+     end
+       
+     # open a new changeset
+     cs = Changeset.new
+     cs.tags = cstags
+     cs.user_id = user.id
+     # smsm1 doesn't like the next two lines and thinks they need to be abstracted to the model more/better
+     cs.created_at = Time.now.getutc
+     cs.closed_at = cs.created_at + Changeset::IDLE_TIMEOUT
+     cs.save_with_tags!
+     return [0,cs.id]
+   end
    # Return presets (default tags, localisation etc.):
    # uses POTLATCH_PRESETS global, set up in OSM::Potlatch.
  
    def getpresets() #:doc:
-       return POTLATCH_PRESETS
+     return POTLATCH_PRESETS
    end
  
+   ##
    # Find all the ways, POI nodes (i.e. not part of ways), and relations
    # in a given bounding box. Nodes are returned in full; ways and relations 
    # are IDs only. 
+   #
+   # return is of the form: 
+   # [error_code, 
+   #  [[way_id, way_version], ...],
+   #  [[node_id, lat, lon, [tags, ...], node_version], ...],
+   #  [[rel_id, rel_version], ...]]
+   # where the ways are any visible ways which refer to any visible
+   # nodes in the bbox, nodes are any visible nodes in the bbox but not
+   # used in any way, rel is any relation which refers to either a way
+   # or node that we're returning.
    def whichways(xmin, ymin, xmax, ymax) #:doc:
-       enlarge = [(xmax-xmin)/8,0.01].min
-       xmin -= enlarge; ymin -= enlarge
-       xmax += enlarge; ymax += enlarge
-       if POTLATCH_USE_SQL then
-         way_ids = sql_find_way_ids_in_area(xmin, ymin, xmax, ymax)
-         points = sql_find_pois_in_area(xmin, ymin, xmax, ymax)
-         relation_ids = sql_find_relations_in_area_and_ways(xmin, ymin, xmax, ymax, way_ids)
-       else
-         # find the way ids in an area
-         nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => "current_nodes.visible = 1", :include => :ways)
-         way_ids = nodes_in_area.collect { |node| node.way_ids }.flatten.uniq
-         # find the node ids in an area that aren't part of ways
-         nodes_not_used_in_area = nodes_in_area.select { |node| node.ways.empty? }
-         points = nodes_not_used_in_area.collect { |n| [n.id, n.lon, n.lat, n.tags_as_hash] }
-         # find the relations used by those nodes and ways
-         relations = Relation.find_for_nodes(nodes_in_area.collect { |n| n.id }, :conditions => "visible = 1") +
-                   Relation.find_for_ways(way_ids, :conditions => "visible = 1")
-         relation_ids = relations.collect { |relation| relation.id }.uniq
-       end
-       [way_ids, points, relation_ids]
+     enlarge = [(xmax-xmin)/8,0.01].min
+     xmin -= enlarge; ymin -= enlarge
+     xmax += enlarge; ymax += enlarge
+     
+     # check boundary is sane and area within defined
+     # see /config/application.yml
+     check_boundaries(xmin, ymin, xmax, ymax)
+     if POTLATCH_USE_SQL then
+       ways = sql_find_ways_in_area(xmin, ymin, xmax, ymax)
+       points = sql_find_pois_in_area(xmin, ymin, xmax, ymax)
+       relations = sql_find_relations_in_area_and_ways(xmin, ymin, xmax, ymax, ways.collect {|x| x[0]})
+     else
+       # find the way ids in an area
+       nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => ["current_nodes.visible = ?", true], :include => :ways)
+       ways = nodes_in_area.inject([]) { |sum, node| 
+         visible_ways = node.ways.select { |w| w.visible? }
+         sum + visible_ways.collect { |w| [w.id,w.version] }
+       }.uniq
+       ways.delete([])
+       # find the node ids in an area that aren't part of ways
+       nodes_not_used_in_area = nodes_in_area.select { |node| node.ways.empty? }
+       points = nodes_not_used_in_area.collect { |n| [n.id, n.lon, n.lat, n.tags, n.version] }.uniq
+       # find the relations used by those nodes and ways
+       relations = Relation.find_for_nodes(nodes_in_area.collect { |n| n.id }, :conditions => {:visible => true}) +
+                   Relation.find_for_ways(ways.collect { |w| w[0] }, :conditions => {:visible => true})
+       relations = relations.collect { |relation| [relation.id,relation.version] }.uniq
+     end
+     [0, ways, points, relations]
+   rescue Exception => err
+     [-2,"Sorry - I can't get the map for that area."]
    end
  
    # Find deleted ways in current bounding box (similar to whichways, but ways
    # with a deleted node only - not POIs or relations).
  
    def whichways_deleted(xmin, ymin, xmax, ymax) #:doc:
-       xmin -= 0.01; ymin -= 0.01
-       xmax += 0.01; ymax += 0.01
-       nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => "current_nodes.visible = 0 AND current_ways.visible = 0", :include => :ways_via_history)
-       way_ids = nodes_in_area.collect { |node| node.ways_via_history_ids }.flatten.uniq
-       [way_ids]
+     enlarge = [(xmax-xmin)/8,0.01].min
+     xmin -= enlarge; ymin -= enlarge
+     xmax += enlarge; ymax += enlarge
+     # check boundary is sane and area within defined
+     # see /config/application.yml
+     begin
+       check_boundaries(xmin, ymin, xmax, ymax)
+     rescue Exception => err
+       return [-2,"Sorry - I can't get the map for that area."]
+     end
+     nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => ["current_ways.visible = ?", false], :include => :ways_via_history)
+     way_ids = nodes_in_area.collect { |node| node.ways_via_history_ids }.flatten.uniq
+     [0,way_ids]
    end
  
    # Get a way including nodes and tags.
-   # Returns 0 (success), a Potlatch-style array of points, and a hash of tags.
+   # Returns the way id, a Potlatch-style array of points, a hash of tags, and the version number.
  
    def getway(wayid) #:doc:
-       if POTLATCH_USE_SQL then
-         points = sql_get_nodes_in_way(wayid)
-         tags = sql_get_tags_in_way(wayid)
-       else
-         # Ideally we would do ":include => :nodes" here but if we do that
-         # then rails only seems to return the first copy of a node when a
-         # way includes a node more than once
-         way = Way.find(wayid)
-         points = way.nodes.collect do |node|
-               nodetags=node.tags_as_hash
-               nodetags.delete('created_by')
-               [node.lon, node.lat, node.id, nodetags]
-         end
-         tags = way.tags
-       end
-       [wayid, points, tags]
+     if POTLATCH_USE_SQL then
+         points = sql_get_nodes_in_way(wayid)
+         tags = sql_get_tags_in_way(wayid)
+         version = sql_get_way_version(wayid)
+       else
+         # Ideally we would do ":include => :nodes" here but if we do that
+         # then rails only seems to return the first copy of a node when a
+         # way includes a node more than once
+         begin
+           way = Way.find(wayid)
+         rescue ActiveRecord::RecordNotFound
+           return [wayid,[],{}]
+         end
+         # check case where way has been deleted or doesn't exist
+         return [wayid,[],{}] if way.nil? or !way.visible
+         points = way.nodes.collect do |node|
+         nodetags=node.tags
+         nodetags.delete('created_by')
+         [node.lon, node.lat, node.id, nodetags, node.version]
+       end
+       tags = way.tags
+       version = way.version
+     end
+     [wayid, points, tags, version]
    end
--
++  
    # Get an old version of a way, and all constituent nodes.
    #
-   # For undelete (version=0), always uses the most recent version of each node, 
-   # even if it's moved.  For revert (version=1+), uses the node in existence 
+   # For undelete (version<0), always uses the most recent version of each node, 
+   # even if it's moved.  For revert (version >= 0), uses the node in existence 
    # at the time, generating a new id if it's still visible and has been moved/
    # retagged.
-   def getway_old(id, version) #:doc:
-       if version < 0
-         old_way = OldWay.find(:first, :conditions => ['visible = 1 AND id = ?', id], :order => 'version DESC')
-         points = old_way.get_nodes_undelete
-       else
-         old_way = OldWay.find(:first, :conditions => ['id = ? AND version = ?', id, version])
-         points = old_way.get_nodes_revert
-       end
-       old_way.tags['history'] = "Retrieved from v#{old_way.version}"
-       [0, id, points, old_way.tags, old_way.version]
+   #
+   # Returns:
+   # 0. success code, 
+   # 1. id, 
+   # 2. array of points, 
+   # 3. hash of tags, 
+   # 4. version, 
+   # 5. is this the current, visible version? (boolean)
+   
+   def getway_old(id, timestamp) #:doc:
+     if timestamp == ''
+       # undelete
+       old_way = OldWay.find(:first, :conditions => ['visible = ? AND id = ?', true, id], :order => 'version DESC')
+       points = old_way.get_nodes_undelete unless old_way.nil?
+     else
+       begin
+         # revert
+         timestamp = DateTime.strptime(timestamp.to_s, "%d %b %Y, %H:%M:%S")
+         old_way = OldWay.find(:first, :conditions => ['id = ? AND timestamp <= ?', id, timestamp], :order => 'timestamp DESC')
+         unless old_way.nil?
+           points = old_way.get_nodes_revert(timestamp)
+           if !old_way.visible
+             return [-1, "Sorry, the way was deleted at that time - please revert to a previous version."]
+           end
+         end
+       rescue ArgumentError
+         # thrown by date parsing method. leave old_way as nil for
+         # the superb error handler below.
+       end
+     end
+     if old_way.nil?
+       # *** FIXME: shouldn't this be returning an error?
+       return [-1, id, [], {}, -1,0]
+     else
+       curway=Way.find(id)
+       old_way.tags['history'] = "Retrieved from v#{old_way.version}"
+       return [0, id, points, old_way.tags, curway.version, (curway.version==old_way.version and curway.visible)]
+     end
    end
    
-   # Find history of a way. Returns 'way', id, and 
-   # an array of previous versions.
+   # Find history of a way.
+   # Returns 'way', id, and an array of previous versions:
+   # - formerly [old_way.version, old_way.timestamp.strftime("%d %b %Y, %H:%M"), old_way.visible ? 1 : 0, user, uid]
+   # - now [timestamp,user,uid]
+   #
+   # Heuristic: Find all nodes that have ever been part of the way; 
+   # get a list of their revision dates; add revision dates of the way;
+   # sort and collapse list (to within 2 seconds); trim all dates before the 
+   # start date of the way.
  
    def getway_history(wayid) #:doc:
-       history = Way.find(wayid).old_ways.reverse.collect do |old_way|
-         user = old_way.user.data_public? ? old_way.user.display_name : 'anonymous'
-         uid  = old_way.user.data_public? ? old_way.user.id : 0
-         [old_way.version, old_way.timestamp.strftime("%d %b %Y, %H:%M"), old_way.visible ? 1 : 0, user, uid]
-       end
  
-       ['way',wayid,history]
+     begin
+       # Find list of revision dates for way and all constituent nodes
+       revdates=[]
+       revusers={}
+       Way.find(wayid).old_ways.collect do |a|
+         revdates.push(a.timestamp)
+         unless revusers.has_key?(a.timestamp.to_i) then revusers[a.timestamp.to_i]=change_user(a) end
+         a.nds.each do |n|
+           Node.find(n).old_nodes.collect do |o|
+             revdates.push(o.timestamp)
+             unless revusers.has_key?(o.timestamp.to_i) then revusers[o.timestamp.to_i]=change_user(o) end
+           end
+         end
+       end
+       waycreated=revdates[0]
+       revdates.uniq!
+       revdates.sort!
+         revdates.reverse!
+       # Remove any dates (from nodes) before first revision date of way
+       revdates.delete_if { |d| d<waycreated }
+       # Remove any elements where 2 seconds doesn't elapse before next one
+       revdates.delete_if { |d| revdates.include?(d+1) or revdates.include?(d+2) }
+       # Collect all in one nested array
+       revdates.collect! {|d| [d.strftime("%d %b %Y, %H:%M:%S")] + revusers[d.to_i] }
+       return ['way',wayid,revdates]
+     rescue ActiveRecord::RecordNotFound
+       return ['way', wayid, []]
+     end
    end
-   # Find history of a node. Returns 'node', id, and 
-   # an array of previous versions.
+   
+   # Find history of a node. Returns 'node', id, and an array of previous versions as above.
  
    def getnode_history(nodeid) #:doc:
-       history = Node.find(nodeid).old_nodes.reverse.collect do |old_node|
-         user = old_node.user.data_public? ? old_node.user.display_name : 'anonymous'
-         uid  = old_node.user.data_public? ? old_node.user.id : 0
-         [old_node.timestamp.to_i, old_node.timestamp.strftime("%d %b %Y, %H:%M"), old_node.visible ? 1 : 0, user, uid]
-       end
+     begin 
+       history = Node.find(nodeid).old_nodes.reverse.collect do |old_node|
+         [old_node.timestamp.strftime("%d %b %Y, %H:%M:%S")] + change_user(old_node)
+       end
+       return ['node', nodeid, history]
+     rescue ActiveRecord::RecordNotFound
+       return ['node', nodeid, []]
+     end
+   end
  
-       ['node',nodeid,history]
+   def change_user(obj)
+     user_object = obj.changeset.user
+     user = user_object.data_public? ? user_object.display_name : 'anonymous'
+     uid  = user_object.data_public? ? user_object.id : 0
+     [user,uid]
+   end
+   # Find GPS traces with specified name/id.
+   # Returns array listing GPXs, each one comprising id, name and description.
+   
+   def findgpx(searchterm, usertoken)
+     user = getuser(usertoken)
+     if !uid then return -1,"You must be logged in to search for GPX traces." end
+     gpxs = []
+     if searchterm.to_i>0 then
+       gpx = Trace.find(searchterm.to_i, :conditions => ["visible=? AND (public=? OR user_id=?)",true,true,user.id] )
+       if gpx then
+         gpxs.push([gpx.id, gpx.name, gpx.description])
+       end
+     else
+       Trace.find(:all, :limit => 21, :conditions => ["visible=? AND (public=? OR user_id=?) AND MATCH(name) AGAINST (?)",true,true,user.id,searchterm] ).each do |gpx|
+       gpxs.push([gpx.id, gpx.name, gpx.description])
+         end
+       end
+     gpxs
    end
  
    # Get a relation with all tags and members.
    # Returns:
    # 0. relation id,
    # 1. hash of tags,
-   # 2. list of members.
+   # 2. list of members,
+   # 3. version.
    
    def getrelation(relid) #:doc:
-       rel = Relation.find(relid)
-       [relid, rel.tags, rel.members]
+     begin
+       rel = Relation.find(relid)
+     rescue ActiveRecord::RecordNotFound
+       return [relid, {}, []]
+     end
+     return [relid, {}, [], nil] if rel.nil? or !rel.visible
+     [relid, rel.tags, rel.members, rel.version]
    end
  
    # Find relations with specified name/id.
    # Returns array of relations, each in same form as getrelation.
    
    def findrelations(searchterm)
-       rels = []
-       if searchterm.to_i>0 then
-         rel = Relation.find(searchterm.to_i)
-         if rel and rel.visible then
-           rels.push([rel.id, rel.tags, rel.members])
-         end
-       else
-         RelationTag.find(:all, :limit => 11, :conditions => ["match(v) against (?)", searchterm] ).each do |t|
-               if t.relation.visible then
+     rels = []
+     if searchterm.to_i>0 then
+       rel = Relation.find(searchterm.to_i)
+       if rel and rel.visible then
+         rels.push([rel.id, rel.tags, rel.members])
+       end
+     else
+       RelationTag.find(:all, :limit => 11, :conditions => ["match(v) against (?)", searchterm] ).each do |t|
+       if t.relation.visible then
              rels.push([t.relation.id, t.relation.tags, t.relation.members])
            end
          end
        end
-       rels
+     rels
    end
  
    # Save a relation.
    # 1. original relation id (unchanged),
    # 2. new relation id.
  
-   def putrelation(renumberednodes, renumberedways, usertoken, relid, tags, members, visible) #:doc:
-       uid = getuserid(usertoken)
-       if !uid then return -1,"You are not logged in, so the relation could not be saved." end
-       relid = relid.to_i
-       visible = visible.to_i
-       # create a new relation, or find the existing one
-       if relid <= 0
-         rel = Relation.new
-       else
-         rel = Relation.find(relid)
-       end
-       # check the members are all positive, and correctly type
-       typedmembers = []
-       members.each do |m|
-         mid = m[1].to_i
-         if mid < 0
-               mid = renumberednodes[mid] if m[0] == 'node'
-               mid = renumberedways[mid] if m[0] == 'way'
-         end
-       if mid
-           typedmembers << [m[0], mid, m[2]]
-         end
-       end
-       # assign new contents
-       rel.members = typedmembers
-       rel.tags = tags
-       rel.visible = visible
-       rel.user_id = uid
-       # check it then save it
-       # BUG: the following is commented out because it always fails on my
-       #  install. I think it's a Rails bug.
-       #if !rel.preconditions_ok?
-       #  return -2, "Relation preconditions failed"
-       #else
-         rel.save_with_history!
-       #end
-       [0, relid, rel.id]
+   def putrelation(renumberednodes, renumberedways, usertoken, changeset_id, version, relid, tags, members, visible) #:doc:
+     user = getuser(usertoken)
+     if !user then return -1,"You are not logged in, so the relation could not be saved." end
+     relid = relid.to_i
+     visible = (visible.to_i != 0)
+     new_relation = nil
+     relation = nil
+     Relation.transaction do
+       # create a new relation, or find the existing one
+       if relid > 0
+         relation = Relation.find(relid)
+       end
+       # We always need a new node, based on the data that has been sent to us
+       new_relation = Relation.new
+       # check the members are all positive, and correctly type
+       typedmembers = []
+       members.each do |m|
+         mid = m[1].to_i
+         if mid < 0
+           mid = renumberednodes[mid] if m[0] == 'node'
+           mid = renumberedways[mid] if m[0] == 'way'
+         end
+         if mid
+           typedmembers << [m[0], mid, m[2]]
+         end
+       end
+       # assign new contents
+       new_relation.members = typedmembers
+       new_relation.tags = tags
+       new_relation.visible = visible
+       new_relation.changeset_id = changeset_id
+       new_relation.version = version
+       # NOTE: id or relid here? id doesn't seem to be set above
+       if relid <= 0
+         # We're creating the node
+         new_relation.create_with_history(user)
+       elsif visible
+         # We're updating the node
+         relation.update_from(new_relation, user)
+       else
+         # We're deleting the node
+         relation.delete_with_history!(new_relation, user)
+       end
+     end # transaction
+       
+     if id <= 0
+       return [0, relid, new_relation.id, new_relation.version]
+     else
+       return [0, relid, relation.id, relation.version]
+     end
+   rescue OSM::APIChangesetAlreadyClosedError => ex
+     return [-1, "The changeset #{ex.changeset.id} was closed at #{ex.changeset.closed_at}."]
+   rescue OSM::APIVersionMismatchError => ex
+     # Really need to check to see whether this is a server load issue, and the 
+     # last version was in the same changeset, or belongs to the same user, then
+     # we can return something different
+     return [-3, "Sorry, someone else has changed this relation since you started editing. Please click the 'Edit' tab to reload the area."]
+   rescue OSM::APIAlreadyDeletedError => ex
+     return [-1, "The relation has already been deleted."]
+   rescue OSM::APIError => ex
+     # Some error that we don't specifically catch
+     return [-2, "Something really bad happened :-( ."]
    end
  
    # Save a way to the database, including all nodes. Any nodes in the previous
    # version and no longer used are deleted.
    # 
+   # Parameters:
+   # 0. hash of renumbered nodes (added by amf_controller)
+   # 1. current user token (for authentication)
+   # 2. current changeset
+   # 3. new way version
+   # 4. way ID
+   # 5. list of nodes in way
+   # 6. hash of way tags
+   # 7. array of nodes to change (each one is [lon,lat,id,version,tags])
+   # 
    # Returns:
    # 0. '0' (code for success),
    # 1. original way id (unchanged),
    # 2. new way id,
-   # 3. hash of renumbered nodes (old id=>new id)
+   # 3. hash of renumbered nodes (old id=>new id),
+   # 4. way version,
+   # 5. hash of node versions (node=>version)
  
-   def putway(renumberednodes, usertoken, originalway, points, attributes) #:doc:
+   def putway(renumberednodes, usertoken, changeset_id, wayversion, originalway, pointlist, attributes, nodes) #:doc:
  
-       # -- Initialise and carry out checks
+     # -- Initialise
        
-       uid = getuserid(usertoken)
-       if !uid then return -1,"You are not logged in, so the way could not be saved." end
-       originalway = originalway.to_i
-       points.each do |a|
-         if a[2] == 0 or a[2].nil? then return -2,"Server error - node with id 0 found in way #{originalway}." end
-         if a[1] == 90 then return -2,"Server error - node with lat -90 found in way #{originalway}." end
-       end
-       if points.length < 2 then return -2,"Server error - way is only #{points.length} points long." end
-       # -- Get unique nodes
-       if originalway < 0
-         way = Way.new
-         uniques = []
-       else
-         way = Way.find(originalway)
-         uniques = way.unshared_node_ids
-       end
-       # -- Compare nodes and save changes to any that have changed
-       nodes = []
-       points.each do |n|
-         lon = n[0].to_f
-         lat = n[1].to_f
-         id = n[2].to_i
-         savenode = false
-         if renumberednodes[id]
-           id = renumberednodes[id]
-         elsif id < 0
-               # Create new node
-               node = Node.new
-               savenode = true
-         else
-               node = Node.find(id)
-               nodetags=node.tags_as_hash
-               nodetags.delete('created_by')
-               if !fpcomp(lat, node.lat) or !fpcomp(lon, node.lon) or
-                  n[4] != nodetags or !node.visible?
-                 savenode = true
-               end
-         end
-         if savenode
-               node.user_id = uid
-           node.lat = lat
+     user = getuser(usertoken)
+     if !user then return -1,"You are not logged in, so the way could not be saved." end
+     if pointlist.length < 2 then return -2,"Server error - way is only #{points.length} points long." end
+     originalway = originalway.to_i
+       pointlist.collect! {|a| a.to_i }
+     way=nil   # this is returned, so scope it outside the transaction
+     nodeversions = {}
+     Way.transaction do
+       # -- Get unique nodes
+       if originalway <= 0
+         uniques = []
+       else
+         way = Way.find(originalway)
+         uniques = way.unshared_node_ids
+       end
+       #-- Update each changed node
+       nodes.each do |a|
+         lon = a[0].to_f
+         lat = a[1].to_f
+         id = a[2].to_i
+         version = a[3].to_i
+         if id == 0  then return -2,"Server error - node with id 0 found in way #{originalway}." end
+         if lat== 90 then return -2,"Server error - node with latitude -90 found in way #{originalway}." end
+         if renumberednodes[id] then id = renumberednodes[id] end
+         node = Node.new
+         node.changeset_id = changeset_id
+         node.lat = lat
          node.lon = lon
-           node.tags = Tags.join(n[4])
-           node.visible = true
-           node.save_with_history!
-               if id != node.id
-                 renumberednodes[id] = node.id
-                 id = node.id
-           end
-         end
-         uniques = uniques - [id]
-         nodes.push(id)
-       end
-       # -- Save revised way
-       way.tags = attributes
-       way.nds = nodes
-       way.user_id = uid
-       way.visible = true
-       way.save_with_history!
-       # -- Delete any unique nodes
-       
-       uniques.each do |n|
-         deleteitemrelations(n, 'node')
-         node = Node.find(n)
-         node.user_id = uid
-         node.visible = false
-         node.save_with_history!
-       end
-       [0, originalway, way.id, renumberednodes]
+         node.tags = a[4]
+         node.tags.delete('created_by')
+         node.version = version
+         if id <= 0
+           # We're creating the node
+           node.create_with_history(user)
+           renumberednodes[id] = node.id
+           nodeversions[node.id] = node.version
+         else
+           # We're updating an existing node
+           previous=Node.find(id)
+           previous.update_from(node, user)
+           nodeversions[previous.id] = previous.version
+         end
+       end
+       # -- Save revised way
+         pointlist.collect! {|a|
+               renumberednodes[a] ? renumberednodes[a]:a
+         } # renumber nodes
+       new_way = Way.new
+       new_way.tags = attributes
+       new_way.nds = pointlist
+       new_way.changeset_id = changeset_id
+       new_way.version = wayversion
+       if originalway <= 0
+         new_way.create_with_history(user)
+         way=new_way   # so we can get way.id and way.version
+       elsif way.tags!=attributes or way.nds!=pointlist or !way.visible?
+         way.update_from(new_way, user)
+       end
+       # -- Delete any unique nodes no longer used
+       uniques=uniques-pointlist
+       uniques.each do |n|
+         node = Node.find(n)
+         deleteitemrelations(user, changeset_id, id, 'node', node.version)
+         new_node = Node.new
+         new_node.changeset_id = changeset_id
+         new_node.version = node.version
+         node.delete_with_history!(new_node, user)
+       end
+     end # transaction
+     [0, originalway, way.id, renumberednodes, way.version, nodeversions]
+   rescue OSM::APIChangesetAlreadyClosedError => ex
+     return [-2, "Sorry, your changeset #{ex.changeset.id} has been closed (at #{ex.changeset.closed_at})."]
+   rescue OSM::APIVersionMismatchError => ex
+     # Really need to check to see whether this is a server load issue, and the 
+     # last version was in the same changeset, or belongs to the same user, then
+     # we can return something different
+     return [-3, "Sorry, someone else has changed this way since you started editing. Please click the 'Edit' tab to reload the area."]
+   rescue OSM::APITooManyWayNodesError => ex
+     return [-1, "You have tried to upload a really long way with #{ex.provided} points: only #{ex.max} are allowed."]
+   rescue OSM::APIAlreadyDeletedError => ex
+     return [-1, "The point has already been deleted."]
+   rescue OSM::APIError => ex
+     # Some error that we don't specifically catch
+     return [-2, "Something really bad happened :-(."]
    end
  
    # Save POI to the database.
    # Refuses save if the node has since become part of a way.
-   # Returns:
+   # Returns array with:
    # 0. 0 (success),
    # 1. original node id (unchanged),
-   # 2. new node id.
-   def putpoi(usertoken, id, lon, lat, tags, visible) #:doc:
-       uid = getuserid(usertoken)
-       if !uid then return -1,"You are not logged in, so the point could not be saved." end
-       id = id.to_i
-       visible = (visible.to_i == 1)
-       if id > 0 then
-         node = Node.find(id)
-         if !visible then
-           unless node.ways.empty? then return -1,"The point has since become part of a way, so you cannot save it as a POI." end
-           deleteitemrelations(id, 'node')
-         end
-       else
-         node = Node.new
-       end
-       node.user_id = uid
-       node.lat = lat
-       node.lon = lon
-       node.tags = Tags.join(tags)
-       node.visible = visible
-       node.save_with_history!
-       [0, id, node.id]
+   # 2. new node id,
+   # 3. version.
+   def putpoi(usertoken, changeset_id, version, id, lon, lat, tags, visible) #:doc:
+     user = getuser(usertoken)
+     if !user then return -1,"You are not logged in, so the point could not be saved." end
+     id = id.to_i
+     visible = (visible.to_i == 1)
+     node = nil
+     new_node = nil
+     Node.transaction do
+       if id > 0 then
+         node = Node.find(id)
+         if !visible then
+           unless node.ways.empty? then return -1,"The point has since become part of a way, so you cannot save it as a POI." end
+         end
+       end
+       # We always need a new node, based on the data that has been sent to us
+       new_node = Node.new
+       new_node.changeset_id = changeset_id
+       new_node.version = version
+       new_node.lat = lat
+       new_node.lon = lon
+       new_node.tags = tags
+       if id <= 0 
+         # We're creating the node
+         new_node.create_with_history(user)
+       elsif visible
+         # We're updating the node
+         node.update_from(new_node, user)
+       else
+         # We're deleting the node
+         node.delete_with_history!(new_node, user)
+       end
+      end # transaction
+     if id <= 0
+       return [0, id, new_node.id, new_node.version]
+     else
+       return [0, id, node.id, node.version]
+     end 
+   rescue OSM::APIChangesetAlreadyClosedError => ex
+     return [-1, "The changeset #{ex.changeset.id} was closed at #{ex.changeset.closed_at}"]
+   rescue OSM::APIVersionMismatchError => ex
+     # Really need to check to see whether this is a server load issue, and the 
+     # last version was in the same changeset, or belongs to the same user, then
+     # we can return something different
+     return [-3, "Sorry, someone else has changed this point since you started editing. Please click the 'Edit' tab to reload the area."]
+   rescue OSM::APIAlreadyDeletedError => ex
+     return [-1, "The point has already been deleted"]
+   rescue OSM::APIError => ex
+     # Some error that we don't specifically catch
+     return [-2, "Something really bad happened :-()"]
    end
  
    # Read POI from database
    # (only called on revert: POIs are usually read by whichways).
    #
-   # Returns array of id, long, lat, hash of tags.
+   # Returns array of id, long, lat, hash of tags, version.
  
    def getpoi(id,timestamp) #:doc:
-       if timestamp>0 then
-         n = OldNode.find(id, :conditions=>['UNIX_TIMESTAMP(timestamp)=?',timestamp])
-       else
-         n = Node.find(id)
-       end
-       if n
-         return [n.id, n.lon, n.lat, n.tags_as_hash]
-       else
-         return [nil, nil, nil, '']
-       end
+     if timestamp == '' then
+       n = Node.find(id)
+     else
+       n = OldNode.find(id, :conditions=>['timestamp=?',DateTime.strptime(timestamp, "%d %b %Y, %H:%M:%S")])
+     end
+     if n
+       return [n.id, n.lon, n.lat, n.tags, n.version]
+     else
+       return [nil, nil, nil, {}, nil]
+     end
    end
  
    # Delete way and all constituent nodes. Also removes from any relations.
+   # Params:
+   # * The user token
+   # * the changeset id
+   # * the id of the way to change
+   # * the version of the way that was downloaded
+   # * a hash of the id and versions of all the nodes that are in the way, if any 
+   # of the nodes have been changed by someone else then, there is a problem!
    # Returns 0 (success), unchanged way id.
  
-   def deleteway(usertoken, way_id) #:doc:
-       uid = getuserid(usertoken)
-       if !uid then return -1,"You are not logged in, so the way could not be deleted." end
-       # FIXME: would be good not to make two history entries when removing
-       #                two nodes from the same relation
-       user = User.find(uid)
-       way = Way.find(way_id)
-       way.unshared_node_ids.each do |n|
-         deleteitemrelations(n, 'node')
-       end
-       deleteitemrelations(way_id, 'way')
-       way.delete_with_relations_and_nodes_and_history(user)  
-       [0, way_id]
+   def deleteway(usertoken, changeset_id, way_id, way_version, node_id_version) #:doc:
+     user = getuser(usertoken)
+     unless user then return -1,"You are not logged in, so the way could not be deleted." end
+       
+     way_id = way_id.to_i
+     # Need a transaction so that if one item fails to delete, the whole delete fails.
+     Way.transaction do
+       # delete the way
+       old_way = Way.find(way_id)
+       delete_way = Way.new
+       delete_way.version = way_version
+       delete_way.changeset_id = changeset_id
+       old_way.delete_with_history!(delete_way, user)
+       old_way.unshared_node_ids.each do |node_id|
+         # delete the node
+         node = Node.find(node_id)
+         delete_node = Node.new
+         delete_node.changeset_id = changeset_id
+         if node_id_version[node_id.to_s]
+           delete_node.version = node_id_version[node_id.to_s]
+         else
+           # in case the node wasn't passed (i.e. if it was previously removed
+           # from the way in Potlatch)
+           deleteitemrelations(user, changeset_id, node_id, 'node', node.version)
+             delete_node.version = node.version
+           end
+         node.delete_with_history!(delete_node, user)
+       end
+     end # transaction
+     [0, way_id]
+   rescue OSM::APIChangesetAlreadyClosedError => ex
+     return [-1, "The changeset #{ex.changeset.id} was closed at #{ex.changeset.closed_at}"]
+   rescue OSM::APIVersionMismatchError => ex
+     # Really need to check to see whether this is a server load issue, and the 
+     # last version was in the same changeset, or belongs to the same user, then
+     # we can return something different
+     return [-3, "Sorry, someone else has changed this way since you started editing. Please click the 'Edit' tab to reload the area."]
+   rescue OSM::APIAlreadyDeletedError => ex
+     return [-1, "The way has already been deleted."]
+   rescue OSM::APIError => ex
+     # Some error that we don't specifically catch
+     return [-2, "Something really bad happened :-( ."]
    end
  
  
    # Support functions
  
    # Remove a node or way from all relations
+   # This is only used by putway and deleteway when deleting nodes removed 
+   # from a way (because Potlatch itself doesn't keep track of these - 
+   # possible FIXME).
  
-   def deleteitemrelations(objid, type) #:doc:
-       relations = RelationMember.find(:all, 
+   def deleteitemrelations(user, changeset_id, objid, type, version) #:doc:
+     relations = RelationMember.find(:all, 
                                                                        :conditions => ['member_type = ? and member_id = ?', type, objid], 
                                                                        :include => :relation).collect { |rm| rm.relation }.uniq
  
-       relations.each do |rel|
-         rel.members.delete_if { |x| x[0] == type and x[1] == objid }
-         rel.save_with_history!
-       end
-   end
-   # Break out node tags into a hash
-   # (should become obsolete as of API 0.6)
-   def tagstring_to_hash(a) #:doc:
-       tags={}
-       Tags.split(a) do |k, v|
-         tags[k]=v
-       end
-       tags
+     relations.each do |rel|
+       rel.members.delete_if { |x| x[0] == type and x[1] == objid }
+       new_rel = Relation.new
+       new_rel.tags = rel.tags
+       new_rel.visible = rel.visible
+       new_rel.version = rel.version
+       new_rel.members = rel.members
+       new_rel.changeset_id = changeset_id
+       rel.update_from(new_rel, user)
+     end
    end
  
    # Authenticate token
    # (can also be of form user:pass)
-   def getuserid(token) #:doc:
-       if (token =~ /^(.+)\:(.+)$/) then
-         user = User.authenticate(:username => $1, :password => $2)
-       else
-         user = User.authenticate(:token => token)
-       end
-       return user ? user.id : nil;
-   end
-   # Compare two floating-point numbers to within 0.0000001
-   def fpcomp(a,b) #:doc:
-       return ((a/0.0000001).round==(b/0.0000001).round)
+   # When we are writing to the api, we need the actual user model, 
+   # not just the id, hence this abstraction
+   def getuser(token) #:doc:
+     if (token =~ /^(.+)\:(.+)$/) then
+       user = User.authenticate(:username => $1, :password => $2)
+     else
+       user = User.authenticate(:token => token)
+     end
+     return user
    end
  
    # Send AMF response
    
    def sendresponse(results)
-       a,b=results.length.divmod(256)
-       render :content_type => "application/x-amf", :text => proc { |response, output| 
-         output.write 0.chr+0.chr+0.chr+0.chr+a.chr+b.chr
-         results.each do |k,v|
-               output.write(v)
-         end
-       }
+     a,b=results.length.divmod(256)
+     render :content_type => "application/x-amf", :text => proc { |response, output| 
+       # ** move amf writing loop into here - 
+       # basically we read the messages in first (into an array of some sort),
+       # then iterate through that array within here, and do all the AMF writing
+       output.write 0.chr+0.chr+0.chr+0.chr+a.chr+b.chr
+       results.each do |k,v|
+         output.write(v)
+       end
+     }
    end
  
  
    # ====================================================================
    # Alternative SQL queries for getway/whichways
  
-   def sql_find_way_ids_in_area(xmin,ymin,xmax,ymax)
-       sql=<<-EOF
-   SELECT DISTINCT current_way_nodes.id AS wayid
-               FROM current_way_nodes
-   INNER JOIN current_nodes ON current_nodes.id=current_way_nodes.node_id
-   INNER JOIN current_ways  ON current_ways.id =current_way_nodes.id
-          WHERE current_nodes.visible=1 
-                AND current_ways.visible=1 
-                AND #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")}
-       EOF
-       return ActiveRecord::Base.connection.select_all(sql).collect { |a| a['wayid'].to_i }
+   def sql_find_ways_in_area(xmin,ymin,xmax,ymax)
+     sql=<<-EOF
+     SELECT DISTINCT current_ways.id AS wayid,current_ways.version AS version
+       FROM current_way_nodes
+     INNER JOIN current_nodes ON current_nodes.id=current_way_nodes.node_id
+     INNER JOIN current_ways  ON current_ways.id =current_way_nodes.id
+        WHERE current_nodes.visible=TRUE 
+        AND current_ways.visible=TRUE 
+        AND #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")}
+     EOF
+     return ActiveRecord::Base.connection.select_all(sql).collect { |a| [a['wayid'].to_i,a['version'].to_i] }
    end
        
    def sql_find_pois_in_area(xmin,ymin,xmax,ymax)
-       sql=<<-EOF
-                 SELECT current_nodes.id,current_nodes.latitude*0.0000001 AS lat,current_nodes.longitude*0.0000001 AS lon,current_nodes.tags 
+     pois=[]
+     sql=<<-EOF
+                 SELECT current_nodes.id,current_nodes.latitude*0.0000001 AS lat,current_nodes.longitude*0.0000001 AS lon,current_nodes.version 
                        FROM current_nodes 
-  LEFT OUTER JOIN current_way_nodes cwn ON cwn.node_id=current_nodes.id 
-                  WHERE current_nodes.visible=1
+        LEFT OUTER JOIN current_way_nodes cwn ON cwn.node_id=current_nodes.id 
+                  WHERE current_nodes.visible=TRUE
                         AND cwn.id IS NULL
                         AND #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")}
-       EOF
-       return ActiveRecord::Base.connection.select_all(sql).collect { |n| [n['id'].to_i,n['lon'].to_f,n['lat'].to_f,tagstring_to_hash(n['tags'])] }
+     EOF
+     ActiveRecord::Base.connection.select_all(sql).each do |row|
+       poitags={}
+       ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_node_tags WHERE id=#{row['id']}").each do |n|
+         poitags[n['k']]=n['v']
+       end
+       pois << [row['id'].to_i, row['lon'].to_f, row['lat'].to_f, poitags, row['version'].to_i]
+     end
+     pois
    end
        
    def sql_find_relations_in_area_and_ways(xmin,ymin,xmax,ymax,way_ids)
-       # ** It would be more Potlatchy to get relations for nodes within ways
-       #    during 'getway', not here
-       sql=<<-EOF
-         SELECT DISTINCT cr.id AS relid 
-               FROM current_relations cr
-   INNER JOIN current_relation_members crm ON crm.id=cr.id 
-   INNER JOIN current_nodes cn ON crm.member_id=cn.id AND crm.member_type='node' 
-          WHERE #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "cn.")}
-       EOF
-       unless way_ids.empty?
-         sql+=<<-EOF
-          UNION
-         SELECT DISTINCT cr.id AS relid
-               FROM current_relations cr
-   INNER JOIN current_relation_members crm ON crm.id=cr.id
-          WHERE crm.member_type='way' 
-                AND crm.member_id IN (#{way_ids.join(',')})
-         EOF
-       end
-       return ActiveRecord::Base.connection.select_all(sql).collect { |a| a['relid'].to_i }.uniq
+     # ** It would be more Potlatchy to get relations for nodes within ways
+     #    during 'getway', not here
+     sql=<<-EOF
+       SELECT DISTINCT cr.id AS relid,cr.version AS version 
+       FROM current_relations cr
+       INNER JOIN current_relation_members crm ON crm.id=cr.id 
+       INNER JOIN current_nodes cn ON crm.member_id=cn.id AND crm.member_type='node' 
+        WHERE #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "cn.")}
+       EOF
+     unless way_ids.empty?
+       sql+=<<-EOF
+        UNION
+         SELECT DISTINCT cr.id AS relid,cr.version AS version
+         FROM current_relations cr
+         INNER JOIN current_relation_members crm ON crm.id=cr.id
+          WHERE crm.member_type='way' 
+          AND crm.member_id IN (#{way_ids.join(',')})
+         EOF
+     end
+     return ActiveRecord::Base.connection.select_all(sql).collect { |a| [a['relid'].to_i,a['version'].to_i] }
    end
        
    def sql_get_nodes_in_way(wayid)
-       points=[]
-       sql=<<-EOF
-               SELECT latitude*0.0000001 AS lat,longitude*0.0000001 AS lon,current_nodes.id,tags 
-                 FROM current_way_nodes,current_nodes 
-                WHERE current_way_nodes.id=#{wayid.to_i} 
+     points=[]
+     sql=<<-EOF
+       SELECT latitude*0.0000001 AS lat,longitude*0.0000001 AS lon,current_nodes.id 
+       FROM current_way_nodes,current_nodes 
+        WHERE current_way_nodes.id=#{wayid.to_i} 
                   AND current_way_nodes.node_id=current_nodes.id 
-                  AND current_nodes.visible=1
-         ORDER BY sequence_id
+                  AND current_nodes.visible=TRUE
+       ORDER BY sequence_id
          EOF
-       ActiveRecord::Base.connection.select_all(sql).each do |row|
-         nodetags=tagstring_to_hash(row['tags'])
-         nodetags.delete('created_by')
-         points << [row['lon'].to_f,row['lat'].to_f,row['id'].to_i,nodetags]
-       end
-       points
+     ActiveRecord::Base.connection.select_all(sql).each do |row|
+       nodetags={}
+       ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_node_tags WHERE id=#{row['id']}").each do |n|
+         nodetags[n['k']]=n['v']
+       end
+       nodetags.delete('created_by')
+       points << [row['lon'].to_f,row['lat'].to_f,row['id'].to_i,nodetags]
+     end
+     points
    end
        
    def sql_get_tags_in_way(wayid)
-       tags={}
-       ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_way_tags WHERE id=#{wayid.to_i}").each do |row|
-         tags[row['k']]=row['v']
-       end
-       tags
+     tags={}
+     ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_way_tags WHERE id=#{wayid.to_i}").each do |row|
+       tags[row['k']]=row['v']
+     end
+     tags
    end
  
+   def sql_get_way_version(wayid)
+     ActiveRecord::Base.connection.select_one("SELECT version FROM current_ways WHERE id=#{wayid.to_i}")
+   end
  end
  
- # Local Variables:
- # indent-tabs-mode: t
- # tab-width: 4
- # End:
index a773d4b72d2c177cadad071b0de2a77777f3f8f0,754cc4b8216b15acd900d3646cdb399c31d6faba..ab25fcbc67a2ed407955953287d3550c99c46812
@@@ -2,18 -2,24 +2,24 @@@ class ExportController < ApplicationCon
    def start
    end
  
+   #When the user clicks 'Export' we redirect to a URL which generates the export download
    def finish
      bbox = BoundingBox.new(params[:minlon], params[:minlat], params[:maxlon], params[:maxlat])
      format = params[:format]
  
      if format == "osm"
+       #redirect to API map get
        redirect_to "http://api.openstreetmap.org/api/#{API_VERSION}/map?bbox=#{bbox}"
 -      
++
      elsif format == "mapnik"
+       #redirect to a special 'export' cgi script
        format = params[:mapnik_format]
        scale = params[:mapnik_scale]
  
        redirect_to "http://tile.openstreetmap.org/cgi-bin/export?bbox=#{bbox}&scale=#{scale}&format=#{format}"
 -      
++
      elsif format == "osmarender"
+       #redirect to the t@h 'MapOf' service
        format = params[:osmarender_format]
        zoom = params[:osmarender_zoom].to_i
        width = bbox.slippy_width(zoom).to_i
index 438de836d463be146c4364213471b9674fb1244c,a62be6a55d94ad9a9a35a0b861dc78f6cae71b13..c76ca23ae620055bdbe1db9aca8569bfaf371468
@@@ -2,13 -2,17 +2,17 @@@
  <h2><%= h(@this_user.display_name) %></h2>
  <div id="userinformation">
  <% if @user and @this_user.id == @user.id %>
+ <!-- Displaying user's own profile page -->
  <%= link_to 'my diary', :controller => 'diary_entry', :action => 'list', :display_name => @user.display_name %>
  | <%= link_to 'new diary entry', :controller => 'diary_entry', :action => 'new', :display_name => @user.display_name %>
+ | <%= link_to 'my edits', :controller => 'changeset', :action => 'list_user', :display_name => @user.display_name %>
  | <%= link_to 'my traces', :controller => 'trace', :action=>'mine' %>
  | <%= link_to 'my settings', :controller => 'user', :action => 'account', :display_name => @user.display_name %>
  <% else %>
+ <!-- Displaying another user's profile page -->
  <%= link_to 'send message', :controller => 'message', :action => 'new', :user_id => @this_user.id %>
  | <%= link_to 'diary', :controller => 'diary_entry', :action => 'list', :display_name => @this_user.display_name %>
+ | <%= link_to 'edits', :controller => 'changeset', :action => 'list_user', :display_name => @this_user.display_name %>
  | <%= link_to 'traces', :controller => 'trace', :action => 'view', :display_name => @this_user.display_name %>
  | <% if @user and @user.is_friends_with?(@this_user) %>
    <%= link_to 'remove as friend', :controller => 'user', :action => 'remove_friend', :display_name => @this_user.display_name %>
  <% end %>
  </div>
  
+ <% if @this_user != nil %>
+ <P>
+ <b>Mapper since : </b><%= @this_user.creation_time %> (<%= time_ago_in_words(@this_user.creation_time) %> ago)
+ </P>
+ <% end %>
+   
  <h3>User image</h3>
  <% if @this_user.image %>
    <%= image_tag url_for_file_column(@this_user, "image") %>
      <% end %>
    <% end %>
  <% end %>
 -<% end %>
+ <br/>
+ <br/>
+ <% if @user and @this_user.id == @user.id %>
+ <%= link_to 'change your settings', :controller => 'user', :action => 'account', :display_name => @user.display_name %>
++<% end %>