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.
master
Doug Fales 2006-12-04 06:47:41 +00:00
parent fcf2b9e9a9
commit 022c7c6813
11 changed files with 99 additions and 92 deletions

2
README
View File

@ -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 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. 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 the fastest possible solution for working with GPX data, especially if you are
working with tracks from several days or weeks. working with tracks from several days or weeks.

View File

@ -21,7 +21,8 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++ #++
$:.unshift(File.dirname(__FILE__)) $:.unshift(File.dirname(__FILE__))
require 'rexml/document' require 'rubygems'
require 'xml/libxml'
require 'date' require 'date'
require 'time' require 'time'
require 'csv' require 'csv'

View File

@ -44,11 +44,11 @@ module GPX
end end
def to_xml def to_xml
bnd = REXML::Element.new('bounds') bnd = XML::Node.new('bounds')
bnd.attributes['minlat'] = min_lat bnd['minlat'] = min_lat.to_s
bnd.attributes['minlon'] = min_lon bnd['minlon'] = min_lon.to_s
bnd.attributes['maxlat'] = max_lat bnd['maxlat'] = max_lat.to_s
bnd.attributes['maxlon'] = max_lon bnd['maxlon'] = max_lon.to_s
bnd bnd
end end

View File

@ -26,9 +26,10 @@ 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
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 # elements with the names given in the "text_elements" array. Each
# element found underneath "parent" with a name in "text_elements" causes # element found underneath "parent" with a name in "text_elements" causes
# an attribute to be initialized on the instance. This means you don't # an attribute to be initialized on the instance. This means you don't
@ -37,8 +38,8 @@ module GPX
# attributes to this method. # attributes to this method.
def instantiate_with_text_elements(parent, text_elements) def instantiate_with_text_elements(parent, text_elements)
text_elements.each do |el| text_elements.each do |el|
unless parent.elements[el].nil? unless parent.find(el).empty?
val = parent.elements[el].text val = parent.find(el).first.content
code = <<-code code = <<-code
attr_accessor #{ el } attr_accessor #{ el }
#{el}=#{val} #{el}=#{val}

View File

@ -44,14 +44,15 @@ module GPX
@duration = 0 @duration = 0
if(opts[:gpx_file]) if(opts[:gpx_file])
gpx_file = opts[:gpx_file] gpx_file = opts[:gpx_file]
case gpx_file #case gpx_file
when String #when String
gpx_file = File.open(gpx_file) # gpx_file = File.open(gpx_file)
end #end
gpx_file = gpx_file.name if gpx_file.is_a?(File)
reset_meta_data 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 if bounds_element
@bounds.min_lat = get_bounds_attr_value(bounds_element, %w{ min_lat minlat minLat }) @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.min_lon = get_bounds_attr_value(bounds_element, %w{ min_lon minlon minLon})
@ -61,13 +62,16 @@ module GPX
get_bounds = true get_bounds = true
end 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) trk = Track.new(:element => trk, :gpx_file => self)
update_meta_data(trk, get_bounds) update_meta_data(trk, get_bounds)
trk @tracks << trk
end end
@waypoints = XPath.match(@xml, "/gpx/wpt").collect { |wpt| Waypoint.new(:element => wpt, :gpx_file => self) } @waypoints = []
@routes = XPath.match(@xml, "/gpx/rte").collect { |rte| Route.new(:element => rte, :gpx_file => self) } @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? } @tracks.delete_if { |t| t.empty? }
@ -85,7 +89,7 @@ module GPX
def get_bounds_attr_value(el, possible_names) def get_bounds_attr_value(el, possible_names)
result = nil result = nil
possible_names.each do |name| possible_names.each do |name|
result = el.attributes[name] result = el[name]
break unless result.nil? break unless result.nil?
end end
return (result.to_f rescue nil) return (result.to_f rescue nil)
@ -184,32 +188,32 @@ module GPX
def write(filename) def write(filename)
doc = Document.new doc = Document.new
gpx_elem = Element.new('gpx') doc.root = Node.new('gpx')
doc.add(gpx_elem) gpx_elem = doc.root
gpx_elem.attributes['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance" gpx_elem['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance"
gpx_elem.attributes['xmlns'] = "http://www.topografix.com/GPX/1/1" gpx_elem['xmlns'] = "http://www.topografix.com/GPX/1/1"
gpx_elem.attributes['version'] = "1.1" gpx_elem['version'] = "1.1"
gpx_elem.attributes['creator'] = "GPX RubyGem 0.1 Copyright 2006 Doug Fales -- http://walkingboss.com" gpx_elem['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" 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') meta_data_elem = Node.new('metadata')
name_elem = Element.new('name') name_elem = Node.new('name')
name_elem.text = File.basename(filename) name_elem << File.basename(filename)
meta_data_elem.elements << name_elem meta_data_elem << name_elem
time_elem = Element.new('time') time_elem = Node.new('time')
time_elem.text = Time.now.xmlschema time_elem << Time.now.xmlschema
meta_data_elem.elements << time_elem 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? tracks.each { |t| gpx_elem << t.to_xml } unless tracks.nil?
waypoints.each { |w| gpx_elem.add_element w.to_xml } unless waypoints.nil? waypoints.each { |w| gpx_elem << w.to_xml } unless waypoints.nil?
routes.each { |r| gpx_elem.add_element r.to_xml } unless routes.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 end
private private

View File

@ -29,16 +29,16 @@ module GPX
# When you need to manipulate individual points, you can create a Point # When you need to manipulate individual points, you can create a Point
# object with a latitude, a longitude, an elevation, and a time. In # 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. # relevant info will be parsed out.
def initialize(opts = {:lat => 0.0, :lon => 0.0, :elevation => 0.0, :time => Time.now } ) def initialize(opts = {:lat => 0.0, :lon => 0.0, :elevation => 0.0, :time => Time.now } )
if (opts[:element]) if (opts[:element])
elem = 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) @latr, @lonr = (D_TO_R * @lat), (D_TO_R * @lon)
#'-'? yyyy '-' mm '-' dd 'T' hh ':' mm ':' ss ('.' s+)? (zzzzzz)? #'-'? yyyy '-' mm '-' dd 'T' hh ':' mm ':' ss ('.' s+)? (zzzzzz)?
@time = (Time.xmlschema(elem.elements["time"].text) rescue nil) @time = (Time.xmlschema(elem.find("gpx:time", NS).first.content) rescue nil)
@elevation = elem.elements["ele"].text.to_f if elem.elements["ele"] @elevation = elem.find("gpx:ele", NS).first.content.to_f unless elem.find("gpx:ele", NS).empty?
else else
@lat = opts[:lat] @lat = opts[:lat]
@lon = opts[:lon] @lon = opts[:lon]
@ -85,19 +85,19 @@ module GPX
@lon = longitude @lon = longitude
end end
# Convert this point to a REXML::Element. # Convert this point to a XML::Node.
def to_xml(elem_name = 'trkpt') def to_xml(elem_name = 'trkpt')
pt = Element.new('trkpt') pt = Node.new('trkpt')
pt.attributes['lat'] = lat pt['lat'] = lat.to_s
pt.attributes['lon'] = lon pt['lon'] = lon.to_s
unless time.nil? unless time.nil?
time_elem = Element.new('time') time_elem = Node.new('time')
time_elem.text = time.xmlschema time_elem << time.xmlschema
pt.elements << time_elem pt << time_elem
end end
elev = Element.new('ele') elev = Node.new('ele')
elev.text = elevation elev << elevation
pt.elements << elev pt << elev
pt pt
end end

View File

@ -29,13 +29,13 @@ module GPX
attr_reader :points, :name, :gpx_file attr_reader :points, :name, :gpx_file
# Initialize a Route from a REXML::Element. # Initialize a Route from a XML::Node.
def initialize(opts = {}) def initialize(opts = {})
rte_element = opts[:element] rte_element = opts[:element]
@gpx_file = opts[:gpx_file] @gpx_file = opts[:gpx_file]
@name = rte_element.elements["child::name"].text @name = rte_element.find("child::gpx:name", NS).first.content
@points = [] @points = []
XPath.each(rte_element, "child::rtept") do |point| rte_element.find("child::gpx:rtept", NS).each do |point|
@points << Point.new(:element => point) @points << Point.new(:element => point)
end end
@ -51,13 +51,13 @@ module GPX
points.delete_if{ |pt| area.contains? pt } points.delete_if{ |pt| area.contains? pt }
end end
# Convert this Route to a REXML::Element. # Convert this Route to a XML::Node.
def to_xml def to_xml
rte = Element.new('rte') rte = Node.new('rte')
name_elem = Element.new('name') name_elem = Node.new('name')
name_elem.text = name name_elem << name
rte.elements << name_elem rte << name_elem
points.each { |rte_pt| rte.elements << rte_pt.to_xml('rtept') } points.each { |rte_pt| rte << rte_pt.to_xml('rtept') }
rte rte
end end

View File

@ -31,7 +31,7 @@ module GPX
attr_reader :earliest_point, :latest_point, :bounds, :highest_point, :lowest_point, :distance attr_reader :earliest_point, :latest_point, :bounds, :highest_point, :lowest_point, :distance
attr_accessor :points, :track 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. # Segment based on its contents. Otherwise, a blank Segment is created.
def initialize(opts = {}) def initialize(opts = {})
@track = opts[:track] @track = opts[:track]
@ -45,8 +45,8 @@ module GPX
if(opts[:element]) if(opts[:element])
segment_element = opts[:element] segment_element = opts[:element]
last_pt = nil last_pt = nil
unless segment_element.is_a?(Text) if segment_element.is_a?(XML::Node)
XPath.each(segment_element, "child::trkpt") do |trkpt| segment_element.find("child::gpx:trkpt", NS).each do |trkpt|
pt = TrackPoint.new(:element => trkpt, :segment => self) pt = TrackPoint.new(:element => trkpt, :segment => self)
unless pt.time.nil? unless pt.time.nil?
@earliest_point = pt if(@earliest_point.nil? or pt.time < @earliest_point.time) @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)) (points.nil? or (points.size == 0))
end end
# Converts this Segment to a REXML::Element object. # Converts this Segment to a XML::Node object.
def to_xml def to_xml
seg = Element.new('trkseg') seg = Node.new('trkseg')
points.each { |pt| seg.elements << pt.to_xml } points.each { |pt| seg << pt.to_xml }
seg seg
end end

View File

@ -32,7 +32,7 @@ module GPX
attr_reader :points, :bounds, :lowest_point, :highest_point, :distance attr_reader :points, :bounds, :lowest_point, :highest_point, :distance
attr_accessor :segments, :name, :gpx_file 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. # passed, initialize a blank Track object.
def initialize(opts = {}) def initialize(opts = {})
@gpx_file = opts[:gpx_file] @gpx_file = opts[:gpx_file]
@ -41,8 +41,8 @@ module GPX
reset_meta_data reset_meta_data
if(opts[:element]) if(opts[:element])
trk_element = opts[:element] trk_element = opts[:element]
@name = (trk_element.elements["child::name"].text rescue "") @name = (trk_element.find("child::gpx:name", NS).first.content rescue "")
XPath.each(trk_element, "child::trkseg") do |seg_element| trk_element.find("child::gpx:trkseg", NS).each do |seg_element|
seg = Segment.new(:element => seg_element, :track => self) seg = Segment.new(:element => seg_element, :track => self)
update_meta_data(seg) update_meta_data(seg)
@segments << seg @segments << seg
@ -101,14 +101,14 @@ module GPX
(points.nil? or points.size.zero?) (points.nil? or points.size.zero?)
end 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 def to_xml
trk= Element.new('trk') trk= Node.new('trk')
name_elem = Element.new('name') name_elem = Node.new('name')
name_elem.text = name name_elem << name
trk.elements << name_elem trk << name_elem
segments.each do |seg| segments.each do |seg|
trk.elements << seg.to_xml trk << seg.to_xml
end end
trk trk
end end

View File

@ -39,7 +39,7 @@ module GPX
def delete_area(area) def delete_area(area)
end end
# Initializes a waypoint from a REXML::Element. # Initializes a waypoint from a XML::Node.
def initialize(opts = {}) def initialize(opts = {})
wpt_elem = opts[:element] wpt_elem = opts[:element]
super(:element => wpt_elem) super(:element => wpt_elem)
@ -47,25 +47,25 @@ module GPX
@gpx_file = opts[:gpx_file] @gpx_file = opts[:gpx_file]
end end
# Converts a waypoint to a REXML::Element. # Converts a waypoint to a XML::Node.
def to_xml def to_xml
wpt = Element.new('wpt') wpt = Node.new('wpt')
wpt.attributes['lat'] = lat wpt.attributes['lat'] = lat
wpt.attributes['lon'] = lon wpt.attributes['lon'] = lon
if self.respond_to? :name if self.respond_to? :name
name_elem = Element.new('name') name_elem = Node.new('name')
name_elem.text = self.name name_elem << self.name
wpt.elements << name_elem wpt << name_elem
end end
if self.respond_to? :sym if self.respond_to? :sym
sym_elem = Element.new('sym') sym_elem = Node.new('sym')
sym_elem.text = self.sym sym_elem << self.sym
wpt.elements << sym_elem wpt << sym_elem
end end
if self.respond_to? :ele if self.respond_to? :ele
elev_elem = Element.new('ele') elev_elem = Node.new('ele')
elev_elem.text = self.ele elev_elem << self.ele
wpt.elements << elev_elem wpt << elev_elem
end end
wpt wpt
end end

View File

@ -1,4 +1,5 @@
require 'test/unit' require 'test/unit'
require 'yaml'
require File.dirname(__FILE__) + '/../lib/gpx' require File.dirname(__FILE__) + '/../lib/gpx'
class TestSegment < Test::Unit::TestCase class TestSegment < Test::Unit::TestCase