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 (!regionWhereNumberIsValid.equals("ZZ")) {
     76             // If we can't assign the phone number as definitely belonging to only one territory,
     77             // then we return nothing.
     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
    109    */
    110   public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode) {
    111     String langStr = languageCode.getLanguage();
    112     String scriptStr = "";  // No script is specified
    113     String regionStr = languageCode.getCountry();
    114 
    115     String areaDescription;
    116     String mobileToken = PhoneNumberUtil.getCountryMobileToken(number.getCountryCode());
    117     String nationalNumber = phoneUtil.getNationalSignificantNumber(number);
    118     if (!mobileToken.equals("") && nationalNumber.startsWith(mobileToken)) {
    119       // In some countries, eg. Argentina, mobile numbers have a mobile token before the national
    120       // destination code, this should be removed before geocoding.
    121       nationalNumber = nationalNumber.substring(mobileToken.length());
    122       String region = phoneUtil.getRegionCodeForCountryCode(number.getCountryCode());
    123       PhoneNumber copiedNumber;
    124       try {
    125         copiedNumber = phoneUtil.parse(nationalNumber, region);
    126       } catch (NumberParseException e) {
    127         // If this happens, just reuse what we had.
    128         copiedNumber = number;
    129       }
    130       areaDescription = prefixFileReader.getDescriptionForNumber(copiedNumber, langStr, scriptStr,
    131                                                                  regionStr);
    132     } else {
    133       areaDescription = prefixFileReader.getDescriptionForNumber(number, langStr, scriptStr,
    134                                                                  regionStr);
    135     }
    136     return (areaDescription.length() > 0)
    137         ? areaDescription : getCountryNameForNumber(number, languageCode);
    138   }
    139 
    140   /**
    141    * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but also considers the
    142    * region of the user. If the phone number is from the same region as the user, only a lower-level
    143    * description will be returned, if one exists. Otherwise, the phone number's region will be
    144    * returned, with optionally some more detailed information.
    145    *
    146    * <p>For example, for a user from the region "US" (United States), we would show "Mountain View,
    147    * CA" for a particular number, omitting the United States from the description. For a user from
    148    * the United Kingdom (region "GB"), for the same number we may show "Mountain View, CA, United
    149    * States" or even just "United States".
    150    *
    151    * <p>This method assumes the validity of the number passed in has already been checked.
    152    *
    153    * @param number  the phone number for which we want to get a text description
    154    * @param languageCode  the language code for which the description should be written
    155    * @param userRegion  the region code for a given user. This region will be omitted from the
    156    *     description if the phone number comes from this region. It is a two-letter uppercase ISO
    157    *     country code as defined by ISO 3166-1.
    158    * @return  a text description for the given language code for the given phone number, or empty
    159    *     string if the number passed in is invalid
    160    */
    161   public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode,
    162                                              String userRegion) {
    163     // If the user region matches the number's region, then we just show the lower-level
    164     // description, if one exists - if no description exists, we will show the region(country) name
    165     // for the number.
    166     String regionCode = phoneUtil.getRegionCodeForNumber(number);
    167     if (userRegion.equals(regionCode)) {
    168       return getDescriptionForValidNumber(number, languageCode);
    169     }
    170     // Otherwise, we just show the region(country) name for now.
    171     return getRegionDisplayName(regionCode, languageCode);
    172     // TODO: Concatenate the lower-level and country-name information in an appropriate
    173     // way for each language.
    174   }
    175 
    176   /**
    177    * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but explicitly checks
    178    * the validity of the number passed in.
    179    *
    180    * @param number  the phone number for which we want to get a text description
    181    * @param languageCode  the language code for which the description should be written
    182    * @return  a text description for the given language code for the given phone number, or empty
    183    *     string if the number passed in is invalid
    184    */
    185   public String getDescriptionForNumber(PhoneNumber number, Locale languageCode) {
    186     PhoneNumberType numberType = phoneUtil.getNumberType(number);
    187     if (numberType == PhoneNumberType.UNKNOWN) {
    188       return "";
    189     } else if (!canBeGeocoded(numberType)) {
    190       return getCountryNameForNumber(number, languageCode);
    191     }
    192     return getDescriptionForValidNumber(number, languageCode);
    193   }
    194 
    195   /**
    196    * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale, String)} but
    197    * explicitly checks the validity of the number passed in.
    198    *
    199    * @param number  the phone number for which we want to get a text description
    200    * @param languageCode  the language code for which the description should be written
    201    * @param userRegion  the region code for a given user. This region will be omitted from the
    202    *     description if the phone number comes from this region. It is a two-letter uppercase ISO
    203    *     country code as defined by ISO 3166-1.
    204    * @return  a text description for the given language code for the given phone number, or empty
    205    *     string if the number passed in is invalid
    206    */
    207   public String getDescriptionForNumber(PhoneNumber number, Locale languageCode,
    208                                         String userRegion) {
    209     PhoneNumberType numberType = phoneUtil.getNumberType(number);
    210     if (numberType == PhoneNumberType.UNKNOWN) {
    211       return "";
    212     } else if (!canBeGeocoded(numberType)) {
    213       return getCountryNameForNumber(number, languageCode);
    214     }
    215     return getDescriptionForValidNumber(number, languageCode, userRegion);
    216   }
    217 
    218   /**
    219    * A similar method is implemented as PhoneNumberUtil.isNumberGeographical, which performs a
    220    * stricter check, as it determines if a number has a geographical association. Also, if new
    221    * phone number types were added, we should check if this other method should be updated too.
    222    */
    223   private boolean canBeGeocoded(PhoneNumberType numberType) {
    224     return (numberType == PhoneNumberType.FIXED_LINE ||
    225             numberType == PhoneNumberType.MOBILE ||
    226             numberType == PhoneNumberType.FIXED_LINE_OR_MOBILE);
    227   }
    228 }
    229