class WhereWasI::Gpx

Use a GPX file as a data source for location inferences.

Constants

Infinity

Attributes

intersegment_behavior[R]

Public Class Methods

new(gpx_file:nil, gpx_data:nil, intersegment_behavior:nil) click to toggle source

@param [String] gpx_file Path to a GPX file. @param [String] gpx_data GPX XML data string. @param [nil,Symbol] intersegment_behavior How to handle times that fall between track segments.

nil: return nil
:interpolate: Interpolate a location from the ends of the nearest segments

@example with a gpx file

g = WhereWasI::Gpx.new(gpx_file: '/path/to/data.gpx')

@example with gpx data

g = WhereWasI::Gpx.new(gpx_data: '<?xml version="1.0"><gpx ...')
# File lib/where_was_i/gpx.rb, line 21
def initialize(gpx_file:nil, gpx_data:nil, intersegment_behavior:nil)
  if gpx_file
    @gpx_data = open(gpx_file)
  elsif gpx_data
    @gpx_data = gpx_data
  else
    raise ArgumentError, "Must supply gpx_file or gpx_data."
  end

  valid_values = [nil, :interpolate, :nearest]
  if !valid_values.include?(intersegment_behavior)
    raise ArgumentError, "intersegment_behavior must be one of: #{valid_values.inspect}"
  end
  @intersegment_behavior = intersegment_behavior

  @tracks_added = false
end

Public Instance Methods

add_tracks() click to toggle source

extract track data from gpx data

it's not necessary to call this directly

# File lib/where_was_i/gpx.rb, line 47
def add_tracks
  @tracks = []
  doc = Nokogiri::XML(@gpx_data)

  doc.css('xmlns|trk').each do |trk|
    track = Track.new
    trk.css('xmlns|trkpt').each do |trkpt|
      # https://en.wikipedia.org/wiki/GPS_Exchange_Format#Units
      # decimal degrees, wgs84.
      # elevation in meters.
      track.add_point(
        lat: trkpt.attributes['lat'].text.to_f,
        lon: trkpt.attributes['lon'].text.to_f,
        elevation: trkpt.at_css('xmlns|ele').text.to_f,
        time: Time.parse(trkpt.at_css('xmlns|time').text)
      )
    end
    @tracks << track
  end

  @intersegments = []
  @tracks.each_with_index do |track,i|
    next if i == 0

    this_track = track
    prev_track = @tracks[i-1]

    inter_track = Track.new
    inter_track.add_point(
      lat: prev_track.end_location[0],
      lon: prev_track.end_location[1],
      elevation: prev_track.end_location[2],
      time: prev_track.end_time
    )
    inter_track.add_point(
      lat: this_track.start_location[0],
      lon: this_track.start_location[1],
      elevation: this_track.start_location[2],
      time: this_track.start_time
    )
    @intersegments << inter_track
  end

  @tracks_added = true
end
at(time) click to toggle source

infer a location from track data and a time

@param [Time,String,Fixnum] time @return [Hash] @see Track#at

# File lib/where_was_i/gpx.rb, line 98
def at(time)
  add_tracks if ! @tracks_added

  if time.is_a?(String)
    time = Time.parse(time)
  end
  time = time.to_i

  location = nil

  @tracks.each do |track|
    location = track.at(time)
    break if location
  end

  if ! location
    case @intersegment_behavior
    when :interpolate then
      @intersegments.each do |track|
        location = track.at(time)
        break if location
      end
    when :nearest then
      # hash is sorted in ascending time order.
      # all start/end points for all segments
      points = {}
      @tracks.each do |t|
        points[t.start_time.to_i] = t.start_location
        points[t.end_time.to_i] = t.end_location
      end

      last_diff = Infinity
      last_time = -1
      points.each do |p_time,p_location|
        this_diff = (p_time.to_i - time).abs

        # as long as the differences keep getting smaller, we keep going
        # as soon as we see a larger one, we step back one and use that value.
        if this_diff > last_diff
          l = points[last_time]
          location = Track.array_to_hash(points[last_time])
          break
        else
          last_diff = this_diff
          last_time = p_time
        end
      end

      # if we got here, time is > the end of the last segment
      location = Track.array_to_hash(points[last_time])
    end
  end

  # each segment has a begin and end time.
  # which one is this time closest to?
  # array of times in order. compute abs diff between time and each point.
  # put times in order. abs diff to each, until we get a larger value or we run out of points. then back up one and use that.
  # {time => [lat, lon, elev], time => [lat, lon, elev]}

  location
end
tracks() click to toggle source
# File lib/where_was_i/gpx.rb, line 39
def tracks
  add_tracks if ! @tracks_added
  @tracks
end