steam-condenser 0.8.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +10 -7
- data/VERSION.yml +4 -0
- data/lib/byte_buffer.rb +28 -28
- data/lib/datagram_channel.rb +5 -4
- data/lib/exceptions/rcon_ban_exception.rb +12 -0
- data/lib/socket_channel.rb +21 -14
- data/lib/steam-condenser.rb +11 -0
- data/lib/steam/community/cacheable.rb +95 -0
- data/lib/steam/community/defense_grid/defense_grid_stats.rb +124 -0
- data/lib/steam/community/game_stats.rb +44 -26
- data/lib/steam/community/l4d/l4d_map.rb +3 -3
- data/lib/steam/community/steam_group.rb +77 -6
- data/lib/steam/community/steam_id.rb +127 -101
- data/lib/steam/community/tf2/tf2_stats.rb +5 -5
- data/lib/steam/packets/rcon/rcon_exec_response.rb +6 -8
- data/lib/steam/packets/s2a_rules_packet.rb +15 -8
- data/lib/steam/servers/game_server.rb +2 -4
- data/lib/steam/servers/goldsrc_server.rb +5 -5
- data/lib/steam/servers/source_server.rb +11 -11
- data/lib/steam/sockets/rcon_socket.rb +18 -17
- data/lib/steam/sockets/steam_socket.rb +5 -8
- data/lib/steam/steam_player.rb +5 -5
- data/test/byte_buffer_tests.rb +76 -0
- data/test/datagram_channel_tests.rb +42 -0
- data/test/query_tests.rb +1 -3
- data/test/rcon_tests.rb +1 -3
- data/test/socket_channel_tests.rb +43 -0
- data/test/steam/communtiy/steam_community_test_suite.rb +9 -0
- data/test/steam/communtiy/steam_group_tests.rb +43 -0
- data/test/steam/communtiy/steam_id_tests.rb +48 -0
- data/test/steam_community_tests.rb +17 -9
- metadata +27 -10
data/Rakefile
CHANGED
@@ -13,15 +13,18 @@ test_files = Dir.glob(File.join("test", "**", "*.rb"))
|
|
13
13
|
|
14
14
|
# Gem specification
|
15
15
|
Jeweler::Tasks.new do |s|
|
16
|
-
s.name = "steam-condenser"
|
17
|
-
s.date = Time.now
|
18
|
-
s.description = s.summary = 'A multi-language library for querying Source, GoldSrc servers and Steam master servers'
|
19
16
|
s.authors = ['Sebastian Staudt']
|
20
17
|
s.email = 'koraktor@gmail.com'
|
21
|
-
s.
|
22
|
-
|
23
|
-
s.
|
18
|
+
s.description = 'A multi-language library for querying the Steam Community, Source, GoldSrc servers and Steam master servers'
|
19
|
+
s.date = Time.now
|
20
|
+
s.homepage = 'http://koraktor.github.com/steam-condenser'
|
21
|
+
s.name = s.rubyforge_project = "steam-condenser"
|
22
|
+
s.summary = 'Steam Condenser - A Steam query library'
|
23
|
+
|
24
|
+
s.files = %w(README.md Rakefile LICENSE VERSION.yml) + src_files + test_files
|
24
25
|
s.rdoc_options = ["--all", "--inline-source", "--line-numbers", "--charset=utf-8", "--webcvs=http://github.com/koraktor/steam-condenser/source/blob/master/ruby/%s"]
|
26
|
+
|
27
|
+
s.add_dependency('hpricot', '>= 0.6')
|
25
28
|
end
|
26
29
|
|
27
30
|
# Create a rake task +:rdoc+ to build the documentation
|
@@ -31,7 +34,7 @@ Rake::RDocTask.new do |rdoc|
|
|
31
34
|
rdoc.rdoc_files.include ["lib/**/*.rb", "test/**/*.rb", "LICENSE", "README.md"]
|
32
35
|
rdoc.main = "README.md"
|
33
36
|
rdoc.rdoc_dir = "rdoc"
|
34
|
-
rdoc.options = ["--all", "--inline-source", "--line-numbers", "--charset=utf-8", "--webcvs=http://github.com/koraktor/steam-condenser/
|
37
|
+
rdoc.options = ["--all", "--inline-source", "--line-numbers", "--charset=utf-8", "--webcvs=http://github.com/koraktor/steam-condenser/blob/master/ruby/%s"]
|
35
38
|
end
|
36
39
|
|
37
40
|
# Task for cleaning documentation and package directories
|
data/VERSION.yml
ADDED
data/lib/byte_buffer.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# This code is free software; you can redistribute it and/or modify it under the
|
2
2
|
# terms of the new BSD License.
|
3
3
|
#
|
4
|
-
# Copyright (c) 2008, Sebastian Staudt
|
4
|
+
# Copyright (c) 2008-2009, Sebastian Staudt
|
5
5
|
|
6
6
|
require "exceptions/buffer_underflow_exception"
|
7
7
|
|
@@ -11,71 +11,71 @@ class ByteBuffer
|
|
11
11
|
attr_accessor :limit
|
12
12
|
attr_reader :position
|
13
13
|
protected :initialize
|
14
|
-
|
14
|
+
|
15
15
|
# Creates a new buffer with the given size in bytes
|
16
16
|
def self.allocate(length)
|
17
17
|
return ByteBuffer.new(0.chr * length)
|
18
18
|
end
|
19
|
-
|
20
|
-
# Creates a new buffer from an existing String
|
19
|
+
|
20
|
+
# Creates a new buffer from an existing String
|
21
21
|
def self.wrap(byte_array)
|
22
22
|
return ByteBuffer.new(byte_array.to_s)
|
23
23
|
end
|
24
|
-
|
25
|
-
#
|
24
|
+
|
25
|
+
#
|
26
26
|
def initialize(byte_array)
|
27
27
|
if byte_array.is_a? ByteBuffer
|
28
28
|
raise Exception.new
|
29
29
|
end
|
30
|
-
|
30
|
+
|
31
31
|
@byte_array = byte_array.to_s
|
32
32
|
@capacity = @byte_array.length
|
33
33
|
@limit = @capacity
|
34
34
|
@position = 0
|
35
35
|
@mark = -1
|
36
36
|
end
|
37
|
-
|
37
|
+
|
38
38
|
def array
|
39
39
|
return @byte_array
|
40
40
|
end
|
41
|
-
|
41
|
+
|
42
42
|
def clear
|
43
43
|
@limit = @capacity
|
44
44
|
@position = 0
|
45
45
|
@mark = -1
|
46
|
-
|
46
|
+
|
47
47
|
return self
|
48
48
|
end
|
49
|
-
|
49
|
+
|
50
50
|
def flip
|
51
51
|
@limit = @position
|
52
52
|
@position = 0
|
53
53
|
@mark = -1
|
54
|
-
|
54
|
+
|
55
55
|
return self
|
56
56
|
end
|
57
|
-
|
57
|
+
|
58
58
|
def get(length = nil)
|
59
59
|
if length == nil
|
60
60
|
length = @limit - @position
|
61
61
|
elsif length > self.remaining
|
62
62
|
BufferUnderflowException.new
|
63
63
|
end
|
64
|
-
|
64
|
+
|
65
65
|
data = @byte_array[@position, length]
|
66
66
|
@position += length
|
67
|
-
|
67
|
+
|
68
68
|
return data
|
69
69
|
end
|
70
|
-
|
70
|
+
|
71
71
|
def get_byte
|
72
72
|
return self.get(1)[0]
|
73
73
|
end
|
74
|
-
|
74
|
+
|
75
75
|
def get_float
|
76
76
|
return self.get(4).unpack("e")[0]
|
77
77
|
end
|
78
|
-
|
78
|
+
|
79
79
|
def get_long
|
80
80
|
return self.get(4).unpack("V")[0]
|
81
81
|
end
|
@@ -87,39 +87,39 @@ class ByteBuffer
|
|
87
87
|
def get_signed_long
|
88
88
|
return self.get(4).unpack('l')[0]
|
89
89
|
end
|
90
|
-
|
90
|
+
|
91
91
|
def get_string
|
92
92
|
zero_byte_index = @byte_array.index("\0", @position)
|
93
93
|
if zero_byte_index == nil
|
94
|
-
|
94
|
+
nil
|
95
95
|
else
|
96
96
|
data_string = self.get(zero_byte_index - @position)
|
97
|
-
@position += 1
|
98
|
-
|
97
|
+
@position += 1
|
98
|
+
data_string
|
99
99
|
end
|
100
100
|
end
|
101
|
-
|
101
|
+
|
102
102
|
def length
|
103
103
|
return @byte_array.length
|
104
104
|
end
|
105
|
-
|
105
|
+
|
106
106
|
def remaining
|
107
107
|
return @limit - @position
|
108
108
|
end
|
109
|
-
|
109
|
+
|
110
110
|
def rewind
|
111
111
|
@position = 0
|
112
112
|
@mark = -1
|
113
|
-
|
113
|
+
|
114
114
|
return self
|
115
115
|
end
|
116
|
-
|
116
|
+
|
117
117
|
def put(source_byte_array)
|
118
118
|
new_position = [source_byte_array.length, self.remaining].min
|
119
119
|
|
120
120
|
@byte_array[@position, new_position] = source_byte_array
|
121
121
|
@position = new_position
|
122
|
-
|
122
|
+
|
123
123
|
return self
|
124
124
|
end
|
125
125
|
|
data/lib/datagram_channel.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
# This code is free software; you can redistribute it and/or modify it under the
|
2
2
|
# terms of the new BSD License.
|
3
3
|
#
|
4
|
-
# Copyright (c) 2008, Sebastian Staudt
|
5
|
-
|
6
|
-
|
4
|
+
# Copyright (c) 2008-2009, Sebastian Staudt
|
5
|
+
|
6
|
+
require 'ipaddr'
|
7
|
+
require 'socket'
|
7
8
|
|
8
|
-
require
|
9
|
+
require 'byte_buffer'
|
9
10
|
|
10
11
|
class DatagramChannel
|
11
12
|
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# This code is free software; you can redistribute it and/or modify it under the
|
2
|
+
# terms of the new BSD License.
|
3
|
+
#
|
4
|
+
# Copyright (c) 2009, Sebastian Staudt
|
5
|
+
|
6
|
+
class RCONBanException < Exception
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
super 'You have been banned from this server.'
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
data/lib/socket_channel.rb
CHANGED
@@ -3,52 +3,59 @@
|
|
3
3
|
#
|
4
4
|
# Copyright (c) 2008-2009, Sebastian Staudt
|
5
5
|
|
6
|
-
require '
|
6
|
+
require 'ipaddr'
|
7
|
+
require 'socket'
|
7
8
|
require 'timeout'
|
8
9
|
|
10
|
+
require 'byte_buffer'
|
11
|
+
|
9
12
|
class SocketChannel
|
10
|
-
|
13
|
+
|
11
14
|
attr_reader :socket
|
12
|
-
|
15
|
+
|
13
16
|
def self.open
|
14
17
|
return SocketChannel.new
|
15
18
|
end
|
16
|
-
|
19
|
+
|
20
|
+
def close
|
21
|
+
@socket.close
|
22
|
+
end
|
23
|
+
|
17
24
|
def connect(*args)
|
18
25
|
timeout(1) do
|
19
26
|
@socket = TCPSocket.new args[0][0][3], args[0][0][1]
|
20
27
|
@connected = true
|
21
28
|
end
|
22
|
-
|
29
|
+
|
23
30
|
return self
|
24
31
|
end
|
25
|
-
|
32
|
+
|
26
33
|
def initialize
|
27
34
|
@connected = false
|
28
35
|
end
|
29
|
-
|
36
|
+
|
30
37
|
def connected?
|
31
38
|
return @connected
|
32
39
|
end
|
33
|
-
|
40
|
+
|
34
41
|
def read(destination_buffer)
|
35
42
|
if !destination_buffer.is_a? ByteBuffer
|
36
43
|
raise ArgumentError
|
37
44
|
end
|
38
|
-
|
45
|
+
|
39
46
|
length = destination_buffer.remaining
|
40
47
|
data = @socket.recv length
|
41
48
|
destination_buffer.put data
|
42
|
-
|
49
|
+
|
43
50
|
return data.length
|
44
51
|
end
|
45
|
-
|
52
|
+
|
46
53
|
def write(source_buffer)
|
47
54
|
if !source_buffer.is_a? ByteBuffer
|
48
55
|
raise ArgumentError
|
49
56
|
end
|
50
|
-
|
57
|
+
|
51
58
|
return @socket.send(source_buffer.get, 0)
|
52
59
|
end
|
53
|
-
|
54
|
-
end
|
60
|
+
|
61
|
+
end
|
data/lib/steam-condenser.rb
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
# This code is free software; you can redistribute it and/or modify it under the
|
2
|
+
# terms of the new BSD License.
|
3
|
+
#
|
4
|
+
# Copyright (c) 2009, Sebastian Staudt
|
5
|
+
|
1
6
|
libdir = File.dirname(__FILE__)
|
2
7
|
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
|
3
8
|
|
9
|
+
module SteamCondenser
|
10
|
+
|
11
|
+
version = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'VERSION.yml'))
|
12
|
+
VERSION = "#{version[:major]}.#{version[:minor]}.#{version[:patch]}"
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# This code is free software; you can redistribute it and/or modify it under the
|
2
|
+
# terms of the new BSD License.
|
3
|
+
#
|
4
|
+
# Copyright (c) 2009, Sebastian Staudt
|
5
|
+
|
6
|
+
# This module implements caching functionality to be used in any object using a
|
7
|
+
# +fetch+ method to fetch data, e.g. using a HTTP download.
|
8
|
+
module Cacheable
|
9
|
+
|
10
|
+
def self.included(base) #:nodoc:
|
11
|
+
|
12
|
+
base.extend ClassMethods
|
13
|
+
base.class_eval 'class_variable_set :@@cache, {}'
|
14
|
+
base.class_eval 'class_variable_set :@@cache_ids, {}'
|
15
|
+
|
16
|
+
class << base
|
17
|
+
alias_method :create, :new
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
module ClassMethods
|
23
|
+
|
24
|
+
# Defines wich instance variables should be used to index the cached objects
|
25
|
+
# A call to this method is needed, if you want a class including this module
|
26
|
+
# to really use the cache.
|
27
|
+
def cacheable_with_ids(*ids)
|
28
|
+
class_variable_set(:@@cache_ids, ids)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns whether the requested object +id+ is already cached
|
32
|
+
def cached?(id)
|
33
|
+
if id.is_a? String
|
34
|
+
id.downcase!
|
35
|
+
end
|
36
|
+
class_variable_get(:@@cache).key?(id)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Clears the object cache
|
40
|
+
def clear_cache
|
41
|
+
class_variable_set :@@cache, {}
|
42
|
+
end
|
43
|
+
|
44
|
+
# This checks the cache for an existing object. If it exists it is returned.
|
45
|
+
# Otherwise a new object is created.
|
46
|
+
# Overrides the default constructor.
|
47
|
+
def new(id, fetch = true, bypass_cache = false)
|
48
|
+
if cached?(id) and !bypass_cache
|
49
|
+
object = class_variable_get(:@@cache)[id]
|
50
|
+
object.fetch_data if fetch and !object.fetched?
|
51
|
+
object
|
52
|
+
else
|
53
|
+
super(id, fetch)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_reader :fetch_time
|
60
|
+
|
61
|
+
# Creates a new object for the given +id+, either numeric or
|
62
|
+
# the custom URL specified by the user. If +fetch+ is +true+ (default),
|
63
|
+
# fetch is used to load data into the object.
|
64
|
+
# This method is overridden by Cacheable::ClassMethods#new.
|
65
|
+
def initialize(fetch = true) #:notnew:
|
66
|
+
self.fetch if fetch
|
67
|
+
cache
|
68
|
+
end
|
69
|
+
|
70
|
+
# Saves this object in the cache
|
71
|
+
def cache
|
72
|
+
cache = self.class.class_eval 'class_variable_get :@@cache'
|
73
|
+
cache_ids = self.class.class_eval 'class_variable_get :@@cache_ids'
|
74
|
+
|
75
|
+
cache_ids.each do |cache_id|
|
76
|
+
cache_id_value = instance_variable_get('@' + cache_id.to_s)
|
77
|
+
unless cache_id_value.nil? or cache.key?(cache_id_value)
|
78
|
+
cache[cache_id_value] = self
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
true
|
83
|
+
end
|
84
|
+
|
85
|
+
# Sets the time this object has been fetched the last time
|
86
|
+
def fetch
|
87
|
+
@fetch_time = Time.now
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns whether the data for this SteamID has already been fetched
|
91
|
+
def fetched?
|
92
|
+
!@fetch_time.nil?
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# This code is free software; you can redistribute it and/or modify it under the
|
2
|
+
# terms of the new BSD License.
|
3
|
+
#
|
4
|
+
# Copyright (c) 2009, Sebastian Staudt
|
5
|
+
|
6
|
+
require 'steam/community/game_stats'
|
7
|
+
|
8
|
+
# The DefenseGridStats class represents the game statistics for a single user in
|
9
|
+
# Defense Grid: The Awakening
|
10
|
+
class DefenseGridStats < GameStats
|
11
|
+
|
12
|
+
attr_reader :bronze_medals, :damage_done, :damage_campaign, :damage_challenge,
|
13
|
+
:encountered, :gold_medals, :heat_damage, :interest, :killed,
|
14
|
+
:killed_campaign, :killed_challenge, :levels_played,
|
15
|
+
:levels_played_campaign, :levels_played_challenge, :levels_won,
|
16
|
+
:levels_won_campaign, :levels_won_challenge, :orbital_laser_fired,
|
17
|
+
:orbital_laser_damage, :resources, :silver_medals, :time_played
|
18
|
+
|
19
|
+
# Creates a DefenseGridStats object by calling the super constructor with the
|
20
|
+
# game name "defensegrid:awakening"
|
21
|
+
def initialize(steam_id)
|
22
|
+
super(steam_id, 'defensegrid:awakening')
|
23
|
+
|
24
|
+
if public?
|
25
|
+
general_data = @xml_data.elements['stats/general']
|
26
|
+
|
27
|
+
@bronze_medals = general_data.elements['bronze_medals_won/value'].text.to_i
|
28
|
+
@silver_medals = general_data.elements['silver_medals_won/value'].text.to_i
|
29
|
+
@gold_medals = general_data.elements['gold_medals_won/value'].text.to_i
|
30
|
+
@levels_played = general_data.elements['levels_played_total/value'].text.to_i
|
31
|
+
@levels_played_campagin = general_data.elements['levels_played_campaign/value'].text.to_i
|
32
|
+
@levels_played_challenge = general_data.elements['levels_played_challenge/value'].text.to_i
|
33
|
+
@levels_won = general_data.elements['levels_won_total/value'].text.to_i
|
34
|
+
@levels_won_campaign = general_data.elements['levels_won_campaign/value'].text.to_i
|
35
|
+
@levels_won_challenge = general_data.elements['levels_won_challenge/value'].text.to_i
|
36
|
+
@encountered = general_data.elements['total_aliens_encountered/value'].text.to_i
|
37
|
+
@killed = general_data.elements['total_aliens_killed/value'].text.to_i
|
38
|
+
@killed_campaign = general_data.elements['total_aliens_killed_campaign/value'].text.to_i
|
39
|
+
@killed_challenge = general_data.elements['total_aliens_killed_challenge/value'].text.to_i
|
40
|
+
@resources = general_data.elements['resources_recovered/value'].text.to_i
|
41
|
+
@heat_damage = general_data.elements['heatdamage/value'].text.to_f
|
42
|
+
@time_played = general_data.elements['time_played/value'].text.to_f
|
43
|
+
@interest = general_data.elements['interest_gained/value'].text.to_f
|
44
|
+
@damage = general_data.elements['tower_damage_total/value'].text.to_f
|
45
|
+
@damage_campaign = general_data.elements['tower_damage_total_campaign/value'].text.to_f
|
46
|
+
@damage_challenge = general_data.elements['tower_damage_total_challenge/value'].text.to_f
|
47
|
+
@orbital_laser_fired = @xml_data.elements['stats/orbitallaser/fired/value'].text.to_i
|
48
|
+
@orbital_laser_damage = @xml_data.elements['stats/orbitallaser/damage/value'].text.to_f
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns stats about the towers built
|
53
|
+
#
|
54
|
+
# The Hash returned uses the names of the aliens as keys. Every value of the
|
55
|
+
# Hash is an Array containing the number of aliens encountered as the first
|
56
|
+
# element and the number of aliens killed as the second element.
|
57
|
+
def alien_stats
|
58
|
+
return unless public?
|
59
|
+
|
60
|
+
if @alien_stats.nil?
|
61
|
+
alien_data = @xml_data.elements['stats/aliens']
|
62
|
+
@alien_stats = {}
|
63
|
+
aliens = %w{swarmer juggernaut crasher spire grunt bulwark drone manta dart
|
64
|
+
decoy rumbler seeker turtle walker racer stealth}
|
65
|
+
|
66
|
+
aliens.each do |alien|
|
67
|
+
@alien_stats[alien] = [
|
68
|
+
alien_data.elements["#{alien}/encountered/value"].text.to_i,
|
69
|
+
alien_data.elements["#{alien}/killed/value"].text.to_i
|
70
|
+
]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
@alien_stats
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns stats about the towers built
|
78
|
+
#
|
79
|
+
# The Hash returned uses the names of the towers as keys. Every value of
|
80
|
+
# the Hash is another Hash using the keys 1 to 3 for different tower levels.
|
81
|
+
# The values of these Hash is an Array containing the number of towers built
|
82
|
+
# as the first element and the damage dealt by this specific tower type as the
|
83
|
+
# second element.
|
84
|
+
#
|
85
|
+
# The Command tower uses the resources gained as second element.
|
86
|
+
# The Temporal tower doesn't have a second element.
|
87
|
+
def tower_stats
|
88
|
+
return unless public?
|
89
|
+
|
90
|
+
if @tower_stats.nil?
|
91
|
+
tower_data = @xml_data.elements['stats/towers']
|
92
|
+
@tower_stats = {}
|
93
|
+
towers = %w{cannon flak gun inferno laser meteor missile tesla}
|
94
|
+
|
95
|
+
towers.each do |tower|
|
96
|
+
@tower_stats[tower] = {}
|
97
|
+
(1..3).each do |i|
|
98
|
+
@tower_stats[tower][i] = [
|
99
|
+
tower_data.elements["#{tower}[@level=#{i}]/built/value"].text.to_i,
|
100
|
+
tower_data.elements["#{tower}[@level=#{i}]/damage/value"].text.to_f
|
101
|
+
]
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
@tower_stats['command'] = {}
|
106
|
+
(1..3).each do |i|
|
107
|
+
@tower_stats['command'][i] = [
|
108
|
+
tower_data.elements["command[@level=#{i}]/built/value"].text.to_i,
|
109
|
+
tower_data.elements["command[@level=#{i}]/resource/value"].text.to_f
|
110
|
+
]
|
111
|
+
end
|
112
|
+
|
113
|
+
@tower_stats['temporal'] = {}
|
114
|
+
(1..3).each do |i|
|
115
|
+
@tower_stats['temporal'][i] = [
|
116
|
+
tower_data.elements["temporal[@level=#{i}]/built/value"].text.to_i,
|
117
|
+
]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
@tower_stats
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|