# frozen_string_literal: true require 'georuby' require 'sequel' require 'sequel-postgis-georuby' require 'singleton' module TacScribe # Acts as the interface to the back-end datastore and hides all datastore # implementation details from callers. Note that ruby does not support # exception chaining we are not wrapping exceptions yet. This will happen # when https://bugs.ruby-lang.org/issues/8257 is fixed. class Datastore include Singleton include GeoRuby::SimpleFeatures attr_accessor :db @configuration = nil @db = nil def configuration @configuration ||= Configuration.new end def configure configuration yield(@configuration) if block_given? end # Contains all the connection configuration required for connectin # to a postgresql database. Defaults to localhost with ubuntu presets class Configuration attr_accessor :host, :port, :database, :username, :password def initialize @host = 'localhost' @port = '5432' @database = 'tac_scribe' @username = 'tac_scribe' @password = 'tac_scribe' end end def connect configure @db = Sequel.connect(connection_string, max_connections: 49) @db.extension :postgis_georuby end def truncate_table @db[:units].truncate end def write_object(event, timestamp) unit = get_unit(event[:object_id]) # Tacview omits values that don't change to save # data bandwidth and storage. If something has not changed then # use the old value current_position = get_position(event, unit) if unit update_unit(event, unit, current_position, timestamp) else insert_unit(event, current_position, timestamp) end end def delete_object(object_id) count = @db[:units].where(id: object_id).delete "Deleted #{object_id} #{object_id.class} - #{count}" end private def connection_string if RUBY_PLATFORM == 'java' "jdbc:postgresql://#{@configuration.host}:#{@configuration.port}/" \ "#{@configuration.database}?user=#{@configuration.username}&" \ "password=#{configuration.password}" else "postgres://#{@configuration.username}:#{@configuration.password}@" \ "#{@configuration.host}:#{@configuration.port}" \ "/#{@configuration.database}" end end def get_unit(id) @db[:units].where(id: id).first end def update_unit(event, unit, current_position, timestamp) heading = calculate_heading(unit[:position], current_position, unit[:heading]) @db[:units].where(id: event[:object_id]).update( position: current_position[:lat_lon], altitude: current_position[:altitude], heading: heading, updated_at: timestamp ) end def insert_unit(event, current_position, timestamp) @db[:units].insert(id: event[:object_id], position: current_position[:lat_lon], altitude: current_position[:altitude], type: event[:type], name: event[:name], group: event[:group], pilot: event[:pilot], coalition: event[:coalition] == 'Allies' ? 0 : 1, updated_at: timestamp) end def get_position(event, unit) { lat_lon: Point.from_x_y( event.fetch(:longitude) { unit ? unit[:position].y : nil }, event.fetch(:latitude) { unit ? unit[:position].x : nil } ), altitude: event.fetch(:altitude) { unit ? unit[:altitude] : nil } } end def calculate_heading(old_position, current_position, current_heading) return current_heading if old_position == current_position[:lat_lon] begin old_position.bearing_to(current_position[:lat_lon]).to_i rescue Math::DomainError => e puts 'Could not calculate heading: ' + e.message puts 'Old Position: ' + old_position.inspect puts 'New Position: ' + current_position.inspect end end end end