Merge branch 'master' into openstreetbugs
authorTom Hughes <tom@compton.nu>
Fri, 29 Apr 2011 16:05:59 +0000 (17:05 +0100)
committerTom Hughes <tom@compton.nu>
Fri, 29 Apr 2011 16:05:59 +0000 (17:05 +0100)
34 files changed:
app/controllers/browse_controller.rb
app/controllers/map_bugs_controller.rb [new file with mode: 0644]
app/models/map_bug.rb [new file with mode: 0644]
app/models/map_bug_comment.rb [new file with mode: 0644]
app/views/browse/_map.html.erb
app/views/browse/bug.html.erb [new file with mode: 0644]
app/views/map_bugs/_bugs_paging_nav.html.erb [new file with mode: 0644]
app/views/map_bugs/_user.html.erb [new file with mode: 0644]
app/views/map_bugs/get_bugs.gpx.builder [new file with mode: 0644]
app/views/map_bugs/get_bugs.js.erb [new file with mode: 0644]
app/views/map_bugs/get_bugs.rss.builder [new file with mode: 0644]
app/views/map_bugs/get_bugs.xml.builder [new file with mode: 0644]
app/views/map_bugs/my_bugs.html.erb [new file with mode: 0644]
app/views/map_bugs/read.rss.builder [new file with mode: 0644]
app/views/map_bugs/read.xml.builder [new file with mode: 0644]
app/views/map_bugs/rss.rss.builder [new file with mode: 0644]
app/views/site/index.html.erb
config/locales/en.yml
config/routes.rb
db/migrate/053_add_map_bug_tables.rb [new file with mode: 0644]
db/migrate/054_refactor_map_bug_tables.rb [new file with mode: 0644]
db/migrate/055_change_map_bug_comment_type.rb [new file with mode: 0644]
db/migrate/056_add_date_closed.rb [new file with mode: 0644]
db/migrate/057_add_map_bug_comment_event.rb [new file with mode: 0644]
lib/geo_record.rb
lib/map_boundary.rb
lib/osm.rb
public/images/closed_bug_marker.png [new file with mode: 0644]
public/images/open_bug_marker.png [new file with mode: 0644]
public/javascripts/openstreetbugs.js [new file with mode: 0644]
public/stylesheets/openstreetbugs.css [new file with mode: 0644]
test/fixtures/map_bug_comment.yml [new file with mode: 0644]
test/fixtures/map_bugs.yml [new file with mode: 0644]
test/functional/map_bugs_controller_test.rb [new file with mode: 0644]

index a7dd5f5c95ff8b745b19fc3cac1831ac38c2177a..f6bd89d4de9396ef74b398dd9074aa2b93a42e0c 100644 (file)
@@ -79,4 +79,13 @@ class BrowseController < ApplicationController
   rescue ActiveRecord::RecordNotFound
     render :action => "not_found", :status => :not_found
   end
+
+  def bug
+    @type = "bug"
+    @bug = MapBug.find(params[:id])
+    @next = MapBug.find(:first, :order => "id ASC", :conditions => [ "status != 'hidden' AND id > :id", { :id => @bug.id }] )
+    @prev = MapBug.find(:first, :order => "id DESC", :conditions => [ "status != 'hidden' AND id < :id", { :id => @bug.id }] )
+  rescue ActiveRecord::RecordNotFound
+    render :action => "not_found", :status => :not_found
+  end
 end
diff --git a/app/controllers/map_bugs_controller.rb b/app/controllers/map_bugs_controller.rb
new file mode 100644 (file)
index 0000000..bbde214
--- /dev/null
@@ -0,0 +1,317 @@
+class MapBugsController < ApplicationController
+
+  layout 'site', :only => [:my_bugs]
+
+  before_filter :check_api_readable
+  before_filter :authorize_web, :only => [:add_bug, :close_bug, :edit_bug, :delete, :my_bugs]
+  before_filter :check_api_writable, :only => [:add_bug, :close_bug, :edit_bug, :delete]
+  before_filter :require_moderator, :only => [:delete]
+  before_filter :set_locale, :only => [:my_bugs]
+  after_filter :compress_output
+  around_filter :api_call_handle_error, :api_call_timeout
+
+  # Help methods for checking boundary sanity and area size
+  include MapBoundary
+
+  def get_bugs
+
+       # Figure out the bbox
+    bbox = params['bbox']
+
+    if bbox and bbox.count(',') == 3
+      bbox = bbox.split(',')
+         @min_lon, @min_lat, @max_lon, @max_lat = sanitise_boundaries(bbox)
+       else
+         #Fallback to old style, this is deprecated and should not be used
+         raise OSM::APIBadUserInput.new("No l was given") unless params['l']
+         raise OSM::APIBadUserInput.new("No r was given") unless params['r']
+         raise OSM::APIBadUserInput.new("No b was given") unless params['b']
+         raise OSM::APIBadUserInput.new("No t was given") unless params['t']
+
+         @min_lon = params['l'].to_f
+         @max_lon = params['r'].to_f
+         @min_lat = params['b'].to_f
+         @max_lat = params['t'].to_f
+    end
+       limit = getLimit
+       conditions = closedCondition
+       
+    check_boundaries(@min_lon, @min_lat, @max_lon, @max_lat, :false)
+
+       @bugs = MapBug.find_by_area_no_quadtile(@min_lat, @min_lon, @max_lat, @max_lon, :include => :map_bug_comment, :order => "last_changed DESC", :limit => limit, :conditions => conditions)
+
+       respond_to do |format|
+         format.html {render :template => 'map_bugs/get_bugs.js', :content_type => "text/javascript"}
+         format.rss {render :template => 'map_bugs/get_bugs.rss'}
+         format.js
+         format.xml {render :template => 'map_bugs/get_bugs.xml'}
+         format.json { render :json => @bugs.to_json(:methods => [:lat, :lon], :only => [:id, :status, :date_created], :include => { :map_bug_comment => { :only => [:commenter_name, :date_created, :comment]}}) }      
+#        format.gpx {render :template => 'map_bugs/get_bugs.gpx'}
+       end
+  end
+
+  def add_bug
+       raise OSM::APIBadUserInput.new("No lat was given") unless params['lat']
+       raise OSM::APIBadUserInput.new("No lon was given") unless params['lon']
+       raise OSM::APIBadUserInput.new("No text was given") unless params['text']
+
+       lon = params['lon'].to_f
+       lat = params['lat'].to_f
+       comment = params['text']
+
+       name = "NoName";
+       name = params['name'] if params['name'];
+
+       #Include in a transaction to ensure that there is always a map_bug_comment for every map_bug
+       MapBug.transaction do
+         @bug = MapBug.create_bug(lat, lon)
+
+
+         #TODO: move this into a helper function
+         begin
+               url = "http://nominatim.openstreetmap.org/reverse?lat=" + lat.to_s + "&lon=" + lon.to_s + "&zoom=16" 
+               response = REXML::Document.new(Net::HTTP.get(URI.parse(url))) 
+               
+               if result = response.get_text("reversegeocode/result") 
+                 @bug.nearby_place = result.to_s 
+               else 
+                 @bug.nearby_place = "unknown"
+               end
+         rescue Exception => err
+               @bug.nearby_place = "unknown"
+         end
+         
+         @bug.save;
+         add_comment(@bug, comment, name,"opened");
+       end
+       render_ok
+  end
+
+  def edit_bug
+       raise OSM::APIBadUserInput.new("No id was given") unless params['id']
+       raise OSM::APIBadUserInput.new("No text was given") unless params['text']
+
+       name = "NoName";
+       name = params['name'] if params['name'];
+       
+       id = params['id'].to_i
+
+       bug = MapBug.find_by_id(id);
+       raise OSM::APINotFoundError unless bug
+       raise OSM::APIAlreadyDeletedError unless bug.visible
+
+       MapBug.transaction do
+         bug_comment = add_comment(bug, params['text'], name,"commented");
+       end
+
+       render_ok
+  end
+
+  def close_bug
+       raise OSM::APIBadUserInput.new("No id was given") unless params['id']
+       
+       id = params['id'].to_i
+       name = "NoName";
+       name = params['name'] if params['name'];
+
+       bug = MapBug.find_by_id(id);
+       raise OSM::APINotFoundError unless bug
+       raise OSM::APIAlreadyDeletedError unless bug.visible
+
+       MapBug.transaction do
+         bug.close_bug;
+         add_comment(bug,:nil,name,"closed")
+       end
+
+       render_ok
+  end 
+
+
+  def rss
+
+       # Figure out the bbox
+    bbox = params['bbox']
+
+    if bbox and bbox.count(',') == 3
+      bbox = bbox.split(',')
+         @min_lon, @min_lat, @max_lon, @max_lat = sanitise_boundaries(bbox)
+       else
+         @min_lon = -180.0
+         @min_lat = -90.0
+         @max_lon = 180.0
+         @max_lat = 90.0
+    end
+       limit = getLimit
+       conditions = closedCondition
+       conditions = cond_merge conditions, [OSM.sql_for_area_no_quadtile(@min_lat, @min_lon, @max_lat, @max_lon)]
+       
+    check_boundaries(@min_lon, @min_lat, @max_lon, @max_lat, :false)
+
+       @comments = MapBugComment.find(:all, :limit => limit, :order => "date_created DESC", :joins => :map_bug, :include => :map_bug, :conditions => conditions);
+       render :template => 'map_bugs/rss.rss'
+  end
+
+  def gpx_bugs
+       request.format = :xml
+       get_bugs
+  end
+
+  def read
+       @bug = MapBug.find(params['id'])
+       raise OSM::APINotFoundError unless @bug
+       raise OSM::APIAlreadyDeletedError unless @bug.visible
+
+       respond_to do |format|
+         format.rss
+         format.xml
+         format.json { render :json => @bug.to_json(:methods => [:lat, :lon], :only => [:id, :status, :date_created], :include => { :map_bug_comment => { :only => [:commenter_name, :date_created, :comment]}}) }       
+       end
+  end
+
+  def delete
+       bug = MapBug.find(params['id'])
+       raise OSM::APINotFoundError unless @bug
+       raise OSM::APIAlreadyDeletedError unless @bug.visible
+       MapBug.transaction do
+         bug.status = "hidden";
+         bug.save
+         add_comment(bug,:nil,name,"hidden")
+       end
+
+       render :text => "ok\n", :content_type => "text/html" 
+  end
+
+  def search
+       raise OSM::APIBadUserInput.new("No query string was given") unless params['q']
+       limit = getLimit
+       conditions = closedCondition
+       conditions = cond_merge conditions, ['map_bug_comment.comment ~ ?', params['q']]
+       
+       #TODO: There should be a better way to do this.   CloseConditions are ignored at the moment
+
+       bugs2 = MapBug.find(:all, :limit => limit, :order => "last_changed DESC", :joins => :map_bug_comment, :include => :map_bug_comment,
+                                               :conditions => conditions)
+       @bugs = bugs2.uniq
+       respond_to do |format|
+         format.html {render :template => 'map_bugs/get_bugs.js', :content_type => "text/javascript"}
+         format.rss {render :template => 'map_bugs/get_bugs.rss'}
+         format.js
+         format.xml {render :template => 'map_bugs/get_bugs.xml'}
+         format.json { render :json => @bugs.to_json(:methods => [:lat, :lon], :only => [:id, :status, :date_created], :include => { :map_bug_comment => { :only => [:commenter_name, :date_created, :comment]}}) }
+#        format.gpx {render :template => 'map_bugs/get_bugs.gpx'}
+       end
+  end
+
+  def my_bugs
+    if params[:display_name] 
+      @user2 = User.find_by_display_name(params[:display_name], :conditions => { :status => ["active", "confirmed"] }) 
+      if @user2  
+        if @user2.data_public? or @user2 == @user 
+          conditions = ['map_bug_comment.commenter_id = ?', @user2.id] 
+        else 
+          conditions = ['false'] 
+        end 
+      else #if request.format == :html 
+        @title = t 'user.no_such_user.title' 
+        @not_found_user = params[:display_name] 
+        render :template => 'user/no_such_user', :status => :not_found 
+               return
+      end 
+    end
+
+       if @user2 
+      user_link = render_to_string :partial => "user", :object => @user2 
+    end 
+
+       @title =  t 'bugs.user.title_user', :user => @user2.display_name 
+    @heading =  t 'bugs.user.heading_user', :user => @user2.display_name 
+    @description = t 'bugs.user.description_user', :user => user_link
+
+       @page = (params[:page] || 1).to_i 
+    @page_size = 10
+
+       @bugs = MapBug.find(:all, 
+                                               :include => [:map_bug_comment, {:map_bug_comment => :user}],
+                                               :joins => :map_bug_comment,
+                                               :order => "last_changed DESC",
+                                               :conditions => conditions,
+                                               :offset => (@page - 1) * @page_size, 
+                                               :limit => @page_size).uniq
+       
+  end
+
+private 
+  #------------------------------------------------------------ 
+  # utility functions below. 
+  #------------------------------------------------------------   
+  ## 
+  # merge two conditions 
+  # TODO: this is a copy from changeset_controler.rb and should be factored out to share
+  def cond_merge(a, b) 
+    if a and b 
+      a_str = a.shift 
+      b_str = b.shift 
+      return [ a_str + " AND " + b_str ] + a + b 
+    elsif a  
+      return a 
+    else b 
+      return b 
+    end 
+  end 
+
+
+
+  def render_ok
+       output_js = :false
+       output_js = :true if params['format'] == "js"
+
+       if output_js == :true
+         render :text => "osbResponse();", :content_type => "text/javascript" 
+       else
+         render :text => "ok " + @bug.id.to_s + "\n", :content_type => "text/html" if @bug
+         render :text => "ok\n", :content_type => "text/html" unless @bug
+       end
+  end
+
+  def getLimit
+       limit = 100;
+       limit = params['limit'] if ((params['limit']) && (params['limit'].to_i < 10000) && (params['limit'].to_i > 0))
+       return limit
+  end
+
+  def closedCondition
+       closed_since = 7 unless params['closed']
+       closed_since = params['closed'].to_i if params['closed']
+       
+       if closed_since < 0
+         conditions = ["status != 'hidden'"]
+       elsif closed_since > 0
+         conditions = ["((status = 'open') OR ((status = 'closed' ) AND (date_closed > '" + (Time.now - closed_since.days).to_s + "')))"]
+       else
+         conditions = ["status = 'open'"]
+       end
+
+       return conditions
+  end
+
+  def add_comment(bug, comment, name,event) 
+    t = Time.now.getutc 
+    bug_comment = bug.map_bug_comment.create(:date_created => t, :visible => true, :event => event);
+       bug_comment.comment = comment unless comment == :nil
+    if @user  
+      bug_comment.commenter_id = @user.id
+         bug_comment.commenter_name = @user.display_name
+    else  
+      bug_comment.commenter_ip = request.remote_ip
+         bug_comment.commenter_name = name + " (a)"
+    end
+    bug_comment.save; 
+    bug.last_changed = t 
+    bug.save 
+  end
+
+end
diff --git a/app/models/map_bug.rb b/app/models/map_bug.rb
new file mode 100644 (file)
index 0000000..0effe38
--- /dev/null
@@ -0,0 +1,57 @@
+class MapBug < ActiveRecord::Base
+  include GeoRecord
+
+  set_table_name 'map_bugs'
+
+  validates_presence_of :id, :on => :update
+  validates_uniqueness_of :id
+  validates_numericality_of :latitude, :only_integer => true
+  validates_numericality_of :longitude, :only_integer => true
+  validates_presence_of :date_created
+  validates_presence_of :last_changed
+  validates_prensence_of :date_closed if :status == "closed"
+  validates_inclusion_of :status, :in => [ "open", "closed", "hidden" ]
+
+  has_many :map_bug_comment, :foreign_key => :bug_id, :order => :date_created, :conditions => "visible = true and comment is not null"
+
+
+  def self.create_bug(lat, lon)
+       bug = MapBug.new(:lat => lat, :lon => lon);
+       raise OSM::APIBadUserInput.new("The node is outside this world") unless bug.in_world?
+       bug.date_created = Time.now.getutc
+       bug.last_changed = Time.now.getutc
+       bug.status = "open";
+       return bug;
+  end
+
+  def close_bug
+       self.status = "closed"
+       close_time = Time.now.getutc
+       self.last_changed = close_time
+       self.date_closed = close_time
+
+       self.save;
+  end
+
+  def flatten_comment ( separator_char, upto_timestamp = :nil)
+       resp = ""
+       comment_no = 1
+       self.map_bug_comment.each do |comment|
+         next if upto_timestamp != :nil and comment.date_created > upto_timestamp
+        resp += (comment_no == 1 ? "" : separator_char)
+               resp += comment.comment if comment.comment
+               resp += " [ " 
+               resp += comment.commenter_name if comment.commenter_name
+               resp += " " + comment.date_created.to_s + " ]"
+               comment_no += 1
+       end
+
+       return resp
+
+  end
+
+  def visible
+       return status != "hidden"
+  end
+
+end
diff --git a/app/models/map_bug_comment.rb b/app/models/map_bug_comment.rb
new file mode 100644 (file)
index 0000000..166e3fb
--- /dev/null
@@ -0,0 +1,15 @@
+class MapBugComment < ActiveRecord::Base
+
+  set_table_name 'map_bug_comment'
+
+  belongs_to :map_bug, :foreign_key => 'bug_id'
+  belongs_to :user, :foreign_key => 'commenter_id'
+
+   validates_inclusion_of :event, :in => [ "opened", "closed", "reopened", "commented", "hidden" ]
+
+  validates_presence_of :id, :on => :update
+  validates_uniqueness_of :id
+  validates_presence_of :visible
+  validates_presence_of :date_created
+
+end
index 13a352820fc026366e12496d8e02c4dcb0ffb11c..2e20f07f38fc0a80ab7d1f1ca10a8f7d0676c379 100644 (file)
 
         $("area_larger_map").href = '/?minlon='+minlon+'&minlat='+minlat+'&maxlon='+maxlon+'&maxlat='+maxlat+'&box=yes';
         $("area_larger_map").innerHTML = "<%= t 'browse.map.larger.area' %>";
+      <% else if map.instance_of? MapBug %>
+               $("loading").innerHTML = "";
+               var centre = new OpenLayers.LonLat(<%= map.lon %>, <%= map.lat %>);
+        var zoom = 16;
+        setMapCenter(centre, zoom);
+               marker = addMarkerToMap(centre);
+               $("area_larger_map").href = '/?mlon=<%= map.lon %>&mlat=<%=map.lat %>';
+        $("area_larger_map").innerHTML = "<%= t 'browse.map.larger.area' %>";
       <% else %>
         var obj_type = "<%= map.class.name.downcase %>";
         var obj_id = <%= map.id %>;
@@ -69,7 +77,7 @@
             $("small_map").style.display = "none";
           }
         });
-      <% end %>
+      <% end end %>
     }
 
     window.onload = init;
diff --git a/app/views/browse/bug.html.erb b/app/views/browse/bug.html.erb
new file mode 100644 (file)
index 0000000..5c65b3e
--- /dev/null
@@ -0,0 +1,87 @@
+<table width="100%">
+  <tr>
+    <td width="100%">
+      <h2>
+               <% if @bug.status == "closed" %>
+                       <%= image_tag("closed_bug_marker.png", :alt => 'closed') %>
+                       <%= t'browse.bug.closed_title', :bug_name => @bug.id %>
+               <% else %>
+                       <%= image_tag("open_bug_marker.png", :alt => 'open') %>
+                       <%=     t'browse.bug.open_title', :bug_name => @bug.id %>
+               <% end %>
+               </h2>
+    </td>
+    <td>
+      <%= render :partial => "navigation" %>
+    </td>
+  </tr>
+  <tr valign="top">
+    <td>
+               <table>
+                       <tr>
+                           <th><%= t 'browse.bug.created_at' %></th>
+                           <td><%= l @bug.date_created %></td>
+                       </tr>  
+                       
+                       <tr>
+                               <th><%= t 'browse.bug.edited_at' %></th>
+                               <td><%= l @bug.last_changed %></td>
+                       </tr>
+                       <% if @bug.status == "closed" %>
+                       <tr>
+                           <th><%= t 'browse.bug.closed_at' %></th>
+                           <td><%= l @bug.date_closed %></td>
+                       </tr>
+                       <% end %>
+                       <tr>
+                               <th><%= t 'browse.bug.opened_by' %></th>
+                               <% if @bug.map_bug_comment[0].user.nil? %>
+                                       <td> <%= @bug.map_bug_comment[0].commenter_name %> </td>
+                               <% else %>
+                                       <td><%= link_to h(@bug.map_bug_comment[0].user.display_name), :controller => "user", :action => "view", :display_name => @bug.map_bug_comment[0].user.display_name %></td>                                      
+                               <% end %>
+                       </tr>
+                       <tr>
+                               <th><%= t 'browse.bug.description' %></th>
+                               <td><%= h(@bug.map_bug_comment[0].comment) %></td>
+                       </tr>
+
+                       <tr>
+                               <th><%= t 'browse.node_details.coordinates' %></th>
+                               <td><div class="geo"><%= link_to ("<span class='latitude'>#{number_with_delimiter(@bug.lat)}</span>, <span class='longitude'>#{number_with_delimiter(@bug.lon)}</span>"), {:controller => 'site', :action => 'index', :lat => h(@bug.lat), :lon => h(@bug.lon), :zoom => "18"} %></div></td>
+                               </tr>
+
+               </table>
+
+               <br>
+
+       <%if @bug.map_bug_comment.length > 1 %>
+
+               <table>
+                       <tr>
+                               <th width="20%"> <%= t 'browse.bug.comment_by' %></th> <th width="60%"> <%= t 'browse.bug.comment' %></th> <th width="20%"> <%= t 'browse.bug.date' %></th> 
+                       </tr>
+                       <% @bug.map_bug_comment[1..-1].each do |bug_comment| %>
+                               <tr>
+                                       
+                                       <td>
+                                               <% if bug_comment.user.nil? %>
+                                                       <%= bug_comment.commenter_name %>
+                                               <% else %>
+                                                       <%= link_to h(bug_comment.user.display_name), :controller => "user", :action => "view", :display_name => bug_comment.user.display_name %>                                       
+                                               <% end %>
+                                       </td>
+                                       <td> <%= h(bug_comment.comment) %> </td>
+                                       <td> <%= l bug_comment.date_created %> </td>
+                               </tr>
+                       <% end %>
+               </table>
+
+       <% end %>
+       
+      <hr />
+      
+    </td>
+    <%= render :partial => "map", :object => @bug %>
+  </tr>
+</table>
\ No newline at end of file
diff --git a/app/views/map_bugs/_bugs_paging_nav.html.erb b/app/views/map_bugs/_bugs_paging_nav.html.erb
new file mode 100644 (file)
index 0000000..3de1790
--- /dev/null
@@ -0,0 +1,17 @@
+<p>
+
+<% if @page > 1 %>
+<%= link_to t('changeset.changeset_paging_nav.previous'), params.merge({ :page => @page - 1 }) %>
+<% else %>
+<%= t('changeset.changeset_paging_nav.previous') %>
+<% end %>
+
+| <%= t('changeset.changeset_paging_nav.showing_page', :page => @page) %> |
+
+<% if @bugs.size < @page_size %>
+<%= t('changeset.changeset_paging_nav.next') %>
+<% else %>
+<%= link_to t('changeset.changeset_paging_nav.next'), params.merge({ :page => @page + 1 }) %>
+<% end %>
+
+</p>
diff --git a/app/views/map_bugs/_user.html.erb b/app/views/map_bugs/_user.html.erb
new file mode 100644 (file)
index 0000000..0e95076
--- /dev/null
@@ -0,0 +1 @@
+<%= link_to user.display_name, :controller => "user", :action => "view", :display_name => user.display_name %>
diff --git a/app/views/map_bugs/get_bugs.gpx.builder b/app/views/map_bugs/get_bugs.gpx.builder
new file mode 100644 (file)
index 0000000..be7e9cf
--- /dev/null
@@ -0,0 +1,23 @@
+xml.instruct!
+
+
+xml.gpx("version" => "1.1", 
+        "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance",
+           "xsi:schemaLocation" => "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd") do
+
+       for bug in @bugs
+      xml.wpt("lon" => bug.lon, "lat" => bug.lat) do
+               xml.desc do
+                       xml.cdata! bug.flatten_comment("<hr />")
+               end
+               xml.extension do
+                       if bug.status = "open"
+                               xml.closed "0"
+                       else
+                               xml.closed "1"
+                       end
+                       xml.id bug.id
+               end
+      end
+       end
+end
diff --git a/app/views/map_bugs/get_bugs.js.erb b/app/views/map_bugs/get_bugs.js.erb
new file mode 100644 (file)
index 0000000..5bc9aaf
--- /dev/null
@@ -0,0 +1,7 @@
+<% if @bugs.empty? %>
+
+<% else %>
+       <% @bugs.each do |bug| %> 
+putAJAXMarker(<%= bug.id.to_s %> , <%= bug.lon.to_s %> , <%= bug.lat.to_s %> , '<%= escape_javascript(bug.flatten_comment("<hr />")) %>', <%= (bug.status=="open"?"0":"1") %> );
+    <% end %>
+<% end %>
\ No newline at end of file
diff --git a/app/views/map_bugs/get_bugs.rss.builder b/app/views/map_bugs/get_bugs.rss.builder
new file mode 100644 (file)
index 0000000..a8ae98d
--- /dev/null
@@ -0,0 +1,34 @@
+xml.instruct!
+
+xml.rss("version" => "2.0", 
+        "xmlns:geo" => "http://www.w3.org/2003/01/geo/wgs84_pos#",
+        "xmlns:georss" => "http://www.georss.org/georss") do
+  xml.channel do
+    xml.title "OpenStreetBugs"
+    xml.description t('bugs.rss.description_area', :min_lat => @min_lat, :min_lon => @min_lon, :max_lat => @max_lat, :max_lon => @max_lon )
+    xml.link url_for(:controller => "site", :action => "index", :only_path => false)
+
+       for bug in @bugs
+      xml.item do
+               if bug.status == "closed"
+                       xml.title t('bugs.rss.closed', :place => bug.nearby_place)      
+               else if bug.map_bug_comment.length > 1
+                       xml.title t('bugs.rss.comment', :place => bug.nearby_place)
+               else
+                       xml.title t('bugs.rss.new', :place => bug.nearby_place)
+               end     end
+        
+        xml.link url_for(:controller => "browse", :action => "bug", :id => bug.id, :only_path => false)
+               xml.guid url_for(:controller => "browse", :action => "bug", :id => bug.id, :only_path => false)
+        xml.description  htmlize(bug.flatten_comment("<br><br>"))
+               if (!bug.map_bug_comment.empty?)
+               xml.author bug.map_bug_comment[-1].commenter_name
+               end
+        xml.pubDate bug.last_changed.to_s(:rfc822)
+          xml.geo :lat, bug.lat
+          xml.geo :long, bug.lon
+          xml.georss :point, "#{bug.lat} #{bug.lon}"
+      end
+       end
+  end
+end
diff --git a/app/views/map_bugs/get_bugs.xml.builder b/app/views/map_bugs/get_bugs.xml.builder
new file mode 100644 (file)
index 0000000..a594506
--- /dev/null
@@ -0,0 +1,29 @@
+xml.instruct!
+
+xml.bugs do
+       for bug in @bugs
+               xml.bug("lon" => bug.lon, "lat" => bug.lat) do
+                       xml.id bug.id
+                       xml.date_created bug.date_created
+                       xml.nearby bug.nearby_place
+                       xml.status bug.status
+                       if bug.status == "closed"
+                               xml.date_closed bug.date_closed
+                       end
+                       xml.comments do
+                               for comment in bug.map_bug_comment
+                                       xml.comment do
+                                               xml.date comment.date_created
+                                               if !comment.commenter_id.nil?
+                                                       xml.uid comment.commenter_id
+                                                       xml.user comment.user.display_name      
+                                               else
+                                                       xml.user comment.commenter_name
+                                               end
+                                               xml.text comment.comment
+                                       end     
+                               end
+                       end
+               end
+       end
+end
diff --git a/app/views/map_bugs/my_bugs.html.erb b/app/views/map_bugs/my_bugs.html.erb
new file mode 100644 (file)
index 0000000..97e5700
--- /dev/null
@@ -0,0 +1,45 @@
+<h1><%= @heading %></h1>
+<p><%= @description %></p>
+
+<%= render :partial => 'bugs_paging_nav' %>
+
+<table id="bug_list" cellpadding="3">
+       <tr>
+       <th></th>
+               <th><%= t'bugs.user.id' %></th>
+               <th><%= t'changeset.changesets.user' %></th>
+               <th><%= t'changeset.changesets.comment' %></th>
+       <th><%= t'changeset.changesets.saved_at' %></th>
+               <th><%= t'bugs.user.last_changed' %></th>
+       
+       
+       </tr>
+<% @bugs.each do |bug| %>
+<% if bug.map_bug_comment[0].user == @user2 %>
+       <tr>
+<% else %>     
+       <tr bgcolor=#EEEEEE>
+<% end %>      
+               <td>
+                       <% if bug.status == "closed" %>
+                               <%= image_tag("closed_bug_marker.png", :alt => 'closed') %>
+                       <% else %>
+                               <%= image_tag("open_bug_marker.png", :alt => 'open') %>
+                       <% end %>
+               </td>
+               <td><%= link_to bug.id.to_s, :controller => "browse", :action => "bug", :id => bug.id %></td>
+               
+               <% if bug.map_bug_comment[0].user.nil? %> 
+                       <td> <%= bug.map_bug_comment[0].commenter_name %> </td> 
+               <% else %> 
+                       <td><%= link_to h(bug.map_bug_comment[0].user.display_name), :controller => "user", :action => "view", :display_name => bug.map_bug_comment[0].user.display_name %></td>
+        <% end %>
+               <td> <%= htmlize bug.map_bug_comment[0].comment  %> </td>       
+               <td><%= l bug.date_created %></td>
+               <td><%= l bug.last_changed %></td>
+       </tr>
+<% end %>
+</table>
+
+
+<%= render :partial => 'bugs_paging_nav' %>
diff --git a/app/views/map_bugs/read.rss.builder b/app/views/map_bugs/read.rss.builder
new file mode 100644 (file)
index 0000000..501ed6b
--- /dev/null
@@ -0,0 +1,32 @@
+xml.instruct!
+
+xml.rss("version" => "2.0", 
+        "xmlns:geo" => "http://www.w3.org/2003/01/geo/wgs84_pos#",
+        "xmlns:georss" => "http://www.georss.org/georss") do
+  xml.channel do
+    xml.title "OpenStreetBugs"
+    xml.description t('bugs.rss.description_item',:id => @bug.id)
+    xml.link url_for(:controller => "site", :action => "index", :only_path => false)
+
+    xml.item do
+               if @bug.status == "closed"
+                       xml.title t('bugs.rss.closed', :place => @bug.nearby_place)     
+               else if @bug.map_bug_comment.length > 1
+                       xml.title t('bugs.rss.comment', :place => @bug.nearby_place)
+               else
+                       xml.title t('bugs.rss.new', :place => @bug.nearby_place)
+               end     end
+        
+        xml.link url_for(:controller => "browse", :action => "bug", :id => @bug.id, :only_path => false)
+               xml.guid url_for(:controller => "map_bugs", :action => "read", :id => @bug.id, :only_path => false)
+        xml.description  htmlize(@bug.flatten_comment("<br><br>"))
+               if (!@bug.map_bug_comment.empty?)
+               xml.author @bug.map_bug_comment[-1].commenter_name
+               end
+        xml.pubDate @bug.last_changed.to_s(:rfc822)
+          xml.geo :lat, @bug.lat
+          xml.geo :long, @bug.lon
+          xml.georss :point, "#{@bug.lat} #{@bug.lon}"
+       end
+  end
+end
diff --git a/app/views/map_bugs/read.xml.builder b/app/views/map_bugs/read.xml.builder
new file mode 100644 (file)
index 0000000..f9d7dda
--- /dev/null
@@ -0,0 +1,25 @@
+xml.instruct!
+
+xml.bug("lon" => @bug.lon, "lat" => @bug.lat) do
+       xml.id @bug.id
+       xml.date_created @bug.date_created
+       xml.nearby @bug.nearby_place
+       xml.status @bug.status
+       if @bug.status == "closed"
+               xml.date_closed @bug.date_closed
+       end
+       xml.comments do
+               for comment in @bug.map_bug_comment
+                       xml.comment do
+                               xml.date comment.date_created
+                               if !comment.commenter_id.nil?
+                                       xml.uid comment.commenter_id
+                                       xml.user comment.user.display_name      
+                               else
+                                       xml.user comment.commenter_name
+                               end
+                               xml.text comment.comment
+                       end     
+               end
+       end
+end
diff --git a/app/views/map_bugs/rss.rss.builder b/app/views/map_bugs/rss.rss.builder
new file mode 100644 (file)
index 0000000..a6487a0
--- /dev/null
@@ -0,0 +1,49 @@
+xml.instruct!
+
+xml.rss("version" => "2.0", 
+        "xmlns:geo" => "http://www.w3.org/2003/01/geo/wgs84_pos#",
+        "xmlns:georss" => "http://www.georss.org/georss") do
+  xml.channel do
+    xml.title "OpenStreetBugs"
+    xml.description t('bugs.rss.description_area', :min_lat => @min_lat, :min_lon => @min_lon, :max_lat => @max_lat, :max_lon => @max_lon )
+    xml.link url_for(:controller => "site", :action => "index", :only_path => false)
+
+       for comment in @comments
+      xml.item do
+               if comment.event == "closed"
+                       xml.title t('bugs.rss.closed', :place => comment.map_bug.nearby_place)  
+               else if comment.event == "commented"
+                       xml.title t('bugs.rss.comment', :place => comment.map_bug.nearby_place)
+               else if comment.event == "opened"
+                       xml.title t('bugs.rss.new', :place => comment.map_bug.nearby_place)
+               else
+                       xml.title "unknown event"
+               end     end end
+        
+        xml.link url_for(:controller => "browse", :action => "bug", :id => comment.map_bug.id, :only_path => false)
+               xml.guid url_for(:controller => "browse", :action => "bug", :id => comment.map_bug.id, :only_path => false)
+
+               description_text = ""
+        if (comment.event == "commented") and (not comment.nil?)
+                       description_text += "<B> Comment: </B><br>"
+                       description_text += htmlize(comment.comment)
+                       description_text += "<br>"
+               end
+               description_text += "<B> Full bug report: </B><br>"
+               description_text += comment.map_bug.flatten_comment("<br>", comment.date_created)
+               xml.description description_text 
+
+               if (comment.user.nil?)
+                       xml.author comment.commenter_name
+               else
+                       xml.author comment.user.display_name
+               end
+               
+        xml.pubDate comment.date_created.to_s(:rfc822)
+          xml.geo :lat, comment.map_bug.lat
+          xml.geo :long, comment.map_bug.lon
+          xml.georss :point, "#{comment.map_bug.lat} #{comment.map_bug.lon}"
+      end
+       end
+  end
+end
index 0a0b7c1da5e873a9e2a9a450dee08e963773d77a..7450eb2bae5ff10ecbe069d2998d4178c423faed 100644 (file)
@@ -24,7 +24,8 @@
 
 <div id="permalink">
   <a href="/" id="permalinkanchor"><%= t 'site.index.permalink' %></a><br/>
-  <a href="/" id="shortlinkanchor"><%= t 'site.index.shortlink' %></a>
+  <a href="/" id="shortlinkanchor"><%= t 'site.index.shortlink' %></a><br/>
+  <a href="javascript:void()" id="ReportBug">Report a problem</a>      
 </div>
 
 <div id="attribution">
@@ -113,8 +114,11 @@ else
 end
 %>
 
-<%= javascript_include_tag '/openlayers/OpenLayers.js' %>
+<!--Use stock OpenLayers for now, as the OSM one is missing some needed classes-->
+<script src="http://www.openlayers.org/api/OpenLayers.js"></script>
+<!-- < %= javascript_include_tag '/openlayers/OpenLayers.js' % > -->
 <%= javascript_include_tag '/openlayers/OpenStreetMap.js' %>
+<%= javascript_include_tag 'openstreetbugs.js' %>
 <%= javascript_include_tag 'map.js' %>
 
 <script type="text/javascript" defer="defer">
@@ -125,6 +129,11 @@ end
 
   OpenLayers.Lang.setCode("<%= I18n.locale.to_s %>");
 
+  function createBugCallBack() {
+       map.osbControl.deactivate();
+       document.getElementById("OpenLayers.Map_18_OpenLayers_Container").style.cursor = "default";
+  }
+
   function mapInit(){
     map = createMap("map");
 
@@ -132,6 +141,30 @@ end
       map.dataLayer = new OpenLayers.Layer("<%= I18n.t 'browse.start_rjs.data_layer_name' %>", { "visibility": false });
       map.dataLayer.events.register("visibilitychanged", map.dataLayer, toggleData);
       map.addLayer(map.dataLayer);
+
+      map.osbLayer = new OpenLayers.Layer.OpenStreetBugs("OpenStreetBugs", {
+          serverURL : "/api/0.6/",
+          iconOpen : new OpenLayers.Icon("http://openstreetbugs.schokokeks.org/client/open_bug_marker.png", new OpenLayers.Size(22, 22), new OpenLayers.Pixel(-11, -11)),
+          iconClosed : new OpenLayers.Icon("http://openstreetbugs.schokokeks.org/client/closed_bug_marker.png", new OpenLayers.Size(22, 22), new OpenLayers.Pixel(-11, -11)),
+          readonly : false,
+          setCookie : false,
+          cookieLifetime : 1000, 
+          cookiePath : "/my/map/", 
+          permalinkURL : "http://www.openstreetmap.org/", 
+          theme : "http://osm.cdauth.de/map/openstreetbugs.css" 
+      });
+
+      map.addLayer(map.osbLayer);
+
+      map.osbControl = new OpenLayers.Control.OpenStreetBugs(map.osbLayer); 
+         
+      map.addControl(map.osbControl);
+               
+         var lBug = document.getElementById('ReportBug');
+      lBug.addEventListener('click',function (e) {
+               map.osbControl.activate(); document.getElementById("OpenLayers.Map_18_OpenLayers_Container").style.cursor = "crosshair" },false);
+
+
     <% end %>
 
     <% unless object_zoom %>
index 7b341e7f452a01dd780f2a7c15ece999fdc7bf83..10e2eef7550664898ed6a40014561341369c4f17 100644 (file)
@@ -275,6 +275,17 @@ en:
       download_xml: "Download XML"
       view_history: "view history"
       edit: "edit"
+    bug:
+      open_title: "Unresolved issue: {{bug_name}}"
+      closed_title: "Resolved issue: {{bug_name}}"
+      created_at: "Created at:"
+      edited_at: "Edited at:"
+      closed_at: "Closed at:"
+      opened_by: "Opened by:"
+      description: "Description:"
+      comment_by: "Comment by: "
+      comment: "Comment:"
+      date: "Date:"
   changeset:
     changeset_paging_nav:
       showing_page: "Showing page {{page}}"
@@ -1851,6 +1862,20 @@ en:
       back: "View all blocks"
       revoker: "Revoker:"
       needs_view: "The user needs to log in before this block will be cleared."
+  bugs:
+    rss:
+      description_area: "A list of bugs, reported, commented on or closed in your area [({{min_lat}}|{{min_lon}}) -- ({{max_lat}}|{{max_lon}})]"
+      description_item: "An rss feed for bug {{id}}"
+      closed: "closed bug (near {{place}})"
+      new: "new bug (near {{place}})"
+      comment: "new comment (near {{place}})"
+    user:
+      title_user: "Bugs submitted or commented on by {{user}}"
+      heading_user: "{{user}}'s bugs"
+      description_user: "Bugs submitted or commented on by {{user}}"
+      id: "Id"
+      last_changed: "Last changed"
+
   javascripts:
     map:
       base:
index 56a59a207d7a52ef57f5b649cae6fafe22103d58..e648f2a198c7a49da7e7590d723d0d4d32fb2940 100644 (file)
@@ -74,6 +74,27 @@ ActionController::Routing::Routes.draw do |map|
   map.connect "api/#{API_VERSION}/amf/read", :controller =>'amf', :action =>'amf_read'
   map.connect "api/#{API_VERSION}/amf/write", :controller =>'amf', :action =>'amf_write'
   map.connect "api/#{API_VERSION}/swf/trackpoints", :controller =>'swf', :action =>'trackpoints'
+
+  # Map Bugs API  
+  map.connect "api/#{API_VERSION}/bugs/getBugs", :controller =>'map_bugs', :action =>'get_bugs'
+  map.connect "api/#{API_VERSION}/bugs/addPOIexec", :controller =>'map_bugs', :action =>'add_bug'
+  map.connect "api/#{API_VERSION}/bugs/closePOIexec", :controller =>'map_bugs', :action =>'close_bug'
+  map.connect "api/#{API_VERSION}/bugs/editPOIexec", :controller =>'map_bugs', :action =>'edit_bug'
+  map.connect "api/#{API_VERSION}/bugs/getGPX", :controller =>'map_bugs', :action =>'gpx_bugs'
+  map.connect "api/#{API_VERSION}/bugs/getRSSfeed", :controller =>'map_bugs', :action =>'rss'
+
+  map.connect "api/#{API_VERSION}/bugs", :controller => 'map_bugs', :action => 'get_bugs'
+  map.connect "api/#{API_VERSION}/bugs/search", :controller => 'map_bugs', :action => 'search'
+  map.connect "api/#{API_VERSION}/bugs/rss", :controller =>'map_bugs', :action =>'rss'
+  map.connect "api/#{API_VERSION}/bug/create", :controller => 'map_bugs', :action => 'add_bug'
+  map.connect "api/#{API_VERSION}/bug/:id/comment", :controller => 'map_bugs', :action => 'edit_bug', :id => /\d+/
+  map.connect "api/#{API_VERSION}/bug/:id/close", :controller => 'map_bugs', :action => 'close_bug', :id => /\d+/
+  map.connect "api/#{API_VERSION}/bug/:id", :controller => 'map_bugs', :action => 'read', :id => /\d+/, :conditions => { :method => :get }
+  map.connect "api/#{API_VERSION}/bug/:id", :controller => 'map_bugs', :action => 'delete', :id => /\d+/, :conditions => { :method => :delete }
+
+  map.connect '/user/:display_name/bugs', :controller => 'map_bugs', :action => 'my_bugs'
+  
+
   
   # Data browsing
   map.connect '/browse/start', :controller => 'browse', :action => 'start'
@@ -88,6 +109,7 @@ ActionController::Routing::Routes.draw do |map|
   map.connect '/user/:display_name/edits', :controller => 'changeset', :action => 'list'
   map.connect '/browse/changesets/feed', :controller => 'changeset', :action => 'list', :format => :atom
   map.connect '/browse/changesets', :controller => 'changeset', :action => 'list'
+  map.connect '/browse/bug/:id', :controller => 'browse', :action => 'bug', :id => /\d+/
   map.connect '/browse', :controller => 'changeset', :action => 'list'
 
   # web site
diff --git a/db/migrate/053_add_map_bug_tables.rb b/db/migrate/053_add_map_bug_tables.rb
new file mode 100644 (file)
index 0000000..2d3b734
--- /dev/null
@@ -0,0 +1,33 @@
+require 'lib/migrate'
+
+class AddMapBugTables < ActiveRecord::Migration
+  def self.up
+
+       create_enumeration :map_bug_status_enum, ["open", "closed","hidden"]
+
+    create_table :map_bugs do |t|
+      t.column :id, :bigint, :null => false 
+      t.integer :latitude, :null => false 
+      t.integer :longitude, :null => false 
+      t.column :tile, :bigint, :null => false
+      t.datetime :last_changed, :null => false
+      t.datetime :date_created, :null => false 
+      t.string :nearby_place 
+      t.string :text
+         t.column :status, :map_bug_status_enum, :null => false
+         
+    end
+
+    add_index :map_bugs, [:tile,:status], :name => "map_bugs_tile_idx"
+       add_index :map_bugs, [:last_changed], :name => "map_bugs_changed_idx"
+       add_index :map_bugs, [:date_created], :name => "map_bugs_created_idx"
+  end
+
+  def self.down
+    remove_index :map_bugs, :name => "map_bugs_tile_idx"
+       remove_index :map_bugs, :name => "map_bugs_changed_idx"
+       remove_index :map_bugs, :name => "map_bugs_created_idx"
+    drop_table :map_bugs
+       drop_enumeration :map_bug_status_enum
+  end
+end
diff --git a/db/migrate/054_refactor_map_bug_tables.rb b/db/migrate/054_refactor_map_bug_tables.rb
new file mode 100644 (file)
index 0000000..b45f935
--- /dev/null
@@ -0,0 +1,36 @@
+require 'lib/migrate'
+
+class RefactorMapBugTables < ActiveRecord::Migration
+  def self.up
+
+
+    create_table :map_bug_comment do |t|
+      t.column :id, :bigint, :null => false
+         t.column :bug_id, :bigint, :null => false
+      t.boolean :visible, :null => false 
+      t.datetime :date_created, :null => false
+         t.string :commenter_name
+         t.string :commenter_ip
+         t.column :commenter_id, :bigint
+      t.string :comment
+    end
+
+       remove_column :map_bugs, :text 
+
+    add_index :map_bug_comment, [:bug_id], :name => "map_bug_comment_id_idx"
+       add_foreign_key :map_bug_comment, [:bug_id], :map_bugs, [:id]
+       add_foreign_key :map_bug_comment, [:commenter_id], :users, [:id]
+
+  end
+
+  def self.down
+
+       add_column :map_bugs, :text, :string
+
+    remove_index :map_bugs, :name => "map_bug_comment_id_idx"
+       remove_foreign_key :map_bug_comment, [:bug_id]
+       remove_foreign_key :map_bug_comment, [:commenter_id]
+
+    drop_table :map_bugs_comment
+  end
+end
diff --git a/db/migrate/055_change_map_bug_comment_type.rb b/db/migrate/055_change_map_bug_comment_type.rb
new file mode 100644 (file)
index 0000000..bf009c3
--- /dev/null
@@ -0,0 +1,11 @@
+require 'lib/migrate'
+
+class ChangeMapBugCommentType < ActiveRecord::Migration
+  def self.up
+       change_column :map_bug_comment, :comment, :text
+  end
+
+  def self.down
+       change_column :map_bug_comment, :comment, :string
+  end
+end
diff --git a/db/migrate/056_add_date_closed.rb b/db/migrate/056_add_date_closed.rb
new file mode 100644 (file)
index 0000000..7f609ca
--- /dev/null
@@ -0,0 +1,13 @@
+require 'lib/migrate'
+
+class AddDateClosed < ActiveRecord::Migration
+  def self.up
+
+       add_column :map_bugs, :date_closed, :timestamp
+  end
+
+  def self.down
+
+       remove_column :map_bugs, :date_closed 
+  end
+end
diff --git a/db/migrate/057_add_map_bug_comment_event.rb b/db/migrate/057_add_map_bug_comment_event.rb
new file mode 100644 (file)
index 0000000..ff5f0b3
--- /dev/null
@@ -0,0 +1,14 @@
+require 'lib/migrate'
+
+class AddMapBugCommentEvent < ActiveRecord::Migration
+  def self.up
+       create_enumeration :map_bug_event_enum, ["opened", "closed","reopened","commented","hidden"]
+       add_column :map_bug_comment, :event, :map_bug_event_enum
+  end
+
+  def self.down
+
+       remove_column :map_bug_comment, :event
+       drop_enumeration :map_bug_event_enum
+  end
+end
index 2740eab0c5472da4c76d95128c5f8253dd440cbb..d44227dd85d77d07ec30db995041595d6dea3c92 100644 (file)
@@ -53,6 +53,11 @@ private
       self.with_scope(:find => {:conditions => OSM.sql_for_area(minlat, minlon, maxlat, maxlon)}) do
         return self.find(:all, options)
       end
+    end
+       def find_by_area_no_quadtile(minlat, minlon, maxlat, maxlon, options)
+      self.with_scope(:find => {:conditions => OSM.sql_for_area_no_quadtile(minlat, minlon, maxlat, maxlon)}) do
+        return self.find(:all, options)
+      end
     end
   end
 end
index f3accf2da4e5b12241c0a0ac4091ec9d5791e929..7d20d2920ba06f25fe317bec859d02a8553fc6a0 100644 (file)
@@ -9,7 +9,7 @@ module MapBoundary
     return min_lon, min_lat, max_lon, max_lat
   end
 
-  def check_boundaries(min_lon, min_lat, max_lon, max_lat)
+  def check_boundaries(min_lon, min_lat, max_lon, max_lat, limit_small_area = :true)
     # check the bbox is sane
     unless min_lon <= max_lon
       raise OSM::APIBadBoundingBox.new("The minimum longitude must be less than the maximum longitude, but it wasn't")
@@ -22,6 +22,8 @@ module MapBoundary
       raise OSM::APIBadBoundingBox.new("The latitudes must be between -90 and 90, and longitudes between -180 and 180")
     end
 
+       return unless limit_small_area == :true
+
     # check the bbox isn't too large
     requested_area = (max_lat-min_lat)*(max_lon-min_lon)
     if requested_area > MAX_REQUEST_AREA
index eaee7c328032f0373073b7f70aab777f20c169a0..78d32e739e39cb7a0c259665ff544e4b4352780e 100644 (file)
@@ -498,7 +498,7 @@ module OSM
 
   # Return an SQL fragment to select a given area of the globe
   def self.sql_for_area(minlat, minlon, maxlat, maxlon, prefix = nil)
-    tilesql = QuadTile.sql_for_area(minlat, minlon, maxlat, maxlon, prefix)
+       tilesql = QuadTile.sql_for_area(minlat, minlon, maxlat, maxlon, prefix)
     minlat = (minlat * 10000000).round
     minlon = (minlon * 10000000).round
     maxlat = (maxlat * 10000000).round
@@ -507,6 +507,16 @@ module OSM
     return "#{tilesql} AND #{prefix}latitude BETWEEN #{minlat} AND #{maxlat} AND #{prefix}longitude BETWEEN #{minlon} AND #{maxlon}"
   end
 
+  # Return an SQL fragment to select a given area of the globe without using the quadtile index
+  def self.sql_for_area_no_quadtile(minlat, minlon, maxlat, maxlon, prefix = nil, without_quadtile = :false)
+    minlat = (minlat * 10000000).round
+    minlon = (minlon * 10000000).round
+    maxlat = (maxlat * 10000000).round
+    maxlon = (maxlon * 10000000).round
+
+    return "#{prefix}latitude BETWEEN #{minlat} AND #{maxlat} AND #{prefix}longitude BETWEEN #{minlon} AND #{maxlon}"
+  end
+
   # Return a spam score for a chunk of text
   def self.spam_score(text)
     link_count = 0
diff --git a/public/images/closed_bug_marker.png b/public/images/closed_bug_marker.png
new file mode 100644 (file)
index 0000000..bf6d6bb
Binary files /dev/null and b/public/images/closed_bug_marker.png differ
diff --git a/public/images/open_bug_marker.png b/public/images/open_bug_marker.png
new file mode 100644 (file)
index 0000000..a580316
Binary files /dev/null and b/public/images/open_bug_marker.png differ
diff --git a/public/javascripts/openstreetbugs.js b/public/javascripts/openstreetbugs.js
new file mode 100644 (file)
index 0000000..72eeb60
--- /dev/null
@@ -0,0 +1,920 @@
+/*
+       This OpenStreetBugs client is free software: you can redistribute it
+       and/or modify it under the terms of the GNU Affero General Public License
+       as published by the Free Software Foundation, either version 3 of the
+       License, or (at your option) any later version.
+
+       This file is distributed in the hope that it will be useful, but
+       WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+       or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
+       License <http://www.gnu.org/licenses/> for more details.
+*/
+
+/**
+ * A fully functional OpenStreetBugs layer. See http://openstreetbugs.schokokeks.org/.
+ * Even though the OpenStreetBugs API originally does not intend this, you can create multiple instances of this Layer and add them to different maps (or to one single map for whatever crazy reason) without problems.
+*/
+
+OpenLayers.Layer.OpenStreetBugs = new OpenLayers.Class(OpenLayers.Layer.Markers, {
+       /**
+        * The URL of the OpenStreetBugs API.
+        * @var String
+       */
+       serverURL : "http://openstreetbugs.schokokeks.org/api/0.1/",
+
+       /**
+        * Associative array (index: bug ID) that is filled with the bugs loaded in this layer
+        * @var String
+       */
+       bugs : { },
+
+       /**
+        * The username to be used to change or create bugs on OpenStreetBugs
+        * @var String
+       */
+       username : "NoName",
+
+       /**
+        * The icon to be used for an open bug
+        * @var OpenLayers.Icon
+       */
+       iconOpen : new OpenLayers.Icon("http://openstreetbugs.schokokeks.org/client/open_bug_marker.png", new OpenLayers.Size(22, 22), new OpenLayers.Pixel(-11, -11)),
+
+       /**
+        * The icon to be used for a closed bug
+        * @var OpenLayers.Icon
+       */
+       iconClosed : new OpenLayers.Icon("http://openstreetbugs.schokokeks.org/client/closed_bug_marker.png", new OpenLayers.Size(22, 22), new OpenLayers.Pixel(-11, -11)),
+
+       /**
+        * The projection of the coordinates sent by the OpenStreetBugs API.
+        * @var OpenLayers.Projection
+       */
+       apiProjection : new OpenLayers.Projection("EPSG:4326"),
+
+       /**
+        * If this is set to true, the user may not commit comments or close bugs.
+        * @var Boolean
+       */
+       readonly : false,
+
+       /**
+        * When the layer is hidden, all open popups are stored in this array in order to be re-opened again when the layer is made visible again.
+       */
+       reopenPopups : [ ],
+
+       /**
+        * The user name will be saved in a cookie if this isn’t set to false.
+        * @var Boolean
+       */
+       setCookie : true,
+
+       /**
+        * The lifetime of the user name cookie in days.
+        * @var Number
+       */
+       cookieLifetime : 1000,
+
+       /**
+        * The path where the cookie will be available on this server.
+        * @var String
+       */
+       cookiePath : null,
+
+       /**
+        * A URL to append lon=123&lat=123&zoom=123 for the Permalinks.
+        * @var String
+       */
+       permalinkURL : "http://www.openstreetmap.org/",
+
+       /**
+        * A CSS file to be included. Set to null if you don’t need this.
+        * @var String
+       */
+       theme : "http://osm.cdauth.de/map/openstreetbugs.css",
+
+       /**
+        * @param String name
+       */
+       initialize : function(name, options)
+       {
+               OpenLayers.Layer.Markers.prototype.initialize.apply(this, [ name, OpenLayers.Util.extend({ opacity: 0.7, projection: new OpenLayers.Projection("EPSG:4326") }, options) ]);
+               putAJAXMarker.layers.push(this);
+               this.events.addEventType("markerAdded");
+
+               this.events.register("visibilitychanged", this, this.updatePopupVisibility);
+               this.events.register("visibilitychanged", this, this.loadBugs);
+
+               var cookies = document.cookie.split(/;\s*/);
+               for(var i=0; i<cookies.length; i++)
+               {
+                       var cookie = cookies[i].split("=");
+                       if(cookie[0] == "osbUsername")
+                       {
+                               this.username = decodeURIComponent(cookie[1]);
+                               break;
+                       }
+               }
+
+               /* Copied from OpenLayers.Map */
+               if(this.theme) {
+            // check existing links for equivalent url
+            var addNode = true;
+            var nodes = document.getElementsByTagName('link');
+            for(var i=0, len=nodes.length; i<len; ++i) {
+                if(OpenLayers.Util.isEquivalentUrl(nodes.item(i).href,
+                                                   this.theme)) {
+                    addNode = false;
+                    break;
+                }
+            }
+            // only add a new node if one with an equivalent url hasn't already
+            // been added
+            if(addNode) {
+                var cssNode = document.createElement('link');
+                cssNode.setAttribute('rel', 'stylesheet');
+                cssNode.setAttribute('type', 'text/css');
+                cssNode.setAttribute('href', this.theme);
+                document.getElementsByTagName('head')[0].appendChild(cssNode);
+            }
+        }
+       },
+
+       /**
+        * Is automatically called when the layer is added to an OpenLayers.Map. Initialises the automatic bug loading in the visible bounding box.
+       */
+       afterAdd : function()
+       {
+               var ret = OpenLayers.Layer.Markers.prototype.afterAdd.apply(this, arguments);
+
+               this.map.events.register("moveend", this, this.loadBugs);
+               this.loadBugs();
+
+               return ret;
+       },
+
+       /**
+        * At the moment the OSB API responses to requests using JavaScript code. This way the Same Origin Policy can be worked around. Unfortunately, this makes communicating with the API a bit too asynchronous, at the moment there is no way to tell to which request the API actually responses.
+        * This method creates a new script HTML element that imports the API request URL. The API JavaScript response then executes the global functions provided below.
+        * @param String url The URL this.serverURL + url is requested.
+       */
+       apiRequest : function(url) {
+               var script = document.createElement("script");
+               script.type = "text/javascript";
+               script.src = this.serverURL + url + "&nocache="+(new Date()).getTime();
+               document.body.appendChild(script);
+       },
+
+       /**
+        * Is automatically called when the visibility of the layer changes. When the layer is hidden, all visible popups
+        * are closed and their visibility is saved. When the layer is made visible again, these popups are re-opened.
+       */
+       updatePopupVisibility : function()
+       {
+               if(this.getVisibility())
+               {
+                       for(var i=0; i<this.reopenPopups.length; i++)
+                               this.reopenPopups[i].show();
+                       this.reopenPopups = [ ];
+               }
+               else
+               {
+                       for(var i=0; i<this.markers.length; i++)
+                       {
+                               if(this.markers[i].feature.popup && this.markers[i].feature.popup.visible())
+                               {
+                                       this.markers[i].feature.popup.hide();
+                                       this.reopenPopups.push(this.markers[i].feature.popup);
+                               }
+                       }
+               }
+       },
+
+       /**
+        * Sets the user name to be used for interactions with OpenStreetBugs.
+       */
+       setUserName : function(username)
+       {
+               if(this.username == username)
+                       return;
+
+               this.username = username;
+
+               if(this.setCookie)
+               {
+                       var cookie = "osbUsername="+encodeURIComponent(username);
+                       if(this.cookieLifetime)
+                               cookie += ";expires="+(new Date((new Date()).getTime() + this.cookieLifetime*86400000)).toGMTString();
+                       if(this.cookiePath)
+                               cookie += ";path="+this.cookiePath;
+                       document.cookie = cookie;
+               }
+
+               for(var i=0; i<this.markers.length; i++)
+               {
+                       if(!this.markers[i].feature.popup) continue;
+                       var els = this.markers[i].feature.popup.contentDom.getElementsByTagName("input");
+                       for(var j=0; j<els.length; j++)
+                       {
+                               if(els[j].className != "osbUsername") continue;
+                               els[j].value = username;
+                       }
+               }
+       },
+
+       /**
+        * Returns the currently set username or “NoName” if none is set.
+       */
+
+       getUserName : function()
+       {
+               if(this.username)
+                       return this.username;
+               else
+                       return "NoName";
+       },
+
+       /**
+        * Loads the bugs in the current bounding box. Is automatically called by an event handler ("moveend" event) that is created in the afterAdd() method.
+       */
+       loadBugs : function()
+       {
+               if(!this.getVisibility())
+                       return true;
+
+               var bounds = this.map.getExtent();
+               if(!bounds) return false;
+               bounds.transform(this.map.getProjectionObject(), this.apiProjection);
+
+               this.apiRequest("bugs"
+                       + "?bbox="+this.round(bounds.left, 5)
+            + ","+this.round(bounds.bottom, 5)
+                   + ","+this.round(bounds.right, 5)                   
+                       + ","+this.round(bounds.top, 5));
+       },
+
+       /**
+        * Rounds the given number to the given number of digits after the floating point.
+        * @param Number number
+        * @param Number digits
+        * @return Number
+       */
+       round : function(number, digits)
+       {
+               var factor = Math.pow(10, digits);
+               return Math.round(number*factor)/factor;
+       },
+
+       /**
+        * Adds an OpenLayers.Marker representing a bug to the map. Is usually called by loadBugs().
+        * @param Number id The bug ID
+       */
+       createMarker: function(id)
+       {
+               if(this.bugs[id])
+               {
+                       if(this.bugs[id].popup && !this.bugs[id].popup.visible())
+                               this.setPopupContent(id);
+                       if(this.bugs[id].closed != putAJAXMarker.bugs[id][2])
+                               this.bugs[id].destroy();
+                       else
+                               return;
+               }
+
+               var lonlat = putAJAXMarker.bugs[id][0].clone().transform(this.apiProjection, this.map.getProjectionObject());
+               var comments = putAJAXMarker.bugs[id][1];
+               var closed = putAJAXMarker.bugs[id][2];
+               var feature = new OpenLayers.Feature(this, lonlat, { icon: (closed ? this.iconClosed : this.iconOpen).clone(), autoSize: true });
+               feature.popupClass = OpenLayers.Popup.FramedCloud.OpenStreetBugs;
+               feature.osbId = id;
+               feature.closed = closed;
+
+               var marker = feature.createMarker();
+               marker.feature = feature;
+               marker.events.register("click", feature, this.markerClick);
+               marker.events.register("mouseover", feature, this.markerMouseOver);
+               marker.events.register("mouseout", feature, this.markerMouseOut);
+               this.addMarker(marker);
+
+               this.bugs[id] = feature;
+               this.events.triggerEvent("markerAdded");
+       },
+
+       /**
+        * Recreates the content of the popup of a marker.
+        * @param Number id The bug ID
+       */
+
+       setPopupContent: function(id) {
+               if(!this.bugs[id].popup)
+                       return;
+
+               var el1,el2,el3;
+               var layer = this;
+
+               var newContent = document.createElement("div");
+
+               el1 = document.createElement("h3");
+               el1.appendChild(document.createTextNode(closed ? OpenLayers.i18n("Fixed Error") : OpenLayers.i18n("Unresolved Error")));
+
+               el1.appendChild(document.createTextNode(" ["));
+               el2 = document.createElement("a");
+               el2.href = "#";
+               el2.onclick = function(){ layer.map.setCenter(putAJAXMarker.bugs[id][0].clone().transform(layer.apiProjection, layer.map.getProjectionObject()), 15); };
+               el2.appendChild(document.createTextNode(OpenLayers.i18n("Zoom")));
+               el1.appendChild(el2);
+               el1.appendChild(document.createTextNode("]"));
+
+               if(this.permalinkURL)
+               {
+                       el1.appendChild(document.createTextNode(" ["));
+                       el2 = document.createElement("a");
+                       el2.href = this.permalinkURL + (this.permalinkURL.indexOf("?") == -1 ? "?" : "&") + "lon="+putAJAXMarker.bugs[id][0].lon+"&lat="+putAJAXMarker.bugs[id][0].lat+"&zoom=15";
+                       el2.appendChild(document.createTextNode(OpenLayers.i18n("Permalink")));
+                       el1.appendChild(el2);
+                       el1.appendChild(document.createTextNode("]"));
+               }
+               newContent.appendChild(el1);
+
+               var containerDescription = document.createElement("div");
+               newContent.appendChild(containerDescription);
+
+               var containerChange = document.createElement("div");
+               newContent.appendChild(containerChange);
+
+               var displayDescription = function(){
+                       containerDescription.style.display = "block";
+                       containerChange.style.display = "none";
+                       layer.bugs[id].popup.updateSize();
+               };
+               var displayChange = function(){
+                       containerDescription.style.display = "none";
+                       containerChange.style.display = "block";
+                       layer.bugs[id].popup.updateSize();
+               };
+               displayDescription();
+
+               el1 = document.createElement("dl");
+               for(var i=0; i<putAJAXMarker.bugs[id][1].length; i++)
+               {
+                       el2 = document.createElement("dt");
+                       el2.className = (i == 0 ? "osb-description" : "osb-comment");
+                       el2.appendChild(document.createTextNode(i == 0 ? OpenLayers.i18n("Description") : OpenLayers.i18n("Comment")));
+                       el1.appendChild(el2);
+                       el2 = document.createElement("dd");
+                       el2.className = (i == 0 ? "osb-description" : "osb-comment");
+                       el2.appendChild(document.createTextNode(putAJAXMarker.bugs[id][1][i]));
+                       el1.appendChild(el2);
+               }
+               containerDescription.appendChild(el1);
+
+               if(putAJAXMarker.bugs[id][2])
+               {
+                       el1 = document.createElement("p");
+                       el1.className = "osb-fixed";
+                       el2 = document.createElement("em");
+                       el2.appendChild(document.createTextNode(OpenLayers.i18n("Has been fixed.")));
+                       el1.appendChild(el2);
+                       containerDescription.appendChild(el1);
+               }
+               else if(!this.readonly)
+               {
+                       el1 = document.createElement("div");
+                       el2 = document.createElement("input");
+                       el2.setAttribute("type", "button");
+                       el2.onclick = function(){ displayChange(); };
+                       el2.value = OpenLayers.i18n("Comment/Close");
+                       el1.appendChild(el2);
+                       containerDescription.appendChild(el1);
+
+                       var el_form = document.createElement("form");
+                       el_form.onsubmit = function(){ if(inputComment.value.match(/^\s*$/)) return false; layer.submitComment(id, inputComment.value); layer.hidePopup(id); return false; };
+
+                       el1 = document.createElement("dl");
+                       el2 = document.createElement("dt");
+                       el2.appendChild(document.createTextNode(OpenLayers.i18n("Nickname")));
+                       el1.appendChild(el2);
+                       el2 = document.createElement("dd");
+                       var inputUsername = document.createElement("input");
+                       inputUsername.value = this.username;
+                       inputUsername.className = "osbUsername";
+                       inputUsername.onkeyup = function(){ layer.setUserName(inputUsername.value); };
+                       el2.appendChild(inputUsername);
+                       el1.appendChild(el2);
+
+                       el2 = document.createElement("dt");
+                       el2.appendChild(document.createTextNode(OpenLayers.i18n("Comment")));
+                       el1.appendChild(el2);
+                       el2 = document.createElement("dd");
+                       var inputComment = document.createElement("input");
+                       el2.appendChild(inputComment);
+                       el1.appendChild(el2);
+                       el_form.appendChild(el1);
+
+                       el1 = document.createElement("ul");
+                       el1.className = "buttons";
+                       el2 = document.createElement("li");
+                       el3 = document.createElement("input");
+                       el3.setAttribute("type", "submit");
+                       el3.value = OpenLayers.i18n("Add comment");
+                       el2.appendChild(el3);
+                       el1.appendChild(el2);
+
+                       el2 = document.createElement("li");
+                       el3 = document.createElement("input");
+                       el3.setAttribute("type", "button");
+                       el3.onclick = function(){ this.form.onsubmit(); layer.closeBug(id); layer.bugs[id].popup.hide(); return false; };
+                       el3.value = OpenLayers.i18n("Mark as fixed");
+                       el2.appendChild(el3);
+                       el1.appendChild(el2);
+                       el_form.appendChild(el1);
+                       containerChange.appendChild(el_form);
+
+                       el1 = document.createElement("div");
+                       el2 = document.createElement("input");
+                       el2.setAttribute("type", "button");
+                       el2.onclick = function(){ displayDescription(); };
+                       el2.value = OpenLayers.i18n("Cancel");
+                       el1.appendChild(el2);
+                       containerChange.appendChild(el1);
+               }
+
+               this.bugs[id].popup.setContentHTML(newContent);
+       },
+
+       /**
+        * Creates a new bug.
+        * @param OpenLayers.LonLat lonlat The coordinates in the API projection.
+        * @param String description
+       */
+       createBug: function(lonlat, description) {
+               this.apiRequest("bug/create"
+                       + "?lat="+encodeURIComponent(lonlat.lat)
+                       + "&lon="+encodeURIComponent(lonlat.lon)
+                       + "&text="+encodeURIComponent(description)
+                       + "&name="+encodeURIComponent(this.getUserName())
+                       + "&format=js"
+               );
+               createBugCallBack();
+       },
+
+       /**
+        * Adds a comment to a bug.
+        * @param Number id
+        * @param String comment
+       */
+       submitComment: function(id, comment) {
+               this.apiRequest("bug/"+encodeURIComponent(id)+"/comment"
+                       + "?text="+encodeURIComponent(comment)
+                       + "&name="+encodeURIComponent(this.getUserName())
+                       + "&format=js"
+               );
+       },
+
+       /**
+        * Marks a bug as fixed.
+        * @param Number id
+       */
+       closeBug: function(id) {
+               this.apiRequest("bug/"+encodeURIComponent(id)+"/close"
+                       + "?format=js"
+               );
+       },
+
+       /**
+        * Removes the content of a marker popup (to reduce the amount of needed resources).
+        * @param Number id
+       */
+       resetPopupContent: function(id) {
+               if(!this.bugs[id].popup)
+                       return;
+
+               this.bugs[id].popup.setContentHTML(document.createElement("div"));
+       },
+
+       /**
+        * Makes the popup of the given marker visible. Makes sure that the popup content is created if it does not exist yet.
+        * @param Number id
+       */
+       showPopup: function(id) {
+               var add = null;
+               if(!this.bugs[id].popup)
+               {
+                       add = this.bugs[id].createPopup(true);
+                       add.events.register("close", this, function(){ this.resetPopupContent(id); if(this.bugs[id].osbClicked) this.bugs[id].osbClicked = false; });
+               }
+               else if(this.bugs[id].popup.visible())
+                       return;
+
+               this.setPopupContent(id);
+               if(add)
+                       this.map.addPopup(add);
+               this.bugs[id].popup.show();
+               this.bugs[id].popup.updateSize();
+       },
+
+       /**
+        * Hides the popup of the given marker.
+        * @param Number id
+       */
+       hidePopup: function(id) {
+               if(!this.bugs[id].popup || !this.bugs[id].popup.visible())
+                       return;
+
+               this.bugs[id].popup.hide();
+               this.bugs[id].popup.events.triggerEvent("close");
+       },
+
+       /**
+        * Is run on the “click” event of a marker in the context of its OpenLayers.Feature. Toggles the visibility of the popup.
+       */
+       markerClick: function(e) {
+               var feature = this; // Context is the feature
+
+               feature.osbClicked = !feature.osbClicked;
+               if(feature.osbClicked)
+                       feature.layer.showPopup(feature.osbId);
+               else
+                       feature.layer.hidePopup(feature.osbId);
+               OpenLayers.Event.stop(e);
+       },
+
+       /**
+        * Is run on the “mouseover” event of a marker in the context of its OpenLayers.Feature. Makes the popup visible.
+       */
+       markerMouseOver: function(e) {
+               var feature = this; // Context is the feature
+
+               feature.layer.showPopup(feature.osbId);
+               OpenLayers.Event.stop(e);
+       },
+
+       /**
+        * Is run on the “mouseout” event of a marker in the context of its OpenLayers.Feature. Hides the popup (if it has not been clicked).
+       */
+       markerMouseOut: function(e) {
+               var feature = this; // Context is the feature
+
+               if(!feature.osbClicked)
+                       feature.layer.hidePopup(feature.osbId);
+               OpenLayers.Event.stop(e);
+       },
+
+       CLASS_NAME: "OpenLayers.Layer.OpenStreetBugs"
+});
+
+/**
+ * An OpenLayers control to create new bugs on mouse clicks on the map. Add an instance of this to your map using
+ * the OpenLayers.Map.addControl() method and activate() it.
+*/
+
+OpenLayers.Control.OpenStreetBugs = new OpenLayers.Class(OpenLayers.Control, {
+       title : null, // See below because of translation call
+
+       /**
+        * The icon to be used for the temporary markers that the “create bug” popup belongs to.
+        * @var OpenLayers.Icon
+       */
+       icon : new OpenLayers.Icon("http://openstreetbugs.schokokeks.org/client/icon_error_add.png", new OpenLayers.Size(22, 22), new OpenLayers.Pixel(-11, -11)),
+
+       /**
+        * An instance of the OpenStreetBugs layer that this control shall be connected to. Is set in the constructor.
+        * @var OpenLayers.Layer.OpenStreetBugs
+       */
+       osbLayer : null,
+
+       /**
+        * @param OpenLayers.Layer.OpenStreetBugs osbLayer The OpenStreetBugs layer that this control will be connected to.
+       */
+       initialize: function(osbLayer, options) {
+               this.osbLayer = osbLayer;
+
+               this.title = OpenLayers.i18n("Create OpenStreetBug");
+
+               OpenLayers.Control.prototype.initialize.apply(this, [ options ]);
+
+               this.events.register("activate", this, function() {
+                       if(!this.osbLayer.getVisibility())
+                               this.osbLayer.setVisibility(true);
+               });
+
+               this.osbLayer.events.register("visibilitychanged", this, function() {
+                       if(this.active && !this.osbLayer.getVisibility())
+                               this.osbLayer.setVisibility(true);
+               });
+       },
+
+       destroy: function() {
+               if (this.handler)
+                       this.handler.destroy();
+               this.handler = null;
+
+               OpenLayers.Control.prototype.destroy.apply(this, arguments);
+       },
+
+       draw: function() {
+               this.handler = new OpenLayers.Handler.Click(this, {'click': this.click}, { 'single': true, 'double': false, 'pixelTolerance': 0, 'stopSingle': false, 'stopDouble': false });
+       },
+
+       /**
+        * Map clicking event handler. Adds a temporary marker with a popup to the map, the popup contains the form to add a bug.
+       */
+       click: function(e) {
+               if(!this.map) return true;
+
+               var control = this;
+               var lonlat = this.map.getLonLatFromViewPortPx(e.xy);
+               var lonlatApi = lonlat.clone().transform(this.map.getProjectionObject(), this.osbLayer.apiProjection);
+               var feature = new OpenLayers.Feature(this.osbLayer, lonlat, { icon: this.icon.clone(), autoSize: true });
+               feature.popupClass = OpenLayers.Popup.FramedCloud.OpenStreetBugs;
+               var marker = feature.createMarker();
+               marker.feature = feature;
+               this.osbLayer.addMarker(marker);
+
+               var newContent = document.createElement("div");
+               var el1,el2,el3;
+               el1 = document.createElement("h3");
+               el1.appendChild(document.createTextNode(OpenLayers.i18n("Create bug")));
+               newContent.appendChild(el1);
+
+               var el_form = document.createElement("form");
+               el_form.onsubmit = function() { control.osbLayer.createBug(lonlatApi, inputDescription.value); marker.feature = null; feature.destroy(); return false; };
+
+               el1 = document.createElement("dl");
+               el2 = document.createElement("dt");
+               el2.appendChild(document.createTextNode(OpenLayers.i18n("Nickname")));
+               el1.appendChild(el2);
+               el2 = document.createElement("dd");
+               var inputUsername = document.createElement("input");
+               inputUsername.value = this.osbLayer.username;
+               inputUsername.className = "osbUsername";
+               inputUsername.onkeyup = function(){ control.osbLayer.setUserName(inputUsername.value); };
+               el2.appendChild(inputUsername);
+               el1.appendChild(el2);
+
+               el2 = document.createElement("dt");
+               el2.appendChild(document.createTextNode(OpenLayers.i18n("Bug description")));
+               el1.appendChild(el2);
+               el2 = document.createElement("dd");
+               var inputDescription = document.createElement("input");
+               el2.appendChild(inputDescription);
+               el1.appendChild(el2);
+               el_form.appendChild(el1);
+
+               el1 = document.createElement("div");
+               el2 = document.createElement("input");
+               el2.setAttribute("type", "submit");
+               el2.value = OpenLayers.i18n("Create");
+               el1.appendChild(el2);
+               el_form.appendChild(el1);
+               newContent.appendChild(el_form);
+
+               feature.data.popupContentHTML = newContent;
+               var popup = feature.createPopup(true);
+               popup.events.register("close", this, function(){ feature.destroy(); });
+               this.map.addPopup(popup);
+               popup.updateSize();
+       },
+
+       CLASS_NAME: "OpenLayers.Control.OpenStreetBugs"
+});
+
+
+/**
+ * This class changes the usual OpenLayers.Popup.FramedCloud class by using a DOM element instead of an innerHTML string as content for the popup.
+ * This is necessary for creating valid onclick handlers that still work with multiple OpenStreetBugs layer objects.
+*/
+
+OpenLayers.Popup.FramedCloud.OpenStreetBugs = new OpenLayers.Class(OpenLayers.Popup.FramedCloud, {
+       contentDom : null,
+       autoSize : true,
+
+       /**
+        * See OpenLayers.Popup.FramedCloud.initialize() for parameters. As fourth parameter, pass a DOM node instead of a string.
+       */
+       initialize: function() {
+               this.displayClass = this.displayClass + " " + this.CLASS_NAME.replace("OpenLayers.", "ol").replace(/\./g, "");
+
+               var args = new Array(arguments.length);
+               for(var i=0; i<arguments.length; i++)
+                       args[i] = arguments[i];
+
+               // Unset original contentHTML parameter
+               args[3] = null;
+
+               var closeCallback = arguments[6];
+
+               // Add close event trigger to the closeBoxCallback parameter
+               args[6] = function(e){ if(closeCallback) closeCallback(); else this.hide(); OpenLayers.Event.stop(e); this.events.triggerEvent("close"); };
+
+               OpenLayers.Popup.FramedCloud.prototype.initialize.apply(this, args);
+
+               this.events.addEventType("close");
+
+               this.setContentHTML(arguments[3]);
+       },
+
+       /**
+        * Like OpenLayers.Popup.FramedCloud.setContentHTML(), but takes a DOM element as parameter.
+       */
+       setContentHTML: function(contentDom) {
+               if(contentDom != null)
+                       this.contentDom = contentDom;
+
+               if(this.contentDiv == null || this.contentDom == null || this.contentDom == this.contentDiv.firstChild)
+                       return;
+
+               while(this.contentDiv.firstChild)
+                       this.contentDiv.removeChild(this.contentDiv.firstChild);
+
+               this.contentDiv.appendChild(this.contentDom);
+
+               // Copied from OpenLayers.Popup.setContentHTML():
+               if(this.autoSize)
+               {
+                       this.registerImageListeners();
+                       this.updateSize();
+               }
+       },
+
+       destroy: function() {
+               this.contentDom = null;
+               OpenLayers.Popup.FramedCloud.prototype.destroy.apply(this, arguments);
+       },
+
+       CLASS_NAME: "OpenLayers.Popup.FramedCloud.OpenStreetBugs"
+});
+
+/**
+ * Necessary improvement to the translate function: Fall back to default language if translated string is not
+ * available (see http://trac.openlayers.org/ticket/2308).
+*/
+
+OpenLayers.i18n = OpenLayers.Lang.translate = function(key, context) {
+       var message = OpenLayers.Lang[OpenLayers.Lang.getCode()][key];
+       if(!message)
+       {
+               if(OpenLayers.Lang[OpenLayers.Lang.defaultCode][key])
+                       message = OpenLayers.Lang[OpenLayers.Lang.defaultCode][key];
+               else
+                       message = key;
+       }
+       if(context)
+               message = OpenLayers.String.format(message, context);
+       return message;
+};
+
+/**
+ * This global function is executed by the OpenStreetBugs API getBugs script.
+ * Each OpenStreetBugs layer adds itself to the putAJAXMarker.layer array. The putAJAXMarker() function executes the createMarker() method
+ * on each layer in that array each time it is called. This has the side-effect that bugs displayed in one map on a page are already loaded
+ * on the other map as well.
+*/
+
+function putAJAXMarker(id, lon, lat, text, closed)
+{
+       var comments = text.split(/<hr \/>/);
+       for(var i=0; i<comments.length; i++)
+               comments[i] = comments[i].replace(/&quot;/g, "\"").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
+       putAJAXMarker.bugs[id] = [
+               new OpenLayers.LonLat(lon, lat),
+               comments,
+               closed
+       ];
+       for(var i=0; i<putAJAXMarker.layers.length; i++)
+               putAJAXMarker.layers[i].createMarker(id);
+}
+
+/**
+ * This global function is executed by the OpenStreetBugs API. The “create bug”, “comment” and “close bug” scripts execute it to give information about their success.
+ * In case of success, this function is called without a parameter, in case of an error, the error message is passed. This is lousy workaround to make it any functional at all, the OSB API is likely to be extended later (then it will provide additional information such as the ID of a created bug and similar).
+*/
+
+function osbResponse(error)
+{
+       if(error)
+               alert("Error: "+error);
+
+       for(var i=0; i<putAJAXMarker.layers.length; i++)
+               putAJAXMarker.layers[i].loadBugs();
+}
+
+putAJAXMarker.layers = [ ];
+putAJAXMarker.bugs = { };
+
+
+/* Translations */
+
+OpenLayers.Lang.en = OpenLayers.Util.extend(OpenLayers.Lang.en, {
+       "Fixed Error" : "Fixed Error",
+       "Unresolved Error" : "Unresolved Error",
+       "Description" : "Description",
+       "Comment" : "Comment",
+       "Has been fixed." : "This error has been fixed already. However, it might take a couple of days before the map image is updated.",
+       "Comment/Close" : "Comment/Close",
+       "Nickname" : "Nickname",
+       "Add comment" : "Add comment",
+       "Mark as fixed" : "Mark as fixed",
+       "Cancel" : "Cancel",
+       "Create OpenStreetBug" : "Create OpenStreetBug",
+       "Create bug" : "Create bug",
+       "Bug description" : "Bug description",
+       "Create" : "Create",
+       "Permalink" : "Permalink",
+       "Zoom" : "Zoom"
+});
+
+OpenLayers.Lang.de = OpenLayers.Util.extend(OpenLayers.Lang.de, {
+       "Fixed Error" : "Behobener Fehler",
+       "Unresolved Error" : "Offener Fehler",
+       "Description" : "Beschreibung",
+       "Comment" : "Kommentar",
+       "Has been fixed." : "Der Fehler wurde bereits behoben. Es kann jedoch bis zu einigen Tagen dauern, bis die Kartenansicht aktualisiert wird.",
+       "Comment/Close" : "Kommentieren/Schließen",
+       "Nickname" : "Benutzername",
+       "Add comment" : "Kommentar hinzufügen",
+       "Mark as fixed" : "Als behoben markieren",
+       "Cancel" : "Abbrechen",
+       "Create OpenStreetBug" : "OpenStreetBug melden",
+       "Create bug" : "Bug anlegen",
+       "Bug description" : "Fehlerbeschreibung",
+       "Create" : "Anlegen",
+       "Permalink" : "Permalink",
+       "Zoom" : "Zoom"
+});
+
+OpenLayers.Lang.fr = OpenLayers.Util.extend(OpenLayers.Lang.fr, {
+       "Fixed Error" : "Erreur corrigée",
+       "Unresolved Error" : "Erreur non corrigée",
+       "Description" : "Description",
+       "Comment" : "Commentaire",
+       "Has been fixed." : "Cette erreur a déjà été corrigée. Cependant, il peut être nécessaire d'attendre quelques jours avant que l'image de la carte ne soit mise à jour.",
+       "Comment/Close" : "Commenter/Fermer",
+       "Nickname" : "Surnom",
+       "Add comment" : "Ajouter un commentaire",
+       "Mark as fixed" : "Marquer comme corrigé",
+       "Cancel" : "Annuler",
+       "Create OpenStreetBug" : "Créer OpenStreetBug",
+       "Create bug" : "Ajouter un bug",
+       "Bug description" : "Description du bug",
+       "Create" : "Créer",
+       "Permalink" : "Lien permanent",
+       "Zoom" : "Zoom"
+});
+
+OpenLayers.Lang.nl = OpenLayers.Util.extend(OpenLayers.Lang.nl, {
+       "Fixed Error" : "Fout verholpen",
+       "Unresolved Error" : "Openstaande fout",
+       "Description" : "Beschrijving",
+       "Comment" : "Kommentaar",
+       "Has been fixed." : "De fout is al eerder opgelost. Het kan echter nog een paar dagen duren voordat het kaartmateriaal geactualiseerd is.",
+       "Comment/Close" : "Bekommentariëren/Sluiten",
+       "Nickname" : "Gebruikersnaam",
+       "Add comment" : "Kommentaar toevoegen",
+       "Mark as fixed" : "Als opgelost aanmerken",
+       "Cancel" : "Afbreken",
+       "Create OpenStreetBug" : "OpenStreetBug melden",
+       "Create bug" : "Bug melden",
+       "Bug description" : "Foutomschrijving",
+       "Create" : "Aanmaken",
+       "Permalink" : "Permalink",
+       "Zoom" : "Zoom"
+});
+
+OpenLayers.Lang.it = OpenLayers.Util.extend(OpenLayers.Lang.it, {
+       "Fixed Error" : "Sbaglio coretto",
+       "Unresolved Error" : "Sbaglio non coretto",
+       "Description" : "Descrizione",
+       "Comment" : "Commento",
+       "Has been fixed." : "Questo sbaglio è già coretto. Forse ci metto qualche giorni per aggiornare anche i quadri.",
+       "Comment/Close" : "Commenta/Chiude",
+       "Nickname" : "Nome",
+       "Add comment" : "Aggiunge commento",
+       "Mark as fixed" : "Marca che è coretto",
+       "Cancel" : "Annulla",
+       "Create OpenStreetBug" : "Aggiunge OpenStreetBug",
+       "Create bug" : "Aggiunge un sbaglio",
+       "Bug description" : "Descrizione del sbaglio",
+       "Create" : "Aggiunge",
+       "Permalink" : "Permalink",
+       "Zoom" : "Zoom"
+});
+
+OpenLayers.Lang.ro = OpenLayers.Util.extend(OpenLayers.Lang.ro, {
+       "Fixed Error" : "Eroare rezolvată",
+       "Unresolved Error" : "Eroare nerezolvată",
+       "Description" : "Descriere",
+       "Comment" : "Comentariu",
+       "Has been fixed." : "Această eroare a fost rezolvată. Totuși este posibil să dureze câteva zile până când imaginea hărții va fi actualizată.",
+       "Comment/Close" : "Comentariu/Închide",
+       "Nickname" : "Nume",
+       "Add comment" : "Adaugă comentariu",
+       "Mark as fixed" : "Marchează ca rezolvată",
+       "Cancel" : "Anulează",
+       "Create OpenStreetBug" : "Crează OpenStreetBug",
+       "Create bug" : "Adaugă eroare",
+       "Bug description" : "Descrierea erorii",
+       "Create" : "Adaugă",
+       "Permalink" : "Permalink",
+       "Zoom" : "Zoom"
+});
diff --git a/public/stylesheets/openstreetbugs.css b/public/stylesheets/openstreetbugs.css
new file mode 100644 (file)
index 0000000..b1859f7
--- /dev/null
@@ -0,0 +1,8 @@
+.olPopupFramedCloudOpenStreetBugs dl { margin:0; padding:0; }
+.olPopupFramedCloudOpenStreetBugs dt { margin:0; padding:0; font-weight:bold; float:left; clear:left; }
+.olPopupFramedCloudOpenStreetBugs dt:after { content:": "; }
+* html .olPopupFramedCloudOpenStreetBugs dt { margin-right:1ex; }
+.olPopupFramedCloudOpenStreetBugs dd { margin:0; padding:0; }
+.olPopupFramedCloudOpenStreetBugs ul.buttons { list-style-type:none; padding:0; margin:0 }
+.olPopupFramedCloudOpenStreetBugs ul.buttons li { display:inline; margin:0; padding:0; }
+.olPopupFramedCloudOpenStreetBugs h3 { font-size:1.2em; margin:.2em 0 .7em 0; }
diff --git a/test/fixtures/map_bug_comment.yml b/test/fixtures/map_bug_comment.yml
new file mode 100644 (file)
index 0000000..2214c07
--- /dev/null
@@ -0,0 +1,128 @@
+t1:
+  id: 1
+  bug_id: 1
+  visible: true
+  date_created: 2007-01-01 00:00:00
+  commenter_name: 'testname'
+  commenter_ip: '192.168.1.1'
+  comment: 'This is the initial description of the bug 1'
+
+
+t2:
+  id: 2
+  bug_id: 2
+  visible: true
+  date_created: 2007-01-01 00:00:00
+  commenter_name: 'testname'
+  commenter_ip: '192.168.1.1'
+  comment: 'This is the initial description of the bug 2'
+
+
+t3:
+  id: 3
+  bug_id: 2
+  visible: true
+  date_created: 2007-02-01 00:00:00
+  commenter_name: 'testname'
+  commenter_ip: '192.168.1.1'
+  comment: 'This is an additional comment for bug 2'
+
+
+t4:
+  id: 4
+  bug_id: 3
+  visible: true
+  date_created: 2007-01-01 00:00:00
+  commenter_name: 'testname'
+  commenter_ip: '192.168.1.1'
+  comment: 'This is the initial comment for bug 3'
+
+t5:
+  id: 5
+  bug_id: 4
+  visible: true
+  date_created: 2007-01-01 00:00:00
+  commenter_name: 'testname'
+  commenter_ip: '192.168.1.1'
+  comment: 'Spam for bug 4'
+
+
+t6:
+  id: 6
+  bug_id: 5
+  visible: true
+  date_created: 2007-01-01 00:00:00
+  commenter_name: 'testname'
+  commenter_ip: '192.168.1.1'
+  comment: 'Valid comment for bug 5'
+
+t7:
+  id: 7
+  bug_id: 5
+  visible: false
+  date_created: 2007-02-01 00:00:00
+  commenter_name: 'testname'
+  commenter_ip: '192.168.1.1'
+  comment: 'Spam for bug 5'
+
+t8:
+  id: 8
+  bug_id: 5
+  visible: true
+  date_created: 2007-02-01 00:00:00
+  commenter_name: 'testname'
+  commenter_ip: '192.168.1.1'
+  comment: 'Another valid comment for bug 5'
+
+t9:
+  id: 9
+  bug_id: 6
+  visible: true
+  date_created: 2007-01-01 00:00:00
+  event: opened
+  commenter_id: 1
+  comment: 'This is a bug with from a logged-in user'
+
+
+t10:
+  id: 10
+  bug_id: 6
+  visible: true
+  date_created: 2007-02-01 00:00:00
+  event: commented
+  commenter_id: 4
+  comment: 'A comment from another logged-in user'
+
+
+t11:
+  id: 11
+  bug_id: 7
+  visible: true
+  event: opened
+  date_created: 2007-01-01 00:00:00
+  commenter_name: 'testname'
+  commenter_ip: '192.168.1.1'
+  comment: 'Initial bug description'
+
+
+t12:
+  id: 12
+  bug_id: 7
+  visible: true
+  event: commented
+  date_created: 2007-02-01 00:00:00
+  commenter_name: 'testname'
+  commenter_ip: '192.168.1.1'
+  comment: 'A comment description'
+
+t13:
+  id: 13
+  bug_id: 7
+  visible: true
+  event: closed
+  date_created: 2007-03-01 00:00:00
+  commenter_id: 4
+
+
+
+
diff --git a/test/fixtures/map_bugs.yml b/test/fixtures/map_bugs.yml
new file mode 100644 (file)
index 0000000..7c465ab
--- /dev/null
@@ -0,0 +1,69 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+<% SCALE = 10000000 unless defined?(SCALE) %>
+
+open_bug:
+  id: 1
+  latitude: <%= 1*SCALE %>
+  longitude: <%= 1*SCALE %>
+  status: open
+  tile: <%= QuadTile.tile_for_point(1,1) %>
+  date_created: 2007-01-01 00:00:00
+  last_changed: 2007-01-01 00:00:00
+
+open_bug_with_comment:
+  id: 2
+  latitude: <%= 1.1*SCALE %>
+  longitude: <%= 1.1*SCALE %>
+  status: open
+  tile: <%= QuadTile.tile_for_point(1.1,1.1) %>
+  date_created: 2007-01-01 00:00:00
+  last_changed: 2007-02-01 00:00:00
+
+closed_bug_with_comment:
+  id: 3
+  latitude: <%= 1.2*SCALE %>
+  longitude: <%= 1.2*SCALE %>
+  status: closed
+  tile: <%= QuadTile.tile_for_point(1.2,1.2) %>
+  date_created: 2007-01-01 00:00:00
+  last_changed: 2007-03-01 00:00:00
+  date_closed:  2007-03-01 00:00:00
+
+hidden_bug_with_comment:
+  id: 4
+  latitude: <%= 1.3*SCALE %>
+  longitude: <%= 1.3*SCALE %>
+  status: hidden
+  tile: <%= QuadTile.tile_for_point(1.3,1.3) %>
+  date_created: 2007-01-01 00:00:00
+  last_changed: 2007-03-01 00:00:00
+
+bug_with_hidden_comment:
+  id: 5
+  latitude: <%= 1.4*SCALE %>
+  longitude: <%= 1.4*SCALE %>
+  status: open
+  tile: <%= QuadTile.tile_for_point(1.4,1.4) %>
+  date_created: 2007-01-01 00:00:00
+  last_changed: 2007-03-01 00:00:00
+
+bug_with_comments_by_users:
+  id: 6
+  latitude: <%= 1.5*SCALE %>
+  longitude: <%= 1.5*SCALE %>
+  status: open
+  tile: <%= QuadTile.tile_for_point(1.5,1.5) %>
+  date_created: 2007-01-01 00:00:00
+  last_changed: 2007-03-01 00:00:00
+
+
+bug_closed__by_user:
+  id: 7
+  latitude: <%= 1.6*SCALE %>
+  longitude: <%= 1.6*SCALE %>
+  status: closed
+  tile: <%= QuadTile.tile_for_point(1.6,1.6) %>
+  date_created: 2007-01-01 00:00:00
+  last_changed: 2007-03-01 00:00:00
+  date_closed:  2007-03-01 00:00:00
+
diff --git a/test/functional/map_bugs_controller_test.rb b/test/functional/map_bugs_controller_test.rb
new file mode 100644 (file)
index 0000000..c3335d2
--- /dev/null
@@ -0,0 +1,174 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class MapBugsControllerTest < ActionController::TestCase
+  fixtures :users, :map_bugs, :map_bug_comment
+    
+  def test_map_bug_create_success
+    assert_difference('MapBug.count') do
+         assert_difference('MapBugComment.count') do
+        post :add_bug, {:lat => -1.0, :lon => -1.0, :name => "new_tester", :text => "This is a comment"}
+         end
+       end
+    assert_response :success
+       id = @response.body.sub(/ok/,"").to_i
+       get :read, {:id => id, :format => 'json'}
+    assert_response :success
+       js = @response.body
+       assert_match "\"status\":\"open\"", js
+       assert_match "\"comment\":\"This is a comment\"", js
+       assert_match "\"commenter_name\":\"new_tester (a)\"", js
+  end
+
+  def test_map_bug_comment_create_success
+    assert_difference('MapBugComment.count') do
+        post :edit_bug, {:id => 2, :name => "new_tester2", :text => "This is an additional comment"}
+    end
+    assert_response :success
+
+       get :read, {:id => 2, :format => 'json'}
+    assert_response :success
+       js = @response.body
+       assert_match "\"id\":2", js
+       assert_match "\"status\":\"open\"", js
+       assert_match "\"comment\":\"This is an additional comment\"", js
+       assert_match "\"commenter_name\":\"new_tester2 (a)\"", js
+
+  end
+
+  def test_map_bug_read_success
+    get :read, {:id => 1}
+    assert_response :success      
+
+    get :read, {:id => 1,  :format => 'xml'}
+    assert_response :success
+
+    get :read, {:id => 1,  :format => 'rss'}
+    assert_response :success
+
+    get :read, {:id => 1,  :format => 'json'}
+    assert_response :success
+
+    get :read, {:id => 1,  :format => 'gpx'}
+    assert_response :success
+  end
+
+  def test_map_bug_close_success
+       post :close_bug, {:id => 2}
+    assert_response :success
+
+       get :read, {:id => 2, :format => 'json'}
+       js = @response.body
+       assert_match "\"id\":2", js
+       assert_match "\"status\":\"closed\"", js
+  end
+
+  def test_get_bugs_success
+       get :get_bugs, {:bbox=>'1,1,1.2,1.2'}
+       assert_response :success
+
+       get :get_bugs, {:bbox=>'1,1,1.2,1.2', :format => 'rss'}
+       assert_response :success
+
+       get :get_bugs, {:bbox=>'1,1,1.2,1.2', :format => 'json'}
+       assert_response :success
+
+       get :get_bugs, {:bbox=>'1,1,1.2,1.2', :format => 'xml'}
+       assert_response :success
+
+       get :get_bugs, {:bbox=>'1,1,1.2,1.2', :format => 'gpx'}
+       assert_response :success
+  end
+
+  def test_get_bugs_large_area_success
+       get :get_bugs, {:bbox=>'-10,-10,12,12'}
+       assert_response :success
+  end
+
+  def test_get_bugs_closed_7_success
+       get :get_bugs, {:bbox=>'1,1,1.2,1.2', :closed => '7'}
+       assert_response :success
+  end
+
+  def test_get_bugs_closed_0_success
+       get :get_bugs, {:bbox=>'1,1,1.2,1.2', :closed => '0'}
+       assert_response :success
+  end
+
+  def test_get_bugs_closed_n1_success
+       get :get_bugs, {:bbox=>'1,1,1.2,1.2', :closed => '-1'}
+       assert_response :success
+  end
+
+
+  def test_search_success
+       get :search, {:bbox=>'1,1,1.2,1.2', :q => 'bug 1'}
+       assert_response :success
+
+       get :search, {:bbox=>'1,1,1.2,1.2', :q => 'bug 1', :format => 'xml'}
+       assert_response :success
+
+       get :search, {:bbox=>'1,1,1.2,1.2', :q => 'bug 1', :format => 'json'}
+       assert_response :success
+
+       get :search, {:bbox=>'1,1,1.2,1.2', :q => 'bug 1', :format => 'rss'}
+       assert_response :success
+
+       get :search, {:bbox=>'1,1,1.2,1.2', :q => 'bug 1', :format => 'gpx'}
+       assert_response :success
+  end
+
+  def test_rss_success
+       get :rss, {:bbox=>'1,1,1.2,1.2'}
+       assert_response :success
+       
+       get :rss
+       assert_response :success
+  end
+
+  def test_user_bugs_success
+       get :my_bugs, {:display_name=>'test'}
+       assert_response :success
+
+       get :my_bugs, {:display_name=>'pulibc_test2'}
+       assert_response :success
+
+       get :my_bugs, {:display_name=>'non-existent'}
+       assert_response :not_found
+       
+  end
+
+  def test_map_bug_comment_create_not_found
+    assert_no_difference('MapBugComment.count') do
+        post :edit_bug, {:id => 12345, :name => "new_tester", :text => "This is an additional comment"}
+    end
+    assert_response :not_found
+  end
+
+  def test_map_bug_close_not_found
+       post :close_bug, {:id => 12345}
+    assert_response :not_found
+  end
+
+  def test_map_bug_read_not_found
+       get :read, {:id => 12345}
+    assert_response :not_found
+  end
+
+  def test_map_bug_read_gone
+       get :read, {:id => 4}
+    assert_response :gone
+  end
+
+  def test_map_bug_hidden_comment
+       get :read, {:id => 5, :format => 'json'}
+       assert_response :success
+       js = @response.body
+       assert_match "\"id\":5", js
+       assert_match "\"comment\":\"Valid comment for bug 5\"", js
+       assert_match "\"comment\":\"Another valid comment for bug 5\"", js
+       assert_no_match /\"comment\":\"Spam for bug 5\"/, js
+  end
+  
+
+  
+end