Merge 14394:14533 from trunk.
authorTom Hughes <tom@compton.nu>
Thu, 16 Apr 2009 21:11:12 +0000 (21:11 +0000)
committerTom Hughes <tom@compton.nu>
Thu, 16 Apr 2009 21:11:12 +0000 (21:11 +0000)
16 files changed:
1  2 
app/controllers/amf_controller.rb
app/controllers/api_controller.rb
app/controllers/application.rb
app/controllers/browse_controller.rb
app/controllers/diary_entry_controller.rb
app/controllers/message_controller.rb
app/controllers/node_controller.rb
app/controllers/old_node_controller.rb
app/controllers/old_way_controller.rb
app/controllers/relation_controller.rb
app/controllers/trace_controller.rb
app/controllers/user_controller.rb
app/controllers/way_controller.rb
app/views/layouts/site.rhtml
app/views/site/edit.rhtml
config/environment.rb

index 1ee5e9cee2b14beacc1552c409694583dd1075c4,7f85280b79d29d5b320460d01f028f14727a3fd0..de3c7583b42d9977f255a581afbc5fe9265121b2
@@@ -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_write_availability
+   before_filter :check_api_writable
  
    # Main AMF handlers: process the raw AMF string (using AMF library) and
    # calls each action (private method) accordingly.
    # ** 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
 -
 -      # Parse request
 -
 -      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
 +    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
 +
 +    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 '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 f0ba325682c567f400c94d6bd0d398f1f9b9f804,029e4c8a6544353845213575e57243c49e67f5b4..ebf729afc322812002231adeeefe6e486a756367
@@@ -1,7 -1,7 +1,7 @@@
  class ApiController < ApplicationController
  
    session :off
-   before_filter :check_read_availability, :except => [:capabilities]
+   before_filter :check_api_readable, :except => [:capabilities]
    after_filter :compress_output
  
    # Help methods for checking boundary sanity and area size
    @@count = COUNT
  
    # The maximum area you're allowed to request, in square degrees
 -  MAX_REQUEST_AREA = 0.25
 +  MAX_REQUEST_AREA = APP_CONFIG['max_request_area']
  
    # Number of GPS trace/trackpoints returned per-page
 -  TRACEPOINTS_PER_PAGE = 5000
 +  TRACEPOINTS_PER_PAGE = APP_CONFIG['tracepoints_per_page']
  
 -  
 +  # Get an XML response containing a list of tracepoints that have been uploaded
 +  # within the specified bounding box, and in the specified page.
    def trackpoints
      @@count+=1
      #retrieve the page number
      render :text => doc.to_s, :content_type => "text/xml"
    end
  
 +  # This is probably the most common call of all. It is used for getting the 
 +  # OSM data for a specified bounding box, usually for editing. First the
 +  # bounding box (bbox) is checked to make sure that it is sane. All nodes 
 +  # are searched, then all the ways that reference those nodes are found.
 +  # All Nodes that are referenced by those ways are fetched and added to the list
 +  # of nodes.
 +  # Then all the relations that reference the already found nodes and ways are
 +  # fetched. All the nodes and ways that are referenced by those ways are then 
 +  # fetched. Finally all the xml is returned.
    def map
      GC.start
      @@count+=1
        return
      end
  
 -    @nodes = Node.find_by_area(min_lat, min_lon, max_lat, max_lon, :conditions => "visible = 1", :limit => APP_CONFIG['max_number_of_nodes']+1)
 +    # FIXME um why is this area using a different order for the lat/lon from above???
 +    @nodes = Node.find_by_area(min_lat, min_lon, max_lat, max_lon, :conditions => {:visible => true}, :limit => APP_CONFIG['max_number_of_nodes']+1)
      # get all the nodes, by tag not yet working, waiting for change from NickB
      # need to be @nodes (instance var) so tests in /spec can be performed
      #@nodes = Node.search(bbox, params[:tag])
  
      node_ids = @nodes.collect(&:id)
      if node_ids.length > APP_CONFIG['max_number_of_nodes']
 -      report_error("You requested too many nodes (limit is 50,000). Either request a smaller area, or use planet.osm")
 +      report_error("You requested too many nodes (limit is #{APP_CONFIG['max_number_of_nodes']}). Either request a smaller area, or use planet.osm")
        return
      end
      if node_ids.length == 0
 -      render :text => "<osm version='0.5'></osm>", :content_type => "text/xml"
 +      render :text => "<osm version='#{API_VERSION}' generator='#{GENERATOR}'></osm>", :content_type => "text/xml"
        return
      end
  
        end
      end 
  
 -    relations = Relation.find_for_nodes(visible_nodes.keys, :conditions => "visible = 1") +
 -                Relation.find_for_ways(way_ids, :conditions => "visible = 1")
 +    relations = Relation.find_for_nodes(visible_nodes.keys, :conditions => {:visible => true}) +
 +                Relation.find_for_ways(way_ids, :conditions => {:visible => true})
  
      # we do not normally return the "other" partners referenced by an relation, 
      # e.g. if we return a way A that is referenced by relation X, and there's 
      # another way B also referenced, that is not returned. But we do make 
      # an exception for cases where an relation references another *relation*; 
      # in that case we return that as well (but we don't go recursive here)
 -    relations += Relation.find_for_relations(relations.collect { |r| r.id }, :conditions => "visible = 1")
 +    relations += Relation.find_for_relations(relations.collect { |r| r.id }, :conditions => {:visible => true})
  
      # this "uniq" may be slightly inefficient; it may be better to first collect and output
      # all node-related relations, then find the *not yet covered* way-related ones etc.
      end
    end
  
 +  # Get a list of the tiles that have changed within a specified time
 +  # period
    def changes
      zoom = (params[:zoom] || '12').to_i
  
        endtime = Time.parse(params[:end])
      else
        hours = (params[:hours] || '1').to_i.hours
 -      endtime = Time.now
 +      endtime = Time.now.getutc
        starttime = endtime - hours
      end
  
      if zoom >= 1 and zoom <= 16 and
 -       endtime >= starttime and endtime - starttime <= 24.hours
 +       endtime > starttime and endtime - starttime <= 24.hours
        mask = (1 << zoom) - 1
  
        tiles = Node.count(:conditions => ["timestamp BETWEEN ? AND ?", starttime, endtime],
  
        render :text => doc.to_s, :content_type => "text/xml"
      else
 -      render :nothing => true, :status => :bad_request
 +      render :text => "Requested zoom is invalid, or the supplied start is after the end time, or the start duration is more than 24 hours", :status => :bad_request
      end
    end
  
 +  # External apps that use the api are able to query the api to find out some 
 +  # parameters of the API. It currently returns: 
 +  # * minimum and maximum API versions that can be used.
 +  # * maximum area that can be requested in a bbox request in square degrees
 +  # * number of tracepoints that are returned in each tracepoints page
    def capabilities
      doc = OSM::API.new.get_xml_doc
  
      api = XML::Node.new 'api'
      version = XML::Node.new 'version'
 -    version['minimum'] = '0.5';
 -    version['maximum'] = '0.5';
 +    version['minimum'] = "#{API_VERSION}";
 +    version['maximum'] = "#{API_VERSION}";
      api << version
      area = XML::Node.new 'area'
      area['maximum'] = MAX_REQUEST_AREA.to_s;
      api << area
 +    tracepoints = XML::Node.new 'tracepoints'
 +    tracepoints['per_page'] = APP_CONFIG['tracepoints_per_page'].to_s
 +    api << tracepoints
 +    waynodes = XML::Node.new 'waynodes'
 +    waynodes['maximum'] = APP_CONFIG['max_number_of_way_nodes'].to_s
 +    api << waynodes
      
      doc.root << api
  
index f5ea0063db38b76c7d9cdb74b536b1c8f25cd182,6150226569365df57a19a7e261e328ed2acb7879..21f691bb344c85036cf3681f00af222b3a8c1c2b
@@@ -2,13 -2,13 +2,13 @@@
  # Likewise, all the methods added will be available for all controllers.
  class ApplicationController < ActionController::Base
  
-   if OSM_STATUS == :database_offline
+   if OSM_STATUS == :database_readonly or OSM_STATUS == :database_offline
      session :off
    end
  
    def authorize_web
      if session[:user]
 -      @user = User.find(session[:user], :conditions => "visible = 1")
 +      @user = User.find(session[:user], :conditions => {:visible => true})
      elsif session[:token]
        @user = User.authenticate(:token => session[:token])
        session[:user] = @user.id
      redirect_to :controller => 'user', :action => 'login', :referer => request.request_uri unless @user
    end
  
 -  def authorize(realm='Web Password', errormessage="Couldn't authenticate you") 
 +  ##
 +  # sets up the @user object for use by other methods. this is mostly called
 +  # from the authorize method, but can be called elsewhere if authorisation
 +  # is optional.
 +  def setup_user_auth
      username, passwd = get_auth_data # parse from headers
      # authenticate per-scheme
      if username.nil?
      else
        @user = User.authenticate(:username => username, :password => passwd) # basic auth
      end
 +  end
 +
 +  def authorize(realm='Web Password', errormessage="Couldn't authenticate you") 
 +    # make the @user object from any auth sources we have
 +    setup_user_auth
  
      # handle authenticate pass/fail
      unless @user
      end 
    end 
  
-   def check_database_availability(need_api = false)
+   def check_database_readable(need_api = false)
      if OSM_STATUS == :database_offline or (need_api and OSM_STATUS == :api_offline)
        redirect_to :controller => 'site', :action => 'offline'
      end
    end
  
-   def check_read_availability
+   def check_database_writable(need_api = false)
+     if OSM_STATUS == :database_offline or OSM_STATUS == :database_readonly or
+        (need_api and (OSM_STATUS == :api_offline or OSM_STATUS == :api_readonly))
+       redirect_to :controller => 'site', :action => 'offline'
+     end
+   end
+   def check_api_readable
      if OSM_STATUS == :database_offline or OSM_STATUS == :api_offline
        response.headers['Error'] = "Database offline for maintenance"
        render :nothing => true, :status => :service_unavailable
@@@ -66,8 -64,9 +73,9 @@@
      end
    end
  
-   def check_write_availability
-     if OSM_STATUS == :database_offline or OSM_STATUS == :api_offline or OSM_STATUS == :api_readonly
+   def check_api_writable
+     if OSM_STATUS == :database_offline or OSM_STATUS == :database_readonly or
+        OSM_STATUS == :api_offline or OSM_STATUS == :api_readonly
        response.headers['Error'] = "Database offline for maintenance"
        render :nothing => true, :status => :service_unavailable
        return false
@@@ -80,7 -79,7 +88,7 @@@
    #  phrase from that, we can also put the error message into the status
    #  message. For now, rails won't let us)
    def report_error(message)
 -    render :nothing => true, :status => :bad_request
 +    render :text => message, :status => :bad_request
      # Todo: some sort of escaping of problem characters in the message
      response.headers['Error'] = message
    end
@@@ -91,8 -90,6 +99,8 @@@ privat
    def get_auth_data 
      if request.env.has_key? 'X-HTTP_AUTHORIZATION'          # where mod_rewrite might have put it 
        authdata = request.env['X-HTTP_AUTHORIZATION'].to_s.split 
 +    elsif request.env.has_key? 'REDIRECT_X_HTTP_AUTHORIZATION'          # mod_fcgi 
 +      authdata = request.env['REDIRECT_X_HTTP_AUTHORIZATION'].to_s.split 
      elsif request.env.has_key? 'HTTP_AUTHORIZATION'         # regular location
        authdata = request.env['HTTP_AUTHORIZATION'].to_s.split
      end 
index 465184f5032c59c1a697bf5a2a4d186a4410f1f6,df48a209044b32891958adb1df72a65bf60bcf52..6ace0817b01353a7f1f4a270354b9075c05e5e27
@@@ -2,114 -2,108 +2,114 @@@ class BrowseController < ApplicationCon
    layout 'site'
  
    before_filter :authorize_web  
-   before_filter { |c| c.check_database_availability(true) }
+   before_filter { |c| c.check_database_readable(true) }
  
    def start 
    end
    
 -  def index
 -    @nodes = Node.find(:all, :order => "timestamp DESC", :limit=> 20)  
 -  end
 +  
    
    def relation 
 -    begin
 -      @relation = Relation.find(params[:id])
 -     
 -      @name = @relation.tags['name'].to_s 
 -      if @name.length == 0:
 -      @name = "#" + @relation.id.to_s
 -      end
 -      
 -      @title = 'Relation | ' + (@name)
 -      @next = Relation.find(:first, :order => "id ASC", :conditions => [ "visible = true AND id > :id", { :id => @relation.id }] ) 
 -      @prev = Relation.find(:first, :order => "id DESC", :conditions => [ "visible = true AND id < :id", { :id => @relation.id }] ) 
 -    rescue ActiveRecord::RecordNotFound
 -      render :nothing => true, :status => :not_found
 +    @relation = Relation.find(params[:id])
 +   
 +    @name = @relation.tags['name'].to_s 
 +    if @name.length == 0:
 +      @name = "#" + @relation.id.to_s
      end
 +      
 +    @title = 'Relation | ' + (@name)
 +    @next = Relation.find(:first, :order => "id ASC", :conditions => [ "visible = true AND id > :id", { :id => @relation.id }] ) 
 +    @prev = Relation.find(:first, :order => "id DESC", :conditions => [ "visible = true AND id < :id", { :id => @relation.id }] ) 
 +  rescue ActiveRecord::RecordNotFound
 +    @type = "relation"
 +    render :action => "not_found", :status => :not_found
    end
    
    def relation_history
 -    begin
 -      @relation = Relation.find(params[:id])
 +    @relation = Relation.find(params[:id])
       
 -      @name = @relation.tags['name'].to_s 
 -      if @name.length == 0:
 -      @name = "#" + @relation.id.to_s
 -      end
 -      
 -      @title = 'Relation History | ' + (@name)
 -    rescue ActiveRecord::RecordNotFound
 -      render :nothing => true, :status => :not_found
 +    @name = @relation.tags['name'].to_s 
 +    if @name.length == 0:
 +      @name = "#" + @relation.id.to_s
      end
 +      
 +    @title = 'Relation History | ' + (@name)
 +  rescue ActiveRecord::RecordNotFound
 +    @type = "relation"
 +    render :action => "not_found", :status => :not_found
    end
    
    def way 
 -    begin
 -      @way = Way.find(params[:id])
 +    @way = Way.find(params[:id])
       
 -      @name = @way.tags['name'].to_s 
 -      if @name.length == 0:
 -      @name = "#" + @way.id.to_s
 -      end
 -      
 -      @title = 'Way | ' + (@name)
 -      @next = Way.find(:first, :order => "id ASC", :conditions => [ "visible = true AND id > :id", { :id => @way.id }] ) 
 -      @prev = Way.find(:first, :order => "id DESC", :conditions => [ "visible = true AND id < :id", { :id => @way.id }] ) 
 -    rescue ActiveRecord::RecordNotFound
 -      render :nothing => true, :status => :not_found
 +    @name = @way.tags['name'].to_s 
 +    if @name.length == 0:
 +      @name = "#" + @way.id.to_s
      end
 +      
 +    @title = 'Way | ' + (@name)
 +    @next = Way.find(:first, :order => "id ASC", :conditions => [ "visible = true AND id > :id", { :id => @way.id }] ) 
 +    @prev = Way.find(:first, :order => "id DESC", :conditions => [ "visible = true AND id < :id", { :id => @way.id }] ) 
 +  rescue ActiveRecord::RecordNotFound
 +    @type = "way"
 +    render :action => "not_found", :status => :not_found
    end
    
    def way_history 
 -    begin
 -      @way = Way.find(params[:id])
 +    @way = Way.find(params[:id])
       
 -      @name = @way.tags['name'].to_s 
 -      if @name.length == 0:
 -      @name = "#" + @way.id.to_s
 -      end
 -      
 -      @title = 'Way History | ' + (@name)
 -    rescue ActiveRecord::RecordNotFound
 -      render :nothing => true, :status => :not_found
 +    @name = @way.tags['name'].to_s 
 +    if @name.length == 0:
 +      @name = "#" + @way.id.to_s
      end
 +      
 +    @title = 'Way History | ' + (@name)
 +  rescue ActiveRecord::RecordNotFound
 +    @type = "way"
 +    render :action => "not_found", :status => :not_found
    end
  
    def node 
 -    begin
 -      @node = Node.find(params[:id])
 +    @node = Node.find(params[:id])
       
 -      @name = @node.tags_as_hash['name'].to_s 
 -      if @name.length == 0:
 -      @name = "#" + @node.id.to_s
 -      end
 -      
 -      @title = 'Node | ' + (@name)
 -      @next = Node.find(:first, :order => "id ASC", :conditions => [ "visible = true AND id > :id", { :id => @node.id }] ) 
 -      @prev = Node.find(:first, :order => "id DESC", :conditions => [ "visible = true AND id < :id", { :id => @node.id }] ) 
 -    rescue ActiveRecord::RecordNotFound
 -      render :nothing => true, :status => :not_found
 +    @name = @node.tags_as_hash['name'].to_s 
 +    if @name.length == 0:
 +      @name = "#" + @node.id.to_s
      end
 +      
 +    @title = 'Node | ' + (@name)
 +    @next = Node.find(:first, :order => "id ASC", :conditions => [ "visible = true AND id > :id", { :id => @node.id }] ) 
 +    @prev = Node.find(:first, :order => "id DESC", :conditions => [ "visible = true AND id < :id", { :id => @node.id }] ) 
 +  rescue ActiveRecord::RecordNotFound
 +    @type = "node"
 +    render :action => "not_found", :status => :not_found
    end
    
    def node_history 
 -    begin
 -      @node = Node.find(params[:id])
 +    @node = Node.find(params[:id])
       
 -      @name = @node.tags_as_hash['name'].to_s 
 -      if @name.length == 0:
 -      @name = "#" + @node.id.to_s
 -      end
 -      
 -      @title = 'Node History | ' + (@name)
 -    rescue ActiveRecord::RecordNotFound
 -      render :nothing => true, :status => :not_found
 +    @name = @node.tags_as_hash['name'].to_s 
 +    if @name.length == 0:
 +      @name = "#" + @node.id.to_s
      end
 +      
 +    @title = 'Node History | ' + (@name)
 +  rescue ActiveRecord::RecordNotFound
 +    @type = "way"
 +    render :action => "not_found", :status => :not_found
 +  end
 +  
 +  def changeset
 +    @changeset = Changeset.find(params[:id])
 +    @node_pages, @nodes = paginate(:old_nodes, :conditions => {:changeset_id => @changeset.id}, :per_page => 20, :parameter => 'node_page')
 +    @way_pages, @ways = paginate(:old_ways, :conditions => {:changeset_id => @changeset.id}, :per_page => 20, :parameter => 'way_page')
 +    @relation_pages, @relations = paginate(:old_relations, :conditions => {:changeset_id => @changeset.id}, :per_page => 20, :parameter => 'relation_page')
 +      
 +    @title = "Changeset | #{@changeset.id}"
 +    @next = Changeset.find(:first, :order => "id ASC", :conditions => [ "id > :id", { :id => @changeset.id }] ) 
 +    @prev = Changeset.find(:first, :order => "id DESC", :conditions => [ "id < :id", { :id => @changeset.id }] ) 
 +  rescue ActiveRecord::RecordNotFound
 +    @type = "changeset"
 +    render :action => "not_found", :status => :not_found
    end
  end
index 3592ccb4fb683ce50558ca14dd76580ac9e98fde,52e2ab22bcae7bb7742be8a98bed12c797220165..3ee36af21a9fb53ac5777344ada62cc4a23bce9e
@@@ -3,7 -3,8 +3,8 @@@ class DiaryEntryController < Applicatio
  
    before_filter :authorize_web
    before_filter :require_user, :only => [:new, :edit]
-   before_filter :check_database_availability
+   before_filter :check_database_readable
+   before_filter :check_database_writable, :only => [:new, :edit]
  
    def new
      @title = 'New diary entry'
@@@ -38,8 -39,6 +39,8 @@@
          redirect_to :controller => 'diary_entry', :action => 'view', :id => params[:id]
        end
      end
 +  rescue ActiveRecord::RecordNotFound
 +    render :action => "no_such_entry", :status => :not_found
    end
  
    def comment
@@@ -56,7 -55,7 +57,7 @@@
  
    def list
      if params[:display_name]
 -      @this_user = User.find_by_display_name(params[:display_name], :conditions => "visible = 1")
 +      @this_user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
  
        if @this_user
          @title = @this_user.display_name + "'s diary"
@@@ -72,7 -71,7 +73,7 @@@
      else
        @title = "Users' diaries"
        @entry_pages, @entries = paginate(:diary_entries, :include => :user,
 -                                        :conditions => "users.visible = 1",
 +                                        :conditions => ["users.visible = ?", true],
                                          :order => 'created_at DESC',
                                          :per_page => 20)
      end
  
    def rss
      if params[:display_name]
 -      user = User.find_by_display_name(params[:display_name], :conditions => "visible = 1")
 +      user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
  
        if user
          @entries = DiaryEntry.find(:all, :conditions => ['user_id = ?', user.id], :order => 'created_at DESC', :limit => 20)
          @title = "OpenStreetMap diary entries for #{user.display_name}"
          @description = "Recent OpenStreetmap diary entries from #{user.display_name}"
 -        @link = "http://www.openstreetmap.org/user/#{user.display_name}/diary"
 +        @link = "http://#{SERVER_URL}/user/#{user.display_name}/diary"
  
          render :content_type => Mime::RSS
        else
        end
      else
        @entries = DiaryEntry.find(:all, :include => :user,
 -                                 :conditions => "users.visible = 1",
 +                                 :conditions => ["users.visible = ?", true],
                                   :order => 'created_at DESC', :limit => 20)
        @title = "OpenStreetMap diary entries"
        @description = "Recent diary entries from users of OpenStreetMap"
 -      @link = "http://www.openstreetmap.org/diary"
 +      @link = "http://#{SERVER_URL}/diary"
  
        render :content_type => Mime::RSS
      end
    end
  
    def view
 -    user = User.find_by_display_name(params[:display_name], :conditions => "visible = 1")
 +    user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
  
      if user
        @entry = DiaryEntry.find(:first, :conditions => ['user_id = ? AND id = ?', user.id, params[:id]])
 +      @title = "Users' diaries | #{params[:display_name]}"
      else
        @not_found_user = params[:display_name]
  
index a04aa82c0c7959e0b504d2a7f0da211a1fde99b7,d6e5d7fdadc4b86c326bc06217530fc76a169f48..8f866e512c388fda530304d0da6a84f793268f73
@@@ -3,19 -3,16 +3,21 @@@ class MessageController < ApplicationCo
  
    before_filter :authorize_web
    before_filter :require_user
+   before_filter :check_database_readable
+   before_filter :check_database_writable, :only => [:new, :reply, :mark]
  
 +  # Allow the user to write a new message to another user. This action also 
 +  # deals with the sending of that message to the other user when the user
 +  # clicks send.
 +  # The user_id param is the id of the user that the message is being sent to.
    def new
      @title = 'send message'
 +    @to_user = User.find(params[:user_id])
      if params[:message]
        @message = Message.new(params[:message])
 -      @message.to_user_id = params[:user_id]
 +      @message.to_user_id = @to_user.id
        @message.from_user_id = @user.id
 -      @message.sent_on = Time.now
 +      @message.sent_on = Time.now.getutc
     
        if @message.save
          flash[:notice] = 'Message sent'
      else
        @title = params[:title]
      end
 +  rescue ActiveRecord::RecordNotFound
 +    render :action => 'no_such_user', :status => :not_found
    end
  
 +  # Allow the user to reply to another message.
    def reply
      message = Message.find(params[:message_id], :conditions => ["to_user_id = ? or from_user_id = ?", @user.id, @user.id ])
      @body = "On #{message.sent_on} #{message.sender.display_name} wrote:\n\n#{message.body.gsub(/^/, '> ')}" 
      @title = "Re: #{message.title.sub(/^Re:\s*/, '')}"
      @user_id = message.from_user_id
 +    @to_user = User.find(message.to_user_id)
      render :action => 'new'
    rescue ActiveRecord::RecordNotFound
 -    render :nothing => true, :status => :not_found
 +    render :action => 'no_such_user', :status => :not_found
    end
  
 +  # Show a message
    def read
      @title = 'read message'
      @message = Message.find(params[:message_id], :conditions => ["to_user_id = ? or from_user_id = ?", @user.id, @user.id ])
 -    @message.message_read = 1 if @message.to_user_id == @user.id
 +    @message.message_read = true if @message.to_user_id == @user.id
      @message.save
    rescue ActiveRecord::RecordNotFound
 -    render :nothing => true, :status => :not_found
 +    render :action => 'no_such_user', :status => :not_found
    end
  
 +  # Display the list of messages that have been sent to the user.
    def inbox
      @title = 'inbox'
      if @user and params[:display_name] == @user.display_name
@@@ -60,7 -51,6 +62,7 @@@
      end
    end
  
 +  # Display the list of messages that the user has sent to other users.
    def outbox
      @title = 'outbox'
      if @user and params[:display_name] == @user.display_name
      end
    end
  
 +  # Set the message as being read or unread.
    def mark
      if params[:message_id]
        id = params[:message_id]
        message = Message.find_by_id(id)
        if params[:mark] == 'unread'
 -        message_read = 0 
 +        message_read = false 
          mark_type = 'unread'
        else
 -        message_read = 1
 +        message_read = true
          mark_type = 'read'
        end
        message.message_read = message_read
@@@ -87,7 -76,5 +89,7 @@@
          redirect_to :controller => 'message', :action => 'inbox', :display_name => @user.display_name
        end
      end
 +  rescue ActiveRecord::RecordNotFound
 +    render :action => 'no_such_user', :status => :not_found
    end
  end
index c03f3c4fbf26767135213e4e65c0d2b5de9f11fc,6ce18f5662b51fb3fe1bf2b7caeb8a49484bc6b5..80a3b30d5cfd04d61c24ee5dd7c1723f0b617646
@@@ -5,27 -5,26 +5,27 @@@ class NodeController < ApplicationContr
  
    session :off
    before_filter :authorize, :only => [:create, :update, :delete]
-   before_filter :check_write_availability, :only => [:create, :update, :delete]
-   before_filter :check_read_availability, :except => [:create, :update, :delete]
+   before_filter :check_api_writable, :only => [:create, :update, :delete]
+   before_filter :check_api_readable, :except => [:create, :update, :delete]
    after_filter :compress_output
  
    # Create a node from XML.
    def create
 -    if request.put?
 -      node = Node.from_xml(request.raw_post, true)
 -
 -      if node
 -        node.user_id = @user.id
 -        node.visible = true
 -        node.save_with_history!
 +    begin
 +      if request.put?
 +        node = Node.from_xml(request.raw_post, true)
  
 -        render :text => node.id.to_s, :content_type => "text/plain"
 +        if node
 +          node.create_with_history @user
 +          render :text => node.id.to_s, :content_type => "text/plain"
 +        else
 +          render :nothing => true, :status => :bad_request
 +        end
        else
 -        render :nothing => true, :status => :bad_request
 +        render :nothing => true, :status => :method_not_allowed
        end
 -    else
 -      render :nothing => true, :status => :method_not_allowed
 +    rescue OSM::APIError => ex
 +      render ex.render_opts
      end
    end
  
@@@ -33,7 -32,7 +33,7 @@@
    def read
      begin
        node = Node.find(params[:id])
 -      if node.visible
 +      if node.visible?
          response.headers['Last-Modified'] = node.timestamp.rfc822
          render :text => node.to_xml.to_s, :content_type => "text/xml"
         else
@@@ -43,7 -42,7 +43,7 @@@
        render :nothing => true, :status => :not_found
      end
    end
 -
 +  
    # Update a node from given XML
    def update
      begin
        new_node = Node.from_xml(request.raw_post)
  
        if new_node and new_node.id == node.id
 -        node.user_id = @user.id
 -        node.latitude = new_node.latitude 
 -        node.longitude = new_node.longitude
 -        node.tags = new_node.tags
 -        node.visible = true
 -        node.save_with_history!
 -
 -        render :nothing => true
 +        node.update_from(new_node, @user)
 +        render :text => node.version.to_s, :content_type => "text/plain"
        else
          render :nothing => true, :status => :bad_request
        end
 +    rescue OSM::APIError => ex
 +      render ex.render_opts
      rescue ActiveRecord::RecordNotFound
        render :nothing => true, :status => :not_found
      end
    end
  
 -  # Delete a node. Doesn't actually delete it, but retains its history in a wiki-like way.
 -  # FIXME remove all the fricking SQL
 +  # Delete a node. Doesn't actually delete it, but retains its history 
 +  # in a wiki-like way. We therefore treat it like an update, so the delete
 +  # method returns the new version number.
    def delete
      begin
        node = Node.find(params[:id])
 -
 -      if node.visible
 -        if WayNode.find(:first, :joins => "INNER JOIN current_ways ON current_ways.id = current_way_nodes.id", :conditions => [ "current_ways.visible = 1 AND current_way_nodes.node_id = ?", node.id ])
 -          render :text => "", :status => :precondition_failed
 -        elsif RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", :conditions => [ "visible = 1 AND member_type='node' and member_id=?", params[:id]])
 -          render :text => "", :status => :precondition_failed
 -        else
 -          node.user_id = @user.id
 -          node.visible = 0
 -          node.save_with_history!
 -
 -          render :nothing => true
 -        end
 +      new_node = Node.from_xml(request.raw_post)
 +      
 +      if new_node and new_node.id == node.id
 +        node.delete_with_history!(new_node, @user)
 +        render :text => node.version.to_s, :content_type => "text/plain"
        else
 -        render :text => "", :status => :gone
 +        render :nothing => true, :status => :bad_request
        end
      rescue ActiveRecord::RecordNotFound
        render :nothing => true, :status => :not_found
 +    rescue OSM::APIError => ex
 +      render ex.render_opts
      end
    end
  
 -  # WTF does this do?
 +  # Dump the details on many nodes whose ids are given in the "nodes" parameter.
    def nodes
      ids = params['nodes'].split(',').collect { |n| n.to_i }
  
index 56397625c967490fdd1b365e664bb94588ede6a7,40f4093e3d99171105c3a0a532e551cbb7a1222c..0976a0c9aae87c679bd254b028b45830f9a47077
@@@ -2,7 -2,7 +2,7 @@@ class OldNodeController < ApplicationCo
    require 'xml/libxml'
  
    session :off
-   before_filter :check_read_availability
+   before_filter :check_api_readable
    after_filter :compress_output
  
    def history
        render :nothing => true, :status => :internal_server_error
      end
    end
 +  
 +  def version
 +    begin
 +      old_node = OldNode.find(:first, :conditions => {:id => params[:id], :version => params[:version]} )
 +      
 +      response.headers['Last-Modified'] = old_node.timestamp.rfc822
 +
 +      doc = OSM::API.new.get_xml_doc
 +      doc.root << old_node.to_xml_node
 +
 +      render :text => doc.to_s, :content_type => "text/xml"
 +    rescue ActiveRecord::RecordNotFound
 +      render :nothing => true, :status => :not_found
 +    rescue
 +      render :nothing => true, :status => :internal_server_error
 +    end
 +  end
  end
index da4e26d67be706e07aebd6297b38838ce73f7813,3d913c0f7660ce49b86a251bf3a19ccc4918d229..a42496687311d255c3d9c391634f9b6fe55cf728
@@@ -2,7 -2,7 +2,7 @@@ class OldWayController < ApplicationCon
    require 'xml/libxml'
  
    session :off
-   before_filter :check_read_availability
+   before_filter :check_api_readable
    after_filter :compress_output
  
    def history
@@@ -13,7 -13,7 +13,7 @@@
  
        way.old_ways.each do |old_way|
          doc.root << old_way.to_xml_node
 -     end
 +      end
  
        render :text => doc.to_s, :content_type => "text/xml"
      rescue ActiveRecord::RecordNotFound
        render :nothing => true, :status => :internal_server_error
      end
    end
 +  
 +  def version
 +    begin
 +      old_way = OldWay.find(:first, :conditions => {:id => params[:id], :version => params[:version]} )
 +      
 +      response.headers['Last-Modified'] = old_way.timestamp.rfc822
 +      
 +      doc = OSM::API.new.get_xml_doc
 +      doc.root << old_way.to_xml_node
 +      
 +      render :text => doc.to_s, :content_type => "text/xml"
 +    rescue ActiveRecord::RecordNotFound
 +      render :nothing => true, :status => :not_found
 +    rescue
 +      render :nothing => true, :status => :internal_server_error
 +    end
 +  end
  end
index ec86bb63584c431c0395323da4597d3bec626cfc,45724916229594526ea3c8b84385554a1d11a5b9..3d3fa21858ad35bbba5a0183572adf4e3076d19e
@@@ -3,28 -3,28 +3,28 @@@ class RelationController < ApplicationC
  
    session :off
    before_filter :authorize, :only => [:create, :update, :delete]
-   before_filter :check_write_availability, :only => [:create, :update, :delete]
-   before_filter :check_read_availability, :except => [:create, :update, :delete]
+   before_filter :check_api_writable, :only => [:create, :update, :delete]
+   before_filter :check_api_readable, :except => [:create, :update, :delete]
    after_filter :compress_output
  
    def create
 -    if request.put?
 -      relation = Relation.from_xml(request.raw_post, true)
 -
 -      if relation
 -        if !relation.preconditions_ok?
 -          render :text => "", :status => :precondition_failed
 -        else
 -          relation.user_id = @user.id
 -          relation.save_with_history!
 -
 -        render :text => relation.id.to_s, :content_type => "text/plain"
 -        end
 +    begin
 +      if request.put?
 +        relation = Relation.from_xml(request.raw_post, true)
 +
 +        # We assume that an exception has been thrown if there was an error 
 +        # generating the relation
 +        #if relation
 +          relation.create_with_history @user
 +          render :text => relation.id.to_s, :content_type => "text/plain"
 +        #else
 +         # render :text => "Couldn't get turn the input into a relation.", :status => :bad_request
 +        #end
        else
 -        render :nothing => true, :status => :bad_request
 +        render :nothing => true, :status => :method_not_allowed
        end
 -    else
 -      render :nothing => true, :status => :method_not_allowed
 +    rescue OSM::APIError => ex
 +      render ex.render_opts
      end
    end
  
    end
  
    def update
 +    logger.debug request.raw_post
      begin
        relation = Relation.find(params[:id])
        new_relation = Relation.from_xml(request.raw_post)
  
        if new_relation and new_relation.id == relation.id
 -        if !new_relation.preconditions_ok?
 -          render :text => "", :status => :precondition_failed
 -        else
 -          relation.user_id = @user.id
 -          relation.tags = new_relation.tags
 -          relation.members = new_relation.members
 -          relation.visible = true
 -          relation.save_with_history!
 -
 -          render :nothing => true
 -        end
 +        relation.update_from new_relation, @user
 +        render :text => relation.version.to_s, :content_type => "text/plain"
        else
          render :nothing => true, :status => :bad_request
        end
      rescue ActiveRecord::RecordNotFound
        render :nothing => true, :status => :not_found
 -    rescue
 -      render :nothing => true, :status => :internal_server_error
 +    rescue OSM::APIError => ex
 +      render ex.render_opts
      end
    end
  
    def delete
 -#XXX check if member somewhere!
      begin
        relation = Relation.find(params[:id])
 -
 -      if relation.visible
 -        if RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", :conditions => [ "visible = 1 AND member_type='relation' and member_id=?", params[:id]])
 -          render :text => "", :status => :precondition_failed
 -        else
 -          relation.user_id = @user.id
 -          relation.tags = []
 -          relation.members = []
 -          relation.visible = false
 -          relation.save_with_history!
 -
 -          render :nothing => true
 -        end
 +      new_relation = Relation.from_xml(request.raw_post)
 +      if new_relation and new_relation.id == relation.id
 +        relation.delete_with_history!(new_relation, @user)
 +        render :text => relation.version.to_s, :content_type => "text/plain"
        else
 -        render :text => "", :status => :gone
 +        render :nothing => true, :status => :bad_request
        end
 +    rescue OSM::APIError => ex
 +      render ex.render_opts
      rescue ActiveRecord::RecordNotFound
        render :nothing => true, :status => :not_found
 -    rescue
 -      render :nothing => true, :status => :internal_server_error
      end
    end
  
          # first collect nodes, ways, and relations referenced by this relation.
          
          ways = Way.find_by_sql("select w.* from current_ways w,current_relation_members rm where "+
 -            "rm.member_type='way' and rm.member_id=w.id and rm.id=#{relation.id}");
 +            "rm.member_type='Way' and rm.member_id=w.id and rm.id=#{relation.id}");
          nodes = Node.find_by_sql("select n.* from current_nodes n,current_relation_members rm where "+
 -            "rm.member_type='node' and rm.member_id=n.id and rm.id=#{relation.id}");
 +            "rm.member_type='Node' and rm.member_id=n.id and rm.id=#{relation.id}");
          # note query is built to exclude self just in case.
          relations = Relation.find_by_sql("select r.* from current_relations r,current_relation_members rm where "+
 -            "rm.member_type='relation' and rm.member_id=r.id and rm.id=#{relation.id} and r.id<>rm.id");
 +            "rm.member_type='Relation' and rm.member_id=r.id and rm.id=#{relation.id} and r.id<>rm.id");
  
          # now additionally collect nodes referenced by ways. Note how we recursively 
          # evaluate ways but NOT relations.
          render :text => doc.to_s, :content_type => "text/xml"
  
        else
 -
 -        render :text => "", :status => :gone
 +        render :nothing => true, :status => :gone
        end
  
      rescue ActiveRecord::RecordNotFound
  
        render :text => doc.to_s, :content_type => "text/xml"
      else
 -      render :nothing => true, :status => :bad_request
 +      render :text => "You need to supply a comma separated list of ids.", :status => :bad_request
      end
 +  rescue ActiveRecord::RecordNotFound
 +    render :text => "Could not find one of the relations", :status => :not_found
    end
  
    def relations_for_way
 -    relations_for_object("way")
 +    relations_for_object("Way")
    end
    def relations_for_node
 -    relations_for_object("node")
 +    relations_for_object("Node")
    end
    def relations_for_relation
 -    relations_for_object("relation")
 +    relations_for_object("Relation")
    end
  
    def relations_for_object(objtype)
 -    relationids = RelationMember.find(:all, :conditions => ['member_type=? and member_id=?', objtype, params[:id]]).collect { |ws| ws.id }.uniq
 +    relationids = RelationMember.find(:all, :conditions => ['member_type=? and member_id=?', objtype, params[:id]]).collect { |ws| ws.id[0] }.uniq
  
      doc = OSM::API.new.get_xml_doc
  
      Relation.find(relationids).each do |relation|
 -      doc.root << relation.to_xml_node
 +      doc.root << relation.to_xml_node if relation.visible
      end
  
      render :text => doc.to_s, :content_type => "text/xml"
index b52be7f349bbb83af740eae7b48a0d5c315919b7,3942cb7fe3d8964eb80eb7733eacee0f9c490de3..0603567c45a2310291374aa3b7cb4cb460014281
@@@ -1,11 -1,13 +1,13 @@@
  class TraceController < ApplicationController
    layout 'site'
  
 -  before_filter :authorize_web  
 -  before_filter :require_user, :only => [:mine, :edit, :delete, :make_public]
 +  before_filter :authorize_web
 +  before_filter :require_user, :only => [:mine, :create, :edit, :delete, :make_public]
    before_filter :authorize, :only => [:api_details, :api_data, :api_create]
-   before_filter :check_database_availability, :except => [:api_details, :api_data, :api_create]
-   before_filter :check_read_availability, :only => [:api_details, :api_data, :api_create]
+   before_filter :check_database_readable, :except => [:api_details, :api_data, :api_create]
+   before_filter :check_database_writable, :only => [:create, :edit, :delete, :make_public]
+   before_filter :check_api_readable, :only => [:api_details, :api_data]
+   before_filter :check_api_writable, :only => [:api_create]
   
    # Counts and selects pages of GPX traces for various criteria (by user, tags, public etc.).
    #  target_user - if set, specifies the user to fetch traces for.  if not set will fetch all traces
@@@ -13,7 -15,7 +15,7 @@@
      # from display name, pick up user id if one user's traces only
      display_name = params[:display_name]
      if target_user.nil? and !display_name.blank?
 -      target_user = User.find(:first, :conditions => [ "visible = 1 and display_name = ?", display_name])
 +      target_user = User.find(:first, :conditions => [ "visible = ? and display_name = ?", true, display_name])
      end
  
      # set title
      # 4 - user's traces, not logged in as that user = all user's public traces
      if target_user.nil? # all traces
        if @user
 -        conditions = ["(gpx_files.public = 1 OR gpx_files.user_id = ?)", @user.id] #1
 +        conditions = ["(gpx_files.public = ? OR gpx_files.user_id = ?)", true, @user.id] #1
        else
 -        conditions  = ["gpx_files.public = 1"] #2
 +        conditions  = ["gpx_files.public = ?", true] #2
        end
      else
        if @user and @user == target_user
          conditions = ["gpx_files.user_id = ?", @user.id] #3 (check vs user id, so no join + can't pick up non-public traces by changing name)
        else
 -        conditions = ["gpx_files.public = 1 AND gpx_files.user_id = ?", target_user.id] #4
 +        conditions = ["gpx_files.public = ? AND gpx_files.user_id = ?", true, target_user.id] #4
        end
      end
      
@@@ -53,8 -55,7 +55,8 @@@
        conditions[0] += " AND gpx_files.id IN (#{files.join(',')})"
      end
      
 -    conditions[0] += " AND gpx_files.visible = 1"
 +    conditions[0] += " AND gpx_files.visible = ?"
 +    conditions << true
  
      @trace_pages, @traces = paginate(:traces,
                                       :include => [:user, :tags],
    end
  
    def create
 -    logger.info(params[:trace][:gpx_file].class.name)
 -    if params[:trace][:gpx_file].respond_to?(:read)
 -      do_create(params[:trace][:gpx_file], params[:trace][:tagstring],
 -                params[:trace][:description], params[:trace][:public])
 +    if params[:trace]
 +      logger.info(params[:trace][:gpx_file].class.name)
 +      if params[:trace][:gpx_file].respond_to?(:read)
 +        do_create(params[:trace][:gpx_file], params[:trace][:tagstring],
 +                  params[:trace][:description], params[:trace][:public])
  
 -      if @trace.id
 -        logger.info("id is #{@trace.id}")
 -        flash[:notice] = "Your GPX file has been uploaded and is awaiting insertion in to the database. This will usually happen within half an hour, and an email will be sent to you on completion."
 +        if @trace.id
 +          logger.info("id is #{@trace.id}")
 +          flash[:notice] = "Your GPX file has been uploaded and is awaiting insertion in to the database. This will usually happen within half an hour, and an email will be sent to you on completion."
  
 -        redirect_to :action => 'mine'
 +          redirect_to :action => 'mine'
 +        end
 +      else
 +        @trace = Trace.new({:name => "Dummy",
 +                            :tagstring => params[:trace][:tagstring],
 +                            :description => params[:trace][:description],
 +                            :public => params[:trace][:public],
 +                            :inserted => false, :user => @user,
 +                            :timestamp => Time.now.getutc})
 +        @trace.valid?
 +        @trace.errors.add(:gpx_file, "can't be blank")
        end
 -    else
 -      @trace = Trace.new({:name => "Dummy",
 -                          :tagstring => params[:trace][:tagstring],
 -                          :description => params[:trace][:description],
 -                          :public => params[:trace][:public],
 -                          :inserted => false, :user => @user,
 -                          :timestamp => Time.now})
 -      @trace.valid?
 -      @trace.errors.add(:gpx_file, "can't be blank")
      end
    end
  
    end
  
    def georss
 -    conditions = ["gpx_files.public = 1"]
 +    conditions = ["gpx_files.public = ?", true]
  
      if params[:display_name]
        conditions[0] += " AND users.display_name = ?"
  
    def api_create
      if request.post?
 -      do_create(params[:file], params[:tags], params[:description], params[:public])
 -
 -      if @trace.id
 -        render :text => @trace.id.to_s, :content_type => "text/plain"
 -      elsif @trace.valid?
 -        render :nothing => true, :status => :internal_server_error
 +      tags = params[:tags] || ""
 +      description = params[:description] || ""
 +      pub = params[:public] || false
 +      
 +      if params[:file].respond_to?(:read)
 +        do_create(params[:file], tags, description, pub)
 +
 +        if @trace.id
 +          render :text => @trace.id.to_s, :content_type => "text/plain"
 +        elsif @trace.valid?
 +          render :nothing => true, :status => :internal_server_error
 +        else
 +          render :nothing => true, :status => :bad_request
 +        end
        else
          render :nothing => true, :status => :bad_request
        end
@@@ -322,7 -313,7 +324,7 @@@ privat
        :public => public,
        :inserted => true,
        :user => @user,
 -      :timestamp => Time.now
 +      :timestamp => Time.now.getutc
      })
  
      # Save the trace object
        # Remove the file as we have failed to update the database
        FileUtils.rm_f(filename)
      end
 +    
 +    # Finally save whether the user marked the trace as being public
 +    if @trace.public?
 +      if @user.trace_public_default.nil?
 +        @user.preferences.create(:k => "gps.trace.public", :v => "default")
 +      end
 +    else
 +      pref = @user.trace_public_default
 +      pref.destroy unless pref.nil?
 +    end
 +    
    end
  
  end
index 553e841aa3658af2a3342a09a94be7391097bc44,1cd34900a19835b6553e29b676a9c5c61a9ef38f..9544dd8a888c29aa5caf9990360398451b302dda
@@@ -4,8 -4,9 +4,9 @@@ class UserController < ApplicationContr
    before_filter :authorize, :only => [:api_details, :api_gpx_files]
    before_filter :authorize_web, :except => [:api_details, :api_gpx_files]
    before_filter :require_user, :only => [:set_home, :account, :go_public, :make_friend, :remove_friend, :upload_image, :delete_image]
-   before_filter :check_database_availability, :except => [:api_details, :api_gpx_files]
-   before_filter :check_read_availability, :only => [:api_details, :api_gpx_files]
+   before_filter :check_database_readable, :except => [:api_details, :api_gpx_files]
+   before_filter :check_database_writable, :only => [:login, :new, :set_home, :account, :go_public, :make_friend, :remove_friend, :upload_image, :delete_image]
+   before_filter :check_api_readable, :only => [:api_details, :api_gpx_files]
  
    filter_parameter_logging :password, :pass_crypt, :pass_crypt_confirmation
  
@@@ -82,7 -83,7 +83,7 @@@
    def lost_password
      @title = 'lost password'
      if params[:user] and params[:user][:email]
 -      user = User.find_by_email(params[:user][:email], :conditions => "visible = 1")
 +      user = User.find_by_email(params[:user][:email], :conditions => {:visible => true})
  
        if user
          token = user.tokens.create
  
    def new
      @title = 'create account'
 +    # The user is logged in already, so don't show them the signup page, instead
 +    # send them to the home page
 +    redirect_to :controller => 'site', :action => 'index' if session[:user]
    end
  
    def login
 +    if session[:user]
 +      # The user is logged in already, if the referer param exists, redirect them to that
 +      if params[:referer]
 +        redirect_to params[:referer]
 +      else
 +        redirect_to :controller => 'site', :action => 'index'
 +      end
 +      return
 +    end
      @title = 'login'
      if params[:user]
        email_or_display_name = params[:user][:email]
    end
  
    def view
 -    @this_user = User.find_by_display_name(params[:display_name], :conditions => "visible = 1")
 +    @this_user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
  
      if @this_user
        @title = @this_user.display_name
    def make_friend
      if params[:display_name]     
        name = params[:display_name]
 -      new_friend = User.find_by_display_name(name, :conditions => "visible = 1")
 +      new_friend = User.find_by_display_name(name, :conditions => {:visible => true})
        friend = Friend.new
        friend.user_id = @user.id
        friend.friend_user_id = new_friend.id
    def remove_friend
      if params[:display_name]     
        name = params[:display_name]
 -      friend = User.find_by_display_name(name, :conditions => "visible = 1")
 +      friend = User.find_by_display_name(name, :conditions => {:visible => true})
        if @user.is_friends_with?(friend)
          Friend.delete_all "user_id = #{@user.id} AND friend_user_id = #{friend.id}"
          flash[:notice] = "#{friend.display_name} was removed from your friends."
index 80c75d91c3644d4c72fb04c8ea8758687217fa5a,9b93ec2b3689b2cc375cf07f9cdd1cc1fd80d44a..e28945dcd90d7ad898905bd854b0bd61fe4ebfe6
@@@ -3,27 -3,28 +3,27 @@@ class WayController < ApplicationContro
  
    session :off
    before_filter :authorize, :only => [:create, :update, :delete]
-   before_filter :check_write_availability, :only => [:create, :update, :delete]
-   before_filter :check_read_availability, :except => [:create, :update, :delete]
+   before_filter :check_api_writable, :only => [:create, :update, :delete]
+   before_filter :check_api_readable, :except => [:create, :update, :delete]
    after_filter :compress_output
  
    def create
 -    if request.put?
 -      way = Way.from_xml(request.raw_post, true)
 -
 -      if way
 -        if !way.preconditions_ok?
 -          render :text => "", :status => :precondition_failed
 -        else
 -          way.user_id = @user.id
 -          way.save_with_history!
 +    begin
 +      if request.put?
 +        way = Way.from_xml(request.raw_post, true)
  
 +        if way
 +          way.create_with_history @user
            render :text => way.id.to_s, :content_type => "text/plain"
 +        else
 +          render :nothing => true, :status => :bad_request
          end
        else
 -        render :nothing => true, :status => :bad_request
 +        render :nothing => true, :status => :method_not_allowed
        end
 -    else
 -      render :nothing => true, :status => :method_not_allowed
 +    rescue OSM::APIError => ex
 +      logger.warn request.raw_post
 +      render ex.render_opts
      end
    end
  
@@@ -38,8 -39,6 +38,8 @@@
        else
          render :text => "", :status => :gone
        end
 +    rescue OSM::APIError => ex
 +      render ex.render_opts
      rescue ActiveRecord::RecordNotFound
        render :nothing => true, :status => :not_found
      end
        new_way = Way.from_xml(request.raw_post)
  
        if new_way and new_way.id == way.id
 -        if !new_way.preconditions_ok?
 -          render :text => "", :status => :precondition_failed
 -        else
 -          way.user_id = @user.id
 -          way.tags = new_way.tags
 -          way.nds = new_way.nds
 -          way.visible = true
 -          way.save_with_history!
 -
 -          render :nothing => true
 -        end
 +        way.update_from(new_way, @user)
 +        render :text => way.version.to_s, :content_type => "text/plain"
        else
          render :nothing => true, :status => :bad_request
        end
 +    rescue OSM::APIError => ex
 +      logger.warn request.raw_post
 +      render ex.render_opts
      rescue ActiveRecord::RecordNotFound
        render :nothing => true, :status => :not_found
      end
    def delete
      begin
        way = Way.find(params[:id])
 -      way.delete_with_relations_and_history(@user)
 -
 -      # if we get here, all is fine, otherwise something will catch below.  
 -      render :nothing => true
 -    rescue OSM::APIAlreadyDeletedError
 -      render :text => "", :status => :gone
 -    rescue OSM::APIPreconditionFailedError
 -      render :text => "", :status => :precondition_failed
 +      new_way = Way.from_xml(request.raw_post)
 +
 +      if new_way and new_way.id == way.id
 +        way.delete_with_history!(new_way, @user)
 +        render :text => way.version.to_s, :content_type => "text/plain"
 +      else
 +        render :nothing => true, :status => :bad_request
 +      end
 +    rescue OSM::APIError => ex
 +      render ex.render_opts
      rescue ActiveRecord::RecordNotFound
        render :nothing => true, :status => :not_found
      end
@@@ -89,7 -92,7 +89,7 @@@
  
        if way.visible
          nd_ids = way.nds + [-1]
 -        nodes = Node.find(:all, :conditions => "visible = 1 AND id IN (#{nd_ids.join(',')})")
 +        nodes = Node.find(:all, :conditions => ["visible = ? AND id IN (#{nd_ids.join(',')})", true])
  
          # Render
          doc = OSM::API.new.get_xml_doc
      end
    end
  
 +  ##
 +  # returns all the ways which are currently using the node given in the 
 +  # :id parameter. note that this used to return deleted ways as well, but
 +  # this seemed not to be the expected behaviour, so it was removed.
    def ways_for_node
 -    wayids = WayNode.find(:all, :conditions => ['node_id = ?', params[:id]]).collect { |ws| ws.id[0] }.uniq
 +    wayids = WayNode.find(:all, 
 +                          :conditions => ['node_id = ?', params[:id]]
 +                          ).collect { |ws| ws.id[0] }.uniq
  
      doc = OSM::API.new.get_xml_doc
  
      Way.find(wayids).each do |way|
 -      doc.root << way.to_xml_node
 +      doc.root << way.to_xml_node if way.visible
      end
  
      render :text => doc.to_s, :content_type => "text/xml"
index bc51fa98b3a576eaee6ec6d89ead45607f271da7,ff17fcf1d3c071cbe798e2fbda5c0a6f173af6cc..534003031a4d9903fc522333b32d6cf2d505632b
@@@ -7,7 -7,6 +7,7 @@@
      <%= stylesheet_link_tag 'site' %>
      <%= stylesheet_link_tag 'print', :media => "print" %>
      <%= tag("link", { :rel => "search", :type => "application/opensearchdescription+xml", :title => "OpenStreetMap Search", :href => "/opensearch/osm.xml" }) %>
 +    <%= tag("meta", { :name => "description", :content => "OpenStreetMap is the free wiki world map." }) %>
      <%= yield :head %>
      <title>OpenStreetMap<%= ' | '+ h(@title) if @title %></title>
    </head>
          <%
          viewclass = ''
          editclass = ''
 +        historyclass = ''
          exportclass = ''
          traceclass = ''
          viewclass = 'active' if params['controller'] == 'site' and params['action'] == 'index' 
          editclass = 'active' if params['controller'] == 'site' and params['action'] == 'edit' 
 +        historyclass = 'active' if params['controller'] == 'changeset' and params['action'] == 'list_bbox' 
          exportclass = 'active' if params['controller'] == 'site' and params['action'] == 'export'
          traceclass = 'active' if params['controller'] == 'trace'
          diaryclass = 'active' if params['controller'] == 'diary_entry'
          %>
          <li><%= link_to 'View', {:controller => 'site', :action => 'index'}, {:id => 'viewanchor', :title => 'view maps', :class => viewclass} %></li>
          <li><%= link_to 'Edit', {:controller => 'site', :action => 'edit'}, {:id => 'editanchor', :title => 'edit maps', :class => editclass} %></li>
 +        <li><%= link_to 'History', {:controller => 'history' }, {:id => 'historyanchor', :title => 'changeset history', :class => historyclass} %></li>
          <% if params['controller'] == 'site' and (params['action'] == 'index' or params['action'] == 'export') %>
          <li><%= link_to_remote 'Export', {:url => {:controller => 'export', :action => 'start'}}, {:id => 'exportanchor', :title => 'export map data', :class => exportclass, :href => url_for(:controller => 'site', :action => 'export')} %></li>
          <% else %>
@@@ -94,7 -90,7 +94,7 @@@
          The OpenStreetMap database is currently offline while
          essential database maintenance work is carried out.
        </div>
-       <% elsif OSM_STATUS == :api_readonly %>
+       <% elsif OSM_STATUS == :database_readonly or OSM_STATUS == :api_readonly %>
        <div id="alert">
          The OpenStreetMap database is currently in read-only mode while
          essential database maintenance work is carried out.
          </div>
  
          <div id="cclogo" class="button" style="width: 88px">
 -          <a href="http://creativecommons.org/licenses/by-sa/2.0/"><img src="/images/cc_button.png" border="0" alt="" /></a>
 +          <%= link_to image_tag("cc_button.png", :alt => "CC by-sa 2.0", :border => "0"), "http://creativecommons.org/licenses/by-sa/2.0/" %>
          </div>
        </center>
      </div>
index daa78f3b9f72bbda47848a991865f07c02037996,7d80a1705be3c51473f16589e7afddfcc114c5b1..e341305f591cad809733c2ca1e14e21f4ceb257c
@@@ -2,7 -2,7 +2,7 @@@
  <p>The OpenStreetMap database is currently offline while
     essential database maintenance work is carried out.
  </p>
- <% elsif OSM_STATUS == :api_readonly %>
+ <% elsif OSM_STATUS == :database_readonly or OSM_STATUS == :api_readonly %>
  <p>The OpenStreetMap database is currently in read-only mode while
     essential database maintenance work is carried out.
  </p>
  <%= render :partial => 'sidebar', :locals => { :onopen => "resizeMap();", :onclose => "resizeMap();" } %>
  <%= render :partial => 'search' %>
  
 -<% session[:token] = @user.tokens.create.token unless session[:token] %>
 +<%
 +session[:token] = @user.tokens.create.token unless session[:token]
  
 -<% if params['mlon'] and params['mlat'] %>
 -      <% lon =  h(params['mlon']) %>
 -      <% lat =  h(params['mlat'])  %>
 -      <% zoom =  h(params['zoom'] || '14') %>
 -<% elsif @user and params['lon'].nil? and params['lat'].nil? and params['gpx'].nil? %> 
 -      <% lon =  @user.home_lon %>
 -      <% lat =  @user.home_lat %>
 -      <% zoom = '14' %>
 -<% else %>
 -      <% lon =  h(params['lon'] || 'null') %>
 -      <% lat =  h(params['lat'] || 'null') %>
 -      <% zoom =  h(params['zoom'] || '14') %>
 -<% end %>
 +# Decide on a lat lon to initialise potlatch with. Various ways of doing this
 +if params['lon'] and params['lat']
 +      lon =  h(params['lon']) 
 +      lat =  h(params['lat']) 
 +      zoom =  h(params['zoom'])
 +
 +elsif params['mlon'] and params['mlat'] 
 +      lon =  h(params['mlon']) 
 +      lat =  h(params['mlat']) 
 +      zoom =  h(params['zoom'])
 +      
 +elsif params['gpx']
 +      #use gpx id to locate (dealt with below)
 +      
 +elsif cookies.key?("_osm_location")
 +      lon,lat,zoom,layers = cookies["_osm_location"].split("|")
 +      
 +elsif @user and !@user.home_lon.nil? and !@user.home_lat.nil? 
 +      lon =  @user.home_lon
 +      lat =  @user.home_lat
 +
 +else
 +      #catch all.  Do nothing.  lat=nil, lon=nil
 +      #Currently this results in potlatch starting up at 0,0 (Atlantic ocean).
 +end
 +
 +zoom='14' if zoom.nil?
 +%>
  
  <div id="map">
        You need a Flash player to use Potlatch, the
diff --combined config/environment.rb
index ce1216b83c6966a0c653b69f607b0fb563eb2855,930fc81e9bdbf30143b82bf6f7bc2c66c33e2269..7f83fd4ad7f87670a6b4e17a770bcce4345796e0
@@@ -5,22 -5,20 +5,23 @@@
  ENV['RAILS_ENV'] ||= 'production'
  
  # Specifies gem version of Rails to use when vendor/rails is not present
 -RAILS_GEM_VERSION = '2.0.2' unless defined? RAILS_GEM_VERSION
 +RAILS_GEM_VERSION = '2.1.2' unless defined? RAILS_GEM_VERSION
  
  # Set the server URL
  SERVER_URL = ENV['OSM_SERVER_URL'] || 'www.openstreetmap.org'
  
 +# Set the generator
 +GENERATOR = ENV['OSM_SERVER_GENERATOR'] || 'OpenStreetMap server'
 +
  # Application constants needed for routes.rb - must go before Initializer call
 -API_VERSION = ENV['OSM_API_VERSION'] || '0.5'
 +API_VERSION = ENV['OSM_API_VERSION'] || '0.6'
  
  # Set application status - possible settings are:
  #
  #   :online - online and operating normally
  #   :api_readonly - site online but API in read-only mode
  #   :api_offline - site online but API offline
+ #   :database_readonly - database and site in read-only mode
  #   :database_offline - database offline with site in emergency mode
  #
  OSM_STATUS = :online
@@@ -40,16 -38,6 +41,16 @@@ Rails::Initializer.run do |config
      config.frameworks -= [ :active_record ]
    end
  
 +  # Specify gems that this application depends on. 
 +  # They can then be installed with "rake gems:install" on new installations.
 +  # config.gem "bj"
 +  # config.gem "hpricot", :version => '0.6', :source => "http://code.whytheluckystiff.net"
 +  # config.gem "aws-s3", :lib => "aws/s3"
 +  config.gem 'composite_primary_keys', :version => '1.1.0'
 +  config.gem 'libxml-ruby', :version => '>= 1.1.1', :lib => 'libxml'
 +  config.gem 'rmagick', :lib => 'RMagick'
 +  config.gem 'mysql'
 +
    # Only load the plugins named here, in the order given. By default, all plugins 
    # in vendor/plugins are loaded in alphabetical order.
    # :all can be used as a placeholder for all plugins not explicitly named
    # (create the session table with 'rake db:sessions:create')
    config.action_controller.session_store = :sql_session_store
  
 +  # We will use the old style of migrations, rather than the newer
 +  # timestamped migrations that were introduced with Rails 2.1, as
 +  # it will be confusing to have the numbered and timestamped migrations
 +  # together in the same folder.
 +  config.active_record.timestamped_migrations = false
 +
    # Use SQL instead of Active Record's schema dumper when creating the test database.
    # This is necessary if your schema can't be completely dumped by the schema dumper,
    # like if you have constraints or database-specific column types
@@@ -91,5 -73,5 +92,5 @@@
    # config.active_record.observers = :cacher, :garbage_collector
  
    # Make Active Record use UTC-base instead of local time
 -  config.active_record.default_timezone = :utc
 +  config.active_record.default_timezone = :utc
  end