class SiteController < ApplicationController
- layout 'site',:except => [:key]
+ layout 'site', :except => [:key, :permalink]
before_filter :authorize_web
before_filter :set_locale
render :action => 'index'
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
expires_in 7.days, :public => true
end
</noscript>
<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 id="attribution">
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.'
permalink: Permalink
+ shortlink: Shortlink
license:
notice: "Licensed under the {{license_name}} license by the {{project_name}} and its contributors."
license_name: "Creative Commons Attribution-Share Alike 2.0"
map.connect '/create-account.html', :controller => 'user', :action => 'new'
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
map.connect '/traces', :controller => 'trace', :action => 'list'
map.connect '/traces/page/:page', :controller => 'trace', :action => 'list'
--- /dev/null
+##
+# 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
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) {
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;
+}
bottom:15px;
right:15px;
font-size:smaller;
+ text-align: right;
}
#attribution {
--- /dev/null
+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
--- /dev/null
+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