itinerary 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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=
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in itinerary.gemspec
4
+ gemspec
@@ -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.
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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