Merge branch 'master' into openstreetbugs
authorTom Hughes <tom@compton.nu>
Wed, 22 Jun 2011 21:36:43 +0000 (22:36 +0100)
committerTom Hughes <tom@compton.nu>
Wed, 22 Jun 2011 21:36:43 +0000 (22:36 +0100)
49 files changed:
app/controllers/browse_controller.rb
app/controllers/note_controller.rb [new file with mode: 0644]
app/models/note.rb [new file with mode: 0644]
app/models/note_comment.rb [new file with mode: 0644]
app/models/notifier.rb
app/views/browse/_map.html.erb
app/views/browse/note.html.erb [new file with mode: 0644]
app/views/note/_note.gpx.builder [new file with mode: 0644]
app/views/note/_note.rss.builder [new file with mode: 0644]
app/views/note/_note.xml.builder [new file with mode: 0644]
app/views/note/_notes_paging_nav.html.erb [new file with mode: 0644]
app/views/note/_user.html.erb [new file with mode: 0644]
app/views/note/list.gpx.builder [new file with mode: 0644]
app/views/note/list.rjs [new file with mode: 0644]
app/views/note/list.rss.builder [new file with mode: 0644]
app/views/note/list.xml.builder [new file with mode: 0644]
app/views/note/mine.html.erb [new file with mode: 0644]
app/views/note/read.gpx.builder [new file with mode: 0644]
app/views/note/read.rss.builder [new file with mode: 0644]
app/views/note/read.xml.builder [new file with mode: 0644]
app/views/note/rss.rss.builder [new file with mode: 0644]
app/views/notifier/note_comment_notification.html.erb [new file with mode: 0644]
app/views/site/index.html.erb
app/views/user/view.html.erb
config/example.application.yml
config/initializers/mime_types.rb
config/locales/de.yml
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]
db/migrate/20110508145337_cleanup_bug_tables.rb [new file with mode: 0644]
db/migrate/20110521142405_rename_bugs_to_notes.rb [new file with mode: 0644]
lib/geo_record.rb
lib/map_boundary.rb
lib/migrate.rb
lib/osm.rb
public/images/closed_note_marker.png [new file with mode: 0644]
public/images/icon_error_add.png [new file with mode: 0644]
public/images/open_note_marker.png [new file with mode: 0644]
public/javascripts/openstreetbugs.js [new file with mode: 0644]
public/stylesheets/large.css
public/stylesheets/openstreetbugs.css [new file with mode: 0644]
test/fixtures/note_comments.yml [new file with mode: 0644]
test/fixtures/notes.yml [new file with mode: 0644]
test/functional/note_controller_test.rb [new file with mode: 0644]

index a7dd5f5c95ff8b745b19fc3cac1831ac38c2177a..2036e4f10a83f547244b22d18e00590c44afe042 100644 (file)
@@ -79,4 +79,13 @@ class BrowseController < ApplicationController
   rescue ActiveRecord::RecordNotFound
     render :action => "not_found", :status => :not_found
   end
+
+  def note
+    @type = "note"
+    @note = Note.find(params[:id])
+    @next = Note.find(:first, :order => "id ASC", :conditions => [ "status != 'hidden' AND id > :id", { :id => @note.id }] )
+    @prev = Note.find(:first, :order => "id DESC", :conditions => [ "status != 'hidden' AND id < :id", { :id => @note.id }] )
+  rescue ActiveRecord::RecordNotFound
+    render :action => "not_found", :status => :not_found
+  end
 end
diff --git a/app/controllers/note_controller.rb b/app/controllers/note_controller.rb
new file mode 100644 (file)
index 0000000..c461450
--- /dev/null
@@ -0,0 +1,387 @@
+class NoteController < ApplicationController
+
+  layout 'site', :only => [:mine]
+
+  before_filter :check_api_readable
+  before_filter :authorize_web, :only => [:create, :close, :update, :delete, :mine]
+  before_filter :check_api_writable, :only => [:create, :close, :update, :delete]
+  before_filter :set_locale, :only => [:mine]
+  after_filter :compress_output
+  around_filter :api_call_handle_error, :api_call_timeout
+
+  # Help methods for checking boundary sanity and area size
+  include MapBoundary
+
+  ##
+  # Return a list of notes in a given area
+  def list
+    # Figure out the bbox - we prefer a bbox argument but also
+    # support the old, deprecated, method with four arguments
+    if params[:bbox]
+      raise OSM::APIBadUserInput.new("Invalid bbox") unless params[:bbox].count(",") == 3
+
+      bbox = params[:bbox].split(",")
+    else
+      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]
+
+      bbox = [ params[:l], params[:b], params[:r], params[:t] ]
+    end
+
+    # Get the sanitised boundaries
+    @min_lon, @min_lat, @max_lon, @max_lat = sanitise_boundaries(bbox)
+
+    # Get any conditions that need to be applied
+    conditions = closed_condition
+
+    # Check that the boundaries are valid
+    check_boundaries(@min_lon, @min_lat, @max_lon, @max_lat, MAX_NOTE_REQUEST_AREA)
+
+    # Find the notes we want to return
+    @notes = Note.find_by_area(@min_lat, @min_lon, @max_lat, @max_lon,
+                               :include => :comments, 
+                               :conditions => conditions,
+                               :order => "updated_at DESC", 
+                               :limit => result_limit)
+
+    # Render the result
+    respond_to do |format|
+      format.html { render :format => :rjs, :content_type => "text/javascript" }
+      format.rss
+      format.js
+      format.xml
+      format.json { render :json => @notes.to_json }
+      format.gpx
+    end
+  end
+
+  ##
+  # Create a new note
+  def create
+    # Check the arguments are sane
+    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]
+
+    # Extract the arguments
+    lon = params[:lon].to_f
+    lat = params[:lat].to_f
+    comment = params[:text]
+    name = params[:name]
+
+    # Include in a transaction to ensure that there is always a note_comment for every note
+    Note.transaction do
+      # Create the note
+      @note = Note.create(:lat => lat, :lon => lon)
+      raise OSM::APIBadUserInput.new("The note is outside this world") unless @note.in_world?
+
+      #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") 
+          @note.nearby_place = result.to_s 
+        else 
+          @note.nearby_place = "unknown"
+        end
+      rescue Exception => err
+        @note.nearby_place = "unknown"
+      end
+
+      # Save the note
+      @note.save
+
+      # Add a comment to the note
+      add_comment(@note, comment, name, "opened")
+    end
+
+    # Send an OK response
+    render_ok
+  end
+
+  ##
+  # Add a comment to an existing note
+  def update
+    # Check the arguments are sane
+    raise OSM::APIBadUserInput.new("No id was given") unless params[:id]
+    raise OSM::APIBadUserInput.new("No text was given") unless params[:text]
+
+    # Extract the arguments
+    id = params[:id].to_i
+    comment = params[:text]
+    name = params[:name] or "NoName"
+
+    # Find the note and check it is valid
+    note = Note.find(id)
+    raise OSM::APINotFoundError unless note
+    raise OSM::APIAlreadyDeletedError unless note.visible?
+
+    # Add a comment to the note
+    Note.transaction do
+      add_comment(note, comment, name, "commented")
+    end
+
+    # Send an OK response
+    render_ok
+  end
+
+  ##
+  # Close a note
+  def close
+    # Check the arguments are sane
+    raise OSM::APIBadUserInput.new("No id was given") unless params[:id]
+
+    # Extract the arguments
+    id = params[:id].to_i
+    name = params[:name]
+
+    # Find the note and check it is valid
+    note = Note.find_by_id(id)
+    raise OSM::APINotFoundError unless note
+    raise OSM::APIAlreadyDeletedError unless note.visible?
+
+    # Close the note and add a comment
+    Note.transaction do
+      note.close
+
+      add_comment(note, nil, name, "closed")
+    end
+
+    # Send an OK response
+    render_ok
+  end 
+
+  ##
+  # Get a feed of recent notes and comments
+  def rss
+    # Get any conditions that need to be applied
+    conditions = closed_condition
+
+    # Process any bbox
+    if params[:bbox]
+      raise OSM::APIBadUserInput.new("Invalid bbox") unless params[:bbox].count(",") == 3
+
+      @min_lon, @min_lat, @max_lon, @max_lat = sanitise_boundaries(params[:bbox].split(','))
+
+      check_boundaries(@min_lon, @min_lat, @max_lon, @max_lat, MAX_NOTE_REQUEST_AREA)
+
+      conditions = cond_merge conditions, [OSM.sql_for_area(@min_lat, @min_lon, @max_lat, @max_lon, "notes.")]
+    end
+
+    # Find the comments we want to return
+    @comments = NoteComment.find(:all, 
+                                 :conditions => conditions,
+                                 :order => "created_at DESC",
+                                 :limit => result_limit,
+                                 :joins => :note, 
+                                 :include => :note)
+
+    # Render the result
+    respond_to do |format|
+      format.rss
+    end
+  end
+
+  ##
+  # Read a note
+  def read
+    # Check the arguments are sane
+    raise OSM::APIBadUserInput.new("No id was given") unless params[:id]
+
+    # Find the note and check it is valid
+    @note = Note.find(params[:id])
+    raise OSM::APINotFoundError unless @note
+    raise OSM::APIAlreadyDeletedError unless @note.visible?
+    
+    # Render the result
+    respond_to do |format|
+      format.xml
+      format.rss
+      format.json { render :json => @note.to_json }
+      format.gpx
+    end
+  end
+
+  ##
+  # Delete (hide) a note
+  def delete
+    # Check the arguments are sane
+    raise OSM::APIBadUserInput.new("No id was given") unless params[:id]
+
+    # Extract the arguments
+    id = params[:id].to_i
+    name = params[:name]
+
+    # Find the note and check it is valid
+    note = Note.find(id)
+    raise OSM::APINotFoundError unless note
+    raise OSM::APIAlreadyDeletedError unless note.visible?
+
+    # Mark the note as hidden
+    Note.transaction do
+      note.status = "hidden"
+      note.save
+
+      add_comment(note, nil, name, "hidden")
+    end
+
+    # Render the result
+    render :text => "ok\n", :content_type => "text/html" 
+  end
+
+  ##
+  # Return a list of notes matching a given string
+  def search
+    # Check the arguments are sane
+    raise OSM::APIBadUserInput.new("No query string was given") unless params[:q]
+
+    # Get any conditions that need to be applied
+    conditions = closed_condition
+    conditions = cond_merge conditions, ['note_comments.body ~ ?', params[:q]]
+       
+    # Find the notes we want to return
+    @notes = Note.find(:all, 
+                       :conditions => conditions,
+                       :order => "updated_at DESC",
+                       :limit => result_limit,
+                       :joins => :comments,
+                       :include => :comments)
+
+    # Render the result
+    respond_to do |format|
+      format.html { render :action => :list, :format => :rjs, :content_type => "text/javascript"}
+      format.rss { render :action => :list }
+      format.js
+      format.xml { render :action => :list }
+      format.json { render :json => @notes.to_json }
+      format.gpx { render :action => :list }
+    end
+  end
+
+  def mine
+    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 = ['note_comments.author_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 'note.mine.title', :user => @user2.display_name 
+    @heading =  t 'note.mine.heading', :user => @user2.display_name 
+    @description = t 'note.mine.description', :user => user_link
+    
+    @page = (params[:page] || 1).to_i 
+    @page_size = 10
+
+    @notes = Note.find(:all, 
+                       :include => [:comments, {:comments => :author}],
+                       :joins => :comments,
+                       :order => "updated_at 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 
+
+  ##
+  # Render an OK response
+  def render_ok
+    if params[:format] == "js"
+      render :text => "osbResponse();", :content_type => "text/javascript" 
+    else
+      render :text => "ok " + @note.id.to_s + "\n", :content_type => "text/plain" if @note
+      render :text => "ok\n", :content_type => "text/plain" unless @note
+    end
+  end
+
+  ##
+  # Get the maximum number of results to return
+  def result_limit
+    if params[:limit] and params[:limit].to_i > 0 and params[:limit].to_i < 10000
+      params[:limit].to_i
+    else
+      100
+    end
+  end
+
+  ##
+  # Generate a condition to choose which bugs we want based
+  # on their status and the user's request parameters
+  def closed_condition
+    if params[:closed]
+      closed_since = params[:closed].to_i
+    else
+      closed_since = 7
+    end
+       
+    if closed_since < 0
+      conditions = ["status != 'hidden'"]
+    elsif closed_since > 0
+      conditions = ["(status = 'open' OR (status = 'closed' AND closed_at > '#{Time.now - closed_since.days}'))"]
+    else
+      conditions = ["status = 'open'"]
+    end
+
+    return conditions
+  end
+
+  ##
+  # Add a comment to a note
+  def add_comment(note, text, name, event)
+    name = "NoName" if name.nil?
+
+    attributes = { :visible => true, :event => event, :body => text }
+
+    if @user  
+      attributes[:author_id] = @user.id
+      attributes[:author_name] = @user.display_name
+    else  
+      attributes[:author_ip] = request.remote_ip
+      attributes[:author_name] = name + " (a)"
+    end
+
+    note.comments.create(attributes)
+
+    note.comments.map { |c| c.author }.uniq.each do |user|
+      if user and user != @user
+        Notifier.deliver_note_comment_notification(comment, user)
+      end
+    end
+  end
+end
diff --git a/app/models/note.rb b/app/models/note.rb
new file mode 100644 (file)
index 0000000..892ada1
--- /dev/null
@@ -0,0 +1,88 @@
+class Note < ActiveRecord::Base
+  include GeoRecord
+
+  has_many :comments, :class_name => "NoteComment",
+                      :foreign_key => :note_id,
+                      :order => :created_at,
+                      :conditions => { :visible => true }
+
+  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 :closed_at if :status == "closed"
+  validates_inclusion_of :status, :in => ["open", "closed", "hidden"]
+  validate :validate_position
+
+  # Sanity check the latitude and longitude and add an error if it's broken
+  def validate_position
+    errors.add_to_base("Note is not in the world") unless in_world?
+  end
+
+  # Fill in default values for new notes
+  def after_initialize
+    self.status = "open" unless self.attribute_present?(:status)
+  end
+
+  # Close a note
+  def close
+    self.status = "closed"
+    self.closed_at = Time.now.getutc
+    self.save
+  end
+
+  # Return a flattened version of the comments for a note
+  def flatten_comment(separator_char, upto_timestamp = :nil)
+    resp = ""
+    comment_no = 1
+    self.comments.each do |comment|
+      next if upto_timestamp != :nil and comment.created_at > upto_timestamp
+      resp += (comment_no == 1 ? "" : separator_char)
+      resp += comment.body if comment.body
+      resp += " [ " 
+      resp += comment.author_name if comment.author_name
+      resp += " " + comment.created_at.to_s + " ]"
+      comment_no += 1
+    end
+
+    return resp
+  end
+
+  # Check if a note is visible
+  def visible?
+    return status != "hidden"
+  end
+
+  # Return the author object, derived from the first comment
+  def author
+    self.comments.first.author
+  end
+
+  # Return the author IP address, derived from the first comment
+  def author_ip
+    self.comments.first.author_ip
+  end
+
+  # Return the author id, derived from the first comment
+  def author_id
+    self.comments.first.author_id
+  end
+
+  # Return the author name, derived from the first comment
+  def author_name
+    self.comments.first.author_name
+  end
+
+  # Custom JSON output routine for notes
+  def to_json(options = {})
+    super options.reverse_merge(
+      :methods => [ :lat, :lon ], 
+      :only => [ :id, :status, :created_at ],
+      :include => {
+         :comments => {
+           :only => [ :event, :author_name, :created_at, :body ]
+         }
+      }
+    )
+  end
+end
diff --git a/app/models/note_comment.rb b/app/models/note_comment.rb
new file mode 100644 (file)
index 0000000..bcbcf79
--- /dev/null
@@ -0,0 +1,21 @@
+class NoteComment < ActiveRecord::Base
+  belongs_to :note, :foreign_key => :note_id
+  belongs_to :author, :class_name => "User", :foreign_key => :author_id
+
+  validates_presence_of :id, :on => :update
+  validates_uniqueness_of :id
+  validates_presence_of :note_id
+  validates_associated :note
+  validates_presence_of :visible
+  validates_associated :author
+  validates_inclusion_of :event, :in => [ "opened", "closed", "reopened", "commented", "hidden" ]
+
+  # Return the author name
+  def author_name
+    if self.author_id.nil?
+      self.read_attribute(:author_name)
+    else
+      self.author.display_name
+    end
+  end
+end
index e6058d4b7374e486e51e37ce91e7b1707047c092..f025da7b19c6ddda686a687b0e30a5cf2d085f97 100644 (file)
@@ -95,6 +95,22 @@ class Notifier < ActionMailer::Base
     body :friend => friend
   end
 
+  def note_comment_notification(comment, recipient)
+    common_headers recipient
+    owner = (recipient == comment.note.author);
+    subject I18n.t('notifier.note_plain.subject_own', :commenter => comment.author_name) if owner
+    subject I18n.t('notifier.note_plain.subject_other', :commenter => comment.author_name) unless owner
+
+    body :nodeurl => url_for(:host => SERVER_URL,
+                             :controller => "browse",
+                             :action => "note",
+                             :id => comment.note_id),
+         :place => comment.note.nearby_place,
+         :comment => comment.body,
+         :owner => owner,
+         :commenter => comment.author_name
+  end
+
 private
 
   def common_headers(recipient)
index 13a352820fc026366e12496d8e02c4dcb0ffb11c..dd4ec74df1a6d57a507a98e07958bc89493e3af1 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' %>";
+      <% elsif map.instance_of? Note %>
+        var centre = new OpenLayers.LonLat(<%= map.lon %>, <%= map.lat %>);
+
+        setMapCenter(centre, 16);
+        addMarkerToMap(centre);
+
+        $("loading").innerHTML = "";
+
+        $("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 %>;
diff --git a/app/views/browse/note.html.erb b/app/views/browse/note.html.erb
new file mode 100644 (file)
index 0000000..5c42fdd
--- /dev/null
@@ -0,0 +1,83 @@
+<table width="100%">
+  <tr>
+    <td width="100%">
+      <h2>
+        <% if @note.status == "closed" %>
+          <%= image_tag("closed_note_marker.png", :alt => 'closed') %>
+          <%= t 'browse.note.closed_title', :note_name => @note.id %>
+        <% else %>
+          <%= image_tag("open_note_marker.png", :alt => 'open') %>
+          <%= t 'browse.note.open_title', :note_name => @note.id %>
+        <% end %>
+      </h2>
+    </td>
+    <td>
+      <%= render :partial => "navigation" %>
+    </td>
+  </tr>
+  <tr valign="top">
+    <td>
+      <table>
+        <tr>
+          <th><%= t 'browse.note.created_at' %></th>
+          <td><%= l @note.created_at %></td>
+        </tr>  
+       <tr>
+         <th><%= t 'browse.note.edited_at' %></th>
+         <td><%= l @note.updated_at %></td>
+       </tr>
+       <% if @note.status == "closed" %>
+         <tr>
+           <th><%= t 'browse.note.closed_at' %></th>
+           <td><%= l @note.closed_at %></td>
+         </tr>
+       <% end %>
+       <tr>
+         <th><%= t 'browse.note.opened_by' %></th>
+         <% if @note.author.nil? %>
+           <td> <%= @note.author_name %> </td>
+         <% else %>
+           <td><%= link_to h(@note.author_name), :controller => "user", :action => "view", :display_name => @note.author_name %></td>
+         <% end %>
+       </tr>
+       <tr>
+         <th><%= t 'browse.note.description' %></th>
+         <td><%= h(@note.comments.first.body) %></td>
+       </tr>
+       <tr>
+         <th><%= t 'browse.node_details.coordinates' %></th>
+         <td><div class="geo"><%= link_to ("<span class='latitude'>#{number_with_delimiter(@note.lat)}</span>, <span class='longitude'>#{number_with_delimiter(@note.lon)}</span>"), {:controller => 'site', :action => 'index', :lat => h(@note.lat), :lon => h(@note.lon), :zoom => "18"} %></div></td>
+       </tr>
+    </table>
+
+    <br />
+
+    <% if @note.comments.length > 1 %>
+      <table>
+        <tr>
+          <th width="20%"> <%= t 'browse.note.comment_by' %></th>
+          <th width="60%"> <%= t 'browse.note.comment' %></th>
+          <th width="20%"> <%= t 'browse.note.date' %></th> 
+        </tr>
+        <% @note.comments[1..-1].each do |comment| %>
+          <tr>
+            <td>
+              <% if comment.author.nil? %>
+                <%= comment.author_name %>
+              <% else %>
+                <%= link_to h(comment.author.display_name), :controller => "user", :action => "view", :display_name => comment.author.display_name %>                                  
+              <% end %>
+            </td>
+            <td> <%= h(comment.body) %> </td>
+            <td> <%= l comment.created_at %> </td>
+          </tr>
+        <% end %>
+      </table>
+    <% end %>
+       
+    <hr />
+    </td>
+
+    <%= render :partial => "map", :object => @note %>
+  </tr>
+</table>
diff --git a/app/views/note/_note.gpx.builder b/app/views/note/_note.gpx.builder
new file mode 100644 (file)
index 0000000..91c0cbd
--- /dev/null
@@ -0,0 +1,15 @@
+xml.wpt("lon" => note.lon, "lat" => note.lat) do
+  xml.desc do
+    xml.cdata! note.flatten_comment("<hr />")
+  end
+
+  xml.extension do
+    if note.status = "open"
+      xml.closed "0"
+    else
+      xml.closed "1"
+    end
+
+    xml.id note.id
+  end
+end
diff --git a/app/views/note/_note.rss.builder b/app/views/note/_note.rss.builder
new file mode 100644 (file)
index 0000000..856b4cc
--- /dev/null
@@ -0,0 +1,18 @@
+xml.item do
+  if note.status == "closed"
+    xml.title t('note.rss.closed', :place => note.nearby_place)        
+  elsif note.comments.length > 1
+    xml.title t('note.rss.comment', :place => note.nearby_place)
+  else
+    xml.title t('note.rss.new', :place => note.nearby_place)
+  end
+
+  xml.link url_for(:controller => "browse", :action => "note", :id => note.id, :only_path => false)
+  xml.guid url_for(:controller => "note", :action => "read", :id => note.id, :only_path => false)
+  xml.description  htmlize(note.flatten_comment("<br><br>"))
+  xml.author note.author_name
+  xml.pubDate note.updated_at.to_s(:rfc822)
+  xml.geo :lat, note.lat
+  xml.geo :long, note.lon
+  xml.georss :point, "#{note.lat} #{note.lon}"
+end
diff --git a/app/views/note/_note.xml.builder b/app/views/note/_note.xml.builder
new file mode 100644 (file)
index 0000000..2a2b2ff
--- /dev/null
@@ -0,0 +1,21 @@
+xml.note("lon" => note.lon, "lat" => note.lat) do
+  xml.id note.id
+  xml.date_created note.created_at
+  xml.nearby note.nearby_place
+  xml.status note.status
+
+  if note.status == "closed"
+    xml.date_closed note.closed_at
+  end
+
+  xml.comments do
+    note.comments.each do |comment|
+      xml.comment do
+        xml.date comment.created_at
+        xml.uid comment.author_id unless comment.author_id.nil?
+        xml.user comment.author_name
+        xml.text comment.body
+      end      
+    end
+  end
+end
diff --git a/app/views/note/_notes_paging_nav.html.erb b/app/views/note/_notes_paging_nav.html.erb
new file mode 100644 (file)
index 0000000..108cbb3
--- /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 @notes.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/note/_user.html.erb b/app/views/note/_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/note/list.gpx.builder b/app/views/note/list.gpx.builder
new file mode 100644 (file)
index 0000000..7a30460
--- /dev/null
@@ -0,0 +1,7 @@
+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
+  xml << render(:partial => "note", :collection => @notes)
+end
diff --git a/app/views/note/list.rjs b/app/views/note/list.rjs
new file mode 100644 (file)
index 0000000..1113c3f
--- /dev/null
@@ -0,0 +1,6 @@
+@notes.each do |note|
+  page.call "putAJAXMarker",
+            note.id, note.lon, note.lat,
+            note.flatten_comment("<hr />"),
+            note.status == "open" ? 0 : 1
+end
diff --git a/app/views/note/list.rss.builder b/app/views/note/list.rss.builder
new file mode 100644 (file)
index 0000000..d6ee2bb
--- /dev/null
@@ -0,0 +1,13 @@
+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 t('note.rss.title')
+    xml.description t('note.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)
+
+    xml << render(:partial => "note", :collection => @notes)
+  end
+end
diff --git a/app/views/note/list.xml.builder b/app/views/note/list.xml.builder
new file mode 100644 (file)
index 0000000..38b239a
--- /dev/null
@@ -0,0 +1,3 @@
+xml.instruct!
+
+xml << render(:partial => "note", :collection => @notes)
diff --git a/app/views/note/mine.html.erb b/app/views/note/mine.html.erb
new file mode 100644 (file)
index 0000000..d818243
--- /dev/null
@@ -0,0 +1,37 @@
+<h1><%= @heading %></h1>
+<p><%= @description %></p>
+
+<%= render :partial => 'notes_paging_nav' %>
+
+<table id="note_list" cellpadding="3">
+  <tr>
+    <th></th>
+    <th><%= t'note.mine.id' %></th>
+    <th><%= t'changeset.changesets.user' %></th>
+    <th><%= t'changeset.changesets.comment' %></th>
+    <th><%= t'changeset.changesets.saved_at' %></th>
+    <th><%= t'note.mine.last_changed' %></th>
+  </tr>
+<% @notes.each do |note| %>
+  <tr<% if note.author != @user2 %> bgcolor="#EEEEEE"<% end %>>
+    <td>
+      <% if note.status == "closed" %>
+        <%= image_tag("closed_note_marker.png", :alt => 'closed') %>
+      <% else %>
+        <%= image_tag("open_note_marker.png", :alt => 'open') %>
+      <% end %>
+    </td>
+    <td><%= link_to note.id.to_s, :controller => "browse", :action => "note", :id => note.id %></td>
+    <% if note.author.nil? %> 
+      <td> <%= note.author_name %> </td> 
+    <% else %> 
+      <td><%= link_to h(note.author_name), :controller => "user", :action => "view", :display_name => note.author_name %></td>
+    <% end %>
+    <td> <%= htmlize note.comments.first.body  %> </td>        
+    <td><%= l note.created_at %></td>
+    <td><%= l note.updated_at %></td>
+  </tr>
+<% end %>
+</table>
+
+<%= render :partial => 'notes_paging_nav' %>
diff --git a/app/views/note/read.gpx.builder b/app/views/note/read.gpx.builder
new file mode 100644 (file)
index 0000000..e54d772
--- /dev/null
@@ -0,0 +1,7 @@
+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
+  xml << render(:partial => "note", :object => @note)
+end
diff --git a/app/views/note/read.rss.builder b/app/views/note/read.rss.builder
new file mode 100644 (file)
index 0000000..e566ff0
--- /dev/null
@@ -0,0 +1,13 @@
+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 t('note.rss.title')
+    xml.description t('note.rss.description_item', :id => @note.id)
+    xml.link url_for(:controller => "site", :action => "index", :only_path => false)
+
+    xml << render(:partial => "note", :object => @note)
+  end
+end
diff --git a/app/views/note/read.xml.builder b/app/views/note/read.xml.builder
new file mode 100644 (file)
index 0000000..cfb28c2
--- /dev/null
@@ -0,0 +1,3 @@
+xml.instruct!
+
+xml << render(:partial => "note", :object => @note)
diff --git a/app/views/note/rss.rss.builder b/app/views/note/rss.rss.builder
new file mode 100644 (file)
index 0000000..d22d673
--- /dev/null
@@ -0,0 +1,46 @@
+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 t('note.rss.title')
+    xml.description t('note.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)
+
+    @comments.each do |comment|
+      xml.item do
+        if comment.event == "closed"
+          xml.title t('note.rss.closed', :place => comment.note.nearby_place)  
+        elsif comment.event == "commented"
+          xml.title t('note.rss.comment', :place => comment.note.nearby_place)
+        elsif comment.event == "opened"
+          xml.title t('note.rss.new', :place => comment.note.nearby_place)
+        else
+          xml.title "unknown event"
+        end
+        
+        xml.link url_for(:controller => "browse", :action => "note", :id => comment.note.id, :only_path => false)
+        xml.guid url_for(:controller => "browse", :action => "note", :id => comment.note.id, :only_path => false)
+
+        description_text = ""
+
+        if comment.event == "commented" and not comment.nil?
+          description_text += "<b>Comment:</b><br>"
+          description_text += htmlize(comment.body)
+          description_text += "<br>"
+        end
+
+        description_text += "<b>Full note:</b><br>"
+        description_text += comment.note.flatten_comment("<br>", comment.created_at)
+
+        xml.description description_text 
+        xml.author comment.author_name
+        xml.pubDate comment.created_at.to_s(:rfc822)
+        xml.geo :lat, comment.note.lat
+        xml.geo :long, comment.note.lon
+        xml.georss :point, "#{comment.note.lat} #{comment.note.lon}"
+      end
+    end
+  end
+end
diff --git a/app/views/notifier/note_comment_notification.html.erb b/app/views/notifier/note_comment_notification.html.erb
new file mode 100644 (file)
index 0000000..fade148
--- /dev/null
@@ -0,0 +1,15 @@
+<%= t 'notifier.note_plain.greeting' %>
+
+<% if @owner %>
+<%= t 'notifier.note_plain.your_note', :commenter => @commenter, :place => @place %>
+<% else %>
+<%= t 'notifier.note_plain.commented_note', :commenter => @commenter, :place => @place %>
+<% end %>
+
+==
+<%= @comment %>
+==
+
+<%= t 'notifier.note_plain.details', :URL => @noteurl %>
+
+
index 0a0b7c1da5e873a9e2a9a450dee08e963773d77a..a61baca811fb8e0b590ecf566fbb40c0d7d7288a 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="#" id="reportbuganchor">Report a problem</a>        
 </div>
 
 <div id="attribution">
@@ -115,6 +116,7 @@ end
 
 <%= 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 +127,10 @@ end
 
   OpenLayers.Lang.setCode("<%= I18n.locale.to_s %>");
 
+  <% if @user %>
+    var loginName = "<%= @user.display_name %>"
+  <% end %>
+
   function mapInit(){
     map = createMap("map");
 
@@ -132,6 +138,25 @@ 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("Notes", {
+          serverURL: "/api/0.6/",
+          iconOpen: new OpenLayers.Icon("<%= image_path "open_note_marker.png" %>", new OpenLayers.Size(22, 22), new OpenLayers.Pixel(-11, -11)),
+          iconClosed: new OpenLayers.Icon("<%= image_path "closed_noe_marker.png" %>", new OpenLayers.Size(22, 22), new OpenLayers.Pixel(-11, -11)),
+          readonly: false,
+          setCookie: false,
+          permalinkURL: "http://www.openstreetmap.org/",
+          theme: "<%= stylesheet_path "openstreetbugs" %>",
+          visibility: false
+      });
+      map.addLayer(map.osbLayer);
+
+      map.osbControl = new OpenLayers.Control.OpenStreetBugs(map.osbLayer); 
+      map.addControl(map.osbControl);
+
+      $("reportbuganchor").observe("click", addBug);
+
+      map.events.register("zoomend", map, allowBugReports);
     <% end %>
 
     <% unless object_zoom %>
@@ -295,6 +320,19 @@ end
     <% end %>
   }
 
+  function addBug() {
+    map.osbControl.activate();
+    map.osbControl.addTemporaryMarker(map.getCenter());
+  }
+
+  function allowBugReports() { 
+    if (map.getZoom() > 11) {
+      $("reportbuganchor").style.visibility = "visible";
+    } else {
+      $("reportbuganchor").style.visibility = "hidden";
+    }
+  }
+
   mapInit();
 
   Event.observe(window, "load", installEditHandler);
index 2fe92de5bd4c2da0725d61d291bc6146f91ec6a5..5659cd133d5288d4b538608fedfc8a1b42c2237f 100644 (file)
@@ -23,7 +23,9 @@
     |
     <%= link_to t('user.view.my edits'), :controller => 'changeset', :action => 'list', :display_name => @user.display_name %>
     |
-    <%= link_to t('user.view.my traces'), :controller => 'trace', :action=>'mine' %>
+    <%= link_to t('user.view.my traces'), :controller => 'trace', :action=> 'mine' %>
+    |
+    <%= link_to t('user.view.my notes'), :controller => 'note', :action=> 'mine' %>
     |
     <%= link_to t('user.view.my settings'), :controller => 'user', :action => 'account', :display_name => @user.display_name %>
     |
@@ -43,6 +45,8 @@
     |
     <%= link_to t('user.view.traces'), :controller => 'trace', :action => 'view', :display_name => @this_user.display_name %>
     |
+    <%= link_to t('user.view.notes'), :controller => 'note', :action=> 'mine' %>
+    |
     <% if @user and @user.is_friends_with?(@this_user) %>
       <%= link_to t('user.view.remove as friend'), :controller => 'user', :action => 'remove_friend', :display_name => @this_user.display_name %>
     <% else %>
index a8ebb4d8fbb938426c1b32b96061ba32217955eb..eef48a7bbe5b6eca55d15ec93e7cd7c3d6553627 100644 (file)
@@ -24,6 +24,8 @@ standard_settings: &standard_settings
   max_number_of_nodes: 50000
   # Maximum number of nodes that can be in a way (checked on save)
   max_number_of_way_nodes: 2000
+  # The maximum area you're allowed to request notes from, in square degrees
+  max_note_request_area: 25
   # Zoom level to use for postcode results from the geocoder
   postcode_zoom: 15
   # Zoom level to use for geonames results from the geocoder
index 72aca7e441e1855f8c7a7ac1f1cbe5d42cd1235b..18df05cf2c4f6ffb1be0c7e794cb7b7c4eb606b1 100644 (file)
@@ -1,5 +1,3 @@
-# Be sure to restart your server when you modify this file.
-
 # Add new mime types for use in respond_to blocks:
-# Mime::Type.register "text/richtext", :rtf
-# Mime::Type.register_alias "text/html", :iphone
+
+Mime::Type.register "application/gpx+xml", :gpx
index 2951da225e17efe91de7bfc0ec0ab2eccdd89f2b..cee5e0183c25a2f656e7e2eb175640b90434f71b 100644 (file)
@@ -906,6 +906,22 @@ de:
       history_disabled_tooltip: Reinzoomen um Änderungen für diesen Bereich anzuzeigen
       history_tooltip: Änderungen für diesen Bereich anzeigen
       history_zoom_alert: Du musst näher heranzoomen, um die Chronik zu sehen
+    osb:
+      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: Anlegeeen
+      Permalink: Permalink
   layouts: 
     community_blogs: Blogs
     community_blogs_title: Blogs von Mitwirkenden bei OpenStreetMap
index e41048d0fb0259da4112c6ad6601dc8abbc5a078..5b6d575d3f158e9cc65e91dcbeabf1845f3f7168 100644 (file)
@@ -277,6 +277,17 @@ en:
       download_xml: "Download XML"
       view_history: "view history"
       edit: "edit"
+    note:
+      open_title: "Unresolved issue: %{note_name}"
+      closed_title: "Resolved issue: %{note_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}"
@@ -1170,6 +1181,13 @@ en:
       greeting: "Hi,"
       hopefully_you: "Someone (possibly you) has asked for the password to be reset on this email address's openstreetmap.org account."
       click_the_link: "If this is you, please click the link below to reset your password."
+    note_plain:
+      subject_own: "[OpenStreetMap] %{commenter} has commented on one of your notes"
+      subject_other: "[OpenStreetMap] %{commenter} has commented on a note you are interested in"
+      greeting: "Hi,"
+      your_note: "%{commenter} has left a comment on one of your map notes near %{place}."
+      commented_note: "%{commenter} has left a comment on a map note you have commented on. The note is near %{place}."
+      details: "More details about the note can be found at %{URL}."
   message:
     inbox:
       title: "Inbox"
@@ -1636,6 +1654,7 @@ en:
       new diary entry: new diary entry
       my edits: my edits
       my traces: my traces
+      my notes: my map notes
       my settings: my settings
       oauth settings: oauth settings
       blocks on me: blocks on me
@@ -1644,6 +1663,7 @@ en:
       diary: diary
       edits: edits
       traces: traces
+      notes: map notes
       remove as friend: remove as friend
       add as friend: add as friend
       mapper since: "Mapper since:"
@@ -1895,6 +1915,21 @@ en:
       back: "View all blocks"
       revoker: "Revoker:"
       needs_view: "The user needs to log in before this block will be cleared."
+  note:
+    rss:
+      title: "OpenStreetMap Notes"
+      description_area: "A list of notes, reported, commented on or closed in your area [(%{min_lat}|%{min_lon}) -- (%{max_lat}|%{max_lon})]"
+      description_item: "An rss feed for note %{id}"
+      closed: "closed note (near %{place})"
+      new: "new note (near %{place})"
+      comment: "new comment (near %{place})"
+    mine:
+      title: "Notes submitted or commented on by %{user}"
+      heading: "%{user}'s notes"
+      description: "Notes submitted or commented on by %{user}"
+      id: "Id"
+      last_changed: "Last changed"
+
   javascripts:
     map:
       base:
@@ -1911,3 +1946,25 @@ en:
       history_tooltip: View edits for this area
       history_disabled_tooltip: Zoom in to view edits for this area
       history_zoom_alert: You must zoom in to view edits for this area
+    osb:
+      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: Report a problem with the map
+      Bug description: Problem description
+      Create: Report problem
+      Permalink: Permalink
+      Login: Login
+      Details: Details
+      edityourself: You can also edit the map directly your self
+      draghelp1: Please drag the marker to the location of the problem
+      draghelp2: and descripe it as accurate as possible
+
index 56a59a207d7a52ef57f5b649cae6fafe22103d58..334cb74221eaa27c5dc1ca0046027318efd0dbad 100644 (file)
@@ -74,7 +74,23 @@ 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 notes API
+  map.connect "api/#{API_VERSION}/notes", :controller => 'note', :action => 'list'
+  map.connect "api/#{API_VERSION}/notes/search", :controller => 'note', :action => 'search'
+  map.connect "api/#{API_VERSION}/notes/rss", :controller =>'notes', :action => 'rss'
+  map.connect "api/#{API_VERSION}/note/create", :controller => 'note', :action => 'create'
+  map.connect "api/#{API_VERSION}/note/:id/comment", :controller => 'note', :action => 'update', :id => /\d+/
+  map.connect "api/#{API_VERSION}/note/:id/close", :controller => 'note', :action => 'close', :id => /\d+/
+  map.connect "api/#{API_VERSION}/note/:id", :controller => 'note', :action => 'read', :id => /\d+/, :conditions => { :method => :get }
+  map.connect "api/#{API_VERSION}/note/:id", :controller => 'note', :action => 'delete', :id => /\d+/, :conditions => { :method => :delete }
+  map.connect "api/#{API_VERSION}/notes/getBugs", :controller => 'note', :action => 'list'
+  map.connect "api/#{API_VERSION}/notes/addPOIexec", :controller => 'note', :action => 'create'
+  map.connect "api/#{API_VERSION}/notes/closePOIexec", :controller => 'note', :action => 'close'
+  map.connect "api/#{API_VERSION}/notes/editPOIexec", :controller => 'note', :action => 'update'
+  map.connect "api/#{API_VERSION}/notes/getGPX", :controller => 'note', :action => 'list', :format => :gpx
+  map.connect "api/#{API_VERSION}/notes/getRSSfeed", :controller => 'note', :action => 'rss'
+
   # Data browsing
   map.connect '/browse/start', :controller => 'browse', :action => 'start'
   map.connect '/browse/way/:id', :controller => 'browse', :action => 'way', :id => /\d+/
@@ -88,6 +104,8 @@ 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/note/:id', :controller => 'browse', :action => 'note', :id => /\d+/
+  map.connect '/user/:display_name/notes', :controller => 'note', :action => 'mine'
   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..8d444a4
--- /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..6d259d2
--- /dev/null
@@ -0,0 +1,34 @@
+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
+    remove_foreign_key :map_bug_comment, [:commenter_id]
+    remove_foreign_key :map_bug_comment, [:bug_id]
+
+    remove_index :map_bugs, :name => "map_bug_comment_id_idx"
+
+    add_column :map_bugs, :text, :string
+
+    drop_table :map_bug_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..2a64bf2
--- /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..c5aa2c2
--- /dev/null
@@ -0,0 +1,11 @@
+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..c13c1f9
--- /dev/null
@@ -0,0 +1,15 @@
+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
diff --git a/db/migrate/20110508145337_cleanup_bug_tables.rb b/db/migrate/20110508145337_cleanup_bug_tables.rb
new file mode 100644 (file)
index 0000000..e7dfcb7
--- /dev/null
@@ -0,0 +1,25 @@
+class CleanupBugTables < ActiveRecord::Migration
+  def self.up
+    rename_column :map_bugs, :date_created, :created_at
+    rename_column :map_bugs, :last_changed, :updated_at
+    rename_column :map_bugs, :date_closed, :closed_at
+
+    rename_column :map_bug_comment, :date_created, :created_at
+    rename_column :map_bug_comment, :commenter_name, :author_name
+    rename_column :map_bug_comment, :commenter_ip, :author_ip
+    rename_column :map_bug_comment, :commenter_id, :author_id
+    rename_column :map_bug_comment, :comment, :body
+  end
+
+  def self.down
+    rename_column :map_bug_comment, :body, :comment
+    rename_column :map_bug_comment, :author_id, :commenter_id
+    rename_column :map_bug_comment, :author_ip, :commenter_ip
+    rename_column :map_bug_comment, :author_name, :commenter_name
+    rename_column :map_bug_comment, :created_at, :date_created
+
+    rename_column :map_bugs, :closed_at, :date_closed
+    rename_column :map_bugs, :updated_at, :last_changed
+    rename_column :map_bugs, :created_at, :date_created
+  end
+end
diff --git a/db/migrate/20110521142405_rename_bugs_to_notes.rb b/db/migrate/20110521142405_rename_bugs_to_notes.rb
new file mode 100644 (file)
index 0000000..240d447
--- /dev/null
@@ -0,0 +1,55 @@
+require 'lib/migrate'
+
+class RenameBugsToNotes < ActiveRecord::Migration
+  def self.up
+    rename_enumeration "map_bug_status_enum", "note_status_enum"
+    rename_enumeration "map_bug_event_enum", "note_event_enum"
+
+    rename_table :map_bugs, :notes
+    rename_sequence :notes, "map_bugs_id_seq", "notes_id_seq"
+    rename_index :notes, "map_bugs_pkey", "notes_pkey"
+    rename_index :notes, "map_bugs_changed_idx", "notes_updated_at_idx"
+    rename_index :notes, "map_bugs_created_idx", "notes_created_at_idx"
+    rename_index :notes, "map_bugs_tile_idx", "notes_tile_status_idx"
+
+    remove_foreign_key :map_bug_comment, [:bug_id], :map_bugs, [:id]
+    rename_column :map_bug_comment, :author_id, :commenter_id
+    remove_foreign_key :map_bug_comment, [:commenter_id], :users, [:id]
+    rename_column :map_bug_comment, :commenter_id, :author_id
+
+    rename_table :map_bug_comment, :note_comments
+    rename_column :note_comments, :bug_id, :note_id
+    rename_sequence :note_comments, "map_bug_comment_id_seq", "note_comments_id_seq"
+    rename_index :note_comments, "map_bug_comment_pkey", "note_comments_pkey"
+    rename_index :note_comments, "map_bug_comment_id_idx", "note_comments_note_id_idx"
+
+    add_foreign_key :note_comments, [:note_id], :notes, [:id]
+    add_foreign_key :note_comments, [:author_id], :users, [:id]
+  end
+
+  def self.down
+    remove_foreign_key :note_comments, [:author_id], :users, [:id]
+    remove_foreign_key :note_comments, [:note_id], :notes, [:id]
+
+    rename_index :note_comments, "note_comments_note_id_idx", "map_bug_comment_id_idx"
+    rename_index :notes, "note_comments_pkey", "map_bug_comment_pkey"
+    rename_column :note_comments, :note_id, :bug_id
+    rename_sequence :note_comments, "note_comments_id_seq", "map_bug_comment_id_seq"
+    rename_table :note_comments, :map_bug_comment
+
+    rename_column :map_bug_comment, :author_id, :commenter_id
+    add_foreign_key :map_bug_comment, [:commenter_id], :users, [:id]
+    rename_column :map_bug_comment, :commenter_id, :author_id
+    add_foreign_key :map_bug_comment, [:bug_id], :notes, [:id]
+
+    rename_index :notes, "notes_tile_status_idx", "map_bugs_tile_idx"
+    rename_index :notes, "notes_created_at_idx", "map_bugs_created_idx"
+    rename_index :notes, "notes_updated_at_idx", "map_bugs_changed_idx"
+    rename_index :notes, "notes_pkey", "map_bugs_pkey"
+    rename_sequence :notes, "notes_id_seq", "map_bugs_id_seq"
+    rename_table :notes, :map_bugs
+
+    rename_enumeration "note_event_enum", "map_bug_event_enum"
+    rename_enumeration "note_status_enum", "map_bug_status_enum"
+  end
+end
index 2740eab0c5472da4c76d95128c5f8253dd440cbb..90dee5f1dc43ceb424d1f78c934082d2370e0ce1 100644 (file)
@@ -56,4 +56,3 @@ private
     end
   end
 end
-
index f3accf2da4e5b12241c0a0ac4091ec9d5791e929..b3085d0ec0c83ed6ceb3150bca329eb35d28dd1e 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, max_area = MAX_REQUEST_AREA)
     # 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")
@@ -24,8 +24,8 @@ module MapBoundary
 
     # check the bbox isn't too large
     requested_area = (max_lat-min_lat)*(max_lon-min_lon)
-    if requested_area > MAX_REQUEST_AREA
-      raise OSM::APIBadBoundingBox.new("The maximum bbox size is " + MAX_REQUEST_AREA.to_s + 
+    if requested_area > max_area
+      raise OSM::APIBadBoundingBox.new("The maximum bbox size is " + max_area.to_s + 
         ", and your request was too large. Either request a smaller area, or use planet.osm")
     end
   end
index 81cdd4d0541bc485f362c87f4b40b995466b21f6..8e6629f0d2639d25e40590cfcbc7cd5ce7233a21 100644 (file)
@@ -105,11 +105,11 @@ module ActiveRecord
         @enumerations ||= Hash.new
       end
 
-      def create_enumeration (enumeration_name, values)
+      def create_enumeration(enumeration_name, values)
         enumerations[enumeration_name] = values
       end
 
-      def drop_enumeration (enumeration_name)
+      def drop_enumeration(enumeration_name)
         enumerations.delete(enumeration_name)
       end
 
@@ -158,29 +158,34 @@ module ActiveRecord
         return ""
       end
  
-      def change_engine (table_name, engine)
+      def change_engine(table_name, engine)
       end
 
-      def add_fulltext_index (table_name, column)
-        execute "CREATE INDEX #{table_name}_#{column}_idx on #{table_name} (#{column})"
+      def add_fulltext_index(table_name, column)
+        execute "CREATE INDEX #{table_name}_#{column}_idx ON #{table_name} (#{column})"
       end
 
       def enumerations
         @enumerations ||= Hash.new
       end
 
-      def create_enumeration (enumeration_name, values)
+      def create_enumeration(enumeration_name, values)
         enumerations[enumeration_name] = values
-        execute "create type #{enumeration_name} as enum ('#{values.join '\',\''}')"
+        execute "CREATE TYPE #{enumeration_name} AS ENUM ('#{values.join '\',\''}')"
       end
 
-      def drop_enumeration (enumeration_name)
-        execute "drop type #{enumeration_name}"
+      def drop_enumeration(enumeration_name)
+        execute "DROP TYPE #{enumeration_name}"
         enumerations.delete(enumeration_name)
       end
 
+      def rename_enumeration(old_name, new_name)
+        execute "ALTER TYPE #{quote_table_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
+      end
+
       def alter_primary_key(table_name, new_columns)
-        execute "alter table #{table_name} drop constraint #{table_name}_pkey; alter table #{table_name} add primary key (#{new_columns.join(',')})"
+        execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{table_name}_pkey"
+        execute "ALTER TABLE #{table_name} ADD PRIMARY KEY (#{new_columns.join(',')})"
       end
 
       def interval_constant(interval)
@@ -201,6 +206,14 @@ module ActiveRecord
         quoted_column_names = column_names.map { |e| quote_column_name(e) }.join(", ")
         execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} USING #{index_method} (#{quoted_column_names})"
       end
+
+      def rename_index(table_name, old_name, new_name)
+        execute "ALTER INDEX #{quote_table_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
+      end
+
+      def rename_sequence(table_name, old_name, new_name)
+        execute "ALTER SEQUENCE #{quote_table_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
+      end
     end
   end
 end
index eaee7c328032f0373073b7f70aab777f20c169a0..9ee36e0c7aa6b26db8b085b152d2acf73d52eeec 100644 (file)
@@ -499,6 +499,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)
+
     minlat = (minlat * 10000000).round
     minlon = (minlon * 10000000).round
     maxlat = (maxlat * 10000000).round
diff --git a/public/images/closed_note_marker.png b/public/images/closed_note_marker.png
new file mode 100644 (file)
index 0000000..bf6d6bb
Binary files /dev/null and b/public/images/closed_note_marker.png differ
diff --git a/public/images/icon_error_add.png b/public/images/icon_error_add.png
new file mode 100644 (file)
index 0000000..671cf42
Binary files /dev/null and b/public/images/icon_error_add.png differ
diff --git a/public/images/open_note_marker.png b/public/images/open_note_marker.png
new file mode 100644 (file)
index 0000000..a580316
Binary files /dev/null and b/public/images/open_note_marker.png differ
diff --git a/public/javascripts/openstreetbugs.js b/public/javascripts/openstreetbugs.js
new file mode 100644 (file)
index 0000000..12bd7d8
--- /dev/null
@@ -0,0 +1,989 @@
+/*
+       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.
+*/
+
+/** This version has been adapted from the original javascript library to fit the openstreetmap rails_port implementation */
+
+OpenLayers.Layer.OpenStreetBugs = new OpenLayers.Class(OpenLayers.Layer.Markers, {
+       /**
+        * The URL of the OpenStreetBugs API.
+        * @var String
+       */
+       serverURL : "/api/0.6/",
+
+       /**
+        * 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("/images/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("/images/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("notes"
+                       + "?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(putAJAXMarker.bugs[id][2] ? i18n("javascripts.osb.Fixed Error") : i18n("javascripts.osb.Unresolved Error")));
+
+               el1.appendChild(document.createTextNode(" ["));
+               el2 = document.createElement("a");
+               el2.href = "/browse/bug/" + id;
+               el2.onclick = function(){ layer.map.setCenter(putAJAXMarker.bugs[id][0].clone().transform(layer.apiProjection, layer.map.getProjectionObject()), 15); };
+               el2.appendChild(document.createTextNode(i18n("javascripts.osb.Details")));
+               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(i18n("javascripts.osb.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 ? i18n("javascripts.osb.Description") : i18n("javascripts.osb.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);
+            if (i == 0) { el2 = document.createElement("br"); 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(i18n("javascripts.osb.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 = i18n("javascripts.osb.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(i18n("javascripts.osb.Nickname")));
+                       el1.appendChild(el2);
+                       el2 = document.createElement("dd");
+                       var inputUsername = document.createElement("input");
+                       var inputUsername = document.createElement("input");;
+                       if (typeof loginName === 'undefined') {
+                               inputUsername.value = this.username;
+                       } else {
+                               inputUsername.value = loginName;
+                               inputUsername.setAttribute('disabled','true');
+                       }
+                       inputUsername.className = "osbUsername";
+                       inputUsername.onkeyup = function(){ layer.setUserName(inputUsername.value); };
+                       el2.appendChild(inputUsername);
+                       el3 = document.createElement("a");
+                       el3.setAttribute("href","login");
+                       el3.className = "hide_if_logged_in";
+                       el3.appendChild(document.createTextNode(i18n("javascripts.osb.Login")));
+                       el2.appendChild(el3)
+                       el1.appendChild(el2);                   
+
+                       el2 = document.createElement("dt");
+                       el2.appendChild(document.createTextNode(i18n("javascripts.osb.Comment")));
+                       el1.appendChild(el2);
+                       el2 = document.createElement("dd");
+                       var inputComment = document.createElement("textarea");
+                       inputComment.setAttribute("cols",40);
+            inputComment.setAttribute("rows",3);
+       
+                       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", "button");
+            el3.onclick = function(){ this.form.onsubmit(); return false; };
+                       el3.value = i18n("javascripts.osb.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 = i18n("javascripts.osb.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 = i18n("javascripts.osb.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("note/create"
+                       + "?lat="+encodeURIComponent(lonlat.lat)
+                       + "&lon="+encodeURIComponent(lonlat.lon)
+                       + "&text="+encodeURIComponent(description)
+                       + "&name="+encodeURIComponent(this.getUserName())
+                       + "&format=js"
+               );
+       },
+
+       /**
+        * Adds a comment to a bug.
+        * @param Number id
+        * @param String comment
+       */
+       submitComment: function(id, comment) {
+               this.apiRequest("note/"+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("note/"+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("/images/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 = i18n("javascripts.osb.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) {
+               var lonlat = this.map.getLonLatFromViewPortPx(e.xy);
+               this.addTemporaryMarker(lonlat);
+       },
+
+       addTemporaryMarker: function(lonlat) {
+               if(!this.map) return true;
+               deactivateControl();
+
+               var control = this;
+               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);
+
+
+               /** Implement a drag and drop for markers */
+               /* TODO: veryfy that the scoping of variables works correctly everywhere */             
+               var dragging = false;
+               var dragFunction = function(e) {
+                       map.events.unregister("mouseup",map,dragFunction);
+                       lonlat = map.getLonLatFromViewPortPx(e.xy);
+                       lonlatApi = lonlat.clone().transform(map.getProjectionObject(), map.osbLayer.apiProjection);
+                       marker.moveTo(map.getLayerPxFromViewPortPx(e.xy));
+                       marker.popup.moveTo(map.getLayerPxFromViewPortPx(e.xy));                        
+                       marker.popup.updateRelativePosition();
+                       dragging = false;
+                       return false;
+               };
+
+               marker.events.register("mouseover", this,
+                               function(){ document.getElementById("OpenLayers.Map_18_OpenLayers_Container").style.cursor = "move"; });
+               marker.events.register("mouseout", this,
+                               function(){ if (!dragging) {document.getElementById("OpenLayers.Map_18_OpenLayers_Container").style.cursor = "default"; }});
+               marker.events.register("mousedown", this,
+                               function() { dragging = true; map.events.register("mouseup",map, dragFunction); return false;});
+               
+
+               var newContent = document.createElement("div");
+               var el1,el2,el3;
+               el1 = document.createElement("h3");
+               el1.appendChild(document.createTextNode(i18n("javascripts.osb.Create bug")));
+               newContent.appendChild(el1);
+               newContent.appendChild(document.createTextNode(i18n("javascripts.osb.draghelp1")));
+               newContent.appendChild(document.createElement("br"));
+               newContent.appendChild(document.createTextNode(i18n("javascripts.osb.draghelp2")));
+               newContent.appendChild(document.createElement("br"));
+               newContent.appendChild(document.createElement("br"));
+
+               var el_form = document.createElement("form");
+
+               el1 = document.createElement("dl");
+               el2 = document.createElement("dt");
+               el2.appendChild(document.createTextNode(i18n("javascripts.osb.Nickname")));
+               el1.appendChild(el2);
+               el2 = document.createElement("dd");
+               var inputUsername = document.createElement("input");;
+               if (typeof loginName === 'undefined') {
+                   inputUsername.value = this.osbLayer.username;
+               } else {
+                       inputUsername.value = loginName;
+                       inputUsername.setAttribute('disabled','true');
+               }               
+               inputUsername.className = "osbUsername";
+               
+               inputUsername.onkeyup = function(){ control.osbLayer.setUserName(inputUsername.value); };
+               el2.appendChild(inputUsername);
+               el3 = document.createElement("a");
+               el3.setAttribute("href","login");
+               el3.className = "hide_if_logged_in";
+               el3.appendChild(document.createTextNode(i18n("javascripts.osb.Login")));
+               el2.appendChild(el3);
+               el1.appendChild(el2);
+               el2 = document.createElement("br");
+        el1.appendChild(el2);
+
+               el2 = document.createElement("dt");
+               el2.appendChild(document.createTextNode(i18n("javascripts.osb.Bug description")));
+               el1.appendChild(el2);
+               el2 = document.createElement("dd");
+               var inputDescription = document.createElement("textarea");
+               inputDescription.setAttribute("cols",40);
+               inputDescription.setAttribute("rows",3);
+               el2.appendChild(inputDescription);
+               el1.appendChild(el2);
+               el_form.appendChild(el1);
+
+               el1 = document.createElement("div");
+               el2 = document.createElement("input");
+               el2.setAttribute("type", "button");
+               el2.value = i18n("javascripts.osb.Create");
+        el2.onclick = function() { control.osbLayer.createBug(lonlatApi, inputDescription.value); marker.feature = null; feature.destroy(); return false; };
+               el1.appendChild(el2);
+               el2 = document.createElement("input");
+               el2.setAttribute("type", "button");
+               el2.value = i18n("javascripts.osb.Cancel");
+               el2.onclick = function(){ feature.destroy(); };
+               el1.appendChild(el2);
+               el_form.appendChild(el1);
+               newContent.appendChild(el_form);
+
+               el2 = document.createElement("hr");
+               el1.appendChild(el2);
+               el2 = document.createElement("a");
+               el2.setAttribute("href","edit");
+               el2.appendChild(document.createTextNode(i18n("javascripts.osb.edityourself")));
+               el1.appendChild(el2);
+
+               feature.data.popupContentHTML = newContent;
+               var popup = feature.createPopup(true);
+               popup.events.register("close", this, function(){ feature.destroy(); });
+               this.map.addPopup(popup);
+               popup.updateSize();
+               marker.popup = popup;
+       },
+
+       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"
+});
+
+
+/**
+ * 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 = { };
+
+function deactivateControl() { 
+    map.osbControl.deactivate(); 
+    document.getElementById("OpenLayers.Map_18_OpenLayers_Container").style.cursor = "default"; 
+  }
+
+
+/* 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"
+});
+*/
index a1efa8583621c7b894955d708648f3cf3e2be61b..5d25e7fbadfd39349cc27a509a483e12f92c483b 100644 (file)
@@ -18,3 +18,9 @@
 .olControlPanZoom {
   display: none;
 }
+
+/* Rules for map bug reporting */
+
+#reportbuganchor { 
+  font-size: 150%;
+}
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/note_comments.yml b/test/fixtures/note_comments.yml
new file mode 100644 (file)
index 0000000..e078b99
--- /dev/null
@@ -0,0 +1,117 @@
+t1:
+  id: 1
+  note_id: 1
+  visible: true
+  created_at: 2007-01-01 00:00:00
+  author_name: 'testname'
+  author_ip: '192.168.1.1'
+  body: 'This is the initial description of the note 1'
+
+t2:
+  id: 2
+  note_id: 2
+  visible: true
+  created_at: 2007-01-01 00:00:00
+  author_name: 'testname'
+  author_ip: '192.168.1.1'
+  body: 'This is the initial description of the note 2'
+
+t3:
+  id: 3
+  note_id: 2
+  visible: true
+  created_at: 2007-02-01 00:00:00
+  author_name: 'testname'
+  author_ip: '192.168.1.1'
+  body: 'This is an additional comment for note 2'
+
+t4:
+  id: 4
+  note_id: 3
+  visible: true
+  created_at: 2007-01-01 00:00:00
+  author_name: 'testname'
+  author_ip: '192.168.1.1'
+  body: 'This is the initial comment for note 3'
+
+t5:
+  id: 5
+  note_id: 4
+  visible: true
+  created_at: 2007-01-01 00:00:00
+  author_name: 'testname'
+  author_ip: '192.168.1.1'
+  body: 'Spam for note 4'
+
+t6:
+  id: 6
+  note_id: 5
+  visible: true
+  created_at: 2007-01-01 00:00:00
+  author_name: 'testname'
+  author_ip: '192.168.1.1'
+  body: 'Valid comment for note 5'
+
+t7:
+  id: 7
+  note_id: 5
+  visible: false
+  created_at: 2007-02-01 00:00:00
+  author_name: 'testname'
+  author_ip: '192.168.1.1'
+  body: 'Spam for note 5'
+
+t8:
+  id: 8
+  note_id: 5
+  visible: true
+  created_at: 2007-02-01 00:00:00
+  author_name: 'testname'
+  author_ip: '192.168.1.1'
+  body: 'Another valid comment for note 5'
+
+t9:
+  id: 9
+  note_id: 6
+  visible: true
+  created_at: 2007-01-01 00:00:00
+  event: opened
+  author_id: 1
+  body: 'This is a note with from a logged-in user'
+
+t10:
+  id: 10
+  note_id: 6
+  visible: true
+  created_at: 2007-02-01 00:00:00
+  event: commented
+  author_id: 4
+  body: 'A comment from another logged-in user'
+
+t11:
+  id: 11
+  note_id: 7
+  visible: true
+  event: opened
+  created_at: 2007-01-01 00:00:00
+  author_name: 'testname'
+  author_ip: '192.168.1.1'
+  body: 'Initial note description'
+
+t12:
+  id: 12
+  note_id: 7
+  visible: true
+  event: commented
+  created_at: 2007-02-01 00:00:00
+  author_name: 'testname'
+  author_ip: '192.168.1.1'
+  body: 'A comment description'
+
+t13:
+  id: 13
+  note_id: 7
+  visible: true
+  event: closed
+  created_at: 2007-03-01 00:00:00
+  author_id: 4
diff --git a/test/fixtures/notes.yml b/test/fixtures/notes.yml
new file mode 100644 (file)
index 0000000..ffecba8
--- /dev/null
@@ -0,0 +1,67 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+<% SCALE = 10000000 unless defined?(SCALE) %>
+
+open_note:
+  id: 1
+  latitude: <%= 1*SCALE %>
+  longitude: <%= 1*SCALE %>
+  status: open
+  tile: <%= QuadTile.tile_for_point(1,1) %>
+  created_at: 2007-01-01 00:00:00
+  updated_at: 2007-01-01 00:00:00
+
+open_note_with_comment:
+  id: 2
+  latitude: <%= 1.1*SCALE %>
+  longitude: <%= 1.1*SCALE %>
+  status: open
+  tile: <%= QuadTile.tile_for_point(1.1,1.1) %>
+  created_at: 2007-01-01 00:00:00
+  updated_at: 2007-02-01 00:00:00
+
+closed_note_with_comment:
+  id: 3
+  latitude: <%= 1.2*SCALE %>
+  longitude: <%= 1.2*SCALE %>
+  status: closed
+  tile: <%= QuadTile.tile_for_point(1.2,1.2) %>
+  created_at: 2007-01-01 00:00:00
+  updated_at: 2007-03-01 00:00:00
+  closed_at:  2007-03-01 00:00:00
+
+hidden_note_with_comment:
+  id: 4
+  latitude: <%= 1.3*SCALE %>
+  longitude: <%= 1.3*SCALE %>
+  status: hidden
+  tile: <%= QuadTile.tile_for_point(1.3,1.3) %>
+  created_at: 2007-01-01 00:00:00
+  updated_at: 2007-03-01 00:00:00
+
+note_with_hidden_comment:
+  id: 5
+  latitude: <%= 1.4*SCALE %>
+  longitude: <%= 1.4*SCALE %>
+  status: open
+  tile: <%= QuadTile.tile_for_point(1.4,1.4) %>
+  created_at: 2007-01-01 00:00:00
+  updated_at: 2007-03-01 00:00:00
+
+note_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) %>
+  created_at: 2007-01-01 00:00:00
+  updated_at: 2007-03-01 00:00:00
+
+note_closed_by_user:
+  id: 7
+  latitude: <%= 1.6*SCALE %>
+  longitude: <%= 1.6*SCALE %>
+  status: closed
+  tile: <%= QuadTile.tile_for_point(1.6,1.6) %>
+  created_at: 2007-01-01 00:00:00
+  updated_at: 2007-03-01 00:00:00
+  closed_at:  2007-03-01 00:00:00
diff --git a/test/functional/note_controller_test.rb b/test/functional/note_controller_test.rb
new file mode 100644 (file)
index 0000000..2e4a01b
--- /dev/null
@@ -0,0 +1,324 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class NoteControllerTest < ActionController::TestCase
+  fixtures :users, :notes, :note_comments
+
+  def test_note_create_success
+    assert_difference('Note.count') do
+      assert_difference('NoteComment.count') do
+        post :create, {: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 = ActiveSupport::JSON.decode(@response.body)
+    assert_not_nil js
+    assert_equal id, js["note"]["id"]
+    assert_equal "open", js["note"]["status"]
+    assert_equal "opened", js["note"]["comments"].last["event"]
+    assert_equal "This is a comment", js["note"]["comments"].last["body"]
+    assert_equal "new_tester (a)", js["note"]["comments"].last["author_name"]
+  end
+
+  def test_note_create_fail
+    assert_no_difference('Note.count') do
+      assert_no_difference('NoteComment.count') do
+        post :create, {:lon => -1.0, :name => "new_tester", :text => "This is a comment"}
+      end
+    end
+    assert_response :bad_request
+
+    assert_no_difference('Note.count') do
+      assert_no_difference('NoteComment.count') do
+        post :create, {:lat => -1.0, :name => "new_tester", :text => "This is a comment"}
+      end
+    end
+    assert_response :bad_request
+
+    assert_no_difference('Note.count') do
+      assert_no_difference('NoteComment.count') do
+        post :create, {:lat => -1.0, :lon => -1.0, :name => "new_tester"}
+      end
+    end
+    assert_response :bad_request
+
+    assert_no_difference('Note.count') do
+      assert_no_difference('NoteComment.count') do
+        post :create, {:lat => -100.0, :lon => -1.0, :name => "new_tester", :text => "This is a comment"}
+      end
+    end
+    assert_response :bad_request
+
+    assert_no_difference('Note.count') do
+      assert_no_difference('NoteComment.count') do
+        post :create, {:lat => -1.0, :lon => -200.0, :name => "new_tester", :text => "This is a comment"}
+      end
+    end
+    assert_response :bad_request
+  end
+
+  def test_note_comment_create_success
+    assert_difference('NoteComment.count') do
+      post :update, {:id => notes(:open_note_with_comment).id, :name => "new_tester2", :text => "This is an additional comment"}
+    end
+    assert_response :success
+
+    get :read, {:id => notes(:open_note_with_comment).id, :format => 'json'}
+    assert_response :success
+    js = ActiveSupport::JSON.decode(@response.body)
+    assert_not_nil js
+    assert_equal notes(:open_note_with_comment).id, js["note"]["id"]
+    assert_equal "open", js["note"]["status"]
+    assert_equal "commented", js["note"]["comments"].last["event"]
+    assert_equal "This is an additional comment", js["note"]["comments"].last["body"]
+    assert_equal "new_tester2 (a)", js["note"]["comments"].last["author_name"]
+  end
+
+  def test_note_comment_create_fail
+    assert_no_difference('NoteComment.count') do
+      post :update, {:name => "new_tester2", :text => "This is an additional comment"}
+    end
+    assert_response :bad_request
+
+    assert_no_difference('NoteComment.count') do
+      post :update, {:id => notes(:open_note_with_comment).id, :name => "new_tester2"}
+    end
+    assert_response :bad_request
+
+    assert_no_difference('NoteComment.count') do
+      post :update, {:id => 12345, :name => "new_tester2", :text => "This is an additional comment"}
+    end
+    assert_response :not_found
+
+    assert_no_difference('NoteComment.count') do
+      post :update, {:id => notes(:hidden_note_with_comment).id, :name => "new_tester2", :text => "This is an additional comment"}
+    end
+    assert_response :gone
+  end
+
+  def test_note_close_success
+    post :close, {:id => notes(:open_note_with_comment).id}
+    assert_response :success
+
+    get :read, {:id => notes(:open_note_with_comment).id, :format => 'json'}
+    assert_response :success
+    js = ActiveSupport::JSON.decode(@response.body)
+    assert_not_nil js
+    assert_equal notes(:open_note_with_comment).id, js["note"]["id"]
+    assert_equal "closed", js["note"]["status"]
+    assert_equal "closed", js["note"]["comments"].last["event"]
+    assert_equal "NoName (a)", js["note"]["comments"].last["author_name"]
+  end
+
+  def test_note_close_fail
+    post :close
+    assert_response :bad_request
+
+    post :close, {:id => 12345}
+    assert_response :not_found
+
+    post :close, {:id => notes(:hidden_note_with_comment).id}
+    assert_response :gone
+  end
+
+  def test_note_read_success
+    get :read, {:id => notes(:open_note).id}
+    assert_response :success      
+    assert_equal "application/xml", @response.content_type
+
+    get :read, {:id => notes(:open_note).id, :format => "xml"}
+    assert_response :success
+    assert_equal "application/xml", @response.content_type
+
+    get :read, {:id => notes(:open_note).id, :format => "rss"}
+    assert_response :success
+    assert_equal "application/rss+xml", @response.content_type
+
+    get :read, {:id => notes(:open_note).id, :format => "json"}
+    assert_response :success
+    assert_equal "application/json", @response.content_type
+
+    get :read, {:id => notes(:open_note).id, :format => "gpx"}
+    assert_response :success
+    assert_equal "application/gpx+xml", @response.content_type
+  end
+
+  def test_note_read_hidden_comment
+    get :read, {:id => notes(:note_with_hidden_comment).id, :format => 'json'}
+    assert_response :success
+    js = ActiveSupport::JSON.decode(@response.body)
+    assert_not_nil js
+    assert_equal notes(:note_with_hidden_comment).id, js["note"]["id"]
+    assert_equal 2, js["note"]["comments"].count
+    assert_equal "Valid comment for note 5", js["note"]["comments"][0]["body"]
+    assert_equal "Another valid comment for note 5", js["note"]["comments"][1]["body"]
+  end
+
+  def test_note_read_fail
+    post :read
+    assert_response :bad_request
+
+    get :read, {:id => 12345}
+    assert_response :not_found
+
+    get :read, {:id => notes(:hidden_note_with_comment).id}
+    assert_response :gone
+  end
+
+  def test_note_delete_success
+    delete :delete, {:id => notes(:open_note_with_comment).id}
+    assert_response :success
+
+    get :read, {:id => notes(:open_note_with_comment).id, :format => 'json'}
+    assert_response :gone
+  end
+
+  def test_note_delete_fail
+    delete :delete
+    assert_response :bad_request
+
+    delete :delete, {:id => 12345}
+    assert_response :not_found
+
+    delete :delete, {:id => notes(:hidden_note_with_comment).id}
+    assert_response :gone
+  end
+
+  def test_get_notes_success
+    get :list, {:bbox => '1,1,1.2,1.2'}
+    assert_response :success
+    assert_equal "text/javascript", @response.content_type
+
+    get :list, {:bbox => '1,1,1.2,1.2', :format => 'rss'}
+    assert_response :success
+    assert_equal "application/rss+xml", @response.content_type
+
+    get :list, {:bbox => '1,1,1.2,1.2', :format => 'json'}
+    assert_response :success
+    assert_equal "application/json", @response.content_type
+
+    get :list, {:bbox => '1,1,1.2,1.2', :format => 'xml'}
+    assert_response :success
+    assert_equal "application/xml", @response.content_type
+
+    get :list, {:bbox => '1,1,1.2,1.2', :format => 'gpx'}
+    assert_response :success
+    assert_equal "application/gpx+xml", @response.content_type
+  end
+
+  def test_get_notes_large_area
+    get :list, {:bbox => '-2.5,-2.5,2.5,2.5'}
+    assert_response :success
+
+    get :list, {:l => '-2.5', :b => '-2.5', :r => '2.5', :t => '2.5'}
+    assert_response :success
+
+    get :list, {:bbox => '-10,-10,12,12'}
+    assert_response :bad_request
+
+    get :list, {:l => '-10', :b => '-10', :r => '12', :t => '12'}
+    assert_response :bad_request
+  end
+
+  def test_get_notes_closed
+    get :list, {:bbox=>'1,1,1.7,1.7', :closed => '7', :format => 'json'}
+    assert_response :success
+    assert_equal "application/json", @response.content_type
+    js = ActiveSupport::JSON.decode(@response.body)
+    assert_not_nil js
+    assert_equal 4, js.count
+
+    get :list, {:bbox=>'1,1,1.7,1.7', :closed => '0', :format => 'json'}
+    assert_response :success
+    assert_equal "application/json", @response.content_type
+    js = ActiveSupport::JSON.decode(@response.body)
+    assert_not_nil js
+    assert_equal 4, js.count
+
+    get :list, {:bbox=>'1,1,1.7,1.7', :closed => '-1', :format => 'json'}
+    assert_response :success
+    assert_equal "application/json", @response.content_type
+    js = ActiveSupport::JSON.decode(@response.body)
+    assert_not_nil js
+    assert_equal 6, js.count
+  end
+
+  def test_get_notes_bad_params
+    get :list, {:bbox => '-2.5,-2.5,2.5'}
+    assert_response :bad_request
+
+    get :list, {:bbox => '-2.5,-2.5,2.5,2.5,2.5'}
+    assert_response :bad_request
+
+    get :list, {:b => '-2.5', :r => '2.5', :t => '2.5'}
+    assert_response :bad_request
+
+    get :list, {:l => '-2.5', :r => '2.5', :t => '2.5'}
+    assert_response :bad_request
+
+    get :list, {:l => '-2.5', :b => '-2.5', :t => '2.5'}
+    assert_response :bad_request
+
+    get :list, {:l => '-2.5', :b => '-2.5', :r => '2.5'}
+    assert_response :bad_request
+  end
+
+  def test_search_success
+    get :search, {:q => 'note 1'}
+    assert_response :success
+    assert_equal "text/javascript", @response.content_type
+
+    get :search, {:q => 'note 1', :format => 'xml'}
+    assert_response :success
+    assert_equal "application/xml", @response.content_type
+
+    get :search, {:q => 'note 1', :format => 'json'}
+    assert_response :success
+    assert_equal "application/json", @response.content_type
+
+    get :search, {:q => 'note 1', :format => 'rss'}
+    assert_response :success
+    assert_equal "application/rss+xml", @response.content_type
+
+    get :search, {:q => 'note 1', :format => 'gpx'}
+    assert_response :success
+    assert_equal "application/gpx+xml", @response.content_type
+  end
+
+  def test_search_bad_params
+    get :search
+    assert_response :bad_request
+  end
+
+  def test_rss_success
+    get :rss
+    assert_response :success
+    assert_equal "application/rss+xml", @response.content_type
+
+    get :rss, {:bbox=>'1,1,1.2,1.2'}
+    assert_response :success   
+    assert_equal "application/rss+xml", @response.content_type
+  end
+
+  def test_rss_fail
+    get :rss, {:bbox=>'1,1,1.2'}
+    assert_response :bad_request
+
+    get :rss, {:bbox=>'1,1,1.2,1.2,1.2'}
+    assert_response :bad_request
+  end
+
+  def test_user_notes_success
+    get :mine, {:display_name=>'test'}
+    assert_response :success
+
+    get :mine, {:display_name=>'pulibc_test2'}
+    assert_response :success
+
+    get :mine, {:display_name=>'non-existent'}
+    assert_response :not_found 
+  end
+end