oozou-fusion_tables 0.2.3.dev.20110408163600

Sign up to get free protection for your applications and to get access to all the features.
@@ -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