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
--- /dev/null
+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
end
end
+ def friendly_date(date)
+ content_tag(:span, time_ago_in_words(date), :title => l(date, :format => :friendly))
+ end
+
private
def javascript_strings_for_key(key)
end
end
+ def note_author(object)
+ if object.author.nil?
+ h(object.author_name)
+ else
+ link_to h(object.author_name), :controller => "user", :action => "view", :display_name => object.author_name
+ end
+ end
+
private
ICON_TAGS = [
--- /dev/null
+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
--- /dev/null
+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
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)
</iframe>
<div id="browse_map">
- <% if map.instance_of? Changeset or (map.instance_of? Node and map.version > 1) or map.visible %>
+ <% if map.instance_of? Changeset or (map.instance_of? Node and map.version > 1) or map.visible? %>
<div id="small_map">
</div>
<span id="loading"><%= t 'browse.map.loading' %></span>
+ <% if map.instance_of? Note -%>
+ <%= link_to(t("browse.map.larger.area"), { :controller => :site, :action => :index, :notes => "yes" }, { :id => "area_larger_map", :class => "geolink bbox" }) %>
+ <% else -%>
<%= link_to(t("browse.map.larger.area"), { :controller => :site, :action => :index, :box => "yes" }, { :id => "area_larger_map", :class => "geolink bbox" }) %>
+ <% end -%>
<br />
<%= link_to(t("browse.map.edit.area"), { :controller => :site, :action => :edit }, { :id => "area_edit", :class => "geolink bbox" }) %>
- <% unless map.instance_of? Changeset %>
+ <% unless map.instance_of? Changeset or map.instance_of? Note %>
<br />
<%= link_to("", { :controller => :site, :action => :index }, { :id => "object_larger_map", :class => "geolink object" }) %>
<br />
</ul>
</div>
-<% if map.instance_of? Changeset or (map.instance_of? Node and map.version > 1) or map.visible %>
+<% if map.instance_of? Changeset or (map.instance_of? Node and map.version > 1) or map.visible? %>
<script type="text/javascript">
OpenLayers.Lang.setCode("<%= I18n.locale.to_s %>");
<% end %>
updatelinks(centre.lon, centre.lat, 16, null, minlon, minlat, maxlon, maxlat)
+ <% elsif map.instance_of? Note %>
+ var centre = new OpenLayers.LonLat(<%= map.lon %>, <%= map.lat %>);
+
+ setMapCenter(centre, 16);
+ addMarkerToMap(centre);
+
+ var bbox = getMapExtent();
+
+ $("loading").style.display = "none";
+
+ $$("#browse_map .geolink").each(function (link) {
+ link.style.display = "inline";
+ });
+
+ $("remote_area_edit").observe("click", function (event) {
+ remoteEditHandler(event, bbox);
+ });
+
+ <% if preferred_editor == "remote" %>
+ $("area_edit").observe("click", function (event) {
+ remoteEditHandler(event, bbox);
+ });
+ <% end %>
+
+ updatelinks(centre.lon, centre.lat, 16, null, bbox.left, bbox.bottom, bbox.right, bbox.top)
<% else %>
var obj_type = "<%= map.class.name.downcase %>";
var obj_id = <%= map.id %>;
--- /dev/null
+<%= render :partial => "navigation" %>
+
+<h2>
+ <%= image_tag "#{@note.status}_note_marker.png", :alt => @note.status %>
+ <%= t "browse.note.#{@note.status}_title", :note_name => @note.id %>
+</h2>
+
+<%= render :partial => "map", :object => @note %>
+
+<table class="browse_details">
+
+ <tr>
+ <th><%= t "browse.note.opened" %></th>
+ <td><%= t "browse.note.at_by", :when => friendly_date(@note.created_at), :user => note_author(@note) %></td>
+ </tr>
+
+ <% if @note.status == "closed" %>
+ <tr>
+ <th><%= t "browse.note.closed" %></th>
+ <td><%= t "browse.note.at_by", :when => friendly_date(@note.closed_at), :user => note_author(@note.comments.last) %></td>
+ </tr>
+ <% elsif @note.comments.length > 1 %>
+ <tr>
+ <th><%= t "browse.note.last_modified" %></th>
+ <td><%= t "browse.note.at_by", :when => friendly_date(@note.updated_at), :user => note_author(@note.comments.last) %></td>
+ </tr>
+ <% end %>
+
+ <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>
+
+ <% if @note.comments.length > 1 %>
+ <tr valign="top">
+ <th><%= t "browse.note.comments" %></th>
+ <td class="browse_comments">
+ <table>
+ <% @note.comments[1..-1].each do |comment| %>
+ <tr>
+ <td>
+ <%= h(comment.body) %>
+ <br />
+ <span class="by"><%= t "browse.note.at_by", :when => friendly_date(comment.created_at), :user => note_author(comment) %></span>
+ </td>
+ </tr>
+ <% end %>
+ </table>
+ </td>
+ </tr>
+ <% end %>
+
+</table>
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+<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>
--- /dev/null
+<%= link_to user.display_name, :controller => "user", :action => "view", :display_name => user.display_name %>
--- /dev/null
+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
--- /dev/null
+@notes.each do |note|
+ page.call "putAJAXMarker",
+ note.id, note.lon, note.lat,
+ note.flatten_comment("<hr />"),
+ note.status == "open" ? 0 : 1
+end
--- /dev/null
+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
--- /dev/null
+xml.instruct!
+
+xml << render(:partial => "note", :collection => @notes)
--- /dev/null
+<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' %>
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+xml.instruct!
+
+xml << render(:partial => "note", :object => @note)
--- /dev/null
+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
--- /dev/null
+<%= 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 %>
+
+
<div id="permalink">
<a href="/" id="permalinkanchor" class="geolink llz layers object"><%= t 'site.index.permalink' %></a><br/>
<a href="/" id="shortlinkanchor"><%= t 'site.index.shortlink' %></a>
+ <a href="#" id="createnoteanchor">Report a problem</a>
</div>
</div>
<%= javascript_include_tag '/openlayers/OpenLayers.js' %>
<%= javascript_include_tag '/openlayers/OpenStreetMap.js' %>
+<%= javascript_include_tag 'notes.js' %>
<%= javascript_include_tag 'map.js' %>
<%= render :partial => 'resize' %>
OpenLayers.Lang.setCode("<%= I18n.locale.to_s %>");
+ <% if @user %>
+ var loginName = "<%= @user.display_name %>"
+ <% end %>
+
function mapInit(){
map = createMap("map");
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.noteLayer = new OpenLayers.Layer.Notes("Notes", {
+ setCookie: false,
+ permalinkURL: "http://www.openstreetmap.org/",
+ visibility: <%= params[:notes] == "yes" %>
+ });
+ map.addLayer(map.noteLayer);
+
+ $("createnoteanchor").observe("click", addNote);
+
+ map.events.register("zoomend", map, allowNoteReports);
<% end %>
<% unless object_zoom %>
<% end %>
}
+ function addNote() {
+ map.noteLayer.setVisibility(true);
+ map.noteLayer.addNote(map.getCenter());
+ }
+
+ function allowNoteReports() {
+ if (map.getZoom() > 11) {
+ $("createnoteanchor").style.visibility = "visible";
+ } else {
+ $("createnoteanchor").style.visibility = "hidden";
+ }
+ }
+
document.observe("dom:loaded", mapInit);
document.observe("dom:loaded", installEditHandler);
document.observe("dom:loaded", handleResize);
|
<%= 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 %>
|
|
<%= 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 %>
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
-# 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
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
next_relation_tooltip: "Next relation"
prev_changeset_tooltip: "Previous changeset"
next_changeset_tooltip: "Next changeset"
+ prev_note_tooltip: "Previous note"
+ next_note_tooltip: "Next note"
changeset_details:
created_at: "Created at:"
closed_at: "Closed at:"
download_xml: "Download XML"
view_history: "view history"
edit: "edit"
+ note:
+ open_title: "Unresolved issue: %{note_name}"
+ closed_title: "Resolved issue: %{note_name}"
+ opened: "Opened:"
+ last_modified: "Last modified:"
+ closed: "Closed:"
+ at_by: "%{when} ago by %{user}"
+ description: "Description:"
+ comments: "Comments:"
+
+ comment_by: "Comment by: "
+ comment: "Comment:"
+ date: "Date:"
changeset:
changeset_paging_nav:
showing_page: "Showing page %{page}"
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"
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
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:"
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:
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
+ note:
+ closed: Closed Note
+ open: Open Note
+ details: Details
+ permalink: Permalink
+ description: Description
+ comment: Comment
+ render_warning: This error has been fixed already. However, it might take a couple of days before the map image is updated.
+ update: Update
+ nickname: Nickname
+ login: Login
+ add_comment: Add Comment
+ close: Close
+ cancel: Cancel
+ create: Create Note
+ create_title: Report a problem with the map
+ create_help1: Please drag the marker to the location of the problem
+ create_help2: and descripe it as accurate as possible
+ report: Report Problem
+ edityourself: You can also edit the map directly your self
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+/
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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
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")
# 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
@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
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)
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
# 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
--- /dev/null
+/*
+ Dervied from the OpenStreetBugs client, which is available
+ under the following license.
+
+ 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.
+*/
+
+OpenLayers.Layer.Notes = new OpenLayers.Class(OpenLayers.Layer.Markers, {
+ /**
+ * The URL of the OpenStreetMap API.
+ *
+ * @var String
+ */
+ serverURL : "/api/0.6/",
+
+ /**
+ * Associative array (index: note ID) that is filled with the notes
+ * loaded in this layer.
+ *
+ * @var String
+ */
+ notes : { },
+
+ /**
+ * The username to be used to change or create notes on OpenStreetMap.
+ *
+ * @var String
+ */
+ username : "NoName",
+
+ /**
+ * The icon to be used for an open note.
+ *
+ * @var OpenLayers.Icon
+ */
+ iconOpen : new OpenLayers.Icon("/images/open_note_marker.png", new OpenLayers.Size(22, 22), new OpenLayers.Pixel(-11, -11)),
+
+ /**
+ * The icon to be used for a closed note.
+ *
+ * @var OpenLayers.Icon
+ */
+ iconClosed : new OpenLayers.Icon("/images/closed_note_marker.png", new OpenLayers.Size(22, 22), new OpenLayers.Pixel(-11, -11)),
+
+ /**
+ * The icon to be used when adding a new note.
+ *
+ * @var OpenLayers.Icon
+ */
+ iconNew : new OpenLayers.Icon("/images/new_note_marker.png", new OpenLayers.Size(22, 22), new OpenLayers.Pixel(-11, -11)),
+
+ /**
+ * The projection of the coordinates sent by the OpenStreetMap API.
+ *
+ * @var OpenLayers.Projection
+ */
+ apiProjection : new OpenLayers.Projection("EPSG:4326"),
+
+ /**
+ * If this is set to true, the user may not commit comments or close notes.
+ *
+ * @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 : [ ],
+
+ /**
+ * 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 : "/stylesheets/notes.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.loadNotes);
+
+ 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);
+ }
+ }
+ },
+
+ /**
+ * Called automatically called when the layer is added to a map.
+ * Initialises the automatic note 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.loadNotes);
+ this.loadNotes();
+
+ return ret;
+ },
+
+ /**
+ * At the moment the OpenStreetMap 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 OpenStreetMap.
+ */
+ setUserName: function(username) {
+ if (this.username == username)
+ return;
+
+ this.username = username;
+
+ for (var i = 0; i < this.markers.length; i++) {
+ var popup = this.markers[i].feature.popup;
+
+ if (popup) {
+ var els = popup.contentDom.getElementsByTagName("input");
+
+ for (var j = 0; j < els.length; j++) {
+ if (els[j].className == "username")
+ 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 notes in the current bounding box. Is automatically
+ * called by an event handler ("moveend" event) that is created in
+ * the afterAdd() method.
+ */
+ loadNotes: function() {
+ var bounds = this.map.getExtent();
+
+ if (bounds && this.getVisibility()) {
+ 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 scale = Math.pow(10, digits);
+
+ return Math.round(number * scale) / scale;
+ },
+
+ /**
+ * Adds an OpenLayers.Marker representing a note to the map. Is
+ * usually called by loadNotes().
+ *
+ * @param Number id The note ID
+ */
+ createMarker: function(id) {
+ if (this.notes[id]) {
+ if (this.notes[id].popup && !this.notes[id].popup.visible())
+ this.setPopupContent(this.notes[id].popup, id);
+
+ if (this.notes[id].closed != putAJAXMarker.notes[id][2])
+ this.notes[id].destroy();
+ else
+ return;
+ }
+
+ var lonlat = putAJAXMarker.notes[id][0].clone().transform(this.apiProjection, this.map.getProjectionObject());
+ var comments = putAJAXMarker.notes[id][1];
+ var closed = putAJAXMarker.notes[id][2];
+ var icon = closed ? this.iconClosed : this.iconOpen;
+
+ var feature = new OpenLayers.Feature(this, lonlat, {
+ icon: icon.clone(),
+ autoSize: true
+ });
+ feature.popupClass = OpenLayers.Popup.FramedCloud.Notes;
+ feature.noteId = id;
+ feature.closed = closed;
+ this.notes[id] = feature;
+
+ 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.events.triggerEvent("markerAdded");
+ },
+
+ /**
+ * Recreates the content of the popup of a marker.
+ *
+ * @param OpenLayers.Popup popup
+ * @param Number id The note ID
+ */
+ setPopupContent: function(popup, id) {
+ var el1,el2,el3;
+ var layer = this;
+
+ var newContent = document.createElement("div");
+
+ el1 = document.createElement("h3");
+ el1.appendChild(document.createTextNode(putAJAXMarker.notes[id][2] ? i18n("javascripts.note.closed") : i18n("javascripts.note.open")));
+
+ el1.appendChild(document.createTextNode(" ["));
+ el2 = document.createElement("a");
+ el2.href = "/browse/note/" + id;
+ el2.onclick = function() {
+ layer.map.setCenter(putAJAXMarker.notes[id][0].clone().transform(layer.apiProjection, layer.map.getProjectionObject()), 15);
+ };
+ el2.appendChild(document.createTextNode(i18n("javascripts.note.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.notes[id][0].lon+"&lat="+putAJAXMarker.notes[id][0].lat+"&zoom=15";
+ el2.appendChild(document.createTextNode(i18n("javascripts.note.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";
+ popup.updateSize();
+ };
+ var displayChange = function() {
+ containerDescription.style.display = "none";
+ containerChange.style.display = "block";
+ popup.updateSize();
+ };
+ displayDescription();
+
+ el1 = document.createElement("dl");
+ for (var i = 0; i < putAJAXMarker.notes[id][1].length; i++) {
+ el2 = document.createElement("dt");
+ el2.className = (i == 0 ? "note-description" : "note-comment");
+ el2.appendChild(document.createTextNode(i == 0 ? i18n("javascripts.note.description") : i18n("javascripts.note.comment")));
+ el1.appendChild(el2);
+ el2 = document.createElement("dd");
+ el2.className = (i == 0 ? "note-description" : "note-comment");
+ el2.appendChild(document.createTextNode(putAJAXMarker.notes[id][1][i]));
+ el1.appendChild(el2);
+ if (i == 0) {
+ el2 = document.createElement("br");
+ el1.appendChild(el2);
+ };
+ }
+ containerDescription.appendChild(el1);
+
+ if (putAJAXMarker.notes[id][2]) {
+ el1 = document.createElement("p");
+ el1.className = "note-fixed";
+ el2 = document.createElement("em");
+ el2.appendChild(document.createTextNode(i18n("javascripts.note.render_warning")));
+ 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.note.update");
+ 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(popup);
+ return false;
+ };
+
+ el1 = document.createElement("dl");
+ el2 = document.createElement("dt");
+ el2.appendChild(document.createTextNode(i18n("javascripts.note.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 = "username";
+ 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.note.login")));
+ el2.appendChild(el3)
+ el1.appendChild(el2);
+
+ el2 = document.createElement("dt");
+ el2.appendChild(document.createTextNode(i18n("javascripts.note.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.note.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.closeNote(id);
+ popup.hide();
+ return false;
+ };
+ el3.value = i18n("javascripts.note.close");
+ 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.note.cancel");
+ el1.appendChild(el2);
+ containerChange.appendChild(el1);
+ }
+
+ popup.setContentHTML(newContent);
+ },
+
+ /**
+ * Creates a new note.
+ *
+ * @param OpenLayers.LonLat lonlat The coordinates in the API projection.
+ * @param String description
+ */
+ createNote: 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 note.
+ *
+ * @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 note as fixed.
+ *
+ * @param Number id
+ */
+ closeNote: 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 OpenLayers.Popup popup
+ */
+ resetPopupContent: function(popup) {
+ if (popup)
+ 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 OpenLayers.Feature feature
+ */
+ showPopup: function(feature) {
+ var popup = feature.popup;
+
+ if (!popup) {
+ popup = feature.createPopup(true);
+
+ popup.events.register("close", this, function() {
+ this.resetPopupContent(popup);
+ });
+ }
+
+ this.setPopupContent(popup, feature.noteId);
+
+ if (!popup.map)
+ this.map.addPopup(popup);
+
+ popup.updateSize();
+
+ if (!popup.visible())
+ popup.show();
+ },
+
+ /**
+ * Hides the popup of the given marker.
+ *
+ * @param OpenLayers.Feature feature
+ */
+ hidePopup: function(feature) {
+ if (feature.popup && feature.popup.visible()) {
+ feature.popup.hide();
+ feature.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;
+
+ if (feature.popup && feature.popup.visible())
+ feature.layer.hidePopup(feature);
+ else
+ feature.layer.showPopup(feature);
+
+ 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;
+
+ feature.layer.showPopup(feature);
+
+ 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;
+
+ if (feature.popup && feature.popup.visible())
+ feature.layer.hidePopup(feature);
+
+ OpenLayers.Event.stop(e);
+ },
+
+ /**
+ * Add a new note.
+ */
+ addNote: function(lonlat) {
+ var layer = this;
+ var map = this.map;
+ var lonlatApi = lonlat.clone().transform(map.getProjectionObject(), this.apiProjection);
+ var feature = new OpenLayers.Feature(this, lonlat, { icon: this.iconNew.clone(), autoSize: true });
+ feature.popupClass = OpenLayers.Popup.FramedCloud.Notes;
+ var marker = feature.createMarker();
+ marker.feature = feature;
+ this.addMarker(marker);
+
+
+ /** Implement a drag and drop for markers */
+ /* TODO: veryfy that the scoping of variables works correctly everywhere */
+ var dragging = false;
+ var dragMove = function(e) {
+ lonlat = map.getLonLatFromViewPortPx(e.xy);
+ lonlatApi = lonlat.clone().transform(map.getProjectionObject(), map.noteLayer.apiProjection);
+ marker.moveTo(map.getLayerPxFromViewPortPx(e.xy));
+ marker.popup.moveTo(map.getLayerPxFromViewPortPx(e.xy));
+ marker.popup.updateRelativePosition();
+ return false;
+ };
+ var dragComplete = function(e) {
+ map.events.unregister("mousemove", map, dragMove);
+ map.events.unregister("mouseup", map, dragComplete);
+ dragMove(e);
+ dragging = false;
+ return false;
+ };
+
+ marker.events.register("mouseover", this, function() {
+ map.viewPortDiv.style.cursor = "move";
+ });
+ marker.events.register("mouseout", this, function() {
+ if (!dragging)
+ map.viewPortDiv.style.cursor = "default";
+ });
+ marker.events.register("mousedown", this, function() {
+ dragging = true;
+ map.events.register("mousemove", map, dragMove);
+ map.events.register("mouseup", map, dragComplete);
+ return false;
+ });
+
+ var newContent = document.createElement("div");
+ var el1,el2,el3;
+ el1 = document.createElement("h3");
+ el1.appendChild(document.createTextNode(i18n("javascripts.note.create_title")));
+ newContent.appendChild(el1);
+ newContent.appendChild(document.createTextNode(i18n("javascripts.note.create_help1")));
+ newContent.appendChild(document.createElement("br"));
+ newContent.appendChild(document.createTextNode(i18n("javascripts.note.create_help2")));
+ 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.note.nickname")));
+ el1.appendChild(el2);
+ el2 = document.createElement("dd");
+ var inputUsername = document.createElement("input");;
+ if (typeof loginName === 'undefined') {
+ inputUsername.value = this.username;
+ } else {
+ inputUsername.value = loginName;
+ inputUsername.setAttribute('disabled','true');
+ }
+ inputUsername.className = "username";
+
+ inputUsername.onkeyup = function() {
+ this.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.note.login")));
+ el2.appendChild(el3);
+ el1.appendChild(el2);
+ el2 = document.createElement("br");
+ el1.appendChild(el2);
+
+ el2 = document.createElement("dt");
+ el2.appendChild(document.createTextNode(i18n("javascripts.note.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.note.report");
+ el2.onclick = function() {
+ layer.createNote(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.note.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.note.edityourself")));
+ el1.appendChild(el2);
+
+ feature.data.popupContentHTML = newContent;
+ var popup = feature.createPopup(true);
+ popup.events.register("close", this, function() {
+ feature.destroy();
+ });
+ map.addPopup(popup);
+ popup.updateSize();
+ marker.popup = popup;
+ },
+
+ CLASS_NAME: "OpenLayers.Layer.Notes"
+});
+
+
+/**
+ * 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 Notes layer objects.
+ */
+OpenLayers.Popup.FramedCloud.Notes = 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.Notes"
+});
+
+
+/**
+ * This global function is executed by the OpenStreetMap API getBugs script.
+ *
+ * Each Notes 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 notes 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(/"/g, "\"").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
+ putAJAXMarker.notes[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 OpenStreetMap API. The
+ * “create note”, “comment” and “close note” 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 note and similar).
+ */
+function osbResponse(error)
+{
+ if(error)
+ alert("Error: "+error);
+
+ for(var i=0; i<putAJAXMarker.layers.length; i++)
+ putAJAXMarker.layers[i].loadNotes();
+}
+
+putAJAXMarker.layers = [ ];
+putAJAXMarker.notes = { };
white-space: nowrap;
}
+td.browse_comments {
+ padding: 0px;
+}
+
+td.browse_comments table {
+ border-collapse: collapse;
+}
+
+td.browse_comments table td {
+ padding-bottom: 10px;
+}
+
+td.browse_comments table td span.by {
+ font-size: small;
+ color: #999999;
+}
+
#browse_map {
width: 250px;
}
.olControlPanZoom {
display: none;
}
+
+/* Rules for map bug reporting */
+
+#reportbuganchor {
+ font-size: 150%;
+}
--- /dev/null
+.olPopupFramedCloudNotes dl {
+ margin: 0px;
+ padding: 0px;
+}
+
+.olPopupFramedCloudNotes dt {
+ margin: 0px;
+ padding: 0px;
+ font-weight: bold;
+ float: left;
+ clear: left;
+}
+
+.olPopupFramedCloudNotes dt:after {
+ content: ": ";
+}
+
+.olPopupFramedCloudNotes dt {
+ margin-right: 1ex;
+}
+
+.olPopupFramedCloudNotes dd {
+ margin: 0px;
+ padding: 0px;
+}
+
+.olPopupFramedCloudNotes ul.buttons {
+ list-style-type: none;
+ padding: 0px;
+ margin: 0px;
+}
+
+.olPopupFramedCloudNotes ul.buttons li {
+ display: inline;
+ margin: 0px;
+ padding: 0px;
+}
+
+.olPopupFramedCloudNotes h3 {
+ font-size: 1.2em;
+ margin: 0.2em 0em 0.7em 0em;
+}
--- /dev/null
+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
--- /dev/null
+# 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
--- /dev/null
+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