Skip to content

Instantly share code, notes, and snippets.

@RandomEtc
Created December 30, 2013 07:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RandomEtc/8179002 to your computer and use it in GitHub Desktop.
Save RandomEtc/8179002 to your computer and use it in GitHub Desktop.
Mercator Map Tile functions for Ruby
TILE_SIZE = 256.0
PX_TO_COORD = 1.0 / TILE_SIZE
RAD_2_DEG = 180.0 / Math::PI
DEG_2_RAD = Math::PI / 180.0
TWO_PI = Math::PI * 2.0
MAX_ZOOM = 20
module TileMaps
def lng_lat_to_mercator(lng, lat)
# turn degrees into radians
lng_radians = lng * DEG_2_RAD
lat_radians = lat * DEG_2_RAD
# stretch out latitude to fit a square
[ lng_radians, Math::log(Math::tan(0.25 * Math::PI + 0.5 * lat_radians)) ]
end
def mercator_to_lng_lat(m_lng, m_lat)
lng_radians = m_lng
lat_radians = 2.0 * Math::atan(Math::E ** m_lat) - 0.5 * Math::PI
[ lng_radians * RAD_2_DEG, lat_radians * RAD_2_DEG ]
end
def mercator_to_coordinate(m_lng, m_lat, zoom=0)
# scale x, y between 0 and 1
x = (m_lng + Math::PI) / TWO_PI
y = 1.0 - ((m_lat + Math::PI) / TWO_PI) # flip axis
# scale to zoom level
scale = 2 ** zoom
[ x * scale, y * scale]
end
def coordinate_to_mercator(x, y, zoom=0)
# put it back at zoom level 0
scale = 2 ** zoom
x /= scale
y /= scale
# scale it up to PI again
m_lng = (x * 2.0 * Math::PI) - Math::PI
m_lat = ((1.0 - y) * 2.0 * Math::PI) - Math::PI
[ m_lng, m_lat ]
end
def coordinate_to_lng_lat(x, y, zoom=0)
mercator_to_lng_lat(*coordinate_to_mercator(x, y, zoom))
end
def lng_lat_to_coordinate(lng, lat, zoom=0)
mercator_to_coordinate(*lng_lat_to_mercator(lng, lat), zoom)
end
def best_llz_for_locations(locations, width, height)
# transpose is a bit like Python's unzip:
# [[a,b],[c,d],[e,f]] --> [[a,c,e],[b,d,f]]
min_lng, min_lat, max_lng, max_lat = locations.transpose.map(&:minmax).transpose.flatten
# get the bounding rectangle of all the locations on zoom level 0 mercator map tile
min_x, min_y = lng_lat_to_coordinate(min_lng, max_lat, 0) # NOTE: max_lat --> min_y
max_x, max_y = lng_lat_to_coordinate(max_lng, min_lat, 0) # NOTE: min_lat --> max_y
# size of mercator rectangle on zoom level 0 tile
x_size = (max_x - min_x) * TILE_SIZE
y_size = (max_y - min_y) * TILE_SIZE
# best zoom for x
max_x_scale = width / x_size
max_x_zoom = Math::log2(max_x_scale).to_int
# best zoom for y
max_y_scale = height / y_size
max_y_zoom = Math::log2(max_y_scale).to_int
# compromise
zoom = [ max_x_zoom, max_y_zoom, MAX_ZOOM ].min
# get the lat, lng of the center of the bounding rectangle
center_x = (min_x + max_x) / 2
center_y = (min_y + max_y) / 2
lng, lat = coordinate_to_lng_lat(center_x, center_y, 0)
[ lng, lat, zoom ]
end
def best_padded_llz_for_locations(locations, width, height, padding_top, padding_right, padding_bottom, padding_left)
# markers only permitted in this zone
visible_width = width - padding_left - padding_right
visible_height = height - padding_top - padding_bottom
# figure out what the map position would be for a smaller viewport
lng, lat, zoom = best_llz_for_locations(locations, visible_width, visible_height)
# get the tile coordinate for that location/zoom
center_x, center_y = lng_lat_to_coordinate(lng, lat, zoom)
# move center to accomodate padding
center_x += PX_TO_COORD * (padding_right - padding_left) / 2.0
center_y += PX_TO_COORD * (padding_bottom - padding_top) / 2.0
# update to new location
lng, lat = coordinate_to_lng_lat(center_x, center_y, zoom)
[ lng, lat, zoom ]
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment