still doing its own language detection. Scrap that and use I18n.locale instead, now Potlatch is guaranteed to use whatever translation the rest of the site uses. I've tested this by switching the language in preferences back and forth, and also ensured that Potlatch will fall back on English if the specified language doesn't exist in its list of translations.
951 lines
34 KiB
Ruby
951 lines
34 KiB
Ruby
# amf_controller is a semi-standalone API for Flash clients, particularly
|
||
# Potlatch. All interaction between Potlatch (as a .SWF application) and the
|
||
# OSM database takes place using this controller. Messages are
|
||
# encoded in the Actionscript Message Format (AMF).
|
||
#
|
||
# Helper functions are in /lib/potlatch.rb
|
||
#
|
||
# Author:: editions Systeme D / Richard Fairhurst 2004-2008
|
||
# Licence:: public domain.
|
||
#
|
||
# == General structure
|
||
#
|
||
# Apart from the amf_read and amf_write methods (which distribute the requests
|
||
# from the AMF message), each method generally takes arguments in the order
|
||
# they were sent by the Potlatch SWF. Do not assume typing has been preserved.
|
||
# Methods all return an array to the SWF.
|
||
#
|
||
# == API 0.6
|
||
#
|
||
# Note that this requires a patched version of composite_primary_keys 1.1.0
|
||
# (see http://groups.google.com/group/compositekeys/t/a00e7562b677e193)
|
||
# if you are to run with POTLATCH_USE_SQL=false .
|
||
#
|
||
# == Debugging
|
||
#
|
||
# Any method that returns a status code (0 for ok) can also send:
|
||
# return(-1,"message") <-- just puts up a dialogue
|
||
# return(-2,"message") <-- also asks the user to e-mail me
|
||
# return(-3,["type",v],id) <-- version conflict
|
||
# return(-4,"type",id) <-- object not found
|
||
# -5 indicates the method wasn't called (due to a previous error)
|
||
#
|
||
# To write to the Rails log, use logger.info("message").
|
||
|
||
# Remaining issues:
|
||
# * version conflict when POIs and ways are reverted
|
||
|
||
class AmfController < ApplicationController
|
||
require 'stringio'
|
||
|
||
include Potlatch
|
||
|
||
# Help methods for checking boundary sanity and area size
|
||
include MapBoundary
|
||
|
||
before_filter :check_api_writable
|
||
|
||
# Main AMF handlers: process the raw AMF string (using AMF library) and
|
||
# calls each action (private method) accordingly.
|
||
# ** FIXME: refactor to reduce duplication of code across read/write
|
||
|
||
def amf_read
|
||
if request.post?
|
||
req=StringIO.new(request.raw_post+0.chr)# Get POST data as request
|
||
# (cf http://www.ruby-forum.com/topic/122163)
|
||
req.read(2) # Skip version indicator and client ID
|
||
results={} # Results of each body
|
||
|
||
# Parse request
|
||
|
||
headers=AMF.getint(req) # Read number of headers
|
||
|
||
headers.times do # Read each header
|
||
name=AMF.getstring(req) # |
|
||
req.getc # | skip boolean
|
||
value=AMF.getvalue(req) # |
|
||
header["name"]=value # |
|
||
end
|
||
|
||
bodies=AMF.getint(req) # Read number of bodies
|
||
bodies.times do # Read each body
|
||
message=AMF.getstring(req) # | get message name
|
||
index=AMF.getstring(req) # | get index in response sequence
|
||
bytes=AMF.getlong(req) # | get total size in bytes
|
||
args=AMF.getvalue(req) # | get response (probably an array)
|
||
logger.info("Executing AMF #{message}(#{args.join(',')}):#{index}")
|
||
|
||
case message
|
||
when 'getpresets'; results[index]=AMF.putdata(index,getpresets(I18n.locale))
|
||
when 'whichways'; results[index]=AMF.putdata(index,whichways(*args))
|
||
when 'whichways_deleted'; results[index]=AMF.putdata(index,whichways_deleted(*args))
|
||
when 'getway'; results[index]=AMF.putdata(index,getway(args[0].to_i))
|
||
when 'getrelation'; results[index]=AMF.putdata(index,getrelation(args[0].to_i))
|
||
when 'getway_old'; results[index]=AMF.putdata(index,getway_old(args[0].to_i,args[1]))
|
||
when 'getway_history'; results[index]=AMF.putdata(index,getway_history(args[0].to_i))
|
||
when 'getnode_history'; results[index]=AMF.putdata(index,getnode_history(args[0].to_i))
|
||
when 'findgpx'; results[index]=AMF.putdata(index,findgpx(*args))
|
||
when 'findrelations'; results[index]=AMF.putdata(index,findrelations(*args))
|
||
when 'getpoi'; results[index]=AMF.putdata(index,getpoi(*args))
|
||
end
|
||
end
|
||
logger.info("Encoding AMF results")
|
||
sendresponse(results)
|
||
else
|
||
render :nothing => true, :status => :method_not_allowed
|
||
end
|
||
end
|
||
|
||
def amf_write
|
||
if request.post?
|
||
req=StringIO.new(request.raw_post+0.chr)
|
||
req.read(2)
|
||
results={}
|
||
renumberednodes={} # Shared across repeated putways
|
||
renumberedways={} # Shared across repeated putways
|
||
|
||
headers=AMF.getint(req) # Read number of headers
|
||
headers.times do # Read each header
|
||
name=AMF.getstring(req) # |
|
||
req.getc # | skip boolean
|
||
value=AMF.getvalue(req) # |
|
||
header["name"]=value # |
|
||
end
|
||
|
||
bodies=AMF.getint(req) # Read number of bodies
|
||
bodies.times do # Read each body
|
||
message=AMF.getstring(req) # | get message name
|
||
index=AMF.getstring(req) # | get index in response sequence
|
||
bytes=AMF.getlong(req) # | get total size in bytes
|
||
args=AMF.getvalue(req) # | get response (probably an array)
|
||
err=false # Abort batch on error
|
||
|
||
logger.info("Executing AMF #{message}:#{index}")
|
||
if err
|
||
results[index]=[-5,nil]
|
||
else
|
||
case message
|
||
when 'putway'; orn=renumberednodes.dup
|
||
r=putway(renumberednodes,*args)
|
||
r[4]=renumberednodes.reject { |k,v| orn.has_key?(k) }
|
||
if r[2] != r[3] then renumberedways[r[2]] = r[3] end
|
||
results[index]=AMF.putdata(index,r)
|
||
when 'putrelation'; results[index]=AMF.putdata(index,putrelation(renumberednodes, renumberedways, *args))
|
||
when 'deleteway'; results[index]=AMF.putdata(index,deleteway(*args))
|
||
when 'putpoi'; r=putpoi(*args)
|
||
if r[2] != r[3] then renumberednodes[r[2]] = r[3] end
|
||
results[index]=AMF.putdata(index,r)
|
||
when 'startchangeset'; results[index]=AMF.putdata(index,startchangeset(*args))
|
||
end
|
||
if results[index][0]==-3 then err=true end # If a conflict is detected, don't execute any more writes
|
||
end
|
||
end
|
||
logger.info("Encoding AMF results")
|
||
sendresponse(results)
|
||
else
|
||
render :nothing => true, :status => :method_not_allowed
|
||
end
|
||
end
|
||
|
||
private
|
||
|
||
def amf_handle_error(call)
|
||
yield
|
||
rescue OSM::APIVersionMismatchError => ex
|
||
return [-3, [ex.type.downcase, ex.latest, ex.id]]
|
||
rescue OSM::APIUserChangesetMismatchError => ex
|
||
return [-2, ex.to_s]
|
||
rescue OSM::APIBadBoundingBox => ex
|
||
return [-2, "Sorry - I can't get the map for that area. The server said: #{ex.to_s}"]
|
||
rescue OSM::APIError => ex
|
||
return [-1, ex.to_s]
|
||
rescue Exception => ex
|
||
return [-2, "An unusual error happened (in #{call}). The server said: #{ex.to_s}"]
|
||
end
|
||
|
||
def amf_handle_error_with_timeout(call)
|
||
amf_handle_error(call) do
|
||
Timeout::timeout(APP_CONFIG['api_timeout'], OSM::APITimeoutError) do
|
||
yield
|
||
end
|
||
end
|
||
end
|
||
|
||
# Start new changeset
|
||
# Returns success_code,success_message,changeset id
|
||
|
||
def startchangeset(usertoken, cstags, closeid, closecomment, opennew)
|
||
amf_handle_error("'startchangeset'") do
|
||
user = getuser(usertoken)
|
||
if !user then return -1,"You are not logged in, so Potlatch can't write any changes to the database." end
|
||
|
||
# close previous changeset and add comment
|
||
if closeid
|
||
cs = Changeset.find(closeid)
|
||
cs.set_closed_time_now
|
||
if cs.user_id!=user.id
|
||
raise OSM::APIUserChangesetMismatchError.new
|
||
elsif closecomment.empty?
|
||
cs.save!
|
||
else
|
||
cs.tags['comment']=closecomment
|
||
cs.save_with_tags!
|
||
end
|
||
end
|
||
|
||
# open a new changeset
|
||
if opennew!=0
|
||
cs = Changeset.new
|
||
cs.tags = cstags
|
||
cs.user_id = user.id
|
||
if !closecomment.empty? then cs.tags['comment']=closecomment end
|
||
# smsm1 doesn't like the next two lines and thinks they need to be abstracted to the model more/better
|
||
cs.created_at = Time.now.getutc
|
||
cs.closed_at = cs.created_at + Changeset::IDLE_TIMEOUT
|
||
cs.save_with_tags!
|
||
return [0,'',cs.id]
|
||
else
|
||
return [0,'',nil]
|
||
end
|
||
end
|
||
end
|
||
|
||
# Return presets (default tags, localisation etc.):
|
||
# uses POTLATCH_PRESETS global, set up in OSM::Potlatch.
|
||
|
||
def getpresets(lang) #:doc:
|
||
begin
|
||
logger.info("Loading Potlatch/#{lang} localisation")
|
||
localised = YAML::load(File.open("#{RAILS_ROOT}/config/potlatch/localised/#{lang}/localised.yaml"))
|
||
rescue
|
||
logger.info("Loading Potlatch/#{lang} localisation failed, using English defaults")
|
||
localised = "" # guess we'll just have to use the hardcoded English text instead
|
||
end
|
||
|
||
begin
|
||
help = File.read("#{RAILS_ROOT}/config/potlatch/localised/#{lang}/help.html")
|
||
rescue
|
||
help = File.read("#{RAILS_ROOT}/config/potlatch/localised/en/help.html")
|
||
end
|
||
return POTLATCH_PRESETS+[localised,help]
|
||
end
|
||
|
||
##
|
||
# Find all the ways, POI nodes (i.e. not part of ways), and relations
|
||
# in a given bounding box. Nodes are returned in full; ways and relations
|
||
# are IDs only.
|
||
#
|
||
# return is of the form:
|
||
# [success_code, success_message,
|
||
# [[way_id, way_version], ...],
|
||
# [[node_id, lat, lon, [tags, ...], node_version], ...],
|
||
# [[rel_id, rel_version], ...]]
|
||
# where the ways are any visible ways which refer to any visible
|
||
# nodes in the bbox, nodes are any visible nodes in the bbox but not
|
||
# used in any way, rel is any relation which refers to either a way
|
||
# or node that we're returning.
|
||
def whichways(xmin, ymin, xmax, ymax) #:doc:
|
||
amf_handle_error_with_timeout("'whichways'") do
|
||
enlarge = [(xmax-xmin)/8,0.01].min
|
||
xmin -= enlarge; ymin -= enlarge
|
||
xmax += enlarge; ymax += enlarge
|
||
|
||
# check boundary is sane and area within defined
|
||
# see /config/application.yml
|
||
check_boundaries(xmin, ymin, xmax, ymax)
|
||
|
||
if POTLATCH_USE_SQL then
|
||
ways = sql_find_ways_in_area(xmin, ymin, xmax, ymax)
|
||
points = sql_find_pois_in_area(xmin, ymin, xmax, ymax)
|
||
relations = sql_find_relations_in_area_and_ways(xmin, ymin, xmax, ymax, ways.collect {|x| x[0]})
|
||
else
|
||
# find the way ids in an area
|
||
nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => ["current_nodes.visible = ?", true], :include => :ways)
|
||
ways = nodes_in_area.inject([]) { |sum, node|
|
||
visible_ways = node.ways.select { |w| w.visible? }
|
||
sum + visible_ways.collect { |w| [w.id,w.version] }
|
||
}.uniq
|
||
ways.delete([])
|
||
|
||
# find the node ids in an area that aren't part of ways
|
||
nodes_not_used_in_area = nodes_in_area.select { |node| node.ways.empty? }
|
||
points = nodes_not_used_in_area.collect { |n| [n.id, n.lon, n.lat, n.tags, n.version] }.uniq
|
||
|
||
# find the relations used by those nodes and ways
|
||
relations = Relation.find_for_nodes(nodes_in_area.collect { |n| n.id }, :conditions => {:visible => true}) +
|
||
Relation.find_for_ways(ways.collect { |w| w[0] }, :conditions => {:visible => true})
|
||
relations = relations.collect { |relation| [relation.id,relation.version] }.uniq
|
||
end
|
||
|
||
[0, '', ways, points, relations]
|
||
end
|
||
end
|
||
|
||
# Find deleted ways in current bounding box (similar to whichways, but ways
|
||
# with a deleted node only - not POIs or relations).
|
||
|
||
def whichways_deleted(xmin, ymin, xmax, ymax) #:doc:
|
||
amf_handle_error_with_timeout("'whichways_deleted'") do
|
||
enlarge = [(xmax-xmin)/8,0.01].min
|
||
xmin -= enlarge; ymin -= enlarge
|
||
xmax += enlarge; ymax += enlarge
|
||
|
||
# check boundary is sane and area within defined
|
||
# see /config/application.yml
|
||
check_boundaries(xmin, ymin, xmax, ymax)
|
||
|
||
nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => ["current_ways.visible = ?", false], :include => :ways_via_history)
|
||
way_ids = nodes_in_area.collect { |node| node.ways_via_history_ids }.flatten.uniq
|
||
|
||
[0,'',way_ids]
|
||
end
|
||
end
|
||
|
||
# Get a way including nodes and tags.
|
||
# Returns the way id, a Potlatch-style array of points, a hash of tags, the version number, and the user ID.
|
||
|
||
def getway(wayid) #:doc:
|
||
amf_handle_error_with_timeout("'getway' #{wayid}") do
|
||
if POTLATCH_USE_SQL then
|
||
points = sql_get_nodes_in_way(wayid)
|
||
tags = sql_get_tags_in_way(wayid)
|
||
version = sql_get_way_version(wayid)
|
||
uid = sql_get_way_user(wayid)
|
||
else
|
||
# Ideally we would do ":include => :nodes" here but if we do that
|
||
# then rails only seems to return the first copy of a node when a
|
||
# way includes a node more than once
|
||
way = Way.find(:first, :conditions => { :id => wayid }, :include => { :nodes => :node_tags })
|
||
|
||
# check case where way has been deleted or doesn't exist
|
||
return [-4, 'way', wayid, [], {}, nil] if way.nil? or !way.visible
|
||
|
||
points = way.nodes.collect do |node|
|
||
nodetags=node.tags
|
||
nodetags.delete('created_by')
|
||
[node.lon, node.lat, node.id, nodetags, node.version]
|
||
end
|
||
tags = way.tags
|
||
version = way.version
|
||
uid = way.changeset.user.id
|
||
end
|
||
|
||
[0, '', wayid, points, tags, version, uid]
|
||
end
|
||
end
|
||
|
||
# Get an old version of a way, and all constituent nodes.
|
||
#
|
||
# For undelete (version<0), always uses the most recent version of each node,
|
||
# even if it's moved. For revert (version >= 0), uses the node in existence
|
||
# at the time, generating a new id if it's still visible and has been moved/
|
||
# retagged.
|
||
#
|
||
# Returns:
|
||
# 0. success code,
|
||
# 1. id,
|
||
# 2. array of points,
|
||
# 3. hash of tags,
|
||
# 4. version,
|
||
# 5. is this the current, visible version? (boolean)
|
||
|
||
def getway_old(id, timestamp) #:doc:
|
||
amf_handle_error_with_timeout("'getway_old' #{id}, #{timestamp}") do
|
||
if timestamp == ''
|
||
# undelete
|
||
old_way = OldWay.find(:first, :conditions => ['visible = ? AND id = ?', true, id], :order => 'version DESC')
|
||
points = old_way.get_nodes_undelete unless old_way.nil?
|
||
else
|
||
begin
|
||
# revert
|
||
timestamp = DateTime.strptime(timestamp.to_s, "%d %b %Y, %H:%M:%S")
|
||
old_way = OldWay.find(:first, :conditions => ['id = ? AND timestamp <= ?', id, timestamp], :order => 'timestamp DESC')
|
||
unless old_way.nil?
|
||
points = old_way.get_nodes_revert(timestamp)
|
||
if !old_way.visible
|
||
return [-1, "Sorry, the way was deleted at that time - please revert to a previous version.", id, [], {}, nil, false]
|
||
end
|
||
end
|
||
rescue ArgumentError
|
||
# thrown by date parsing method. leave old_way as nil for
|
||
# the error handler below.
|
||
end
|
||
end
|
||
|
||
if old_way.nil?
|
||
return [-1, "Sorry, the server could not find a way at that time.", id, [], {}, nil, false]
|
||
else
|
||
curway=Way.find(id)
|
||
old_way.tags['history'] = "Retrieved from v#{old_way.version}"
|
||
return [0, '', id, points, old_way.tags, curway.version, (curway.version==old_way.version and curway.visible)]
|
||
end
|
||
end
|
||
end
|
||
|
||
# Find history of a way.
|
||
# Returns 'way', id, and an array of previous versions:
|
||
# - formerly [old_way.version, old_way.timestamp.strftime("%d %b %Y, %H:%M"), old_way.visible ? 1 : 0, user, uid]
|
||
# - now [timestamp,user,uid]
|
||
#
|
||
# Heuristic: Find all nodes that have ever been part of the way;
|
||
# get a list of their revision dates; add revision dates of the way;
|
||
# sort and collapse list (to within 2 seconds); trim all dates before the
|
||
# start date of the way.
|
||
|
||
def getway_history(wayid) #:doc:
|
||
begin
|
||
# Find list of revision dates for way and all constituent nodes
|
||
revdates=[]
|
||
revusers={}
|
||
Way.find(wayid).old_ways.collect do |a|
|
||
revdates.push(a.timestamp)
|
||
unless revusers.has_key?(a.timestamp.to_i) then revusers[a.timestamp.to_i]=change_user(a) end
|
||
a.nds.each do |n|
|
||
Node.find(n).old_nodes.collect do |o|
|
||
revdates.push(o.timestamp)
|
||
unless revusers.has_key?(o.timestamp.to_i) then revusers[o.timestamp.to_i]=change_user(o) end
|
||
end
|
||
end
|
||
end
|
||
waycreated=revdates[0]
|
||
revdates.uniq!
|
||
revdates.sort!
|
||
revdates.reverse!
|
||
|
||
# Remove any dates (from nodes) before first revision date of way
|
||
revdates.delete_if { |d| d<waycreated }
|
||
# Remove any elements where 2 seconds doesn't elapse before next one
|
||
revdates.delete_if { |d| revdates.include?(d+1) or revdates.include?(d+2) }
|
||
# Collect all in one nested array
|
||
revdates.collect! {|d| [d.succ.strftime("%d %b %Y, %H:%M:%S")] + revusers[d.to_i] }
|
||
revdates.uniq!
|
||
|
||
return ['way', wayid, revdates]
|
||
rescue ActiveRecord::RecordNotFound
|
||
return ['way', wayid, []]
|
||
end
|
||
end
|
||
|
||
# Find history of a node. Returns 'node', id, and an array of previous versions as above.
|
||
|
||
def getnode_history(nodeid) #:doc:
|
||
begin
|
||
history = Node.find(nodeid).old_nodes.reverse.collect do |old_node|
|
||
[old_node.timestamp.succ.strftime("%d %b %Y, %H:%M:%S")] + change_user(old_node)
|
||
end
|
||
return ['node', nodeid, history]
|
||
rescue ActiveRecord::RecordNotFound
|
||
return ['node', nodeid, []]
|
||
end
|
||
end
|
||
|
||
def change_user(obj)
|
||
user_object = obj.changeset.user
|
||
user = user_object.data_public? ? user_object.display_name : 'anonymous'
|
||
uid = user_object.data_public? ? user_object.id : 0
|
||
[user,uid]
|
||
end
|
||
|
||
# Find GPS traces with specified name/id.
|
||
# Returns array listing GPXs, each one comprising id, name and description.
|
||
|
||
def findgpx(searchterm, usertoken)
|
||
amf_handle_error_with_timeout("'findgpx'") do
|
||
user = getuser(usertoken)
|
||
if !uid then return -1,"You must be logged in to search for GPX traces.",[] end
|
||
|
||
gpxs = []
|
||
if searchterm.to_i>0 then
|
||
gpx = Trace.find(searchterm.to_i, :conditions => ["visible=? AND (public=? OR user_id=?)",true,true,user.id] )
|
||
if gpx then
|
||
gpxs.push([gpx.id, gpx.name, gpx.description])
|
||
end
|
||
else
|
||
Trace.find(:all, :limit => 21, :conditions => ["visible=? AND (public=? OR user_id=?) AND MATCH(name) AGAINST (?)",true,true,user.id,searchterm] ).each do |gpx|
|
||
gpxs.push([gpx.id, gpx.name, gpx.description])
|
||
end
|
||
end
|
||
[0,'',gpxs]
|
||
end
|
||
end
|
||
|
||
# Get a relation with all tags and members.
|
||
# Returns:
|
||
# 0. success code?
|
||
# 1. object type?
|
||
# 2. relation id,
|
||
# 3. hash of tags,
|
||
# 4. list of members,
|
||
# 5. version.
|
||
|
||
def getrelation(relid) #:doc:
|
||
amf_handle_error("'getrelation' #{relid}") do
|
||
rel = Relation.find(:first, :conditions => { :id => relid })
|
||
|
||
return [-4, 'relation', relid, {}, [], nil] if rel.nil? or !rel.visible
|
||
[0, '', relid, rel.tags, rel.members, rel.version]
|
||
end
|
||
end
|
||
|
||
# Find relations with specified name/id.
|
||
# Returns array of relations, each in same form as getrelation.
|
||
|
||
def findrelations(searchterm)
|
||
rels = []
|
||
if searchterm.to_i>0 then
|
||
rel = Relation.find(searchterm.to_i)
|
||
if rel and rel.visible then
|
||
rels.push([rel.id, rel.tags, rel.members, rel.version])
|
||
end
|
||
else
|
||
RelationTag.find(:all, :limit => 11, :conditions => ["v like ?", "%#{searchterm}%"] ).each do |t|
|
||
if t.relation.visible then
|
||
rels.push([t.relation.id, t.relation.tags, t.relation.members, t.relation.version])
|
||
end
|
||
end
|
||
end
|
||
rels
|
||
end
|
||
|
||
# Save a relation.
|
||
# Returns
|
||
# 0. 0 (success),
|
||
# 1. original relation id (unchanged),
|
||
# 2. new relation id,
|
||
# 3. version.
|
||
|
||
def putrelation(renumberednodes, renumberedways, usertoken, changeset_id, version, relid, tags, members, visible) #:doc:
|
||
amf_handle_error("'putrelation' #{relid}") do
|
||
user = getuser(usertoken)
|
||
if !user then return -1,"You are not logged in, so the relation could not be saved." end
|
||
|
||
relid = relid.to_i
|
||
visible = (visible.to_i != 0)
|
||
|
||
new_relation = nil
|
||
relation = nil
|
||
Relation.transaction do
|
||
# create a new relation, or find the existing one
|
||
if relid > 0
|
||
relation = Relation.find(relid)
|
||
end
|
||
# We always need a new node, based on the data that has been sent to us
|
||
new_relation = Relation.new
|
||
|
||
# check the members are all positive, and correctly type
|
||
typedmembers = []
|
||
members.each do |m|
|
||
mid = m[1].to_i
|
||
if mid < 0
|
||
mid = renumberednodes[mid] if m[0] == 'Node'
|
||
mid = renumberedways[mid] if m[0] == 'Way'
|
||
end
|
||
if mid
|
||
typedmembers << [m[0], mid, m[2]]
|
||
end
|
||
end
|
||
|
||
# assign new contents
|
||
new_relation.members = typedmembers
|
||
new_relation.tags = tags
|
||
new_relation.visible = visible
|
||
new_relation.changeset_id = changeset_id
|
||
new_relation.version = version
|
||
|
||
if relid <= 0
|
||
# We're creating the relation
|
||
new_relation.create_with_history(user)
|
||
elsif visible
|
||
# We're updating the relation
|
||
new_relation.id = relid
|
||
relation.update_from(new_relation, user)
|
||
else
|
||
# We're deleting the relation
|
||
new_relation.id = relid
|
||
relation.delete_with_history!(new_relation, user)
|
||
end
|
||
end # transaction
|
||
|
||
if relid <= 0
|
||
return [0, '', relid, new_relation.id, new_relation.version]
|
||
else
|
||
return [0, '', relid, relid, relation.version]
|
||
end
|
||
end
|
||
end
|
||
|
||
# Save a way to the database, including all nodes. Any nodes in the previous
|
||
# version and no longer used are deleted.
|
||
#
|
||
# Parameters:
|
||
# 0. hash of renumbered nodes (added by amf_controller)
|
||
# 1. current user token (for authentication)
|
||
# 2. current changeset
|
||
# 3. new way version
|
||
# 4. way ID
|
||
# 5. list of nodes in way
|
||
# 6. hash of way tags
|
||
# 7. array of nodes to change (each one is [lon,lat,id,version,tags]),
|
||
# 8. hash of nodes to delete (id->version).
|
||
#
|
||
# Returns:
|
||
# 0. '0' (code for success),
|
||
# 1. message,
|
||
# 2. original way id (unchanged),
|
||
# 3. new way id,
|
||
# 4. hash of renumbered nodes (old id=>new id),
|
||
# 5. way version,
|
||
# 6. hash of node versions (node=>version)
|
||
|
||
def putway(renumberednodes, usertoken, changeset_id, wayversion, originalway, pointlist, attributes, nodes, deletednodes) #:doc:
|
||
amf_handle_error("'putway' #{originalway}") do
|
||
# -- Initialise
|
||
|
||
user = getuser(usertoken)
|
||
if !user then return -1,"You are not logged in, so the way could not be saved." end
|
||
if pointlist.length < 2 then return -2,"Server error - way is only #{points.length} points long." end
|
||
|
||
originalway = originalway.to_i
|
||
pointlist.collect! {|a| a.to_i }
|
||
|
||
way=nil # this is returned, so scope it outside the transaction
|
||
nodeversions = {}
|
||
Way.transaction do
|
||
|
||
# -- Update each changed node
|
||
|
||
nodes.each do |a|
|
||
lon = a[0].to_f
|
||
lat = a[1].to_f
|
||
id = a[2].to_i
|
||
version = a[3].to_i
|
||
if id == 0 then return -2,"Server error - node with id 0 found in way #{originalway}." end
|
||
if lat== 90 then return -2,"Server error - node with latitude -90 found in way #{originalway}." end
|
||
if renumberednodes[id] then id = renumberednodes[id] end
|
||
|
||
node = Node.new
|
||
node.changeset_id = changeset_id
|
||
node.lat = lat
|
||
node.lon = lon
|
||
node.tags = a[4]
|
||
node.tags.delete('created_by')
|
||
node.version = version
|
||
if id <= 0
|
||
# We're creating the node
|
||
node.create_with_history(user)
|
||
renumberednodes[id] = node.id
|
||
nodeversions[node.id] = node.version
|
||
else
|
||
# We're updating an existing node
|
||
previous=Node.find(id)
|
||
node.id=id
|
||
previous.update_from(node, user)
|
||
nodeversions[previous.id] = previous.version
|
||
end
|
||
end
|
||
|
||
# -- Save revised way
|
||
|
||
pointlist.collect! {|a|
|
||
renumberednodes[a] ? renumberednodes[a]:a
|
||
} # renumber nodes
|
||
new_way = Way.new
|
||
new_way.tags = attributes
|
||
new_way.nds = pointlist
|
||
new_way.changeset_id = changeset_id
|
||
new_way.version = wayversion
|
||
if originalway <= 0
|
||
new_way.create_with_history(user)
|
||
way=new_way # so we can get way.id and way.version
|
||
else
|
||
way = Way.find(originalway)
|
||
if way.tags!=attributes or way.nds!=pointlist or !way.visible?
|
||
new_way.id=originalway
|
||
way.update_from(new_way, user)
|
||
end
|
||
end
|
||
|
||
# -- Delete unwanted nodes
|
||
|
||
deletednodes.each do |id,v|
|
||
node = Node.find(id.to_i)
|
||
new_node = Node.new
|
||
new_node.changeset_id = changeset_id
|
||
new_node.version = v.to_i
|
||
new_node.id = id.to_i
|
||
begin
|
||
node.delete_with_history!(new_node, user)
|
||
rescue OSM::APIPreconditionFailedError => ex
|
||
# We don't do anything here as the node is being used elsewhere
|
||
# and we don't want to delete it
|
||
end
|
||
end
|
||
|
||
end # transaction
|
||
|
||
[0, '', originalway, way.id, renumberednodes, way.version, nodeversions, deletednodes]
|
||
end
|
||
end
|
||
|
||
# Save POI to the database.
|
||
# Refuses save if the node has since become part of a way.
|
||
# Returns array with:
|
||
# 0. 0 (success),
|
||
# 1. success message,
|
||
# 2. original node id (unchanged),
|
||
# 3. new node id,
|
||
# 4. version.
|
||
|
||
def putpoi(usertoken, changeset_id, version, id, lon, lat, tags, visible) #:doc:
|
||
amf_handle_error("'putpoi' #{id}") do
|
||
user = getuser(usertoken)
|
||
if !user then return -1,"You are not logged in, so the point could not be saved." end
|
||
|
||
id = id.to_i
|
||
visible = (visible.to_i == 1)
|
||
node = nil
|
||
new_node = nil
|
||
Node.transaction do
|
||
if id > 0 then
|
||
node = Node.find(id)
|
||
|
||
if !visible then
|
||
unless node.ways.empty? then return -1,"The point has since become part of a way, so you cannot save it as a POI.",id,id,version end
|
||
end
|
||
end
|
||
# We always need a new node, based on the data that has been sent to us
|
||
new_node = Node.new
|
||
|
||
new_node.changeset_id = changeset_id
|
||
new_node.version = version
|
||
new_node.lat = lat
|
||
new_node.lon = lon
|
||
new_node.tags = tags
|
||
if id <= 0
|
||
# We're creating the node
|
||
new_node.create_with_history(user)
|
||
elsif visible
|
||
# We're updating the node
|
||
new_node.id=id
|
||
node.update_from(new_node, user)
|
||
else
|
||
# We're deleting the node
|
||
new_node.id=id
|
||
node.delete_with_history!(new_node, user)
|
||
end
|
||
|
||
end # transaction
|
||
|
||
if id <= 0
|
||
return [0, '', id, new_node.id, new_node.version]
|
||
else
|
||
return [0, '', id, node.id, node.version]
|
||
end
|
||
end
|
||
end
|
||
|
||
# Read POI from database
|
||
# (only called on revert: POIs are usually read by whichways).
|
||
#
|
||
# Returns array of id, long, lat, hash of tags, (current) version.
|
||
|
||
def getpoi(id,timestamp) #:doc:
|
||
amf_handle_error("'getpoi' #{id}") do
|
||
id = id.to_i
|
||
n = Node.find(id)
|
||
v = n.version
|
||
unless timestamp == ''
|
||
n = OldNode.find(:first, :conditions => ['id = ? AND timestamp <= ?', id, timestamp], :order => 'timestamp DESC')
|
||
end
|
||
|
||
if n
|
||
return [0, '', n.id, n.lon, n.lat, n.tags, v]
|
||
else
|
||
return [-4, 'node', id, nil, nil, {}, nil]
|
||
end
|
||
end
|
||
end
|
||
|
||
# Delete way and all constituent nodes.
|
||
# Params:
|
||
# * The user token
|
||
# * the changeset id
|
||
# * the id of the way to change
|
||
# * the version of the way that was downloaded
|
||
# * a hash of the id and versions of all the nodes that are in the way, if any
|
||
# of the nodes have been changed by someone else then, there is a problem!
|
||
# Returns 0 (success), unchanged way id, new way version, new node versions.
|
||
|
||
def deleteway(usertoken, changeset_id, way_id, way_version, deletednodes) #:doc:
|
||
amf_handle_error("'deleteway' #{way_id}") do
|
||
user = getuser(usertoken)
|
||
unless user then return -1,"You are not logged in, so the way could not be deleted." end
|
||
|
||
way_id = way_id.to_i
|
||
nodeversions = {}
|
||
old_way=nil # returned, so scope it outside the transaction
|
||
# Need a transaction so that if one item fails to delete, the whole delete fails.
|
||
Way.transaction do
|
||
|
||
# -- Delete the way
|
||
|
||
old_way = Way.find(way_id)
|
||
delete_way = Way.new
|
||
delete_way.version = way_version
|
||
delete_way.changeset_id = changeset_id
|
||
delete_way.id = way_id
|
||
old_way.delete_with_history!(delete_way, user)
|
||
|
||
# -- Delete unwanted nodes
|
||
|
||
deletednodes.each do |id,v|
|
||
node = Node.find(id.to_i)
|
||
new_node = Node.new
|
||
new_node.changeset_id = changeset_id
|
||
new_node.version = v.to_i
|
||
new_node.id = id.to_i
|
||
begin
|
||
node.delete_with_history!(new_node, user)
|
||
nodeversions[node.id]=node.version
|
||
rescue OSM::APIPreconditionFailedError => ex
|
||
# We don't do anything with the exception as the node is in use
|
||
# elsewhere and we don't want to delete it
|
||
end
|
||
end
|
||
|
||
end # transaction
|
||
[0, '', way_id, old_way.version, nodeversions]
|
||
end
|
||
end
|
||
|
||
|
||
# ====================================================================
|
||
# Support functions
|
||
|
||
# Authenticate token
|
||
# (can also be of form user:pass)
|
||
# When we are writing to the api, we need the actual user model,
|
||
# not just the id, hence this abstraction
|
||
|
||
def getuser(token) #:doc:
|
||
if (token =~ /^(.+)\:(.+)$/) then
|
||
user = User.authenticate(:username => $1, :password => $2)
|
||
else
|
||
user = User.authenticate(:token => token)
|
||
end
|
||
return user
|
||
end
|
||
|
||
# Send AMF response
|
||
|
||
def sendresponse(results)
|
||
a,b=results.length.divmod(256)
|
||
render :content_type => "application/x-amf", :text => proc { |response, output|
|
||
# ** move amf writing loop into here -
|
||
# basically we read the messages in first (into an array of some sort),
|
||
# then iterate through that array within here, and do all the AMF writing
|
||
output.write 0.chr+0.chr+0.chr+0.chr+a.chr+b.chr
|
||
results.each do |k,v|
|
||
output.write(v)
|
||
end
|
||
}
|
||
end
|
||
|
||
|
||
# ====================================================================
|
||
# Alternative SQL queries for getway/whichways
|
||
|
||
def sql_find_ways_in_area(xmin,ymin,xmax,ymax)
|
||
sql=<<-EOF
|
||
SELECT DISTINCT current_ways.id AS wayid,current_ways.version AS version
|
||
FROM current_way_nodes
|
||
INNER JOIN current_nodes ON current_nodes.id=current_way_nodes.node_id
|
||
INNER JOIN current_ways ON current_ways.id =current_way_nodes.id
|
||
WHERE current_nodes.visible=TRUE
|
||
AND current_ways.visible=TRUE
|
||
AND #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")}
|
||
EOF
|
||
return ActiveRecord::Base.connection.select_all(sql).collect { |a| [a['wayid'].to_i,a['version'].to_i] }
|
||
end
|
||
|
||
def sql_find_pois_in_area(xmin,ymin,xmax,ymax)
|
||
pois=[]
|
||
sql=<<-EOF
|
||
SELECT current_nodes.id,current_nodes.latitude*0.0000001 AS lat,current_nodes.longitude*0.0000001 AS lon,current_nodes.version
|
||
FROM current_nodes
|
||
LEFT OUTER JOIN current_way_nodes cwn ON cwn.node_id=current_nodes.id
|
||
WHERE current_nodes.visible=TRUE
|
||
AND cwn.id IS NULL
|
||
AND #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")}
|
||
EOF
|
||
ActiveRecord::Base.connection.select_all(sql).each do |row|
|
||
poitags={}
|
||
ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_node_tags WHERE id=#{row['id']}").each do |n|
|
||
poitags[n['k']]=n['v']
|
||
end
|
||
pois << [row['id'].to_i, row['lon'].to_f, row['lat'].to_f, poitags, row['version'].to_i]
|
||
end
|
||
pois
|
||
end
|
||
|
||
def sql_find_relations_in_area_and_ways(xmin,ymin,xmax,ymax,way_ids)
|
||
# ** It would be more Potlatchy to get relations for nodes within ways
|
||
# during 'getway', not here
|
||
sql=<<-EOF
|
||
SELECT DISTINCT cr.id AS relid,cr.version AS version
|
||
FROM current_relations cr
|
||
INNER JOIN current_relation_members crm ON crm.id=cr.id
|
||
INNER JOIN current_nodes cn ON crm.member_id=cn.id AND crm.member_type='Node'
|
||
WHERE #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "cn.")}
|
||
EOF
|
||
unless way_ids.empty?
|
||
sql+=<<-EOF
|
||
UNION
|
||
SELECT DISTINCT cr.id AS relid,cr.version AS version
|
||
FROM current_relations cr
|
||
INNER JOIN current_relation_members crm ON crm.id=cr.id
|
||
WHERE crm.member_type='Way'
|
||
AND crm.member_id IN (#{way_ids.join(',')})
|
||
EOF
|
||
end
|
||
ActiveRecord::Base.connection.select_all(sql).collect { |a| [a['relid'].to_i,a['version'].to_i] }
|
||
end
|
||
|
||
def sql_get_nodes_in_way(wayid)
|
||
points=[]
|
||
sql=<<-EOF
|
||
SELECT latitude*0.0000001 AS lat,longitude*0.0000001 AS lon,current_nodes.id,current_nodes.version
|
||
FROM current_way_nodes,current_nodes
|
||
WHERE current_way_nodes.id=#{wayid.to_i}
|
||
AND current_way_nodes.node_id=current_nodes.id
|
||
AND current_nodes.visible=TRUE
|
||
ORDER BY sequence_id
|
||
EOF
|
||
ActiveRecord::Base.connection.select_all(sql).each do |row|
|
||
nodetags={}
|
||
ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_node_tags WHERE id=#{row['id']}").each do |n|
|
||
nodetags[n['k']]=n['v']
|
||
end
|
||
nodetags.delete('created_by')
|
||
points << [row['lon'].to_f,row['lat'].to_f,row['id'].to_i,nodetags,row['version'].to_i]
|
||
end
|
||
points
|
||
end
|
||
|
||
def sql_get_tags_in_way(wayid)
|
||
tags={}
|
||
ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_way_tags WHERE id=#{wayid.to_i}").each do |row|
|
||
tags[row['k']]=row['v']
|
||
end
|
||
tags
|
||
end
|
||
|
||
def sql_get_way_version(wayid)
|
||
ActiveRecord::Base.connection.select_one("SELECT version FROM current_ways WHERE id=#{wayid.to_i}")['version']
|
||
end
|
||
|
||
def sql_get_way_user(wayid)
|
||
ActiveRecord::Base.connection.select_one("SELECT user FROM current_ways,changesets WHERE current_ways.id=#{wayid.to_i} AND current_ways.changeset=changesets.id")['user']
|
||
end
|
||
end
|
||
|