fdc 0.0.2 → 0.0.3
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/lib/fdc.rb +1 -1
- data/lib/fdc/converter.rb +216 -241
- data/lib/fdc/utilities.rb +49 -2
- metadata +3 -2
data/lib/fdc.rb
CHANGED
data/lib/fdc/converter.rb
CHANGED
@@ -4,281 +4,256 @@ require 'builder'
|
|
4
4
|
require 'fdc/utilities'
|
5
5
|
require 'fdc/exceptions'
|
6
6
|
|
7
|
-
|
7
|
+
# Class to convert IGC files to KML.
|
8
|
+
#
|
9
|
+
# @!attribute [r] kml
|
10
|
+
# @return [String] The KML document
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# converter = Converter.new
|
14
|
+
# converter.parse(path/to/file.igc)
|
15
|
+
# converter.compile
|
16
|
+
# converter.export(output/dir)
|
17
|
+
class Fdc::Converter
|
8
18
|
|
9
|
-
#
|
10
|
-
|
11
|
-
|
12
|
-
#
|
19
|
+
# FileLoader mixing
|
20
|
+
include Fdc::FileLoader
|
21
|
+
|
22
|
+
# The compiled KML document
|
23
|
+
attr_reader :kml
|
24
|
+
|
25
|
+
# Load and parse an IGC file from the supplied path.
|
13
26
|
#
|
14
|
-
# @
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
|
19
|
-
class Converter
|
27
|
+
# @param [String] file The path to the IGC file
|
28
|
+
# @param [String] encoding The encoding of the input file
|
29
|
+
# @raise [Fdc::FileReadError] If file could not be loaded
|
30
|
+
# @raise [Fdc::FileFormatError] If the file format is invalid
|
31
|
+
def parse(file, encoding="ISO-8859-1")
|
20
32
|
|
21
|
-
|
22
|
-
|
33
|
+
load(file, encoding)
|
34
|
+
parse_file
|
23
35
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
@path = Pathname.new(file)
|
34
|
-
@encoding = encoding
|
35
|
-
|
36
|
-
# Do work
|
37
|
-
load_file
|
38
|
-
parse_file
|
39
|
-
|
40
|
-
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Compile the KML document from the parsed IGC file.
|
39
|
+
#
|
40
|
+
# @param [Boolean] clamp Whether the track should be clamped to the ground
|
41
|
+
# @param [Boolean] extrude Whether the track should be extruded to the ground
|
42
|
+
# @param [Boolean] gps Whether GPS altitude information should be used
|
43
|
+
# @raise [RuntimeError] If {#parse} was not called before
|
44
|
+
def compile(clamp=false, extrude=false, gps=false)
|
41
45
|
|
42
|
-
#
|
43
|
-
|
44
|
-
|
45
|
-
#
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
html.
|
56
|
-
|
57
|
-
|
58
|
-
html.strong "
|
59
|
-
html.dfn
|
46
|
+
# 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
|
60
84
|
html.br
|
61
85
|
end
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
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
|
95
90
|
end
|
96
|
-
|
97
|
-
html.strong "Date:"
|
98
|
-
html.dfn @date[3..5].join(".")
|
99
|
-
html.br
|
100
91
|
end
|
101
92
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
93
|
+
html.strong "Date:"
|
94
|
+
html.dfn @date[3..5].join(".")
|
95
|
+
html.br
|
96
|
+
end
|
97
|
+
|
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
|
127
122
|
end
|
128
123
|
end
|
129
124
|
end
|
130
125
|
end
|
131
126
|
end
|
132
|
-
|
133
127
|
end
|
134
128
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|
143
148
|
end
|
144
|
-
xml.
|
145
|
-
xml.
|
149
|
+
xml.LineStyle do
|
150
|
+
xml.color "99ffac59"
|
151
|
+
xml.width "4"
|
146
152
|
end
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
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
|
157
163
|
end
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
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(" ")
|
173
168
|
end
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
@kml = xml.target!
|
169
|
+
end
|
170
|
+
}
|
178
171
|
end
|
179
172
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
file.close
|
212
|
-
|
173
|
+
@kml = xml.target!
|
174
|
+
end
|
175
|
+
|
176
|
+
# Export the compiled KML document
|
177
|
+
#
|
178
|
+
# @param [String] dir The alternative output directory.
|
179
|
+
# If nothing is supplied the files are written to the same location
|
180
|
+
# as the IGC input file.
|
181
|
+
# @raise [RuntimeError] If {#parse} and {#compile} were not called before
|
182
|
+
# @raise [Fdc::FileWriteError] If dirname is not a directory or write protected
|
183
|
+
def export(dir = nil)
|
184
|
+
|
185
|
+
# Assert state
|
186
|
+
raise RuntimeError, "Cannot export before compile was called" unless @kml
|
187
|
+
|
188
|
+
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}"
|
213
204
|
end
|
214
205
|
|
215
|
-
|
206
|
+
file.write(@kml)
|
207
|
+
file.close
|
216
208
|
|
217
|
-
|
218
|
-
def load_file
|
219
|
-
|
220
|
-
raise Fdc::FileReadError, "Invalid file extension: #{@path.to_s}" unless @path.extname == ".igc"
|
209
|
+
end
|
221
210
|
|
222
|
-
|
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
|
211
|
+
private
|
230
212
|
|
231
|
-
|
232
|
-
|
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
|
233
219
|
|
234
|
-
|
220
|
+
# Parse igc file content
|
221
|
+
def parse_file
|
235
222
|
|
236
|
-
|
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
|
223
|
+
begin
|
242
224
|
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
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
|
-
|
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
|
+
|
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}"
|
269
244
|
end
|
270
245
|
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
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}"
|
278
254
|
end
|
279
|
-
summary << " on #{@date[3..5].join(".")}"
|
280
255
|
end
|
281
|
-
|
256
|
+
summary << " on #{@date[3..5].join(".")}"
|
282
257
|
end
|
283
|
-
|
258
|
+
|
284
259
|
end
|
data/lib/fdc/utilities.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
-
|
1
|
+
require 'pathname'
|
2
|
+
require 'fdc/exceptions'
|
3
|
+
|
4
|
+
# Utility modules used by {Fdc::Converter}
|
2
5
|
module Fdc
|
3
6
|
|
4
|
-
#
|
7
|
+
# Helper functions for geocoordinate conversion
|
5
8
|
module GeoLocation
|
6
9
|
|
7
10
|
# Convert geocoordinates from mindec notation of IGC to dec notation
|
@@ -28,5 +31,49 @@ module Fdc
|
|
28
31
|
end
|
29
32
|
|
30
33
|
end
|
34
|
+
|
35
|
+
# Module for file loading
|
36
|
+
module FileLoader
|
37
|
+
|
38
|
+
# Loaded file content
|
39
|
+
attr_reader :file
|
40
|
+
|
41
|
+
# Loaded file path
|
42
|
+
attr_reader :path
|
43
|
+
|
44
|
+
# Loaded file encoding
|
45
|
+
attr_reader :encoding
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# Load a file from the supplied path
|
50
|
+
#
|
51
|
+
# @param [String] file The path to the file
|
52
|
+
# @param [String] encoding The encoding of the file
|
53
|
+
# @raise [Fdc::FileReadError] If file could not be loaded
|
54
|
+
def load(file, encoding)
|
55
|
+
|
56
|
+
# The path of the file
|
57
|
+
@path = Pathname.new(file)
|
58
|
+
|
59
|
+
# The file encoding
|
60
|
+
@encoding = encoding
|
61
|
+
|
62
|
+
raise Fdc::FileReadError, "Invalid file extension: #{@path.to_s}" unless @path.extname == ".igc"
|
63
|
+
|
64
|
+
# Load file
|
65
|
+
begin
|
66
|
+
f = File.new(@path, "r", :encoding => @encoding)
|
67
|
+
rescue Errno::EISDIR => e
|
68
|
+
raise Fdc::FileReadError, "Input file is a directory: #{@path.to_s}"
|
69
|
+
rescue Errno::ENOENT => e
|
70
|
+
raise Fdc::FileReadError, "Input file does not exist: #{@path.to_s}"
|
71
|
+
end
|
72
|
+
# The loaded file
|
73
|
+
@file = f.read
|
74
|
+
f.close
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
31
78
|
|
32
79
|
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.
|
4
|
+
version: 0.0.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-06-
|
12
|
+
date: 2012-06-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: builder
|
@@ -81,3 +81,4 @@ signing_key:
|
|
81
81
|
specification_version: 3
|
82
82
|
summary: Convert flight data format files (IGC) to keyhole markup language (KML)
|
83
83
|
test_files: []
|
84
|
+
has_rdoc:
|