001    /*
002     * Copyright 2008-2011 Marc Wick, geonames.org
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     *     http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     *
016     */
017    package org.geonames;
018    
019    import java.io.BufferedReader;
020    import java.io.IOException;
021    import java.io.InputStream;
022    import java.io.InputStreamReader;
023    import java.lang.reflect.Field;
024    import java.net.HttpURLConnection;
025    import java.net.MalformedURLException;
026    import java.net.URL;
027    import java.net.URLConnection;
028    import java.net.URLEncoder;
029    import java.text.ParseException;
030    import java.text.SimpleDateFormat;
031    import java.util.ArrayList;
032    import java.util.List;
033    import java.util.TimeZone;
034    import java.util.logging.Level;
035    import java.util.logging.Logger;
036    
037    import org.jdom.Document;
038    import org.jdom.Element;
039    import org.jdom.input.SAXBuilder;
040    
041    /**
042     * provides static methods to access the <a
043     * href="http://www.geonames.org/export/ws-overview.html">GeoNames web
044     * services</a>.
045     * <p>
046     * Note : values for some fields are only returned with sufficient {@link Style}
047     * . Accessing these fields (admin codes and admin names, elevation,population)
048     * will throw an {@link InsufficientStyleException} if the {@link Style} was not
049     * sufficient.
050     * 
051     * @author marc@geonames
052     * 
053     */
054    public class WebService {
055    
056            private static Logger logger = Logger.getLogger("org.geonames");
057    
058            private static String USER_AGENT = "gnwsc/1.1.5";
059    
060            private static boolean isAndroid = false;
061    
062            private static String geoNamesServer = "http://api.geonames.org";
063    
064            private static String geoNamesServerFailover = "http://api.geonames.org";
065    
066            private static long timeOfLastFailureMainServer;
067    
068            private static long averageConnectTime;
069    
070            private static long averageSampleSize = 20;
071    
072            private static Style defaultStyle = Style.MEDIUM;
073    
074            private static int readTimeOut = 120000;
075    
076            private static int connectTimeOut = 10000;
077    
078            private static String DATEFMT = "yyyy-MM-dd HH:mm:ss";
079    
080            static {
081                    USER_AGENT += " (";
082                    String os = System.getProperty("os.name");
083                    if (os != null) {
084                            USER_AGENT += os + ",";
085                    }
086                    String osVersion = System.getProperty("os.version");
087                    if (osVersion != null) {
088                            USER_AGENT += osVersion;
089                    }
090                    USER_AGENT += ")";
091    
092                    // android version
093                    try {
094                            Class aClass = Class.forName("android.os.Build");
095                            if (aClass != null) {
096                                    isAndroid = true;
097                                    Field[] fields = aClass.getFields();
098                                    if (fields != null) {
099                                            for (Field field : fields) {
100                                                    if ("MODEL".equalsIgnoreCase(field.getName())) {
101                                                            USER_AGENT += "(" + field.get(aClass) + ", ";
102                                                    }
103                                            }
104                                    }
105                                    aClass = Class.forName("android.os.Build$VERSION");
106                                    if (aClass != null) {
107                                            fields = aClass.getFields();
108                                            if (fields != null) {
109                                                    for (Field field : fields) {
110                                                            if ("RELEASE".equalsIgnoreCase(field.getName())) {
111                                                                    USER_AGENT += field.get(aClass);
112                                                            }
113                                                    }
114                                            }
115                                    }
116                                    USER_AGENT += ")";
117                            }
118                    } catch (Throwable t) {
119                    }
120            }
121    
122            /**
123             * user name to pass to commercial web services for authentication and
124             * authorization
125             */
126            private static String userName;
127    
128            /**
129             * token to pass to as optional authentication parameter to the commercial
130             * web services.
131             */
132            private static String token;
133    
134            /**
135             * adds the username stored in a static variable to the url. It also adds a
136             * token if one has been set with the static setter previously.
137             * 
138             * @param url
139             * @return url with the username appended
140             */
141            private static String addUserName(String url) {
142                    if (userName != null) {
143                            url = url + "&username=" + userName;
144                    }
145                    if (token != null) {
146                            url = url + "&token=" + token;
147                    }
148                    return url;
149            }
150    
151            /**
152             * adds the default style to the url. The default style can be set with the
153             * static setter. It is 'MEDIUM' if not set.
154             * 
155             * @param url
156             * @return url with the style parameter appended
157             */
158            private static String addDefaultStyle(String url) {
159                    if (defaultStyle != Style.MEDIUM) {
160                            url = url + "&style=" + defaultStyle.name();
161                    }
162                    return url;
163            }
164    
165            /**
166             * returns the currently active server. Normally this is the main server, if
167             * the main server recently failed then the failover server is returned. If
168             * the main server is not available we don't want to try with every request
169             * whether it is available again. We switch to the failover server and try
170             * from time to time whether the main server is again accessible.
171             * 
172             * @return
173             */
174            private static String getCurrentlyActiveServer() {
175                    if (timeOfLastFailureMainServer == 0) {
176                            // no problems with main server
177                            return geoNamesServer;
178                    }
179                    // we had problems with main server
180                    if (System.currentTimeMillis() - timeOfLastFailureMainServer > 1000l * 60l * 10l) {
181                            // but is was some time ago and we switch back to the main server to
182                            // retry. The problem may have been solved in the mean time.
183                            timeOfLastFailureMainServer = 0;
184                            return geoNamesServer;
185                    }
186                    if (System.currentTimeMillis() < timeOfLastFailureMainServer) {
187                            throw new Error("time of last failure cannot be in future.");
188                    }
189                    // the problems have been very recent and we continue with failover
190                    // server
191                    if (geoNamesServerFailover != null) {
192                            return geoNamesServerFailover;
193                    }
194                    return geoNamesServer;
195            }
196    
197            /**
198             * @return the isAndroid
199             */
200            public static boolean isAndroid() {
201                    return isAndroid;
202            }
203    
204            /**
205             * opens the connection to the url and sets the user agent. In case of an
206             * IOException it checks whether a failover server is set and connects to
207             * the failover server if it has been defined and if it is different from
208             * the normal server.
209             * 
210             * @param url
211             *            the url to connect to
212             * @return returns the inputstream for the connection
213             * @throws IOException
214             */
215            private static InputStream connect(String url) throws IOException {
216                    int status = 0;
217                    String currentlyActiveServer = getCurrentlyActiveServer();
218                    try {
219                            long begin = System.currentTimeMillis();
220                            HttpURLConnection httpConnection = (HttpURLConnection) new URL(
221                                            currentlyActiveServer + url).openConnection();
222                            httpConnection.setConnectTimeout(connectTimeOut);
223                            httpConnection.setReadTimeout(readTimeOut);
224                            httpConnection.setRequestProperty("User-Agent", USER_AGENT);
225                            InputStream in = httpConnection.getInputStream();
226                            status = httpConnection.getResponseCode();
227    
228                            if (status == 200) {
229                                    long elapsedTime = System.currentTimeMillis() - begin;
230                                    averageConnectTime = (averageConnectTime
231                                                    * (averageSampleSize - 1) + elapsedTime)
232                                                    / averageSampleSize;
233                                    // if the average elapsed time is too long we switch server
234                                    if (geoNamesServerFailover != null
235                                                    && averageConnectTime > 5000
236                                                    && !currentlyActiveServer
237                                                                    .equals(geoNamesServerFailover)) {
238                                            timeOfLastFailureMainServer = System.currentTimeMillis();
239                                    }
240                                    return in;
241                            }
242                    } catch (IOException e) {
243                            return tryFailoverServer(url, currentlyActiveServer, 0, e);
244                    }
245                    // we only get here if we had a statuscode <> 200
246                    IOException ioException = new IOException("status code " + status
247                                    + " for " + url);
248                    return tryFailoverServer(url, currentlyActiveServer, status,
249                                    ioException);
250            }
251    
252            private static InputStream tryFailoverServer(String url,
253                            String currentlyActiveServer, int status, IOException e)
254                            throws MalformedURLException, IOException {
255                    // we cannot reach the server
256                    logger.log(Level.WARNING, "problems connecting to geonames server "
257                                    + currentlyActiveServer, e);
258                    // is a failover server defined?
259                    if (geoNamesServerFailover == null
260                    // is it different from the one we are using?
261                                    || currentlyActiveServer.equals(geoNamesServerFailover)) {
262                            if (currentlyActiveServer.equals(geoNamesServerFailover)) {
263                                    // failover server is not accessible, we throw exception
264                                    // and switch back to main server.
265                                    timeOfLastFailureMainServer = 0;
266                            }
267                            throw e;
268                    }
269                    timeOfLastFailureMainServer = System.currentTimeMillis();
270                    logger.info("trying to connect to failover server "
271                                    + geoNamesServerFailover);
272                    // try failover server
273                    URLConnection conn = new URL(geoNamesServerFailover + url)
274                                    .openConnection();
275                    String userAgent = USER_AGENT + " failover from " + geoNamesServer;
276                    if (status != 0) {
277                            userAgent += " " + status;
278                    }
279                    conn.setRequestProperty("User-Agent", userAgent);
280                    InputStream in = conn.getInputStream();
281                    return in;
282            }
283    
284            private static Toponym getToponymFromElement(Element toponymElement) {
285                    Toponym toponym = new Toponym();
286    
287                    toponym.setName(toponymElement.getChildText("name"));
288                    toponym.setAlternateNames(toponymElement.getChildText("alternateNames"));
289                    toponym.setLatitude(Double.parseDouble(toponymElement
290                                    .getChildText("lat")));
291                    toponym.setLongitude(Double.parseDouble(toponymElement
292                                    .getChildText("lng")));
293    
294                    String geonameId = toponymElement.getChildText("geonameId");
295                    if (geonameId != null) {
296                            toponym.setGeoNameId(Integer.parseInt(geonameId));
297                    }
298    
299                    toponym.setContinentCode(toponymElement.getChildText("continentCode"));
300                    toponym.setCountryCode(toponymElement.getChildText("countryCode"));
301                    toponym.setCountryName(toponymElement.getChildText("countryName"));
302    
303                    toponym.setFeatureClass(FeatureClass.fromValue(toponymElement
304                                    .getChildText("fcl")));
305                    toponym.setFeatureCode(toponymElement.getChildText("fcode"));
306    
307                    toponym.setFeatureClassName(toponymElement.getChildText("fclName"));
308                    toponym.setFeatureCodeName(toponymElement.getChildText("fCodeName"));
309    
310                    String population = toponymElement.getChildText("population");
311                    if (population != null && !"".equals(population)) {
312                            toponym.setPopulation(Long.parseLong(population));
313                    }
314                    String elevation = toponymElement.getChildText("elevation");
315                    if (elevation != null && !"".equals(elevation)) {
316                            toponym.setElevation(Integer.parseInt(elevation));
317                    }
318    
319                    toponym.setAdminCode1(toponymElement.getChildText("adminCode1"));
320                    toponym.setAdminName1(toponymElement.getChildText("adminName1"));
321                    toponym.setAdminCode2(toponymElement.getChildText("adminCode2"));
322                    toponym.setAdminName2(toponymElement.getChildText("adminName2"));
323                    toponym.setAdminCode3(toponymElement.getChildText("adminCode3"));
324                    toponym.setAdminCode4(toponymElement.getChildText("adminCode4"));
325    
326                    Element timezoneElement = toponymElement.getChild("timezone");
327                    if (timezoneElement != null) {
328                            Timezone timezone = new Timezone();
329                            timezone.setTimezoneId(timezoneElement.getValue());
330                            timezone.setDstOffset(Double.parseDouble(timezoneElement
331                                            .getAttributeValue("dstOffset")));
332                            timezone.setGmtOffset(Double.parseDouble(timezoneElement
333                                            .getAttributeValue("gmtOffset")));
334                            toponym.setTimezone(timezone);
335                    }
336                    return toponym;
337            }
338    
339            private static WikipediaArticle getWikipediaArticleFromElement(
340                            Element wikipediaArticleElement) {
341                    WikipediaArticle wikipediaArticle = new WikipediaArticle();
342                    wikipediaArticle.setLanguage(wikipediaArticleElement
343                                    .getChildText("lang"));
344                    wikipediaArticle
345                                    .setTitle(wikipediaArticleElement.getChildText("title"));
346                    wikipediaArticle.setSummary(wikipediaArticleElement
347                                    .getChildText("summary"));
348                    wikipediaArticle.setFeature(wikipediaArticleElement
349                                    .getChildText("feature"));
350                    wikipediaArticle.setWikipediaUrl(wikipediaArticleElement
351                                    .getChildText("wikipediaUrl"));
352                    wikipediaArticle.setThumbnailImg(wikipediaArticleElement
353                                    .getChildText("thumbnailImg"));
354    
355                    wikipediaArticle.setLatitude(Double.parseDouble(wikipediaArticleElement
356                                    .getChildText("lat")));
357                    wikipediaArticle.setLongitude(Double
358                                    .parseDouble(wikipediaArticleElement.getChildText("lng")));
359    
360                    wikipediaArticle.setRank(Integer.parseInt(wikipediaArticleElement
361                                    .getChildText("rank")));
362    
363                    String population = wikipediaArticleElement.getChildText("population");
364                    if (population != null && !"".equals(population)) {
365                            wikipediaArticle.setPopulation(Integer.parseInt(population));
366                    }
367    
368                    String elevation = wikipediaArticleElement.getChildText("elevation");
369                    if (elevation != null && !"".equals(elevation)) {
370                            wikipediaArticle.setElevation(Integer.parseInt(elevation));
371                    }
372                    return wikipediaArticle;
373            }
374    
375            private static TimeZone utc = TimeZone.getTimeZone("UTC");
376    
377            private static WeatherObservation getWeatherObservationFromElement(
378                            Element weatherObservationElement) throws ParseException {
379                    WeatherObservation weatherObservation = new WeatherObservation();
380                    weatherObservation.setObservation(weatherObservationElement
381                                    .getChildText("observation"));
382                    SimpleDateFormat df = new SimpleDateFormat(DATEFMT);
383                    df.setTimeZone(utc);
384                    weatherObservation.setObservationTime(df
385                                    .parse(weatherObservationElement
386                                                    .getChildText("observationTime")));
387                    weatherObservation.setStationName(weatherObservationElement
388                                    .getChildText("stationName"));
389                    weatherObservation.setIcaoCode(weatherObservationElement
390                                    .getChildText("ICAO"));
391                    weatherObservation.setCountryCode(weatherObservationElement
392                                    .getChildText("countryCode"));
393                    String elevation = weatherObservationElement.getChildText("elevation");
394                    if (elevation != null && !"".equals(elevation)) {
395                            weatherObservation.setElevation(Integer.parseInt(elevation));
396                    }
397                    weatherObservation.setLatitude(Double
398                                    .parseDouble(weatherObservationElement.getChildText("lat")));
399                    weatherObservation.setLongitude(Double
400                                    .parseDouble(weatherObservationElement.getChildText("lng")));
401                    String temperature = weatherObservationElement
402                                    .getChildText("temperature");
403                    if (temperature != null && !"".equals(temperature)) {
404                            weatherObservation.setTemperature(Double.parseDouble(temperature));
405                    }
406                    String dewPoint = weatherObservationElement.getChildText("dewPoint");
407                    if (dewPoint != null && !"".equals(dewPoint)) {
408                            weatherObservation.setDewPoint(Double.parseDouble(dewPoint));
409                    }
410                    String humidity = weatherObservationElement.getChildText("humidity");
411                    if (humidity != null && !"".equals(humidity)) {
412                            weatherObservation.setHumidity(Double.parseDouble(humidity));
413                    }
414                    weatherObservation.setClouds(weatherObservationElement
415                                    .getChildText("clouds"));
416                    weatherObservation.setWeatherCondition(weatherObservationElement
417                                    .getChildText("weatherCondition"));
418                    weatherObservation.setWindSpeed(weatherObservationElement
419                                    .getChildText("windSpeed"));
420                    return weatherObservation;
421    
422            }
423    
424            /**
425             * returns a list of postal codes for the given parameters. This method is
426             * for convenience.
427             * 
428             * @param postalCode
429             * @param placeName
430             * @param countryCode
431             * @return
432             * @throws Exception
433             */
434            public static List<PostalCode> postalCodeSearch(String postalCode,
435                            String placeName, String countryCode) throws Exception {
436                    PostalCodeSearchCriteria postalCodeSearchCriteria = new PostalCodeSearchCriteria();
437                    postalCodeSearchCriteria.setPostalCode(postalCode);
438                    postalCodeSearchCriteria.setPlaceName(placeName);
439                    postalCodeSearchCriteria.setCountryCode(countryCode);
440                    return postalCodeSearch(postalCodeSearchCriteria);
441            }
442    
443            /**
444             * returns a list of postal codes for the given search criteria matching a
445             * full text search on the GeoNames postal codes database.
446             * 
447             * @param postalCodeSearchCriteria
448             * @return
449             * @throws Exception
450             */
451            public static List<PostalCode> postalCodeSearch(
452                            PostalCodeSearchCriteria postalCodeSearchCriteria) throws Exception {
453                    List<PostalCode> postalCodes = new ArrayList<PostalCode>();
454    
455                    String url = "/postalCodeSearch?";
456                    if (postalCodeSearchCriteria.getPostalCode() != null) {
457                            url = url
458                                            + "postalcode="
459                                            + URLEncoder.encode(
460                                                            postalCodeSearchCriteria.getPostalCode(), "UTF8");
461                    }
462                    if (postalCodeSearchCriteria.getPlaceName() != null) {
463                            if (!url.endsWith("&")) {
464                                    url = url + "&";
465                            }
466                            url = url
467                                            + "placename="
468                                            + URLEncoder.encode(
469                                                            postalCodeSearchCriteria.getPlaceName(), "UTF8");
470                    }
471                    if (postalCodeSearchCriteria.getAdminCode1() != null) {
472                            url = url
473                                            + "&adminCode1="
474                                            + URLEncoder.encode(
475                                                            postalCodeSearchCriteria.getAdminCode1(), "UTF8");
476                    }
477    
478                    if (postalCodeSearchCriteria.getCountryCode() != null) {
479                            if (!url.endsWith("&")) {
480                                    url = url + "&";
481                            }
482                            url = url + "country=" + postalCodeSearchCriteria.getCountryCode();
483                    }
484                    if (postalCodeSearchCriteria.getCountryBias() != null) {
485                            if (!url.endsWith("&")) {
486                                    url = url + "&";
487                            }
488                            url = url + "countryBias="
489                                            + postalCodeSearchCriteria.getCountryBias();
490                    }
491                    if (postalCodeSearchCriteria.getMaxRows() > 0) {
492                            url = url + "&maxRows=" + postalCodeSearchCriteria.getMaxRows();
493                    }
494                    if (postalCodeSearchCriteria.getStartRow() > 0) {
495                            url = url + "&startRow=" + postalCodeSearchCriteria.getStartRow();
496                    }
497                    if (postalCodeSearchCriteria.isOROperator()) {
498                            url = url + "&operator=OR";
499                    }
500                    if (postalCodeSearchCriteria.isReduced() != null) {
501                            url = url + "&isReduced="
502                                            + postalCodeSearchCriteria.isReduced().toString();
503                    }
504                    url = addUserName(url);
505    
506                    SAXBuilder parser = new SAXBuilder();
507                    Document doc = parser.build(connect(url));
508    
509                    Element root = rootAndCheckException(doc);
510                    for (Object obj : root.getChildren("code")) {
511                            Element codeElement = (Element) obj;
512                            PostalCode code = new PostalCode();
513                            code.setPostalCode(codeElement.getChildText("postalcode"));
514                            code.setPlaceName(codeElement.getChildText("name"));
515                            code.setCountryCode(codeElement.getChildText("countryCode"));
516                            code.setAdminCode1(codeElement.getChildText("adminCode1"));
517                            code.setAdminCode2(codeElement.getChildText("adminCode2"));
518                            code.setAdminName1(codeElement.getChildText("adminName1"));
519                            code.setAdminName2(codeElement.getChildText("adminName2"));
520    
521                            code.setLatitude(Double.parseDouble(codeElement.getChildText("lat")));
522                            code.setLongitude(Double.parseDouble(codeElement
523                                            .getChildText("lng")));
524    
525                            postalCodes.add(code);
526                    }
527    
528                    return postalCodes;
529            }
530    
531            /**
532             * returns a list of postal codes
533             * 
534             * @param postalCodeSearchCriteria
535             * @return
536             * @throws Exception
537             */
538            public static List<PostalCode> findNearbyPostalCodes(
539                            PostalCodeSearchCriteria postalCodeSearchCriteria) throws Exception {
540    
541                    List<PostalCode> postalCodes = new ArrayList<PostalCode>();
542    
543                    String url = "/findNearbyPostalCodes?";
544                    if (postalCodeSearchCriteria.getPostalCode() != null) {
545                            url = url
546                                            + "&postalcode="
547                                            + URLEncoder.encode(
548                                                            postalCodeSearchCriteria.getPostalCode(), "UTF8");
549                    }
550                    if (postalCodeSearchCriteria.getPlaceName() != null) {
551                            url = url
552                                            + "&placename="
553                                            + URLEncoder.encode(
554                                                            postalCodeSearchCriteria.getPlaceName(), "UTF8");
555                    }
556                    if (postalCodeSearchCriteria.getCountryCode() != null) {
557                            url = url + "&country=" + postalCodeSearchCriteria.getCountryCode();
558                    }
559    
560                    if (postalCodeSearchCriteria.getLatitude() != null) {
561                            url = url + "&lat=" + postalCodeSearchCriteria.getLatitude();
562                    }
563                    if (postalCodeSearchCriteria.getLongitude() != null) {
564                            url = url + "&lng=" + postalCodeSearchCriteria.getLongitude();
565                    }
566                    if (postalCodeSearchCriteria.getStyle() != null) {
567                            url = url + "&style=" + postalCodeSearchCriteria.getStyle();
568                    }
569                    if (postalCodeSearchCriteria.getMaxRows() > 0) {
570                            url = url + "&maxRows=" + postalCodeSearchCriteria.getMaxRows();
571                    }
572    
573                    if (postalCodeSearchCriteria.getRadius() > 0) {
574                            url = url + "&radius=" + postalCodeSearchCriteria.getRadius();
575                    }
576                    url = addUserName(url);
577    
578                    SAXBuilder parser = new SAXBuilder();
579                    Document doc = parser.build(connect(url));
580    
581                    Element root = rootAndCheckException(doc);
582                    for (Object obj : root.getChildren("code")) {
583                            Element codeElement = (Element) obj;
584                            PostalCode code = new PostalCode();
585                            code.setPostalCode(codeElement.getChildText("postalcode"));
586                            code.setPlaceName(codeElement.getChildText("name"));
587                            code.setCountryCode(codeElement.getChildText("countryCode"));
588    
589                            code.setLatitude(Double.parseDouble(codeElement.getChildText("lat")));
590                            code.setLongitude(Double.parseDouble(codeElement
591                                            .getChildText("lng")));
592    
593                            code.setAdminName1(codeElement.getChildText("adminName1"));
594                            code.setAdminCode1(codeElement.getChildText("adminCode1"));
595                            code.setAdminName2(codeElement.getChildText("adminName2"));
596                            code.setAdminCode2(codeElement.getChildText("adminCode2"));
597    
598                            if (codeElement.getChildText("distance") != null) {
599                                    code.setDistance(Double.parseDouble(codeElement
600                                                    .getChildText("distance")));
601                            }
602    
603                            postalCodes.add(code);
604                    }
605    
606                    return postalCodes;
607            }
608    
609            /**
610             * convenience method for
611             * {@link #findNearbyPlaceName(double,double,double,int)}
612             * 
613             * @param latitude
614             * @param longitude
615             * @return
616             * @throws IOException
617             * @throws Exception
618             */
619            public static List<Toponym> findNearbyPlaceName(double latitude,
620                            double longitude) throws IOException, Exception {
621                    return findNearbyPlaceName(latitude, longitude, 0, 0);
622            }
623    
624            public static List<Toponym> findNearbyPlaceName(double latitude,
625                            double longitude, double radius, int maxRows) throws IOException,
626                            Exception {
627                    List<Toponym> places = new ArrayList<Toponym>();
628    
629                    String url = "/findNearbyPlaceName?";
630    
631                    url = url + "&lat=" + latitude;
632                    url = url + "&lng=" + longitude;
633                    if (radius > 0) {
634                            url = url + "&radius=" + radius;
635                    }
636                    if (maxRows > 0) {
637                            url = url + "&maxRows=" + maxRows;
638                    }
639                    url = addUserName(url);
640                    url = addDefaultStyle(url);
641    
642                    SAXBuilder parser = new SAXBuilder();
643                    Document doc = parser.build(connect(url));
644    
645                    Element root = rootAndCheckException(doc);
646                    for (Object obj : root.getChildren("geoname")) {
647                            Element toponymElement = (Element) obj;
648                            Toponym toponym = getToponymFromElement(toponymElement);
649                            places.add(toponym);
650                    }
651    
652                    return places;
653            }
654    
655            public static List<Toponym> findNearby(double latitude, double longitude,
656                            FeatureClass featureClass, String[] featureCodes)
657                            throws IOException, Exception {
658                    return findNearby(latitude, longitude, 0, featureClass, featureCodes,
659                                    null, 0);
660            }
661    
662            /* Overload function to allow backward compatibility */
663            /**
664             * Based on the following inforamtion: Webservice Type : REST
665             * ws.geonames.org/findNearbyWikipedia? Parameters : lang : language code
666             * (around 240 languages) (default = en) lat,lng, radius (in km), maxRows
667             * (default = 10) featureClass featureCode Example:
668             * http://ws.geonames.org/findNearby?lat=47.3&lng=9
669             * 
670             * @param: latitude
671             * @param: longitude
672             * @param: radius
673             * @param: feature Class
674             * @param: feature Codes
675             * @param: language
676             * @param: maxRows
677             * @return: list of wikipedia articles
678             * @throws: Exception
679             */
680            public static List<Toponym> findNearby(double latitude, double longitude,
681                            double radius, FeatureClass featureClass, String[] featureCodes,
682                            String language, int maxRows) throws IOException, Exception {
683                    List<Toponym> places = new ArrayList<Toponym>();
684    
685                    String url = "/findNearby?";
686    
687                    url += "&lat=" + latitude;
688                    url += "&lng=" + longitude;
689                    if (radius > 0) {
690                            url = url + "&radius=" + radius;
691                    }
692                    if (maxRows > 0) {
693                            url = url + "&maxRows=" + maxRows;
694                    }
695    
696                    if (language != null) {
697                            url = url + "&lang=" + language;
698                    }
699    
700                    if (featureClass != null) {
701                            url += "&featureClass=" + featureClass;
702                    }
703                    if (featureCodes != null && featureCodes.length > 0) {
704                            for (String featureCode : featureCodes) {
705                                    url += "&featureCode=" + featureCode;
706                            }
707                    }
708    
709                    url = addUserName(url);
710                    url = addDefaultStyle(url);
711    
712                    SAXBuilder parser = new SAXBuilder();
713                    Document doc = parser.build(connect(url));
714    
715                    Element root = rootAndCheckException(doc);
716                    for (Object obj : root.getChildren("geoname")) {
717                            Element toponymElement = (Element) obj;
718                            Toponym toponym = getToponymFromElement(toponymElement);
719                            places.add(toponym);
720                    }
721    
722                    return places;
723            }
724    
725            /**
726             * 
727             * @param geoNameId
728             * @param language
729             *            - optional
730             * @param style
731             *            - optional
732             * @return the toponym for the geoNameId
733             * @throws IOException
734             * @throws Exception
735             */
736            public static Toponym get(int geoNameId, String language, String style)
737                            throws IOException, Exception {
738                    String url = "/get?";
739    
740                    url += "geonameId=" + geoNameId;
741    
742                    if (language != null) {
743                            url = url + "&lang=" + language;
744                    }
745    
746                    if (style != null) {
747                            url = url + "&style=" + style;
748                    } else {
749                            url = addDefaultStyle(url);
750                    }
751                    url = addUserName(url);
752    
753                    SAXBuilder parser = new SAXBuilder();
754                    Document doc = parser.build(connect(url));
755    
756                    Element root = rootAndCheckException(doc);
757                    Toponym toponym = getToponymFromElement(root);
758                    return toponym;
759            }
760    
761            public static Address findNearestAddress(double latitude, double longitude)
762                            throws IOException, Exception {
763    
764                    String url = "/findNearestAddress?";
765    
766                    url = url + "&lat=" + latitude;
767                    url = url + "&lng=" + longitude;
768                    url = addUserName(url);
769    
770                    SAXBuilder parser = new SAXBuilder();
771                    Document doc = parser.build(connect(url));
772    
773                    Element root = rootAndCheckException(doc);
774                    for (Object obj : root.getChildren("address")) {
775                            Element codeElement = (Element) obj;
776                            Address address = new Address();
777                            address.setStreet(codeElement.getChildText("street"));
778                            address.setStreetNumber(codeElement.getChildText("streetNumber"));
779                            address.setMtfcc(codeElement.getChildText("mtfcc"));
780    
781                            address.setPostalCode(codeElement.getChildText("postalcode"));
782                            address.setPlaceName(codeElement.getChildText("placename"));
783                            address.setCountryCode(codeElement.getChildText("countryCode"));
784    
785                            address.setLatitude(Double.parseDouble(codeElement
786                                            .getChildText("lat")));
787                            address.setLongitude(Double.parseDouble(codeElement
788                                            .getChildText("lng")));
789    
790                            address.setAdminName1(codeElement.getChildText("adminName1"));
791                            address.setAdminCode1(codeElement.getChildText("adminCode1"));
792                            address.setAdminName2(codeElement.getChildText("adminName2"));
793                            address.setAdminCode2(codeElement.getChildText("adminCode2"));
794    
795                            address.setDistance(Double.parseDouble(codeElement
796                                            .getChildText("distance")));
797    
798                            return address;
799                    }
800    
801                    return null;
802            }
803    
804            public static Intersection findNearestIntersection(double latitude,
805                            double longitude) throws Exception {
806                    return findNearestIntersection(latitude, longitude, 0);
807            }
808    
809            public static Intersection findNearestIntersection(double latitude,
810                            double longitude, double radius) throws Exception {
811    
812                    String url = "/findNearestIntersection?";
813    
814                    url = url + "&lat=" + latitude;
815                    url = url + "&lng=" + longitude;
816                    if (radius > 0) {
817                            url = url + "&radius=" + radius;
818                    }
819                    url = addUserName(url);
820    
821                    SAXBuilder parser = new SAXBuilder();
822                    Document doc = parser.build(connect(url));
823    
824                    Element root = rootAndCheckException(doc);
825                    for (Object obj : root.getChildren("intersection")) {
826                            Element e = (Element) obj;
827                            Intersection intersection = new Intersection();
828                            intersection.setStreet1(e.getChildText("street1"));
829                            intersection.setStreet2(e.getChildText("street2"));
830                            intersection.setLatitude(Double.parseDouble(e.getChildText("lat")));
831                            intersection
832                                            .setLongitude(Double.parseDouble(e.getChildText("lng")));
833                            intersection.setDistance(Double.parseDouble(e
834                                            .getChildText("distance")));
835                            intersection.setPostalCode(e.getChildText("postalcode"));
836                            intersection.setPlaceName(e.getChildText("placename"));
837                            intersection.setCountryCode(e.getChildText("countryCode"));
838                            intersection.setAdminName2(e.getChildText("adminName2"));
839                            intersection.setAdminCode1(e.getChildText("adminCode1"));
840                            intersection.setAdminName1(e.getChildText("adminName1"));
841                            return intersection;
842                    }
843                    return null;
844            }
845    
846            /**
847             * 
848             * @see <a * href=
849             *      "http://www.geonames.org/maps/reverse-geocoder.html#findNearbyStreets"
850             *      > web service documentation</a>
851             * 
852             * @param latitude
853             * @param longitude
854             * @param radius
855             * @return
856             * @throws Exception
857             */
858            public static List<StreetSegment> findNearbyStreets(double latitude,
859                            double longitude, double radius) throws Exception {
860    
861                    String url = "/findNearbyStreets?";
862    
863                    url = url + "&lat=" + latitude;
864                    url = url + "&lng=" + longitude;
865                    if (radius > 0) {
866                            url = url + "&radius=" + radius;
867                    }
868                    url = addUserName(url);
869    
870                    List<StreetSegment> segments = new ArrayList<StreetSegment>();
871    
872                    SAXBuilder parser = new SAXBuilder();
873                    Document doc = parser.build(connect(url));
874    
875                    Element root = rootAndCheckException(doc);
876                    for (Object obj : root.getChildren("streetSegment")) {
877                            Element e = (Element) obj;
878                            StreetSegment streetSegment = new StreetSegment();
879                            String line = e.getChildText("line");
880                            String[] points = line.split(",");
881                            double[] latArray = new double[points.length];
882                            double[] lngArray = new double[points.length];
883                            for (int i = 0; i < points.length; i++) {
884                                    String[] coords = points[i].split(" ");
885                                    lngArray[i] = Double.parseDouble(coords[0]);
886                                    latArray[i] = Double.parseDouble(coords[1]);
887                            }
888    
889                            streetSegment.setCfcc(e.getChildText("cfcc"));
890                            streetSegment.setName(e.getChildText("name"));
891                            streetSegment.setFraddl(e.getChildText("fraddl"));
892                            streetSegment.setFraddr(e.getChildText("fraddr"));
893                            streetSegment.setToaddl(e.getChildText("toaddl"));
894                            streetSegment.setToaddr(e.getChildText("toaddr"));
895                            streetSegment.setPostalCode(e.getChildText("postalcode"));
896                            streetSegment.setPlaceName(e.getChildText("placename"));
897                            streetSegment.setCountryCode(e.getChildText("countryCode"));
898                            streetSegment.setAdminName2(e.getChildText("adminName2"));
899                            streetSegment.setAdminCode1(e.getChildText("adminCode1"));
900                            streetSegment.setAdminName1(e.getChildText("adminName1"));
901                            segments.add(streetSegment);
902                    }
903                    return segments;
904            }
905    
906            public static List<StreetSegment> findNearbyStreetsOSM(double latitude,
907                            double longitude, double radius) throws Exception {
908    
909                    String url = "/findNearbyStreetsOSM?";
910    
911                    url = url + "&lat=" + latitude;
912                    url = url + "&lng=" + longitude;
913                    if (radius > 0) {
914                            url = url + "&radius=" + radius;
915                    }
916                    url = addUserName(url);
917    
918                    List<StreetSegment> segments = new ArrayList<StreetSegment>();
919    
920                    SAXBuilder parser = new SAXBuilder();
921                    Document doc = parser.build(connect(url));
922    
923                    Element root = rootAndCheckException(doc);
924                    for (Object obj : root.getChildren("streetSegment")) {
925                            Element e = (Element) obj;
926                            StreetSegment streetSegment = new StreetSegment();
927                            String line = e.getChildText("line");
928                            String[] points = line.split(",");
929                            double[] latArray = new double[points.length];
930                            double[] lngArray = new double[points.length];
931                            for (int i = 0; i < points.length; i++) {
932                                    String[] coords = points[i].split(" ");
933                                    lngArray[i] = Double.parseDouble(coords[0]);
934                                    latArray[i] = Double.parseDouble(coords[1]);
935                            }
936    
937                            streetSegment.setName(e.getChildText("name"));
938                            segments.add(streetSegment);
939                    }
940                    return segments;
941            }
942    
943            /**
944             * convenience method for {@link #search(ToponymSearchCriteria)}
945             * 
946             * @see <a href="http://www.geonames.org/export/geonames-search.html">search
947             *      web service documentation</a>
948             * 
949             * @param q
950             * @param countryCode
951             * @param name
952             * @param featureCodes
953             * @param startRow
954             * @return
955             * @throws Exception
956             */
957            public static ToponymSearchResult search(String q, String countryCode,
958                            String name, String[] featureCodes, int startRow) throws Exception {
959                    return search(q, countryCode, name, featureCodes, startRow, null, null,
960                                    null);
961            }
962    
963            /**
964             * convenience method for {@link #search(ToponymSearchCriteria)}
965             * 
966             * The string fields will be transparently utf8 encoded within the call.
967             * 
968             * @see <a href="http://www.geonames.org/export/geonames-search.html">search
969             *      web service documentation</a>
970             * 
971             * @param q
972             *            search over all fields
973             * @param countryCode
974             * @param name
975             *            search over name only
976             * @param featureCodes
977             * @param startRow
978             * @param language
979             * @param style
980             * @param exactName
981             * @return
982             * @throws Exception
983             */
984            public static ToponymSearchResult search(String q, String countryCode,
985                            String name, String[] featureCodes, int startRow, String language,
986                            Style style, String exactName) throws Exception {
987                    ToponymSearchCriteria searchCriteria = new ToponymSearchCriteria();
988                    searchCriteria.setQ(q);
989                    searchCriteria.setCountryCode(countryCode);
990                    searchCriteria.setName(name);
991                    searchCriteria.setFeatureCodes(featureCodes);
992                    searchCriteria.setStartRow(startRow);
993                    searchCriteria.setLanguage(language);
994                    searchCriteria.setStyle(style);
995                    searchCriteria.setNameEquals(exactName);
996                    return search(searchCriteria);
997            }
998    
999            /**
1000             * full text search on the GeoNames database.
1001             * 
1002             * This service gets the number of toponyms defined by the 'maxRows'
1003             * parameter. The parameter 'style' determines which fields are returned by
1004             * the service.
1005             * 
1006             * @see <a href="http://www.geonames.org/export/geonames-search.html">search
1007             *      web service documentation</a>
1008             * 
1009             * <br>
1010             * 
1011             *      <pre>
1012             * ToponymSearchCriteria searchCriteria = new ToponymSearchCriteria();
1013             * searchCriteria.setQ(&quot;z&amp;uumlrich&quot;);
1014             * ToponymSearchResult searchResult = WebService.search(searchCriteria);
1015             * for (Toponym toponym : searchResult.toponyms) {
1016             *      System.out.println(toponym.getName() + &quot; &quot; + toponym.getCountryName());
1017             * }
1018             * </pre>
1019             * 
1020             * 
1021             * @param searchCriteria
1022             * @return
1023             * @throws Exception
1024             */
1025            public static ToponymSearchResult search(
1026                            ToponymSearchCriteria searchCriteria) throws Exception {
1027                    ToponymSearchResult searchResult = new ToponymSearchResult();
1028    
1029                    String url = "/search?";
1030    
1031                    if (searchCriteria.getQ() != null) {
1032                            url = url + "q=" + URLEncoder.encode(searchCriteria.getQ(), "UTF8");
1033                    }
1034                    if (searchCriteria.getNameEquals() != null) {
1035                            url = url + "&name_equals="
1036                                            + URLEncoder.encode(searchCriteria.getNameEquals(), "UTF8");
1037                    }
1038                    if (searchCriteria.getNameStartsWith() != null) {
1039                            url = url
1040                                            + "&name_startsWith="
1041                                            + URLEncoder.encode(searchCriteria.getNameStartsWith(),
1042                                                            "UTF8");
1043                    }
1044    
1045                    if (searchCriteria.getName() != null) {
1046                            url = url + "&name="
1047                                            + URLEncoder.encode(searchCriteria.getName(), "UTF8");
1048                    }
1049    
1050                    if (searchCriteria.getTag() != null) {
1051                            url = url + "&tag="
1052                                            + URLEncoder.encode(searchCriteria.getTag(), "UTF8");
1053                    }
1054    
1055                    if (searchCriteria.getCountryCode() != null) {
1056                            url = url + "&country=" + searchCriteria.getCountryCode();
1057                    }
1058                    if (searchCriteria.getCountryBias() != null) {
1059                            if (!url.endsWith("&")) {
1060                                    url = url + "&";
1061                            }
1062                            url = url + "countryBias=" + searchCriteria.getCountryBias();
1063                    }
1064                    if (searchCriteria.getContinentCode() != null) {
1065                            url = url + "&continentCode=" + searchCriteria.getContinentCode();
1066                    }
1067    
1068                    if (searchCriteria.getAdminCode1() != null) {
1069                            url = url + "&adminCode1="
1070                                            + URLEncoder.encode(searchCriteria.getAdminCode1(), "UTF8");
1071                    }
1072                    if (searchCriteria.getAdminCode2() != null) {
1073                            url = url + "&adminCode2="
1074                                            + URLEncoder.encode(searchCriteria.getAdminCode2(), "UTF8");
1075                    }
1076                    if (searchCriteria.getAdminCode3() != null) {
1077                            url = url + "&adminCode3="
1078                                            + URLEncoder.encode(searchCriteria.getAdminCode3(), "UTF8");
1079                    }
1080                    if (searchCriteria.getAdminCode4() != null) {
1081                            url = url + "&adminCode4="
1082                                            + URLEncoder.encode(searchCriteria.getAdminCode4(), "UTF8");
1083                    }
1084    
1085                    if (searchCriteria.getLanguage() != null) {
1086                            url = url + "&lang=" + searchCriteria.getLanguage();
1087                    }
1088    
1089                    if (searchCriteria.getFeatureClass() != null) {
1090                            url = url + "&featureClass=" + searchCriteria.getFeatureClass();
1091                    }
1092    
1093                    if (searchCriteria.getFeatureCodes() != null) {
1094                            for (String featureCode : searchCriteria.getFeatureCodes()) {
1095                                    url = url + "&fcode=" + featureCode;
1096                            }
1097                    }
1098                    if (searchCriteria.getMaxRows() > 0) {
1099                            url = url + "&maxRows=" + searchCriteria.getMaxRows();
1100                    }
1101                    if (searchCriteria.getStartRow() > 0) {
1102                            url = url + "&startRow=" + searchCriteria.getStartRow();
1103                    }
1104    
1105                    if (searchCriteria.getStyle() != null) {
1106                            url = url + "&style=" + searchCriteria.getStyle();
1107                    } else {
1108                            url = addDefaultStyle(url);
1109                    }
1110                    url = addUserName(url);
1111    
1112                    SAXBuilder parser = new SAXBuilder();
1113                    Document doc = parser.build(connect(url));
1114    
1115                    Element root = rootAndCheckException(doc);
1116    
1117                    searchResult.totalResultsCount = Integer.parseInt(root
1118                                    .getChildText("totalResultsCount"));
1119                    searchResult.setStyle(Style.valueOf(root.getAttributeValue("style")));
1120    
1121                    for (Object obj : root.getChildren("geoname")) {
1122                            Element toponymElement = (Element) obj;
1123                            Toponym toponym = getToponymFromElement(toponymElement);
1124                            toponym.setStyle(searchResult.getStyle());
1125                            searchResult.toponyms.add(toponym);
1126                    }
1127    
1128                    return searchResult;
1129            }
1130    
1131            /**
1132             * returns the children in the administrative hierarchy of a toponym.
1133             * 
1134             * @param geonameId
1135             * @param language
1136             * @param style
1137             * @return
1138             * @throws Exception
1139             */
1140            public static ToponymSearchResult children(int geonameId, String language,
1141                            Style style) throws Exception {
1142                    ToponymSearchResult searchResult = new ToponymSearchResult();
1143    
1144                    String url = "/children?";
1145    
1146                    url = url + "geonameId=" + geonameId;
1147    
1148                    if (language != null) {
1149                            url = url + "&lang=" + language;
1150                    }
1151    
1152                    if (style != null) {
1153                            url = url + "&style=" + style;
1154                    } else {
1155                            url = addDefaultStyle(url);
1156                    }
1157                    url = addUserName(url);
1158    
1159                    SAXBuilder parser = new SAXBuilder();
1160                    Document doc = parser.build(connect(url));
1161    
1162                    Element root = rootAndCheckException(doc);
1163    
1164                    searchResult.totalResultsCount = Integer.parseInt(root
1165                                    .getChildText("totalResultsCount"));
1166                    searchResult.setStyle(Style.valueOf(root.getAttributeValue("style")));
1167    
1168                    for (Object obj : root.getChildren("geoname")) {
1169                            Element toponymElement = (Element) obj;
1170                            Toponym toponym = getToponymFromElement(toponymElement);
1171                            searchResult.toponyms.add(toponym);
1172                    }
1173    
1174                    return searchResult;
1175            }
1176    
1177            /**
1178             * returns the neighbours of a toponym.
1179             * 
1180             * @param geonameId
1181             * @param language
1182             * @param style
1183             * @return
1184             * @throws Exception
1185             */
1186            public static ToponymSearchResult neighbours(int geonameId,
1187                            String language, Style style) throws Exception {
1188                    ToponymSearchResult searchResult = new ToponymSearchResult();
1189    
1190                    String url = "/neighbours?";
1191    
1192                    url = url + "geonameId=" + geonameId;
1193    
1194                    if (language != null) {
1195                            url = url + "&lang=" + language;
1196                    }
1197    
1198                    if (style != null) {
1199                            url = url + "&style=" + style;
1200                    } else {
1201                            url = addDefaultStyle(url);
1202                    }
1203                    url = addUserName(url);
1204    
1205                    SAXBuilder parser = new SAXBuilder();
1206                    Document doc = parser.build(connect(url));
1207    
1208                    Element root = rootAndCheckException(doc);
1209    
1210                    searchResult.totalResultsCount = Integer.parseInt(root
1211                                    .getChildText("totalResultsCount"));
1212                    searchResult.setStyle(Style.valueOf(root.getAttributeValue("style")));
1213    
1214                    for (Object obj : root.getChildren("geoname")) {
1215                            Element toponymElement = (Element) obj;
1216                            Toponym toponym = getToponymFromElement(toponymElement);
1217                            searchResult.toponyms.add(toponym);
1218                    }
1219    
1220                    return searchResult;
1221            }
1222    
1223            /**
1224             * returns the hierarchy for a geonameId
1225             * 
1226             * @see <a
1227             *      href="http://www.geonames.org/export/place-hierarchy.html#hierarchy">Hierarchy
1228             *      service description</a>
1229             * 
1230             * @param geonameId
1231             * @param language
1232             * @param style
1233             * @return
1234             * @throws Exception
1235             */
1236            public static List<Toponym> hierarchy(int geonameId, String language,
1237                            Style style) throws Exception {
1238    
1239                    String url = "/hierarchy?";
1240    
1241                    url = url + "geonameId=" + geonameId;
1242    
1243                    if (language != null) {
1244                            url = url + "&lang=" + language;
1245                    }
1246    
1247                    if (style != null) {
1248                            url = url + "&style=" + style;
1249                    } else {
1250                            url = addDefaultStyle(url);
1251                    }
1252                    url = addUserName(url);
1253    
1254                    SAXBuilder parser = new SAXBuilder();
1255                    Document doc = parser.build(connect(url));
1256    
1257                    Element root = rootAndCheckException(doc);
1258    
1259                    List<Toponym> toponyms = new ArrayList<Toponym>();
1260                    for (Object obj : root.getChildren("geoname")) {
1261                            Element toponymElement = (Element) obj;
1262                            Toponym toponym = getToponymFromElement(toponymElement);
1263                            toponyms.add(toponym);
1264                    }
1265    
1266                    return toponyms;
1267            }
1268    
1269            public static void saveTags(String[] tags, Toponym toponym,
1270                            String username, String password) throws Exception {
1271                    if (toponym.getGeoNameId() == 0) {
1272                            throw new Error("no geonameid specified");
1273                    }
1274    
1275                    // FIXME proper url
1276                    String url = "/servlet/geonames?srv=61";
1277    
1278                    url = url + "&geonameId=" + toponym.getGeoNameId();
1279                    url = addUserName(url);
1280    
1281                    StringBuilder tagsCommaseparated = new StringBuilder();
1282                    for (String tag : tags) {
1283                            tagsCommaseparated.append(tag + ",");
1284                    }
1285                    url = url + "&tag=" + tagsCommaseparated;
1286    
1287                    SAXBuilder parser = new SAXBuilder();
1288                    Document doc = parser.build(connect(url));
1289    
1290                    Element root = rootAndCheckException(doc);
1291            }
1292    
1293            private static Element rootAndCheckException(Document doc)
1294                            throws GeoNamesException {
1295                    Element root = doc.getRootElement();
1296                    checkException(root);
1297                    return root;
1298            }
1299    
1300            private static void checkException(Element root) throws GeoNamesException {
1301                    Element message = root.getChild("status");
1302                    if (message != null) {
1303                            int code = 0;
1304                            try {
1305                                    code = Integer.parseInt(message.getAttributeValue("value"));
1306                            } catch (NumberFormatException numberFormatException) {
1307                            }
1308                            throw new GeoNamesException(code,
1309                                            message.getAttributeValue("message"));
1310                    }
1311            }
1312    
1313            /**
1314             * full text search on geolocated wikipedia articles.
1315             * 
1316             * @param q
1317             * @param language
1318             * @return
1319             * @throws Exception
1320             */
1321            public static List<WikipediaArticle> wikipediaSearch(String q,
1322                            String language) throws Exception {
1323                    List<WikipediaArticle> articles = new ArrayList<WikipediaArticle>();
1324    
1325                    String url = "/wikipediaSearch?";
1326    
1327                    url = url + "q=" + URLEncoder.encode(q, "UTF8");
1328    
1329                    if (language != null) {
1330                            url = url + "&lang=" + language;
1331                    }
1332                    url = addUserName(url);
1333    
1334                    SAXBuilder parser = new SAXBuilder();
1335                    Document doc = parser.build(connect(url));
1336    
1337                    Element root = rootAndCheckException(doc);
1338                    for (Object obj : root.getChildren("entry")) {
1339                            Element wikipediaArticleElement = (Element) obj;
1340                            WikipediaArticle wikipediaArticle = getWikipediaArticleFromElement(wikipediaArticleElement);
1341                            articles.add(wikipediaArticle);
1342                    }
1343    
1344                    return articles;
1345            }
1346    
1347            /**
1348             * full text search on geolocated wikipedia articles.
1349             * 
1350             * @param title
1351             * @param language
1352             * @return
1353             * @throws Exception
1354             */
1355            public static List<WikipediaArticle> wikipediaSearchForTitle(String title,
1356                            String language) throws Exception {
1357                    List<WikipediaArticle> articles = new ArrayList<WikipediaArticle>();
1358    
1359                    String url = "/wikipediaSearch?";
1360    
1361                    url = url + "title=" + URLEncoder.encode(title, "UTF8");
1362    
1363                    if (language != null) {
1364                            url = url + "&lang=" + language;
1365                    }
1366                    url = addUserName(url);
1367    
1368                    SAXBuilder parser = new SAXBuilder();
1369                    Document doc = parser.build(connect(url));
1370    
1371                    Element root = rootAndCheckException(doc);
1372                    for (Object obj : root.getChildren("entry")) {
1373                            Element wikipediaArticleElement = (Element) obj;
1374                            WikipediaArticle wikipediaArticle = getWikipediaArticleFromElement(wikipediaArticleElement);
1375                            articles.add(wikipediaArticle);
1376                    }
1377    
1378                    return articles;
1379            }
1380    
1381            public static List<WikipediaArticle> findNearbyWikipedia(double latitude,
1382                            double longitude, String language) throws Exception {
1383                    return findNearbyWikipedia(latitude, longitude, 0, language, 0);
1384            }
1385    
1386            /* Overload function to allow backward compatibility */
1387            /**
1388             * Based on the following inform: Webservice Type : REST
1389             * ws.geonames.org/findNearbyWikipedia? Parameters : lang : language code
1390             * (around 240 languages) (default = en) lat,lng, radius (in km), maxRows
1391             * (default = 5) Example:
1392             * http://ws.geonames.org/findNearbyWikipedia?lat=47&lng=9
1393             * 
1394             * @param: latitude
1395             * @param: longitude
1396             * @param: radius
1397             * @param: language
1398             * @param: maxRows
1399             * @return: list of wikipedia articles
1400             * @throws: Exception
1401             */
1402            public static List<WikipediaArticle> findNearbyWikipedia(double latitude,
1403                            double longitude, double radius, String language, int maxRows)
1404                            throws Exception {
1405    
1406                    List<WikipediaArticle> articles = new ArrayList<WikipediaArticle>();
1407    
1408                    String url = "/findNearbyWikipedia?";
1409    
1410                    url = url + "lat=" + latitude;
1411                    url = url + "&lng=" + longitude;
1412                    if (radius > 0) {
1413                            url = url + "&radius=" + radius;
1414                    }
1415                    if (maxRows > 0) {
1416                            url = url + "&maxRows=" + maxRows;
1417                    }
1418    
1419                    if (language != null) {
1420                            url = url + "&lang=" + language;
1421                    }
1422                    url = addUserName(url);
1423    
1424                    SAXBuilder parser = new SAXBuilder();
1425                    Document doc = parser.build(connect(url));
1426    
1427                    Element root = rootAndCheckException(doc);
1428                    for (Object obj : root.getChildren("entry")) {
1429                            Element wikipediaArticleElement = (Element) obj;
1430                            WikipediaArticle wikipediaArticle = getWikipediaArticleFromElement(wikipediaArticleElement);
1431                            articles.add(wikipediaArticle);
1432                    }
1433    
1434                    return articles;
1435            }
1436    
1437            /**
1438             * GTOPO30 is a global digital elevation model (DEM) with a horizontal grid
1439             * spacing of 30 arc seconds (approximately 1 kilometer). GTOPO30 was
1440             * derived from several raster and vector sources of topographic
1441             * information.
1442             * 
1443             * @param latitude
1444             * @param longitude
1445             * @return a single number giving the elevation in meters according to
1446             *         gtopo30, ocean areas have been masked as "no data" and have been
1447             *         assigned a value of -9999
1448             * @throws IOException
1449             */
1450            public static int gtopo30(double latitude, double longitude)
1451                            throws IOException {
1452                    String url = "/gtopo30?lat=" + latitude + "&lng=" + longitude;
1453                    url = addUserName(url);
1454                    BufferedReader in = new BufferedReader(new InputStreamReader(
1455                                    connect(url)));
1456                    String gtopo30 = in.readLine();
1457                    in.close();
1458                    return Integer.parseInt(gtopo30);
1459            }
1460    
1461            /**
1462             * Shuttle Radar Topography Mission (SRTM) elevation data. SRTM consisted of
1463             * a specially modified radar system that flew onboard the Space Shuttle
1464             * Endeavour during an 11-day mission in February of 2000. The dataset
1465             * covers land areas between 60 degrees north and 56 degrees south. This web
1466             * service is using SRTM3 data with data points located every 3-arc-second
1467             * (approximately 90 meters) on a latitude/longitude grid.
1468             * 
1469             * @param latitude
1470             * @param longitude
1471             * @return elevation or -32768 if unknown
1472             * @throws IOException
1473             */
1474            public static int srtm3(double latitude, double longitude)
1475                            throws IOException {
1476                    String url = "/srtm3?lat=" + latitude + "&lng=" + longitude;
1477                    url = addUserName(url);
1478                    BufferedReader in = new BufferedReader(new InputStreamReader(
1479                                    connect(url)));
1480                    String srtm3 = in.readLine();
1481                    in.close();
1482                    return Integer.parseInt(srtm3);
1483            }
1484    
1485            public static int[] srtm3(double[] latitude, double[] longitude)
1486                            throws IOException {
1487                    if (latitude.length != longitude.length) {
1488                            throw new Error("number of lats and longs must be equal");
1489                    }
1490                    int[] elevation = new int[latitude.length];
1491                    String lats = "";
1492                    String lngs = "";
1493                    for (int i = 0; i < elevation.length; i++) {
1494                            lats += latitude[i] + ",";
1495                            lngs += longitude[i] + ",";
1496                    }
1497                    String url = "/srtm3?lats=" + lats + "&lngs=" + lngs;
1498                    url = addUserName(url);
1499                    BufferedReader in = new BufferedReader(new InputStreamReader(
1500                                    connect(url)));
1501                    for (int i = 0; i < elevation.length; i++) {
1502                            String srtm3 = in.readLine();
1503                            elevation[i] = Integer.parseInt(srtm3);
1504                    }
1505                    in.close();
1506                    return elevation;
1507            }
1508    
1509            public static int astergdem(double latitude, double longitude)
1510                            throws IOException {
1511                    String url = "/astergdem?lat=" + latitude + "&lng=" + longitude;
1512                    url = addUserName(url);
1513                    BufferedReader in = new BufferedReader(new InputStreamReader(
1514                                    connect(url)));
1515                    String astergdem = in.readLine();
1516                    in.close();
1517                    return Integer.parseInt(astergdem);
1518            }
1519    
1520            public static int[] astergdem(double[] latitude, double[] longitude)
1521                            throws IOException {
1522                    if (latitude.length != longitude.length) {
1523                            throw new Error("number of lats and longs must be equal");
1524                    }
1525                    int[] elevation = new int[latitude.length];
1526                    String lats = "";
1527                    String lngs = "";
1528                    for (int i = 0; i < elevation.length; i++) {
1529                            lats += latitude[i] + ",";
1530                            lngs += longitude[i] + ",";
1531                    }
1532                    String url = "/astergdem?lats=" + lats + "&lngs=" + lngs;
1533                    url = addUserName(url);
1534                    BufferedReader in = new BufferedReader(new InputStreamReader(
1535                                    connect(url)));
1536                    for (int i = 0; i < elevation.length; i++) {
1537                            String astergdem = in.readLine();
1538                            elevation[i] = Integer.parseInt(astergdem);
1539                    }
1540                    in.close();
1541                    return elevation;
1542            }
1543    
1544            /**
1545             * The iso country code of any given point. It is calling
1546             * {@link #countryCode(double, double, double)} with radius=0.0
1547             * 
1548             * @param latitude
1549             * @param longitude
1550             * @return
1551             * @throws IOException
1552             */
1553            public static String countryCode(double latitude, double longitude)
1554                            throws IOException {
1555                    return countryCode(latitude, longitude, 0);
1556            }
1557    
1558            /**
1559             * The iso country code of any given point with radius for coastal areas.
1560             * 
1561             * @param latitude
1562             * @param longitude
1563             * @param radius
1564             * 
1565             * @return iso country code for the given latitude/longitude
1566             * @throws IOException
1567             */
1568            public static String countryCode(double latitude, double longitude,
1569                            double radius) throws IOException {
1570                    String url = "/countryCode?lat=" + latitude + "&lng=" + longitude;
1571                    if (radius != 0) {
1572                            url += "&radius=" + radius;
1573                    }
1574                    url = addUserName(url);
1575                    BufferedReader in = new BufferedReader(new InputStreamReader(
1576                                    connect(url)));
1577                    String cc = in.readLine();
1578                    in.close();
1579                    if (cc != null && cc.length() == 2) {
1580                            return cc;
1581                    }
1582                    return null;
1583            }
1584    
1585            /**
1586             * get the timezone for a given location
1587             * 
1588             * @param latitude
1589             * @param longitude
1590             * @return timezone at the given location
1591             * @throws IOException
1592             * @throws Exception
1593             */
1594            public static Timezone timezone(double latitude, double longitude)
1595                            throws IOException, Exception {
1596    
1597                    String url = "/timezone?";
1598    
1599                    url = url + "&lat=" + latitude;
1600                    url = url + "&lng=" + longitude;
1601                    url = addUserName(url);
1602    
1603                    SAXBuilder parser = new SAXBuilder();
1604                    Document doc = parser.build(connect(url));
1605    
1606                    Element root = rootAndCheckException(doc);
1607                    for (Object obj : root.getChildren("timezone")) {
1608                            Element codeElement = (Element) obj;
1609                            Timezone timezone = new Timezone();
1610                            timezone.setTimezoneId(codeElement.getChildText("timezoneId"));
1611                            timezone.setCountryCode(codeElement.getChildText("countryCode"));
1612    
1613                            if (codeElement.getChildText("time") != null) {
1614                                    String minuteDateFmt = "yyyy-MM-dd HH:mm";
1615                                    SimpleDateFormat df = null;
1616                                    if (codeElement.getChildText("time").length() == minuteDateFmt
1617                                                    .length()) {
1618                                            df = new SimpleDateFormat(minuteDateFmt);
1619                                    } else {
1620                                            df = new SimpleDateFormat(DATEFMT);
1621                                    }
1622                                    timezone.setTime(df.parse(codeElement.getChildText("time")));
1623                                    if (codeElement.getChildText("sunrise") != null) {
1624                                            timezone.setSunrise(df.parse(codeElement
1625                                                            .getChildText("sunrise")));
1626                                    }
1627                                    if (codeElement.getChildText("sunset") != null) {
1628                                            timezone.setSunset(df.parse(codeElement
1629                                                            .getChildText("sunset")));
1630                                    }
1631                                    timezone.setGmtOffset(Double.parseDouble(codeElement
1632                                                    .getChildText("gmtOffset")));
1633                                    timezone.setDstOffset(Double.parseDouble(codeElement
1634                                                    .getChildText("dstOffset")));
1635                            }
1636                            return timezone;
1637                    }
1638    
1639                    return null;
1640            }
1641    
1642            /**
1643             * 
1644             * @param latitude
1645             * @param longitude
1646             * @return
1647             * @throws IOException
1648             * @throws Exception
1649             */
1650            public static WeatherObservation findNearByWeather(double latitude,
1651                            double longitude) throws IOException, Exception {
1652    
1653                    String url = "/findNearByWeatherXML?";
1654    
1655                    url = url + "&lat=" + latitude;
1656                    url = url + "&lng=" + longitude;
1657                    url = addUserName(url);
1658    
1659                    SAXBuilder parser = new SAXBuilder();
1660                    Document doc = parser.build(connect(url));
1661    
1662                    Element root = rootAndCheckException(doc);
1663                    for (Object obj : root.getChildren("observation")) {
1664                            Element weatherObservationElement = (Element) obj;
1665                            WeatherObservation weatherObservation = getWeatherObservationFromElement(weatherObservationElement);
1666                            return weatherObservation;
1667                    }
1668    
1669                    return null;
1670            }
1671    
1672            public static WeatherObservation weatherIcao(String icaoCode)
1673                            throws IOException, Exception {
1674    
1675                    String url = "/weatherIcaoXML?";
1676    
1677                    url = url + "&ICAO=" + icaoCode;
1678                    url = addUserName(url);
1679    
1680                    SAXBuilder parser = new SAXBuilder();
1681                    Document doc = parser.build(connect(url));
1682    
1683                    Element root = rootAndCheckException(doc);
1684                    for (Object obj : root.getChildren("observation")) {
1685                            Element weatherObservationElement = (Element) obj;
1686                            WeatherObservation weatherObservation = getWeatherObservationFromElement(weatherObservationElement);
1687                            return weatherObservation;
1688                    }
1689    
1690                    return null;
1691            }
1692    
1693            /**
1694             * @return the geoNamesServer, default is http://ws.geonames.org
1695             */
1696            public static String getGeoNamesServer() {
1697                    return geoNamesServer;
1698            }
1699    
1700            /**
1701             * @return the geoNamesServerFailover
1702             */
1703            public static String getGeoNamesServerFailover() {
1704                    return geoNamesServerFailover;
1705            }
1706    
1707            /**
1708             * sets the server name for the GeoNames server to be used for the requests.
1709             * Default is ws.geonames.org
1710             * 
1711             * @param geoNamesServer
1712             *            the geonamesServer to set
1713             */
1714            public static void setGeoNamesServer(String pGeoNamesServer) {
1715                    if (pGeoNamesServer == null) {
1716                            throw new Error();
1717                    }
1718                    pGeoNamesServer = pGeoNamesServer.trim().toLowerCase();
1719                    // add default http protocol if it is missing
1720                    if (!pGeoNamesServer.startsWith("http://")
1721                                    && !pGeoNamesServer.startsWith("https://")) {
1722                            pGeoNamesServer = "http://" + pGeoNamesServer;
1723                    }
1724                    WebService.geoNamesServer = pGeoNamesServer;
1725            }
1726    
1727            /**
1728             * sets the default failover server for requests in case the main server is
1729             * not accessible. Default is ws.geonames.org<br>
1730             * The failover server is only called if it is different from the main
1731             * server.<br>
1732             * The failover server is used for commercial GeoNames web service users.
1733             * 
1734             * @param geoNamesServerFailover
1735             *            the geoNamesServerFailover to set
1736             */
1737            public static void setGeoNamesServerFailover(String geoNamesServerFailover) {
1738                    if (geoNamesServerFailover != null) {
1739                            geoNamesServerFailover = geoNamesServerFailover.trim()
1740                                            .toLowerCase();
1741                            if (!geoNamesServerFailover.startsWith("http://")) {
1742                                    geoNamesServerFailover = "http://" + geoNamesServerFailover;
1743                            }
1744                    }
1745                    WebService.geoNamesServerFailover = geoNamesServerFailover;
1746            }
1747    
1748            /**
1749             * @return the userName
1750             */
1751            public static String getUserName() {
1752                    return userName;
1753            }
1754    
1755            /**
1756             * Sets the user name to be used for the requests. Needed to access the
1757             * commercial GeoNames web services.
1758             * 
1759             * @param userName
1760             *            the userName to set
1761             */
1762            public static void setUserName(String userName) {
1763                    WebService.userName = userName;
1764            }
1765    
1766            /**
1767             * @return the token
1768             */
1769            public static String getToken() {
1770                    return token;
1771            }
1772    
1773            /**
1774             * sets the token to be used to authenticate the requests. This is an
1775             * optional parameter for the commercial version of the GeoNames web
1776             * services.
1777             * 
1778             * @param token
1779             *            the token to set
1780             */
1781            public static void setToken(String token) {
1782                    WebService.token = token;
1783            }
1784    
1785            /**
1786             * @return the defaultStyle
1787             */
1788            public static Style getDefaultStyle() {
1789                    return defaultStyle;
1790            }
1791    
1792            /**
1793             * @param defaultStyle
1794             *            the defaultStyle to set
1795             */
1796            public static void setDefaultStyle(Style defaultStyle) {
1797                    WebService.defaultStyle = defaultStyle;
1798            }
1799    
1800            /**
1801             * @return the readTimeOut
1802             */
1803            public static int getReadTimeOut() {
1804                    return readTimeOut;
1805            }
1806    
1807            /**
1808             * @param readTimeOut
1809             *            the readTimeOut to set
1810             */
1811            public static void setReadTimeOut(int readTimeOut) {
1812                    WebService.readTimeOut = readTimeOut;
1813            }
1814    
1815            /**
1816             * @return the connectTimeOut
1817             */
1818            public static int getConnectTimeOut() {
1819                    return connectTimeOut;
1820            }
1821    
1822            /**
1823             * @param connectTimeOut
1824             *            the connectTimeOut to set
1825             */
1826            public static void setConnectTimeOut(int connectTimeOut) {
1827                    WebService.connectTimeOut = connectTimeOut;
1828            }
1829    
1830    }