From 19a5ab6104906133a31f38bee317a20fabee1287 Mon Sep 17 00:00:00 2001 From: Guillaume DOTT Date: Sun, 16 Jun 2013 23:27:05 +0200 Subject: [PATCH] REINDENT ALL THE FILES Use 2 spaces for indentation and remove some tabs. --- lib/gpx/bounds.rb | 92 +++--- lib/gpx/gpx.rb | 33 ++- lib/gpx/gpx_file.rb | 519 +++++++++++++++++----------------- lib/gpx/magellan_track_log.rb | 177 ++++++------ lib/gpx/point.rb | 115 ++++---- lib/gpx/route.rb | 59 ++-- lib/gpx/segment.rb | 345 +++++++++++----------- lib/gpx/track.rb | 195 +++++++------ lib/gpx/trackpoint.rb | 22 +- lib/gpx/waypoint.rb | 2 - 10 files changed, 772 insertions(+), 787 deletions(-) diff --git a/lib/gpx/bounds.rb b/lib/gpx/bounds.rb index 2b483f9..b85537d 100644 --- a/lib/gpx/bounds.rb +++ b/lib/gpx/bounds.rb @@ -21,54 +21,54 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ module GPX - class Bounds < Base - attr_accessor :min_lat, :max_lat, :max_lon, :min_lon, :center_lat, :center_lon + class Bounds < Base + 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 - # and latitudes. - 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_lon, @max_lon = opts[:min_lon].to_f, opts[:max_lon].to_f + # Creates a new bounds object with the passed-in min and max longitudes + # and latitudes. + 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_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 - # Returns the middle latitude. - def center_lat - distance = (max_lat - min_lat)/2.0 - (min_lat + distance) - 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 - # 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 - - # 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 diff --git a/lib/gpx/gpx.rb b/lib/gpx/gpx.rb index 9e7438f..6fa6a61 100644 --- a/lib/gpx/gpx.rb +++ b/lib/gpx/gpx.rb @@ -24,24 +24,23 @@ module GPX # A common base class which provides a useful initializer method to many # class in the GPX library. 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 - # 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 diff --git a/lib/gpx/gpx_file.rb b/lib/gpx/gpx_file.rb index a8b4ecc..5fa6306 100644 --- a/lib/gpx/gpx_file.rb +++ b/lib/gpx/gpx_file.rb @@ -21,271 +21,268 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ module GPX - class GPXFile < Base - attr_accessor :tracks, :routes, :waypoints, :bounds, :lowest_point, :highest_point, :duration, :ns, :time, :name + class GPXFile < Base + 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 - # 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). - # To read an existing GPX file, do this: - # gpx_file = GPXFile.new(:gpx_file => 'mygpxfile.gpx') - # puts "Speed: #{gpx_file.average_speed}" - # puts "Duration: #{gpx_file.duration}" - # puts "Bounds: #{gpx_file.bounds}" - # - # To read a GPX file from a string, use :gpx_data. - # gpx_file = GPXFile.new(:gpx_data => '...) - # To create a new blank GPXFile instance: - # gpx_file = GPXFile.new - # 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') - # gpx_file = GPXFile.new(:tracks => [some_track]) - # - def initialize(opts = {}) - @duration = 0 - if(opts[:gpx_file] or opts[:gpx_data]) - if opts[:gpx_file] - gpx_file = opts[:gpx_file] - gpx_file = File.open(gpx_file) unless gpx_file.is_a?(File) - @xml = Nokogiri::XML(gpx_file) - else - @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 . - # 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? - } + # 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 + # add tracks and points and write it out to a new file later). + # To read an existing GPX file, do this: + # gpx_file = GPXFile.new(:gpx_file => 'mygpxfile.gpx') + # puts "Speed: #{gpx_file.average_speed}" + # puts "Duration: #{gpx_file.duration}" + # puts "Bounds: #{gpx_file.bounds}" + # + # To read a GPX file from a string, use :gpx_data. + # gpx_file = GPXFile.new(:gpx_data => '...) + # To create a new blank GPXFile instance: + # gpx_file = GPXFile.new + # 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') + # gpx_file = GPXFile.new(:tracks => [some_track]) + # + def initialize(opts = {}) + @duration = 0 + if(opts[:gpx_file] or opts[:gpx_data]) + if opts[:gpx_file] + gpx_file = opts[:gpx_file] + gpx_file = File.open(gpx_file) unless gpx_file.is_a?(File) + @xml = Nokogiri::XML(gpx_file) + else + @xml = Nokogiri::XML(opts[:gpx_data]) 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 . + # 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 - # Calculates and sets the duration attribute by subtracting the time on - # the very first point from the time on the very last point. - def calculate_duration - @duration = 0 - if(@tracks.nil? or @tracks.size.zero? or @tracks[0].segments.nil? or @tracks[0].segments.size.zero?) - return @duration - end - @duration = (@tracks[-1].segments[-1].points[-1].time - @tracks.first.segments.first.points.first.time) - rescue - @duration = 0 + return doc + end + + # Calculates and sets the duration attribute by subtracting the time on + # the very first point from the time on the very last point. + def calculate_duration + @duration = 0 + if(@tracks.nil? or @tracks.size.zero? or @tracks[0].segments.nil? or @tracks[0].segments.size.zero?) + return @duration end - - - end + @duration = (@tracks[-1].segments[-1].points[-1].time - @tracks.first.segments.first.points.first.time) + rescue + @duration = 0 + end + end end diff --git a/lib/gpx/magellan_track_log.rb b/lib/gpx/magellan_track_log.rb index b14c1b5..7daf7e2 100644 --- a/lib/gpx/magellan_track_log.rb +++ b/lib/gpx/magellan_track_log.rb @@ -20,118 +20,113 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ - -require 'csv' - 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, - # which is a NMEA formatted CSV list of points. + #NOTE: The Latitude and Longitude Fields are shown as having two decimal + # 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 - # 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. + # $PMGNTRK,llll.ll,a,yyyyy.yy,a,xxxxx,a,hhmmss.ss,A,c----c,ddmmyy*hh + require 'csv' - #NOTE: The Latitude and Longitude Fields are shown as having two decimal - # places. As many additional decimal places may be added as long as the total - # length of the message does not exceed 82 bytes. + LAT = 1 + LAT_HEMI = 2 + 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 - require 'csv' + FEET_TO_METERS = 0.3048 - LAT = 1 - LAT_HEMI = 2 - LON = 3 - LON_HEMI = 4 - ELE = 5 - ELE_UNITS = 6 - TIME = 7 - INVALID_FLAG = 8 - DATE = 10 + class << self - 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 - # writes the result to gpx_filename. - def convert_to_gpx(magellan_filename, gpx_filename) + CSV.open(magellan_filename, "r").each do |row| + next if(row.size < 10 or row[INVALID_FLAG] == 'V') - 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| - next if(row.size < 10 or row[INVALID_FLAG] == 'V') + lat = lat_deg.to_f + (lat_min.to_f / 60.0) + lat = (-lat) if(lat_hemi == 'S') - lat_deg = row[LAT][0..1] - lat_min = row[LAT][2...-1] - lat_hemi = row[LAT_HEMI] + lon_deg = row[LON][0..2] + lon_min = row[LON][3..-1] + lon_hemi = row[LON_HEMI] - lat = lat_deg.to_f + (lat_min.to_f / 60.0) - lat = (-lat) if(lat_hemi == 'S') - - 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') + lon = lon_deg.to_f + (lon_min.to_f / 60.0) + lon = (-lon) if(lon_hemi == 'W') - ele = row[ELE] - ele_units = row[ELE_UNITS] - ele = ele.to_f - if(ele_units == 'F') - ele *= FEET_TO_METERS - end + ele = row[ELE] + ele_units = row[ELE_UNITS] + ele = ele.to_f + if(ele_units == 'F') + ele *= FEET_TO_METERS + end - hrs = row[TIME][0..1].to_i - mins = row[TIME][2..3].to_i - secs = row[TIME][4..5].to_i - day = row[DATE][0..1].to_i - mon = row[DATE][2..3].to_i - yr = 2000 + row[DATE][4..5].to_i + hrs = row[TIME][0..1].to_i + mins = row[TIME][2..3].to_i + secs = row[TIME][4..5].to_i + day = row[DATE][0..1].to_i + mon = row[DATE][2..3].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 - pt = TrackPoint.new(:lat => lat, :lon => lon, :time => time, :elevation => ele) - segment.append_point(pt) + #must create point + pt = TrackPoint.new(:lat => lat, :lon => lon, :time => time, :elevation => ele) + segment.append_point(pt) - end + end - trk = Track.new - trk.append_segment(segment) - gpx_file = GPXFile.new(:tracks => [trk]) - gpx_file.write(gpx_filename) + trk = Track.new + trk.append_segment(segment) + gpx_file = GPXFile.new(:tracks => [trk]) + 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 + # 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 diff --git a/lib/gpx/point.rb b/lib/gpx/point.rb index e42076e..adc6225 100644 --- a/lib/gpx/point.rb +++ b/lib/gpx/point.rb @@ -20,72 +20,71 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ -include Math module GPX - # The base class for all points. Trackpoint and Waypoint both descend from this base class. - class Point < Base - D_TO_R = PI/180.0; - 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 + # The base class for all points. Trackpoint and Waypoint both descend from this base class. + class Point < Base + D_TO_R = Math::PI/180.0; + 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 + 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 - # given delimeter. This is useful for passing a point into another API - # (i.e. the Google Maps javascript API). - def lon_lat(delim = ', ') - "#{lon}#{delim}#{lat}" - 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 - # Latitude in radians. - def latr - @latr ||= (@lat * D_TO_R) - end + # Returns the longitude and latitude (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 lon_lat(delim = ', ') + "#{lon}#{delim}#{lat}" + end - # Longitude in radians. - def lonr - @lonr ||= (@lon * D_TO_R) - end + # Latitude in radians. + def latr + @latr ||= (@lat * D_TO_R) + end - # Set the latitude (in degrees). - def lat=(latitude) - @latr = (latitude * D_TO_R) - @lat = latitude - end + # Longitude in radians. + def lonr + @lonr ||= (@lon * D_TO_R) + end - # Set the longitude (in degrees). - def lon=(longitude) - @lonr = (longitude * D_TO_R) - @lon = longitude - end - end + # Set the latitude (in degrees). + def lat=(latitude) + @latr = (latitude * D_TO_R) + @lat = latitude + end + + # Set the longitude (in degrees). + def lon=(longitude) + @lonr = (longitude * D_TO_R) + @lon = longitude + end + end end diff --git a/lib/gpx/route.rb b/lib/gpx/route.rb index 9e69ec8..9878154 100644 --- a/lib/gpx/route.rb +++ b/lib/gpx/route.rb @@ -21,39 +21,38 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ 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 - # 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 + 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 - # Delete points outside of a given area. - def crop(area) - points.delete_if{ |pt| not area.contains? pt } - end + end - # Delete points within the given area. - def delete_area(area) - points.delete_if{ |pt| area.contains? pt } - end - end + # Delete points outside of a given area. + def crop(area) + points.delete_if{ |pt| not area.contains? pt } + end + + # Delete points within the given area. + def delete_area(area) + points.delete_if{ |pt| area.contains? pt } + end + end end diff --git a/lib/gpx/segment.rb b/lib/gpx/segment.rb index 3159ac2..a474bf0 100644 --- a/lib/gpx/segment.rb +++ b/lib/gpx/segment.rb @@ -21,195 +21,194 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ 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 - # (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 + attr_reader :earliest_point, :latest_point, :bounds, :highest_point, :lowest_point, :distance + attr_accessor :points, :track - attr_reader :earliest_point, :latest_point, :bounds, :highest_point, :lowest_point, :distance - attr_accessor :points, :track - - # If a XML::Node object is passed-in, this will initialize a new - # Segment based on its contents. Otherwise, a blank Segment is created. - def initialize(opts = {}) - @gpx_file = opts[:gpx_file] - @track = opts[:track] - @points = [] - @earliest_point = nil - @latest_point = nil - @highest_point = nil - @lowest_point = nil - @distance = 0.0 - @bounds = Bounds.new - if(opts[:element]) - segment_element = opts[:element] - last_pt = nil - if segment_element.is_a?(Nokogiri::XML::Node) - segment_element.search("trkpt").each do |trkpt| - pt = TrackPoint.new(:element => trkpt, :segment => self, :gpx_file => @gpx_file) - 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 + # If a XML::Node object is passed-in, this will initialize a new + # Segment based on its contents. Otherwise, a blank Segment is created. + def initialize(opts = {}) + @gpx_file = opts[:gpx_file] + @track = opts[:track] + @points = [] + @earliest_point = nil + @latest_point = nil + @highest_point = nil + @lowest_point = nil + @distance = 0.0 + @bounds = Bounds.new + if(opts[:element]) + segment_element = opts[:element] + last_pt = nil + if segment_element.is_a?(Nokogiri::XML::Node) + segment_element.search("trkpt").each do |trkpt| + pt = TrackPoint.new(:element => trkpt, :segment => self, :gpx_file => @gpx_file) + 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 - end - - # 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 + 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 - end - @points = keep_points + @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 - # Returns true if this Segment has no points. - def empty? - (points.nil? or (points.size == 0)) + # 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 + @points = keep_points + end - # Prints out a nice summary of this Segment. - def to_s - result = "Track Segment\n" - result << "\tSize: #{points.size} points\n" - result << "\tDistance: #{distance} km\n" - 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 + # Returns true if this Segment has no points. + def empty? + (points.nil? or (points.size == 0)) + end + + # Prints out a nice summary of this Segment. + def to_s + result = "Track Segment\n" + result << "\tSize: #{points.size} points\n" + result << "\tDistance: #{distance} km\n" + 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 + if time >= pts[midpoint].time and time <= pts[midpoint+1].time - 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 - if time >= pts[midpoint].time and time <= pts[midpoint+1].time + return pts[midpoint] - return pts[midpoint] - - elsif(time <= pts[midpoint].time) - return find_closest(pts[0..midpoint], time) - else - return find_closest(pts[(midpoint+1)..-1], time) - end + elsif(time <= pts[midpoint].time) + return find_closest(pts[0..midpoint], time) + else + return find_closest(pts[(midpoint+1)..-1], time) 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 - # the library uses to calculate the cumulative distance of GPX files. - def haversine_distance(p1, p2) - d_lat = p2.latr - p1.latr; - 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); - c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); - d = RADIUS * c; - return d; + # Calculate the Haversine distance between two points. This is the method + # the library uses to calculate the cumulative distance of GPX files. + def haversine_distance(p1, p2) + d_lat = p2.latr - p1.latr; + 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); + c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + d = RADIUS * c; + 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 - - # 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) + 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 - # 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 - 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 diff --git a/lib/gpx/track.rb b/lib/gpx/track.rb index ce473eb..a0a7835 100644 --- a/lib/gpx/track.rb +++ b/lib/gpx/track.rb @@ -21,117 +21,116 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ 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 - # 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 - - # Initialize a track from a XML::Node, or, if no :element option is - # passed, initialize a blank Track object. - def initialize(opts = {}) - @gpx_file = opts[:gpx_file] - @segments = [] - @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 + # Initialize a track from a XML::Node, or, if no :element option is + # passed, initialize a blank Track object. + def initialize(opts = {}) + @gpx_file = opts[:gpx_file] + @segments = [] + @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 - # Append a segment to this track, updating its meta data along the way. - def append_segment(seg) - update_meta_data(seg) - @segments << seg - @points.concat(seg.points) unless seg.nil? + # Append a segment to this track, updating its meta data along the way. + def append_segment(seg) + update_meta_data(seg) + @segments << seg + @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 + return false + 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 - return false + # Finds the closest point (to "time") within this track. Useful for + # correlating things like pictures, video, and other events, if you are + # working with a timestamp. + def closest_point(time) + segment = segments.select { |s| s.contains_time?(time) } + 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 + segments.delete_if { |seg| seg.empty? } + end - # Finds the closest point (to "time") within this track. Useful for - # correlating things like pictures, video, and other events, if you are - # working with a timestamp. - def closest_point(time) - segment = segments.select { |s| s.contains_time?(time) } - segment.first + # Deletes all points within a given area and updates the meta data. + def delete_area(area) + 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 - # 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 - segments.delete_if { |seg| seg.empty? } - end + # Returns true if this track has no points in it. This should return + # true even when the track has empty segments. + def empty? + (points.nil? or points.size.zero?) + end - # Deletes all points within a given area and updates the meta data. - def delete_area(area) - 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 + # Prints out a friendly summary of this track (sans points). Useful for + # debugging and sanity checks. - # Returns true if this track has no points in it. This should return - # true even when the track has empty segments. - def empty? - (points.nil? or points.size.zero?) - end + def to_s + result = "Track \n" + result << "\tName: #{name}\n" + result << "\tSize: #{points.size} points\n" + 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 - # debugging and sanity checks. + protected - def to_s - result = "Track \n" - result << "\tName: #{name}\n" - result << "\tSize: #{points.size} points\n" - 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 + def update_meta_data(seg) + @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 - protected + def reset_meta_data + @bounds = Bounds.new + @highest_point = nil + @lowest_point = nil + @distance = 0.0 + @points = [] + end - def update_meta_data(seg) - @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 diff --git a/lib/gpx/trackpoint.rb b/lib/gpx/trackpoint.rb index 9ac033b..1b7edec 100644 --- a/lib/gpx/trackpoint.rb +++ b/lib/gpx/trackpoint.rb @@ -21,15 +21,15 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ module GPX - # Basically the same as a point, the TrackPoint class is supposed to - # represent the points that are children of Segment elements. So, the only - # real difference is that TrackPoints hold a reference to their parent - # Segments. - class TrackPoint < Point - attr_accessor :segment - def initialize(opts = {}) - super(opts) - @segment = opts[:segment] - end - end + # Basically the same as a point, the TrackPoint class is supposed to + # represent the points that are children of Segment elements. So, the only + # real difference is that TrackPoints hold a reference to their parent + # Segments. + class TrackPoint < Point + attr_accessor :segment + def initialize(opts = {}) + super(opts) + @segment = opts[:segment] + end + end end diff --git a/lib/gpx/waypoint.rb b/lib/gpx/waypoint.rb index 9cba358..37804da 100644 --- a/lib/gpx/waypoint.rb +++ b/lib/gpx/waypoint.rb @@ -20,9 +20,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ - module GPX - # 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. class Waypoint < Point