igc 0.1.0

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.
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: []