Home | History | Annotate | Download | only in geocoding
      1 /*
      2  * Copyright (C) 2011 The Libphonenumber Authors
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.google.i18n.phonenumbers.geocoding;
     18 
     19 import com.google.i18n.phonenumbers.NumberParseException;
     20 import com.google.i18n.phonenumbers.PhoneNumberUtil;
     21 import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberType;
     22 import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
     23 import com.google.i18n.phonenumbers.prefixmapper.PrefixFileReader;
     24 
     25 import java.util.List;
     26 import java.util.Locale;
     27 
     28 /**
     29  * An offline geocoder which provides geographical information related to a phone number.
     30  *
     31  * @author Shaopeng Jia
     32  */
     33 public class PhoneNumberOfflineGeocoder {
     34   private static PhoneNumberOfflineGeocoder instance = null;
     35   private static final String MAPPING_DATA_DIRECTORY =
     36       "/com/google/i18n/phonenumbers/geocoding/data/";
     37   private PrefixFileReader prefixFileReader = null;
     38 
     39   private final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
     40 
     41   // @VisibleForTesting
     42   PhoneNumberOfflineGeocoder(String phonePrefixDataDirectory) {
     43     prefixFileReader = new PrefixFileReader(phonePrefixDataDirectory);
     44   }
     45 
     46   /**
     47    * Gets a {@link PhoneNumberOfflineGeocoder} instance to carry out international phone number
     48    * geocoding.
     49    *
     50    * <p> The {@link PhoneNumberOfflineGeocoder} is implemented as a singleton. Therefore, calling
     51    * this method multiple times will only result in one instance being created.
     52    *
     53    * @return  a {@link PhoneNumberOfflineGeocoder} instance
     54    */
     55   public static synchronized PhoneNumberOfflineGeocoder getInstance() {
     56     if (instance == null) {
     57       instance = new PhoneNumberOfflineGeocoder(MAPPING_DATA_DIRECTORY);
     58     }
     59     return instance;
     60   }
     61 
     62   /**
     63    * Returns the customary display name in the given language for the given territory the phone
     64    * number is from. If it could be from many territories, nothing is returned.
     65    */
     66   private String getCountryNameForNumber(PhoneNumber number, Locale language) {
     67     List<String> regionCodes =
     68         phoneUtil.getRegionCodesForCountryCode(number.getCountryCode());
     69     if (regionCodes.size() == 1) {
     70       return getRegionDisplayName(regionCodes.get(0), language);
     71     } else {
     72       String regionWhereNumberIsValid = "ZZ";
     73       for (String regionCode : regionCodes) {
     74         if (phoneUtil.isValidNumberForRegion(number, regionCode)) {
     75           // If the number has already been found valid for one region, then we don't know which
     76           // region it belongs to so we return nothing.
     77           if (!regionWhereNumberIsValid.equals("ZZ")) {
     78             return "";
     79           }
     80           regionWhereNumberIsValid = regionCode;
     81         }
     82       }
     83       return getRegionDisplayName(regionWhereNumberIsValid, language);
     84     }
     85   }
     86 
     87   /**
     88    * Returns the customary display name in the given language for the given region.
     89    */
     90   private String getRegionDisplayName(String regionCode, Locale language) {
     91     return (regionCode == null || regionCode.equals("ZZ")
     92         || regionCode.equals(PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY))
     93         ? "" : new Locale("", regionCode).getDisplayCountry(language);
     94   }
     95 
     96   /**
     97    * Returns a text description for the given phone number, in the language provided. The
     98    * description might consist of the name of the country where the phone number is from, or the
     99    * name of the geographical area the phone number is from if more detailed information is
    100    * available.
    101    *
    102    * <p>This method assumes the validity of the number passed in has already been checked, and that
    103    * the number is suitable for geocoding. We consider fixed-line and mobile numbers possible
    104    * candidates for geocoding.
    105    *
    106    * @param number  a valid phone number for which we want to get a text description
    107    * @param languageCode  the language code for which the description should be written
    108    * @return  a text description for the given language code for the given phone number, or an
    109    *     empty string if the number could come from multiple countries, or the country code is
    110    *     in fact invalid
    111    */
    112   public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode) {
    113     String langStr = languageCode.getLanguage();
    114     String scriptStr = "";  // No script is specified
    115     String regionStr = languageCode.getCountry();
    116 
    117     String areaDescription;
    118     String mobileToken = PhoneNumberUtil.getCountryMobileToken(number.getCountryCode());
    119     String nationalNumber = phoneUtil.getNationalSignificantNumber(number);
    120     if (!mobileToken.equals("") && nationalNumber.startsWith(mobileToken)) {
    121       // In some countries, eg. Argentina, mobile numbers have a mobile token before the national
    122       // destination code, this should be removed before geocoding.
    123       nationalNumber = nationalNumber.substring(mobileToken.length());
    124       String region = phoneUtil.getRegionCodeForCountryCode(number.getCountryCode());
    125       PhoneNumber copiedNumber;
    126       try {
    127         copiedNumber = phoneUtil.parse(nationalNumber, region);
    128       } catch (NumberParseException e) {
    129         // If this happens, just reuse what we had.
    130         copiedNumber = number;
    131       }
    132       areaDescription = prefixFileReader.getDescriptionForNumber(copiedNumber, langStr, scriptStr,
    133                                                                  regionStr);
    134     } else {
    135       areaDescription = prefixFileReader.getDescriptionForNumber(number, langStr, scriptStr,
    136                                                                  regionStr);
    137     }
    138     return (areaDescription.length() > 0)
    139         ? areaDescription : getCountryNameForNumber(number, languageCode);
    140   }
    141 
    142   /**
    143    * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but also considers the
    144    * region of the user. If the phone number is from the same region as the user, only a lower-level
    145    * description will be returned, if one exists. Otherwise, the phone number's region will be
    146    * returned, with optionally some more detailed information.
    147    *
    148    * <p>For example, for a user from the region "US" (United States), we would show "Mountain View,
    149    * CA" for a particular number, omitting the United States from the description. For a user from
    150    * the United Kingdom (region "GB"), for the same number we may show "Mountain View, CA, United
    151    * States" or even just "United States".
    152    *
    153    * <p>This method assumes the validity of the number passed in has already been checked.
    154    *
    155    * @param number  the phone number for which we want to get a text description
    156    * @param languageCode  the language code for which the description should be written
    157    * @param userRegion  the region code for a given user. This region will be omitted from the
    158    *     description if the phone number comes from this region. It should be a two-letter
    159    *     uppercase ISO country code as defined by ISO 3166-1.
    160    * @return  a text description for the given language code for the given phone number, or an
    161    *     empty string if the number could come from multiple countries, or the country code is
    162    *     in fact invalid
    163    */
    164   public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode,
    165                                              String userRegion) {
    166     // If the user region matches the number's region, then we just show the lower-level
    167     // description, if one exists - if no description exists, we will show the region(country) name
    168     // for the number.
    169     String regionCode = phoneUtil.getRegionCodeForNumber(number);
    170     if (userRegion.equals(regionCode)) {
    171       return getDescriptionForValidNumber(number, languageCode);
    172     }
    173     // Otherwise, we just show the region(country) name for now.
    174     return getRegionDisplayName(regionCode, languageCode);
    175     // TODO: Concatenate the lower-level and country-name information in an appropriate
    176     // way for each language.
    177   }
    178 
    179   /**
    180    * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but explicitly checks
    181    * the validity of the number passed in.
    182    *
    183    * @param number  the phone number for which we want to get a text description
    184    * @param languageCode  the language code for which the description should be written
    185    * @return  a text description for the given language code for the given phone number, or empty
    186    *     string if the number passed in is invalid or could belong to multiple countries
    187    */
    188   public String getDescriptionForNumber(PhoneNumber number, Locale languageCode) {
    189     PhoneNumberType numberType = phoneUtil.getNumberType(number);
    190     if (numberType == PhoneNumberType.UNKNOWN) {
    191       return "";
    192     } else if (!phoneUtil.isNumberGeographical(numberType, number.getCountryCode())) {
    193       return getCountryNameForNumber(number, languageCode);
    194     }
    195     return getDescriptionForValidNumber(number, languageCode);
    196   }
    197 
    198   /**
    199    * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale, String)} but
    200    * explicitly checks the validity of the number passed in.
    201    *
    202    * @param number  the phone number for which we want to get a text description
    203    * @param languageCode  the language code for which the description should be written
    204    * @param userRegion  the region code for a given user. This region will be omitted from the
    205    *     description if the phone number comes from this region. It should be a two-letter
    206    *     uppercase ISO country code as defined by ISO 3166-1.
    207    * @return  a text description for the given language code for the given phone number, or empty
    208    *     string if the number passed in is invalid or could belong to multiple countries
    209    */
    210   public String getDescriptionForNumber(PhoneNumber number, Locale languageCode,
    211                                         String userRegion) {
    212     PhoneNumberType numberType = phoneUtil.getNumberType(number);
    213     if (numberType == PhoneNumberType.UNKNOWN) {
    214       return "";
    215     } else if (!phoneUtil.isNumberGeographical(numberType, number.getCountryCode())) {
    216       return getCountryNameForNumber(number, languageCode);
    217     }
    218     return getDescriptionForValidNumber(number, languageCode, userRegion);
    219   }
    220 }
    221