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