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.android.i18n.phonenumbers.geocoding;
     18 
     19 import com.android.i18n.phonenumbers.PhoneNumberUtil;
     20 import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber;
     21 
     22 import java.io.IOException;
     23 import java.io.InputStream;
     24 import java.io.ObjectInputStream;
     25 import java.util.HashMap;
     26 import java.util.Locale;
     27 import java.util.Map;
     28 import java.util.logging.Level;
     29 import java.util.logging.Logger;
     30 
     31 /**
     32  * An offline geocoder which provides geographical information related to a phone number.
     33  *
     34  * @author Shaopeng Jia
     35  */
     36 public class PhoneNumberOfflineGeocoder {
     37   private static PhoneNumberOfflineGeocoder instance = null;
     38   private static final String MAPPING_DATA_DIRECTORY =
     39       "/com/android/i18n/phonenumbers/geocoding/data/";
     40   private static final Logger LOGGER = Logger.getLogger(PhoneNumberOfflineGeocoder.class.getName());
     41 
     42   private final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
     43   private final String phonePrefixDataDirectory;
     44 
     45   // The mappingFileProvider knows for which combination of countryCallingCode and language a phone
     46   // prefix mapping file is available in the file system, so that a file can be loaded when needed.
     47   private MappingFileProvider mappingFileProvider = new MappingFileProvider();
     48 
     49   // A mapping from countryCallingCode_lang to the corresponding phone prefix map that has been
     50   // loaded.
     51   private Map<String, AreaCodeMap> availablePhonePrefixMaps = new HashMap<String, AreaCodeMap>();
     52 
     53   // @VisibleForTesting
     54   PhoneNumberOfflineGeocoder(String phonePrefixDataDirectory) {
     55     this.phonePrefixDataDirectory = phonePrefixDataDirectory;
     56     loadMappingFileProvider();
     57   }
     58 
     59   private void loadMappingFileProvider() {
     60     InputStream source =
     61         PhoneNumberOfflineGeocoder.class.getResourceAsStream(phonePrefixDataDirectory + "config");
     62     ObjectInputStream in = null;
     63     try {
     64       in = new ObjectInputStream(source);
     65       mappingFileProvider.readExternal(in);
     66     } catch (IOException e) {
     67       LOGGER.log(Level.WARNING, e.toString());
     68     } finally {
     69       close(in);
     70     }
     71   }
     72 
     73   private AreaCodeMap getPhonePrefixDescriptions(
     74       int prefixMapKey, String language, String script, String region) {
     75     String fileName = mappingFileProvider.getFileName(prefixMapKey, language, script, region);
     76     if (fileName.length() == 0) {
     77       return null;
     78     }
     79     if (!availablePhonePrefixMaps.containsKey(fileName)) {
     80       loadAreaCodeMapFromFile(fileName);
     81     }
     82     return availablePhonePrefixMaps.get(fileName);
     83   }
     84 
     85   private void loadAreaCodeMapFromFile(String fileName) {
     86     InputStream source =
     87         PhoneNumberOfflineGeocoder.class.getResourceAsStream(phonePrefixDataDirectory + fileName);
     88     ObjectInputStream in = null;
     89     try {
     90       in = new ObjectInputStream(source);
     91       AreaCodeMap map = new AreaCodeMap();
     92       map.readExternal(in);
     93       availablePhonePrefixMaps.put(fileName, map);
     94     } catch (IOException e) {
     95       LOGGER.log(Level.WARNING, e.toString());
     96     } finally {
     97       close(in);
     98     }
     99   }
    100 
    101   private static void close(InputStream in) {
    102     if (in != null) {
    103       try {
    104         in.close();
    105       } catch (IOException e) {
    106         LOGGER.log(Level.WARNING, e.toString());
    107       }
    108     }
    109   }
    110 
    111   /**
    112    * Gets a {@link PhoneNumberOfflineGeocoder} instance to carry out international phone number
    113    * geocoding.
    114    *
    115    * <p> The {@link PhoneNumberOfflineGeocoder} is implemented as a singleton. Therefore, calling
    116    * this method multiple times will only result in one instance being created.
    117    *
    118    * @return  a {@link PhoneNumberOfflineGeocoder} instance
    119    */
    120   public static synchronized PhoneNumberOfflineGeocoder getInstance() {
    121     if (instance == null) {
    122       instance = new PhoneNumberOfflineGeocoder(MAPPING_DATA_DIRECTORY);
    123     }
    124     return instance;
    125   }
    126 
    127   /**
    128    * Returns the customary display name in the given language for the given territory the phone
    129    * number is from.
    130    */
    131   private String getCountryNameForNumber(PhoneNumber number, Locale language) {
    132     String regionCode = phoneUtil.getRegionCodeForNumber(number);
    133     return getRegionDisplayName(regionCode, language);
    134   }
    135 
    136   /**
    137    * Returns the customary display name in the given language for the given region.
    138    */
    139   private String getRegionDisplayName(String regionCode, Locale language) {
    140     return (regionCode == null || regionCode.equals("ZZ") ||
    141             regionCode.equals(PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY))
    142         ? "" : new Locale("", regionCode).getDisplayCountry(language);
    143   }
    144 
    145   /**
    146    * Returns a text description for the given phone number, in the language provided. The
    147    * description might consist of the name of the country where the phone number is from, or the
    148    * name of the geographical area the phone number is from if more detailed information is
    149    * available.
    150    *
    151    * <p>This method assumes the validity of the number passed in has already been checked.
    152    *
    153    * @param number  a valid 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    * @return  a text description for the given language code for the given phone number
    156    */
    157   public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode) {
    158     String langStr = languageCode.getLanguage();
    159     String scriptStr = "";  // No script is specified
    160     String regionStr = languageCode.getCountry();
    161 
    162     String areaDescription =
    163         getAreaDescriptionForNumber(number, langStr, scriptStr, regionStr);
    164     return (areaDescription.length() > 0)
    165         ? areaDescription : getCountryNameForNumber(number, languageCode);
    166   }
    167 
    168   /**
    169    * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but also considers the
    170    * region of the user. If the phone number is from the same region as the user, only a lower-level
    171    * description will be returned, if one exists. Otherwise, the phone number's region will be
    172    * returned, with optionally some more detailed information.
    173    *
    174    * <p>For example, for a user from the region "US" (United States), we would show "Mountain View,
    175    * CA" for a particular number, omitting the United States from the description. For a user from
    176    * the United Kingdom (region "GB"), for the same number we may show "Mountain View, CA, United
    177    * States" or even just "United States".
    178    *
    179    * <p>This method assumes the validity of the number passed in has already been checked.
    180    *
    181    * @param number  the phone number for which we want to get a text description
    182    * @param languageCode  the language code for which the description should be written
    183    * @param userRegion  the region code for a given user. This region will be omitted from the
    184    *     description if the phone number comes from this region. It is a two-letter uppercase ISO
    185    *     country code as defined by ISO 3166-1.
    186    * @return  a text description for the given language code for the given phone number, or empty
    187    *     string if the number passed in is invalid
    188    */
    189   public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode,
    190                                              String userRegion) {
    191     // If the user region matches the number's region, then we just show the lower-level
    192     // description, if one exists - if no description exists, we will show the region(country) name
    193     // for the number.
    194     String regionCode = phoneUtil.getRegionCodeForNumber(number);
    195     if (userRegion.equals(regionCode)) {
    196       return getDescriptionForValidNumber(number, languageCode);
    197     }
    198     // Otherwise, we just show the region(country) name for now.
    199     return getRegionDisplayName(regionCode, languageCode);
    200     // TODO: Concatenate the lower-level and country-name information in an appropriate
    201     // way for each language.
    202   }
    203 
    204   /**
    205    * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but explicitly checks
    206    * the validity of the number passed in.
    207    *
    208    * @param number  the phone number for which we want to get a text description
    209    * @param languageCode  the language code for which the description should be written
    210    * @return  a text description for the given language code for the given phone number, or empty
    211    *     string if the number passed in is invalid
    212    */
    213   public String getDescriptionForNumber(PhoneNumber number, Locale languageCode) {
    214     if (!phoneUtil.isValidNumber(number)) {
    215       return "";
    216     }
    217     return getDescriptionForValidNumber(number, languageCode);
    218   }
    219 
    220   /**
    221    * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale, String)} but
    222    * explicitly checks the validity of the number passed in.
    223    *
    224    * @param number  the phone number for which we want to get a text description
    225    * @param languageCode  the language code for which the description should be written
    226    * @param userRegion  the region code for a given user. This region will be omitted from the
    227    *     description if the phone number comes from this region. It is a two-letter uppercase ISO
    228    *     country code as defined by ISO 3166-1.
    229    * @return  a text description for the given language code for the given phone number, or empty
    230    *     string if the number passed in is invalid
    231    */
    232   public String getDescriptionForNumber(PhoneNumber number, Locale languageCode,
    233                                         String userRegion) {
    234     if (!phoneUtil.isValidNumber(number)) {
    235       return "";
    236     }
    237     return getDescriptionForValidNumber(number, languageCode, userRegion);
    238   }
    239 
    240   /**
    241    * Returns an area-level text description in the given language for the given phone number.
    242    *
    243    * @param number  the phone number for which we want to get a text description
    244    * @param lang  two-letter lowercase ISO language codes as defined by ISO 639-1
    245    * @param script  four-letter titlecase (the first letter is uppercase and the rest of the letters
    246    *     are lowercase) ISO script codes as defined in ISO 15924
    247    * @param region  two-letter uppercase ISO country codes as defined by ISO 3166-1
    248    * @return  an area-level text description in the given language for the given phone number, or an
    249    *     empty string if such a description is not available
    250    */
    251   private String getAreaDescriptionForNumber(
    252       PhoneNumber number, String lang, String script, String region) {
    253     int countryCallingCode = number.getCountryCode();
    254     // As the NANPA data is split into multiple files covering 3-digit areas, use a phone number
    255     // prefix of 4 digits for NANPA instead, e.g. 1650.
    256     int phonePrefix = (countryCallingCode != 1) ?
    257         countryCallingCode : (1000 + (int) (number.getNationalNumber() / 10000000));
    258     AreaCodeMap phonePrefixDescriptions =
    259         getPhonePrefixDescriptions(phonePrefix, lang, script, region);
    260     String description = (phonePrefixDescriptions != null)
    261         ? phonePrefixDescriptions.lookup(number)
    262         : null;
    263     // When a location is not available in the requested language, fall back to English.
    264     if ((description == null || description.length() == 0) && mayFallBackToEnglish(lang)) {
    265       AreaCodeMap defaultMap = getPhonePrefixDescriptions(phonePrefix, "en", "", "");
    266       if (defaultMap == null) {
    267         return "";
    268       }
    269       description = defaultMap.lookup(number);
    270     }
    271     return description != null ? description : "";
    272   }
    273 
    274   private boolean mayFallBackToEnglish(String lang) {
    275     // Don't fall back to English if the requested language is among the following:
    276     // - Chinese
    277     // - Japanese
    278     // - Korean
    279     return !lang.equals("zh") && !lang.equals("ja") && !lang.equals("ko");
    280   }
    281 }
    282