Implemented osmChange diff downloads for changesets and a couple of tests.
authorMatt Amos <zerebubuth@gmail.com>
Mon, 27 Oct 2008 17:50:28 +0000 (17:50 +0000)
committerMatt Amos <zerebubuth@gmail.com>
Mon, 27 Oct 2008 17:50:28 +0000 (17:50 +0000)
app/controllers/changeset_controller.rb
config/routes.rb
test/functional/changeset_controller_test.rb

index bb8c3cef222fd6a867242223cba587b598a0a630..7ac4eb91a81dc378331a443d7adb3ae2258b2425 100644 (file)
@@ -106,4 +106,77 @@ class ChangesetController < ApplicationController
   rescue OSM::APIError => ex
     render ex.render_opts
   end
+
+  ##
+  # download the changeset as an osmChange document.
+  #
+  # to make it easier to revert diffs it would be better if the osmChange
+  # format were reversible, i.e: contained both old and new versions of 
+  # modified elements. but it doesn't at the moment...
+  #
+  # this method cannot order the database changes fully (i.e: timestamp and
+  # version number may be too coarse) so the resulting diff may not apply
+  # to a different database. however since changesets are not atomic this 
+  # behaviour cannot be guaranteed anyway and is the result of a design
+  # choice.
+  def download
+    changeset = Changeset.find(params[:id])
+    
+    # get all the elements in the changeset and stick them in a big array.
+    elements = [changeset.old_nodes, 
+                changeset.old_ways, 
+                changeset.old_relations].flatten
+    
+    # sort the elements by timestamp and version number, as this is the 
+    # almost sensible ordering available. this would be much nicer if 
+    # global (SVN-style) versioning were used - then that would be 
+    # unambiguous.
+    elements.sort! do |a, b| 
+      if (a.timestamp == b.timestamp)
+        a.version <=> b.version
+      else
+        a.timestamp <=> b.timestamp 
+      end
+    end
+    
+    # create an osmChange document for the output
+    result = OSM::API.new.get_xml_doc
+    result.root.name = "osmChange"
+
+    # generate an output element for each operation. note: we avoid looking
+    # at the history because it is simpler - but it would be more correct to 
+    # check these assertions.
+    elements.each do |elt|
+      result.root <<
+        if (elt.version == 1) 
+          # first version, so it must be newly-created.
+          created = XML::Node.new "create"
+          created << elt.to_xml_node
+        else
+          # get the previous version from the element history
+          prev_elt = elt.class.find(:first, :conditions => 
+                                    ['id = ? and version = ?',
+                                     elt.id, elt.version])
+          unless elt.visible
+            # if the element isn't visible then it must have been deleted, so
+            # output the *previous* XML
+            deleted = XML::Node.new "delete"
+            deleted << prev_elt.to_xml_node
+          else
+            # must be a modify, for which we don't need the previous version
+            # yet...
+            modified = XML::Node.new "modify"
+            modified << elt.to_xml_node
+          end
+        end
+    end
+
+    render :text => result.to_s, :content_type => "text/xml"
+            
+  rescue ActiveRecord::RecordNotFound
+    render :nothing => true, :status => :not_found
+  rescue OSM::APIError => ex
+    render ex.render_opts
+  end
+
 end
index 39e2a1e749cd0cc95b223a91d689c7c70cd4a2ab..139611bc6132ede96fddf37c4bde040c804ee569 100644 (file)
@@ -5,6 +5,7 @@ ActionController::Routing::Routes.draw do |map|
   
   map.connect "api/#{API_VERSION}/changeset/create", :controller => 'changeset', :action => 'create'
   map.connect "api/#{API_VERSION}/changeset/:id/upload", :controller => 'changeset', :action => 'upload', :id => /\d+/
+  map.connect "api/#{API_VERSION}/changeset/:id/download", :controller => 'changeset', :action => 'download', :id => /\d+/
   map.connect "api/#{API_VERSION}/changeset/:id", :controller => 'changeset', :action => 'read', :id => /\d+/
   map.connect "api/#{API_VERSION}/changeset/:id/close", :controller => 'changeset', :action => 'close', :id =>/\d+/
   
index 946d139d8714bdd90d8f622e06ca922a72c09171..848b6d5f58e3a4ca43bad1a044afdbe4f2e994e1 100644 (file)
@@ -381,4 +381,104 @@ EOF
       "shouldn't be able to upload an element without version: #{@response.body}"
   end
 
+  ##
+  # when we make some simple changes we get the same changes back from the 
+  # diff download.
+  def test_diff_download_simple
+    basic_authorization(users(:normal_user).email, "test")
+
+    # create a temporary changeset
+    content "<osm><changeset>" +
+      "<tag k='created_by' v='osm test suite checking changesets'/>" + 
+      "</changeset></osm>"
+    put :create
+    assert_response :success
+    changeset_id = @response.body.to_i
+
+    # add a diff to it
+    diff = <<EOF
+<osmChange>
+ <modify>
+  <node id='1' lon='0' lat='0' changeset='#{changeset_id}' version='1'/>
+  <node id='1' lon='1' lat='0' changeset='#{changeset_id}' version='2'/>
+  <node id='1' lon='1' lat='1' changeset='#{changeset_id}' version='3'/>
+  <node id='1' lon='1' lat='2' changeset='#{changeset_id}' version='4'/>
+  <node id='1' lon='2' lat='2' changeset='#{changeset_id}' version='5'/>
+  <node id='1' lon='3' lat='2' changeset='#{changeset_id}' version='6'/>
+  <node id='1' lon='3' lat='3' changeset='#{changeset_id}' version='7'/>
+  <node id='1' lon='9' lat='9' changeset='#{changeset_id}' version='8'/>
+ </modify>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => changeset_id
+    assert_response :success, 
+      "can't upload multiple versions of an element in a diff: #{@response.body}"
+    
+    get :download, :id => changeset_id
+    assert_response :success
+
+    assert_select "osmChange", 1
+    assert_select "osmChange>modify", 8
+    assert_select "osmChange>modify>node", 8
+  end
+  
+  ##
+  # when we make some complex changes we get the same changes back from the 
+  # diff download.
+  def test_diff_download_complex
+    basic_authorization(users(:normal_user).email, "test")
+
+    # create a temporary changeset
+    content "<osm><changeset>" +
+      "<tag k='created_by' v='osm test suite checking changesets'/>" + 
+      "</changeset></osm>"
+    put :create
+    assert_response :success
+    changeset_id = @response.body.to_i
+
+    # add a diff to it
+    diff = <<EOF
+<osmChange>
+ <delete>
+  <node id='1' lon='0' lat='0' changeset='#{changeset_id}' version='1'/>
+ </delete>
+ <create>
+  <node id='-1' lon='9' lat='9' changeset='#{changeset_id}' version='0'/>
+  <node id='-2' lon='8' lat='9' changeset='#{changeset_id}' version='0'/>
+  <node id='-3' lon='7' lat='9' changeset='#{changeset_id}' version='0'/>
+ </create>
+ <modify>
+  <node id='3' lon='20' lat='15' changeset='#{changeset_id}' version='1'/>
+  <way id='1' changeset='#{changeset_id}' version='1'>
+   <nd ref='3'/>
+   <nd ref='-1'/>
+   <nd ref='-2'/>
+   <nd ref='-3'/>
+  </way>
+ </modify>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => changeset_id
+    assert_response :success, 
+      "can't upload multiple versions of an element in a diff: #{@response.body}"
+    
+    get :download, :id => changeset_id
+    assert_response :success
+
+    assert_select "osmChange", 1
+    assert_select "osmChange>create", 3
+    assert_select "osmChange>delete", 1
+    assert_select "osmChange>modify", 2
+    assert_select "osmChange>create>node", 3
+    assert_select "osmChange>delete>node", 1 
+    assert_select "osmChange>modify>node", 1
+    assert_select "osmChange>modify>way", 1
+  end
+  
 end