itinerary 0.1

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,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