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.
data/Gemfile CHANGED
@@ -1,7 +1,7 @@
1
1
  source "http://rubygems.org"
2
2
 
3
3
  gem 'nokogiri'
4
- gem 'mini_exiftool', git: 'git://github.com/akwiatkowski/mini_exiftool.git'
4
+ gem 'mini_exiftool'
5
5
  gem 'builder'
6
6
 
7
7
  group :development do
@@ -1,9 +1,3 @@
1
- GIT
2
- remote: git://github.com/akwiatkowski/mini_exiftool.git
3
- revision: e2ca8e0c3de9190bd08520d6c4204d510963eb54
4
- specs:
5
- mini_exiftool (1.5.1)
6
-
7
1
  GEM
8
2
  remote: http://rubygems.org/
9
3
  specs:
@@ -14,8 +8,9 @@ GEM
14
8
  bundler (~> 1.0)
15
9
  git (>= 1.2.5)
16
10
  rake
11
+ mini_exiftool (1.5.1)
17
12
  multi_json (1.3.6)
18
- nokogiri (1.5.3)
13
+ nokogiri (1.5.5)
19
14
  rake (0.9.2.2)
20
15
  rspec (2.3.0)
21
16
  rspec-core (~> 2.3.0)
@@ -37,7 +32,7 @@ DEPENDENCIES
37
32
  builder
38
33
  bundler (~> 1.0.0)
39
34
  jeweler (~> 1.6.4)
40
- mini_exiftool!
35
+ mini_exiftool
41
36
  nokogiri
42
37
  rspec (~> 2.3.0)
43
38
  simplecov
data/README.md CHANGED
@@ -65,6 +65,34 @@ How to use it
65
65
 
66
66
  generate_garmin_waypoints -y samples/sample_yaml_pois.yml -o file.gpx
67
67
 
68
+
69
+ Render track with OpenStreetMap
70
+ ---------------------
71
+
72
+ You can "convert" your tracks to images using this command.
73
+
74
+ How to use it
75
+ -------------
76
+
77
+ 1. Please check if you have installed RMagick gem.
78
+
79
+ 2. Run command.
80
+
81
+ gpx2png -g <input GPX file> -s <image size, format: WIDTHxHEIGHT> -o <output PPNG file>
82
+
83
+ Example:
84
+
85
+ gpx2png -g spec/fixtures/sample.gpx -s 800x600 -o map.png
86
+
87
+ 3. You can specify zoom.
88
+
89
+ gpx2png -g <input GPX file> -z <zoom, best results if between 9 and 15, max 18> -o <output PPNG file>
90
+
91
+ Example:
92
+
93
+ gpx2png -g spec/fixtures/sample.gpx -z 11 -o map.png
94
+
95
+
68
96
  Contributing to gpx2xif
69
97
  -------------------------------
70
98
 
data/Rakefile CHANGED
@@ -22,7 +22,7 @@ Jeweler::Tasks.new do |gem|
22
22
  gem.email = "bobikx@poczta.fm"
23
23
  gem.authors = ["Aleksander Kwiatkowski"]
24
24
  # dependencies defined in Gemfile
25
- gem.executables = ['geotag_all_images', 'geotag_simulate']
25
+ gem.executables = ['geotag_all_images', 'geotag_simulate', 'generate_garmin_waypoints', 'gpx2png']
26
26
 
27
27
  gem.files = FileList[
28
28
  "[A-Z]*", "{bin,generators,lib,test}/**/*"
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.4
1
+ 0.1.0
@@ -22,7 +22,7 @@ OptionParser.new do |opts|
22
22
  end
23
23
  end.parse!
24
24
 
25
- g = GarminUtils::WaypointListGenerator.new
25
+ g = GpxUtils::WaypointsExporter.new
26
26
  if options[:yaml]
27
27
  g.add_yaml_file(options[:yaml])
28
28
  end
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'rubygems'
4
- require 'gpx2exif'
4
+ require 'geotagger/geotagger'
5
5
 
6
6
  puts "Are you sure? It is evil script which probably eat photos of your dog and family. Uppercase 'yes' and enter if you want to continue."
7
7
  str = gets
@@ -12,7 +12,7 @@ time_offset = time_offset.to_i
12
12
 
13
13
  exit(0) unless str.strip == 'YES'
14
14
 
15
- g = Gpx2exif::GeoManager.new
15
+ g = Geotagger::Geotagger.new(verbose: true)
16
16
  g.add_all_files(time_offset)
17
17
  g.match_up
18
18
  g.save!
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'rubygems'
4
- require 'gpx2exif'
4
+ require 'geotagger/geotagger'
5
5
 
6
6
  #puts "Are you sure? It is evil script which probably eat photos of your dog and family. Uppercase 'yes' and enter if you want to continue."
7
7
  #str = gets
@@ -11,7 +11,7 @@ puts "Do you want to add offset to image time? Default is 0 seconds."
11
11
  time_offset = gets
12
12
  time_offset = time_offset.to_i
13
13
 
14
- g = Gpx2exif::GeoManager.new
14
+ g = Geotagger::Geotagger.new(verbose: true)
15
15
  g.add_all_files(time_offset)
16
16
  g.match_up
17
17
  g.simulate
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'gpx2exif'
5
+ require 'gpx2png/osm'
6
+ require 'optparse'
7
+
8
+ options = { }
9
+ OptionParser.new do |opts|
10
+ opts.banner = "Usage: gpx2png [options]"
11
+
12
+ opts.on("-g", "--gpx FILE", "Input GPX file") do |v|
13
+ options[:gpx] = v
14
+ end
15
+ opts.on("-z", "--zoom ZOOM", "Set zoom") do |v|
16
+ options[:zoom] = v
17
+ end
18
+ opts.on("-s", "--size WIDTHxHEIGHT", "Set output image size (better result)") do |v|
19
+ options[:size] = v
20
+ end
21
+ opts.on("-o", "--output FILE", "Output image file") do |v|
22
+ options[:output_file] = v
23
+ end
24
+ end.parse!
25
+
26
+ unless options[:gpx]
27
+ puts "Input GPX file needed"
28
+ @fail = true
29
+ end
30
+ unless options[:output_file]
31
+ puts "Output image file needed"
32
+ @fail = true
33
+ end
34
+ if options[:zoom].nil? and options[:size].nil?
35
+ puts "Zoom or image size needed"
36
+ @fail = true
37
+ end
38
+
39
+ unless @fail
40
+ g = GpxUtils::TrackImporter.new
41
+ g.add_file(options[:gpx])
42
+
43
+ e = Gpx2png::Osm.new
44
+ e.coords = g.coords
45
+
46
+ if options[:size]
47
+ # constant size
48
+ options[:size] =~ /(\d+)x(\d+)/
49
+ e.fixed_size($1.to_i, $1.to_i)
50
+ else
51
+ # constant zoom
52
+ e.zoom = options[:zoom].to_i
53
+ e.renderer_options = {crop_enabled: true}
54
+ end
55
+
56
+ e.save(options[:output_file])
57
+ end
@@ -0,0 +1,9 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+
3
+ require 'gpx_utils/waypoints_exporter'
4
+ require 'gpx_utils/waypoints_importer'
5
+
6
+ require 'gpx_utils/track_importer'
7
+
8
+ module Geotagger
9
+ end
@@ -3,12 +3,13 @@ require 'mini_exiftool'
3
3
 
4
4
  $:.unshift(File.dirname(__FILE__))
5
5
 
6
- module Gpx2exif
6
+ module Geotagger
7
7
  class ExifEditor
8
8
 
9
- def initialize
9
+ def initialize(options = {})
10
10
  @images = Array.new
11
11
  @global_time_offset = 0
12
+ @verbose = options[:verbose]
12
13
  end
13
14
 
14
15
  attr_reader :images
@@ -20,7 +21,7 @@ module Gpx2exif
20
21
  :time => get_photo_time(path) + time_offset + @global_time_offset
21
22
  }
22
23
  @images << i
23
- puts "Added file #{path}, time #{i[:time]}"
24
+ puts "Added file #{path}, time #{i[:time]}" if @verbose
24
25
  end
25
26
 
26
27
  def get_photo_time(path)
@@ -54,14 +55,13 @@ module Gpx2exif
54
55
  photo.save
55
56
 
56
57
  photo2 = MiniExiftool.new path
57
- puts " - coord saved lat #{photo2['GPSLatitude']} lon #{photo2['GPSLongitude']}"
58
+ puts " - coord saved lat #{photo2['GPSLatitude']} lon #{photo2['GPSLongitude']}" if @verbose
58
59
 
59
60
  # exiftool -GPSMapDatum="WGS-84" -gps:GPSLatitude="34,57,57"
60
61
  # -gps:GPSLatitudeRef="N" -gps:GPSLongitude="83,17,59" -gps:GPSLongitudeRef="W"
61
62
  # -gps:GPSAltitudeRef="0" -GPSAltitude=1426 -gps:GPSMeasureMode=2 -City="RabunBald"
62
63
  # -State="North Carolina" -Country="USA" ~/Desktop/RabunBaldSummit_NC.jpg
63
-
64
64
  end
65
65
 
66
66
  end
67
- end
67
+ end
@@ -1,15 +1,21 @@
1
1
  require 'rubygems'
2
+ require 'gpx_utils'
3
+ require 'geotagger/exif_editor'
4
+ require 'geotagger/track_importer'
2
5
 
3
6
  $:.unshift(File.dirname(__FILE__))
4
7
 
5
- module Gpx2exif
6
- class GeoManager
8
+ module Geotagger
9
+ class Geotagger
7
10
 
8
- def initialize
11
+ def initialize(options = {})
12
+ @verbose = options[:verbose]
9
13
  @ee = ExifEditor.new
10
- @gp = GpxParser.new
14
+ @ti = TrackImporter.new
15
+ @ti.verbose = @verbose
11
16
  end
12
17
 
18
+ # Add all GPX and images with
13
19
  def add_all_files(time_offset = 0)
14
20
  # add all GPX
15
21
  Dir.glob("**/*.GPX", File::FNM_CASEFOLD).each do |f|
@@ -26,7 +32,7 @@ module Gpx2exif
26
32
  end
27
33
 
28
34
  def add_gpx_file(path)
29
- @gp.add_file(path)
35
+ @ti.add_file(path)
30
36
  end
31
37
 
32
38
  def add_image(path, time_offset = 0)
@@ -35,10 +41,10 @@ module Gpx2exif
35
41
 
36
42
  def match_up
37
43
  @ee.images.each do |i|
38
- puts "* searching for #{i[:path]}, time #{i[:time]}"
39
- i[:coord] = @gp.find_by_time(i[:time])
44
+ puts "* searching for #{i[:path]}, time #{i[:time]}" if @verbose
45
+ i[:coord] = @ti.find_by_time(i[:time])
40
46
  if i[:coord].nil?
41
- puts " - not found"
47
+ puts " - not found" if @verbose
42
48
  end
43
49
  end
44
50
 
@@ -48,7 +54,7 @@ module Gpx2exif
48
54
  def save!
49
55
  @ee.images.each do |i|
50
56
  if not i[:coord].nil?
51
- puts "! saving for #{i[:path]}"
57
+ puts "! saving for #{i[:path]}" if @verbose
52
58
  @ee.set_photo_coords_internal(i)
53
59
  end
54
60
 
@@ -57,7 +63,7 @@ module Gpx2exif
57
63
 
58
64
  def simulate
59
65
  to_process = @ee.images.select { |i| not i[:coord].nil? }
60
- puts "Result: to update #{to_process.size} from #{@ee.images.size}"
66
+ puts "Result: to update #{to_process.size} from #{@ee.images.size}" if @verbose
61
67
  end
62
68
 
63
69
  end
@@ -0,0 +1,33 @@
1
+ require 'rubygems'
2
+ require 'nokogiri'
3
+
4
+ $:.unshift(File.dirname(__FILE__))
5
+
6
+ # Simple parsing GPX file
7
+ module Geotagger
8
+ class TrackImporter < GpxUtils::TrackImporter
9
+
10
+ THRESHOLD = 5*60
11
+
12
+ attr_accessor :verbose
13
+
14
+ # Only import valid coords
15
+ def self.coord_valid?(lat, lon, elevation, time)
16
+ return true if lat and lon and time
17
+ return false
18
+ end
19
+
20
+ def find_by_time(time)
21
+ selected_coords = @coords.select { |c| (c[:time].localtime - time.localtime).abs < THRESHOLD }
22
+ selected_coords = selected_coords.sort { |a, b| (a[:time].localtime - time.localtime).abs <=> (b[:time].localtime - time.localtime).abs }
23
+ puts " - found #{selected_coords.size} coords within #{THRESHOLD}s from image time" if @verbose
24
+ if selected_coords.size > 0
25
+ puts " - best is #{selected_coords.first[:time].localtime}, time offset #{selected_coords.first[:time].localtime - time.localtime}" if @verbose
26
+ puts " - lat #{selected_coords.first[:lat]} lon #{selected_coords.first[:lon]}" if @verbose
27
+ end
28
+
29
+ return selected_coords.first
30
+ end
31
+
32
+ end
33
+ end
@@ -1,10 +1,6 @@
1
1
  $:.unshift(File.dirname(__FILE__))
2
2
 
3
- require 'garmin_utils'
3
+ require 'gpx_utils'
4
4
 
5
- require 'gpx2exif/gpx_parser'
6
- require 'gpx2exif/exif_editor'
7
- require 'gpx2exif/geo_manager'
8
-
9
- module Gpx2exif
10
- end
5
+ #module Gpx2exif
6
+ #end
@@ -0,0 +1,30 @@
1
+ require 'rubygems'
2
+
3
+ $:.unshift(File.dirname(__FILE__))
4
+
5
+ module Gpx2png
6
+ class Base
7
+
8
+ def initialize
9
+ @coords = Array.new
10
+ @zoom = 9
11
+ @verbose = true
12
+ end
13
+
14
+ def add(lat, lon)
15
+ @coords << { lat: lat, lon: lon }
16
+ end
17
+
18
+ attr_accessor :zoom, :color, :coords
19
+
20
+ # Some math stuff
21
+ def self.rad2deg(rad)
22
+ return rad * 180.0 / Math::PI
23
+ end
24
+
25
+ def self.deg2rad(deg)
26
+ return deg * Math::PI / 180.0
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,210 @@
1
+ require 'rubygems'
2
+ require 'chunky_png'
3
+ require 'net/http'
4
+ require "uri"
5
+
6
+
7
+ $:.unshift(File.dirname(__FILE__))
8
+
9
+ module Gpx2png
10
+ class Gpx2png
11
+
12
+ TILE_WIDTH = 256
13
+ TILE_HEIGHT = 256
14
+
15
+ def initialize
16
+ @coords = Array.new
17
+ @zoom = 9
18
+ @color = ChunkyPNG::Color.from_hex('#FF0000')
19
+ end
20
+
21
+ def add(lat, lon)
22
+ @coords << { lat: lat, lon: lon }
23
+ end
24
+
25
+ attr_accessor :zoom, :color, :coords
26
+
27
+ def dev
28
+ zoom = 15
29
+ @coords.collect { |c|
30
+ {
31
+ url: self.class.url(zoom, [c[:lat], c[:lon]]),
32
+ tile: self.class.convert(zoom, [c[:lat], c[:lon]]),
33
+ return: self.class.reverse_convert(zoom,
34
+ self.class.convert(zoom, [c[:lat], c[:lon]])
35
+ ),
36
+ point: self.class.point_on_image(zoom, [c[:lat], c[:lon]])
37
+ }
38
+ }
39
+ end
40
+
41
+ def to_png(filename)
42
+ download_and_join_tiles
43
+ @full_image.save(filename)
44
+ filename
45
+ end
46
+
47
+ # http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#X_and_Y
48
+ def self.convert(zoom, coord)
49
+ lat_deg, lon_deg = coord
50
+ lat_rad = deg2rad(lat_deg)
51
+ x = (((lon_deg + 180) / 360) * (2 ** zoom)).floor
52
+ y = ((1 - Math.log(Math.tan(lat_rad) + 1 / Math.cos(lat_rad)) / Math::PI) /2 * (2 ** zoom)).floor
53
+
54
+ return [x, y]
55
+ end
56
+
57
+ def self.url_convert(zoom, coord, server = 'b.')
58
+ x, y = convert(zoom, coord)
59
+ url(zoom, [x, y], server)
60
+ end
61
+
62
+ def self.url(zoom, coord, server = 'b.')
63
+ x, y = coord
64
+ url = "http://#{server}tile.openstreetmap.org\/#{zoom}\/#{x}\/#{y}.png"
65
+ return url
66
+ end
67
+
68
+ # top-left corner
69
+ def self.reverse_convert(zoom, coord)
70
+ x, y = coord
71
+ n = 2 ** zoom
72
+ lon_deg = x.to_f / n.to_f * 360.0 - 180.0
73
+ lat_deg = rad2deg(Math.atan(Math.sinh(Math::PI * (1.to_f - 2.to_f * y.to_f / n.to_f))))
74
+ return [lat_deg, lon_deg]
75
+ end
76
+
77
+ # return where you should put point on tile
78
+ def self.point_on_image(zoom, geo_coord)
79
+ osm_tile_coord = convert(zoom, geo_coord)
80
+ top_left_corner = reverse_convert(zoom, osm_tile_coord)
81
+ bottom_right_corner = reverse_convert(zoom, [
82
+ osm_tile_coord[0] + 1, osm_tile_coord[1] + 1
83
+ ])
84
+
85
+ # some line y = ax + b math
86
+
87
+ x_geo = geo_coord[1]
88
+ # offset
89
+ x_offset = x_geo - top_left_corner[1]
90
+ # scale
91
+ x_distance = (bottom_right_corner[1] - top_left_corner[1])
92
+ x = (TILE_WIDTH.to_f * (x_offset / x_distance)).round
93
+
94
+ y_geo = geo_coord[0]
95
+ # offset
96
+ y_offset = y_geo - top_left_corner[0]
97
+ # scale
98
+ y_distance = (bottom_right_corner[0] - top_left_corner[0])
99
+ y = (TILE_HEIGHT.to_f * (y_offset / y_distance)).round
100
+
101
+ return { osm_title_coord: osm_tile_coord, pixel_offset: [x, y] }
102
+ end
103
+
104
+
105
+ attr_reader :lat_min, :lat_max, :lon_min, :lon_max
106
+ attr_reader :tile_x_distance, :tile_y_distance
107
+
108
+ def download_and_join_tiles
109
+ @lat_min = @coords.collect { |c| c[:lat] }.min
110
+ @lat_max = @coords.collect { |c| c[:lat] }.max
111
+ @lon_min = @coords.collect { |c| c[:lon] }.min
112
+ @lon_max = @coords.collect { |c| c[:lon] }.max
113
+
114
+ @border_tiles = [
115
+ self.class.convert(@zoom, [@lat_min, @lon_min]),
116
+ self.class.convert(@zoom, [@lat_max, @lon_max])
117
+ ]
118
+
119
+ @tile_x_range = (@border_tiles[0][0])..(@border_tiles[1][0])
120
+ @tile_y_range = (@border_tiles[1][1])..(@border_tiles[0][1])
121
+
122
+ # new image
123
+ @full_image_x = (1 + @tile_x_range.max - @tile_x_range.min) * TILE_WIDTH
124
+ @full_image_y = (1 + @tile_y_range.max - @tile_y_range.min) * TILE_HEIGHT
125
+ puts "Output image dimension #{@full_image_x}x#{@full_image_y}"
126
+ @full_image = ChunkyPNG::Image.new(
127
+ @full_image_x,
128
+ @full_image_y,
129
+ ChunkyPNG::Color::WHITE
130
+ )
131
+
132
+ # {:x, :y, :blob}
133
+ @images = Array.new
134
+
135
+ @tile_x_range.each do |x|
136
+ @tile_y_range.each do |y|
137
+ url = self.class.url(@zoom, [x,y])
138
+
139
+ # blob time
140
+ uri = URI.parse(url)
141
+ response = Net::HTTP.get_response(uri)
142
+ blob = response.body
143
+ image = ChunkyPNG::Image.from_blob(blob)
144
+
145
+ @images << {
146
+ url: url,
147
+ image: image,
148
+ x: x,
149
+ y: y
150
+ }
151
+
152
+ # compose image
153
+ x_offset = (x - @tile_x_range.min) * TILE_WIDTH
154
+ y_offset = (y - @tile_y_range.min) * TILE_HEIGHT
155
+ @full_image.compose!(
156
+ image,
157
+ x_offset,
158
+ y_offset
159
+ )
160
+
161
+ 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})"
162
+ end
163
+ end
164
+
165
+ # sweet, image is joined
166
+
167
+ # add some coords to the map
168
+ (1...@coords.size).each do |i|
169
+ lat_from = @coords[i-1][:lat]
170
+ lon_from = @coords[i-1][:lon]
171
+
172
+ lat_to = @coords[i][:lat]
173
+ lon_to = @coords[i][:lon]
174
+
175
+ point_from = self.class.point_on_image(@zoom, [lat_from, lon_from])
176
+ point_to = self.class.point_on_image(@zoom, [lat_to, lon_to])
177
+ # { osm_title_coord: osm_tile_coord, pixel_offset: [x, y] }
178
+
179
+ # first point
180
+ bitmap_xa = (point_from[:osm_title_coord][0] - @tile_x_range.min) * TILE_WIDTH + point_from[:pixel_offset][0]
181
+ bitmap_ya = (point_from[:osm_title_coord][1] - @tile_y_range.min) * TILE_HEIGHT + point_from[:pixel_offset][1]
182
+ bitmap_xb = (point_to[:osm_title_coord][0] - @tile_x_range.min) * TILE_WIDTH + point_to[:pixel_offset][0]
183
+ bitmap_yb = (point_to[:osm_title_coord][1] - @tile_y_range.min) * TILE_HEIGHT + point_to[:pixel_offset][1]
184
+
185
+ @full_image.line(
186
+ bitmap_xa, bitmap_ya,
187
+ bitmap_xb, bitmap_yb,
188
+ @color
189
+ )
190
+
191
+ end
192
+ end
193
+
194
+ def expand_map
195
+ # TODO expand min and max ranges
196
+ end
197
+
198
+
199
+ # Some math stuff
200
+ def self.rad2deg(rad)
201
+ return rad * 180.0 / Math::PI
202
+ end
203
+
204
+ def self.deg2rad(deg)
205
+ return deg * Math::PI / 180.0
206
+ end
207
+
208
+
209
+ end
210
+ end