1 /* 2 * AddressFmt.js - Format an address 3 * 4 * Copyright © 2013-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 address addressres regionnames 21 22 var ilib = require("./ilib.js"); 23 var Utils = require("./Utils.js"); 24 var JSUtils = require("./JSUtils.js"); 25 26 var Locale = require("./Locale.js"); 27 var Address = require("./Address.js"); 28 var IString = require("./IString.js"); 29 var ResBundle = require("./ResBundle.js"); 30 31 /** 32 * @class 33 * Create a new formatter object to format physical addresses in a particular way. 34 * 35 * The options object may contain the following properties, both of which are optional: 36 * 37 * <ul> 38 * <li><i>locale</i> - the locale to use to format this address. If not specified, it uses the default locale 39 * 40 * <li><i>style</i> - the style of this address. The default style for each country usually includes all valid 41 * fields for that country. 42 * 43 * <li><i>onLoad</i> - a callback function to call when the address info for the 44 * locale is fully loaded and the address has been parsed. When the onLoad 45 * option is given, the address formatter object 46 * will attempt to load any missing locale data using the ilib loader callback. 47 * When the constructor is done (even if the data is already preassembled), the 48 * onLoad function is called with the current instance as a parameter, so this 49 * callback can be used with preassembled or dynamic loading or a mix of the two. 50 * 51 * <li><i>sync</i> - tell whether to load any missing locale data synchronously or 52 * asynchronously. If this option is given as "false", then the "onLoad" 53 * callback must be given, as the instance returned from this constructor will 54 * not be usable for a while. 55 * 56 * <li><i>loadParams</i> - an object containing parameters to pass to the 57 * loader callback function when locale data is missing. The parameters are not 58 * interpretted or modified in any way. They are simply passed along. The object 59 * may contain any property/value pairs as long as the calling code is in 60 * agreement with the loader callback function as to what those parameters mean. 61 * </ul> 62 * 63 * 64 * @constructor 65 * @param {Object} options options that configure how this formatter should work 66 * Returns a formatter instance that can format multiple addresses. 67 */ 68 var AddressFmt = function(options) { 69 this.sync = true; 70 this.styleName = 'default'; 71 this.loadParams = {}; 72 this.locale = new Locale(); 73 74 if (options) { 75 if (options.locale) { 76 this.locale = (typeof(options.locale) === 'string') ? new Locale(options.locale) : options.locale; 77 } 78 79 if (typeof(options.sync) !== 'undefined') { 80 this.sync = !!options.sync; 81 } 82 83 if (options.style) { 84 this.styleName = options.style; 85 } 86 87 if (options.loadParams) { 88 this.loadParams = options.loadParams; 89 } 90 } 91 92 // console.log("Creating formatter for region: " + this.locale.region); 93 Utils.loadData({ 94 name: "address.json", 95 object: "AddressFmt", 96 locale: this.locale, 97 sync: this.sync, 98 loadParams: this.loadParams, 99 callback: ilib.bind(this, function(info) { 100 if (!info || JSUtils.isEmpty(info)) { 101 // load the "unknown" locale instead 102 Utils.loadData({ 103 name: "address.json", 104 object: "AddressFmt", 105 locale: new Locale("XX"), 106 sync: this.sync, 107 loadParams: this.loadParams, 108 callback: ilib.bind(this, function(info) { 109 this.info = info; 110 this._init(); 111 if (options && typeof(options.onLoad) === 'function') { 112 options.onLoad(this); 113 } 114 }) 115 }); 116 } else { 117 this.info = info; 118 this._init(); 119 if (options && typeof(options.onLoad) === 'function') { 120 options.onLoad(this); 121 } 122 } 123 }) 124 }); 125 }; 126 127 /** 128 * @private 129 */ 130 AddressFmt.prototype._init = function () { 131 this.style = this.info && this.info.formats && this.info.formats[this.styleName]; 132 133 // use generic default -- should not happen, but just in case... 134 this.style = this.style || (this.info && this.info.formats && this.info.formats["default"]) || "{streetAddress}\n{locality} {region} {postalCode}\n{country}"; 135 }; 136 137 /** 138 * This function formats a physical address (Address instance) for display. 139 * Whitespace is trimmed from the beginning and end of final resulting string, and 140 * multiple consecutive whitespace characters in the middle of the string are 141 * compressed down to 1 space character. 142 * 143 * If the Address instance is for a locale that is different than the locale for this 144 * formatter, then a hybrid address is produced. The country name is located in the 145 * correct spot for the current formatter's locale, but the rest of the fields are 146 * formatted according to the default style of the locale of the actual address. 147 * 148 * Example: a mailing address in China, but formatted for the US might produce the words 149 * "People's Republic of China" in English at the last line of the address, and the 150 * Chinese-style address will appear in the first line of the address. In the US, the 151 * country is on the last line, but in China the country is usually on the first line. 152 * 153 * @param {Address} address Address to format 154 * @returns {string} Returns a string containing the formatted address 155 */ 156 AddressFmt.prototype.format = function (address) { 157 var ret, template, other, format; 158 159 if (!address) { 160 return ""; 161 } 162 // console.log("formatting address: " + JSON.stringify(address)); 163 if (address.countryCode && 164 address.countryCode !== this.locale.region && 165 Locale._isRegionCode(this.locale.region) && 166 this.locale.region !== "XX") { 167 // we are formatting an address that is sent from this country to another country, 168 // so only the country should be in this locale, and the rest should be in the other 169 // locale 170 // console.log("formatting for another locale. Loading in its settings: " + address.countryCode); 171 other = new AddressFmt({ 172 locale: new Locale(address.countryCode), 173 style: this.styleName 174 }); 175 return other.format(address); 176 } 177 178 if (typeof(this.style) === 'object') { 179 format = this.style[address.format || "latin"]; 180 } else { 181 format = this.style; 182 } 183 184 // console.log("Using format: " + format); 185 // make sure we have a blank string for any missing parts so that 186 // those template parts get blanked out 187 var params = { 188 country: address.country || "", 189 region: address.region || "", 190 locality: address.locality || "", 191 streetAddress: address.streetAddress || "", 192 postalCode: address.postalCode || "", 193 postOffice: address.postOffice || "" 194 }; 195 template = new IString(format); 196 ret = template.format(params); 197 ret = ret.replace(/[ \t]+/g, ' '); 198 ret = ret.replace("\n ", "\n"); 199 ret = ret.replace(" \n", "\n"); 200 return ret.replace(/\n+/g, '\n').trim(); 201 }; 202 203 204 /** 205 * Return true if this is an asian locale. 206 * @private 207 * @returns {boolean} true if this is an asian locale, or false otherwise 208 */ 209 function isAsianLocale(locale) { 210 return locale.language === "zh" || locale.language === "ja" || locale.language === "ko"; 211 } 212 213 /** 214 * Invert the properties and values, filtering out all the regions. Regions either 215 * have values with numbers (eg. "150" for Europe), or they are on a short list of 216 * known regions with actual ISO codes. 217 * 218 * @private 219 * @returns {Object} the inverted object 220 */ 221 function invertAndFilter(object) { 222 var ret = []; 223 var regions = ["AQ", "EU", "EZ", "UN", "ZZ"] 224 for (var p in object) { 225 if (p && !object[p].match(/\d/) && regions.indexOf(object[p]) === -1) { 226 ret.push({ 227 code: object[p], 228 name: p 229 }); 230 } 231 } 232 233 return ret; 234 } 235 236 /** 237 * Return information about the address format that can be used 238 * by UI builders to display a locale-sensitive set of input fields 239 * based on the current formatter's settings.<p> 240 * 241 * The object returned by this method is an array of address rows. Each 242 * row is itself an array which may have one to four address 243 * components in that row. Each address component is an object 244 * that contains a component property and a label to display 245 * with it. The label is written in the given locale, or the 246 * locale of this formatter if it was not given.<p> 247 * 248 * Optionally, if the address component is constrained to a 249 * particular pattern or to a fixed list of possible values, then 250 * the constraint rules are given in the "constraint" property.<p> 251 * 252 * If an address component must conform to a particular pattern, 253 * the regular expression that matches that pattern 254 * is returned in "constraint". Mostly, it is only the postal code 255 * component that can be validated in this way.<p> 256 * 257 * If an address component should be limited 258 * to a fixed list of values, then the constraint property will be 259 * set to an array that lists those values. The constraint contains 260 * an array of objects in the correct sorted order for the locale 261 * where each object contains an code property containing the ISO code, 262 * and a name field to show in UI. 263 * The ISO codes should not be shown to the user and are intended to 264 * represent the values in code. The names are translated to the given 265 * locale or to the locale of this formatter if it was not given. For 266 * the most part, it is the region and country components that 267 * are constrained in this way.<p> 268 * 269 * Here is what the result would look like for a US address: 270 * <pre> 271 * [ 272 * [{ 273 * "component": "streetAddress", 274 * "label": "Street Address" 275 * }], 276 * [{ 277 * "component": "locality", 278 * "label": "City" 279 * },{ 280 * "component": "region", 281 * "label": "State", 282 * "constraint": [{ 283 * "code": "AL", 284 * "name": "Alabama" 285 * },{ 286 * "code": "AK", 287 * "name": "Alaska" 288 * },{ 289 * ... 290 * },{ 291 * "code": "WY", 292 * "name": "Wyoming" 293 * } 294 * },{ 295 * "component": "postalCode", 296 * "label": "Zip Code", 297 * "constraint": "[0-9]{5}(-[0-9]{4})?" 298 * }], 299 * [{ 300 * "component": "country", 301 * "label": "Country", 302 * "constraint": [{ 303 * "code": "AF", 304 * "name": "Afghanistan" 305 * },{ 306 * "code": "AL", 307 * "name": "Albania" 308 * },{ 309 * ... 310 * },{ 311 * "code": "ZW", 312 * "name": "Zimbabwe" 313 * }] 314 * }] 315 * ] 316 * </pre> 317 * <p> 318 * @example <caption>Example of calling the getFormatInfo method</caption> 319 * 320 * // the AddressFmt should be created with the locale of the address you 321 * // would like the user to enter. For example, if you have a "country" 322 * // selector, you would create a new AddressFmt instance each time the 323 * // selector is changed. 324 * new AddressFmt({ 325 * locale: 'nl-NL', // for addresses in the Netherlands 326 * onLoad: ilib.bind(this, function(fmt) { 327 * fmt.getAddressFormatInfo({ 328 * // The following is the locale of the UI you would like to see the labels 329 * // like "City" and "Postal Code" translated to. In this example, we 330 * // are showing an input form for Dutch addresses, but the labels are 331 * // written in US English. 332 * locale: "en-US", 333 * onLoad: ilib.bind(this, function(rows) { 334 * // iterate through the rows array and dynamically create the input 335 * // elements with the given labels 336 * }) 337 * }); 338 * }) 339 * }); 340 * 341 * @param {Locale|string=} locale the locale to translate the labels 342 * to. If not given, the locale of the formatter will be used. 343 * @param {boolean=} sync true if this method should load the data 344 * synchronously, false if async 345 * @param {Function=} callback a callback to call when the data 346 * is ready 347 * @returns {Array.<Object>} An array of rows of address components 348 */ 349 AddressFmt.prototype.getFormatInfo = function(locale, sync, callback) { 350 var info; 351 var loc = new Locale(this.locale); 352 if (locale) { 353 if (typeof(locale) === "string") { 354 locale = new Locale(locale); 355 } 356 loc.language = locale.getLanguage(); 357 loc.spec = undefined; 358 } 359 360 Utils.loadData({ 361 name: "regionnames.json", 362 object: "AddressFmt", 363 locale: loc, 364 sync: this.sync, 365 loadParams: JSUtils.merge(this.loadParams, {returnOne: true}, true), 366 callback: ilib.bind(this, function(regions) { 367 this.regions = regions; 368 369 new ResBundle({ 370 locale: loc, 371 name: "addressres", 372 sync: this.sync, 373 loadParams: this.loadParams, 374 onLoad: ilib.bind(this, function (rb) { 375 var type, format, fields = this.info.fields; 376 if (this.info.multiformat) { 377 type = isAsianLocale(this.locale) ? "asian" : "latin"; 378 fields = this.info.fields[type]; 379 } 380 381 if (typeof(this.style) === 'object') { 382 format = this.style[type || "latin"]; 383 } else { 384 format = this.style; 385 } 386 new Address(" ", { 387 locale: loc, 388 sync: this.sync, 389 loadParams: this.loadParams, 390 onLoad: ilib.bind(this, function(localeAddress) { 391 var rows = format.split(/\n/g); 392 info = rows.map(ilib.bind(this, function(row) { 393 return row.split("}").filter(function(component) { 394 return component.length > 0; 395 }).map(ilib.bind(this, function(component) { 396 var name = component.replace(/.*{/, ""); 397 var obj = { 398 component: name, 399 label: rb.getStringJS(this.info.fieldNames[name]) 400 }; 401 var field = fields.filter(function(f) { 402 return f.name === name; 403 }); 404 if (field && field[0] && field[0].pattern) { 405 if (typeof(field[0].pattern) === "string") { 406 obj.constraint = field[0].pattern; 407 } 408 } 409 if (name === "country") { 410 obj.constraint = invertAndFilter(localeAddress.ctrynames); 411 } else if (name === "region" && this.regions[loc.getRegion()]) { 412 obj.constraint = this.regions[loc.getRegion()]; 413 } 414 return obj; 415 })); 416 })); 417 418 if (callback && typeof(callback) === "function") { 419 callback(info); 420 } 421 }) 422 }); 423 }) 424 }); 425 }) 426 }); 427 428 return info; 429 }; 430 431 module.exports = AddressFmt; 432