tac_scribe 0.3.0-java → 0.7.3-java
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 +4 -4
- data/.rubocop.yml +14 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +43 -0
- data/data/whitelist.example +5 -0
- data/db/001_create_unit_table.rb +2 -0
- data/exe/tac_scribe +22 -2
- data/lib/tac_scribe/cache.rb +162 -0
- data/lib/tac_scribe/daemon.rb +111 -17
- data/lib/tac_scribe/datastore.rb +22 -68
- data/lib/tac_scribe/event_processor.rb +45 -29
- data/lib/tac_scribe/event_queue.rb +12 -22
- data/lib/tac_scribe/version.rb +1 -1
- data/tac_scribe.gemspec +7 -2
- metadata +47 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c39a4f860637756b22a7ce9fd09d74e2c7190478a94dbc779373ba0d213d0b35
|
|
4
|
+
data.tar.gz: 5abb08ea5e750d3e460ade56d89924866badb2a842b62b20672df2f0c349eb53
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0d0ba804851aa3143c0f5b563d7401706dd129b826dc5398174ead33b982a22d48d0871d0d5498eb922c4ca08c83598a7c3928e3f1a69a7cd2094a3a5acf8801
|
|
7
|
+
data.tar.gz: 641fe1a451dff87f915c69fdf6ea36a4053f4924d77141a3617d7019dcf27ff9d9f884f65894d20ae75414d3b18c0b2e40642d2248d9a022cc8061455855afd3
|
data/.rubocop.yml
CHANGED
|
@@ -14,7 +14,10 @@ Metrics/BlockLength:
|
|
|
14
14
|
# There isn't much sense breaking up the OptionParser block since splitting
|
|
15
15
|
# into db and tacview option methods will just break the method length cop
|
|
16
16
|
# and splitting further doesn't aid readability
|
|
17
|
-
- exe/
|
|
17
|
+
- exe/tac_scribe
|
|
18
|
+
# Cannot really avoid this
|
|
19
|
+
- tac_scribe.gemspec
|
|
20
|
+
|
|
18
21
|
Metrics/MethodLength:
|
|
19
22
|
Exclude:
|
|
20
23
|
# Breaking up the initializer doesn't really do much for readability
|
|
@@ -29,3 +32,13 @@ Style/GlobalVars:
|
|
|
29
32
|
Exclude:
|
|
30
33
|
# Using a global variable makes it available to the Pry console
|
|
31
34
|
- bin/console
|
|
35
|
+
AllCops:
|
|
36
|
+
Exclude:
|
|
37
|
+
# These files will need a MAJOR refactoring to get rid of these warnings
|
|
38
|
+
- lib/tac_scribe/cache.rb
|
|
39
|
+
- lib/tac_scribe/datastore.rb
|
|
40
|
+
- lib/tac_scribe/daemon.rb
|
|
41
|
+
ClassVars:
|
|
42
|
+
Exclude:
|
|
43
|
+
# We are fine with this and understand the risks. We are not inheriting.
|
|
44
|
+
- lib/tac_scribe/event_processor.rb
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
ruby-2.7.0
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.7.3] - 2020-08-10
|
|
10
|
+
### Changed
|
|
11
|
+
- Bug fixes after adding CinC support.
|
|
12
|
+
|
|
13
|
+
## [0.7.2] - 2020-08-10
|
|
14
|
+
### Added
|
|
15
|
+
- Support for "Commander-In-Chief" to get airbase information
|
|
16
|
+
|
|
17
|
+
## [0.7.1] - 2020-08-10
|
|
18
|
+
### Changed
|
|
19
|
+
- Fixed issue where "name" fields were not being populated
|
|
20
|
+
if airfields were not being loaded first
|
|
21
|
+
|
|
22
|
+
## [0.7.0] - 2020-07-5
|
|
23
|
+
### Changed
|
|
24
|
+
- Synced Lat/Lon calculations with DCS and fixed issue where
|
|
25
|
+
calculations were incorrect if only one value was updated
|
|
26
|
+
|
|
27
|
+
## [0.6.2] - 2020-04-19
|
|
28
|
+
### Changed
|
|
29
|
+
- Fixed typo causing app to fail
|
|
30
|
+
|
|
31
|
+
## [0.6.1] - 2020-04-19
|
|
32
|
+
### Changed
|
|
33
|
+
- Test build for gem issue
|
|
34
|
+
|
|
35
|
+
## [0.6.0] - 2020-04-19
|
|
36
|
+
### Added
|
|
37
|
+
- Calculates and stores speed of units
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- Made more robust against failures and more logging messages
|
|
41
|
+
|
|
42
|
+
## [0.5.0] - 2020-03-17
|
|
43
|
+
### Changed
|
|
44
|
+
- Added missing db column to migration file
|
|
45
|
+
- Clear the cache on server restart
|
|
46
|
+
- Add more info to logging
|
|
47
|
+
|
|
48
|
+
## [0.4.0] - 2020-03-13
|
|
49
|
+
### Changed
|
|
50
|
+
- Switch to periodic writing to the database
|
|
51
|
+
|
|
9
52
|
## [0.3.0] - 2019-10-27
|
|
10
53
|
### Changed
|
|
11
54
|
- Changed the command-line name to match gem
|
data/db/001_create_unit_table.rb
CHANGED
data/exe/tac_scribe
CHANGED
|
@@ -10,6 +10,8 @@ options = {
|
|
|
10
10
|
tacview_port: 42_674,
|
|
11
11
|
tacview_password: nil,
|
|
12
12
|
tacview_client_name: 'TacScribe',
|
|
13
|
+
cinc_enabled: false,
|
|
14
|
+
cinc_port: 9000,
|
|
13
15
|
db_host: 'localhost',
|
|
14
16
|
db_port: 5432,
|
|
15
17
|
db_name: 'tac_scribe',
|
|
@@ -43,6 +45,17 @@ OptionParser.new do |opts|
|
|
|
43
45
|
options[:tacview_client_name] = v
|
|
44
46
|
end
|
|
45
47
|
|
|
48
|
+
opts.separator "\nCommander-In-Chief Options"
|
|
49
|
+
opts.on('-b', '--enable-cinc',
|
|
50
|
+
'Is Cinc enabled? (Default: false)') do |_v|
|
|
51
|
+
options[:cinc_enabled] = true
|
|
52
|
+
end
|
|
53
|
+
opts.on('-m', '--cinc-port=port',
|
|
54
|
+
'Cinc server port (Default: 9001)') do |v|
|
|
55
|
+
options[:cinc_port] = v
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
|
|
46
59
|
opts.separator "\nDatabase Options"
|
|
47
60
|
opts.on('-o', '--db-host=host',
|
|
48
61
|
'Postgresql server hostname / IP (Default: localhost)') do |v|
|
|
@@ -52,7 +65,7 @@ OptionParser.new do |opts|
|
|
|
52
65
|
'Postgresql server port (Default: 5432)') do |v|
|
|
53
66
|
options[:db_port] = v
|
|
54
67
|
end
|
|
55
|
-
opts.on('-u', '--db-
|
|
68
|
+
opts.on('-u', '--db-user=username',
|
|
56
69
|
'Postgresql username (Required)') do |v|
|
|
57
70
|
options[:db_user] = v
|
|
58
71
|
end
|
|
@@ -92,6 +105,11 @@ end.parse!
|
|
|
92
105
|
exit(1)
|
|
93
106
|
end
|
|
94
107
|
|
|
108
|
+
if(options[:populate_airfields] && options[:cinc_enabled])
|
|
109
|
+
puts "You cannot populate airfields AND enable Cinc at the same time"
|
|
110
|
+
exit(1)
|
|
111
|
+
end
|
|
112
|
+
|
|
95
113
|
TacScribe::Daemon.new(
|
|
96
114
|
tacview_host: options[:tacview_host],
|
|
97
115
|
tacview_port: options[:tacview_port],
|
|
@@ -105,5 +123,7 @@ TacScribe::Daemon.new(
|
|
|
105
123
|
verbose_logging: options[:verbose],
|
|
106
124
|
thread_count: options[:threads].to_i,
|
|
107
125
|
populate_airfields: options[:populate_airfields],
|
|
108
|
-
whitelist: options[:whitelist]
|
|
126
|
+
whitelist: options[:whitelist],
|
|
127
|
+
cinc_enabled: options[:cinc_enabled],
|
|
128
|
+
cinc_port: options[:cinc_port]
|
|
109
129
|
).start_processing
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'georuby'
|
|
4
|
+
require 'sequel'
|
|
5
|
+
require 'sequel-postgis-georuby'
|
|
6
|
+
require 'singleton'
|
|
7
|
+
require 'concurrent-ruby'
|
|
8
|
+
require 'haversine'
|
|
9
|
+
|
|
10
|
+
module TacScribe
|
|
11
|
+
# The in-memory cache that is updated by events in real-time before the data
|
|
12
|
+
# is synced to the DB
|
|
13
|
+
class Cache
|
|
14
|
+
include Singleton
|
|
15
|
+
include GeoRuby::SimpleFeatures
|
|
16
|
+
|
|
17
|
+
@@cache = Concurrent::Hash.new
|
|
18
|
+
|
|
19
|
+
attr_accessor :reference_latitude, :reference_longitude
|
|
20
|
+
|
|
21
|
+
def data
|
|
22
|
+
@@cache
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def write_object(object)
|
|
26
|
+
if (reference_latitude != 0 || reference_longitude != 0) && object[:type] != "Ground+Static+Aerodrome"
|
|
27
|
+
localize_position(object)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
id = object[:object_id]
|
|
31
|
+
id ||= object[:id].to_s
|
|
32
|
+
object[:id] = id
|
|
33
|
+
|
|
34
|
+
cache_object = @@cache[id]
|
|
35
|
+
|
|
36
|
+
object[:position] = set_position(object, cache_object)
|
|
37
|
+
|
|
38
|
+
%i[object_id latitude longitude color country].each do |key|
|
|
39
|
+
object.delete(key)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# https://wiki.hoggitworld.com/view/DCS_singleton_coalition
|
|
43
|
+
# Tacview returns a string, CinC returns an integer so we
|
|
44
|
+
# only do the conversion if there is a string.
|
|
45
|
+
if object[:coalition] && object[:coalition].is_a?(String)
|
|
46
|
+
object[:coalition] = case object[:coalition]
|
|
47
|
+
when 'Enemies' # Enemies is Bluefor
|
|
48
|
+
2
|
|
49
|
+
when 'Allies' # Allies is Redfor
|
|
50
|
+
1
|
|
51
|
+
else # Neutral
|
|
52
|
+
0
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if object[:type] = "Ground+Static+Aerodrome"
|
|
57
|
+
object[:heading] = object[:wind_heading] ? object[:wind_heading] : 0
|
|
58
|
+
object[:speed] = object[:wind_speed] ? object[:wind_speec] : 0
|
|
59
|
+
object.delete(:wind_heading)
|
|
60
|
+
object.delete(:wind_speed)
|
|
61
|
+
object.delete(:category)
|
|
62
|
+
cache_object = object
|
|
63
|
+
# No-op
|
|
64
|
+
elsif cache_object
|
|
65
|
+
object[:heading] = calculate_heading(cache_object, object)
|
|
66
|
+
object[:speed] = calculate_speed(cache_object, object)
|
|
67
|
+
cache_object.merge!(object)
|
|
68
|
+
else
|
|
69
|
+
object[:heading] = -1
|
|
70
|
+
object[:speed] = 0
|
|
71
|
+
cache_object = object
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if !cache_object.key?(:altitude) || !cache_object[:altitude]
|
|
75
|
+
cache_object[:altitude] = 0
|
|
76
|
+
end
|
|
77
|
+
if !cache_object.key?(:coalition) || !cache_object[:coalition]
|
|
78
|
+
cache_object[:coalition] = 2
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
cache_object[:pilot] = nil unless cache_object.key?(:pilot)
|
|
82
|
+
|
|
83
|
+
cache_object[:group] = nil unless cache_object.key?(:group)
|
|
84
|
+
|
|
85
|
+
cache_object[:deleted] = false unless cache_object.key?(:deleted)
|
|
86
|
+
|
|
87
|
+
cache_object[:updated_at] = Time.now
|
|
88
|
+
|
|
89
|
+
@@cache[id] = cache_object
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def delete_object(id)
|
|
93
|
+
@@cache[id][:deleted] = true if @@cache[id]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def clear
|
|
97
|
+
@@cache.clear
|
|
98
|
+
self.reference_latitude = nil
|
|
99
|
+
self.reference_longitude = nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def localize_position(object)
|
|
105
|
+
if reference_latitude && object.key?(:latitude)
|
|
106
|
+
object[:latitude] = reference_latitude + object[:latitude]
|
|
107
|
+
end
|
|
108
|
+
if reference_longitude && object.key?(:longitude)
|
|
109
|
+
object[:longitude] = reference_longitude + object[:longitude]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def set_position(object, cache_object)
|
|
114
|
+
longitude = if object.key?(:longitude)
|
|
115
|
+
object[:longitude]
|
|
116
|
+
else
|
|
117
|
+
cache_object[:position].x
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
latitude = if object.key?(:latitude)
|
|
121
|
+
object[:latitude]
|
|
122
|
+
else
|
|
123
|
+
cache_object[:position].y
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# This "Point" class is not lat/long aware which is why we are
|
|
127
|
+
# flipping things up here because when it converts to SQL we need
|
|
128
|
+
# the long to be first since that is what postgresql expects.
|
|
129
|
+
Point.from_x_y(longitude, latitude)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def calculate_heading(cache_object, object)
|
|
133
|
+
if cache_object[:position] == object[:position]
|
|
134
|
+
return cache_object[:heading]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
begin
|
|
138
|
+
cache_object[:position].bearing_to(object[:position]).to_i
|
|
139
|
+
rescue Math::DomainError => e
|
|
140
|
+
puts 'Could not calculate heading: ' + e.message
|
|
141
|
+
puts 'Old Position: ' + cache_object[:position].inspect
|
|
142
|
+
puts 'New Position: ' + object[:position].inspect
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def calculate_speed(cache_object, object)
|
|
147
|
+
time = object[:game_time] - cache_object[:game_time]
|
|
148
|
+
|
|
149
|
+
start_point = cache_object[:position]
|
|
150
|
+
end_point = object[:position]
|
|
151
|
+
|
|
152
|
+
# Because of the above issues with Point and Lat/Lon
|
|
153
|
+
# the values are reverse here :(
|
|
154
|
+
distance = Haversine.distance(start_point.y, start_point.x,
|
|
155
|
+
end_point.y, end_point.x).to_meters
|
|
156
|
+
|
|
157
|
+
speed = distance / time
|
|
158
|
+
|
|
159
|
+
speed.to_i
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
data/lib/tac_scribe/daemon.rb
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'tacview_client'
|
|
5
5
|
require 'set'
|
|
6
|
+
require 'cinc'
|
|
6
7
|
require_relative 'datastore'
|
|
8
|
+
require_relative 'cache'
|
|
7
9
|
require_relative 'event_queue'
|
|
8
10
|
require_relative 'event_processor'
|
|
9
11
|
|
|
@@ -14,7 +16,7 @@ module TacScribe
|
|
|
14
16
|
def initialize(db_host:, db_port:, db_name:, db_user:, db_password:,
|
|
15
17
|
tacview_host:, tacview_port:, tacview_password:,
|
|
16
18
|
tacview_client_name:, verbose_logging:, thread_count:,
|
|
17
|
-
populate_airfields:, whitelist: nil)
|
|
19
|
+
populate_airfields:, whitelist: nil, cinc_enabled:, cinc_port:)
|
|
18
20
|
Datastore.instance.configure do |config|
|
|
19
21
|
config.host = db_host
|
|
20
22
|
config.port = db_port
|
|
@@ -24,20 +26,29 @@ module TacScribe
|
|
|
24
26
|
end
|
|
25
27
|
Datastore.instance.connect
|
|
26
28
|
|
|
27
|
-
@event_queue = EventQueue.new
|
|
29
|
+
@event_queue = EventQueue.new
|
|
30
|
+
|
|
31
|
+
@verbose_logging = verbose_logging
|
|
28
32
|
|
|
29
33
|
@populate_airfields = populate_airfields
|
|
30
34
|
@thread_count = thread_count
|
|
31
|
-
@
|
|
35
|
+
@threads = {}
|
|
32
36
|
@whitelist = Set.new(IO.read(whitelist).split) if whitelist
|
|
33
37
|
|
|
34
|
-
@
|
|
38
|
+
@tacview_client = TacviewClient::Client.new(
|
|
35
39
|
host: tacview_host,
|
|
36
40
|
port: tacview_port,
|
|
37
41
|
password: tacview_password,
|
|
38
42
|
processor: @event_queue,
|
|
39
43
|
client_name: tacview_client_name
|
|
40
44
|
)
|
|
45
|
+
|
|
46
|
+
if cinc_enabled
|
|
47
|
+
@cinc_client = Cinc::Client.new(
|
|
48
|
+
host: tacview_host,
|
|
49
|
+
port: cinc_port
|
|
50
|
+
)
|
|
51
|
+
end
|
|
41
52
|
end
|
|
42
53
|
|
|
43
54
|
# Starts processing and reconnects if the client was disconnected.
|
|
@@ -47,43 +58,126 @@ module TacScribe
|
|
|
47
58
|
# for example
|
|
48
59
|
def start_processing
|
|
49
60
|
loop do
|
|
50
|
-
|
|
61
|
+
puts 'Starting processing loop'
|
|
51
62
|
@event_queue.clear
|
|
52
63
|
Datastore.instance.truncate_table
|
|
64
|
+
Cache.instance.clear
|
|
53
65
|
start_processing_threads
|
|
66
|
+
start_cinc_thread
|
|
67
|
+
start_db_sync_thread
|
|
68
|
+
start_reporting_thread
|
|
54
69
|
populate_airfields if @populate_airfields
|
|
55
|
-
@
|
|
70
|
+
@threads.each_pair do |key, _value|
|
|
71
|
+
puts "#{key} thread started"
|
|
72
|
+
end
|
|
73
|
+
@tacview_client.connect
|
|
74
|
+
# If this code is executed it means we have been disconnected without
|
|
75
|
+
# exceptions. We will still sleep to stop a retry storm. Just not as
|
|
76
|
+
# long.
|
|
77
|
+
puts "Disconnected from Tacview"
|
|
78
|
+
kill_threads
|
|
79
|
+
sleep 10
|
|
56
80
|
# Rescuing reliably from Net::HTTP is a complete bear so rescue
|
|
57
81
|
# StandardError. It ain't pretty but it is reliable. We will puts
|
|
58
82
|
# the exception just in case
|
|
59
83
|
# https://stackoverflow.com/questions/5370697/what-s-the-best-way-to-handle-exceptions-from-nethttp
|
|
60
84
|
rescue StandardError => e
|
|
85
|
+
puts 'Exception in processing loop'
|
|
61
86
|
puts e.message
|
|
62
87
|
puts e.backtrace
|
|
88
|
+
kill_threads
|
|
63
89
|
sleep 30
|
|
64
90
|
next
|
|
65
91
|
end
|
|
66
92
|
end
|
|
67
93
|
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
94
|
+
def kill_threads
|
|
95
|
+
puts 'Killing Threads'
|
|
96
|
+
@threads.each_pair do |key, thread|
|
|
97
|
+
puts "Killing #{key} thread"
|
|
98
|
+
thread.kill
|
|
99
|
+
thread.join
|
|
72
100
|
end
|
|
73
101
|
end
|
|
74
102
|
|
|
75
103
|
def start_processing_threads
|
|
76
|
-
@
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
104
|
+
@event_processor = EventProcessor.new(cache: Cache.instance,
|
|
105
|
+
datastore: Datastore.instance,
|
|
106
|
+
event_queue: @event_queue,
|
|
107
|
+
whitelist: @whitelist)
|
|
108
|
+
event_processor_thread = Thread.new do
|
|
109
|
+
@event_processor.start
|
|
110
|
+
end
|
|
111
|
+
event_processor_thread.name = 'Event Processor'
|
|
112
|
+
@threads[:processing] = event_processor_thread
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def start_cinc_thread
|
|
116
|
+
return unless @cinc_client
|
|
117
|
+
cinc_thread = Thread.new do
|
|
118
|
+
loop do
|
|
119
|
+
@cinc_client.connect
|
|
120
|
+
while true
|
|
121
|
+
@cinc_client.get_airbases.each do |airbase|
|
|
122
|
+
@event_queue.update_object Hash[airbase.map{ |k, v| [k.to_sym, v] }]
|
|
123
|
+
end
|
|
124
|
+
sleep 60
|
|
125
|
+
end
|
|
126
|
+
rescue StandardError => e
|
|
127
|
+
puts 'Exception in cinc thread'
|
|
128
|
+
puts e.message
|
|
129
|
+
puts e.backtrace
|
|
130
|
+
sleep 30
|
|
131
|
+
next
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
cinc_thread.name = 'Cinc Processor'
|
|
136
|
+
@threads[:cinc] = cinc_thread
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def start_db_sync_thread
|
|
140
|
+
db_write_thread = Thread.new do
|
|
141
|
+
loop do
|
|
142
|
+
sleep 1
|
|
143
|
+
deleted = Datastore.instance.write_objects(Cache.instance.data.values)
|
|
144
|
+
deleted.each { |id| Cache.instance.data.delete(id) }
|
|
145
|
+
rescue StandardError
|
|
146
|
+
next
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
db_write_thread.name = 'Database Writing'
|
|
150
|
+
@threads[:database] = db_write_thread
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def start_reporting_thread
|
|
154
|
+
return unless @verbose_logging
|
|
155
|
+
|
|
156
|
+
reporting_thread = Thread.new do
|
|
157
|
+
sleep 5
|
|
158
|
+
loop do
|
|
159
|
+
puts "#{Time.now.strftime('%FT%T')}\t" \
|
|
160
|
+
"Events Incoming: #{@event_queue.events_written}\t" \
|
|
161
|
+
"Processed: #{@event_processor.events_processed}\t" \
|
|
162
|
+
"Ignored: #{@event_processor.events_ignored}\t" \
|
|
163
|
+
"Queue Size: #{@event_queue.size}\t" \
|
|
164
|
+
"Objects Written: #{Datastore.instance.written}\t" \
|
|
165
|
+
"Deleted: #{Datastore.instance.deleted}"
|
|
166
|
+
@event_queue.events_written = 0
|
|
167
|
+
@event_processor.events_processed = 0
|
|
168
|
+
@event_processor.events_ignored = 0
|
|
169
|
+
Datastore.instance.written = 0
|
|
170
|
+
Datastore.instance.deleted = 0
|
|
171
|
+
sleep 1
|
|
81
172
|
end
|
|
82
173
|
end
|
|
174
|
+
reporting_thread.name = 'Reporting'
|
|
175
|
+
@threads[:reporting] = reporting_thread
|
|
83
176
|
end
|
|
84
177
|
|
|
85
178
|
def populate_airfields
|
|
86
|
-
json = File.read(File.join(File.dirname(__FILE__),
|
|
179
|
+
json = File.read(File.join(File.dirname(__FILE__),
|
|
180
|
+
'../../data/airfields.json'))
|
|
87
181
|
airfields = JSON.parse(json)
|
|
88
182
|
airfields.each_with_index do |airfield, i|
|
|
89
183
|
@event_queue.update_object(
|
|
@@ -93,7 +187,7 @@ module TacScribe
|
|
|
93
187
|
altitude: BigDecimal(airfield['alt'].to_s),
|
|
94
188
|
type: 'Ground+Static+Aerodrome',
|
|
95
189
|
name: airfield['name'],
|
|
96
|
-
coalition:
|
|
190
|
+
coalition: 0 # Neutral
|
|
97
191
|
)
|
|
98
192
|
end
|
|
99
193
|
end
|
data/lib/tac_scribe/datastore.rb
CHANGED
|
@@ -14,7 +14,7 @@ module TacScribe
|
|
|
14
14
|
include Singleton
|
|
15
15
|
include GeoRuby::SimpleFeatures
|
|
16
16
|
|
|
17
|
-
attr_accessor :db
|
|
17
|
+
attr_accessor :db, :written, :deleted
|
|
18
18
|
|
|
19
19
|
@configuration = nil
|
|
20
20
|
@db = nil
|
|
@@ -52,24 +52,29 @@ module TacScribe
|
|
|
52
52
|
@db[:units].truncate
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
def
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# use the old value
|
|
61
|
-
current_position = get_position(event, unit)
|
|
62
|
-
|
|
63
|
-
if unit
|
|
64
|
-
update_unit(event, unit, current_position, timestamp)
|
|
65
|
-
else
|
|
66
|
-
insert_unit(event, current_position, timestamp)
|
|
55
|
+
def write_objects(objects)
|
|
56
|
+
objects = objects.map do |object|
|
|
57
|
+
obj = object.clone
|
|
58
|
+
obj.delete(:game_time)
|
|
59
|
+
obj
|
|
67
60
|
end
|
|
68
|
-
end
|
|
69
61
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
62
|
+
@db[:units].insert_conflict(
|
|
63
|
+
constraint: :units_pkey,
|
|
64
|
+
update: { position: Sequel[:excluded][:position],
|
|
65
|
+
altitude: Sequel[:excluded][:altitude],
|
|
66
|
+
heading: Sequel[:excluded][:heading],
|
|
67
|
+
speed: Sequel[:excluded][:speed],
|
|
68
|
+
updated_at: Sequel[:excluded][:updated_at],
|
|
69
|
+
deleted: Sequel[:excluded][:deleted] }
|
|
70
|
+
)
|
|
71
|
+
.multi_insert(objects)
|
|
72
|
+
deleted_ids = @db[:units].where(deleted: true).select_map(:id)
|
|
73
|
+
@db[:units].where(deleted: true).delete
|
|
74
|
+
self.written = objects.size
|
|
75
|
+
self.deleted = deleted_ids.size
|
|
76
|
+
|
|
77
|
+
deleted_ids
|
|
73
78
|
end
|
|
74
79
|
|
|
75
80
|
private
|
|
@@ -85,56 +90,5 @@ module TacScribe
|
|
|
85
90
|
"/#{@configuration.database}"
|
|
86
91
|
end
|
|
87
92
|
end
|
|
88
|
-
|
|
89
|
-
def get_unit(id)
|
|
90
|
-
@db[:units].where(id: id).first
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def update_unit(event, unit, current_position, timestamp)
|
|
94
|
-
heading = calculate_heading(unit[:position],
|
|
95
|
-
current_position,
|
|
96
|
-
unit[:heading])
|
|
97
|
-
|
|
98
|
-
@db[:units].where(id: event[:object_id]).update(
|
|
99
|
-
position: current_position[:lat_lon],
|
|
100
|
-
altitude: current_position[:altitude],
|
|
101
|
-
heading: heading,
|
|
102
|
-
updated_at: timestamp
|
|
103
|
-
)
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def insert_unit(event, current_position, timestamp)
|
|
107
|
-
@db[:units].insert(id: event[:object_id],
|
|
108
|
-
position: current_position[:lat_lon],
|
|
109
|
-
altitude: current_position[:altitude],
|
|
110
|
-
type: event[:type],
|
|
111
|
-
name: event[:name],
|
|
112
|
-
group: event[:group],
|
|
113
|
-
pilot: event[:pilot],
|
|
114
|
-
coalition: event[:coalition] == 'Allies' ? 0 : 1,
|
|
115
|
-
updated_at: timestamp)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def get_position(event, unit)
|
|
119
|
-
{
|
|
120
|
-
lat_lon: Point.from_x_y(
|
|
121
|
-
event.fetch(:longitude) { unit ? unit[:position].y : nil },
|
|
122
|
-
event.fetch(:latitude) { unit ? unit[:position].x : nil }
|
|
123
|
-
),
|
|
124
|
-
altitude: event.fetch(:altitude) { unit ? unit[:altitude] : nil }
|
|
125
|
-
}
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def calculate_heading(old_position, current_position, current_heading)
|
|
129
|
-
return current_heading if old_position == current_position[:lat_lon]
|
|
130
|
-
|
|
131
|
-
begin
|
|
132
|
-
old_position.bearing_to(current_position[:lat_lon]).to_i
|
|
133
|
-
rescue Math::DomainError => e
|
|
134
|
-
puts 'Could not calculate heading: ' + e.message
|
|
135
|
-
puts 'Old Position: ' + old_position.inspect
|
|
136
|
-
puts 'New Position: ' + current_position.inspect
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
93
|
end
|
|
140
94
|
end
|
|
@@ -3,26 +3,32 @@
|
|
|
3
3
|
require 'tacview_client/base_processor'
|
|
4
4
|
require 'time'
|
|
5
5
|
require 'concurrent-ruby'
|
|
6
|
-
require_relative '
|
|
6
|
+
require_relative 'cache'
|
|
7
7
|
|
|
8
8
|
module TacScribe
|
|
9
9
|
# Processes the events emitted by the Ruby Tacview Client
|
|
10
10
|
class EventProcessor
|
|
11
11
|
@@ignored_units = Concurrent::Set.new
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
attr_accessor :events_processed, :events_ignored
|
|
14
|
+
|
|
15
|
+
def initialize(cache:, datastore:, event_queue:, whitelist: nil)
|
|
16
|
+
@cache = cache
|
|
14
17
|
@datastore = datastore
|
|
15
18
|
@event_queue = event_queue
|
|
16
19
|
@whitelist = whitelist
|
|
20
|
+
self.events_processed = 0
|
|
21
|
+
self.events_ignored = 0
|
|
17
22
|
end
|
|
18
23
|
|
|
19
24
|
def start
|
|
20
25
|
loop do
|
|
21
|
-
wrapped_event = @event_queue.
|
|
26
|
+
wrapped_event = @event_queue.shift
|
|
22
27
|
process_event(wrapped_event)
|
|
23
28
|
rescue StandardError => e
|
|
24
29
|
puts wrapped_event
|
|
25
30
|
puts e.inspect
|
|
31
|
+
puts e.backtrace
|
|
26
32
|
next
|
|
27
33
|
end
|
|
28
34
|
end
|
|
@@ -33,6 +39,10 @@ module TacScribe
|
|
|
33
39
|
update_object(wrapped_event[:event], wrapped_event[:time])
|
|
34
40
|
when :delete_object
|
|
35
41
|
delete_object(wrapped_event[:object_id])
|
|
42
|
+
when :set_latitude
|
|
43
|
+
update_latitude wrapped_event[:value]
|
|
44
|
+
when :set_longitude
|
|
45
|
+
update_longitude wrapped_event[:value]
|
|
36
46
|
end
|
|
37
47
|
end
|
|
38
48
|
|
|
@@ -56,44 +66,50 @@ module TacScribe
|
|
|
56
66
|
# @option event [BigDecimal] :altitude The object altitude above sea level
|
|
57
67
|
# in meters to 1 decimal place.
|
|
58
68
|
def update_object(event, time)
|
|
59
|
-
return if
|
|
69
|
+
return if ignore_unit?(event)
|
|
60
70
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
# Hack to make sure the :name field is present so that it is included
|
|
72
|
+
# in the SQL
|
|
73
|
+
event[:name] = nil unless event.has_key?(:name)
|
|
74
|
+
event[:game_time] = time
|
|
75
|
+
|
|
76
|
+
self.events_processed += 1
|
|
77
|
+
@cache.write_object(event)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def ignore_unit?(event)
|
|
81
|
+
if @@ignored_units.include?(event[:object_id])
|
|
82
|
+
self.events_ignored += 1
|
|
83
|
+
return true
|
|
64
84
|
end
|
|
65
85
|
|
|
66
|
-
if @
|
|
67
|
-
|
|
86
|
+
if @whitelist && event[:type] && !@whitelist.include?(event[:type])
|
|
87
|
+
@@ignored_units << event[:object_id]
|
|
88
|
+
self.events_ignored += 1
|
|
89
|
+
return true
|
|
68
90
|
end
|
|
69
91
|
|
|
70
|
-
|
|
92
|
+
false
|
|
71
93
|
end
|
|
72
94
|
|
|
73
95
|
# Process a delete event for an object
|
|
74
96
|
#
|
|
75
|
-
# @param
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
97
|
+
# @param id [String] A hexadecimal number representing the object ID
|
|
98
|
+
def delete_object(id)
|
|
99
|
+
if @@ignored_units.delete?(id)
|
|
100
|
+
nil
|
|
101
|
+
else
|
|
102
|
+
@cache.delete_object(id)
|
|
103
|
+
end
|
|
104
|
+
self.events_processed += 1
|
|
105
|
+
end
|
|
79
106
|
|
|
80
|
-
|
|
107
|
+
def update_latitude(value)
|
|
108
|
+
Cache.instance.reference_latitude = value
|
|
81
109
|
end
|
|
82
110
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def localize_position(event)
|
|
86
|
-
# There is no combination of layouts for these lines that doesn' trip
|
|
87
|
-
# at least one rubocop cop so just ignore the guard clause. The best
|
|
88
|
-
# one is using an inline if but that breaks the line length.
|
|
89
|
-
# rubocop:disable Style/GuardClause
|
|
90
|
-
if event[:latitude]
|
|
91
|
-
event[:latitude] = @event_queue.reference_latitude + event[:latitude]
|
|
92
|
-
end
|
|
93
|
-
if event[:longitude]
|
|
94
|
-
event[:longitude] = @event_queue.reference_longitude + event[:longitude]
|
|
95
|
-
end
|
|
96
|
-
# rubocop:enable Style/GuardClause
|
|
111
|
+
def update_longitude(value)
|
|
112
|
+
Cache.instance.reference_longitude = value
|
|
97
113
|
end
|
|
98
114
|
end
|
|
99
115
|
end
|
|
@@ -6,32 +6,22 @@ require 'time'
|
|
|
6
6
|
module TacScribe
|
|
7
7
|
# Processes the events emitted by the Ruby Tacview Client
|
|
8
8
|
class EventQueue < TacviewClient::BaseProcessor
|
|
9
|
-
attr_accessor :
|
|
9
|
+
attr_accessor :events_written
|
|
10
10
|
|
|
11
|
-
def initialize
|
|
12
|
-
@verbose_logging = verbose_logging
|
|
11
|
+
def initialize
|
|
13
12
|
@events = Queue.new
|
|
14
|
-
|
|
15
|
-
@event_read = 0
|
|
16
|
-
|
|
17
|
-
return unless verbose_logging == true
|
|
18
|
-
|
|
19
|
-
Thread.new do
|
|
20
|
-
loop do
|
|
21
|
-
puts "#{Time.now.strftime('%FT%T')} - Queue Size: #{@events.size} \t Events Added: #{@event_write} \t Events Processed: #{@event_read}"
|
|
22
|
-
@event_write = 0
|
|
23
|
-
@event_read = 0
|
|
24
|
-
sleep 1
|
|
25
|
-
end
|
|
26
|
-
end
|
|
13
|
+
self.events_written = 0
|
|
27
14
|
end
|
|
28
15
|
|
|
29
16
|
def clear
|
|
30
17
|
@events.clear
|
|
31
18
|
end
|
|
32
19
|
|
|
33
|
-
def
|
|
34
|
-
@
|
|
20
|
+
def size
|
|
21
|
+
@events.size
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def shift
|
|
35
25
|
@events.shift
|
|
36
26
|
end
|
|
37
27
|
|
|
@@ -53,7 +43,7 @@ module TacScribe
|
|
|
53
43
|
# @option event [BigDecimal] :altitude The object altitude above sea level
|
|
54
44
|
# in meters to 1 decimal place.
|
|
55
45
|
def update_object(event)
|
|
56
|
-
|
|
46
|
+
self.events_written += 1
|
|
57
47
|
@events << { type: :update_object, event: event, time: @time }
|
|
58
48
|
end
|
|
59
49
|
|
|
@@ -62,7 +52,7 @@ module TacScribe
|
|
|
62
52
|
# @param object_id [String] A hexadecimal number representing the object
|
|
63
53
|
# ID
|
|
64
54
|
def delete_object(object_id)
|
|
65
|
-
|
|
55
|
+
self.events_written += 1
|
|
66
56
|
@events << { type: :delete_object, object_id: object_id }
|
|
67
57
|
end
|
|
68
58
|
|
|
@@ -81,9 +71,9 @@ module TacScribe
|
|
|
81
71
|
def set_property(property:, value:)
|
|
82
72
|
case property
|
|
83
73
|
when 'ReferenceLatitude'
|
|
84
|
-
|
|
74
|
+
@events << { type: :set_latitude, value: BigDecimal(value) }
|
|
85
75
|
when 'ReferenceLongitude'
|
|
86
|
-
|
|
76
|
+
@events << { type: :set_longitude, value: BigDecimal(value) }
|
|
87
77
|
when 'ReferenceTime'
|
|
88
78
|
@reference_time = @time = Time.parse(value)
|
|
89
79
|
end
|
data/lib/tac_scribe/version.rb
CHANGED
data/tac_scribe.gemspec
CHANGED
|
@@ -4,6 +4,8 @@ lib = File.expand_path('lib', __dir__)
|
|
|
4
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
5
|
require 'tac_scribe/version'
|
|
6
6
|
|
|
7
|
+
puts "RUBY_PLATFORM = #{RUBY_PLATFORM}"
|
|
8
|
+
|
|
7
9
|
Gem::Specification.new do |spec|
|
|
8
10
|
spec.name = 'tac_scribe'
|
|
9
11
|
spec.version = TacScribe::VERSION
|
|
@@ -24,9 +26,9 @@ Gem::Specification.new do |spec|
|
|
|
24
26
|
|
|
25
27
|
spec.metadata['yard.run'] = 'yri'
|
|
26
28
|
|
|
29
|
+
spec.platform = RUBY_PLATFORM == 'java' ? 'java' : 'ruby'
|
|
30
|
+
|
|
27
31
|
if RUBY_PLATFORM == 'java'
|
|
28
|
-
# TODO: Specifying a verison chokes bundler for some reason
|
|
29
|
-
spec.platform = 'java'
|
|
30
32
|
spec.add_dependency 'activerecord-jdbcpostgresql-adapter'
|
|
31
33
|
else
|
|
32
34
|
spec.add_dependency 'pg', '~>1.1'
|
|
@@ -38,8 +40,10 @@ Gem::Specification.new do |spec|
|
|
|
38
40
|
# of an API breaking change are higher then normal. Therefore lock the
|
|
39
41
|
# version
|
|
40
42
|
spec.add_dependency 'concurrent-ruby', '~>1.1.5'
|
|
43
|
+
spec.add_dependency 'haversine', '~>0.3'
|
|
41
44
|
spec.add_dependency 'sequel-postgis-georuby', '0.1.2'
|
|
42
45
|
spec.add_dependency 'tacview_client', '~>0.1'
|
|
46
|
+
spec.add_dependency 'cinc', '~>0.1'
|
|
43
47
|
|
|
44
48
|
spec.add_development_dependency 'bundler', '~> 2.0'
|
|
45
49
|
spec.add_development_dependency 'rake', '~> 10.0'
|
|
@@ -47,4 +51,5 @@ Gem::Specification.new do |spec|
|
|
|
47
51
|
spec.add_development_dependency 'rubocop', '~>0.73'
|
|
48
52
|
spec.add_development_dependency 'simplecov', '~>0.17'
|
|
49
53
|
spec.add_development_dependency 'yard', '~>0.9'
|
|
54
|
+
spec.add_development_dependency 'pry-byebug', '~>3.9'
|
|
50
55
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tac_scribe
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3
|
|
4
|
+
version: 0.7.3
|
|
5
5
|
platform: java
|
|
6
6
|
authors:
|
|
7
7
|
- Jeffrey Jones
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2020-08-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -66,6 +66,20 @@ dependencies:
|
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
68
|
version: 1.1.5
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0.3'
|
|
75
|
+
name: haversine
|
|
76
|
+
prerelease: false
|
|
77
|
+
type: :runtime
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0.3'
|
|
69
83
|
- !ruby/object:Gem::Dependency
|
|
70
84
|
requirement: !ruby/object:Gem::Requirement
|
|
71
85
|
requirements:
|
|
@@ -94,6 +108,20 @@ dependencies:
|
|
|
94
108
|
- - "~>"
|
|
95
109
|
- !ruby/object:Gem::Version
|
|
96
110
|
version: '0.1'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0.1'
|
|
117
|
+
name: cinc
|
|
118
|
+
prerelease: false
|
|
119
|
+
type: :runtime
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0.1'
|
|
97
125
|
- !ruby/object:Gem::Dependency
|
|
98
126
|
requirement: !ruby/object:Gem::Requirement
|
|
99
127
|
requirements:
|
|
@@ -178,6 +206,20 @@ dependencies:
|
|
|
178
206
|
- - "~>"
|
|
179
207
|
- !ruby/object:Gem::Version
|
|
180
208
|
version: '0.9'
|
|
209
|
+
- !ruby/object:Gem::Dependency
|
|
210
|
+
requirement: !ruby/object:Gem::Requirement
|
|
211
|
+
requirements:
|
|
212
|
+
- - "~>"
|
|
213
|
+
- !ruby/object:Gem::Version
|
|
214
|
+
version: '3.9'
|
|
215
|
+
name: pry-byebug
|
|
216
|
+
prerelease: false
|
|
217
|
+
type: :development
|
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
219
|
+
requirements:
|
|
220
|
+
- - "~>"
|
|
221
|
+
- !ruby/object:Gem::Version
|
|
222
|
+
version: '3.9'
|
|
181
223
|
description: Write Tacview data to PostGIS database
|
|
182
224
|
email:
|
|
183
225
|
- jeff@jones.be
|
|
@@ -201,10 +243,12 @@ files:
|
|
|
201
243
|
- bin/console
|
|
202
244
|
- bin/setup
|
|
203
245
|
- data/airfields.json
|
|
246
|
+
- data/whitelist.example
|
|
204
247
|
- db/001_create_unit_table.rb
|
|
205
248
|
- db/README.md
|
|
206
249
|
- exe/tac_scribe
|
|
207
250
|
- lib/tac_scribe.rb
|
|
251
|
+
- lib/tac_scribe/cache.rb
|
|
208
252
|
- lib/tac_scribe/daemon.rb
|
|
209
253
|
- lib/tac_scribe/datastore.rb
|
|
210
254
|
- lib/tac_scribe/event_processor.rb
|
|
@@ -233,7 +277,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
233
277
|
version: '0'
|
|
234
278
|
requirements: []
|
|
235
279
|
rubyforge_project:
|
|
236
|
-
rubygems_version: 2.7.
|
|
280
|
+
rubygems_version: 2.7.10
|
|
237
281
|
signing_key:
|
|
238
282
|
specification_version: 4
|
|
239
283
|
summary: Write Tacview data to PostGIS database
|