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