mapstatic 0.0.1
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/Gemfile +3 -0
- data/Gemfile.lock +54 -0
- data/bin/mapstatic +7 -0
- data/lib/mapstatic/cli.rb +52 -0
- data/lib/mapstatic/conversion.rb +28 -0
- data/lib/mapstatic/map.rb +158 -0
- data/lib/mapstatic/tile.rb +14 -0
- data/lib/mapstatic/tile_source.rb +60 -0
- data/lib/mapstatic/version.rb +3 -0
- data/lib/mapstatic.rb +8 -0
- data/spec/fixtures/maps/london.png +0 -0
- data/spec/fixtures/maps/thames.png +0 -0
- data/spec/fixtures/vcr_cassettes/osm-london.yml +12138 -0
- data/spec/fixtures/vcr_cassettes/osm-thames.yml +17295 -0
- data/spec/models/map_spec.rb +91 -0
- data/spec/spec_helper.rb +7 -0
- metadata +189 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
mapstatic (0.0.1)
|
5
|
+
awesome_print (~> 1.2.0)
|
6
|
+
faraday (~> 0.8.8)
|
7
|
+
mini_magick (~> 3.6.0)
|
8
|
+
thor (~> 0.18.1)
|
9
|
+
typhoeus (~> 0.6.6)
|
10
|
+
|
11
|
+
GEM
|
12
|
+
remote: https://rubygems.org/
|
13
|
+
specs:
|
14
|
+
addressable (2.3.5)
|
15
|
+
awesome_print (1.2.0)
|
16
|
+
crack (0.4.1)
|
17
|
+
safe_yaml (~> 0.9.0)
|
18
|
+
diff-lcs (1.2.4)
|
19
|
+
ethon (0.6.1)
|
20
|
+
ffi (>= 1.3.0)
|
21
|
+
mime-types (~> 1.18)
|
22
|
+
faraday (0.8.8)
|
23
|
+
multipart-post (~> 1.2.0)
|
24
|
+
ffi (1.9.3)
|
25
|
+
mime-types (1.25.1)
|
26
|
+
mini_magick (3.6.0)
|
27
|
+
subexec (~> 0.2.1)
|
28
|
+
multipart-post (1.2.0)
|
29
|
+
rspec (2.13.0)
|
30
|
+
rspec-core (~> 2.13.0)
|
31
|
+
rspec-expectations (~> 2.13.0)
|
32
|
+
rspec-mocks (~> 2.13.0)
|
33
|
+
rspec-core (2.13.1)
|
34
|
+
rspec-expectations (2.13.0)
|
35
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
36
|
+
rspec-mocks (2.13.1)
|
37
|
+
safe_yaml (0.9.7)
|
38
|
+
subexec (0.2.3)
|
39
|
+
thor (0.18.1)
|
40
|
+
typhoeus (0.6.6)
|
41
|
+
ethon (~> 0.6.1)
|
42
|
+
vcr (2.7.0)
|
43
|
+
webmock (1.15.2)
|
44
|
+
addressable (>= 2.2.7)
|
45
|
+
crack (>= 0.3.2)
|
46
|
+
|
47
|
+
PLATFORMS
|
48
|
+
ruby
|
49
|
+
|
50
|
+
DEPENDENCIES
|
51
|
+
mapstatic!
|
52
|
+
rspec (~> 2.13.0)
|
53
|
+
vcr (~> 2.7.0)
|
54
|
+
webmock (~> 1.15.2)
|
data/bin/mapstatic
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'mapstatic'
|
2
|
+
require 'awesome_print'
|
3
|
+
require 'thor'
|
4
|
+
|
5
|
+
class Mapstatic::CLI < Thor
|
6
|
+
|
7
|
+
desc "map FILENAME", "Generate a map"
|
8
|
+
long_desc <<-LONGDESC
|
9
|
+
`mapstatic map FILENAME` will create a new static map.
|
10
|
+
|
11
|
+
A map can be created in two ways:
|
12
|
+
|
13
|
+
1. With a bounding box, e.g.
|
14
|
+
|
15
|
+
$ mapstatic map uk.png --zoom=6 --bbox=-10.93,49.64,3.15,59.57
|
16
|
+
|
17
|
+
When creating a map with a bounding box, the width and height of the map
|
18
|
+
will be determined by the zoom level.
|
19
|
+
|
20
|
+
2. With a center lat, lng, width and height, e.g.
|
21
|
+
|
22
|
+
$ mapstatic map greenwich.png --zoom=12 \
|
23
|
+
--lat=51.477222 \
|
24
|
+
--lng=0 \
|
25
|
+
--width=700 \
|
26
|
+
--height=700
|
27
|
+
|
28
|
+
By default, the map will be generated with the OpenStreetMap tile set (Copyright
|
29
|
+
OpenStreetMap contributors).
|
30
|
+
|
31
|
+
You can generate a map using any tile set by passing the --provider option.
|
32
|
+
LONGDESC
|
33
|
+
|
34
|
+
option :zoom, :required => true
|
35
|
+
option :provider, :default => 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
|
36
|
+
option :bbox
|
37
|
+
option :lat
|
38
|
+
option :lng
|
39
|
+
option :width, :default => 256
|
40
|
+
option :height, :default => 256
|
41
|
+
option :dryrun, :type => :boolean, :default => false
|
42
|
+
|
43
|
+
def map(filename)
|
44
|
+
params = Hash[options.map{|(k,v)| [k.to_sym,v]}]
|
45
|
+
|
46
|
+
map = Mapstatic::Map.new(params)
|
47
|
+
|
48
|
+
map.render_map(filename) unless options[:dryrun]
|
49
|
+
ap map.metadata
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Mapstatic
|
2
|
+
|
3
|
+
class Conversion
|
4
|
+
|
5
|
+
def lng_to_x(lng, zoom)
|
6
|
+
n = 2 ** zoom
|
7
|
+
((lng.to_f + 180) / 360) * n
|
8
|
+
end
|
9
|
+
|
10
|
+
def x_to_lng(x, zoom)
|
11
|
+
n = 2.0 ** zoom
|
12
|
+
lon_deg = x / n * 360.0 - 180.0
|
13
|
+
end
|
14
|
+
|
15
|
+
def lat_to_y(lat, zoom)
|
16
|
+
n = 2 ** zoom
|
17
|
+
lat_rad = (lat / 180) * Math::PI
|
18
|
+
(1 - Math.log( Math.tan(lat_rad) + (1 / Math.cos(lat_rad)) ) / Math::PI) / 2 * n
|
19
|
+
end
|
20
|
+
|
21
|
+
def y_to_lat(y, zoom)
|
22
|
+
n = 2.0 ** zoom
|
23
|
+
lat_rad = Math.atan(Math.sinh(Math::PI * (1 - 2 * y / n)))
|
24
|
+
lat_deg = lat_rad / (Math::PI / 180.0)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'mini_magick'
|
2
|
+
|
3
|
+
module Mapstatic
|
4
|
+
|
5
|
+
class Map
|
6
|
+
TILE_SIZE = 256
|
7
|
+
|
8
|
+
attr_reader :zoom, :lat, :lng, :width, :height
|
9
|
+
attr_accessor :tile_source
|
10
|
+
|
11
|
+
def initialize(params={})
|
12
|
+
if params[:bbox]
|
13
|
+
@bounding_box = params[:bbox].split(',').map(&:to_f)
|
14
|
+
else
|
15
|
+
@lat = params.fetch(:lat).to_f
|
16
|
+
@lng = params.fetch(:lng).to_f
|
17
|
+
@width = params.fetch(:width).to_f
|
18
|
+
@height = params.fetch(:height).to_f
|
19
|
+
end
|
20
|
+
@zoom = params.fetch(:zoom).to_i
|
21
|
+
@tile_source = TileSource.new(params[:provider])
|
22
|
+
end
|
23
|
+
|
24
|
+
def width
|
25
|
+
@width ||= begin
|
26
|
+
left, bottom, right, top = bounding_box_in_tiles
|
27
|
+
(right - left) * TILE_SIZE
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def height
|
32
|
+
@height ||= begin
|
33
|
+
left, bottom, right, top = bounding_box_in_tiles
|
34
|
+
(bottom - top) * TILE_SIZE
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def render_map(filename)
|
39
|
+
base_image = create_uncropped_image
|
40
|
+
base_image = fill_image_with_tiles(base_image)
|
41
|
+
crop_to_size base_image
|
42
|
+
base_image.write filename
|
43
|
+
end
|
44
|
+
|
45
|
+
def metadata
|
46
|
+
{
|
47
|
+
:bbox => bounding_box.join(','),
|
48
|
+
:width => width.to_i,
|
49
|
+
:height => height.to_i,
|
50
|
+
:zoom => zoom,
|
51
|
+
:number_of_tiles => required_tiles.length,
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def x_tile_space
|
58
|
+
Conversion.new.lng_to_x(lng, zoom)
|
59
|
+
end
|
60
|
+
|
61
|
+
def y_tile_space
|
62
|
+
Conversion.new.lat_to_y(lat, zoom)
|
63
|
+
end
|
64
|
+
|
65
|
+
def width_tile_space
|
66
|
+
width / TILE_SIZE
|
67
|
+
end
|
68
|
+
|
69
|
+
def height_tile_space
|
70
|
+
height / TILE_SIZE
|
71
|
+
end
|
72
|
+
|
73
|
+
def bounding_box
|
74
|
+
@bounding_box ||= begin
|
75
|
+
converter = Conversion.new
|
76
|
+
left = converter.x_to_lng( x_tile_space - (width_tile_space / 2), zoom)
|
77
|
+
right = converter.x_to_lng( x_tile_space + ( width_tile_space / 2 ), zoom)
|
78
|
+
top = converter.y_to_lat( y_tile_space - ( height_tile_space / 2 ), zoom)
|
79
|
+
bottom = converter.y_to_lat( y_tile_space + ( height_tile_space / 2 ), zoom)
|
80
|
+
|
81
|
+
[ left, bottom, right, top ]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def bounding_box_in_tiles
|
86
|
+
left, bottom, right, top = bounding_box
|
87
|
+
converter = Conversion.new
|
88
|
+
[
|
89
|
+
converter.lng_to_x(left, zoom),
|
90
|
+
converter.lat_to_y(bottom, zoom),
|
91
|
+
converter.lng_to_x(right, zoom),
|
92
|
+
converter.lat_to_y(top, zoom)
|
93
|
+
]
|
94
|
+
end
|
95
|
+
|
96
|
+
def required_x_tiles
|
97
|
+
left, bottom, right, top = bounding_box_in_tiles
|
98
|
+
Range.new(*[left, right].map(&:floor)).to_a
|
99
|
+
end
|
100
|
+
|
101
|
+
def required_y_tiles
|
102
|
+
left, bottom, right, top = bounding_box_in_tiles
|
103
|
+
Range.new(*[top, bottom].map(&:floor)).to_a
|
104
|
+
end
|
105
|
+
|
106
|
+
def required_tiles
|
107
|
+
required_y_tiles.map do |y|
|
108
|
+
required_x_tiles.map{|x| Tile.new(x,y,zoom) }
|
109
|
+
end.flatten
|
110
|
+
end
|
111
|
+
|
112
|
+
def map_tiles
|
113
|
+
@map_tiles ||= tile_source.get_tiles(required_tiles)
|
114
|
+
end
|
115
|
+
|
116
|
+
def crop_to_size(image)
|
117
|
+
distance_from_left = (bounding_box_in_tiles[0] - required_x_tiles[0]) * TILE_SIZE
|
118
|
+
distance_from_top = (bounding_box_in_tiles[3] - required_y_tiles[0]) * TILE_SIZE
|
119
|
+
|
120
|
+
image.crop "#{width}x#{height}+#{distance_from_left}+#{distance_from_top}"
|
121
|
+
end
|
122
|
+
|
123
|
+
def create_uncropped_image
|
124
|
+
image = MiniMagick::Image.read(map_tiles[0])
|
125
|
+
|
126
|
+
uncropped_width = required_x_tiles.length * TILE_SIZE
|
127
|
+
uncropped_height = required_y_tiles.length * TILE_SIZE
|
128
|
+
|
129
|
+
image.combine_options do |c|
|
130
|
+
c.background 'none'
|
131
|
+
c.extent [uncropped_width,uncropped_height].join('x')
|
132
|
+
end
|
133
|
+
|
134
|
+
image
|
135
|
+
end
|
136
|
+
|
137
|
+
def fill_image_with_tiles(image)
|
138
|
+
start = 0
|
139
|
+
|
140
|
+
required_y_tiles.length.times do |row|
|
141
|
+
length = required_x_tiles.length
|
142
|
+
|
143
|
+
map_tiles.slice(start, length).each_with_index do |tile, column|
|
144
|
+
image = image.composite( MiniMagick::Image.read(tile) ) do |c|
|
145
|
+
c.geometry "+#{ (column) * TILE_SIZE }+#{ (row) * TILE_SIZE }"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
start += length
|
150
|
+
end
|
151
|
+
|
152
|
+
image
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'typhoeus'
|
3
|
+
require 'typhoeus/adapters/faraday'
|
4
|
+
|
5
|
+
module Mapstatic
|
6
|
+
|
7
|
+
class TileSource
|
8
|
+
|
9
|
+
attr_reader :url
|
10
|
+
|
11
|
+
def initialize(url)
|
12
|
+
@url = url
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_tile(tile)
|
16
|
+
connection.get(tile_url(tile)).body
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_tiles(tiles)
|
20
|
+
responses = []
|
21
|
+
|
22
|
+
connection.in_parallel do
|
23
|
+
tiles.each do |tile|
|
24
|
+
responses << connection.get(tile_url(tile))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
responses.map(&:body)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :connection
|
34
|
+
|
35
|
+
def tile_url(tile)
|
36
|
+
url.
|
37
|
+
gsub(/\{x\}/,tile.x.to_s).
|
38
|
+
gsub(/\{y\}/,tile.y.to_s).
|
39
|
+
gsub(/\{z\}/,tile.zoom.to_s).
|
40
|
+
gsub(/\{s\}/,subdomain_for_tile(tile))
|
41
|
+
end
|
42
|
+
|
43
|
+
def subdomain_for_tile(tile)
|
44
|
+
i = (tile.x + tile.y) % subdomains.length
|
45
|
+
subdomains[i]
|
46
|
+
end
|
47
|
+
|
48
|
+
def subdomains
|
49
|
+
['a','b','c']
|
50
|
+
end
|
51
|
+
|
52
|
+
def connection
|
53
|
+
@connection ||= Faraday.new do |builder|
|
54
|
+
builder.adapter :typhoeus
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
data/lib/mapstatic.rb
ADDED
Binary file
|
Binary file
|