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 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