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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f53013c85197970f92c1b1d77608bbb6c0f6d7d70b09640a4d566282ecd2ecd0
4
- data.tar.gz: e05a92c6f3da05c2f2925bb0ab18740ec31e3629906c56472e3fa94b5c1891c8
3
+ metadata.gz: c39a4f860637756b22a7ce9fd09d74e2c7190478a94dbc779373ba0d213d0b35
4
+ data.tar.gz: 5abb08ea5e750d3e460ade56d89924866badb2a842b62b20672df2f0c349eb53
5
5
  SHA512:
6
- metadata.gz: 54e42d58f65f1d4e49f1affc4211d8829d4b26a1dfc810c5656c37346e552fad692371c3e0738f7a7629d378d88e481aad570a1a8ab47e329cc2c91aac601f20
7
- data.tar.gz: 71d84cd363227f4ffd2684e00c04ae97e714b74f25339f1cf004d5741c33bf770131d7fb51b2b346f112d575135549ef001c0cb6c71b914cf0a72310659c926b
6
+ metadata.gz: 0d0ba804851aa3143c0f5b563d7401706dd129b826dc5398174ead33b982a22d48d0871d0d5498eb922c4ca08c83598a7c3928e3f1a69a7cd2094a3a5acf8801
7
+ data.tar.gz: 641fe1a451dff87f915c69fdf6ea36a4053f4924d77141a3617d7019dcf27ff9d9f884f65894d20ae75414d3b18c0b2e40642d2248d9a022cc8061455855afd3
@@ -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/start_scribe
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
@@ -1 +1 @@
1
- jruby
1
+ ruby-2.7.0
@@ -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
@@ -0,0 +1,5 @@
1
+ Sea+Watercraft+AircraftCarrier
2
+ Air+Rotorcraft
3
+ Air+FixedWing
4
+ Ground+Static+Aerodrome
5
+ Navaid+Static+Bullseye
@@ -12,7 +12,9 @@ Sequel.migration do
12
12
  String :group, null: true
13
13
  Integer :coalition
14
14
  Integer :heading
15
+ Integer :speed
15
16
  Time :updated_at
17
+ Boolean :deleted
16
18
  # TODO: GIST Index on the position
17
19
  end
18
20
  end
@@ -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-username=username',
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
@@ -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 verbose_logging: verbose_logging
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
- @processing_threads = []
35
+ @threads = {}
32
36
  @whitelist = Set.new(IO.read(whitelist).split) if whitelist
33
37
 
34
- @client = TacviewClient::Client.new(
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
- kill_processing_threads
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
- @client.connect
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 kill_processing_threads
69
- @processing_threads.each do |thr|
70
- thr.kill
71
- thr.join
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
- @thread_count.times do
77
- @processing_threads << Thread.new do
78
- EventProcessor.new(datastore: Datastore.instance,
79
- event_queue: @event_queue,
80
- whitelist: @whitelist).start
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__), '../../data/airfields.json'))
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: 2
190
+ coalition: 0 # Neutral
97
191
  )
98
192
  end
99
193
  end
@@ -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 write_object(event, timestamp)
56
- unit = get_unit(event[:object_id])
57
-
58
- # Tacview omits values that don't change to save
59
- # data bandwidth and storage. If something has not changed then
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
- def delete_object(object_id)
71
- count = @db[:units].where(id: object_id).delete
72
- "Deleted #{object_id} #{object_id.class} - #{count}"
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 'datastore'
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
- def initialize(datastore:, event_queue:, whitelist: nil)
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.get_event
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 @@ignored_units.include?(event[:object_id])
69
+ return if ignore_unit?(event)
60
70
 
61
- if @whitelist && event[:type] && !@whitelist.include?(event[:type])
62
- @@ignored_units << event[:object_id]
63
- return
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 @event_queue.reference_latitude || @event_queue.reference_longitude
67
- localize_position(event)
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
- @datastore.write_object(event, time)
92
+ false
71
93
  end
72
94
 
73
95
  # Process a delete event for an object
74
96
  #
75
- # @param object_id [String] A hexadecimal number representing the object
76
- # ID
77
- def delete_object(object_id)
78
- return if @@ignored_units.delete?(object_id)
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
- @datastore.delete_object(object_id)
107
+ def update_latitude(value)
108
+ Cache.instance.reference_latitude = value
81
109
  end
82
110
 
83
- # If we have reference lat/long then use that as a base and update the event
84
- # position
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 :reference_latitude, :reference_longitude
9
+ attr_accessor :events_written
10
10
 
11
- def initialize(verbose_logging:)
12
- @verbose_logging = verbose_logging
11
+ def initialize
13
12
  @events = Queue.new
14
- @event_write = 0
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 get_event
34
- @event_read += 1
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
- @event_write += 1
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
- @event_write += 1
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
- self.reference_latitude = BigDecimal(value)
74
+ @events << { type: :set_latitude, value: BigDecimal(value) }
85
75
  when 'ReferenceLongitude'
86
- self.reference_longitude = BigDecimal(value)
76
+ @events << { type: :set_longitude, value: BigDecimal(value) }
87
77
  when 'ReferenceTime'
88
78
  @reference_time = @time = Time.parse(value)
89
79
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TacScribe
4
- VERSION = '0.3.0'
4
+ VERSION = '0.7.3'
5
5
  end
@@ -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.0
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: 2019-10-26 00:00:00.000000000 Z
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.6
280
+ rubygems_version: 2.7.10
237
281
  signing_key:
238
282
  specification_version: 4
239
283
  summary: Write Tacview data to PostGIS database