igc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f5b777bf1ad64d5f6aff66fbc145891d0e534d63dafe531a8d7dcf92fee2abea
4
+ data.tar.gz: 1435504cd86eede5245597371a46ef0093878374de6ac55a5538b026e0a1917e
5
+ SHA512:
6
+ metadata.gz: 93145a9bb546aaebe2b9f44acc46be27dbe72d2a0558abf14c34147bbf5cc31d452aa542c95f7238c68c310fe3af051eaf46f9f6926a4b52981e7cb5a3f58d82
7
+ data.tar.gz: c922a2021aa53e4054ec3a6266668347720cf4133d035c78b3b7dc045b61ede49c9f053153e25a4e6a2bd5c863f6bf1f9214909405a9a2873e0dd8ee1a7843dc
data/.rubocop.yml ADDED
@@ -0,0 +1,31 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.6
3
+
4
+ Style/StringLiterals:
5
+ Enabled: true
6
+ EnforcedStyle: double_quotes
7
+
8
+ Style/StringLiteralsInInterpolation:
9
+ Enabled: true
10
+ EnforcedStyle: double_quotes
11
+
12
+ Layout/LineLength:
13
+ Max: 120
14
+
15
+ Style/FrozenStringLiteralComment:
16
+ SafeAutoCorrect: true
17
+
18
+ Naming/MethodParameterName:
19
+ Enabled: false
20
+
21
+ Metrics/MethodLength:
22
+ Enabled: false
23
+
24
+ Metrics/AbcSize:
25
+ Enabled: false
26
+
27
+ Metrics/CyclomaticComplexity:
28
+ Enabled: false
29
+
30
+ Metrics/ClassLength:
31
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2023-04-01
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in igc.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "minitest", "~> 5.0"
11
+
12
+ gem "rubocop", "~> 1.21"
13
+
14
+ gem "snapshot_testing"
data/Gemfile.lock ADDED
@@ -0,0 +1,47 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ igc (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ ast (2.4.2)
10
+ json (2.6.3)
11
+ minitest (5.18.0)
12
+ parallel (1.22.1)
13
+ parser (3.2.2.0)
14
+ ast (~> 2.4.1)
15
+ rainbow (3.1.1)
16
+ rake (13.0.6)
17
+ regexp_parser (2.7.0)
18
+ rexml (3.2.5)
19
+ rubocop (1.48.1)
20
+ json (~> 2.3)
21
+ parallel (~> 1.10)
22
+ parser (>= 3.2.0.0)
23
+ rainbow (>= 2.2.2, < 4.0)
24
+ regexp_parser (>= 1.8, < 3.0)
25
+ rexml (>= 3.2.5, < 4.0)
26
+ rubocop-ast (>= 1.26.0, < 2.0)
27
+ ruby-progressbar (~> 1.7)
28
+ unicode-display_width (>= 2.4.0, < 3.0)
29
+ rubocop-ast (1.28.0)
30
+ parser (>= 3.2.1.0)
31
+ ruby-progressbar (1.13.0)
32
+ snapshot_testing (0.3.4)
33
+ unicode-display_width (2.4.2)
34
+
35
+ PLATFORMS
36
+ arm64-darwin-22
37
+ x86_64-linux
38
+
39
+ DEPENDENCIES
40
+ igc!
41
+ minitest (~> 5.0)
42
+ rake (~> 13.0)
43
+ rubocop (~> 1.21)
44
+ snapshot_testing
45
+
46
+ BUNDLED WITH
47
+ 2.4.9
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Alberto Restifo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # IGC Parser
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/igc.svg)](https://badge.fury.io/rb/igc)
4
+ [![Build Status](https://github.com/wefly-world/igc/actions/workflows/main.yml/badge.svg)](https://github.com/wefly-world/igc/actions)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ IGC is a Ruby library for parsing International Gliding Commission (IGC) files. It is fully compliant with the IGC specification and allows you to easily extract flight data from IGC files.
8
+
9
+ IGC files are used to record flight data such as GPS coordinates, altitude, and timestamps from gliders, paragliders, and other free-flying aircraft.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'igc'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ ```sh
22
+ $ bundle install
23
+ ```
24
+
25
+ Or install it yourself as:
26
+
27
+ ```sh
28
+ $ gem install igc
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ Using the IGC library is simple. You can parse an IGC file with just one line of code:
34
+
35
+ ```ruby
36
+ flight = IGC.parse("path_to_file.igc")
37
+ ```
38
+
39
+ Example
40
+
41
+ ```ruby
42
+ require 'igc'
43
+
44
+ # Parse an IGC file
45
+ flight = IGC.parse("path_to_file.igc")
46
+
47
+ # Access flight data
48
+ puts "Pilot: #{flight.pilot}"
49
+ puts "Glider Type: #{flight.glider_type}"
50
+ puts "Glider ID: #{flight.glider_id}"
51
+ puts "Date: #{flight.date}"
52
+
53
+ # Iterate through fixes
54
+ flight.fixes.each do |fix|
55
+ puts "Time: #{fix.time} Latitude: #{fix.location.latitude} Longitude: #{fix.location.longitude} Altitude: #{fix.gps_altitude}"
56
+ end
57
+ ```
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ end
11
+
12
+ require "rubocop/rake_task"
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[test rubocop]
data/igc.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/igc/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "igc"
7
+ spec.version = IGC::VERSION
8
+ spec.authors = ["Alberto Restifo"]
9
+ spec.email = ["alberto@restifo.dev"]
10
+
11
+ spec.summary = "IGC flight log parser"
12
+ spec.description = "Parses IGC flight log files according to the latest IGC specification"
13
+ spec.homepage = "https://github.com/wefly-world/igc"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 2.6.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/wefly-world/igc"
19
+ spec.metadata["changelog_uri"] = "https://github.com/wefly-world/igc/blob/master/CHANGELOG.md"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(__dir__) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
26
+ end
27
+ end
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
32
+ # Uncomment to register a new dependency of your gem
33
+ # spec.add_dependency "example-gem", "~> 1.0"
34
+
35
+ # For more information and examples about making a new gem, check out our
36
+ # guide at: https://bundler.io/guides/creating_gem.html
37
+ end
data/lib/igc/fix.rb ADDED
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IGC
4
+ # IGC::Fix represents a single fix in a recorder flight
5
+ class Fix
6
+ attr_accessor :location,
7
+ :time,
8
+ :validity,
9
+ :pressure_altitude,
10
+ :gps_altitude,
11
+ :extensions,
12
+ :magnetic_heading,
13
+ :true_heading,
14
+ :air_speed,
15
+ :satellite_in_use,
16
+ :wind_direction,
17
+ :wind_speed,
18
+ :horizontal_accuracy,
19
+ :vertical_accuracy,
20
+ :compensated_variomenter,
21
+ :uncompensated_variometer
22
+
23
+ def initialize
24
+ @extensions = {}
25
+ end
26
+
27
+ def assign_known_extensions!
28
+ @extensions.each do |key, value|
29
+ case key
30
+ when "HDM"
31
+ @magnetic_heading = value.to_i
32
+ when "HDT"
33
+ @true_heading = value.to_i
34
+ when "IAS"
35
+ @air_speed = value.to_i
36
+ when "SIU"
37
+ @satellite_in_use = value.to_i
38
+ when "WDI"
39
+ @wind_direction = value.to_i
40
+ when "WSP"
41
+ @wind_speed = value.to_i
42
+ when "FXA"
43
+ @horizontal_accuracy = value.to_i
44
+ when "VXA"
45
+ @vertical_accuracy = value.to_i
46
+ when "VAT"
47
+ @compensated_variomenter = value.to_i / 100
48
+ when "VAR"
49
+ @uncompensated_variometer = value.to_i / 100
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
data/lib/igc/flight.rb ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IGC
4
+ # IGC::Flight represents a the fligth as recorder in the IGC file
5
+ class Flight
6
+ attr_accessor :reorder_id,
7
+ :raw_headers,
8
+ :gps_altitude_ref,
9
+ :pressure_altitude_ref,
10
+ :competition_class,
11
+ :competition_id,
12
+ :flight_recorder_type,
13
+ :glider_id,
14
+ :glider_type,
15
+ :pilot,
16
+ :pressure_sensor,
17
+ :firmware_version,
18
+ :hardware_version,
19
+ :timezone,
20
+ :date,
21
+ :flight_number,
22
+ :fix_extensions,
23
+ :data_extensions,
24
+ :fixes
25
+
26
+ def initialize
27
+ @raw_headers = {}
28
+ @fix_extensions = {}
29
+ @data_extensions = {}
30
+ @fixes = []
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IGC
4
+ # IGC::Location represents a location in the world using WGS84 coordinates
5
+ class Location
6
+ attr_accessor :latitude,
7
+ :longitude
8
+
9
+ def initialize(latitude:, longitude:)
10
+ @latitude = latitude
11
+ @longitude = longitude
12
+ end
13
+ end
14
+ end
data/lib/igc/parser.rb ADDED
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+ require "date"
5
+
6
+ module IGC
7
+ # :nodoc:
8
+ class Parser
9
+ def initialize(file)
10
+ @buffer = StringScanner.new(file)
11
+ @flight = Flight.new
12
+ end
13
+
14
+ def parse
15
+ parse_line until @buffer.eos?
16
+ @flight
17
+ end
18
+
19
+ private
20
+
21
+ def parse_line
22
+ case @buffer.peek(1)
23
+ when "A"
24
+ skip
25
+ @flight.reorder_id = read_to_eol
26
+ when "H"
27
+ parse_header_line
28
+ when "B"
29
+ parse_fix_line
30
+ when "I"
31
+ parse_extension_line
32
+ when "J"
33
+ parse_extension_line
34
+ else
35
+ # Skip the line
36
+ read_to_eol
37
+ end
38
+ end
39
+
40
+ # Reads the buffer until the end of the line,
41
+ # leaving the buffer at the beginning of the next line
42
+ def read_to_eol
43
+ @buffer.scan_until(/\n/).chomp
44
+ end
45
+
46
+ # Advances the buffer to the next char
47
+ def skip(n = 1)
48
+ n.times { @buffer.getch }
49
+ end
50
+
51
+ # Consumes n characters from the buffer
52
+ def take(n = 1)
53
+ res = ""
54
+ n.times do
55
+ res += @buffer.getch
56
+ end
57
+ res
58
+ end
59
+
60
+ # Parses a header definition line
61
+ def parse_header_line
62
+ skip(2) # HF
63
+
64
+ short_code = take(3)
65
+ subject = parse_header_subject
66
+ value = read_to_eol
67
+
68
+ # Assign the raw headers
69
+ @flight.raw_headers[short_code] = { subject: subject, value: value }
70
+ assign_known_header(short_code, value)
71
+ end
72
+
73
+ def parse_header_subject
74
+ start_pos = @buffer.pos
75
+
76
+ # Check and see if we have a `:` before reaching end of line
77
+ subject = @buffer.scan_until(/[\r\n]|:/)
78
+
79
+ # If we have a `:`, then we have a subject
80
+ return subject.chop if subject.include?(":")
81
+
82
+ # Otherwise resert to the initial position as we don't have a subject
83
+ @buffer.pos = start_pos
84
+ nil
85
+ end
86
+
87
+ def assign_known_header(short_code, value)
88
+ case short_code
89
+ when "ALG"
90
+ @flight.gps_altitude_ref = value
91
+ when "ALP"
92
+ @flight.pressure_altitude_ref = value
93
+ when "CCL"
94
+ @flight.competition_class = value
95
+ when "CID"
96
+ @flight.competition_id = value
97
+ when "FTY"
98
+ @flight.flight_recorder_type = value
99
+ when "GID"
100
+ @flight.glider_id = value
101
+ when "GTY"
102
+ @flight.glider_type = value
103
+ when "PLT"
104
+ @flight.pilot = value
105
+ when "PRS"
106
+ @flight.pressure_sensor = value
107
+ when "RFW"
108
+ @flight.firmware_version = value
109
+ when "RHW"
110
+ @flight.hardware_version = value
111
+ when "TZN"
112
+ @flight.timezone = value
113
+ when "DTE"
114
+ parse_date(value)
115
+ end
116
+ end
117
+
118
+ def parse_date(value)
119
+ day = value[0..1].to_i
120
+ month = value[2..3].to_i
121
+ year = value[4..5].to_i
122
+
123
+ @flight.date = Date.new(2000 + year, month, day)
124
+
125
+ return unless value.length > 5
126
+
127
+ @flight.flight_number = value[6..]
128
+ end
129
+
130
+ def parse_extension_line
131
+ kind = take(1) == "I" ? :fix_extensions : :data_extensions
132
+
133
+ nr_extensions = take(2).to_i
134
+ nr_extensions.times do
135
+ start_byte = take(2).to_i
136
+ end_byte = take(2).to_i
137
+ short_code = take(3)
138
+
139
+ # Assign the range to the extension map
140
+ @flight.send(kind)[short_code] = start_byte..end_byte
141
+ end
142
+ end
143
+
144
+ def parse_fix_line
145
+ # Read the entire line, this will make it easier to parse the extensions
146
+ # later on, but we also reset the position of the buffer to the beginning
147
+ current_pos = @buffer.pos
148
+ fix_line = read_to_eol
149
+ @buffer.pos = current_pos
150
+
151
+ skip
152
+
153
+ fix = Fix.new
154
+
155
+ hours = take(2).to_i
156
+ minutes = take(2).to_i
157
+ seconds = take(2).to_i
158
+
159
+ fix.time = Time.utc(@flight.date.year, @flight.date.month, @flight.date.day, hours, minutes, seconds)
160
+ fix.location = parse_location
161
+ fix.validity = take(1)
162
+ fix.pressure_altitude = take(5).to_i
163
+ fix.gps_altitude = take(5).to_i
164
+ fix.extensions = read_extensions_values(fix_line, @flight.fix_extensions.clone)
165
+ fix.assign_known_extensions!
166
+
167
+ @flight.fixes << fix
168
+ end
169
+
170
+ # kind here is either :lat or :lon
171
+ def parse_coord(kind)
172
+ size = kind == :lat ? 2 : 3
173
+ degrees = take(size).to_f
174
+ minutes = take(5).to_f / 1000
175
+
176
+ pole = take(1)
177
+ sign = %w[N E].include?(pole) ? 1 : -1
178
+
179
+ sign * (degrees + minutes / 60)
180
+ end
181
+
182
+ def parse_location
183
+ lat = parse_coord(:lat)
184
+ lon = parse_coord(:lon)
185
+ Location.new(latitude: lat, longitude: lon)
186
+ end
187
+
188
+ def read_extensions_values(line, extensions)
189
+ extensions.transform_values! { |range| line[range] }
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IGC
4
+ VERSION = "0.1.0"
5
+ end
data/lib/igc.rb ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "igc/version"
4
+ require_relative "igc/flight"
5
+ require_relative "igc/location"
6
+ require_relative "igc/fix"
7
+ require_relative "igc/parser"
8
+
9
+ # IGC is a library to parse IGC files
10
+ #
11
+ # IGC.parse_file("path/to/file.igc")
12
+ #
13
+ module IGC
14
+ class Error < StandardError; end
15
+
16
+ def self.parse(str)
17
+ Parser.new(str).parse
18
+ end
19
+
20
+ def self.parse_file(path)
21
+ parse(File.read(path))
22
+ end
23
+ end
data/sig/igc.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module IGC
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: igc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Alberto Restifo
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-04-01 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Parses IGC flight log files according to the latest IGC specification
14
+ email:
15
+ - alberto@restifo.dev
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".rubocop.yml"
21
+ - CHANGELOG.md
22
+ - Gemfile
23
+ - Gemfile.lock
24
+ - LICENSE.txt
25
+ - README.md
26
+ - Rakefile
27
+ - igc.gemspec
28
+ - lib/igc.rb
29
+ - lib/igc/fix.rb
30
+ - lib/igc/flight.rb
31
+ - lib/igc/location.rb
32
+ - lib/igc/parser.rb
33
+ - lib/igc/version.rb
34
+ - sig/igc.rbs
35
+ homepage: https://github.com/wefly-world/igc
36
+ licenses:
37
+ - MIT
38
+ metadata:
39
+ homepage_uri: https://github.com/wefly-world/igc
40
+ source_code_uri: https://github.com/wefly-world/igc
41
+ changelog_uri: https://github.com/wefly-world/igc/blob/master/CHANGELOG.md
42
+ post_install_message:
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 2.6.0
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubygems_version: 3.4.9
58
+ signing_key:
59
+ specification_version: 4
60
+ summary: IGC flight log parser
61
+ test_files: []