Merge rails_port as of r4613 & fix tests.
authorGabriel Ebner <gabriel@svn.openstreetmap.org>
Thu, 20 Sep 2007 14:37:29 +0000 (14:37 +0000)
committerGabriel Ebner <gabriel@svn.openstreetmap.org>
Thu, 20 Sep 2007 14:37:29 +0000 (14:37 +0000)
20 files changed:
1  2 
app/controllers/amf_controller.rb
app/controllers/api_controller.rb
app/controllers/node_controller.rb
app/controllers/relation_controller.rb
app/controllers/swf_controller.rb
app/controllers/way_controller.rb
app/models/relation.rb
app/models/way.rb
config/environment.rb
db/migrate/006_add_relations.rb
db/migrate/007_remove_segments.rb
db/migrate/007_remove_segments_helper.cc
test/fixtures/current_relation_members.yml
test/fixtures/current_relation_tags.yml
test/fixtures/current_relations.yml
test/fixtures/relation_members.yml
test/fixtures/relation_tags.yml
test/fixtures/relations.yml
test/functional/relation_controller_test.rb
test/unit/node_test.rb

@@@ -43,6 -43,8 +43,8 @@@ class AmfController < ApplicationContro
                  when 'putway';                results[index]=putdata(index,putway(args))
                  when 'deleteway';             results[index]=putdata(index,deleteway(args))
                  when 'makeway';               results[index]=putdata(index,makeway(args))
+                 when 'putpoi';                results[index]=putdata(index,putpoi(args))
+                 when 'getpoi';                results[index]=putdata(index,getpoi(args))
        end
      end
  
@@@ -71,8 -73,8 +73,8 @@@
  
    def getpresets
      presets={}
-     presetmenus={}; presetmenus['point']=[]; presetmenus['way']=[]
-     presetnames={}; presetnames['point']={}; presetnames['way']={}
+     presetmenus={}; presetmenus['point']=[]; presetmenus['way']=[]; presetmenus['POI']=[]
+     presetnames={}; presetnames['point']={}; presetnames['way']={}; presetnames['POI']={}
      presettype=''
      presetcategory=''
  
@@@ -97,8 -99,8 +99,8 @@@ unclassified road: highway=unclassified
  
  way/footway
  footpath: highway=footway,foot=yes
- bridleway: highway=bridleway,foot=yes,horse=yes,bicycle=yes
- byway: highway=byway,foot=yes,horse=yes,bicycle=yes,motorcar=yes
+ bridleway: highway=bridleway,foot=yes
+ byway: highway=unsurfaced,foot=yes
  permissive path: highway=footway,foot=permissive
  
  way/cycleway
@@@ -124,13 -126,8 +126,8 @@@ disused railway tracks: railway=disuse
  course of old railway: railway=abandoned
  
  way/natural
- forest: natural=wood,landuse=forest
- woodland: natural=wood,landuse=
- reservoir: natural=water,landuse=reservoir
- lake: natural=water,landuse=
- marsh: natural=marsh
- beach: natural=beach
- coastline: natural=coastline
+ lake: landuse=water
+ forest: landuse=forest
  
  point/road
  mini roundabout: highway=mini_roundabout
@@@ -158,7 -155,33 +155,33 @@@ viaduct: railway=viaduc
  level crossing: railway=crossing
  
  point/natural
- peak: natural=peak
+ peak: point=peak
+ POI/road
+ car park: amenity=parking
+ petrol station: amenity=fuel
+ POI/cycleway
+ bike park: amenity=bicycle_parking
+ POI/place
+ city: place=city,name=(type name here),is_in=(type region or county)
+ town: place=town,name=(type name here),is_in=(type region or county)
+ suburb: place=suburb,name=(type name here),is_in=(type region or county)
+ village: place=village,name=(type name here),is_in=(type region or county)
+ hamlet: place=hamlet,name=(type name here),is_in=(type region or county)
+ POI/tourism
+ attraction: tourism=attraction,amenity=,religion=,denomination=
+ church: tourism=,amenity=place_of_worship,name=(type name here),religion=christian,denomination=(type denomination here)
+ hotel: tourism=hotel,amenity=,religion=,denomination=
+ other religious: tourism=,amenity=place_of_worship,name=(type name here),religion=(type religion),denomination=
+ post box: amenity=post_box,tourism=,name=,religion=,denomination=
+ post office: amenity=post_office,tourism=,name=,religion=,denomination=
+ pub: tourism=,amenity=pub,name=(type name here),religion=,denomination=
+ POI/natural
+ peak: point=peak
  EOF
  
      StringIO.open(txt) do |file|
      ymin = args[1].to_f-0.01
      xmax = args[2].to_f+0.01
      ymax = args[3].to_f+0.01
+       baselong    = args[4]
+       basey       = args[5]
+       masterscale = args[6]
  
      RAILS_DEFAULT_LOGGER.info("  Message: whichways, bbox=#{xmin},#{ymin},#{xmax},#{ymax}")
  
 -    waylist=WaySegment.find_by_sql("SELECT DISTINCT current_way_segments.id AS wayid"+
 -       "  FROM current_way_segments,current_segments,current_nodes,current_ways "+
 +    waylist=WayNode.find_by_sql("SELECT DISTINCT current_way_nodes.id AS wayid"+
 +       "  FROM current_way_nodes,current_segments,current_nodes,current_ways "+
         " WHERE segment_id=current_segments.id "+
         "   AND current_segments.visible=1 "+
         "   AND node_a=current_nodes.id "+
 -         "   AND current_ways.id=current_way_segments.id "+
 +         "   AND current_ways.id=current_way_nodes.id "+
           "   AND current_ways.visible=1 "+
         "   AND (latitude  BETWEEN "+ymin.to_s+" AND "+ymax.to_s+") "+
         "   AND (longitude BETWEEN "+xmin.to_s+" AND "+xmax.to_s+")")
  
         ways = waylist.collect {|a| a.wayid.to_i } # get an array of way id's
  
-        pointlist =ActiveRecord::Base.connection.select_all("SELECT current_nodes.id,current_nodes.tags "+
+        pointlist =ActiveRecord::Base.connection.select_all("SELECT current_nodes.id,latitude,longitude,current_nodes.tags "+
         "  FROM current_nodes "+
         "  LEFT OUTER JOIN current_segments cs1 ON cs1.node_a=current_nodes.id "+
         "  LEFT OUTER JOIN current_segments cs2 ON cs2.node_b=current_nodes.id "+
         "   AND cs1.id IS NULL AND cs2.id IS NULL "+
         "   AND current_nodes.visible=1")
  
-           points = pointlist.collect {|a| [a['id'],tag2array(a['tags'])]      } # get a list of node ids and their tags
+           points = pointlist.collect {|a| [a['id'],long2coord(a['longitude'].to_f,baselong,masterscale),lat2coord(a['latitude'].to_f,basey,masterscale),tag2array(a['tags'])] } # get a list of node ids and their tags
  
      return [ways,points]
    end
      ActiveRecord::Base.connection.execute("DROP TABLE #{db_uqs}")
      ActiveRecord::Base.connection.execute("DROP TABLE #{db_uqn}")
  
 -    #         insert new version of route into way_segments
 +    #         insert new version of route into way_nodes
  
      insertsql =''
      currentsql=''
        sequence  +=1
      end
  
 -    ActiveRecord::Base.connection.execute("DELETE FROM current_way_segments WHERE id=#{way}");
 -    ActiveRecord::Base.connection.insert("INSERT INTO         way_segments (id,segment_id,version    ) VALUES #{insertsql}");
 -    ActiveRecord::Base.connection.insert("INSERT INTO current_way_segments (id,segment_id,sequence_id) VALUES #{currentsql}");
 +    ActiveRecord::Base.connection.execute("DELETE FROM current_way_nodes WHERE id=#{way}");
 +    ActiveRecord::Base.connection.insert("INSERT INTO         way_nodes (id,segment_id,version    ) VALUES #{insertsql}");
 +    ActiveRecord::Base.connection.insert("INSERT INTO current_way_nodes (id,segment_id,sequence_id) VALUES #{currentsql}");
  
      # -- 7. insert new way tags
  
      [originalway,way,renumberednodes,numberedsegments,xmin,xmax,ymin,ymax]
    end
  
+   # -----     putpoi (user token, id, x,y,tag array,visible,baselong,basey,masterscale)
+   #                   returns current id, new id
+   #                   if new: add new row to current_nodes and nodes
+   #                   if old: add new row to nodes, update current_nodes
+   def putpoi(args)
+       usertoken,id,x,y,tags,visible,baselong,basey,masterscale=args
+       uid=getuserid(usertoken)
+       return if !uid
+     db_now='@now'+uid.to_s+id.to_i.abs.to_s+Time.new.to_i.to_s        # 'now' variable name, typically 51 chars
+     ActiveRecord::Base.connection.execute("SET #{db_now}=NOW()")
+       id=id.to_i
+       visible=visible.to_i
+       x=coord2long(x.to_f,masterscale,baselong)
+       y=coord2lat(y.to_f,masterscale,basey)
+       tagsql="'"+sqlescape(array2tag(tags))+"'"
+       
+       if (id>0) then
+               ActiveRecord::Base.connection.insert("INSERT INTO nodes (id,latitude,longitude,timestamp,user_id,visible,tags) VALUES (#{id},#{y},#{x},#{db_now},#{uid},#{visible},#{tagsql})");
+               ActiveRecord::Base.connection.update("UPDATE current_nodes SET latitude=#{y},longitude=#{x},timestamp=#{db_now},user_id=#{uid},visible=#{visible},tags=#{tagsql} WHERE id=#{id}");
+               newid=id
+       else
+               newid=ActiveRecord::Base.connection.insert("INSERT INTO current_nodes (latitude,longitude,timestamp,user_id,visible,tags) VALUES (#{y},#{x},#{db_now},#{uid},#{visible},#{tagsql})");
+                         ActiveRecord::Base.connection.update("INSERT INTO nodes (id,latitude,longitude,timestamp,user_id,visible,tags) VALUES (#{newid},#{y},#{x},#{db_now},#{uid},#{visible},#{tagsql})");
+       end
+       [id,newid]
+   end
+   # -----     getpoi (id,baselong,basey,masterscale)
+   #                   returns id,x,y,tag array
+   
+   def getpoi(args)
+       id,baselong,basey,masterscale=args; id=id.to_i
+       poi=ActiveRecord::Base.connection.select_one("SELECT latitude,longitude,tags "+
+               "FROM current_nodes WHERE visible=1 AND id=#{id}")
+       if poi.nil? then return [nil,nil,nil,''] end
+       [id,
+        long2coord(poi['longitude'].to_f,baselong,masterscale),
+        lat2coord(poi['latitude'].to_f,basey,masterscale),
+        tag2array(poi['tags'])]
+   end
    # -----     deleteway (user token, way)
    #                   returns way ID only
  
        
        ActiveRecord::Base.connection.insert("INSERT INTO ways (id,user_id,timestamp,visible) VALUES (#{way},#{uid},#{db_now},0)")
        ActiveRecord::Base.connection.update("UPDATE current_ways SET user_id=#{uid},timestamp=#{db_now},visible=0 WHERE id=#{way}")
 -      ActiveRecord::Base.connection.execute("DELETE FROM current_way_segments WHERE id=#{way}")
 +      ActiveRecord::Base.connection.execute("DELETE FROM current_way_nodes WHERE id=#{way}")
        ActiveRecord::Base.connection.execute("DELETE FROM current_way_tags WHERE id=#{way}")
        
        way
@@@ -537,7 -606,7 +606,7 @@@ def makeway(args
                  FROM current_nodes AS cn1,
                       current_nodes AS cn2,
                       current_segments AS cs 
 -                     LEFT OUTER JOIN current_way_segments ON segment_id=cs.id 
 +                     LEFT OUTER JOIN current_way_nodes ON segment_id=cs.id 
                 WHERE (cn1.longitude BETWEEN #{xs1} AND #{xs2}) 
                   AND (cn1.latitude  BETWEEN #{ys1} AND #{ys2}) 
                   AND segment_id IS NULL 
@@@ -603,7 -672,7 +672,7 @@@ def findconnect(id,nodesused,lookfor,to
                  FROM current_nodes AS cn1,
                       current_nodes AS cn2,
                       current_segments AS cs 
 -                     LEFT OUTER JOIN current_way_segments ON segment_id=cs.id 
 +                     LEFT OUTER JOIN current_way_nodes ON segment_id=cs.id 
                 WHERE segment_id IS NULL 
                     AND cs.visible=1
                   AND cn1.id=node_a AND cn1.visible=1 
                  FROM current_nodes AS cn1,
                       current_nodes AS cn2,
                       current_segments AS cs 
 -                     LEFT OUTER JOIN current_way_segments ON segment_id=cs.id 
 +                     LEFT OUTER JOIN current_way_nodes ON segment_id=cs.id 
                 WHERE segment_id IS NULL 
                     AND cs.visible=1
                   AND cn1.id=node_a AND cn1.visible=1 
@@@ -663,8 -732,8 +732,8 @@@ en
  def readwayquery(id)
    ActiveRecord::Base.connection.select_all "SELECT n1.latitude AS lat1,n1.longitude AS long1,n1.id AS id1,n1.tags as tags1, "+
        "                 n2.latitude AS lat2,n2.longitude AS long2,n2.id AS id2,n2.tags as tags2,segment_id "+
 -      "    FROM current_way_segments,current_segments,current_nodes AS n1,current_nodes AS n2 "+
 -      "   WHERE current_way_segments.id=#{id} "+
 +      "    FROM current_way_nodes,current_segments,current_nodes AS n1,current_nodes AS n2 "+
 +      "   WHERE current_way_nodes.id=#{id} "+
        "     AND segment_id=current_segments.id "+
          "     AND current_segments.visible=1 "+
        "     AND n1.id=node_a and n2.id=node_b "+
@@@ -677,9 -746,9 +746,9 @@@ def createuniquesegments(way,uqs_name,s
    sql=<<-EOF
        CREATE TEMPORARY TABLE #{uqs_name}
                SELECT a.segment_id
 -                FROM (SELECT DISTINCT segment_id FROM current_way_segment
 +                FROM (SELECT DISTINCT segment_id FROM current_way_node
                    WHERE id = #{way}) a
 -             LEFT JOIN current_way_segments b 
 +             LEFT JOIN current_way_nodes b 
                  ON b.segment_id = a.segment_id
                   AND b.id != #{way}
                 WHERE b.segment_id IS NULL
@@@ -691,18 -760,33 +760,33 @@@ en
  def createuniquenodes(uqs_name,uqn_name)
        # Finds nodes which appear in uniquesegments but no other segments
        sql=<<-EOF
-               CREATE TEMPORARY TABLE #{uqn_name}
-                          SELECT DISTINCT node_id
-                             FROM (SELECT cn.id AS node_id
-                                                 FROM current_nodes AS cn,
-                                                      current_segments AS cs,
-                                                      #{uqs_name} AS us
-                                                WHERE cs.id=us.segment_id
-                                                  AND (cn.id=cs.node_a OR cn.id=cs.node_b)) AS n
-                                        LEFT JOIN current_segments AS cs2 ON node_id=cs2.node_a AND cs2.visible=1
-                                        LEFT JOIN current_segments AS cs3 ON node_id=cs3.node_b AND cs3.visible=1
-                                            WHERE cs2.node_a IS NULL
-                                              AND cs3.node_b IS NULL
+       CREATE TEMPORARY TABLE #{uqn_name}
+          SELECT DISTINCT node_id
+          FROM (SELECT cn.id AS node_id
+                FROM current_nodes AS cn,
+                     current_segments AS cs,
+                     #{uqs_name} AS us
+                WHERE cs.id=us.segment_id
+                  AND cn.id=cs.node_a) AS n
+          LEFT JOIN current_segments AS cs2 ON node_id=cs2.node_a AND cs2.visible=1
+          LEFT JOIN current_segments AS cs3 ON node_id=cs3.node_b AND cs3.visible=1
+          WHERE cs2.node_a IS NULL
+            AND cs3.node_b IS NULL
+       EOF
+       ActiveRecord::Base.connection.execute(sql)
+       sql=<<-EOF
+       INSERT INTO #{uqn_name}
+          SELECT DISTINCT node_id
+          FROM (SELECT cn.id AS node_id
+                FROM current_nodes AS cn,
+                     current_segments AS cs,
+                     #{uqs_name} AS us
+                WHERE cs.id=us.segment_id
+                  AND cn.id=cs.node_b) AS n
+          LEFT JOIN current_segments AS cs2 ON node_id=cs2.node_a AND cs2.visible=1
+          LEFT JOIN current_segments AS cs3 ON node_id=cs3.node_b AND cs3.visible=1
+          WHERE cs2.node_a IS NULL
+            AND cs3.node_b IS NULL
        EOF
        ActiveRecord::Base.connection.execute(sql)
  end
@@@ -63,13 -63,8 +63,8 @@@ class ApiController < ApplicationContro
        return
      end
  
-     # integerise
-     min_lat = min_lat * 1000000
-     max_lat = max_lat * 1000000
-     min_lon = min_lon * 1000000
-     max_lon = max_lon * 1000000
      # get all the points
-     points = Tracepoint.find(:all, :conditions => ['latitude BETWEEN ? AND ? AND longitude BETWEEN ? AND ?', min_lat.to_i, max_lat.to_i, min_lon.to_i, max_lon.to_i], :select => "DISTINCT *", :offset => offset, :limit => TRACEPOINTS_PER_PAGE, :order => "timestamp DESC" )
+     points = Tracepoint.find_by_area(min_lat, min_lon, max_lat, max_lon, :offset => offset, :limit => TRACEPOINTS_PER_PAGE, :order => "timestamp DESC" )
  
      doc = XML::Document.new
      doc.encoding = 'UTF-8'
      # check the bbox isn't too large
      requested_area = (max_lat-min_lat)*(max_lon-min_lon)
      if requested_area > MAX_REQUEST_AREA
 -      report_error("The maximum bbox size is " + MAX_REQUEST_AREA.to_s + ", and your request was too large. Either request a smaller area, or use planet.osm")
 +      report_error("The maximum bbox size is " + MAX_REQUEST_AREA.to_s + 
 +        ", and your request was too large. Either request a smaller area, or use planet.osm")
        return
      end
  
      # get all the nodes
-     nodes = Node.find(:all, :conditions => 
-         ['latitude > ? AND longitude > ? AND latitude < ? AND longitude < ? AND visible = 1', min_lat, min_lon, max_lat, max_lon])
+     nodes = Node.find(:all, :conditions => ['latitude BETWEEN ? AND ? AND longitude BETWEEN ? AND ? AND visible = 1', min_lat, max_lat, min_lon, max_lon])
  
      node_ids = nodes.collect {|node| node.id }
  
      end
  
      if node_ids.length == 0
 -      render :text => "<osm version='0.4'></osm>", :content_type => "text/xml"
 +      render :text => "<osm version='0.5'></osm>", :content_type => "text/xml"
        return
      end
  
 -    # grab the segments
 -    segments = Array.new
 -    if node_ids.length > 0
 -      node_ids_sql = "(#{node_ids.join(',')})"
 -      # get the referenced segments
 -      segments = Segment.find_by_sql "select * from current_segments where visible = 1 and (node_a in #{node_ids_sql} or node_b in #{node_ids_sql})"
 -    end
 -    # see if we have any missing nodes
 -    segments_nodes = segments.collect {|segment| segment.node_a }
 -    segments_nodes += segments.collect {|segment| segment.node_b }
 -
 -    segments_nodes.uniq!
 -
 -    missing_nodes = segments_nodes - node_ids
 -
 -    # get missing nodes if there are any
 -    nodes += Node.find(missing_nodes) if missing_nodes.length > 0
 +    relations = Array.new
  
      doc = OSM::API.new.get_xml_doc
  
      # get ways
      # find which ways are needed
 -    segment_ids = segments.collect {|segment| segment.id }
      ways = Array.new
 -    if segment_ids.length > 0
 -      way_segments = WaySegment.find_all_by_segment_id(segment_ids)
 -      way_ids = way_segments.collect {|way_segment| way_segment.id }
 -      ways = Way.find(way_ids) # NB: doesn't pick up segments, tags from db until accessed via way.way_segments etc.
 -
 -      # seg_ids = way_segments.collect {|way_segment| way_segment.segment_id }
 -
 -      list_of_way_segs = ways.collect {|way| way.way_segments}
 -      list_of_way_segs.flatten!
 +    if node_ids.length > 0
 +      way_nodes = WayNode.find_all_by_node_id(node_ids)
 +      way_ids = way_nodes.collect {|way_node| way_node.id }
 +      ways = Way.find(way_ids)
  
 -      list_of_way_segments = list_of_way_segs.collect { |way_seg| way_seg.segment_id }
 +      list_of_way_nodes = ways.collect { |way|
 +      way.way_nodes.collect { |way_node| way_node.node_id }
 +      }
 +      list_of_way_nodes.flatten!
  
 -      else
 -        list_of_way_segments = Array.new
 +    else
 +      list_of_way_nodes = Array.new
      end
  
 -    # - [0] in case some thing links to segment 0 which doesn't exist. Shouldn't actually ever happen but it does. FIXME: file a ticket for this
 -    segments_to_fetch = (list_of_way_segments.uniq - segment_ids) - [0]
 +    # - [0] in case some thing links to node 0 which doesn't exist. Shouldn't actually ever happen but it does. FIXME: file a ticket for this
 +    nodes_to_fetch = (list_of_way_nodes.uniq - node_ids) - [0]
  
 -    if segments_to_fetch.length > 0
 -      segments += Segment.find(segments_to_fetch)
 +    if nodes_to_fetch.length > 0
 +      nodes += Node.find(nodes_to_fetch)
      end
  
 -    # get more nodes
 -    #
 -
 -    segments_nodes = segments.collect {|segment| segment.node_a }
 -    segments_nodes += segments.collect {|segment| segment.node_b }
 -
 -    node_ids_a = nodes.collect {|node| node.id }
 -
 -    nodes_to_get = segments_nodes - node_ids_a
 -    nodes += Node.find(nodes_to_get) if nodes_to_get.length > 0
 -
      visible_nodes = {}
      user_display_name_cache = {}
  
        end
      end
  
 -    visible_segments = {}
 -
 -    segments.each do |segment|
 -      if visible_nodes[segment.node_a] and visible_nodes[segment.node_b] and segment.visible?
 -        doc.root << segment.to_xml_node(user_display_name_cache) 
 -        visible_segments[segment.id] = segment
 -      end
 -    end
 -
 +    way_ids = Array.new
      ways.each do |way|
 -      doc.root << way.to_xml_node(visible_segments, user_display_name_cache) if way.visible?
 +      if way.visible?
 +        doc.root << way.to_xml_node(visible_nodes, user_display_name_cache)
 +        way_ids << way.id
 +      end
      end 
  
 +    # collect relationships. currently done in one big block at the end;
 +    # may need to move this upwards if people want automatic completion of
 +    # relationships, i.e. deliver referenced objects like we do with ways...
 +    relations = Relation.find_by_sql("select e.* from current_relations e,current_relation_members em where " +
 +        "e.visible=1 and " +
 +        "em.id = e.id and em.member_type='node' and em.member_id in (#{visible_nodes.keys.join(',')})")
 +    relations += Relation.find_by_sql("select e.* from current_relations e,current_relation_members em where " +
 +        "e.visible=1 and " +
 +        "em.id = e.id and em.member_type='way' and em.member_id in (#{way_ids.join(',')})")
 +    # 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)
 +    relation_ids = relations.collect { |relation| relation.id }
 +    if relation_ids.length > 0
 +        relations += Relation.find_by_sql("select e.* from current_relations e,current_relation_members em where " +
 +            "e.visible=1 and " +
 +            "em.id = e.id and em.member_type='relation' and em.member_id in (#{relation_ids.join(',')})")
 +    end
 +
 +    # 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.
 +    relations.uniq.each do |relation|
 +      doc.root << relation.to_xml_node(user_display_name_cache)
 +    end
 +
      render :text => doc.to_s, :content_type => "text/xml"
      
      #exit when we have too many requests
  
      api = XML::Node.new 'api'
      version = XML::Node.new 'version'
 -    version['minimum'] = '0.4';
 -    version['maximum'] = '0.4';
 +    version['minimum'] = '0.5';
 +    version['maximum'] = '0.5';
      api << version
      area = XML::Node.new 'area'
      area['maximum'] = MAX_REQUEST_AREA.to_s;
@@@ -13,12 -13,9 +13,9 @@@ class NodeController < ApplicationContr
        if node
          node.user_id = @user.id
          node.visible = true
+         node.save_with_history!
  
-         if node.save_with_history
-           render :text => node.id.to_s, :content_type => "text/plain"
-         else
-           render :nothing => true, :status => :internal_server_error
-         end
+         render :text => node.id.to_s, :content_type => "text/plain"
        else
          render :nothing => true, :status => :bad_request
        end
@@@ -38,8 -35,6 +35,6 @@@
        end
      rescue ActiveRecord::RecordNotFound
        render :nothing => true, :status => :not_found
-     rescue
-       render :nothing => true, :status => :internal_server_error
      end
    end
  
            node.latitude = new_node.latitude 
            node.longitude = new_node.longitude
            node.tags = new_node.tags
+           node.save_with_history!
  
-           if node.save_with_history
-             render :nothing => true
-           else
-             render :nothing => true, :status => :internal_server_error
-           end
+           render :nothing => true
          else
            render :nothing => true, :status => :bad_request
          end
@@@ -69,8 -61,6 +61,6 @@@
        end
      rescue ActiveRecord::RecordNotFound
        render :nothing => true, :status => :not_found
-     rescue
-       render :nothing => true, :status => :internal_server_error
      end
    end
  
        node = Node.find(params[:id])
  
        if node.visible
 -        if Segment.find(:first, :conditions => [ "visible = 1 and (node_a = ? or node_b = ?)", node.id, node.id])
 +        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 :nothing => true, :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 :nothing => true, :status => :precondition_failed
          else
            node.user_id = @user.id
            node.visible = 0
-           node.save_with_history
+           node.save_with_history!
            render :nothing => true
          end
        else
@@@ -94,8 -83,6 +85,6 @@@
        end
      rescue ActiveRecord::RecordNotFound
        render :nothing => true, :status => :not_found
-     rescue
-       render :nothing => true, :status => :internal_server_error
      end
    end
  
index efd5bf9,0000000..d00e9e0
mode 100644,000000..100644
--- /dev/null
@@@ -1,227 -1,0 +1,218 @@@
-           if relation.save_with_history
-             render :text => relation.id.to_s, :content_type => "text/plain"
-           else
-             render :text => "save error", :status => :internal_server_error
-           end
 +class RelationController < ApplicationController
 +  require 'xml/libxml'
 +
 +  before_filter :authorize, :only => [:create, :update, :delete]
 +  before_filter :check_availability, :only => [: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 :nothing => true, :status => :precondition_failed
 +        else
 +          relation.user_id = @user.id
++          relation.save_with_history!
 +
-             if relation.save_with_history
-               render :nothing => true
-             else
-               render :nothing => true, :status => :internal_server_error
-             end
++        render :text => relation.id.to_s, :content_type => "text/plain"
 +        end
 +      else
 +        render :nothing => true, :status => :bad_request
 +      end
 +    else
 +      render :nothing => true, :status => :method_not_allowed
 +    end
 +  end
 +
 +  def read
 +    begin
 +      relation = Relation.find(params[:id])
 +
 +      if relation.visible
 +        render :text => relation.to_xml.to_s, :content_type => "text/xml"
 +      else
 +        render :nothing => true, :status => :gone
 +      end
 +    rescue ActiveRecord::RecordNotFound
 +      render :nothing => true, :status => :not_found
 +    rescue
 +      render :nothing => true, :status => :internal_server_error
 +    end
 +  end
 +
 +  def update
 +    begin
 +      relation = Relation.find(params[:id])
 +
 +      if relation.visible
 +        new_relation = Relation.from_xml(request.raw_post)
 +
 +        if new_relation and new_relation.id == relation.id
 +          if !new_relation.preconditions_ok?
 +            render :nothing => true, :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!
 +
-           if relation.save_with_history
-             render :nothing => true
-           else
-             render :nothing => true, :status => :internal_server_error
-           end
++            render :nothing => true
 +          end
 +        else
 +          render :nothing => true, :status => :bad_request
 +        end
 +      else
 +        render :nothing => true, :status => :gone
 +      end
 +    rescue ActiveRecord::RecordNotFound
 +      render :nothing => true, :status => :not_found
 +    rescue
 +      render :nothing => true, :status => :internal_server_error
 +    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 :nothing => true, :status => :precondition_failed
 +        else
 +          relation.user_id = @user.id
 +          relation.tags = []
 +          relation.members = []
 +          relation.visible = false
++          relation.save_with_history!
 +
++          render :nothing => true
 +        end
 +      else
 +        render :nothing => true, :status => :gone
 +      end
 +    rescue ActiveRecord::RecordNotFound
 +      render :nothing => true, :status => :not_found
 +    rescue
 +      render :nothing => true, :status => :internal_server_error
 +    end
 +  end
 +
 +  # -----------------------------------------------------------------
 +  # full
 +  # 
 +  # input parameters: id
 +  #
 +  # returns XML representation of one relation object plus all its
 +  # members, plus all nodes part of member ways
 +  # -----------------------------------------------------------------
 +  def full
 +    begin
 +      relation = Relation.find(params[:id])
 +
 +      if relation.visible
 +
 +        # 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}");
 +        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}");
 +        # 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");
 +
 +        # now additionally collect nodes referenced by ways. Note how we recursively 
 +        # evaluate ways but NOT relations.
 +
 +        node_ids = nodes.collect {|node| node.id }
 +        way_node_ids = ways.collect { |way|
 +           way.way_nodes.collect { |way_node| way_node.node_id }
 +        }
 +        way_node_ids.flatten!
 +        way_node_ids.uniq
 +        way_node_ids -= node_ids
 +        nodes += Node.find(way_node_ids)
 +    
 +        # create XML.
 +        doc = OSM::API.new.get_xml_doc
 +        user_display_name_cache = {}
 +
 +        nodes.each do |node|
 +          if node.visible? # should be unnecessary if data is consistent.
 +            doc.root << node.to_xml_node(user_display_name_cache)
 +          end
 +        end
 +        ways.each do |way|
 +          if way.visible? # should be unnecessary if data is consistent.
 +            doc.root << way.to_xml_node(user_display_name_cache)
 +          end
 +        end
 +        relations.each do |rel|
 +          if rel.visible? # should be unnecessary if data is consistent.
 +            doc.root << rel.to_xml_node(user_display_name_cache)
 +          end
 +        end
 +        # finally add self and output
 +        doc.root << relation.to_xml_node(user_display_name_cache)
 +        render :text => doc.to_s, :content_type => "text/xml"
 +
 +      else
 +
 +        render :nothing => true, :status => :gone
 +      end
 +
 +    rescue ActiveRecord::RecordNotFound
 +      render :nothing => true, :status => :not_found
 +
 +    rescue
 +      render :nothing => true, :status => :internal_server_error
 +    end
 +  end
 +
 +  def relations
 +    ids = params['relations'].split(',').collect { |w| w.to_i }
 +
 +    if ids.length > 0
 +      doc = OSM::API.new.get_xml_doc
 +
 +      Relation.find(ids).each do |relation|
 +        doc.root << relation.to_xml_node
 +      end
 +
 +      render :text => doc.to_s, :content_type => "text/xml"
 +    else
 +      render :nothing => true, :status => :bad_request
 +    end
 +  end
 +
 +  def relations_for_way
 +    relations_for_object("way")
 +  end
 +  def relations_for_node
 +    relations_for_object("node")
 +  end
 +  def relations_for_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
 +
 +    if relationids.length > 0
 +      doc = OSM::API.new.get_xml_doc
 +
 +      Relation.find(relationids).each do |relation|
 +        doc.root << relation.to_xml_node
 +      end
 +
 +      render :text => doc.to_s, :content_type => "text/xml"
 +    else
 +      render :nothing => true, :status => :not_found
 +    end
 +  end
 +end
@@@ -20,10 -20,10 +20,10 @@@ class SwfController < ApplicationContro
                basey           =params['basey'].to_f
                masterscale     =params['masterscale'].to_f
        
-               xmin=params['xmin'].to_f; xminr=xmin/0.000001
-               xmax=params['xmax'].to_f; xmaxr=xmax/0.000001
-               ymin=params['ymin'].to_f; yminr=ymin/0.000001
-               ymax=params['ymax'].to_f; ymaxr=ymax/0.000001
+               xmin=params['xmin'].to_f;
+               xmax=params['xmax'].to_f;
+               ymin=params['ymin'].to_f;
+               ymax=params['ymax'].to_f;
        
                # -     Begin movie
        
        
                if params['token']
                          user=User.authenticate(:token => params[:token])
-                       sql="SELECT gps_points.latitude*0.000001 AS lat,gps_points.longitude*0.000001 AS lon,gpx_files.id AS fileid,UNIX_TIMESTAMP(gps_points.timestamp) AS ts "+
+                       sql="SELECT gps_points.latitude*0.0000001 AS lat,gps_points.longitude*0.0000001 AS lon,gpx_files.id AS fileid,UNIX_TIMESTAMP(gps_points.timestamp) AS ts "+
                                 " FROM gpx_files,gps_points "+
                                 "WHERE gpx_files.id=gpx_id "+
                                 "  AND gpx_files.user_id=#{user.id} "+
-                                "  AND (gps_points.longitude BETWEEN #{xminr} AND #{xmaxr}) "+
-                                "  AND (gps_points.latitude BETWEEN #{yminr} AND #{ymaxr}) "+
+                                "  AND "+OSM.sql_for_area(ymin,xmin,ymax,xmax,"gps_points.")+
                                 "  AND (gps_points.timestamp IS NOT NULL) "+
                                 "ORDER BY fileid DESC,ts "+
                                 "LIMIT 10000"
                else
-                       sql="SELECT latitude*0.000001 AS lat,longitude*0.000001 AS lon,gpx_id AS fileid,UNIX_TIMESTAMP(timestamp) AS ts "+
+                       sql="SELECT latitude*0.0000001 AS lat,longitude*0.0000001 AS lon,gpx_id AS fileid,UNIX_TIMESTAMP(timestamp) AS ts "+
                                 " FROM gps_points "+
-                                "WHERE (longitude BETWEEN #{xminr} AND #{xmaxr}) "+
-                                "  AND (latitude  BETWEEN #{yminr} AND #{ymaxr}) "+
+                                "WHERE "+OSM.sql_for_area(ymin,xmin,ymax,xmax,"gps_points.")+
                                 "  AND (gps_points.timestamp IS NOT NULL) "+
                                 "ORDER BY fileid DESC,ts "+
                                 "LIMIT 10000"
@@@ -94,7 -92,7 +92,7 @@@
                        sql="SELECT cn1.latitude AS lat1,cn1.longitude AS lon1,"+
                                "               cn2.latitude AS lat2,cn2.longitude AS lon2 "+
                                "  FROM current_segments "+
 -                              "       LEFT OUTER JOIN current_way_segments"+
 +                              "       LEFT OUTER JOIN current_way_nodes"+
                                "       ON segment_id=current_segments.id,"+
                                "       current_nodes AS cn1,current_nodes AS cn2"+
                                " WHERE (cn1.longitude BETWEEN #{xmin} AND #{xmax})"+
@@@ -15,12 -15,9 +15,9 @@@ class WayController < ApplicationContro
            render :nothing => true, :status => :precondition_failed
          else
            way.user_id = @user.id
+           way.save_with_history!
  
-           if way.save_with_history
-             render :text => way.id.to_s, :content_type => "text/plain"
-           else
-             render :nothing => true, :status => :internal_server_error
-           end
+           render :text => way.id.to_s, :content_type => "text/plain"
          end
        else
          render :nothing => true, :status => :bad_request
@@@ -41,8 -38,6 +38,6 @@@
        end
      rescue ActiveRecord::RecordNotFound
        render :nothing => true, :status => :not_found
-     rescue
-       render :nothing => true, :status => :internal_server_error
      end
    end
  
            else
              way.user_id = @user.id
              way.tags = new_way.tags
 -            way.segs = new_way.segs
 +            way.nds = new_way.nds
              way.visible = true
+             way.save_with_history!
  
-             if way.save_with_history
-               render :nothing => true
-             else
-               render :nothing => true, :status => :internal_server_error
-             end
+             render :nothing => true
            end
          else
            render :nothing => true, :status => :bad_request
@@@ -76,8 -68,6 +68,6 @@@
        end
      rescue ActiveRecord::RecordNotFound
        render :nothing => true, :status => :not_found
-     rescue
-       render :nothing => true, :status => :internal_server_error
      end
    end
  
        way = Way.find(params[:id])
  
        if way.visible
 -        way.user_id = @user.id
 -        way.tags = []
 -        way.segs = []
 -        way.visible = false
 -        way.save_with_history!
 +        if RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", :conditions => [ "visible = 1 AND member_type='way' and member_id=?", params[:id]])
 +          render :nothing => true, :status => :precondition_failed
 +        else
 +          way.user_id = @user.id
 +          way.tags = []
 +          way.nds = []
 +          way.visible = false
++        way.save_with_history!
  
-           if way.save_with_history
-             render :nothing => true
-           else
-             render :nothing => true, :status => :internal_server_error
-           end
 -        render :nothing => true
++        render :nothing => true
 +        end
        else
          render :nothing => true, :status => :gone
        end
      rescue ActiveRecord::RecordNotFound
        render :nothing => true, :status => :not_found
-     rescue
-       render :nothing => true, :status => :internal_server_error
++    rescue => ex
++      puts ex
      end
    end
  
        way = Way.find(params[:id])
  
        if way.visible
 -        # In future, we might want to do all the data fetch in one step
 -        seg_ids = way.segs + [-1]
 -        segments = Segment.find_by_sql "select * from current_segments where visible = 1 and id IN (#{seg_ids.join(',')})"
 -
 -        node_ids = segments.collect {|segment| segment.node_a }
 -        node_ids += segments.collect {|segment| segment.node_b }
 -        node_ids += [-1]
 -        nodes = Node.find(:all, :conditions => "visible = 1 AND id IN (#{node_ids.join(',')})")
 +        nd_ids = way.nds + [-1]
 +        nodes = Node.find(:all, :conditions => "visible = 1 AND id IN (#{nd_ids.join(',')})")
  
          # Render
          doc = OSM::API.new.get_xml_doc
          nodes.each do |node|
            doc.root << node.to_xml_node()
          end
 -        segments.each do |segment|
 -          doc.root << segment.to_xml_node()
 -        end
          doc.root << way.to_xml_node()
  
          render :text => doc.to_s, :content_type => "text/xml"
        end
      rescue ActiveRecord::RecordNotFound
        render :nothing => true, :status => :not_found
-     rescue
-       render :nothing => true, :status => :internal_server_error
      end
    end
  
      end
    end
  
 -  def ways_for_segment
 -    wayids = WaySegment.find(:all, :conditions => ['segment_id = ?', params[:id]]).collect { |ws| ws.id }.uniq
 +  def ways_for_node
 +    wayids = WayNode.find(:all, :conditions => ['node_id = ?', params[:id]]).collect { |ws| ws.id }.uniq
  
      if wayids.length > 0
        doc = OSM::API.new.get_xml_doc
diff --combined app/models/relation.rb
index f74a149,0000000..a5d463f
mode 100644,000000..100644
--- /dev/null
@@@ -1,213 -1,0 +1,207 @@@
-   def save_with_history
-     begin
-       Relation.transaction do
-         t = Time.now
-         self.timestamp = t
-         self.save!
-         tags = self.tags
-         RelationTag.delete_all(['id = ?', self.id])
-         tags.each do |k,v|
-           tag = RelationTag.new
-           tag.k = k
-           tag.v = v
-           tag.id = self.id
-           tag.save!
-         end
-         members = self.members
 +class Relation < ActiveRecord::Base
 +  require 'xml/libxml'
 +  
 +  belongs_to :user
 +
 +  has_many :relation_members, :foreign_key => 'id'
 +  has_many :relation_tags, :foreign_key => 'id'
 +
 +  has_many :old_relations, :foreign_key => 'id', :order => 'version'
 +
 +  set_table_name 'current_relations'
 +
 +  def self.from_xml(xml, create=false)
 +    begin
 +      p = XML::Parser.new
 +      p.string = xml
 +      doc = p.parse
 +
 +      relation = Relation.new
 +
 +      doc.find('//osm/relation').each do |pt|
 +        if !create and pt['id'] != '0'
 +          relation.id = pt['id'].to_i
 +        end
 +
 +        if create
 +          relation.timestamp = Time.now
 +          relation.visible = true
 +        else
 +          if pt['timestamp']
 +            relation.timestamp = Time.parse(pt['timestamp'])
 +          end
 +        end
 +
 +        pt.find('tag').each do |tag|
 +          relation.add_tag_keyval(tag['k'], tag['v'])
 +        end
 +
 +        pt.find('member').each do |member|
 +          relation.add_member(member['type'], member['ref'], member['role'])
 +        end
 +      end
 +    rescue
 +      relation = nil
 +    end
 +
 +    return relation
 +  end
 +
 +  def to_xml
 +    doc = OSM::API.new.get_xml_doc
 +    doc.root << to_xml_node()
 +    return doc
 +  end
 +
 +  def to_xml_node(user_display_name_cache = nil)
 +    el1 = XML::Node.new 'relation'
 +    el1['id'] = self.id.to_s
 +    el1['visible'] = self.visible.to_s
 +    el1['timestamp'] = self.timestamp.xmlschema
 +
 +    user_display_name_cache = {} if user_display_name_cache.nil?
 +    
 +    if user_display_name_cache and user_display_name_cache.key?(self.user_id)
 +      # use the cache if available
 +    elsif self.user.data_public?
 +      user_display_name_cache[self.user_id] = self.user.display_name
 +    else
 +      user_display_name_cache[self.user_id] = nil
 +    end
 +
 +    el1['user'] = user_display_name_cache[self.user_id] unless user_display_name_cache[self.user_id].nil?
 +
 +    self.relation_members.each do |member|
 +      p=0
 +      #if visible_members
 +      #  # if there is a list of visible members then use that to weed out deleted segments
 +      #  if visible_members[member.member_type][member.member_id]
 +      #    p=1
 +      #  end
 +      #else
 +        # otherwise, manually go to the db to check things
 +        if member.member.visible?
 +          p=1
 +        end
 +      #end
 +      if p
 +        e = XML::Node.new 'member'
 +        e['type'] = member.member_type
 +        e['ref'] = member.member_id.to_s 
 +        e['role'] = member.member_role
 +        el1 << e
 +       end
 +    end
 +
 +    self.relation_tags.each do |tag|
 +      e = XML::Node.new 'tag'
 +      e['k'] = tag.k
 +      e['v'] = tag.v
 +      el1 << e
 +    end
 +    return el1
 +  end 
 +
 +  # FIXME is this really needed?
 +  def members
 +    unless @members
 +        @members = Array.new
 +        self.relation_members.each do |member|
 +            @members += [[member.member_type,member.member_id,member.member_role]]
 +        end
 +    end
 +    @members
 +  end
 +
 +  def tags
 +    unless @tags
 +        @tags = Hash.new
 +        self.relation_tags.each do |tag|
 +            @tags[tag.k] = tag.v
 +        end
 +    end
 +    @tags
 +  end
 +
 +  def members=(m)
 +    @members = m
 +  end
 +
 +  def tags=(t)
 +    @tags = t
 +  end
 +
 +  def add_member(type,id,role)
 +    @members = Array.new unless @members
 +    @members += [[type,id,role]]
 +  end
 +
 +  def add_tag_keyval(k, v)
 +    @tags = Hash.new unless @tags
 +    @tags[k] = v
 +  end
 +
-         RelationMember.delete_all(['id = ?', self.id])
++  def save_with_history!
++    Relation.transaction do
++      t = Time.now
++      self.timestamp = t
++      self.save!
++
++      tags = self.tags
++
++      RelationTag.delete_all(['id = ?', self.id])
++
++      tags.each do |k,v|
++      tag = RelationTag.new
++      tag.k = k
++      tag.v = v
++      tag.id = self.id
++      tag.save!
++      end
 +
-         members.each do |n|
-           mem = RelationMember.new
-           mem.id = self.id
-           mem.member_type = n[0];
-           mem.member_id = n[1];
-           mem.member_role = n[2];
-           mem.save!
-         end
++      members = self.members
 +
-         old_relation = OldRelation.from_relation(self)
-         old_relation.timestamp = t
-         old_relation.save_with_dependencies!
++      RelationMember.delete_all(['id = ?', self.id])
 +
-       return true
-     rescue Exception => ex
-       return nil
++      members.each do |n|
++      mem = RelationMember.new
++      mem.id = self.id
++      mem.member_type = n[0];
++      mem.member_id = n[1];
++      mem.member_role = n[2];
++      mem.save!
 +      end
 +
++      old_relation = OldRelation.from_relation(self)
++      old_relation.timestamp = t
++      old_relation.save_with_dependencies!
 +    end
 +  end
 +
 +  def preconditions_ok?
 +    self.members.each do |m|
 +      if (m[0] == "node")
 +        n = Node.find(:first, :conditions => ["id = ?", m[1]])
 +        unless n and n.visible 
 +          return false
 +        end
 +      elsif (m[0] == "way")
 +        w = Way.find(:first, :conditions => ["id = ?", m[1]])
 +        unless w and w.visible and w.preconditions_ok?
 +          return false
 +        end
 +      elsif (m[0] == "relation")
 +        e = Relation.find(:first, :conditions => ["id = ?", m[1]])
 +        unless e and e.visible and e.preconditions_ok?
 +          return false
 +        end
 +      else
 +        return false
 +      end
 +    end
 +    return true
 +  rescue
 +    return false
 +  end
 +
 +end
diff --combined app/models/way.rb
@@@ -3,7 -3,7 +3,7 @@@ class Way < ActiveRecord::Bas
    
    belongs_to :user
  
 -  has_many :way_segments, :foreign_key => 'id', :order => 'sequence_id'
 +  has_many :way_nodes, :foreign_key => 'id', :order => 'sequence_id'
    has_many :way_tags, :foreign_key => 'id'
  
    has_many :old_ways, :foreign_key => 'id', :order => 'version'
@@@ -36,8 -36,8 +36,8 @@@
            way.add_tag_keyval(tag['k'], tag['v'])
          end
  
 -        pt.find('seg').each do |seg|
 -          way.add_seg_num(seg['id'])
 +        pt.find('nd').each do |nd|
 +          way.add_nd_num(nd['ref'])
          end
        end
      rescue
@@@ -53,7 -53,7 +53,7 @@@
      return doc
    end
  
 -  def to_xml_node(visible_segments = nil, user_display_name_cache = nil)
 +  def to_xml_node(visible_nodes = nil, user_display_name_cache = nil)
      el1 = XML::Node.new 'way'
      el1['id'] = self.id.to_s
      el1['visible'] = self.visible.to_s
  
      el1['user'] = user_display_name_cache[self.user_id] unless user_display_name_cache[self.user_id].nil?
  
 -    # make sure segments are output in sequence_id order
 -    ordered_segments = []
 -    self.way_segments.each do |seg|
 -      if visible_segments
 -        # if there is a list of visible segments then use that to weed out deleted segments
 -        if visible_segments[seg.segment_id]
 -          ordered_segments[seg.sequence_id] = seg.segment_id.to_s
 +    # make sure nodes are output in sequence_id order
 +    ordered_nodes = []
 +    self.way_nodes.each do |nd|
 +      if visible_nodes
 +        # if there is a list of visible nodes then use that to weed out deleted nodes
 +        if visible_nodes[nd.node_id]
 +          ordered_nodes[nd.sequence_id] = nd.node_id.to_s
          end
        else
          # otherwise, manually go to the db to check things
 -        if seg.segment.visible? and seg.segment.from_node.visible? and seg.segment.to_node.visible?
 -          ordered_segments[seg.sequence_id] = seg.segment_id.to_s
 +        if nd.node.visible? and nd.node.visible?
 +          ordered_nodes[nd.sequence_id] = nd.node_id.to_s
          end
        end
      end
  
 -    ordered_segments.each do |seg_id|
 -      if seg_id and seg_id != '0'
 -        e = XML::Node.new 'seg'
 -        e['id'] = seg_id
 +    ordered_nodes.each do |nd_id|
 +      if nd_id and nd_id != '0'
 +        e = XML::Node.new 'nd'
 +        e['ref'] = nd_id
          el1 << e
        end
      end
      return el1
    end 
  
 -  def segs
 -    unless @segs
 -        @segs = Array.new
 -        self.way_segments.each do |seg|
 -            @segs += [seg.segment_id]
 +  def nds
 +    unless @nds
 +        @nds = Array.new
 +        self.way_nodes.each do |nd|
 +            @nds += [nd.node_id]
          end
      end
 -    @segs
 +    @nds
    end
  
    def tags
      @tags
    end
  
 -  def segs=(s)
 -    @segs = s
 +  def nds=(s)
 +    @nds = s
    end
  
    def tags=(t)
      @tags = t
    end
  
 -  def add_seg_num(n)
 -    @segs = Array.new unless @segs
 -    @segs << n.to_i
 +  def add_nd_num(n)
 +    @nds = Array.new unless @nds
 +    @nds << n.to_i
    end
  
    def add_tag_keyval(k, v)
      @tags[k] = v
    end
  
-   def save_with_history
-     begin
-       Way.transaction do
-         t = Time.now
-         self.timestamp = t
-         self.save!
+   def save_with_history!
+     t = Time.now
  
-         tags = self.tags
+     Way.transaction do
+       self.timestamp = t
+       self.save!
+     end
  
-         WayTag.delete_all(['id = ?', self.id])
+     WayTag.transaction do
+       tags = self.tags
  
-         tags.each do |k,v|
-           tag = WayTag.new
-           tag.k = k
-           tag.v = v
-           tag.id = self.id
-           tag.save!
-         end
+       WayTag.delete_all(['id = ?', self.id])
  
-         nds = self.nds
+       tags.each do |k,v|
+         tag = WayTag.new
+         tag.k = k
+         tag.v = v
+         tag.id = self.id
+         tag.save!
+       end
+     end
  
-         WayNode.delete_all(['id = ?', self.id])
 -    WaySegment.transaction do
 -      segs = self.segs
++    WayNode.transaction do
++      nds = self.nds
  
-         i = 1
-         nds.each do |n|
-           nd = WayNode.new
-           nd.id = self.id
-           nd.node_id = n
-           nd.sequence_id = i
-           nd.save!
-           i += 1
-         end
 -      WaySegment.delete_all(['id = ?', self.id])
++      WayNode.delete_all(['id = ?', self.id])
  
-         old_way = OldWay.from_way(self)
-         old_way.timestamp = t
-         old_way.save_with_dependencies!
+       i = 1
 -      segs.each do |n|
 -        seg = WaySegment.new
 -        seg.id = self.id
 -        seg.segment_id = n
 -        seg.sequence_id = i
 -        seg.save!
++      nds.each do |n|
++        nd = WayNode.new
++        nd.id = self.id
++        nd.node_id = n
++        nd.sequence_id = i
++        nd.save!
+         i += 1
        end
-       return true
-     rescue => ex
-       puts ex
-       return nil
      end
+     old_way = OldWay.from_way(self)
+     old_way.timestamp = t
+     old_way.save_with_dependencies!
    end
  
    def preconditions_ok?
 -    return false if self.segs.empty?
 -    self.segs.each do |n|
 -      segment = Segment.find(:first, :conditions => ["id = ?", n])
 -      unless segment and segment.visible and segment.preconditions_ok?
 +    return false if self.nds.empty?
 +    self.nds.each do |n|
 +      node = Node.find(:first, :conditions => ["id = ?", n])
 +      unless node and node.visible
          return false
        end
      end
diff --combined config/environment.rb
@@@ -1,5 -1,8 +1,8 @@@
  # Be sure to restart your web server when you modify this file.
  
+ # Limit each rails process to a 512Mb resident set size
+ Process.setrlimit Process::RLIMIT_AS, 640*1024*1024, Process::RLIM_INFINITY
  # Uncomment below to force Rails into production mode when 
  # you don't control web/app server and can't set it the proper way
  ENV['RAILS_ENV'] ||= 'production'
@@@ -14,7 -17,7 +17,7 @@@ RAILS_GEM_VERSION = '1.2.3
  require File.join(File.dirname(__FILE__), 'boot')
  
  # Application constants needed for routes.rb - must go before Initializer call
 -API_VERSION = ENV['OSM_API_VERSION'] || '0.4'
 +API_VERSION = ENV['OSM_API_VERSION'] || '0.5'
  
  # Custom logger class to format messages sensibly
  class OSMLogger < Logger
index a30642e,0000000..a30642e
mode 100644,000000..100644
--- /dev/null
@@@ -1,83 -1,0 +1,83 @@@
 +class AddRelations < ActiveRecord::Migration
 +  def self.up
 +    # a relation can have members much like a way can have nodes.
 +    # differences:
 +    # way: only nodes / relation: any kind of member
 +    # way: ordered sequence of nodes / relation: free-form "role" string
 +    create_table "current_relation_members", innodb_table do |t|
 +      t.column "id",          :bigint, :limit => 64, :null => false
 +      t.column "member_type", :string, :limit => 11, :null => false
 +      t.column "member_id",   :bigint, :limit => 11, :null => false
 +      t.column "member_role", :string
 +    end
 +    # enums work like strings but are more efficient
 +    execute "alter table current_relation_members change column member_type member_type enum('node','way','relation');"
 +
 +    add_primary_key "current_relation_members", ["id", "member_type", "member_id", "member_role"]
 +    add_index "current_relation_members", ["member_type", "member_id"], :name => "current_relation_members_member_idx"
 +    # the following is obsolete given the primary key, is it not?
 +    # add_index "current_relation_members", ["id"], :name => "current_relation_members_id_idx"
 +    create_table "current_relation_tags", myisam_table do |t|
 +      t.column "id", :bigint, :limit => 64, :null => false
 +      t.column "k",  :string, :default => "", :null => false
 +      t.column "v",  :string, :default => "", :null => false
 +    end
 +
 +    add_index "current_relation_tags", ["id"], :name => "current_relation_tags_id_idx"
 +    execute "CREATE FULLTEXT INDEX `current_relation_tags_v_idx` ON `current_relation_tags` (`v`)"
 +
 +    create_table "current_relations", innodb_table do |t|
 +      t.column "id",        :bigint,   :limit => 64, :null => false
 +      t.column "user_id",   :bigint,   :limit => 20, :null => false
 +      t.column "timestamp", :datetime, :null => false
 +      t.column "visible",   :boolean,  :null => false
 +    end
 +
 +    add_primary_key "current_relations", ["id"]
 +    change_column "current_relations", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
 +
 +    create_table "relation_members", myisam_table do |t|
 +      t.column "id",          :bigint,  :limit => 64, :default => 0, :null => false
 +      t.column "member_type", :string, :limit => 11, :null => false
 +      t.column "member_id",   :bigint, :limit => 11, :null => false
 +      t.column "member_role", :string
 +      t.column "version",     :bigint,  :limit => 20, :default => 0, :null => false
 +    end
 +
 +    execute "alter table relation_members change column member_type member_type enum('node','way','relation');" 
 +    add_primary_key "relation_members", ["id", "version", "member_type", "member_id", "member_role"]
 +    add_index "relation_members", ["member_type", "member_id"], :name => "relation_members_member_idx"
 +
 +    create_table "relation_tags", myisam_table do |t|
 +      t.column "id",      :bigint,  :limit => 64, :default => 0, :null => false
 +      t.column "k",       :string, :null => false, :default => ""
 +      t.column "v",       :string, :null => false, :default => ""
 +      t.column "version", :bigint,  :limit => 20, :null => false
 +    end
 +
 +    add_index "relation_tags", ["id", "version"], :name => "relation_tags_id_version_idx"
 +
 +    create_table "relations", myisam_table do |t|
 +      t.column "id",        :bigint,   :limit => 64, :null => false, :default => 0
 +      t.column "user_id",   :bigint,   :limit => 20, :null => false
 +      t.column "timestamp", :datetime,               :null => false
 +      t.column "version",   :bigint,   :limit => 20, :null => false
 +      t.column "visible",   :boolean,                :null => false, :default => true
 +    end
 +
 +    add_primary_key "relations", ["id", "version"]
 +    add_index "relations", ["timestamp"], :name => "relations_timestamp_idx"
 +    
 +    change_column "relations", "version", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
 +  end
 +
 +
 +  def self.down
 +    drop_table :relations
 +    drop_table :current_relations
 +    drop_table :relation_tags
 +    drop_table :current_relation_tags
 +    drop_table :relation_members
 +    drop_table :current_relation_members
 +  end
 +end
index de9557c,0000000..492beda
mode 100644,000000..100644
--- /dev/null
@@@ -1,89 -1,0 +1,89 @@@
-       prefix = File.join Dir.tmpdir, "006_remove_segments.#{$$}."
 +require 'lib/migrate'
 +
 +class RemoveSegments < ActiveRecord::Migration
 +  def self.up
 +    have_segs = select_value("SELECT count(*) FROM current_segments").to_i != 0
 +
 +    if have_segs
-       cmd = "db/migrate/006_remove_segments_helper"
++      prefix = File.join Dir.tmpdir, "007_remove_segments.#{$$}."
 +
++      cmd = "db/migrate/007_remove_segments_helper"
 +      src = "#{cmd}.cc"
 +      if not File.exists? cmd or File.mtime(cmd) < File.mtime(src) then 
 +      system 'c++ -O3 -Wall `mysql_config --cflags --libs` ' +
 +        "#{src} -o #{cmd}" or fail
 +      end
 +
 +      conn_opts = ActiveRecord::Base.connection.
 +      instance_eval { @connection_options }
 +      args = conn_opts.map { |arg| arg.to_s } + [prefix]
 +      fail "#{cmd} failed" unless system cmd, *args
 +
 +      tempfiles = ['ways', 'way_nodes', 'way_tags',
 +      'relations', 'relation_members', 'relation_tags'].
 +      map { |base| prefix + base }
 +      ways, way_nodes, way_tags,
 +      relations, relation_members, relation_tags = tempfiles
 +    end
 +
 +    drop_table :segments
 +    drop_table :way_segments
 +    create_table :way_nodes, myisam_table do |t|
 +      t.column :id,          :bigint, :limit => 64, :null => false
 +      t.column :node_id,     :bigint, :limit => 64, :null => false
 +      t.column :version,     :bigint, :limit => 20, :null => false
 +      t.column :sequence_id, :bigint, :limit => 11, :null => false
 +    end
 +    add_primary_key :way_nodes, [:id, :version, :sequence_id]
 +
 +    drop_table :current_segments
 +    drop_table :current_way_segments
 +    create_table :current_way_nodes, innodb_table do |t|
 +      t.column :id,          :bigint, :limit => 64, :null => false
 +      t.column :node_id,     :bigint, :limit => 64, :null => false
 +      t.column :sequence_id, :bigint, :limit => 11, :null => false
 +    end
 +    add_primary_key :current_way_nodes, [:id, :sequence_id]
 +
 +    execute "TRUNCATE way_tags"
 +    execute "TRUNCATE ways"
 +    execute "TRUNCATE current_way_tags"
 +    execute "TRUNCATE current_ways"
 +
 +    # now get the data back
 +    csvopts = "FIELDS TERMINATED BY ',' ENCLOSED BY '\"' ESCAPED BY '\"' LINES TERMINATED BY '\\n'"
 +
 +    tempfiles.each { |fn| File.chmod 0644, fn } if have_segs
 +
 +    if have_segs
 +      execute "LOAD DATA LOCAL INFILE '#{ways}' INTO TABLE ways #{csvopts} (id, user_id, timestamp) SET visible = 1, version = 1"
 +      execute "LOAD DATA LOCAL INFILE '#{way_nodes}' INTO TABLE way_nodes #{csvopts} (id, node_id, sequence_id) SET version = 1"
 +      execute "LOAD DATA LOCAL INFILE '#{way_tags}' INTO TABLE way_tags #{csvopts} (id, k, v) SET version = 1"
 +
 +      execute "INSERT INTO current_ways SELECT id, user_id, timestamp, visible FROM ways"
 +      execute "INSERT INTO current_way_nodes SELECT id, node_id, sequence_id FROM way_nodes"
 +      execute "INSERT INTO current_way_tags SELECT id, k, v FROM way_tags"
 +    end
 +
 +    # and then readd the index
 +    add_index :current_way_nodes, [:node_id], :name => "current_way_nodes_node_idx"
 +
 +    if have_segs
 +      execute "LOAD DATA LOCAL INFILE '#{relations}' INTO TABLE relations #{csvopts} (id, user_id, timestamp) SET visible = 1, version = 1"
 +      execute "LOAD DATA LOCAL INFILE '#{relation_members}' INTO TABLE relation_members #{csvopts} (id, member_type, member_id, member_role) SET version = 1"
 +      execute "LOAD DATA LOCAL INFILE '#{relation_tags}' INTO TABLE relation_tags #{csvopts} (id, k, v) SET version = 1"
 +
 +      # FIXME: This will only work if there were no relations before the
 +      # migration!
 +      execute "INSERT INTO current_relations SELECT id, user_id, timestamp, visible FROM relations"
 +      execute "INSERT INTO current_relation_members SELECT id, member_type, member_id, member_role FROM relation_members"
 +      execute "INSERT INTO current_relation_tags SELECT id, k, v FROM relation_tags"
 +    end
 +
 +    tempfiles.each { |fn| File.unlink fn } if have_segs
 +  end
 +
 +  def self.down
 +    raise IrreversibleMigration.new
 +  end
 +end
index dd2176a,0000000..2234a3b
mode 100644,000000..100644
--- /dev/null
@@@ -1,591 -1,0 +1,591 @@@
-     fprintf(stderr, "005_remove_segments_helper: MySQL error: %s\n", err);
 +#include <mysql.h>
 +#include <stdint.h>
 +#include <stdio.h>
 +#include <stdlib.h>
 +#include <string.h>
 +#include <vector>
 +#include <list>
 +#include <sstream>
 +
 +#ifdef __amd64__
 +
 +#define F_U64 "%lu"
 +#define F_U32 "%u"
 +
 +#else
 +
 +#define F_U64 "%Lu"
 +#define F_U32 "%u"
 +
 +#endif
 +
 +using namespace std;
 +
 +template <typename T>
 +static T parse(const char *str) {
 +  istringstream in(str);
 +  T t;
 +  in >> t;
 +  return t;
 +}
 +
 +static void exit_mysql_err(MYSQL *mysql) {
 +  const char *err = mysql_error(mysql);
 +  if (err) {
-     fprintf(stderr, "005_remove_segments_helper: MySQL error\n");
++    fprintf(stderr, "007_remove_segments_helper: MySQL error: %s\n", err);
 +  } else {
-     fprintf(stderr, "005_remove_segments_helper: MySQL stmt error: %s\n", err);
++    fprintf(stderr, "007_remove_segments_helper: MySQL error\n");
 +  }
 +  abort();
 +  exit(EXIT_FAILURE);
 +}
 +
 +static void exit_stmt_err(MYSQL_STMT *stmt) {
 +  const char *err = mysql_stmt_error(stmt);
 +  if (err) {
-     fprintf(stderr, "005_remove_segments_helper: MySQL stmt error\n");
++    fprintf(stderr, "007_remove_segments_helper: MySQL stmt error: %s\n", err);
 +  } else {
-     printf("Usage: 006_remove_segments_helper host user passwd database port socket prefix\n");
++    fprintf(stderr, "007_remove_segments_helper: MySQL stmt error\n");
 +  }
 +  abort();
 +  exit(EXIT_FAILURE);
 +}
 +
 +struct segment {
 +  uint32_t from, to;
 +};
 +
 +struct data {
 +  MYSQL *mysql, *mysql2;
 +
 +  uint64_t seg_maxid, way_maxid;
 +  uint64_t new_way_id;
 +  uint64_t new_relation_id;
 +
 +  size_t segs_len;
 +  struct segment *segs;
 +  unsigned char *rem_segs;
 +
 +  FILE *ways, *way_nodes, *way_tags,
 +    *relations, *relation_members, *relation_tags;
 +};
 +
 +static uint64_t select_u64(MYSQL *mysql, const char *q) {
 +  MYSQL_RES *res;
 +  MYSQL_ROW row;
 +  uint64_t ret;
 +
 +  if (mysql_query(mysql, q))
 +    exit_mysql_err(mysql);
 +
 +  res = mysql_store_result(mysql);
 +  if (!res) exit_mysql_err(mysql);
 +
 +  row = mysql_fetch_row(res);
 +  if (!row) exit_mysql_err(mysql);
 +
 +  if (row[0]) {
 +    ret = parse<uint64_t>(row[0]);
 +  } else {
 +    ret = 0;
 +  }
 +
 +  mysql_free_result(res);
 +
 +  return ret;
 +}
 +
 +static void find_maxids(struct data *d) {
 +  d->seg_maxid = select_u64(d->mysql, "SELECT max(id) FROM current_segments");
 +  d->segs_len = d->seg_maxid + 1;
 +  d->way_maxid = select_u64(d->mysql, "SELECT max(id) FROM current_ways");
 +  d->new_way_id = d->way_maxid + 1;
 +  d->new_relation_id = select_u64(d->mysql, "SELECT max(id) FROM current_relations") + 1;
 +}
 +
 +static void populate_segs(struct data *d) {
 +  MYSQL_RES *res;
 +  MYSQL_ROW row;
 +  size_t id;
 +
 +  d->segs = (segment *) malloc(sizeof(struct segment) * d->segs_len);
 +  memset(d->segs, 0, sizeof(struct segment) * d->segs_len);
 +
 +  d->rem_segs = (unsigned char *) malloc(d->segs_len);
 +  memset(d->rem_segs, 0, d->segs_len);
 +
 +  if (mysql_query(d->mysql, "SELECT id, node_a, node_b "
 +      "FROM current_segments WHERE visible"))
 +    exit_mysql_err(d->mysql);
 +
 +  res = mysql_use_result(d->mysql);
 +  if (!res) exit_mysql_err(d->mysql);
 +
 +  while ((row = mysql_fetch_row(res))) {
 +    id = parse<size_t>(row[0]);
 +    if (id >= d->segs_len) continue;
 +    d->segs[id].from = parse<uint32_t>(row[1]);
 +    d->segs[id].to   = parse<uint32_t>(row[2]);
 +    d->rem_segs[id] = 1;
 +  }
 +  if (mysql_errno(d->mysql)) exit_mysql_err(d->mysql);
 +
 +  mysql_free_result(res);
 +}
 +
 +static void write_csv_col(FILE *f, const char *str, char end) {
 +  char *out = (char *) malloc(2 * strlen(str) + 4);
 +  char *o = out;
 +  size_t len;
 +
 +  *(o++) = '\"';
 +  for (; *str; str++) {
 +    if (*str == '\0') {
 +      break;
 +    } else if (*str == '\"') {
 +      *(o++) = '\"';
 +      *(o++) = '\"';
 +    } else {
 +      *(o++) = *str;
 +    }
 +  }
 +  *(o++) = '\"';
 +  *(o++) = end;
 +  *(o++) = '\0';
 +
 +  len = strlen(out);
 +  if (fwrite(out, len, 1, f) != 1) {
 +    perror("fwrite");
 +    exit(EXIT_FAILURE);
 +  }
 +
 +  free(out);
 +}
 +
 +static void convert_ways(struct data *d) {
 +  MYSQL_RES *res;
 +  MYSQL_ROW row;
 +  MYSQL_STMT *load_segs, *load_tags;
 +  const char
 +    load_segs_stmt[] = "SELECT segment_id FROM current_way_segments "
 +      "WHERE id = ? ORDER BY sequence_id",
 +    load_tags_stmt[] = "SELECT k, v FROM current_way_tags WHERE id = ?";
 +  char *k, *v;
 +  const size_t max_tag_len = 1 << 16;
 +  long long mysql_id, mysql_seg_id;
 +  unsigned long res_len;
 +  my_bool res_error;
 +  MYSQL_BIND bind[1], seg_bind[1], tag_bind[2];
 +
 +  /* F***ing libmysql only support fixed size buffers for string results of
 +   * prepared statements.  So allocate 65k for the tag key and the tag value
 +   * and hope it'll suffice. */
 +  k = (char *) malloc(max_tag_len);
 +  v = (char *) malloc(max_tag_len);
 +
 +  load_segs = mysql_stmt_init(d->mysql2);
 +  if (!load_segs) exit_mysql_err(d->mysql2);
 +  if (mysql_stmt_prepare(load_segs, load_segs_stmt, sizeof(load_segs_stmt)))
 +    exit_stmt_err(load_segs);
 +
 +  memset(bind, 0, sizeof(bind));
 +  bind[0].buffer_type = MYSQL_TYPE_LONGLONG;
 +  bind[0].buffer = (char *) &mysql_id;
 +  bind[0].is_null = 0;
 +  bind[0].length = 0;
 +  if (mysql_stmt_bind_param(load_segs, bind))
 +    exit_stmt_err(load_segs);
 +
 +  memset(bind, 0, sizeof(seg_bind));
 +  seg_bind[0].buffer_type = MYSQL_TYPE_LONGLONG;
 +  seg_bind[0].buffer = (char *) &mysql_seg_id;
 +  seg_bind[0].is_null = 0;
 +  seg_bind[0].length = 0;
 +  seg_bind[0].error = &res_error;
 +  if (mysql_stmt_bind_result(load_segs, seg_bind))
 +    exit_stmt_err(load_segs);
 +
 +  load_tags = mysql_stmt_init(d->mysql2);
 +  if (!load_tags) exit_mysql_err(d->mysql2);
 +  if (mysql_stmt_prepare(load_tags, load_tags_stmt, sizeof(load_tags_stmt)))
 +    exit_stmt_err(load_tags);
 +
 +  memset(bind, 0, sizeof(bind));
 +  bind[0].buffer_type = MYSQL_TYPE_LONGLONG;
 +  bind[0].buffer = (char *) &mysql_id;
 +  bind[0].is_null = 0;
 +  bind[0].length = 0;
 +
 +  if (mysql_stmt_bind_param(load_tags, bind))
 +    exit_stmt_err(load_tags);
 +
 +  memset(bind, 0, sizeof(tag_bind));
 +  tag_bind[0].buffer_type = MYSQL_TYPE_STRING;
 +  tag_bind[0].buffer = k;
 +  tag_bind[0].is_null = 0;
 +  tag_bind[0].length = &res_len;
 +  tag_bind[0].error = &res_error;
 +  tag_bind[0].buffer_length = max_tag_len;
 +  tag_bind[1].buffer_type = MYSQL_TYPE_STRING;
 +  tag_bind[1].buffer = v;
 +  tag_bind[1].is_null = 0;
 +  tag_bind[1].length = &res_len;
 +  tag_bind[1].error = &res_error;
 +  tag_bind[1].buffer_length = max_tag_len;
 +  if (mysql_stmt_bind_result(load_tags, tag_bind))
 +    exit_stmt_err(load_tags);
 +
 +  if (mysql_query(d->mysql, "SELECT id, user_id, timestamp "
 +      "FROM current_ways WHERE visible"))
 +    exit_mysql_err(d->mysql);
 +
 +  res = mysql_use_result(d->mysql);
 +  if (!res) exit_mysql_err(d->mysql);
 +
 +  while ((row = mysql_fetch_row(res))) {
 +    uint64_t id;
 +    const char *user_id, *timestamp;
 +
 +    id = parse<uint64_t>(row[0]);
 +    user_id = row[1];
 +    timestamp = row[2];
 +
 +    mysql_id = (long long) id;
 +
 +    if (mysql_stmt_execute(load_segs))
 +      exit_stmt_err(load_segs);
 +
 +    if (mysql_stmt_store_result(load_segs))
 +      exit_stmt_err(load_segs);
 +
 +    list<segment> segs;
 +    while (!mysql_stmt_fetch(load_segs)) {
 +      if (((uint64_t) mysql_seg_id) >= d->segs_len) continue;
 +      segs.push_back(d->segs[mysql_seg_id]);
 +      d->rem_segs[mysql_seg_id] = 0;
 +    }
 +
 +    list<list<uint32_t> > node_lists;
 +    while (segs.size()) {
 +      list<uint32_t> node_list;
 +      node_list.push_back(segs.front().from);
 +      node_list.push_back(segs.front().to);
 +      segs.pop_front();
 +      while (true) {
 +      bool found = false;
 +      for (list<segment>::iterator it = segs.begin();
 +          it != segs.end(); ) {
 +        if (it->from == node_list.back()) {
 +          node_list.push_back(it->to);
 +          segs.erase(it++);
 +          found = true;
 +        } else if (it->to == node_list.front()) {
 +          node_list.insert(node_list.begin(), it->from);
 +          segs.erase(it++);
 +          found = true;
 +        } else {
 +          ++it;
 +        }
 +      }
 +      if (!found) break;
 +      }
 +      node_lists.push_back(node_list);
 +    }
 +
 +    vector<uint64_t> ids; ids.reserve(node_lists.size());
 +    bool orig_id_used = false;
 +    for (list<list<uint32_t> >::iterator it = node_lists.begin();
 +      it != node_lists.end(); ++it) {
 +      uint64_t way_id;
 +      int sid;
 +      if (orig_id_used) {
 +      way_id = d->new_way_id++;
 +      } else {
 +      way_id = id;
 +      orig_id_used = true;
 +      }
 +      ids.push_back(way_id);
 +
 +      fprintf(d->ways, "\"" F_U64 "\",", way_id);
 +      write_csv_col(d->ways, user_id, ',');
 +      write_csv_col(d->ways, timestamp, '\n');
 +
 +      sid = 1;
 +      for (list<uint32_t>::iterator nit = it->begin();
 +        nit != it->end(); ++nit) {
 +      fprintf(d->way_nodes, "\"" F_U64 "\",\"" F_U32 "\",\"%i\"\n", way_id, *nit, sid++);
 +      }
 +    }
 +
 +    if (mysql_stmt_execute(load_tags))
 +      exit_stmt_err(load_tags);
 +
 +    if (mysql_stmt_store_result(load_tags))
 +      exit_stmt_err(load_tags);
 +
 +    bool multiple_parts = ids.size() > 1,
 +      create_multipolygon = false;
 +
 +    while (!mysql_stmt_fetch(load_tags)) {
 +      if (multiple_parts && !create_multipolygon) {
 +      if (!strcmp(k, "natural")) {
 +        if (strcmp(v, "coastline")) {
 +          create_multipolygon = true;
 +        }
 +      } else if (!strcmp(k, "waterway")) {
 +        if (!strcmp(v, "riverbank")) {
 +          create_multipolygon = true;
 +        }
 +      } else if (!strcmp(k, "leisure") || !strcmp(k, "landuse")
 +          || !strcmp(k, "sport") || !strcmp(k, "amenity")
 +          || !strcmp(k, "tourism") || !strcmp(k, "building")) {
 +        create_multipolygon = true;
 +      }
 +      }
 +
 +      for (vector<uint64_t>::iterator it = ids.begin();
 +        it != ids.end(); ++it) {
 +      fprintf(d->way_tags, "\"" F_U64 "\",", *it);
 +      write_csv_col(d->way_tags, k, ',');
 +      write_csv_col(d->way_tags, v, '\n');
 +      }
 +    }
 +
 +    if (multiple_parts && create_multipolygon) {
 +      uint64_t ent_id = d->new_relation_id++;
 +
 +      fprintf(d->relations, "\"" F_U64 "\",", ent_id);
 +      write_csv_col(d->relations, user_id, ',');
 +      write_csv_col(d->relations, timestamp, '\n');
 +
 +      fprintf(d->relation_tags,
 +      "\"" F_U64 "\",\"type\",\"multipolygon\"\n", ent_id);
 +
 +      for (vector<uint64_t>::iterator it = ids.begin();
 +        it != ids.end(); ++it) {
 +      fprintf(d->relation_members,
 +        "\"" F_U64 "\",\"way\",\"" F_U64 "\",\"\"\n", ent_id, *it);
 +      }
 +    }
 +  }
 +  if (mysql_errno(d->mysql)) exit_stmt_err(load_tags);
 +
 +  mysql_stmt_close(load_segs);
 +  mysql_stmt_close(load_tags);
 +
 +  mysql_free_result(res);
 +  free(k);
 +  free(v);
 +}
 +
 +static int read_seg_tags(char **tags, char **k, char **v) {
 +  if (!**tags) return 0;
 +  char *i = strchr(*tags, ';');
 +  if (!i) i = *tags + strlen(*tags);
 +  char *j = strchr(*tags, '=');
 +  *k = *tags;
 +  if (j && j < i) {
 +    *v = j + 1;
 +  } else {
 +    *v = i;
 +  }
 +  *tags = *i ? i + 1 : i;
 +  *i = '\0';
 +  if (j) *j = '\0';
 +  return 1;
 +}
 +
 +static void mark_tagged_segs(struct data *d) {
 +  MYSQL_RES *res;
 +  MYSQL_ROW row;
 +
 +  if (mysql_query(d->mysql, "SELECT id, tags FROM current_segments "
 +      "WHERE visible && tags != '' && tags != 'created_by=JOSM'"))
 +    exit_mysql_err(d->mysql);
 +
 +  res = mysql_use_result(d->mysql);
 +  if (!res) exit_mysql_err(d->mysql);
 +
 +  while ((row = mysql_fetch_row(res))) {
 +    size_t id = parse<size_t>(row[0]);
 +    if (d->rem_segs[id]) continue;
 +    char *tags_it = row[1], *k, *v;
 +    while (read_seg_tags(&tags_it, &k, &v)) {
 +      if (strcmp(k, "created_by") &&
 +        strcmp(k, "tiger:county") &&
 +        strcmp(k, "tiger:upload_uuid") &&
 +        strcmp(k, "converted_by") &&
 +        (strcmp(k, "natural") || strcmp(v, "coastline")) &&
 +        (strcmp(k, "source") || strcmp(v, "PGS"))) {
 +      d->rem_segs[id] = 1;
 +      break;
 +      }
 +    }
 +  }
 +
 +  mysql_free_result(res);
 +}
 +
 +static void convert_remaining_segs(struct data *d) {
 +  MYSQL_STMT *load_seg;
 +  MYSQL_BIND args[1], res[3];
 +  const size_t max_tag_len = 1 << 16;
 +  char *tags, timestamp[100];
 +  char *k, *v;
 +  int user_id;
 +  long long mysql_id;
 +  unsigned long res_len;
 +  my_bool res_error;
 +  const char load_seg_stmt[] =
 +    "SELECT user_id, tags, CAST(timestamp AS CHAR) FROM current_segments "
 +    "WHERE visible && id = ?";
 +
 +  tags = (char *) malloc(max_tag_len);
 +
 +  load_seg = mysql_stmt_init(d->mysql);
 +  if (!load_seg) exit_mysql_err(d->mysql);
 +  if (mysql_stmt_prepare(load_seg, load_seg_stmt, sizeof(load_seg_stmt)))
 +    exit_stmt_err(load_seg);
 +
 +  memset(args, 0, sizeof(args));
 +  args[0].buffer_type = MYSQL_TYPE_LONGLONG;
 +  args[0].buffer = (char *) &mysql_id;
 +  args[0].is_null = 0;
 +  args[0].length = 0;
 +  if (mysql_stmt_bind_param(load_seg, args))
 +    exit_stmt_err(load_seg);
 +
 +  memset(res, 0, sizeof(res));
 +  res[0].buffer_type = MYSQL_TYPE_LONG;
 +  res[0].buffer = (char *) &user_id;
 +  res[0].is_null = 0;
 +  res[0].length = 0;
 +  res[0].error = &res_error;
 +  res[1].buffer_type = MYSQL_TYPE_STRING;
 +  res[1].buffer = tags;
 +  res[1].is_null = 0;
 +  res[1].length = &res_len;
 +  res[1].error = &res_error;
 +  res[1].buffer_length = max_tag_len;
 +  res[2].buffer_type = MYSQL_TYPE_STRING;
 +  res[2].buffer = timestamp;
 +  res[2].is_null = 0;
 +  res[2].length = &res_len;
 +  res[2].error = &res_error;
 +  res[2].buffer_length = sizeof(timestamp);
 +  if (mysql_stmt_bind_result(load_seg, res))
 +    exit_stmt_err(load_seg);
 +
 +  for (size_t seg_id = 0; seg_id < d->segs_len; seg_id++) {
 +    if (!d->rem_segs[seg_id]) continue;
 +    segment seg = d->segs[seg_id];
 +
 +    mysql_id = seg_id;
 +    if (mysql_stmt_execute(load_seg)) exit_stmt_err(load_seg);
 +    if (mysql_stmt_store_result(load_seg)) exit_stmt_err(load_seg);
 +
 +    while (!mysql_stmt_fetch(load_seg)) {
 +      uint64_t way_id = d->new_way_id++;
 +
 +      fprintf(d->ways, "\"" F_U64 "\",\"%i\",", way_id, user_id);
 +      write_csv_col(d->ways, timestamp, '\n');
 +
 +      fprintf(d->way_nodes, "\"" F_U64 "\",\"" F_U32 "\",\"%i\"\n", way_id, seg.from, 1);
 +      fprintf(d->way_nodes, "\"" F_U64 "\",\"" F_U32 "\",\"%i\"\n", way_id, seg.to, 2);
 +
 +      char *tags_it = tags;
 +      while (read_seg_tags(&tags_it, &k, &v)) {
 +      fprintf(d->way_tags, "\"" F_U64 "\",", way_id);
 +      write_csv_col(d->way_tags, k, ',');
 +      write_csv_col(d->way_tags, v, '\n');
 +      }
 +    }
 +  }
 +
 +  mysql_stmt_close(load_seg);
 +
 +  free(tags);
 +}
 +
 +static MYSQL *connect_to_mysql(char **argv) {
 +  MYSQL *mysql = mysql_init(NULL);
 +  if (!mysql) exit_mysql_err(mysql);
 +
 +  if (!mysql_real_connect(mysql, argv[1], argv[2], argv[3], argv[4],
 +      argv[5][0] ? atoi(argv[5]) : 0, argv[6][0] ? argv[6] : NULL, 0))
 +    exit_mysql_err(mysql);
 +
 +  if (mysql_set_character_set(mysql, "utf8"))
 +    exit_mysql_err(mysql);
 +
 +  return mysql;
 +}
 +
 +static void open_file(FILE **f, char *fn) {
 +  *f = fopen(fn, "w+");
 +  if (!*f) {
 +    perror("fopen");
 +    exit(EXIT_FAILURE);
 +  }
 +}
 +
 +int main(int argc, char **argv) {
 +  struct data data;
 +  struct data *d = &data;
 +  size_t prefix_len;
 +  char *tempfn;
 +
 +  if (argc != 8) {
++    printf("Usage: 007_remove_segments_helper host user passwd database port socket prefix\n");
 +    exit(EXIT_FAILURE);
 +  }
 +
 +  d->mysql = connect_to_mysql(argv);
 +  d->mysql2 = connect_to_mysql(argv);
 +
 +  prefix_len = strlen(argv[7]);
 +  tempfn = (char *) malloc(prefix_len + 15);
 +  strcpy(tempfn, argv[7]);
 +
 +  strcpy(tempfn + prefix_len, "ways");
 +  open_file(&d->ways, tempfn);
 +
 +  strcpy(tempfn + prefix_len, "way_nodes");
 +  open_file(&d->way_nodes, tempfn);
 +
 +  strcpy(tempfn + prefix_len, "way_tags");
 +  open_file(&d->way_tags, tempfn);
 +
 +  strcpy(tempfn + prefix_len, "relations");
 +  open_file(&d->relations, tempfn);
 +
 +  strcpy(tempfn + prefix_len, "relation_members");
 +  open_file(&d->relation_members, tempfn);
 +
 +  strcpy(tempfn + prefix_len, "relation_tags");
 +  open_file(&d->relation_tags, tempfn);
 +
 +  free(tempfn);
 +
 +  find_maxids(d);
 +  populate_segs(d);
 +  convert_ways(d);
 +  mark_tagged_segs(d);
 +  convert_remaining_segs(d);
 +
 +  mysql_close(d->mysql);
 +  mysql_close(d->mysql2);
 +
 +  fclose(d->ways);
 +  fclose(d->way_nodes);
 +  fclose(d->way_tags);
 +
 +  fclose(d->relations);
 +  fclose(d->relation_members);
 +  fclose(d->relation_tags);
 +
 +  free(d->segs);
 +  free(d->rem_segs);
 +
 +  exit(EXIT_SUCCESS);
 +}
index 7921ac1,0000000..bddc8a0
mode 100644,000000..100644
--- /dev/null
@@@ -1,5 -1,0 +1,23 @@@
 +t1:
++  id: 1
++  member_role: "some"
++  member_type: "way"
++  member_id: 3
++
++t2:
 +  id: 1
 +  member_role: "some"
 +  member_type: "node"
++  member_id: 5
++
++t3:
++  id: 1
++  member_role: "some"
++  member_type: "relation"
 +  member_id: 3
++
++t4:
++  id: 3
++  member_role: "some"
++  member_type: "node"
++  member_id: 5
index ba795b6,0000000..aaf06a3
mode 100644,000000..100644
--- /dev/null
@@@ -1,9 -1,0 +1,14 @@@
 +t1:
 +  id: 1
 +  k: test
 +  v: yes
 +
 +t2:
 +  id: 2
 +  k: test
 +  v: yes
++
++t2:
++  id: 3
++  k: test
++  v: yes
index 67c8ddb,0000000..c1f77d4
mode 100644,000000..100644
--- /dev/null
@@@ -1,11 -1,0 +1,17 @@@
 +visible_relation:
 +  id: 1
 +  user_id: 1
 +  timestamp: 2007-01-01 00:00:00
 +  visible: 1
 +
 +invisible_relation:
 +  id: 2
 +  user_id: 1
 +  timestamp: 2007-01-01 00:00:00
 +  visible: 0
++
++used_relation:
++  id: 3
++  user_id: 1
++  timestamp: 2007-01-01 00:00:00
++  visible: 1
index f424e6a,0000000..27e8b53
mode 100644,000000..100644
--- /dev/null
@@@ -1,6 -1,0 +1,24 @@@
 +t1:
++  id: 1
++  member_role: "some"
++  member_type: "way"
++  member_id: 3
++  version: 1
++t2:
 +  id: 1
 +  member_role: "some"
 +  member_type: "node"
++  member_id: 5
++  version: 1
++t3:
++  id: 1
++  member_role: "some"
++  member_type: "relation"
 +  member_id: 3
 +  version: 1
++t4:
++  id: 3
++  member_role: "some"
++  member_type: "node"
++  member_id: 5
++  version: 1
index d1c69a6,0000000..39f4bd5
mode 100644,000000..100644
--- /dev/null
@@@ -1,11 -1,0 +1,17 @@@
 +t1:
 +  id: 1
 +  k: test
 +  v: yes
 +  version: 1
 +
 +t2:
 +  id: 2
 +  k: test
 +  v: yes
 +  version: 1
++
++t3:
++  id: 3
++  k: test
++  v: yes
++  version: 1
index f1e4026,0000000..cf1d1ff
mode 100644,000000..100644
--- /dev/null
@@@ -1,13 -1,0 +1,20 @@@
 +visible_relation:
 +  id: 1
 +  user_id: 1
 +  timestamp: 2007-01-01 00:00:00
 +  visible: 1
 +  version: 1
 +
 +invisible_relation:
 +  id: 2
 +  user_id: 1
 +  timestamp: 2007-01-01 00:00:00
 +  visible: 0
 +  version: 1
++
++used_relation:
++  id: 3
++  user_id: 1
++  timestamp: 2007-01-01 00:00:00
++  visible: 1
++  version: 1
index 2893ba9,0000000..8f8b727
mode 100644,000000..100644
--- /dev/null
@@@ -1,209 -1,0 +1,209 @@@
-     get :relations_for_node, :id => current_nodes(:used_node_1).id
 +require File.dirname(__FILE__) + '/../test_helper'
 +require 'relation_controller'
 +
 +# Re-raise errors caught by the controller.
 +class RelationController; def rescue_action(e) raise e end; end
 +
 +class RelationControllerTest < Test::Unit::TestCase
 +  api_fixtures
 +  fixtures :relations, :current_relations, :relation_members, :current_relation_members, :relation_tags, :current_relation_tags
 +  set_fixture_class :current_relations => :Relation
 +  set_fixture_class :relations => :OldRelation
 +
 +  def setup
 +    @controller = RelationController.new
 +    @request    = ActionController::TestRequest.new
 +    @response   = ActionController::TestResponse.new
 +  end
 +
 +  def basic_authorization(user, pass)
 +    @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
 +  end
 +
 +  def content(c)
 +    @request.env["RAW_POST_DATA"] = c
 +  end
 +
 +  # -------------------------------------
 +  # Test reading relations.
 +  # -------------------------------------
 +
 +  def test_read
 +    # check that a visible relation is returned properly
 +    get :read, :id => current_relations(:visible_relation).id
 +    assert_response :success
 +
 +    # check that an invisible relation is not returned
 +    get :read, :id => current_relations(:invisible_relation).id
 +    assert_response :gone
 +
 +    # check chat a non-existent relation is not returned
 +    get :read, :id => 0
 +    assert_response :not_found
 +
 +    # check the "relations for node" mode
-     get :relations_for_node, :id => current_relations(:used_relation).id
++    get :relations_for_node, :id => current_nodes(:node_used_by_relationship).id
 +    assert_response :success
 +    # FIXME check whether this contains the stuff we want!
 +    if $VERBOSE
 +        print @response.body
 +    end
 +
 +    # check the "relations for way" mode
 +    get :relations_for_way, :id => current_ways(:used_way).id
 +    assert_response :success
 +    # FIXME check whether this contains the stuff we want!
 +    if $VERBOSE
 +        print @response.body
 +    end
 +
 +
 +    # check the "relations for relation" mode
-     get :full, :id => current_relations(:relation_using_all).id
++    get :relations_for_relation, :id => current_relations(:used_relation).id
 +    assert_response :success
 +    # FIXME check whether this contains the stuff we want!
 +    if $VERBOSE
 +        print @response.body
 +    end
 +
 +    # check the "full" mode
++    get :full, :id => current_relations(:visible_relation).id
 +    assert_response :success
 +    # FIXME check whether this contains the stuff we want!
 +    if $VERBOSE
 +        print @response.body
 +    end
 +  end
 +
 +  # -------------------------------------
 +  # Test simple relation creation.
 +  # -------------------------------------
 +
 +  def test_create
 +    basic_authorization "test@openstreetmap.org", "test"
 +
 +    # create an relation without members
 +    content "<osm><relation><tag k='test' v='yes' /></relation></osm>"
 +    put :create
 +    # hope for success
 +    assert_response :success, 
 +        "relation upload did not return success status"
 +    # read id of created relation and search for it
 +    relationid = @response.body
 +    checkrelation = Relation.find(relationid)
 +    assert_not_nil checkrelation, 
 +        "uploaded relation not found in data base after upload"
 +    # compare values
 +    assert_equal checkrelation.members.length, 0, 
 +        "saved relation contains members but should not"
 +    assert_equal checkrelation.tags.length, 1, 
 +        "saved relation does not contain exactly one tag"
 +    assert_equal users(:normal_user).id, checkrelation.user_id, 
 +        "saved relation does not belong to user that created it"
 +    assert_equal true, checkrelation.visible, 
 +        "saved relation is not visible"
 +    # ok the relation is there but can we also retrieve it?
 +    get :read, :id => relationid
 +    assert_response :success
 +
 +
 +    # create an relation with a node as member
 +    nid = current_nodes(:used_node_1).id
 +    content "<osm><relation><member type='node' ref='#{nid}' role='some'/>" +
 +        "<tag k='test' v='yes' /></relation></osm>"
 +    put :create
 +    # hope for success
 +    assert_response :success, 
 +        "relation upload did not return success status"
 +    # read id of created relation and search for it
 +    relationid = @response.body
 +    checkrelation = Relation.find(relationid)
 +    assert_not_nil checkrelation, 
 +        "uploaded relation not found in data base after upload"
 +    # compare values
 +    assert_equal checkrelation.members.length, 1, 
 +        "saved relation does not contain exactly one member"
 +    assert_equal checkrelation.tags.length, 1, 
 +        "saved relation does not contain exactly one tag"
 +    assert_equal users(:normal_user).id, checkrelation.user_id, 
 +        "saved relation does not belong to user that created it"
 +    assert_equal true, checkrelation.visible, 
 +        "saved relation is not visible"
 +    # ok the relation is there but can we also retrieve it?
 +    
 +    get :read, :id => relationid
 +    assert_response :success
 +
 +    # create an relation with a way and a node as members
 +    nid = current_nodes(:used_node_1).id
 +    wid = current_ways(:used_way).id
 +    content "<osm><relation><member type='node' ref='#{nid}' role='some'/>" +
 +        "<member type='way' ref='#{wid}' role='other'/>" +
 +        "<tag k='test' v='yes' /></relation></osm>"
 +    put :create
 +    # hope for success
 +    assert_response :success, 
 +        "relation upload did not return success status"
 +    # read id of created relation and search for it
 +    relationid = @response.body
 +    checkrelation = Relation.find(relationid)
 +    assert_not_nil checkrelation, 
 +        "uploaded relation not found in data base after upload"
 +    # compare values
 +    assert_equal checkrelation.members.length, 2, 
 +        "saved relation does not have exactly two members"
 +    assert_equal checkrelation.tags.length, 1, 
 +        "saved relation does not contain exactly one tag"
 +    assert_equal users(:normal_user).id, checkrelation.user_id, 
 +        "saved relation does not belong to user that created it"
 +    assert_equal true, checkrelation.visible, 
 +        "saved relation is not visible"
 +    # ok the relation is there but can we also retrieve it?
 +    get :read, :id => relationid
 +    assert_response :success
 +
 +  end
 +
 +  # -------------------------------------
 +  # Test creating some invalid relations.
 +  # -------------------------------------
 +
 +  def test_create_invalid
 +    basic_authorization "test@openstreetmap.org", "test"
 +
 +    # create a relation with non-existing node as member
 +    content "<osm><relation><member type='node' ref='0'/><tag k='test' v='yes' /></relation></osm>"
 +    put :create
 +    # expect failure
 +    assert_response :precondition_failed, 
 +        "relation upload with invalid node did not return 'precondition failed'"
 +  end
 +
 +  # -------------------------------------
 +  # Test deleting relations.
 +  # -------------------------------------
 +  
 +  def test_delete
 +  return true
 +
 +    # first try to delete relation without auth
 +    delete :delete, :id => current_relations(:visible_relation).id
 +    assert_response :unauthorized
 +
 +    # now set auth
 +    basic_authorization("test@openstreetmap.org", "test");  
 +
 +    # this should work
 +    delete :delete, :id => current_relations(:visible_relation).id
 +    assert_response :success
 +
 +    # this won't work since the relation is already deleted
 +    delete :delete, :id => current_relations(:invisible_relation).id
 +    assert_response :gone
 +
 +    # this won't work since the relation never existed
 +    delete :delete, :id => 0
 +    assert_response :not_found
 +  end
 +
 +end
diff --combined test/unit/node_test.rb
@@@ -11,7 -11,7 +11,7 @@@ class NodeTest < Test::Unit::TestCas
                               :user_id => users(:normal_user).id,
                               :visible => 1,
                               :tags => "")
--    assert node_template.save_with_history
++    assert node_template.save_with_history!
  
      node = Node.find(node_template.id)
      assert_not_nil node
@@@ -44,7 -44,7 +44,7 @@@
      node_template.latitude = 12.3456
      node_template.longitude = 65.4321
      node_template.tags = "updated=yes"
--    assert node_template.save_with_history
++    assert node_template.save_with_history!
  
      node = Node.find(node_template.id)
      assert_not_nil node
@@@ -76,7 -76,7 +76,7 @@@
      assert_not_nil old_node_template
  
      node_template.visible = 0
--    assert node_template.save_with_history
++    assert node_template.save_with_history!
  
      node = Node.find(node_template.id)
      assert_not_nil node