/**
* Copyright (C) 2021 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 .
*/
package sevenUnitsGUI;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.OptionalDouble;
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.BritishImperial;
import sevenUnits.unit.LinearUnitValue;
import sevenUnits.unit.Metric;
import sevenUnits.unit.Unit;
import sevenUnits.unit.UnitDatabase;
import sevenUnits.unit.UnitPrefix;
import sevenUnits.unit.UnitValue;
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 String DEFAULT_SETTINGS_FILEPATH = "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
*/
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 getLinesFromResource(String filename) {
final List 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);
}
/**
* Accepts a collection and returns a set with the unique elements in that
* collection
*
* @param type of element in collection
* @param collection collection to uniquify
* @return unique collection
* @since 2022-02-26
*/
private static Set unique(Collection collection) {
final Set uniqueSet = new HashSet<>();
for (final E e : collection) {
if (!uniqueSet.contains(e)) {
uniqueSet.add(e);
}
}
return uniqueSet;
}
/**
* @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(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.
*/
private Function 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 numberDisplayRule;
/**
* 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> prefixRepetitionRule;
/**
* 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 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 oneWayConversion;
/**
* If this is false, duplicate units will be removed from the unit view in
* views that show units as a list to choose from.
*/
private boolean showDuplicateUnits;
/**
* 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);
}
}
/**
* Gets settings from the view and applies them to both view and presenter.
*
* @since 2021-12-15
*/
public void applySettings() {}
/**
* 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 double value = from.asUnitValue().convertTo(to).getValue();
xcview.showExpressionConversionOutput(fromExpression, toExpression,
value);
} 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 fromUnitOptional = ucview.getFromSelection();
final Optional toUnitOptional = ucview.getToSelection();
final OptionalDouble valueOptional = ucview.getInputValue();
// ensure everything is obtained
final Unit fromUnit, toUnit;
final double value;
if (fromUnitOptional.isPresent()) {
fromUnit = fromUnitOptional.orElseThrow();
} else {
this.view.showErrorMessage("Unit Conversion Error",
"Please specify a From unit");
return;
}
if (toUnitOptional.isPresent()) {
toUnit = toUnitOptional.orElseThrow();
} else {
this.view.showErrorMessage("Unit Conversion Error",
"Please specify a To unit");
return;
}
if (valueOptional.isPresent()) {
value = valueOptional.orElseThrow();
} else {
this.view.showErrorMessage("Unit Conversion Error",
"Please specify a valid value");
return;
}
if (!fromUnit.canConvertTo(toUnit))
throw new AssertionError(
"From and To units incompatible (should be impossible)");
// convert!
final UnitValue initialValue = UnitValue.of(fromUnit, value);
final UnitValue converted = initialValue.convertTo(toUnit);
ucview.showUnitConversionOutput(initialValue, converted);
} else
throw new UnsupportedOperationException(
"This function can only be called when the view is a UnitConversionView.");
}
/**
* Loads settings from the user's settings file and applies them to the view.
*
* @since 2021-12-15
*/
public void loadSettings() {}
/**
* 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.setDimensions(unique(this.database.dimensionMap().values()));
}
}
void prefixSelected() {}
/**
* Gets user settings from the view then saves them to the user's settings
* file.
*
* @since 2021-12-15
*/
public void saveSettings() {}
void unitNameSelected() {}
/**
* 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 ObjectProduct viewDimension = ucview
.getSelectedDimension().orElseThrow();
final Set units = this.database
.unitMapPrefixless(this.showDuplicateUnits).entrySet().stream()
.map(Map.Entry::getValue)
.filter(u -> viewDimension.equals(u.getDimension()))
.collect(Collectors.toSet());
ucview.setFromUnits(units);
ucview.setToUnits(units);
}
}
}