gpx2exif 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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