Nominatim (Reverse Geocoder) via PL/PYTHON for RubyOnRails Mapping App

January 6th, 2012 rupert No comments

Even google maps enterprise have restrictions on their geocoding/reverse-geocoding services, 100k if my memory serves me correctly. So, I have to rollout our own service to allow millions of lonlats for reverse geocoding. Have a look at Nominatim, yes it’s opensource. If you need to get it up and running, have a read of my nominatim installation via homebrew on OSX.

The nominatim www interface which spits out xml/json depending on the format parameter is done in php.

Anyway, I wanted to expose/use this webservice for our Rails3 app. It will also be good if we don’t use the nominatim webservice all the time if the lonlat was already requested–caching.

Python-Nominatim https://github.com/rdeguzman/python-nominatim

This project was forked from Austin’s Gabels python-nominatim. I added the ability to pass a base_url to the classes and added reverse_geocode.py. So assuming you have Python installed, you can do a reverse geocode like this…

from nominatim import ReverseGeocoder
client = ReverseGeocoder("http://127.0.0.1/nominatim/reverse.php?format=json")
response = client.geocode(-37.856206, 145.233980)
 
print response['full_address']
#Amesbury Avenue, Wantirna, City of Knox, 3152, Australia

PL/PYTHON
Now we wrap this python code via PL/PYTHON so Postgres can call it. Checkout setup.sql

CREATE PROCEDURAL LANGUAGE 'plpythonu' HANDLER plpython_call_handler;
 
CREATE OR REPLACE FUNCTION reverse_geocode(geocoding_url text, latitude float, longitude float) RETURNS
  text
  AS
  $$
    import nominatim
    client = nominatim.ReverseGeocoder(geocoding_url)
    response = client.geocode(latitude, longitude)
    RETURN response['full_address']
  $$
  LANGUAGE 'plpythonu';

With the snippet above, we can now call this with a regular SELECT statement…

SELECT reverse_geocode('http://127.0.0.1/nominatim/reverse.php?format=json', -37.856206, 145.233980); 
                       reverse_geocode
  ----------------------------------------------------------
   Amesbury Avenue, Wantirna, City of Knox, 3152, Australia
  (1 row)

Rails ActiveRecord

    create_table :locations do |t|
      t.float    :latitude
      t.float    :longitude
      t.text     :address
    end

In AR, I created a location model above and exposed a reverse_geocode method below

class Location < ActiveRecord::Base
 
  def self.reverse_geocode(geocode_url, lat, lon)
    sql_string = "SELECT reverse_geocode('#{geocode_url}', #{lat}, #{lon}) as address, #{lat} as latitude, #{lon} as longitude"
    loc_array = self.find_by_sql sql_string
    loc_array[0]
  end
 
end

So now, in one of my models, I could simply do..

class ActiveSession < ActiveRecord::Base
...
  def location_address
    if self.has_gps?
      loc = Location.reverse_geocode('http://path/to/reverse.php?format=json', self.gps_latitude, self.gps_longitude)
      loc.address
    else
      nil
    end
  end
...
end

In the view, we can simple call model.location_address to retrieve the location details. Below is a code snippet which creates a google marker and adds the location details in the infoWindow.

<% location = active_session.location_address %>
 
var latlong = new google.maps.LatLng(<%= active_session.gps_latitude %>, <%= active_session.gps_longitude %>);
 
var content = '<div style="width: 300px;">';
content = content + '<p><%= escape_javascript location %></p>';
content = content + '<p><%= active_session.gps_longitude %>,<%= active_session.gps_latitude %></p>';

marker.png

Caching
Our last step is to improve performance via caching. I have opted to do this from the PL/PYTHON end but using a Rails activerecord model/table. This way, the Rails activerecord has no idea that it is cached when it calls model.location_address. Below, I wrap the new reverse_geocode PL/PYTHON function in a rails migration.

class CreateFunctionReverseGeocoder < ActiveRecord::Migration
  ActiveRecord::Base.connection.schema_search_path = "public"
 
  def self.up
    execute 'CREATE OR REPLACE FUNCTION reverse_geocode(geocoding_url text, latitude float, longitude float) RETURNS
      text
      AS
      $$
        plan = plpy.prepare("SELECT address FROM locations WHERE latitude = $1 AND longitude = $2", [ "float", "float" ])
        rv = plpy.execute(plan, [ latitude, longitude ], 1)
 
        if rv.nrows() > 0:
          result = rv[0]["address"]
        else:
          import nominatim
          client = nominatim.ReverseGeocoder(geocoding_url)
          response = client.geocode(latitude, longitude)
          result = response["full_address"]
          insert_plan = plpy.prepare("INSERT INTO locations(latitude, longitude, address) VALUES($1, $2, $3)", ["float", "float", "text"])
          plpy.execute(insert_plan, [ latitude, longitude, result ])
 
        return result
      $$
      language plpythonu;'
  end
 
  def self.down
    execute 'DROP FUNCTION IF EXISTS reverse_geocode(text, double precision, double precision);'
  end
end

Benchmarks
I plotted 1000 records on my MBP (old core2duo early 2009 4GB RAM). Initial launch takes 108 seconds to load, ~ 2 minutes? But subsequent requests loads < 2 secs.

For 1000 records:
Completed 200 OK in 110478ms (Views: 1608.8ms | ActiveRecord: 108674.6ms)
Completed 200 OK in 1744ms (Views: 1110.7ms | ActiveRecord: 443.3ms)

Below is an architecture diagram of how the systems talk to each other. The locations cache is inside the geo_app_development db. Ofcourse, the nominatim database (gazetteer_au) is separate from our domain so it goes into a different db/server whereever.
archi.png

Categories: geocoding, google Tags: , ,

homebrew + python

December 30th, 2011 rupert No comments

Read http://docs.python-guide.org/en/latest/starting/installation/

Installation

/usr/local/Cellar% brew install python --framework
==> Downloading http://www.python.org/ftp/python/2.7.2/Python-2.7.2.tar.bz2
File already downloaded in /Users/rupert/Library/Caches/Homebrew
==> Patching
patching file Lib/whichdb.py
Hunk #1 succeeded at 91 with fuzz 1.
==> ./configure --prefix=/usr/local/Cellar/python/2.7.2 --enable-framework=/usr/local/Cellar/python/2.7.2/Frameworks
==> make
==> make install
==> Downloading http://pypi.python.org/packages/source/d/distribute/distribute-0.6.24.tar.gz
File already downloaded in /Users/rupert/Library/Caches/Homebrew
==> /usr/local/Cellar/python/2.7.2/bin/python setup.py install
==> Caveats
A "distutils.cfg" has been written to:
  /usr/local/Cellar/python/2.7.2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/distutils
specifing the install-scripts folder as:
  /usr/local/share/python
 
If you install Python packages via "python setup.py install", easy_install, pip,
any provided scripts will go into the install-scripts folder above, so you may
want to add it to your PATH.
 
Distribute has been installed, so easy_install is available.
To update distribute itself outside of Homebrew:
    /usr/local/share/python/easy_install pip
    /usr/local/share/python/pip install --upgrade distribute
 
See: https://github.com/mxcl/homebrew/wiki/Homebrew-and-Python
 
Framework Python was installed to:
  /usr/local/Cellar/python/2.7.2/Frameworks/Python.framework
 
You may want to symlink this Framework to a standard OS X location,
such as:
    mkdir ~/Frameworks
    ln -s "/usr/local/Cellar/python/2.7.2/Frameworks/Python.framework" ~/Frameworks
==> Summary
/usr/local/Cellar/python/2.7.2: 4808 files, 77M, built in 86 seconds
brew install python --framework  76.78s user 21.49s system 112% cpu 1:27.00 total

Symlinks

The following links were created below to ensure that 2.7 is the latest Python picked up during installation of other software (i.e postgresql)

sudo ln -s /usr/local/Cellar/python/2.7.2/Frameworks/Python.framework/Versions/2.7 /System/Library/Frameworks/Python.framework/Versions/2.7
sudo ln -s /usr/local/share/python/easy_install /usr/bin/easy_install
sudo ln -s /usr/local/share/python/easy_install-2.7 /usr/bin/easy_install-2.7
sudo ln -s /System/Library/Frameworks/Python.framework/Versions/2.7/bin/python2.7 /usr/bin/python2.7
sudo ln -s /System/Library/Frameworks/Python.framework/Versions/2.7/bin/python2.7-config /usr/bin/python2.7-config
sudo ln -s /System/Library/Frameworks/Python.framework/Versions/2.7/bin/pydoc2.7 /usr/bin/pydoc2.7
sudo ln -s /usr/bin/python2.7 /usr/bin/python
sudo ln -s /System/Library/Frameworks/Python.framework/Versions/2.7 /System/Library/Frameworks/Python.framework/Versions/Current
mkdir -p /Library/Python/2.7
ln -s /usr/local/lib/python2.7/site-packages /Library/Python/2.7/site-packages

Site-Packages. Where?

Note that site-packages will be installed in

/usr/local/lib/python2.7/site-packages
~% ls -la /Library/Python/2.6/site-packages
total 136
drwxrwxr-x  14 root    admin    476 30 Dec 18:13 ./
drwxrwxr-x   4 root    admin    136 30 Dec 18:11 ../
-rw-r--r--@  1 rupert  admin   6148 30 Dec 18:13 .DS_Store
-rw-rw-r--   1 root    admin    119 11 Feb  2010 README
-rw-r--r--   1 rupert  admin    241 30 Dec 18:01 easy-install.pth
-rw-r--r--   1 rupert  admin   3129 30 Dec 18:02 googlemaps-1.0.2-py2.6.egg-info
-rw-r--r--   1 rupert  admin  19703 16 Oct  2009 googlemaps.py
-rw-r--r--   1 rupert  admin  19153 30 Dec 18:02 googlemaps.pyc
drwxr-xr-x  39 root    admin   1326  7 Feb  2010 mod_python/
-rw-r--r--   1 root    admin    267  7 Feb  2010 mod_python-3.3.2_dev_20080819-py2.6.egg-info
drwxr-xr-x   8 rupert  admin    272 30 Dec 18:13 nominatim/
-rw-r--r--   1 rupert  admin   4462 30 Dec 18:08 nominatim-0.90-py2.6.egg
drwxr-xr-x  15 rupert  admin    510 30 Dec 18:01 simplejson/
drwxr-xr-x   4 rupert  admin    136 30 Dec 18:13 simplejson-2.3.1-py2.6.egg/
~% ls -la /Library/Python/2.7/site-packages
lrwxr-xr-x  1 root  admin  38 30 Dec 18:23 /Library/Python/2.7/site-packages@ -> /usr/local/lib/python2.7/site-packages
~% which python
/usr/local/bin/python
~% python --version
Python 2.7.2

Installing PHP on OSX

December 28th, 2011 rupert No comments

I need wordpress + nominatim running on OSX and since it uses php…

http://www.php.net/manual/en/install.unix.apache2.php

1. Installation

./configure --prefix=/usr/local/php-5.3.8 --with-mysql --with-pgsql=/usr/local/postgresql --with-apxs2=/usr/local/apache2/bin/apxs
make
sudo make install
rupert-mbp~/Desktop/php-5.3.8% sudo make install
Password:
Installing PHP SAPI module:       apache2handler
/usr/local/apache2.2.14/build/instdso.sh SH_LIBTOOL='/usr/local/apache2.2.14/build/libtool' libs/libphp5.so /usr/local/apache2.2.14/modules
/usr/local/apache2.2.14/build/libtool --mode=install cp libs/libphp5.so /usr/local/apache2.2.14/modules/
cp libs/libphp5.so /usr/local/apache2.2.14/modules/libphp5.so
Warning!  dlname not found in /usr/local/apache2.2.14/modules/libphp5.so.
Assuming installing a .so rather than a libtool archive.
chmod 755 /usr/local/apache2.2.14/modules/libphp5.so
[activating module `php5' in /usr/local/apache2.2.14/conf/httpd.conf]
Installing PHP CLI binary:        /usr/local/php-5.3.8/bin/
Installing PHP CLI man page:      /usr/local/php-5.3.8/man/man1/
Installing build environment:     /usr/local/php-5.3.8/lib/php/build/
Installing header files:          /usr/local/php-5.3.8/include/php/
Installing helper programs:       /usr/local/php-5.3.8/bin/
  program: phpize
  program: php-config
Installing man pages:             /usr/local/php-5.3.8/man/man1/
  page: phpize.1
  page: php-config.1
Installing PEAR environment:      /usr/local/php-5.3.8/lib/php/
[PEAR] Archive_Tar    - installed: 1.3.7
[PEAR] Console_Getopt - installed: 1.3.0
[PEAR] Structures_Graph- installed: 1.0.4
[PEAR] XML_Util       - installed: 1.2.1
[PEAR] PEAR           - installed: 1.9.4
Wrote PEAR system config file at: /usr/local/php-5.3.8/etc/pear.conf
You may want to add: /usr/local/php-5.3.8/lib/php to your php.ini include_path
/Users/rupert/Desktop/php-5.3.8/build/shtool install -c ext/phar/phar.phar /usr/local/php-5.3.8/bin
ln -s -f /usr/local/php-5.3.8/bin/phar.phar /usr/local/php-5.3.8/bin/phar
Installing PDO headers:          /usr/local/php-5.3.8/include/php/ext/pdo/
sudo make install  5.52s user 9.35s system 79% cpu 18.819 total

2. Configuration
Specify php.ini per virtualhost

PHPINIDir /etc

Categories: Uncategorized Tags: ,

freebsd + rvm + rails + passenger

November 22nd, 2011 rupert No comments

1. Install rvm

[rupert@rupert-bsd ~]$ sudo bash < <(curl -sk https://raw.github.com/wayneeseguin/rvm/master/binscripts/rvm-installer)
Password:
Cloning into 'rvm'...
remote: Counting objects: 5903, done.
remote: Compressing objects: 100% (2833/2833), done.
remote: Total 5903 (delta 3875), reused 4154 (delta 2307)
Receiving objects: 100% (5903/5903), 1.95 MiB | 501 KiB/s, done.
Resolving deltas: 100% (3875/3875), done.
 
  RVM:  Shell scripts enabling management of multiple ruby environments.
  RTFM: https://rvm.beginrescueend.com/
  HELP: http://webchat.freenode.net/?channels=rvm (#rvm on irc.freenode.net)
 
Installing RVM to /usr/local/rvm/
    Correct permissions for base binaries in /usr/local/rvm/bin...
    Copying manpages into place.
Creating RVM system user group 'rvm'
    Recording config files for rubies.
 
root,
 
If you have any questions, issues and/or ideas for improvement please
fork the project and issue a pull request.
 
If you wish to disable the project .rvmrc file functionality, set
rvm_project_rvmrc=0 in either /etc/rvmrc or ~/.rvmrc.
 
NOTE: To Multi-User installers, please do NOT forget to add your users to the 'rvm'.
  The installer no longer auto-adds root or users to the rvm group. Admins must do this.
  Also, please note that group memberships are ONLY evaluated at login time.
  This means that users must log out then back in before group membership takes effect!
 
Thank you for using RVM!
 
I sincerely hope that RVM helps to make your life easier and more enjoyable!!!
 
  ~Wayne
 
 
SYSTEM NOTES:
 
If you do not wish to enable reading of per-project .rvmrc files, simply set:
        export rvm_project_rvmrc=0
within either your /etc/rvmrc or $HOME/.rvmrc file, then log out and back in.
 
 
In case your shell exits on entering directory with freshly checked out sources
you should update .rvmrc file, and replace any `exit ` with `return `.
 
 
You _must_ read 'rvm requirements' for additional OS specific requirements for
various rubies, and native-extension gems. Expect failures until those are met!
 
 
Installation of RVM to /usr/local/rvm/ is complete.

2. Add user to rvm group

# vim /etc/group
....
rvm:*:1003:rupert

Load rvm script to shell

source "/usr/local/rvm/scripts/rvm"

Note: You don’t need to add this to .bashrc, check /etc/rvmrc /etc/profile

3. Install 1.9.2

[root@rupert-bsd ~]# rvm install 1.9.2-p180
[root@rupert-bsd ~]# rvm install 1.9.2
Installing Ruby from source to: /usr/local/rvm/rubies/ruby-1.9.2-p290, this may take a while depending on your cpu(s)...
 
ruby-1.9.2-p290 - #fetching 
ruby-1.9.2-p290 - #downloading ruby-1.9.2-p290, this may take a while depending on your connection...
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  3 8604k    3  340k    0     0  27173      0  0:05:24  0:00:12  0:05:12 64100

4. Use ruby1.9

# rvm --default use 1.9.2-p180
# gem install bundler -V

5. Install passenger

# gem install passenger -V
# passenger-install-apache2-module

6. Update httpd.conf

LoadModule rewrite_module libexec/apache22/mod_rewrite.so
 
LoadModule passenger_module /usr/local/rvm/gems/ruby-1.9.2-p180/gems/passenger-3.0.9/ext/apache2/mod_passenger.so
PassengerRoot /usr/local/rvm/gems/ruby-1.9.2-p180/gems/passenger-3.0.9
PassengerRuby /usr/local/rvm/wrappers/ruby-1.9.2-p180/ruby
<VirtualHost 192.168.4.197:80>
   ServerAdmin rupert@datalinktech.com.au
   ServerName rupert-bsd.datalink.loc
   ServerAlias rupert-bsd.datalink.loc
 
   DocumentRoot "/usr/local/2rmobile/myproject/current/public"
   <Directory "/usr/local/2rmobile/myproject/current/public">
    Options Indexes MultiViews
    AllowOverride None 
    Order allow,deny
    Allow from all
   </Directory>
 
   #CustomLog /var/log/httpd/cws-error.log combinedio
   #LogLevel warn 
</VirtualHost>
Categories: freebsd Tags: , ,

freebsd + git server + gitweb

November 22nd, 2011 rupert No comments

1. install git server

$ cd /usr/ports/devel/git
$ make install clean

Note: Include gitweb as an option

2. Modify /etc/rc.conf

git_daemon_enable="YES"
git_daemon_directory="/var/db/git/repo"
git_daemon_flags="--export-all --syslog --enable=receive-pack --listen=ip_address --verbose"

3. Create git user

$ pw user add git
$ passwd git
$ chsh git
Login: git
Password: *******************
Uid [#]: 1002
Gid [# or name]: 1002
Change [month day year]:
Expire [month day year]:
Class:
Home directory: /var/db/git
Shell: /usr/local/bin/git-shell
Full Name: User &
Office Location:
Office Phone:
Home Phone:
Other information:

“git” user would create repositories. We could also add “rupert” to the git group.

pw user mod rupert -G git

4. Let’s create a repository

$ cd /var/db/git/repo
$ sudo mkdir cws-rails.git
$ cd cws-rails.git/
$ sudo git --bare init
Initialized empty Git repository in /var/db/git/repo/cws-rails.git/
$ cd ..
$ ls -l
total 4
drwxr-xr-x  7 root  git  512 Nov 16 12:54 cws-rails.git
drwxrwxr-x  7 git   git  512 Nov 16 11:52 myproject.git
$ sudo chown -Rf git:git cws-rails.git
$ sudo chmod -Rf 775 cws-rails.git

Make sure to change the ownership to git. We also make it writable to the group so users like rupert will have write access.

5. Let’s clone, commit and push
So now in my MBP, i will clone myproject.git, change some file and push.

$ git clone rupert@rupert-bsd:/var/db/git/repo/myproject.git
Cloning into myproject...
Password:
remote: Counting objects: 12, done.
remote: Compressing objects: 100% (9/9), done.
remote: Total 12 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (12/12), done.
~/Desktop/myproject[master]% gc -m "Updated CHANGELOG" CHANGELOG 
[master d9fd0f7] Updated CHANGELOG
 1 files changed, 1 insertions(+), 0 deletions(-)
~/Desktop/myproject[master]% git push
Password:
Counting objects: 5, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 332 bytes, done.
Total 3 (delta 1), reused 0 (delta 0)
To rupert@rupert-bsd:/var/db/git/repo/myproject.git
   5d16498..d9fd0f7  master -> master

Note: So that we don’t need to specify the password all the time, generate an ssh-keygen -t rsa for id_rsa.pub and append it to rupert@rupert-bsd:/home/rupert/.ssh/authorized_keys

6. Setup Git, Configure and Restart Apache2

# Copy the gitweb directory to your apache22 directory
$ sudo cp -Rf /usr/local/share/examples/git/gitweb /usr/local/www/apache22/data/
 
# Edit gitweb.cgi to point to your repo
$ vim /usr/local/www/apache22/data/gitweb/gitweb.cgi
our $projectroot = "/var/db/git/repo";
# Allow apache to execute gitweb.cgi
Alias /gitweb /usr/local/www/apache22/data/gitweb
 
<Directory /usr/local/www/apache22/data/gitweb>
  Options FollowSymLinks +ExecCGI
  AddHandler cgi-script .cgi
</Directory>

Browse http://servername/gitweb/gitweb.cgi

Categories: freebsd Tags: ,