/*
* LengthUnit.java 22 nov. 2008
*
* Sweet Home 3D, Copyright (c) 2006-2008 Emmanuel PUYBARET / eTeks <info@eteks.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.eteks.sweethome3d.model;
import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.text.Format;
import java.text.MessageFormat;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.Locale;
import java.util.ResourceBundle;
/**
* Unit used for lengths.
*/
public enum LengthUnit {
/**
* Millimeter unit.
* @since 2.0
*/
MILLIMETER {
private Locale formatLocale;
private String name;
private DecimalFormat lengthFormatWithUnit;
private DecimalFormat lengthFormat;
private DecimalFormat areaFormatWithUnit;
@Override
public Format getFormatWithUnit() {
checkLocaleChange();
return this.lengthFormatWithUnit;
}
@Override
public Format getAreaFormatWithUnit() {
checkLocaleChange();
return this.areaFormatWithUnit;
}
@Override
public Format getFormat() {
checkLocaleChange();
return this.lengthFormat;
}
@Override
public String getName() {
checkLocaleChange();
return this.name;
}
private void checkLocaleChange() {
// Instantiate formats if locale changed
if (!Locale.getDefault().equals(this.formatLocale)) {
this.formatLocale = Locale.getDefault();
ResourceBundle resource = ResourceBundle.getBundle(LengthUnit.class.getName());
this.name = resource.getString("millimeterUnit");
this.lengthFormatWithUnit = new MeterFamilyFormat("#,##0 " + this.name, 10);
this.lengthFormat = new MeterFamilyFormat("#,##0", 10);
String squareMeterUnit = resource.getString("squareMeterUnit");
this.areaFormatWithUnit = new SquareMeterAreaFormatWithUnit(squareMeterUnit);
}
}
@Override
public float getMagnetizedLength(float length, float maxDelta) {
return getMagnetizedMeterLength(length, maxDelta);
}
@Override
public float getMinimumLength() {
return 0.1f;
}
@Override
public float getMaximumLength() {
return 100000f;
}
@Override
public float centimeterToUnit(float length) {
return length * 10;
}
@Override
public float unitToCentimeter(float length) {
return length / 10;
}
},
/**
* Centimeter unit.
*/
CENTIMETER {
private Locale formatLocale;
private String name;
private DecimalFormat lengthFormatWithUnit;
private DecimalFormat lengthFormat;
private DecimalFormat areaFormatWithUnit;
@Override
public Format getFormatWithUnit() {
checkLocaleChange();
return this.lengthFormatWithUnit;
}
@Override
public Format getAreaFormatWithUnit() {
checkLocaleChange();
return this.areaFormatWithUnit;
}
@Override
public Format getFormat() {
checkLocaleChange();
return this.lengthFormat;
}
@Override
public String getName() {
checkLocaleChange();
return this.name;
}
private void checkLocaleChange() {
// Instantiate formats if locale changed
if (!Locale.getDefault().equals(this.formatLocale)) {
this.formatLocale = Locale.getDefault();
ResourceBundle resource = ResourceBundle.getBundle(LengthUnit.class.getName());
this.name = resource.getString("centimeterUnit");
this.lengthFormatWithUnit = new MeterFamilyFormat("#,##0.# " + this.name, 1);
this.lengthFormat = new MeterFamilyFormat("#,##0.#", 1);
String squareMeterUnit = resource.getString("squareMeterUnit");
this.areaFormatWithUnit = new SquareMeterAreaFormatWithUnit(squareMeterUnit);
}
}
@Override
public float getMagnetizedLength(float length, float maxDelta) {
return getMagnetizedMeterLength(length, maxDelta);
}
@Override
public float getMinimumLength() {
return 0.1f;
}
@Override
public float getMaximumLength() {
return 100000f;
}
@Override
public float centimeterToUnit(float length) {
return length;
}
@Override
public float unitToCentimeter(float length) {
return length;
}
},
/**
* Meter unit.
* @since 2.0
*/
METER {
private Locale formatLocale;
private String name;
private DecimalFormat lengthFormatWithUnit;
private DecimalFormat lengthFormat;
private DecimalFormat areaFormatWithUnit;
@Override
public Format getFormatWithUnit() {
checkLocaleChange();
return this.lengthFormatWithUnit;
}
@Override
public Format getAreaFormatWithUnit() {
checkLocaleChange();
return this.areaFormatWithUnit;
}
@Override
public Format getFormat() {
checkLocaleChange();
return this.lengthFormat;
}
@Override
public String getName() {
checkLocaleChange();
return this.name;
}
private void checkLocaleChange() {
// Instantiate formats if locale changed
if (!Locale.getDefault().equals(this.formatLocale)) {
this.formatLocale = Locale.getDefault();
ResourceBundle resource = ResourceBundle.getBundle(LengthUnit.class.getName());
this.name = resource.getString("meterUnit");
this.lengthFormatWithUnit = new MeterFamilyFormat("#,##0.00# " + this.name, 0.01f);
this.lengthFormat = new MeterFamilyFormat("#,##0.00#", 0.01f);
String squareMeterUnit = resource.getString("squareMeterUnit");
this.areaFormatWithUnit = new SquareMeterAreaFormatWithUnit(squareMeterUnit);
}
}
@Override
public float getMagnetizedLength(float length, float maxDelta) {
return getMagnetizedMeterLength(length, maxDelta);
}
@Override
public float getMinimumLength() {
return 0.1f;
}
@Override
public float getMaximumLength() {
return 100000f;
}
@Override
public float centimeterToUnit(float length) {
return length / 100;
}
@Override
public float unitToCentimeter(float length) {
return length * 100;
}
},
/**
* Foot/Inch unit followed by fractions.
*/
INCH {
private Locale formatLocale;
private String name;
private DecimalFormat lengthFormat;
private DecimalFormat areaFormatWithUnit;
@Override
public Format getFormatWithUnit() {
checkLocaleChange();
return this.lengthFormat;
}
@Override
public Format getFormat() {
return getFormatWithUnit();
}
@Override
public Format getAreaFormatWithUnit() {
checkLocaleChange();
return this.areaFormatWithUnit;
}
@Override
public String getName() {
checkLocaleChange();
return this.name;
}
private void checkLocaleChange() {
// Instantiate format if locale changed
if (!Locale.getDefault().equals(this.formatLocale)) {
this.formatLocale = Locale.getDefault();
ResourceBundle resource = ResourceBundle.getBundle(LengthUnit.class.getName());
this.name = resource.getString("inchUnit");
// Create format for feet and inches
final MessageFormat footFormat = new MessageFormat(resource.getString("footFormat"));
final MessageFormat footInchFormat = new MessageFormat(resource.getString("footInchFormat"));
final MessageFormat footInchEighthFormat = new MessageFormat(resource.getString("footInchEighthFormat"));
final String footInchSeparator = resource.getString("footInchSeparator");
final NumberFormat footNumberFormat = NumberFormat.getIntegerInstance();
final NumberFormat inchNumberFormat = NumberFormat.getNumberInstance();
final char [] inchFractionCharacters = {'\u215b', // 1/8
'\u00bc', // 1/4
'\u215c', // 3/8
'\u00bd', // 1/2
'\u215d', // 5/8
'\u00be', // 3/4
'\u215e'}; // 7/8
this.lengthFormat = new DecimalFormat("0.000\"") {
@Override
public StringBuffer format(double number, StringBuffer result,
FieldPosition fieldPosition) {
double feet = Math.floor(centimeterToFoot((float)number));
float remainingInches = centimeterToInch((float)number - footToCentimeter((float)feet));
if (remainingInches >= 11.9375f) {
feet++;
remainingInches -= 12;
}
fieldPosition.setEndIndex(fieldPosition.getEndIndex() + 1);
// Format remaining inches only if it's larger that 0.0005
if (remainingInches >= 0.0005f) {
// Try to format decimals with 1/8, 1/4, 1/2 fractions first
int integerPart = (int)Math.floor(remainingInches);
float fractionPart = remainingInches - integerPart;
int eighth = Math.round(fractionPart * 8);
if (eighth == 0 || eighth == 8) {
footInchFormat.format(new Object [] {feet, Math.round(remainingInches * 8) / 8f}, result, fieldPosition);
} else {
footInchEighthFormat.format(new Object [] {feet, integerPart, inchFractionCharacters [eighth - 1]}, result, fieldPosition);
}
} else {
footFormat.format(new Object [] {feet}, result, fieldPosition);
}
return result;
}
@Override
public Number parse(String text, ParsePosition parsePosition) {
double value = 0;
ParsePosition numberPosition = new ParsePosition(parsePosition.getIndex());
skipWhiteSpaces(text, numberPosition);
// Parse feet
int quoteIndex = text.indexOf('\'', parsePosition.getIndex());
if (quoteIndex != -1) {
Number feet = footNumberFormat.parse(text, numberPosition);
if (feet == null) {
parsePosition.setErrorIndex(numberPosition.getErrorIndex());
return null;
}
skipWhiteSpaces(text, numberPosition);
if (numberPosition.getIndex() != quoteIndex) {
parsePosition.setErrorIndex(numberPosition.getIndex());
return null;
}
value = footToCentimeter(feet.intValue());
numberPosition = new ParsePosition(quoteIndex + 1);
skipWhiteSpaces(text, numberPosition);
// Test optional foot inch separator
if (numberPosition.getIndex() < text.length()
&& footInchSeparator.indexOf(text.charAt(numberPosition.getIndex())) >= 0) {
numberPosition.setIndex(numberPosition.getIndex() + 1);
skipWhiteSpaces(text, numberPosition);
}
if (numberPosition.getIndex() == text.length()) {
parsePosition.setIndex(text.length());
return value;
}
}
// Parse inches
Number inches = inchNumberFormat.parse(text, numberPosition);
if (inches == null) {
parsePosition.setErrorIndex(numberPosition.getErrorIndex());
return null;
}
value += inchToCentimeter(inches.floatValue());
// Parse fraction
skipWhiteSpaces(text, numberPosition);
if (numberPosition.getIndex() == text.length()) {
parsePosition.setIndex(text.length());
return value;
}
if (text.charAt(numberPosition.getIndex()) == '\"') {
parsePosition.setIndex(numberPosition.getIndex() + 1);
return value;
}
char fractionChar = text.charAt(numberPosition.getIndex());
for (int i = 0; i < inchFractionCharacters.length; i++) {
if (inchFractionCharacters [i] == fractionChar) {
// Check no decimal fraction was specified
int lastDecimalSeparatorIndex = text.lastIndexOf(getDecimalFormatSymbols().getDecimalSeparator(),
numberPosition.getIndex() - 1);
if (lastDecimalSeparatorIndex > quoteIndex) {
return null;
} else {
value += inchToCentimeter((i + 1) / 8f);
parsePosition.setIndex(numberPosition.getIndex() + 1);
skipWhiteSpaces(text, parsePosition);
if (parsePosition.getIndex() < text.length()
&& text.charAt(parsePosition.getIndex()) == '\"') {
parsePosition.setIndex(parsePosition.getIndex() + 1);
}
return value;
}
}
}
parsePosition.setIndex(numberPosition.getIndex());
return value;
}
/**
* Increases the index of <code>fieldPosition</code> to skip white spaces.
*/
private void skipWhiteSpaces(String text, ParsePosition fieldPosition) {
while (fieldPosition.getIndex() < text.length()
&& Character.isWhitespace(text.charAt(fieldPosition.getIndex()))) {
fieldPosition.setIndex(fieldPosition.getIndex() + 1);
}
}
};
String squareFootUnit = resource.getString("squareFootUnit");
this.areaFormatWithUnit = new SquareFootAreaFormatWithUnit("#,##0 " + squareFootUnit);
}
}
@Override
public float getMagnetizedLength(float length, float maxDelta) {
return getMagnetizedInchLength(length, maxDelta);
}
@Override
public float getMinimumLength() {
return LengthUnit.inchToCentimeter(0.125f);
}
@Override
public float getMaximumLength() {
return LengthUnit.inchToCentimeter(99974.4f); // 3280 ft
}
@Override
public float centimeterToUnit(float length) {
return centimeterToInch(length);
}
@Override
public float unitToCentimeter(float length) {
return inchToCentimeter(length);
}
},
/**
* Inch unit with decimals.
* @since 4.0
*/
INCH_DECIMALS {
private Locale formatLocale;
private String name;
private DecimalFormat lengthFormat;
private DecimalFormat areaFormatWithUnit;
@Override
public Format getFormatWithUnit() {
checkLocaleChange();
return this.lengthFormat;
}
@Override
public Format getFormat() {
return getFormatWithUnit();
}
@Override
public Format getAreaFormatWithUnit() {
checkLocaleChange();
return this.areaFormatWithUnit;
}
@Override
public String getName() {
checkLocaleChange();
return this.name;
}
private void checkLocaleChange() {
// Instantiate format if locale changed
if (!Locale.getDefault().equals(this.formatLocale)) {
this.formatLocale = Locale.getDefault();
ResourceBundle resource = ResourceBundle.getBundle(LengthUnit.class.getName());
this.name = resource.getString("inchUnit");
// Create format for inches
final MessageFormat inchDecimalsFormat = new MessageFormat(resource.getString("inchDecimalsFormat"));
final NumberFormat inchNumberFormat = NumberFormat.getNumberInstance();
this.lengthFormat = new DecimalFormat("0.###") {
@Override
public StringBuffer format(double number, StringBuffer result,
FieldPosition fieldPosition) {
float inches = centimeterToInch((float)number);
fieldPosition.setEndIndex(fieldPosition.getEndIndex() + 1);
inchDecimalsFormat.format(new Object [] {inches}, result, fieldPosition);
return result;
}
@Override
public Number parse(String text, ParsePosition parsePosition) {
ParsePosition numberPosition = new ParsePosition(parsePosition.getIndex());
skipWhiteSpaces(text, numberPosition);
// Parse inches
Number inches = inchNumberFormat.parse(text, numberPosition);
if (inches == null) {
parsePosition.setErrorIndex(numberPosition.getErrorIndex());
return null;
}
double value = inchToCentimeter(inches.floatValue());
// Parse "
skipWhiteSpaces(text, numberPosition);
if (numberPosition.getIndex() < text.length()
&& text.charAt(numberPosition.getIndex()) == '\"') {
parsePosition.setIndex(numberPosition.getIndex() + 1);
} else {
parsePosition.setIndex(numberPosition.getIndex());
}
return value;
}
/**
* Increases the index of <code>fieldPosition</code> to skip white spaces.
*/
private void skipWhiteSpaces(String text, ParsePosition fieldPosition) {
while (fieldPosition.getIndex() < text.length()
&& Character.isWhitespace(text.charAt(fieldPosition.getIndex()))) {
fieldPosition.setIndex(fieldPosition.getIndex() + 1);
}
}
};
String squareFootUnit = resource.getString("squareFootUnit");
this.areaFormatWithUnit = new SquareFootAreaFormatWithUnit("#,##0.## " + squareFootUnit);
}
}
@Override
public float getMagnetizedLength(float length, float maxDelta) {
return getMagnetizedInchLength(length, maxDelta);
}
@Override
public float getMinimumLength() {
return LengthUnit.inchToCentimeter(0.125f);
}
@Override
public float getMaximumLength() {
return LengthUnit.inchToCentimeter(99974.4f); // 3280 ft
}
@Override
public float centimeterToUnit(float length) {
return centimeterToInch(length);
}
@Override
public float unitToCentimeter(float length) {
return inchToCentimeter(length);
}
};
/**
* Returns the <code>length</code> given in centimeters converted to inches.
*/
public static float centimeterToInch(float length) {
return length / 2.54f;
}
/**
* Returns the <code>length</code> given in centimeters converted to feet.
*/
public static float centimeterToFoot(float length) {
return length / 2.54f / 12;
}
/**
* Returns the <code>length</code> given in inches converted to centimeters.
*/
public static float inchToCentimeter(float length) {
return length * 2.54f;
}
/**
* Returns the <code>length</code> given in feet converted to centimeters.
*/
public static float footToCentimeter(float length) {
return length * 2.54f * 12;
}
/**
* Returns a format able to format lengths with their localized unit.
*/
public abstract Format getFormatWithUnit();
/**
* Returns a format able to format lengths.
*/
public abstract Format getFormat();
/**
* A decimal format for meter family units.
*/
private static class MeterFamilyFormat extends DecimalFormat {
private final float unitMultiplier;
public MeterFamilyFormat(String pattern, float unitMultiplier) {
super(pattern);
this.unitMultiplier = unitMultiplier;
}
@Override
public StringBuffer format(double number, StringBuffer result,
FieldPosition fieldPosition) {
// Convert centimeter to millimeter
return super.format(number * this.unitMultiplier, result, fieldPosition);
}
@Override
public StringBuffer format(long number, StringBuffer result,
FieldPosition fieldPosition) {
return format((double)number, result, fieldPosition);
}
@Override
public Number parse(String text, ParsePosition pos) {
Number number = super.parse(text, pos);
if (number == null) {
return null;
} else {
return number.floatValue() / this.unitMultiplier;
}
}
}
/**
* Returns a format able to format areas with their localized unit.
*/
public abstract Format getAreaFormatWithUnit();
/**
* A decimal format for square meter.
*/
private static class SquareMeterAreaFormatWithUnit extends DecimalFormat {
public SquareMeterAreaFormatWithUnit(String squareMeterUnit) {
super("#,##0.## " + squareMeterUnit);
}
@Override
public StringBuffer format(double number, StringBuffer result,
FieldPosition fieldPosition) {
// Convert square centimeter to square meter
return super.format(number / 10000, result, fieldPosition);
}
}
/**
* A decimal format for square foot.
*/
private static class SquareFootAreaFormatWithUnit extends DecimalFormat {
public SquareFootAreaFormatWithUnit(String pattern) {
super(pattern);
}
@Override
public StringBuffer format(double number, StringBuffer result,
FieldPosition fieldPosition) {
// Convert square centimeter to square foot
return super.format(number / 929.0304, result, fieldPosition);
}
}
/**
* Returns a localized name of this unit.
*/
public abstract String getName();
/**
* Returns the value close to the given <code>length</code> in centimeter under magnetism.
*/
public abstract float getMagnetizedLength(float length, float maxDelta);
/**
* Returns the value close to the given length under magnetism for meter units.
*/
private static float getMagnetizedMeterLength(float length, float maxDelta) {
// Use a maximum precision of 1 mm depending on maxDelta
maxDelta *= 2;
float precision = 1 / 10f;
if (maxDelta > 100) {
precision = 100;
} else if (maxDelta > 10) {
precision = 10;
} else if (maxDelta > 5) {
precision = 5;
} else if (maxDelta > 1) {
precision = 1;
} else if (maxDelta > 0.5f) {
precision = 0.5f;
}
float magnetizedLength = Math.round(length / precision) * precision;
if (magnetizedLength == 0 && length > 0) {
return length;
} else {
return magnetizedLength;
}
}
/**
* Returns the value close to the given length under magnetism for inch units.
*/
private static float getMagnetizedInchLength(float length, float maxDelta) {
// Use a maximum precision of 1/8 inch depending on maxDelta
maxDelta = centimeterToInch(maxDelta) * 2;
float precision = 1 / 8f;
if (maxDelta > 6) {
precision = 6;
} else if (maxDelta > 3) {
precision = 3;
} else if (maxDelta > 1) {
precision = 1;
} else if (maxDelta > 0.5f) {
precision = 0.5f;
} else if (maxDelta > 0.25f) {
precision = 0.25f;
}
float magnetizedLength = inchToCentimeter(Math.round(centimeterToInch(length) / precision) * precision);
if (magnetizedLength == 0 && length > 0) {
return length;
} else {
return magnetizedLength;
}
}
/**
* Returns the minimum value for length in centimeter.
*/
public abstract float getMinimumLength();
/**
* Returns the maximum value for length in centimeter.
* @since 3.4
*/
public abstract float getMaximumLength();
/**
* Returns the maximum value for elevation in centimeter.
* @since 3.4
*/
public float getMaximumElevation() {
return getMaximumLength() / 10;
}
/**
* Returns the <code>length</code> given in centimeters converted
* to a value expressed in this unit.
* @since 2.0
*/
public abstract float centimeterToUnit(float length);
/**
* Returns the <code>length</code> given in this unit converted
* to a value expressed in centimeter.
* @since 2.0
*/
public abstract float unitToCentimeter(float length);
}