]> git.openstreetmap.org Git - rails.git/blob - lib/gpx.rb
Merge remote-tracking branch 'upstream/pull/5699'
[rails.git] / lib / gpx.rb
1 module GPX
2   class File
3     require "libxml"
4
5     include LibXML
6
7     attr_reader :possible_points, :actual_points, :tracksegs
8
9     def initialize(file, options = {})
10       @file = file
11       @maximum_points = options[:maximum_points] || Float::INFINITY
12     end
13
14     def parse_file(reader)
15       point = nil
16
17       while reader.read
18         case reader.node_type
19         when XML::Reader::TYPE_ELEMENT
20           if reader.name == "trkpt"
21             point = TrkPt.new(@tracksegs, reader["lat"].to_f, reader["lon"].to_f)
22             @possible_points += 1
23             raise FileTooBigError if @possible_points > @maximum_points
24           elsif reader.name == "ele" && point
25             point.altitude = reader.read_string.to_f
26           elsif reader.name == "time" && point
27             point.timestamp = Time.parse(reader.read_string).utc
28           end
29         when XML::Reader::TYPE_END_ELEMENT
30           if reader.name == "trkpt" && point&.valid?
31             point.altitude ||= 0
32             yield point
33             @actual_points += 1
34             @lats << point.latitude
35             @lons << point.longitude
36           elsif reader.name == "trkseg"
37             @tracksegs += 1
38           end
39         end
40       end
41     end
42
43     def points(&block)
44       return enum_for(:points) unless block
45
46       @possible_points = 0
47       @actual_points = 0
48       @tracksegs = 0
49       @lats = []
50       @lons = []
51
52       begin
53         Archive::Reader.open_filename(@file).each_entry_with_data do |entry, data|
54           parse_file(XML::Reader.string(data), &block) if entry.regular?
55         end
56       rescue Archive::Error
57         io = ::File.open(@file)
58
59         case Marcel::MimeType.for(io)
60         when "application/gzip" then io = Zlib::GzipReader.open(@file)
61         when "application/x-bzip" then io = Bzip2::FFI::Reader.open(@file)
62         end
63
64         parse_file(XML::Reader.io(io, :options => XML::Parser::Options::NOERROR), &block)
65       end
66     end
67
68     def picture(min_lat, min_lon, max_lat, max_lon, num_points)
69       nframes = 10
70       width = 250
71       height = 250
72       delay = 50
73
74       points_per_frame = (num_points.to_f / nframes).ceil
75
76       proj = OSM::Mercator.new(min_lat, min_lon, max_lat, max_lon, width, height)
77
78       frames = []
79
80       (0...nframes).each do |n|
81         frames[n] = GD2::Image::IndexedColor.new(width, height)
82         black = frames[n].palette.allocate(GD2::Color[0, 0, 0])
83         white = frames[n].palette.allocate(GD2::Color[255, 255, 255])
84         grey = frames[n].palette.allocate(GD2::Color[187, 187, 187])
85
86         frames[n].draw do |pen|
87           pen.color = white
88           pen.rectangle(0, 0, width, height, true)
89         end
90
91         frames[n].draw do |pen|
92           pen.color = black
93           pen.anti_aliasing = true
94           pen.dont_blend = false
95
96           oldpx = 0.0
97           oldpy = 0.0
98
99           first = true
100
101           @actual_points.times do |pt|
102             px = proj.x @lons[pt]
103             py = proj.y @lats[pt]
104
105             if (pt >= (points_per_frame * n)) && (pt <= (points_per_frame * (n + 1)))
106               pen.thickness = 3
107               pen.color = black
108             else
109               pen.thickness = 1
110               pen.color = grey
111             end
112
113             pen.line(px, py, oldpx, oldpy) unless first
114             first = false
115             oldpy = py
116             oldpx = px
117           end
118         end
119       end
120
121       image = GD2::AnimatedGif.new
122       image.add(frames.first)
123       frames.each do |frame|
124         image.add(frame, :delay => delay)
125       end
126       image.end
127
128       output = StringIO.new
129       image.export(output)
130       output
131     end
132
133     def icon(min_lat, min_lon, max_lat, max_lon)
134       width = 50
135       height = 50
136       proj = OSM::Mercator.new(min_lat, min_lon, max_lat, max_lon, width, height)
137
138       image = GD2::Image::IndexedColor.new(width, height)
139
140       black = image.palette.allocate(GD2::Color[0, 0, 0])
141       white = image.palette.allocate(GD2::Color[255, 255, 255])
142
143       image.draw do |pen|
144         pen.color = white
145         pen.rectangle(0, 0, width, height, true)
146       end
147
148       image.draw do |pen|
149         pen.color = black
150         pen.anti_aliasing = true
151         pen.dont_blend = false
152
153         oldpx = 0.0
154         oldpy = 0.0
155
156         first = true
157
158         @actual_points.times do |pt|
159           px = proj.x @lons[pt]
160           py = proj.y @lats[pt]
161
162           pen.line(px, py, oldpx, oldpy) unless first
163
164           first = false
165           oldpy = py
166           oldpx = px
167         end
168       end
169
170       StringIO.new(image.gif)
171     end
172   end
173
174   TrkPt = Struct.new(:segment, :latitude, :longitude, :altitude, :timestamp) do
175     def valid?
176       latitude && longitude && timestamp &&
177         latitude >= -90 && latitude <= 90 &&
178         longitude >= -180 && longitude <= 180
179     end
180   end
181
182   class FileTooBigError < RuntimeError
183     def initialise
184       super("GPX File contains too many points")
185     end
186   end
187 end