2
\$\begingroup\$

The story started here (Parsing GPS Data) and continued here (Class for median filter)

I've done a lot work and faced new questions.

In short - I made:

  1. Classes for parsers. TrackParser and inherited from it GPXParser, TESParser, CSVParser.

  2. Method in module ParserSelector.choose which returns object of parser by extension and file data.

  3. Class for track points.

  4. Method in module for median filter.

I want ask you guys about "3. Class for track points", because I think this part of my code still smells bad.

In model Track I have:

before_save :parse_file

def parse_file

    if self.new_record?

      trkseg = Tracksegment.create!
      self.tracksegments << trkseg

      parser = ParserSelector::choose(trackfile[:data], trackfile[:ext])
      return false if parser.nil?

      track_points = parser.parse track_index.to_i
      track_points.process_data!

      return false if track_points.points.count < 10

      track_points.create_points { |point| point.tracksegment = trkseg}

      self.ff_start = track_points.ff_start
      self.ff_end = track_points.ff_end

    end

  end

In parser:

def parse(index = 0)
    track_points = TrackPoints.new
    ...
    track_points.add(point)
    ...
    track_points
  end

And this is the TrackPoints class:

require 'geospatial'
require 'velocity'
require 'median_filter'
include MedianFilter

class TrackPoints

  attr_reader :points, :fl_time

  def initialize(track = nil)

    @points = []
    @fl_time = 0

    read(track) if track.present?

  end

  def add(row)
    @points << row
  end

  def compact!
    @points.compact!
    self
  end

  def process_data!

    filter_by_freq!
    corr_elevation!
    calc_parameters!
    median_filter!

  end

  def create_points
    Point.create (@points) do |point|
      if block_given?
        yield point
      end
    end
  end

  def trim!(from, to)

    if from.present?
      @points = @points.drop_while{ |x| x[:fl_time_abs] < from}
    end
    if to.present? && ff_end > 0 && (from.blank? || to > from)
      @points = @points.reverse.drop_while{ |x| x[:fl_time_abs] > to}.reverse
    end

    self

  end

  def ff_start

    ff_start_val = 0

    start_point = @points.reverse.detect { |x| x[:elevation] >= (@max_h - 15) }
    ff_start_val = start_point[:fl_time] if start_point.present?

    start_point = @points.detect { |x| (x[:fl_time] > ff_start_val && x[:v_speed] > 25) }
    ff_start_val = start_point[:fl_time] if start_point.present?

    ff_start_val

  end

  def ff_end

    end_point = @points.detect { |x| x[:elevation] < (@min_h + 50) }
    end_point.present? ? end_point[:fl_time] : @fl_time

  end

  def empty?
    @points.empty?
  end

  def min_h
    @points.min_by { |x| x[:elevation] }[:elevation]
  end

  def max_h
    @points.max_by { |x| x[:elevation] }[:elevation]
  end

  private

  def read(track)

    points = Point.joins(:tracksegment).where('tracksegments.track_id' => track.id).to_a

    prev_point = nil

    points.each do |point|
      if prev_point != nil
        @fl_time += point.point_created_at - prev_point.point_created_at
        @points << {:fl_time => point.point_created_at - prev_point.point_created_at,
                :fl_time_abs => fl_time,
                :elevation_diff => (prev_point.elevation - point.elevation).round(2),
                :elevation => point.elevation.round(2),
                :abs_altitude => point.abs_altitude,
                :latitude => point.latitude,
                :longitude => point.longitude,
                :distance => point.distance.to_i,
                :h_speed => point.h_speed.round(2),
                :v_speed => point.v_speed.round(2),
                :glrat => (point.h_speed.round(2) / point.v_speed.round(2)).round(2)
        }
      end
      prev_point = point
    end

  end

  # Приведение прочитанных данных к формату 1 Гц
  def filter_by_freq!
    @points.uniq!{ |x| DateTime.strptime(x[:point_created_at], '%Y-%m-%dT%H:%M:%S') }
  end

  # Корректировка высоты от уровня земли
  def corr_elevation!
    min_h = @points.min_by{ |x| x[:elevation] }[:elevation]
    @points.each do |x|
      x[:elevation] -= min_h
    end
  end

  # Медианный фильтр для расстояния, высоты, скоростей
  def median_filter!
    @points = MedianFilter.process( @points,
                                   3,
                                   [:distance, :elevation, :h_speed, :v_speed] )
  end

  def calc_parameters!

    prev_point = nil

    @points.each do |point|
      point[:distance] = 0 if prev_point.nil?
      unless prev_point.nil?
        datetime_1 = DateTime.strptime(point[:point_created_at], '%Y-%m-%dT%H:%M:%S')
        datetime_2 = DateTime.strptime(prev_point[:point_created_at], '%Y-%m-%dT%H:%M:%S')
        fl_time_diff = (datetime_1 - datetime_2) * 1.days
        @fl_time += fl_time_diff

        point[:distance] = Geospatial.distance [prev_point[:latitude], prev_point[:longitude]], [point[:latitude], point[:longitude]]
        point[:h_speed] = Velocity.to_kmh(point[:distance] / fl_time_diff)
        point[:v_speed] = Velocity.to_kmh((prev_point[:elevation] - point[:elevation]) / fl_time_diff)
      end
      point[:fl_time] = @fl_time
      prev_point = point
    end

  end

end

I really don't like:

def process_data!

def read(track)

def calc_parameters!

But don't know how to make it better.

Could you guys give me directions to refactor it?

Update:

I think I got it. I have to implement separate classes - one as track_points_processor (or post processor), which I'll use after I parse file and one class which will be use for preparing data to different views (chart, maps, earth)

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.