parent
853b967974
commit
19a5ab6104
|
@ -21,54 +21,54 @@
|
||||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
#++
|
#++
|
||||||
module GPX
|
module GPX
|
||||||
class Bounds < Base
|
class Bounds < Base
|
||||||
attr_accessor :min_lat, :max_lat, :max_lon, :min_lon, :center_lat, :center_lon
|
attr_accessor :min_lat, :max_lat, :max_lon, :min_lon, :center_lat, :center_lon
|
||||||
|
|
||||||
# Creates a new bounds object with the passed-in min and max longitudes
|
# Creates a new bounds object with the passed-in min and max longitudes
|
||||||
# and latitudes.
|
# and latitudes.
|
||||||
def initialize(opts = { :min_lat => 90.0, :max_lat => -90.0, :min_lon => 180.0, :max_lon => -180.0})
|
def initialize(opts = { :min_lat => 90.0, :max_lat => -90.0, :min_lon => 180.0, :max_lon => -180.0})
|
||||||
@min_lat, @max_lat = opts[:min_lat].to_f, opts[:max_lat].to_f
|
@min_lat, @max_lat = opts[:min_lat].to_f, opts[:max_lat].to_f
|
||||||
@min_lon, @max_lon = opts[:min_lon].to_f, opts[:max_lon].to_f
|
@min_lon, @max_lon = opts[:min_lon].to_f, opts[:max_lon].to_f
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the middle latitude.
|
||||||
|
def center_lat
|
||||||
|
distance = (max_lat - min_lat)/2.0
|
||||||
|
(min_lat + distance)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the middle longitude.
|
||||||
|
def center_lon
|
||||||
|
distance = (max_lon - min_lon)/2.0
|
||||||
|
(min_lon + distance)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns true if the pt is within these bounds.
|
||||||
|
def contains?(pt)
|
||||||
|
(pt.lat >= min_lat and pt.lat <= max_lat and pt.lon >= min_lon and pt.lon <= max_lon)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Adds an item to itself, expanding its min/max lat/lon as needed to
|
||||||
|
# contain the given item. The item can be either another instance of
|
||||||
|
# Bounds or a Point.
|
||||||
|
def add(item)
|
||||||
|
if(item.respond_to?(:lat) and item.respond_to?(:lon))
|
||||||
|
@min_lat = item.lat if item.lat < @min_lat
|
||||||
|
@min_lon = item.lon if item.lon < @min_lon
|
||||||
|
@max_lat = item.lat if item.lat > @max_lat
|
||||||
|
@max_lon = item.lon if item.lon > @max_lon
|
||||||
|
else
|
||||||
|
@min_lat = item.min_lat if item.min_lat < @min_lat
|
||||||
|
@min_lon = item.min_lon if item.min_lon < @min_lon
|
||||||
|
@max_lat = item.max_lat if item.max_lat > @max_lat
|
||||||
|
@max_lon = item.max_lon if item.max_lon > @max_lon
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Returns the middle latitude.
|
# Returns the min_lat, min_lon, max_lat, and max_lon in a labeled string.
|
||||||
def center_lat
|
def to_s
|
||||||
distance = (max_lat - min_lat)/2.0
|
"min_lat: #{min_lat} min_lon: #{min_lon} max_lat: #{max_lat} max_lon: #{max_lon}"
|
||||||
(min_lat + distance)
|
end
|
||||||
end
|
|
||||||
|
|
||||||
# Returns the middle longitude.
|
end
|
||||||
def center_lon
|
|
||||||
distance = (max_lon - min_lon)/2.0
|
|
||||||
(min_lon + distance)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns true if the pt is within these bounds.
|
|
||||||
def contains?(pt)
|
|
||||||
(pt.lat >= min_lat and pt.lat <= max_lat and pt.lon >= min_lon and pt.lon <= max_lon)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Adds an item to itself, expanding its min/max lat/lon as needed to
|
|
||||||
# contain the given item. The item can be either another instance of
|
|
||||||
# Bounds or a Point.
|
|
||||||
def add(item)
|
|
||||||
if(item.respond_to?(:lat) and item.respond_to?(:lon))
|
|
||||||
@min_lat = item.lat if item.lat < @min_lat
|
|
||||||
@min_lon = item.lon if item.lon < @min_lon
|
|
||||||
@max_lat = item.lat if item.lat > @max_lat
|
|
||||||
@max_lon = item.lon if item.lon > @max_lon
|
|
||||||
else
|
|
||||||
@min_lat = item.min_lat if item.min_lat < @min_lat
|
|
||||||
@min_lon = item.min_lon if item.min_lon < @min_lon
|
|
||||||
@max_lat = item.max_lat if item.max_lat > @max_lat
|
|
||||||
@max_lon = item.max_lon if item.max_lon > @max_lon
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns the min_lat, min_lon, max_lat, and max_lon in a labeled string.
|
|
||||||
def to_s
|
|
||||||
"min_lat: #{min_lat} min_lon: #{min_lon} max_lat: #{max_lat} max_lon: #{max_lon}"
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -24,24 +24,23 @@ module GPX
|
||||||
# A common base class which provides a useful initializer method to many
|
# A common base class which provides a useful initializer method to many
|
||||||
# class in the GPX library.
|
# class in the GPX library.
|
||||||
class Base
|
class Base
|
||||||
|
# This initializer can take an XML::Node and scrape out any text
|
||||||
|
# elements with the names given in the "text_elements" array. Each
|
||||||
|
# element found underneath "parent" with a name in "text_elements" causes
|
||||||
|
# an attribute to be initialized on the instance. This means you don't
|
||||||
|
# have to pick out individual text elements in each initializer of each
|
||||||
|
# class (Route, TrackPoint, Track, etc). Just pass an array of possible
|
||||||
|
# attributes to this method.
|
||||||
|
def instantiate_with_text_elements(parent, text_elements)
|
||||||
|
text_elements.each do |el|
|
||||||
|
child_xpath = "#{el}"
|
||||||
|
unless parent.at(child_xpath).nil?
|
||||||
|
val = parent.at(child_xpath).inner_text
|
||||||
|
self.send("#{el}=", val)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# This initializer can take an XML::Node and scrape out any text
|
end
|
||||||
# elements with the names given in the "text_elements" array. Each
|
|
||||||
# element found underneath "parent" with a name in "text_elements" causes
|
|
||||||
# an attribute to be initialized on the instance. This means you don't
|
|
||||||
# have to pick out individual text elements in each initializer of each
|
|
||||||
# class (Route, TrackPoint, Track, etc). Just pass an array of possible
|
|
||||||
# attributes to this method.
|
|
||||||
def instantiate_with_text_elements(parent, text_elements)
|
|
||||||
text_elements.each do |el|
|
|
||||||
child_xpath = "#{el}"
|
|
||||||
unless parent.at(child_xpath).nil?
|
|
||||||
val = parent.at(child_xpath).inner_text
|
|
||||||
self.send("#{el}=", val)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,271 +21,268 @@
|
||||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
#++
|
#++
|
||||||
module GPX
|
module GPX
|
||||||
class GPXFile < Base
|
class GPXFile < Base
|
||||||
attr_accessor :tracks, :routes, :waypoints, :bounds, :lowest_point, :highest_point, :duration, :ns, :time, :name
|
attr_accessor :tracks, :routes, :waypoints, :bounds, :lowest_point, :highest_point, :duration, :ns, :time, :name
|
||||||
|
|
||||||
|
# This initializer can be used to create a new GPXFile from an existing
|
||||||
# This initializer can be used to create a new GPXFile from an existing
|
# file or to create a new GPXFile instance with no data (so that you can
|
||||||
# file or to create a new GPXFile instance with no data (so that you can
|
# add tracks and points and write it out to a new file later).
|
||||||
# add tracks and points and write it out to a new file later).
|
# To read an existing GPX file, do this:
|
||||||
# To read an existing GPX file, do this:
|
# gpx_file = GPXFile.new(:gpx_file => 'mygpxfile.gpx')
|
||||||
# gpx_file = GPXFile.new(:gpx_file => 'mygpxfile.gpx')
|
# puts "Speed: #{gpx_file.average_speed}"
|
||||||
# puts "Speed: #{gpx_file.average_speed}"
|
# puts "Duration: #{gpx_file.duration}"
|
||||||
# puts "Duration: #{gpx_file.duration}"
|
# puts "Bounds: #{gpx_file.bounds}"
|
||||||
# puts "Bounds: #{gpx_file.bounds}"
|
#
|
||||||
#
|
# To read a GPX file from a string, use :gpx_data.
|
||||||
# To read a GPX file from a string, use :gpx_data.
|
# gpx_file = GPXFile.new(:gpx_data => '<xml ...><gpx>...</gpx>)
|
||||||
# gpx_file = GPXFile.new(:gpx_data => '<xml ...><gpx>...</gpx>)
|
# To create a new blank GPXFile instance:
|
||||||
# To create a new blank GPXFile instance:
|
# gpx_file = GPXFile.new
|
||||||
# gpx_file = GPXFile.new
|
# Note that you can pass in any instance variables to this form of the initializer, including Tracks or Segments:
|
||||||
# Note that you can pass in any instance variables to this form of the initializer, including Tracks or Segments:
|
# some_track = get_track_from_csv('some_other_format.csv')
|
||||||
# some_track = get_track_from_csv('some_other_format.csv')
|
# gpx_file = GPXFile.new(:tracks => [some_track])
|
||||||
# gpx_file = GPXFile.new(:tracks => [some_track])
|
#
|
||||||
#
|
def initialize(opts = {})
|
||||||
def initialize(opts = {})
|
@duration = 0
|
||||||
@duration = 0
|
if(opts[:gpx_file] or opts[:gpx_data])
|
||||||
if(opts[:gpx_file] or opts[:gpx_data])
|
if opts[:gpx_file]
|
||||||
if opts[:gpx_file]
|
gpx_file = opts[:gpx_file]
|
||||||
gpx_file = opts[:gpx_file]
|
gpx_file = File.open(gpx_file) unless gpx_file.is_a?(File)
|
||||||
gpx_file = File.open(gpx_file) unless gpx_file.is_a?(File)
|
@xml = Nokogiri::XML(gpx_file)
|
||||||
@xml = Nokogiri::XML(gpx_file)
|
else
|
||||||
else
|
@xml = Nokogiri::XML(opts[:gpx_data])
|
||||||
@xml = Nokogiri::XML(opts[:gpx_data])
|
|
||||||
end
|
|
||||||
|
|
||||||
reset_meta_data
|
|
||||||
bounds_element = (@xml.at("metadata/bounds") rescue nil)
|
|
||||||
if bounds_element
|
|
||||||
@bounds.min_lat = get_bounds_attr_value(bounds_element, %w{ min_lat minlat minLat })
|
|
||||||
@bounds.min_lon = get_bounds_attr_value(bounds_element, %w{ min_lon minlon minLon})
|
|
||||||
@bounds.max_lat = get_bounds_attr_value(bounds_element, %w{ max_lat maxlat maxLat})
|
|
||||||
@bounds.max_lon = get_bounds_attr_value(bounds_element, %w{ max_lon maxlon maxLon})
|
|
||||||
else
|
|
||||||
get_bounds = true
|
|
||||||
end
|
|
||||||
|
|
||||||
@time = Time.parse(@xml.at("metadata/time").inner_text) rescue nil
|
|
||||||
@name = @xml.at("metadata/name").inner_text rescue nil
|
|
||||||
@tracks = []
|
|
||||||
@xml.search("trk").each do |trk|
|
|
||||||
trk = Track.new(:element => trk, :gpx_file => self)
|
|
||||||
update_meta_data(trk, get_bounds)
|
|
||||||
@tracks << trk
|
|
||||||
end
|
|
||||||
@waypoints = []
|
|
||||||
@xml.search("wpt").each { |wpt| @waypoints << Waypoint.new(:element => wpt, :gpx_file => self) }
|
|
||||||
@routes = []
|
|
||||||
@xml.search("rte").each { |rte| @routes << Route.new(:element => rte, :gpx_file => self) }
|
|
||||||
@tracks.delete_if { |t| t.empty? }
|
|
||||||
|
|
||||||
calculate_duration
|
|
||||||
else
|
|
||||||
reset_meta_data
|
|
||||||
opts.each { |attr_name, value| instance_variable_set("@#{attr_name.to_s}", value) }
|
|
||||||
unless(@tracks.nil? or @tracks.size.zero?)
|
|
||||||
@tracks.each { |trk| update_meta_data(trk) }
|
|
||||||
calculate_duration
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@tracks ||= []
|
|
||||||
@routes ||= []
|
|
||||||
@waypoints ||= []
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_bounds_attr_value(el, possible_names)
|
|
||||||
result = nil
|
|
||||||
possible_names.each do |name|
|
|
||||||
result = el[name]
|
|
||||||
break unless result.nil?
|
|
||||||
end
|
|
||||||
return (result.to_f rescue nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns the distance, in kilometers, meters, or miles, of all of the
|
|
||||||
# tracks and segments contained in this GPXFile.
|
|
||||||
def distance(opts = { :units => 'kilometers' })
|
|
||||||
case opts[:units]
|
|
||||||
when /kilometers/i
|
|
||||||
return @distance
|
|
||||||
when /meters/i
|
|
||||||
return (@distance * 1000)
|
|
||||||
when /miles/i
|
|
||||||
return (@distance * 0.62)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns the average speed, in km/hr, meters/hr, or miles/hr, of this
|
|
||||||
# GPXFile. The calculation is based on the total distance divided by the
|
|
||||||
# total duration of the entire file.
|
|
||||||
def average_speed(opts = { :units => 'kilometers' })
|
|
||||||
case opts[:units]
|
|
||||||
when /kilometers/i
|
|
||||||
return @distance / (@duration/3600.0)
|
|
||||||
when /meters/i
|
|
||||||
return (@distance * 1000) / (@duration/3600.0)
|
|
||||||
when /miles/i
|
|
||||||
return (@distance * 0.62) / (@duration/3600.0)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Crops any points falling within a rectangular area. Identical to the
|
|
||||||
# delete_area method in every respect except that the points outside of
|
|
||||||
# the given area are deleted. Note that this method automatically causes
|
|
||||||
# the meta data to be updated after deletion.
|
|
||||||
def crop(area)
|
|
||||||
reset_meta_data
|
|
||||||
keep_tracks = []
|
|
||||||
tracks.each do |trk|
|
|
||||||
trk.crop(area)
|
|
||||||
unless trk.empty?
|
|
||||||
update_meta_data(trk)
|
|
||||||
keep_tracks << trk
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@tracks = keep_tracks
|
|
||||||
routes.each { |rte| rte.crop(area) }
|
|
||||||
waypoints.each { |wpt| wpt.crop(area) }
|
|
||||||
end
|
|
||||||
|
|
||||||
# Deletes any points falling within a rectangular area. The "area"
|
|
||||||
# parameter is usually an instance of the Bounds class. Note that this
|
|
||||||
# method cascades into similarly named methods of subordinate classes
|
|
||||||
# (i.e. Track, Segment), which means, if you want the deletion to apply
|
|
||||||
# to all the data, you only call this one (and not the one in Track or
|
|
||||||
# Segment classes). Note that this method automatically causes the meta
|
|
||||||
# data to be updated after deletion.
|
|
||||||
def delete_area(area)
|
|
||||||
reset_meta_data
|
|
||||||
keep_tracks = []
|
|
||||||
tracks.each do |trk|
|
|
||||||
trk.delete_area(area)
|
|
||||||
unless trk.empty?
|
|
||||||
update_meta_data(trk)
|
|
||||||
keep_tracks << trk
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@tracks = keep_tracks
|
|
||||||
routes.each { |rte| rte.delete_area(area) }
|
|
||||||
waypoints.each { |wpt| wpt.delete_area(area) }
|
|
||||||
end
|
|
||||||
|
|
||||||
# Resets the meta data for this GPX file. Meta data includes the bounds,
|
|
||||||
# the high and low points, and the distance.
|
|
||||||
def reset_meta_data
|
|
||||||
@bounds = Bounds.new
|
|
||||||
@highest_point = nil
|
|
||||||
@lowest_point = nil
|
|
||||||
@distance = 0.0
|
|
||||||
end
|
|
||||||
|
|
||||||
# Updates the meta data for this GPX file. Meta data includes the
|
|
||||||
# bounds, the high and low points, and the distance. This is useful when
|
|
||||||
# you modify the GPX data (i.e. by adding or deleting points) and you
|
|
||||||
# want the meta data to accurately reflect the new data.
|
|
||||||
def update_meta_data(trk, get_bounds = true)
|
|
||||||
@lowest_point = trk.lowest_point if(@lowest_point.nil? or (!trk.lowest_point.nil? and trk.lowest_point.elevation < @lowest_point.elevation))
|
|
||||||
@highest_point = trk.highest_point if(@highest_point.nil? or (!trk.highest_point.nil? and trk.highest_point.elevation > @highest_point.elevation))
|
|
||||||
@bounds.add(trk.bounds) if get_bounds
|
|
||||||
@distance += trk.distance
|
|
||||||
end
|
|
||||||
|
|
||||||
# Serialize the current GPXFile to a gpx file named <filename>.
|
|
||||||
# If the file does not exist, it is created. If it does exist, it is overwritten.
|
|
||||||
def write(filename, update_time = true)
|
|
||||||
@time = Time.now if(@time.nil? or update_time)
|
|
||||||
@name ||= File.basename(filename)
|
|
||||||
doc = generate_xml_doc
|
|
||||||
File.open(filename, 'w') { |f| f.write(doc.to_xml) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_s(update_time = true)
|
|
||||||
@time = Time.now if(@time.nil? or update_time)
|
|
||||||
doc = generate_xml_doc
|
|
||||||
doc.to_xml
|
|
||||||
end
|
|
||||||
|
|
||||||
def inspect
|
|
||||||
"<#{self.class.name}:...>"
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def generate_xml_doc
|
|
||||||
version = '1.1'
|
|
||||||
version_dir = version.gsub('.','/')
|
|
||||||
|
|
||||||
doc = Nokogiri::XML::Builder.new do |xml|
|
|
||||||
xml.gpx(
|
|
||||||
'xsi' => "http://www.w3.org/2001/XMLSchema-instance",
|
|
||||||
'version' => version.to_s,
|
|
||||||
'creator' => "GPX RubyGem #{GPX::VERSION}",
|
|
||||||
'xsi:schemaLocation' => "http://www.topografix.com/GPX/#{version_dir} http://www.topografix.com/GPX/#{version_dir}/gpx.xsd") \
|
|
||||||
{
|
|
||||||
xml.metadata {
|
|
||||||
xml.name @name
|
|
||||||
xml.time @time.xmlschema
|
|
||||||
xml.bound(
|
|
||||||
minlat: bounds.min_lat,
|
|
||||||
minlon: bounds.min_lon,
|
|
||||||
maxlat: bounds.max_lat,
|
|
||||||
maxlon: bounds.max_lon,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
tracks.each do |t|
|
|
||||||
xml.trk {
|
|
||||||
xml.name t.name
|
|
||||||
|
|
||||||
t.segments.each do |seg|
|
|
||||||
xml.trkseg {
|
|
||||||
seg.points.each do |p|
|
|
||||||
xml.trkpt(lat: p.lat, lon: p.lon) {
|
|
||||||
xml.time p.time.xmlschema unless p.time.nil?
|
|
||||||
xml.ele p.elevation unless p.elevation.nil?
|
|
||||||
}
|
|
||||||
end
|
|
||||||
}
|
|
||||||
end
|
|
||||||
}
|
|
||||||
end unless tracks.nil?
|
|
||||||
|
|
||||||
waypoints.each do |w|
|
|
||||||
xml.wpt(lat: w.lat, lon: w.lon) {
|
|
||||||
Waypoint::SUB_ELEMENTS.each do |sub_elem|
|
|
||||||
xml.send(sub_elem, w.send(sub_elem)) if w.respond_to?(sub_elem) && !w.send(sub_elem).nil?
|
|
||||||
end
|
|
||||||
}
|
|
||||||
end unless waypoints.nil?
|
|
||||||
|
|
||||||
routes.each do |r|
|
|
||||||
xml.rte {
|
|
||||||
xml.name r.name
|
|
||||||
|
|
||||||
r.points.each do |p|
|
|
||||||
xml.rtept(lat: p.lat, lon: p.lon) {
|
|
||||||
xml.time p.time.xmlschema unless p.time.nil?
|
|
||||||
xml.ele p.elevation unless p.elevation.nil?
|
|
||||||
}
|
|
||||||
end
|
|
||||||
}
|
|
||||||
end unless routes.nil?
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return doc
|
reset_meta_data
|
||||||
|
bounds_element = (@xml.at("metadata/bounds") rescue nil)
|
||||||
|
if bounds_element
|
||||||
|
@bounds.min_lat = get_bounds_attr_value(bounds_element, %w{ min_lat minlat minLat })
|
||||||
|
@bounds.min_lon = get_bounds_attr_value(bounds_element, %w{ min_lon minlon minLon})
|
||||||
|
@bounds.max_lat = get_bounds_attr_value(bounds_element, %w{ max_lat maxlat maxLat})
|
||||||
|
@bounds.max_lon = get_bounds_attr_value(bounds_element, %w{ max_lon maxlon maxLon})
|
||||||
|
else
|
||||||
|
get_bounds = true
|
||||||
|
end
|
||||||
|
|
||||||
|
@time = Time.parse(@xml.at("metadata/time").inner_text) rescue nil
|
||||||
|
@name = @xml.at("metadata/name").inner_text rescue nil
|
||||||
|
@tracks = []
|
||||||
|
@xml.search("trk").each do |trk|
|
||||||
|
trk = Track.new(:element => trk, :gpx_file => self)
|
||||||
|
update_meta_data(trk, get_bounds)
|
||||||
|
@tracks << trk
|
||||||
|
end
|
||||||
|
@waypoints = []
|
||||||
|
@xml.search("wpt").each { |wpt| @waypoints << Waypoint.new(:element => wpt, :gpx_file => self) }
|
||||||
|
@routes = []
|
||||||
|
@xml.search("rte").each { |rte| @routes << Route.new(:element => rte, :gpx_file => self) }
|
||||||
|
@tracks.delete_if { |t| t.empty? }
|
||||||
|
|
||||||
|
calculate_duration
|
||||||
|
else
|
||||||
|
reset_meta_data
|
||||||
|
opts.each { |attr_name, value| instance_variable_set("@#{attr_name.to_s}", value) }
|
||||||
|
unless(@tracks.nil? or @tracks.size.zero?)
|
||||||
|
@tracks.each { |trk| update_meta_data(trk) }
|
||||||
|
calculate_duration
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@tracks ||= []
|
||||||
|
@routes ||= []
|
||||||
|
@waypoints ||= []
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_bounds_attr_value(el, possible_names)
|
||||||
|
result = nil
|
||||||
|
possible_names.each do |name|
|
||||||
|
result = el[name]
|
||||||
|
break unless result.nil?
|
||||||
|
end
|
||||||
|
return (result.to_f rescue nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the distance, in kilometers, meters, or miles, of all of the
|
||||||
|
# tracks and segments contained in this GPXFile.
|
||||||
|
def distance(opts = { :units => 'kilometers' })
|
||||||
|
case opts[:units]
|
||||||
|
when /kilometers/i
|
||||||
|
return @distance
|
||||||
|
when /meters/i
|
||||||
|
return (@distance * 1000)
|
||||||
|
when /miles/i
|
||||||
|
return (@distance * 0.62)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the average speed, in km/hr, meters/hr, or miles/hr, of this
|
||||||
|
# GPXFile. The calculation is based on the total distance divided by the
|
||||||
|
# total duration of the entire file.
|
||||||
|
def average_speed(opts = { :units => 'kilometers' })
|
||||||
|
case opts[:units]
|
||||||
|
when /kilometers/i
|
||||||
|
return @distance / (@duration/3600.0)
|
||||||
|
when /meters/i
|
||||||
|
return (@distance * 1000) / (@duration/3600.0)
|
||||||
|
when /miles/i
|
||||||
|
return (@distance * 0.62) / (@duration/3600.0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Crops any points falling within a rectangular area. Identical to the
|
||||||
|
# delete_area method in every respect except that the points outside of
|
||||||
|
# the given area are deleted. Note that this method automatically causes
|
||||||
|
# the meta data to be updated after deletion.
|
||||||
|
def crop(area)
|
||||||
|
reset_meta_data
|
||||||
|
keep_tracks = []
|
||||||
|
tracks.each do |trk|
|
||||||
|
trk.crop(area)
|
||||||
|
unless trk.empty?
|
||||||
|
update_meta_data(trk)
|
||||||
|
keep_tracks << trk
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@tracks = keep_tracks
|
||||||
|
routes.each { |rte| rte.crop(area) }
|
||||||
|
waypoints.each { |wpt| wpt.crop(area) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Deletes any points falling within a rectangular area. The "area"
|
||||||
|
# parameter is usually an instance of the Bounds class. Note that this
|
||||||
|
# method cascades into similarly named methods of subordinate classes
|
||||||
|
# (i.e. Track, Segment), which means, if you want the deletion to apply
|
||||||
|
# to all the data, you only call this one (and not the one in Track or
|
||||||
|
# Segment classes). Note that this method automatically causes the meta
|
||||||
|
# data to be updated after deletion.
|
||||||
|
def delete_area(area)
|
||||||
|
reset_meta_data
|
||||||
|
keep_tracks = []
|
||||||
|
tracks.each do |trk|
|
||||||
|
trk.delete_area(area)
|
||||||
|
unless trk.empty?
|
||||||
|
update_meta_data(trk)
|
||||||
|
keep_tracks << trk
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@tracks = keep_tracks
|
||||||
|
routes.each { |rte| rte.delete_area(area) }
|
||||||
|
waypoints.each { |wpt| wpt.delete_area(area) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Resets the meta data for this GPX file. Meta data includes the bounds,
|
||||||
|
# the high and low points, and the distance.
|
||||||
|
def reset_meta_data
|
||||||
|
@bounds = Bounds.new
|
||||||
|
@highest_point = nil
|
||||||
|
@lowest_point = nil
|
||||||
|
@distance = 0.0
|
||||||
|
end
|
||||||
|
|
||||||
|
# Updates the meta data for this GPX file. Meta data includes the
|
||||||
|
# bounds, the high and low points, and the distance. This is useful when
|
||||||
|
# you modify the GPX data (i.e. by adding or deleting points) and you
|
||||||
|
# want the meta data to accurately reflect the new data.
|
||||||
|
def update_meta_data(trk, get_bounds = true)
|
||||||
|
@lowest_point = trk.lowest_point if(@lowest_point.nil? or (!trk.lowest_point.nil? and trk.lowest_point.elevation < @lowest_point.elevation))
|
||||||
|
@highest_point = trk.highest_point if(@highest_point.nil? or (!trk.highest_point.nil? and trk.highest_point.elevation > @highest_point.elevation))
|
||||||
|
@bounds.add(trk.bounds) if get_bounds
|
||||||
|
@distance += trk.distance
|
||||||
|
end
|
||||||
|
|
||||||
|
# Serialize the current GPXFile to a gpx file named <filename>.
|
||||||
|
# If the file does not exist, it is created. If it does exist, it is overwritten.
|
||||||
|
def write(filename, update_time = true)
|
||||||
|
@time = Time.now if(@time.nil? or update_time)
|
||||||
|
@name ||= File.basename(filename)
|
||||||
|
doc = generate_xml_doc
|
||||||
|
File.open(filename, 'w') { |f| f.write(doc.to_xml) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s(update_time = true)
|
||||||
|
@time = Time.now if(@time.nil? or update_time)
|
||||||
|
doc = generate_xml_doc
|
||||||
|
doc.to_xml
|
||||||
|
end
|
||||||
|
|
||||||
|
def inspect
|
||||||
|
"<#{self.class.name}:...>"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def generate_xml_doc
|
||||||
|
version = '1.1'
|
||||||
|
version_dir = version.gsub('.','/')
|
||||||
|
|
||||||
|
doc = Nokogiri::XML::Builder.new do |xml|
|
||||||
|
xml.gpx(
|
||||||
|
'xsi' => "http://www.w3.org/2001/XMLSchema-instance",
|
||||||
|
'version' => version.to_s,
|
||||||
|
'creator' => "GPX RubyGem #{GPX::VERSION}",
|
||||||
|
'xsi:schemaLocation' => "http://www.topografix.com/GPX/#{version_dir} http://www.topografix.com/GPX/#{version_dir}/gpx.xsd") \
|
||||||
|
{
|
||||||
|
xml.metadata {
|
||||||
|
xml.name @name
|
||||||
|
xml.time @time.xmlschema
|
||||||
|
xml.bound(
|
||||||
|
minlat: bounds.min_lat,
|
||||||
|
minlon: bounds.min_lon,
|
||||||
|
maxlat: bounds.max_lat,
|
||||||
|
maxlon: bounds.max_lon,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
tracks.each do |t|
|
||||||
|
xml.trk {
|
||||||
|
xml.name t.name
|
||||||
|
|
||||||
|
t.segments.each do |seg|
|
||||||
|
xml.trkseg {
|
||||||
|
seg.points.each do |p|
|
||||||
|
xml.trkpt(lat: p.lat, lon: p.lon) {
|
||||||
|
xml.time p.time.xmlschema unless p.time.nil?
|
||||||
|
xml.ele p.elevation unless p.elevation.nil?
|
||||||
|
}
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end unless tracks.nil?
|
||||||
|
|
||||||
|
waypoints.each do |w|
|
||||||
|
xml.wpt(lat: w.lat, lon: w.lon) {
|
||||||
|
Waypoint::SUB_ELEMENTS.each do |sub_elem|
|
||||||
|
xml.send(sub_elem, w.send(sub_elem)) if w.respond_to?(sub_elem) && !w.send(sub_elem).nil?
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end unless waypoints.nil?
|
||||||
|
|
||||||
|
routes.each do |r|
|
||||||
|
xml.rte {
|
||||||
|
xml.name r.name
|
||||||
|
|
||||||
|
r.points.each do |p|
|
||||||
|
xml.rtept(lat: p.lat, lon: p.lon) {
|
||||||
|
xml.time p.time.xmlschema unless p.time.nil?
|
||||||
|
xml.ele p.elevation unless p.elevation.nil?
|
||||||
|
}
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end unless routes.nil?
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Calculates and sets the duration attribute by subtracting the time on
|
return doc
|
||||||
# the very first point from the time on the very last point.
|
end
|
||||||
def calculate_duration
|
|
||||||
@duration = 0
|
# Calculates and sets the duration attribute by subtracting the time on
|
||||||
if(@tracks.nil? or @tracks.size.zero? or @tracks[0].segments.nil? or @tracks[0].segments.size.zero?)
|
# the very first point from the time on the very last point.
|
||||||
return @duration
|
def calculate_duration
|
||||||
end
|
@duration = 0
|
||||||
@duration = (@tracks[-1].segments[-1].points[-1].time - @tracks.first.segments.first.points.first.time)
|
if(@tracks.nil? or @tracks.size.zero? or @tracks[0].segments.nil? or @tracks[0].segments.size.zero?)
|
||||||
rescue
|
return @duration
|
||||||
@duration = 0
|
|
||||||
end
|
end
|
||||||
|
@duration = (@tracks[-1].segments[-1].points[-1].time - @tracks.first.segments.first.points.first.time)
|
||||||
|
rescue
|
||||||
end
|
@duration = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,118 +20,113 @@
|
||||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
#++
|
#++
|
||||||
|
|
||||||
require 'csv'
|
|
||||||
|
|
||||||
module GPX
|
module GPX
|
||||||
|
# This class will parse the lat/lon and time data from a Magellan track log,
|
||||||
|
# which is a NMEA formatted CSV list of points.
|
||||||
|
class MagellanTrackLog
|
||||||
|
#PMGNTRK
|
||||||
|
# This message is to be used to transmit Track information (basically a list of previous position fixes)
|
||||||
|
# which is often displayed on the plotter or map screen of the unit. The first field in this message
|
||||||
|
# is the Latitude, followed by N or S. The next field is the Longitude followed by E or W. The next
|
||||||
|
# field is the altitude followed by “F” for feet or “M” for meters. The next field is
|
||||||
|
# the UTC time of the fix. The next field consists of a status letter of “A” to indicated that
|
||||||
|
# the data is valid, or “V” to indicate that the data is not valid. The last character field is
|
||||||
|
# the name of the track, for those units that support named tracks. The last field contains the UTC date
|
||||||
|
# of the fix. Note that this field is (and its preceding comma) is only produced by the unit when the
|
||||||
|
# command PMGNCMD,TRACK,2 is given. It is not present when a simple command of PMGNCMD,TRACK is issued.
|
||||||
|
|
||||||
# This class will parse the lat/lon and time data from a Magellan track log,
|
#NOTE: The Latitude and Longitude Fields are shown as having two decimal
|
||||||
# which is a NMEA formatted CSV list of points.
|
# places. As many additional decimal places may be added as long as the total
|
||||||
|
# length of the message does not exceed 82 bytes.
|
||||||
|
|
||||||
class MagellanTrackLog
|
# $PMGNTRK,llll.ll,a,yyyyy.yy,a,xxxxx,a,hhmmss.ss,A,c----c,ddmmyy*hh<CR><LF>
|
||||||
#PMGNTRK
|
require 'csv'
|
||||||
# This message is to be used to transmit Track information (basically a list of previous position fixes)
|
|
||||||
# which is often displayed on the plotter or map screen of the unit. The first field in this message
|
|
||||||
# is the Latitude, followed by N or S. The next field is the Longitude followed by E or W. The next
|
|
||||||
# field is the altitude followed by “F” for feet or “M” for meters. The next field is
|
|
||||||
# the UTC time of the fix. The next field consists of a status letter of “A” to indicated that
|
|
||||||
# the data is valid, or “V” to indicate that the data is not valid. The last character field is
|
|
||||||
# the name of the track, for those units that support named tracks. The last field contains the UTC date
|
|
||||||
# of the fix. Note that this field is (and its preceding comma) is only produced by the unit when the
|
|
||||||
# command PMGNCMD,TRACK,2 is given. It is not present when a simple command of PMGNCMD,TRACK is issued.
|
|
||||||
|
|
||||||
#NOTE: The Latitude and Longitude Fields are shown as having two decimal
|
LAT = 1
|
||||||
# places. As many additional decimal places may be added as long as the total
|
LAT_HEMI = 2
|
||||||
# length of the message does not exceed 82 bytes.
|
LON = 3
|
||||||
|
LON_HEMI = 4
|
||||||
|
ELE = 5
|
||||||
|
ELE_UNITS = 6
|
||||||
|
TIME = 7
|
||||||
|
INVALID_FLAG = 8
|
||||||
|
DATE = 10
|
||||||
|
|
||||||
# $PMGNTRK,llll.ll,a,yyyyy.yy,a,xxxxx,a,hhmmss.ss,A,c----c,ddmmyy*hh<CR><LF>
|
FEET_TO_METERS = 0.3048
|
||||||
require 'csv'
|
|
||||||
|
|
||||||
LAT = 1
|
class << self
|
||||||
LAT_HEMI = 2
|
|
||||||
LON = 3
|
|
||||||
LON_HEMI = 4
|
|
||||||
ELE = 5
|
|
||||||
ELE_UNITS = 6
|
|
||||||
TIME = 7
|
|
||||||
INVALID_FLAG = 8
|
|
||||||
DATE = 10
|
|
||||||
|
|
||||||
FEET_TO_METERS = 0.3048
|
# Takes the name of a magellan file, converts the contents to GPX, and
|
||||||
|
# writes the result to gpx_filename.
|
||||||
|
def convert_to_gpx(magellan_filename, gpx_filename)
|
||||||
|
|
||||||
class << self
|
segment = Segment.new
|
||||||
|
|
||||||
# Takes the name of a magellan file, converts the contents to GPX, and
|
CSV.open(magellan_filename, "r").each do |row|
|
||||||
# writes the result to gpx_filename.
|
next if(row.size < 10 or row[INVALID_FLAG] == 'V')
|
||||||
def convert_to_gpx(magellan_filename, gpx_filename)
|
|
||||||
|
|
||||||
segment = Segment.new
|
lat_deg = row[LAT][0..1]
|
||||||
|
lat_min = row[LAT][2...-1]
|
||||||
|
lat_hemi = row[LAT_HEMI]
|
||||||
|
|
||||||
CSV.open(magellan_filename, "r").each do |row|
|
lat = lat_deg.to_f + (lat_min.to_f / 60.0)
|
||||||
next if(row.size < 10 or row[INVALID_FLAG] == 'V')
|
lat = (-lat) if(lat_hemi == 'S')
|
||||||
|
|
||||||
lat_deg = row[LAT][0..1]
|
lon_deg = row[LON][0..2]
|
||||||
lat_min = row[LAT][2...-1]
|
lon_min = row[LON][3..-1]
|
||||||
lat_hemi = row[LAT_HEMI]
|
lon_hemi = row[LON_HEMI]
|
||||||
|
|
||||||
lat = lat_deg.to_f + (lat_min.to_f / 60.0)
|
lon = lon_deg.to_f + (lon_min.to_f / 60.0)
|
||||||
lat = (-lat) if(lat_hemi == 'S')
|
lon = (-lon) if(lon_hemi == 'W')
|
||||||
|
|
||||||
lon_deg = row[LON][0..2]
|
|
||||||
lon_min = row[LON][3..-1]
|
|
||||||
lon_hemi = row[LON_HEMI]
|
|
||||||
|
|
||||||
lon = lon_deg.to_f + (lon_min.to_f / 60.0)
|
|
||||||
lon = (-lon) if(lon_hemi == 'W')
|
|
||||||
|
|
||||||
|
|
||||||
ele = row[ELE]
|
ele = row[ELE]
|
||||||
ele_units = row[ELE_UNITS]
|
ele_units = row[ELE_UNITS]
|
||||||
ele = ele.to_f
|
ele = ele.to_f
|
||||||
if(ele_units == 'F')
|
if(ele_units == 'F')
|
||||||
ele *= FEET_TO_METERS
|
ele *= FEET_TO_METERS
|
||||||
end
|
end
|
||||||
|
|
||||||
hrs = row[TIME][0..1].to_i
|
hrs = row[TIME][0..1].to_i
|
||||||
mins = row[TIME][2..3].to_i
|
mins = row[TIME][2..3].to_i
|
||||||
secs = row[TIME][4..5].to_i
|
secs = row[TIME][4..5].to_i
|
||||||
day = row[DATE][0..1].to_i
|
day = row[DATE][0..1].to_i
|
||||||
mon = row[DATE][2..3].to_i
|
mon = row[DATE][2..3].to_i
|
||||||
yr = 2000 + row[DATE][4..5].to_i
|
yr = 2000 + row[DATE][4..5].to_i
|
||||||
|
|
||||||
time = Time.gm(yr, mon, day, hrs, mins, secs)
|
time = Time.gm(yr, mon, day, hrs, mins, secs)
|
||||||
|
|
||||||
#must create point
|
#must create point
|
||||||
pt = TrackPoint.new(:lat => lat, :lon => lon, :time => time, :elevation => ele)
|
pt = TrackPoint.new(:lat => lat, :lon => lon, :time => time, :elevation => ele)
|
||||||
segment.append_point(pt)
|
segment.append_point(pt)
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
trk = Track.new
|
trk = Track.new
|
||||||
trk.append_segment(segment)
|
trk.append_segment(segment)
|
||||||
gpx_file = GPXFile.new(:tracks => [trk])
|
gpx_file = GPXFile.new(:tracks => [trk])
|
||||||
gpx_file.write(gpx_filename)
|
gpx_file.write(gpx_filename)
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
# Tests to see if the given file is a magellan NMEA track log.
|
|
||||||
def is_magellan_file?(filename)
|
|
||||||
i = 0
|
|
||||||
File.open(filename, "r") do |f|
|
|
||||||
f.each do |line|
|
|
||||||
i += 1
|
|
||||||
if line =~ /^\$PMGNTRK/
|
|
||||||
return true
|
|
||||||
elsif line =~ /<\?xml/
|
|
||||||
return false
|
|
||||||
elsif(i > 10)
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
# Tests to see if the given file is a magellan NMEA track log.
|
||||||
|
def is_magellan_file?(filename)
|
||||||
|
i = 0
|
||||||
|
File.open(filename, "r") do |f|
|
||||||
|
f.each do |line|
|
||||||
|
i += 1
|
||||||
|
if line =~ /^\$PMGNTRK/
|
||||||
|
return true
|
||||||
|
elsif line =~ /<\?xml/
|
||||||
|
return false
|
||||||
|
elsif(i > 10)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
115
lib/gpx/point.rb
115
lib/gpx/point.rb
|
@ -20,72 +20,71 @@
|
||||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
#++
|
#++
|
||||||
include Math
|
|
||||||
module GPX
|
module GPX
|
||||||
# The base class for all points. Trackpoint and Waypoint both descend from this base class.
|
# The base class for all points. Trackpoint and Waypoint both descend from this base class.
|
||||||
class Point < Base
|
class Point < Base
|
||||||
D_TO_R = PI/180.0;
|
D_TO_R = Math::PI/180.0;
|
||||||
attr_accessor :lat, :lon, :time, :elevation, :gpx_file, :speed
|
attr_accessor :lat, :lon, :time, :elevation, :gpx_file, :speed
|
||||||
|
|
||||||
# When you need to manipulate individual points, you can create a Point
|
|
||||||
# object with a latitude, a longitude, an elevation, and a time. In
|
|
||||||
# addition, you can pass an XML element to this initializer, and the
|
|
||||||
# relevant info will be parsed out.
|
|
||||||
def initialize(opts = {:lat => 0.0, :lon => 0.0, :elevation => 0.0, :time => Time.now } )
|
|
||||||
@gpx_file = opts[:gpx_file]
|
|
||||||
if (opts[:element])
|
|
||||||
elem = opts[:element]
|
|
||||||
@lat, @lon = elem["lat"].to_f, elem["lon"].to_f
|
|
||||||
@latr, @lonr = (D_TO_R * @lat), (D_TO_R * @lon)
|
|
||||||
#'-'? yyyy '-' mm '-' dd 'T' hh ':' mm ':' ss ('.' s+)? (zzzzzz)?
|
|
||||||
@time = (Time.xmlschema(elem.at("time").inner_text) rescue nil)
|
|
||||||
@elevation = elem.at("ele").inner_text.to_f unless elem.at("ele").nil?
|
|
||||||
@speed = elem.at("speed").inner_text.to_f unless elem.at("speed").nil?
|
|
||||||
else
|
|
||||||
@lat = opts[:lat]
|
|
||||||
@lon = opts[:lon]
|
|
||||||
@elevation = opts[:elevation]
|
|
||||||
@time = opts[:time]
|
|
||||||
@speed = opts[:speed]
|
|
||||||
end
|
|
||||||
|
|
||||||
|
# When you need to manipulate individual points, you can create a Point
|
||||||
|
# object with a latitude, a longitude, an elevation, and a time. In
|
||||||
|
# addition, you can pass an XML element to this initializer, and the
|
||||||
|
# relevant info will be parsed out.
|
||||||
|
def initialize(opts = {:lat => 0.0, :lon => 0.0, :elevation => 0.0, :time => Time.now } )
|
||||||
|
@gpx_file = opts[:gpx_file]
|
||||||
|
if (opts[:element])
|
||||||
|
elem = opts[:element]
|
||||||
|
@lat, @lon = elem["lat"].to_f, elem["lon"].to_f
|
||||||
|
@latr, @lonr = (D_TO_R * @lat), (D_TO_R * @lon)
|
||||||
|
#'-'? yyyy '-' mm '-' dd 'T' hh ':' mm ':' ss ('.' s+)? (zzzzzz)?
|
||||||
|
@time = (Time.xmlschema(elem.at("time").inner_text) rescue nil)
|
||||||
|
@elevation = elem.at("ele").inner_text.to_f unless elem.at("ele").nil?
|
||||||
|
@speed = elem.at("speed").inner_text.to_f unless elem.at("speed").nil?
|
||||||
|
else
|
||||||
|
@lat = opts[:lat]
|
||||||
|
@lon = opts[:lon]
|
||||||
|
@elevation = opts[:elevation]
|
||||||
|
@time = opts[:time]
|
||||||
|
@speed = opts[:speed]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
# Returns the latitude and longitude (in that order), separated by the
|
|
||||||
# given delimeter. This is useful for passing a point into another API
|
|
||||||
# (i.e. the Google Maps javascript API).
|
|
||||||
def lat_lon(delim = ', ')
|
|
||||||
"#{lat}#{delim}#{lon}"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns the longitude and latitude (in that order), separated by the
|
# Returns the latitude and longitude (in that order), separated by the
|
||||||
# given delimeter. This is useful for passing a point into another API
|
# given delimeter. This is useful for passing a point into another API
|
||||||
# (i.e. the Google Maps javascript API).
|
# (i.e. the Google Maps javascript API).
|
||||||
def lon_lat(delim = ', ')
|
def lat_lon(delim = ', ')
|
||||||
"#{lon}#{delim}#{lat}"
|
"#{lat}#{delim}#{lon}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Latitude in radians.
|
# Returns the longitude and latitude (in that order), separated by the
|
||||||
def latr
|
# given delimeter. This is useful for passing a point into another API
|
||||||
@latr ||= (@lat * D_TO_R)
|
# (i.e. the Google Maps javascript API).
|
||||||
end
|
def lon_lat(delim = ', ')
|
||||||
|
"#{lon}#{delim}#{lat}"
|
||||||
|
end
|
||||||
|
|
||||||
# Longitude in radians.
|
# Latitude in radians.
|
||||||
def lonr
|
def latr
|
||||||
@lonr ||= (@lon * D_TO_R)
|
@latr ||= (@lat * D_TO_R)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Set the latitude (in degrees).
|
# Longitude in radians.
|
||||||
def lat=(latitude)
|
def lonr
|
||||||
@latr = (latitude * D_TO_R)
|
@lonr ||= (@lon * D_TO_R)
|
||||||
@lat = latitude
|
end
|
||||||
end
|
|
||||||
|
|
||||||
# Set the longitude (in degrees).
|
# Set the latitude (in degrees).
|
||||||
def lon=(longitude)
|
def lat=(latitude)
|
||||||
@lonr = (longitude * D_TO_R)
|
@latr = (latitude * D_TO_R)
|
||||||
@lon = longitude
|
@lat = latitude
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
# Set the longitude (in degrees).
|
||||||
|
def lon=(longitude)
|
||||||
|
@lonr = (longitude * D_TO_R)
|
||||||
|
@lon = longitude
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,39 +21,38 @@
|
||||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
#++
|
#++
|
||||||
module GPX
|
module GPX
|
||||||
|
# A Route in GPX is very similar to a Track, but it is created by a user
|
||||||
|
# from a series of Waypoints, whereas a Track is created by the GPS device
|
||||||
|
# automatically logging your progress at regular intervals.
|
||||||
|
class Route < Base
|
||||||
|
|
||||||
# A Route in GPX is very similar to a Track, but it is created by a user
|
attr_accessor :points, :name, :gpx_file
|
||||||
# from a series of Waypoints, whereas a Track is created by the GPS device
|
|
||||||
# automatically logging your progress at regular intervals.
|
|
||||||
class Route < Base
|
|
||||||
|
|
||||||
attr_accessor :points, :name, :gpx_file
|
|
||||||
|
|
||||||
# Initialize a Route from a XML::Node.
|
|
||||||
def initialize(opts = {})
|
|
||||||
if(opts[:gpx_file] and opts[:element])
|
|
||||||
rte_element = opts[:element]
|
|
||||||
@gpx_file = opts[:gpx_file]
|
|
||||||
@name = rte_element.at("name").inner_text
|
|
||||||
@points = []
|
|
||||||
rte_element.search("rtept").each do |point|
|
|
||||||
@points << Point.new(:element => point, :gpx_file => @gpx_file)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
@points = (opts[:points] or [])
|
|
||||||
@name = (opts[:name])
|
|
||||||
end
|
|
||||||
|
|
||||||
|
# Initialize a Route from a XML::Node.
|
||||||
|
def initialize(opts = {})
|
||||||
|
if(opts[:gpx_file] and opts[:element])
|
||||||
|
rte_element = opts[:element]
|
||||||
|
@gpx_file = opts[:gpx_file]
|
||||||
|
@name = rte_element.at("name").inner_text
|
||||||
|
@points = []
|
||||||
|
rte_element.search("rtept").each do |point|
|
||||||
|
@points << Point.new(:element => point, :gpx_file => @gpx_file)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
@points = (opts[:points] or [])
|
||||||
|
@name = (opts[:name])
|
||||||
end
|
end
|
||||||
|
|
||||||
# Delete points outside of a given area.
|
end
|
||||||
def crop(area)
|
|
||||||
points.delete_if{ |pt| not area.contains? pt }
|
|
||||||
end
|
|
||||||
|
|
||||||
# Delete points within the given area.
|
# Delete points outside of a given area.
|
||||||
def delete_area(area)
|
def crop(area)
|
||||||
points.delete_if{ |pt| area.contains? pt }
|
points.delete_if{ |pt| not area.contains? pt }
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
# Delete points within the given area.
|
||||||
|
def delete_area(area)
|
||||||
|
points.delete_if{ |pt| area.contains? pt }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,195 +21,194 @@
|
||||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
#++
|
#++
|
||||||
module GPX
|
module GPX
|
||||||
|
# A segment is the basic container in a GPX file. A Segment contains points
|
||||||
|
# (in this lib, they're called TrackPoints). A Track contains Segments. An
|
||||||
|
# instance of Segment knows its highest point, lowest point, earliest and
|
||||||
|
# latest points, distance, and bounds.
|
||||||
|
class Segment < Base
|
||||||
|
|
||||||
# A segment is the basic container in a GPX file. A Segment contains points
|
attr_reader :earliest_point, :latest_point, :bounds, :highest_point, :lowest_point, :distance
|
||||||
# (in this lib, they're called TrackPoints). A Track contains Segments. An
|
attr_accessor :points, :track
|
||||||
# instance of Segment knows its highest point, lowest point, earliest and
|
|
||||||
# latest points, distance, and bounds.
|
|
||||||
class Segment < Base
|
|
||||||
|
|
||||||
attr_reader :earliest_point, :latest_point, :bounds, :highest_point, :lowest_point, :distance
|
# If a XML::Node object is passed-in, this will initialize a new
|
||||||
attr_accessor :points, :track
|
# Segment based on its contents. Otherwise, a blank Segment is created.
|
||||||
|
def initialize(opts = {})
|
||||||
# If a XML::Node object is passed-in, this will initialize a new
|
@gpx_file = opts[:gpx_file]
|
||||||
# Segment based on its contents. Otherwise, a blank Segment is created.
|
@track = opts[:track]
|
||||||
def initialize(opts = {})
|
@points = []
|
||||||
@gpx_file = opts[:gpx_file]
|
@earliest_point = nil
|
||||||
@track = opts[:track]
|
@latest_point = nil
|
||||||
@points = []
|
@highest_point = nil
|
||||||
@earliest_point = nil
|
@lowest_point = nil
|
||||||
@latest_point = nil
|
@distance = 0.0
|
||||||
@highest_point = nil
|
@bounds = Bounds.new
|
||||||
@lowest_point = nil
|
if(opts[:element])
|
||||||
@distance = 0.0
|
segment_element = opts[:element]
|
||||||
@bounds = Bounds.new
|
last_pt = nil
|
||||||
if(opts[:element])
|
if segment_element.is_a?(Nokogiri::XML::Node)
|
||||||
segment_element = opts[:element]
|
segment_element.search("trkpt").each do |trkpt|
|
||||||
last_pt = nil
|
pt = TrackPoint.new(:element => trkpt, :segment => self, :gpx_file => @gpx_file)
|
||||||
if segment_element.is_a?(Nokogiri::XML::Node)
|
unless pt.time.nil?
|
||||||
segment_element.search("trkpt").each do |trkpt|
|
@earliest_point = pt if(@earliest_point.nil? or pt.time < @earliest_point.time)
|
||||||
pt = TrackPoint.new(:element => trkpt, :segment => self, :gpx_file => @gpx_file)
|
@latest_point = pt if(@latest_point.nil? or pt.time > @latest_point.time)
|
||||||
unless pt.time.nil?
|
|
||||||
@earliest_point = pt if(@earliest_point.nil? or pt.time < @earliest_point.time)
|
|
||||||
@latest_point = pt if(@latest_point.nil? or pt.time > @latest_point.time)
|
|
||||||
end
|
|
||||||
unless pt.elevation.nil?
|
|
||||||
@lowest_point = pt if(@lowest_point.nil? or pt.elevation < @lowest_point.elevation)
|
|
||||||
@highest_point = pt if(@highest_point.nil? or pt.elevation > @highest_point.elevation)
|
|
||||||
end
|
|
||||||
@bounds.min_lat = pt.lat if pt.lat < @bounds.min_lat
|
|
||||||
@bounds.min_lon = pt.lon if pt.lon < @bounds.min_lon
|
|
||||||
@bounds.max_lat = pt.lat if pt.lat > @bounds.max_lat
|
|
||||||
@bounds.max_lon = pt.lon if pt.lon > @bounds.max_lon
|
|
||||||
|
|
||||||
@distance += haversine_distance(last_pt, pt) unless last_pt.nil?
|
|
||||||
|
|
||||||
@points << pt
|
|
||||||
last_pt = pt
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
unless pt.elevation.nil?
|
||||||
end
|
@lowest_point = pt if(@lowest_point.nil? or pt.elevation < @lowest_point.elevation)
|
||||||
|
@highest_point = pt if(@highest_point.nil? or pt.elevation > @highest_point.elevation)
|
||||||
# Tack on a point to this Segment. All meta-data will be updated.
|
|
||||||
def append_point(pt)
|
|
||||||
last_pt = @points[-1]
|
|
||||||
@earliest_point = pt if(@earliest_point.nil? or pt.time < @earliest_point.time)
|
|
||||||
@latest_point = pt if(@latest_point.nil? or pt.time > @latest_point.time)
|
|
||||||
@lowest_point = pt if(@lowest_point.nil? or pt.elevation < @lowest_point.elevation)
|
|
||||||
@highest_point = pt if(@highest_point.nil? or pt.elevation > @highest_point.elevation)
|
|
||||||
@bounds.min_lat = pt.lat if pt.lat < @bounds.min_lat
|
|
||||||
@bounds.min_lon = pt.lon if pt.lon < @bounds.min_lon
|
|
||||||
@bounds.max_lat = pt.lat if pt.lat > @bounds.max_lat
|
|
||||||
@bounds.max_lon = pt.lon if pt.lon > @bounds.max_lon
|
|
||||||
@distance += haversine_distance(last_pt, pt) unless last_pt.nil?
|
|
||||||
@points << pt
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns true if the given time is within this Segment.
|
|
||||||
def contains_time?(time)
|
|
||||||
(time >= @earliest_point.time and time <= @latest_point.time) rescue false
|
|
||||||
end
|
|
||||||
|
|
||||||
# Finds the closest point in time to the passed-in time argument. Useful
|
|
||||||
# for matching up time-based objects (photos, video, etc) with a
|
|
||||||
# geographic location.
|
|
||||||
def closest_point(time)
|
|
||||||
find_closest(points, time)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Deletes all points within this Segment that lie outside of the given
|
|
||||||
# area (which should be a Bounds object).
|
|
||||||
def crop(area)
|
|
||||||
delete_if { |pt| not area.contains?(pt) }
|
|
||||||
end
|
|
||||||
|
|
||||||
# Deletes all points in this Segment that lie within the given area.
|
|
||||||
def delete_area(area)
|
|
||||||
delete_if{ |pt| area.contains?(pt) }
|
|
||||||
end
|
|
||||||
|
|
||||||
# A handy method that deletes points based on a block that is passed in.
|
|
||||||
# If the passed-in block returns true when given a point, then that point
|
|
||||||
# is deleted. For example:
|
|
||||||
# delete_if{ |pt| area.contains?(pt) }
|
|
||||||
def delete_if
|
|
||||||
reset_meta_data
|
|
||||||
keep_points = []
|
|
||||||
last_pt = nil
|
|
||||||
points.each do |pt|
|
|
||||||
unless yield(pt)
|
|
||||||
keep_points << pt
|
|
||||||
update_meta_data(pt, last_pt)
|
|
||||||
last_pt = pt
|
|
||||||
end
|
end
|
||||||
end
|
@bounds.min_lat = pt.lat if pt.lat < @bounds.min_lat
|
||||||
@points = keep_points
|
@bounds.min_lon = pt.lon if pt.lon < @bounds.min_lon
|
||||||
|
@bounds.max_lat = pt.lat if pt.lat > @bounds.max_lat
|
||||||
|
@bounds.max_lon = pt.lon if pt.lon > @bounds.max_lon
|
||||||
|
|
||||||
|
@distance += haversine_distance(last_pt, pt) unless last_pt.nil?
|
||||||
|
|
||||||
|
@points << pt
|
||||||
|
last_pt = pt
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Returns true if this Segment has no points.
|
# Tack on a point to this Segment. All meta-data will be updated.
|
||||||
def empty?
|
def append_point(pt)
|
||||||
(points.nil? or (points.size == 0))
|
last_pt = @points[-1]
|
||||||
|
@earliest_point = pt if(@earliest_point.nil? or pt.time < @earliest_point.time)
|
||||||
|
@latest_point = pt if(@latest_point.nil? or pt.time > @latest_point.time)
|
||||||
|
@lowest_point = pt if(@lowest_point.nil? or pt.elevation < @lowest_point.elevation)
|
||||||
|
@highest_point = pt if(@highest_point.nil? or pt.elevation > @highest_point.elevation)
|
||||||
|
@bounds.min_lat = pt.lat if pt.lat < @bounds.min_lat
|
||||||
|
@bounds.min_lon = pt.lon if pt.lon < @bounds.min_lon
|
||||||
|
@bounds.max_lat = pt.lat if pt.lat > @bounds.max_lat
|
||||||
|
@bounds.max_lon = pt.lon if pt.lon > @bounds.max_lon
|
||||||
|
@distance += haversine_distance(last_pt, pt) unless last_pt.nil?
|
||||||
|
@points << pt
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns true if the given time is within this Segment.
|
||||||
|
def contains_time?(time)
|
||||||
|
(time >= @earliest_point.time and time <= @latest_point.time) rescue false
|
||||||
|
end
|
||||||
|
|
||||||
|
# Finds the closest point in time to the passed-in time argument. Useful
|
||||||
|
# for matching up time-based objects (photos, video, etc) with a
|
||||||
|
# geographic location.
|
||||||
|
def closest_point(time)
|
||||||
|
find_closest(points, time)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Deletes all points within this Segment that lie outside of the given
|
||||||
|
# area (which should be a Bounds object).
|
||||||
|
def crop(area)
|
||||||
|
delete_if { |pt| not area.contains?(pt) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Deletes all points in this Segment that lie within the given area.
|
||||||
|
def delete_area(area)
|
||||||
|
delete_if{ |pt| area.contains?(pt) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# A handy method that deletes points based on a block that is passed in.
|
||||||
|
# If the passed-in block returns true when given a point, then that point
|
||||||
|
# is deleted. For example:
|
||||||
|
# delete_if{ |pt| area.contains?(pt) }
|
||||||
|
def delete_if
|
||||||
|
reset_meta_data
|
||||||
|
keep_points = []
|
||||||
|
last_pt = nil
|
||||||
|
points.each do |pt|
|
||||||
|
unless yield(pt)
|
||||||
|
keep_points << pt
|
||||||
|
update_meta_data(pt, last_pt)
|
||||||
|
last_pt = pt
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@points = keep_points
|
||||||
|
end
|
||||||
|
|
||||||
# Prints out a nice summary of this Segment.
|
# Returns true if this Segment has no points.
|
||||||
def to_s
|
def empty?
|
||||||
result = "Track Segment\n"
|
(points.nil? or (points.size == 0))
|
||||||
result << "\tSize: #{points.size} points\n"
|
end
|
||||||
result << "\tDistance: #{distance} km\n"
|
|
||||||
result << "\tEarliest Point: #{earliest_point.time.to_s} \n"
|
# Prints out a nice summary of this Segment.
|
||||||
result << "\tLatest Point: #{latest_point.time.to_s} \n"
|
def to_s
|
||||||
result << "\tLowest Point: #{lowest_point.elevation} \n"
|
result = "Track Segment\n"
|
||||||
result << "\tHighest Point: #{highest_point.elevation}\n "
|
result << "\tSize: #{points.size} points\n"
|
||||||
result << "\tBounds: #{bounds.to_s}"
|
result << "\tDistance: #{distance} km\n"
|
||||||
result
|
result << "\tEarliest Point: #{earliest_point.time.to_s} \n"
|
||||||
|
result << "\tLatest Point: #{latest_point.time.to_s} \n"
|
||||||
|
result << "\tLowest Point: #{lowest_point.elevation} \n"
|
||||||
|
result << "\tHighest Point: #{highest_point.elevation}\n "
|
||||||
|
result << "\tBounds: #{bounds.to_s}"
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
def find_closest(pts, time)
|
||||||
|
return pts.first if pts.size == 1
|
||||||
|
midpoint = pts.size/2
|
||||||
|
if pts.size == 2
|
||||||
|
diff_1 = pts[0].time - time
|
||||||
|
diff_2 = pts[1].time - time
|
||||||
|
return (diff_1 < diff_2 ? pts[0] : pts[1])
|
||||||
end
|
end
|
||||||
|
if time >= pts[midpoint].time and time <= pts[midpoint+1].time
|
||||||
|
|
||||||
protected
|
return pts[midpoint]
|
||||||
def find_closest(pts, time)
|
|
||||||
return pts.first if pts.size == 1
|
|
||||||
midpoint = pts.size/2
|
|
||||||
if pts.size == 2
|
|
||||||
diff_1 = pts[0].time - time
|
|
||||||
diff_2 = pts[1].time - time
|
|
||||||
return (diff_1 < diff_2 ? pts[0] : pts[1])
|
|
||||||
end
|
|
||||||
if time >= pts[midpoint].time and time <= pts[midpoint+1].time
|
|
||||||
|
|
||||||
return pts[midpoint]
|
elsif(time <= pts[midpoint].time)
|
||||||
|
return find_closest(pts[0..midpoint], time)
|
||||||
elsif(time <= pts[midpoint].time)
|
else
|
||||||
return find_closest(pts[0..midpoint], time)
|
return find_closest(pts[(midpoint+1)..-1], time)
|
||||||
else
|
|
||||||
return find_closest(pts[(midpoint+1)..-1], time)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
RADIUS = 6371; # earth's mean radius in km
|
RADIUS = 6371; # earth's mean radius in km
|
||||||
|
|
||||||
# Calculate the Haversine distance between two points. This is the method
|
# Calculate the Haversine distance between two points. This is the method
|
||||||
# the library uses to calculate the cumulative distance of GPX files.
|
# the library uses to calculate the cumulative distance of GPX files.
|
||||||
def haversine_distance(p1, p2)
|
def haversine_distance(p1, p2)
|
||||||
d_lat = p2.latr - p1.latr;
|
d_lat = p2.latr - p1.latr;
|
||||||
d_lon = p2.lonr - p1.lonr;
|
d_lon = p2.lonr - p1.lonr;
|
||||||
a = Math.sin(d_lat/2) * Math.sin(d_lat/2) + Math.cos(p1.latr) * Math.cos(p2.latr) * Math.sin(d_lon/2) * Math.sin(d_lon/2);
|
a = Math.sin(d_lat/2) * Math.sin(d_lat/2) + Math.cos(p1.latr) * Math.cos(p2.latr) * Math.sin(d_lon/2) * Math.sin(d_lon/2);
|
||||||
c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
||||||
d = RADIUS * c;
|
d = RADIUS * c;
|
||||||
return d;
|
return d;
|
||||||
|
end
|
||||||
|
|
||||||
|
# Calculate the plain Pythagorean difference between two points. Not currently used.
|
||||||
|
def pythagorean_distance(p1, p2)
|
||||||
|
Math.sqrt((p2.latr - p1.latr)**2 + (p2.lonr - p1.lonr)**2)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Calculates the distance between two points using the Law of Cosines formula. Not currently used.
|
||||||
|
def law_of_cosines_distance(p1, p2)
|
||||||
|
(Math.acos(Math.sin(p1.latr)*Math.sin(p2.latr) + Math.cos(p1.latr)*Math.cos(p2.latr)*Math.cos(p2.lonr-p1.lonr)) * RADIUS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset_meta_data
|
||||||
|
@earliest_point = nil
|
||||||
|
@latest_point = nil
|
||||||
|
@highest_point = nil
|
||||||
|
@lowest_point = nil
|
||||||
|
@distance = 0.0
|
||||||
|
@bounds = Bounds.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_meta_data(pt, last_pt)
|
||||||
|
unless pt.time.nil?
|
||||||
|
@earliest_point = pt if(@earliest_point.nil? or pt.time < @earliest_point.time)
|
||||||
|
@latest_point = pt if(@latest_point.nil? or pt.time > @latest_point.time)
|
||||||
end
|
end
|
||||||
|
unless pt.elevation.nil?
|
||||||
# Calculate the plain Pythagorean difference between two points. Not currently used.
|
@lowest_point = pt if(@lowest_point.nil? or pt.elevation < @lowest_point.elevation)
|
||||||
def pythagorean_distance(p1, p2)
|
@highest_point = pt if(@highest_point.nil? or pt.elevation > @highest_point.elevation)
|
||||||
Math.sqrt((p2.latr - p1.latr)**2 + (p2.lonr - p1.lonr)**2)
|
|
||||||
end
|
end
|
||||||
|
@bounds.add(pt)
|
||||||
|
@distance += haversine_distance(last_pt, pt) unless last_pt.nil?
|
||||||
|
end
|
||||||
|
|
||||||
# Calculates the distance between two points using the Law of Cosines formula. Not currently used.
|
end
|
||||||
def law_of_cosines_distance(p1, p2)
|
|
||||||
(Math.acos(Math.sin(p1.latr)*Math.sin(p2.latr) + Math.cos(p1.latr)*Math.cos(p2.latr)*Math.cos(p2.lonr-p1.lonr)) * RADIUS)
|
|
||||||
end
|
|
||||||
|
|
||||||
def reset_meta_data
|
|
||||||
@earliest_point = nil
|
|
||||||
@latest_point = nil
|
|
||||||
@highest_point = nil
|
|
||||||
@lowest_point = nil
|
|
||||||
@distance = 0.0
|
|
||||||
@bounds = Bounds.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_meta_data(pt, last_pt)
|
|
||||||
unless pt.time.nil?
|
|
||||||
@earliest_point = pt if(@earliest_point.nil? or pt.time < @earliest_point.time)
|
|
||||||
@latest_point = pt if(@latest_point.nil? or pt.time > @latest_point.time)
|
|
||||||
end
|
|
||||||
unless pt.elevation.nil?
|
|
||||||
@lowest_point = pt if(@lowest_point.nil? or pt.elevation < @lowest_point.elevation)
|
|
||||||
@highest_point = pt if(@highest_point.nil? or pt.elevation > @highest_point.elevation)
|
|
||||||
end
|
|
||||||
@bounds.add(pt)
|
|
||||||
@distance += haversine_distance(last_pt, pt) unless last_pt.nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
195
lib/gpx/track.rb
195
lib/gpx/track.rb
|
@ -21,117 +21,116 @@
|
||||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
#++
|
#++
|
||||||
module GPX
|
module GPX
|
||||||
|
# In GPX, a single Track can hold multiple Segments, each of which hold
|
||||||
|
# multiple points (in this library, those points are instances of
|
||||||
|
# TrackPoint). Each instance of this class has its own meta-data, including
|
||||||
|
# low point, high point, and distance. Of course, each track references an
|
||||||
|
# array of the segments that copmrise it, but additionally each track holds
|
||||||
|
# a reference to all of its points as one big array called "points".
|
||||||
|
class Track < Base
|
||||||
|
attr_reader :points, :bounds, :lowest_point, :highest_point, :distance
|
||||||
|
attr_accessor :segments, :name, :gpx_file
|
||||||
|
|
||||||
# In GPX, a single Track can hold multiple Segments, each of which hold
|
# Initialize a track from a XML::Node, or, if no :element option is
|
||||||
# multiple points (in this library, those points are instances of
|
# passed, initialize a blank Track object.
|
||||||
# TrackPoint). Each instance of this class has its own meta-data, including
|
def initialize(opts = {})
|
||||||
# low point, high point, and distance. Of course, each track references an
|
@gpx_file = opts[:gpx_file]
|
||||||
# array of the segments that copmrise it, but additionally each track holds
|
@segments = []
|
||||||
# a reference to all of its points as one big array called "points".
|
@points = []
|
||||||
class Track < Base
|
reset_meta_data
|
||||||
attr_reader :points, :bounds, :lowest_point, :highest_point, :distance
|
if(opts[:element])
|
||||||
attr_accessor :segments, :name, :gpx_file
|
trk_element = opts[:element]
|
||||||
|
@name = (trk_element.at("name").inner_text rescue "")
|
||||||
# Initialize a track from a XML::Node, or, if no :element option is
|
trk_element.search("trkseg").each do |seg_element|
|
||||||
# passed, initialize a blank Track object.
|
seg = Segment.new(:element => seg_element, :track => self, :gpx_file => @gpx_file)
|
||||||
def initialize(opts = {})
|
update_meta_data(seg)
|
||||||
@gpx_file = opts[:gpx_file]
|
@segments << seg
|
||||||
@segments = []
|
end
|
||||||
@points = []
|
|
||||||
reset_meta_data
|
|
||||||
if(opts[:element])
|
|
||||||
trk_element = opts[:element]
|
|
||||||
@name = (trk_element.at("name").inner_text rescue "")
|
|
||||||
trk_element.search("trkseg").each do |seg_element|
|
|
||||||
seg = Segment.new(:element => seg_element, :track => self, :gpx_file => @gpx_file)
|
|
||||||
update_meta_data(seg)
|
|
||||||
@segments << seg
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Append a segment to this track, updating its meta data along the way.
|
# Append a segment to this track, updating its meta data along the way.
|
||||||
def append_segment(seg)
|
def append_segment(seg)
|
||||||
update_meta_data(seg)
|
update_meta_data(seg)
|
||||||
@segments << seg
|
@segments << seg
|
||||||
@points.concat(seg.points) unless seg.nil?
|
@points.concat(seg.points) unless seg.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns true if the given time occurs within any of the segments of this track.
|
||||||
|
def contains_time?(time)
|
||||||
|
segments.each do |seg|
|
||||||
|
return true if seg.contains_time?(time)
|
||||||
end
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
# Returns true if the given time occurs within any of the segments of this track.
|
# Finds the closest point (to "time") within this track. Useful for
|
||||||
def contains_time?(time)
|
# correlating things like pictures, video, and other events, if you are
|
||||||
segments.each do |seg|
|
# working with a timestamp.
|
||||||
return true if seg.contains_time?(time)
|
def closest_point(time)
|
||||||
end
|
segment = segments.select { |s| s.contains_time?(time) }
|
||||||
return false
|
segment.first
|
||||||
|
end
|
||||||
|
|
||||||
|
# Removes all points outside of a given area and updates the meta data.
|
||||||
|
# The "area" paremeter is usually a Bounds object.
|
||||||
|
def crop(area)
|
||||||
|
reset_meta_data
|
||||||
|
segments.each do |seg|
|
||||||
|
seg.crop(area)
|
||||||
|
update_meta_data(seg) unless seg.empty?
|
||||||
end
|
end
|
||||||
|
segments.delete_if { |seg| seg.empty? }
|
||||||
|
end
|
||||||
|
|
||||||
# Finds the closest point (to "time") within this track. Useful for
|
# Deletes all points within a given area and updates the meta data.
|
||||||
# correlating things like pictures, video, and other events, if you are
|
def delete_area(area)
|
||||||
# working with a timestamp.
|
reset_meta_data
|
||||||
def closest_point(time)
|
segments.each do |seg|
|
||||||
segment = segments.select { |s| s.contains_time?(time) }
|
seg.delete_area(area)
|
||||||
segment.first
|
update_meta_data(seg) unless seg.empty?
|
||||||
end
|
end
|
||||||
|
segments.delete_if { |seg| seg.empty? }
|
||||||
|
end
|
||||||
|
|
||||||
# Removes all points outside of a given area and updates the meta data.
|
# Returns true if this track has no points in it. This should return
|
||||||
# The "area" paremeter is usually a Bounds object.
|
# true even when the track has empty segments.
|
||||||
def crop(area)
|
def empty?
|
||||||
reset_meta_data
|
(points.nil? or points.size.zero?)
|
||||||
segments.each do |seg|
|
end
|
||||||
seg.crop(area)
|
|
||||||
update_meta_data(seg) unless seg.empty?
|
|
||||||
end
|
|
||||||
segments.delete_if { |seg| seg.empty? }
|
|
||||||
end
|
|
||||||
|
|
||||||
# Deletes all points within a given area and updates the meta data.
|
# Prints out a friendly summary of this track (sans points). Useful for
|
||||||
def delete_area(area)
|
# debugging and sanity checks.
|
||||||
reset_meta_data
|
|
||||||
segments.each do |seg|
|
|
||||||
seg.delete_area(area)
|
|
||||||
update_meta_data(seg) unless seg.empty?
|
|
||||||
end
|
|
||||||
segments.delete_if { |seg| seg.empty? }
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns true if this track has no points in it. This should return
|
def to_s
|
||||||
# true even when the track has empty segments.
|
result = "Track \n"
|
||||||
def empty?
|
result << "\tName: #{name}\n"
|
||||||
(points.nil? or points.size.zero?)
|
result << "\tSize: #{points.size} points\n"
|
||||||
end
|
result << "\tSegments: #{segments.size} \n"
|
||||||
|
result << "\tDistance: #{distance} km\n"
|
||||||
|
result << "\tLowest Point: #{lowest_point.elevation} \n"
|
||||||
|
result << "\tHighest Point: #{highest_point.elevation}\n "
|
||||||
|
result << "\tBounds: #{bounds.to_s}"
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
# Prints out a friendly summary of this track (sans points). Useful for
|
protected
|
||||||
# debugging and sanity checks.
|
|
||||||
|
|
||||||
def to_s
|
def update_meta_data(seg)
|
||||||
result = "Track \n"
|
@lowest_point = seg.lowest_point if(@lowest_point.nil? or seg.lowest_point.elevation < @lowest_point.elevation)
|
||||||
result << "\tName: #{name}\n"
|
@highest_point = seg.highest_point if(@highest_point.nil? or seg.highest_point.elevation > @highest_point.elevation)
|
||||||
result << "\tSize: #{points.size} points\n"
|
@bounds.add(seg.bounds)
|
||||||
result << "\tSegments: #{segments.size} \n"
|
@distance += seg.distance
|
||||||
result << "\tDistance: #{distance} km\n"
|
@points.concat(seg.points)
|
||||||
result << "\tLowest Point: #{lowest_point.elevation} \n"
|
end
|
||||||
result << "\tHighest Point: #{highest_point.elevation}\n "
|
|
||||||
result << "\tBounds: #{bounds.to_s}"
|
|
||||||
result
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
def reset_meta_data
|
||||||
|
@bounds = Bounds.new
|
||||||
|
@highest_point = nil
|
||||||
|
@lowest_point = nil
|
||||||
|
@distance = 0.0
|
||||||
|
@points = []
|
||||||
|
end
|
||||||
|
|
||||||
def update_meta_data(seg)
|
end
|
||||||
@lowest_point = seg.lowest_point if(@lowest_point.nil? or seg.lowest_point.elevation < @lowest_point.elevation)
|
|
||||||
@highest_point = seg.highest_point if(@highest_point.nil? or seg.highest_point.elevation > @highest_point.elevation)
|
|
||||||
@bounds.add(seg.bounds)
|
|
||||||
@distance += seg.distance
|
|
||||||
@points.concat(seg.points)
|
|
||||||
end
|
|
||||||
|
|
||||||
def reset_meta_data
|
|
||||||
@bounds = Bounds.new
|
|
||||||
@highest_point = nil
|
|
||||||
@lowest_point = nil
|
|
||||||
@distance = 0.0
|
|
||||||
@points = []
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,15 +21,15 @@
|
||||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
#++
|
#++
|
||||||
module GPX
|
module GPX
|
||||||
# Basically the same as a point, the TrackPoint class is supposed to
|
# Basically the same as a point, the TrackPoint class is supposed to
|
||||||
# represent the points that are children of Segment elements. So, the only
|
# represent the points that are children of Segment elements. So, the only
|
||||||
# real difference is that TrackPoints hold a reference to their parent
|
# real difference is that TrackPoints hold a reference to their parent
|
||||||
# Segments.
|
# Segments.
|
||||||
class TrackPoint < Point
|
class TrackPoint < Point
|
||||||
attr_accessor :segment
|
attr_accessor :segment
|
||||||
def initialize(opts = {})
|
def initialize(opts = {})
|
||||||
super(opts)
|
super(opts)
|
||||||
@segment = opts[:segment]
|
@segment = opts[:segment]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,9 +20,7 @@
|
||||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
#++
|
#++
|
||||||
|
|
||||||
module GPX
|
module GPX
|
||||||
|
|
||||||
# This class supports the concept of a waypoint. Beware that this class has
|
# This class supports the concept of a waypoint. Beware that this class has
|
||||||
# not seen much use yet, since WalkingBoss does not use waypoints right now.
|
# not seen much use yet, since WalkingBoss does not use waypoints right now.
|
||||||
class Waypoint < Point
|
class Waypoint < Point
|
||||||
|
|
Loading…
Reference in New Issue