1 /*
  2  * LengthUnit.js
  3  *
  4  * Sweet Home 3D, Copyright (c) 2024 Space Mushrooms <info@sweethome3d.com>
  5  *
  6  * This program is free software; you can redistribute it and/or modify
  7  * it under the terms of the GNU General Public License as published by
  8  * the Free Software Foundation; either version 2 of the License, or
  9  * (at your option) any later version.
 10  *
 11  * This program is distributed in the hope that it will be useful,
 12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 14  * GNU General Public License for more details.
 15  *
 16  * You should have received a copy of the GNU General Public License
 17  * along with this program; if not, write to the Free Software
 18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 19  */
 20 
 21 /**
 22  * Unit used for lengths.
 23  * @constructor
 24  * @author Emmanuel Puybaret
 25  */
 26 var LengthUnit = {};
 27 
 28 LengthUnit.UNITS_LOCALIZATION = {
 29   "":   {"meterUnit": "m",
 30          "centimeterUnit": "cm",
 31          "millimeterUnit": "mm",
 32          "inchUnit": "inch",
 33          "footUnit": "ft",
 34          "squareMeterUnit": "m\u00b2",
 35          "squareFootUnit": "sq ft"},
 36   "ar": {"meterUnit": "\u0645",
 37          "centimeterUnit": "\u0633\u0645",
 38          "millimeterUnit": "\u0645\u0645",
 39          "inchUnit": "\u0627\u0646\u0634",
 40          "squareMeterUnit": "\u0645\u062a\u0631 \u0645\u0631\u0628\u0639",
 41          "squareFootUnit": "\u0642\u062f\u0645 \u0645\u0631\u0628\u0639"},
 42   "bg": {"inchUnit": "inch"},
 43   "cs": {"inchUnit": "palce",
 44          "squareFootUnit": "ft\u00b2"},
 45   "de": {"inchUnit": "inch",
 46          "footUnit": "ft",
 47          "squareFootUnit": "ft\u00b2"},
 48   "el": {"inchUnit": "\u03af\u03bd\u03c4\u03c3\u03b1",
 49          "squareMeterUnit": "m\u00b2",
 50          "squareFootUnit": "\u03c4\u03b5\u03c4\u03c1\u03b1\u03b3\u03c9\u03bd\u03b9\u03ba\u03cc \u03c0\u03cc\u03b4\u03b9"},
 51   "es": {"inchUnit": "pulgada",
 52          "squareFootUnit": "ft\u00b2"},
 53   "eu": {"inchUnit": "hazbete",
 54          "squareFootUnit": "sq ft"},
 55   "fi": {"inchUnit": "\"",
 56          "squareFootUnit": "ft\u00b2"},
 57   "fr": {"inchUnit": "pouce",
 58          "footUnit": "pied",
 59          "squareFootUnit": "ft\u00b2"},
 60   "hu": {"inchUnit": "inch",
 61          "squareFootUnit": "ft\u00b2"},
 62   "in": {"inchUnit": "inci",
 63          "squareFootUnit": "ft\u00b2"},
 64   "it": {"inchUnit": "pollice",
 65          "squareFootUnit": "ft\u00b2"},
 66   "ja": {"inchUnit": "inch",
 67          "squareFootUnit": "sq ft"},
 68   "nl": {"inchUnit": "inch",
 69          "squareFootUnit": "vt\u00b2"},
 70   "pl": {"inchUnit": "cal",
 71          "squareFootUnit": "ft\u00b2"},
 72   "pt_BR": {"inchUnit": "pol",
 73             "squareFootUnit": "ft\u00b2"},
 74   "pt": {"inchUnit": "pol",
 75          "squareFootUnit": "p\u00e9\u00b2"},
 76   "ru": {"meterUnit": "\u043c",
 77          "centimeterUnit": "\u0441\u043c",
 78          "millimeterUnit": "\u043c\u043c",
 79          "inchUnit": "\u0434\u044e\u0439\u043c",
 80          "squareMeterUnit": "\u043c\u00b2",
 81          "squareFootUnit": "ft\u00b2"},
 82   "sl": {"inchUnit": "in\u010d",
 83          "squareFootUnit": "kv \u010dl"},
 84   "sr": {"inchUnit": "inch",
 85          "squareFootUnit": "sq ft"},
 86   "sv": {"inchUnit": "tum",
 87          "squareFootUnit": "ft\u00b2"},
 88   "th": {"meterUnit": "\u0e21.",
 89           "centimeterUnit": "\u0e0b\u0e21.",
 90           "millimeterUnit": "\u0e21\u0e21.",
 91           "inchUnit": "\u0e19\u0e34\u0e49\u0e27",
 92           "squareMeterUnit": "\u0e21.\u00b2",
 93           "squareFootUnit": "\u0e15\u0e32\u0e23\u0e32\u0e07\u0e1f\u0e38\u0e15"},
 94   "tr": {"inchUnit": "in\u00e7",
 95          "squareFootUnit": "kare ft "},
 96   "uk": {"meterUnit": "\u043c",
 97          "centimeterUnit": "\u0441\u043c",
 98          "millimeterUnit": "\u043c\u043c",
 99          "inchUnit": "\u0434\u044e\u0439\u043c",
100          "squareMeterUnit": "\u043c\u00b2",
101          "squareFootUnit": "\u043a\u0432. \u0444\u0442"},
102   "vi": {"inchUnit": "inch",
103          "squareFootUnit": "sq ft"},
104   "zh_CN": {"centimeterUnit": "\u5398\u7c73",
105             "inchUnit": "\u82f1\u5bf8",
106             "millimeterUnit": "\u6beb\u7c73",
107             "meterUnit": "\u7c73",
108             "squareMeterUnit": "m\u00b2",
109             "squareFootUnit": "\u5e73\u65b9\u82f1\u5c3a"},
110   "zh_TW": {"centimeterUnit": "\u516c\u5206",
111             "inchUnit": "\u82f1\u540b",
112             "millimeterUnit": "\u516c\u91d0",
113             "meterUnit": "\u516c\u5c3a",
114             "squareMeterUnit": "m\u00b2",
115             "squareFootUnit": "ft\u00b2"}
116   };
117   
118 /**
119  * Returns the unit name in default locale. 
120  * @param {string} unit
121  */
122 LengthUnit.getLocalizedUnit = function(unit) {
123   var locale = Locale.getDefault();
124   var localizedUnits = LengthUnit.UNITS_LOCALIZATION [locale];
125   if (localizedUnits !== undefined) {
126 	var localizedUnit = localizedUnits [unit];
127     if (localizedUnit !== undefined) {
128       return localizedUnit;
129 	}
130   }
131   if (locale.indexOf("_") > 0) {
132     locale = locale.substring(0, locale.indexOf("_"));	
133     var localizedUnits = LengthUnit.UNITS_LOCALIZATION [locale];
134     if (localizedUnits !== undefined) {
135 	  var localizedUnit = localizedUnits [unit];
136       if (localizedUnit !== undefined) {
137         return localizedUnit;
138 	  }
139     }
140   }
141   return LengthUnit.UNITS_LOCALIZATION [""][unit];
142 } 
143 
144 /**
145  * Returns the value close to the given length under magnetism for meter units.
146  * @param {number} length
147  * @param {number} maxDelta
148  * @private
149  */
150 LengthUnit.getMagnetizedMeterLength = function(length, maxDelta) {
151   // Use a maximum precision of 1 mm depending on maxDelta
152   maxDelta *= 2;
153   var precision = 1 / 10.;
154   if (maxDelta > 100) {
155     precision = 100;
156   } else if (maxDelta > 10) {
157     precision = 10;
158   } else if (maxDelta > 5) {
159     precision = 5;
160   } else if (maxDelta > 1) {
161     precision = 1;
162   } else if  (maxDelta > 0.5) {
163     precision = 0.5;
164   } 
165   var magnetizedLength = Math.round(length / precision) * precision;
166   if (magnetizedLength === 0 && length > 0) {
167     return length;
168   } else {
169     return magnetizedLength;
170   }
171 }
172 
173 /**
174  * Returns the value close to the given length under magnetism for inch units.
175  * @param {number} length
176  * @param {number} maxDelta
177  * @private
178  */
179 LengthUnit.getMagnetizedInchLength = function(length, maxDelta) {
180   // Use a maximum precision of 1/8 inch depending on maxDelta
181   maxDelta = LengthUnit.centimeterToInch(maxDelta) * 2;
182   var precision = 1 / 8.;
183   if (maxDelta > 6) {
184     precision = 6;
185   } else if (maxDelta > 3) {
186     precision = 3;
187   } else if (maxDelta > 1) {
188     precision = 1;
189   } else if  (maxDelta > 0.5) {
190     precision = 0.5;
191   } else if  (maxDelta > 0.25) {
192     precision = 0.25;
193   }
194   var magnetizedLength = LengthUnit.inchToCentimeter(Math.round(LengthUnit.centimeterToInch(length) / precision) * precision);
195   if (magnetizedLength === 0 && length > 0) {
196     return length;
197   } else {
198     return magnetizedLength;
199   }
200 }
201 
202 /**
203  * Increases the index of <code>fieldPosition</code> to skip white spaces.
204  * @param {string} text
205  * @param {ParsePosition} fieldPosition
206  * @private 
207  */
208 LengthUnit.skipWhiteSpaces = function(text, fieldPosition) {
209   while (fieldPosition.getIndex() < text.length
210       && /\s/.test(text.charAt(fieldPosition.getIndex()))) {
211     fieldPosition.setIndex(fieldPosition.getIndex() + 1);
212   }
213 }
214 
215 /**
216  * @param {number} length
217  * Returns the <code>length</code> given in centimeters converted to inches.
218  */
219 LengthUnit.centimeterToInch = function(length) {
220   return length / 2.54;
221 }
222 
223 /**
224  * @param {number} length
225  * Returns the <code>length</code> given in centimeters converted to feet.
226  */
227 LengthUnit.centimeterToFoot = function(length) {
228   return length / 2.54 / 12;
229 }
230 
231 /**
232  * @param {number} length
233  * Returns the <code>length</code> given in inches converted to centimeters.
234  */
235 LengthUnit.inchToCentimeter = function(length) {
236   return length * 2.54;
237 }
238 
239 /**
240  * @param {number} length
241  * Returns the <code>length</code> given in feet converted to centimeters.
242  */
243 LengthUnit.footToCentimeter = function(length) {
244   return length * 2.54 * 12;
245 }
246 
247 /**
248  * Returns the enum name of the given unit.
249  * <b>WARNING</b> enum name (CENTIMETERS, MILLIMETERS, ..) is different from unit's name (cm, mm, ..)
250  * @param {LengthUnit} unit
251  */
252 LengthUnit.nameOf = function(unit) {
253   if (unit != null) {
254     var unitEnumNames = Object.keys(LengthUnit);
255     for (var i = 0; i < unitEnumNames.length; i++) {
256       var unitEnumName = unitEnumNames[i];
257       if (LengthUnit[unitEnumName] == unit) {
258         return unitEnumName;
259       }
260     }
261   }
262   return null;
263 }
264 
265 /**
266  * Gets a LengthUnit by its enum name
267  * <br/>
268  * <b>WARNING</b> enum name (CENTIMETER, MILLIMETER, ..) is different from unit's name (cm, mm, ..)
269  * @param {string} unitEnumName
270  * @return {LengthUnit}
271  */
272 LengthUnit.valueOf = function(unitEnumName) {
273   return LengthUnit[unitEnumName];
274 }
275 
276 /**
277  * Millimeter unit.
278  */
279 LengthUnit.MILLIMETER = {
280     formatLocale : null
281 };
282 
283 LengthUnit.MILLIMETER.name = function() {
284   return LengthUnit.nameOf(this);
285 }
286   
287 LengthUnit.MILLIMETER.getFormatWithUnit = function() {
288   this.checkLocaleChange();
289   return this.lengthFormatWithUnit;
290 }
291 
292 LengthUnit.MILLIMETER.getAreaFormatWithUnit = function() {
293   this.checkLocaleChange();
294   return this.areaFormatWithUnit;
295 }
296 
297 LengthUnit.MILLIMETER.getFormat = function() {
298   this.checkLocaleChange();
299   return this.lengthFormat;
300 }
301 
302 LengthUnit.MILLIMETER.getName = function() {
303   this.checkLocaleChange();
304   return this.unitName; // Use unitName rather than name field to avoid clashes with name() method
305 }
306 
307 LengthUnit.MILLIMETER.checkLocaleChange = function() {
308   // Instantiate formats if locale changed
309   if (Locale.getDefault() != this.formatLocale) {
310     this.formatLocale = Locale.getDefault();  
311     this.unitName = LengthUnit.getLocalizedUnit("millimeterUnit");
312     this.lengthFormatWithUnit = new MeterFamilyFormat("0", 10, this.unitName);
313     this.lengthFormat = new MeterFamilyFormat("0", 10);
314     var squareMeterUnit = LengthUnit.getLocalizedUnit("squareMeterUnit");
315     this.areaFormatWithUnit = new SquareMeterAreaFormatWithUnit(squareMeterUnit);
316   }
317 }
318 
319 LengthUnit.MILLIMETER.getMagnetizedLength = function(length, maxDelta) {
320   return LengthUnit.getMagnetizedMeterLength(length, maxDelta);
321 }
322 
323 LengthUnit.MILLIMETER.getMinimumLength = function() {
324   return 0.1;
325 }
326 
327 LengthUnit.MILLIMETER.getMaximumLength = function() {
328   return 100000.;
329 }
330 
331 LengthUnit.MILLIMETER.getMaximumElevation = function() {
332   return this.getMaximumLength() / 10;
333 }
334 
335 /**
336  * @since 6.6
337  */
338 LengthUnit.MILLIMETER.getStepSize = function() {
339   return 0.5;
340 }
341 
342 LengthUnit.MILLIMETER.centimeterToUnit = function(length) {
343   return length * 10.;
344 }
345 
346 LengthUnit.MILLIMETER.unitToCentimeter = function(length) {
347   return length / 10.;
348 }
349  
350 /**
351  * @since 7.0
352  */
353 LengthUnit.MILLIMETER.isMetric = function() {
354   return true;
355 }
356 
357 
358 /**
359  * Centimeter unit.
360  */
361 LengthUnit.CENTIMETER = {
362     formatLocale : null
363 };
364 
365 LengthUnit.CENTIMETER.name = function() {
366   return LengthUnit.nameOf(this);
367 }
368 
369 LengthUnit.CENTIMETER.getFormatWithUnit = function() {
370   this.checkLocaleChange();
371   return this.lengthFormatWithUnit;
372 }
373 
374 LengthUnit.CENTIMETER.getAreaFormatWithUnit = function() {
375   this.checkLocaleChange();
376   return this.areaFormatWithUnit;
377 }
378 
379 LengthUnit.CENTIMETER.getFormat = function() {
380   this.checkLocaleChange();
381   return this.lengthFormat;
382 }
383   
384 LengthUnit.CENTIMETER.getName = function() {
385   this.checkLocaleChange();
386   return this.unitName; // Use unitName rather than name field to avoid clashes with name() method
387 }
388   
389 LengthUnit.CENTIMETER.checkLocaleChange = function() {
390   // Instantiate formats if locale changed
391   if (Locale.getDefault() != this.formatLocale) {
392     this.formatLocale = Locale.getDefault();  
393     this.unitName = LengthUnit.getLocalizedUnit("centimeterUnit");
394     this.lengthFormatWithUnit = new MeterFamilyFormat("0.#", 1, this.unitName);
395     this.lengthFormat = new MeterFamilyFormat("0.#", 1);
396     var squareMeterUnit = LengthUnit.getLocalizedUnit("squareMeterUnit");
397     this.areaFormatWithUnit = new SquareMeterAreaFormatWithUnit(squareMeterUnit);
398   }
399 }
400 
401 LengthUnit.CENTIMETER.getMagnetizedLength = function(length, maxDelta) {
402   return LengthUnit.getMagnetizedMeterLength(length, maxDelta);
403 }
404 
405 LengthUnit.CENTIMETER.getMinimumLength = function() {
406   return 0.1;
407 }
408   
409 LengthUnit.CENTIMETER.getMaximumLength = function() {
410   return 100000.;
411 }
412 
413 LengthUnit.CENTIMETER.getMaximumElevation = function() {
414   return this.getMaximumLength() / 10;
415 }
416 
417 /**
418  * Returns the preferred step size in centimeter used to increment / decrement values of this unit.
419  * @since 6.6
420  */
421 LengthUnit.CENTIMETER.getStepSize = function() {
422   return 0.5;
423 }
424 
425 LengthUnit.CENTIMETER.centimeterToUnit = function(length) {
426   return length;
427 }
428 
429 LengthUnit.CENTIMETER.unitToCentimeter = function(length) {
430   return length;
431 } 
432 
433 /**
434  * @since 7.0
435  */
436 LengthUnit.CENTIMETER.isMetric = function() {
437   return true;
438 }
439 
440 
441 /**
442  * Meter unit.
443  */
444 LengthUnit.METER = {
445     formatLocale : null
446 };
447 
448 LengthUnit.METER.name = function() {
449   return LengthUnit.nameOf(this);
450 }
451 
452 LengthUnit.METER.getFormatWithUnit = function() {
453   this.checkLocaleChange();
454   return this.lengthFormatWithUnit;
455 }
456 
457 LengthUnit.METER.getAreaFormatWithUnit = function() {
458   this.checkLocaleChange();
459   return this.areaFormatWithUnit;
460 }
461 
462 LengthUnit.METER.getFormat = function() {
463   this.checkLocaleChange();
464   return this.lengthFormat;
465 }
466 
467 LengthUnit.METER.getName = function() {
468   this.checkLocaleChange();
469   return this.unitName; // Use unitName rather than name field to avoid clashes with name() method
470 }
471 
472 LengthUnit.METER.checkLocaleChange = function() {
473   // Instantiate formats if locale changed
474   if (Locale.getDefault() != this.formatLocale) {
475     this.formatLocale = Locale.getDefault();
476     this.unitName = LengthUnit.getLocalizedUnit("meterUnit");
477     this.lengthFormatWithUnit = new MeterFamilyFormat("0.00#", 0.01, this.unitName);
478     this.lengthFormat = new MeterFamilyFormat("0.00#", 0.01);
479     var squareMeterUnit = LengthUnit.getLocalizedUnit("squareMeterUnit");
480     this.areaFormatWithUnit = new SquareMeterAreaFormatWithUnit(squareMeterUnit);
481   }
482 }
483 
484 LengthUnit.METER.getMagnetizedLength = function(length, maxDelta) {
485   return LengthUnit.getMagnetizedMeterLength(length, maxDelta);
486 }
487 
488 LengthUnit.METER.getMinimumLength = function() {
489   return 0.1;
490 }
491 
492 LengthUnit.METER.getMaximumLength = function() {
493   return 100000.;
494 }
495 
496 LengthUnit.METER.getMaximumElevation = function() {
497   return this.getMaximumLength() / 10;
498 }
499 
500 /**
501  * @since 6.6
502  */
503 LengthUnit.METER.getStepSize = function() {
504   return 0.5;
505 }
506 
507 LengthUnit.METER.centimeterToUnit = function(length) {
508   return length / 100;
509 }
510 
511 LengthUnit.METER.unitToCentimeter = function(length) {
512   return length * 100;
513 }
514 
515 /**
516  * @since 7.0
517  */
518 LengthUnit.METER.isMetric = function() {
519   return true;
520 }
521 
522 
523 /**
524  * Foot/Inch unit followed by fraction.
525  */
526 LengthUnit.INCH = {
527     formatLocale : null
528 };
529 
530 LengthUnit.INCH.name = function() {
531   return LengthUnit.nameOf(this);
532 }
533 
534 LengthUnit.INCH.getFormatWithUnit = function() {
535   this.checkLocaleChange();
536   return this.lengthFormat;
537 }
538 
539 LengthUnit.INCH.getFormat = function() {
540   return this.getFormatWithUnit();
541 }
542 
543 LengthUnit.INCH.getAreaFormatWithUnit = function() {
544   this.checkLocaleChange();
545   return this.areaFormatWithUnit;
546 }
547 
548 LengthUnit.INCH.getName = function() {
549   this.checkLocaleChange();
550   return this.unitName; // Use unitName rather than name field to avoid clashes with name() method
551 }
552 
553 LengthUnit.INCH.checkLocaleChange = function() {
554   // Instantiate format if locale changed
555   if (Locale.getDefault() != this.formatLocale) {
556     this.formatLocale = Locale.getDefault();
557     this.unitName = LengthUnit.getLocalizedUnit("inchUnit");
558     var footInchSeparator = "";
559     this.lengthFormat = new InchFractionFormat(true, footInchSeparator);
560     var squareFootUnit = LengthUnit.getLocalizedUnit("squareFootUnit");
561     this.areaFormatWithUnit = new SquareFootAreaFormatWithUnit("0", squareFootUnit);
562   }
563 }
564 
565 LengthUnit.INCH.getMagnetizedLength = function(length, maxDelta) {
566   return LengthUnit.getMagnetizedInchLength(length, maxDelta);
567 }
568 
569 LengthUnit.INCH.getMinimumLength = function() {        
570   return LengthUnit.inchToCentimeter(0.125);
571 }
572 
573 LengthUnit.INCH.getMaximumLength = function() {
574   return LengthUnit.footToCentimeter(3280);
575 }
576 
577 LengthUnit.INCH.getMaximumElevation = function() {
578   return this.getMaximumLength() / 10;
579 }
580 
581 /**
582  * @since 6.6
583  */
584 LengthUnit.INCH.getStepSize = function() {
585   return LengthUnit.inchToCentimeter(0.125);
586 }
587 
588 LengthUnit.INCH.centimeterToUnit = function(length) {
589   return LengthUnit.centimeterToInch(length);
590 }
591 
592 LengthUnit.INCH.unitToCentimeter = function(length) {
593   return LengthUnit.inchToCentimeter(length);
594 }
595 
596 /**
597  * @since 7.0
598  */
599 LengthUnit.INCH.isMetric = function() {
600   return false;
601 }
602 
603 
604 /**
605  * Inch unit followed by fraction.
606  * @since 7.0
607  */
608 LengthUnit.INCH_FRACTION = {
609     formatLocale : null
610 };
611 
612 LengthUnit.INCH_FRACTION.name = function() {
613   return LengthUnit.nameOf(this);
614 }
615 
616 LengthUnit.INCH_FRACTION.getFormatWithUnit = function() {
617   this.checkLocaleChange();
618   return this.lengthFormat;
619 }
620 
621 LengthUnit.INCH_FRACTION.getFormat = function() {
622   return this.getFormatWithUnit();
623 }
624 
625 LengthUnit.INCH_FRACTION.getAreaFormatWithUnit = function() {
626   this.checkLocaleChange();
627   return this.areaFormatWithUnit;
628 }
629 
630 LengthUnit.INCH_FRACTION.getName = function() {
631   this.checkLocaleChange();
632   return this.unitName; // Use unitName rather than name field to avoid clashes with name() method
633 }
634 
635 LengthUnit.INCH_FRACTION.checkLocaleChange = function() {
636   // Instantiate format if locale changed
637   if (Locale.getDefault() != this.formatLocale) {
638     this.formatLocale = Locale.getDefault();
639     this.unitName = LengthUnit.getLocalizedUnit("inchUnit");
640     var footInchSeparator = "";
641     this.lengthFormat = new InchFractionFormat(false, footInchSeparator);
642     var squareFootUnit = LengthUnit.getLocalizedUnit("squareFootUnit");
643     this.areaFormatWithUnit = new SquareFootAreaFormatWithUnit("0", squareFootUnit);
644   }
645 }
646 
647 LengthUnit.INCH_FRACTION.getMagnetizedLength = function(length, maxDelta) {
648   return LengthUnit.getMagnetizedInchLength(length, maxDelta);
649 }
650 
651 LengthUnit.INCH_FRACTION.getMinimumLength = function() {        
652   return LengthUnit.inchToCentimeter(0.125);
653 }
654 
655 LengthUnit.INCH_FRACTION.getMaximumLength = function() {
656   return LengthUnit.footToCentimeter(3280);
657 }
658 
659 LengthUnit.INCH_FRACTION.getMaximumElevation = function() {
660   return this.getMaximumLength() / 10;
661 }
662 
663 LengthUnit.INCH_FRACTION.getStepSize = function() {
664   return LengthUnit.inchToCentimeter(0.125);
665 }
666 
667 LengthUnit.INCH_FRACTION.centimeterToUnit = function(length) {
668   return LengthUnit.centimeterToInch(length);
669 }
670 
671 LengthUnit.INCH_FRACTION.unitToCentimeter = function(length) {
672   return LengthUnit.inchToCentimeter(length);
673 }
674 
675 /**
676  * @since 7.0
677  */
678 LengthUnit.INCH_FRACTION.isMetric = function() {
679   return false;
680 }
681 
682 
683 /**
684  * Inch unit with decimals.
685  */
686 LengthUnit.INCH_DECIMALS = {
687     formatLocale : null
688 };
689 
690 LengthUnit.INCH_DECIMALS.name = function() {
691   return LengthUnit.nameOf(this);
692 }
693 
694 LengthUnit.INCH_DECIMALS.getFormatWithUnit = function() {
695   this.checkLocaleChange();
696   return this.lengthFormatWithUnit;
697 }
698 
699 LengthUnit.INCH_DECIMALS.getFormat = function() {
700   this.checkLocaleChange();
701   return this.lengthFormat;
702 }
703 
704 LengthUnit.INCH_DECIMALS.getAreaFormatWithUnit = function() {
705   this.checkLocaleChange();
706   return this.areaFormatWithUnit;
707 }
708 
709 LengthUnit.INCH_DECIMALS.getName = function() {
710   this.checkLocaleChange();
711   return this.unitName; // Use unitName rather than name field to avoid clashes with name() method
712 }
713   
714 LengthUnit.INCH_DECIMALS.checkLocaleChange = function() {  
715   // Instantiate format if locale changed
716   if (Locale.getDefault() != this.formatLocale) {
717     this.formatLocale = Locale.getDefault();
718     this.unitName = LengthUnit.getLocalizedUnit("inchUnit");
719     this.lengthFormat = new InchDecimalsFormat("0.###");
720     this.lengthFormatWithUnit = new InchDecimalsFormat("0.###", "\""); 
721     var squareFootUnit = LengthUnit.getLocalizedUnit("squareFootUnit");
722     this.areaFormatWithUnit = new SquareFootAreaFormatWithUnit("0.##", squareFootUnit);
723   }
724 }
725   
726 LengthUnit.INCH_DECIMALS.getMagnetizedLength = function(length, maxDelta) {
727   return LengthUnit.getMagnetizedInchLength(length, maxDelta);
728 }
729 
730 LengthUnit.INCH_DECIMALS.getMinimumLength = function() {        
731   return LengthUnit.inchToCentimeter(0.125);
732 }
733 
734 LengthUnit.INCH_DECIMALS.getMaximumLength = function() {
735   return LengthUnit.footToCentimeter(3280);
736 }
737 
738 LengthUnit.INCH_DECIMALS.getMaximumElevation = function() {
739   return this.getMaximumLength() / 10;
740 }
741 
742 /**
743  * @since 6.6
744  */
745 LengthUnit.INCH_DECIMALS.getStepSize = function() {
746   return LengthUnit.inchToCentimeter(0.125);
747 }
748 
749 LengthUnit.INCH_DECIMALS.centimeterToUnit = function(length) {
750   return LengthUnit.centimeterToInch(length);
751 }
752 
753 LengthUnit.INCH_DECIMALS.unitToCentimeter = function(length) {
754   return LengthUnit.inchToCentimeter(length);
755 }
756 
757 /**
758  * @since 7.0
759  */
760 LengthUnit.INCH_DECIMALS.isMetric = function() {
761   return false;
762 }
763 
764 
765 /**
766  * Inch unit with decimals.
767  * @since 7.0
768  */
769 LengthUnit.FOOT_DECIMALS = {
770     formatLocale : null
771 };
772 
773 LengthUnit.FOOT_DECIMALS.name = function() {
774   return LengthUnit.nameOf(this);
775 }
776 
777 LengthUnit.FOOT_DECIMALS.getFormatWithUnit = function() {
778   this.checkLocaleChange();
779   return this.lengthFormatWithUnit;
780 }
781 
782 LengthUnit.FOOT_DECIMALS.getFormat = function() {
783   this.checkLocaleChange();
784   return this.lengthFormat;
785 }
786 
787 LengthUnit.FOOT_DECIMALS.getAreaFormatWithUnit = function() {
788   this.checkLocaleChange();
789   return this.areaFormatWithUnit;
790 }
791 
792 LengthUnit.FOOT_DECIMALS.getName = function() {
793   this.checkLocaleChange();
794   return this.unitName; // Use unitName rather than name field to avoid clashes with name() method
795 }
796   
797 LengthUnit.FOOT_DECIMALS.checkLocaleChange = function() {  
798   // Instantiate format if locale changed
799   if (Locale.getDefault() != this.formatLocale) {
800     this.formatLocale = Locale.getDefault();
801     this.unitName = LengthUnit.getLocalizedUnit("footUnit");
802     this.lengthFormat = new FootDecimalsFormat("0.###");
803     this.lengthFormatWithUnit = new FootDecimalsFormat("0.###", "\'"); 
804     var squareFootUnit = LengthUnit.getLocalizedUnit("squareFootUnit");
805     this.areaFormatWithUnit = new SquareFootAreaFormatWithUnit("0.##", squareFootUnit);
806   }
807 }
808   
809 LengthUnit.FOOT_DECIMALS.getMagnetizedLength = function(length, maxDelta) {
810   return LengthUnit.getMagnetizedInchLength(length, maxDelta);
811 }
812 
813 LengthUnit.FOOT_DECIMALS.getMinimumLength = function() {        
814   return LengthUnit.inchToCentimeter(0.125);
815 }
816 
817 LengthUnit.FOOT_DECIMALS.getMaximumLength = function() {
818   return LengthUnit.footToCentimeter(3280); 
819 }
820 
821 LengthUnit.FOOT_DECIMALS.getMaximumElevation = function() {
822   return this.getMaximumLength() / 10;
823 }
824 
825 LengthUnit.FOOT_DECIMALS.getStepSize = function() {
826   return LengthUnit.inchToCentimeter(0.125);
827 }
828 
829 LengthUnit.FOOT_DECIMALS.centimeterToUnit = function(length) {
830   return LengthUnit.centimeterToFoot(length);
831 }
832 
833 LengthUnit.FOOT_DECIMALS.unitToCentimeter = function(length) {
834   return LengthUnit.footToCentimeter(length);
835 }
836 
837 LengthUnit.FOOT_DECIMALS.isMetric = function() {
838   return false;
839 }
840 
841 
842 // Specific format classes for lengths
843 
844 /** 
845  * @constructor
846  * @extends DecimalFormat
847  * @private 
848  */
849 function MeterFamilyFormat(pattern, unitMultiplier, unit) {
850   DecimalFormat.call(this, pattern);
851   this.setGroupingUsed(true);
852   this.unitMultiplier = unitMultiplier;
853   this.unit = unit;
854 }
855 MeterFamilyFormat.prototype = Object.create(DecimalFormat.prototype);
856 MeterFamilyFormat.prototype.constructor = MeterFamilyFormat;
857 
858 MeterFamilyFormat.prototype.format = function(number) {
859   var formattedNumber = DecimalFormat.prototype.format.call(this, number * this.unitMultiplier); 
860   return formattedNumber + (this.unit ? " " + this.unit : "");
861 }
862 
863 MeterFamilyFormat.prototype.parse = function(text, parsePosition) {
864   var number = DecimalFormat.prototype.parse.call(this, text, parsePosition);
865   if (number === null) {
866     return null;
867   } else {
868     return number / this.unitMultiplier;
869   }
870 }
871 
872 /** 
873  * @constructor
874  * @extends DecimalFormat
875  * @private 
876  */
877 function SquareMeterAreaFormatWithUnit(squareMeterUnit) {
878   DecimalFormat.call(this, "0.##");
879   this.setGroupingUsed(true);
880   this.squareMeterUnit = squareMeterUnit;
881 }
882 SquareMeterAreaFormatWithUnit.prototype = Object.create(DecimalFormat.prototype);
883 SquareMeterAreaFormatWithUnit.prototype.constructor = SquareMeterAreaFormatWithUnit;
884 
885 SquareMeterAreaFormatWithUnit.prototype.format = function(number) {
886   var formattedNumber = DecimalFormat.prototype.format.call(this, number / 10000); 
887   return formattedNumber + (this.squareMeterUnit ? " " + this.squareMeterUnit : "");
888 }
889 
890 
891 /** 
892  * A decimal format for inch lengths with fraction.
893  * @param {boolean} footInch
894  * @param {string} footInchSeparator
895  * @constructor
896  * @extends DecimalFormat
897  * @private 
898  */
899 function InchFractionFormat(footInch, footInchSeparator) {
900   DecimalFormat.call(this, "0.###");
901   this.setGroupingUsed(true);
902   this.footInch = footInch;
903   this.footInchSeparator = footInchSeparator;
904 }
905 InchFractionFormat.prototype = Object.create(DecimalFormat.prototype);
906 InchFractionFormat.prototype.constructor = InchFractionFormat;
907 
908 InchFractionFormat.INCH_FRACTION_CHARACTERS = ['\u215b',   // 1/8
909                                                '\u00bc',   // 1/4  
910                                                '\u215c',   // 3/8
911                                                '\u00bd',   // 1/2
912                                                '\u215d',   // 5/8
913                                                '\u00be',   // 3/4
914                                                '\u215e'];  // 7/8        
915 InchFractionFormat.INCH_FRACTION_STRINGS = ["1/8",
916                                             "1/4",  
917                                             "3/8",
918                                             "1/2",
919                                             "5/8",
920                                             "3/4",
921                                             "7/8"];     
922 
923 InchFractionFormat.prototype.format = function(number) {
924   var absoluteValue = Math.abs(number);
925   var feet = Math.floor(LengthUnit.centimeterToFoot(absoluteValue));              
926   var remainingInches = LengthUnit.centimeterToInch(absoluteValue - LengthUnit.footToCentimeter(feet));
927   if (remainingInches >= 11.9375) {
928     feet++;
929     remainingInches -= 12;
930   }
931   var result = number >= 0 ? "" : "-";
932   // Format remaining inches only if it's larger that 0.0005
933   var feetString = DecimalFormat.prototype.format.call(this, feet);
934   if (remainingInches >= 0.0005) {
935     // Try to format decimals with 1/8, 1/4, 1/2 fractions first
936     var integerPart = Math.floor(remainingInches);
937     var fractionPart = remainingInches - integerPart;
938     var eighth = Math.round(fractionPart * 8); 
939     if (this.footInch) {
940       result += feetString;
941       if (eighth === 0 || eighth === 8) {
942         result += "'";
943         result += Math.round(remainingInches * 8) / 8;
944       } else {
945         result += "'";
946         result += integerPart;
947         result += InchFractionFormat.INCH_FRACTION_CHARACTERS[eighth - 1];
948       }
949     } else {
950       if (eighth === 0 || eighth === 8) {
951         result += feet * 12 + Math.round(remainingInches * 8) / 8;
952       } else {
953         result += feet * 12 + integerPart;
954         result += InchFractionFormat.INCH_FRACTION_CHARACTERS[eighth - 1];
955       }
956     }
957     result += "\"";
958   } else {
959     if (this.footInch) {
960       result += feetString;
961       result += "'";
962     } else {
963       result += DecimalFormat.prototype.format.call(this, feet * 12);
964       result += "\"";
965     }
966   }
967   return result;
968 }
969 
970 InchFractionFormat.prototype.parse = function(text, parsePosition) {
971   var value = 0;
972   var numberPosition = new ParsePosition(parsePosition.getIndex());
973   LengthUnit.skipWhiteSpaces(text, numberPosition);
974   var footNumberFormat = NumberFormat.getIntegerInstance();
975   // Parse feet
976   var quoteIndex = text.indexOf('\'', parsePosition.getIndex());
977   var negative = numberPosition.getIndex() < text.length  
978       && text.charAt(numberPosition.getIndex()) === '-';
979   var footValue = false;
980   if (quoteIndex !== -1) {
981     var feet = footNumberFormat.parse(text, numberPosition);
982     if (feet === null) {
983       parsePosition.setErrorIndex(numberPosition.getErrorIndex());
984       return null;
985     }
986     LengthUnit.skipWhiteSpaces(text, numberPosition);
987     if (numberPosition.getIndex() === quoteIndex) {
988       value = LengthUnit.footToCentimeter(feet);
989       footValue = true;
990       numberPosition = new ParsePosition(quoteIndex + 1);
991       LengthUnit.skipWhiteSpaces(text, numberPosition);
992       // Test optional foot inch separator
993       if (numberPosition.getIndex() < text.length
994           && this.footInchSeparator.indexOf(text.charAt(numberPosition.getIndex())) >= 0) {
995         numberPosition.setIndex(numberPosition.getIndex() + 1);
996         LengthUnit.skipWhiteSpaces(text, numberPosition);
997       }
998       if (numberPosition.getIndex() === text.length) {
999         parsePosition.setIndex(text.length);
1000         return value;
1001       }
1002     } else {
1003       if (this.decimalSeparator === text.charAt(numberPosition.getIndex())) {
1004         var decimalNumberPosition = new ParsePosition(parsePosition.getIndex());
1005         if (DecimalFormat.prototype.parse.call(this, text, decimalNumberPosition) !== null
1006             && decimalNumberPosition.getIndex() === quoteIndex) {
1007           // Don't allow a decimal number in front of a quote
1008           parsePosition.setErrorIndex(numberPosition.getErrorIndex());
1009           return null;
1010         }
1011       }
1012       // Try to parse beginning as inches
1013       numberPosition.setIndex(parsePosition.getIndex());
1014     }
1015   }
1016     
1017   // Parse inches
1018   var inches = DecimalFormat.prototype.parse.call(this, text, numberPosition);
1019   if (inches === null) {
1020     if (footValue) {
1021       parsePosition.setIndex(numberPosition.getIndex());
1022       return value;
1023     } else {
1024       parsePosition.setErrorIndex(numberPosition.getErrorIndex());
1025       return null;
1026     }
1027   }
1028   if (negative) {
1029     if (quoteIndex === -1) {
1030       value = LengthUnit.inchToCentimeter(inches);
1031     } else {
1032       value -= LengthUnit.inchToCentimeter(inches);
1033     }
1034   } else {
1035     value += LengthUnit.inchToCentimeter(inches);
1036   }
1037   // Parse fraction
1038   LengthUnit.skipWhiteSpaces(text, numberPosition);
1039   if (numberPosition.getIndex() === text.length) {
1040     parsePosition.setIndex(text.length);
1041     return value;
1042   }
1043   if (text.charAt(numberPosition.getIndex()) === '\"') {
1044     parsePosition.setIndex(numberPosition.getIndex() + 1);
1045     return value;
1046   }
1047 
1048   var fractionChar = text.charAt(numberPosition.getIndex());    
1049   var fractionString = text.length - numberPosition.getIndex() >= 3 
1050       ? text.substring(numberPosition.getIndex(), numberPosition.getIndex() + 3)
1051       : null;
1052   for (var i = 0; i < InchFractionFormat.INCH_FRACTION_CHARACTERS.length; i++) {
1053     if (InchFractionFormat.INCH_FRACTION_CHARACTERS [i] === fractionChar
1054         || InchFractionFormat.INCH_FRACTION_STRINGS [i] == fractionString) {
1055       // Check no decimal fraction was specified
1056       var lastDecimalSeparatorIndex = text.lastIndexOf(this.decimalSeparator, 
1057           numberPosition.getIndex() - 1);
1058       if (lastDecimalSeparatorIndex > quoteIndex) {
1059         return null;
1060       } else {
1061         if (negative) {
1062           value -= LengthUnit.inchToCentimeter((i + 1) / 8);
1063         } else {
1064           value += LengthUnit.inchToCentimeter((i + 1) / 8);
1065         }
1066         parsePosition.setIndex(numberPosition.getIndex() 
1067             + (InchFractionFormat.INCH_FRACTION_CHARACTERS [i] === fractionChar ? 1 : 3));
1068         LengthUnit.skipWhiteSpaces(text, parsePosition);
1069         if (parsePosition.getIndex() < text.length
1070             && text.charAt(parsePosition.getIndex()) === '\"') {
1071           parsePosition.setIndex(parsePosition.getIndex() + 1);
1072         }
1073         return value;
1074       }
1075     }
1076   }
1077   
1078   parsePosition.setIndex(numberPosition.getIndex());
1079   return value;
1080 }
1081 
1082 /** 
1083  * @constructor
1084  * @extends DecimalFormat
1085  * @private 
1086  */
1087 function SquareFootAreaFormatWithUnit(pattern, unit) {
1088   DecimalFormat.call(this, pattern);
1089   this.setGroupingUsed(true);
1090   this.unit = unit;
1091 }
1092 SquareFootAreaFormatWithUnit.prototype = Object.create(DecimalFormat.prototype);
1093 SquareFootAreaFormatWithUnit.prototype.constructor = SquareFootAreaFormatWithUnit;
1094 
1095 SquareFootAreaFormatWithUnit.prototype.format = function(number) {
1096   var formattedNumber = DecimalFormat.prototype.format.call(this, number / 929.0304); 
1097   return formattedNumber + (this.unit ? " " + this.unit : "");
1098 }
1099 
1100 /** 
1101  * @constructor
1102  * @extends DecimalFormat
1103  * @private 
1104  */
1105 function InchDecimalsFormat(pattern, unit) {
1106   DecimalFormat.call(this, pattern);
1107   this.setGroupingUsed(true);
1108   this.unit = unit;
1109 }
1110 InchDecimalsFormat.prototype = Object.create(DecimalFormat.prototype);
1111 InchDecimalsFormat.prototype.constructor = InchDecimalsFormat;
1112 
1113 InchDecimalsFormat.prototype.format = function(number) {
1114   var formattedNumber = DecimalFormat.prototype.format.call(this, LengthUnit.centimeterToInch(number));
1115   return formattedNumber + (this.unit ? this.unit : "");
1116 }
1117 
1118 InchDecimalsFormat.prototype.parse = function(text, parsePosition) {
1119   var numberPosition = new ParsePosition(parsePosition.getIndex());
1120   LengthUnit.skipWhiteSpaces(text, numberPosition);
1121   // Parse inches
1122   var inches = DecimalFormat.prototype.parse.call(this, text, numberPosition);
1123   if (inches === null) {
1124     parsePosition.setErrorIndex(numberPosition.getErrorIndex());
1125     return null;
1126   }
1127   var value = LengthUnit.inchToCentimeter(inches);
1128   // Parse "
1129   LengthUnit.skipWhiteSpaces(text, numberPosition);
1130   if (numberPosition.getIndex() < text.length 
1131       && text.charAt(numberPosition.getIndex()) === '\"') {
1132     parsePosition.setIndex(numberPosition.getIndex() + 1);
1133   } else {
1134     parsePosition.setIndex(numberPosition.getIndex());
1135   }
1136   return value;
1137 }
1138 
1139 /** 
1140  * @constructor
1141  * @extends DecimalFormat
1142  * @private 
1143  */
1144 function FootDecimalsFormat(pattern, unit) {
1145   DecimalFormat.call(this, pattern);
1146   this.setGroupingUsed(true);
1147   this.unit = unit;
1148 }
1149 FootDecimalsFormat.prototype = Object.create(DecimalFormat.prototype);
1150 FootDecimalsFormat.prototype.constructor = FootDecimalsFormat;
1151 
1152 FootDecimalsFormat.prototype.format = function(number) {
1153   var formattedNumber = DecimalFormat.prototype.format.call(this, LengthUnit.centimeterToFoot(number));
1154   return formattedNumber + (this.unit ? this.unit : "");
1155 }
1156 
1157 FootDecimalsFormat.prototype.parse = function(text, parsePosition) {
1158   var numberPosition = new ParsePosition(parsePosition.getIndex());
1159   LengthUnit.skipWhiteSpaces(text, numberPosition);
1160   // Parse feet
1161   var feet = DecimalFormat.prototype.parse.call(this, text, numberPosition);
1162   if (feet === null) {
1163     parsePosition.setErrorIndex(numberPosition.getErrorIndex());
1164     return null;
1165   }
1166   var value = LengthUnit.footToCentimeter(feet);
1167   // Parse '
1168   LengthUnit.skipWhiteSpaces(text, numberPosition);
1169   if (numberPosition.getIndex() < text.length 
1170       && text.charAt(numberPosition.getIndex()) === '\'') {
1171     parsePosition.setIndex(numberPosition.getIndex() + 1);
1172   } else {
1173     parsePosition.setIndex(numberPosition.getIndex());
1174   }
1175   return value;
1176 }
1177