tac_scribe 0.3.0-java → 0.7.3-java
Sign up to get free protection for your applications and to get access to all the features.
- 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
|