tac_scribe 0.2.0-java → 0.7.0-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 +38 -1
- data/README.md +24 -1
- data/data/whitelist.example +5 -0
- data/db/001_create_unit_table.rb +2 -0
- data/exe/{start_scribe → tac_scribe} +9 -3
- data/lib/tac_scribe/cache.rb +151 -0
- data/lib/tac_scribe/daemon.rb +88 -24
- data/lib/tac_scribe/datastore.rb +22 -68
- data/lib/tac_scribe/event_processor.rb +50 -24
- data/lib/tac_scribe/event_queue.rb +19 -18
- data/lib/tac_scribe/version.rb +1 -1
- data/tac_scribe.gemspec +6 -2
- metadata +35 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 74ae9992c2a1db103b6b19fbfcfb298c505e8654138d0db9d673cc8fbf7651a2
|
4
|
+
data.tar.gz: 104b3949d73dead8bdcbef52d85b4e56ad9c04a54a0fdb7338847018ccb95082
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3df16168564a673864ce8eca754c76ace35221ef411473ea9056ff7f9af8420927b20d1eec751f3b3c6021264e14db1ad84131f1632cc6a351805b592a89de6b
|
7
|
+
data.tar.gz: 8595fe735b69f74f012f1c64ec4888ce129f63ac142e3e17b073e0119238a0b414e32d13f8764f912ff26bd616c5a33facf50af3f7cabc1ad7eb20c910821dad
|
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
@@ -4,7 +4,44 @@ All notable changes to this project will be documented in this file.
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
6
|
|
7
|
-
## [
|
7
|
+
## [0.6.2]
|
8
|
+
### Changed
|
9
|
+
- Fixed typo causing app to fail
|
10
|
+
|
11
|
+
## [0.6.1]
|
12
|
+
### Changed
|
13
|
+
- Test build for gem issue
|
14
|
+
|
15
|
+
## [0.6.0]
|
16
|
+
### Added
|
17
|
+
- Calculates and stores speed of units
|
18
|
+
|
19
|
+
### Fixed
|
20
|
+
- Made more robust against failures and more logging messages
|
21
|
+
|
22
|
+
## [0.5.0]
|
23
|
+
### Changed
|
24
|
+
- Added missing db column to migration file
|
25
|
+
- Clear the cache on server restart
|
26
|
+
- Add more info to logging
|
27
|
+
|
28
|
+
## [0.4.0]
|
29
|
+
### Changed
|
30
|
+
- Switch to periodic writing to the database
|
31
|
+
|
32
|
+
## [0.3.0] - 2019-10-27
|
33
|
+
### Changed
|
34
|
+
- Changed the command-line name to match gem
|
35
|
+
|
36
|
+
### Fixed
|
37
|
+
- Sleep between connection attempts
|
38
|
+
|
39
|
+
### Added
|
40
|
+
- Unit type filtering
|
41
|
+
|
42
|
+
## [0.2.1] - 2019-10-13
|
43
|
+
### Changed
|
44
|
+
- Added separate events in / events out for the queue logging
|
8
45
|
|
9
46
|
## [0.2.0] - 2019-10-13
|
10
47
|
### Added
|
data/README.md
CHANGED
@@ -21,9 +21,32 @@ Or install it yourself as:
|
|
21
21
|
|
22
22
|
## Usage
|
23
23
|
|
24
|
-
You can run the tool using the `
|
24
|
+
You can run the tool using the `tac_scribe` command. Use the `--help`
|
25
25
|
option for information on required arguments
|
26
26
|
|
27
|
+
### Whitelist
|
28
|
+
|
29
|
+
If you want to restrict what units get written to the database (e.g. for
|
30
|
+
performance reasons) then create a whitelist file and pass that in with
|
31
|
+
the whitelist option. For example if you only care about air units
|
32
|
+
and landing areas create a file like the following:
|
33
|
+
|
34
|
+
`whitelist.txt`
|
35
|
+
|
36
|
+
With the following content
|
37
|
+
|
38
|
+
```
|
39
|
+
Sea+Watercraft+AircraftCarrier
|
40
|
+
Air+Rotorcraft
|
41
|
+
Air+FixedWing
|
42
|
+
Ground+Static+Aerodrome
|
43
|
+
Navaid+Static+Bullseye
|
44
|
+
```
|
45
|
+
|
46
|
+
and add `-w whitelist.txt` to the command-line.
|
47
|
+
|
48
|
+
The unit type is the type provided by Tacview and it must be an exact match
|
49
|
+
|
27
50
|
## Development
|
28
51
|
|
29
52
|
After checking out the repo, run `bin/setup` to install dependencies. Then,
|
data/db/001_create_unit_table.rb
CHANGED
@@ -15,7 +15,8 @@ options = {
|
|
15
15
|
db_name: 'tac_scribe',
|
16
16
|
verbose_logging: false,
|
17
17
|
threads: 1,
|
18
|
-
populate_airfields: false
|
18
|
+
populate_airfields: false,
|
19
|
+
whitelist: nil
|
19
20
|
}
|
20
21
|
|
21
22
|
OptionParser.new do |opts|
|
@@ -51,7 +52,7 @@ OptionParser.new do |opts|
|
|
51
52
|
'Postgresql server port (Default: 5432)') do |v|
|
52
53
|
options[:db_port] = v
|
53
54
|
end
|
54
|
-
opts.on('-u', '--db-
|
55
|
+
opts.on('-u', '--db-user=username',
|
55
56
|
'Postgresql username (Required)') do |v|
|
56
57
|
options[:db_user] = v
|
57
58
|
end
|
@@ -76,6 +77,10 @@ OptionParser.new do |opts|
|
|
76
77
|
'Verbose logging') do |_v|
|
77
78
|
options[:verbose] = true
|
78
79
|
end
|
80
|
+
opts.on('-w', '--whitelist=whitelist',
|
81
|
+
'Whitelist filename') do |v|
|
82
|
+
options[:whitelist] = v
|
83
|
+
end
|
79
84
|
end.parse!
|
80
85
|
|
81
86
|
%i[db_user db_password].each do |parameter|
|
@@ -99,5 +104,6 @@ TacScribe::Daemon.new(
|
|
99
104
|
db_password: options[:db_password],
|
100
105
|
verbose_logging: options[:verbose],
|
101
106
|
thread_count: options[:threads].to_i,
|
102
|
-
populate_airfields: options[:populate_airfields]
|
107
|
+
populate_airfields: options[:populate_airfields],
|
108
|
+
whitelist: options[:whitelist]
|
103
109
|
).start_processing
|
@@ -0,0 +1,151 @@
|
|
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
|
27
|
+
localize_position(object)
|
28
|
+
end
|
29
|
+
|
30
|
+
id = object[:object_id]
|
31
|
+
object[:id] = id
|
32
|
+
|
33
|
+
cache_object = @@cache[id]
|
34
|
+
|
35
|
+
object[:position] = set_position(object, cache_object)
|
36
|
+
|
37
|
+
%i[object_id latitude longitude color country].each do |key|
|
38
|
+
object.delete(key)
|
39
|
+
end
|
40
|
+
|
41
|
+
# https://wiki.hoggitworld.com/view/DCS_singleton_coalition
|
42
|
+
if object[:coalition]
|
43
|
+
object[:coalition] = case object[:coalition]
|
44
|
+
when 'Enemies' # Enemies is Bluefor
|
45
|
+
2
|
46
|
+
when 'Allies' # Allies is Redfor
|
47
|
+
1
|
48
|
+
else # Neutral
|
49
|
+
0
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
if cache_object
|
54
|
+
object[:heading] = calculate_heading(cache_object, object)
|
55
|
+
object[:speed] = calculate_speed(cache_object, object)
|
56
|
+
cache_object.merge!(object)
|
57
|
+
else
|
58
|
+
object[:heading] = -1
|
59
|
+
object[:speed] = 0
|
60
|
+
cache_object = object
|
61
|
+
end
|
62
|
+
|
63
|
+
if !cache_object.key?(:altitude) || !cache_object[:altitude]
|
64
|
+
cache_object[:altitude] = 0
|
65
|
+
end
|
66
|
+
if !cache_object.key?(:coalition) || !cache_object[:coalition]
|
67
|
+
cache_object[:coalition] = 2
|
68
|
+
end
|
69
|
+
|
70
|
+
cache_object[:pilot] = nil unless cache_object.key?(:pilot)
|
71
|
+
|
72
|
+
cache_object[:group] = nil unless cache_object.key?(:group)
|
73
|
+
|
74
|
+
cache_object[:deleted] = false unless cache_object.key?(:deleted)
|
75
|
+
|
76
|
+
cache_object[:updated_at] = Time.now
|
77
|
+
|
78
|
+
@@cache[id] = cache_object
|
79
|
+
end
|
80
|
+
|
81
|
+
def delete_object(id)
|
82
|
+
@@cache[id][:deleted] = true if @@cache[id]
|
83
|
+
end
|
84
|
+
|
85
|
+
def clear
|
86
|
+
@@cache.clear
|
87
|
+
self.reference_latitude = nil
|
88
|
+
self.reference_longitude = nil
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def localize_position(object)
|
94
|
+
if reference_latitude && object.key?(:latitude)
|
95
|
+
object[:latitude] = reference_latitude + object[:latitude]
|
96
|
+
end
|
97
|
+
if reference_longitude && object.key?(:longitude)
|
98
|
+
object[:longitude] = reference_longitude + object[:longitude]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def set_position(object, cache_object)
|
103
|
+
longitude = if object.key?(:longitude)
|
104
|
+
object[:longitude]
|
105
|
+
else
|
106
|
+
cache_object[:position].x
|
107
|
+
end
|
108
|
+
|
109
|
+
latitude = if object.key?(:latitude)
|
110
|
+
object[:latitude]
|
111
|
+
else
|
112
|
+
cache_object[:position].y
|
113
|
+
end
|
114
|
+
|
115
|
+
# This "Point" class is not lat/long aware which is why we are
|
116
|
+
# flipping things up here because when it converts to SQL we need
|
117
|
+
# the long to be first since that is what postgresql expects.
|
118
|
+
Point.from_x_y(longitude, latitude)
|
119
|
+
end
|
120
|
+
|
121
|
+
def calculate_heading(cache_object, object)
|
122
|
+
if cache_object[:position] == object[:position]
|
123
|
+
return cache_object[:heading]
|
124
|
+
end
|
125
|
+
|
126
|
+
begin
|
127
|
+
cache_object[:position].bearing_to(object[:position]).to_i
|
128
|
+
rescue Math::DomainError => e
|
129
|
+
puts 'Could not calculate heading: ' + e.message
|
130
|
+
puts 'Old Position: ' + cache_object[:position].inspect
|
131
|
+
puts 'New Position: ' + object[:position].inspect
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def calculate_speed(cache_object, object)
|
136
|
+
time = object[:game_time] - cache_object[:game_time]
|
137
|
+
|
138
|
+
start_point = cache_object[:position]
|
139
|
+
end_point = object[:position]
|
140
|
+
|
141
|
+
# Because of the above issues with Point and Lat/Lon
|
142
|
+
# the values are reverse here :(
|
143
|
+
distance = Haversine.distance(start_point.y, start_point.x,
|
144
|
+
end_point.y, end_point.x).to_meters
|
145
|
+
|
146
|
+
speed = distance / time
|
147
|
+
|
148
|
+
speed.to_i
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
data/lib/tac_scribe/daemon.rb
CHANGED
@@ -2,7 +2,9 @@
|
|
2
2
|
|
3
3
|
require 'json'
|
4
4
|
require 'tacview_client'
|
5
|
+
require 'set'
|
5
6
|
require_relative 'datastore'
|
7
|
+
require_relative 'cache'
|
6
8
|
require_relative 'event_queue'
|
7
9
|
require_relative 'event_processor'
|
8
10
|
|
@@ -13,7 +15,7 @@ module TacScribe
|
|
13
15
|
def initialize(db_host:, db_port:, db_name:, db_user:, db_password:,
|
14
16
|
tacview_host:, tacview_port:, tacview_password:,
|
15
17
|
tacview_client_name:, verbose_logging:, thread_count:,
|
16
|
-
populate_airfields:)
|
18
|
+
populate_airfields:, whitelist: nil)
|
17
19
|
Datastore.instance.configure do |config|
|
18
20
|
config.host = db_host
|
19
21
|
config.port = db_port
|
@@ -23,11 +25,14 @@ module TacScribe
|
|
23
25
|
end
|
24
26
|
Datastore.instance.connect
|
25
27
|
|
26
|
-
@event_queue = EventQueue.new
|
28
|
+
@event_queue = EventQueue.new
|
29
|
+
|
30
|
+
@verbose_logging = verbose_logging
|
27
31
|
|
28
32
|
@populate_airfields = populate_airfields
|
29
33
|
@thread_count = thread_count
|
30
|
-
@
|
34
|
+
@threads = {}
|
35
|
+
@whitelist = Set.new(IO.read(whitelist).split) if whitelist
|
31
36
|
|
32
37
|
@client = TacviewClient::Client.new(
|
33
38
|
host: tacview_host,
|
@@ -45,53 +50,112 @@ module TacScribe
|
|
45
50
|
# for example
|
46
51
|
def start_processing
|
47
52
|
loop do
|
48
|
-
|
49
|
-
@event_queue.
|
53
|
+
puts 'Starting processing loop'
|
54
|
+
@event_queue.clear
|
50
55
|
Datastore.instance.truncate_table
|
56
|
+
Cache.instance.clear
|
51
57
|
start_processing_threads
|
58
|
+
start_db_sync_thread
|
59
|
+
start_reporting_thread
|
52
60
|
populate_airfields if @populate_airfields
|
61
|
+
@threads.each_pair do |key, _value|
|
62
|
+
puts "#{key} thread started"
|
63
|
+
end
|
53
64
|
@client.connect
|
54
|
-
|
65
|
+
# If this code is executed it means we have been disconnected without
|
66
|
+
# exceptions. We will still sleep to stop a retry storm. Just not as
|
67
|
+
# long.
|
68
|
+
puts "Disconnected from Tacview"
|
69
|
+
kill_threads
|
70
|
+
sleep 10
|
55
71
|
# Rescuing reliably from Net::HTTP is a complete bear so rescue
|
56
72
|
# StandardError. It ain't pretty but it is reliable. We will puts
|
57
73
|
# the exception just in case
|
58
74
|
# https://stackoverflow.com/questions/5370697/what-s-the-best-way-to-handle-exceptions-from-nethttp
|
59
75
|
rescue StandardError => e
|
76
|
+
puts 'Exception in processing loop'
|
60
77
|
puts e.message
|
61
78
|
puts e.backtrace
|
79
|
+
kill_threads
|
80
|
+
sleep 30
|
62
81
|
next
|
63
82
|
end
|
64
83
|
end
|
65
84
|
|
66
|
-
def
|
67
|
-
|
68
|
-
|
69
|
-
|
85
|
+
def kill_threads
|
86
|
+
puts 'Killing Threads'
|
87
|
+
@threads.each_pair do |key, thread|
|
88
|
+
puts "Killing #{key} thread"
|
89
|
+
thread.kill
|
90
|
+
thread.join
|
70
91
|
end
|
71
92
|
end
|
72
93
|
|
73
94
|
def start_processing_threads
|
74
|
-
@
|
75
|
-
|
76
|
-
|
77
|
-
|
95
|
+
@event_processor = EventProcessor.new(cache: Cache.instance,
|
96
|
+
datastore: Datastore.instance,
|
97
|
+
event_queue: @event_queue,
|
98
|
+
whitelist: @whitelist)
|
99
|
+
event_processor_thread = Thread.new do
|
100
|
+
@event_processor.start
|
101
|
+
end
|
102
|
+
event_processor_thread.name = 'Event Processor'
|
103
|
+
@threads[:processing] = event_processor_thread
|
104
|
+
end
|
105
|
+
|
106
|
+
def start_db_sync_thread
|
107
|
+
db_write_thread = Thread.new do
|
108
|
+
loop do
|
109
|
+
sleep 1
|
110
|
+
deleted = Datastore.instance.write_objects(Cache.instance.data.values)
|
111
|
+
deleted.each { |id| Cache.instance.data.delete(id) }
|
112
|
+
rescue StandardError
|
113
|
+
next
|
114
|
+
end
|
115
|
+
end
|
116
|
+
db_write_thread.name = 'Database Writing'
|
117
|
+
@threads[:database] = db_write_thread
|
118
|
+
end
|
119
|
+
|
120
|
+
def start_reporting_thread
|
121
|
+
return unless @verbose_logging
|
122
|
+
|
123
|
+
reporting_thread = Thread.new do
|
124
|
+
sleep 5
|
125
|
+
loop do
|
126
|
+
puts "#{Time.now.strftime('%FT%T')}\t" \
|
127
|
+
"Events Incoming: #{@event_queue.events_written}\t" \
|
128
|
+
"Processed: #{@event_processor.events_processed}\t" \
|
129
|
+
"Ignored: #{@event_processor.events_ignored}\t" \
|
130
|
+
"Queue Size: #{@event_queue.size}\t" \
|
131
|
+
"Objects Written: #{Datastore.instance.written}\t" \
|
132
|
+
"Deleted: #{Datastore.instance.deleted}"
|
133
|
+
@event_queue.events_written = 0
|
134
|
+
@event_processor.events_processed = 0
|
135
|
+
@event_processor.events_ignored = 0
|
136
|
+
Datastore.instance.written = 0
|
137
|
+
Datastore.instance.deleted = 0
|
138
|
+
sleep 1
|
78
139
|
end
|
79
140
|
end
|
141
|
+
reporting_thread.name = 'Reporting'
|
142
|
+
@threads[:reporting] = reporting_thread
|
80
143
|
end
|
81
144
|
|
82
145
|
def populate_airfields
|
83
|
-
json = File.read(File.join(File.dirname(__FILE__),
|
146
|
+
json = File.read(File.join(File.dirname(__FILE__),
|
147
|
+
'../../data/airfields.json'))
|
84
148
|
airfields = JSON.parse(json)
|
85
149
|
airfields.each_with_index do |airfield, i|
|
86
|
-
@event_queue.update_object(
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
150
|
+
@event_queue.update_object(
|
151
|
+
object_id: (45_000_000 + i).to_s,
|
152
|
+
latitude: BigDecimal(airfield['lat'].to_s),
|
153
|
+
longitude: BigDecimal(airfield['lon'].to_s),
|
154
|
+
altitude: BigDecimal(airfield['alt'].to_s),
|
155
|
+
type: 'Ground+Static+Aerodrome',
|
156
|
+
name: airfield['name'],
|
157
|
+
coalition: 0 # Neutral
|
158
|
+
)
|
95
159
|
end
|
96
160
|
end
|
97
161
|
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
|
@@ -2,23 +2,33 @@
|
|
2
2
|
|
3
3
|
require 'tacview_client/base_processor'
|
4
4
|
require 'time'
|
5
|
-
|
5
|
+
require 'concurrent-ruby'
|
6
|
+
require_relative 'cache'
|
6
7
|
|
7
8
|
module TacScribe
|
8
9
|
# Processes the events emitted by the Ruby Tacview Client
|
9
10
|
class EventProcessor
|
10
|
-
|
11
|
+
@@ignored_units = Concurrent::Set.new
|
12
|
+
|
13
|
+
attr_accessor :events_processed, :events_ignored
|
14
|
+
|
15
|
+
def initialize(cache:, datastore:, event_queue:, whitelist: nil)
|
16
|
+
@cache = cache
|
11
17
|
@datastore = datastore
|
12
18
|
@event_queue = event_queue
|
19
|
+
@whitelist = whitelist
|
20
|
+
self.events_processed = 0
|
21
|
+
self.events_ignored = 0
|
13
22
|
end
|
14
23
|
|
15
24
|
def start
|
16
25
|
loop do
|
17
|
-
wrapped_event = @event_queue.
|
26
|
+
wrapped_event = @event_queue.shift
|
18
27
|
process_event(wrapped_event)
|
19
28
|
rescue StandardError => e
|
20
29
|
puts wrapped_event
|
21
30
|
puts e.inspect
|
31
|
+
puts e.backtrace
|
22
32
|
next
|
23
33
|
end
|
24
34
|
end
|
@@ -29,6 +39,10 @@ module TacScribe
|
|
29
39
|
update_object(wrapped_event[:event], wrapped_event[:time])
|
30
40
|
when :delete_object
|
31
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]
|
32
46
|
end
|
33
47
|
end
|
34
48
|
|
@@ -52,35 +66,47 @@ module TacScribe
|
|
52
66
|
# @option event [BigDecimal] :altitude The object altitude above sea level
|
53
67
|
# in meters to 1 decimal place.
|
54
68
|
def update_object(event, time)
|
55
|
-
if
|
56
|
-
|
69
|
+
return if ignore_unit?(event)
|
70
|
+
|
71
|
+
event[:game_time] = time
|
72
|
+
|
73
|
+
self.events_processed += 1
|
74
|
+
@cache.write_object(event)
|
75
|
+
end
|
76
|
+
|
77
|
+
def ignore_unit?(event)
|
78
|
+
if @@ignored_units.include?(event[:object_id])
|
79
|
+
self.events_ignored += 1
|
80
|
+
return true
|
57
81
|
end
|
58
82
|
|
59
|
-
@
|
83
|
+
if @whitelist && event[:type] && !@whitelist.include?(event[:type])
|
84
|
+
@@ignored_units << event[:object_id]
|
85
|
+
self.events_ignored += 1
|
86
|
+
return true
|
87
|
+
end
|
88
|
+
|
89
|
+
false
|
60
90
|
end
|
61
91
|
|
62
92
|
# Process a delete event for an object
|
63
93
|
#
|
64
|
-
# @param
|
65
|
-
|
66
|
-
|
67
|
-
|
94
|
+
# @param id [String] A hexadecimal number representing the object ID
|
95
|
+
def delete_object(id)
|
96
|
+
if @@ignored_units.delete?(id)
|
97
|
+
nil
|
98
|
+
else
|
99
|
+
@cache.delete_object(id)
|
100
|
+
end
|
101
|
+
self.events_processed += 1
|
68
102
|
end
|
69
103
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
# rubocop:disable Style/GuardClause
|
77
|
-
if event[:latitude]
|
78
|
-
event[:latitude] = @event_queue.reference_latitude + event[:latitude]
|
79
|
-
end
|
80
|
-
if event[:longitude]
|
81
|
-
event[:longitude] = @event_queue.reference_longitude + event[:longitude]
|
82
|
-
end
|
83
|
-
# rubocop:enable Style/GuardClause
|
104
|
+
def update_latitude(value)
|
105
|
+
Cache.instance.reference_latitude = value
|
106
|
+
end
|
107
|
+
|
108
|
+
def update_longitude(value)
|
109
|
+
Cache.instance.reference_longitude = value
|
84
110
|
end
|
85
111
|
end
|
86
112
|
end
|
@@ -6,22 +6,23 @@ 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
|
-
|
13
|
+
self.events_written = 0
|
14
|
+
end
|
15
15
|
|
16
|
-
|
16
|
+
def clear
|
17
|
+
@events.clear
|
18
|
+
end
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
end
|
20
|
+
def size
|
21
|
+
@events.size
|
22
|
+
end
|
23
|
+
|
24
|
+
def shift
|
25
|
+
@events.shift
|
25
26
|
end
|
26
27
|
|
27
28
|
# Process an update event for an object
|
@@ -42,8 +43,8 @@ module TacScribe
|
|
42
43
|
# @option event [BigDecimal] :altitude The object altitude above sea level
|
43
44
|
# in meters to 1 decimal place.
|
44
45
|
def update_object(event)
|
45
|
-
|
46
|
-
events << { type: :update_object, event: event, time: @time }
|
46
|
+
self.events_written += 1
|
47
|
+
@events << { type: :update_object, event: event, time: @time }
|
47
48
|
end
|
48
49
|
|
49
50
|
# Process a delete event for an object
|
@@ -51,8 +52,8 @@ module TacScribe
|
|
51
52
|
# @param object_id [String] A hexadecimal number representing the object
|
52
53
|
# ID
|
53
54
|
def delete_object(object_id)
|
54
|
-
|
55
|
-
events << { type: :delete_object, object_id: object_id }
|
55
|
+
self.events_written += 1
|
56
|
+
@events << { type: :delete_object, object_id: object_id }
|
56
57
|
end
|
57
58
|
|
58
59
|
# Process a time update event
|
@@ -70,9 +71,9 @@ module TacScribe
|
|
70
71
|
def set_property(property:, value:)
|
71
72
|
case property
|
72
73
|
when 'ReferenceLatitude'
|
73
|
-
|
74
|
+
@events << { type: :set_latitude, value: BigDecimal(value) }
|
74
75
|
when 'ReferenceLongitude'
|
75
|
-
|
76
|
+
@events << { type: :set_longitude, value: BigDecimal(value) }
|
76
77
|
when 'ReferenceTime'
|
77
78
|
@reference_time = @time = Time.parse(value)
|
78
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'
|
@@ -37,6 +39,8 @@ Gem::Specification.new do |spec|
|
|
37
39
|
# The following gem is newish and not heavily updated so the chances
|
38
40
|
# of an API breaking change are higher then normal. Therefore lock the
|
39
41
|
# version
|
42
|
+
spec.add_dependency 'concurrent-ruby', '~>1.1.5'
|
43
|
+
spec.add_dependency 'haversine', '~>0.3'
|
40
44
|
spec.add_dependency 'sequel-postgis-georuby', '0.1.2'
|
41
45
|
spec.add_dependency 'tacview_client', '~>0.1'
|
42
46
|
|
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.
|
4
|
+
version: 0.7.0
|
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-07-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +52,34 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '5.22'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 1.1.5
|
61
|
+
name: concurrent-ruby
|
62
|
+
prerelease: false
|
63
|
+
type: :runtime
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
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'
|
55
83
|
- !ruby/object:Gem::Dependency
|
56
84
|
requirement: !ruby/object:Gem::Requirement
|
57
85
|
requirements:
|
@@ -168,7 +196,7 @@ description: Write Tacview data to PostGIS database
|
|
168
196
|
email:
|
169
197
|
- jeff@jones.be
|
170
198
|
executables:
|
171
|
-
-
|
199
|
+
- tac_scribe
|
172
200
|
extensions: []
|
173
201
|
extra_rdoc_files: []
|
174
202
|
files:
|
@@ -187,10 +215,12 @@ files:
|
|
187
215
|
- bin/console
|
188
216
|
- bin/setup
|
189
217
|
- data/airfields.json
|
218
|
+
- data/whitelist.example
|
190
219
|
- db/001_create_unit_table.rb
|
191
220
|
- db/README.md
|
192
|
-
- exe/
|
221
|
+
- exe/tac_scribe
|
193
222
|
- lib/tac_scribe.rb
|
223
|
+
- lib/tac_scribe/cache.rb
|
194
224
|
- lib/tac_scribe/daemon.rb
|
195
225
|
- lib/tac_scribe/datastore.rb
|
196
226
|
- lib/tac_scribe/event_processor.rb
|
@@ -219,7 +249,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
219
249
|
version: '0'
|
220
250
|
requirements: []
|
221
251
|
rubyforge_project:
|
222
|
-
rubygems_version: 2.7.
|
252
|
+
rubygems_version: 2.7.10
|
223
253
|
signing_key:
|
224
254
|
specification_version: 4
|
225
255
|
summary: Write Tacview data to PostGIS database
|