barton 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +12 -5
- data/README.md +78 -45
- data/Rakefile +2 -3
- data/TODO.md +16 -0
- data/barton.gemspec +5 -2
- data/bin/barton +17 -2
- data/config.ru +2 -1
- data/data/nsw-state.yaml +2 -0
- data/data/qld-state.yaml +133 -5
- data/lib/barton.rb +14 -5
- data/lib/barton/app.rb +45 -19
- data/lib/barton/core.rb +158 -101
- data/lib/barton/docs.html +173 -0
- data/lib/barton/version.rb +1 -1
- data/specs/data/data-processed.yaml +19 -13
- data/specs/data/data-raw.yaml +10 -13
- data/specs/data/geo/data-processed.yaml +7 -7
- data/specs/data/geo/data-raw.yaml +4 -4
- data/specs/data/master/data-processed.yaml +19 -13
- data/specs/data/master/data-raw.yaml +7 -8
- data/specs/data/master/geo.yaml +7 -7
- data/specs/find_spec.rb +80 -0
- data/specs/route_spec.rb +21 -7
- data/specs/search_spec.rb +18 -39
- data/specs/setup_spec.rb +31 -30
- metadata +59 -3
data/lib/barton.rb
CHANGED
@@ -2,12 +2,21 @@ require "barton/core"
|
|
2
2
|
|
3
3
|
module Barton
|
4
4
|
class << self
|
5
|
-
def
|
6
|
-
@@
|
5
|
+
def environment
|
6
|
+
@@environment = ENV['BARTON'] || 'development'
|
7
7
|
end
|
8
|
-
|
9
|
-
def
|
10
|
-
|
8
|
+
|
9
|
+
def environment=(env)
|
10
|
+
ENV['BARTON'] = env
|
11
|
+
@@environment = env
|
12
|
+
end
|
13
|
+
|
14
|
+
def api_url
|
15
|
+
@@api_url ||= 'http://localhost:4567'
|
16
|
+
end
|
17
|
+
|
18
|
+
def api_url=(env)
|
19
|
+
@@api_url = env
|
11
20
|
end
|
12
21
|
end
|
13
22
|
end
|
data/lib/barton/app.rb
CHANGED
@@ -1,20 +1,22 @@
|
|
1
1
|
require 'sinatra'
|
2
2
|
require 'json'
|
3
|
+
require 'uri'
|
3
4
|
require 'barton'
|
4
5
|
require 'barton/core'
|
5
6
|
|
6
|
-
module Barton
|
7
|
+
module Barton
|
7
8
|
class App < Sinatra::Base
|
8
|
-
|
9
|
-
# routes
|
9
|
+
|
10
|
+
# routes
|
10
11
|
allowed_formats = ['', 'json']
|
11
12
|
|
12
|
-
|
13
|
-
|
13
|
+
# helpers
|
14
|
+
before do
|
15
|
+
Barton.api_url = "#{request.base_url }"
|
14
16
|
end
|
15
17
|
|
16
|
-
get '/
|
17
|
-
|
18
|
+
get '/' do
|
19
|
+
File.read( 'lib/barton/docs.html' )
|
18
20
|
end
|
19
21
|
|
20
22
|
# electorate resource
|
@@ -23,10 +25,12 @@ module Barton
|
|
23
25
|
geo, tags, address = params[:geo], params[:tags], params[:address]
|
24
26
|
tags = tags.split(',') unless tags.nil?
|
25
27
|
raise Sinatra::NotFound unless allowed_formats.include?( format )
|
26
|
-
geo = Barton::Data.address( address ) if address
|
27
28
|
if not id.empty?
|
28
29
|
results = Barton.electorates( {:id => id} )
|
29
30
|
prepare_response( results )
|
31
|
+
elsif address
|
32
|
+
results = Barton.electorates( {:tags => tags, :address => address} )
|
33
|
+
prepare_response( results )
|
30
34
|
elsif geo or tags
|
31
35
|
results = Barton.electorates( {:tags => tags, :geo => geo} )
|
32
36
|
prepare_response( results )
|
@@ -35,31 +39,53 @@ module Barton
|
|
35
39
|
end
|
36
40
|
end
|
37
41
|
|
42
|
+
# electorate resource
|
43
|
+
get %r{^/api/members/?(\w*)/?.?([A-z]*)} do
|
44
|
+
id, format = params[:captures]
|
45
|
+
geo, tags, address = params[:geo], params[:tags], params[:address]
|
46
|
+
tags = tags.split(',') unless tags.nil?
|
47
|
+
raise Sinatra::NotFound unless allowed_formats.include?( format )
|
48
|
+
if not id.empty?
|
49
|
+
results = Barton.members( {:id => id} )
|
50
|
+
prepare_response( results )
|
51
|
+
elsif address
|
52
|
+
results = Barton.members( {:tags => tags, :address => address} )
|
53
|
+
prepare_response( results )
|
54
|
+
elsif geo or tags
|
55
|
+
results = Barton.members( {:tags => tags, :geo => geo} )
|
56
|
+
prepare_response( results )
|
57
|
+
else
|
58
|
+
prepare_response
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
38
63
|
# base api
|
39
64
|
get %r{^/api/?.?([A-z]*)$} do
|
40
65
|
prepare_response
|
41
66
|
end
|
42
|
-
|
67
|
+
|
43
68
|
# views
|
44
|
-
|
69
|
+
|
45
70
|
# Prepares and array of results for the API response
|
46
71
|
def prepare_response( args=nil )
|
47
72
|
# add generic API meta data
|
48
|
-
app_url = 'http://barton.experimentsindemocracy.org'
|
49
73
|
response = {}
|
50
74
|
response[:name] = "Barton API"
|
51
|
-
response[:disclaimer] = "This data is crowded sourced and provided free of charge for informational purposes only. No guarantees
|
75
|
+
response[:disclaimer] = "This data is crowded sourced and provided free of charge for informational purposes only. No guarantees regarding data quality are made whatsoever apart from it's rather considerable coolness."
|
52
76
|
response[:license] = "MIT License http://www.opensource.org/licenses/mit-license.php"
|
53
77
|
unless args.nil?
|
54
|
-
response[:result_count] = args.length
|
78
|
+
response[:result_count] = args.length
|
55
79
|
response[:results] = args
|
80
|
+
status 404 if args.length == 0
|
56
81
|
end
|
57
|
-
response[:resources] = { :home =>
|
58
|
-
response[:examples] = {
|
59
|
-
:
|
60
|
-
:geo => "#{
|
61
|
-
:tags => "#{
|
62
|
-
:mixed => "#{
|
82
|
+
response[:resources] = { :home => Barton.api_url, :api => "#{Barton.api_url}/api", :electorates => "#{Barton.api_url}/api/electorates", :members => "#{Barton.api_url}/api/members" }
|
83
|
+
response[:examples] = {
|
84
|
+
:id => "#{Barton.api_url}/api/members/0db2ec",
|
85
|
+
:geo => "#{Barton.api_url}/api/electorates?geo=151.2054563,-33.8438383",
|
86
|
+
:tags => "#{Barton.api_url}/api/members?tags=sydney,local",
|
87
|
+
:mixed => "#{Barton.api_url}/api/electorates?geo=151.2054563,-33.8438383&tags=federal",
|
88
|
+
}
|
63
89
|
# format
|
64
90
|
content_type 'application/json;charset=utf-8'
|
65
91
|
JSON.pretty_generate( response )
|
data/lib/barton/core.rb
CHANGED
@@ -5,7 +5,7 @@ require 'json'
|
|
5
5
|
require 'barton/app'
|
6
6
|
|
7
7
|
module Barton
|
8
|
-
|
8
|
+
|
9
9
|
# Returns an array of electorates matching the search criteria
|
10
10
|
# Accepts a hash of criteria
|
11
11
|
# :id
|
@@ -13,50 +13,75 @@ module Barton
|
|
13
13
|
# :geo
|
14
14
|
# :address
|
15
15
|
# Returns an array of hashes
|
16
|
-
def
|
16
|
+
def self.electorates( query = {} )
|
17
17
|
Find.electorates( query )
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
# Returns an array of member matching the search criteria
|
21
21
|
# Accepts a hash of criteria
|
22
|
-
# Returns an array of hashes
|
23
|
-
def members
|
24
|
-
|
22
|
+
# Returns an array of hashes
|
23
|
+
def self.members( query={} )
|
24
|
+
Find.members( query )
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
# Loads electoral data from the yaml files into elasticsearch
|
28
28
|
def self.setup
|
29
|
-
Data.config
|
30
29
|
Data.purge_es
|
31
30
|
Dir['data/*.yaml'].each do |f|
|
32
31
|
Setup.load_file( f )
|
33
32
|
end
|
34
|
-
Barton.data_loaded = true
|
35
33
|
end
|
36
|
-
|
37
|
-
# Configuration options for the Gem
|
34
|
+
|
38
35
|
def self.config
|
39
|
-
|
40
|
-
#Data.config
|
36
|
+
Data.config
|
41
37
|
end
|
42
|
-
|
38
|
+
|
43
39
|
module Find
|
44
|
-
|
40
|
+
def self.electorates( query )
|
41
|
+
docs = Find.documents( query, 'electorate' )
|
42
|
+
# remove unwanted fields
|
43
|
+
docs.each do |e|
|
44
|
+
e.keys.each { |k| e.delete( k ) if k.match( /_|geobox|boundaries|highlight|sort/ ) }
|
45
|
+
e[:url] = "#{Barton.api_url}/api/electorates/#{e[:id]}"
|
46
|
+
members = Array.new
|
47
|
+
if e.has_key?( :members )
|
48
|
+
e[:members].each do |m|
|
49
|
+
m = m.to_hash
|
50
|
+
m[:url] = "#{Barton.api_url}/api/members/#{m[:id]}"
|
51
|
+
m.delete( :id )
|
52
|
+
members.push( m )
|
53
|
+
end
|
54
|
+
end
|
55
|
+
e[:members] = members if members.length > 0
|
56
|
+
end
|
57
|
+
docs
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.members( query )
|
61
|
+
results = Array.new
|
62
|
+
docs = Find.documents( query, 'member' )
|
63
|
+
docs.each do |m|
|
64
|
+
m.keys.each { |k| m.delete( k ) if k.match( /_|highlight|sort/ ) }
|
65
|
+
m[:url] = "#{Barton.api_url}/api/members/#{m[:id]}"
|
66
|
+
e = m[:electorate].to_hash
|
67
|
+
e[:url] = "#{Barton.api_url}/api/electorates/#{e[:id]}"
|
68
|
+
e.delete( :id )
|
69
|
+
m[:electorate] = e
|
70
|
+
end
|
71
|
+
docs
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.documents( query={}, type=nil )
|
45
75
|
results = Array.new
|
46
76
|
terms = ["id:#{query[:id]}"] if query.has_key?( :id )
|
47
77
|
terms = query[:tags] if query.has_key?( :tags )
|
48
78
|
geo = query[:geo] if query.has_key?( :geo )
|
49
79
|
geo = self.address( query[:address] ) if query.has_key?( :address ) and not query.has_key?( :geo )
|
50
|
-
s = Data.search( terms, geo )
|
51
|
-
s.results.each
|
52
|
-
e = e.to_hash
|
53
|
-
e.keys.each { |k| e.delete( k ) if k.match( /_|geobox|boundaries|highlight|sort/ ) }
|
54
|
-
e[:url] = "#{@api_url}/electorates/#{e[:id]}"
|
55
|
-
results.push( e )
|
56
|
-
end
|
80
|
+
s = Data.search( terms, geo, type )
|
81
|
+
s.results.each { |e| results.push( e.to_hash ) }
|
57
82
|
results
|
58
|
-
|
59
|
-
|
83
|
+
end
|
84
|
+
|
60
85
|
# Geocode lookup to google
|
61
86
|
def self.address( address )
|
62
87
|
# http://maps.googleapis.com/maps/api/geocode/json?address=address&sensor=false®ion=au
|
@@ -73,7 +98,7 @@ module Barton
|
|
73
98
|
puts e
|
74
99
|
end
|
75
100
|
end
|
76
|
-
|
101
|
+
|
77
102
|
# Ray casting algorithm to find if point is in polygon
|
78
103
|
def self.point_in_poly?( geo, boundaries )
|
79
104
|
# get pairs of points of boundaries
|
@@ -102,19 +127,93 @@ module Barton
|
|
102
127
|
ax, ay, bx, by = ax.to_f, ay.to_f, bx.to_f, by.to_f
|
103
128
|
return 0 if ay < long and by < long
|
104
129
|
return ( ( ax < lat and lat < bx ) or ( bx < lat and lat < ax ) ) ? 1 : 0
|
105
|
-
end
|
130
|
+
end
|
106
131
|
end
|
107
|
-
|
132
|
+
|
133
|
+
module Data
|
134
|
+
@index_name = 'electorates'
|
135
|
+
|
136
|
+
# Set elasticsearch config
|
137
|
+
def self.config
|
138
|
+
case Barton.environment
|
139
|
+
when 'test'
|
140
|
+
@index_name = 'test_electorates'
|
141
|
+
when ENV['FOUNDELASTICSEARCH_URL']
|
142
|
+
Tire::Configuration.url ENV['FOUNDELASTICSEARCH_URL']
|
143
|
+
@index_name = 'electorates'
|
144
|
+
else
|
145
|
+
@index_name = 'electorates'
|
146
|
+
end
|
147
|
+
#if ENV['BONSAI_INDEX_URL']
|
148
|
+
# Tire.configure do
|
149
|
+
# url "http://index.bonsai.io"
|
150
|
+
# end
|
151
|
+
# @index_name = ENV['BONSAI_INDEX_URL'][/[^\/]+$/]
|
152
|
+
#end
|
153
|
+
return Barton.environment
|
154
|
+
end
|
155
|
+
|
156
|
+
# Purge electorate data from elasticsearch
|
157
|
+
def self.purge_es
|
158
|
+
self.query_es( 'purge' )
|
159
|
+
end
|
160
|
+
|
161
|
+
# Load electorate data to elasticsearch
|
162
|
+
def self.update_es( data )
|
163
|
+
self.query_es( 'update', data )
|
164
|
+
end
|
165
|
+
|
166
|
+
# Query elasticsearch
|
167
|
+
def self.query_es( action, data=nil )
|
168
|
+
self.config
|
169
|
+
begin
|
170
|
+
Tire.index "#{@index_name}" do
|
171
|
+
delete if action == 'purge'
|
172
|
+
create if action == 'purge'
|
173
|
+
import data if action == 'update' and not data.nil?
|
174
|
+
end
|
175
|
+
rescue Exception => e
|
176
|
+
puts "Elasticsearch error: #{e}"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def self.search( terms, geo, type=nil )
|
181
|
+
self.config
|
182
|
+
s = Tire.search "#{@index_name}" do
|
183
|
+
query do
|
184
|
+
boolean do
|
185
|
+
if geo
|
186
|
+
long, lat = geo.split( ',' )
|
187
|
+
must { range :north, { :gte => lat } }
|
188
|
+
must { range :south, { :lte => lat } }
|
189
|
+
must { range :east, { :gte => long } }
|
190
|
+
must { range :west, { :lte => long } }
|
191
|
+
elsif terms
|
192
|
+
terms.each { |t| must { string t } }
|
193
|
+
end
|
194
|
+
must { string "type:#{type}" } if type
|
195
|
+
end
|
196
|
+
end
|
197
|
+
# apply filters only with geo search
|
198
|
+
filter :terms, :_all => terms if geo and terms
|
199
|
+
size 100
|
200
|
+
end
|
201
|
+
s
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
108
205
|
module Setup
|
109
206
|
# Parses a electorate yaml file, merges it with a geo yaml file,
|
110
207
|
# loads them to the datastore and resaves the yaml files
|
111
208
|
def self.parse_yaml( filename )
|
112
|
-
data =
|
209
|
+
data = Array.new
|
210
|
+
electorates = YAML::load_file( filename ) if File.exist?( filename )
|
113
211
|
geofile = "#{File.dirname( filename )}/geo/#{File.basename( filename )}"
|
114
212
|
geo = YAML::load_file( geofile ) if File.exist?( geofile )
|
115
|
-
if
|
116
|
-
|
117
|
-
|
213
|
+
if electorates.respond_to?( 'each' )
|
214
|
+
electorates.each do |e|
|
215
|
+
# id is the first 6 digits of a hash of object name with file name EG BulimbaQLD-State.yaml
|
216
|
+
e['id'] = Digest::SHA1.hexdigest( [e['name'], filename].join )[0..5].force_encoding('utf-8') unless e['id'] or not e['name']
|
118
217
|
# make geobox and update geo.yaml if boundaries present
|
119
218
|
if e.has_key?( 'boundaries')
|
120
219
|
e['geobox'] = self.create_geobox( e['boundaries'] )
|
@@ -123,14 +222,37 @@ module Barton
|
|
123
222
|
e['boundaries'] = geo[e['id']]['boundaries']
|
124
223
|
e['geobox'] = geo[e['id']]['geobox']
|
125
224
|
end
|
225
|
+
|
226
|
+
ei = e.clone # this will be the electorate that is indexed
|
227
|
+
ei['type'] = 'electorate'
|
228
|
+
|
229
|
+
if e.has_key?( 'members' )
|
230
|
+
# create seperate types for member details
|
231
|
+
members = Array.new
|
232
|
+
e['members'].each do |m|
|
233
|
+
m['role'] = "Member for #{e['name']}" unless m['role']
|
234
|
+
m['id'] = Digest::SHA1.hexdigest( [m['role'], filename].join )[0..5].force_encoding('utf-8') unless m['id'] or not m['role']
|
235
|
+
# only add this for indexing
|
236
|
+
mi = m.clone
|
237
|
+
mi['type'] = 'member'
|
238
|
+
mi['tags'] = [] unless m.has_key?( 'tags' )
|
239
|
+
mi['tags'] |= e['tags'] if e.has_key?( 'tags' )
|
240
|
+
mi['electorate'] = {'name' => e['name'], 'id' => e['id']}
|
241
|
+
data.push( mi )
|
242
|
+
members.push( {'role' => m['role'], 'name' => m['name'], 'id' => m['id']})
|
243
|
+
end
|
244
|
+
# only index limited member data
|
245
|
+
ei['members'] = members
|
246
|
+
end
|
247
|
+
data.push( ei )
|
126
248
|
end
|
127
249
|
# save parsed yaml
|
128
250
|
data_yaml = Array.new
|
129
251
|
geo_yaml = Hash.new
|
130
|
-
|
252
|
+
electorates.each do |e|
|
131
253
|
# remove geo from data
|
132
254
|
geo_yaml[e['id']] = Hash['boundaries' => e['boundaries'], 'geobox' => e['geobox']] if e.has_key?( 'boundaries' )
|
133
|
-
data_yaml.push( e.select { |k,v| not ['boundaries', 'geobox'].include? k } )
|
255
|
+
data_yaml.push( e.select { |k,v| not ['boundaries', 'geobox', 'type'].include? k } )
|
134
256
|
end
|
135
257
|
File.open( filename, 'w+' ) { |f| f.puts( YAML.dump( data_yaml ) ) }
|
136
258
|
File.open( geofile, 'w+' ) { |f| f.puts( YAML.dump( geo_yaml ) ) }
|
@@ -139,7 +261,7 @@ module Barton
|
|
139
261
|
nil
|
140
262
|
end
|
141
263
|
end
|
142
|
-
|
264
|
+
|
143
265
|
# Creates a minimum bounded box encapsulating the boundary polygons
|
144
266
|
def self.create_geobox( boundaries )
|
145
267
|
return nil unless boundaries.instance_of? Array
|
@@ -155,79 +277,14 @@ module Barton
|
|
155
277
|
end
|
156
278
|
box
|
157
279
|
end
|
158
|
-
|
280
|
+
|
159
281
|
# Load data from yaml source file
|
160
282
|
def self.load_file( filename )
|
161
|
-
puts "Loading data from #{filename}....."
|
283
|
+
puts "Loading data from #{filename} to #{Barton.environment}....."
|
162
284
|
electorates = self.parse_yaml( filename )
|
163
285
|
puts "Failed to load #{filename}" if electorates.nil?
|
164
286
|
connected = Data.update_es( electorates )
|
165
287
|
abort "Connection to Elasticsearch failed" if connected.nil?
|
166
288
|
end
|
167
289
|
end
|
168
|
-
|
169
|
-
|
170
|
-
module Data
|
171
|
-
|
172
|
-
@index_name = ENV['RAKE_ENV']
|
173
|
-
|
174
|
-
# Set elasticsearch config
|
175
|
-
def self.config
|
176
|
-
@index_name = ENV['RAKE_ENV']
|
177
|
-
if ENV['BONSAI_INDEX_URL']
|
178
|
-
Tire.configure do
|
179
|
-
url "http://index.bonsai.io"
|
180
|
-
end
|
181
|
-
@index_name = ENV['BONSAI_INDEX_URL'][/[^\/]+$/]
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
# Purge electorate data from elasticsearch
|
186
|
-
def self.purge_es
|
187
|
-
self.query_es( 'purge' )
|
188
|
-
end
|
189
|
-
|
190
|
-
# Load electorate data to elasticsearch
|
191
|
-
def self.update_es( data )
|
192
|
-
self.query_es( 'update', data )
|
193
|
-
end
|
194
|
-
|
195
|
-
# Query elasticsearch
|
196
|
-
def self.query_es( action, data=nil )
|
197
|
-
begin
|
198
|
-
Tire.index "#{@index_name}-electorates" do
|
199
|
-
delete if action == 'purge'
|
200
|
-
create if action == 'purge'
|
201
|
-
import data if action == 'update' and not data.nil?
|
202
|
-
end
|
203
|
-
rescue => e
|
204
|
-
puts e
|
205
|
-
nil
|
206
|
-
end
|
207
|
-
end
|
208
|
-
|
209
|
-
def self.search( terms, geo )
|
210
|
-
s = Tire.search "#{@index_name}-electorates" do
|
211
|
-
query do
|
212
|
-
boolean do
|
213
|
-
if geo
|
214
|
-
long, lat = geo.split( ',' )
|
215
|
-
must { range :north, { :gte => lat } }
|
216
|
-
must { range :south, { :lte => lat } }
|
217
|
-
must { range :east, { :gte => long } }
|
218
|
-
must { range :west, { :lte => long } }
|
219
|
-
elsif terms
|
220
|
-
terms.each { |t| must { string t } }
|
221
|
-
end
|
222
|
-
end
|
223
|
-
end
|
224
|
-
# apply filters only with geo search
|
225
|
-
if geo and terms
|
226
|
-
filter :terms, :_all => terms
|
227
|
-
end
|
228
|
-
size 100
|
229
|
-
end
|
230
|
-
s
|
231
|
-
end
|
232
|
-
end
|
233
290
|
end
|