mapstatic 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
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,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.expand_path("../../lib", __FILE__)
4
+
5
+ require "mapstatic/cli"
6
+
7
+ Mapstatic::CLI.start
@@ -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,14 @@
1
+ module Mapstatic
2
+
3
+ class Tile
4
+ attr_accessor :x, :y, :zoom
5
+
6
+ def initialize(x,y,zoom)
7
+ @x = x.floor
8
+ @y = y.floor
9
+ @zoom = zoom
10
+ end
11
+
12
+ end
13
+
14
+ 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
@@ -0,0 +1,3 @@
1
+ module Mapstatic
2
+ VERSION = '0.0.1'
3
+ end
data/lib/mapstatic.rb ADDED
@@ -0,0 +1,8 @@
1
+ module Mapstatic
2
+ end
3
+
4
+ require 'mapstatic/version'
5
+ require 'mapstatic/conversion'
6
+ require 'mapstatic/map'
7
+ require 'mapstatic/tile'
8
+ require 'mapstatic/tile_source'
Binary file
Binary file