gpx2exif 0.0.4 → 0.1.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.
@@ -0,0 +1,51 @@
1
+ require 'rubygems'
2
+ require 'gpx2png/osm_base'
3
+
4
+ $:.unshift(File.dirname(__FILE__))
5
+
6
+ module Gpx2png
7
+ class Osm < OsmBase
8
+
9
+ DEFAULT_RENDERER = :rmagick
10
+ attr_accessor :renderer
11
+
12
+ def initialize
13
+ super
14
+ @renderer ||= DEFAULT_RENDERER
15
+ @r = nil
16
+ end
17
+
18
+ def save(filename)
19
+ render
20
+ @r.save(filename)
21
+ end
22
+
23
+ def to_png
24
+ render
25
+ @r.to_png
26
+ end
27
+
28
+ def render
29
+ setup_renderer
30
+ initial_calculations
31
+ download_and_join_tiles
32
+ end
33
+
34
+ attr_accessor :renderer_options
35
+
36
+ # Get proper renderer class
37
+ def setup_renderer
38
+ case @renderer
39
+ when :chunky_png
40
+ require 'gpx2png/renderers/chunky_png_renderer'
41
+ @r = ChunkyPngRenderer.new(@renderer_options)
42
+ when :rmagick
43
+ require 'gpx2png/renderers/rmagick_renderer'
44
+ @r = RmagickRenderer.new(@renderer_options)
45
+ else
46
+ raise ArgumentError
47
+ end
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,280 @@
1
+ require 'rubygems'
2
+ require 'gpx2png/base'
3
+ require 'net/http'
4
+ require "uri"
5
+
6
+ $:.unshift(File.dirname(__FILE__))
7
+
8
+ module Gpx2png
9
+ class OsmBase < Base
10
+ TILE_WIDTH = 256
11
+ TILE_HEIGHT = 256
12
+
13
+ # if true it will not download tiles
14
+ attr_accessor :simulate_download
15
+
16
+ # http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#X_and_Y
17
+ # Convert latlon deg to OSM tile coords
18
+ def self.convert(zoom, coord)
19
+ lat_deg, lon_deg = coord
20
+ lat_rad = deg2rad(lat_deg)
21
+ x = (((lon_deg + 180) / 360) * (2 ** zoom)).floor
22
+ y = ((1 - Math.log(Math.tan(lat_rad) + 1 / Math.cos(lat_rad)) / Math::PI) /2 * (2 ** zoom)).floor
23
+
24
+ return [x, y]
25
+ end
26
+
27
+ # Convert latlon deg to OSM tile url
28
+ # TODO add algorithm to choose from diff. servers
29
+ def self.url_convert(zoom, coord, server = 'b.')
30
+ x, y = convert(zoom, coord)
31
+ url(zoom, [x, y], server)
32
+ end
33
+
34
+ # Convert OSM tile coords to url
35
+ def self.url(zoom, coord, server = 'b.')
36
+ x, y = coord
37
+ url = "http://#{server}tile.openstreetmap.org\/#{zoom}\/#{x}\/#{y}.png"
38
+ return url
39
+ end
40
+
41
+ # Convert OSM tile coords to latlon deg in top-left corner
42
+ def self.reverse_convert(zoom, coord)
43
+ x, y = coord
44
+ n = 2 ** zoom
45
+ lon_deg = x.to_f / n.to_f * 360.0 - 180.0
46
+ lat_deg = rad2deg(Math.atan(Math.sinh(Math::PI * (1.to_f - 2.to_f * y.to_f / n.to_f))))
47
+ return [lat_deg, lon_deg]
48
+ end
49
+
50
+ # Lazy calc proper zoom for drawing
51
+ def self.calc_zoom(lat_min, lat_max, lon_min, lon_max, width, height)
52
+ # because I'm lazy! :] and math is not my best side
53
+
54
+ last_zoom = 2
55
+ (5..18).each do |zoom|
56
+ # calculate drawing tile size and pixel size
57
+ tile_min = point_on_absolute_image(zoom, [lat_min, lon_min])
58
+ tile_max = point_on_absolute_image(zoom, [lat_max, lon_max])
59
+ current_pixel_x_distance = tile_max[0] - tile_min[0]
60
+ current_pixel_y_distance = tile_min[1] - tile_max[1]
61
+ if current_pixel_x_distance > width or current_pixel_y_distance > height
62
+ return last_zoom
63
+ end
64
+ last_zoom = zoom
65
+ end
66
+ return 18
67
+ end
68
+
69
+ # Convert latlon deg coords to image point (x,y) and OSM tile coord
70
+ # return where you should put point on tile
71
+ def self.point_on_image(zoom, geo_coord)
72
+ osm_tile_coord = convert(zoom, geo_coord)
73
+ top_left_corner = reverse_convert(zoom, osm_tile_coord)
74
+ bottom_right_corner = reverse_convert(zoom, [
75
+ osm_tile_coord[0] + 1, osm_tile_coord[1] + 1
76
+ ])
77
+
78
+ # some line math: y = ax + b
79
+
80
+ x_geo = geo_coord[1]
81
+ # offset
82
+ x_offset = x_geo - top_left_corner[1]
83
+ # scale
84
+ x_distance = (bottom_right_corner[1] - top_left_corner[1])
85
+ x = (TILE_WIDTH.to_f * (x_offset / x_distance)).round
86
+
87
+ y_geo = geo_coord[0]
88
+ # offset
89
+ y_offset = y_geo - top_left_corner[0]
90
+ # scale
91
+ y_distance = (bottom_right_corner[0] - top_left_corner[0])
92
+ y = (TILE_HEIGHT.to_f * (y_offset / y_distance)).round
93
+
94
+ return { osm_title_coord: osm_tile_coord, pixel_offset: [x, y] }
95
+ end
96
+
97
+ # Useful for calculating distance on output image
98
+ # It is not position on output image because we don't know tile coords
99
+ # For upper-left tile
100
+ def self.point_on_absolute_image(zoom, geo_coord)
101
+ _p = point_on_image(zoom, geo_coord)
102
+ _x = _p[:osm_title_coord][0] * TILE_WIDTH + _p[:pixel_offset][0]
103
+ _y = _p[:osm_title_coord][1] * TILE_WIDTH + _p[:pixel_offset][1]
104
+ return [_x, _y]
105
+ end
106
+
107
+ # Create image with fixed size
108
+ def fixed_size(_width, _height)
109
+ @fixed_width = _width
110
+ @fixed_height = _height
111
+ end
112
+
113
+ def initial_calculations
114
+ @lat_min = @coords.collect { |c| c[:lat] }.min
115
+ @lat_max = @coords.collect { |c| c[:lat] }.max
116
+ @lon_min = @coords.collect { |c| c[:lon] }.min
117
+ @lon_max = @coords.collect { |c| c[:lon] }.max
118
+
119
+ # auto zoom must be here
120
+ # drawing must fit into fixed resolution
121
+ # map must be bigger than fixed resolution
122
+ if @fixed_width and @fixed_height
123
+ @new_zoom = self.class.calc_zoom(
124
+ @lat_min, @lat_max,
125
+ @lon_min, @lon_max,
126
+ @fixed_width, @fixed_height
127
+ )
128
+ puts "Calculated new zoom #{@new_zoom} (was #{@zoom})" if @verbose
129
+ @zoom = @new_zoom
130
+ end
131
+
132
+ @border_tiles = [
133
+ self.class.convert(@zoom, [@lat_min, @lon_min]),
134
+ self.class.convert(@zoom, [@lat_max, @lon_max])
135
+ ]
136
+
137
+ @tile_x_range = (@border_tiles[0][0])..(@border_tiles[1][0])
138
+ @tile_y_range = (@border_tiles[1][1])..(@border_tiles[0][1])
139
+
140
+ # enlarging ranges to fill up map area
141
+ # both sizes are enlarged
142
+ # = ( ( (preferred size - real size) / tile width ) / 2 ).ceil
143
+ if @fixed_width and @fixed_height
144
+ x_axis_expand_count = ((@fixed_width - (1 + @tile_x_range.max - @tile_x_range.min) * TILE_WIDTH).to_f / (TILE_WIDTH.to_f * 2.0)).ceil
145
+ y_axis_expand_count = ((@fixed_height - (1 + @tile_y_range.max - @tile_y_range.min) * TILE_HEIGHT).to_f / (TILE_HEIGHT.to_f * 2.0)).ceil
146
+ puts "Expanding X tiles from both sides #{x_axis_expand_count}" if @verbose
147
+ puts "Expanding Y tiles from both sides #{y_axis_expand_count}" if @verbose
148
+ @tile_x_range = ((@tile_x_range.min - x_axis_expand_count)..(@tile_x_range.max + x_axis_expand_count))
149
+ @tile_y_range = ((@tile_y_range.min - y_axis_expand_count)..(@tile_y_range.max + y_axis_expand_count))
150
+ end
151
+
152
+ # new/full image size
153
+ @full_image_x = (1 + @tile_x_range.max - @tile_x_range.min) * TILE_WIDTH
154
+ @full_image_y = (1 + @tile_y_range.max - @tile_y_range.min) * TILE_HEIGHT
155
+ @r.x = @full_image_x
156
+ @r.y = @full_image_y
157
+
158
+ if @fixed_width and @fixed_height
159
+ calculate_for_crop_with_auto_zoom
160
+ else
161
+ calculate_for_crop
162
+ end
163
+ end
164
+
165
+ # Calculate zoom level
166
+ def auto_zoom_for(x = 0, y = 0)
167
+ # TODO
168
+ end
169
+
170
+ attr_reader :lat_min, :lat_max, :lon_min, :lon_max
171
+ attr_reader :tile_x_distance, :tile_y_distance
172
+ # points for cropping
173
+ attr_reader :bitmap_point_x_max, :bitmap_point_x_min, :bitmap_point_y_max, :bitmap_point_y_min
174
+
175
+ # Do everything
176
+ def download_and_join_tiles
177
+ puts "Output image dimension #{@full_image_x}x#{@full_image_y}" if @verbose
178
+ @r.new_image
179
+
180
+ # {:x, :y, :blob}
181
+ @images = Array.new
182
+
183
+
184
+ @tile_x_range.each do |x|
185
+ @tile_y_range.each do |y|
186
+ url = self.class.url(@zoom, [x, y])
187
+
188
+ # blob time
189
+ unless @simulate_download
190
+ uri = URI.parse(url)
191
+ response = Net::HTTP.get_response(uri)
192
+ blob = response.body
193
+ else
194
+ blob = @r.blank_tile(TILE_WIDTH, TILE_HEIGHT, x+y)
195
+ end
196
+
197
+ @r.add_tile(
198
+ blob,
199
+ (x - @tile_x_range.min) * TILE_WIDTH,
200
+ (y - @tile_y_range.min) * TILE_HEIGHT
201
+ )
202
+
203
+ @images << {
204
+ url: url,
205
+ x: x,
206
+ y: y
207
+ }
208
+
209
+ puts "processed #{x - @tile_x_range.min}x#{y - @tile_y_range.min} (max #{@tile_x_range.max - @tile_x_range.min}x#{@tile_y_range.max - @tile_y_range.min})" if @verbose
210
+ end
211
+ end
212
+
213
+ # sweet, image is joined
214
+
215
+ # min/max points used for cropping
216
+ @bitmap_point_x_max = (@full_image_x / 2).round
217
+ @bitmap_point_x_min = (@full_image_x / 2).round
218
+ @bitmap_point_y_max = (@full_image_y / 2).round
219
+ @bitmap_point_y_min = (@full_image_y / 2).round
220
+
221
+ # add some coords to the map
222
+ (1...@coords.size).each do |i|
223
+ lat_from = @coords[i-1][:lat]
224
+ lon_from = @coords[i-1][:lon]
225
+
226
+ lat_to = @coords[i][:lat]
227
+ lon_to = @coords[i][:lon]
228
+
229
+ point_from = self.class.point_on_image(@zoom, [lat_from, lon_from])
230
+ point_to = self.class.point_on_image(@zoom, [lat_to, lon_to])
231
+ # { osm_title_coord: osm_tile_coord, pixel_offset: [x, y] }
232
+
233
+ # first point
234
+ bitmap_xa = (point_from[:osm_title_coord][0] - @tile_x_range.min) * TILE_WIDTH + point_from[:pixel_offset][0]
235
+ bitmap_ya = (point_from[:osm_title_coord][1] - @tile_y_range.min) * TILE_HEIGHT + point_from[:pixel_offset][1]
236
+ bitmap_xb = (point_to[:osm_title_coord][0] - @tile_x_range.min) * TILE_WIDTH + point_to[:pixel_offset][0]
237
+ bitmap_yb = (point_to[:osm_title_coord][1] - @tile_y_range.min) * TILE_HEIGHT + point_to[:pixel_offset][1]
238
+
239
+ @r.line(
240
+ bitmap_xa, bitmap_ya,
241
+ bitmap_xb, bitmap_yb
242
+ )
243
+ end
244
+
245
+ #calculate_for_crop
246
+ end
247
+
248
+ # Calculate some numbers for cropping operation
249
+ def calculate_for_crop
250
+ point_min = self.class.point_on_image(@zoom, [@lat_min, @lon_min])
251
+ point_max = self.class.point_on_image(@zoom, [@lat_max, @lon_max])
252
+ @bitmap_point_x_min = (point_min[:osm_title_coord][0] - @tile_x_range.min) * TILE_WIDTH + point_min[:pixel_offset][0]
253
+ @bitmap_point_x_max = (point_max[:osm_title_coord][0] - @tile_x_range.min) * TILE_WIDTH + point_max[:pixel_offset][0]
254
+ @bitmap_point_y_max = (point_min[:osm_title_coord][1] - @tile_y_range.min) * TILE_HEIGHT + point_min[:pixel_offset][1]
255
+ @bitmap_point_y_min = (point_max[:osm_title_coord][1] - @tile_y_range.min) * TILE_HEIGHT + point_max[:pixel_offset][1]
256
+
257
+ @r.set_crop(@bitmap_point_x_min, @bitmap_point_x_max, @bitmap_point_y_min, @bitmap_point_y_max)
258
+ end
259
+
260
+ # Calculate some numbers for cropping operation with autozoom
261
+ def calculate_for_crop_with_auto_zoom
262
+ point_min = self.class.point_on_image(@zoom, [@lat_min, @lon_min])
263
+ point_max = self.class.point_on_image(@zoom, [@lat_max, @lon_max])
264
+ @bitmap_point_x_min = (point_min[:osm_title_coord][0] - @tile_x_range.min) * TILE_WIDTH + point_min[:pixel_offset][0]
265
+ @bitmap_point_x_max = (point_max[:osm_title_coord][0] - @tile_x_range.min) * TILE_WIDTH + point_max[:pixel_offset][0]
266
+ @bitmap_point_y_max = (point_min[:osm_title_coord][1] - @tile_y_range.min) * TILE_HEIGHT + point_min[:pixel_offset][1]
267
+ @bitmap_point_y_min = (point_max[:osm_title_coord][1] - @tile_y_range.min) * TILE_HEIGHT + point_max[:pixel_offset][1]
268
+
269
+ bitmap_x_center = (@bitmap_point_x_min + @bitmap_point_x_max) / 2
270
+ bitmap_y_center = (@bitmap_point_y_min + @bitmap_point_y_max) / 2
271
+
272
+ @r.set_crop_fixed(bitmap_x_center, bitmap_y_center, @fixed_width, @fixed_height)
273
+ end
274
+
275
+ def expand_map
276
+ # TODO expand min and max ranges
277
+ end
278
+
279
+ end
280
+ end
@@ -0,0 +1,61 @@
1
+ require 'rubygems'
2
+ require 'chunky_png'
3
+
4
+ $:.unshift(File.dirname(__FILE__))
5
+
6
+ # This renderer has some/a lot of features missing
7
+ module Gpx2png
8
+ class ChunkyPngRenderer
9
+ def initialize(_options = {})
10
+ @options = _options || {}
11
+ @_color = @options[:color] || '#FF0000'
12
+ @color = ChunkyPNG::Color.from_hex(@_color)
13
+ end
14
+
15
+ attr_accessor :x, :y
16
+
17
+ # Create new (full) image
18
+ def new_image
19
+ @image = ChunkyPNG::Image.new(
20
+ @x,
21
+ @y,
22
+ ChunkyPNG::Color::WHITE
23
+ )
24
+ end
25
+
26
+ # Add one tile to full image
27
+ def add_tile(blob, x_offset, y_offset)
28
+ tile_image = ChunkyPNG::Image.from_blob(blob)
29
+ @image.compose!(
30
+ tile_image,
31
+ x_offset,
32
+ y_offset
33
+ )
34
+ end
35
+
36
+ def line(xa, ya, xb, yb)
37
+ @image.line(
38
+ xa, ya,
39
+ xb, yb,
40
+ @color
41
+ )
42
+ end
43
+
44
+ def set_crop(x_min, x_max, y_min, y_max)
45
+ # TODO
46
+ end
47
+
48
+ def crop
49
+ # TODO
50
+ end
51
+
52
+ def save(filename)
53
+ @image.save(filename)
54
+ end
55
+
56
+ def to_png
57
+ # TODO
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,134 @@
1
+ require 'rubygems'
2
+ require 'RMagick'
3
+
4
+ $:.unshift(File.dirname(__FILE__))
5
+
6
+ module Gpx2png
7
+ class RmagickRenderer
8
+ def initialize(_options = { })
9
+ @options = _options || { }
10
+ @color = @options[:color] || '#FF0000'
11
+ @width = @options[:width] || 3
12
+ @aa = @options[:aa] == true
13
+ @opacity = @options[:opacity] || 1.0
14
+ @licence_string = "Map data OpenStreetMap (CC-by-SA 2.0)"
15
+ @crop_margin = @options[:crop_margin] || 50
16
+ @crop_enabled = @options[:crop_enabled] == true
17
+
18
+ @line = Magick::Draw.new
19
+ @line.stroke_antialias(@aa)
20
+ @line.stroke(@color)
21
+ @line.stroke_opacity(@opacity)
22
+ @line.stroke_width(@width)
23
+
24
+ @licence_text = Magick::Draw.new
25
+ @licence_text.text_antialias(@aa)
26
+ @licence_text.font_family('helvetica')
27
+ @licence_text.font_style(Magick::NormalStyle)
28
+ @licence_text.text_align(Magick::RightAlign)
29
+ @licence_text.pointsize(10)
30
+ end
31
+
32
+ attr_accessor :x, :y
33
+
34
+ # Create new (full) image
35
+ def new_image
36
+ @image = Magick::Image.new(
37
+ @x,
38
+ @y
39
+ )
40
+ end
41
+
42
+ # Add one tile to full image
43
+ def add_tile(blob, x_offset, y_offset)
44
+ tile_image = Magick::Image.from_blob(blob)[0]
45
+ @image = @image.composite(
46
+ tile_image,
47
+ x_offset,
48
+ y_offset,
49
+ Magick::OverCompositeOp
50
+ )
51
+ end
52
+
53
+ def line(xa, ya, xb, yb)
54
+ @line.line(
55
+ xa, ya,
56
+ xb, yb
57
+ )
58
+ end
59
+
60
+ # Setup crop image using CSS padding style data
61
+ def set_crop(x_min, x_max, y_min, y_max)
62
+ # puts @x, @y, @crop_margin, x_min, x_max, y_min, y_max
63
+
64
+ @crop_t = y_min - @crop_margin
65
+ @crop_r = (@x - x_max) - @crop_margin
66
+ @crop_b = (@y - y_max) - @crop_margin
67
+ @crop_l = x_min - @crop_margin
68
+
69
+ @crop_t = 0 if @crop_t < 0
70
+ @crop_r = 0 if @crop_r < 0
71
+ @crop_b = 0 if @crop_b < 0
72
+ @crop_l = 0 if @crop_l < 0
73
+ end
74
+
75
+ # Setup crop for autozoom/fixed size
76
+ def set_crop_fixed(x_center, y_center, width, height)
77
+ @crop_margin = 0
78
+ @crop_enabled = true
79
+
80
+ set_crop(
81
+ x_center - (width / 2),
82
+ x_center + (width / 2),
83
+ y_center - (height / 2),
84
+ y_center + (height / 2)
85
+ )
86
+ end
87
+
88
+ # Setup crop image using CSS padding style data
89
+ def crop!
90
+ return unless @crop_enabled
91
+
92
+ @new_x = @x - @crop_r.to_i - @crop_l.to_i
93
+ @new_y = @y - @crop_b.to_i - @crop_t.to_i
94
+ @image = @image.crop(@crop_l.to_i, @crop_t.to_i, @new_x, @new_y, true)
95
+ # changing image size
96
+ @x = @new_x
97
+ @y = @new_y
98
+ end
99
+
100
+ def render
101
+ @line.draw(@image)
102
+ # crop after drawing lines, before drawing "legend"
103
+ crop!
104
+ # "static" elements
105
+ @licence_text.text(@x - 10, @y - 10, @licence_string)
106
+ @licence_text.draw(@image)
107
+ end
108
+
109
+ def save(filename)
110
+ render
111
+ @image.write(filename)
112
+ end
113
+
114
+ def to_png
115
+ render
116
+ @image.format = 'PNG'
117
+ @image.to_blob
118
+ end
119
+
120
+ def blank_tile(width, height, index = 0)
121
+ _image = Magick::Image.new(
122
+ width,
123
+ height
124
+ ) do |i|
125
+ _color = "#dddddd"
126
+ _color = "#eeeeee" if index % 2 == 0
127
+ i.background_color = _color
128
+ end
129
+ _image.format = 'PNG'
130
+ _image.to_blob
131
+ end
132
+
133
+ end
134
+ end