fdc 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/lib/fdc.rb CHANGED
@@ -1,11 +1,6 @@
1
- # Ruby core and stdlib
2
- require 'date'
3
- require 'pathname'
4
-
5
- # Gems
6
- require 'builder'
7
-
8
1
  # Fdc
9
2
  require 'fdc/utilities'
10
3
  require 'fdc/converter'
11
- require 'fdc/exceptions'
4
+ require 'fdc/exceptions'
5
+ require 'fdc/parser'
6
+ require 'fdc/compiler'
@@ -0,0 +1,170 @@
1
+ require 'builder'
2
+ require 'date'
3
+ require 'fdc/parser'
4
+
5
+ # Compile KML documents from a {Fdc::Parser}
6
+ class Fdc::Compiler
7
+
8
+ # The compiled KML document
9
+ attr_reader :kml
10
+ attr_accessor :parser
11
+
12
+ # Create a new instance
13
+ #
14
+ # @param [Fdc::Parser] parser A parser from which the KML document should be compiled.
15
+ def initialize(parser)
16
+ @parser = parser
17
+ end
18
+
19
+ # Compile the KML document from the parsed IGC file.
20
+ #
21
+ # @param [String] track_name The name of the KML track
22
+ # @param [Boolean] clamp Whether the track should be clamped to the ground
23
+ # @param [Boolean] extrude Whether the track should be extruded to the ground
24
+ # @param [Boolean] gps Whether GPS altitude information should be used
25
+ # @raise [RuntimeError] If the supplied parser is not ready
26
+ def compile(track_name, clamp=false, extrude=false, gps=false)
27
+
28
+ raise RuntimeError, "Parser not ready" unless @parser.ready?
29
+
30
+ # Build HTML for balloon description
31
+ html = Builder::XmlMarkup.new(:indent => 2)
32
+ html.div :style => "width: 250;" do
33
+ html.p do
34
+ unless @parser.a_record[3].nil? then
35
+ html.strong "Device:"
36
+ html.dfn @parser.a_record[3].strip
37
+ html.br
38
+ end
39
+ end
40
+ html.p do
41
+ @parser.h_records.each do |h|
42
+ if h.include? "PLT" and not h[2].strip.empty? then
43
+ html.strong "Pilot:"
44
+ html.dfn h[2].strip
45
+ html.br
46
+ end
47
+ if h.include? "CID" and not h[2].strip.empty? then
48
+ html.strong "Competition ID:"
49
+ html.dfn h[2].strip
50
+ html.br
51
+ end
52
+ if h.include? "GTY" and not h[2].strip.empty? then
53
+ html.strong "Glider:"
54
+ html.dfn h[2].strip
55
+ html.br
56
+ end
57
+ if h.include? "GID" and not h[2].strip.empty? then
58
+ html.strong "Glider ID:"
59
+ html.dfn h[2].strip
60
+ html.br
61
+ end
62
+ if h.include? "CCL" and not h[2].strip.empty? then
63
+ html.strong "Competition class:"
64
+ html.dfn h[2].strip
65
+ html.br
66
+ end
67
+ if h.include? "SIT" and not h[2].strip.empty? then
68
+ html.strong "Site:"
69
+ html.dfn h[2].strip
70
+ html.br
71
+ end
72
+ end
73
+
74
+ html.strong "Date:"
75
+ html.dfn @parser.date_record[3..5].join(".")
76
+ html.br
77
+ end
78
+
79
+ # Manufacturer-dependent L records
80
+ case @parser.a_record[1]
81
+ when "XSX"
82
+ @parser.l_records.each do |l|
83
+ if matches = l[1].scan(/(\w*):(-?\d+.?\d+)/) then
84
+ html.p do
85
+ matches.each do |match|
86
+ case match[0]
87
+ when "MC"
88
+ html.strong "Max. climb:"
89
+ html.dfn match[1] << " m/s"
90
+ html.br
91
+ when "MS"
92
+ html.strong "Max. sink:"
93
+ html.dfn match[1] << " m/s"
94
+ html.br
95
+ when "MSP"
96
+ html.strong "Max. speed:"
97
+ html.dfn match[1] << " km/h"
98
+ html.br
99
+ when "Dist"
100
+ html.strong "Track distance:"
101
+ html.dfn match[1] << " km"
102
+ html.br
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ end
111
+
112
+ # Build KML
113
+ xml = Builder::XmlMarkup.new(:indent => 2)
114
+ xml.instruct!
115
+ xml.kml "xmlns" => "http://www.opengis.net/kml/2.2", "xmlns:gx" => "http://www.google.com/kml/ext/2.2" do
116
+ xml.Placemark {
117
+ xml.name track_name
118
+ xml.Snippet :maxLines => "2" do
119
+ xml.text! snippet
120
+ end
121
+ xml.description do
122
+ xml.cdata! html.target!
123
+ end
124
+ xml.Style do
125
+ xml.IconStyle do
126
+ xml.Icon do
127
+ xml.href "http://earth.google.com/images/kml-icons/track-directional/track-0.png"
128
+ end
129
+ end
130
+ xml.LineStyle do
131
+ xml.color "99ffac59"
132
+ xml.width "4"
133
+ end
134
+ end
135
+ xml.gx:Track do
136
+
137
+ clamp ? xml.altitudeMode("clampToGround") : xml.altitudeMode("absolute")
138
+ extrude ? xml.extrude("1") : xml.extrude("0")
139
+
140
+ @parser.b_records.each do |b_record|
141
+ time = DateTime.new(2000 + @parser.date_record[5].to_i, @parser.date_record[4].to_i, @parser.date_record[3].to_i,
142
+ b_record[1].to_i, b_record[2].to_i, b_record[3].to_i)
143
+ xml.when time
144
+ end
145
+ @parser.b_records.each do |b_record|
146
+ coords = Fdc::GeoLocation.to_dec(b_record[5], b_record[4])
147
+ gps ? coords << b_record[8].to_f : coords << b_record[7].to_f
148
+ xml.gx :coord, coords.join(" ")
149
+ end
150
+ end
151
+ }
152
+ end
153
+
154
+ @kml = xml.target!
155
+ end
156
+
157
+ private
158
+
159
+ # Generate Snippet tag content
160
+ def snippet
161
+ summary = "Flight"
162
+ @parser.h_records.each do |h|
163
+ if h.include? "SIT" and not h[2].strip.empty? then
164
+ summary << " from #{h[2].strip}"
165
+ end
166
+ end
167
+ summary << " on #{@parser.date_record[3..5].join(".")}"
168
+ end
169
+
170
+ end
@@ -1,10 +1,11 @@
1
- require 'date'
2
1
  require 'pathname'
3
- require 'builder'
2
+
4
3
  require 'fdc/utilities'
5
4
  require 'fdc/exceptions'
5
+ require 'fdc/parser'
6
+ require 'fdc/compiler'
6
7
 
7
- # Class to convert IGC files to KML.
8
+ # Convert IGC Files to KML
8
9
  #
9
10
  # @!attribute [r] kml
10
11
  # @return [String] The KML document
@@ -16,11 +17,14 @@ require 'fdc/exceptions'
16
17
  # converter.export(output/dir)
17
18
  class Fdc::Converter
18
19
 
19
- # FileLoader mixing
20
+ # Mixins
20
21
  include Fdc::FileLoader
21
-
22
- # The compiled KML document
23
- attr_reader :kml
22
+ include Fdc::FileWriter
23
+
24
+ def initialize
25
+ @parser = Fdc::Parser.new
26
+ @compiler = Fdc::Compiler.new @parser
27
+ end
24
28
 
25
29
  # Load and parse an IGC file from the supplied path.
26
30
  #
@@ -29,9 +33,11 @@ class Fdc::Converter
29
33
  # @raise [Fdc::FileReadError] If file could not be loaded
30
34
  # @raise [Fdc::FileFormatError] If the file format is invalid
31
35
  def parse(file, encoding="ISO-8859-1")
32
-
33
- load(file, encoding)
34
- parse_file
36
+
37
+ path = Pathname.new(file)
38
+ load(path, encoding)
39
+ raise Fdc::FileReadError, "Invalid file extension: #{path.to_s}" unless path.extname == ".igc"
40
+ @parser.parse @file
35
41
 
36
42
  end
37
43
 
@@ -44,133 +50,11 @@ class Fdc::Converter
44
50
  def compile(clamp=false, extrude=false, gps=false)
45
51
 
46
52
  # State assertion
47
- raise RuntimeError, "Cannot compile before successfull parse" if @file.nil? or @date.nil?
48
-
49
- # Build HTML for balloon description
50
- html = Builder::XmlMarkup.new(:indent => 2)
51
- html.div :style => "width: 250;" do
52
- html.p do
53
- unless @a_records[3].nil? then
54
- html.strong "Device:"
55
- html.dfn @a_records[3].strip
56
- html.br
57
- end
58
- end
59
- html.p do
60
- @h_records.each do |h|
61
- if h.include? "PLT" and not h[2].strip.empty? then
62
- html.strong "Pilot:"
63
- html.dfn h[2].strip
64
- html.br
65
- end
66
- if h.include? "CID" and not h[2].strip.empty? then
67
- html.strong "Competition ID:"
68
- html.dfn h[2].strip
69
- html.br
70
- end
71
- if h.include? "GTY" and not h[2].strip.empty? then
72
- html.strong "Glider:"
73
- html.dfn h[2].strip
74
- html.br
75
- end
76
- if h.include? "GID" and not h[2].strip.empty? then
77
- html.strong "Glider ID:"
78
- html.dfn h[2].strip
79
- html.br
80
- end
81
- if h.include? "CCL" and not h[2].strip.empty? then
82
- html.strong "Competition class:"
83
- html.dfn h[2].strip
84
- html.br
85
- end
86
- if h.include? "SIT" and not h[2].strip.empty? then
87
- html.strong "Site:"
88
- html.dfn h[2].strip
89
- html.br
90
- end
91
- end
92
-
93
- html.strong "Date:"
94
- html.dfn @date[3..5].join(".")
95
- html.br
96
- end
53
+ raise RuntimeError, "Cannot compile without preceding parse" unless @parser.ready?
97
54
 
98
- # Manufacturer-dependent L records
99
- case @a_records[1]
100
- when "XSX"
101
- @l_records.each do |l|
102
- if matches = l[1].scan(/(\w*):(-?\d+.?\d+)/) then
103
- html.p do
104
- matches.each do |match|
105
- case match[0]
106
- when "MC"
107
- html.strong "Max. climb:"
108
- html.dfn match[1] << " m/s"
109
- html.br
110
- when "MS"
111
- html.strong "Max. sink:"
112
- html.dfn match[1] << " m/s"
113
- html.br
114
- when "MSP"
115
- html.strong "Max. speed:"
116
- html.dfn match[1] << " km/h"
117
- html.br
118
- when "Dist"
119
- html.strong "Track distance:"
120
- html.dfn match[1] << " km"
121
- html.br
122
- end
123
- end
124
- end
125
- end
126
- end
127
- end
128
-
129
- end
130
-
131
- # Build KML
132
- xml = Builder::XmlMarkup.new(:indent => 2)
133
- xml.instruct!
134
- xml.kml "xmlns" => "http://www.opengis.net/kml/2.2", "xmlns:gx" => "http://www.google.com/kml/ext/2.2" do
135
- xml.Placemark {
136
- xml.name @path.basename(@path.extname)
137
- xml.Snippet :maxLines => "2" do
138
- xml.text! snippet
139
- end
140
- xml.description do
141
- xml.cdata! html.target!
142
- end
143
- xml.Style do
144
- xml.IconStyle do
145
- xml.Icon do
146
- xml.href "http://earth.google.com/images/kml-icons/track-directional/track-0.png"
147
- end
148
- end
149
- xml.LineStyle do
150
- xml.color "99ffac59"
151
- xml.width "4"
152
- end
153
- end
154
- xml.gx:Track do
155
-
156
- clamp ? xml.altitudeMode("clampToGround") : xml.altitudeMode("absolute")
157
- extrude ? xml.extrude("1") : xml.extrude("0")
158
-
159
- @b_records.each do |b_record|
160
- time = DateTime.new(2000 + @date[5].to_i, @date[4].to_i, @date[3].to_i,
161
- b_record[1].to_i, b_record[2].to_i, b_record[3].to_i)
162
- xml.when time
163
- end
164
- @b_records.each do |b_record|
165
- coords = Fdc::GeoLocation.to_dec(b_record[5], b_record[4])
166
- gps ? coords << b_record[8].to_f : coords << b_record[7].to_f
167
- xml.gx :coord, coords.join(" ")
168
- end
169
- end
170
- }
171
- end
55
+ name = @path.basename(@path.extname)
56
+ @compiler.compile name, clamp, extrude, gps
172
57
 
173
- @kml = xml.target!
174
58
  end
175
59
 
176
60
  # Export the compiled KML document
@@ -183,77 +67,17 @@ class Fdc::Converter
183
67
  def export(dir = nil)
184
68
 
185
69
  # Assert state
186
- raise RuntimeError, "Cannot export before compile was called" unless @kml
70
+ raise RuntimeError, "Cannot export before compile was called" unless @compiler.kml
187
71
 
188
72
  dir = @path.dirname.to_s unless dir
189
-
190
- # Create Pathname for easier handling
191
- dest = Pathname.new(dir)
192
-
193
- # Create output file name
194
- dest += @path.basename(@path.extname)
195
-
196
- begin
197
- file = File.new("#{dest.to_s}.kml", "w:UTF-8")
198
- rescue Errno::EACCES => e
199
- raise Fdc::FileWriteError, "Destination is write-protected: #{dir.to_s}"
200
- rescue Errno::ENOTDIR => e
201
- raise Fdc::FileWriteError, "Destination is not a directory: #{dir.to_s}"
202
- rescue Errno::ENOENT => e
203
- raise Fdc::FileWriteError, "Destination does not exist: #{dir.to_s}"
204
- end
205
-
206
- file.write(@kml)
207
- file.close
73
+ write Pathname.new(dir) + (@path.basename(@path.extname).to_s << ".kml"), @compiler.kml
208
74
 
209
75
  end
210
-
211
- private
212
-
213
- # Regular expressions for file parsing
214
- REGEX_A = /^[a]([a-z\d]{3})([a-z\d]{3})?(.*)$/i
215
- REGEX_H = /^[h][f|o|p]([\w]{3})(.*):(.*)$/i
216
- REGEX_H_DTE = /^hf(dte)((\d{2})(\d{2})(\d{2}))/i
217
- REGEX_B = /^(B)(\d{2})(\d{2})(\d{2})(\d{7}[NS])(\d{8}[EW])([AV])(\d{5})(\d{5})/
218
- REGEX_L = /^l([a-z0-9]{3}|[plt]|[pfc])(.*)/i
219
-
220
- # Parse igc file content
221
- def parse_file
222
-
223
- begin
224
-
225
- # parse utc date
226
- @date = @file.match(REGEX_H_DTE)
227
- raise Fdc::FileFormatError, "Invalid file format - header date is missing: #{@path.to_s}" unless @date
228
-
229
- # parse a records
230
- @a_records = @file.match(REGEX_A)
231
- raise Fdc::FileFormatError, "Invalid file format: #{@path.to_s}" unless @a_records
232
76
 
233
- # parse h records
234
- @h_records = @file.scan(REGEX_H)
235
-
236
- # parse b records
237
- @b_records = @file.scan(REGEX_B)
238
-
239
- # parse l records
240
- @l_records = @file.scan(REGEX_L)
241
-
242
- rescue ArgumentError => e
243
- raise Fdc::FileFormatError, "Wrong file encoding: #{e.message}"
244
- end
245
-
246
- end
247
-
248
- # Generate Snippet tag content
249
- def snippet
250
- summary = "Flight"
251
- @h_records.each do |h|
252
- if h.include? "SIT" and not h[2].strip.empty? then
253
- summary << " from #{h[2].strip}"
254
- end
255
- end
256
- summary << " on #{@date[3..5].join(".")}"
77
+ # Get the compiled KML document
78
+ # @return [String] The compiled KML document
79
+ def kml
80
+ @compiler.kml
257
81
  end
258
82
 
259
83
  end
@@ -0,0 +1,65 @@
1
+ # Parse record types from IGC file data
2
+ class Fdc::Parser
3
+
4
+ attr_reader :date_record
5
+ attr_reader :a_record
6
+ attr_reader :h_records
7
+ attr_reader :b_records
8
+ attr_reader :l_records
9
+
10
+ def initialize
11
+ @ready = false
12
+ end
13
+
14
+ # Parse the supplied IGC file content
15
+ #
16
+ # @param [String] igc_file The IGC file content
17
+ # @raise [Fdc::FileFormatError] If the file format is invalid
18
+ def parse(igc_file)
19
+
20
+ begin
21
+ # parse utc date
22
+ unless @date_record = igc_file.match(REGEX_H_DTE) then
23
+ @ready = false
24
+ raise Fdc::FileFormatError, "Invalid file format - header date is missing"
25
+ end
26
+
27
+ # parse a records
28
+ unless @a_record = igc_file.match(REGEX_A) then
29
+ @ready = false
30
+ raise Fdc::FileFormatError, "Invalid file format" unless @a_record
31
+ end
32
+
33
+ # parse h records
34
+ @h_records = igc_file.scan(REGEX_H)
35
+
36
+ # parse b records
37
+ @b_records = igc_file.scan(REGEX_B)
38
+
39
+ # parse l records
40
+ @l_records = igc_file.scan(REGEX_L)
41
+
42
+ rescue ArgumentError => e
43
+ @ready = false
44
+ raise Fdc::FileFormatError, "Wrong file encoding: #{e.message}"
45
+ end
46
+
47
+ @ready = true
48
+
49
+ end
50
+
51
+ # @return [Boolean] true if {#parse} was successfully called
52
+ def ready?
53
+ return @ready
54
+ end
55
+
56
+ private
57
+
58
+ # Regular expressions for parsing igc records
59
+ REGEX_A = /^[a]([a-z\d]{3})([a-z\d]{3})?(.*)$/i
60
+ REGEX_H = /^[h][f|o|p]([\w]{3})(.*):(.*)$/i
61
+ REGEX_H_DTE = /^hf(dte)((\d{2})(\d{2})(\d{2}))/i
62
+ REGEX_B = /^(B)(\d{2})(\d{2})(\d{2})(\d{7}[NS])(\d{8}[EW])([AV])(\d{5})(\d{5})/
63
+ REGEX_L = /^l([a-z0-9]{3}|[plt]|[pfc])(.*)/i
64
+
65
+ end
@@ -32,7 +32,7 @@ module Fdc
32
32
 
33
33
  end
34
34
 
35
- # Module for file loading
35
+ # Common file loading operations
36
36
  module FileLoader
37
37
 
38
38
  # Loaded file content
@@ -48,32 +48,51 @@ module Fdc
48
48
 
49
49
  # Load a file from the supplied path
50
50
  #
51
- # @param [String] file The path to the file
51
+ # @param [Pathname] path The path to the file
52
52
  # @param [String] encoding The encoding of the file
53
53
  # @raise [Fdc::FileReadError] If file could not be loaded
54
- def load(file, encoding)
54
+ def load(path, encoding)
55
55
 
56
- # The path of the file
57
- @path = Pathname.new(file)
58
-
59
- # The file encoding
56
+ @path = path
60
57
  @encoding = encoding
61
-
62
- raise Fdc::FileReadError, "Invalid file extension: #{@path.to_s}" unless @path.extname == ".igc"
63
58
 
64
- # Load file
65
59
  begin
66
60
  f = File.new(@path, "r", :encoding => @encoding)
61
+ @file = f.read
67
62
  rescue Errno::EISDIR => e
68
63
  raise Fdc::FileReadError, "Input file is a directory: #{@path.to_s}"
69
64
  rescue Errno::ENOENT => e
70
65
  raise Fdc::FileReadError, "Input file does not exist: #{@path.to_s}"
66
+ ensure
67
+ if f then f.close end
71
68
  end
72
- # The loaded file
73
- @file = f.read
74
- f.close
75
69
  end
76
70
 
77
71
  end
72
+
73
+ # Common file writing operations
74
+ module FileWriter
75
+
76
+ private
77
+ # Write a file to the file system
78
+ #
79
+ # @param [Pathname] path The path for the file
80
+ # @param [String] data The data of the file
81
+ # @raise [Fdc::FileWriteError] If dirname is not a directory or write protected
82
+ def write(path, data)
83
+ begin
84
+ file = File.new(path, "w:UTF-8")
85
+ file.write(data)
86
+ rescue Errno::EACCES => e
87
+ raise Fdc::FileWriteError, "Destination is write-protected: #{path.to_s}"
88
+ rescue Errno::ENOTDIR => e
89
+ raise Fdc::FileWriteError, "Destination is not a directory: #{path.to_s}"
90
+ rescue Errno::ENOENT => e
91
+ raise Fdc::FileWriteError, "Destination does not exist: #{path.to_s}"
92
+ ensure
93
+ if file then file.close end
94
+ end
95
+ end
96
+ end
78
97
 
79
98
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fdc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -51,8 +51,10 @@ executables:
51
51
  extensions: []
52
52
  extra_rdoc_files: []
53
53
  files:
54
+ - lib/fdc/compiler.rb
54
55
  - lib/fdc/converter.rb
55
56
  - lib/fdc/exceptions.rb
57
+ - lib/fdc/parser.rb
56
58
  - lib/fdc/utilities.rb
57
59
  - lib/fdc.rb
58
60
  - bin/fdc