Adding 'shortlink' functions which will allow URLs like http://osm.org/go/XXXX suitable for use in twitter, etc...
This commit is contained in:
parent
2f6aab7124
commit
1d8e66016c
9 changed files with 213 additions and 2 deletions
|
@ -1,5 +1,5 @@
|
||||||
class SiteController < ApplicationController
|
class SiteController < ApplicationController
|
||||||
layout 'site',:except => [:key]
|
layout 'site', :except => [:key, :permalink]
|
||||||
|
|
||||||
before_filter :authorize_web
|
before_filter :authorize_web
|
||||||
before_filter :set_locale
|
before_filter :set_locale
|
||||||
|
@ -9,6 +9,24 @@ class SiteController < ApplicationController
|
||||||
render :action => 'index'
|
render :action => 'index'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def permalink
|
||||||
|
lon, lat, zoom = ShortLink::decode(params[:code])
|
||||||
|
new_params = params.clone
|
||||||
|
new_params.delete :code
|
||||||
|
if new_params.has_key? :m
|
||||||
|
new_params.delete :m
|
||||||
|
new_params[:mlat] = lat
|
||||||
|
new_params[:mlon] = lon
|
||||||
|
else
|
||||||
|
new_params[:lat] = lat
|
||||||
|
new_params[:lon] = lon
|
||||||
|
end
|
||||||
|
new_params[:zoom] = zoom
|
||||||
|
new_params[:controller] = 'site'
|
||||||
|
new_params[:action] = 'index'
|
||||||
|
redirect_to new_params
|
||||||
|
end
|
||||||
|
|
||||||
def key
|
def key
|
||||||
expires_in 7.days, :public => true
|
expires_in 7.days, :public => true
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,7 +17,10 @@
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|
||||||
<div id="map">
|
<div id="map">
|
||||||
<div id="permalink"><a href="/" id="permalinkanchor"><%= t 'site.index.permalink' %></a></div>
|
<div id="permalink">
|
||||||
|
<a href="/" id="permalinkanchor"><%= t 'site.index.permalink' %></a><br/>
|
||||||
|
<a href="/" id="shortlinkanchor"><%= t 'site.index.shortlink' %></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="attribution">
|
<div id="attribution">
|
||||||
|
|
|
@ -541,6 +541,7 @@ en:
|
||||||
js_2: "OpenStreetMap uses javascript for its slippy map."
|
js_2: "OpenStreetMap uses javascript for its slippy map."
|
||||||
js_3: 'You may want to try the <a href="http://tah.openstreetmap.org/Browse/">Tiles@Home static tile browser</a> if you are unable to enable javascript.'
|
js_3: 'You may want to try the <a href="http://tah.openstreetmap.org/Browse/">Tiles@Home static tile browser</a> if you are unable to enable javascript.'
|
||||||
permalink: Permalink
|
permalink: Permalink
|
||||||
|
shortlink: Shortlink
|
||||||
license:
|
license:
|
||||||
notice: "Licensed under the {{license_name}} license by the {{project_name}} and its contributors."
|
notice: "Licensed under the {{license_name}} license by the {{project_name}} and its contributors."
|
||||||
license_name: "Creative Commons Attribution-Share Alike 2.0"
|
license_name: "Creative Commons Attribution-Share Alike 2.0"
|
||||||
|
|
|
@ -114,6 +114,9 @@ ActionController::Routing::Routes.draw do |map|
|
||||||
map.connect '/create-account.html', :controller => 'user', :action => 'new'
|
map.connect '/create-account.html', :controller => 'user', :action => 'new'
|
||||||
map.connect '/forgot-password.html', :controller => 'user', :action => 'lost_password'
|
map.connect '/forgot-password.html', :controller => 'user', :action => 'lost_password'
|
||||||
|
|
||||||
|
# permalink
|
||||||
|
map.connect '/go/:code', :controller => 'site', :action => 'permalink', :code => /[a-zA-Z0-9_@]+=*/
|
||||||
|
|
||||||
# traces
|
# traces
|
||||||
map.connect '/traces', :controller => 'trace', :action => 'list'
|
map.connect '/traces', :controller => 'trace', :action => 'list'
|
||||||
map.connect '/traces/page/:page', :controller => 'trace', :action => 'list'
|
map.connect '/traces/page/:page', :controller => 'trace', :action => 'list'
|
||||||
|
|
79
lib/short_link.rb
Normal file
79
lib/short_link.rb
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
##
|
||||||
|
# Encodes and decodes locations from Morton-coded "quad tile" strings. Each
|
||||||
|
# variable-length string encodes to a precision of one pixel per tile (roughly,
|
||||||
|
# since this computation is done in lat/lon coordinates, not mercator).
|
||||||
|
# Each character encodes 3 bits of x and 3 of y, so there are extra characters
|
||||||
|
# tacked on the end to make the zoom levels "work".
|
||||||
|
module ShortLink
|
||||||
|
|
||||||
|
# array of 64 chars to encode 6 bits. this is almost like base64 encoding, but
|
||||||
|
# the symbolic chars are different, as base64's + and / aren't very
|
||||||
|
# URL-friendly.
|
||||||
|
ARRAY = ('A'..'Z').to_a + ('a'..'z').to_a + ('0'..'9').to_a + ['_','@']
|
||||||
|
|
||||||
|
##
|
||||||
|
# Given a string encoding a location, returns the [lon, lat, z] tuple of that
|
||||||
|
# location.
|
||||||
|
def self.decode(str)
|
||||||
|
x = 0
|
||||||
|
y = 0
|
||||||
|
z = 0
|
||||||
|
z_offset = 0
|
||||||
|
|
||||||
|
str.each_char do |c|
|
||||||
|
t = ARRAY.index c
|
||||||
|
if t.nil?
|
||||||
|
z_offset -= 1
|
||||||
|
else
|
||||||
|
3.times do
|
||||||
|
x <<= 1; x = x | 1 unless (t & 32).zero?; t <<= 1
|
||||||
|
y <<= 1; y = y | 1 unless (t & 32).zero?; t <<= 1
|
||||||
|
end
|
||||||
|
z += 3
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# pack the coordinates out to their original 32 bits.
|
||||||
|
x <<= (32 - z)
|
||||||
|
y <<= (32 - z)
|
||||||
|
|
||||||
|
# project the parameters back to their coordinate ranges.
|
||||||
|
[(x * 360.0 / 2**32) - 180.0,
|
||||||
|
(y * 180.0 / 2**32) - 90.0,
|
||||||
|
z - 8 - (z_offset % 3)]
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# given a location and zoom, return a short string representing it.
|
||||||
|
def self.encode(lon, lat, z)
|
||||||
|
code = interleave_bits(((lon + 180.0) * 2**32 / 360.0).to_i,
|
||||||
|
((lat + 90.0) * 2**32 / 180.0).to_i)
|
||||||
|
str = ""
|
||||||
|
# add eight to the zoom level, which approximates an accuracy of
|
||||||
|
# one pixel in a tile.
|
||||||
|
((z + 8)/3.0).ceil.times do |i|
|
||||||
|
digit = (code >> (58 - 6 * i)) & 0x3f
|
||||||
|
str << ARRAY[digit]
|
||||||
|
end
|
||||||
|
# append characters onto the end of the string to represent
|
||||||
|
# partial zoom levels (characters themselves have a granularity
|
||||||
|
# of 3 zoom levels).
|
||||||
|
((z + 8) % 3).times { str << "=" }
|
||||||
|
|
||||||
|
return str
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
##
|
||||||
|
# interleaves the bits of two 32-bit numbers. the result is known
|
||||||
|
# as a Morton code.
|
||||||
|
def self.interleave_bits(x, y)
|
||||||
|
c = 0
|
||||||
|
31.downto(0) do |i|
|
||||||
|
c = (c << 1) | ((x >> i) & 1)
|
||||||
|
c = (c << 1) | ((y >> i) & 1)
|
||||||
|
end
|
||||||
|
c
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -84,6 +84,20 @@ function updatelinks(lon,lat,zoom,layers,minlon,minlat,maxlon,maxlat) {
|
||||||
node.style.fontStyle = 'italic';
|
node.style.fontStyle = 'italic';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
node = document.getElementById("shortlinkanchor");
|
||||||
|
if (node) {
|
||||||
|
var args = getArgs(node.href);
|
||||||
|
var code = makeShortCode(lat, lon, zoom);
|
||||||
|
// little hack. may the gods of hardcoding please forgive me, or
|
||||||
|
// show me the Right way to do it.
|
||||||
|
if (layers && (layers != "B000FTF")) {
|
||||||
|
args["layers"] = layers;
|
||||||
|
node.href = setArgs("/go/" + code, args);
|
||||||
|
} else {
|
||||||
|
node.href = "/go/" + code;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArgs(url) {
|
function getArgs(url) {
|
||||||
|
@ -158,3 +172,34 @@ function i18n(string, keys) {
|
||||||
|
|
||||||
return string;
|
return string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeShortCode(lat, lon, zoom) {
|
||||||
|
char_array = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_@";
|
||||||
|
var x = Math.round((lon + 180.0) * ((1 << 30) / 90.0));
|
||||||
|
var y = Math.round((lat + 90.0) * ((1 << 30) / 45.0));
|
||||||
|
// hack around the fact that JS apparently only allows 53-bit integers?!?
|
||||||
|
// note that, although this reduces the accuracy of the process, it's fine for
|
||||||
|
// z18 so we don't need to care for now.
|
||||||
|
var c1 = 0, c2 = 0;
|
||||||
|
for (var i = 31; i > 16; --i) {
|
||||||
|
c1 = (c1 << 1) | ((x >> i) & 1);
|
||||||
|
c1 = (c1 << 1) | ((y >> i) & 1);
|
||||||
|
}
|
||||||
|
for (var i = 16; i > 1; --i) {
|
||||||
|
c2 = (c2 << 1) | ((x >> i) & 1);
|
||||||
|
c2 = (c2 << 1) | ((y >> i) & 1);
|
||||||
|
}
|
||||||
|
var str = "";
|
||||||
|
for (var i = 0; i < Math.ceil((zoom + 8) / 3.0) && i < 5; ++i) {
|
||||||
|
digit = (c1 >> (24 - 6 * i)) & 0x3f;
|
||||||
|
str += char_array.charAt(digit);
|
||||||
|
}
|
||||||
|
for (var i = 5; i < Math.ceil((zoom + 8) / 3.0); ++i) {
|
||||||
|
digit = (c2 >> (24 - 6 * (i - 5))) & 0x3f;
|
||||||
|
str += char_array.charAt(digit);
|
||||||
|
}
|
||||||
|
for (var i = 0; i < ((zoom + 8) % 3); ++i) {
|
||||||
|
str += "=";
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
|
@ -617,6 +617,7 @@ input[type="submit"] {
|
||||||
bottom:15px;
|
bottom:15px;
|
||||||
right:15px;
|
right:15px;
|
||||||
font-size:smaller;
|
font-size:smaller;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
#attribution {
|
#attribution {
|
||||||
|
|
35
test/integration/short_link_test.rb
Normal file
35
test/integration/short_link_test.rb
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
require File.dirname(__FILE__) + '/../test_helper'
|
||||||
|
|
||||||
|
class ShortLinkTest < ActionController::IntegrationTest
|
||||||
|
##
|
||||||
|
# test the short link with various parameters and ensure they're
|
||||||
|
# kept in the redirect.
|
||||||
|
def test_short_link_params
|
||||||
|
assert_short_link_redirect('1N8H@P_5W')
|
||||||
|
assert_short_link_redirect('euu4oTas==')
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# utility method to test short links
|
||||||
|
def assert_short_link_redirect(short_link)
|
||||||
|
lon, lat, zoom = ShortLink::decode(short_link)
|
||||||
|
|
||||||
|
# test without marker
|
||||||
|
get '/go/' + short_link
|
||||||
|
assert_redirected_to :controller => 'site', :action => 'index', :lat => lat, :lon => lon, :zoom => zoom
|
||||||
|
|
||||||
|
# test with marker
|
||||||
|
get '/go/' + short_link + "?m"
|
||||||
|
assert_redirected_to :controller => 'site', :action => 'index', :mlat => lat, :mlon => lon, :zoom => zoom
|
||||||
|
|
||||||
|
# test with layers and a marker
|
||||||
|
get '/go/' + short_link + "?m&layers=B000FTF"
|
||||||
|
assert_redirected_to :controller => 'site', :action => 'index', :mlat => lat, :mlon => lon, :zoom => zoom, :layers => "B000FTF"
|
||||||
|
get '/go/' + short_link + "?layers=B000FTF&m"
|
||||||
|
assert_redirected_to :controller => 'site', :action => 'index', :mlat => lat, :mlon => lon, :zoom => zoom, :layers => "B000FTF"
|
||||||
|
|
||||||
|
# test with some random query parameters we haven't even implemented yet
|
||||||
|
get '/go/' + short_link + "?foobar=yes"
|
||||||
|
assert_redirected_to :controller => 'site', :action => 'index', :lat => lat, :lon => lon, :zoom => zoom, :foobar => "yes"
|
||||||
|
end
|
||||||
|
end
|
26
test/unit/short_link_test.rb
Normal file
26
test/unit/short_link_test.rb
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
require File.dirname(__FILE__) + '/../test_helper'
|
||||||
|
|
||||||
|
class ShortLinkTest < ActiveSupport::TestCase
|
||||||
|
##
|
||||||
|
# tests that encoding and decoding are working to within
|
||||||
|
# the acceptable quantisation range.
|
||||||
|
def test_encode_decode
|
||||||
|
cases = Array.new
|
||||||
|
1000.times do
|
||||||
|
cases << [ 180.0 * rand - 90.0, 360.0 * rand - 180.0, (18 * rand).to_i ]
|
||||||
|
end
|
||||||
|
|
||||||
|
cases.each do |lat, lon, zoom|
|
||||||
|
lon2, lat2, zoom2 = ShortLink.decode(ShortLink.encode(lon, lat, zoom))
|
||||||
|
# zooms should be identical
|
||||||
|
assert_equal zoom, zoom2, "Decoding a encoded short link gives different zoom for (#{lat}, #{lon}, #{zoom})."
|
||||||
|
# but the location has a quantisation error introduced at roughly
|
||||||
|
# one pixel (i.e: zoom + 8). the sqrt(5) is because each position
|
||||||
|
# has an extra bit of accuracy in the lat coordinate, due to the
|
||||||
|
# smaller range.
|
||||||
|
distance = Math.sqrt((lat - lat2) ** 2 + (lon - lon2) ** 2)
|
||||||
|
max_distance = 360.0 / (1 << (zoom + 8)) * 0.5 * Math.sqrt(5)
|
||||||
|
assert max_distance > distance, "Maximum expected error exceeded: #{max_distance} <= #{distance} for (#{lat}, #{lon}, #{zoom})."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Add table
Add a link
Reference in a new issue