pr2gpx 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2012 David Tischler
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,70 @@
1
+ pr2gpx
2
+ ======
3
+
4
+ Converts Winlink position reports sent or received with Airmail into GPX tracks or waypoints.
5
+
6
+ It understands:
7
+ - sent position reports (Window->Winlink-2000->Position Report)
8
+ - reports requested for specific stations (Window->Winlink-2000->Position Request)
9
+ - lists of nearby stations (Window->Catalogs->WL2K->Global->WL2K_USERS->WL2K_NEARBY)
10
+
11
+ pr2gpx scans a directory for files containing position reports in various formats. It can filter them to include only stations with a given callsign, and limit the results to the last N reports per station.
12
+
13
+ The output is either one track for each station, a list of waypoints for every position report, or both.
14
+
15
+ This can either be saved into a single file containing everything, or it can be split into one file for each station.
16
+
17
+
18
+ Installing
19
+ ----------
20
+
21
+ First make sure you have Ruby >= 1.9 running.
22
+
23
+ Then install pr2gpx:
24
+
25
+ gem install pr2gpx
26
+
27
+
28
+ Using
29
+ -----
30
+
31
+ pr2gpx --help
32
+
33
+ Displays information about the usage.
34
+
35
+
36
+ pr2gpx --input c:\ProgramData\Airmail\Outbox
37
+
38
+ Writes all position reports in the outbox to STDOUT.
39
+
40
+
41
+ pr2gpx --input c:\ProgramData\Airmail\Inbox --last 1 --callsign CALL1,CALL2,CALL3 --output C:\ProgramData\opencpn\layers\positions.gpx
42
+
43
+ Writes the most recent position report of CALL1, CALL2 and CALL3 into positions.gpx in C:\ProgramData\opencpn\layers.
44
+
45
+
46
+ pr2gpx --input c:\ProgramData\Airmail\Inbox --last 10 --callsign CALL1,CALL2,CALL3 --output C:\ProgramData\opencpn\layers --split
47
+
48
+ Writes the 10 most recent position reports of CALL1, CALL2 and CALL3 into PR_CALL1.gpx, PR_CALL2.gpx and PR_CALL3.gpx in C:\ProgramData\opencpn\layers.
49
+
50
+
51
+ Developing
52
+ ----------
53
+
54
+ Source: http://github.com/tischdla/pr2gpx
55
+
56
+
57
+ Run tests:
58
+
59
+ rake test
60
+
61
+
62
+ Create gem package:
63
+
64
+ rake package
65
+
66
+
67
+ Copyright
68
+ ---------
69
+
70
+ Copyright (c) 2012 David Tischler, licensed under the MIT License.
data/bin/pr2gpx ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env ruby
2
+ require 'pr2gpx.rb'
data/lib/pr2gpx.rb ADDED
@@ -0,0 +1,124 @@
1
+ require 'nokogiri'
2
+ require 'nokogiri/xml'
3
+ require 'pr2gpx/parser'
4
+ require 'pr2gpx/options'
5
+
6
+ def enumerate_files search_path
7
+ Enumerator.new do |e|
8
+ Dir
9
+ .glob(search_path)
10
+ .each do |filename|
11
+ File.open filename do |file|
12
+ e.yield file.read()
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ def load_data content_enum, filter
19
+ reportParser = ReportParser.new
20
+ stations = Hash.new
21
+
22
+ content_enum.each do |content|
23
+ reports = reportParser.parse(content)
24
+ if reports
25
+ reports.each do |report|
26
+ if filter.include? report
27
+ stations[report.callsign] = Hash.new unless stations.has_key? report.callsign
28
+ stations[report.callsign][report.date] = report unless stations[report.callsign].has_key? report.date
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ stations
35
+ end
36
+
37
+ def filter_data! stations, last
38
+ stations.each do |callsign, reports|
39
+ stations[callsign] = reports.values
40
+ .sort { |report1, report2| report1.date <=> report2.date }
41
+
42
+ stations[callsign] = stations[callsign]
43
+ .reverse
44
+ .take(last)
45
+ .reverse if last
46
+ end
47
+ end
48
+
49
+ def build_gpx stations, create_trk, create_wpt
50
+ builder = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
51
+ xml.gpx(xmlns: 'http://www.topografix.com/GPX/1/1') do
52
+ if create_trk
53
+ stations.each do |callsign, reports|
54
+ xml.trk do
55
+ xml.name callsign
56
+ xml.trkseg do
57
+ reports.each do |report|
58
+ xml.send('trkpt', lat: report.position.latitude, lon: report.position.longitude) do
59
+ xml.name report.comment
60
+ xml.time report.date.strftime('%FT%TZ')
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ if create_wpt
68
+ stations.each do |callsign, reports|
69
+ last = reports.last
70
+ reports.each do |report|
71
+ xml.send('wpt', lat: report.position.latitude, lon: report.position.longitude) do
72
+ xml.name report.callsign
73
+ xml.desc report.comment
74
+ xml.time report.date.strftime('%FT%TZ')
75
+ xml.type 'WPT'
76
+ xml.sym report == last ? 'triangle' : 'circle'
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ builder.to_xml
84
+ end
85
+
86
+ def write_gpx filename, gpx
87
+ File.open filename, 'w:UTF-8' do |file|
88
+ file.write gpx
89
+ end
90
+ end
91
+
92
+ options = parse_options ARGV
93
+ exit if not options
94
+
95
+ options[:input].gsub!('\\', '/')
96
+ options[:output].gsub!('\\', '/') if options[:output]
97
+
98
+ search_path = "#{options[:input]}/#{options[:recurse] ? '**/' : ''}*.*"
99
+ $stderr.puts "Searching #{search_path}" if $verbose
100
+
101
+ filter = ReportFilter.new options[:callsign]
102
+
103
+ stations = load_data(enumerate_files(search_path), filter)
104
+ filter_data! stations, options[:last]
105
+
106
+ if options[:split]
107
+ stations.each do |callsign, reports|
108
+ gpx = build_gpx({ callsign => reports }, options[:create_trk], options[:create_wpt])
109
+
110
+ if options[:output] then
111
+ write_gpx "#{options[:output]}/#{options[:prefix]}#{callsign}.gpx", gpx
112
+ else
113
+ puts gpx
114
+ end
115
+ end
116
+ else
117
+ gpx = build_gpx(stations, options[:create_trk], options[:create_wpt])
118
+
119
+ if options[:output] then
120
+ write_gpx options[:output], gpx
121
+ else
122
+ puts gpx
123
+ end
124
+ end
@@ -0,0 +1,98 @@
1
+ require 'optparse'
2
+
3
+ def parse_options argv
4
+ options = {}
5
+ mandatory = [:input]
6
+
7
+ def set_option options, name
8
+ ->(options, name, value) { options[name] = value }.curry.(options, name)
9
+ end
10
+
11
+ OptionParser.new do |o|
12
+ options[:input] = nil
13
+ o.on '-i', '--input PATH',
14
+ 'Specifies the path to be searched for files containing position reports.',
15
+ set_option(options, :input)
16
+
17
+ options[:recurse] = false
18
+ o.on '-r', '--recurse',
19
+ 'Enables recursive searching.' do
20
+ options[:recurse] = true
21
+ end
22
+
23
+ options[:output] = nil
24
+ o.on '-o', '--output [PATH]',
25
+ 'Specifies the file or directory (when using --split) to write to. Default is STDOUT.',
26
+ set_option(options, :output)
27
+
28
+ options[:split] = false
29
+ o.on '-s', '--split',
30
+ 'Creates one GPX document per station. If --output is specified, it is interpreted as a directory, and one file is created per station, named [PREFIX][STATION].gpx.' do |value|
31
+ options[:split] = value
32
+ end
33
+
34
+ options[:prefix] = 'PR_'
35
+ o.on '-p', '--prefix [PREFIX]',
36
+ 'Specifies the prefix to be used when using --split, default is \'PR_\'.',
37
+ set_option(options, :output)
38
+
39
+ options[:callsign] = nil
40
+ o.on '-c', '--callsign [CALLSIGN[,CALLSIGN]]', Array,
41
+ 'Processes only the stations with the given callsigns.',
42
+ set_option(options, :callsign)
43
+
44
+ options[:last] = nil
45
+ o.on '-l', '--last [n]', Integer,
46
+ 'Limits the result to the last N entries for each station.',
47
+ set_option(options, :last)
48
+
49
+ options[:help] = false
50
+ o.on '-h', '--help',
51
+ 'Displays this screen.' do
52
+ options[:help] = true
53
+ end
54
+
55
+ options[:create_trk] = options[:create_wpt] = true
56
+ o.on '-f', '--format FORMAT[,FORMAT]', Array,
57
+ 'Selects one or more output formats. Supported values are \'TRK\', \'WPT\'.' do |values|
58
+ values.each do |value|
59
+ options[:create_trk] = options[:create_wpt] = false
60
+ case(value)
61
+ when 'TRK' then options[:create_trk] = true
62
+ when 'WPT' then options[:create_wpt] = true
63
+ else raise OptionParser::InvalidOption.new value
64
+ end
65
+ end
66
+ end
67
+
68
+ $verbose = false
69
+ o.on '-v', '--verbose',
70
+ 'Turns on verbose mode.' do |value|
71
+ $verbose = value
72
+ end
73
+
74
+ begin
75
+ o.parse! argv
76
+
77
+ if not options[:help]
78
+ missing = mandatory.select { |param| options[param].nil? }
79
+ if not missing.empty?
80
+ puts "Missing options: #{missing.join(', ')}"
81
+ puts
82
+ options[:help] = true
83
+ end
84
+ end
85
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument
86
+ puts $!.to_s
87
+ puts
88
+ options[:help] = true
89
+ end
90
+
91
+ if options[:help]
92
+ puts o
93
+ return nil
94
+ end
95
+ end
96
+
97
+ options
98
+ end
@@ -0,0 +1,119 @@
1
+ require 'date'
2
+
3
+ PositionReport = Struct.new "PositionReport", :callsign, :date, :position, :comment
4
+
5
+ class Position
6
+ attr_reader :latitude, :longitude
7
+
8
+ def initialize latitude, longitude
9
+ @latitude = parse_angle latitude
10
+ @longitude = parse_angle longitude
11
+ end
12
+
13
+ def == other
14
+ self.latitude == other.latitude and
15
+ self.longitude == other.longitude
16
+ end
17
+
18
+ def parse_angle value
19
+ if /(?<degrees>\d+)-(?<minutes>\d+\.\d+)(?<sign>[NSWE])/ =~ value
20
+ (degrees.to_f + minutes.to_f / 60) * ((sign == 'S' or sign == 'W') ? -1 : 1)
21
+ end
22
+ end
23
+ end
24
+
25
+ class ReportParser
26
+ def initialize
27
+ @parsers = [NearbyStationsParser.new, ReportsListParser.new, OutboundReportParser.new]
28
+ end
29
+
30
+ def parse input
31
+ parser = @parsers.find { |parser| parser.can_parse? input }
32
+
33
+ if parser
34
+ parser.parse input
35
+ else
36
+ nil
37
+ end
38
+ end
39
+ end
40
+
41
+ class OutboundReportParser
42
+ def can_parse? input
43
+ /Subject: POSITION REPORT/ =~ input
44
+ end
45
+
46
+ def parse input
47
+ Enumerator.new do |e|
48
+ if %r{
49
+ (X-From:[ ](?<callsign>[^\n]*))\n
50
+ .*
51
+ (TIME:[ ](?<year>\d+)/(?<month>\d+)/(?<day>\d+)[ ](?<hour>\d+):(?<minute>\d+))\n
52
+ (LATITUDE:[ ](?<latitude>[^\n]*))\n
53
+ (LONGITUDE:[ ](?<longitude>[^\n]*))\n
54
+ (COMMENT:[ ](?<comment>[^\n]*))}xm =~ input
55
+ then
56
+ e.yield PositionReport.new callsign,
57
+ DateTime.new(year.to_i, month.to_i, day.to_i, hour.to_i, minute.to_i, 0),
58
+ Position.new(latitude, longitude),
59
+ comment
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ class ReportsListParser
66
+ def can_parse? input
67
+ /Automated Reply Message from Winlink 2000 Position Report Processor/ =~ input
68
+ end
69
+
70
+ def parse input
71
+ Enumerator.new do |e|
72
+ input.scan %r{
73
+ \n\n(?<callsign>\w*)[ ]
74
+ (?<year>\d+)/(?<month>\d+)/(?<day>\d+)[ ](?<hour>\d+):(?<minute>\d+)[ ]
75
+ (?<latitude>[^\n]*)[ ]
76
+ (?<longitude>[^\n]*)\n
77
+ Comment:[ ](?<comment>[^\n]*)
78
+ }xm do |callsign, year, month, day, hour, minute, latitude, longitude, comment|
79
+ e.yield PositionReport.new callsign,
80
+ DateTime.new(year.to_i, month.to_i, day.to_i, hour.to_i, minute.to_i, 0),
81
+ Position.new(latitude, longitude),
82
+ comment
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ class NearbyStationsParser
89
+ def can_parse? input
90
+ /List of users nearby/ =~ input
91
+ end
92
+
93
+ def parse input
94
+ Enumerator.new do |e|
95
+ input.scan %r{^
96
+ (?<callsign>[^ ]*)[ ].*[ ][ ][ ]
97
+ (?<latitude>[^ ]*)[ ]
98
+ (?<longitude>[^ ]*)[ ][ ]
99
+ (?<year>\d+)/(?<month>\d+)/(?<day>\d+)[ ](?<hour>\d+):(?<minute>\d+)[ ][ ]
100
+ (?<comment>[^\n]*)
101
+ }x do |callsign, latitude, longitude, year, month, day, hour, minute, comment|
102
+ e.yield PositionReport.new callsign,
103
+ DateTime.new(year.to_i, month.to_i, day.to_i, hour.to_i, minute.to_i, 0),
104
+ Position.new(latitude, longitude),
105
+ comment
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ class ReportFilter
112
+ def initialize callsigns
113
+ @callsigns = callsigns
114
+ end
115
+
116
+ def include? report
117
+ not @callsigns or @callsigns.include? report.callsign
118
+ end
119
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pr2gpx
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - David Tischler
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-07 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: nokogiri
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.5.5
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: 1.5.5
30
+ description: ! 'Converts Winlink position reports sent or received with Airmail into
31
+ GPX tracks or waypoints.
32
+
33
+ '
34
+ email: david.tischler@gmx.at
35
+ executables:
36
+ - pr2gpx
37
+ extensions: []
38
+ extra_rdoc_files: []
39
+ files:
40
+ - lib/pr2gpx/options.rb
41
+ - lib/pr2gpx/parser.rb
42
+ - lib/pr2gpx.rb
43
+ - bin/pr2gpx
44
+ - MIT-LICENSE
45
+ - README.md
46
+ homepage:
47
+ licenses: []
48
+ post_install_message:
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ! '>='
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements:
65
+ - none
66
+ rubyforge_project:
67
+ rubygems_version: 1.8.24
68
+ signing_key:
69
+ specification_version: 3
70
+ summary: pr2gpx
71
+ test_files: []