pr2gpx 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +7 -0
- data/README.md +70 -0
- data/bin/pr2gpx +2 -0
- data/lib/pr2gpx.rb +124 -0
- data/lib/pr2gpx/options.rb +98 -0
- data/lib/pr2gpx/parser.rb +119 -0
- metadata +71 -0
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
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: []
|