Skip to content

Commit

Permalink
First stab at using libxml-ruby instead of REXML. I'm seeing the unit…
Browse files Browse the repository at this point in the history
… tests

finish in under 14 seconds.  That is compared to 2 minutes using REXML.
  • Loading branch information
dougfales committed Dec 4, 2006
1 parent fcf2b9e commit 022c7c6
Show file tree
Hide file tree
Showing 11 changed files with 99 additions and 92 deletions.
2 changes: 1 addition & 1 deletion README
Expand Up @@ -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.

Expand Down
3 changes: 2 additions & 1 deletion lib/gpx.rb
Expand Up @@ -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'
Expand Down
10 changes: 5 additions & 5 deletions lib/gpx/bounds.rb
Expand Up @@ -44,11 +44,11 @@ def center_lon
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

Expand Down
9 changes: 5 additions & 4 deletions lib/gpx/gpx.rb
Expand Up @@ -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
Expand All @@ -37,8 +38,8 @@ class Base
# 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}
Expand Down
66 changes: 35 additions & 31 deletions lib/gpx/gpx_file.rb
Expand Up @@ -44,14 +44,15 @@ def initialize(opts = {})
@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})
Expand All @@ -61,13 +62,16 @@ def initialize(opts = {})
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? }

Expand All @@ -85,7 +89,7 @@ def initialize(opts = {})
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)
Expand Down Expand Up @@ -184,32 +188,32 @@ def update_meta_data(trk, get_bounds = true)
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
Expand Down
28 changes: 14 additions & 14 deletions lib/gpx/point.rb
Expand Up @@ -29,16 +29,16 @@ class Point < Base

# 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]
Expand Down Expand Up @@ -85,19 +85,19 @@ def lon=(longitude)
@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

Expand Down
18 changes: 9 additions & 9 deletions lib/gpx/route.rb
Expand Up @@ -29,13 +29,13 @@ class Route < Base

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

Expand All @@ -51,13 +51,13 @@ def delete_area(area)
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

Expand Down
12 changes: 6 additions & 6 deletions lib/gpx/segment.rb
Expand Up @@ -31,7 +31,7 @@ class Segment < Base
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]
Expand All @@ -45,8 +45,8 @@ def initialize(opts = {})
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)
Expand Down Expand Up @@ -131,10 +131,10 @@ def empty?
(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

Expand Down
18 changes: 9 additions & 9 deletions lib/gpx/track.rb
Expand Up @@ -32,7 +32,7 @@ class Track < Base
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]
Expand All @@ -41,8 +41,8 @@ def initialize(opts = {})
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
Expand Down Expand Up @@ -101,14 +101,14 @@ def empty?
(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
Expand Down
24 changes: 12 additions & 12 deletions lib/gpx/waypoint.rb
Expand Up @@ -39,33 +39,33 @@ def crop(area)
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)
instantiate_with_text_elements(wpt_elem, SUB_ELEMENTS)
@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
Expand Down
1 change: 1 addition & 0 deletions tests/segment_test.rb
@@ -1,4 +1,5 @@
require 'test/unit'
require 'yaml'
require File.dirname(__FILE__) + '/../lib/gpx'

class TestSegment < Test::Unit::TestCase
Expand Down

0 comments on commit 022c7c6

Please sign in to comment.