Home | History | Annotate | Download | only in geocoding
      1 /*
      2  * Copyright (C) 2011 Google Inc.
      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;
     63     try {
     64       in = new ObjectInputStream(source);
     65       mappingFileProvider.readExternal(in);
     66     } catch (IOException e) {
     67       LOGGER.log(Level.WARNING, e.toString());
     68     }
     69   }
     70 
     71   private AreaCodeMap getPhonePrefixDescriptions(
     72       int countryCallingCode, String language, String script, String region) {
     73     String fileName = mappingFileProvider.getFileName(countryCallingCode, language, script, region);
     74     if (fileName.length() == 0) {
     75       return null;
     76     }
     77     if (!availablePhonePrefixMaps.containsKey(fileName)) {
     78       loadAreaCodeMapFromFile(fileName);
     79     }
     80     return availablePhonePrefixMaps.get(fileName);
     81   }
     82 
     83   private void loadAreaCodeMapFromFile(String fileName) {
     84     InputStream source =
     85         PhoneNumberOfflineGeocoder.class.getResourceAsStream(phonePrefixDataDirectory + fileName);
     86     ObjectInputStream in;
     87     try {
     88       in = new ObjectInputStream(source);
     89       AreaCodeMap map = new AreaCodeMap();
     90       map.readExternal(in);
     91       availablePhonePrefixMaps.put(fileName, map);
     92     } catch (IOException e) {
     93       LOGGER.log(Level.WARNING, e.toString());
     94     }
     95   }
     96 
     97   /**
     98    * Gets a {@link PhoneNumberOfflineGeocoder} instance to carry out international phone number
     99    * geocoding.
    100    *
    101    * <p> The {@link PhoneNumberOfflineGeocoder} is implemented as a singleton. Therefore, calling
    102    * this method multiple times will only result in one instance being created.
    103    *
    104    * @return  a {@link PhoneNumberOfflineGeocoder} instance
    105    */
    106   public static synchronized PhoneNumberOfflineGeocoder getInstance() {
    107     if (instance == null) {
    108       instance = new PhoneNumberOfflineGeocoder(MAPPING_DATA_DIRECTORY);
    109     }
    110     return instance;
    111   }
    112 
    113   /**
    114    * Returns the customary display name in the given language for the given territory the phone
    115    * number is from.
    116    */
    117   private String getCountryNameForNumber(PhoneNumber number, Locale language) {
    118     String regionCode = phoneUtil.getRegionCodeForNumber(number);
    119     return (regionCode == null || regionCode.equals("ZZ"))
    120         ? "" : new Locale("", regionCode).getDisplayCountry(language);
    121   }
    122 
    123   /**
    124    * Returns a text description for the given language code for the given phone number. The
    125    * description might consist of the name of the country where the phone number is from and/or the
    126    * name of the geographical area the phone number is from. This method assumes the validity of the
    127    * number passed in has already been checked.
    128    *
    129    * @param number  a valid phone number for which we want to get a text description
    130    * @param languageCode  the language code for which the description should be written
    131    * @return  a text description for the given language code for the given phone number
    132    */
    133   public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode) {
    134     String langStr = languageCode.getLanguage();
    135     String scriptStr = "";  // No script is specified
    136     String regionStr = languageCode.getCountry();
    137 
    138     String areaDescription =
    139         getAreaDescriptionForNumber(number, langStr, scriptStr, regionStr);
    140     return (areaDescription.length() > 0)
    141         ? areaDescription : getCountryNameForNumber(number, languageCode);
    142   }
    143 
    144   /**
    145    * Returns a text description for the given language code for the given phone number. The
    146    * description might consist of the name of the country where the phone number is from and/or the
    147    * name of the geographical area the phone number is from. This method explictly checkes the
    148    * validity of the number passed in.
    149    *
    150    * @param number  the phone number for which we want to get a text description
    151    * @param languageCode  the language code for which the description should be written
    152    * @return  a text description for the given language code for the given phone number, or empty
    153    *     string if the number passed in is invalid
    154    */
    155   public String getDescriptionForNumber(PhoneNumber number, Locale languageCode) {
    156     if (!phoneUtil.isValidNumber(number)) {
    157       return "";
    158     }
    159     return getDescriptionForValidNumber(number, languageCode);
    160   }
    161 
    162   /**
    163    * Returns an area-level text description in the given language for the given phone number.
    164    *
    165    * @param number  the phone number for which we want to get a text description
    166    * @param lang  two-letter lowercase ISO language codes as defined by ISO 639-1
    167    * @param script  four-letter titlecase (the first letter is uppercase and the rest of the letters
    168    *     are lowercase) ISO script codes as defined in ISO 15924
    169    * @param region  two-letter uppercase ISO country codes as defined by ISO 3166-1
    170    * @return  an area-level text description in the given language for the given phone number, or an
    171    *     empty string if such a description is not available
    172    */
    173   private String getAreaDescriptionForNumber(
    174       PhoneNumber number, String lang, String script, String region) {
    175     int countryCallingCode = number.getCountryCode();
    176     // As the NANPA data is split into multiple files covering 3-digit areas, use a phone number
    177     // prefix of 4 digits for NANPA instead, e.g. 1650.
    178     int phonePrefix = (countryCallingCode != 1) ?
    179         countryCallingCode : (1000 + (int) (number.getNationalNumber() / 10000000));
    180     AreaCodeMap phonePrefixDescriptions =
    181         getPhonePrefixDescriptions(phonePrefix, lang, script, region);
    182     return (phonePrefixDescriptions != null) ? phonePrefixDescriptions.lookup(number) : "";
    183   }
    184 }
    185