1 /* 2 * PhoneGeoLocator.js - Represent a phone number geolocator object. 3 * 4 * Copyright © 2014-2015, 2018, JEDLSoft 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * 16 * See the License for the specific language governing permissions and 17 * limitations under the License. 18 */ 19 20 // !data iddarea area extarea extstates phoneres 21 22 var ilib = require("./ilib.js"); 23 var Utils = require("./Utils.js"); 24 var JSUtils = require("./JSUtils.js"); 25 26 var PhoneNumber = require("./PhoneNumber.js"); 27 var NumberingPlan = require("./NumberingPlan.js"); 28 var PhoneLocale = require("./PhoneLocale.js"); 29 var ResBundle = require("./ResBundle.js"); 30 31 /** 32 * @class 33 * Create an instance that can geographically locate a phone number.<p> 34 * 35 * The location of the number is calculated according to the following rules: 36 * 37 * <ol> 38 * <li>If the areaCode property is undefined or empty, or if the number specifies a 39 * country code for which we do not have information, then the area property may be 40 * missing from the returned object. In this case, only the country object will be returned. 41 * 42 * <li>If there is no area code, but there is a mobile prefix, service code, or emergency 43 * code, then a fixed string indicating the type of number will be returned. 44 * 45 * <li>The country object is filled out according to the countryCode property of the phone 46 * number. 47 * 48 * <li>If the phone number does not have an explicit country code, the MCC will be used if 49 * it is available. The country code can be gleaned directly from the MCC. If the MCC 50 * of the carrier to which the phone is currently connected is available, it should be 51 * passed in so that local phone numbers will look correct. 52 * 53 * <li>If the country's dialling plan mandates a fixed length for phone numbers, and a 54 * particular number exceeds that length, then the area code will not be given on the 55 * assumption that the number has problems in the first place and we cannot guess 56 * correctly. 57 * </ol> 58 * 59 * The returned area property varies in specificity according 60 * to the locale. In North America, the area is no finer than large parts of states 61 * or provinces. In Germany and the UK, the area can be as fine as small towns.<p> 62 * 63 * If the number passed in is invalid, no geolocation will be performed. If the location 64 * information about the country where the phone number is located is not available, 65 * then the area information will be missing and only the country will be available.<p> 66 * 67 * The options parameter can contain any one of the following properties: 68 * 69 * <ul> 70 * <li><i>locale</i> The locale parameter is used to load translations of the names of regions and 71 * areas if available. For example, if the locale property is given as "en-US" (English for USA), 72 * but the phone number being geolocated is in Germany, then this class would return the the names 73 * of the country (Germany) and region inside of Germany in English instead of German. That is, a 74 * phone number in Munich and return the country "Germany" and the area code "Munich" 75 * instead of "Deutschland" and "München". The default display locale is the current ilib locale. 76 * If translations are not available, the region and area names are given in English, which should 77 * always be available. 78 * <li><i>mcc</i> The mcc of the current mobile carrier, if known. 79 * 80 * <li><i>onLoad</i> - a callback function to call when the data for the 81 * locale is fully loaded. When the onLoad option is given, this object 82 * will attempt to load any missing locale data using the ilib loader callback. 83 * When the constructor is done (even if the data is already preassembled), the 84 * onLoad function is called with the current instance as a parameter, so this 85 * callback can be used with preassembled or dynamic loading or a mix of the two. 86 * 87 * <li><i>sync</i> - tell whether to load any missing locale data synchronously or 88 * asynchronously. If this option is given as "false", then the "onLoad" 89 * callback must be given, as the instance returned from this constructor will 90 * not be usable for a while. 91 * 92 * <li><i>loadParams</i> - an object containing parameters to pass to the 93 * loader callback function when locale data is missing. The parameters are not 94 * interpretted or modified in any way. They are simply passed along. The object 95 * may contain any property/value pairs as long as the calling code is in 96 * agreement with the loader callback function as to what those parameters mean. 97 * </ul> 98 * 99 * @constructor 100 * @param {Object} options parameters controlling the geolocation of the phone number. 101 */ 102 var PhoneGeoLocator = function(options) { 103 var sync = true, 104 loadParams = {}, 105 locale = ilib.getLocale(); 106 107 if (options) { 108 if (options.locale) { 109 locale = options.locale; 110 } 111 112 if (typeof(options.sync) === 'boolean') { 113 sync = options.sync; 114 } 115 116 if (options.loadParams) { 117 loadParams = options.loadParams; 118 } 119 } 120 121 new PhoneLocale({ 122 locale: locale, 123 mcc: options && options.mcc, 124 countryCode: options && options.countryCode, 125 sync: sync, 126 loadParams: loadParams, 127 onLoad: ilib.bind(this, function (loc) { 128 this.locale = loc; 129 new NumberingPlan({ 130 locale: this.locale, 131 sync: sync, 132 loadParams: loadParams, 133 onLoad: ilib.bind(this, function (plan) { 134 this.plan = plan; 135 136 new ResBundle({ 137 locale: this.locale, 138 name: "phoneres", 139 sync: sync, 140 loadParams: loadParams, 141 onLoad: ilib.bind(this, function (rb) { 142 this.rb = rb; 143 144 Utils.loadData({ 145 name: "iddarea.json", 146 object: "PhoneGeoLocator", 147 nonlocale: true, 148 sync: sync, 149 loadParams: loadParams, 150 callback: ilib.bind(this, function (data) { 151 this.regiondata = data; 152 Utils.loadData({ 153 name: "area.json", 154 object: "PhoneGeoLocator", 155 locale: this.locale, 156 sync: sync, 157 loadParams: JSUtils.merge(loadParams, { 158 returnOne: true 159 }), 160 callback: ilib.bind(this, function (areadata) { 161 this.areadata = areadata; 162 163 if (options && typeof(options.onLoad) === 'function') { 164 options.onLoad(this); 165 } 166 }) 167 }); 168 }) 169 }); 170 }) 171 }); 172 }) 173 }); 174 }) 175 }); 176 }; 177 178 PhoneGeoLocator.prototype = { 179 /** 180 * @private 181 * 182 * Used for locales where the area code is very general, and you need to add in 183 * the initial digits of the subscriber number in order to get the area 184 * 185 * @param {string} number 186 * @param {Object} stateTable 187 */ 188 _parseAreaAndSubscriber: function (number, stateTable) { 189 var ch, 190 i, 191 handlerMethod, 192 newState, 193 consumed, 194 lastLeaf, 195 currentState, 196 dot = 14; // special transition which matches all characters. See AreaCodeTableMaker.java 197 198 if (!number || !stateTable) { 199 // can't parse anything 200 return undefined; 201 } 202 203 //console.log("GeoLocator._parseAreaAndSubscriber: parsing number " + number); 204 205 currentState = stateTable; 206 i = 0; 207 while (i < number.length) { 208 ch = PhoneNumber._getCharacterCode(number.charAt(i)); 209 if (ch >= 0) { 210 // newState = stateData.states[state][ch]; 211 newState = currentState.s && currentState.s[ch]; 212 213 if (!newState && currentState.s && currentState.s[dot]) { 214 newState = currentState.s[dot]; 215 } 216 217 if (typeof(newState) === 'object') { 218 if (typeof(newState.l) !== 'undefined') { 219 // save for latter if needed 220 lastLeaf = newState; 221 consumed = i; 222 } 223 // console.info("recognized digit " + ch + " continuing..."); 224 // recognized digit, so continue parsing 225 currentState = newState; 226 i++; 227 } else { 228 if (typeof(newState) === 'undefined' || newState === 0) { 229 // this is possibly a look-ahead and it didn't work... 230 // so fall back to the last leaf and use that as the 231 // final state 232 newState = lastLeaf; 233 i = consumed; 234 } 235 236 if ((typeof(newState) === 'number' && newState) || 237 (typeof(newState) === 'object' && typeof(newState.l) !== 'undefined')) { 238 // final state 239 var stateNumber = typeof(newState) === 'number' ? newState : newState.l; 240 handlerMethod = PhoneNumber._states[stateNumber]; 241 242 //console.info("reached final state " + newState + " handler method is " + handlerMethod + " and i is " + i); 243 244 return (handlerMethod === "area") ? number.substring(0, i+1) : undefined; 245 } else { 246 // failed parse. Either no last leaf to fall back to, or there was an explicit 247 // zero in the table 248 break; 249 } 250 } 251 } else if (ch === -1) { 252 // non-transition character, continue parsing in the same state 253 i++; 254 } else { 255 // should not happen 256 // console.info("skipping character " + ch); 257 // not a digit, plus, pound, or star, so this is probably a formatting char. Skip it. 258 i++; 259 } 260 } 261 return undefined; 262 }, 263 /** 264 * @private 265 * @param prefix 266 * @param table 267 * @returns 268 */ 269 _matchPrefix: function(prefix, table) { 270 var i, matchedDot, matchesWithDots = []; 271 272 if (table[prefix]) { 273 return table[prefix]; 274 } 275 for (var entry in table) { 276 if (entry && typeof(entry) === 'string') { 277 i = 0; 278 matchedDot = false; 279 while (i < entry.length && (entry.charAt(i) === prefix.charAt(i) || entry.charAt(i) === '.')) { 280 if (entry.charAt(i) === '.') { 281 matchedDot = true; 282 } 283 i++; 284 } 285 if (i >= entry.length) { 286 if (matchedDot) { 287 matchesWithDots.push(entry); 288 } else { 289 return table[entry]; 290 } 291 } 292 } 293 } 294 295 // match entries with dots last, so sort the matches so that the entry with the 296 // most dots sorts last. The entry that ends up at the beginning of the list is 297 // the best match because it has the fewest dots 298 if (matchesWithDots.length > 0) { 299 matchesWithDots.sort(function (left, right) { 300 return (right < left) ? -1 : ((left < right) ? 1 : 0); 301 }); 302 return table[matchesWithDots[0]]; 303 } 304 305 return undefined; 306 }, 307 /** 308 * @private 309 * @param number 310 * @param data 311 * @param locale 312 * @param plan 313 * @param options 314 * @returns {Object} 315 */ 316 _getAreaInfo: function(number, data, locale, plan, options) { 317 var sync = true, 318 ret = {}, 319 countryCode, 320 areaInfo, 321 temp, 322 areaCode, 323 geoTable, 324 tempNumber, 325 prefix; 326 327 if (options && typeof(options.sync) === 'boolean') { 328 sync = options.sync; 329 } 330 331 prefix = number.areaCode || number.serviceCode; 332 geoTable = data; 333 334 if (prefix !== undefined) { 335 if (plan.getExtendedAreaCode()) { 336 // for countries where the area code is very general and large, and you need a few initial 337 // digits of the subscriber number in order find the actual area 338 tempNumber = prefix + number.subscriberNumber; 339 tempNumber = tempNumber.replace(/[wWpPtT\+#\*]/g, ''); // fix for NOV-108200 340 341 Utils.loadData({ 342 name: "extarea.json", 343 object: "PhoneGeoLocator", 344 locale: locale, 345 sync: sync, 346 loadParams: JSUtils.merge((options && options.loadParams) || {}, {returnOne: true}), 347 callback: ilib.bind(this, function (data) { 348 this.extarea = data; 349 Utils.loadData({ 350 name: "extstates.json", 351 object: "PhoneGeoLocator", 352 locale: locale, 353 sync: sync, 354 loadParams: JSUtils.merge((options && options.loadParams) || {}, {returnOne: true}), 355 callback: ilib.bind(this, function (data) { 356 this.extstates = data; 357 geoTable = this.extarea; 358 if (this.extarea && this.extstates) { 359 prefix = this._parseAreaAndSubscriber(tempNumber, this.extstates); 360 } 361 362 if (!prefix) { 363 // not a recognized prefix, so now try the general table 364 geoTable = this.areadata; 365 prefix = number.areaCode || number.serviceCode; 366 } 367 368 if ((!plan.fieldLengths || 369 plan.getFieldLength('maxLocalLength') === undefined || 370 !number.subscriberNumber || 371 number.subscriberNumber.length <= plan.fieldLengths('maxLocalLength'))) { 372 areaInfo = this._matchPrefix(prefix, geoTable); 373 if (areaInfo && areaInfo.sn && areaInfo.ln) { 374 //console.log("Found areaInfo " + JSON.stringify(areaInfo)); 375 ret.area = { 376 sn: this.rb.getString(areaInfo.sn).toString(), 377 ln: this.rb.getString(areaInfo.ln).toString() 378 }; 379 } 380 } 381 }) 382 }); 383 }) 384 }); 385 386 } else if (!plan || 387 plan.getFieldLength('maxLocalLength') === undefined || 388 !number.subscriberNumber || 389 number.subscriberNumber.length <= plan.getFieldLength('maxLocalLength')) { 390 if (geoTable) { 391 areaCode = prefix.replace(/[wWpPtT\+#\*]/g, ''); 392 areaInfo = this._matchPrefix(areaCode, geoTable); 393 394 if (areaInfo && areaInfo.sn && areaInfo.ln) { 395 ret.area = { 396 sn: this.rb.getString(areaInfo.sn).toString(), 397 ln: this.rb.getString(areaInfo.ln).toString() 398 }; 399 } else if (number.serviceCode) { 400 ret.area = { 401 sn: this.rb.getString("Service Number").toString(), 402 ln: this.rb.getString("Service Number").toString() 403 }; 404 } 405 } else { 406 countryCode = number.locale._mapRegiontoCC(this.locale.getRegion()); 407 if (countryCode !== "0" && this.regiondata) { 408 temp = this.regiondata[countryCode]; 409 if (temp && temp.sn) { 410 ret.country = { 411 sn: this.rb.getString(temp.sn).toString(), 412 ln: this.rb.getString(temp.ln).toString(), 413 code: this.locale.getRegion() 414 }; 415 } 416 } 417 } 418 } else { 419 countryCode = number.locale._mapRegiontoCC(this.locale.getRegion()); 420 if (countryCode !== "0" && this.regiondata) { 421 temp = this.regiondata[countryCode]; 422 if (temp && temp.sn) { 423 ret.country = { 424 sn: this.rb.getString(temp.sn).toString(), 425 ln: this.rb.getString(temp.ln).toString(), 426 code: this.locale.getRegion() 427 }; 428 } 429 } 430 } 431 432 } else if (number.mobilePrefix) { 433 ret.area = { 434 sn: this.rb.getString("Mobile Number").toString(), 435 ln: this.rb.getString("Mobile Number").toString() 436 }; 437 } else if (number.emergency) { 438 ret.area = { 439 sn: this.rb.getString("Emergency Services Number").toString(), 440 ln: this.rb.getString("Emergency Services Number").toString() 441 }; 442 } 443 444 return ret; 445 }, 446 /** 447 * Returns a the location of the given phone number, if known. 448 * The returned object has 2 properties, each of which has an sn (short name) 449 * and an ln (long name) string. Additionally, the country code, if given, 450 * includes the 2 letter ISO code for the recognized country. 451 * { 452 * "country": { 453 * "sn": "North America", 454 * "ln": "North America and the Caribbean Islands", 455 * "code": "us" 456 * }, 457 * "area": { 458 * "sn": "California", 459 * "ln": "Central California: San Jose, Los Gatos, Milpitas, Sunnyvale, Cupertino, Gilroy" 460 * } 461 * } 462 * 463 * The location name is subject to the following rules: 464 * 465 * If the areaCode property is undefined or empty, or if the number specifies a 466 * country code for which we do not have information, then the area property may be 467 * missing from the returned object. In this case, only the country object will be returned. 468 * 469 * If there is no area code, but there is a mobile prefix, service code, or emergency 470 * code, then a fixed string indicating the type of number will be returned. 471 * 472 * The country object is filled out according to the countryCode property of the phone 473 * number. 474 * 475 * If the phone number does not have an explicit country code, the MCC will be used if 476 * it is available. The country code can be gleaned directly from the MCC. If the MCC 477 * of the carrier to which the phone is currently connected is available, it should be 478 * passed in so that local phone numbers will look correct. 479 * 480 * If the country's dialling plan mandates a fixed length for phone numbers, and a 481 * particular number exceeds that length, then the area code will not be given on the 482 * assumption that the number has problems in the first place and we cannot guess 483 * correctly. 484 * 485 * The returned area property varies in specificity according 486 * to the locale. In North America, the area is no finer than large parts of states 487 * or provinces. In Germany and the UK, the area can be as fine as small towns. 488 * 489 * The strings returned from this function are already localized to the 490 * given locale, and thus are ready for display to the user. 491 * 492 * If the number passed in is invalid, an empty object is returned. If the location 493 * information about the country where the phone number is located is not available, 494 * then the area information will be missing and only the country will be returned. 495 * 496 * The options parameter can contain any one of the following properties: 497 * 498 * <ul> 499 * <li><i>locale</i> The locale parameter is used to load translations of the names of regions and 500 * areas if available. For example, if the locale property is given as "en-US" (English for USA), 501 * but the phone number being geolocated is in Germany, then this class would return the the names 502 * of the country (Germany) and region inside of Germany in English instead of German. That is, a 503 * phone number in Munich and return the country "Germany" and the area code "Munich" 504 * instead of "Deutschland" and "München". The default display locale is the current ilib locale. 505 * If translations are not available, the region and area names are given in English, which should 506 * always be available. 507 * <li><i>mcc</i> The mcc of the current mobile carrier, if known. 508 * 509 * <li><i>onLoad</i> - a callback function to call when the data for the 510 * locale is fully loaded. When the onLoad option is given, this object 511 * will attempt to load any missing locale data using the ilib loader callback. 512 * When the constructor is done (even if the data is already preassembled), the 513 * onLoad function is called with the current instance as a parameter, so this 514 * callback can be used with preassembled or dynamic loading or a mix of the two. 515 * 516 * <li><i>sync</i> - tell whether to load any missing locale data synchronously or 517 * asynchronously. If this option is given as "false", then the "onLoad" 518 * callback must be given, as the instance returned from this constructor will 519 * not be usable for a while. 520 * 521 * <li><i>loadParams</i> - an object containing parameters to pass to the 522 * loader callback function when locale data is missing. The parameters are not 523 * interpretted or modified in any way. They are simply passed along. The object 524 * may contain any property/value pairs as long as the calling code is in 525 * agreement with the loader callback function as to what those parameters mean. 526 * </ul> 527 * 528 * @param {PhoneNumber} number phone number to locate 529 * @param {Object} options options governing the way this ares is loaded 530 * @return {Object} an object 531 * that describes the country and the area in that country corresponding to this 532 * phone number. Each of the country and area contain a short name (sn) and long 533 * name (ln) that describes the location. 534 */ 535 locate: function(number, options) { 536 var loadParams = {}, 537 ret = {}, 538 region, 539 countryCode, 540 temp, 541 plan, 542 areaResult, 543 phoneLoc = this.locale, 544 sync = true; 545 546 if (number === undefined || typeof(number) !== 'object' || !(number instanceof PhoneNumber)) { 547 return ret; 548 } 549 550 if (options) { 551 if (typeof(options.sync) !== 'undefined') { 552 sync = !!options.sync; 553 } 554 555 if (options.loadParams) { 556 loadParams = options.loadParams; 557 } 558 } 559 560 // console.log("GeoLocator.locate: looking for geo for number " + JSON.stringify(number)); 561 region = this.locale.getRegion(); 562 if (number.countryCode !== undefined && this.regiondata) { 563 countryCode = number.countryCode.replace(/[wWpPtT\+#\*]/g, ''); 564 temp = this.regiondata[countryCode]; 565 phoneLoc = number.destinationLocale; 566 plan = number.destinationPlan; 567 ret.country = { 568 sn: this.rb.getString(temp.sn).toString(), 569 ln: this.rb.getString(temp.ln).toString(), 570 code: phoneLoc.getRegion() 571 }; 572 } 573 574 if (!plan) { 575 plan = this.plan; 576 } 577 578 Utils.loadData({ 579 name: "area.json", 580 object: "PhoneGeoLocator", 581 locale: phoneLoc, 582 sync: sync, 583 loadParams: JSUtils.merge(loadParams, { 584 returnOne: true 585 }), 586 callback: ilib.bind(this, function (areadata) { 587 if (areadata) { 588 this.areadata = areadata; 589 } 590 areaResult = this._getAreaInfo(number, this.areadata, phoneLoc, plan, options); 591 ret = JSUtils.merge(ret, areaResult); 592 593 if (ret.country === undefined) { 594 countryCode = number.locale._mapRegiontoCC(region); 595 596 if (countryCode !== "0" && this.regiondata) { 597 temp = this.regiondata[countryCode]; 598 if (temp && temp.sn) { 599 ret.country = { 600 sn: this.rb.getString(temp.sn).toString(), 601 ln: this.rb.getString(temp.ln).toString(), 602 code: this.locale.getRegion() 603 }; 604 } 605 } 606 } 607 }) 608 }); 609 610 return ret; 611 }, 612 613 /** 614 * Returns a string that describes the ISO-3166-2 country code of the given phone 615 * number.<p> 616 * 617 * If the phone number is a local phone number and does not contain 618 * any country information, this routine will return the region for the current 619 * formatter instance. 620 * 621 * @param {PhoneNumber} number An PhoneNumber instance 622 * @return {string} 623 */ 624 country: function(number) { 625 var countryCode, 626 region, 627 phoneLoc; 628 629 if (!number || !(number instanceof PhoneNumber)) { 630 return ""; 631 } 632 633 phoneLoc = number.locale; 634 635 region = (number.countryCode && phoneLoc._mapCCtoRegion(number.countryCode)) || 636 (number.locale && number.locale.region) || 637 phoneLoc.locale.getRegion() || 638 this.locale.getRegion(); 639 640 countryCode = number.countryCode || phoneLoc._mapRegiontoCC(region); 641 642 if (number.areaCode) { 643 region = phoneLoc._mapAreatoRegion(countryCode, number.areaCode); 644 } else if (countryCode === "33" && number.serviceCode) { 645 // french departments are in the service code, not the area code 646 region = phoneLoc._mapAreatoRegion(countryCode, number.serviceCode); 647 } 648 return region; 649 } 650 }; 651 652 module.exports = PhoneGeoLocator; 653