Skip to content

Commit

Permalink
Major rewrite. Now uses local facilities.db.
Browse files Browse the repository at this point in the history
  • Loading branch information
courtney-rosenthal committed Mar 9, 2012
1 parent d9b91a8 commit 7a21314
Show file tree
Hide file tree
Showing 12 changed files with 262 additions and 174 deletions.
11 changes: 0 additions & 11 deletions bin/findit-nearby

This file was deleted.

23 changes: 12 additions & 11 deletions 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
5 changes: 5 additions & 0 deletions config.ru
@@ -0,0 +1,5 @@
require 'rubygems'
require 'sinatra'

load "./bin/findit-svc"
run Sinatra::Application
Binary file added facilities.db
Binary file not shown.
237 changes: 100 additions & 137 deletions 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{<Point><coordinates>(.*),(.*)</coordinates></Point>})
[$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.
Expand Down
3 changes: 3 additions & 0 deletions public/.htaccess
@@ -0,0 +1,3 @@
Options -Indexes
DirectoryIndex nearby.html
PassengerEnabled on
File renamed without changes.

0 comments on commit 7a21314

Please sign in to comment.