All Downloads are FREE. Search and download functionalities are using the official Maven repository.

it.tidalwave.geo.geocoding.geonamesprovider.GeoNamesProvider Maven / Gradle / Ivy

/***********************************************************************************************************************
 *
 * forceTen - open source geography
 * Copyright (C) 2007-2012 by Tidalwave s.a.s. (http://www.tidalwave.it)
 *
 ***********************************************************************************************************************
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations under the License.
 *
 ***********************************************************************************************************************
 *
 * WWW: http://forceten.tidalwave.it
 * SCM: https://bitbucket.org/tidalwave/forceten-src
 *
 **********************************************************************************************************************/
package it.tidalwave.geo.geocoding.geonamesprovider;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.io.Serializable;
import javax.xml.namespace.QName;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.openide.util.lookup.ServiceProvider;
import it.tidalwave.util.logging.Logger;
import it.tidalwave.util.Finder;
import it.tidalwave.util.NotFoundException;
import it.tidalwave.util.spi.SimpleFinderSupport;
import it.tidalwave.geo.Coordinate;
import it.tidalwave.geo.geocoding.GeoCoder;
import it.tidalwave.geo.geocoding.GeoCoderSupport;
import it.tidalwave.geo.geocoding.GeoCoderEntity;
import it.tidalwave.geo.geocoding.GeoCoderEntity.Type;
import it.tidalwave.geo.geocoding.GeoCoderEntity.FactSheet;
import it.tidalwave.geo.geocoding.GeoCoderEntity.FactSheet.Key;

abstract class GeoNamesFinderSupport extends SimpleFinderSupport
  {
    public GeoNamesFinderSupport (final @Nonnull String description,
                                  final @Nonnegative int defaultMaxResults)
      {
        super(description);
        maxResults = defaultMaxResults;
      }
  }

/***********************************************************************************************************************
 *
 * @author  Fabrizio Giudici
 * @version $Id$
 *
 **********************************************************************************************************************/
@edu.umd.cs.findbugs.annotations.SuppressWarnings("REC_CATCH_EXCEPTION")
@ServiceProvider(service=GeoCoder.class)
public class GeoNamesProvider extends GeoCoderSupport 
  {
    private static final String CLASS = GeoNamesProvider.class.getName();
    private static final Logger logger = Logger.getLogger(CLASS);
    
    private GeoCoderEntity rootGeoEntity;
    
    private static final XPathFactory xpathFactory = XPathFactory.newInstance();
    private static final XPath xpath = xpathFactory.newXPath();
//    private static final XPathExpression NAME;
//    private static final XPathExpression GEONAME_ID;
    /* package */  static final XPathExpression NAME2;
    /* package */  static final XPathExpression GEONAME_ID2;
    /* package */  static final XPathExpression FCODE;
    /* package */  static final XPathExpression TIMEZONE;
    /* package */  static final XPathExpression POPULATION;
    /* package */  static final XPathExpression ELEVATION;
    /* package */  static final XPathExpression ALTERNATE_NAME;
    /* package */  static final XPathExpression CONTINENT_CODE;
    /* package */  static final XPathExpression COUNTRY_CODE;
    /* package */  static final XPathExpression ADMIN1_CODE;
    /* package */  static final XPathExpression ADMIN2_CODE;
    /* package */  static final XPathExpression LAT;
    /* package */  static final XPathExpression LNG;
    /* package */  static final XPathExpression GEONAME_SET;

//    private static final XPathExpression SUBDIVISION_COUNTRY_CODE;
//    private static final XPathExpression SUBDIVISION_COUNTRY_NAME;
//    private static final XPathExpression SUBDIVISION_ADMIN_CODE;
//    private static final XPathExpression SUBDIVISION_ADMIN_NAME;

    public static final String URI_BASE = "http://sws.geonames.org/";

    /* package */ static final Map TYPE_MAP = new HashMap();
    private static final Map CODE_MAP = new HashMap();

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    static 
      {
        try 
          {
            TYPE_MAP.put("PLANET", Type.PLANET);
            TYPE_MAP.put("CONT", Type.CONTINENT);
            TYPE_MAP.put("PCLI", Type.COUNTRY);
            TYPE_MAP.put("ADM1", Type.ADMIN_DIVISION_1);
            TYPE_MAP.put("ADM2", Type.ADMIN_DIVISION_2);
            TYPE_MAP.put("ADM3", Type.ADMIN_DIVISION_3);
            TYPE_MAP.put("ADM4", Type.ADMIN_DIVISION_4);
            TYPE_MAP.put("PPL", Type.INHABITED_PLACE);

//            NAME = xpath.compile("//geonames/geoname/name");
//            GEONAME_ID = xpath.compile("//geonames/geoname/geonameId");

            NAME2 = xpath.compile("name");
            GEONAME_ID2 = xpath.compile("geonameId");
            LAT = xpath.compile("lat");
            LNG = xpath.compile("lng");
            FCODE = xpath.compile("fcode");
            TIMEZONE = xpath.compile("timezone");
            POPULATION = xpath.compile("population");
            ELEVATION = xpath.compile("elevation");
            ALTERNATE_NAME = xpath.compile("alternateName");

            CONTINENT_CODE = xpath.compile("continentCode");
            COUNTRY_CODE = xpath.compile("countryCode");
            ADMIN1_CODE = xpath.compile("adminCode1");
            ADMIN2_CODE = xpath.compile("adminCode2");

            CODE_MAP.put("CONT", CONTINENT_CODE);
            CODE_MAP.put("PCLI", COUNTRY_CODE);
            CODE_MAP.put("ADM1", ADMIN1_CODE);
            CODE_MAP.put("ADM2", ADMIN2_CODE);
            
            GEONAME_SET = xpath.compile("//geonames/geoname");

//            SUBDIVISION_COUNTRY_CODE = xpath.compile("//geonames/countrySubdivision/countryCode");
//            SUBDIVISION_COUNTRY_NAME = xpath.compile("//geonames/countrySubdivision/countryName");
//            SUBDIVISION_ADMIN_CODE = xpath.compile("//geonames/countrySubdivision/adminCode1");
//            SUBDIVISION_ADMIN_NAME = xpath.compile("//geonames/countrySubdivision/adminName1");
          } 
        catch (Exception e)
          {
            throw new ExceptionInInitializerError(e);
          }
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public String getName()
      {
        return "GeoNames";
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public String getId()
      {
        return URI_BASE;
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public synchronized GeoCoderEntity getRoot()
      {
        if (rootGeoEntity == null)
          {
            rootGeoEntity = new EarthGeoCoderEntity(this);
          }
        
        return rootGeoEntity;
      }
    
    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public synchronized GeoCoderEntity findGeoEntityById (@Nonnull String id)
      throws NotFoundException
      {
        logger.finer("findGeoEntityById(%s)", id);

        if (id.startsWith(URI_BASE))
          {
            id = id.substring(URI_BASE.length()).replace("/", "");
          }

        if (id.equals(getRoot().getId())) // getRoot() is lazily created
          {
            return getRoot();
          }

        GeoCoderEntity gcEntity = findCachedGeoEntityById(id, null);
        
        if ((gcEntity == null) || !gcEntity.isParentSet())
          {
            gcEntity = null;
            final Document document = GeoNames.retrieveDocument("hierarchy", "geonameId", id, "style", "FULL");

            try
              {
                final NodeList nodes = (NodeList)GEONAME_SET.evaluate(document, XPathConstants.NODESET);

                for (int i = 0; i < nodes.getLength(); i++) 
                  {
                    gcEntity = createGeoCoderEntity(nodes.item(i), gcEntity);
                  }
              }
            catch (Exception e)
              {
                throw new RuntimeException(e);
              }
          }
        
        return NotFoundException.throwWhenNull(gcEntity, "");
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public Finder findChildren (final @Nonnull String id)
      {
        logger.finer("findChildren(%s)", id);
        return createSparseGeoCoderEntities("findChildren()", "children", 9999999, "geonameId", id, "style", "FULL");
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public Finder findNeighbours (final @Nonnull String id)
      {
        logger.finer("findNeighbours(%s)", id);
        return createSparseGeoCoderEntities("findNeighbours()", "neighbours", 10, "geonameId", id, "style", "FULL");
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public Finder findSiblings (final @Nonnull String id)
      {
        logger.finer("findSiblings(%s)", id);
        return createSparseGeoCoderEntities("findSiblings()", "siblings", 10, "geonameId", id, "style", "FULL");
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public Finder findNearbyPostalCodes (final @Nonnull Coordinate coordinates,
                                                 final @Nonnegative float radius)
      {
        throw new UnsupportedOperationException("Not supported yet."); // TODO
      }


    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public Finder findNearestEntity (final @Nonnull Coordinate coordinate)
      {
        logger.finer("findNearestEntity(%s)", coordinate);
        return createSparseGeoCoderEntities("findNearestEntity()", "findNearbyPlaceName", 1,
                                                                    "lat", coordinate.getLatitude(),
                                                                    "lng", coordinate.getLongitude(),
                                                                    "style", "FULL");
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public Finder findEntities (final @Nonnull Coordinate coordinate,
                                                      final @Nonnegative float radius)
      {
        logger.finer("findNearByEntities(%s, %f)", coordinate, radius);
        return createSparseGeoCoderEntities("findNearByEntities()", "findNearbyPlaceName", 10,
                                                                    "lat", coordinate.getLatitude(),
                                                                    "lng", coordinate.getLongitude(),
                                                                    "radius", radius,
                                                                    "style", "FULL");
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public String findCountryCode (final @Nonnull Coordinate coordinate)
      {
        logger.finer("findCountryCode(%s)", coordinate);
        return GeoNames.retrieveString("countrycode", "lat", coordinate.getLatitude(), "lng", coordinate.getLongitude());
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public Type typeFromString (final @Nonnull String typeAsString)
      {
        final Type type = TYPE_MAP.get(typeAsString);
        return (type != null) ? type : Type.UNKNOWN;
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    protected GeoCoderEntity createGeoEntity (final @CheckForNull GeoCoderEntity parent,
                                              final @Nonnull String id,
                                              final @Nonnull String name,
                                              final @Nonnull Coordinate coordinates,
                                              final @Nonnull String code,
                                              final @Nonnull String typeAsString,
                                              final @Nonnull Serializable ... capabilities)
      {
        final QName qName = new QName(URI_BASE + id +"/");
        final List temp = new ArrayList(Arrays.asList(capabilities));
        temp.add(qName);
        temp.add(new EmblemFactory(id));
        return super.createGeoEntity(parent, id, name, coordinates, code, typeAsString, 
                                     temp.toArray(new Serializable[temp.size()]));
      }

    /*******************************************************************************************************************
     *
     * Creates a collection of {@link GeoCoderEntity} instances out of a {@link Document}. This method assumes that the
     * entities are sparse, that is not bound by a hierarchy relation, so they get an initally {@code null} parent, that
     * will be set later, on demand.
     *
     * @param   finderName  the finder name
     * @param   document    the XML document describing the entities
     * @return              the entities
     *
     ******************************************************************************************************************/
    @Nonnull
    private Finder createSparseGeoCoderEntities (final @Nonnull String finderName,
                                                                 final @Nonnull String command,
                                                                 final @Nonnegative int defaultMaxResults,
                                                                 final @Nonnull Object ... args)
      {
        return new GeoNamesFinderSupport(finderName, defaultMaxResults)
          {
            @Override @Nonnull
            protected List computeResults()
              {
                final List args2 = new ArrayList(Arrays.asList(args));
                args2.addAll(Arrays.asList("maxRows", maxResults));
                final Document document = GeoNames.retrieveDocument(command, args2.toArray());
                final List result = new ArrayList();

                try
                  {
                    final NodeList nodes = (NodeList)GEONAME_SET.evaluate(document, XPathConstants.NODESET);

                    for (int i = 0; i < nodes.getLength(); i++)
                      {
                        result.add(createGeoCoderEntity(nodes.item(i), null));
                      }
                  }
                catch (Exception e)
                  {
                    throw new RuntimeException(e);
                  }

                return result;
              }
          };
      }

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    @Nonnull
    private GeoCoderEntity createGeoCoderEntity (final @Nonnull Node node,
                                                 final @CheckForNull GeoCoderEntity gcEntityParent)
      throws XPathExpressionException
      {
        final String geonameId = (String)GEONAME_ID2.evaluate(node, XPathConstants.STRING);
        final String name = (String)NAME2.evaluate(node, XPathConstants.STRING);
        final String typeAsString = (String)FCODE.evaluate(node, XPathConstants.STRING);
        final XPathExpression codeExpression = CODE_MAP.get(typeAsString);
        final String code = (codeExpression == null) ? "" : (String)codeExpression.evaluate(node, XPathConstants.STRING);
        final Coordinate coordinates = createCoordinates(node, LAT, LNG);

        final Map, Object> map = new HashMap, Object>();

        final String timeZoneAsString = (String)TIMEZONE.evaluate(node, XPathConstants.STRING);
        final TimeZone timeZone = TimeZone.getTimeZone(timeZoneAsString);

        if (timeZone != null)
          {
            map.put(FactSheet.TIMEZONE, timeZone);
          }

        final String populationAsString = (String)POPULATION.evaluate(node, XPathConstants.STRING);

        if ((populationAsString != null) && !"".equals(populationAsString))
          {
            map.put(FactSheet.POPULATION, Long.parseLong(populationAsString));
          }

        final String elevationAsString = (String)ELEVATION.evaluate(node, XPathConstants.STRING);

        if ((elevationAsString != null) && !"".equals(elevationAsString))
          {
            map.put(FactSheet.ELEVATION, Integer.parseInt(elevationAsString));
          }

        final FactSheet factSheet = new FactSheet(map);

        return createGeoEntity(gcEntityParent, geonameId, name, coordinates, code, typeAsString, factSheet);
      }

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    @Nonnull
    private static Coordinate createCoordinates (final @Nonnull Node node,
                                                 final @Nonnull XPathExpression latExpression,
                                                 final @Nonnull XPathExpression lngExpression)
      throws XPathExpressionException
      {
        return new Coordinate(Double.parseDouble((String)latExpression.evaluate(node, XPathConstants.STRING)),
                              Double.parseDouble((String)lngExpression.evaluate(node, XPathConstants.STRING)));
      }
  }