summaryrefslogtreecommitdiff
path: root/src/unitConverter/converterGUI/UnitConverterGUI.java
blob: b54e0da14756c3fb764f5a228609f3324d1fc361 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
/**
 * 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 <https://www.gnu.org/licenses/>.
 */
package unitConverter.converterGUI;

import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.io.File;
import java.math.BigDecimal;
import java.math.MathContext;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel;

import unitConverter.UnitsDatabase;
import unitConverter.dimension.StandardDimensions;
import unitConverter.dimension.UnitDimension;
import unitConverter.unit.AbstractUnit;
import unitConverter.unit.NonlinearUnits;
import unitConverter.unit.SI;
import unitConverter.unit.Unit;
import unitConverter.unit.UnitPrefix;

/**
 * @author Adrien Hopkins
 * @since 2018-12-27
 * @since v0.1.0
 */
final class UnitConverterGUI {
	private static class Presenter {
		/** The presenter's associated view. */
		private final View view;

		/** The units known by the program. */
		private final UnitsDatabase units;

		/** The names of all of the units */
		private final List<String> unitNames;

		/** The names of all of the units, but filtered */
		private final DelegateListModel<String> unitNamesFiltered;

		/** The names of all of the prefixes */
		private final List<String> prefixNames;

		/** The names of all of the prefixes */
		private final DelegateListModel<String> prefixNamesFiltered;

		private final Comparator<String> 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.units = new UnitsDatabase();
			this.units.addUnit("metre", SI.METRE);
			this.units.addUnit("kilogram", SI.KILOGRAM);
			this.units.addUnit("gram", SI.KILOGRAM.dividedBy(1000));
			this.units.addUnit("second", SI.SECOND);
			this.units.addUnit("ampere", SI.AMPERE);
			this.units.addUnit("kelvin", SI.KELVIN);
			this.units.addUnit("mole", SI.MOLE);
			this.units.addUnit("candela", SI.CANDELA);
			this.units.addUnit("bit", SI.SI.getBaseUnit(StandardDimensions.INFORMATION));
			this.units.addUnit("unit", SI.SI.getBaseUnit(UnitDimension.EMPTY));
			// nonlinear units - must be loaded manually
			this.units.addUnit("tempCelsius", NonlinearUnits.CELSIUS);
			this.units.addUnit("tempFahrenheit", NonlinearUnits.FAHRENHEIT);

			this.units.addAllFromFile(new File("unitsfile.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.units.containsPrefixName(o1))
					return -1;
				else if (!Presenter.this.units.containsPrefixName(o2))
					return 1;

				final UnitPrefix p1 = Presenter.this.units.getPrefix(o1);
				final UnitPrefix p2 = Presenter.this.units.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.units.prefixlessUnitNameSet());
			this.unitNames.sort(null); // sorts it using Comparable

			this.unitNamesFiltered = new DelegateListModel<>(new ArrayList<>(this.units.prefixlessUnitNameSet()));
			this.unitNamesFiltered.sort(null); // sorts it using Comparable

			this.prefixNames = new ArrayList<>(this.units.prefixNameSet());
			this.prefixNames.sort(this.prefixNameComparator); // sorts it using my comparator

			this.prefixNamesFiltered = new DelegateListModel<>(new ArrayList<>(this.units.prefixNameSet()));
			this.prefixNamesFiltered.sort(this.prefixNameComparator); // sorts it using my comparator

			System.out.printf("Successfully loaded %d units (%d base units)", AbstractUnit.getUnitCount(),
					AbstractUnit.getBaseUnitCount());
		}

		/**
		 * Runs whenever the convert button is pressed.
		 * 
		 * <p>
		 * 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.
		 * </p>
		 * 
		 * @since 2019-01-26
		 * @since v0.1.0
		 */
		public final void convert() {
			final String fromUnitString = this.view.getFromText();
			final String toUnitString = this.view.getToText();

			// try to parse from
			final Unit from;
			try {
				from = this.units.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 {
				to = this.units.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 = to.convertFromBase(from.convertToBase(1));

			// 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);
				}
			}

			this.view.setOutputText(String.format("%s = %s %s", fromUnitString, output, toUnitString));
		}

		/**
		 * Filters the filtered model for units
		 * 
		 * @param filter
		 *            filter to use
		 * @since 2019-01-15
		 * @since v0.1.0
		 */
		private final void filterFilteredPrefixModel(final Predicate<String> filter) {
			this.prefixNamesFiltered.clear();
			for (final String prefixName : this.prefixNames) {
				if (filter.test(prefixName)) {
					this.prefixNamesFiltered.add(prefixName);
				}
			}
		}

		/**
		 * Filters the filtered model for units
		 * 
		 * @param filter
		 *            filter to use
		 * @since 2019-01-15
		 * @since v0.1.0
		 */
		private final void filterFilteredUnitModel(final Predicate<String> filter) {
			this.unitNamesFiltered.clear();
			for (final String unitName : this.unitNames) {
				if (filter.test(unitName)) {
					this.unitNamesFiltered.add(unitName);
				}
			}
		}

		/**
		 * @return a list model of all of the unit keys
		 * @since 2019-01-14
		 * @since v0.1.0
		 */
		public final ListModel<String> keyListModel() {
			return this.unitNamesFiltered;
		}

		/**
		 * Runs whenever the prefix filter is changed.
		 * <p>
		 * Filters the prefix list then sorts it using a {@code FilterComparator}.
		 * </p>
		 * 
		 * @since 2019-01-15
		 * @since v0.1.0
		 */
		public final void prefixFilterUpdated() {
			final String filter = this.view.getPrefixFilterText();
			if (filter.equals("")) {
				this.filterFilteredPrefixModel(t -> true);
			} else {
				this.filterFilteredPrefixModel(t -> t.contains(filter));
			}
			this.prefixNamesFiltered.sort(new FilterComparator(filter));
		}

		/**
		 * @return a list model of all of the prefix names
		 * @since 2019-01-15
		 */
		public final ListModel<String> prefixNameListModel() {
			return this.prefixNamesFiltered;
		}

		/**
		 * Runs whenever a prefix is selected in the viewer.
		 * <p>
		 * Shows its information in the text box to the right.
		 * </p>
		 * 
		 * @since 2019-01-15
		 * @since v0.1.0
		 */
		public final void prefixSelected() {
			final int index = this.view.getPrefixListSelection();
			if (index == -1)
				return;
			else {
				final String prefixName = this.prefixNamesFiltered.get(index);
				final UnitPrefix prefix = this.units.getPrefix(prefixName);

				this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s", prefixName, prefix.getMultiplier()));
			}
		}

		/**
		 * @param significantFigures
		 *            new value of significantFigures
		 * @since 2019-01-15
		 */
		public final void setSignificantFigures(final int significantFigures) {
			this.significantFigures = significantFigures;
		}

		/**
		 * Runs whenever the unit filter is changed.
		 * <p>
		 * Filters the unit list then sorts it using a {@code FilterComparator}.
		 * </p>
		 * 
		 * @since 2019-01-15
		 * @since v0.1.0
		 */
		public final void unitFilterUpdated() {
			final String filter = this.view.getUnitFilterText();
			if (filter.equals("")) {
				this.filterFilteredUnitModel(t -> true);
			} else {
				this.filterFilteredUnitModel(t -> t.contains(filter));
			}
			this.unitNamesFiltered.sort(new FilterComparator(filter));
		}

		/**
		 * Runs whenever a unit is selected in the viewer.
		 * <p>
		 * Shows its information in the text box to the right.
		 * </p>
		 * 
		 * @since 2019-01-15
		 * @since v0.1.0
		 */
		public void unitNameSelected() {
			final int index = this.view.getUnitListSelection();
			if (index == -1)
				return;
			else {
				final String unitName = this.unitNamesFiltered.get(index);
				final Unit unit = this.units.getUnit(unitName);

				this.view.setUnitTextBoxText(unit.toString());
			}
		}
	}

	private static class View {
		/** The view's frame. */
		private final JFrame frame;
		/** The view's associated presenter. */
		private final Presenter presenter;

		/** The list of unit names in the unit viewer */
		private final JList<String> unitNameList;
		/** The list of prefix names in the prefix viewer */
		private final JList<String> prefixNameList;
		/** The unit search box in the unit viewer */
		private final JTextField unitFilterEntry;
		/** The text box for unit data in the unit viewer */
		private final JTextArea unitTextBox;
		/** The prefix search box in the prefix viewer */
		private final JTextField prefixFilterEntry;
		/** The text box for prefix data in the prefix viewer */
		private final JTextArea prefixTextBox;
		/** 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;

		/**
		 * 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 JList<>(this.presenter.keyListModel());
			this.prefixNameList = new JList<>(this.presenter.prefixNameListModel());
			this.unitFilterEntry = new JTextField();
			this.unitTextBox = new JTextArea();
			this.prefixFilterEntry = new JTextField();
			this.prefixTextBox = new JTextArea();
			this.fromEntry = new JTextField();
			this.toEntry = new JTextField();
			this.output = new JTextArea(2, 32);

			// create more components
			this.initComponents();

			this.frame.pack();
		}

		/**
		 * @return text in "From" box in converter panel
		 * @since 2019-01-15
		 * @since v0.1.0
		 */
		public String getFromText() {
			return this.fromEntry.getText();
		}

		/**
		 * @return text in prefix filter
		 * @since 2019-01-15
		 * @since v0.1.0
		 */
		public String getPrefixFilterText() {
			return this.prefixFilterEntry.getText();
		}

		/**
		 * @return index of selected prefix
		 * @since 2019-01-15
		 * @since v0.1.0
		 */
		public int getPrefixListSelection() {
			return this.prefixNameList.getSelectedIndex();
		}

		/**
		 * @return text in "To" box in converter panel
		 * @since 2019-01-26
		 * @since v0.1.0
		 */
		public String getToText() {
			return this.toEntry.getText();
		}

		/**
		 * @return text in unit filter
		 * @see javax.swing.text.JTextComponent#getText()
		 */
		public String getUnitFilterText() {
			return this.unitFilterEntry.getText();
		}

		/**
		 * @return index of selected unit
		 * @since 2019-01-15
		 * @since v0.1.0
		 */
		public int getUnitListSelection() {
			return this.unitNameList.getSelectedIndex();
		}

		/**
		 * 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);

				{ // panel for unit conversion
					final JPanel convertPanel = new JPanel();
					masterPane.addTab("Convert Units", convertPanel);

					convertPanel.setLayout(new GridLayout(5, 1));

					{ // panel for units to convert from
						final JPanel fromPanel = new JPanel();
						convertPanel.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();
						convertPanel.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!");
						convertPanel.add(convertButton);

						convertButton.addActionListener(e -> this.presenter.convert());
					}

					{ // output of conversion
						final JPanel outputPanel = new JPanel();
						convertPanel.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();
						convertPanel.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());

					{ // panel for listing and searching
						final JPanel listPanel = new JPanel();
						unitLookupPanel.add(listPanel);

						listPanel.setLayout(new BorderLayout());

						{ // unit search box
							listPanel.add(this.unitFilterEntry, BorderLayout.PAGE_START);
							this.unitFilterEntry.addCaretListener(e -> this.presenter.unitFilterUpdated());
						}

						{ // a list of units
							listPanel.add(new JScrollPane(this.unitNameList), BorderLayout.CENTER);
							this.unitNameList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // temp
							this.unitNameList.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
						final JPanel prefixListPanel = new JPanel();
						prefixLookupPanel.add(prefixListPanel);

						prefixListPanel.setLayout(new BorderLayout());

						{ // prefix search box
							prefixListPanel.add(this.prefixFilterEntry, BorderLayout.PAGE_START);
							this.prefixFilterEntry.addCaretListener(e -> this.presenter.prefixFilterUpdated());
						}

						{ // a list of prefixes
							prefixListPanel.add(new JScrollPane(this.prefixNameList), BorderLayout.CENTER);
							this.prefixNameList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // temp
							this.prefixNameList.addListSelectionListener(e -> {
								this.presenter.prefixSelected();
							});
						}
					}

					{ // the text box for prefix's toString
						prefixLookupPanel.add(this.prefixTextBox);
						this.unitTextBox.setEditable(false);
					}
				}
			}
		}

		/**
		 * 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 setOutputText(final String text) {
			this.output.setText(text);
		}

		/**
		 * Sets the text of the prefix text box.
		 * 
		 * @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.
		 * 
		 * @param t
		 *            text to set
		 * @see javax.swing.text.JTextComponent#setText(java.lang.String)
		 */
		public void setUnitTextBoxText(final String t) {
			this.unitTextBox.setText(t);
		}

		/**
		 * 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();
	}
}