fdc 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/bin/fdc ADDED
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'fdc'
4
+ require 'optparse'
5
+
6
+ ERROR_MISSING_ARGUMENT = -1
7
+ ERROR_INVALID_OPTION = -2
8
+ ERROR_FILE_EXPORT = -3
9
+ ERROR_INCOMPATIBLE_RUBY_VERSION = -4
10
+
11
+ # Lock out older and knowingly incompatible ruby versions
12
+ if RUBY_VERSION[0..2].to_f < 1.9 then
13
+ STDERR.puts <<-EOS
14
+ Incompatible Ruby version: #{RUBY_VERSION} (at least 1.9.1 required)
15
+ Get the latest version from ruby-lang.org!
16
+ EOS
17
+ exit(ERROR_INCOMPATIBLE_RUBY_VERSION)
18
+ end
19
+
20
+ options = {}
21
+
22
+ optparse = OptionParser.new do |opts|
23
+ opts.banner = "Usage: fdc [options] <file>..."
24
+
25
+ opts.separator ""
26
+ opts.separator "Options:"
27
+
28
+ # Define alternative destination directory
29
+ options[:dest] = nil
30
+ opts.on( "-d", "--destination DEST", String, "Alternative destination directory" ) do |dest|
31
+ options[:dest] = dest
32
+ end
33
+
34
+ # Define alternative destination directory
35
+ options[:stdout] = false
36
+ opts.on( "-s", "--stdout", String, "Print converted KML to STDOUT" ) do
37
+ options[:stdout] = true
38
+ end
39
+
40
+ # Clamp track to ground and ignore altitude information
41
+ options[:clamp] = false
42
+ opts.on( "-c", "--clamp", "Clamp track to ground") do
43
+ options[:clamp] = true
44
+ end
45
+
46
+ # Extrude track to ground to emphasize absolute height
47
+ options[:extrude] = false
48
+ opts.on( "-e", "--extrude", "Extrude track to ground") do
49
+ options[:extrude] = true
50
+ end
51
+
52
+ # Extrude track to ground to emphasize absolute height
53
+ options[:gps] = false
54
+ opts.on( "-g", "--gps-alt", "Use gps instead of barometric altitude") do
55
+ options[:gps] = true
56
+ end
57
+
58
+ # UTF-8 input file encoding
59
+ options[:utf] = "ISO-8859-1"
60
+ opts.on( "-u", "--utf8", "Set input file encoding to UTF-8") do
61
+ options[:utf] = "UTF-8"
62
+ end
63
+
64
+ # Verbose output
65
+ options[:verbose] = false
66
+ opts.on( "-v", "--verbose", "Verbose output") do
67
+ options[:verbose] = true
68
+ end
69
+
70
+ # Define help
71
+ opts.on_tail( "-h", "--help", "Display this help screen" ) do
72
+ STDERR.puts opts
73
+ exit
74
+ end
75
+
76
+ end
77
+
78
+ begin
79
+ optparse.parse!
80
+ rescue OptionParser::MissingArgument => e
81
+ STDERR.puts e.message
82
+ exit(ERROR_MISSING_ARGUMENT)
83
+ rescue OptionParser::InvalidOption => e
84
+ STDERR.puts e.message
85
+ exit(ERROR_INVALID_OPTION)
86
+ end
87
+
88
+ STDERR.puts optparse if ARGV.empty?
89
+
90
+ @converter = Fdc::Converter.new
91
+
92
+ ARGV.each do |file|
93
+
94
+ begin
95
+ @converter.parse(file, encoding=options[:utf])
96
+ rescue Fdc::FileReadError => e
97
+ STDERR.puts e.message
98
+ next
99
+ end
100
+
101
+ @converter.compile(clamp=options[:clamp], extrude=options[:extrude], gps=options[:gps])
102
+
103
+ if options[:stdout]
104
+ STDOUT.puts @converter.kml
105
+ else
106
+ begin
107
+ options[:dest] ? @converter.export(options[:dest]) : @converter.export
108
+ STDERR.puts "Successfully converted file: #{file}" if options[:verbose]
109
+ rescue Fdc::FileWriteError => e
110
+ STDERR.puts e.message
111
+ exit(ERROR_FILE_EXPORT)
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,284 @@
1
+ require 'date'
2
+ require 'pathname'
3
+ require 'builder'
4
+ require 'fdc/utilities'
5
+ require 'fdc/exceptions'
6
+
7
+ module Fdc
8
+
9
+ # Class to convert IGC files to KML.
10
+ #
11
+ # @!attribute [r] kml
12
+ # @return [String] The KML document
13
+ #
14
+ # @example
15
+ # converter = Converter.new
16
+ # converter.parse(path/to/file.igc)
17
+ # converter.compile
18
+ # converter.export(output/dir)
19
+ class Converter
20
+
21
+ # The compiled KML document
22
+ attr_accessor :kml
23
+
24
+ # Load and parse an IGC file from the supplied path.
25
+ #
26
+ # @param [String] file The path to the IGC file
27
+ # @param [String] encoding The encoding of the input file
28
+ # @raise [Fdc::FileReadError] If file could not be loaded
29
+ # @raise [Fdc::FileFormatError] If the file format is invalid
30
+ def parse(file, encoding="ISO-8859-1")
31
+
32
+ # Update state
33
+ @path = Pathname.new(file)
34
+ @encoding = encoding
35
+
36
+ # Do work
37
+ load_file
38
+ parse_file
39
+
40
+ end
41
+
42
+ # Compile the KML document from the parsed IGC file.
43
+ #
44
+ # @param [Boolean] clamp Whether the track should be clamped to the ground
45
+ # @param [Boolean] extrude Whether the track should be extruded to the ground
46
+ # @param [Boolean] gps Whether GPS altitude information should be used
47
+ # @raise [RuntimeError] If {#parse} was not called before
48
+ def compile(clamp=false, extrude=false, gps=false)
49
+
50
+ # State assertion
51
+ raise RuntimeError, "Cannot compile before successfull parse" if @igc.nil? or @date.nil?
52
+
53
+ # Build HTML for balloon description
54
+ html = Builder::XmlMarkup.new(:indent => 2)
55
+ html.div :style => "width: 250;" do
56
+ html.p do
57
+ unless @a_records[3].nil? then
58
+ html.strong "Device:"
59
+ html.dfn @a_records[3].strip
60
+ html.br
61
+ end
62
+ end
63
+ html.p do
64
+ @h_records.each do |h|
65
+ if h.include? "PLT" and not h[2].strip.empty? then
66
+ html.strong "Pilot:"
67
+ html.dfn h[2].strip
68
+ html.br
69
+ end
70
+ if h.include? "CID" and not h[2].strip.empty? then
71
+ html.strong "Competition ID:"
72
+ html.dfn h[2].strip
73
+ html.br
74
+ end
75
+ if h.include? "GTY" and not h[2].strip.empty? then
76
+ html.strong "Glider:"
77
+ html.dfn h[2].strip
78
+ html.br
79
+ end
80
+ if h.include? "GID" and not h[2].strip.empty? then
81
+ html.strong "Glider ID:"
82
+ html.dfn h[2].strip
83
+ html.br
84
+ end
85
+ if h.include? "CCL" and not h[2].strip.empty? then
86
+ html.strong "Competition class:"
87
+ html.dfn h[2].strip
88
+ html.br
89
+ end
90
+ if h.include? "SIT" and not h[2].strip.empty? then
91
+ html.strong "Site:"
92
+ html.dfn h[2].strip
93
+ html.br
94
+ end
95
+ end
96
+
97
+ html.strong "Date:"
98
+ html.dfn @date[3..5].join(".")
99
+ html.br
100
+ end
101
+
102
+ # Manufacturer-dependent L records
103
+ case @a_records[1]
104
+ when "XSX"
105
+ @l_records.each do |l|
106
+ if matches = l[1].scan(/(\w*):(-?\d+.?\d+)/) then
107
+ html.p do
108
+ matches.each do |match|
109
+ case match[0]
110
+ when "MC"
111
+ html.strong "Max. climb:"
112
+ html.dfn match[1] << " m/s"
113
+ html.br
114
+ when "MS"
115
+ html.strong "Max. sink:"
116
+ html.dfn match[1] << " m/s"
117
+ html.br
118
+ when "MSP"
119
+ html.strong "Max. speed:"
120
+ html.dfn match[1] << " km/h"
121
+ html.br
122
+ when "Dist"
123
+ html.strong "Track distance:"
124
+ html.dfn match[1] << " km"
125
+ html.br
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+
133
+ end
134
+
135
+ # Build KML
136
+ xml = Builder::XmlMarkup.new(:indent => 2)
137
+ xml.instruct!
138
+ xml.kml "xmlns" => "http://www.opengis.net/kml/2.2", "xmlns:gx" => "http://www.google.com/kml/ext/2.2" do
139
+ xml.Placemark {
140
+ xml.name @path.basename(@path.extname)
141
+ xml.Snippet :maxLines => "2" do
142
+ xml.text! snippet
143
+ end
144
+ xml.description do
145
+ xml.cdata! html.target!
146
+ end
147
+ xml.Style do
148
+ xml.IconStyle do
149
+ xml.Icon do
150
+ xml.href "http://earth.google.com/images/kml-icons/track-directional/track-0.png"
151
+ end
152
+ end
153
+ xml.LineStyle do
154
+ xml.color "99ffac59"
155
+ xml.width "4"
156
+ end
157
+ end
158
+ xml.gx:Track do
159
+
160
+ clamp ? xml.altitudeMode("clampToGround") : xml.altitudeMode("absolute")
161
+ extrude ? xml.extrude("1") : xml.extrude("0")
162
+
163
+ @b_records.each do |b_record|
164
+ time = DateTime.new(2000 + @date[5].to_i, @date[4].to_i, @date[3].to_i,
165
+ b_record[1].to_i, b_record[2].to_i, b_record[3].to_i)
166
+ xml.when time
167
+ end
168
+ @b_records.each do |b_record|
169
+ coords = Fdc::GeoLocation.to_dec(b_record[5], b_record[4])
170
+ gps ? coords << b_record[8].to_f : coords << b_record[7].to_f
171
+ xml.gx :coord, coords.join(" ")
172
+ end
173
+ end
174
+ }
175
+ end
176
+
177
+ @kml = xml.target!
178
+ end
179
+
180
+ # Export the compiled KML document
181
+ #
182
+ # @param [String] dir The alternative output directory.
183
+ # If nothing is supplied the files are written to the same location
184
+ # as the IGC input file.
185
+ # @raise [RuntimeError] If {#parse} and {#compile} were not called before
186
+ # @raise [Fdc::FileWriteError] If dirname is not a directory or write protected
187
+ def export(dir = nil)
188
+
189
+ # Assert state
190
+ raise RuntimeError, "Cannot export before compile was called" unless @kml
191
+
192
+ dir = @path.dirname.to_s unless dir
193
+
194
+ # Create Pathname for easier handling
195
+ dest = Pathname.new(dir)
196
+
197
+ # Create output file name
198
+ dest += @path.basename(@path.extname)
199
+
200
+ begin
201
+ file = File.new(dest.to_s << ".kml", "w:UTF-8")
202
+ rescue Errno::EACCES => e
203
+ raise Fdc::FileWriteError, "Destination is write-protected: #{dir.to_s}"
204
+ rescue Errno::ENOTDIR => e
205
+ raise Fdc::FileWriteError, "Destination is not a directory: #{dir.to_s}"
206
+ rescue Errno::ENOENT => e
207
+ raise Fdc::FileWriteError, "Destination does not exist: #{dir.to_s}"
208
+ end
209
+
210
+ file.write(@kml)
211
+ file.close
212
+
213
+ end
214
+
215
+ private
216
+
217
+ # Load igc file from supplied path
218
+ def load_file
219
+
220
+ raise Fdc::FileReadError, "Invalid file extension: #{@path.to_s}" unless @path.extname == ".igc"
221
+
222
+ # Load file
223
+ begin
224
+ file = File.new(@path, "r", :encoding => @encoding)
225
+ rescue Errno::EISDIR => e
226
+ raise Fdc::FileReadError, "Input file is a directory: #{@path.to_s}"
227
+ rescue Errno::ENOENT => e
228
+ raise Fdc::FileReadError, "Input file does not exist: #{@path.to_s}"
229
+ end
230
+
231
+ @igc = file.read
232
+ file.close
233
+
234
+ end
235
+
236
+ # Regular expressions for file parsing
237
+ REGEX_A = /^[a]([a-z\d]{3})([a-z\d]{3})?(.*)$/i
238
+ REGEX_H = /^[h][f|o|p]([\w]{3})(.*):(.*)$/i
239
+ REGEX_H_DTE = /^hf(dte)((\d{2})(\d{2})(\d{2}))/i
240
+ REGEX_B = /^(B)(\d{2})(\d{2})(\d{2})(\d{7}[NS])(\d{8}[EW])([AV])(\d{5})(\d{5})/
241
+ REGEX_L = /^l([a-z0-9]{3}|[plt]|[pfc])(.*)/i
242
+
243
+ # Parse igc file content
244
+ def parse_file
245
+
246
+ begin
247
+
248
+ # parse utc date
249
+ @date = @igc.match(REGEX_H_DTE)
250
+ raise Fdc::FileFormatError, "Invalid file format - header date is missing: #{@path.to_s}" unless @date
251
+
252
+ # parse a records
253
+ @a_records = @igc.match(REGEX_A)
254
+ raise Fdc::FileFormatError, "Invalid file format: #{@path.to_s}" unless @a_records
255
+
256
+ # parse h records
257
+ @h_records = @igc.scan(REGEX_H)
258
+
259
+ # parse b records
260
+ @b_records = @igc.scan(REGEX_B)
261
+
262
+ # parse l records
263
+ @l_records = @igc.scan(REGEX_L)
264
+
265
+ rescue ArgumentError => e
266
+ raise Fdc::FileFormatError, "Wrong file encoding: #{e.message}"
267
+ end
268
+
269
+ end
270
+
271
+ # Generate Snippet tag content
272
+ def snippet
273
+ summary = "Flight"
274
+ @h_records.each do |h|
275
+ if h.include? "SIT" and not h[2].strip.empty? then
276
+ summary << " from #{h[2].strip}"
277
+ end
278
+ end
279
+ summary << " on #{@date[3..5].join(".")}"
280
+ end
281
+
282
+ end
283
+
284
+ end
@@ -0,0 +1,12 @@
1
+ module Fdc
2
+
3
+ # Exception caused by invalid input file format
4
+ class FileFormatError < StandardError; end
5
+
6
+ # Exception that is raised when a file cannot be read
7
+ class FileReadError < StandardError; end
8
+
9
+ # Exception that is raised when a files cannot be written
10
+ class FileWriteError < StandardError; end
11
+
12
+ end
@@ -0,0 +1,32 @@
1
+ # Utility classes used by {IGCConverter}
2
+ module Fdc
3
+
4
+ # Module with helper functions for geocoordinate conversion
5
+ module GeoLocation
6
+
7
+ # Convert geocoordinates from mindec notation of IGC to dec notation
8
+ #
9
+ # @param [String] long The longitude from the igc file
10
+ # @param [String] lat The Latitude from the igc file
11
+ # @return [Float, Float] Longitude and Latitude in decimal notation
12
+ # @example Convert a pair of coordinates
13
+ # GeoLocation.to_dec("01343272E", "4722676N") #=>[13.7212,47.37793333333333]
14
+ def GeoLocation.to_dec(long, lat)
15
+
16
+ long_m = long.match(/^(\d{3})((\d{2})(\d{3}))(E|W)/)
17
+ lat_m = lat.match(/^(\d{2})((\d{2})(\d{3}))(N|S)/)
18
+
19
+ # Convert minutes to decimal
20
+ long_dec = long_m[1].to_f + (long_m[2].to_f / 1000 / 60)
21
+ lat_dec = lat_m[1].to_f + (lat_m[2].to_f / 1000 / 60)
22
+
23
+ # Change signs according to direction
24
+ long_dec *= (-1) if long_m[5] == "W"
25
+ lat_dec *= (-1) if lat_m[5] == "S"
26
+
27
+ return long_dec, lat_dec
28
+ end
29
+
30
+ end
31
+
32
+ end
data/lib/fdc.rb ADDED
@@ -0,0 +1,11 @@
1
+ # Ruby stdlib
2
+ require 'date'
3
+ require 'pathname'
4
+
5
+ # Gems
6
+ require 'builder'
7
+
8
+ # Fdc
9
+ require 'fdc/utilities'
10
+ require 'fdc/converter'
11
+ require 'fdc/exceptions'
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fdc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tobias Noiges
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-06-20 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: builder
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 3.0.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 0.9.2
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 0.9.2
46
+ description: Convert files in the avionics flight recorder data format (IGC) to the
47
+ keyhole markup language (KML) for display in Applications such as Google Earth.
48
+ email: tobias@noig.es
49
+ executables:
50
+ - fdc
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - lib/fdc/converter.rb
55
+ - lib/fdc/exceptions.rb
56
+ - lib/fdc/utilities.rb
57
+ - lib/fdc.rb
58
+ - bin/fdc
59
+ homepage: https://github.com/nokinen/igc-kml
60
+ licenses: []
61
+ post_install_message:
62
+ rdoc_options: []
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubyforge_project:
79
+ rubygems_version: 1.8.24
80
+ signing_key:
81
+ specification_version: 3
82
+ summary: Convert flight data format files (IGC) to keyhole markup language (KML)
83
+ test_files: []