/** * Copyright (C) 2018 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 org.unitConverter.converterGUI; import java.awt.BorderLayout; import java.awt.GridLayout; import java.io.File; import java.math.BigDecimal; import java.math.MathContext; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.Predicate; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JFormattedTextField; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JSlider; import javax.swing.JTabbedPane; import javax.swing.JTextArea; import javax.swing.JTextField; import org.unitConverter.math.ObjectProduct; import org.unitConverter.unit.BaseDimension; import org.unitConverter.unit.LinearUnit; import org.unitConverter.unit.SI; import org.unitConverter.unit.Unit; import org.unitConverter.unit.UnitDatabase; import org.unitConverter.unit.UnitPrefix; /** * @author Adrien Hopkins * @since 2018-12-27 * @since v0.1.0 */ final class UnitConverterGUI { private static class Presenter { /** * 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", SI.METRE); database.addUnit("kilogram", SI.KILOGRAM); database.addUnit("gram", SI.KILOGRAM.dividedBy(1000)); database.addUnit("second", SI.SECOND); database.addUnit("ampere", SI.AMPERE); database.addUnit("kelvin", SI.KELVIN); database.addUnit("mole", SI.MOLE); database.addUnit("candela", SI.CANDELA); database.addUnit("bit", SI.BIT); database.addUnit("unit", SI.ONE); // nonlinear units - must be loaded manually database.addUnit("tempCelsius", SI.CELSIUS); // database.addUnit("tempFahrenheit", NonlinearUnits.FAHRENHEIT); // load initial dimensions database.addDimension("LENGTH", SI.Dimensions.LENGTH); database.addDimension("MASS", SI.Dimensions.MASS); database.addDimension("TIME", SI.Dimensions.TIME); database.addDimension("TEMPERATURE", SI.Dimensions.TEMPERATURE); } /** The presenter's associated view. */ private final View view; /** The units known by the program. */ private final UnitDatabase database; /** The names of all of the units */ private final List unitNames; /** The names of all of the prefixes */ private final List prefixNames; /** The names of all of the dimensions */ private final List dimensionNames; private final Comparator prefixNameComparator; private int significantFigures = 6; /** * Creates the presenter. * * @param view * presenter's associated view * @since 2018-12-27 * @since v0.1.0 */ Presenter(final View view) { this.view = view; // load initial units this.database = new UnitDatabase(); Presenter.addDefaults(this.database); this.database.loadUnitsFile(new File("unitsfile.txt")); this.database.loadDimensionFile(new File("dimensionfile.txt")); // a comparator that can be used to compare prefix names // any name that does not exist is less than a name that does. // otherwise, they are compared by value this.prefixNameComparator = (o1, o2) -> { if (!Presenter.this.database.containsPrefixName(o1)) return -1; else if (!Presenter.this.database.containsPrefixName(o2)) return 1; final UnitPrefix p1 = Presenter.this.database.getPrefix(o1); final UnitPrefix p2 = Presenter.this.database.getPrefix(o2); if (p1.getMultiplier() < p2.getMultiplier()) return -1; else if (p1.getMultiplier() > p2.getMultiplier()) return 1; return o1.compareTo(o2); }; this.unitNames = new ArrayList<>(this.database.unitMapPrefixless().keySet()); this.unitNames.sort(null); // sorts it using Comparable this.prefixNames = new ArrayList<>(this.database.prefixMap().keySet()); this.prefixNames.sort(this.prefixNameComparator); // sorts it using my comparator this.dimensionNames = new DelegateListModel<>(new ArrayList<>(this.database.dimensionMap().keySet())); this.dimensionNames.sort(null); // sorts it using Comparable // a Predicate that returns true iff the argument is a full base unit final Predicate 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", new HashSet<>(this.database.unitMapPrefixless().values()).size(), this.database.unitMapPrefixless().size(), new HashSet<>(this.database.unitMapPrefixless().values()).stream().filter(isFullBase).count()); } /** * Converts in the dimension-based converter * * @since 2019-04-13 * @since v0.2.0 */ public final void convertDimensionBased() { final String fromSelection = this.view.getFromSelection(); if (fromSelection == null) { this.view.showErrorDialog("Error", "No unit selected in From field"); return; } final String toSelection = this.view.getToSelection(); if (toSelection == null) { this.view.showErrorDialog("Error", "No unit selected in To field"); return; } final Unit from = this.database.getUnit(fromSelection); final Unit to = this.database.getUnit(toSelection); final String input = this.view.getDimensionConverterInput(); if (input.equals("")) { this.view.showErrorDialog("Error", "No value to convert entered."); return; } final double beforeValue = Double.parseDouble(input); final double value = from.convertTo(to, beforeValue); final String output = this.getRoundedString(value); this.view.setDimensionConverterOutputText( String.format("%s %s = %s %s", input, fromSelection, output, toSelection)); } /** * Runs whenever the convert button is pressed. * *

* Reads and parses a unit expression from the from and to boxes, then converts {@code from} to {@code to}. Any * errors are shown in JOptionPanes. *

* * @since 2019-01-26 * @since v0.1.0 */ public final void convertExpressions() { final String fromUnitString = this.view.getFromText(); final String toUnitString = this.view.getToText(); if (fromUnitString.isEmpty()) { this.view.showErrorDialog("Parse Error", "Please enter a unit expression in the From: box."); return; } if (toUnitString.isEmpty()) { this.view.showErrorDialog("Parse Error", "Please enter a unit expression in the To: box."); return; } // try to parse from final Unit from; try { from = this.database.getUnitFromExpression(fromUnitString); } catch (final IllegalArgumentException e) { this.view.showErrorDialog("Parse Error", "Could not recognize text in From entry: " + e.getMessage()); return; } final double value; // try to parse to final Unit to; try { if (this.database.containsUnitName(toUnitString)) { // if it's a unit, convert to that to = this.database.getUnit(toUnitString); } else { to = this.database.getUnitFromExpression(toUnitString); } } catch (final IllegalArgumentException e) { this.view.showErrorDialog("Parse Error", "Could not recognize text in To entry: " + e.getMessage()); return; } // if I can't convert, leave if (!from.canConvertTo(to)) { this.view.showErrorDialog("Conversion Error", String.format("Cannot convert between %s and %s", fromUnitString, toUnitString)); return; } value = from.convertTo(to, 1); // round value final String output = this.getRoundedString(value); this.view.setExpressionConverterOutputText( String.format("%s = %s %s", fromUnitString, output, toUnitString)); } /** * @return a list of all of the unit dimensions * @since 2019-04-13 * @since v0.2.0 */ public final List dimensionNameList() { return this.dimensionNames; } /** * @return a comparator to compare prefix names * @since 2019-04-14 * @since v0.2.0 */ public final Comparator getPrefixNameComparator() { return this.prefixNameComparator; } /** * @param value * value to round * @return string of that value rounded to {@code significantDigits} significant digits. * @since 2019-04-14 * @since v0.2.0 */ private final String getRoundedString(final double value) { // round value final BigDecimal bigValue = new BigDecimal(value).round(new MathContext(this.significantFigures)); String output = bigValue.toString(); // remove trailing zeroes if (output.contains(".")) { while (output.endsWith("0")) { output = output.substring(0, output.length() - 1); } if (output.endsWith(".")) { output = output.substring(0, output.length() - 1); } } return output; } /** * @return a set of all prefix names in the database * @since 2019-04-14 * @since v0.2.0 */ public final Set prefixNameSet() { return this.database.prefixMap().keySet(); } /** * Runs whenever a prefix is selected in the viewer. *

* Shows its information in the text box to the right. *

* * @since 2019-01-15 * @since v0.1.0 */ public final void prefixSelected() { final String prefixName = this.view.getPrefixViewerSelection(); if (prefixName == null) return; else { final UnitPrefix prefix = this.database.getPrefix(prefixName); this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s", prefixName, prefix.getMultiplier())); } } /** * @param significantFigures * new value of significantFigures * @since 2019-01-15 * @since v0.1.0 */ public final void setSignificantFigures(final int significantFigures) { this.significantFigures = significantFigures; } /** * Returns true if and only if the unit represented by {@code unitName} has the dimension represented by * {@code dimensionName}. * * @param unitName * name of unit to test * @param dimensionName * name of dimension to test * @return whether unit has dimenision * @since 2019-04-13 * @since v0.2.0 */ public final boolean unitMatchesDimension(final String unitName, final String dimensionName) { final Unit unit = this.database.getUnit(unitName); final ObjectProduct dimension = this.database.getDimension(dimensionName); return unit.getDimension().equals(dimension); } /** * Runs whenever a unit is selected in the viewer. *

* Shows its information in the text box to the right. *

* * @since 2019-01-15 * @since v0.1.0 */ public final void unitNameSelected() { final String unitName = this.view.getUnitViewerSelection(); if (unitName == null) return; else { final Unit unit = this.database.getUnit(unitName); this.view.setUnitTextBoxText(unit.toString()); } } /** * @return a set of all of the unit names * @since 2019-04-14 * @since v0.2.0 */ public final Set unitNameSet() { return this.database.unitMapPrefixless().keySet(); } } private static class View { /** The view's frame. */ private final JFrame frame; /** The view's associated presenter. */ private final Presenter presenter; // DIMENSION-BASED CONVERTER /** The panel for inputting values in the dimension-based converter */ private final JTextField valueInput; /** The panel for "From" in the dimension-based converter */ private final SearchBoxList fromSearch; /** The panel for "To" in the dimension-based converter */ private final SearchBoxList toSearch; /** The output area in the dimension-based converter */ private final JTextArea dimensionBasedOutput; // EXPRESSION-BASED CONVERTER /** The "From" entry in the conversion panel */ private final JTextField fromEntry; /** The "To" entry in the conversion panel */ private final JTextField toEntry; /** The output area in the conversion panel */ private final JTextArea output; // UNIT AND PREFIX VIEWERS /** The searchable list of unit names in the unit viewer */ private final SearchBoxList unitNameList; /** The searchable list of prefix names in the prefix viewer */ private final SearchBoxList prefixNameList; /** The text box for unit data in the unit viewer */ private final JTextArea unitTextBox; /** The text box for prefix data in the prefix viewer */ private final JTextArea prefixTextBox; /** * Creates the {@code View}. * * @since 2019-01-14 * @since v0.1.0 */ public View() { this.presenter = new Presenter(this); this.frame = new JFrame("Unit Converter"); this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // create the components this.unitNameList = new SearchBoxList(this.presenter.unitNameSet()); this.prefixNameList = new SearchBoxList(this.presenter.prefixNameSet(), this.presenter.getPrefixNameComparator(), true); this.unitTextBox = new JTextArea(); this.prefixTextBox = new JTextArea(); this.fromSearch = new SearchBoxList(this.presenter.unitNameSet()); this.toSearch = new SearchBoxList(this.presenter.unitNameSet()); this.valueInput = new JFormattedTextField(new DecimalFormat("###############0.################")); this.dimensionBasedOutput = new JTextArea(2, 32); this.fromEntry = new JTextField(); this.toEntry = new JTextField(); this.output = new JTextArea(2, 32); // create more components this.initComponents(); this.frame.pack(); } /** * @return value in dimension-based converter * @since 2019-04-13 * @since v0.2.0 */ public String getDimensionConverterInput() { return this.valueInput.getText(); } /** * @return selection in "From" selector in dimension-based converter * @since 2019-04-13 * @since v0.2.0 */ public String getFromSelection() { return this.fromSearch.getSelectedValue(); } /** * @return text in "From" box in converter panel * @since 2019-01-15 * @since v0.1.0 */ public String getFromText() { return this.fromEntry.getText(); } /** * @return index of selected prefix in prefix viewer * @since 2019-01-15 * @since v0.1.0 */ public String getPrefixViewerSelection() { return this.prefixNameList.getSelectedValue(); } /** * @return selection in "To" selector in dimension-based converter * @since 2019-04-13 * @since v0.2.0 */ public String getToSelection() { return this.toSearch.getSelectedValue(); } /** * @return text in "To" box in converter panel * @since 2019-01-26 * @since v0.1.0 */ public String getToText() { return this.toEntry.getText(); } /** * @return index of selected unit in unit viewer * @since 2019-01-15 * @since v0.1.0 */ public String getUnitViewerSelection() { return this.unitNameList.getSelectedValue(); } /** * Starts up the application. * * @since 2018-12-27 * @since v0.1.0 */ public final void init() { this.frame.setVisible(true); } /** * Initializes the view's components. * * @since 2018-12-27 * @since v0.1.0 */ private final void initComponents() { final JPanel masterPanel = new JPanel(); this.frame.add(masterPanel); masterPanel.setLayout(new BorderLayout()); { // pane with all of the tabs final JTabbedPane masterPane = new JTabbedPane(); masterPanel.add(masterPane, BorderLayout.CENTER); { // a panel for unit conversion using a selector final JPanel convertUnitPanel = new JPanel(); masterPane.addTab("Convert Units", convertUnitPanel); convertUnitPanel.setLayout(new BorderLayout()); { // panel for input part final JPanel inputPanel = new JPanel(); convertUnitPanel.add(inputPanel, BorderLayout.CENTER); inputPanel.setLayout(new GridLayout(1, 3)); final JComboBox dimensionSelector = new JComboBox<>( this.presenter.dimensionNameList().toArray(new String[0])); dimensionSelector.setSelectedItem("LENGTH"); // handle dimension filter final MutablePredicate dimensionFilter = new MutablePredicate<>(s -> true); // panel for From things inputPanel.add(this.fromSearch); this.fromSearch.addSearchFilter(dimensionFilter); { // for dimension selector and arrow that represents conversion final JPanel inBetweenPanel = new JPanel(); inputPanel.add(inBetweenPanel); inBetweenPanel.setLayout(new BorderLayout()); { // dimension selector inBetweenPanel.add(dimensionSelector, BorderLayout.PAGE_START); } { // the arrow in the middle final JLabel arrowLabel = new JLabel("->"); inBetweenPanel.add(arrowLabel, BorderLayout.CENTER); } } // panel for To things inputPanel.add(this.toSearch); this.toSearch.addSearchFilter(dimensionFilter); // code for dimension filter dimensionSelector.addItemListener(e -> { dimensionFilter.setPredicate(string -> View.this.presenter.unitMatchesDimension(string, (String) dimensionSelector.getSelectedItem())); this.fromSearch.reapplyFilter(); this.toSearch.reapplyFilter(); }); // apply the item listener once because I have a default selection dimensionFilter.setPredicate(string -> View.this.presenter.unitMatchesDimension(string, (String) dimensionSelector.getSelectedItem())); this.fromSearch.reapplyFilter(); this.toSearch.reapplyFilter(); } { // panel for submit and output, and also value entry final JPanel outputPanel = new JPanel(); convertUnitPanel.add(outputPanel, BorderLayout.PAGE_END); outputPanel.setLayout(new GridLayout(3, 1)); { // unit input final JPanel valueInputPanel = new JPanel(); outputPanel.add(valueInputPanel); valueInputPanel.setLayout(new BorderLayout()); { // prompt final JLabel valuePrompt = new JLabel("Value to convert: "); valueInputPanel.add(valuePrompt, BorderLayout.LINE_START); } { // value to convert valueInputPanel.add(this.valueInput, BorderLayout.CENTER); } } { // button to convert final JButton convertButton = new JButton("Convert"); outputPanel.add(convertButton); convertButton.addActionListener(e -> this.presenter.convertDimensionBased()); } { // output of conversion outputPanel.add(this.dimensionBasedOutput); this.dimensionBasedOutput.setEditable(false); } } } { // panel for unit conversion using expressions final JPanel convertExpressionPanel = new JPanel(); masterPane.addTab("Convert Unit Expressions", convertExpressionPanel); convertExpressionPanel.setLayout(new GridLayout(5, 1)); { // panel for units to convert from final JPanel fromPanel = new JPanel(); convertExpressionPanel.add(fromPanel); fromPanel.setBorder(BorderFactory.createTitledBorder("From")); fromPanel.setLayout(new GridLayout(1, 1)); { // entry for units fromPanel.add(this.fromEntry); } } { // panel for units to convert to final JPanel toPanel = new JPanel(); convertExpressionPanel.add(toPanel); toPanel.setBorder(BorderFactory.createTitledBorder("To")); toPanel.setLayout(new GridLayout(1, 1)); { // entry for units toPanel.add(this.toEntry); } } { // button to convert final JButton convertButton = new JButton("Convert!"); convertExpressionPanel.add(convertButton); convertButton.addActionListener(e -> this.presenter.convertExpressions()); } { // output of conversion final JPanel outputPanel = new JPanel(); convertExpressionPanel.add(outputPanel); outputPanel.setBorder(BorderFactory.createTitledBorder("Output")); outputPanel.setLayout(new GridLayout(1, 1)); { // output outputPanel.add(this.output); this.output.setEditable(false); } } { // panel for specifying precision final JPanel sigDigPanel = new JPanel(); convertExpressionPanel.add(sigDigPanel); sigDigPanel.setBorder(BorderFactory.createTitledBorder("Significant Digits")); { // slider final JSlider sigDigSlider = new JSlider(0, 12); sigDigPanel.add(sigDigSlider); sigDigSlider.setMajorTickSpacing(4); sigDigSlider.setMinorTickSpacing(1); sigDigSlider.setSnapToTicks(true); sigDigSlider.setPaintTicks(true); sigDigSlider.setPaintLabels(true); sigDigSlider.addChangeListener( e -> this.presenter.setSignificantFigures(sigDigSlider.getValue())); } } } { // panel to look up units final JPanel unitLookupPanel = new JPanel(); masterPane.addTab("Unit Viewer", unitLookupPanel); unitLookupPanel.setLayout(new GridLayout()); { // search panel unitLookupPanel.add(this.unitNameList); this.unitNameList.getSearchList() .addListSelectionListener(e -> this.presenter.unitNameSelected()); } { // the text box for unit's toString unitLookupPanel.add(this.unitTextBox); this.unitTextBox.setEditable(false); this.unitTextBox.setLineWrap(true); } } { // panel to look up prefixes final JPanel prefixLookupPanel = new JPanel(); masterPane.addTab("Prefix Viewer", prefixLookupPanel); prefixLookupPanel.setLayout(new GridLayout(1, 2)); { // panel for listing and seaching prefixLookupPanel.add(this.prefixNameList); this.prefixNameList.getSearchList() .addListSelectionListener(e -> this.presenter.prefixSelected()); } { // the text box for prefix's toString prefixLookupPanel.add(this.prefixTextBox); this.prefixTextBox.setEditable(false); this.prefixTextBox.setLineWrap(true); } } } } /** * Sets the text in the output of the dimension-based converter. * * @param text * text to set * @since 2019-04-13 * @since v0.2.0 */ public void setDimensionConverterOutputText(final String text) { this.dimensionBasedOutput.setText(text); } /** * Sets the text in the output of the conversion panel. * * @param text * text to set * @since 2019-01-15 * @since v0.1.0 */ public void setExpressionConverterOutputText(final String text) { this.output.setText(text); } /** * Sets the text of the prefix text box in the prefix viewer. * * @param text * text to set * @since 2019-01-15 * @since v0.1.0 */ public void setPrefixTextBoxText(final String text) { this.prefixTextBox.setText(text); } /** * Sets the text of the unit text box in the unit viewer. * * @param text * text to set * @since 2019-01-15 * @since v0.1.0 */ public void setUnitTextBoxText(final String text) { this.unitTextBox.setText(text); } /** * Shows an error dialog. * * @param title * title of dialog * @param message * message in dialog * @since 2019-01-14 * @since v0.1.0 */ public void showErrorDialog(final String title, final String message) { JOptionPane.showMessageDialog(this.frame, message, title, JOptionPane.ERROR_MESSAGE); } } public static void main(final String[] args) { new View().init(); } }