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