simple_mercator_location 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/simple_mercator_location.rb +137 -0
- data/spec/lib/simple_mercator_location_spec.rb +130 -0
- data/spec/spec_helper.rb +19 -0
- metadata +66 -0
@@ -0,0 +1,137 @@
|
|
1
|
+
class SimpleMercatorLocation
|
2
|
+
|
3
|
+
attr_accessor :lat_deg, :lon_deg, :zoom
|
4
|
+
|
5
|
+
def initialize(*args)
|
6
|
+
@zoom = 2
|
7
|
+
@lat_deg = 0
|
8
|
+
@lon_deg = 0
|
9
|
+
|
10
|
+
if args.first.is_a?(Hash)
|
11
|
+
@lat_deg = args.first[:lat] if args.first.has_key?(:lat)
|
12
|
+
@lon_deg = args.first[:lon] if args.first.has_key?(:lon)
|
13
|
+
@zoom = args.first[:zoom] if args.first.has_key?(:zoom)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
# latitude in radiants
|
19
|
+
def lat_rad
|
20
|
+
shift_to_rad * lat_deg
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
# longitude in radiants
|
25
|
+
def lon_rad
|
26
|
+
shift_to_rad * lon_deg
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
# factor for scaling to radiants
|
31
|
+
def shift_to_rad
|
32
|
+
Rational(Math::PI, 180)
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
# factor for scaling to degrees
|
37
|
+
def shift_to_deg
|
38
|
+
Rational(180.0, Math::PI)
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# size of our tiles
|
43
|
+
def tile_size
|
44
|
+
256
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
# earth radius in meters
|
49
|
+
def earth_radius
|
50
|
+
6378137
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
# set the zoom-level and returns self
|
55
|
+
def zoom_at(scale)
|
56
|
+
@zoom = scale
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
# origin of google map lieves on top-left. For getting pixels,
|
62
|
+
# we assume displaying the earh an zoomm-level 0 and a square tile
|
63
|
+
# of tile_size * tile_size pixels. The origin of our wsg84 system
|
64
|
+
# (lat, lon = (0,0) sits and (128,128)px.
|
65
|
+
def origin_px
|
66
|
+
[tile_size / 2, tile_size / 2]
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
# return the pixels per degree
|
71
|
+
def pixel_per_degree
|
72
|
+
Rational(tile_size, 360)
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
# return the pixels per radiant
|
77
|
+
def pixel_per_rad
|
78
|
+
tile_size / (2 * Math::PI) # tile_size / (360 * shift_to_rad)
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
# scale the latitude via mercator projections
|
83
|
+
# see http://en.wikipedia.org/wiki/Mercator_projection#Derivation_of_the_Mercator_projection
|
84
|
+
# return the scaled latitude as radiant
|
85
|
+
def lat_scaled_rad
|
86
|
+
Math.log( Math.tan( Rational(Math::PI,4) + Rational(lat_rad,2) ))
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
# returns the scaled latitude as degrees
|
91
|
+
def lat_scaled_deg
|
92
|
+
lat_scaled_rad * shift_to_deg
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
# returns the mercotors meters count for the location
|
97
|
+
def to_m
|
98
|
+
mx = earth_radius * lon_rad
|
99
|
+
my = earth_radius * lat_scaled_rad
|
100
|
+
|
101
|
+
my = (my.round(8) == 0)? 0 : my
|
102
|
+
|
103
|
+
[mx.to_f, my.to_f]
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
# calculates the google world-coordinates as described here:
|
108
|
+
# https://developers.google.com/maps/documentation/javascript/examples/map-coordinates
|
109
|
+
def to_w
|
110
|
+
px = origin_px.first + pixel_per_rad * lon_rad
|
111
|
+
py = origin_px.last - pixel_per_rad * lat_scaled_rad
|
112
|
+
|
113
|
+
return [px, py]
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
# returns the pixel coordinates at the given zoom level
|
118
|
+
def to_px
|
119
|
+
tiles_count = 2**zoom
|
120
|
+
wx,wy = self.to_w
|
121
|
+
px = wx * tiles_count
|
122
|
+
py = wy * tiles_count
|
123
|
+
return [px.to_i, py.to_i]
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
# calculates the tile numbers for google maps at the given zoom level
|
128
|
+
def to_tile
|
129
|
+
px,py = self.to_px
|
130
|
+
|
131
|
+
tx = Rational(px, tile_size).to_i
|
132
|
+
ty = Rational(py, tile_size).to_i
|
133
|
+
|
134
|
+
return [tx,ty]
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe SimpleMercatorLocation do
|
4
|
+
|
5
|
+
describe "accessors" do
|
6
|
+
|
7
|
+
let(:loc) { SimpleMercatorLocation.new }
|
8
|
+
|
9
|
+
attrs = [:lat_deg, :lon_deg, :zoom]
|
10
|
+
|
11
|
+
attrs.each do |attr|
|
12
|
+
it "should have getter and setter for #{attr}" do
|
13
|
+
loc.should respond_to("#{attr}".to_sym)
|
14
|
+
loc.should respond_to("#{attr}=".to_sym)
|
15
|
+
loc.send("#{attr}=".to_sym, "asd")
|
16
|
+
loc.send("#{attr}".to_sym).should eql "asd"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
describe "#initialize" do
|
23
|
+
context "without args" do
|
24
|
+
|
25
|
+
let(:loc) { SimpleMercatorLocation.new }
|
26
|
+
|
27
|
+
it "returns a location object" do
|
28
|
+
loc.should be_an_instance_of(SimpleMercatorLocation)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
context "given a hash with lat, lon and zoom" do
|
34
|
+
let(:loc) { SimpleMercatorLocation.new(lat: 1, lon: 2, zoom: 12) }
|
35
|
+
|
36
|
+
it "returns a location object" do
|
37
|
+
loc.should be_an_instance_of(SimpleMercatorLocation)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "return a location object with lat and lon set" do
|
41
|
+
loc.lat_deg.should eql 1
|
42
|
+
loc.lon_deg.should eql 2
|
43
|
+
loc.zoom.should eql 12
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
describe "#zoom_at" do
|
50
|
+
let(:loc) { SimpleMercatorLocation.new }
|
51
|
+
it "sets the zoom scale and returns self" do
|
52
|
+
loc.zoom_at(15).should eql loc
|
53
|
+
loc.zoom_at(15).zoom.should eql 15
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "to_m" do
|
58
|
+
context "given no args (so using lat = 0 and lon = 0)" do
|
59
|
+
let(:loc) { SimpleMercatorLocation.new }
|
60
|
+
it "should return origin" do
|
61
|
+
loc.to_m.should eql [0.0,0.0]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context "given a location" do
|
66
|
+
places =
|
67
|
+
[
|
68
|
+
{ lat: 48.16608541901253, lon: 11.6455078125, mx: 1296371.99971659, my: 6134530.14205511 },
|
69
|
+
{ lat: 48.10743118848038, lon: 11.42578125, mx: 1271912.15066533, my: 6124746.20243460 },
|
70
|
+
{ lat: 48.22467264956519, lon: 12.12890625, mx: 1350183.66762935, my: 6144314.08167561 },
|
71
|
+
{ lat: 0, lon: 0, mx: 0.0, my: 0.0 },
|
72
|
+
]
|
73
|
+
places.each do |place|
|
74
|
+
it "calculates the mercator projection of (lat: #{place[:lat]}, lon: #{place[:lon]}) to (meters x: #{place[:mx]}, meters y: #{place[:my]})" do
|
75
|
+
meters = SimpleMercatorLocation.new(lon: place[:lon], lat: place[:lat]).to_m
|
76
|
+
meters.map!{|m| m.round(8) }.should eql([place[:mx], place[:my]])
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "to_w" do
|
83
|
+
places =
|
84
|
+
[
|
85
|
+
{ lat: 41.850033, lon: -87.65005229999997, wx: 65.67107392000001, wy: 95.1748950436046 },
|
86
|
+
#{ lat: 48.10743118848038, lon: 11.42578125, wx: 1271912.15066533, wy: 6124746.20243460 },
|
87
|
+
#{ lat: 48.22467264956519, lon: 12.12890625, wx: 1350183.66762935, wy: 6144314.08167561 },
|
88
|
+
#{ lat: 0, lon: 0, wx: 0.0, wy: 0.0 },
|
89
|
+
]
|
90
|
+
places.each do |place|
|
91
|
+
it "calculates the mercator projection of (lat: #{place[:lat]}, lon: #{place[:lon]}) to (world coordinate x: #{place[:wx]}, world coordinate y: #{place[:wy]})" do
|
92
|
+
SimpleMercatorLocation.new(lon: place[:lon], lat: place[:lat]).to_w.should eql([place[:wx], place[:wy]])
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
describe "to_px" do
|
101
|
+
places =
|
102
|
+
[
|
103
|
+
{ zoom: 11, lat: 49.38237278700955, lon: 8.61328125, px: 274688, py: 179200 },
|
104
|
+
{ zoom: 14, lat: 49.38237278700955, lon: 8.61328125, px: 2197504, py: 1433600 },
|
105
|
+
]
|
106
|
+
|
107
|
+
places.each do |place|
|
108
|
+
it "calculates the pixels of (lat: #{place[:lat]}, lon: #{place[:lon]}) to (px: #{place[:px]}, py: #{place[:py]})" do
|
109
|
+
SimpleMercatorLocation.new(lon: place[:lon], lat: place[:lat], zoom: place[:zoom]).to_px.should eql([place[:px], place[:py]])
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
describe "to_tile" do
|
116
|
+
places =
|
117
|
+
[
|
118
|
+
{ zoom: 11, lat: 49.412758, lon: 8.671938, tx: 1073, ty: 699 },
|
119
|
+
{ zoom: 11, lat: 40.689359, lon: -74.045197, tx: 602, ty: 770 },
|
120
|
+
{ zoom: 15, lat: 40.689359, lon: -74.045197, tx: 9644, ty: 12322 },
|
121
|
+
]
|
122
|
+
|
123
|
+
places.each do |place|
|
124
|
+
it "calculates the tile of (lat: #{place[:lat]}, lon: #{place[:lon]}) to (tile x: #{place[:tx]}, tile y: #{place[:ty]})" do
|
125
|
+
SimpleMercatorLocation.new(lon: place[:lon], lat: place[:lat], zoom: place[:zoom]).to_tile.should eql([place[:tx], place[:ty]])
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require "rspec"
|
2
|
+
require "awesome_print"
|
3
|
+
|
4
|
+
RSpec.configure do |config|
|
5
|
+
config.color_enabled = true
|
6
|
+
config.filter_run :focus => true
|
7
|
+
config.run_all_when_everything_filtered = true
|
8
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
9
|
+
end
|
10
|
+
|
11
|
+
def timed(name)
|
12
|
+
start = Time.now
|
13
|
+
puts "\n[STARTED: #{name}]"
|
14
|
+
yield if block_given?
|
15
|
+
finish = Time.now
|
16
|
+
puts "[FINISHED: #{name} in #{(finish - start) * 1000} milliseconds]"
|
17
|
+
end
|
18
|
+
|
19
|
+
require "simple_mercator_location"
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: simple_mercator_location
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Roman Lehnert
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-06-29 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '2.5'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '2.5'
|
30
|
+
description: Converts WSG84 Coordinates via Mercator-projection to meters and tiles
|
31
|
+
email: roman.lehnert@googlemail.com
|
32
|
+
executables: []
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- lib/simple_mercator_location.rb
|
37
|
+
- spec/lib/simple_mercator_location_spec.rb
|
38
|
+
- spec/spec_helper.rb
|
39
|
+
homepage: https://github.com/romanlehnert/simple_mercator_location
|
40
|
+
licenses:
|
41
|
+
- MIT
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options: []
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
none: false
|
48
|
+
requirements:
|
49
|
+
- - ! '>='
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ! '>='
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '0'
|
58
|
+
requirements: []
|
59
|
+
rubyforge_project:
|
60
|
+
rubygems_version: 1.8.25
|
61
|
+
signing_key:
|
62
|
+
specification_version: 3
|
63
|
+
summary: A tiny lib for the mercator projecton
|
64
|
+
test_files:
|
65
|
+
- spec/lib/simple_mercator_location_spec.rb
|
66
|
+
- spec/spec_helper.rb
|