simple_mercator_location 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|