From 022c7c68132af12aec2598ff17fddc9989aed382 Mon Sep 17 00:00:00 2001 From: Doug Fales Date: Mon, 4 Dec 2006 06:47:41 +0000 Subject: [PATCH] First stab at using libxml-ruby instead of REXML. I'm seeing the unit tests finish in under 14 seconds. That is compared to 2 minutes using REXML. --- README | 2 +- lib/gpx.rb | 3 +- lib/gpx/bounds.rb | 10 +++---- lib/gpx/gpx.rb | 9 +++--- lib/gpx/gpx_file.rb | 66 +++++++++++++++++++++++-------------------- lib/gpx/point.rb | 28 +++++++++--------- lib/gpx/route.rb | 18 ++++++------ lib/gpx/segment.rb | 12 ++++---- lib/gpx/track.rb | 18 ++++++------ lib/gpx/waypoint.rb | 24 ++++++++-------- tests/segment_test.rb | 1 + 11 files changed, 99 insertions(+), 92 deletions(-) diff --git a/README b/README index 443be5b..43b5439 100644 --- a/README +++ b/README @@ -33,7 +33,7 @@ work-in-progress than an attempt at full GPX compliance. The track side of the library has seen much more use than the route/waypoint side, so if you're doing something with routes or waypoints, you may need to tweak some things. -Since this code uses REXML to read an entire GPX file into memory, it is not +Since this code uses XML to read an entire GPX file into memory, it is not the fastest possible solution for working with GPX data, especially if you are working with tracks from several days or weeks. diff --git a/lib/gpx.rb b/lib/gpx.rb index eaa6489..c7c7f03 100644 --- a/lib/gpx.rb +++ b/lib/gpx.rb @@ -21,7 +21,8 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ $:.unshift(File.dirname(__FILE__)) -require 'rexml/document' +require 'rubygems' +require 'xml/libxml' require 'date' require 'time' require 'csv' diff --git a/lib/gpx/bounds.rb b/lib/gpx/bounds.rb index 33bf8a4..49150bb 100644 --- a/lib/gpx/bounds.rb +++ b/lib/gpx/bounds.rb @@ -44,11 +44,11 @@ module GPX end def to_xml - bnd = REXML::Element.new('bounds') - bnd.attributes['minlat'] = min_lat - bnd.attributes['minlon'] = min_lon - bnd.attributes['maxlat'] = max_lat - bnd.attributes['maxlon'] = max_lon + bnd = XML::Node.new('bounds') + bnd['minlat'] = min_lat.to_s + bnd['minlon'] = min_lon.to_s + bnd['maxlat'] = max_lat.to_s + bnd['maxlon'] = max_lon.to_s bnd end diff --git a/lib/gpx/gpx.rb b/lib/gpx/gpx.rb index 9ae5ffd..b2aa58e 100644 --- a/lib/gpx/gpx.rb +++ b/lib/gpx/gpx.rb @@ -26,9 +26,10 @@ module GPX # A common base class which provides a useful initializer method to many # class in the GPX library. class Base - include REXML + include XML - # This initializer can take a REXML::Element and scrape out any text +NS = 'gpx:http://www.topografix.com/GPX/1/1' + # 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 @@ -37,8 +38,8 @@ module GPX # attributes to this method. def instantiate_with_text_elements(parent, text_elements) text_elements.each do |el| - unless parent.elements[el].nil? - val = parent.elements[el].text + unless parent.find(el).empty? + val = parent.find(el).first.content code = <<-code attr_accessor #{ el } #{el}=#{val} diff --git a/lib/gpx/gpx_file.rb b/lib/gpx/gpx_file.rb index 983174e..8fdf278 100644 --- a/lib/gpx/gpx_file.rb +++ b/lib/gpx/gpx_file.rb @@ -44,14 +44,15 @@ module GPX @duration = 0 if(opts[:gpx_file]) gpx_file = opts[:gpx_file] - case gpx_file - when String - gpx_file = File.open(gpx_file) - end + #case gpx_file + #when String + # gpx_file = File.open(gpx_file) + #end + gpx_file = gpx_file.name if gpx_file.is_a?(File) reset_meta_data - @xml = Document.new(gpx_file, :ignore_whitespace_nodes => :all) + @xml = Document.file(gpx_file) - bounds_element = (XPath.match(@xml, "/gpx/metadata/bounds").first rescue nil) + bounds_element = (@xml.find("//gpx:gpx/gpx:metadata/gpx:bounds", NS).to_a.first 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}) @@ -61,13 +62,16 @@ module GPX get_bounds = true end - @tracks = XPath.match(@xml, "/gpx/trk").collect do |trk| + @tracks = [] + @xml.find("//gpx:gpx/gpx:trk", NS).each do |trk| trk = Track.new(:element => trk, :gpx_file => self) update_meta_data(trk, get_bounds) - trk + @tracks << trk end - @waypoints = XPath.match(@xml, "/gpx/wpt").collect { |wpt| Waypoint.new(:element => wpt, :gpx_file => self) } - @routes = XPath.match(@xml, "/gpx/rte").collect { |rte| Route.new(:element => rte, :gpx_file => self) } + @waypoints = [] + @xml.find("//gpx:gpx/gpx:wpt", NS).each { |wpt| @waypoints << Waypoint.new(:element => wpt, :gpx_file => self) } + @routes = [] + @xml.find("//gpx:gpx/gpx:rte", NS).each { |rte| @routes << Route.new(:element => rte, :gpx_file => self) } @tracks.delete_if { |t| t.empty? } @@ -85,7 +89,7 @@ module GPX def get_bounds_attr_value(el, possible_names) result = nil possible_names.each do |name| - result = el.attributes[name] + result = el[name] break unless result.nil? end return (result.to_f rescue nil) @@ -184,32 +188,32 @@ module GPX def write(filename) doc = Document.new - gpx_elem = Element.new('gpx') - doc.add(gpx_elem) - gpx_elem.attributes['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance" - gpx_elem.attributes['xmlns'] = "http://www.topografix.com/GPX/1/1" - gpx_elem.attributes['version'] = "1.1" - gpx_elem.attributes['creator'] = "GPX RubyGem 0.1 Copyright 2006 Doug Fales -- http://walkingboss.com" - gpx_elem.attributes['xsi:schemaLocation'] = "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" + doc.root = Node.new('gpx') + gpx_elem = doc.root + gpx_elem['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance" + gpx_elem['xmlns'] = "http://www.topografix.com/GPX/1/1" + gpx_elem['version'] = "1.1" + gpx_elem['creator'] = "GPX RubyGem 0.1 Copyright 2006 Doug Fales -- http://walkingboss.com" + gpx_elem['xsi:schemaLocation'] = "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" - meta_data_elem = Element.new('metadata') - name_elem = Element.new('name') - name_elem.text = File.basename(filename) - meta_data_elem.elements << name_elem + meta_data_elem = Node.new('metadata') + name_elem = Node.new('name') + name_elem << File.basename(filename) + meta_data_elem << name_elem - time_elem = Element.new('time') - time_elem.text = Time.now.xmlschema - meta_data_elem.elements << time_elem + time_elem = Node.new('time') + time_elem << Time.now.xmlschema + meta_data_elem << time_elem - meta_data_elem.elements << bounds.to_xml + meta_data_elem << bounds.to_xml - gpx_elem.elements << meta_data_elem + gpx_elem << meta_data_elem - tracks.each { |t| gpx_elem.add_element t.to_xml } unless tracks.nil? - waypoints.each { |w| gpx_elem.add_element w.to_xml } unless waypoints.nil? - routes.each { |r| gpx_elem.add_element r.to_xml } unless routes.nil? + tracks.each { |t| gpx_elem << t.to_xml } unless tracks.nil? + waypoints.each { |w| gpx_elem << w.to_xml } unless waypoints.nil? + routes.each { |r| gpx_elem << r.to_xml } unless routes.nil? - File.open(filename, 'w') { |f| doc.write(f) } + doc.save(filename, true) end private diff --git a/lib/gpx/point.rb b/lib/gpx/point.rb index e697bf5..1343306 100644 --- a/lib/gpx/point.rb +++ b/lib/gpx/point.rb @@ -29,16 +29,16 @@ module GPX # 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 a REXML element to this initializer, and the + # 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 } ) if (opts[:element]) elem = opts[:element] - @lat, @lon = elem.attributes["lat"].to_f, elem.attributes["lon"].to_f + @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.elements["time"].text) rescue nil) - @elevation = elem.elements["ele"].text.to_f if elem.elements["ele"] + @time = (Time.xmlschema(elem.find("gpx:time", NS).first.content) rescue nil) + @elevation = elem.find("gpx:ele", NS).first.content.to_f unless elem.find("gpx:ele", NS).empty? else @lat = opts[:lat] @lon = opts[:lon] @@ -85,19 +85,19 @@ module GPX @lon = longitude end - # Convert this point to a REXML::Element. + # Convert this point to a XML::Node. def to_xml(elem_name = 'trkpt') - pt = Element.new('trkpt') - pt.attributes['lat'] = lat - pt.attributes['lon'] = lon + pt = Node.new('trkpt') + pt['lat'] = lat.to_s + pt['lon'] = lon.to_s unless time.nil? - time_elem = Element.new('time') - time_elem.text = time.xmlschema - pt.elements << time_elem + time_elem = Node.new('time') + time_elem << time.xmlschema + pt << time_elem end - elev = Element.new('ele') - elev.text = elevation - pt.elements << elev + elev = Node.new('ele') + elev << elevation + pt << elev pt end diff --git a/lib/gpx/route.rb b/lib/gpx/route.rb index f3c1717..0928ba0 100644 --- a/lib/gpx/route.rb +++ b/lib/gpx/route.rb @@ -29,13 +29,13 @@ module GPX attr_reader :points, :name, :gpx_file - # Initialize a Route from a REXML::Element. + # Initialize a Route from a XML::Node. def initialize(opts = {}) rte_element = opts[:element] @gpx_file = opts[:gpx_file] - @name = rte_element.elements["child::name"].text + @name = rte_element.find("child::gpx:name", NS).first.content @points = [] - XPath.each(rte_element, "child::rtept") do |point| + rte_element.find("child::gpx:rtept", NS).each do |point| @points << Point.new(:element => point) end @@ -51,13 +51,13 @@ module GPX points.delete_if{ |pt| area.contains? pt } end - # Convert this Route to a REXML::Element. + # Convert this Route to a XML::Node. def to_xml - rte = Element.new('rte') - name_elem = Element.new('name') - name_elem.text = name - rte.elements << name_elem - points.each { |rte_pt| rte.elements << rte_pt.to_xml('rtept') } + rte = Node.new('rte') + name_elem = Node.new('name') + name_elem << name + rte << name_elem + points.each { |rte_pt| rte << rte_pt.to_xml('rtept') } rte end diff --git a/lib/gpx/segment.rb b/lib/gpx/segment.rb index ed83056..8b4dc9b 100644 --- a/lib/gpx/segment.rb +++ b/lib/gpx/segment.rb @@ -31,7 +31,7 @@ module GPX attr_reader :earliest_point, :latest_point, :bounds, :highest_point, :lowest_point, :distance attr_accessor :points, :track - # If a REXML::Element object is passed-in, this will initialize a new + # 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 = {}) @track = opts[:track] @@ -45,8 +45,8 @@ module GPX if(opts[:element]) segment_element = opts[:element] last_pt = nil - unless segment_element.is_a?(Text) - XPath.each(segment_element, "child::trkpt") do |trkpt| + if segment_element.is_a?(XML::Node) + segment_element.find("child::gpx:trkpt", NS).each do |trkpt| pt = TrackPoint.new(:element => trkpt, :segment => self) unless pt.time.nil? @earliest_point = pt if(@earliest_point.nil? or pt.time < @earliest_point.time) @@ -131,10 +131,10 @@ module GPX (points.nil? or (points.size == 0)) end - # Converts this Segment to a REXML::Element object. + # Converts this Segment to a XML::Node object. def to_xml - seg = Element.new('trkseg') - points.each { |pt| seg.elements << pt.to_xml } + seg = Node.new('trkseg') + points.each { |pt| seg << pt.to_xml } seg end diff --git a/lib/gpx/track.rb b/lib/gpx/track.rb index 8ca11d9..14add8e 100644 --- a/lib/gpx/track.rb +++ b/lib/gpx/track.rb @@ -32,7 +32,7 @@ module GPX attr_reader :points, :bounds, :lowest_point, :highest_point, :distance attr_accessor :segments, :name, :gpx_file - # Initialize a track from a REXML::Element, or, if no :element option is + # 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] @@ -41,8 +41,8 @@ module GPX reset_meta_data if(opts[:element]) trk_element = opts[:element] - @name = (trk_element.elements["child::name"].text rescue "") - XPath.each(trk_element, "child::trkseg") do |seg_element| + @name = (trk_element.find("child::gpx:name", NS).first.content rescue "") + trk_element.find("child::gpx:trkseg", NS).each do |seg_element| seg = Segment.new(:element => seg_element, :track => self) update_meta_data(seg) @segments << seg @@ -101,14 +101,14 @@ module GPX (points.nil? or points.size.zero?) end - # Creates a new REXML::Element from the contents of this instance. + # Creates a new XML::Node from the contents of this instance. def to_xml - trk= Element.new('trk') - name_elem = Element.new('name') - name_elem.text = name - trk.elements << name_elem + trk= Node.new('trk') + name_elem = Node.new('name') + name_elem << name + trk << name_elem segments.each do |seg| - trk.elements << seg.to_xml + trk << seg.to_xml end trk end diff --git a/lib/gpx/waypoint.rb b/lib/gpx/waypoint.rb index 838cc11..388ebb2 100644 --- a/lib/gpx/waypoint.rb +++ b/lib/gpx/waypoint.rb @@ -39,7 +39,7 @@ module GPX def delete_area(area) end - # Initializes a waypoint from a REXML::Element. + # Initializes a waypoint from a XML::Node. def initialize(opts = {}) wpt_elem = opts[:element] super(:element => wpt_elem) @@ -47,25 +47,25 @@ module GPX @gpx_file = opts[:gpx_file] end - # Converts a waypoint to a REXML::Element. + # Converts a waypoint to a XML::Node. def to_xml - wpt = Element.new('wpt') + wpt = Node.new('wpt') wpt.attributes['lat'] = lat wpt.attributes['lon'] = lon if self.respond_to? :name - name_elem = Element.new('name') - name_elem.text = self.name - wpt.elements << name_elem + name_elem = Node.new('name') + name_elem << self.name + wpt << name_elem end if self.respond_to? :sym - sym_elem = Element.new('sym') - sym_elem.text = self.sym - wpt.elements << sym_elem + sym_elem = Node.new('sym') + sym_elem << self.sym + wpt << sym_elem end if self.respond_to? :ele - elev_elem = Element.new('ele') - elev_elem.text = self.ele - wpt.elements << elev_elem + elev_elem = Node.new('ele') + elev_elem << self.ele + wpt << elev_elem end wpt end diff --git a/tests/segment_test.rb b/tests/segment_test.rb index e892832..a5aaf01 100644 --- a/tests/segment_test.rb +++ b/tests/segment_test.rb @@ -1,4 +1,5 @@ require 'test/unit' +require 'yaml' require File.dirname(__FILE__) + '/../lib/gpx' class TestSegment < Test::Unit::TestCase