fdc 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/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: []