oozou-fusion_tables 0.2.3.dev.20110408163600

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,6 @@
1
+ == 0.2.2
2
+
3
+ * 1.9.2 compatibility through gdata_19 gem
4
+ * examples updated for 1.9.2
5
+ * datetime columns now include time
6
+
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Tom Verbeure, Simon Tokumine
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,148 @@
1
+ h1. fusion-tables
2
+
3
+ This gem lets you easily interact with Google Fusion Tables from your Ruby application. Here is a "live visualisation of london bike hire availability":http://tables.googlelabs.com/DataSource?snapid=78314 and some "example maps and charts":http://www.tokumine.com/2010/08/10/fusion-tables-gem/.
4
+
5
+ h2. Gem Dependencies
6
+
7
+ * gdata_19 >= 1.1.2
8
+
9
+ h2. Installation
10
+
11
+ bc. gem install fusion_tables
12
+
13
+ h2. Rubies
14
+
15
+ Tested on:
16
+
17
+ * 1.8.7
18
+ * 1.9.2-p0
19
+
20
+ h2. To Use
21
+
22
+ bc. require 'fusion_tables'
23
+
24
+ or in Rails 2.3.x
25
+
26
+ bc. config.gem 'fusion_tables'
27
+
28
+ h2. API examples
29
+
30
+ "Twitter example":http://github.com/tokumine/fusion-tables/blob/master/examples/compare_tweets.rb
31
+ "Boris bike example":http://github.com/tokumine/fusion-tables/blob/master/examples/boris_bikes.rb
32
+ "Tests":http://github.com/tokumine/fusion-tables/tree/master/test/
33
+
34
+ Here is a brief rundown:
35
+
36
+ <pre><code># Connect to service
37
+ @ft = GData::Client::FusionTables.new
38
+ @ft.clientlogin(username, password)
39
+
40
+ # Browse existing tables
41
+ @ft.show_tables
42
+ # => [table_1, table_2]
43
+
44
+ # Getting table id suitable for using with google maps (see more below)
45
+ table_1.id #=> 42342 (the table's google id)
46
+
47
+ # Count data
48
+ table_1.count #=> 1
49
+
50
+ # Select data
51
+ table_1.select
52
+ #=> data
53
+
54
+ # Select data with conditions
55
+ table_1.select "name", "WHERE x=n"
56
+ #=> data
57
+
58
+ # Select ROWIDs
59
+ row_ids = table_1.select "ROWID"
60
+
61
+ # Drop tables
62
+ @ft.drop table_1.id # table id
63
+ @ft.drop [table_1.id, table_2.id] # arrays of table ids
64
+ @ft.drop /yacht/ # regex on table name
65
+
66
+ # Creating a table
67
+ cols = [{:name => "friend name", :type => 'string' },
68
+ {:name => "age", :type => 'number' },
69
+ {:name => "meeting time", :type => 'datetime' },
70
+ {:name => "where", :type => 'location' }]
71
+
72
+ new_table = @ft.create_table "My upcoming meetings", cols
73
+
74
+ # Inserting rows (auto chunks every 500)
75
+ data = [{"friend name" => "Eric Wimp",
76
+ "age" => 25,
77
+ "meeting time" => Time.utc(2010,"aug",10,20,15,1),
78
+ "where" => "29 Acacia Road, Nuttytown"}]
79
+ new_table.insert data
80
+
81
+ # Delete row
82
+ new_table.delete row_id
83
+ </code></pre>
84
+
85
+ h2. Fusion Tables secret Geospatial Sauce
86
+
87
+ *"Geolocated Tweets example":http://tables.googlelabs.com/DataSource?snapid=73106*
88
+
89
+ Fusion Tables is a labs product from Google. You can "read more here":http://tables.googlelabs.com/, but the key thing is that it gives you *access to the google tile mill for fast generation of google map layers across large datasets*
90
+
91
+ Fusion Tables supports the following geometry types:
92
+
93
+ * lat/long
94
+ * addresses (automatically geocodes them for you)
95
+ * KML (point, polyline, polygon, multipolygon)
96
+
97
+ h2. Integrate with google maps v3
98
+
99
+ Adding a fusion tables datalayer with many points/polygons to your v3 map is as simple as:
100
+
101
+ bc. layer = new google.maps.FusionTablesLayer(139529);
102
+
103
+ That's it
104
+
105
+ You can also refine the tiles by SQL, and can even do so dynamically:
106
+
107
+ <pre><code>
108
+ layer = new google.maps.FusionTablesLayer(198945, {
109
+ query: "SELECT address FROM 198945 WHERE ridership > 5000"}
110
+ );
111
+ </code></pre>
112
+
113
+ Finally, fusion tables also lets you make Heatmaps
114
+
115
+ <pre><code>
116
+ layer = new google.maps.FusionTablesLayer(136705, {
117
+ heatmap: true
118
+ });
119
+ </code></pre>
120
+
121
+ You can also export your data (filtered and geocoded) to KML. As an example, here are "all the Gasoline filling stations in the UK":http://tables.googlelabs.com/exporttable?query=select+col0%2Ccol1%2Ccol2%2Ccol3%2Ccol4%2Ccol5%2Ccol6%2Ccol12%2Ccol13%2Ccol14%2Ccol15%2Ccol16%2Ccol17%2Ccol18%2Ccol19%2Ccol20%2Ccol21+from+214045+&o=kmllink&g=col0
122
+
123
+ read "more here":http://code.google.com/apis/maps/documentation/javascript/overlays.html#FusionTables
124
+
125
+ h2. Known Issues
126
+
127
+ # The gem uses the Google gdata_19 gem which conflicts with the GData2 gem. Uninstall gdata2 to regain sanity.
128
+ # Currently you have to make a table public before you can display it on a map, unfortunately, this can only be done on the web interface. A suggested workaround is to put all your data in 1 big public table, and then query for the data you want to display based off a key/flag column
129
+
130
+ h2. Note on Patches/Pull Requests
131
+
132
+ * Fork the project.
133
+ * Make your feature addition or bug fix.
134
+ * Add tests for it. This is important so I don't break it in a
135
+ future version unintentionally.
136
+ * Commit, do not mess with rakefile, version, or history.
137
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
138
+ * Send me a pull request. Bonus points for topic branches.
139
+
140
+ h2. Copyright
141
+
142
+ Largely based on Tom Verbeure's work for MTBGuru: http://code.google.com/p/mtbguru-fusiontables/
143
+
144
+ Copyright (c) 2010 Tom Verbeure, Simon Tokumine. See LICENSE for details.
145
+
146
+
147
+
148
+
@@ -0,0 +1,54 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "oozou-fusion_tables"
8
+ gem.summary = %Q{Google Fusion Tables API wrapper}
9
+ gem.description = %Q{A simple Google Fusion Tables API wrapper. Supports bulk inserts and most API functions}
10
+ gem.email = "simon@tinypla.net"
11
+ gem.homepage = "http://github.com/oozou/fusion-tables"
12
+ gem.authors = ["Simon Tokumine", "Tom Verbeure"]
13
+ gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
14
+ gem.add_dependency "gdata_19", ">= 1.1.2"
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
20
+ end
21
+
22
+ require 'rake/testtask'
23
+ Rake::TestTask.new(:test) do |test|
24
+ test.libs << 'lib' << 'test'
25
+ test.pattern = 'test/**/test_*.rb'
26
+ test.verbose = true
27
+ end
28
+
29
+ begin
30
+ require 'rcov/rcovtask'
31
+ Rcov::RcovTask.new do |test|
32
+ test.libs << 'test'
33
+ test.pattern = 'test/**/test_*.rb'
34
+ test.verbose = true
35
+ end
36
+ rescue LoadError
37
+ task :rcov do
38
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
39
+ end
40
+ end
41
+
42
+ task :test => :check_dependencies
43
+
44
+ task :default => :test
45
+
46
+ require 'rake/rdoctask'
47
+ Rake::RDocTask.new do |rdoc|
48
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
49
+
50
+ rdoc.rdoc_dir = 'rdoc'
51
+ rdoc.title = "fusion_tables #{version}"
52
+ rdoc.rdoc_files.include('README*')
53
+ rdoc.rdoc_files.include('lib/**/*.rb')
54
+ end
data/TODO ADDED
@@ -0,0 +1,11 @@
1
+ Areas of improvement:
2
+
3
+ * response format json/xml
4
+ * "create table" should return name
5
+ * describe table should include name
6
+ * multiple batch statements for all actions (only insert now). Need on all other commands. Better SQL.
7
+ * docs need updating, esp on response codes
8
+ * response codes need to be more standardised
9
+ * irritating can't include ROWID with * in selects
10
+ * add columns
11
+ * better heatmaps - scale is wonky
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.3.dev.20110408163600
@@ -0,0 +1,159 @@
1
+ # This library creates a FT and posts data from the Boris Bikes API to it every minute
2
+ #
3
+ # Add photos to infowindow
4
+ # Add fusion table graphs to the inside of the infowindows too
5
+
6
+
7
+ require 'net/http'
8
+ require 'uri'
9
+ require 'rubygems'
10
+ require 'geo_ruby'
11
+ require 'fusion_tables'
12
+ require 'time'
13
+ require 'json'
14
+ require 'yaml'
15
+ require 'ap'
16
+ include GeoRuby
17
+ include SimpleFeatures
18
+
19
+ class Object
20
+ def try(method, *args, &block)
21
+ send(method, *args, &block)
22
+ end
23
+ end
24
+
25
+ def color max, number_to_color, min=0, opacity=80
26
+ color = ["FFFFB2", "FFFFB2", "FEB24C", "FD8D3C", "F03B20", "BD0026"]
27
+ #color = %w(FEE0D2 FCBBA1 FC9272 FB6A4A EF3B2C CB181D A50F15 67000D)
28
+ color.reverse!
29
+ #color = ["FFFFCC", "D9F0A3", "ADDD8E", "78C679", "31A354", "31A354"] #<- greens
30
+ chunk = (max-min)/color.size
31
+ index = (number_to_color/chunk).floor
32
+ "#{color[index]}#{opacity}"
33
+ end
34
+
35
+ def to_google(x,y)
36
+ a = `echo "#{x} #{y}" | cs2cs + +init=epsg:4326 +to +init=epsg:3785 -f "%.12f"`
37
+ a = a.split(" ")
38
+ {:x => a[0], :y => a[1]}
39
+ end
40
+
41
+ def from_google(x,y)
42
+ a = `echo "#{x} #{y}" | cs2cs + +init=epsg:3785 +to +init=epsg:4326 -f "%.12f"`
43
+ a = a.split(" ")
44
+ {:x => a[0], :y => a[1]}
45
+ end
46
+
47
+
48
+ def buffer(center_x, center_y, radius, quality = 4, precision = 12)
49
+ points = []
50
+ radians = Math::PI / 180
51
+
52
+ coords = to_google(center_x, center_y)
53
+ center_x = coords[:x].to_f
54
+ center_y = coords[:y].to_f
55
+
56
+ 0.step(360, quality) do |i|
57
+ x = center_x + (radius * Math.cos(i * radians))
58
+ y = center_y + (radius * Math.sin(i * radians))
59
+ coords = from_google(x,y)
60
+ points << Point.from_x_y(round(coords[:x].to_f, precision), round(coords[:y].to_f, precision))
61
+ end
62
+ points
63
+ end
64
+
65
+
66
+ def round number, precision = 12
67
+ (number * 10**precision).round.to_f / 10**precision
68
+ end
69
+
70
+ # Configure settings
71
+ config = YAML::load_file(File.join(File.dirname(__FILE__), 'credentials.yml'))
72
+ DEFAULT_SRID = 4328
73
+
74
+
75
+ # Configure fusion tables
76
+ ft = GData::Client::FusionTables.new
77
+ ft.clientlogin(config["google_username"], config["google_password"])
78
+ table_name = "Boris Bikes"
79
+ cols = [
80
+ {:name => 'name', :type => 'string'},
81
+ {:name => 'created_at', :type => 'datetime'},
82
+ {:name => 'updated_at', :type => 'datetime'},
83
+ {:name => 'boris_id', :type => 'number'},
84
+ {:name => 'temporary', :type => 'number'},
85
+ {:name => 'installed', :type => 'number'},
86
+ {:name => 'locked', :type => 'number'},
87
+ {:name => 'nb_empty_docs',:type => 'number'},
88
+ {:name => 'nb_bikes', :type => 'number'},
89
+ {:name => 'nb_docs', :type => 'number'},
90
+ {:name => 'image', :type => 'string'},
91
+ {:name => 'geom', :type => 'location'},
92
+ {:name => 'geom_fill', :type => 'string'},
93
+ {:name => 'geom_border', :type => 'string'},
94
+ ]
95
+
96
+ # Create FT if it doesn't exist
97
+ tables = ft.show_tables
98
+ table = tables.select{|t| t.name == table_name}.first
99
+ table = ft.create_table(table_name, cols) if !table
100
+
101
+ while true do
102
+ bikes = JSON.parse(Net::HTTP.get(URI.parse('http://borisapi.heroku.com/stations.json')))
103
+
104
+ # get largest bike rack to calibrate buffer
105
+ max = 0
106
+ bikes.each do |b|
107
+ slots = b["nb_empty_docks"] + b["nb_bikes"]
108
+ max = slots if slots > max
109
+ end
110
+
111
+ # loop through data constructing fusion table date
112
+ data = []
113
+ max_radius = 150.0 #in meters
114
+ buffer_chunk = max_radius / max
115
+
116
+ bikes.each do |b|
117
+ if b["lat"].to_f > 50 #ignore non geographic ones
118
+ docs = (b["nb_bikes"] + b["nb_empty_docks"])
119
+ geom = Polygon.from_points [buffer(b["long"].to_f, b["lat"].to_f, docs*buffer_chunk)]
120
+ #geom = Point.from_x_y b["long"].to_f, b["lat"].to_f
121
+
122
+ data << {
123
+ "name" => b["name"],
124
+ "created_at" => Time::parse(b["created_at"]),
125
+ "updated_at" => Time::parse(b["updated_at"]),
126
+ "boris_id" => b["id"],
127
+ "temporary" => (b["temporary"] ? 1 : 0),
128
+ "installed" => (b["installed"] ? 1 : 0),
129
+ "locked" => (b["locked"] ? 1 : 0),
130
+ "nb_empty_docs" => b["nb_empty_docks"],
131
+ "nb_bikes" => b["nb_bikes"],
132
+ "nb_docs" => docs,
133
+ "image" => "",
134
+ "geom" => geom.as_kml,
135
+ "geom_fill" => color(max,b["nb_bikes"]),
136
+ "geom_border" => color(max,b["nb_bikes"],0,"FF"),
137
+ }
138
+ puts "packing data for #{b["name"]}"
139
+ end
140
+ end
141
+
142
+ # get current number of rows ready to delete
143
+ row_ids = table.select "ROWID"
144
+
145
+ # put new data up
146
+ puts "sending bikes to fusion tables..."
147
+ table.insert data
148
+
149
+ # remove old data
150
+ puts "deleting old rows"
151
+ row_ids.each do |id|
152
+ table.delete id[:rowid]
153
+ end
154
+
155
+ # Be nice and wait
156
+ puts "...done! sleeping..."
157
+ sleep 500
158
+ end
159
+
@@ -0,0 +1,143 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Twitter Fusion Tables Mashup
4
+ # S.Tokumine 2010
5
+ #
6
+ # looks for tweets in the live stream around certain
7
+ # cities and posts them to fusion tables with KML attached
8
+ #
9
+ # Gem dependencies:
10
+ #
11
+ # tweetstream
12
+ # GeoRuby
13
+ # fusion_tables
14
+ #
15
+ # Output from running this for an evening
16
+ # http://tables.googlelabs.com/DataSource?snapid=72509
17
+ #
18
+ require 'rubygems'
19
+ require 'tweetstream'
20
+ require 'geo_ruby'
21
+ include GeoRuby
22
+ include SimpleFeatures
23
+ require 'fusion_tables'
24
+ require 'time'
25
+ require 'yaml'
26
+
27
+ class Object
28
+ def try(method, *args, &block)
29
+ send(method, *args, &block)
30
+ end
31
+ end
32
+
33
+ # Configure settings
34
+ config = YAML::load_file(File.join(File.dirname(__FILE__), 'credentials.yml'))
35
+ DEFAULT_SRID = 4328
36
+
37
+ # Twitter places
38
+ places = {
39
+ :san_francisco => [-122.75,36.8,-121.75,37.8],
40
+ :new_york => [-74,40,-73,41],
41
+ :tokyo => [139.3,35,140.3,36],
42
+ :london => [-0.54,51.2,0.46,52.2],
43
+ :madrid => [-4.2,40,-3.2,41],
44
+ :paris => [1.75,48.5,2.75, 49.5],
45
+ :beijing => [115.9,39,116.9,40],
46
+ :mumbai => [72.75,18.88,73.75,19.88],
47
+ }
48
+
49
+ # Configure fusion tables
50
+ ft = GData::Client::FusionTables.new
51
+ ft.clientlogin(config["google_username"], config["google_password"])
52
+ table_name = "TwitterFusion"
53
+ cols = [
54
+ {:name => 'screen_name', :type => 'string'},
55
+ {:name => 'avatar', :type => 'string'},
56
+ {:name => 'text', :type => 'string'},
57
+ {:name => 'created', :type => 'datetime'},
58
+ {:name => 'url', :type => 'string'},
59
+ {:name => 'location', :type => 'location'},
60
+ {:name => 'iso', :type => 'location'},
61
+ {:name => 'country_name', :type => 'location'},
62
+ {:name => 'city', :type => 'string'}
63
+ ]
64
+
65
+ # Create FT if it doesn't exist
66
+ tables = ft.show_tables
67
+ table = tables.select{|t| t.name == table_name}.first
68
+ table = ft.create_table(table_name, cols) if !table
69
+
70
+ # Configure Twitter stream client
71
+ data = []
72
+ tw = TweetStream::Client.new(config["twitter_username"],config["twitter_password"])
73
+
74
+ # configure friendly rate limit handling
75
+ tw.on_limit do |skip_count|
76
+ sleep 5
77
+ end
78
+
79
+ # start searching twitter stream and posting to FT
80
+ tw.filter(:locations => places.values.join(",")) do |tweet|
81
+ begin
82
+
83
+ country = "unknown"
84
+ iso = "unknown"
85
+ begin
86
+ country = tweet.try(:[],:place).try(:[], :country)
87
+ iso = tweet.try(:[],:place).try(:[], :country_code)
88
+ rescue
89
+ end
90
+
91
+ # Divine the tweets geometry
92
+ #
93
+ # overly complex due to
94
+ # * some US tweets have their lat/longs flipped (but not all...)
95
+ # * some geo tweets are made using a "place" envelope rather than exact lat/kng
96
+ if tweet[:geo]
97
+ if iso == 'US' && tweet[:geo][:coordinates][1] > 0
98
+ p = Point.from_x_y(tweet[:geo][:coordinates][0],tweet[:geo][:coordinates][1])
99
+ else
100
+ p = Point.from_x_y(tweet[:geo][:coordinates][1],tweet[:geo][:coordinates][0])
101
+ end
102
+ else
103
+ p = Polygon.from_coordinates(tweet[:place][:bounding_box][:coordinates]).envelope.center
104
+ end
105
+
106
+ # work out which city the tweet is from by testing with an extended bounding box
107
+ # BBox extention needed as twitter returns things outside our defined bboxes...
108
+ city = "unknown"
109
+ places.each do |key, value|
110
+ if !(p.x < value[0]-1 || p.x > value[2]+1 || p.y < value[1]-1 || p.y > value[3]+1)
111
+ city = key.to_s.gsub("_"," ")
112
+ break
113
+ end
114
+ end
115
+
116
+ # pack data
117
+ data << {
118
+ "screen_name" => tweet[:user][:screen_name],
119
+ "avatar" => tweet[:user][:profile_image_url],
120
+ "text" => tweet[:text],
121
+ "created" => Time.parse(tweet[:created_at]),
122
+ "url" => "http://twitter.com/#{tweet[:user][:screen_name]}/status/#{tweet[:id]}",
123
+ "location" => p.as_kml,
124
+ "iso" => iso,
125
+ "country_name" => country,
126
+ "city" => city
127
+ }
128
+ rescue => e
129
+ puts "ERROR: #{e.inspect}, #{e.backtrace}"
130
+ #let sleeping dogs lie...
131
+ end
132
+
133
+ # let us know how we're doing
134
+ puts "#{50-data.size}: #{city}, #{tweet.text}"
135
+
136
+ # Post to fusion tables
137
+ if data.size == 50
138
+ puts "sending data to fusion tables..."
139
+ ft_data = data
140
+ data = []
141
+ table.insert ft_data
142
+ end
143
+ end