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 = 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 countryCallingCode, String language, String script, String region) {
     75     String fileName = mappingFileProvider.getFileName(countryCallingCode, 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 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 (regionCode == null || regionCode.equals("ZZ"))
    134         ? "" : new Locale("", regionCode).getDisplayCountry(language);
    135   }
    136 
    137   /**
    138    * Returns a text description for the given language code for the given phone number. The
    139    * description might consist of the name of the country where the phone number is from and/or the
    140    * name of the geographical area the phone number is from. This method assumes the validity of the
    141    * number passed in has already been checked.
    142    *
    143    * @param number  a valid phone number for which we want to get a text description
    144    * @param languageCode  the language code for which the description should be written
    145    * @return  a text description for the given language code for the given phone number
    146    */
    147   public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode) {
    148     String langStr = languageCode.getLanguage();
    149     String scriptStr = "";  // No script is specified
    150     String regionStr = languageCode.getCountry();
    151 
    152     String areaDescription =
    153         getAreaDescriptionForNumber(number, langStr, scriptStr, regionStr);
    154     return (areaDescription.length() > 0)
    155         ? areaDescription : getCountryNameForNumber(number, languageCode);
    156   }
    157 
    158   /**
    159    * Returns a text description for the given language code for the given phone number. The
    160    * description might consist of the name of the country where the phone number is from and/or the
    161    * name of the geographical area the phone number is from. This method explictly checkes the
    162    * validity of the number passed in.
    163    *
    164    * @param number  the phone number for which we want to get a text description
    165    * @param languageCode  the language code for which the description should be written
    166    * @return  a text description for the given language code for the given phone number, or empty
    167    *     string if the number passed in is invalid
    168    */
    169   public String getDescriptionForNumber(PhoneNumber number, Locale languageCode) {
    170     if (!phoneUtil.isValidNumber(number)) {
    171       return "";
    172     }
    173     return getDescriptionForValidNumber(number, languageCode);
    174   }
    175 
    176   /**
    177    * Returns an area-level text description in the given language for the given phone number.
    178    *
    179    * @param number  the phone number for which we want to get a text description
    180    * @param lang  two-letter lowercase ISO language codes as defined by ISO 639-1
    181    * @param script  four-letter titlecase (the first letter is uppercase and the rest of the letters
    182    *     are lowercase) ISO script codes as defined in ISO 15924
    183    * @param region  two-letter uppercase ISO country codes as defined by ISO 3166-1
    184    * @return  an area-level text description in the given language for the given phone number, or an
    185    *     empty string if such a description is not available
    186    */
    187   private String getAreaDescriptionForNumber(
    188       PhoneNumber number, String lang, String script, String region) {
    189     int countryCallingCode = number.getCountryCode();
    190     // As the NANPA data is split into multiple files covering 3-digit areas, use a phone number
    191     // prefix of 4 digits for NANPA instead, e.g. 1650.
    192     int phonePrefix = (countryCallingCode != 1) ?
    193         countryCallingCode : (1000 + (int) (number.getNationalNumber() / 10000000));
    194     AreaCodeMap phonePrefixDescriptions =
    195         getPhonePrefixDescriptions(phonePrefix, lang, script, region);
    196     return (phonePrefixDescriptions != null) ? phonePrefixDescriptions.lookup(number) : "";
    197   }
    198 }
    199