summaryrefslogtreecommitdiff
path: root/src/main/java/sevenUnitsGUI/Presenter.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/sevenUnitsGUI/Presenter.java')
-rw-r--r--src/main/java/sevenUnitsGUI/Presenter.java782
1 files changed, 782 insertions, 0 deletions
diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java
new file mode 100644
index 0000000..4feea44
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/Presenter.java
@@ -0,0 +1,782 @@
+/**
+ * Copyright (C) 2021-2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.Scanner;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import sevenUnits.ProgramInfo;
+import sevenUnits.unit.BaseDimension;
+import sevenUnits.unit.BaseUnit;
+import sevenUnits.unit.BritishImperial;
+import sevenUnits.unit.LinearUnit;
+import sevenUnits.unit.LinearUnitValue;
+import sevenUnits.unit.Metric;
+import sevenUnits.unit.Unit;
+import sevenUnits.unit.UnitDatabase;
+import sevenUnits.unit.UnitPrefix;
+import sevenUnits.unit.UnitType;
+import sevenUnits.unit.UnitValue;
+import sevenUnits.utils.Nameable;
+import sevenUnits.utils.ObjectProduct;
+import sevenUnits.utils.UncertainDouble;
+
+/**
+ * An object that handles interactions between the view and the backend code
+ *
+ * @author Adrien Hopkins
+ * @since 2021-12-15
+ */
+public final class Presenter {
+ /** The default place where settings are stored. */
+ private static final Path DEFAULT_SETTINGS_FILEPATH = Path
+ .of("settings.txt");
+ /** The default place where units are stored. */
+ private static final String DEFAULT_UNITS_FILEPATH = "/unitsfile.txt";
+ /** The default place where dimensions are stored. */
+ private static final String DEFAULT_DIMENSIONS_FILEPATH = "/dimensionfile.txt";
+ /** The default place where exceptions are stored. */
+ private static final String DEFAULT_EXCEPTIONS_FILEPATH = "/metric_exceptions.txt";
+
+ /**
+ * Adds default units and dimensions to a database.
+ *
+ * @param database database to add to
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ private static void addDefaults(final UnitDatabase database) {
+ database.addUnit("metre", Metric.METRE);
+ database.addUnit("kilogram", Metric.KILOGRAM);
+ database.addUnit("gram", Metric.KILOGRAM.dividedBy(1000));
+ database.addUnit("second", Metric.SECOND);
+ database.addUnit("ampere", Metric.AMPERE);
+ database.addUnit("kelvin", Metric.KELVIN);
+ database.addUnit("mole", Metric.MOLE);
+ database.addUnit("candela", Metric.CANDELA);
+ database.addUnit("bit", Metric.BIT);
+ database.addUnit("unit", Metric.ONE);
+ // nonlinear units - must be loaded manually
+ database.addUnit("tempCelsius", Metric.CELSIUS);
+ database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT);
+
+ // load initial dimensions
+ database.addDimension("LENGTH", Metric.Dimensions.LENGTH);
+ database.addDimension("MASS", Metric.Dimensions.MASS);
+ database.addDimension("TIME", Metric.Dimensions.TIME);
+ database.addDimension("TEMPERATURE", Metric.Dimensions.TEMPERATURE);
+ }
+
+ /**
+ * @return text in About file
+ * @since 2022-02-19
+ */
+ public static final String getAboutText() {
+ return Presenter.getLinesFromResource("/about.txt").stream()
+ .map(Presenter::withoutComments).collect(Collectors.joining("\n"))
+ .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION.toString());
+ }
+
+ /**
+ * Gets the text of a resource file as a set of strings (each one is one line
+ * of the text).
+ *
+ * @param filename filename to get resource from
+ * @return contents of file
+ * @since 2021-03-27
+ */
+ private static final List<String> getLinesFromResource(String filename) {
+ final List<String> lines = new ArrayList<>();
+
+ try (InputStream stream = inputStream(filename);
+ Scanner scanner = new Scanner(stream)) {
+ while (scanner.hasNextLine()) {
+ lines.add(scanner.nextLine());
+ }
+ } catch (final IOException e) {
+ throw new AssertionError(
+ "Error occurred while loading file " + filename, e);
+ }
+
+ return lines;
+ }
+
+ /**
+ * Gets an input stream for a resource file.
+ *
+ * @param filepath file to use as resource
+ * @return obtained Path
+ * @since 2021-03-27
+ */
+ private static final InputStream inputStream(String filepath) {
+ return Presenter.class.getResourceAsStream(filepath);
+ }
+
+ /**
+ * @return true iff a and b have any elements in common
+ * @since 2022-04-19
+ */
+ private static final boolean sharesAnyElements(Set<?> a, Set<?> b) {
+ for (final Object e : a) {
+ if (b.contains(e))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @return {@code line} with any comments removed.
+ * @since 2021-03-13
+ */
+ private static final String withoutComments(String line) {
+ final int index = line.indexOf('#');
+ return index == -1 ? line : line.substring(0, index);
+ }
+
+ // ====== SETTINGS ======
+
+ /**
+ * The view that this presenter communicates with
+ */
+ private final View view;
+
+ /**
+ * The database that this presenter communicates with (effectively the model)
+ */
+ final UnitDatabase database;
+
+ /**
+ * The rule used for parsing input numbers. Any number-string inputted into
+ * this program will be parsed using this method. <b>Not implemented yet.</b>
+ */
+ private Function<String, UncertainDouble> numberParsingRule;
+
+ /**
+ * The rule used for displaying the results of unit conversions. The result
+ * of unit conversions will be put into this function, and the resulting
+ * string will be used in the output.
+ */
+ private Function<UncertainDouble, String> numberDisplayRule = StandardDisplayRules
+ .uncertaintyBased();
+
+ /**
+ * A predicate that determines whether or not a certain combination of
+ * prefixes is allowed. If it returns false, a combination of prefixes will
+ * not be allowed. Prefixes are put in the list from right to left.
+ */
+ private Predicate<List<UnitPrefix>> prefixRepetitionRule = DefaultPrefixRepetitionRule.NO_RESTRICTION;
+
+ /**
+ * The set of units that is considered neither metric nor nonmetric for the
+ * purposes of the metric-imperial one-way conversion. These units are
+ * included in both From and To, even if One Way Conversion is enabled.
+ */
+ private final Set<String> metricExceptions;
+
+ /**
+ * If this is true, views that show units as a list will have metric units
+ * removed from the From unit list and imperial/USC units removed from the To
+ * unit list.
+ */
+ private boolean oneWayConversionEnabled = false;
+
+ /**
+ * If this is false, duplicate units and prefixes will be removed from the
+ * unit view in views that show units as a list to choose from.
+ */
+ private boolean showDuplicates = false;
+
+ /**
+ * Creates a Presenter
+ *
+ * @param view the view that this presenter communicates with
+ * @since 2021-12-15
+ */
+ public Presenter(View view) {
+ this.view = view;
+ this.database = new UnitDatabase();
+ addDefaults(this.database);
+
+ // load units and prefixes
+ try (final InputStream units = inputStream(DEFAULT_UNITS_FILEPATH)) {
+ this.database.loadUnitsFromStream(units);
+ } catch (final IOException e) {
+ throw new AssertionError("Loading of unitsfile.txt failed.", e);
+ }
+
+ // load dimensions
+ try (final InputStream dimensions = inputStream(
+ DEFAULT_DIMENSIONS_FILEPATH)) {
+ this.database.loadDimensionsFromStream(dimensions);
+ } catch (final IOException e) {
+ throw new AssertionError("Loading of dimensionfile.txt failed.", e);
+ }
+
+ // load metric exceptions
+ try {
+ this.metricExceptions = new HashSet<>();
+ try (InputStream exceptions = inputStream(DEFAULT_EXCEPTIONS_FILEPATH);
+ Scanner scanner = new Scanner(exceptions)) {
+ while (scanner.hasNextLine()) {
+ final String line = Presenter
+ .withoutComments(scanner.nextLine());
+ if (!line.isBlank()) {
+ this.metricExceptions.add(line);
+ }
+ }
+ }
+ } catch (final IOException e) {
+ throw new AssertionError("Loading of metric_exceptions.txt failed.",
+ e);
+ }
+
+ // set default settings temporarily
+ this.loadSettings(DEFAULT_SETTINGS_FILEPATH);
+
+ // a Predicate that returns true iff the argument is a full base unit
+ final Predicate<Unit> isFullBase = unit -> unit instanceof LinearUnit
+ && ((LinearUnit) unit).isBase();
+
+ // print out unit counts
+ System.out.printf(
+ "Successfully loaded %d units with %d unit names (%d base units).%n",
+ this.database.unitMapPrefixless(false).size(),
+ this.database.unitMapPrefixless(true).size(),
+ this.database.unitMapPrefixless(false).values().stream()
+ .filter(isFullBase).count());
+ }
+
+ /**
+ * Converts from the view's input expression to its output expression.
+ * Displays an error message if any of the required fields are invalid.
+ *
+ * @throws UnsupportedOperationException if the view does not support
+ * expression-based conversion (does
+ * not implement
+ * {@link ExpressionConversionView})
+ * @since 2021-12-15
+ */
+ public void convertExpressions() {
+ if (this.view instanceof ExpressionConversionView) {
+ final ExpressionConversionView xcview = (ExpressionConversionView) this.view;
+
+ final String fromExpression = xcview.getFromExpression();
+ final String toExpression = xcview.getToExpression();
+
+ // expressions must not be empty
+ if (fromExpression.isEmpty()) {
+ this.view.showErrorMessage("Parse Error",
+ "Please enter a unit expression in the From: box.");
+ return;
+ }
+ if (toExpression.isEmpty()) {
+ this.view.showErrorMessage("Parse Error",
+ "Please enter a unit expression in the To: box.");
+ return;
+ }
+
+ // evaluate expressions
+ final LinearUnitValue from;
+ final Unit to;
+ try {
+ from = this.database.evaluateUnitExpression(fromExpression);
+ } catch (final IllegalArgumentException | NoSuchElementException e) {
+ this.view.showErrorMessage("Parse Error",
+ "Could not recognize text in From entry: " + e.getMessage());
+ return;
+ }
+ try {
+ to = this.database.getUnitFromExpression(toExpression);
+ } catch (final IllegalArgumentException | NoSuchElementException e) {
+ this.view.showErrorMessage("Parse Error",
+ "Could not recognize text in To entry: " + e.getMessage());
+ return;
+ }
+
+ // convert and show output
+ if (from.getUnit().canConvertTo(to)) {
+ final UncertainDouble uncertainValue;
+
+ // uncertainty is meaningless for non-linear units, so we will have
+ // to erase uncertainty information for them
+ if (to instanceof LinearUnit) {
+ final var toLinear = (LinearUnit) to;
+ uncertainValue = from.convertTo(toLinear).getValue();
+ } else {
+ final double value = from.asUnitValue().convertTo(to).getValue();
+ uncertainValue = UncertainDouble.of(value, 0);
+ }
+
+ final UnitConversionRecord uc = UnitConversionRecord.valueOf(
+ fromExpression, toExpression, "",
+ this.numberDisplayRule.apply(uncertainValue));
+ xcview.showExpressionConversionOutput(uc);
+ } else {
+ this.view.showErrorMessage("Conversion Error",
+ "Cannot convert between \"" + fromExpression + "\" and \""
+ + toExpression + "\".");
+ }
+
+ } else
+ throw new UnsupportedOperationException(
+ "This function can only be called when the view is an ExpressionConversionView");
+ }
+
+ /**
+ * Converts from the view's input unit to its output unit. Displays an error
+ * message if any of the required fields are invalid.
+ *
+ * @throws UnsupportedOperationException if the view does not support
+ * unit-based conversion (does not
+ * implement
+ * {@link UnitConversionView})
+ * @since 2021-12-15
+ */
+ public void convertUnits() {
+ if (this.view instanceof UnitConversionView) {
+ final UnitConversionView ucview = (UnitConversionView) this.view;
+
+ final Optional<String> fromUnitOptional = ucview.getFromSelection();
+ final Optional<String> toUnitOptional = ucview.getToSelection();
+ final String inputValueString = ucview.getInputValue();
+
+ // extract values from optionals
+ final String fromUnitString, toUnitString;
+ if (fromUnitOptional.isPresent()) {
+ fromUnitString = fromUnitOptional.orElseThrow();
+ } else {
+ this.view.showErrorMessage("Unit Selection Error",
+ "Please specify a From unit");
+ return;
+ }
+ if (toUnitOptional.isPresent()) {
+ toUnitString = toUnitOptional.orElseThrow();
+ } else {
+ this.view.showErrorMessage("Unit Selection Error",
+ "Please specify a To unit");
+ return;
+ }
+
+ // convert strings to data, checking if anything is invalid
+ final Unit fromUnit, toUnit;
+ final UncertainDouble uncertainValue;
+
+ if (this.database.containsUnitName(fromUnitString)) {
+ fromUnit = this.database.getUnit(fromUnitString);
+ } else
+ throw this.viewError("Nonexistent From unit: %s", fromUnitString);
+ if (this.database.containsUnitName(toUnitString)) {
+ toUnit = this.database.getUnit(toUnitString);
+ } else
+ throw this.viewError("Nonexistent To unit: %s", toUnitString);
+ try {
+ uncertainValue = UncertainDouble
+ .fromRoundedString(inputValueString);
+ } catch (final NumberFormatException e) {
+ this.view.showErrorMessage("Value Error",
+ "Invalid value " + inputValueString);
+ return;
+ }
+
+ if (!fromUnit.canConvertTo(toUnit))
+ throw this.viewError("Could not convert between %s and %s",
+ fromUnit, toUnit);
+
+ // convert - we will need to erase uncertainty for non-linear units, so
+ // we need to treat linear and non-linear units differently
+ final String outputValueString;
+ if (fromUnit instanceof LinearUnit && toUnit instanceof LinearUnit) {
+ final LinearUnit fromLinear = (LinearUnit) fromUnit;
+ final LinearUnit toLinear = (LinearUnit) toUnit;
+ final LinearUnitValue initialValue = LinearUnitValue.of(fromLinear,
+ uncertainValue);
+ final LinearUnitValue converted = initialValue.convertTo(toLinear);
+
+ outputValueString = this.numberDisplayRule
+ .apply(converted.getValue());
+ } else {
+ final UnitValue initialValue = UnitValue.of(fromUnit,
+ uncertainValue.value());
+ final UnitValue converted = initialValue.convertTo(toUnit);
+
+ outputValueString = this.numberDisplayRule
+ .apply(UncertainDouble.of(converted.getValue(), 0));
+ }
+
+ ucview.showUnitConversionOutput(
+ UnitConversionRecord.valueOf(fromUnitString, toUnitString,
+ inputValueString, outputValueString));
+ } else
+ throw new UnsupportedOperationException(
+ "This function can only be called when the view is a UnitConversionView.");
+ }
+
+ /**
+ * @return true iff duplicate units are shown in unit lists
+ * @since 2022-03-30
+ */
+ public boolean duplicatesShown() {
+ return this.showDuplicates;
+ }
+
+ /**
+ * Gets a name for this dimension using the database
+ *
+ * @param dimension dimension to name
+ * @return name of dimension
+ * @since 2022-04-16
+ */
+ final String getDimensionName(ObjectProduct<BaseDimension> dimension) {
+ // find this dimension in the database and get its name
+ // if it isn't there, use the dimension's toString instead
+ return this.database.dimensionMap().values().stream()
+ .filter(d -> d.equals(dimension)).findAny().map(Nameable::getName)
+ .orElse(dimension.toString(Nameable::getName));
+ }
+
+ /**
+ * @return the rule that is used by this presenter to convert numbers into
+ * strings
+ * @since 2022-04-10
+ */
+ public Function<UncertainDouble, String> getNumberDisplayRule() {
+ return this.numberDisplayRule;
+ }
+
+ /**
+ * @return the rule that is used by this presenter to convert strings into
+ * numbers
+ * @since 2022-04-10
+ */
+ @SuppressWarnings("unused") // not implemented yet
+ private Function<String, UncertainDouble> getNumberParsingRule() {
+ return this.numberParsingRule;
+ }
+
+ /**
+ * @return the rule that determines whether a set of prefixes is valid
+ * @since 2022-04-19
+ */
+ public Predicate<List<UnitPrefix>> getPrefixRepetitionRule() {
+ return this.prefixRepetitionRule;
+ }
+
+ /**
+ * @return the view associated with this presenter
+ * @since 2022-04-19
+ */
+ public View getView() {
+ return this.view;
+ }
+
+ /**
+ * @return whether or not the provided unit is semi-metric (i.e. an
+ * exception)
+ * @since 2022-04-16
+ */
+ final boolean isSemiMetric(Unit u) {
+ // determine if u is an exception
+ final var primaryName = u.getPrimaryName();
+ final var symbol = u.getSymbol();
+ return primaryName.isPresent()
+ && this.metricExceptions.contains(primaryName.orElseThrow())
+ || symbol.isPresent()
+ && this.metricExceptions.contains(symbol.orElseThrow())
+ || sharesAnyElements(this.metricExceptions, u.getOtherNames());
+ }
+
+ /**
+ * Loads settings from the user's settings file and applies them to the
+ * presenter.
+ *
+ * @param settingsFile file settings should be loaded from
+ * @since 2021-12-15
+ */
+ void loadSettings(Path settingsFile) {
+ try {
+ // read file line by line
+ final int lineNum = 0;
+ for (final String line : Files.readAllLines(settingsFile)) {
+ final int equalsIndex = line.indexOf('=');
+ if (equalsIndex == -1)
+ throw new IllegalStateException(
+ "Settings file is malformed at line " + lineNum);
+
+ final String param = line.substring(0, equalsIndex);
+ final String value = line.substring(equalsIndex + 1);
+
+ switch (param) {
+ // set manually to avoid the unnecessary saving of the non-manual
+ // methods
+ case "number_display_rule":
+ this.numberDisplayRule = StandardDisplayRules
+ .getStandardRule(value);
+ break;
+ case "prefix_rule":
+ this.prefixRepetitionRule = DefaultPrefixRepetitionRule
+ .valueOf(value);
+ this.database.setPrefixRepetitionRule(this.prefixRepetitionRule);
+ break;
+ case "one_way":
+ this.oneWayConversionEnabled = Boolean.valueOf(value);
+ break;
+ case "include_duplicates":
+ this.showDuplicates = Boolean.valueOf(value);
+ break;
+ default:
+ System.err.printf("Warning: unrecognized setting \"%s\".%n",
+ param);
+ break;
+ }
+ }
+ if (this.view.getPresenter() != null) {
+ this.updateView();
+ }
+ } catch (final IOException e) {}
+ }
+
+ /**
+ * @return true iff the One-Way Conversion feature is available (views that
+ * show units as a list will have metric units removed from the From
+ * unit list and imperial/USC units removed from the To unit list)
+ *
+ * @since 2022-03-30
+ */
+ public boolean oneWayConversionEnabled() {
+ return this.oneWayConversionEnabled;
+ }
+
+ /**
+ * Completes creation of the presenter. This part of the initialization
+ * depends on the view's functions, so it cannot be run if the components
+ * they depend on are not created yet.
+ *
+ * @since 2022-02-26
+ */
+ public void postViewInitialize() {
+ // unit conversion specific stuff
+ if (this.view instanceof UnitConversionView) {
+ final UnitConversionView ucview = (UnitConversionView) this.view;
+ ucview.setDimensionNames(this.database.dimensionMap().keySet());
+ }
+
+ this.updateView();
+ }
+
+ void prefixSelected() {
+ final Optional<String> selectedPrefixName = this.view
+ .getViewedPrefixName();
+ final Optional<UnitPrefix> selectedPrefix = selectedPrefixName
+ .map(name -> this.database.containsPrefixName(name)
+ ? this.database.getPrefix(name)
+ : null);
+ selectedPrefix
+ .ifPresent(prefix -> this.view.showPrefix(prefix.getNameSymbol(),
+ String.valueOf(prefix.getMultiplier())));
+ }
+
+ /**
+ * Saves the presenter's current settings to its default filepath.
+ *
+ * @since 2022-04-19
+ */
+ public void saveSettings() {
+ this.saveSettings(DEFAULT_SETTINGS_FILEPATH);
+ }
+
+ /**
+ * Saves the presenter's settings to the user settings file.
+ *
+ * @param settingsFile file settings should be saved to
+ * @since 2021-12-15
+ */
+ void saveSettings(Path settingsFile) {
+ try (BufferedWriter writer = Files.newBufferedWriter(settingsFile)) {
+ writer.write(String.format("number_display_rule=%s\n",
+ this.numberDisplayRule));
+ writer.write(
+ String.format("prefix_rule=%s\n", this.prefixRepetitionRule));
+ writer.write(
+ String.format("one_way=%s\n", this.oneWayConversionEnabled));
+ writer.write(
+ String.format("include_duplicates=%s\n", this.showDuplicates));
+ } catch (final IOException e) {
+ e.printStackTrace();
+ this.view.showErrorMessage("I/O Error",
+ "Error occurred while saving settings: "
+ + e.getLocalizedMessage());
+ }
+ }
+
+ /**
+ * @param numberDisplayRule the new rule that will be used by this presenter
+ * to convert numbers into strings
+ * @since 2022-04-10
+ */
+ public void setNumberDisplayRule(
+ Function<UncertainDouble, String> numberDisplayRule) {
+ this.numberDisplayRule = numberDisplayRule;
+ }
+
+ /**
+ * @param numberParsingRule the new rule that will be used by this presenter
+ * to convert strings into numbers
+ * @since 2022-04-10
+ */
+ @SuppressWarnings("unused") // not implemented yet
+ private void setNumberParsingRule(
+ Function<String, UncertainDouble> numberParsingRule) {
+ this.numberParsingRule = numberParsingRule;
+ }
+
+ /**
+ * @param oneWayConversionEnabled whether not one-way conversion should be
+ * enabled
+ * @since 2022-03-30
+ * @see {@link #isOneWayConversionEnabled}
+ */
+ public void setOneWayConversionEnabled(boolean oneWayConversionEnabled) {
+ this.oneWayConversionEnabled = oneWayConversionEnabled;
+ this.updateView();
+ }
+
+ /**
+ * @param prefixRepetitionRule the rule that determines whether a set of
+ * prefixes is valid
+ * @since 2022-04-19
+ */
+ public void setPrefixRepetitionRule(
+ Predicate<List<UnitPrefix>> prefixRepetitionRule) {
+ this.prefixRepetitionRule = prefixRepetitionRule;
+ this.database.setPrefixRepetitionRule(prefixRepetitionRule);
+ }
+
+ /**
+ * @param showDuplicateUnits whether or not duplicate units should be shown
+ * @since 2022-03-30
+ */
+ public void setShowDuplicates(boolean showDuplicateUnits) {
+ this.showDuplicates = showDuplicateUnits;
+ this.updateView();
+ }
+
+ /**
+ * Shows a unit in the unit viewer
+ *
+ * @param u unit to show
+ * @since 2022-04-16
+ */
+ private final void showUnit(Unit u) {
+ final var nameSymbol = u.getNameSymbol();
+ final boolean isBase = u instanceof BaseUnit
+ || u instanceof LinearUnit && ((LinearUnit) u).isBase();
+ final var definition = isBase ? "(Base unit)" : u.toDefinitionString();
+ final var dimensionString = this.getDimensionName(u.getDimension());
+ final var unitType = UnitType.getType(u, this::isSemiMetric);
+ this.view.showUnit(nameSymbol, definition, dimensionString, unitType);
+ }
+
+ /**
+ * Runs whenever a unit name is selected in the unit viewer. Gets the
+ * description of a unit and displays it.
+ *
+ * @since 2022-04-10
+ */
+ void unitNameSelected() {
+ // get selected unit, if it's there and valid
+ final Optional<String> selectedUnitName = this.view.getViewedUnitName();
+ final Optional<Unit> selectedUnit = selectedUnitName
+ .map(unitName -> this.database.containsUnitName(unitName)
+ ? this.database.getUnit(unitName)
+ : null);
+ selectedUnit.ifPresent(this::showUnit);
+ }
+
+ /**
+ * Updates the view's From and To units, if it has some
+ *
+ * @since 2021-12-15
+ */
+ public void updateView() {
+ if (this.view instanceof UnitConversionView) {
+ final UnitConversionView ucview = (UnitConversionView) this.view;
+ final var selectedDimensionName = ucview.getSelectedDimensionName();
+
+ // load units & prefixes into viewers
+ this.view.setViewableUnitNames(
+ this.database.unitMapPrefixless(this.showDuplicates).keySet());
+ this.view.setViewablePrefixNames(
+ this.database.prefixMap(this.showDuplicates).keySet());
+
+ // get From and To units
+ var fromUnits = this.database.unitMapPrefixless(this.showDuplicates)
+ .entrySet().stream();
+ var toUnits = this.database.unitMapPrefixless(this.showDuplicates)
+ .entrySet().stream();
+
+ // filter by dimension, if one is selected
+ if (selectedDimensionName.isPresent()) {
+ final var viewDimension = this.database
+ .getDimension(selectedDimensionName.orElseThrow());
+ fromUnits = fromUnits.filter(
+ u -> viewDimension.equals(u.getValue().getDimension()));
+ toUnits = toUnits.filter(
+ u -> viewDimension.equals(u.getValue().getDimension()));
+ }
+
+ // filter by unit type, if desired
+ if (this.oneWayConversionEnabled) {
+ fromUnits = fromUnits.filter(u -> UnitType.getType(u.getValue(),
+ this::isSemiMetric) != UnitType.METRIC);
+ toUnits = toUnits.filter(u -> UnitType.getType(u.getValue(),
+ this::isSemiMetric) != UnitType.NON_METRIC);
+ }
+
+ // set unit names
+ ucview.setFromUnitNames(
+ fromUnits.map(Map.Entry::getKey).collect(Collectors.toSet()));
+ ucview.setToUnitNames(
+ toUnits.map(Map.Entry::getKey).collect(Collectors.toSet()));
+ }
+ }
+
+ /**
+ * @param message message to add
+ * @param args string formatting arguments for message
+ * @return AssertionError stating that an error has happened in the view's
+ * code
+ * @since 2022-04-09
+ */
+ private AssertionError viewError(String message, Object... args) {
+ return new AssertionError("View Programming Error (from " + this.view
+ + "): " + String.format(message, args));
+ }
+}