itinerary 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/TODO.md +32 -0
- data/bin/itinerary +35 -0
- data/itinerary.gemspec +33 -0
- data/lib/itinerary.rb +180 -0
- data/lib/itinerary/briar_scraper.rb +91 -0
- data/lib/itinerary/cache.rb +30 -0
- data/lib/itinerary/parse_html.rb +12 -0
- data/lib/itinerary/record.rb +336 -0
- data/lib/itinerary/tool.rb +23 -0
- data/lib/itinerary/tools/convert.rb +27 -0
- data/lib/itinerary/tools/create.rb +38 -0
- data/lib/itinerary/tools/find-dups.rb +34 -0
- data/lib/itinerary/tools/import.rb +20 -0
- data/lib/itinerary/tools/list.rb +36 -0
- data/lib/itinerary/tools/scrape-briar.rb +26 -0
- data/lib/itinerary/version.rb +5 -0
- data/lib/itinerary/view.rb +26 -0
- data/lib/itinerary/views/html.rb +18 -0
- data/lib/itinerary/views/kml.rb +49 -0
- data/lib/itinerary/views/tab.rb +20 -0
- data/lib/itinerary/views/text.rb +12 -0
- metadata +223 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
MjBmNTg1NjNhMTkzNDVmODJhZDBmYTJmODI0YzNmN2E1ZmI2ODdmYg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
M2EwMTEzMzUwNTk0NWJlOTQ3OTI5NDRhZGNlYzhmOWFkZDExMWUzZg==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
YmI4OTFmYWI0MzYwMDE0NTg0MjEyMWEyZTk1M2EzODlkNjY3YmQ5MzZlMzlj
|
10
|
+
MDA4Y2U2ZDJlNWU0Yjc5NDk3NTUzMDE3N2YzMWY1ZWFjZjliN2EyYjJkMDk4
|
11
|
+
Nzg2MTI0YWQyYWUzODM2NmFjZWY3MmFlNzZjYTYyMGE0MmEzZmY=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MDQ2MTM2ZTU3NjEwMDNiOTUyMmU2NWM4YmJlMDhlNDRkMDE0MDhjMGE1MTNi
|
14
|
+
MzAyYjBkYjcxZWI5MjkwN2ViYTEwZjkzNWZkZTBlZDFjNmY5ODFjZjNlNTE4
|
15
|
+
NTA1YmVmZTRhN2QzZGUyMjJhNjMyNzJmYmRlMTRhNTZmNGM1NjY=
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 John Labovitz
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Itinerary
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'itinerary'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install itinerary
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
data/TODO.md
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
- implement coordinate fuzzing
|
2
|
+
|
3
|
+
- read all records at Itinerary instantiation
|
4
|
+
- cache (using Marshal)
|
5
|
+
- rebuild cache if new/deleted files, or files have changed
|
6
|
+
|
7
|
+
- expand routing
|
8
|
+
- multiple routes
|
9
|
+
- travel mode for each leg
|
10
|
+
- date for each node
|
11
|
+
|
12
|
+
- move briar cache to ~?
|
13
|
+
|
14
|
+
- add contact metadata?
|
15
|
+
- date/method/description
|
16
|
+
|
17
|
+
- add status/flags
|
18
|
+
- contacted, to-visit, visited
|
19
|
+
|
20
|
+
- make interactive web app
|
21
|
+
- query:
|
22
|
+
- by flags
|
23
|
+
- by location
|
24
|
+
- by name
|
25
|
+
|
26
|
+
- show private view of map/info, in addition to public view
|
27
|
+
|
28
|
+
- colorize placemark icons by status
|
29
|
+
|
30
|
+
- cluster placemarks by region to avoid clutter
|
31
|
+
http://www.google.ca/earth/outreach/tutorials/region.html
|
32
|
+
https://developers.google.com/kml/documentation/regions
|
data/bin/itinerary
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'itinerary'
|
4
|
+
|
5
|
+
require 'itinerary/tool'
|
6
|
+
|
7
|
+
require 'itinerary/tools/convert'
|
8
|
+
require 'itinerary/tools/create'
|
9
|
+
require 'itinerary/tools/find-dups'
|
10
|
+
require 'itinerary/tools/import'
|
11
|
+
require 'itinerary/tools/list'
|
12
|
+
require 'itinerary/tools/scrape-briar'
|
13
|
+
|
14
|
+
root = nil
|
15
|
+
|
16
|
+
while ARGV.first =~ /^-(\w+)/
|
17
|
+
ARGV.shift
|
18
|
+
case $1
|
19
|
+
when 'd'
|
20
|
+
root = ARGV.shift
|
21
|
+
else
|
22
|
+
raise "Unknown flag: #{ARGV.inspect}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
raise "Must specify root" unless root
|
27
|
+
|
28
|
+
itinerary = Itinerary.new(:root => root)
|
29
|
+
cmd = ARGV.shift or raise "Command not specified"
|
30
|
+
tool = itinerary.make_tool(cmd, ARGV) or raise "Unknown command: #{cmd.inspect}"
|
31
|
+
tool.run
|
32
|
+
|
33
|
+
at_exit do
|
34
|
+
itinerary.cleanup
|
35
|
+
end
|
data/itinerary.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'itinerary/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |gem|
|
8
|
+
gem.name = 'itinerary'
|
9
|
+
gem.version = Itinerary::VERSION
|
10
|
+
gem.authors = 'John Labovitz'
|
11
|
+
gem.email = 'johnl@johnlabovitz.com'
|
12
|
+
gem.summary = %q{Keep track of travel itineraries}
|
13
|
+
gem.description = %q{A Ruby gem to keep track of travel itineraries.}
|
14
|
+
gem.homepage = 'https://github.com/jslabovitz/itinerary.git'
|
15
|
+
|
16
|
+
gem.files = `git ls-files`.split($/)
|
17
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
18
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
19
|
+
gem.require_paths = ['lib']
|
20
|
+
|
21
|
+
gem.add_development_dependency 'bundler'
|
22
|
+
gem.add_development_dependency 'rake'
|
23
|
+
|
24
|
+
gem.add_runtime_dependency 'builder'
|
25
|
+
gem.add_runtime_dependency 'daybreak', '0.2.4'
|
26
|
+
gem.add_runtime_dependency 'faraday'
|
27
|
+
gem.add_runtime_dependency 'faraday_middleware'
|
28
|
+
gem.add_runtime_dependency 'geocoder'
|
29
|
+
gem.add_runtime_dependency 'hashstruct'
|
30
|
+
gem.add_runtime_dependency 'haversine'
|
31
|
+
gem.add_runtime_dependency 'nokogiri'
|
32
|
+
gem.add_runtime_dependency 'rack-cache'
|
33
|
+
end
|
data/lib/itinerary.rb
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
require 'geocoder'
|
5
|
+
require 'hashstruct'
|
6
|
+
require 'haversine'
|
7
|
+
require 'builder'
|
8
|
+
require 'daybreak'
|
9
|
+
|
10
|
+
require 'itinerary/version'
|
11
|
+
require 'itinerary/record'
|
12
|
+
require 'itinerary/view'
|
13
|
+
require 'itinerary/views/html'
|
14
|
+
require 'itinerary/views/kml'
|
15
|
+
require 'itinerary/views/tab'
|
16
|
+
require 'itinerary/views/text'
|
17
|
+
# require 'itinerary/briar_scraper'
|
18
|
+
|
19
|
+
class Itinerary
|
20
|
+
|
21
|
+
DefaultRadius = 100
|
22
|
+
|
23
|
+
attr_accessor :name
|
24
|
+
attr_accessor :root
|
25
|
+
attr_accessor :route
|
26
|
+
|
27
|
+
def initialize(options={})
|
28
|
+
@name = options[:name]
|
29
|
+
@root = options[:root] or raise "Must specify root"
|
30
|
+
@root = Pathname.new(@root).expand_path
|
31
|
+
@geocoding_cache_path = options[:geocoding_cache] || '.geocoding-cache'
|
32
|
+
setup_geocoding_cache
|
33
|
+
@entries = []
|
34
|
+
read_entries
|
35
|
+
read_routes
|
36
|
+
end
|
37
|
+
|
38
|
+
def cleanup
|
39
|
+
@geocoding_cache.close
|
40
|
+
end
|
41
|
+
|
42
|
+
def entries_path
|
43
|
+
@root + 'entries'
|
44
|
+
end
|
45
|
+
|
46
|
+
def route_path
|
47
|
+
@root + 'route'
|
48
|
+
end
|
49
|
+
|
50
|
+
def read_entries
|
51
|
+
;;warn "[initialize] reading entries from #{entries_path}"
|
52
|
+
entries_path.find do |path|
|
53
|
+
import_entry(path) if path.file? && path.basename.to_s[0] != '.'
|
54
|
+
end
|
55
|
+
sort_entries
|
56
|
+
;;warn "[initialize] read #{@entries.length} entries"
|
57
|
+
end
|
58
|
+
|
59
|
+
def sort_entries
|
60
|
+
@entries.sort_by! { |r| r.visited || r.contacted || DateTime.now }
|
61
|
+
end
|
62
|
+
|
63
|
+
def import_entry(path)
|
64
|
+
@entries << Record.load(path)
|
65
|
+
end
|
66
|
+
|
67
|
+
def read_routes
|
68
|
+
;;warn "[initialize] reading route from #{route_path}"
|
69
|
+
if route_path.exist?
|
70
|
+
@route = route_path.readlines.map { |p| geocode_search(p) }
|
71
|
+
end
|
72
|
+
;;warn "[initialize] read #{@route.length} legs of route"
|
73
|
+
end
|
74
|
+
|
75
|
+
def setup_geocoding_cache
|
76
|
+
@geocoding_cache = Daybreak::DB.new(@geocoding_cache_path)
|
77
|
+
@geocoding_cache.compact
|
78
|
+
Geocoder.configure(:cache => @geocoding_cache)
|
79
|
+
end
|
80
|
+
|
81
|
+
def geocode_search(place)
|
82
|
+
begin
|
83
|
+
results = Geocoder.search(place)
|
84
|
+
@geocoding_cache.flush
|
85
|
+
result = results.first or raise "No geocoding result for place #{place.inspect}"
|
86
|
+
HashStruct.new(
|
87
|
+
:city => result.city,
|
88
|
+
:state => result.state_code,
|
89
|
+
:country => result.country_code,
|
90
|
+
:latitude => result.coordinates[0],
|
91
|
+
:longitude => result.coordinates[1])
|
92
|
+
rescue => e
|
93
|
+
warn "Error when geocoding place #{place.inspect}: #{e}"
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def make_tool(cmd, args)
|
99
|
+
if (klass = Tool.find_tool(cmd))
|
100
|
+
klass.new(self, args)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def entries(filters=nil)
|
105
|
+
matched = @entries.dup
|
106
|
+
;;warn "[entries] filtering #{matched.length} entries: #{filters.inspect}"
|
107
|
+
if filters
|
108
|
+
filters.each do |key, value|
|
109
|
+
case key
|
110
|
+
when :near
|
111
|
+
coordinates = case value
|
112
|
+
when Array
|
113
|
+
value
|
114
|
+
when Pathname
|
115
|
+
rec = self[value] or raise "Record #{value.inspect} not found"
|
116
|
+
raise "#{rec.path} is not geocoded" unless rec.geocoded?
|
117
|
+
rec.coordinates
|
118
|
+
when String
|
119
|
+
results = Geocoder.search(value)
|
120
|
+
result = results.first or raise "Can't find location #{value.inspect}"
|
121
|
+
result.coordinates
|
122
|
+
end
|
123
|
+
raise "No coordinates found for #{value.inspect}" unless coordinates
|
124
|
+
matched.select! { |r| r.near(coordinates, filters[:radius] || DefaultRadius) }
|
125
|
+
when :radius
|
126
|
+
# ignored here -- used above
|
127
|
+
when :flags
|
128
|
+
matched.select! do |rec|
|
129
|
+
value.find { |v| rec.method("#{v}?").call }
|
130
|
+
end
|
131
|
+
else
|
132
|
+
raise "Unknown field: #{key.inspect}" unless Record.field(key) || Record.instance_methods.include?(key)
|
133
|
+
matched.select! { |r| r[key] == value }
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
;;warn "[entries] filtered #{matched.length} entries: #{filters.inspect}"
|
138
|
+
matched
|
139
|
+
end
|
140
|
+
|
141
|
+
def near(coords, radius)
|
142
|
+
matches = {}
|
143
|
+
@entries.each do |rec|
|
144
|
+
if (distance = rec.near(coords, radius))
|
145
|
+
matches[distance] ||= []
|
146
|
+
matches[distance] << rec
|
147
|
+
end
|
148
|
+
end
|
149
|
+
matches
|
150
|
+
end
|
151
|
+
|
152
|
+
def [](path)
|
153
|
+
@entries.find { |r| r.path == path }
|
154
|
+
end
|
155
|
+
|
156
|
+
def parse_params(params)
|
157
|
+
filters = HashStruct.new
|
158
|
+
options = HashStruct.new
|
159
|
+
params.select.each do |key, value|
|
160
|
+
case key.to_sym
|
161
|
+
when :near
|
162
|
+
filters.near = value
|
163
|
+
when :radius
|
164
|
+
filters.radius = value.to_f
|
165
|
+
when :flags
|
166
|
+
filters.flags = value.split(',').map { |f| f.to_sym }
|
167
|
+
when :show_fields
|
168
|
+
options.show_fields = value.split(',').map { |f| f.to_sym }
|
169
|
+
when :hide_fields
|
170
|
+
options.hide_fields = value.split(',').map { |f| f.to_sym }
|
171
|
+
# when :fuzz
|
172
|
+
# options.fuzz_placemarks = value
|
173
|
+
else
|
174
|
+
filters[key.to_sym] = value
|
175
|
+
end
|
176
|
+
end
|
177
|
+
[filters, options]
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'faraday_middleware'
|
3
|
+
require 'rack/cache'
|
4
|
+
|
5
|
+
require 'itinerary/record'
|
6
|
+
require 'parse_html'
|
7
|
+
require 'cache'
|
8
|
+
|
9
|
+
class BriarScraper
|
10
|
+
|
11
|
+
CacheDir = Pathname.new(__FILE__).dirname + '../cache'
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@cache = Cache.new(CacheDir)
|
15
|
+
@base_uri = URI.parse('http://www.briarpress.org')
|
16
|
+
@conn = Faraday.new(:url => @base_uri) do |c|
|
17
|
+
# c.use Faraday::Response::Logger
|
18
|
+
c.use FaradayMiddleware::ParseHTML
|
19
|
+
c.use FaradayMiddleware::Caching, @cache
|
20
|
+
c.adapter Faraday.default_adapter
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def scrape_state(state)
|
25
|
+
# ;;warn "Scraping state #{state.inspect}"
|
26
|
+
scrape_state_with_uri(uri_for_state(state))
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def scrape_state_with_uri(state_uri, options={})
|
32
|
+
follow = options.has_key?(:follow) ? options[:follow] : true
|
33
|
+
# ;;warn "Scraping state URI #{state_uri.inspect} (follow = #{follow.inspect})"
|
34
|
+
resp = @conn.get(state_uri)
|
35
|
+
html = resp.body
|
36
|
+
recs = html.xpath("//a[@class='card']").map { |a| scrape_listing(@base_uri + a['href']) }
|
37
|
+
if follow
|
38
|
+
seen = [state_uri]
|
39
|
+
html.xpath('//span[@class="pager-list"]/a').each do |a|
|
40
|
+
if a.text =~ /^\d+$/
|
41
|
+
# ;;warn "Following link #{a.text.inspect} to #{state_uri.inspect}"
|
42
|
+
state_uri = @base_uri + a['href']
|
43
|
+
unless seen.include?(state_uri)
|
44
|
+
recs += scrape_state_with_uri(state_uri, :follow => false)
|
45
|
+
seen << state_uri
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
recs
|
51
|
+
end
|
52
|
+
|
53
|
+
def uri_for_state(state)
|
54
|
+
"/yellowpages/browse?c=222&s=#{state}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def scrape_listing(listing_uri)
|
58
|
+
# ;;warn "Scraping listing URI #{listing_uri.inspect}"
|
59
|
+
resp = @conn.get(listing_uri)
|
60
|
+
html = resp.body
|
61
|
+
detail = html.at_xpath('//div[@class="detail"]')
|
62
|
+
person = detail.at_xpath("div[@class='card']/div[@class='prop sans']").text.strip.squeeze(' ')
|
63
|
+
organization = detail.at_xpath('//h1').children.map { |e| (n = e.text.strip).empty? ? nil : n }.compact.join(': ').squeeze(' ')
|
64
|
+
if (description = detail.at_xpath("//p[@class='intro']"))
|
65
|
+
description = description.text.strip.squeeze(' ')
|
66
|
+
end
|
67
|
+
uri = nil
|
68
|
+
address = nil
|
69
|
+
detail.xpath("//ul[@id='address']/li").each do |li|
|
70
|
+
if (a = li.at_xpath("a[@target='_blank']"))
|
71
|
+
uri = URI.parse(a['href'].gsub(/\s+/, ''))
|
72
|
+
elsif li.text =~ /^\s*(Tel|Fax)\b/
|
73
|
+
# ignore
|
74
|
+
else
|
75
|
+
address = li.children.select { |e| e.name != 'br' && e.text != ', ' && !e.text.empty? }.map { |e| (a = e.text.strip).empty? ? nil : a }.compact.join(', ').squeeze(' ')
|
76
|
+
end
|
77
|
+
end
|
78
|
+
rec = Record.new(
|
79
|
+
:person => person,
|
80
|
+
:organization => organization,
|
81
|
+
:address => address,
|
82
|
+
:uri => uri,
|
83
|
+
:description => description,
|
84
|
+
:group => 'Briar')
|
85
|
+
rec.clean!
|
86
|
+
rec.geocode
|
87
|
+
# ;;warn "Scraped #{rec.make_path} from #{listing_uri.inspect}"
|
88
|
+
rec
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|