diff --git a/bin/findit-nearby b/bin/findit-nearby
deleted file mode 100644
index 0f09a31..0000000
--- a/bin/findit-nearby
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/usr/bin/env - ruby
-
-require "#{File.dirname($0)}/../lib/findit"
-
-# Fix to lat/long of my house.
-current_latitude = 30.367534
-current_longitude = -97.734225
-
-result = FindIt.nearby(current_latitude, current_longitude)
-require "pp"
-pp result
diff --git a/bin/findit-svc b/bin/findit-svc
index 6d0cab8..c6fc4bd 100644
--- a/bin/findit-svc
+++ b/bin/findit-svc
@@ -1,33 +1,34 @@
#!/usr/bin/env -- ruby
-BASEDIR = "#{File.dirname($0)}/.."
require 'rubygems'
-require "#{BASEDIR}/lib/findit"
require 'sinatra'
require 'uri'
require 'json'
+require './lib/findit.rb'
-get '/nearby' do
- send_file "pub/nearby.html"
+get '/' do
+ send_file "public/nearby.html"
end
get '/findit.js' do
- send_file "pub/findit.js"
+ send_file "public/findit.js"
end
get '/about.html' do
- send_file "pub/about.html"
+ send_file "public/about.html"
end
-get '/nearby/:lat,:lng' do |lat, lng|
- result = FindIt.nearby(lat, lng)
+get '/svc/nearby/:lat,:lng' do |lat, lng|
+ result = FindIt.nearby(lat.to_f, lng.to_f)
content_type :json
result.to_json
end
-post '/nearby' do
- a = URI.decode_www_form(request.body.string)
- result = FindIt.nearby(a.assoc('latitude').last, a.assoc('longitude').last)
+post '/svc/nearby' do
+ a = URI.decode_www_form(request.body.read)
+ lat = a.assoc('latitude').last.to_f
+ lng = a.assoc('longitude').last.to_f
+ result = FindIt.nearby(lat, lng)
content_type :json
result.to_json
end
diff --git a/config.ru b/config.ru
new file mode 100644
index 0000000..c249894
--- /dev/null
+++ b/config.ru
@@ -0,0 +1,5 @@
+require 'rubygems'
+require 'sinatra'
+
+load "./bin/findit-svc"
+run Sinatra::Application
diff --git a/facilities.db b/facilities.db
new file mode 100644
index 0000000..5725a18
Binary files /dev/null and b/facilities.db differ
diff --git a/lib/findit.rb b/lib/findit.rb
index 8e4980e..1cf3ca1 100644
--- a/lib/findit.rb
+++ b/lib/findit.rb
@@ -1,163 +1,126 @@
-#!/usr/bin/env - ruby
-#
-# findit-nearby - Find nearby points of interest, proof of concept.
-#
-# This is very slow because it is querying against Google Fusion Tables. The data
-# should be moved local.
-#
-# Next step would be to convert this to a web service that accepts current lat/long,
-# and returns results in a JSON structure.
-#
-
require 'rubygems'
-require 'uri'
-require 'rest-client'
-require 'csv'
+require 'dbi'
-class Float
- # convert a value in degrees to radians
- def to_radians
- self * (Math::PI/180.0)
- end
-end
+#class Float
+# # convert a value in degrees to radians
+# def to_radians
+# self * (Math::PI/180.0)
+# end
+#end
+#
+#class Array
+# def to_radians
+# self.map {|x| x.to_radians}
+# end
+#end
-class FindIt
+class Coordinate
- attr_reader :current_loc
+ attr_accessor :latitude_deg, :longitude_deg, :latitude_rad, :longitude_rad
- def initialize(latitude, longitude)
- @current_loc = [latitude.to_f, longitude.to_f]
- end
+ DEG_TO_RAD = Math::PI / 180.0
+ def initialize(lat, lng, type)
+ case type
+ when :DEG
+ @latitude_deg = lat
+ @longitude_deg = lng
+ @latitude_rad = lat * DEG_TO_RAD
+ @longitude_rad = lng * DEG_TO_RAD
+ when :RAD
+ @latitude_rad = lat
+ @longitude_rad = lng
+ @latitude_deg = lat / DEG_TO_RAD
+ @longitude_deg = lng / DEG_TO_RAD
+ else
+ raise "unknown coordinate type \"#{type}\""
+ end
+ end
+
EARTH_R = 3963.0 # Earth mean radius, in miles
- # Calculate distance (in miles) between two locations (lat/long in degrees)
+ # Calculate distance (in miles) between two locations (lat/long in radians)
# Based on equitorial approximation formula at:
- # http://www.movable-type.co.uk/scripts/latlong.html
- def distance(p1, p2 = current_loc)
- lat1 = p1[0].to_radians
- lon1 = p1[1].to_radians
- lat2 = p2[0].to_radians
- lon2 = p2[1].to_radians
- x = (lon2-lon1) * Math.cos((lat1+lat2)/2);
- y = (lat2-lat1);
- Math.sqrt(x*x + y*y) * EARTH_R;
- end
-
- # Submit a query to a Google Fusion Table which returns CSV text.
- def query_google_fusion_table(table_id, where_clause = '', cols = '*')
- sql = "SELECT #{cols} FROM #{table_id}"
- sql += " WHERE #{where_clause}" unless where_clause.empty?
- url = "https://www.google.com/fusiontables/api/query?sql=" + URI.escape(sql)
- RestClient.get url
- end
-
- # Some of the CoA datasets have lat/long encoded in an XML blob (ewwww...).
- def parse_geometry(geo)
- if geo.match(%r{(.*),(.*)})
- [$2.to_f, $1.to_f]
+ # http://www.movable-type.co.uk/scripts/latlong.html
+ def distance(*args)
+ case args.length
+ when 1
+ p = args[0]
+ when 3
+ p = Coordinate.new(*args)
else
- nil
- end
- end
-
- # Find the closest entry in a Google Fusion Table dataset.
- #
- # Usage: find_closest(response) {|row| extract_latitude_longitude_from_row(row)}
- #
- # Returns: {:location => [LAT,LNG], :distance => MILES, :row => ROW}
- #
- # The "row" is a CSV::Row datatype.
- #
- def find_closest(resp)
- closest = nil
- CSV.parse(resp, :headers => true) do |row|
- p = yield(row)
- if p
- d = distance(p)
- if closest.nil? || d < closest[:distance]
- closest = {
- :location => p,
- :distance => d,
- :row => row
- }
- end
- end
+ raise "arguments should either be a Coordinate object or (lat,lng,type) values"
end
- raise "failed to find a location" if closest.nil?
- closest
- end
+ x = (p.longitude_rad-self.longitude_rad) * Math.cos((self.latitude_rad+p.latitude_rad)/2);
+ y = (p.latitude_rad-self.latitude_rad);
+ Math.sqrt(x*x + y*y) * EARTH_R;
+ end
- # Remove excess whitespace, make naive attempt at capitalization.
- def fixup_dirty_string(s)
- s.split.map {|w| w.capitalize}.join(' ')
- end
+end
- def closest_facility(factype)
- resp = query_google_fusion_table('3046433', "FACILITY='#{factype}'")
- closest = find_closest(resp) do |row|
- parse_geometry(row['geometry'])
- end
- {
- :object_type => factype.downcase.gsub(/\s+/, '_'),
- :name => fixup_dirty_string(closest[:row]['Name']),
- :address => fixup_dirty_string(closest[:row]['ADDRESS']),
- :latitude => closest[:location][0],
- :longitude => closest[:location][1],
- :distance => closest[:distance],
- #:raw_data => closest[:row],
- }
- end
+
+class FindIt
- def closest_post_office
- closest_facility("POST OFFICE")
- end
+ DEFAULT_DATABASE = "facilities.db"
+ DEFAULT_TABLE = "facilities"
+
+ attr_reader :database, :table, :loc, :dbh
- def closest_library
- closest_facility("LIBRARY")
+ def initialize(lat, lng, opts = {})
+ @loc = Coordinate.new(lat, lng, opts[:type] || :DEG)
+ @database = opts[:database] || DEFAULT_DATABASE
+ @table = opts[:table] || DEFAULT_TABLE
+ raise "database file \"#{@database}\" not found" unless File.exist?(@database)
+ @dbh = DBI.connect("DBI:SQLite3:#{@database}")
end
- def closest_fire_station
- resp = query_google_fusion_table('2987477')
- closest = find_closest(resp) do |row|
- [row['Latitude'].to_f, row['Longitude'].to_f]
+
+ def closest_facility(factype, name = nil)
+
+ args = []
+
+ sql = "SELECT * FROM #{@table} WHERE type LIKE ?"
+ args << factype
+
+ if name
+ sql += " AND name LIKE ?"
+ args << name
end
- {
- :object_type => "fire_station",
- :name => nil,
- :address => fixup_dirty_string(closest[:row]['Address']),
- :latitude => closest[:location][0],
- :longitude => closest[:location][1],
- :distance => closest[:distance],
- #:raw_data => closest[:row],
- }
- end
-
- def closest_moon_tower
- resp = query_google_fusion_table('3046440', "BUILDING_N='MOONLIGHT TOWERS'")
- closest = find_closest(resp) do |row|
- parse_geometry(row['geometry'])
+
+ closest = nil
+ @dbh.select_all(sql, *args) do |row|
+ d = @loc.distance(row['latitude_rad'], row['longitude_rad'], :RAD)
+ if closest.nil? || d < closest["distance"]
+ closest = row.to_h
+ closest["distance"] = d
+ end
end
- {
- :object_type => "moon_tower",
- :name => nil,
- :address => fixup_dirty_string(closest[:row]['ADDRESS']),
- :latitude => closest[:location][0],
- :longitude => closest[:location][1],
- :distance => closest[:distance],
- #:raw_data => closest[:row],
- }
+
+ closest.delete_if {|k, v| v.nil?} if closest
+ return closest
end
+
# Find collection of nearby objects.
def nearby
- result = {}
- a = closest_post_office ; result[a[:object_type]] = a
- a = closest_post_office ; result[a[:object_type]] = a
- a = closest_library ; result[a[:object_type]] = a
- a = closest_fire_station ; result[a[:object_type]] = a
- a = closest_moon_tower ; result[a[:object_type]] = a
- result
+ facilities = {}
+
+ a = closest_facility("POST_OFFICE")
+ facilities[a["type"]] = a if a
+
+ a = closest_facility("LIBRARY")
+ facilities[a["type"]] = a if a
+
+ a = closest_facility("FIRE_STATION")
+ facilities[a["type"]] = a if a
+
+ a = closest_facility("HISTORICAL_LANDMARK", "Moonlight Towers")
+ if a
+ a.delete("name")
+ facilities["MOON_TOWER"] = a
+ end
+
+ facilities
end
# Find collection of nearby objects for a given latitude/longitude.
diff --git a/public/.htaccess b/public/.htaccess
new file mode 100644
index 0000000..60072d1
--- /dev/null
+++ b/public/.htaccess
@@ -0,0 +1,3 @@
+Options -Indexes
+DirectoryIndex nearby.html
+PassengerEnabled on
diff --git a/pub/about.html b/public/about.html
similarity index 100%
rename from pub/about.html
rename to public/about.html
diff --git a/pub/findit.js b/public/findit.js
similarity index 86%
rename from pub/findit.js
rename to public/findit.js
index a1dae4b..ef0e905 100644
--- a/pub/findit.js
+++ b/public/findit.js
@@ -13,7 +13,7 @@ function FindIt(opts) {
r.dom_map_elem = document.getElementById(opts.map_id);
if (! r.dom_map_elem) throw "cannot locate element id \"" + opts.map_id + "\" in page";
r.event_handler = opts.event_handler;
- r.svc_endpoint = opts.svc_endpoint || document.URL;
+ r.svc_endpoint = opts.svc_endpoint || (document.URL + "svc/nearby");
r.map = null;
r.marker_me = null;
@@ -22,6 +22,13 @@ function FindIt(opts) {
r.position = null;
r.address = null;
+ r.recognized_features = [
+ {type: "LIBRARY", title: "library"},
+ {type: "POST_OFFICE", title: "post office"},
+ {type: "FIRE_STATION", title: "fire station"},
+ {type: "MOON_TOWER", title: "moon tower"},
+ ];
+
r.marker_images = {
'blue_dot' : new google.maps.MarkerImage(
@@ -113,9 +120,9 @@ FindIt.methods = {
}
if (address) {
- this.send_event("ADDRESS_GOOD", {'address' : address});
+ this.send_event("ADDRESS_GOOD", {'address' : address});
} else {
- this.findAddress(loc);
+ this.findAddress(loc);
}
this.searchNearby(loc);
@@ -141,7 +148,7 @@ FindIt.methods = {
google.maps.event.addListener(marker, 'dragend', dragCallBack);
- this.makeInfoWindow(marker, "
You are here!
Drag this marker to explore the city.
");
+ this.makeInfoWindow(marker, "You are here
Drag this marker to explore the city.");
return marker;
},
@@ -197,11 +204,14 @@ FindIt.methods = {
var nearby = eval('(' + req.responseText + ')');
- this.placeFeatureOnMap(nearby.library, "library");
- this.placeFeatureOnMap(nearby.post_office, "post office");
- this.placeFeatureOnMap(nearby.fire_station, "fire station");
- this.placeFeatureOnMap(nearby.moon_tower, "moon tower");
-
+ for (var i = 0 ; i < this.recognized_features.length ; ++i ) {
+ var type = this.recognized_features[i].type;
+ var title = this.recognized_features[i].title;
+ if (nearby[type]) {
+ this.placeFeatureOnMap(nearby[type], title);
+ }
+ }
+
this.send_event("COMPLETE", {});
},
@@ -269,14 +279,19 @@ FindIt.methods = {
// Build info window content for a feature.
infoWindowContentForFeature : function(feature, descr) {
- var content = "Nearest " + descr + "
\n";
+ var result = ["Nearest " + descr.escapeHTML() + ""];
if (! isEmpty(feature.name)) {
- content = content + feature.name + "
\n";
+ result.push(feature.name.escapeHTML());
+ }
+ result.push(feature.address.escapeHTML());
+ if (! isEmpty(feature.info)) {
+ result.push(feature.info.escapeHTML());
+ }
+ result.push(feature.distance.toFixed(1) + " mi away");
+ if (! isEmpty(feature.link)) {
+ result.push("more info ...");
}
- content = content + feature.address + "
\n" +
- feature.distance.toFixed(1) + " mi away
\n" +
- "clickable link goes here
";
- return content;
+ return result.join("
\n");
}
};
diff --git a/pub/nearby.html b/public/nearby.html
similarity index 100%
rename from pub/nearby.html
rename to public/nearby.html
diff --git a/test/data/empty.db b/test/data/empty.db
new file mode 100644
index 0000000..e69de29
diff --git a/test/src/test-findit.rb b/test/src/test-findit.rb
new file mode 100644
index 0000000..eb2166c
--- /dev/null
+++ b/test/src/test-findit.rb
@@ -0,0 +1,111 @@
+BASEDIR = "#{File.dirname($0)}/../.."
+$:.insert(0, "#{BASEDIR}/lib")
+
+require 'minitest/autorun'
+require 'findit'
+
+P1_LAT_DEG = 30.286
+P1_LNG_DEG = -97.739
+P1_LAT_RAD = 0.52859
+P1_LNG_RAD = -1.7058
+
+P2_LAT_DEG = 30.264
+P2_LNG_DEG = -97.747
+
+# 1 radian ~ 50 - 70 miles => 1 mile ~ 0.14 - 0.20 radians => 0.01 miles ~ 0.0014 - 0.0020 radians
+DELTA_RADIANS = 0.0007 # to round accurately to nearest 1/100th mile
+DELTA_DEGREES = DELTA_RADIANS*(360/Math::PI)
+DELTA_MILES = 0.005
+
+class TestCoordinate < MiniTest::Unit::TestCase
+
+ def test_constructor_degrees
+ p = Coordinate.new(P1_LAT_DEG, P1_LNG_DEG, :DEG)
+ refute_nil p
+ assert_in_delta P1_LAT_DEG, p.latitude_deg, DELTA_DEGREES
+ assert_in_delta P1_LNG_DEG, p.longitude_deg, DELTA_DEGREES
+ assert_in_delta P1_LAT_RAD, p.latitude_rad, DELTA_RADIANS
+ assert_in_delta P1_LNG_RAD, p.longitude_rad, DELTA_RADIANS
+ end
+
+ def test_constructor_radians
+ p = Coordinate.new(P1_LAT_RAD, P1_LNG_RAD, :RAD)
+ refute_nil p
+ assert_in_delta P1_LAT_RAD, p.latitude_rad, DELTA_RADIANS
+ assert_in_delta P1_LNG_RAD, p.longitude_rad, DELTA_RADIANS
+ assert_in_delta P1_LAT_DEG, p.latitude_deg, DELTA_DEGREES
+ assert_in_delta P1_LNG_DEG, p.longitude_deg, DELTA_DEGREES
+ end
+
+ def test_distance_to_coord
+ p1 = Coordinate.new(P1_LAT_DEG, P1_LNG_DEG, :DEG)
+ p2 = Coordinate.new(P2_LAT_DEG, P2_LNG_DEG, :DEG)
+ assert_in_delta 1.594, p1.distance(p2), DELTA_MILES
+ end
+
+ def test_distance_to_location
+ p1 = Coordinate.new(P1_LAT_DEG, P1_LNG_DEG, :DEG)
+ assert_in_delta 1.594, p1.distance(P2_LAT_DEG, P2_LNG_DEG, :DEG), DELTA_MILES
+ end
+
+end
+
+class TestFindIt < MiniTest::Unit::TestCase
+
+ def setup
+ @f = FindIt.new(P1_LAT_DEG, P1_LNG_DEG)
+ end
+
+ def test_constructor
+ refute_nil @f
+ refute_nil @f.loc
+ assert_in_delta P1_LAT_DEG, @f.loc.latitude_deg, DELTA_DEGREES
+ assert_in_delta P1_LNG_DEG, @f.loc.longitude_deg, DELTA_DEGREES
+ assert_equal "facilities.db", @f.database
+ assert_equal "facilities", @f.table
+ refute_nil @f.dbh
+ end
+
+ def test_constructor_options
+ @f1 = FindIt.new(P1_LAT_RAD, P1_LNG_RAD, :type => :RAD, :database => "./test/data/empty.db", :table => "does_not_exist")
+ refute_nil @f
+ refute_nil @f.loc
+ assert_in_delta P1_LAT_RAD, @f.loc.latitude_rad, DELTA_RADIANS
+ assert_in_delta P1_LNG_RAD, @f.loc.longitude_rad, DELTA_RADIANS
+ end
+
+ def test_constructor_database_missing
+ assert_raises(RuntimeError) {
+ FindIt.new(P1_LAT_DEG, P1_LNG_DEG, :database => "does_not_exist.db")
+ }
+ end
+
+ def test_closest
+ a = @f.closest_facility("POST_OFFICE")
+ refute_nil a
+ assert_equal "POST_OFFICE", a["type"]
+ assert_equal "West Mall University Of Texas Station", a["name"]
+ assert_equal "2201 Guadalupe", a["address"]
+ assert_equal "Austin", a["city"]
+ assert_equal "TX", a["state"]
+ assert_in_delta 30.2810, a["latitude"], DELTA_DEGREES
+ assert_in_delta -97.7335, a["longitude"], DELTA_DEGREES
+ assert_in_delta 0.52850, a["latitude_rad"], DELTA_RADIANS
+ assert_in_delta -1.7058, a["longitude_rad"], DELTA_RADIANS
+ assert_in_delta 0.410, a["distance"], DELTA_MILES
+ end
+
+ def test_nearby
+ a = @f.nearby
+ refute_nil a
+ assert_equal 4, a.length
+ assert_equal %w(FIRE_STATION LIBRARY MOON_TOWER POST_OFFICE), a.keys.sort
+ assert_equal "506 W Martin Luther King Blvd", a["FIRE_STATION"]["address"]
+ assert_equal "810 Guadalupe St", a["LIBRARY"]["address"]
+ assert_equal "2110 Nueces Street", a["MOON_TOWER"]["address"]
+ assert_equal "2201 Guadalupe", a["POST_OFFICE"]["address"]
+ end
+
+end
+
+
diff --git a/tmp/.KEEPME b/tmp/.KEEPME
new file mode 100644
index 0000000..2a6d876
--- /dev/null
+++ b/tmp/.KEEPME
@@ -0,0 +1 @@
+This directory is required when deploying under Phusion Passenger.