This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 2d01b06e45e6f9fe35113106b63f4a2696cfb292 Author: Martin Desruisseaux <[email protected]> AuthorDate: Tue Feb 2 12:22:16 2021 +0100 Use `BigDecimal` for computation of intermediate levels for making sure that we do not surprise users with rounding errors before final conversion to `double`. --- .../sis/internal/gui/control/FormatApplicator.java | 19 ++++++++++ .../sis/internal/gui/control/ValueColorMapper.java | 44 ++++++++++++++++------ 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/FormatApplicator.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/FormatApplicator.java index 027e575..3e33683 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/FormatApplicator.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/FormatApplicator.java @@ -16,7 +16,10 @@ */ package org.apache.sis.internal.gui.control; +import java.math.BigDecimal; import java.text.Format; +import java.text.NumberFormat; +import java.text.DecimalFormat; import java.text.ParsePosition; import java.text.ParseException; import javafx.util.StringConverter; @@ -79,6 +82,22 @@ final class FormatApplicator<T> extends StringConverter<T> } /** + * Creates an instance using {@link NumberFormat}. If the {@linkplain DecimalFormat format is decimal}, + * then it will parse {@link BigDecimal} values. The intent is to allow arithmetic operations without + * rounding errors that may surprise the user, for example if we need to compute {@code n * scale} + * where <var>scale</var> has been specified by user as 0.1. + * + * @return an instance for parsing and formatting numbers. + */ + public static FormatApplicator<Number> createNumberFormat() { + final FormatApplicator<Number> f = new FormatApplicator<>(Number.class, NumberFormat.getInstance()); + if (f.format instanceof DecimalFormat) { + ((DecimalFormat) f.format).setParseBigDecimal(true); + } + return f; + } + + /** * Sets listeners on the given editor. The text will be parsed when the field lost focus or when * user presses "Enter" and the result will be stored using {@link TextField#setUserData(Object)}. * diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ValueColorMapper.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ValueColorMapper.java index 553399d..341a1ba 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ValueColorMapper.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ValueColorMapper.java @@ -18,7 +18,7 @@ package org.apache.sis.internal.gui.control; import java.util.Objects; import java.util.Locale; -import java.text.NumberFormat; +import java.math.BigDecimal; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; @@ -189,7 +189,7 @@ public final class ValueColorMapper extends Widget { * (those arguments would be removed if this constructor was public API). */ public ValueColorMapper(final Resources resources, final Vocabulary vocabulary) { - textConverter = new FormatApplicator<>(Number.class, NumberFormat.getInstance()); + textConverter = FormatApplicator.createNumberFormat(); table = createIsolineTable(vocabulary); final MenuItem rangeMenu = new MenuItem(resources.getString(Resources.Keys.RangeOfValues)); final MenuItem clearAll = new MenuItem(resources.getString(Resources.Keys.ClearAll)); @@ -431,7 +431,10 @@ public final class ValueColorMapper extends Widget { rangeEditor.showAndWait().ifPresent((r) -> { final ObservableList<Step> steps = getSteps(); int position = 0; -increment: for (double i=0, value; (value = i*r.interval + r.minimum) <= r.maximum; i++) { // TODO: use Math.fma with JDK9. + BigDecimal decimal = r.minimum; +increment: while (decimal.compareTo(r.maximum) <= 0) { + final double value = decimal.doubleValue(); + decimal = decimal.add(r.interval); while (position < steps.size()) { final double existing = steps.get(position).value.get(); if (existing == value) continue increment; @@ -448,9 +451,10 @@ increment: for (double i=0, value; (value = i*r.interval + r.minimum) <= r.maxi */ private static final class Range { /** - * The bounds and interval of values to create. + * The bounds and interval of values to create. Use {@link BigDecimal} + * for avoiding arithmetic errors when computing intermediate values. */ - final double minimum, maximum, interval; + final BigDecimal minimum, maximum, interval; /** * The constant color to associate with all values. @@ -458,13 +462,29 @@ increment: for (double i=0, value; (value = i*r.interval + r.minimum) <= r.maxi final Color color; /** - * Creates a new range. + * Creates a new range from the current values in given controls. */ - Range(final double minimum, final double maximum, final double interval, final Color color) { - this.minimum = minimum; - this.maximum = maximum; - this.interval = interval; - this.color = color; + private Range(final TextField minimum, final TextField maximum, final TextField interval, final ColorPicker color) { + this.minimum = decimal(minimum); + this.maximum = decimal(maximum); + this.interval = decimal(interval); + this.color = color.getValue(); + } + + /** + * Returns the value of given field as a {@link BigDecimal} instance. + */ + private static BigDecimal decimal(final TextField field) { + final Object value = field.getUserData(); + if (value instanceof BigDecimal) { + return (BigDecimal) value; // Should be the usual case. + } else { + /* + * A NullPointerException or ClassCastException below would + * be a bug in the validation checks performed by this class. + */ + return BigDecimal.valueOf(((Number) value).doubleValue()); + } } /** @@ -508,7 +528,7 @@ increment: for (double i=0, value; (value = i*r.interval + r.minimum) <= r.maxi }; rangeEditor.setResultConverter((button) -> { if (button == ButtonType.APPLY) { - return new Range(valueOf(minimum), valueOf(maximum), valueOf(interval), colorInRange.getValue()); + return new Range(minimum, maximum, interval, colorInRange); } return null; });
