# frozen_string_literal: true require 'georuby' require 'sequel' require 'sequel-postgis-georuby' require 'singleton' require 'concurrent-ruby' require 'haversine' module TacScribe # The in-memory cache that is updated by events in real-time before the data # is synced to the DB class Cache include Singleton include GeoRuby::SimpleFeatures @@cache = Concurrent::Hash.new attr_accessor :reference_latitude, :reference_longitude def data @@cache end def write_object(object) if (reference_latitude != 0 || reference_longitude != 0) localize_position(object) end id = object[:object_id] id ||= object[:id].to_s object[:id] = id cache_object = @@cache[id] object[:position] = set_position(object, cache_object) %i[object_id latitude longitude color country].each do |key| object.delete(key) end # https://wiki.hoggitworld.com/view/DCS_singleton_coalition # Tacview returns a string, CinC returns an integer so we # only do the conversion if there is a string. if object[:coalition] && object[:coalition].is_a?(String) object[:coalition] = case object[:coalition] when 'Enemies' # Enemies is Bluefor 2 when 'Allies' # Allies is Redfor 1 else # Neutral 0 end end if object[:type] == "Ground+Static+Aerodrome" object[:heading] = object[:wind_heading] ? object[:wind_heading] : -1 object[:speed] = object[:wind_speed] ? object[:wind_speed] : 0 object.delete(:wind_heading) object.delete(:wind_speed) object.delete(:category) cache_object = object # No-op elsif cache_object object[:heading] = calculate_heading(cache_object, object) object[:speed] = calculate_speed(cache_object, object) cache_object.merge!(object) else object[:heading] = -1 object[:speed] = 0 cache_object = object end # Hack to make sure the :name field is present so that it is included # in the SQL cache_object[:name] = nil unless cache_object.has_key?(:name) if !cache_object.key?(:altitude) || !cache_object[:altitude] cache_object[:altitude] = 0 end if !cache_object.key?(:coalition) || !cache_object[:coalition] cache_object[:coalition] = 2 end cache_object[:pilot] = nil unless cache_object.key?(:pilot) cache_object[:group] = nil unless cache_object.key?(:group) cache_object[:deleted] = false unless cache_object.key?(:deleted) cache_object[:updated_at] = Time.now @@cache[id] = cache_object end def delete_object(id) @@cache[id][:deleted] = true if @@cache[id] end def clear @@cache.clear self.reference_latitude = nil self.reference_longitude = nil end private def localize_position(object) if reference_latitude && object.key?(:latitude) object[:latitude] = reference_latitude + object[:latitude] end if reference_longitude && object.key?(:longitude) object[:longitude] = reference_longitude + object[:longitude] end end def set_position(object, cache_object) longitude = if object.key?(:longitude) object[:longitude] else cache_object[:position].x end latitude = if object.key?(:latitude) object[:latitude] else cache_object[:position].y end # This "Point" class is not lat/long aware which is why we are # flipping things up here because when it converts to SQL we need # the long to be first since that is what postgresql expects. Point.from_x_y(longitude, latitude) end def calculate_heading(cache_object, object) if cache_object[:position] == object[:position] return cache_object[:heading] end begin cache_object[:position].bearing_to(object[:position]).to_i rescue Math::DomainError => e puts 'Could not calculate heading: ' + e.message puts 'Old Position: ' + cache_object[:position].inspect puts 'New Position: ' + object[:position].inspect end end def calculate_speed(cache_object, object) time = object[:game_time] - cache_object[:game_time] start_point = cache_object[:position] end_point = object[:position] # Because of the above issues with Point and Lat/Lon # the values are reverse here :( distance = Haversine.distance(start_point.y, start_point.x, end_point.y, end_point.x).to_meters speed = distance / time speed.to_i end end end