Although it's not entirely clear how to interpret a timestamp from a client the production servers have effectively been parsing them as UTC because they run in the UTC time zone all year. Using Time.zone to do the parse forces it to happen in the rails time zone rather than the system time zone, which defaults to UTC as well. Fixes #1688
1001 lines
34 KiB
Ruby
1001 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
|
|
include Potlatch
|
|
|
|
skip_before_action :verify_authenticity_token
|
|
before_action :check_api_writable
|
|
|
|
# Main AMF handlers: process the raw AMF string (using AMF library) and
|
|
# calls each action (private method) accordingly.
|
|
|
|
def amf_read
|
|
self.status = :ok
|
|
self.content_type = Mime[:amf]
|
|
self.response_body = Dispatcher.new(request.raw_post) do |message, *args|
|
|
logger.info("Executing AMF #{message}(#{args.join(',')})")
|
|
|
|
case message
|
|
when "getpresets" then result = getpresets(*args)
|
|
when "whichways" then result = whichways(*args)
|
|
when "whichways_deleted" then result = whichways_deleted(*args)
|
|
when "getway" then result = getway(args[0].to_i)
|
|
when "getrelation" then result = getrelation(args[0].to_i)
|
|
when "getway_old" then result = getway_old(args[0].to_i, args[1])
|
|
when "getway_history" then result = getway_history(args[0].to_i)
|
|
when "getnode_history" then result = getnode_history(args[0].to_i)
|
|
when "findgpx" then result = findgpx(*args)
|
|
when "findrelations" then result = findrelations(*args)
|
|
when "getpoi" then result = getpoi(*args)
|
|
end
|
|
|
|
result
|
|
end
|
|
end
|
|
|
|
def amf_write
|
|
renumberednodes = {} # Shared across repeated putways
|
|
renumberedways = {} # Shared across repeated putways
|
|
err = false # Abort batch on error
|
|
|
|
self.status = :ok
|
|
self.content_type = Mime[:amf]
|
|
self.response_body = Dispatcher.new(request.raw_post) do |message, *args|
|
|
logger.info("Executing AMF #{message}")
|
|
|
|
if err
|
|
result = [-5, nil]
|
|
else
|
|
case message
|
|
when "putway" then
|
|
orn = renumberednodes.dup
|
|
result = putway(renumberednodes, *args)
|
|
result[4] = renumberednodes.reject { |k, _v| orn.key?(k) }
|
|
renumberedways[result[2]] = result[3] if result[0].zero? && result[2] != result[3]
|
|
when "putrelation" then
|
|
result = putrelation(renumberednodes, renumberedways, *args)
|
|
when "deleteway" then
|
|
result = deleteway(*args)
|
|
when "putpoi" then
|
|
result = putpoi(*args)
|
|
renumberednodes[result[2]] = result[3] if result[0].zero? && result[2] != result[3]
|
|
when "startchangeset" then
|
|
result = startchangeset(*args)
|
|
end
|
|
|
|
err = true if result[0] == -3 # If a conflict is detected, don't execute any more writes
|
|
end
|
|
|
|
result
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def amf_handle_error(call, rootobj, rootid)
|
|
yield
|
|
rescue OSM::APIAlreadyDeletedError => ex
|
|
return [-4, ex.object, ex.object_id]
|
|
rescue OSM::APIVersionMismatchError => ex
|
|
return [-3, [rootobj, rootid], [ex.type.downcase, ex.id, ex.latest]]
|
|
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}"]
|
|
rescue OSM::APIError => ex
|
|
return [-1, ex.to_s]
|
|
rescue StandardError => ex
|
|
return [-2, "An unusual error happened (in #{call}). The server said: #{ex}"]
|
|
end
|
|
|
|
def amf_handle_error_with_timeout(call, rootobj, rootid)
|
|
amf_handle_error(call, rootobj, rootid) do
|
|
OSM::Timer.timeout(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'", nil, nil) do
|
|
user = getuser(usertoken)
|
|
return -1, "You are not logged in, so Potlatch can't write any changes to the database." unless user
|
|
return -1, t("application.setup_user_auth.blocked") if user.blocks.active.exists?
|
|
return -1, "You must accept the contributor terms before you can edit." if REQUIRE_TERMS_AGREED && user.terms_agreed.nil?
|
|
|
|
if cstags
|
|
return -1, "One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1." unless tags_ok(cstags)
|
|
cstags = strip_non_xml_chars cstags
|
|
end
|
|
|
|
# close previous changeset and add comment
|
|
if closeid
|
|
cs = Changeset.find(closeid.to_i)
|
|
cs.set_closed_time_now
|
|
if cs.user_id != user.id
|
|
raise OSM::APIUserChangesetMismatchError
|
|
elsif closecomment.empty?
|
|
cs.save!
|
|
else
|
|
cs.tags["comment"] = closecomment
|
|
# in case closecomment has chars not allowed in xml
|
|
cs.tags = strip_non_xml_chars cs.tags
|
|
cs.save_with_tags!
|
|
end
|
|
end
|
|
|
|
# open a new changeset
|
|
if opennew.nonzero?
|
|
cs = Changeset.new
|
|
cs.tags = cstags
|
|
cs.user_id = user.id
|
|
unless closecomment.empty?
|
|
cs.tags["comment"] = closecomment
|
|
# in case closecomment has chars not allowed in xml
|
|
cs.tags = strip_non_xml_chars cs.tags
|
|
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(usertoken, _lang)
|
|
user = getuser(usertoken)
|
|
|
|
langs = if user && !user.languages.empty?
|
|
Locale.list(user.languages)
|
|
else
|
|
Locale.list(http_accept_language.user_preferred_languages)
|
|
end
|
|
|
|
lang = getlocales.preferred(langs)
|
|
(real_lang, localised) = getlocalized(lang.to_s)
|
|
|
|
# Tell Potlatch what language it's using
|
|
localised["__potlatch_locale"] = real_lang
|
|
|
|
# Get help from i18n but delete it so we won't pass it around
|
|
# twice for nothing
|
|
help = localised["help_html"]
|
|
localised.delete("help_html")
|
|
|
|
# Populate icon names
|
|
POTLATCH_PRESETS[10].each do |id|
|
|
POTLATCH_PRESETS[11][id] = localised["preset_icon_#{id}"]
|
|
localised.delete("preset_icon_#{id}")
|
|
end
|
|
|
|
POTLATCH_PRESETS + [localised, help]
|
|
end
|
|
|
|
def getlocalized(lang)
|
|
# What we end up actually using. Reported in Potlatch's created_by=* string
|
|
loaded_lang = "en"
|
|
|
|
# Load English defaults
|
|
en = YAML.safe_load(File.open(Rails.root.join("config", "potlatch", "locales", "en.yml")))["en"]
|
|
|
|
if lang == "en"
|
|
return [loaded_lang, en]
|
|
else
|
|
# Use English as a fallback
|
|
begin
|
|
other = YAML.safe_load(File.open(Rails.root.join("config", "potlatch", "locales", "#{lang}.yml")))[lang]
|
|
loaded_lang = lang
|
|
rescue StandardError
|
|
other = en
|
|
end
|
|
|
|
# We have to return a flat list and some of the keys won't be
|
|
# translated (probably)
|
|
return [loaded_lang, en.merge(other)]
|
|
end
|
|
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)
|
|
amf_handle_error_with_timeout("'whichways'", nil, nil) 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
|
|
bbox = BoundingBox.new(xmin, ymin, xmax, ymax)
|
|
bbox.check_boundaries
|
|
bbox.check_size
|
|
|
|
if POTLATCH_USE_SQL
|
|
ways = sql_find_ways_in_area(bbox)
|
|
points = sql_find_pois_in_area(bbox)
|
|
relations = sql_find_relations_in_area_and_ways(bbox, ways.collect { |x| x[0] })
|
|
else
|
|
# find the way ids in an area
|
|
nodes_in_area = Node.bbox(bbox).visible.includes(:ways)
|
|
ways = nodes_in_area.inject([]) do |sum, node|
|
|
visible_ways = node.ways.select(&:visible?)
|
|
sum + visible_ways.collect { |w| [w.id, w.version] }
|
|
end.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.nodes(nodes_in_area.collect(&:id)).visible +
|
|
Relation.ways(ways.collect { |w| w[0] }).visible
|
|
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)
|
|
amf_handle_error_with_timeout("'whichways_deleted'", nil, nil) 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
|
|
bbox = BoundingBox.new(xmin, ymin, xmax, ymax)
|
|
bbox.check_boundaries
|
|
bbox.check_size
|
|
|
|
nodes_in_area = Node.bbox(bbox).joins(:ways_via_history).where(:current_ways => { :visible => false })
|
|
way_ids = nodes_in_area.collect { |node| node.ways_via_history.invisible.collect(&:id) }.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)
|
|
amf_handle_error_with_timeout("'getway' #{wayid}", "way", wayid) do
|
|
if POTLATCH_USE_SQL
|
|
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.where(:id => wayid).first
|
|
|
|
# check case where way has been deleted or doesn't exist
|
|
return [-4, "way", wayid] if way.nil? || !way.visible
|
|
|
|
points = way.nodes.preload(:node_tags).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)
|
|
amf_handle_error_with_timeout("'getway_old' #{id}, #{timestamp}", "way", id) do
|
|
if timestamp == ""
|
|
# undelete
|
|
old_way = OldWay.where(:visible => true, :way_id => id).unredacted.order("version DESC").first
|
|
points = old_way.get_nodes_undelete unless old_way.nil?
|
|
else
|
|
begin
|
|
# revert
|
|
timestamp = Time.zone.strptime(timestamp.to_s, "%d %b %Y, %H:%M:%S")
|
|
old_way = OldWay.where("way_id = ? AND timestamp <= ?", id, timestamp).unredacted.order("timestamp DESC").first
|
|
unless old_way.nil?
|
|
if old_way.visible
|
|
points = old_way.get_nodes_revert(timestamp)
|
|
else
|
|
return [-1, "Sorry, the way was deleted at that time - please revert to a previous version.", id]
|
|
end
|
|
end
|
|
rescue ArgumentError
|
|
# thrown by date parsing method. leave old_way as nil for
|
|
# the error handler below.
|
|
old_way = nil
|
|
end
|
|
end
|
|
|
|
if old_way.nil?
|
|
return [-1, "Sorry, the server could not find a way at that time.", id]
|
|
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 && 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)
|
|
revdates = []
|
|
revusers = {}
|
|
Way.find(wayid).old_ways.unredacted.collect do |a|
|
|
revdates.push(a.timestamp)
|
|
revusers[a.timestamp.to_i] = change_user(a) unless revusers.key?(a.timestamp.to_i)
|
|
a.nds.each do |n|
|
|
Node.find(n).old_nodes.unredacted.collect do |o|
|
|
revdates.push(o.timestamp)
|
|
revusers[o.timestamp.to_i] = change_user(o) unless revusers.key?(o.timestamp.to_i)
|
|
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) || revdates.include?(d + 2) }
|
|
# Collect all in one nested array
|
|
revdates.collect! { |d| [(d + 1).strftime("%d %b %Y, %H:%M:%S")] + revusers[d.to_i] }
|
|
revdates.uniq!
|
|
|
|
return ["way", wayid, revdates]
|
|
rescue ActiveRecord::RecordNotFound
|
|
return ["way", wayid, []]
|
|
end
|
|
|
|
# Find history of a node. Returns 'node', id, and an array of previous versions as above.
|
|
|
|
def getnode_history(nodeid)
|
|
history = Node.find(nodeid).old_nodes.unredacted.reverse.collect do |old_node|
|
|
[(old_node.timestamp + 1).strftime("%d %b %Y, %H:%M:%S")] + change_user(old_node)
|
|
end
|
|
return ["node", nodeid, history]
|
|
rescue ActiveRecord::RecordNotFound
|
|
return ["node", nodeid, []]
|
|
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'", nil, nil) do
|
|
user = getuser(usertoken)
|
|
|
|
return -1, "You must be logged in to search for GPX traces." unless user
|
|
return -1, t("application.setup_user_auth.blocked") if user.blocks.active.exists?
|
|
|
|
query = Trace.visible_to(user)
|
|
query = if searchterm.to_i > 0
|
|
query.where(:id => searchterm.to_i)
|
|
else
|
|
query.where("MATCH(name) AGAINST (?)", searchterm).limit(21)
|
|
end
|
|
gpxs = query.collect do |gpx|
|
|
[gpx.id, gpx.name, gpx.description]
|
|
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)
|
|
amf_handle_error("'getrelation' #{relid}", "relation", relid) do
|
|
rel = Relation.where(:id => relid).first
|
|
|
|
return [-4, "relation", relid] if rel.nil? || !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
|
|
rel = Relation.where(:id => searchterm.to_i).first
|
|
if rel && rel.visible
|
|
rels.push([rel.id, rel.tags, rel.members, rel.version])
|
|
end
|
|
else
|
|
RelationTag.where("v like ?", "%#{searchterm}%").limit(11).each do |t|
|
|
if t.relation.visible
|
|
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)
|
|
amf_handle_error("'putrelation' #{relid}", "relation", relid) do
|
|
user = getuser(usertoken)
|
|
|
|
return -1, "You are not logged in, so the relation could not be saved." unless user
|
|
return -1, t("application.setup_user_auth.blocked") if user.blocks.active.exists?
|
|
return -1, "You must accept the contributor terms before you can edit." if REQUIRE_TERMS_AGREED && user.terms_agreed.nil?
|
|
|
|
return -1, "One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1." unless tags_ok(tags)
|
|
tags = strip_non_xml_chars tags
|
|
|
|
relid = relid.to_i
|
|
visible = visible.to_i.nonzero?
|
|
|
|
new_relation = nil
|
|
relation = nil
|
|
Relation.transaction do
|
|
# create a new relation, or find the existing one
|
|
relation = Relation.find(relid) if relid > 0
|
|
# 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].delete("\000-\037\ufffe\uffff", "^\011\012\015")]
|
|
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
|
|
|
|
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 changed node versions (node=>version)
|
|
# 7. hash of deleted node versions (node=>version)
|
|
|
|
def putway(renumberednodes, usertoken, changeset_id, wayversion, originalway, pointlist, attributes, nodes, deletednodes)
|
|
amf_handle_error("'putway' #{originalway}", "way", originalway) do
|
|
# -- Initialise
|
|
|
|
user = getuser(usertoken)
|
|
return -1, "You are not logged in, so the way could not be saved." unless user
|
|
return -1, t("application.setup_user_auth.blocked") if user.blocks.active.exists?
|
|
return -1, "You must accept the contributor terms before you can edit." if REQUIRE_TERMS_AGREED && user.terms_agreed.nil?
|
|
|
|
return -2, "Server error - way is only #{pointlist.length} points long." if pointlist.length < 2
|
|
|
|
return -1, "One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1." unless tags_ok(attributes)
|
|
attributes = strip_non_xml_chars attributes
|
|
|
|
originalway = originalway.to_i
|
|
pointlist.collect!(&: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
|
|
|
|
return -2, "Server error - node with id 0 found in way #{originalway}." if id.zero?
|
|
return -2, "Server error - node with latitude -90 found in way #{originalway}." if lat == 90
|
|
|
|
id = renumberednodes[id] if renumberednodes[id]
|
|
|
|
node = Node.new
|
|
node.changeset_id = changeset_id
|
|
node.lat = lat
|
|
node.lon = lon
|
|
node.tags = a[4]
|
|
|
|
# fixup node tags in a way as well
|
|
return -1, "One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1." unless tags_ok(node.tags)
|
|
node.tags = strip_non_xml_chars node.tags
|
|
|
|
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! do |a|
|
|
renumberednodes[a] ? renumberednodes[a] : a
|
|
end
|
|
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 || way.nds != pointlist || !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
|
|
# We don't do anything here as the node is being used elsewhere
|
|
# and we don't want to delete it
|
|
end
|
|
end
|
|
end
|
|
|
|
[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)
|
|
amf_handle_error("'putpoi' #{id}", "node", id) do
|
|
user = getuser(usertoken)
|
|
return -1, "You are not logged in, so the point could not be saved." unless user
|
|
return -1, t("application.setup_user_auth.blocked") if user.blocks.active.exists?
|
|
return -1, "You must accept the contributor terms before you can edit." if REQUIRE_TERMS_AGREED && user.terms_agreed.nil?
|
|
|
|
return -1, "One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1." unless tags_ok(tags)
|
|
tags = strip_non_xml_chars tags
|
|
|
|
id = id.to_i
|
|
visible = (visible.to_i == 1)
|
|
node = nil
|
|
new_node = nil
|
|
Node.transaction do
|
|
if id > 0
|
|
begin
|
|
node = Node.find(id)
|
|
rescue ActiveRecord::RecordNotFound
|
|
return [-4, "node", id]
|
|
end
|
|
|
|
unless visible || node.ways.empty?
|
|
return -1, "Point #{id} has since become part of a way, so you cannot save it as a POI.", id, id, version
|
|
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
|
|
|
|
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)
|
|
amf_handle_error("'getpoi' #{id}", "node", id) do
|
|
id = id.to_i
|
|
n = Node.where(:id => id).first
|
|
if n
|
|
v = n.version
|
|
unless timestamp == ""
|
|
n = OldNode.where("node_id = ? AND timestamp <= ?", id, timestamp).unredacted.order("timestamp DESC").first
|
|
end
|
|
end
|
|
|
|
if n
|
|
return [0, "", id, n.lon, n.lat, n.tags, v]
|
|
else
|
|
return [-4, "node", id]
|
|
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)
|
|
amf_handle_error("'deleteway' #{way_id}", "way", way_id) do
|
|
user = getuser(usertoken)
|
|
return -1, "You are not logged in, so the way could not be deleted." unless user
|
|
return -1, t("application.setup_user_auth.blocked") if user.blocks.active.exists?
|
|
return -1, "You must accept the contributor terms before you can edit." if REQUIRE_TERMS_AGREED && user.terms_agreed.nil?
|
|
|
|
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
|
|
# 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
|
|
[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)
|
|
if token =~ /^(.+)\:(.+)$/
|
|
User.authenticate(:username => Regexp.last_match(1), :password => Regexp.last_match(2))
|
|
else
|
|
User.authenticate(:token => token)
|
|
end
|
|
end
|
|
|
|
def getlocales
|
|
@locales ||= Locale.list(Dir.glob(Rails.root.join("config", "potlatch", "locales", "*")).collect { |f| File.basename(f, ".yml") })
|
|
end
|
|
|
|
##
|
|
# check that all key-value pairs are valid UTF-8.
|
|
def tags_ok(tags)
|
|
tags.each do |k, v|
|
|
return false unless UTF8.valid? k
|
|
return false unless UTF8.valid? v
|
|
end
|
|
true
|
|
end
|
|
|
|
##
|
|
# strip characters which are invalid in XML documents from the strings
|
|
# in the +tags+ hash.
|
|
def strip_non_xml_chars(tags)
|
|
new_tags = {}
|
|
unless tags.nil?
|
|
tags.each do |k, v|
|
|
new_k = k.delete "\000-\037\ufffe\uffff", "^\011\012\015"
|
|
new_v = v.delete "\000-\037\ufffe\uffff", "^\011\012\015"
|
|
new_tags[new_k] = new_v
|
|
end
|
|
end
|
|
new_tags
|
|
end
|
|
|
|
# ====================================================================
|
|
# Alternative SQL queries for getway/whichways
|
|
|
|
def sql_find_ways_in_area(bbox)
|
|
sql = <<-SQL
|
|
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(bbox, 'current_nodes.')}
|
|
SQL
|
|
ActiveRecord::Base.connection.select_all(sql).collect { |a| [a["wayid"].to_i, a["version"].to_i] }
|
|
end
|
|
|
|
def sql_find_pois_in_area(bbox)
|
|
pois = []
|
|
sql = <<-SQL
|
|
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(bbox, 'current_nodes.')}
|
|
SQL
|
|
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(bbox, way_ids)
|
|
# ** It would be more Potlatchy to get relations for nodes within ways
|
|
# during 'getway', not here
|
|
sql = <<-SQL
|
|
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(bbox, 'cn.')}
|
|
SQL
|
|
unless way_ids.empty?
|
|
sql += <<-SQL
|
|
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(',')})
|
|
SQL
|
|
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 = <<-SQL
|
|
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
|
|
SQL
|
|
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
|