npolar/api.npolar.no

View on GitHub
lib/tracking.rb

Summary

Maintainability
C
1 day
Test Coverage
require "npolar/api/client/json_api_client"
require "argos"
require "date"

class Tracking < Hashie::Mash

  include Argos::SensorData
  
  # Before lambda for processing a request prior to storage
  # @return [lambda]
  # See Core#handle and Core#before
  def self.before
    lambda {|request|
      if request.put? or request.post?
        Tracking.before_save(request)
      else
        request
      end
    }
  end

  # Process body before saving
  # Body may contain a JSON Array or 1 JSON object
  # @return request
  def self.before_save(request)

    body = request.body.respond_to?(:read) ? request.body.read : request.body.join("")

    tracks = JSON.parse(body)

    
    tracks = tracks.is_a?(Hash) ? [tracks] : tracks

    processed = tracks.map {|track|
      new(track).before_save(request)
    }
    if request.request_method == "PUT"
      processed = processed.first
    end
    body = processed.to_json
    
    request.body = body
    request
  end

  # Add platform/deployment metadata and decode sensor data befor saving
  # @return [Tracking]
  def before_save(request=nil)
    
    self[:collection] = "tracking"
    # @todo self schema ==> current schema
    
    if not warn?
      self[:warn] = []
    end

    # Base URI, used to set source and deployment URIs
    if not base?
      self[:base] = base_uri
    end
    
    # Set source to URI if it's a SHA1 hash
    if source? and source !~ URI::REGEXP and source =~ /\w{40}/
      source_uri = base_uri
      source_uri.path = "/source/#{source}"
      self[:source] = source_uri.to_s
    end

    # Merge in object, species, platform_model, platform_type
    inject_platform_deployment_metadata
    
    # Merge in individual if the platform is known to be attached to one, 
    # ie. only if measured >= deployed (and if terminated is set: measured <= terminated)
    inject_indvidual
    
    # Merge in sensor data
    decode_sensor_data
    
    if self[:warn].none?
      self.delete :warn
    end
    
    self
  end
  alias :empty :before_save
  
  protected
  
  def base_uri
    baseuri = URI.parse(ENV["NPOLAR_API"]||"http://localhost")
  end

  def decode_sensor_data

    # For Argos data data prior to 2014-03-01 (DS/DIAG data) the sensor data may either integer or hex
    # Argos data from 2014-03-01 and onwards (XML from SOAP web service) contain both integer and hex data,
    # as well as platform_model string
    
    if sensor_data?
    
      decoder = nil
      
      if self[:platform_model] =~ /^KiwiSat303/i
        
        decoder = Argos::KiwiSat303Decoder.new
        

      elsif self[:platform_model] =~ /^NorthStar/i
        
        decoder = Argos::NorthStar4BytesDecoder.new
        
      end
      
      # Merge in extracted sensor data
      if not decoder.nil?
        
        if self[:sensor_data].is_a? Array and self[:sensor_data].any?
          
          begin

            # @todo HEX from platform deployment metadata
            
            # Arctic fox legacy DS/DIAG data hack: force hex format for platform series 13xxxx
            if self[:object] == "Arctic fox" and self[:platform].to_s =~ /^13/ and self[:technology] == "argos"
              if self[:type] =~ /^(ds|diag)$/
                decoder.sensor_data_format = "hex"
                self[:sensor_data_format] = "hex"
              end
            end
            
            # If we have sensor_hex => use that (then we at least know the base)
            if self.key?(:sensor_hex) and self[:sensor_hex].size >= 2
              decoder.sensor_data = self[:sensor_hex].scan(/[0-9a-f]{2}/i).map {|h| h.to_i(16) }
            else
              decoder.sensor_data = self[:sensor_data]
            end
            
            self[:decoder] = decoder.class.name
            
            decoder.data.each do |k,v|
              self[k]=v
            end
            
            self[:sensor_variables] = decoder.data.keys
          
          rescue
            self[:warn] << "sensor-decoding-failed"
          end
          
        end
        
      end
    
    end
    
  end

  # Get all deployments from Tracking Deployment API database
  def tracking_deployments
    @@deployment ||= begin
      uri = URI.parse(ENV["NPOLAR_API_COUCHDB"])
      uri.path = "/#{Service.factory("tracking-deployment-api").database}"
      client = Npolar::Api::Client::JsonApiClient.new(uri.to_s)
    
      d = client.get_body("_all_docs", {"include_docs"=>true}).rows.map {|row|
        Hashie::Mash.new(row.doc)
      }
      d
    end
  end
    
  # @return [Array] All platforms 
  # We reject all platforms that (reject messages after terminated time)
  # We dont' require the message measured to be after deployed, because the we want platform metadata also 
  def deployments

    measured = DateTime.parse(self[:measured]||self[:positioned])

    begin
      terminated = DateTime.parse(d.deployed)
    rescue
      terminated = DateTime.new(9999)
    end
    
    # From the deployment database
    tracking_deployments.select {|d|
      # Select deployment with the current platform and technology (before terminated time)
      (d.platform.to_s == platform.to_s) and (d.technology == technology) and (measured <= terminated)
    }  
  end
  
  # Get the matching deployment document
  def deployment_hash
    deployment = deployments
    if deployment.size == 1
      deployment[0]
    elsif deployment.size > 1
      next_deployment_after_measured
    else
      {}
    end
  end
  
  def next_deployment_after_measured
    measured = Time.parse(self[:measured]||self[:positioned])
    
    # Simple case first: measured is after deployed and before terminated    
    if idx = deployments.find_index { |d|
      begin
        measured >= Time.parse(d.deployed) and measured <= Time.parse(d.terminated)
      rescue
        # noop
      end
      }
      deployments[idx]
    else
    
      # But for redeployed platforms the problem is that the measured time may be before 2 or more deployed times,
      # we should use deployment neareast in time after measured time
      times = deployments.map { |d| measured.to_i - Time.parse(d.deployed).to_i }
      
      idx = times.find_index {|t| t == times.min }
      
      deployments[idx]
    end
  end
  
  # Merge in individual for periods after deployed and before terminated
  def inject_indvidual
    deployment = deployment_hash
    
    if deployment.key? :individual
    
      begin
        deployed = DateTime.parse(deployment.deployed)
      rescue
        deployed = DateTime.new(1000)
      end
      
      begin
        terminated = DateTime.parse(deployment.terminated)
      rescue
        terminated = DateTime.new(9999)
      end
      
      measured = DateTime.parse(self[:measured]||self[:positioned])
      
      if measured >= deployed and measured <= terminated
        self[:individual] = deployment_hash.individual
      end
    
    end

  end
  
  # Merge in platform deployment information like object, species, platform model
  def inject_platform_deployment_metadata
    
    deployment = deployment_hash
    
    if not deployment.nil? and deployment.key? :id
      deployment_uri = base_uri
      deployment_uri.path = "/tracking/deployment/#{deployment[:id]}"
      self[:deployment] = deployment_uri.to_s
    end
      
    # We add object like "Arctic fox" - also for messages before/after deployment
    if not object? and deployment.key? :object
      self[:object] = deployment.object
    end
    
    # We add species like "Vulpes lagopus" - also for messages before/after deployment
    if not species? and deployment.key? :species
      self[:species] = deployment.species
    end
    
    # Set platform model
    if not platform_model? and deployment.key? :platform_model
      self[:platform_model] = deployment.platform_model
    end
    
    # Set platform type
    if not platform_type? and deployment.key? :platform_type
      self[:platform_type] = deployment.platform_type
    end
    
    # Set platform_name
    if not platform_name? and deployment.key? :platform_name
      self[:platform_name] = deployment.platform_name
    end
        
    # Add deployed and terminated so that we can later compare these with the Tracking Deployment API
    # and and detect if republishing of data for certain platform is needed.
    # This is useful (1) When tagging the deployed date is not yet Tracking Deployment API, but data flow is real time (ie. needs to be fixed after setting the individual)
    # (2) When deployed / terminated / individual data is corrected
    if not deployed? and deployment.key? :deployed
      self[:deployed] = deployment.deployed
    end
    if not terminated? and deployment.key? :terminated
      self[:terminated] = deployment.terminated
    end
  end

end