Module: Wukong::Geolocated

Defined in:
lib/wu/geo/geolocated.rb

Overview

Defined Under Namespace

Modules: ByCoordinates

Constant Summary collapse

EARTH_RADIUS =

meters

6371000
MIN_LONGITUDE =
-180
MAX_LONGITUDE =
180
MIN_LATITUDE =
-85.05112878
MAX_LATITUDE =
85.05112878
ALLOWED_LONGITUDE =
(MIN_LONGITUDE..MAX_LONGITUDE)
ALLOWED_LATITUDE =
(MIN_LATITUDE..MAX_LATITUDE)
TILE_PIXEL_SIZE =
256
BIT_TO_QUADKEY =

converts from even/odd state of tile x and tile y to quadkey. NOTE: bit order means y, x

{ [false, false] => "0", [false, true] => "1", [true, false] => "2", [true, true] => "3", }
QUADKEY_TO_BIT =

converts from quadkey char to bits. NOTE: bit order means y, x

{ "0" => [0,0], "1" => [0,1], "2" => [1,0], "3" => [1,1]}

Class Method Summary collapse

Class Method Details

.bbox_centroid(left_btm, right_top) ⇒ Array<Float, Float>

Returns the centroid of a bounding box

Parameters:

  • left_btm (Array<Float, Float>)

    Longitude, Latitude of SW point

  • right_top (Array<Float, Float>)

    Longitude, Latitude of NE point

Returns:

  • (Array<Float, Float>)

    Longitude, Latitude of centroid



246
247
248
# File 'lib/wu/geo/geolocated.rb', line 246

def bbox_centroid(left_btm, right_top)
  haversine_midpoint(*left_btm, *right_top)
end

.haversine_distance(left, btm, right, top) ⇒ Object

Return the haversine distance in meters between two points



251
252
253
254
255
256
257
258
259
260
# File 'lib/wu/geo/geolocated.rb', line 251

def haversine_distance(left, btm, right, top)
  delta_lng = (right - left).abs.to_radians
  delta_lat = (top   - btm ).abs.to_radians
  btm_rad = btm.to_radians
  top_rad = top.to_radians

  aa = (Math.sin(delta_lat / 2.0))**2 + Math.cos(top_rad) * Math.cos(btm_rad) * (Math.sin(delta_lng / 2.0))**2
  cc = 2.0 * Math.atan2(Math.sqrt(aa), Math.sqrt(1.0 - aa))
  cc * EARTH_RADIUS
end

.haversine_midpoint(left, btm, right, top) ⇒ Object

Return the haversine midpoint in meters between two points



263
264
265
266
267
268
269
270
271
272
273
# File 'lib/wu/geo/geolocated.rb', line 263

def haversine_midpoint(left, btm, right, top)
  cos_btm   = Math.cos(btm.to_radians)
  cos_top   = Math.cos(top.to_radians)
  bearing_x = cos_btm * Math.cos((right - left).to_radians)
  bearing_y = cos_btm * Math.sin((right - left).to_radians)
  mid_lat   = Math.atan2(
    (Math.sin(top.to_radians) + Math.sin(btm.to_radians)),
    (Math.sqrt((cos_top + bearing_x)**2 + bearing_y**2)))
  mid_lng   = left.to_radians + Math.atan2(bearing_y, (cos_top + bearing_x))
  [mid_lng.to_degrees, mid_lat.to_degrees]
end

.lat_zl_to_tile_yf(latitude, zl) ⇒ Object

Convert latitude in degrees to floating-point tile x,y coordinates at given zoom level

Raises:

  • (ArgumentError)


122
123
124
125
126
127
# File 'lib/wu/geo/geolocated.rb', line 122

def lat_zl_to_tile_yf(latitude, zl)
  raise ArgumentError, "latitude must be within bounds ((#{latitude}) vs #{ALLOWED_LATITUDE})" unless (ALLOWED_LATITUDE.include?(latitude))
  sin_lat = Math.sin(latitude.to_radians)
  yy = Math.log((1 + sin_lat) / (1 - sin_lat)) / (4 * Math::PI)
  (map_tile_size(zl) * (0.5 - yy))
end

.lng_lat_rad_to_bbox(longitude, latitude, radius) ⇒ Object

Returns a bounding box containing the circle created by the lat/lng and radius



232
233
234
235
236
237
238
# File 'lib/wu/geo/geolocated.rb', line 232

def lng_lat_rad_to_bbox(longitude, latitude, radius)
  left, _    = point_east( longitude, latitude, -radius)
  _,     btm = point_north(longitude, latitude, -radius)
  right, _   = point_east( longitude, latitude,  radius)
  _,     top = point_north(longitude, latitude,  radius)
  [left, btm, right, top]
end

.lng_lat_zl_to_pixel_xy(lng, lat, zl) ⇒ Object



324
325
326
327
328
# File 'lib/wu/geo/geolocated.rb', line 324

def lng_lat_zl_to_pixel_xy(lng, lat, zl)
  pixel_x = lng_zl_to_tile_xf(lng, zl)
  pixel_y = lat_zl_to_tile_yf(lat, zl)
  [(pixel_x * TILE_PIXEL_SIZE + 0.5).floor, (pixel_y * TILE_PIXEL_SIZE + 0.5).floor]
end

.lng_lat_zl_to_quadkey(longitude, latitude, zl) ⇒ Object

Convert a lat/lng and zoom level into a quadkey



200
201
202
203
# File 'lib/wu/geo/geolocated.rb', line 200

def lng_lat_zl_to_quadkey(longitude, latitude, zl)
  tile_x, tile_y = lng_lat_zl_to_tile_xy(longitude, latitude, zl)
  tile_xy_zl_to_quadkey(tile_x, tile_y, zl)
end

.lng_lat_zl_to_tile_xy(longitude, latitude, zl) ⇒ Object

Convert latitude in degrees to integer tile x,y coordinates at given zoom level



130
131
132
# File 'lib/wu/geo/geolocated.rb', line 130

def lng_lat_zl_to_tile_xy(longitude, latitude, zl)
  [lng_zl_to_tile_xf(longitude, zl).floor, lat_zl_to_tile_yf(latitude, zl).floor]
end

.lng_zl_to_tile_xf(longitude, zl) ⇒ Object

Convert longitude in degrees to floating-point tile x,y coordinates at given zoom level

Raises:

  • (ArgumentError)


115
116
117
118
119
# File 'lib/wu/geo/geolocated.rb', line 115

def lng_zl_to_tile_xf(longitude, zl)
  raise ArgumentError, "longitude must be within bounds ((#{longitude}) vs #{ALLOWED_LONGITUDE})" unless (ALLOWED_LONGITUDE.include?(longitude))
  xx = (longitude.to_f + 180.0) / 360.0
  (map_tile_size(zl) * xx)
end

.map_pixel_size(zl) ⇒ Object

Width or height of grid bitmap in pixels at given zoom level



295
296
297
# File 'lib/wu/geo/geolocated.rb', line 295

def map_pixel_size(zl)
  TILE_PIXEL_SIZE * map_tile_size(zl)
end

.map_scale_for_dpi(latitude, zl, screen_dpi) ⇒ Object

Map scale at a specified latitude, zoom level, & screen resolution in dpi



306
307
308
# File 'lib/wu/geo/geolocated.rb', line 306

def map_scale_for_dpi(latitude, zl, screen_dpi)
  pixel_resolution(latitude, zl) * screen_dpi / 0.0254
end

.map_tile_size(zl) ⇒ Object

Width or height in number of tiles



106
107
108
# File 'lib/wu/geo/geolocated.rb', line 106

def map_tile_size(zl)
  1 << zl
end

.packed_qk_zl_to_tile_xy(packed_qk, zl = 16) ⇒ Object

Convert a packed quadkey (integer) into tile x,y coordinates and level

Raises:

  • (ArgumentError)


191
192
193
194
195
196
197
# File 'lib/wu/geo/geolocated.rb', line 191

def packed_qk_zl_to_tile_xy(packed_qk, zl=16)
  # don't "optimize" this without testing... string operations are faster than you think in ruby
  raise ArgumentError, "Quadkey must be an integer in range of the zoom level: #{packed_qk}, #{zl}" unless packed_qk.is_a?(Fixnum) && (packed_qk < 2 ** (zl*2))
  quadkey_rhs = packed_qk.to_s(4)
  quadkey     = ("0" * (zl - quadkey_rhs.length)) << quadkey_rhs
  quadkey_to_tile_xy_zl(quadkey)
end

.pixel_resolution(latitude, zl) ⇒ Object

Return pixel resolution in meters per pixel at a specified latitude and zoom level



300
301
302
303
# File 'lib/wu/geo/geolocated.rb', line 300

def pixel_resolution(latitude, zl)
  lat = latitude.clamp(MIN_LATITUDE, MAX_LATITUDE)
  Math.cos(lat.to_radians) * 2 * Math::PI * EARTH_RADIUS / map_pixel_size(zl).to_f
end

.pixel_xy_to_tile_xy(pixel_x, pixel_y) ⇒ Object

Convert from x,y pixel pair into tile x,y coordinates



311
312
313
# File 'lib/wu/geo/geolocated.rb', line 311

def pixel_xy_to_tile_xy(pixel_x, pixel_y)
  [pixel_x / TILE_PIXEL_SIZE, pixel_y / TILE_PIXEL_SIZE]
end

.pixel_xy_zl_to_lng_lat(pixel_x, pixel_y, zl) ⇒ Object



320
321
322
# File 'lib/wu/geo/geolocated.rb', line 320

def pixel_xy_zl_to_lng_lat(pixel_x, pixel_y, zl)
  tile_xy_zl_to_lng_lat(pixel_x.to_f / TILE_PIXEL_SIZE, pixel_y.to_f / TILE_PIXEL_SIZE, zl)
end

.point_east(longitude, latitude, distance) ⇒ Object

From a given point, calculate the change in degrees directly east a given distance



282
283
284
285
286
# File 'lib/wu/geo/geolocated.rb', line 282

def point_east(longitude, latitude, distance)
  radius = EARTH_RADIUS * Math.sin(((Math::PI / 2.0) - latitude.to_radians.abs))
  east_lng = (longitude.to_radians + (distance.to_f / radius)).to_degrees
  [east_lng, latitude]
end

.point_north(longitude, latitude, distance) ⇒ Object

From a given point, calculate the point directly north a specified distance



276
277
278
279
# File 'lib/wu/geo/geolocated.rb', line 276

def point_north(longitude, latitude, distance)
  north_lat = (latitude.to_radians + (distance.to_f / EARTH_RADIUS)).to_degrees
  [longitude, north_lat]
end

.quadkey_containing_bbox(left, btm, right, top) ⇒ Object

Retuns the smallest quadkey containing both of corners of the given bounding box



219
220
221
222
223
224
225
226
227
228
229
# File 'lib/wu/geo/geolocated.rb', line 219

def quadkey_containing_bbox(left, btm, right, top)
  qk_tl = lng_lat_zl_to_quadkey(left,  top, 23)
  qk_2  = lng_lat_zl_to_quadkey(right, btm, 23)
  # the containing qk is the longest one that both agree on
  containing_key = ""
  qk_tl.chars.zip(qk_2.chars).each do |char_tl, char_2|
    break if char_tl != char_2
    containing_key << char_tl
  end
  containing_key
end

.quadkey_to_bbox(quadkey) ⇒ Object

Convert a quadkey into a bounding box using adjacent tile



210
211
212
213
214
215
216
# File 'lib/wu/geo/geolocated.rb', line 210

def quadkey_to_bbox(quadkey)
  tile_x, tile_y, zl = quadkey_to_tile_xy_zl(quadkey)
  # bottom right of me is top left of my southeast neighbor
  left,  top = tile_xy_zl_to_lng_lat(tile_x,     tile_y,     zl)
  right, btm = tile_xy_zl_to_lng_lat(tile_x + 1, tile_y + 1, zl)
  [left, btm, right, top]
end

.quadkey_to_tile_xy_zl(quadkey) ⇒ Object

Convert a quadkey into tile x,y coordinates and level

Raises:

  • (ArgumentError)


170
171
172
173
174
175
176
177
178
179
180
# File 'lib/wu/geo/geolocated.rb', line 170

def quadkey_to_tile_xy_zl(quadkey)
  raise ArgumentError, "Quadkey must contain only the characters 0, 1, 2 or 3: #{quadkey}!" unless quadkey =~ /\A[0-3]*\z/
  zl = quadkey.to_s.length
  tx = 0 ; ty = 0
  quadkey.chars.each do |char|
    ybit, xbit = QUADKEY_TO_BIT[char] # bit order y, x
    tx = (tx << 1) + xbit
    ty = (ty << 1) + ybit
  end
  [tx, ty, zl]
end

.tile_xy_to_pixel_xy(tile_x, tile_y) ⇒ Object

Convert from x,y tile pair into pixel x,y coordinates (top left corner)



316
317
318
# File 'lib/wu/geo/geolocated.rb', line 316

def tile_xy_to_pixel_xy(tile_x, tile_y)
  [tile_x * TILE_PIXEL_SIZE, tile_y * TILE_PIXEL_SIZE]
end

.tile_xy_zl_to_lng_lat(tile_x, tile_y, zl) ⇒ Object

Convert from tile_x, tile_y, zoom level to longitude and latitude in degrees (slight loss of precision).

Tile coordinates may be floats or integer; they must lie within map range.

Raises:

  • (ArgumentError)


138
139
140
141
142
143
144
145
146
# File 'lib/wu/geo/geolocated.rb', line 138

def tile_xy_zl_to_lng_lat(tile_x, tile_y, zl)
  tile_size = map_tile_size(zl)
  raise ArgumentError, "tile index must be within bounds ((#{tile_x},#{tile_y}) vs #{tile_size})" unless ((0..(tile_size-1)).include?(tile_x)) && ((0..(tile_size-1)).include?(tile_x))
  xx =       (tile_x.to_f / tile_size)
  yy = 0.5 - (tile_y.to_f / tile_size)
  lng = 360.0 * xx - 180.0
  lat = 90 - 360 * Math.atan(Math.exp(-yy * 2 * Math::PI)) / Math::PI
  [lng, lat]
end

.tile_xy_zl_to_packed_qk(tile_x, tile_y, zl) ⇒ Object

Convert from tile x,y into a packed quadkey at a specified zoom level



183
184
185
186
187
188
# File 'lib/wu/geo/geolocated.rb', line 183

def tile_xy_zl_to_packed_qk(tile_x, tile_y, zl)
  # don't optimize unless you're sure your way is faster; string ops are
  # faster than you think and loops are slower than you think
  quadkey_str = tile_xy_zl_to_quadkey(tile_x, tile_y, zl)
  quadkey_str.to_i(4)
end

.tile_xy_zl_to_quadkey(tile_x, tile_y, zl) ⇒ Object

Convert from tile x,y into a quadkey at a specified zoom level



158
159
160
161
162
163
164
165
166
167
# File 'lib/wu/geo/geolocated.rb', line 158

def tile_xy_zl_to_quadkey(tile_x, tile_y, zl)
  quadkey_chars = []
  tx = tile_x.to_i
  ty = tile_y.to_i
  zl.times do
    quadkey_chars.push BIT_TO_QUADKEY[[ty.odd?, tx.odd?]] # bit order y,x
    tx >>= 1 ; ty >>= 1
  end
  quadkey_chars.join.reverse
end