PhoneNumberOfflineGeocoder.java revision 9907540e90c6794a028d7e1bd5bab0a6cc4a3e5b
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
17package com.google.i18n.phonenumbers.geocoding;
18
19import com.google.i18n.phonenumbers.NumberParseException;
20import com.google.i18n.phonenumbers.PhoneNumberUtil;
21import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberType;
22import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
23import com.google.i18n.phonenumbers.prefixmapper.PrefixFileReader;
24
25import java.util.List;
26import java.util.Locale;
27
28/**
29 * An offline geocoder which provides geographical information related to a phone number.
30 *
31 * @author Shaopeng Jia
32 */
33public 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