001package votorola.a; // Copyright 2008-2011, Michael Allan.  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE.
002
003import com.google.gson.stream.*;
004import com.sun.jersey.api.uri.*;
005import java.io.*;
006import java.net.*;
007import java.sql.*;
008import java.util.logging.*;
009import votorola.a.*;
010import votorola.g.lang.*;
011import votorola.g.logging.*;
012import votorola.g.net.*;
013
014
015/** A geocoder based on the Google Geocoding API, version 3.  It converts street addresses
016  * to cartographic coordinates.
017  *
018  *     @see <a href='http://code.google.com/apis/maps/documentation/geocoding/'
019  *                  >http://code.google.com/apis/maps/documentation/geocoding/</a>
020  */
021public @ThreadSafe final class GoogleGeocoder
022{
023
024
025    /** Constructs a GoogleGeocoder.
026      *
027      *     @see #geocodeTable()
028      */
029    public GoogleGeocoder( Geocode.Table _geocodeTable ) { geocodeTable = _geocodeTable; }
030
031
032
033   // ------------------------------------------------------------------------------------
034
035
036    /** Geocodes a street address.
037      *
038      *     @see Geocode#region()
039      *     @see Geocode#address()
040      */
041    public Geocode geocode( final String region, final String address )
042      throws IOException, SQLException
043    {
044        final Geocode geocode = new Geocode( region, address, geocodeTable );
045        if( geocode.exists() ) return geocode;
046
047        final URL url;
048        try
049        {
050            final StringBuilder b = new StringBuilder();
051            b.append( "http://maps.googleapis.com/maps/api/geocode/json?address=" );
052            b.append( UriComponent.encode( address, UriComponent.Type.QUERY_PARAM ));
053            b.append( "&region=" );
054            b.append( UriComponent.encode( region, UriComponent.Type.QUERY_PARAM ));
055            b.append( "&sensor=false" );
056            url = new URL( b.toString() );
057        }
058        catch( MalformedURLException x ) { throw new RuntimeException( x ); }
059
060        boolean areCoordinatesSet = false;
061        logger.fine( "querying geocoder: " + url );
062        final HttpURLConnection http = (HttpURLConnection)( url.openConnection() );
063        URLConnectionX.connect( http );
064        try
065        {
066            final JsonReader in = new JsonReader( new InputStreamReader(
067              http.getInputStream(), "UTF-8" )); // assumed encoding, or parse http.getContentType() for actual
068            try
069            {
070                in.beginObject();
071                while( in.hasNext() )
072                {
073                    String name = in.nextName();
074                    if( "status".equals( name ))
075                    {
076                        final String status = in.nextString();
077                        if( !"OK".equals( status )) throw new Geocode.GeocodingException( "geocoder status=" + status + ": " + url );
078                    }
079                    else if( "results".equals( name ))
080                    {
081                        in.beginArray();
082                        if( in.hasNext() )
083                        {
084                            in.beginObject();
085                            while( in.hasNext() )
086                            {
087                                if( !"geometry".equals( in.nextName() ))
088                                {
089                                    in.skipValue();
090                                    continue;
091                                }
092
093                                in.beginObject();
094                                while( in.hasNext() )
095                                {
096                                    if( !"location".equals( in.nextName() ))
097                                    {
098                                        in.skipValue();
099                                        continue;
100                                    }
101
102                                    in.beginObject();
103                                    double lat = Double.NaN;
104                                    double lng = lat;
105                                    while( in.hasNext() )
106                                    {
107                                        name = in.nextName();
108                                        if( "lat".equals( name ))
109                                        {
110                                            lat = Math.toRadians( in.nextDouble() );
111                                        }
112                                        else if( "lng".equals( name ))
113                                        {
114                                            lng = Math.toRadians( in.nextDouble() );
115                                        }
116                                        else in.skipValue();
117                                    }
118                                    if( lat != Double.NaN && lng != Double.NaN )
119                                    {
120                                        geocode.setCoordinates( lat, lng );
121                                        areCoordinatesSet = true;
122                                    }
123                                    in.endObject();
124                                    while( in.hasNext() ) in.skipValue(); // skip others
125                                    break;
126                                }
127                                in.endObject();
128                                while( in.hasNext() ) in.skipValue(); // skip others
129                                break;
130                            }
131                            in.endObject();
132
133                            while( in.hasNext() ) in.skipValue(); // additional results, not expected
134                        }
135                        in.endArray();
136                    }
137                    else in.skipValue();
138                }
139                in.endObject();
140            }
141            finally { in.close(); }
142        }
143        finally{ http.disconnect(); }
144        if( !areCoordinatesSet ) throw new Geocode.GeocodingException( "missing geocode location in response: " + url );
145
146        logger.info( "caching geocode: " + geocode );
147        geocode.write( geocodeTable );
148        return geocode;
149    }
150
151
152
153    /** The relational cache of geocoded residential addresses.
154      *
155      *     @see votorola.a.VoteServer.Run#geocodeTable()
156      */
157    public Geocode.Table geocodeTable() { return geocodeTable; }
158
159
160        private final Geocode.Table geocodeTable;
161
162
163
164//// P r i v a t e ///////////////////////////////////////////////////////////////////////
165
166
167    private static final Logger logger = LoggerX.i( GoogleGeocoder.class );
168
169
170
171}