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