From 1727820320e96228d3dc506f4dafbaf3da16fabb Mon Sep 17 00:00:00 2001 From: lopeznr1 Date: Tue, 17 Sep 2024 20:43:19 -0400 Subject: [PATCH] Changes for 2.0.0: Bulk updates relating to the 2.0.0 public release of GLUM. --- .classpath | 8 +- .gitignore | 4 + .settings/org.eclipse.jdt.core.prefs | 460 +++++++++ .settings/org.eclipse.jdt.ui.prefs | 63 ++ LICENSE | 202 ++++ README.md | 50 + release/glum-2.0.0-src.jar | Bin 0 -> 404702 bytes release/glum-2.0.0.jar | Bin 0 -> 473407 bytes src/glum/app/AppGlum.java | 32 + src/glum/app/AppInfo.java | 39 + src/glum/color/ColorHSBL.java | 121 +++ src/glum/color/ColorHSBLCompartor.java | 125 +++ src/glum/color/ColorTestApp.java | 261 +++++ src/glum/color/KeyAttr.java | 31 + src/glum/coord/Convert.java | 95 -- src/glum/coord/CoordUtil.java | 197 ---- src/glum/coord/Epsilon.java | 43 - src/glum/coord/GeoUtil.java | 97 -- src/glum/coord/LatLon.java | 111 --- src/glum/coord/Point2D.java | 37 - src/glum/coord/Point2Di.java | 27 - src/glum/coord/Point3D.java | 42 - src/glum/coord/RngBrg.java | 47 - src/glum/coord/UV.java | 26 - src/glum/database/QueryItem.java | 13 + src/glum/database/QueryItemComparator.java | 42 +- src/glum/digest/Digest.java | 101 ++ src/glum/digest/DigestType.java | 64 ++ src/glum/digest/DigestUtils.java | 83 ++ src/glum/filter/EnumFilter.java | 90 +- src/glum/filter/Filter.java | 20 + src/glum/filter/FilterUtil.java | 60 +- src/glum/filter/NullFilter.java | 23 +- src/glum/filter/RangeFilter.java | 46 +- src/glum/gui/FocusUtil.java | 13 + src/glum/gui/GuiExeUtil.java | 48 + src/glum/gui/GuiPaneUtil.java | 91 ++ src/glum/gui/GuiUtil.java | 540 +++++------ src/glum/gui/GuiUtilEx.java | 88 ++ src/glum/gui/TableUtil.java | 185 ++++ .../gui/action/ActionComponentProvider.java | 33 + src/glum/gui/action/ClickAction.java | 34 +- src/glum/gui/action/CloseDialog.java | 82 ++ src/glum/gui/action/MakeVisibleAction.java | 46 +- src/glum/gui/action/PopAction.java | 66 ++ src/glum/gui/action/PopupMenu.java | 93 ++ src/glum/gui/component/GBaseTextField.java | 80 ++ src/glum/gui/component/GComboBox.java | 180 +++- src/glum/gui/component/GComponent.java | 43 +- src/glum/gui/component/GFancyLabel.java | 75 +- src/glum/gui/component/GLabel.java | 26 +- src/glum/gui/component/GList.java | 48 +- src/glum/gui/component/GNumberField.java | 404 ++++++-- .../gui/component/GNumberFieldSlider.java | 218 +++++ src/glum/gui/component/GPasswordField.java | 13 + src/glum/gui/component/GSlider.java | 127 ++- src/glum/gui/component/GTextField.java | 75 +- src/glum/gui/component/GToggle.java | 76 -- src/glum/gui/component/banner/Banner.java | 64 +- .../gui/component/banner/BannerConfig.java | 13 + .../gui/component/model/GComboBoxModel.java | 56 +- src/glum/gui/component/model/GListModel.java | 95 +- src/glum/gui/dnd/PlainTransferHandler.java | 24 +- src/glum/gui/dock/BaseDockable.java | 29 +- src/glum/gui/dock/CloseableDockable.java | 13 + .../gui/dock/CustomPlaceholderStrategy.java | 13 + src/glum/gui/dock/DockUtil.java | 122 ++- src/glum/gui/dock/FrontendAddConfigPanel.java | 48 +- .../gui/dock/FrontendManageConfigPanel.java | 179 ++-- src/glum/gui/dock/LookUp.java | 13 + .../gui/dock/PlainDockSituationIgnore.java | 44 +- src/glum/gui/dock/PrimConfig.java | 286 +++--- src/glum/gui/dock/PrimDock.java | 37 +- src/glum/gui/dock/PrimDockFactory.java | 93 +- src/glum/gui/dock/PrimDockable.java | 28 +- src/glum/gui/dock/action/Closeable.java | 13 + src/glum/gui/dock/action/Destroyable.java | 13 + src/glum/gui/dock/action/DismissAction.java | 13 + .../gui/dock/action/MakeVisibleAction.java | 13 + .../gui/dock/action/SimpleDockAction.java | 13 + src/glum/gui/dock/action/ToggleAction.java | 36 +- src/glum/gui/dock/alt/AltScreenDockFrame.java | 13 + .../gui/dock/alt/AltScreenDockStation.java | 62 +- .../dock/alt/AltScreenDockWindowFactory.java | 13 + .../gui/dock/alt/AltSplitDockStation.java | 94 +- src/glum/gui/document/BaseDocument.java | 22 +- src/glum/gui/document/BaseNumberDocument.java | 31 +- src/glum/gui/document/CharDocument.java | 68 +- src/glum/gui/document/NumberDocument.java | 64 +- src/glum/gui/icon/ArrowNorthIcon.java | 13 + src/glum/gui/icon/ArrowSouthIcon.java | 13 + src/glum/gui/icon/BaseIcon.java | 13 + src/glum/gui/icon/DeleteIcon.java | 13 + src/glum/gui/icon/EmptyIcon.java | 36 +- src/glum/gui/icon/IconUtil.java | 45 +- src/glum/gui/info/FilePathInfo.java | 13 + src/glum/gui/info/WindowCfg.java | 162 ++++ src/glum/gui/info/WindowInfo.java | 77 -- .../{dialog => memory}/MemoryUtilDialog.java | 78 +- src/glum/gui/misc/BooleanCellEditor.java | 61 +- src/glum/gui/misc/BooleanCellRenderer.java | 25 +- src/glum/gui/misc/ColorCellRenderer.java | 38 +- src/glum/gui/misc/CustomLCR.java | 79 ++ src/glum/gui/misc/MultiState.java | 13 + src/glum/gui/misc/MultiStateCheckBox.java | 43 +- .../misc/MultiStateCheckBoxCellEditor.java | 13 + .../misc/MultiStateCheckBoxCellRenderer.java | 13 + .../gui/misc/MultiStateCheckBoxHeader.java | 43 +- .../misc/MultiStateCheckBoxHeaderTest.java | 13 + src/glum/gui/misc/MultiStateCheckBoxTest.java | 13 + src/glum/gui/misc/MultiStateIcon.java | 13 + src/glum/gui/misc/MultiStateModel.java | 13 + src/glum/gui/misc/SimpleTitledBorder.java | 13 + src/glum/gui/panel/CardPanel.java | 112 +-- src/glum/gui/panel/ColorInputPanel.java | 219 +++-- src/glum/gui/panel/ColorPanel.java | 21 +- src/glum/gui/panel/ComponentTracker.java | 91 +- src/glum/gui/panel/CredentialPanel.java | 121 ++- .../gui/panel/CustomFocusTraversalPolicy.java | 139 +-- src/glum/gui/panel/FontInputPanel.java | 209 ++++ src/glum/gui/panel/GPanel.java | 63 +- src/glum/gui/panel/GlassPane.java | 295 +++--- src/glum/gui/panel/GlassPaneAncient.java | 13 + src/glum/gui/panel/GlassPanel.java | 56 +- src/glum/gui/panel/NoticePanel.java | 187 ++++ src/glum/gui/panel/PaneType.java | 13 + src/glum/gui/panel/ShadePane.java | 13 + src/glum/gui/panel/StandardPane.java | 40 +- src/glum/gui/panel/WaftPanel.java | 136 +-- .../gui/panel/generic/BaseTextInputPanel.java | 163 ++++ src/glum/gui/panel/generic/GenericCodes.java | 13 + src/glum/gui/panel/generic/LocationPanel.java | 232 +++-- src/glum/gui/panel/generic/MessagePanel.java | 102 +- src/glum/gui/panel/generic/PromptPanel.java | 123 ++- .../gui/panel/generic/SimplePromptPanel.java | 51 +- .../gui/panel/generic/TextInputPanel.java | 281 +++--- .../gui/panel/itemList/BasicItemHandler.java | 569 ----------- .../panel/itemList/BasicItemProcessor.java | 43 +- .../panel/itemList/FilterItemProcessor.java | 52 +- .../panel/itemList/ItemChangeListener.java | 10 - src/glum/gui/panel/itemList/ItemHandler.java | 60 +- .../gui/panel/itemList/ItemListPanel.java | 532 ++++++----- .../panel/itemList/ItemListTableModel.java | 148 +-- .../gui/panel/itemList/ItemProcessor.java | 52 +- .../gui/panel/itemList/RegistryProcessor.java | 54 +- .../gui/panel/itemList/SearchBoxRenderer.java | 40 +- .../panel/itemList/StaticItemProcessor.java | 52 +- .../panel/itemList/TableColumnHandler.java | 521 ++++++++++ .../itemList/config/AddProfilePanel.java | 22 +- .../panel/itemList/config/ConfigHandler.java | 83 +- .../panel/itemList/config/ConfigLookUp.java | 13 + .../panel/itemList/config/EditTablePanel.java | 473 ++++----- .../config/EditTablePanelClassic.java | 145 +-- .../panel/itemList/config/ProfileConfig.java | 256 ++--- .../panel/itemList/query/QueryAttribute.java | 100 +- .../panel/itemList/query/QueryComposer.java | 148 +-- .../itemList/query/QueryItemHandler.java | 73 +- .../query/QueryTableCellRenderer.java | 35 +- src/glum/gui/panel/nub/Action.java | 13 + src/glum/gui/panel/nub/HorizontalNub.java | 185 ++-- src/glum/gui/panel/task/AutoTaskPanel.java | 141 +-- src/glum/gui/panel/task/BaseTaskPanel.java | 92 +- src/glum/gui/panel/task/DualTaskPanel.java | 69 +- src/glum/gui/panel/task/FullTaskPanel.java | 145 +-- src/glum/gui/panel/task/PlainTaskPanel.java | 90 +- src/glum/gui/table/BaseTableModel.java | 90 ++ src/glum/gui/table/ColorCellRenderer.java | 74 ++ src/glum/gui/table/ColorHSBLCellRenderer.java | 76 ++ src/glum/gui/table/KeyValueTableModel.java | 88 +- src/glum/gui/table/NumberRenderer.java | 77 ++ src/glum/gui/table/PrePendRenderer.java | 53 ++ src/glum/gui/table/Scrolling.java | 13 + src/glum/gui/table/SortDir.java | 28 + src/glum/gui/table/TablePopupHandler.java | 74 ++ src/glum/gui/table/TableSorter.java | 899 ++++++++++-------- .../table/sort/DefaultSortIconProvider.java | 75 ++ src/glum/gui/table/sort/SortArrow.java | 100 ++ src/glum/gui/table/sort/SortArrowLegacy.java | 95 ++ src/glum/gui/table/sort/SortIconProvider.java | 46 + src/glum/gui/unit/DateUnitPanel.java | 148 +-- src/glum/gui/unit/DecimalUnitPanel.java | 79 +- src/glum/gui/unit/EditorPanel.java | 13 + src/glum/gui/unit/LatLonUnitPanel.java | 196 ---- src/glum/gui/unit/TimeZoneCellRenderer.java | 37 +- .../gui/unit/UnitConfigurationDialog.java | 186 ++-- src/glum/gui/unit/UnitLabelRenderer.java | 34 +- src/glum/io/ConfigMapTP.java | 33 +- src/glum/io/IoUtil.java | 143 ++- src/glum/io/Loader.java | 318 +++---- src/glum/io/LoaderInfo.java | 105 +- src/glum/io/NullOutputStream.java | 24 +- src/glum/io/ParseUtil.java | 148 +++ src/glum/io/RegExFileFilter.java | 36 +- src/glum/io/SimpleFileFilter.java | 76 +- src/glum/io/StandardFileChooser.java | 22 +- src/glum/io/TokenProcessor.java | 27 +- src/glum/io/WarningTP.java | 55 +- src/glum/io/token/BaseTokenizer.java | 35 +- src/glum/io/token/MatchTokenizer.java | 54 +- src/glum/io/token/SplitTokenizer.java | 43 +- src/glum/io/token/TokenUtil.java | 54 +- src/glum/io/token/Tokenizer.java | 20 +- src/glum/item/BaseItemManager.java | 133 +++ src/glum/item/ConstIdGenerator.java | 38 + src/glum/item/IdGenerator.java | 29 + src/glum/item/IncrIdGenerator.java | 45 + src/glum/item/ItemEventListener.java | 33 + src/glum/item/ItemEventType.java | 38 + src/glum/item/ItemGroup.java | 42 + src/glum/item/ItemManager.java | 60 ++ src/glum/item/ItemManagerUtil.java | 55 ++ src/glum/logic/LogicChunk.java | 29 +- src/glum/logic/LogicChunkEngine.java | 223 +++-- src/glum/logic/MenuItemChunk.java | 22 +- src/glum/logic/ShutdownHook.java | 152 +-- src/glum/logic/SubMenuChunk.java | 24 +- src/glum/logic/dock/FrontendLoadMI.java | 72 +- src/glum/logic/dock/FrontendManageMI.java | 50 +- src/glum/logic/dock/FrontendSaveMI.java | 48 +- src/glum/logic/dock/FrontendShowMI.java | 71 +- src/glum/logic/misc/ConsoleEchoMI.java | 47 +- src/glum/logic/misc/MemoryDialogMI.java | 48 +- src/glum/math/FastMath.java | 614 ------------ src/glum/math/FractionTransformer.java | 93 -- src/glum/math/GradientMesh.java | 170 ---- src/glum/math/GroupNoise.java | 94 -- src/glum/math/Noise.java | 28 - src/glum/math/PerlinNoise.java | 367 ------- src/glum/math/TransformNoise.java | 44 - src/glum/math/TurbulentNoise.java | 40 - src/glum/math/ValueNoise.java | 422 -------- src/glum/math/ViolentNoise1.java.misc | 79 -- src/glum/math/ViolentNoise2.java.misc | 79 -- src/glum/misc/InitListener.java | 31 + src/glum/net/Credential.java | 28 +- src/glum/net/FetchError.java | 47 + src/glum/net/HostUtil.java | 144 +++ src/glum/net/NetUtil.java | 257 ++++- src/glum/net/Result.java | 13 + src/glum/net/UrlUtil.java | 123 +++ src/glum/reflect/Function.java | 6 +- src/glum/reflect/ReflectUtil.java | 300 +++--- src/glum/registry/ConfigMap.java | 13 + src/glum/registry/Registry.java | 252 ++--- src/glum/registry/ResourceListener.java | 13 + src/glum/registry/SelectionListener.java | 13 + src/glum/registry/SelectionManager.java | 101 +- src/glum/source/LocalSource.java | 118 +++ src/glum/source/PlainSource.java | 140 +++ src/glum/source/Source.java | 61 ++ src/glum/source/SourceState.java | 35 + src/glum/source/SourceUtil.java | 277 ++++++ src/glum/task/BufferTask.java | 172 ++++ src/glum/task/ConsoleTask.java | 105 +- src/glum/task/CountTask.java | 96 +- src/glum/task/FileLogTask.java | 166 ---- src/glum/task/IndentTask.java | 153 +++ src/glum/task/InvalidTask.java | 121 +++ src/glum/task/NotifyTask.java | 255 +++++ src/glum/task/PartialTask.java | 93 +- src/glum/task/PrintStreamTask.java | 188 ++++ src/glum/task/SilentTask.java | 84 +- src/glum/task/SplitTask.java | 118 ++- src/glum/task/Task.java | 112 ++- src/glum/task/TaskListener.java | 28 + src/glum/task/TaskState.java | 20 - src/glum/text/SigFigNumberFormat.java | 115 +++ src/glum/unit/BaseUnitProvider.java | 33 +- src/glum/unit/ByteUnit.java | 64 +- src/glum/unit/ConstUnitProvider.java | 36 +- src/glum/unit/Convert.java | 13 + src/glum/unit/DateTimeUnit.java | 113 +++ src/glum/unit/DateUnit.java | 17 +- src/glum/unit/DateUnitProvider.java | 48 +- src/glum/unit/DecimalUnitProvider.java | 99 +- src/glum/unit/EmptyUnitProvider.java | 30 +- src/glum/unit/HeuristicUnit.java | 55 +- src/glum/unit/LatLonUnitProvider.java | 173 ---- src/glum/unit/LatUnit.java | 94 -- src/glum/unit/LonUnit.java | 94 -- src/glum/unit/NumberInverseUnit.java | 21 +- src/glum/unit/NumberUnit.java | 103 +- src/glum/unit/ShiftedUnit.java | 52 +- src/glum/unit/TimeCountUnit.java | 81 +- src/glum/unit/Unit.java | 48 +- src/glum/unit/UnitListener.java | 20 +- src/glum/unit/UnitProvider.java | 30 +- src/glum/unit/UnitUtil.java | 43 + src/glum/util/DateUtil.java | 15 +- src/glum/util/ImageUtil.java | 13 + src/glum/util/MathUtil.java | 13 + src/glum/util/ThreadUtil.java | 163 ++-- src/glum/util/TimeConst.java | 13 + src/glum/util/WallTimer.java | 13 + src/glum/version/PlainVersion.java | 126 +++ src/glum/version/Version.java | 49 + src/glum/version/VersionUtils.java | 109 +++ src/glum/zio/NullableZRS.java | 108 +++ src/glum/zio/ZinStream.java | 52 +- src/glum/zio/ZioObj.java | 19 +- src/glum/zio/ZioObjUtil.java | 89 +- src/glum/zio/ZioRaw.java | 23 +- src/glum/zio/ZioRawUtil.java | 69 -- src/glum/zio/ZioSpawner.java | 35 + src/glum/zio/ZoutStream.java | 49 +- src/glum/zio/stream/BaseZinStream.java | 77 +- src/glum/zio/stream/BaseZoutStream.java | 79 +- src/glum/zio/stream/ByteArrayZinStream.java | 30 +- src/glum/zio/stream/ByteArrayZoutStream.java | 28 +- src/glum/zio/stream/DebugZoutStream.java | 48 +- src/glum/zio/stream/FileZinStream.java | 37 +- src/glum/zio/stream/FileZoutStream.java | 37 +- src/glum/zio/stream/NullZoutStream.java | 42 +- src/glum/zio/util/WrapInputStream.java | 28 +- src/glum/zio/util/WrapOutputStream.java | 35 +- src/glum/zio/util/WrapZinStream.java | 51 +- src/glum/zio/util/WrapZoutStream.java | 29 +- src/glum/zio/util/ZioRawUtil.java | 157 +++ src/glum/zio/util/ZioUtil.java | 144 ++- src/glum/zio/util/ZioWrapUtil.java | 38 +- src/module-info.java.unused | 38 + tools/build.xml | 66 ++ tools/buildRelease | 305 ++++++ 323 files changed, 18557 insertions(+), 10432 deletions(-) create mode 100644 .gitignore create mode 100644 .settings/org.eclipse.jdt.core.prefs create mode 100644 .settings/org.eclipse.jdt.ui.prefs create mode 100644 LICENSE create mode 100644 README.md create mode 100644 release/glum-2.0.0-src.jar create mode 100644 release/glum-2.0.0.jar create mode 100644 src/glum/app/AppGlum.java create mode 100644 src/glum/app/AppInfo.java create mode 100644 src/glum/color/ColorHSBL.java create mode 100644 src/glum/color/ColorHSBLCompartor.java create mode 100644 src/glum/color/ColorTestApp.java create mode 100644 src/glum/color/KeyAttr.java delete mode 100644 src/glum/coord/Convert.java delete mode 100644 src/glum/coord/CoordUtil.java delete mode 100644 src/glum/coord/Epsilon.java delete mode 100644 src/glum/coord/GeoUtil.java delete mode 100644 src/glum/coord/LatLon.java delete mode 100644 src/glum/coord/Point2D.java delete mode 100644 src/glum/coord/Point2Di.java delete mode 100644 src/glum/coord/Point3D.java delete mode 100644 src/glum/coord/RngBrg.java delete mode 100644 src/glum/coord/UV.java create mode 100644 src/glum/digest/Digest.java create mode 100644 src/glum/digest/DigestType.java create mode 100644 src/glum/digest/DigestUtils.java create mode 100644 src/glum/gui/GuiExeUtil.java create mode 100644 src/glum/gui/GuiPaneUtil.java create mode 100644 src/glum/gui/GuiUtilEx.java create mode 100644 src/glum/gui/TableUtil.java create mode 100644 src/glum/gui/action/ActionComponentProvider.java create mode 100644 src/glum/gui/action/CloseDialog.java create mode 100644 src/glum/gui/action/PopAction.java create mode 100644 src/glum/gui/action/PopupMenu.java create mode 100644 src/glum/gui/component/GBaseTextField.java create mode 100644 src/glum/gui/component/GNumberFieldSlider.java delete mode 100644 src/glum/gui/component/GToggle.java create mode 100644 src/glum/gui/info/WindowCfg.java delete mode 100644 src/glum/gui/info/WindowInfo.java rename src/glum/gui/{dialog => memory}/MemoryUtilDialog.java (59%) create mode 100644 src/glum/gui/misc/CustomLCR.java create mode 100644 src/glum/gui/panel/FontInputPanel.java create mode 100644 src/glum/gui/panel/NoticePanel.java create mode 100644 src/glum/gui/panel/generic/BaseTextInputPanel.java delete mode 100644 src/glum/gui/panel/itemList/BasicItemHandler.java delete mode 100644 src/glum/gui/panel/itemList/ItemChangeListener.java create mode 100644 src/glum/gui/panel/itemList/TableColumnHandler.java create mode 100644 src/glum/gui/table/BaseTableModel.java create mode 100644 src/glum/gui/table/ColorCellRenderer.java create mode 100644 src/glum/gui/table/ColorHSBLCellRenderer.java create mode 100644 src/glum/gui/table/NumberRenderer.java create mode 100644 src/glum/gui/table/PrePendRenderer.java create mode 100644 src/glum/gui/table/SortDir.java create mode 100644 src/glum/gui/table/TablePopupHandler.java create mode 100644 src/glum/gui/table/sort/DefaultSortIconProvider.java create mode 100644 src/glum/gui/table/sort/SortArrow.java create mode 100644 src/glum/gui/table/sort/SortArrowLegacy.java create mode 100644 src/glum/gui/table/sort/SortIconProvider.java delete mode 100644 src/glum/gui/unit/LatLonUnitPanel.java create mode 100644 src/glum/io/ParseUtil.java create mode 100644 src/glum/item/BaseItemManager.java create mode 100644 src/glum/item/ConstIdGenerator.java create mode 100644 src/glum/item/IdGenerator.java create mode 100644 src/glum/item/IncrIdGenerator.java create mode 100644 src/glum/item/ItemEventListener.java create mode 100644 src/glum/item/ItemEventType.java create mode 100644 src/glum/item/ItemGroup.java create mode 100644 src/glum/item/ItemManager.java create mode 100644 src/glum/item/ItemManagerUtil.java delete mode 100644 src/glum/math/FastMath.java delete mode 100644 src/glum/math/FractionTransformer.java delete mode 100644 src/glum/math/GradientMesh.java delete mode 100644 src/glum/math/GroupNoise.java delete mode 100644 src/glum/math/Noise.java delete mode 100644 src/glum/math/PerlinNoise.java delete mode 100644 src/glum/math/TransformNoise.java delete mode 100644 src/glum/math/TurbulentNoise.java delete mode 100644 src/glum/math/ValueNoise.java delete mode 100644 src/glum/math/ViolentNoise1.java.misc delete mode 100644 src/glum/math/ViolentNoise2.java.misc create mode 100644 src/glum/misc/InitListener.java create mode 100644 src/glum/net/FetchError.java create mode 100644 src/glum/net/HostUtil.java create mode 100644 src/glum/net/UrlUtil.java create mode 100644 src/glum/source/LocalSource.java create mode 100644 src/glum/source/PlainSource.java create mode 100644 src/glum/source/Source.java create mode 100644 src/glum/source/SourceState.java create mode 100644 src/glum/source/SourceUtil.java create mode 100644 src/glum/task/BufferTask.java delete mode 100644 src/glum/task/FileLogTask.java create mode 100644 src/glum/task/IndentTask.java create mode 100644 src/glum/task/InvalidTask.java create mode 100644 src/glum/task/NotifyTask.java create mode 100644 src/glum/task/PrintStreamTask.java create mode 100644 src/glum/task/TaskListener.java delete mode 100644 src/glum/task/TaskState.java create mode 100644 src/glum/text/SigFigNumberFormat.java create mode 100644 src/glum/unit/DateTimeUnit.java delete mode 100644 src/glum/unit/LatLonUnitProvider.java delete mode 100644 src/glum/unit/LatUnit.java delete mode 100644 src/glum/unit/LonUnit.java create mode 100644 src/glum/unit/UnitUtil.java create mode 100644 src/glum/version/PlainVersion.java create mode 100644 src/glum/version/Version.java create mode 100644 src/glum/version/VersionUtils.java create mode 100644 src/glum/zio/NullableZRS.java delete mode 100644 src/glum/zio/ZioRawUtil.java create mode 100644 src/glum/zio/ZioSpawner.java create mode 100644 src/glum/zio/util/ZioRawUtil.java create mode 100644 src/module-info.java.unused create mode 100644 tools/build.xml create mode 100755 tools/buildRelease diff --git a/.classpath b/.classpath index 9b95a10..e86a2df 100644 --- a/.classpath +++ b/.classpath @@ -1,11 +1,7 @@ - - - - - + @@ -16,7 +12,7 @@ - + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cc4d53b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +bin/ +doc/ + +unused/ diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..c2a3300 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,460 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=16 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=16 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=ignore +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore +org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=16 +org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false +org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647 +org.eclipse.jdt.core.formatter.align_type_members_on_columns=false +org.eclipse.jdt.core.formatter.align_variable_declarations_on_columns=false +org.eclipse.jdt.core.formatter.align_with_spaces=false +org.eclipse.jdt.core.formatter.alignment_for_additive_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_assignment=0 +org.eclipse.jdt.core.formatter.alignment_for_bitwise_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 +org.eclipse.jdt.core.formatter.alignment_for_compact_loops=16 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression_chain=0 +org.eclipse.jdt.core.formatter.alignment_for_enum_constants=48 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header=0 +org.eclipse.jdt.core.formatter.alignment_for_logical_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_module_statements=16 +org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 +org.eclipse.jdt.core.formatter.alignment_for_multiplicative_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references=0 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_relational_operator=0 +org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 +org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_shift_operator=0 +org.eclipse.jdt.core.formatter.alignment_for_string_concatenation=16 +org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_type_arguments=0 +org.eclipse.jdt.core.formatter.alignment_for_type_parameters=0 +org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 +org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_after_package=1 +org.eclipse.jdt.core.formatter.blank_lines_before_field=0 +org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 +org.eclipse.jdt.core.formatter.blank_lines_before_method=1 +org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 +org.eclipse.jdt.core.formatter.blank_lines_before_package=0 +org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 +org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 +org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=next_line +org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block=next_line +org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=next_line +org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=next_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=next_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=next_line +org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=next_line +org.eclipse.jdt.core.formatter.brace_position_for_switch=next_line +org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=next_line +org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped=false +org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions=false +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false +org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=true +org.eclipse.jdt.core.formatter.comment.format_block_comments=true +org.eclipse.jdt.core.formatter.comment.format_header=false +org.eclipse.jdt.core.formatter.comment.format_html=true +org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true +org.eclipse.jdt.core.formatter.comment.format_line_comments=true +org.eclipse.jdt.core.formatter.comment.format_source_code=true +org.eclipse.jdt.core.formatter.comment.indent_parameter_description=false +org.eclipse.jdt.core.formatter.comment.indent_root_tags=true +org.eclipse.jdt.core.formatter.comment.indent_tag_description=false +org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert +org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=insert +org.eclipse.jdt.core.formatter.comment.line_length=120 +org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true +org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true +org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false +org.eclipse.jdt.core.formatter.compact_else_if=true +org.eclipse.jdt.core.formatter.continuation_indentation=2 +org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 +org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off +org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on +org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false +org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=false +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true +org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_empty_lines=false +org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true +org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true +org.eclipse.jdt.core.formatter.indentation.size=4 +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_after_additive_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_case=insert +org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_default=insert +org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_bitwise_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_switch_case_expressions=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert +org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert +org.eclipse.jdt.core.formatter.insert_space_after_logical_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_multiplicative_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_relational_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert +org.eclipse.jdt.core.formatter.insert_space_after_shift_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_string_concatenation=insert +org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_additive_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_case=insert +org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_default=insert +org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_bitwise_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_switch_case_expressions=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert +org.eclipse.jdt.core.formatter.insert_space_before_logical_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_multiplicative_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert +org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_relational_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_shift_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_string_concatenation=insert +org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.join_lines_in_comments=true +org.eclipse.jdt.core.formatter.join_wrapped_lines=true +org.eclipse.jdt.core.formatter.keep_annotation_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_anonymous_type_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_code_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false +org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false +org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_loop_body_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_method_body_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line=false +org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.lineSplit=120 +org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=true +org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=true +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 +org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 +org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause=common_lines +org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true +org.eclipse.jdt.core.formatter.tabulation.char=tab +org.eclipse.jdt.core.formatter.tabulation.size=3 +org.eclipse.jdt.core.formatter.use_on_off_tags=true +org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false +org.eclipse.jdt.core.formatter.wrap_before_additive_operator=true +org.eclipse.jdt.core.formatter.wrap_before_assignment_operator=false +org.eclipse.jdt.core.formatter.wrap_before_bitwise_operator=true +org.eclipse.jdt.core.formatter.wrap_before_conditional_operator=true +org.eclipse.jdt.core.formatter.wrap_before_logical_operator=true +org.eclipse.jdt.core.formatter.wrap_before_multiplicative_operator=true +org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true +org.eclipse.jdt.core.formatter.wrap_before_relational_operator=true +org.eclipse.jdt.core.formatter.wrap_before_shift_operator=true +org.eclipse.jdt.core.formatter.wrap_before_string_concatenation=true +org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true +org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter diff --git a/.settings/org.eclipse.jdt.ui.prefs b/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 0000000..9e24221 --- /dev/null +++ b/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,63 @@ +eclipse.preferences.version=1 +editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true +formatter_profile=_NobesFormat-2019Aug +formatter_settings_version=16 +sp_cleanup.add_default_serial_version_id=true +sp_cleanup.add_generated_serial_version_id=false +sp_cleanup.add_missing_annotations=true +sp_cleanup.add_missing_deprecated_annotations=true +sp_cleanup.add_missing_methods=false +sp_cleanup.add_missing_nls_tags=false +sp_cleanup.add_missing_override_annotations=true +sp_cleanup.add_missing_override_annotations_interface_methods=true +sp_cleanup.add_serial_version_id=false +sp_cleanup.always_use_blocks=true +sp_cleanup.always_use_parentheses_in_expressions=false +sp_cleanup.always_use_this_for_non_static_field_access=false +sp_cleanup.always_use_this_for_non_static_method_access=false +sp_cleanup.convert_functional_interfaces=false +sp_cleanup.convert_to_enhanced_for_loop=false +sp_cleanup.correct_indentation=false +sp_cleanup.format_source_code=false +sp_cleanup.format_source_code_changes_only=false +sp_cleanup.insert_inferred_type_arguments=false +sp_cleanup.make_local_variable_final=true +sp_cleanup.make_parameters_final=false +sp_cleanup.make_private_fields_final=true +sp_cleanup.make_type_abstract_if_missing_method=false +sp_cleanup.make_variable_declarations_final=false +sp_cleanup.never_use_blocks=false +sp_cleanup.never_use_parentheses_in_expressions=true +sp_cleanup.on_save_use_additional_actions=true +sp_cleanup.organize_imports=true +sp_cleanup.qualify_static_field_accesses_with_declaring_class=false +sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_with_declaring_class=false +sp_cleanup.qualify_static_method_accesses_with_declaring_class=false +sp_cleanup.remove_private_constructors=true +sp_cleanup.remove_redundant_modifiers=false +sp_cleanup.remove_redundant_semicolons=false +sp_cleanup.remove_redundant_type_arguments=false +sp_cleanup.remove_trailing_whitespaces=true +sp_cleanup.remove_trailing_whitespaces_all=true +sp_cleanup.remove_trailing_whitespaces_ignore_empty=false +sp_cleanup.remove_unnecessary_casts=true +sp_cleanup.remove_unnecessary_nls_tags=false +sp_cleanup.remove_unused_imports=false +sp_cleanup.remove_unused_local_variables=false +sp_cleanup.remove_unused_private_fields=true +sp_cleanup.remove_unused_private_members=false +sp_cleanup.remove_unused_private_methods=true +sp_cleanup.remove_unused_private_types=true +sp_cleanup.sort_members=false +sp_cleanup.sort_members_all=false +sp_cleanup.use_anonymous_class_creation=false +sp_cleanup.use_blocks=false +sp_cleanup.use_blocks_only_for_return_and_throw=false +sp_cleanup.use_lambda=true +sp_cleanup.use_parentheses_in_expressions=false +sp_cleanup.use_this_for_non_static_field_access=false +sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true +sp_cleanup.use_this_for_non_static_method_access=false +sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cf0073b --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# GLUM: Great Library of User Modules + + +## Description +GLUM is a Java library that provides a collection of generic components, modules, and utilities which aid in development of Java software applications. + +It provides the following high level capabilities: + +- various components and utilities to simplify the development (creation, layout, organization, setup) of a Swing based user interface (UI) + +- a data model focused framework for the display, editing, filtering, and searching of tabular data + +- capability to monitor, query, or alter a “task” (mechanism to allow an end user to get information, track progress, and abort a process) + +- serialization mechanism for saving and loading of application state + +- capability to configure the output of data values (numeric, textual, date/time, etc) + +- framework to allow retrieval (start, pause, resume) and management of data (local and/or remote) resources + + +## Usage +The latest release of GLUM is 2.0.0 and is distributed as a binary jar (glum-2.0.0.jar) and the corresponding source jar (glum-2.0.0-src.jar). These will need to be added to your class path. + +It is intended that support via Maven mechanism will be provided in a future release. + + +## Dependencies +The GLUM library has the following (linking) dependencies utilized while developing the GLUM library: + +- Java 17 +- Guava: 18.0 +- MigLayout: 3.7.2 +- DockingFrames: 1.1.3 + +In addition, to compile GLUM the following software packages are needed: + +- JDK 17+ +- Python 3.6+ +- Apache Ant 1.10.8+ + +Note the following: + +- In theory GLUM should work with later versions of the above listed software, but these were the ones utilized during the primary development phase. +- The DockingFrames dependency is only necessary if gui docking capabilities are desired. + +## Building GLUM +To build GLUM from the console, run the following command: + +    ./tools/buildRelease diff --git a/release/glum-2.0.0-src.jar b/release/glum-2.0.0-src.jar new file mode 100644 index 0000000000000000000000000000000000000000..11c3d790711075db89f3375c7e02b8c43f56328a GIT binary patch literal 404702 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 z zymdZ~cc;k)71z$KE5;v?Cad!;=Ym$9DO1+T0~Zz~+(g`Dp^M*xZ_y4ppMmle~{_ z)0|v$`DyX#Hx>vr!H#_;X6)R1p=$a`;j7}Y-Q4S8X5%w)-W{ZXmkv0od)W+VI7F>- zsy<`0e3ZD!u>n%gDLBy+EC(LtxM>UM_t2}Z6$fE859WBhQx}6?Qf{rR!kwdFZ<{Sl zfy?BG?rgJLzl`?_y;CWt1o(dOjs-pDHvfUD2^-Hib`^uZX0Mw6;GDf;oxPsx46`Q-~{^7;p+iD-mqVn@KUTa@sN@AH7y+&-pCho1M<+_YBnz;xQK9@ z(0!QZPM>P5l@E60tGe&4f_AVokC5;XW}1|H zazju;JOz#{j<Rx4YG+h23 z-z5c6;YmsMh`cwzZdc$G?vYae3**xr*wCpkTE3Jl91~3NX%?z^UV~7kpCK=l_=0%H ztZE+;C)lt9OXf&syB8XF$f>LxLs3iH@-<%48DbTG4I{3OvSsxS{YzK9W&hKujvI$W zC&@p9+D-L^7J$F&eD>QcK=eO){(p&`|30$*SE6UD|MXRU4?0R@|HCCH*E&cL5LJ*h^Lg;bM`W|#}a*6{XDPC5Lv*LoqQ#N1!xc6&pmx-_r^t}X$6ccpD#SQNlsbx_av?=Y zBEOq53O-{zM+B`sgMk6st`yQ#iTLW

|Weueuat(+4|NCx!WFgC^qUgXuw``l}<% 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

LJ7H*SW zSb72!Fsc+WNQ~Tes_tp&(I15Y>Y*Do6}6iWNzyqcadCIjm@jU4@DEoi!1umyY@SijcTb&;AyB zlCei=4a~QZHx%#_*GSiyyR_Lt!e42lsPF_ijJfj8S%Whqjv%}tY=8$;Qc(n=(;$L} zF3mwR<;w}Bf%~P*teQ)d#U*Q`l$dJ@Y%FUr5UT-l|B4a)tIHzk z(fVAr=15Zzd4W<+nYj@@5}knne#o1+&ryH1@RWDA#1cjCRhufeVzhgl zNJWVkM;qcf*Xa^o(JksKY*XpPY!-oi%EB*2tJtg)+V`QLron?jGP&$+DOZ8pcUw>T zd8nDQj@V9Q&X`hiU`gJQg#c!Sb!_f}OsiS^nkdcplFiY@%AQnXQ@3XJ0OW`mH`IwyG2fy(9Vb79qv6?j91TWFv-? zHA2O&ju$(@f*tM+U?K9lkP^|*^KjlOJStxPJHz1^?c8)b>o?&Ro%&J6mm*Al@!RvM zGUAF(CoAaIthsuu0U=h>3&i4O=X&xmcktyu6SFCVuVeLc(;hF`I9i{=Uo4mzJYgH8 zMiahtIQK01eFeruiBx^EIStNU@N-~ewd97lu7F?V}DSzq~&L`Ib*iWA8|Mcw>DsAybsS(yZo z9d8|W!+UQ7E>Cnua9!=o$UsHh+pxvvd@KIu|L3xV#jzn2@nZx6`QgLwk$doy zIqEQLjHa!CbOfp3D8iz@3y#WMTzQ}y zakOU%-NTGHMnkaF-&_e44n#NQEoJ^n*RQtG7RoUsR-LMzC~Rkw4d2;tUKd&9x1gae zVxfF=??GYz$qmp7SLzb3M70&9DhM^|FfT#frn2jxFq&gxr&XO=Fwwytb&vHtSr;X8 z^TQ3OQv`GmCMB8Ver3@hn_Nq>`N@^((;GBv(10h$}EVA85_3 z5(~^Mt$Yo{w!(2)M2DQ)?>(r<0;a^0^Pn0vMkqk4g!vTJrzor?%%{j}Emd@y#j|G% zZ%R5BX8;O_wfBANQFLw79FVenDSVEni2*lJ%tbcZ=I`SiEnvGGd=(2O; z@Q|R6@{7|Lg^_N+Z&_nVv>c$rLHNXW@wwDi`4tMtwI5D#y6Y#-Zee~JFkbM$3&GVt zGjB1<{G9cd$P z1--mUSUNIBlbze{-hwU_oy+u3{Pbk?B%)&uHnf52P;!b4>YtU_*l3%;2w6gP1L`{fAheEZC+Ej?WU&%e^Fn_C;VP|$pvtFXv90P4#X z$2yNxFJEU}%0?)tT}5G`;vMc^`Io*(Zy4IIHczpmr|YwagzbTTB^X2+(CWi*zD_={ zNqf6*v!gz?-!yf5^baO(W5w3vyvg;Bh-tpf&YJ=+!*{>h(DJu-(`b}ia-9Z;=(y!>O>h`gza(Px zy#KM5^&g2lG+EJl^B?xUD}BcaWi6)e6y92x<1*YQv?N7xYTuMp?2o#(5=kdr=Jy@< zqmE+J7|h^Am)UaX_(PV^QnujIs%HlMyn(m%H}AC-2z{4YoGpt7fzNG9FOiF1@ClfU z2-kbsyc|hzYDG(G9=6~bwP}u7_t4Zy6*%gyR`F;&z|6!X{$;74KJY)&b{@`PPl^m# z>E`6<01OxKS_}b8_*`MLgkWG+h*y$%4+2o7b{jE@$hqYDabGeiB#aki;dDKRPJ_U` z`8`)c2>whs`K#Wygfib9awC=;3+>YB#--9~AJ1O8x}C-|$rfc;E^st|HUlFm6mlcJSs-ro~$Tz%R3Gqgi_r1c%-yBC>NW-Cv4mm;|39tu`=|hJV7}^48 zscr~j4)h&OYMv>C?MirB)R&mZq~zrfAmrZOsmWZE=p8W1b^N#XKsn*52CI@ejI>Yy zZXI(vBZ^NrqKf6QE|GCJJ zQ=kCx|D3>tpYI>Se>;VOPEPjj%J!}<7XQ&%j8)mOFXlk-3+p#pB3~dYt9m}Gl9FD$ z4&Ag-Vm%Z4I2{1-akZIYBRy^T{)mNq%(}WhLl~NZ_kWmX590`{tkWWnS`cKx9b7(I z2G>Z3@IO`3%s4yOoO>@BfyScNftFO24CLfVsYT5U<=n9v&Sj&WI$30hI}7XlrJ(t{ zN;M7)2apVP*5ZuV`1Sm*C5YM$GEtu6LfE|68h-v9>$wNC6n6;?QdnaRGBO|>h8kQ* zuCQO|)clIR_oOW|a-4mIEVN(ITCdp^~`Lx4C|I@kd$+_rW zJ|am&^(hSuI`gnZ-6+ORt;@|+Do0W*oT}YnlGaQ7)g5Ex{eYhXka}1Csf9}lhF8>whOx>5a@>!kl4+1zZG<^e*^P(w#423 z*Ny;;dM+z*H4aS|wAs9CacmcPcP{zj?6zEEaK%Lcq<8&idx|)5M`s{zI$GrC#5KP8 zC=|ZpjD;t;O~3b&`qNzAX)Bo*aLgT#|6GZc3ujog-_6HvsCfS;;wuTBKL(Q@X5J@~|2;qQSMbnb zh|%EbDouD=_~pEOOZMY=Y=zGfvLc7 zLg1C|H%ds(;ZQ+%Tp|!aC9iFDy&`b4dX$hhj)ax9lB8-nFinK z@f4V3gIX0*QdKg5doi^ZH8UJz z-*70Gg>GIai81anBx6!ZYqCl${$T`5jxuY#|HA0=`Y9Aix&nvDa_@5!r@w_;y25bn z1uel_MTZd3+JuO9`G$Z0aNN#t6ZfcuEkdZfxp<)-Fm}9PdL45XIBhF_ZS={3Tzf5i zQRP73+!2&)0(o{1VwYsvh2x#z z+Lc9zr`{lkhnMt5)vGZp&o39yLLW#5qN zBHc#}t>sf_LNzm@{t4hfWu-@ek`)(L6jvx*hrB4aiZr-3ELnpb*FEuCM2D8#FNYIe z;Z|D;ij=9YW*{Nk;CP+M=RAltMy74;e2>k>flBqpemC(@U3l}s?p$r6^+{C^@O}d1Sr3c zpupD8oD$!;;ehcjFp80+0Q*t96*ClU2i8i?RXFQ}4$d+_1j9owY((Jb6Xdo2DlA&O z;)*~Q4ft4Pgy90f<%sX+r?J&9MhAY4rV`ven8x_M*Q=Av_~TQ8)e#QVZEXFei|J`6 z_*U;8^<}=$y?SU*5H>lJm81%Xwhh{R+BGk>oxDAjG;v~Az9zKfIuO#U{H6W77*R)m zI88cQruIEU4YQ8s z1g@Aj$AyaJUoPALaM()eP}s*6!AQa}kGvggCDtyCVbA^-+uN;T{ZA+#l=vN3On;bf z#IMWm(eL1ab;QA-`FKq@Sorng9SZk9*6c;s#d>|iXK?;u-vIwCO#U8GvCW^xt^da~ z|L=|4KMJL)i>1x~`c>AeGHACUfWUiDkKxFqXjQH#A4`;)pyDV+I5bBwK8iQRcA@L; zt~*|Pf0N5d2WhqRCu(4rkAr$g)D5Tg&;P3tME_)9Gep1u5nl@cqJh-X(A$*utmv^o2yq! z1mm?gO!ITl5Gbk2V~Zyy#L!F0Tk}(&7yh{HqDT=j4yN{q75e$UB~xcssk(iHOMc4& zR4#Dzj+Le4Z5QYu?(dPT3aa^lMgpklmJ`@yyP8KO^zq%PG4>&%;ll+_>eW{M({=l( zl220rS6K^;IAw_iLY zVB)f^q|kfIC6`NPZkR1~0;*#?lH#;;1o!U4g@1=oW`z=Uht$v_Zhvy2%b3KqI{}jvixkwyvqFESwb z+4+rH%5SSAG2V$_{s82mRcN)i%6PjXvoVl1*V4-Xulk+c7Ob`fL~Lu<+;@C;+)u<1 zWm4^8e?f=rJ)+Rf(Ek~z+P&38T^Nd8;0LL7SF-7B1k%p1(1pqyTrFxPYGp9DHpQS? z9P;6VEsEe{K&MPkE3(RT1eORuo>tO&Pe$}{_K+G9^?(ybcUZvB&56SmskW-nfL0=0 zN<$87Xh0+d@00EnLqJkeMZ_dihKn5-T0K(~3cs5(u3%+?GEXTuvwdJez<4g%Rwg)a zS6pI|{l&AVkjX>Ut$;m!aAuP@rH6RNcP5wi4-Tn0Eexby`#_E`r8e?%5^DiW!g=O! zsB7}}_T*8jJko}q&@(4T-1_~apg(*=CU*e(`=d_~9GqgVuX5PsKNakE`-*nDgP(|D zF_)CugNlua)Pjj#bB=JG1q%r!(6lUR&Bae^nHfweN}5o(<5frv^J^a;uehd~pw@x9*;NNY#NXcLR}J2?`$)7q^~~t!`@%e znssXolSGt4Zp|8~OcZOR`DL8+i$0@t^y1yYfS(Q0m=Pra6;zO-Gg1N^Qyu{Rj}rNR zBVHFmdLQ94!U4R!Eb84uDAX)RjrxovwVJ9B0))|Q$cP6=gbA9GCPF|hVWw_L5Ns$X zLFeZ+d`9l%V~KOqa0WPKe(i2fftvIz$sF{HYMO-y4?1-|axz|)zxg0g&KWs)GIDie zBZHPrJEVM zd{2w5tCyoELtj2Vwzp84*q^1{o-ev9y$QH73NOnfl3UPK*?o zF##wdeTbR`%`P{qiqpSFca}-fMpQQ3^Y=e?5 z^q73_zigdME_}aNJV+ZIbDX9Mb zg?C2 z%Npj)DE$2SK*cBn*Z1X>?mKNYHh&t9PZrF$U_yb4QAptvAT)5}sY+)MTFipnPxc1w z!L~MCMY+ZZO$2v*SC%B<`O?0PE7Av?{0A~aSdA9Xg9h1@0Wm)+Vg)~^sK!adqY3-1 znjsJU&WA24DUdat0QHBicWvPWnk79TX9`0WlvA7>yuK5VK@_g~z86v!3s}ewmJfjY z&UcV5;hu5QyN$c+Se}Rf0RB3p*H{uI;LOeaf>XmVBX^sq!4x~YIXnP7Y|wgw)X_dP zK(2=cPmhHZ5(YIT3_lOU2r1Xqz8-$v zQVLs_&M2bHsy;f^TNSbcw?kO5@4%hlE(qO*N@S>t24tTQ;~T$pnvYvPImz+!?-8r^ zCg=VON-8YWH?T-GFi1Q8H^Rf9N78XZ(XCiVfrJ9{T{;)u=kZv8J5!<~YO*_b3?8e& z>u@o{G{=0<`|Fw3IBn$5ABpTNnPh3O1kDg7N_`>Ny-#gOXNe(a@-{t3JgiI0Ti4um zQ|pL(u$BS!8Xj^Anmbp7)>75ZT~auEctyM_-fQzNTUX!GUY;jba|3%0PmZQNxjL>$ zU^VCWuH;XhFsuJL)bUHXN9gyEt!&2%H9O=tfNi*nLnG?omc-BNzuU;v8b1CwvFRhO zmWZy!6cEpE0E};okndc1FjZe{*?lafbu~w}$?C{54p>(IzPIu4a(pVAooZ{Fslg|w zO}uAFdH@-6?eygDW9hPEQkEwzO$J1YN_*0^+XjYdf7&SZ6A|ve&DK%xK02DFFl! zr5-iMi`CdZO>FyH*Qx{hb(Az-huZznD##hn(+=g9YCaV^RaV)eebG-x1nXI1(#^~G z+fxUy-M{MaP5RV1xZ4kN8!KZvu3PTprt(%F^Qqt}*=2;KJ6&|NaVr(Fr0RtU#Z!5+ zS_X_KN)3iX`EQN8li!58r79ErCiuOafNRDr=dHwrM??W0c>CnLjeTDEWcXK*?@PL0 z;$2DqlxLXg%LZx3USw=sc@%bir5I=-tE4_pG3I;NZ^c}6c)=XGm=m98`*8Gw=0!bi z&k&aDzaH;~WR9Eg#30D}We?c5LSNrp0g*p1fKObQCYzd2CfShfx zQ>5yydMS-1mukr#2y%AJc}^U056=p}*iGD!e~sesYjL|&G4}_8OGBW8f~RS`8UHzwbHVy=S018_wL z!za#H4g^y~tRL#P)wDIcj;`eC4;iZoO|;tu>ay|KM`DL#Xr+it={7-`0)W)5BJoqH zdoM}F09u&LvgeCS|KYDBReZhB0Fr=A10X&oIh>^ZE_?kH&C#Kchds-0WlMJ9iy!g# z4YhOXWJF=l#Z@%n3<>lZ?V+LsS;;24tc3(LM&toKa{?rdkU};jAsN}6^HVZ$eCnax zbW8`Dp7Omtbt_`E=(AeSZewp8TNTiqt!?)S60^p(maT9wnOh;4t>_|;M-gZsTk6}* zXed@b)`&l|MOWj?h2|mAujVEFoy}9rB($I66ncW@=E76Sw{nuK#c$Q6lo&i`DN9gr zCEKAPAs6&qIcDWT-tx)vsjn2cnBmxZw;{p!4wQxAtI1meI7ac9x5-j!gNN)$H~*=P zPB5_CP$7#fvAB9(77d89?s=+760;ev#TX1&yh7Qe1`$DIHCtgWAtmDRgR%G{Xp7|s z=L6J38W!c#*u<9)Rio@xBv|Fxh z>s7Pj*Et}~uLoO`oqkzezg4asI_&I#YT1&JR`+*{q*{b$k4dPN9&{=#(aLjb>L@@K z%O6W#HcdfXy6La>rRL~<3>~LrPr5^DlQ-T(g69K{)rMYKLVNHRc29XA$){OvhaD?z zwEPn!-1`crw4y9);5)cIpKc8j=^ut&^e@nu>WII(W?v## z{3twAW?sxav2Kd#Crt8(<Uq4a7IOnWVn{dP9sCzO=H3ZlfgywkbIYZB79efka8j7D{pZ_Hlp{(s3**fiX;|1^^!HC zaxj}3y6iC5F=14k`Dvx{9bQRd2CfZLvQ-ZL5~}4aqx&N$0I_1oy=A=902|WMU&s>z zLN8_6neahfkrv@T8@g0}SI?rGqJ*o5O`u~FI&-He<8lLPsC=KP(Qx2-oiViW5bJ_= zA^s#T0!oUI*NL(l257#F1(M@XQnvh>>JxREOxcSp*_x^4{FsT7{i_8qf~OvKm-lH! z7n;MQW6YZ2{1mbus0rnUC@_kto@vdxmYk7OUWtPFqT{|DAK*7=#wV=b;H^}tLzKD~VQOay3H< zvotwW&3cD3qj&hZ!lv4T0Yv=PE_l?YChFiqC3SMon8cKo9BCQsFH#bM0l}b{fvmZC z@qAhn3@8d1QA`~8vQ8q0=K*7gvG8N8s`!}dk8-`9qGYVC@ZZfM#*L<>!$*)2AuqKb z=Me~gPIh$y2c{L?f$mM3KatF_oUmc?XuG+#9!A%0i+%g{f3R?#wY-5!sL5n4C|a&} zdz!)K%~u!w_onq|2xm@XVs2Tv8w7DAa*HrGsQV)78qNx8!#()wZ|i>6aQdb1%h^zg>Yd2B>b(Hxz{Ou* z)*5OE@AlYtI3QE-z9>EJnw^{5Nnv8U)NwZ+uo6Dh zO_N_Mtdr8av?NfNe>151Vp(6iMjnnOxj}9H@?$?lbTq5$wnk>wp9W%KvFliM{F8Y# zdC1p`tY=J_HUCY8t3D`HSUt?o@BK@!1Fe(vgsXkDn66xfmz|mCwEB_i#Nt*~gbRnY z)8oyCZX_6j%HY5frswiKSQ+EOp>y}8c46*{zlN5ZOO#Wky6nCW6zl=0ksjF!Cj(Yk zyhUi4f@;XcIz_KriCatg;EW6Wf+f1Pyk+az61Ho*)*5~Nbah*Zf?Uw>CJSTDW&4#ceCaIMDcjRudP!?+LH}S3^(YdWj76lXQY>E8 z&v|o*cuEG-%@)+KGYFE-2Z}&-bN=&;V9x2gX{C#^V;hnD;+A;KvRdV^r_R*Sa})Jv zTvBME%tRU(-TLKBt07Iy{9TCiT&kxa40C18v7QcPv;e}I(TshI;xcMD@Mgf1AUmNzzO9Ju`Sm#bwjnjYvz8=I(E;DBte()_< z1izcVZU|;w5t`Rocn^8xjSq-jxb^+dn)!3beX{H`R!wfN17~(@;f_4P;lVe6y)LP% zbMVymzN(v3cwoO;mZUi0^q4TkJ@hlC65jhdbAn?FeFYkY#%lgZ zj;r5K4)wAkomGQuDj|r9&)>q()_t9)v-uq=J~X9Lsj?uLR>rFLLC5+)0R#yT3F6iMiEV$N5 zo%5$IcqrXF4CeGRKxg)4a&hGy92{xmzZd}LD{+#$|EwIi4LM6YlcNIZ0=Ba7$#Ai3 zd23)0$*c%cq(umkp!G_JhKdU?)=V&KaK$gc|s3R<$u0kc;VD^QL~WqgtA2S_`u^;^#No^XKbA)+ByP zXBm=t9C3BRK~rz4E(C({8W7&MZB?i-92e7d*H{@rJ2@LJYCbe6QZH`8m`73efxB$S z!w^5U3rtKuSHRq=S{6pju=7o&9dH?%^AK?i$7Eh{mpPu@cI@aKL9~Gl?)+3YNKjCx zX^Qg&ZFwx%@ciJq|6dZ_2|~ zucMq)Rox%<1&Xg&|2~|26F4;YVma5d2;q{VaN$o{mZwe$Xq)C$RW~)m*|=mhmbkYfiRc0)Sx2Q%e6_MiaN=N6M z#|>t774~c$EhMH|Oa9s)={!02_pjR^p+S&u(0nv8w*FOQ!-y*%hEA}{#Y;I{N!t5y zKM?G_rDdp7m^&+^+6ZqTXk*1Om-C**BFa^m#@Fa|z%p+Qqx=k6kN+`b6-Ug(#}Z&I ztH_q4%yv8T<(e+zg}>*__RZm$I(7E)&yO=-caTc<#*!JsZ_;GLJh(!a8rMHnX8W2W zh8sl)=T{pu2goXdDTQ5-<>W7Fn1dzNzSE%*iCbD~GLx2*j$Kjs6+vYuW$3F)3xS3w z7Nx{-JXD_ClLUd}4~x-6muyOlq_0@p;UrlJO;=wGQPSf0NUq&n;wD6PGLDar+%0%Z zk}FJ*){PkQUi);FpD*qr=dR-jypT{=qf0UM2kPX{A#U|H6s45zo&9f}J#GG~b51D9 zPLsJ4yPNyx>vOaw&N?V*a>KutkM2vAF>X9BM_Lf0CJ`7sp$A70hg^BO;Em;QMvTe^ zD5wMHIKzkV?*NtI>c+Pg~JI8;fq;?zUAA95Cny4UzYz$d@&lS&*pyLJ-9UqV9P3h@rcH zg5`paktoMyvnCe|M6ZQkv+`XXa4vi66-~I=po^vRA6p0gl}?dg4{e_?${(Vu1v_A9 z7Cwe8{vOr+J@L8N(jLIyyM+tKSO~%2dUxMRVVk}s{Sa31nU@JL$p0+S z{~-h(TbI+xG!>P;WqPL368E&9xu-C?nxN~UXISCS>hH)Tc=yMH>r2=B4SFrebx^ZqJ*4-)1H7?r$U0hJ-mf6wIAw z#;UIk0B#Cle6zbeX0J(UG{(Ru_q`wC|9OVYt|1>`_hath`|H`CKWr9$o4z3ljIZKMe(Fg)%4{QA>>)s9ODSf&``Q9(4c7l%`^1XBE{H?;*$g zz*L@wLeI>S_S5YPMnHmQrHW=Dy%M!#JX!>)>s5{YY7&oM-z~CSG+=hqesVH`90B2M zLzw~f!tBb+q|`5NjCl`|^{sb4u5ZsPsjz;r3L~7 z?FbND+Dyb9hO8lM4&o5}rtTOdTNg2nLE=H(GN0)Dj^i$t{{40i^BC6_Gp(1APp3#R z=Yh{E4g2Eqk#a0+EO%NcA1a4>5x^b_AF@IBHQt7Fr>QRFHzgQ+pBo+{V4d)%ol@47 zN64=luW(ls{M|42lW<2(O02RrsHGW=Uk)1W&?yy`&ab>477h7@#FFWi)*GrWlbfza zMUlxZfe+j?WQ&a^!EB=ZW^3y$GktTD5+4zan9R65U!kccx^KW%@g?(IwBkGA;~p^I zp#QAt>9ccGo1e~t2Lb?q?EkE3@&8U+5VJJ3G5KHn3Rl(tm!KHqH@bq)wHsuzD7Uc@ zNseKh9U?ToTk>_tS(54!_YbO;!Ny^~`wPit0^2~0i@WzM-^V4iB8afAUF?(-_?02} zyZB#ygGAz<#m**&+DZAOymqO&d5yC+28u}`BOw1{ZE9m^VtAc_WI}f-EKqH;g6v@}d`{$2%NJQRuZIjF!?jKn z6|uF>oABVso)qIH+R<%S0sUAi^QkQ@2qTLgJqTOq@^33E>DDQQFm1HS6Fs2N7_yZmWeRyt|evRMld%1N7#0O_*54teo zO&hua53fHO^P>>wn&+Jt&0V62BuT_cqa+j=2zc)!Rt*XF z&>gOawwMz7jHU*KXc8ZeK;bYxvnkq6-PnsBIU#kEuF3 z&kic><5Z0dNq-LV3q~u82NBnVHVRg=I89e~!Xv7!E=X?@<*vz*4w0|`p-3u>E{$Ba zEPHIftlANP+_-4!AS_z3E~RjpatlW1wA+ZX&e_Sh#jl%BdWW+YR26Z+YUx|!2}T#? z7Z+z)E=`0M`_VyZDD@tB3G*fw54kGy*5JtktpqfMR4>>HgLxYEtuO&b$NlAfI2ILt zh>WN>vS@`#3$o>gt!-y^GNZWoNNAl;MKql$rr(U^s)29jQjr?&OaPVOw~nzlRK!4; zXrtq5Dr|NOiyO6a9|BT6zIOxn^8gkHnJTHMT5{h@d3ItIkdYOYwXUK?VV(dC9LB4ihpo9ca=R+qJ9BadQzxbM9mL3 zX>8Wcc4W9y^!=N&G!auKDUMnCG`;*5H}7ccQVHv~8u1s0w@kx7zy8^>dz*}%f2a!B z+}zw@D=^>V!oxlKia9OzT`s$A2zJTdu^)PAf6Y<`>9_YbhU<}ouy$2i${(kQ0{Pvu zsC8JHX)CFOolC)f%f$M@{(rv9f>~D%8h(tY)L8#w3;+N76Y>AxjrM29%Cp)075n@ z(Z1sSVZlbv{N;mD?OP(s)P@M-^?2JqqIcjrxfPr)4Sb>EM}PGs8wFhzTO)%DHW^~8q*0D4Gqo_ece-U+k;;yC+UIF7Z3o_ z_Oq;Rm!?Xq3W%Wh)e}SKAGevJ`Rke+1WUK{m!<2_f9GRqzVoq~Ybmo-Vdfr6 zK;0uG)^KF>8P@%o{l?7eWTd%brPp#ZvU~UX+;}*-G4SN}j3ImDw}APx(?iT!pTPcG zjOX9Z@2=SFp0H1QsqJJih$Um%=Go_DS*-+9%24Bn+{jEw`4)_n>H~xd;&9>U0y8dT zup-L@eQ8TYL?|g5Zs!>~1{gZtSI$Ku=w!i=lm(KG7H*w7L4{4hz3X5w>jNS0KdwUq z!$dz!G>< zRSaX7PLfoV-DM)Z9R!9H(@<3}5b#<&ZwX0{@c7wNiRfJ<<(>ES?tzgrn4g6*Y=nWI zvj;MhKXhORQ66_9Z`2FT;1N%2mC&w=L3BJK;xJJ?%uaI1E2*z^7(^W;M`;$a7;{K< z+f3rOvtJqn*weHVAtaKLVz{z;TmPndz164t#pu z>Pj;#;ZI^D(YN_LcjI~!mx{8pRxO#|x@xrzZ3{PbXw%})yG(Ox5d6n^&YB!$iR$!mxukm|3rzK+8ScEx^MiUA z#AFOeb?dKsIVdgsF5tfLK0906@HUa??iSTsS`|!qc4|P%7%)vdGryvPQV_j;U6km*lSn)>VnO# zuc1ncqZNATHkRb>RX3LiZ-ps#)jZ7a$R4^3+L~NqkRVOI^Jl&lebKT7FZ~+G`Kypg z4OEfMOfWJ-6AYo|so^!;pyScL2!jYGfzDWCbzUm`m3(|pe8^2jIhaf0B%^hI>rJV| zqWyGKLM|ys`ORkAC!gQejAc?@da#fxL8JnktOG1WIgS_$jOd*5=pph7>yQChX9*Is zZgT!myWd8-3o6AOCcvLKLCHD#*C=#nFjK{*`$VOG-9;GFPuokz6E`!sg}q(Se)Egw z=MzwD>S)#6bnIB^Iu-FOib5wmyT2P#0&*-FW|3?*_h0XoXC@~@rTi7pdW%BqiIc+u+^YcOH_V?HcYfy@;m>a_V~a% z3YBf$WNt=xWNrM!DHT8rU;z12L>HZi?*~oh@~UfvU`qLU^=RX+fo{=po~ye>cRko> zhG%0wq=xsPkFQ%ree=USf-3~pGygDk1+a}bZp9p%TYX3uT!fa8d~I1ta#PCWu$a9= z4ECor0mVCk?ld_C9_#L5JW1nlQ;S4Xapp$GuvXf9SI_@z8wr=|=Gb#N+ah_(Da)d1 z^8C{N1MI`1xnHLESOCN?$i5N!H!dQB#7xxWt;2*;Gmz*Ov{LRu#e?TMWPSEh?bTY= zO#Pz4sre&GGM%pO##uhowJ6TbfY<;6(W)$=_#yVkYQ0l5`{{+5w)l61{In;Rzc8Sh z=}bku#7ID!)>C($twyl#B&bqqnH2sE{XO z+Gh@N0Q`?CKCamg*sXu~t6DZwh4c8B=6Ku}?S$q}xk^^8fOi>S&HC z>{U4u-!N9-R1_~ZNw%8=Yid<|aFf?ySgAt*^*Sv`EMfWYAi@qGSW5(b<>iHxmz-hp+(f|AleFJZ=wDuqPU z?3~<)7MeV?Bp#1stW7GYq|EXY6G@r|*&wZ6O04Uhwc|N+IP88%?`WVjY$B=jxM54w zsfh6=*(PnaWyF^j?T0j$DYz~E)g4^UU0|H^cOT(i))?x?xKhm@#df56Uxn4**XOhi zg-0RFKN9CmxsS=4a*=$R3c2ksVEE#ZVm}zhKV@Q5#Tu}r6bwbgi5^CWuMtgHewe-d zKtUczGsvQyz(44xj^ehAVeZ0CiLeI00nL@K378KQ%>?WKXrpe~G3pE?TeZ@Wu}$kd zE?g*jicTg+yT#Cp{R)liO+$?TnE&;g4wrFsOfsdc2Jqi*4bZD*K*s5ptpw+ z!v?mtW~Jb7sQdE~vEx1W=+(g_{aFaVe%QeZuzlhd0pQNg3zvF&HM7huN=uZ*Q`>UK zO{jkD7ilPDtvQte+@*7d_k-66s`sF*X{v69BD-T2aMAnB6b@Oq z^4ZreQ?95EZ>gk@9mzfqaw?HdMh(Bl)HJ5%9x58EVoy0sQqvk{WD(yM?$18~;z;>{ zGJB2rQ99eXf8fvsZ$Ym|MmTTHv&vIiV|P<}+$v-eRC4X1~+?^+IUon!!-!__5)t{4?fy@xDA%PG54Dwk9pm){dVQ~BZ|!r=>Q z+e>2cYR@^_8+qBSTz{{cJ^tlUOXCe^;$UxZar`^G^;W6R`$vF$mM9CFYikhwHf2;f z9*WHhYksx(gS9a+xH{o>tLOs8wyx=RJr*z2NST!6jb+Nm=5!GiAfGMH#PjW~;Ez!u z+g#j@?&rb&P7r%XsE8e;l(a3ZrntELy`Q9hZR~8BR6x^tpZokrNbhmoiM;dsa#;UH z2NC@*nve2#yXJqU_iU+svnp8;KXJYyGWj?}sN9BmX(#2`epRj#K<&4Q@GUH?9}^}1 zlo-AZ`F!FL>t2)00M}yBPcz_0cIonRf4bSTU@tP#vd+?_0fCEoI{RC>$~QNiaHiJD zoU^^YuX@1A@*0!9Y9LftFveo$+`Q?Y?a8Uebgo=yD|B))+P#P9%ehL4jZnE@d_mE- zS0+zM_cs&dmEf}~e|4PtXP7{*&+-l*Dh9UJf~3vZ8h^3b^IXm`r`i5nQY8O|yrmjY zq4Y%4iUHQak}mS?ZTX{tnLxj`fuMisOATSUd|=8BW6=KSo_)72In@u@DlORii6w;D z(a};arp{Uzl=&24Kd3-%$SG<*p z`nK^V1lie%xp55;BykP{Mf?t^Z9QqrL$n_ zs2hlqg2->BsZ_p&ure5KhmptSTV-zf>u#CAI^b{`9SABzpE0C!E6z zQw7Q}CBa}Uu&Tmfoym`iDhPh*ZR39>A<*{c3SCHfik?|t$0k)8C|ropnnPsRFa83J zcJTL>b0wF#h(E(0j8mZ5orptVyXrK)4b=h{wQ$#4v=2<5={!7DZLOSONQ3Oy(dM?a zO$anqYuG*Si^S^AEg2ncfVw(?HCQSo#V^OBlv4?wsatAx$4L(6OTU*tbIh={_Y9YG z2}ChXd$325YXd68BJx6xJTp{SJbQX66T_Jw3nmxc!b~E1VhiffvW`DS#3J&*U(g z%D+#+7UpxX?V5r00__M^t5Wy!mklew;qw&zgQ~|iJWRd@(2Lldr|;uq5xW;83zUAP zBUaNK!IKFzOCrYYQKleAGtshaQvWNPXH!|7GO%XS34;Wyer4X+IVtv!b*e|HBLb;b znoeV-Rc;+azWVRn^m5ewmS4ft08>Yzq{LP6%!F9)2EV$pF2@C@C&U%cL*r1IJ3sCU znxK&ty_9=ay*~Ox1kr`WHTg}95S$G8t3JwbZ#=zi)k zUhuzJ;MEwfS7FlWCuw!9r43csHdbmf!JAnctr6{h;9Oel)jwK`LnWM~(q#~CqhJU@ zYs(uQAl^dZdO!gMxFH_ce>w=eDU47P+OQAE%N9dv>T-V@+S!fxW@bEYl=gfw-9=_6$2;FVdgYpxpKQK3|iS-KJSw z>}kN1mQ6sCQYXfN%HsA7d9s}6MClrtjajOnfn7L}XL&KSNw%}u!f%TzNUZF7F)@(q zhvN1a2MgLqImF%Pzzps`_Tihn13?xfYnvOePSD~zfUJI6$F@Ri;g$qH)jdbiMuE#1 zs$5uS8Q)M6|E$0sHnqE`$!QGO|A3m396n=pMOAwCXCV_6OJ5Ku-m3x18Hoz7CY}wNc zyof6~(;&7h0r>SwkJ57PR6;6Db4FkjYFwc93>HrOAxz&lS3J74DynF;2=Y>g!#QxG z)YkA0gEn*13Nb7A9o(B8(Pc(g;rU>M`|3idXk}cQL>2mexUs2P?Uk7G*hrf1Q2iqL zdMt`;DAlkW)WYXepzmX}AW86v)zAC%`QyLKXIeR(-tph-y>E&t5Z?cCEl3*}{s-2A z>hF~3zZZIX49nq;DhrUF6$W_~7tE5Vl=MfS95rR}ZBXr#S9BtkBA?w%QV}J_plA6Z zgyEa%Y3k=|zX|XgLd|)Wejp+ zc4I9#<;Y?0EP8wzJ+%i*&jtsb+jDkG>pcC|Ej`%uX4RxTJ>eFQ%-q@Zwq<}MzQdw& z$$Ri&B{bdNO90f{r~%p5B$sy~#A(W$5HiD4~MF=)hn51cvi%&$1~=BHy=ojPChlROuS)5T%dQ z6H4R>lU}*%!m!#yLj{{LnsG1z*ylq&NtjS`^PH)tLL4*~ADFEGZ=N8^Cki+X3QMRx z;z)>qNz-5y+oVYspmYU1NxP}hPwaTGLfulVUI{+kofp1>fuO9+##I{&fKs4uHvT?hv6 zSKZ;rysyfkA~mHVa*oahjK{u94rnyEL->Npx?Ed-7qU1E#QHT`xmA+=nlumlJIoDX z)cPfU$cuB=??2p186~6wDNxCgVhtffJopet*~o2}>1_x)n(6(0tsBJVn4| zA#SbBYx!i1XXAcNcPxVrhq9qqMA z158LfUw8%(h{<=OV(oOa7keVv^O*W8xO6UJn z2(puQoEF&;M{_TG4NK*0$;amQpxD7tB%x9zI9;Fz4@S1x3tEgxIBH_gzI>=hT`~r? zK+>nyQe1oA*6|f_29;N{>+DGSY*Vxo8x@}ul%Fdq z!m9yhCZ+~(RyXn%O9EfKe~vJG8PG8)rd0n-P6Qqekch_O3(1D^zV+1XZ+C}}XJf}| zk(Jf+3YTS*H-M1jGN-8zX{w7S1>Q1I=8A)6)M0?Fh{{X01&m@T2dzG{nx8fcv5L8t zy;$9^AJ!L)4RmaO#82(VT(~b=3lBTO>a@=Yw%-|t#JdTBHvn<;skuXTf z>3bu|E=Kuv=hE`p)TPn?{r2b7s4~T%q^ULN(DqKNHk}0X{T*a{3-_5r%PZ~IW{4Ad zWqH7)FRk&n`cf(t8JE!d0dIUI={pt9&Z5Gsj)SEm^^%`06|bs_&e z)n{BPNP8ckTWC$Cz^wQjV<|Gx4KFnEz3p?`%R$HQ=# zjwSO$G|zL5tLkTVs67OKzSJcBL65m_3+3f1qW}~nv>;cVnd}%Gv|OnC$@o+_2CE%5 z*sAh?B&3e_^5RHv>eQfVwHGh3<5GDa35jhR%r)4B@CS(12^kIg8Q_}ufbuM58uhep zUVH$ul;GLTt}7jxIrJDKMq9_9Gj8nr`Q3ISWC#5YzzRX%!6Gyk+ZKV-%32A5G{Ir zQZ8-3^|mWsv7L^*WG>}yelmMf&`gGQ-ms^Az!_a{Mx)O=eq@sa(=SJlKXayXi?dIX zZ#o@Z|2SMx-TVMvm2(*qavlTG#XSy=4$$93T3ghPH>i!cE5i>OtdAYZ``yQ|5cmB1 zpWkME>Yn}k_dcEi?jJ=0|3$bHHLx}E_|Ik(!lJDt~)xdaBBX1`7l{w!xC0sImcq;`|a6d`+$K7rHL;i6}KBrJ1YHo^)h<*#GjBkANnM!HmiANvK zF8`SdIi0eM8S@3g$YMWM0A2Kz$y7A(bg!|X(ZD?%eTDqkF(ivA2L+sUF~>v|(Of&a z>X-_4l;%d3g{&?5fI97Q`c|a|7!p%0wf=G%Xf0dt-clf+=i*+Ij4Us#SuNQU)AL+s z=Xtc*U_UCE+gxsxT}b1eag^$ zk!g&#tS>3`%#%ZiTgLPHDP)IMDh|{xh;|_|K*M`ANq#@e-9)kK{gN|`zHp7i<0BV5*2}ma7faw*M%|JTZOz*1>MEoW)qH7j}mtUX{uhrugEZ7Z2!AOkr z|760M6CX~CFNai)igrHt&clU4}czoZ8pC{qG~71 zDhShrXhWY;fVZOC6ld(7i1^Gi*z(|S!KE>qKhuo;D7fpS0lb9>0A2X>+Ekl@aBb%m zA5GH409+2tCf0`RzS#conWJ6;>p%4B#K7=;EY^gRKF>hCJHsvUc<-2F+MekUTFmRS z{0W3yZI<`%m$CdU*$QWd5&YP1oY?=-Xh5F3d%f%^2U7#j76j5-ic&XRpvldvP!|^a zW<~**=p1n{j?SA>j`s{>NT()_KGmJ0_xBZ+PFon;B02DoW7A||-x9CQ2y3-bZeMf| z+iaY-xTY^q=|=A^f-!bRzkyh~_|w5%M6y{X^oWH12$|dVp^mn$?>Gd*+-bY`T2C)b za9~j>)sA+l^kyFz+LRr2EUHy7f<&rh|JM~t)TPEzZtGr`ip=vh3O6;&$XbJ2_0Ry= zwU~R}-LSxUr5+b&?_Op?3FZnRqYoTlkIrM#rf72+WcloM58mNDT&US~<%n!d1ZPn_ zwX;|gt0+b+2O6x<&Ba+uY6BWzvsa)xs0|Q>P&&WcHj!_do<3wvB$6_|UD7AxfY2U%EXBecv9b5dRBe=p_{X23^b1EDm5n>)VBj^iz4ovjeV;w&f1H za8qaM5d~TB1uxNL5L@#sd(+8jcx`lw;6k@fSYWV+%US)vaOmk!MB|lxViQ3!mGhQ| zS%6gCM9zFi|DamCycQT{N*oSBDa0+-VTz@V-O)8aba4I#hlN@unYkCc^qttghZeofJMMWAA7y0V)eJmdD?ruA7ZV$#k$SXE2;;U(e z$%)xHxnHqrzSkn<&CEBd$*n*5?26XxZGyJ?n`oT+mE3!`vEnh=&7_LcRXib1PRIwx zLBIOj#oL(8RmPL7>LA%^H3IMr%wX{$7uHF>>A#M=TpXFdW^xJ__vWIf+8Od*a+BvUZyX__7?bKqJ2Dz+Dd1=pah|G( zN?SG62^$$On;W!BIQome57w0A%fl*HBw{;k{gt$!z_TBwks!}59ci>9pmz?gACGs( zKY3+qYfA3V*Vlj0)$SfT`fzsU=*-*#?C7(71Ncnu7a8oQFt-PF+AS z8YPE@$`Z6U)CLJVzI_3+)`NyE%?vxcp4>8!m(~Z?IgA{mQ4ZrS7-w{>S`REftz#mR znXwlhNr^0?WB9rcY0HD1^l|J;RFfRT$`s-F9y2J|dh@`{S=?T%L1y+w^pt{{HEOUf zG{zbn{ao!NRZYpq!mV-TYhg`>9Ag!`KhaVk&d07KKff_6`NyIR`8{+y?DpxAS~RBk z7isp*w_tu5l8zxmnavesNRnl_!v zr6KxhxIf!1*BXv-+ycuC7e6sej<>x-MpT=whZm z0wlQ->C&|!(w0!FepYF0+xs@-479$VPZkXCGH++X0`!qG%(J*06Nb@+psiMPz`<9O zDNBJqn~qf1I*5Avj>h{l*%)v18>}%V!^Cnu9%MZmTn%m4= z;bo|qyr4PJHtJu^@*q5M{9R$4@j{UJdVwk!Tb9^Nr&dT)kx|1oaOLjlROsx2Xu>03 zm9>A4vlM@sUs^Tbs_J&?h{zuLK}jKFBHwF6WEI@_;AB#T-yyY~7s))`Z8%ssU+5aX zP7%*~FiXntDn`kxa>)qYm?7c#Nzpn319Ry>sYIafq)9A{ zXhk&d6{a|3e1ne~=Y;ndvKtG`uhdM_4afn2dfYm7Hn-4u@%zRok8Vm5+PVddb!p~T zo+s$b8AJYg#)+WW-INx#TsN_12kh(w3&6X2!D?=d2j-8R0G?bt3bd5Cg0jfrNVJx| zo2>!DGXBfokU;m0=mRO~m=SSL+eN@4zEt7L_qlUL%zjPw0q=l&K^!+)isSo>OxQS3 zPGhb0i4&$K@X70|6?nztF%(j_Wk=hgk@OAOd-6U3kF@8me(>3kU*%aT@9$0e1^(() zgYKpa?6eiFX$_!jRL`PJbSm!<;0oag8wHFX2%(ZrbkR@I=F0bu{ok*jzp+dme&2nI zY0UpTbQ@WiUjr9^aOXNpZB=XE~B8 zog6hQ3H%f#Sdv=I%$N(8rmEC+(MDvScr721cUZe|c`3dIJ!w^}(j#S)JyB@tva1zofAAeSSj@|>|e!(mJDXEz1Dz4 zw(1F#F7&qSnvz`z!REQR0M+GTGg&hpbbkw)p@9Hd>mQSk75StQJ3BYf#;Og{X!}oH z@7UVdSEzYX_V!+3PqutZ1o~iO20t)%9BBg#q1Zs76>7ts__Ln%xO^|1flx(91k$(9 zP>Z{cz;8s3?eCuL^9sw|Mpqi1o!e+W;f|xdv@x}PCMV4i9CKE$$5efdVbbyMxlZH_ zqMMCVR81sG(6%~MsZdVgU)uzwC(UeYpOURFh2_C2LVOY0r@$Z2u^^cXR-IK^)J6E` z1TZfBLUwUpf>Kck(uF|F6vLX_18sXK-N!MaAYS%Gp3Is|yG`K*btY(t?c~ArakPJP zU~qtpHH<~kK@;AK5(Ly{B^KUJ1kgfFv@q@l$XyydPPwRJlhDIkvwA^iJ zeh$i}i2Gqjk+^nn+WTE_ngJOYPYMy1F`|WM8nNV8FLksY5p4jnHWP^APBhEdJQRoC z=%a1LOBQM&7mhJ6V^w=dS%NKW06Eb(gVZTfOKP^w{Au4ZA^|~b6_x(y+PP1Gvm?N7 zMkMx>w z7cXbdMX8{Ma_S07>U{WQvPnpYuS!j58_X>-yYe=>MLKWT%u$hm$cq#sAjmqml8OX_ zve9YOaYAcjotOc;FcTmT4%3RC!i5&?a78!l^Mlib8v(5TA!MlaZ%FB)-UOl4V7XO- zej-8rOWeqg*Q$5!xv1&j48{9*b*rHyKOXSy-~^u^di}yy*bZ>9_ofQdfurV?>%8h~ABqtGS6|>fm_( zpk*z(5A+?tILM)Bc&aC32D zFcb=e**}y? z_#G=k@UEVS=aUWBNSfp?LkEJOlC@y$CD0&Kx(fEKnKG3I&+X-zs!BPxO3Ko>>=+SJ z+C&aa^D9IIcc1|hHFp4Kt{=0%ch|EhY2$OZr8?fnP&v>K+v!!(wKO_(A%j2VT#O=N z!3MJ<|1P-^q7cI*azJXy{`t9(TupnshK!#O@;leW9V&T9Q0Ie>BUG;s|Cl}V*@-sX z=6zT}IAox^U4aRwF9a#4wZ*zcyT!&c7lygjbvmPie2rUwY49bXyAG$|suXFL9ts=K z4qRP_2Q&Zyz~43TD{WA2)8qd&8Dht)FYj9a?EX-z#}^e-1HY1@i2V4$o@*%EF8L>P z?e_Vw$4ocKECYE0^>9d+m-sg*pOM&raE3VpyS3@rsWasjjTPi{~WY3SFzg7)5vv#N=I)}@fX?e`-C?%ha ztw6_RVyB5kRHN~EwpWIe4bgx$n6c58xo)f}2loWS>F2RFK?Ai>gJB|$QUv^)1)C^eveO zJJ=e5BbxNM8uOvxd{#)!qzww<#&EnK9sf|Pt2@!&D7yK>fis+KC^P%~JmKM}J} zA>(SZuj#Ljg>dtX%m~uy&n19y)at;c#k_?@O>{!`ZyGG{C{GDXbj67&bjE4xzU$`b zip09WRL{JPIwpkhv8d%@6s3VqsM&&KF}lA@I&R+DO?GfInMUbGcqLwvWrx5NYhJSk zpSeZSimrJnvo45D@A|zx3tmMDp537=E=s5m^YYWZq6!F```9$6Zxs*0#D_d@gC;aj zC~xGkOU`Y-awqt3B>)Mr0%^ZzI2033SiLuxm0`5j1@8GGxE1>GryODseq)Eqt8xGo z8QI!jI9K6)DE}4%6$NsV1UVJy-QzdO8Wx==8<%BiXd5AaU1xhF^niFJ|@ z+ote6h`xZniR0Un zTCdy4tyS;FDJPs7e}l+5AkJJv<>7a81P@e$#%LWigZ-lZ(mhy49(%kQWLs79uzYez zb-ONxNsp$OPE2f8`AAHR?%Z)P!|g$8-$T@5;?2?#s8QRcrZsegeiY8HfVb+exH&VW z#-Zb$>(R9vMnQPGk6}0KcUZ8J^*Wch24i`~{L-lM-uIpLo{UiIw1p4;uyz_T-pfFL zJ+#oUl}_W%O7R<^>=6|0dV{PZrEc$A(v`6}6CYJsWh4xIXBSjNKl_l-c;492mFWae-YSVG~-&p5;f%|AZeka8A8qveSqN z_aV)4^ zr<6S;`zub@G~Sl2sqbTp>)eRJ>|L+3x8d4E?bJ@4dVzD?NQ9yps3(biTE< z4AQcwPynaZ9b8M0{U*0F3R=2)KCH%_nWo(N`%h4rjkLZkvG;PQ>Yp5dTVnc~*k?sL z+|G9E0dR3NbJ3CNM!Pv|<$k`|G+Z2+bl*lqrZb+nYRX|C(^zgWg*DJ=LZw@FEr@vm zma={gb%;xMCIiPc+`TG42THf(5sVQ&nzjm#uE!=a-X97dxw6m${QEobYOzS`i0;nd zN_0S=hZYRWUX(7SYg_RKi!OB<_oj$r8jnp&KppH|HGw4-d|H&zfRZTrWIxo(JoQ}} zO_y3keZFPirRMN*`h?)}&EY0AX z*!S}^HjTr=rS&TYZ^FCLkCVDar@I+24uSQ-0o5lL^uw>l4;3MLkNmrkfg*!;h?8b< zxJL?-N4irY0P|QAY^N!NTbzOJi8G;V&7rLX%`J>+ujF99C&rtl>Ga$lbpW zoF@e<0%m>-moe$h<(mpQ!}aK#ci8A};+hPWx%V?Z4ZrBXNVt&R1 zMvKiXD4ufZ;>OQicW{Prjm2U)Fa-+pE2>gdwH7f(E}$0$cjR( zpX+%#N;TZSY{R`0#5b;m8J8v~hdXuOH3h|W2<$h}KBhspBDGsIKb!9V;V4X_Ks}xN zPK6s_00EKy&*G|~fvv5H<1fK~|NNgzr_507(^dWrS0N4eJQxT9ia z=@{N5$LhhWDFJeUJ0TPvkf;LS{kj##kBD+ob*t5rI|1lo)Q+#N^YqDp=%7FNDgHYRYRQA9EE*aBT( zR+TyztFI?cfv>)(f)6WpTh8nn0jbI8HHlQ4GVa}1gl2yCUgYtCQsv90IF zyFZJxn{alvA&Y?9H!SU~zEew2h8|o!8Hc~XA&XwmtL~ojATfACJnvR?<3ED1O39i= zT$pCC5zz+(6*&<0MR4*gG?eSYgh=RF7@Hs+=@!)$l+k{`N=PG@)|+lv^VCn;i zV@brnDj>a~D#YJPzd5CtXy>$OhDnOUeVOQ`&8lwrI=vcG?-TvCbI(Ik8i2r%i4YtO z=R=kV0EH}~ysNBL4cq$iP`0d@fvZq|UX`Z$geuOm%{9MGH~SPT2|S|xi1H;K;|jMt zsqRWM3gM5VcYgoI{vG6bpi`CkHTl?ioDNh98g83LvQrh^8c$9Z&zZxP1eoBZ8;P$z zLRb=vC=ilH_@fR=$&Ne>(oCSV)JUZnXc}B>j*FSJ?{rys+<-?4g;=P8x_4V_iHX8& z{ke43!V7p%j$aI5??wiFhs4rj0YU~tcDhUqoO^si!ivF&Kg4gIdg~7YW2m5H?KFj|OaY2z z=rUgeLuF1&frYxXN!SJ$$kcQmW--a63C7fTLojP3nS8Nz==v;g5R$(VKoiG|k-!$% z+QUQT%A!R=K(*u-7;dJvTgfAWg%NlaEM2i9_ByZi&o|xCXLil;%Oo|yq|+gdlF!z> zLbukd-7L;jqX-3k#|x-bPRZT_6c{uBY}Idb+C-i7t#8{odMmTPDxE@;9IF?{=4>yW zCpSfC!AbQqr=pp!b}uh7&aHf15GMsZqB~9EDWw1hdr$EC$cqB~tX%ds2v5Wl3{#E?zk z9E$?5U-wln%6GT-fkAV4I%;flB1PWEYTAqn=IJ#&Q`d^E*x0oKoEk}z6in05R{@hrA7q=iFA+CC|g{LJ~X2O%Z!uski~E= z=QxaN)18>HlBquXptMdmJMRfm!cAI*(bQINkm<&tx7!ckNRF?R+-RvF#NKe}+C7`n z(jzfz`wTQfX2OQdJ7ZODbAd5CQr#f0MJ8x@okh{rw-47L-Pu2!9t4JxTH=@p-XSS< zEv9%gm{9iYh_ic!DTM-5Im;+rYVL6hnZ$W;_;PwA9R`lMJ-Rn!wq7P9%I&(plW66u zr2PS4n`O6Uhg99NH!M~38?H`t!UMM+Y#0)2Al}>t4D}2TVU7OF7s%9CcnDc@m*Tk` zh}aX;P9vqB%p;zbr0{rN6vL!ZVz2S;{$%%Nq`fW)xDHBp?af0HzL5*jb&CEhHnkBb zrG`SE2?>cdq$AxW}RkF0hRY?~ul4cyT8OrbHwahgdIKvvs)h0;%1s{Kp3Osr&bCQx~sRj&82k(c3X@C%KU)4Bg#&b2ql7 zAy(*DiC^$X`0!)qrF$-o5IpjAsl7h1Wjslr!q7N)i7*+E)0xB51It}8ojaOe5dU2a zXS}2Z%lIx=X#F<2Q2z(xf{>l9sfF49IW(9k>DXn`qy7%ja~M`aW-u@0q1^x}E}s|? zM@ta-&8*)?y*9;KJiqkSVx~0%iu|f}XQrD&F6QU!tms!>x z;5E;~!2OiPqg0;8D>&x@R|v+vBsS~_#xNd4qB%x9qBVDaRlAzsVfil6F4yD z&|Ml0!{;Y=zrmSK{}eO3(s+6;?2y94TmrFue!dziLs~!r3+kzqEEimq5S=E8kh`pb zeh1!@G(}FodltQL2~-OT&M7_BEVln?c+<<0rkkF(FD7_wR1gm#buULV6<^GNOF|7F z;hBZ`EOrH|_-m&bGMJed!AO3Vv3N5f2qaWd0;-iJ=0c!x#_a?csU#1g1e|n#QVPxa z-(WL4RD~B0kPcCHb^&OGKYcQBqUBcn@bgzbjXV8>Yqo*L7}ZK^)`XiRt@CsnB$!(Q zaoht>6zwkl1x9V5JW?yM_GigUyJNZDhJBH$rhpX;Bswhuo=y3IuF4SaRr+ zO8F{dT?k`t{mt!=eV{aDI_YIiqlt^X9B36cy7WCReE0> zD7b-@y{jzMV?{u~C{K(gbZCzCGRH9$UJ=3gtW&+%VlPu4D(5R4msp( z90MJ8A^JxW5t7>)iR`MRY!d~ogS4p?kEgwnGpR$Q$x#n5e!brOxPk?j5TE>;L0FsV zr@xyvSdW}wr!|hd6C#E&2tv`k@kv(7%NEV-zJOoM<{c{;=gLKhO z!=yFsL_7JtSMkfw>cB^%I&M1d7*+xq`+g#x5A$fsFE;G}@t1tLzw6Ie-@jhsVc0z( zUjJ(Mwse@KVU!Y2aSN)?S-Ijeoy8qAca0gAx2LXs-v97u!rg7pE?x|{z06OQiWD&9 z9GoGqFR4KF!mw$ZZY5o;0$Rx{8NOjk)=8ox<%&P4+g~d4)^^jP15Q6}*Hz$!c}#XB zdrFIF7+|&yXCbp#fAEf?9pNSwsR>q}F6wur$@0`d(>7`y?=Ia%iLU;!zP#`$F(!A> zU(doS(5<^j)GSguz>Z7#cdZ3n*r_~jd%+n^JT#HHNEaxMh6*jJLuD~t4rxUL(`bWx zGUkQlBB*P8sth+9u^WjU!PmP~p75*u=&@|){BIqqq1?>a~14YM`vify~1if!ArZQH1dE4FRhc2coz+xEQGd*++n zJu^r5mp?h4|L={p?rWXrqVMK8Ee+kob;wBIBEAu4Th-a`yPtx-D1={xZJ?IZ>kJX| z8T-AEx^8!#{WgWQ$n?*bS%u35iHq~2KI1p4Vqe0Gj~0c>lQ*6ihcGsi63&=4`o!MD zPF=lijs>JJE>}{k5Lx_tRrazN7zjG`D6``tz^v=Oxf=#b-)Xc$F3iqB?e2Jr^k%G; zOCpvU^aps|rfgas2cB!+N7O+1R;aqlBV{MFR|)ZXysV9iS9y_vS?4q@w7oacer|t% zPcXvnh2azMl%tQNlHEfPI^A$`PB!_eDxQ44Op}DO(3*+Ud}w+)nOkPCRG)@QX7Uw< zGq%59YfG@}Zn6h3A2AnaZHph44K|8oTpO6W4^|<>`fVA{h`acm6_S z<3*BNZO1EsC!MlyAbA*?jU*U)ZTH|_&KO{Yfe3L?UzvJ7z|1L!xm2pFz;5OS8n^i?V+Q0$smYq6% zg!@K*^qC#`o@eizAI&HaLX|y5#f(#^?F*J-rtkf5YfiJN*xVWX7g+C{a9y2?A^htA8x6WI=y5@H0Ec9P94r>yVv?*fzx(YiyFbo!2(P4J$9HAycc)nsP0Esk3!cjQD!zUZM5@{XfS6%yBPP-Qu1>cgSxsb}{=3aB@Qd(GkS z6y7{!maoL&U7XD5vMG#_k(vEatw91uVE36vciZ*M&BiA0 z&6IXE;>y_GXMZa!E@m=Qbbvaaz9#uVNnB;clP)Xd zd9V^~FX^N9{F%nLZv^p&cjFXh8mzF84n`2wWOm>ek)&02HN`v;qodXY;DiIwAOEU9 z&f$%?u>o9K&7l9^v^04GCnq;MN8|r2$I+-NW4FhO$fMP5cp8sf;D~tNZ~}aGXw`|s zE?{j|E2fD(W=*0J@rO`y< zQt}1T9>_#Smf1?7j7h0oAMXqA<3bdA40ib)i-+}?z%%~0snyog4Re4gj0kvJINe*h zp~%&lhoe1ZlGYde_;Okh&6)=5Gd~F&ijIxMJ4mq>bY76T04{W${xRb*=kK)C3z9~s zV&hT2FhUecV(<>ApKEBc^pYLqs@StSsD^82TCpMpl%81z@)8h zgLe5ct`q?GFGb)uwa=>Fqsks~7ft3_!=oEd+wqh+JtubrxbtsbCH)%N8Hyc_ckhxw zB+}3epzfBpj(jKG&^G&q>_#>mkpTE}-f{OW=cBjp*xx357IP@@;iHc+BZI+O^W3e3 z`8vr|Fu)=pKw?eaCS6@G6cD{jWw6+TP9v)#P@|e%eo6e4N|lxS$D}BmvsMn-+r{&C z!*G(i>zYZ5ozRBEWqx|jU{BFQCpFsQa@(Q@@HCyp?~n6qpe_+E-pl0$@G|Q~+}EC? zn8hLY3PqL1JDn$T=+p}}uTqy@b3Ti!kHdlVAsrhZ3MW?{f*S$Fno))n-@VAfr__;a;z zHb5USppD;n_s_u#LXaGgz`1NSSwIbTpSylcZ5=+Vpn)OU1;{O>m}StE!2)mgA+Qi5 z(fWwcAFlat)(Ln>PVN#s58)(4zUgV>&@%XbrJw0KRAS&Tqo+xR`b%ay%s?SBbuOsk zQ3nuiN~!p%SqFqd1I6*jhz(UFb5&yfW`3Zn`qrAPT+OP@72}l$<>qrn=L%Gf9#6xG z)97z5*NG4rvp@BFYfi!HK~dU(#p3Kasbu*D{Wr+^oa+uOot<0xP4geC#~1bwfrg)) z`MgYOGz9%U{WZiy3!`q5G+~ATY89U5U)uKvD_z1UX)Q?%^OAOAf|yI~snbF=KZQ~@ zN*eGGzhw2d^1A=U%BIv}76t?u1DgY&0Eqv48DeZ_kMetJDCXo&yRmweFrJ) z*a6g_-b3AniDdi$Stl>4jf&uRhp_L^1q%2GUJOgiZkHR_A*wG|8(g@+HmgJ=KhOXE zW#vPHx2=?6p59g&9a3$t9>R)@2!m)KM|FVZCHSAS%QV3xE z&KQ78d?CCf8mFv*P@$_x1IwAFtjXx8Fym)NhunJS^R4{{D(-_B;#Aaj2Vb}ukC-FD zRCSS?Q_I!X<|EmiJA1$h4Qx?-xI4Za**mlU=RQ!^Kzumr&<_kR%EiK25s~ z*c<*nMLW_JWR%tfsOF+NNn^;=!v*8iS{Moc_j0~hXppAo8K->} zB-2D#rA%coN=_JH)qJ>Kwp#Su5{dzqZiM!b*y8C$ZO)FYJ(Nh2!;=*)gI#m?_&a3n z4s`Nzuqmhk|4cL}fjF4Q9c^`QpjwVV%0~N`Qy)P$v)t^K%^_}FWpht@!w4S_SYL1& zGg6#iHRwa&m9yr318_Gg4997`cSur}A{$lx<;p1u(M5(9Dt#d^ZAfHxU{_SuS2Mp) zl8R9pTI|aWf}-1dG^RZ_7a=vLgQQ6K;1JfcpmiH`l7nO!o_Y`t$bM$Z(G?ZHyXY(X zHOx{hCswu8&BjKW+?2JGT3%Rf#-|c`@LY%%3zS88vh-TLa~~8kG7jSPYf9P9 ztoZ~4FV*%mbTW!XsX-DW(R*p8lO#c~$O@G{@CRl|YmR)fk4(EH~emzPVOWCvh%MF9$Z zivQ>8`mZwoKVD;rfHk(pis}QX@q@B6#^c-d<=_|xCLqct8zIkxq_3awJZ|BfE4 z&;Ujc$me9&(cd6}|B%p4@jPXWapBHJR=J6yTZw(7Gqt(9+%VBBN?+Y;YIv5PiG^MT z#Nn{2w&6vdwk2fD*_~x3rWV$!TM}oma$!f??d~9cF(+YBg_npPSpNaCR}P2Gwr9*i zz?Y+H6W3MPg1GNfxH_K|I(HUIT#lWM!XE31Nm8vd-nAKO1Y8#8(EdOsGBnOswsQlK z-e6_}2kUbB+55q;sFo-Dk!XLv_*$G?s1E6GvO$?al5L;}KH!~)A3mAbF!p^S77vj^ zC^NPixz>whCUkX_XYw4V)ZNLb>dft%$YM2&W`D{vnQbCUf43c zGu0>iuKcirF(J*3H)y|#DmbmUP7cS(P*pwUMiw(h636A!D6tjj(XcCX<&hSwDJ?q7 z(LtRMltuw`)wW8zQ%OVd_ho|wnMBk`X~#?!G%7c&Vg3Bbe7-?ZYm_x2fm_>NEjPN> zaR***zUY$-#Te6{g=4!DAC5Yb58`%#2W#J+?#I_K@@g#f6`=S|he&&F_uE!YtYnw`}-yY^0158A$62IzcCK`+Lj*2-3##;JlmrH+*9-t!jrtneH^pS zjATMm?N`GQff(X@?j&Kvg{Po{6gbb|W*;2w;pUI&=Fn&#TI+m=do;f<`9bV2?TDKKr6(L`(dv@+sWnIX!iMkDkE*V9_vLpG;Va*vZlj7bTyOO zj$bRvsdH1mI*PWu?uD-L_&ft_Th06r2z-HEId$Z28Y0GD8lLm(O0PD3$?c+Eeu<^9 z3~e9rq33nRhkLcP`41g*!S`1&oVrio*Pm}K81#SzDggv_TfMD`=u;wQvwYt+FvkwS zgR3K|ezSe9RLl&ySA*?>R1Vk&r816s?=l?X8?|2@{ajKjR(LYEeRDy~^M|#6MAWOf z``F?Gsx@E=%*Jz{dj6V2_xQBSK)TiG+q@`#w)^MpK?pvFVHvPJ>;bli|NG%9WNzU2 zA8!niDhf^jOq(y@@C_l0w|9+eEsk?OMdWlC>yeROJ;{&3$PbV8j*AeoyWO%Q#etTY z;{YGZ(eZh^c&Ze}?N?Xfq6upZvgiI)9$wC8P%HXeusGY(S(XLLLr}P!2k7{HFD+Bl zhyPjWCTb6&)T9i6l)BMpMg_1Ju}Oo8S&)|}aah)XrYJ|u<%h%Wg!d~USLMM#>Iinj zJU|db)GD!9FT)qq$>Wf~8t9Wsf_fjSYak-GtRiCl;1DD}UtKzpCknZ#?3b4sBhQ=( z4)d*yLh)OPYVfM8tlS)Qm}5q|3nVTKZ?RUA;g%VW*I<1K5O+(*Hwhj3HT2Umw{+(K zK1JzwJ%G8a0U`-P#S8$bi?z-4L2JPFmH^Oaq7d;gfL`C?qi{Ws$mu(~pRy71s|2{k zV#peaRDCYUOB(IvQ8dhR8aFpdy$I)s{^z&7mvdU3f;+5_SICib9B)}OHz5FV`E=W< zPzOR-$+`moTY$X@?2~LvkPgzW04-iLR;UNgm1o9|tu{=i0Y%c zUH?H|GcMK*traF3dJ*`6^+s}*bf7foLx#fl^FGLO$jl5VL*P)^pzvAta@~c7KS~s} z?jh5GuU;~#-3W_<+*n0x+o}kTkfR2nqRA?*T^Y?c`8DpZvpbntxkpM=HgffQNn5DS?9GJnN zK_&Vuh>sbJ)sEzpkkI^y?`^k6hmi)>pj0`OxrCasDn z%OhjmPX^J;BU45Elt#C;)gP4HQ+IP8{smz`TwYWraKYNGQ&f3}-eaWl!S^e<=clfC z1A-H$kLJERy;ZsJWz^Zwl+o!B)j0_P5Z*{?UU|ZLUg`LPoxt&-n2nZG=c9F(W-5WJ zS8{YTLJbMdw71nli-)(5p(eI4QDU&flbfI#^-pNd3#jQ6nzOolN2`X$BwtqZDd9BD zIUk**H>!-QL8eQ?!WFPK*$p*}d!hh=x5xW;Q*&JAWtju+9Gdg_kp32DSx3?hz6hvx zynwmd6Ay`Z^)Jc9n(Wb}i`-nd>5x004+E->>eF+}Xjx}NIWw8F(0YUYN*}0CkbmAZ zz~qZ_nE*44DgdhEKgDeRzZ?7i&-+BD+L|La2cl1K^>JDVD6&^g=Sulf>9&^OA{cEz z{ODW~CMY>RiRvt7e8QKHoK>)aea1lghV>{Gro;y4hR>^Dxrmr)SA!+v!#um??B?Vq z6Sg#IC#%xMabl_-g}1Jy3r(exuf)X*w~C|i(OjWYXg(7Iv#PsMva_qJvR&h~m>Fg4 z>W*@e*lb)#-=j9kGr4QiG29tb+T$*s<+WeQa#-41g`ovwS}3W-9)%M!XF`6hBc^p zV>X_~(exanPg0dhaZ-zA{5^O|716-s;akTKW|0eCmscdbrnk&bbSjTEtd3J1_+y(l z05lNKS@w0lJttrHsfFqIzRjJh(De!LW99_kE;ex3KDr}StLYp40(Bs3OBLM#cwp@a zVQZ`OygDhpj2t6FEHgt+aIgq-vTG5p*aFb@?}Y-(qK<2W6O{H{PHk^A=YkdLEMrRi zRFZUwi>EV}9EQa?F9#Z1!{#2an=`+Tn_|hL{ExRL9@o7ly7&3y&%0vy!Q*ifpk;|1 zl&*>^So?&~sUVlz*>Y5^N;0|A`#{d?SmQu}q}KZzk+ORIVf2ZMeaSfC^i zLPoxzFGZ$gf`*Mj&2rwUV0|dk(qDIMU`Qx*4}UiD>pyFCtsG`8kAfbnbknt5Xt;q3 z8pKNx_K-r^h*a{O_M#SqJt}5^M7Y|$__*J0inD*du&jQ?sjC4f8FI}8lxpmVU?11UrulA z%S_+2tN?Ipnx|*sWt$h5vaz{9t`U%r(jTz3wVbfT*VVImSf{D>E=R^5bM6lJr(~ZR z<}P426S`G}TFGhSun>{?ctsW{X!`!93mQxp@0a!47eNTMSrE5973PasJo2-n zM`glquS7#ZOrR+{{2uzlaWf5U?q#lEOaQg7pI2(^T3BIJu?yVXGJo3|c^j+yhmv2O zXQ0_NC30SV=Ja}&d|f3F!VY=S=g zMzMJ{e4fHgg*)n=zWiM8p3Uw9p)>1zc;yI|sZ+iA0xnt1H{f)RvY?E=4}IAiZ+W{m zZ-s6z5Pm&)fvFU-eoU39dq5efoOB@suleWr*f!ep`_pc-E2UolQGjQxuf4$G5&-Ep zX@u1;j$mij{_|8)Y&|I$cZejMfLcWrtVk51Mvtkn7-|!oHP)54J817DL`e?Abmj|)fLE{Q2y=1YGh{aBo2$3 z%njR(l*SRpof?vb*UlgF9{S11OI=!>JAA?Tp)lgs%L-)&DGN!#XNa&T=W41evc z-h?ceU;ZUwW}8CDZIUqCJXym7f;r=i{&;U#e)J(6W)vt<$i=!4^T0?szr%>fG1OLL zJ&g3AW?7#lz45VJ1*+ZTxZPj}1PQ6)b<-!? zKc7g%M*)!5fFwsfpeg=OQ}q_Mrgn6q7S<;62F~UZfRBIMKT3>O_?zAc%I>dj8AVBD zEGgruOk++e!q!SqCe&Hz!fGLG21{T0!JPiO{LDxcHy*pzj*<3Sqo1QbV)L;LPop}9@GejS0LTEx^QXW{L04jE?X(GyoWgSU4 zV2Hk{a{8E_1FM|E=3K%QBJ}*d#ZL(ZIamSMfiX}{0h!DQqBT-28-tCiB{FHuyeq62 ziaLp2W1MbJr6c8T4Ndrt>lu5BZunOQ=2d`Pqbt1n50#DLi##brIg*M6-MCVQd=?XH z8T3eUMCd^=p*rFc1vTS!H5O`mx0E=_x;|H>YHu)nvG-+$(IV)uVBk0_mmQA^VT<nkqDqhm1K3l_Jm!cX$3%K=o%>sl?IlFDx zI_kj6=^>nXO^tEw2B?s6*+m||kpO%j*axxAUlajCINsk@5)Ojc+&{Za=HH;2>eQ7{dI8Lcpo1%>kkp-S(t{v~YQjSPpRLc(RP>yZLpv z{bsktBpIG|`uxB0N1d6ui7~7a`@<>$(TmodVow&T(HgNRfYVEqCJECt7Iyrys>%KEv zdVWCqu>|yfp{IunA~s}?_nBZZ`mG<8s<|<$jpHa2(NHDWm=QlrJuoFr`_OvCpi~|k znaMxmERnvU92_L#j|`=1ALCiD>+S8U_>J2U@r2qYBSuzCE$7)219Bmaw^8)CQP4wC z%M3ssG36?aVHnh9?^h*3K9#M*S;($s<4nbq?-FRVH&cQ@e63c#-u9swZpZEH>C|o_ zzeoy~DtMEh@1RV_=8GyyEi7r)ryrK#Jh(mVLAmc{4(xkl9<)`@ckCdBJ*3w@DgWJ~ zjaCg>!}QKFH>q5_Mw!OK2%3;sC5G(Qandn;+q{hNlllQFD)LREi+@HSg>iihA|!iA zs<{xCxCy)q+GQ;4Gujv7$;6O~CDoU1ZZ{S^GRT;MIzf_SR4TsZ5xq7N6Y}i?42! zSIYf$%SqLq(Ido#92J9{!d=E|=nG6vrr5u4{pFww08LvcPTcB4mm1BI%c~6FWGF zF7+)R$cgUTjge<~a z(q=hOR3pa9tiE4@kY;E;q0ikl4h*fAExsKSs;Z3#OEjOiuxk0?j5f7;EBDzLQ75T4 z>k>DhHCP;Ew+2Y!CMLKVu%!*-K5;rqNb2${?L)+JjSG)VhgNgV$(Rc?{caWEu&|VE z#}bT%-0x%PlaUaTiczOzQ;X0fbr?+|nbFK~hT0D1WQJFa<|7d}Dl6mUYOFAX55I@_ zQ6oO&3?^C99SrrTUAD}Fi_5I(vz4`aW|3Sl*5jb%gA$p}R6~)GUTL3U!^9TsWc?u? ze(;(ZwpZsBNT`cdlT^v>n)inp%a$d&7txo$(RV{`yHH>$G8fJ%(hvj1+Q8oiws2;s z1-&sGs)@cZyONsphc9oxR^-s=EE>-yF8=lB5QLtJ?U&tPQ98jE_ngz)L7#6{<(Oscq4ibN4*pP|x^IZMFg8Yy~FGueKc=rvJlF?z_l_?-%{ zSmVplE9*5UkvrbZ@s}JshmRNV)$z^R^@GI1;DL*9V@1-o5Q z02J}27;^^WcZpBr>wJl#wm6O=d>%jT=$wT*!XxWrReSqTE?JkCS!BFEtwMNA&*sY4 z`ypRFjtKqY!Q>xiCwfeTo%gwyshUwpTjwHb?Xz2n+pEqvEk;0_e!wJEN^+ZZO@o4Z z+`bZy)5euPJu-lNmu|e^y9q~`A{3TAHD)sTbFmIqFGh7ST1H5={x-<#h2l^59a7ge ze_%HN|8x}DN*o3!6=k|UXVy>oyK^sn38(;#&f>m>ieUB4vLyhD2rsq?cn~{eN1Se) z{*32P`$QIT(Fs}p7P4BP;0xI}W6jx>Mpx}?eC#^cIXa?AfA2;LhtaVKr;8d*LQlPpo-B3ebYbaM_COT`DTza%ffr`zYe60)^>;LLGL zgfO@8uMqttV5RRMQf0W5}CYRTR#__W^(Nj`W!^Nk;tLHR5jC$-)~ff+D=A zG15gU557;7AjVok(6(TER$0?PGR6`gZV*jCq9|t3ZGlmL*ThjnJfzCwK}W}+2c3@v z+e0PLg0x?l6holr`zrf0Of#n8MIWd}e@-0^<4)Pf9^B;+Z%C7A)V1ljQ!W~X+s)=} zcn=HHp|LtKCl|~%?B{opV5v9PRH+A0G6yeFF^DG0&b%Ou?|V{5zBw9FzQae`#@!Gb zA@oa8@W@t%Y&c#%i=k;9mI4AqLZmi3UXyTgO1DaliOZO3F=NiVUPAy~2&Obdze0V& zbdsZvXXGg34;He4>W+{-l1l~DYW7Ze_&hTxX%zeEVzj@kD z1u8A@ig~cO5ZRl`qd<4;9)Cg~^Pb(|N*To$Oy%H3WO|KYJ^)|s{Y4**V4wJ>hzaZz z_Pqjo*N->+N0p)Mbk2ApE^Ymb*>JSRai?ucu< z$>Eg*n}NDYlC(sm&GU&!sNsk6@SE@`IbUl(81^aipB-C$-rHKEpz(uo_(p9g?H+n}qPB6yo{VmQL#HU?PMaR6&F-G9H=*;qIk(aE@2J6k9@8#tQ?nVT3{0c@p0Cf3#>#um0pDjSw3lZzu6cyVySt3D0}&Oe_u{1*lavN!IIwL_lM)2Q>psr z(RbD|zc3qqN|;E2zHM-H?eE)CevM$Xx$HXE#8PE7K z4-x}OZt8~-WfG}zvZh~lX<m#{!*& zLtmH6YqjMO38%Sq=w(~Hhh{T*8M{b|3g_bmWDg92@bVDzC1^Q8S zhXVnqi2Rodhifxg!#_s|tnn#Y6`x5NP@N2k+4J z)3!9`4jyZrJTY9?>JbZG9e6bncO4~uSUEuc9o;7R+p}M^LDJ8BiChH~xXuGfsDgt@ znczM-d1<#`u*%g7pmCe54}W1l`{S-cF6Zc7mG%g$=~n~==k3c}ZhB~d>`kzH3;G7-ST>)0we>3ZYft|b zrD^7N)w50Uw#r-i%BPf%3%T9ro>+P5kCsPU)+_!e%s)3YW~KP3b^#3yc|bk*Pgj9| z>=}Y~cGe~awtqJYMH5>9EAT(K3fL&i$nDW1^00Lq&MWb+1d4cR(gw;4uaxPqm>`J_ zXZ|Rv%520F6nKBaE4IK;E|UyB+M7sc+arN8Zrj9GP=)V12c4Xn{MnNy*fN(`XJ3BS zm~+&LJY`;CuO)t_39a3CWoZT0Om7gxI&M)PG(rLs7x+n!gqv7cB1$v!i@2x3Gy}hr z%%2bNQqLYe_5B{Gf0Ko~^%_xYqbJx#zycytqz@5YRgh>KW_Z<4zULwM5B(sL=1y6a zY&5c0#4x@PMw9}3eqfqxd9+`THo$63d32gBG23J_&e>PXYC`0e%bFTdKf_GmLpM1g9zi0CH zRo~om0+S4uz<13Lx$!d(7U9^C=|nw;_DNomuVA;TL&;W_))|lrmRhi&x`48zMb`M1 zB|*~iM4;mAq?}2M`3wR{bX?LK*N2XY4kS#``Tt5W-`fu5sZVL7oiv};%yrE6p{uni zwf9a|`Lm?(m3)`ga%V<*UbUwKKMxO#TPrUjW1mo=o3Tl z`Zr*M7Pm`rXG2)nwxN zk;2^f_~8z#H}}`SiIh$OIBBH-Axhoma6Qb>-%A0(jg31)P&yQ>-O$mFjXTp&u@q)RcW<)2 z_a;zO*be7A#Zm&$ENXaJLn44(jDvbOs=&w*J)kX|k|qtEWG{|t3m1wdF!>;eSym?C z&(LdZ2mov@%!hXXBar%*(AOCvkpw}~^K#yB8|O6y{ahx7QMU-_kH07_?n!9bq^kEA zI_&k=N{S6g#$c)d7sl!*sSY0xqd?v@2&7c(^X@HV;h)+>sD4<2GtCzn?^MO3CRw0Q6E1Q7<^NOW0r``I}E^X(o`Rn$Nq5w+Eeu#|BnSc zuZbzO%XilWa7%Govr{NpIfF^Y{@6jvCFp8ReA(WuorTAbsq$^td3gC`%P!=?#=Jm3 zF8>8*%4Pl(7>NsvHyh35Sv&Ra*$RKj3-?{VaprR|H(bQcR8T*iM*|JZ6tJl|=&hzmpVW$raW3%S7 zeQD1#eb}SA3b}Fhb7IA9o%WULwO=GJ`cmUfuuP$W^tFvc$9G|bw(V9z1v*w)zM^V5 zJ?~tU@*^XjH7`B1UZz3mWqUsBPE#)VYfeyOFeT=OpK+{DS2?$Mv+O*!Gl94Dxl_!Z z<{abWd(yB6a#1l_%5%RfqRlB|M8j87*9|R+x_EZXe>Mj6!xiGpzG6xMm!~pgDUdLt zO*Ef)Ox_C#&Cy8kxqj;=En7O{m+a^IRejApv7Xu%9-hdtiPH?kh8%}7F~9KE0@eIT zp6L-k?r>2XdjaXfrFp5$R@pha>c!L4 z9Cgw9)FaE{S#9HI6EmOs^FXBO6TAG&s=^nfyqoB;U536?H)cqup$o&EZNQ6QQ*J%6 ztgU%h<$6x%+3=_J2HiSUtwTHeWqVLo=hwevVY>tTK;Z$KRvMsGAo)K^h5vT!|Ht*g z)_);_Kf}_q6ZroI^<>oM9k>Tl4KQlw@}e7@n^vX^L={!nbHCm$$zR<3!C)f9iLScd zF1a>q=lrrLSXF70@OL-Cv^(2h@rtAZz3S)TFQsotPF_k5uq)$zK{oQ_k)579*4a}z zFvC)7`*7O*c4{zOIPU{;$K|=|C|#n^xq)0^VrTMKB(`Dpu;A2AxPGtXY*;taRbd$# zNvem!neXK~i(}6AYEy!GPu5(2XE8PvF%FzWq&!I`lnsbu9BKydAMg0?BmfWQGl2jn z@EpwiMbk;Q>Bgy|Qb>d-8>!>0Y;(%lFHp^wcK?%fHgYas^%z(Ey*(ibIm?E>Mldx6 zNrjY>9(|yX=)$AFfQqVfJ{&Y7^-WUUI7OS&?x5mcEX{@jP#}O8q)kr)jPUdp{W5Nh zDI~DBF7%x^e6l-MPdm9>Y4EQon0)+p+--Q$5PL{IA5a@JY|mEoZGQzQVF6P?2e7}3 zw1Z~BPk#IsLp>}i+J_$B*EUT^(@ik+tD$#hW;Ou@TTOJO8fCOd_*pCDkw}I&kGcRqTLlet_>G3 zDy_B#Tt-aB{(0|3PsURtLAt=VNa%sOJI*c{Gz*{%8yIOTQBiL_tnPFth!WtP#VXkf zUh=wo+eEG}#<`QuWZLSAr>9X2zHy$-1(kO|d@vr&-bVxMV4oNYQXYu9t;cJ@+L&JUa@@{&89hoACTW5QX z9nu)fk-_9!zH3&NX`rP&1Nzc^-T34YC})%=se0%hC*JcBkH_t!=+iMd(eYf58=)P1M-;AsoZe$@L2k(--vRNo zAfPE?&)rJd?BTHp*0rnQ)m^zW&ov)cqEqb!24ekjg+sM%iN$vGnduedpD##!X~+6R zfVRX7pxgLQDV~2^kd!QJ?5$0dEu5`Qj0FK>ME^mBF{tuin@!SZm{yjxP<$9575uu* z4s|+T6E1EsBc{nYrfs1}ETpNT+PhaPX6~vXyoyzTp!Wgk8*$L=fkpeQ+a|<7b2B&&xWvzb%Pp@U;@A?Qj z^%3O8y%JS5b3aV`&EIi$0c3u1_*3RB(px6k#pl45U#M~6{BxVdg!%~acSL!`sLakzC&V*zinlec%+-?Ed67* zsbpFRX|rzBUaz`a(@VXtIZ>c;hNu3VksR}HCPU;EDqLn%A?2h;7q*G@G3Gv*l#FYW z?McOR>B|c#Eg+_9rpY`wPbH!vfUsW-3=a2zAA1L`I(lK0Nvmz%{8aw2$$qY zCY4llraBMVxU)E20g2rJapJ9lhf={E5eFgR?|%|h#e9hR6ArVCr@)0uX;V*kkb#LI zY1_>Kwe*-gQzueJuJr8Lr&=8QZrux1+i^r zj%&J7ahztXmeBguh*lJ(9QxyETc55ldRl;hyJn7!qo~ zBGgKfcOSxiWcvkcIdItT^}y%sPqT`7V(dr$j!=k9HIuk6q0AEsV2k0B+y~~RDA?-W(Tdro}2U!xAijE59K@*QA zI%VJHhA*=zm`=7% z-ti^K?!OWMfdd5pRW6FAlE@lT*jsLtmO#G6LZtPc6q*E6)f42QJp;ty`yO@sgkbp~ z?L2U)z<>+~%Xeu=0=(?F>F`bdVW3stvW-qx)Iv)Aen~)X!61e`&@b^1qWu&@^LG;a zMnVQ@$H2^*GfRRgWR57Wsa*6Vq>LH=j7`;Ie*ML8*X+i|7X$~uls%CuR8)fOUPW-4 zJ2&>GbX>X=@&(4m2y7%!My=u6K0ZtYkiQ<0oH^)c`a|C2eQk!Rwn25OPSa0kJKIyB&YYX~xxF2^jGQe|K zu*LCD`7h?2xy^d|^sOOVV)0Le-#J9zwt1Imf#WDxRV=B_kOzVT6U#_basO8Nxg!nw z6oO^P=Hg+Hj`KJM+Ny9++MH?9)`2EX*snxYrw%SK%IdAYO#uI$|{Tn`iBS&O=QBG;Ld=RkG}vcCK8tYEr8~9Yi)dsmZJ)uMCm2YhIFTXI z#pP(7lkZh>(T62U7jJkW+~FNUESvCzPtI{+`JH3bPX6zIE0PaWP20gu(n8BgMaAe& zNx}8+U%0^C#o0KWcT?9!g`X1Uu$`2uzH&?l7))X~MOsNN<{yPlvC}G2#dSYV7m%C} z1m?T)i+2WvWAkmEd=7WRDeB@L&yorz@O7Mu2{E@am8M`IN) zMECy7+g@Jg=?W-je_hSEFj{yI6(RZqC0nl`?zD-}y)N+oK@m?EQX(nn* z4%M1?Tt@_FuMPR6tER-Ee}%UXx7`Ri+YpG)-yS^aV^(BCABKLh z9rv86RsRfn3GOFwjJJILUa#f%g;CnFCkay@;F~n)FJ@oAVu56dSzD+)F5Vb0V?KTP za1BC?bgP`NoF%I5ClK%v5%fHD;k+YPN2cU{(G468I=Q~6apW(3ZVV5#CO`?dpOz1o zYd~rU|IWLg3KT0yjsn_08`H0v5(!{Y-i!cZ}e7O*$`<*rTd2#ix68)%8 z?BhCbLzF}j>$&gpuMhpmjhp53MZ}1hjp&h&zO&U-L+NO9NelE|JXd?du<7=V&`)qe zg#JMDkaFLgcr4l71#t+L*fDchbR3a~z}nsYO?K0$23C$3?x!*pDpS^X&V=NYV&V?6 zEXRea@)qq9Vcz1cUNx_eu&x#)(g;CG&UlW()< z(a{BZ#+-)5fN~&dreWPaAv&qe{j~kgQm)u0Y&~@KWX=RFbzuZYQPOlbs$x6+VWumL zkHraY^%8L_Jf3Cdo_ZRV{x$s2y=x_O=LjUSksOuG}mnPQ^om1st${toOK~A*0 z*t)@mQ(Y86!LEd-5)A}UZD2eHB3pd?rTLrsb$2Rs6ub*C9~>NfJPffEnUh-SRx~uK zYiz%OF{zqG27lH-K#)R?$M&t@hXl__kHmzP!`FN$!t)r?TQmQc@3-NMrkqcZN8PKz zG!W88*AU*8jEq~B#JaliWO<7K6GELr*6=*h5wH2YvJ6qx9TDEG+YLoU9mX+^s3!JO!pcopC2eg;hFYA7 zMZ(zF8l3#U7nNd$ZxGg3Kr2Ey*%;q$IM4Cl#j$f7QE1`6d`3jJ<+*HFbY$v7s1ofa z-e#gA2|NDUvXf%57SxZk7MIX2f-9`nscBuSqz zFTuR=9@H)ox)^*>oDnPee3vYqf-Cc@QIHj@*ERYHkpcMJ72IK})#TB{$~SAzsv14P zz6nv$KNOK5xxQI&A)ONV9&hX*S3PR%zj<|`xigqq3i^M>lzJKXMikli+;mTKH#`HY+ zU9{m}FLkf3#W{KW&~)zkRzI#MnwMY&FfMu-=lzcBS7q0dKm)Y*94rT(825O1t^MM8 zVt*>&HG!;+5rd0hqqqkF&=90WmYz*D@Kx=x2yn8nRWXPh8)EhcD*YL2E!@U~JMO$0 z@YQ9lRktZp`>;;cER0?=5(;lQeqX!7-g0@_q4Ryg(dp}XNL6zTh?2=F(eanbK9{(|eUh#?&PQOVdp6@xt z_bFhnoVpx>oO@;*=0lkJ8G8ko;Yce)zB|9DK96O9@@F z3k^o6UkeQ%J&il4!CGr%`ETJYL#v-ZEST1zn&!?Ahb9sxsUs-b7UdPwfFE3uA@ITD zGzCroaMr5uO<_G3p|uYx%X3-z#b)bKuKv7vW0F@Tl&FMfi+CMxg87Vm?tjzJ1-t2B zrj~lsd6~w-%C+c;5Godhsmjq#6>W$0xc>4%)@cC9hMi%HY}Ust(;8Iq7aNeaVG=k1 zCCMoe9l$&u0dLfssV8k~YS)J)BZ}<)fGX$|ZtwJfDUEyqx~f{A#r-$^*ntC-ShPI8 z*3ud!LAuwUsOAPb5iij^IbKpl!)e&;1QvC+N?{5#Jb3Z&TtL^su=EmAUr+$D*BEWX z=5luREO|r@7u+1pK>aS^(y?i`tW;w2&1kmzs0Pk3iy5i>TEpfn6$|ZOc-!OYivYrN zB7KY)`A{q_gwisRO(Pc4QE=j0T^xOP31P-+2H$qMicPD#(fRYd#%X4)AIzYh+V45t z$CXatHWqu_rSUaAF-guz-9^vD@{FJ{% z*c;#!vqB^Q3+O|FA%mL_de8|SKBkn`xCgg+vpEJKUE}>&H=C7(YBz_|Pv?z?JP0*2 zOQIx=on~gI44=+u=?YJ?Oky_?!x-^bvHpsu=Aw5f2**Hjl0#)$_J%w0?-|Rhq`Rzr zoz1;u^VcgW3~(&hPO@%%-!$X78@iWk<@N|27O63u=pzNM$?$%B;hYuYPWL^0Mu+Vn zp!0~2?)6=kHDmZHpWP-K1AKpLozMSFFOcG1VSoBQ;sn3js{e~<_unXrkh7zct+kYp z!hgHYH6=+qek-+tXXa~84Cr@|>g*3uJ@HU3Xeaau!8tTNaSH*H9xMfuk+FYhKRw$? z{SKR`TjZ3Wr$nC?YTZ%v6H!JxLw<5Q%19Qk4q(6yFS9l;y|s7UpRWUvN(;A_-Pb6PN1o@0 z87%3|+H&E+yR6$`e?6n8^J`v5Yy%3)%ZcQ_#j+;EOEk^}<~(g#w6?xv-*T;he}lCp ziODcJ*S~57!_-j9f1qK)5;cf9ZZS9+7o~9F_%q|^LPy||B~5F#(S5*9MTaW6y8@Sy zu{;b*hNkd78+Jyf2qD~Y>TFKotK7JEszsVsJi9>OgG(JXWVoTtm_9rr4>!=?nDTUB zfy5gmLgv6spdl4co-Z%Y(<18CVPO+9Xm${uC;+`G*= z!neaxylZ$q;aVcOb6D^()6;?+0oN-MXQD(7KK>H4v4nF!R~?;4taw4hPdmrT&RD5L z`RYlq35&GYtM;~LYHOHcxQ{@3hBNv4n3Y{qC60_jo zmoqE6jJHB+3hT~n9B`Z=HXd?_@^HEu+Md;l;tkhX>{G*^I|8OclxoDa6n8kF7UQ%E zfP<`+{0Ggm8YXa`g+~l*oFI`lG7O=7urxyjRsJlRQMzm}d%FM}d8)4ho82b{y(hUD zWS9PS28zm!+jW3N-tX}aFZDHL;xqCyaRKSwE-o~-qKq&G>$KU;et}<-P6nrl{lDkI zr(B+au&K>9?cA42EJ--zCeJxv78DG#iE%f#awyMEDlsK~f@uQ@aZOWGAV((ghgmKHje0|MXHsl!TpQ;Z!MGwf=Z^cL9cRA#L2_*doXZ^ni zw3xB}{{)zw`nFeo7e#s}cNnrSpwme7E1F zgu|<9q|!#!(;)K{%T`pO#}!MmrG#UZH9NU;ULk}$=Sd>pDh*J{8di@2*(KIN#Wg@~hyy=Hcz&)k*+2QXl^_mJ$be1JyD`Lv%Co9uEV3zN3)&XIM}ul1BUFpZEm@ zL2OE*{K$%?S+)vngkf|tWJ~>2dm&HzAZ|dOh#}-ug<->k20^DRDx~2F+$K3xNd+|`r{3x6RA8($}E`d z`3kkUdo@JIb~|2%D69ldu~SO|WnG=^Wo_<;2%4TXfkJ@7FBt{ld90s!&fHE$dGo1d zn6$(}nDUzJTN4+ik*HP(G#^SX%!k&`#-=@njwHFnQu;A>CkX)~Qy${5V*tZr}b zx2o?eyUx?pDE1g03AM#D%68h*CM-)e7!bSpLEEm?o1sg|6(bVg7Y+5MA8W9^=tmF0mhRXYxrN1E(Pp zf1P-eNRD}I&)jVADhO!%$)QZ4{>-21x(%JH^n2xS!D4Ofy?ngFy_pSf(V~WlS54H> zkx-skV(&NQ6pfVStm3tlto`0aS~G-ZF$P5q9#7|^H<1?k>)=wjaXL(9ICUC1qq@(8 zMi6EOm&mqN`RbM?W;oo$@9=yX_qoOjnUTw{6DrJ}aSh(G)ux6U>)-@pD6ML%OAd(> zjI+NdT{C)S2dEgqPU-+7p_Fk24SX*;2mWPC`=0=$2>k26u1=jM^-<)BBVUY7L+p!_wuy|=1geM;Se zCA010U&t}2cLwOP^Z&-HJyV-j5G&BMxnRtTQ4#bauH;%*duL1h!M8~4v~9vsCRK>j zz_TnU)YH_;kYKvdKXP?xX%H&h0rOFov5`N%;3g*BYMt;dOG3?mBFX~_m8x>zpr7OA z6^ywcK)^-~A|CaBhJ zB2*C~C)KmN(KctMXd;WhW^g0TVB)Z0<1$6HuD=hOBm7eE%|5JMwe>)A6Chu$GydqS z5&HY@RKd$C;nB}a#MD|g>f`7RdT*n~ZoKT)+trVMls}H^@c4~?s|z^3V*~#uLgfD# zFG|LaPXF!t8l+-v`|aTWgzGUFOkt+f&Mz-Xh<|iSoKMCLqLN&zAW}-(1m3t>^K51Z z%X;0m9oB&oUt*DHw47OIzMq^d-VVttb5W^c|2bF-J0p|B8}b6N+i%qv;9C4zeBxfT z1g}x@jsT&oOfk7TU~;TY^oPNbS*`RqR z$;H0o{s2uEiL*EaT9(R4IDt24x-Z+7s_Dn4ONX=(P$p)_6^8uM-+zmdqqBdU44+6~ z{7Y-lqLTbW=SsjKh^dXFG-$)Wj$j2{{0Kf^OXDy}=czX>XOhxbA`bt<+C{6l_?uF`{R%w|O>+P~-gYv_W*DHygDNZwI2i_tlGjkhd zQ3nn2$_Ax{l!U+S?B!NSwt40XPK?>5xSt5?zHWE3mOH-U|XAjc``1 z2NvK`At;?-QR})!UfU%0C%XF*@nf|N2&PJ*K)QoJ$P0Xj@97&(Q+AD#qeq|pE=3Pf z=O{X>;)=B5dQ2AmRSw-oISQ#jh45z;?vqm^Mpr{Up=-xHn%~3IO4KW@gK=q+A$XAS ze~UtshL2%;W6Kcr8lbgbJ3Z&B5MV27q-MMRxJ>JdFHY9$Ebn4Z7yMZn=tdARtc}1# zo>Xjg8ogE_C3Tq)ZucS>AyawTeLFqSz&;l1t&PvR)=zJHpF;2qq?t|C2rgUzl09k*!ON+;n>+G`P7Yl>$lo>F z{>udN4FauvmHnG^8;lJDSlQ(=PCB?7FpuMa z4RTTBcEa8#m&UNd!3`e*$y$J|t{(@6-*zc}1vP?AJEB+|QX95`ycUr!K`6fr-0A*= z-I+o{pV*3V1DOt3%mBelg38t}0pxxE67=Xjs9I0g-Mt+f_IKMrj(Taf`7U;J(J6>G@A`R7&lH))qzNu%uTuXd=<>yo)JOgA*ZrS~6U z!_BCT+cU1niR`Ej_3*v`8wy#>aDo#DH+xeY%6+pYtk_9R2+pdWreYeL9Y4N3nu5m8PVq~F$mpM zlPybqMvj5%+53mkX}RkGm3wm*a3N+c8f9!JCTXe8c zKM`>~4#9Rnpjo$;M+AOc5v+Yl9G`8^H(IX?gY|{6iD+Erv1LVuurCZvafkAeY*vI< zkq-E>&FAheMF+Z4piqld2}`cUQos=(HDYXHpB7Si82w#q>aQ&sEBZEMVLuHwsD_=q zV*;%N<~Q{D4@WS!|D+j?$R>Bkq`x+KlJvlwJPtT@@M_5YlNsD_YSxK6Q|$I85O7NT z!I&NGF=z+p{M^RY4LX{bQQOql4t|dVZP}G#WI27HVPYnY7DbT?6E!Dy5^BMQWByUC z0X4}hP0BtGjgc(#DnX?7j>=JPlJK3H7Mr6D_>M8$KdD#^7jC5E^zC@eTtx>@h{*}u zJG(2%E86~@6Py`CLdbVep5CxB1VK7~e;+!1x~@#Rgpx&|^G*W8?ze|NNOpFRB9lI&>(9Q~|MG?1WKg(4(4vp|3Us7Ud zzXw^az+&4N&5YsLsrR>_TFpB7{H`g^5Yz|tVA1pra&b~%b<;H0W*pX716R!}@m-eC z=Rc{e;~hN{OJr=njPw2dH3j%c*^Aav#I+#k2Z-R__%Kb8E!Z-KFW%mWBAUBQIQk1rrUS&DYtz!dQkCfeB+9 zTR!FUffk`^9Rgps9uYl{3nDPw=gQMVtw@ZWe&A1|tMOKx?CaGp z&Y?}kXT}@PGe%stZeibl1Q_vY%$xiR2w(lawf{>f;{VT`{XbfP{^hk?WBB%3W_&N! zMf&-9!s`>|yaEy{PN?O<%@xIEOk>EMWG*J$@MdSO+ngj+l?y7U$QRsSUp?IG4{u1L z1_f^IOgj+TH!&Yi47!9`qe-)NbP=L5*EoGSa^^N{3^d`9h9IgHQAc&b(3)aHkxJiI zERd4z5=b&0KptfUDl`=X2?vduR1MjDfMx|b`12NHD_h<|AlD`ebzL0kffcAAHU(=u z#7gY}7D%-r4V)ct-6V8y7-rs{iSio@RrXS=Ez3bagM?{gZtk1|J94Ud?xm@*)NS2% zP5HPXYc09*+Dvmg zaW-i$IHD0!gHCz~RU<9Xvfw$N#go?(x$JHe>#+0T>qr~e zym|HD$O_qqGFd^IEH2sQ$&@yCf*3gE^@1)b;T1T;eA#K2YX<2*aPMkzJ|`OF4J_Iq zIt4N8K445Dn~yJoDl04?EG43lJJed&UII&1ON%_-U{I9pDxYvH9g1lJs2u5NIK(WE zkVgysi-7IRP934z@8bTGrQz95#Ls<>dgz}HZ$DfbJi)}wL&OiKg2^9!;1RvX(UTcK zCasx_nN1>lqGz`YSX3f1rCQqVc7>K1lNe~_(+9i6om~rvU>a6=*p(XF&lx4YaD9 zH0+5p^IY}iLgJsCx>ycid@VPz+H=dJ1gy%?9(bI;)DOOD!AutI)Olj!eZq#3hP~B0 zSZ=YE)cI(eDRyq6@?dw`J?M=wMK$SC*CR4N(@S;OKo0qS-y0DIN^L@WU*dZ`QG>SP z&0!?=;gWzjf&^?+C&x3%Jsa5|_D}3-e3OL`J*J3ZZivV3R&K-jw!`!V-Kcd3KJMv; z`+po20$fM*WZy?c1UwKB(f@%|lD0K6w)$`Hh@|9ohkwD~HM^8t-Tib#&01~Y2})|q zVd4x{SeaE-N^}rq$PGWID42;ElI}W6L{06HRzX_ldUOTVQ|xqBEg+cWXDayT}w43UO?jK62jvv#}vpf#z!jTYRgQh?3A-+TKkP7>bGq;1_}&lQB>JnKfIVdFk`T4Ew>bx8NRjIB#Ec0U#o)ek;J`t; zUopSABrmi$EHdvv+Lm(`S(#hheaq?Z5J9nN3WDMaWo@X*qvFl&wYBGwKD~}j4;t1B zN<0K%GS+}GHz)qAZC}~q`&`q1lS#JytZ1DZz>au#f8r})n!$_wg0RgesZ2e-*(9cm z0D0;TV{AOKZ#Jo0%BsLLNTj@zF1MNyT6KUk@>uo8o&$g84dKqX(=C;kerMw5)SN>* z^7@#;(|^wD)w~mPrr7B(4qS=u?#%6jB7Q6v!2O1Gxf-N?tiZ`)VR;G}2VI4#!N}m@ zCHze(9S#MZC@?%ZxGsNvJ~6bs}B~_YRuNvTAyHa39+l_EdC;RW3`T_L`nnXOA zAW|c`*G>cg{^&)0%$Z7E+kErb!w5JXWES0{HLC{ z78PmiH$mx$db&s9)tV1G?h46T3_n_>k!8esyh2ST6=!efxNzAE5BDv>OJMU1bg2RX z9kvd_o2BZVJAKgPE71pDJ98yw6_Df)t0yY~utyxkO5X#KSGBpmdopxjv%b`29YM^v>jv-2IUP@oFj|UwM zrX|;BCVd^P24JC&QK`eQSI4`Sa%3qbReV(V`^0~`{>!J*_(^IanM2Oxz2)j3J9>T{ zAVmmXH!KxCW8aj8=+^9N_NZ&y?l<(c@c7)1bYp& zT!5{-T-m4>*fn%&%>=XY%y-)|SG+h)x#cX9iZ$Fw|1H~d`gV7~z86KAT3yHN8+>l{ zvVVab1P@}*-+{3acAl`)a&!?bGH3^HDVG2+yXiKD7r`WxAjg{#YO2xkPT1cdN+v5* z+o?`5Q~e^&)r><8(Km+h{qFDDo%aDoY0&4Pi7HWr$VX$n5Qo(=D@7B5E7lv4fO+66 zN`e?BMDzSE@RG>W|Bj@EqiF7XX3z^2M%j?5&F!z)JOvPV5Lo*JEiXxqn5Peu;puCR zZcx5ejjO+I@q`}r5TE9mO-cT%!OL}u1V%>VyTL1wM68Jf9txTy1$Cv7p%7<`W&Z^( zarHD<%tDW;xY0DFld{c05(q_=yh zel$0IyUP(D%$+5WjUCX=zX9i1a!zn>vu_gE;9?1v9R-(_5ft76`s5xG<@uEBb*M$R zon)LrT6!!TRQ}=PyoyIkXg?<5q{belP&TErPU~qKpPL6L&bzW!F2?I)17orI;*TY6p_&5^E;%y6@uwaW`TSc<(?zfG!_EacAKH)l|-zT0RI>#&r8;w3gL6mV4_gz2cS0>|9Ju z@vOG{vB&J$HNj)7i=0#yy!>5;-R?pRa7U80u1wE#;f%8fyFP|{m7A5#jQCWad)>Nm zdKyq9J+`bh4V`Lv;WIZ0&U~jj<=}CHQwX9*YP1fB628ZaJB7`rVm39KK}!GXdlpyR z$M$8VL@Z(mlJRLAz1G(-{bM(hF{wK=EhZ?yRqN*CNcNV|5()7{9k{uh}(a= zK1`{||A*hnXNa~Ji1UVoUTx0Cy1iAV*-bK&p#B(J1m7$yxm7G5pdi0N^yTfEX!J+@ zE*2QngrwPN&--c9+qKXOf4F{?0+}3Q!yZ36Sd_n)X7?sT$^rhkz7_46du9mlo9lv) z3@1Y4@jo{z5GmdDuPQVEl*4OjMdEFb@V6on&ahTf-G@>$Kygs1h4N1&0KfloV+tX* zg`%V7F#C!3u>E5YkQ`eFe=sjl#%TZrZyD#X^F&Aw7m6#GMu?Rhf=i|U1E=KIEtKIO zz0oB7*33L1KiBikpk67VbiOdgA=z$S<)jfBecAF=+4G{cp47;h;=VtwV(s?n$CNAB zs9zPRFep0KzvY7jn+3}$D+H+Y>h_)|#fY>i^@V>T3QRPWrS5_KTNIgOQe#3ZGU??C ztP*u-#{i@E1GhIso&g#HtUh&ey5@*kEz-sYu<+-A=63}SrmnOsdBfNe>a~yGoD+19 zY;g(bjv8a+z%{H07J+n}y1Df!$n+CMrLnX)1`nzwvVn!WR(+}vO&zPlBos_FQ=YhT z6d19;xCj{>^YTMXCl92gmnGvo)BT=8r!y`IVUi#JAG~`t<4&Q*myi$yT!V^YN<`>L z79)9*lWz&P#^if05e&6S$8>0-2kSmrT-@Vq+Pkb;*b|I``VD>C>rd-$#YAc)*#1O) zWFOigT<@8ytQwh^{Kr}A?vP+;h5(NY1$~)B>A{ssDR_xQeU-s#S`~nHNGFlHJNlXS z?bhdf;1bg4aF%wI;9+ zoknE0wLmr~Vfl}JoBj!Jg2H6>Z>hVpp3E25f|>lmYJBka)8=2Ef$|feoHgwht%H_& zdFE3A_dV9}_`2|rH?K>xE>LVE4PCdke{CCwk@*iPW72lDr?T(O#k&e!taO1(?Pkhd z$ucbaSt==r@#-75d|X{9Sz@y&trr^mWMGQbAgo}`tENIE=x$k18!Lo8sWp0jgOtT| z=&We%`k0vcwy;?AY1&3f=tp?S=F;f;$FI_Phv~419k?KN6hb16#mkhk<%|Toq~@dp zDh!#$@D4s1npcQQN&;uUVL%irye`*9!2^=Fn8Ac{*SJO=6`oB zEGEQ%TR6EF9hnw2^)Tuh@+s~F78(bYzcXIK&*5IKxl`i#NL@xB2aa)-e@em2&mA8%eSC$UTz!Il?FgX%_ z(G48#(!5GikJ%%ZF{~SK_V!c?kfWI zcb#V4imfa(Q1Se((yHe5!i1?id%DIK_;mVoI&s|7;cOegP13m$uNPFP=5ImCW`q~E zL753YQOGU>X?;iN-hqI#i#kx*h$us!aU<1$Ct~E|1zZj-^f8x7cF*_0mo9rG1o6V7 z?v=SodvbsB=)$5cRej9TQFLqN!KyB0qS*QItKbyl)sm?TJKoni)u1duSeT zdL+SHP}@vPs!LtAqCv!(z)qx^sM#3&3!p@;YCfabpkYXTe_4-Bk(o%+^r_Ih6FzCv1xpV^>+8tUMQV0~TF>&hx#4!@ zNa$UUSRlVCiB{!|R7(;Q=AWGvz7eu^kgO^-q)0f=xsKy0dqap#cKJm1E4OYY?~6gv z!&f6soAP^1tiMuT<%?Lr-&*RlEDupf$Hp{Y<$Esu$^|{<#4)=`Ikw~JI5_U`b}=uL`U%tA?G73v zlvs23&d0?QF?*O;VTUGuoiA`coF)X?eO51d6tp0(%7@pqY0dMOJ{%Yvd<~s?ma~r6 zq6*<6B)PX<=mYo3%_&bXsg87wI9nD#@vpA zn5>6#qSPZ~0{*wE)SH0|>wqhYb)|c4GS^55>=%oV)DvcRE&M$4A;5~4woZmI)65_59!xG z5X$M&CigdBARwCW*8}xGzZ?q2j^BY%|Lr1VtRQWhA%MgiqRV(k8Al;3FFO^6BwE~b zF}@n*pha)U19M^XL=2|zE1qOUJe4S`J2%?OrKMD)E@ikGd{~^ zcS#HKzN*525(|T2F}3 zbf&2dW5N4-5R@bMHI+nab$dTJ7sRJ{Vw*4+NZbKgDu~GKly^UtbFi07bH|lmMt$j( zu9c`knfUoj4uhr`dC#A5tzO76y2IgJhK1A?B8hyC6G7=r#A%VC4MAlO_14UqL5mBS z)s2YNd*zpe3%T?s**_Bn(*B|s41Ys&`ZPd5X#e>`k}-DrADzaR8u~Ho!^j_AuV|WR zBBYu}W9D)M-5jIpu8~n_hQ-q|%)&o1q7}|`TmjY3zUfI26PQa$(kqHST!i zqy$*}oLHwkDVTJOgaa)_{BqytvGiBfJ&&ieNBgD?=ok!SnNHTQODSj9PXs#AmK6Hj z#taro1LMfqvu%+={TnZG5_gJ3$5QNxDzTZ%(WS@F}V=sODazk%W@FDe!y2Iz5P z&MfqI!7~zQ`fdC`ItgKBIosj8)|#E1J$PG>Vxutwr|Y*?R6aa8Qq!UfyN6YnvV(d& z?U*tl`^eqeVfS;gd)EVVBC-^2aOb5lX=JduCXKsQ#w(jtOLF>Y8c9;c{bpD}47BQK z4n>1hB4#ADVn!0SL_!@FxjX)YgL7o##?vhb7cd%jw|Enw5z?`x@0CmbKgy$$mmRkcD-&!JJU|XFqzN12n@ATu*UaYH=Hh7q>AX6hmE^m%fiDLG4qFByqbXUr2 z9bkXrT}X3zu>^oEcp*>+Q*PRBO&7JnLfA=RS5Q{R@bGqOlgR&!44=|6KOlKIfB4go z>=w)tZHN|MK-oQpAh8@ZzH-`sb7igQE!r`dmcK+cg~lhPaUz!n#3VNPRNo6nyT_u~ zL-I;`mDOBt5d9lnp9Yznc!;f;uSv%26h$hXRi%JIw{V?A=CB_T3yiMhTA>7o(AJq_ zfiWrx!)_fd8_Xd8PLNo^L7O7nK{0R^eteE<+hqIpI24t<4QtU)u5hvBdY_^V<)Au* zA1ChAX^62aYENx4iK0HJaB#Qi%{O0J_wkm}u|lDU4m6yI{&6r?-#?|2cC;{=0mYt1 z{BZpQV|tpREycbODeLvBrADNOT5EanQ}1n_h#W8X506F_2W9MRNCno9!1Z^jit%ic zD(lAPu*-C~yrxy66BgCR^~3SlM-aLwS0g8DlZ7V5ezleeLY97oz^jAyH?dcnPvY%s zC;W7f@tI)&V4@hYHh6v2_&d|M1paqPdP`zm8Cn10TPR#^X6_#ua-G*k=e_;~r0GC| zY@(c8&o-@)MYQu)-1C!Gg*+p zh@KnBe{&r;aPHH-^i{Uc>Xv8{S1bLjrf-sQK0i=|B|1YTTg06jw2UjQDz$Z8yUfn@ zr4Qhvd;Qfz|0!IAoZwfH5=Kcukf=7{sYYn^UYCXmJmdVV(HX4g)+TAu!QB}T$&)2< z=8juJD3Soou#(EZy}R70Mu)b@pJ)5^o@SeEc{zA1eW29yhd+RaLErA-qo#H6L@SLc4K7|FJ*OC4Z9l5Cu&Rt#?yk1( zl*e|lRwKqb0&mVx17<^*>Du$lbir5-C>(4ZJwG;gRz?)?>Z=O_-K*`c3_0)!>B$i@ zG-IB&a)2$M^jFskLDmTGc>uRip%gio1L#XzR?zzP4SKa{BV3En<}Y9 zlGkVvsiKaYSXDceAvic<3HH*cvtAP=EoqBcLXY#GU`8?oH8ZHFo z3%WYn^1LL&%Fs0oU`7jNnM>#$W6rTLbG?E4l_kTs2R}UFpY^@k=y=sN*CYw3RmqIx zdAg(HjDCkC=S~$@){HaIX&C0}nQis!q~-0}4oE2SZwAK7Uo-^l1)Vz6hwz4(*oc1L(d7C)ES$eq)B0~A)jJxEgR(qv_y zlh-w=9IyHGln6l=;Y2?_i*Zjq{RvFL?na(GCI060=Q0i|wfg4Hj`YF&Pr{go( zZtR@(TX}GFRr;t}CjxB=r~PVnG+Q6|_eGPmZ?p?L_lmsXH9 zRI#93LEjjkuLfu`%t|@q@Fv^lqi5tb>#v$2SLYvSk-DHQgQj}kHtogVg^9+k7M>ii z*i2h-#72KE*dysljgyb&w`eOKdoS}y=@29FJPys^(iG`la9o3WW^8UWNH^e;wR4Xr z(1kz<9?LIS*qRMdA1gy$;5(srP41R`xSOkHa?|HG zkaAm^B*I74B!3x2;|KYH%@>3BaOZ zLvpvVO=i5kj6OSrHy%-C=8{gFdI#R55!vStcE<@6iJ`wY5BojL3GPil=2DXH5@=Jj zq~OsJC$BOwu_5{$^m2!C2&#NY5S~h>Km>))wick5J1`7$fCCZf%^bjc2K8Dlym5)j z!*T6qZ)Qjk-M@HC}{-@CvX(ll;HE=bH$ z3o;FbK(3n1r$ahIE67^-c*(ny5)$BQDLaDTZkYpAU1g3M(n8ZVSDyk+nro6|1t@I) zkqKXME!XL@d1$UFe`^2(owFLiuvJWWLzId$){?hi=C5d(WFl>{&ul0S;;;S_OYvKJ zreF{Gj0>bciDag$QQxcFLRSga-GUTiJj`?Re$=ArL8M%(wN`nWBH7+4E=WVd>fLX{ zF~iU-sWv3}u2^49j?l>}-CF4hbjL+|vlfRE%-k5?%0XB$Wv><#|>7T%T zKQPv;7WZu4aB2h*p2s7ljfN(|+lV(a^7A1{_TAI!#z0d()ydg_wg!d>&G*-n$pQL&mZ{N>NlNk;%vPqfQItM%D z0?yVA6j$`?VbvQKhR#YTQ`?lqy=vX7fB2AMe1AqzJ{MW}q573XvkQP}E4+w?Z7%YExpQZ2LFrBu+_d5{y_7Z*xfxpd!h*8XLlRusgsWY^!fT^q;_NZ87X0|z z=*vI`o+h|j%Tu3BPba7A=5?pD@uZan^}-|b2p!z+=Qs#vk)HN6_=l~e73`V3CD_A& zXzH`XqB#pG-$|8U7=F`-qwZK)x^pit<^`L>x?C$k^}s4gc+|4Uz}!VNH>2E;59A|` z$vUAz7WI8TTWhv>j-g2vW^}4X+J#gUgR_2QYZMCXK~%)%l0g}uk&Qw5MbQn3-+EQt zeWB=GfPBAJf8JotSTo^R>aY78^~gi|%SVScR3>2bY1#E+Hu%f?tJ6OsUl*JLrX_i- zRA6^!ITD_zJQ{IE!7!X4O2jD#JY!|5s}a$^=)0oVWDqG@#kC}AI40FXPX3mLb7xi& z=HFR^){@a)y1p)6UN2X~=@JQ~G?|CAdL$N*Fz$U%na!qdNQ5R7r z->#Pm62__?0{X(crcss0VNC5a$U&60iG0*p{WpJdwg1m4uz%lyKAjhExn zqHLdJLj&Tn_UoM2ReD%nhCF)}6*bM{9E5kFF8q*YT)sS_Z93@i0@d^?Ny z(zc~lp&?J?%d)m2jQAJ#_)0LdhQAUG;3)?UBP$t{uaPcLb+a>xSscW*7-4*)xXrA# zMoUZs7DsUYQQX6|T7nJE0#C+d!2zzmAA!Xo6vUX`@51&v_3k5{JL~glp>#D9OI$ve z&kP@A!|aM%d%A>%jG(+36@a^o;?Dg%J`h}4S?MTi8xopg7_U79#7-I|X3oRXD0s3= z)@3(yTAtK(Anh4-Q+@C7jSvDRcUR<2agbH?_v%c#HsoqC_ZeyQEVL?&`iaM`aR9HX ztH|cwtAGo!jnx+I<%-Bqe;7i#nTdNH-%UI6e0nnN^zNe8`ljJuWEqse15bPtX~S>W z!qPR<$@WV%9ta5TZ+p!AK*lUB^TW1WfJ{H$7!B&Z5d*urRMR*Jgq^|pFVj8S?6Al0 zG<7&;)ig#FLebFRz6>GRMAi2ppN$f~?Cwbj~N!RuXBk z1g?3eNPT%RXWQjC#gNPrwSf0`{#;k)2W{~^;5&q=7gnuj8ThQI%*-{PGmofB*>~Yl z?QoOa)ogUl%>?{hY9R2%{f_(@Yp7=^oy+sX>`L%PpFgR~t!JB8&MVg2gLJc=|3J@~#?$stkA6c&mcW~@9h%=LIP2z=bW}g`A zL%taG1@VyZm=I`ojo|P>w7jv|H+`8RZ+JIy^=txagyUdhV|^cvpt&91Lr1#qTMpeH z=~WS#?%ygsQKKnzxn1u0WxcQaU&ZH)Co-RaZ{c|+A`lSzf5z^_Y#sk+;N9W>TLqQD z=;VMTDXv;cuO9AR9pNUKHBC?3-i%|G*4!phD52o8k@D5e9bSLHffug7Z8BI#vc>!K zxDgjlbW&zjG2~!Oc+#|cHMZ%=pl<&xQdQ^&o79E!%$;N~_1y*qyGbJnmNRg_v>dwH zOZxBdV$Igh3)TXtuN{R!E$qbc!%WLssMRU8NPeF~OdX`}o&a_pzu#aQK4nXM#GzH= zgvOfR`3yE322h}|adtZ^j59P)&~*}XSY(C>IQpv7(W21M`$;2m%oB2%d4j>N5$yyF z=UK%w<7*f4sMc&5v78}7MJe(Mq3I_Mv*(B-9|&BRNySXN-{HmAj(Lnbf6y_FDNLB1 zSoLxKRTSW;)exS!_5YMX!FK4UqfBaHRt(X!cydx}500LLq_UFbE#VF5ZTRPO?DsvV z3g{q^VW=)ZcV0vTHg}C?@duWmJX8nB1IBhNLS1F^xV^x85Q1f+{S1d^5qhRsM1I?Z zFD5M)*-XAFjbI?V!??cNps8qP4G%bb|oRTx2`qqP3{;FrAk#=Q^OuK|S#S+2H z!4RI*#qcdh7ZVPlk_zB7Fl+?{1!TDTIV8|Als|~%Y(xY%V;wpa9EFNl-H!;4Hxzzq z0knpjhwmP^L8*;t#F7m*b_je8I4vy2l!p1myu)YmO_FC~d6CE`<-V}EMHHuErU1fS zR8hwB*quT(*rAMjl0KK^ihToWhg?T7nLvzd1V!?UKB7e$Bo;z_fNMt^E#v%!1bmtQ z#n?NANy4q!x|OzV+qP}nwpnT0wv9@=(zb2eMrED&_uk#7*ShI-5&6Vj-pm+tei-Au z6^LOqIDuNkQ-IRF&|H?vjMd2PV8v?`1m~9a5M42Pxc^`&P!@BCLVQy&Oj5*med)~|GB;8YxnnxVkHj7 z?eg7Oucvde9Jd?BC*oP^54pAdN@1E-Hq@Jlr-}Ez5(ow}990h1%027}Oq9aVmrcsh z?~S(w4P!bKv+(N+6{P(!9TBpy3@)~4Phz>39vOvIHTTW9*`zu1P;cr!pZK}e7a{Eq z%2J9Phth!#oC%TRh0rO-@Ph+bWmv)dd}sDX(DFx(Stfzz zdQt_(H8S+l2~WKlv?uY4a&L&D0Uo`yVDl`^rP=s1T{Wy2BRPtYO+32tYQ`>;kjDXt zzOxbUo0D^);CD!LSPJP}QF&(9Q5^fAK^Eyxb$2wSH;|wOPvD7^oX_t9whJQ<*;llk zy6x1VU}Id2TUa!m*F@gm-sP298+Lw-PBqcQ22bvpfBLm3+5o z-=AB?1S!cV9OxJUa!n5pxZ)knz>krl_vu1Bq!|Tq$KpgSs){_nK^}Wg*(8Ch#zE~J zFpV7RMI_2%2GLrL9rNx*FCfTT8@cbps1- z^##x8`af2+rWxw=U^(hUcNs2xZSk#r-D^I#zW#0Dn=|?X(+^;tPy(!K{mW8=xS6Yo zrI@qxf55FVQ<=91KsEuu=k^H|WwXNSmu72at*k0rCFW39H3GbLpz0y>7dNWfhubdj z&N6Ia=8@6UJeOa1VvMV9Hl|9*F{dc)oV{IQ!_?aK*dH@ ztQ}9NweF6Hbd+*?w9Mhn_33E9{1%!hl3zvG%UGJ`d51=Ob3Dkf7{3co z*1u69gTUv%sUk1xl$KbhWnF_THc*w8MeLeoPomX2--e39{@l6?jZLUKz`)amsErlBW`yc>^ne{ zDdE3b^jxV9AfUXrWg6Mcof%kK%b+QsLxdma6|W$zSs|=^HRxdeF+U+Bfx7j6KeEAa z9gM~mJBMcqcft;yUD1ZHWbnv*{HW7NRgJGd)1IM~*kKrh5H6jEE7vL#M(>-R{;6-3 zIH*51F_STjq6Z~G7G38W%P`?{<1b-!?Y>1eM%{nnkkIIC6!Y>Ev1+R)9oAqlAa{uM z$YHR6{Xq&boH(_@FnC36NWyT}zag@(3$ooy8aITI%YbDV&K1i5wHJ{|ieaz&$@7xV z?Nod!^fBGE0R0aq6vzs)=BQA-_t+2y%y(*-gTvY+BWK>(8X;G)ZTfeDRXoqfF}$-G z_mqSih+W{4e=GzX-^)@Hg9q%tz;Zm;vyy zfbak7$URkkQ~8h+(O15wC>?^ju#sKlo)uOlWb(>1U8)r_9eW2=M~ZbVNg~MwE6sm5 zjTUSj%9c*dh&IrkBkb}r+8y_hLANQyIkY0FTs25M{7(?+nq{ra$gp-wLNc3UVWBFjFC&;rno>b^2s$=qxag5&*j>m#(s{?9xJP6x zg56vMrg_kPP&jCx>@Y#XjYM_+7i0iS#7+2=Ov^IFl}^dbrDWMDon@Kj z5sBeHRAU|2F5`_DcY+_kJsEGXC=h}->x#Q{JkNHwp2FLLtC?c#_l#M8<8Hx=i}-v3 z|JnBdB(DFy0umd(<8f7!L;+eb?U1WYwU@Pu9Z*$GQ58aAH-icru^`J-%7CJUi$f9; zGMh;muM!Rd{Og7stD@@#bzLPKGQURtM&@|Nj;x7fcbuP)mHQA{2@#Q&jGc{G5)^gW zD+d@i_{KmU%w@9M$uXt5^hc_*V%Yrww_9iH z*cvWM=2RuD#VkSkxNWjiil#@_v^2byRZo394ZF``f15~kvlk;*by*A;&KT$g-j0JLlLpHL_Fc zYL2qngP{x4Z(+0Rtu2P*r|&9q>6(?*=Ig4+M3n*S&s7b*`GNE;wQae`b(K}Nm{lWph;7m&{!@WSH1AMdj5Foe zVgILN`^Q}9uI}d=qy`A?GLU))_TC!Eov{58Wr=dC%c#78QPR$+YUD<2WjS@9?#9RV z%#!`sXQUUShr_8-%E#ZbiJgtx(}}8yvNfUxMi2f+7O)296owg?m^T=e?tkYZ_p9a@$z@tfap86N9;VxwaiD*1lFb^ z-Q^7So5imUwb__i)H0dE)mUVW8SHRR8hpEFEIC~vx@jM}WuWCySQtOm@!%Do@h?6! zSQcN%+am8>QfkN}@gl?i_|CpBmgR@H+S9IOe)YXQmSx|v|6*dQ>S_K9_OCUs40ifx z9^hqB3xFWw{8!EU|J3;Z3h{s3pxgi$I!1aA;P!@WQiUT} zQ^Wn%IG5e~TZrnR3#dxxt zPL7e|ltnEI8$uD9c-F;qUMB3%7$7yKr){#gywnX0N1oM^q3I7fc|ad zK|59>8PvLByDS1G-+KD6|MAAm2vdUMb0+(fl}BUpxp~tfDKdarLWwmt|J`%MNpv!3t(mSj)Bgn7{C8aOtbG|%4)NX-2`H8jlNc1k!V@Lp4z zDT=g+HIF^=oZP@=EDFnFC402`c2LDH?^zd@5-F&DCu~1mk*1`1RT`?E2QRq-*PqaJ z*+|^Q#QhbPbvX?Hyidk<=MELpV8|v+<}{4ZYJ9nawxC0~gT96|z&_?=>muwvqqDd0 z-YmS}ScoyMMs0RiaU!f7AN9wFs)}PTWr1_Sbz`XyEm#LJ2P4n0Z?SaYg{2oO%*C~&?>FyT_ zHA`sf@v%O~`g;_C+hv%VLL{f-?va^*kZV$3M3>#qU)m7_5jRG@kzK6&#~RRPpyiuA z^+pOkdm}@^562yFN%n3^+>xsvaY5TxF{RhbjRwTFboU%7!zL@MNv-yqNPh!ck57=K zKuKDTP9Un(&E5zm;M|us+@V$`t}E43+*%jI-JJNC_0*SESigYcpZ2QS-G8mG$1&mE zbl?7~>sz$)B=YRXx{zdZs*SDux@~%S;BkXAYq=C`4iq3XH?Mt)#;@b1* zn!SfgN9)B7Fymn|g_tZ7?68dl(pfOaqYmFu`K7M6em5x6YDOA7ze4nty= zI2&I!$v=4hRxX$sz5cAy)HwJ}Itt>KkZkgxS%5_+H zeaZjGi6CG7kXE+ z1APBIJJd7!L#BtJykxx?=bafj7fKiy6stbZZ4xT)nHzOMvkIFk<*g&8nWD3F`UB{eC?rG=GQP13`73)7eXM0xpPZ9ATNq_~j$ zw}+(>Hp8@zkBcqMoZt6xdidgbPZ-071VaX5UJL?oI{m^f$p-4YDrtRIhJ#B^W0T@7 zYg}zGQN_raLIu)nXi#8Cm=?u3?N%7E^_so;K7tcv6}1|v)uO<0f#9JN=SZ$x_tOB& zK*?{A#O$1qIuN-kVVal7&JEzDcq!;mBH9^~ut2>Onw$yH$hjz3g~%bPt}_1ef{|E< zGJSI_6l1yxB0>(2dLoGQ2`BzCr%`R)JJu~xp%l0_O=o7QmDjOh80m-S#|om{oZA(! zfJ3?~ZAH1ajtxqa5`m>OJp|nh)W9g|EzfeEnkZi-hEbE(mC88ASlK)?rge^;wTw#j zpa-HXN6CpPYsASH@EUyXxM7yiwf}rNKFziT_v;t<;pP4@D!GOu6P7=wJ_3b_*C%A~ zJ;s|I=l2{K>F^oump*1(faSzi`HBpOn5|;QWmFO!W1>r@qNTM`6xe{UiWOxPBs6PB zbB>w}s7lzlDSfV^-?^BsQ#PsJ9i~?1Xo_z5N%J;2J^({ajW0&)#+#%25rZ;jWm4zL z9F5QhkAED=>sjmJxfUjZ*%$vPTLtlUHlJ_Qk}{rDsVv%2N2zW8!eSKM8X$diN7;yM zCrcRPLNDD$3dgLsm&`o+>`Vl^p6APbvuIPNm%=i^7lCCaLkgcHo6wqGHC6pMUJsKm zFXR%GXQP*%ZqaO1pS|?(R|M7N%;mpifPIwnT!5&zvG1DrMzE z3Ed)V`NQ$6mavcg;4FxY)BBa#w1mK~Y0+LOpjHsw4=9wT3h6oE+`=dwS6iT{QjpwL z8uf?Z9fD`qa3sc==d=qlso4_I8JweT3jKrp(o&@=aWwZfbm^c|OVyo0&Mb21BL{cF zysjy_)qO`&$##)*yRZf}wVQHCXKNOVTBs9(iqITp?7DNcDjky(!OEGE;*e*%-m`%~ zto*no|DEY0lc$vJnU)sD`%XW}!&+HhT)uG_)O32`;MR?jp&0l`M9b@iM5<~opKwXa zHeVuzK0WSpc12X-_?{AqS~R`aJi(t0qWZVZDbPjF$;!lXuP&-Pn<)*MCqSOFX>D1L zSj{8l&SJb^mr(Gk!RqB-(>ij-?O7d`+8_)GIJCnM&#bDv!6?Z^p)|36&II24Ib9Cs z;gd{OgF3~M`EFGei-fTJiab1Ho2Q9^ncu%=;J}&wq=(J=xvI9{%8`jb5~CXDS|#3G zwjNF)i+6Qz#0>uky;fU$Ebeb-5hy9@@R22#DF5Q!^t`{!=I}GM4UN1w7kWnN{&D@) zr0wz4J$j~}e$#s~%mKEAQqyqyycVGbvzR~xYJ7~Mj?DUlGi>9n4#L)wcI7zqb%%HR zwi@R{^s$zyd}blXe7`TGL9H#j{g{ES4cp-0ENo~v;q4ZBd!s^RAh)IR=4`E8McYX8`xLKU8aUa8m|r=Tdnw${~@xsuvUY%;i(zUoUUV zav|OI`l;}@AA{MobR_w$-fZmD|EV|Dz zPfiVi$*TQ|z3nK1q!C@Z74Iz_6#*on31W3og?x~Smns`j;a0rZv3q|sMf44HqM<_Ua#A4o0&6m>6tlq16y>2 z-7`FX4w`^k%3u8BRjyh__i>UYw1+Cm6wOJpVkqsasQ_ zRG_RgGoe+|`Xh%JEoOL9MS{RQcHRgc}&4}t^h z4(lDH!tVJ+GgOPtm0(G8^nMm!*5`4Vfy&s~ayJl-_kuchEc$Bv4mA7RORYQqb$GU( z{rA`XnBV{KC8-uOX)(Tl7`6e9`)`1w5&OT&b7wPiTQd_^25~og6IZMMiPIIpzcByj zhK1o2Q52xD;5g? z{iV^D%FJ^|rfy^tspv#^Dee^CeBMWU8##yu^j6aYIhboz)2Bk&U8jKMlE<4xx4}k) zHlTXt9ZZkN`SP*c1#nLFSl}HuvnzWy+fzTYa&no-@PJ<6IMf_XpuJW=@g0PLEUvHx zHf&7Lo#*@_t4hsI%C$ms*^gS#xIlEDDzI@K(%nZ-t(<3$FIrM|WNXuh4vm|x!??Q4 zi!0F7=a3--i^w>fTv7jKnbl^x<06NZH2=uDZc{ECP&a6-8NN&qx07X?VF8}@!sLY5 zreu?`v0LI&ZN6juv50?QmE*d^8w;}cTjLWr-$NQg%eXw(xBUlxFS+As?GlcpTKh4Y zd_go-RFU4_0lI5|_Xyv{)x?_L#vR^%T90I6XtnK6A?+Noi6h=O8tgXA?=t&x(eR%| z@6%E`V-EUN!ow~cThU2JWeG~B`yFpW{X`PZaK#p9Qs|{@lSMdl?KteL+?_|t^SxsJ zI!QoTeH!JCpcJ$1R?eV2=Q0&M?h;IaM3eIR?S(eP$A1>q&W!Bo{Bhx|2}YNmCt$+; z@EO26R5>47`|tC4F7lVS5dY+Iqd1Of0dkK1GI;9T-Fcb1>a?Qcq_ZxDVlJ!Pyds3V zh>yLAiVpXrY|AM11rfiZ2W&FB0m)gfNz2n@7jPyt^vrH_%6R?7ueWQRleTl*<uc>$m@NJ+x`XmDfNqa+Arn( z6qEn9@lBr+FB^_${E-wLsY}bw71!E*{8ZVAWI0SvKksG4SX}RV4&)xld;hA zo3pc*9~+N9xNMh1EvIAs1JM%xzBWou}>vv4pyXWoto`|Etj!2 z{ZtlBpk4R{2L%gz=JR@X^13OzFzGEtn}GiAcH=9>;`#XU z^^woWukx#>j|*F8o{pX%1B|Pq3y{y#qYK+1P)pdeh;ZFP&_45=Br7R579wm~+7dIV zQ{os5C&(hR$sh(W9cjl{SW@E-MXQ?mDs0RcL^BkNHJnz zm!(S}5@oCxnBG$%+F+Ra52ZsQxObfTPWsW zKLq_V&EVOPEc{SOF+y*A^>so`E`w;!`3-M=$}lN%WK4ZAAETp?b) zd!Jtt*6?+I+w6(Ff`r2hGmq^Mk&Dy-fKdJyF|d$A2B!CXGehLB5kC< z(|qKtaXwg_mEsPtnUo^m@m3qj!`LCZfoF+^gKQusS93he)J3iE!3}unw zC{|FR;#LV4)JK#W7lnf3riv+B%Cd^GyA*Ou$o=Tm1_rhl{DVmb18Ecyn&PvO2s_9D@=Xl!aPWri20m61-ss)F|;Lr$H_wC4&?#q(~lx|LGb#*Dy}+q zqD&5K+j4wEW&L=3sS#tqeI2X$YqYVK?Du6V#FQmMqvzmHc%@>0s0?mPkV31R4JjCAN{b(IX|!N zT!+yR;I?}HIJ(GBUAqPB*v7o6Tyb!267>@pCi?Q+SgU&6y`+!7Zp6vY`cP#qB$laa zSGT3~Q#GuC$5fjMA%WSY2M)!_0clV%7lOFQwjY)#5|%niX>T_Fz&nb{0*T|&8)s7r zC1_m_*2QT*I)%=k3}o=<^i;Em@M8$AX$ zkqWm5l{*R^QF9nW47CV$ndLb=MR>0dML2fHXtlzYuG|BU%B*EyBQS)3#D*&kila-{ z3hAw{A~6y4uMRVRL9Vy!)su9)*!!tQDGBr4q0-N%VM*E!K3@H|A%4mjCn7a66yomg z9m%h!#vVL!EX_PL{`EC`=R$}-3?d)<+--J#4TAWsepl-Kra2J!+7V34+0cbz{)hg{ zI*_;XmQjU><37})@Y>bB<>RHqsU9|p7DRW%M)yA^4k}2#?GvZ_LMmNW?6U81IFs#eZB*l5!b;ZAe;b0R7`YcDU--7A!@KWu0CXXqAt2)yVAh=s^0F$>iCTQ`Kw|`LXOs>dgfWr|h*5C)cGJDh| z3U{gJSmE>wdQp@GX6q8oCw^t1q0Xd*ifa=#Cr7K;?V2(iAze~#Hf1f8Z7q8IQ#HO? zQ}B)R-nlm34#Rm8bZ(xs)G3$X$LRvm4SIu+Z|N);sN_$Q z7F!)o-8Jmnfx=4Dm>;g6UZ>9DPWj+^= z5_b=5LSB|Ve-j*bakFjhTUNB}&1bD-{>E0umBqTLO|uKB=Dvh#Pc4chTVbP&yZ=qp z_-i|84*V}W=b~YI{8cA(fHqDV-8o4dD0hX)tet+~JM!_1sS!p_0*&D8L2dFBzb!{1 zi2$0TcU<9GR>W05T#_xkcr%VT9dlmFG+8f%e?e^9re3Gt?03-QRr?C&yR(^Bep z{h*yoL5ry*>s9ULg%A{9E{+40^z*MbWRGhdd;Y>&d5z=P^I>M`W)ry=v(e?KtA_b2 zq@d*x@f5R`+7~-gVEn?(#<}gE+iSG$i zu7)XorsCgdkV;>rf-P$Ti-o{^p756O6y`hZq|_>MI`t#}pRCvP5!sV1cI5hzpm=Ti z*{U6HPTqhbb{g05rgKbBd$YyHvRCo`7Eej96ra#ZvYgke{J^uI6I86ywxGfv`E6u* z%o$AaqWy^gZMS%zho>TqsI4mw5PYGrM|b!%0UB_$W;X63~T6D4|-RC$~3?9tQY7^XlJX4`<+JV^3c#Ci`Ze)zjso zr^-yf96;*~dT$Ue&kG(bmd4U|{yom7oq(Gfp5}Sy{=pAqKY|IPPCv~nHRdOhaL_dqGVX$`l)0Bpv9>M zVEnhGo_iaRU1366CqWGR>^e9|IPK;zrI~78aI357wUh$_?dVMD;2vtu<7}F$oRnHI z2)zrz@g)jUobU*RA~+jgLIU#c29%qp>9#Hpq?pO)Wf0VAs1Wj4G}3Hh{0G$(S9AL5x>4 zA9VaDJ(}a~R(brx)+Lp6AO9lyD#7v(7M7>&aXnMu?6 z(3ol)X##^pSxTS9iV0#78Vg41{PRJ>YpQ(koGJ}&glYyWDUw0B^Xe!K1*6_LgEVzq z@j87%li9v%p`UGt$A2K`q;+4>-pM38V9XL_QMqJ*Te8`ChC18mx3k)VKncIF))I9! zn0e_a2g-S>WE-VWXS;3|U7ogoD=_Rq-^UV5ZNbt(V4*@}0&O@Jz2P2N^4rR;9$Ew} z(dr0n7;%9nXoRDAB1&2MnlI^z8=A@A0WL+MMNZ}xXRDiaiEB^N5)dRW^~KkucKDy`eH)tz2XO8>K#iD7sQv4Jg#7hIA_@ma zG#`hg;|u1(vd_PZ&m_ORRx4OMS}_yU@;tgRPE~9Xv}xa)j7Mk1Qb{4HE7=&-#Qrpj zZ*1-a5ms-#>`CAG{H%6_p5KzpGQfv=rn^wc@Wvw7ls77ffR2Qsq7{Y|1p*LFX(e#(Ku9=^y0q6p z5llA`O_U|s@kcV%rMzOE;dBD$!Zzr`c9X~_+o1e@^npTSDNKJ7zZzWrS@+spu`-WE z)8tyFC#XwWTj?&b%XqI$-kUjh@a^Qq%atW-N@DP3v;9IDh*n~drw0mC?HtpAr;XI7 zb?fT`1Ct%^U?(c)#vE+4Cq9^N&`*CcIv5kXVZqI|;sj^#D_=ry&mz}GVy0BZLyg%} zgBd-Xa~mj0w^kU}D-lP7^`Y3nkN@ZE&5`A$4Om-5N-Q#+L$!y0&HW zwRSf$@%4M!o265Z;Uo8Ui!Ec_8|Vj{hU7l7K-Q z)_;krmz4v+;W-jop739|2FYQi}Oibwid?PG=WDaT_D9C=`l-14QEner#ACE#aTsT=C&+5y*a^mp$r;%3V1Q{LxQ(2r?dK#TSvqm=P z?l1XMXp`w|BKUY4I;VMO3=xAUy#}lzrv)VqqGJ>1t**3C3c$d-A;f5}Ct1ncrwwS* zp@-TTwkjae)Zq|X%L;pAilDab4>HCb;B$3Eo*0y^y0l}>m=BKgc19U-2hY1cFl9+B zLiP9t?H|#6;mh&A1dteGhVPzcQTMTmom9BZ4d-wuYgST|B|nPRhz6C8yY;W&u0WR( zH4u+=%!ClB!;!a>a#W6mn61$9RRhFy$YVxA+jKuzF<2cYS#e{4qt&cX(TKI%*|j)a z$Rz$CJgZJQZ_P2}yOtg~g{bo^Kc{&Ql;35X&0N~-K4y{{q3G|Z8TAp~7Bt#vjcB!m zY_t}mm^=_{vsq%99b%dVIjOp#;_?yMPDz?V8r_^&OVYv_lsg z*mYzgnj%CN&V=>1Zy$1&&oGGEOW*%-LJ96mcO*V@-XC-)gqkz4G}8 zoAqYX7Pt@UR=es4r0K9xZ1)Vx9vUs*KwF+w8ycZie;&N&EgZzM@pMuvb~9+aoB5Wj z*8H;5P^=BT#RI5Ksz-64W;cvaU|i}=!`F7AiPc{VP@n!GYm0cOpI0A2^yQql&6*Fv z9OaU&F{DZfcQdsK4Z~<9zrEZOiE5=mTCCK??!k$2Upb6Vn`NTl(RosSZy)D4)@J(l zJ;RfsM4;{D%y7;lI|rhW?hE3U?>3G0O*58z1CTv2=3B_J*B;F7XW+Q?);5eYTa z?(F+h6M339txC6_w6;Em$ee9VE#E~L$kMC}S`-}+uYdJBz0e{s_U}@{;D)>{9$Llf zZJ%z7cm|=9GF+ayZ{%Zst2=twUtE@rM`@tSkBCScsrjY0_YOLJfb3iY`Ra>g2MQ7G z>&-lM7G#1u=%#lGZ*)_qS0zE81V>df=}u>H>&w%xc*vssMx5jvvt-B<3f8*xu8!OK zD<Awg=!(F3gI?WZ$U=J%dk_J(EqjusDsa+JKW*FXa|aan$Fw6bs8wU8%Ejp6Poo-5J3 z6qPMso^pK3pfM@Vg(z30yD`l{XQ7HyJ}UUQ?{EYdOVdVKwM_`A<= zU-zzX<7VUyJm|SC%HLkVH_8e2^iH4LP&mZ_kJN0{e;%o%^eyzRd^4wnu&Lpu%J`9m z5p!%@6KfHJk^twN7bw+gBx8zN@DGR*a`Z@mb4~%qFVqe8K6MevLh4PeqT35k2#`h( z(`612*3+xCQXICc=~^@Op?z|DOFR1&Bb1-GqrAji(u}ppIw6%V5!n(O(`g5Nv5iw7>XdKy+SK``KQc5TjKlUHa2V!g|Fr6p zz_M+5a9N}w|Kh?Syf>C5*zdGZ*UZb9L(`4>k$@Ng-Z+F(HVe~(on|OqMimw-dtDxW z^DcfKvaa{W)#}YG!(DQZ2<-K&4v! zHxwNW=-PIJ5`Dqlhe^d zS``YyL4;l;)VP^Ht&P7jvhuhrx$4NtBzLdCM9K+q)}Kx(Vwf;N%i6u!~P6=2T~#^e_Txb;ih2b1R+ZnScnIXr?uZjedIX$L`}tpl3;KKua`3a8a%Sx>|qe3RbP!3R!mwU9~(yH39*ROfk;k3+k>BZdQb(YiZ z^~Xqw;@RBy*O~0lC5#UD5vuh`6voZ&IksN5H~HZ<34yw&kW$pwo&;Hi$@L59-&xV8 zIc^bicvU?PC%N^my>p7Pd|xm5cwW(-dNlmwqxu-Lm_B!DnfHACw>W`haWpq!c^Z&c zfF`~KkBGLXr^~mJ1cQoPA zh5P)t3*5)BJMh(kx#NL#SFe!aZGfMR^Xpj!dNGgIes9$> z-(&^09rb3NOvb~7_u6IcMQa1PsUV6x< zQ5$S>#`esq_d_Br!Dm2q{Or4YCkgd6B)os;GIP;%L+QAvc{_&i-PAdMUTGCh6m5Tk z)-k)5QRbt&{BWK)*3w;T6b$(4hywQec`j6H*grT(AkZQ8v}Dx#Ri-(`6LiE7+eYTy zd)d>T_KL6t<#0LslcWazU|>`t^oupx1lvpj#@C=SgC*n++JDQ&>B)*+!a35KV{%_c zR5@kf?@!&VQ7xG4QadSv>eicOE|>MYLLSF{hVCpI40>OFE$=4V)C>$WragVvYq*nN z-w6MDkD>LF*y;kt>;izo;J@QP|NndJf2{{XlltX11pzeiH!5*1GRhXJi%MNb5$Y{* zDYS56FP%*j@4g*3k5$ZI&(|65Fl05mV#(^~sjtWBFC5T%MY+04N;+8#7EV5{KR>#V z_oJyx17()Ckn%N>Zj^8u`9kDWkj(FDui#3-)rpm~HA}rUYti))?+DaWidb`&Ct%q$ z(P42CADF~X?zYdVpvu3YWc3D2Vdcy%uh#`4|y9})C9+@CJe?1QWxT4VmUFKf~t+s5? z9i8-mPT}FsxT4HKG|=tzGvv@ zbJfozx==I&wmCzCrdHY+I9oujG=DfZyUtsybLo?D57R!pgq>fV+t*=fH;$HSeW!Hs z`bBq_#cs5_R)ld+P{KZ5`+9`e(IcVj5g5*AZJODy8A}{2s|nPh{>*s1_*3j5PR-Po zOIP4ip5!iWMEo_1d9Scdm)mz5xIw_8rcljd&k>}K^4>sj>v4b&YIn!EV&xm>Xd_kL z|MU;U@%rb#`89acKMoNA?zt9#d+xv8p#SHjI=H#2xH_8|+5NAlby`ElVN)EDU#Hi2 zPv3H|47oVG2uhhIYP(*ov@gYkR1AT}nbNL%%k0%i4E|x4r>R7jDsr!)Ta~ef%)-6L z0iUPo;?>A9D>d~oG~Cn^)IwpVC;S|EAERjwUO)Rpec`nw&BHSHi3XE3Hc4|Jdw61e zpkHz-25n~zEk=A$zgNMZF$v6CLR%;u01)jLB7y$6Af>oVyT*dEI7gAx+^`WP3c810s>BOJ@b* zy^N)Y&XN7@mZgdh_=TwYb2jATZ#JA2H5E=)S9a3o>kl1}rh`Gua62}iDLc>Jav4g( zR30`7~bZ_HaDw6%@ zK9qVGau2`!ZufkIjanLqc7($HEli=nOh_e49Htlg-96a_ed|%3TcyQxw>lL?h#}L2 zC}AZoRpyM#oK_!C#J=!x6d+XXsU4xX8B&3vWB83C@q}pnkN(*QYdij`0kUj)>u8gjNZY!%0RD@NmW&Xbu0mUO~6|tmSEpCo>#n|Bm2R}Irm*qux);H=&UV$hqATq95<^I@{2aV32>21c^ ztrJ*R-@-s`#8R#|IhdT~q*-M4TMXo%@I>}O@tM?AxgJ5a-Hb6Ck%Kl!qK)K2{{5xV zN<_uEn99ahYG@9d3+~&Dl6WLlE?UsMhSRJG*){o6H1mL}X79c8rlw4f~~ zkhfSEe12-Q8P(M;%B7^zm6oA6p~WXyh1)`-TynVxUsBtkJD*t-cUov+Y5l;gv$4MN zX|Xw_aES~HD8tRwlJUrZXO<7cB(8ktms|}hh+ey{nD^z?P~IKzuGCTJDfT~3)ohgo z@AQ!w%phm-vmTvp=98In=3_sIDE*Y=paIoAEf4m|FpCFVY)$dIdi)19%poOFi*S7%ZtxsYwtQYA--J8>43*d>nC9~+R>)3EXi@maa>-~E-?W1M2OwD{5> z=y3St#sL@%R)Y+t3Zr+lTcH7{?!PU|TNKF6xq~A!glgzaD@AdrLv7Hc5(vPV8w!ju z&!E6b4gy0e8%iO-x(9i;UY3M?lglr$UI$52kt|bdrR9w4? zSxS`76;>yWKCaHoye~LGk4e@0k|oqwFBT zi33`Y2H#v=>}`rdyf7x@X7Y0eNv)F#6?b>Ysp@1c04REVTSvwZ5E=Q9qhdq_yV`j}?J{eu;1E(vFm zB!Xp(YGX34@vi849YG1)m}(ZTG=G|)Hkbu-$nL^~Gu%XhS3sWVwgbxjoNrC zq^tGN`%j-qB7uuZI8)dhY<{@{Re>pp>e4zza3WoLHk^cEEIEv%^xQc*v6vVm$0YNl z`B^ZjU_kUPJC5cvrBf!htqjWWK1LRtIjOR}TUmZJ@owwhF3>(*q&C+o)4Vx_IV>Jf zzO1|vapP_l2KN-`5A>czN3u)gn8$jlULJuzVkxkkr)<8HWp(#&;2?L9@4oNv;D35G zhG1^pA>RsR@=#xNy`i=456}o-9I>H010DyX;7R%07aQls3ka4ZhjIrmK!KbkT0smLsUimQqMni18j{ypp5S754gf)OA(TDMf~nvG|L!5CuP`)Ta(ToGfbx15jJHyC2%B$Y@8{l%<|j=`OCORpnvN*I-HU`=438dufs9g8Tu3_fK5pM!cY%Sm(?AdvCs-C+dPV()qvN(Vu3 z(P<#UVnY#RXQ9c9k32hLe6wMc5aQMzRl4P6{8zKQ1C$SVl2%phsHp|T zke^6b9mg^!=pt6++RNXL6zgrrx(dbnU=wE8QGXwM6T5TuL(enbnF_eHY47YUnDr?k zq=ycY9Z@!Q-LVFcpVF~_o_mcV z>KI3Fc_7RKz?_%nhLfUxbSLGLm?qlI{Jces$FkfLz?TxF?Ug~&C|NA|17X9oNlvAu zD0Jc_b3+Zb5C|Di`wn$WT{ed5qF!WSufZ(6f%gI9jn!Ty*%l4v8AGt)9cGTm=x>4w}O z$T_7W-qrpV036q=chC5*Sm4DPyy5TnUZ4O92#DanlZF2q3urhSIsRLCU?}B3(~8GR zxo$;~Wty!OE@ZIHtU16`Kr7cW+=OeGXT_lNtd72iAiHk?O?{~g-a&kNg z9!%TSeUY&(|4#3|f4oRwNx*fFOIo(3o+U20kUU3HPfg*pTj98H1YOT*-zyFPl`0n` zSi=Vi&D`?64*U$0wHzNGjHYHHTa{xjmcTyiMf{E{&hEG_Fhk8WEAw(UB41_+0$U*} z{w2#gzl(FsXWmO7oQksxBAgMAjYb@lomIA4s_IlVtXEE43PFu;B_v(s#g06iTyn|C zLh*6v<(1o^iRBK^_Ppg#^8xKyA%ytwFm2amLuOvUL3>Ma66;-H&#zJpSEX55JABbh=@l*LD_t&_}P z_}kowp(WLITv*_QxFA9-6cz$jNxg{y&uS)o-(MHb(w4I#x-%W_a_ zU%>gyJahyzRz$v-vwkrWPka`4>izACjj3wh)`!GK}N zEe!l}r?!Opq6^N^@_I8Z-y~T|P{y)7Z13g$q*eTcxRrSAyJB%jI|ohLoS%bj_E)cl zojkN=j`d~7m&M>Z$>QI=o**Z5)M}i zz$9_&x7vv+=q;|EuS2&a;($YMgB9&4X$!I%)9OCLzfsqCT|5fHIqgz4fQqY*qBR}g zj6!8cy>ja=DF-laFTG|m+!M}!EvCZ3MK6r+)y3;K^ZsAS#s3AY|EVM?d&hr!i2?Y} z=)qY3p(O?9<58>H%L1V=9{WJ=iG31z99I!S%Z&Oq#ebX@m%ft+38~job0{`y3xAT{ zc|L7PD{p&4K7qY0UA&pw<6yp8MNvo+a4Ji4$I|%a0vwk z@2+nqW?uRE_=PczUi12~WSvqA#5HHWoz_%-X%HKFqLFk@`qLg^i)O5fd_XX*pJ>lY zY?3v(o;qK;nZz!93VtMaf#6{rigK}2LfyiC2=2+=yq|}(1hBDZ3y+-90^f_HvWP8f z$#yooX2_EH(3#XRH`Q@_3BLtjT?47XS#%E_Q)9)|?akYc9aD4TCKOZ|_pot#ZWqUe zC4%<}U=jfBMf0Mv%eU`;K?s&ziMQ=jNr*rzFZUWd&OXMeDu_o$-#qMpt}SSIB{)hE&3_hnjyKEoMdzW;i7!gM`z&OOqFXe|bY&X&z1y95Oj++^>5F<0T2D?Map zJ>h`=$Q}>@6TZyI5Yj^9m_S?KB|b=Z=lcua8OCdW(tEx`AlH&vWH%`wI+#nfhGZ{@w{Gv1Fz3szmfw?cQUP zC=fN+Qu8M@owdEZ|LcoN*9z0GDHA%r-_X!5DPD8$2FZLmtt-QxBD%xHM_UlRSMNaMgBS0f8?C6_NX z5!YQeZT2TFi+zhc{HQMWTO3jK>CA-QlAX>(%$3H2Ligq#7atSHd#n1G#vm&X!adPUs^iW5QKZm&?cq^hUog z=)P`h)7}M{n*`pNc+>>r*K79>O}EGn4}xBERNmdvIeNLHkOjDGa($>DzDN3>s|-Gp z@x8fk_~P^3b^l91`Y#<<``_Oh$Eft#uYX6J9n@esmC1_Q;rvxqrA8pG&;e7D#Q~@x z+W5e4r0aQjL?4L0`&h)F%2uiiK^^2|JZ*Po{PjyOD_k~i5_hBo<-*VD4m-*muxLt! z5Gdc*pS>54q+by?gcnje3@mkQs*aI_o7DWxCIYZYpQ4DpUcfXb*_&5Xr&g&3hGwGD z6prIy39iBTRy+L_-(3&($2~_J#wb)Lx7CDKCQc4T4P}at$Oa9N^!!Cc_U(>poG#0y z3bF#P5Vfxr-GL!8G)k4DBqZdZ#fgm7TzF{_qv?C|QBlFS4E30-|MDWR^xWXck|VLu zLe@^-p1}Q&v5y*z9Q+77(}iNcabjizOfp3Ce714f?^9`5G-r0OB9`Cj#~2jo5KZAS ze0P7eNZhvNh*r{r{gH&Q{_gRG_gh*D*$*6*4o#uh2VGnkcEgn-(^VVQryB%MI5L;E z$Uq%?+>mD*!2L=x85|&lvEwDmFkXmmu4CALmlQ5}OqA_IO^68APO40-;`)tN6|alRf6AsSo0Wk& z`8=~)I9NFNEq%=rIfC8gT!Mb3q(sQ>Rh!VZtTP)W9>d1|u^=>6Tp-WcNTXoU7XRc} zwzCFz*9GtD<*{oIUIRf;pcBZdzMly>09Xp#%St_v% zzKXEP)nx(5h^M=@EbN-lZ@*CTK_cd!ag~13o*zzz-jbb=M}qM(UOCYW+z>K(JLx@3 z2;PR}@b!(2&e@I)pT=NwrdO7)HItn{&>>g8Q^LMNupHD@gWyrw0L@I~7_?~^)(f59 z3%I$Q+N+L-y6urdNSAx{k|Vd5?E!T(@dPajn>)R8zM-MfdHjGJ6sawynRpCvW-yov z&^q?IL=jf8qkS6e*!fp|IYC8y*KA# z#px4Gi8;>N6lTNjKN~x9sGUUqRBU6rQAzTf^jiuNykk)!|mXGTR`LF{W!#f$gyJLb3uyv)>^}V z%L7>?u{M7UgM)lZ);0E$7grFm$d*8{x3meCibUWKKOu-z(UE$H1CxJ@zsC03O00hz zEvM#56t!2I97B&tJBSy$u@|kmI`b=33!)_p)L49&u$*j36BP zx4*Zy>hym+Kf3c7$k(81ZuXlp_hd4ZMsa_U1cTE3dvd`4G0NK)3 zp)RhcG{-wSMD=UcOsW+s1aEuIsfb^m++9OZoZ=#66&z>eAVp&lX<(7Np|{+Ccw(Kj zl`fYxS_xfAb!pyxRBpW4}9;RT;=+Y8lV23 zcd)WzIXGRxT){m1`jEwYD$OxFX=FEE1KfYsX;L*ku9YNGf1H-(pdI=M6B2ktK6j+H z3B3xkd-2L~G8c6@W24&tc^#=WasKv%uYAEdTeK$MtaAUmkxxFnNi60tki^$W)tD!* z+?Q&j!whVO3C(wLk;)26E1J0`JZGHmSufEvkC>*GI;YDe2&o0lOeV7%2bS0vV9h3} z9})>^*T%LkJt1=62g%htf&;4!%myf2N!*d?R`Z3|Om1%58R;$pPD`ce7CX3t*eccA z*|emys`mj_xAV9E$)c$tc7}n}5}KM&jO0DmNW)W=-W)u8kOQqR{^svGvF>wa0$oX2 zm_-gbMcTqZ*_PC(P;WDoKfT2fqmK#gABKu~TJKA6nI-7%1Lrf>y_2W*mwk{(633)2 z-0F0Yjk6eCb%QZ_^cnrk$)kdq*ivX=Sccg?MW03jOmJX#3gk^Ba2FKL*l}Q->lSvg z@OC#@6n9Huq9I^&Og5=_OEnC1G_if=qx*SAeqNFoVq^G2yyzfD-&*Z2KmH|8J3h;m zJ|7_kpQ zcrz)FiAOE1{}ep$9h)0NbZk1liPD}x+b%m4-@Vt4)Tvf6;$V_Zl)M3d@7^vZku~Z+ zmS!z_fS^cHs$3Z`NDq&nKm)L}?c%`U;8D;GN;oZq2#$XDEfl<83LuyJ6WEH8v?+EFR@$j>An$Y<%bjJc*MuOE7K7XEQJ} zu;#f;PUuavByO z`)yv&BxWmPqr!R632ao3SIo9opc#oIl2jS}&%{?2F&2$?#E(j@$Rz0n&xjDEgJXSM zlGhc2Rq-{k6V<}=MWGpu4u7q}AW>TH^>Lp7@#XF2YGIdP{TRmo1;zI5a>&*Pm4lTN zGCEX(h7BL8-7p2#@Z7s4DQ4Q2$>=~U3He;@;W!%zx;l;=A(_HMa%^6OQ7)%c{QwW> zpTE72k{rc-Rw7b?gz<>^B6TLU8S|Z4lA+v%Hv@uAKwp}0A;;o)aK+?LBTl>*i8SvN zWHQfKXEiKmbEf|1YNoBqUh=CK>9h0<6;F(e?#X@TAzq-oSO&v7pd0oYPAXEgwRD~_(->|rSH271)O z!j#NH-~k9e@PxC3BHyENbbD#qf2`|;4T$dFQHOoqBp|lFuP%7K@Mx zv@zSznl9K({3-5S8R?b8hv%>%APQ&Bwfy@c=7}F zC7@+&q=#F0Pn~*QOJvw$U+ruS8&bwXU&*#l-nA__Zz;9!I5w%`a!E_kH}w8tlC-NH z5psW#Ntko(8o>TFk&5szS7A@v)12s`Tz!v^5`@nzUv zm=#hto*>ev)V8w!cU3$pO8=O;_}lvrSs_y8v=__rDZ$gzW;n#5r3z0@ypF7LoOCZE;OTOJ!SD0|%yO@dvqnVEWd62PI z_n{c7)+VPu7DF2RgD>$O`aGZ^FXx0jc8RRj`V*IkQbOh(`+tCP!yvpf9EE@<$5)Ui z7_l5F7U;3LX(#s@Z8l^>HYMVWAL)Fy4e-q)e`#$X(ELW&Z zj4XJvlx%^E9^gKs#qB)(Ofpdo+d*HaCdzEJ)!xudbNmbzu3k=-?Nga#Cbx3dujdYH zE1ZijhJvY1v((jqN1nus?j_eY&zfb``z3*~!+}+(3-MGIIA4h6Nx-bM-jCR93@mmq z=C$Awh|R~FBxZ$L$*p70Xn)gPxS1Ui7Dj;Af6{DT;50-RrplD4&{dZyCBl=&M%5z^ zKCZK>8{i0S>W#ZY{R;F68=|=+2mO;PZje4nD(^gun~ZX6f|YeKaHnSP0YJ_O8fgo20?1s z0MUW&9>{o5)c75O86Y%*_fEf?y#5qWL_v5^y!BkC+`uaaA+5 ze$YGM@ULZfvnCMf@Vyry{XfV(zv0sVuYk@>#lRu!+fRuhZ$G)Tcs&0*PC;HpE7R)# ziBouUCr)#uU!CMAXgv4W$(XI0LrOIB{ru=4jWjH*T)tuO`2N#&-~YUW{&o($=F!0D zK0E(q)}`J_X|mRYq@mW=_$$xLhP#1jpo!zO(aMM;3rvZOagvMF3{att%d!i`RLztG z5X2r}j4MUY4zpl-01x{7Hd4Pqs;Vv+#7K5m)Iou3u00C;Nx)ixR~smsMgpNGK3hA% zn@d3cfYrhus?VG3oD7@tr`Z7(SdVdCu+3#@gvvUTVo=_(Y>I&f0l<+-o^-~S(~Ua! z0kBbvZ!$dfm=cGp(fN_57;2NCZ3wy!bOkq-v7qm2vRbzu(|2BFGaDbaR_hkJ(!@Ld zN3D=6K!>QjAW$eseRFWajL(340S!YYo$gp;j7DqH>tYN7cTQXN*r0m$T z$5Lc(Oc-^mX;}5!LtYJ2d^jObbRok462}!Vgk)HeFnKbZ9$kv4>nH*v|1K~I%a7z9u z{d)+qJ=_4d=Ng2Y+E}pR)8T%nUB*7bfBkt4!6yqtzGn#FdxlW{_Zi~yAL>s=VJ}xR zA!lbJuYZ4(*US) zy7@~>&D4;?WPxK|T2vX8y2WqXPZl%Z;AfbA%-qh=&n9LIdA0)YZ; zd-yc<18%!r*+$0$60hEz_#a6ti4-$KNtq0~^kT4{AHh=)`yggP_glpcMo$68N(vSx`#^UxFGTt6>t32?V zMhVcE|8e6i(a)P4PF{vlNMR@W8;DZheZIZb&Zz<4`K}T_qt$uCA^(NaR zZ_epw%oWP_4q(#y{2AEM9mljn_sLzp)E-fx1pjcOq>$eHc*2N zEDxhT6zAE?)R=J<1E$mG#1df@F&0(TEg*iYM9uXc3@+jWlW#1vm6ivY&h)$VL? zFh8ghU#B20Ta`#Ux#ZSLiZknU?7o- z9U@`&aD`u9LFFCp3fD1C#p8fyjj15JYRI<-5wc@(SNHb;o`edRqSb0RbiOfV_`74N zi5G*N^W zY;d6YH3tk;@-AOey|TA@=o#@J*I3ln@HV z|6Dw3JIQ?$iXCBFGP4_`@j#Ocb9MH;qun~r{#?5y1KDn#*I7!zR!=%jMU1>IS+xjsI?*kF4K^^9zC_^q-~gNp)QSa^txFn93(ifiz+}JF`RbH zAEdd!L2>ym@g!ai0sJ`5EV)ULJC^VTpen2Tsh8jt+7R1%VFoFD8ZmP}f+ zw#@nWW5&%`Uh$j_8<_VREf40-F05_cIeK)K{vr!6fA7~WZT(}G$mObc<_#JyPDu9^ z;ryS&=AWT{sGt!y3AKZtK%%j1Db|xKh*2m9m#CxbIB8oYhYE|TI;%ikG*}Bco*5NW z1)DI~`XOsQmeE1hBqIjd(szU>yq6n(8`4y_9E@UJSZO1qSxrEzqND#>OI zeeNRM5*A?S%-+r^<;7Zta(VTsiA=5xlv*g4@vg7=mw-*KyM|UoInCzz4b!BFz?RE= zDrHcv;yz8#2n|!GzzKWv@Dhz)v}$YfSh)9Wd@WvkegBZ`tj#;LF`BF+<*0Iwd{YWf zgh}hS0XzQ?p%Khb}{M>p%sL)3KsY)M~8_#7ExM* zPoy{cuJZVDNSxu;9r8Ms&q3z%Yq1i4K!p>8qGOBU+Feao(utU46PHPk6L&wrd88=0 z;;GtV#u(P26Y`&$M6Smun=pW~*OR>ot?yboum83d<|Ko|dBwFh=5B!*dg;O+Q&VY-U-f=wtq%PvD%zw-@MF zECxYgLOq6SsU2j#PL{`VsF~D~T}Yof2QD3Zc85w-j0yQ$4=s=1Guq2h>pc2{?atp3 z3C<{8juBq-e8hYhqE#)g5Y&YH-lFg@Fx9)_%Y9eW@TX~@@`&yJ?}z%d`jd>jUEwJb zgHcqSf|>a(RZv@>=Eqk=7lwh4)zCFji7#mhHJ~%)2oBJV`jHlp5B#z=6k^zZ5t-`4 zyZ!dK@C)ZrJhxFn+G|<4FG8f1ox(tyJ|=P@rf|cOKEa8o!JErAgQauoTlM|3dC|$EW5je0?laiXw4hZ2S9WnBnZbQ^%p{ zM&g7fVO^+V7S(DDv^FUUOs=V3L69|f&K0YL+W41v)1qa!X*htI3RPx)SY?llDY>Xz zntW_S^f))UWYAPm{8V&a_{D3tT+w*=S?G?WkRHUUC@~fqPE6N|V0X|w z3sKWQTNB`B{R?YEJ|LTXuO0&%Fk(TAZuHU&)Jeg{&LeV;i2Kma`Umj|`_M1o;c?$u z^qMyH^!;kD><2&jnl98O?1?xQcuFYyM;^U%(I4Wz8R9%+5Iy&%~` zH9b?AJZ(Z$P7P$?;WrYi6l>7pY;Cx+73&dK1pobpGu^2{x zGDT>DvMJ+*+o|#I(w(80aORz!(~vSy%ZBiq5mpoP^60s#=-3gEvaPmSdG8iv-GKnM z5JGjd06p4H>4Ug&-GZUzz+B)g3K943jAH8e>38*Pl zT^f)1&x7~mtm?V+TRiFMQ|UmXV0{w5Y-(E)FG!gll#slEgYvN>N;^cRm5P%gEH5!B z8l-BWgc4kU@Tn;TNDILF!=)=#$BmRvxlFlfLVBPR=TA|AM0#*et^18{!Y&r8N>yRI z3<~=NJJIqoldJG$+TZmwlq&t8{j_;Z^$sL6JyR1fyb-$z2^S~QJ(6+_hFv>cadj7p zkNi>n`&@JYeC-AV*s=zuB!4A^bJy2!88n}1c626DqxqgCt%r0YlY%xwb6VkWio4h} zcb^1Y8{Ry}@x>`HOKZpi!K?cTOB(^k+SiZF%*4B#KDS9EL=}N)8$84@{Wwoh=qrKf zE{Ls-4e;f3Hk<=eCnsnEKYCMvKT3g!&{tPLGCq{9&qQ~(PmrxB32CSj{H8az9;Kx^ zu6*c1qK+pMzHOlw%=u++Q89@$@R9n4zX&VS#pAx@vzpjr2TD`Sm zEsl%Ej5~Wp&kMzi#yZQ)@0>bGX&VPvDQS1O0V~gA$2~)!p01A%URonWQ}+8Vqm8YR z6Ut5A?t}3kmtI^ce}qTtJhP?w@9$GYd|Dr?HqBw`J@JE2$fN-!k9Hk?p$2BANL>6{ z3Jq>LYDXx?<+AqDu#79#F-=;IhE`%?Pz(-m+fMEjJq3A#c8Yu8?3Qtb=+OLtAPNc` z(XIY<TBy8L~`rD3aiZc1o-UNcqxj0HY{#;+t55TDKE-j zsQ=oDsdmVrS+p_yhhN9N z`!S)xVG*$?IbN;vO|UC3rlGcAsiVn}Cx)G5`&_Ag$0CKVr5zluG!<*H|s)FCncN0Iy&kE@#YO$513)(I$-KV z!9#&0xTDS_npl3$U?{+}Ld_7@;(9MB*92G*Ki2<@z7&!K4H56(#X-p>*Fh1tj{7SkvhkyVgW}7MYxB~bm=-!rxhk}L zqaw^B>GqPXou71msX!v3a5?`bFOU|ca}~4!VaQfURizoU{AU%~d}c!GgS^wgkTsAt zh4~1Yb~^cr_^p@`0$=6#8uSXnIR^SPh9q5-zZF|7$W0_#+;4n9%wFG{KBvZ4jsG>F8;vC4Sn)+3wC-TDQpga>KJh}>jBkJ1` z{^(bZ@sp`s3|Z`Mi;~wU)QAYAMQU}CrlpxlTVg3MI&$&$Ef-5gmizPm62KDhdw4o6 zpScJ_Y3GlR>LVmuNd4NCXkjiz9h$0M(H1NFZx!72Ao`Aqd~pu8pze;`-w=40{wgH}S92|9A;;uU=(V$jQr zBkF`jb~n|DpPj6v9TZbs6eU-k&0yNuYve%`+Fz`FBUH-c)|{8t_fs@dq5o{+JQNlw z8qEx~7tM-4plRPQIUM3CRTuZI%HLES%6O_Els1n;IXORiada1sTBgaxRP$cw8%-mS z8ilb1nI#-?gl5mL4zeh*RO%X!BMFDOB;N5^@;s_9O1Z=q#SdA_1?3I7-zw)z-XL{K@N|Vrw!Ne05Sp&Lc@2o6-4GM!{ZBcUFk8f=iVVFMwX4! zK~QmAhPy30tg-O2#7MLSv1Kzfop2OjZAI+aMv9C#K8T{4vYJ>&wwzjbB1hEKG%wR7 zKy2aMgPsaHDb3?>RR74xx1kbzcLm%^!S+|eC3Eu4l{r)4m=gHVsQ#I$!(evc^va%1 zPqzA)t)uAH)|*>P)?9^8Z^804;xk*8Q6YQ}>+a@c90EL_dO~yEax41DMkzFO`-wqp$9(yvBOaP#m(zlG&?RCK`!WvPBm` zE7rxCLme{pa@>smZ%aC}klD->nVLDYgX2A<=ji9O@(JY+^WL$r=YVh=+ zRVCQ zc52M=%bv%G84F8CT8;N?QEXRUs%@NT(Oo!yI0Q;2dArVm_1W*mC#;wnt`y6)wkni_ zjJYg_8Iu6=_mg0GbtH;s#>r=Upjo?|^bZ-cnQL8=+iYGBOv+={xaMQilAQ)%NvIc> zr}*r5&;BoVf)H_N4)N;niScWKT#*3#p{gSoR7Poij>vvP>O}jSHX~kJ@m&m@lq^1+%sm881l>rjT%&EO+ePZ z0iv5-Zo<@^T{k?VTz0B-vWETFWncBVTe6-Gsuhe?MwTi)sRqJBDP*yF7XO@lfX0z$Nnz9{g3t&F)>-N z{B0jA!|*?DF8()JVC3?Du|~Yq_KDlzK>6}{QmGZlPe7G;Yb~LwU(@Z1W9uf%GVI#O zhYt*Fw$#y+QWYDE%{uPbnT$uHUfUiiNIOPofiljD%h=)LV)gy=r`9BcG0KyNR?BhI z?5*Q%r#33X@GH4Nw(f+bCvxweCF-P5(W%Y!gHOR2xsI~|5IGXrbx5jpql_W*cz1OB zIF0ab!gx?OiWXf_FNyXWEbt>cZg1e>VKq)t!Q57G(+n8lG`(8>2}p|0Y)HA=G=okH z05%(ro;4U@xIdtINFK$?kxKn@Z%wmSOm{jwt^kE3CJzptn}!?igo!xJ05yQ<000~l z&`>`~c>RYCBPt$sR}o_&M-)+wml1NL)M8*E3}{`peG;VrjdsvMP4{PS5&1z`0W%@+ zVlZPm9abI&$g6ZjKBk!N@LJGE z{@<`5BgBYKd|kc(Kd-*+$C;To3(TMeVjsWhScAMAI0lHCncWrEWx=XvHYK%`_z zgI2nCiJ9t$FfBqu8lt4-^EXeLp+v>IXMK+xh6!S0{zonZ!f(5OskQK-@Y!CK^{4xb zJb6Z?nbE}>Sc5VoCY15ilB7BmBAmceHA8EDc=tM@D1^{(Msp_(So2*_sz3``oI%j3 zVN)9QP(3SiL-$GylqWBU2H&u!XmRZmz1~J+N++N1hB}GVdUO0eO{fT8(8(pkbUKGt zue?3s!o`tm?yx+csp||@$ZvP~`sC=CiREg?UNtBtS%7G`I6Qcg%GRsPJS;_`*v-E7 zQk(9jN6CoVX)s1ycW=J82AiIFhN2rAAyV(}Ym4`l>%CQqJ%1dzq<1ya(yikzGxwoo zG^HLsy`B!X1^!shWHW*?ynw#VP77S}Hi9iJXm2sH*Hm9WcWYVZ3`!B~#!;t!=esEw z%*eSHrwIgl-4HfA>;#%)BI(27=U6D8c-KQiFY^pkg7Q&IsV9md=(9+}XjMe}K_nR+ z3_xYBeyC^7R6!GvPI!@ZJ7N0N_?NW4^^+H z8BSG$(gJEdJ9A1TDwDSIO`viDScqpsImY3BOK&x;DJseOGYlakeiy~Vv$2xMZvKJ| zmN*?;1#+_cLiTYBHmT2SCb#^>Z5k`?<>~hwA#vju&4-rPG((e15JB1ze>74@ zXZ>$&k2H$y&HJKF6+hZK!Icp#7L6sbMOauglnAHr22B*X|E$MId7XZLITa-K&(LIC zSN$BmbEMxCBYM`a!C$|!cRl+`ZpNO^!a6U~6PG336`X`lSU?ZP;F59cG9^n(EmkH4 z>}uw&`-hU~Z7iah1%*{+S#T6z8`}$CHh2!%)LE+$1esoJ2I;ghe+#=)bmdjX+TPi0 z!ldjOVCZnrgDRLRu<|z>dA%>u?tS?quDkksoZ?QMk6f>0l3F4xVzYH6)QXDk%{HY? zv{6-(ED)rG+rk`i9i!Lcz~-OJ^&Oam?2A@Z@^3wF!EHK{5v_}0&pGqMROP?k(}>(d z>Rs}H*Ez4@SGf?&P8MQ1& zo_{~}ZnAQO9HtsjJ}yR472WILNN#75==I)ZjcCJ^Sf*`l(>B5ix5`y(SAUk1+P-p?$qlitRfRHS!}i-ko~;I!FMRf7kvZV04C(KU_ig=(jGZUR{x26cq{ zqkmlYTeaTx*HndL5nz=tKoh@ey;|$t>W4!gDvS^>E=-%J3^5e-sYI?0wD=tpmJV=V0Rt=eSmoxbgst)*E@ zcQm7EQmWi2mYa5;Ki6;k?ha1`Q#z%w@DKz-+J>#lOoF-&8w;{(;Af@z`wFJ@nbTNQ z;R}zeGA;BMAt91RgmH;(YpBf@wj94cE+#|RiwtndHf?XZZFCnYm{35v4M>BDFC*Sk zVm~jaYd)dx8ibx4SIf zCFuH$5IePG>E2J!A{{HZAz<8n3>C5xj=cd_uTjcj)T{;=$aV$@8Ea#q#Ri&RwI{&< zWW&H)zt-Wu)3Sa&N{YpCi}8~u%*&h}oEX10s6dY7lJ@{)7+$r`GTNWgS&m9b-m zYM~fA)W~PtnD`+ly8uqp;bG}aL560JZj1sv`LK2|x=TWIu=nGJhdI!>_CkARgWG3c z3wB9K@`NBvm>^mxpcLw)h*f%)(R$S4HGWoZ3~-f*uOSHA!3Bvaz_inMcqUoP+B?-c z1EUk-Zs@4jn=(%Pq}7PF|oyB`WE$;nz11iT{}XWsI1r z=uLb}OWkr+>vJ~*R}%xWf^d|%XE+074Z~#NhgJB&kvW*)wTtz?D0{~yTf3}VH*MRt zz0$UA+qPHQwr$(C?aY<7?RE00bM}s16+7as^JV^l+3!Ba=&kjuHJ0KHlW`n~!{h1; zwl7ymS5q}%`9E{Mu|UE18%RBOhHPU~SwBCm!hy5AGS$G2I+HlK!N0oERDZA(Jh}eL z;A4>%ptOeFM3};>MpZ2ezi&n%QSnn`*Z~SOsDv1olMQ z_-<<)b8DT;)ZgBJGMCsdwPS_Aci;fD<(W5>5T*w8%%z42Vlz37P&^q4swNyfjjE*& zOhC-O*~-bz(lo$8qQYZ}G4FGTaVf`l=-gZEl}!NdR!8Vap#7`ngnl%_{oM9*R@(j2 zxGo+*W}1h~tM3ws!AqJtkt^v7*+vH2D3G=(|7tEP;L{AJzrm~ZLznt(*7N||j3`l< zj$pqJMezdIv?BMNyOM3X8n-c*HgNVT>SFcVMMFVnxV^>z5fQ}k-667wJt(}rE*#LW zUE2J81y!KsqEcwID*na3kM<#__^Hppf9WLAW}%6P?JNC_8j~TFitI*nb>(T@qrDEy zX>DOFdw~!-?^`U>`6dp7_JW*ty>>R{`T4M2+)+O(!jbHZW?kB9oA}&Jw(T3j#p=QE z`VtmVU_S%gPKe)ED#wXRw7ikKQR zlN6dWpb&CMkqX8j2V~R~Yb5lFrB$stQSNEu2i;@4Kg5vD; z3+mwa5E(ro;f+Xn~(Ot-xHfN4g0K~GqWaLCuLW)x9tW*XI)m>5s@)a?zW zS)*%kPf2^M2Bt$+Syo`ybu~<0)vKC5vED!as)n!fo9597Lj~QOs!ei|cDmDB$Chy| zmpedZb{-6h)?Gh|2%wJXeVHO?Dv$DYRji9p0v7{5nCJ~zCfb3_4n$BHM8Eb zpJIy828H;%CN_Lx15`^y4adKvPRf1ZOs;S%k$D!gUY1TmV6{D0d|EIAqhSTacO4z>;zJGMX>fT43G0Ze$vP6Jk#<{j)vcP=lg)?X4Tx;qr2{nmO z2d2*#-byYbAwmE2kfDRSv!dne##+tx<${=TGu3GFoC4WIwiULV`8(kSJJSN(ZpOFt zfd}DACyVTRX3G#0apYEIqD{WJFzm3>%oEd6bM-o51{&zoW{ODa>KF zoOiow%@ztVoeHVXnqNMDt*n;u*3K=Nga(Y`o~X47P``qvjV-N){)iQVSV_>a9j785OOTa^Zb*G$ zjev!Pr9gW_n&Y_wxG!kEh)PNKmEkE}*8!J|7n*y4D3Xr~SxADiLG6jN?q_%t$d1l< z+!^ZCf1%%`9`tcg&v>9bf?+B;3Jh429sZAWNYG{YcL$|MFTAZ?)jfISCYhrjSxZYU zXXy;mXG0>Hw{UnwDrSUajW|BgkUQj< zcJ*B)1I)F zt5iA`kt=z2HWCujw2MYhjuKbIXV8(#27k|qunF|!tJ5;6S<}OuJf??N_ZYM4AG?W+ zzUeA<*ZN$?foYsh5In3@h-zJPv6K4-N5;!EL?g7=q{pEpwwc2!sT!tjrt}F)05fKU z;bz}Oo>5Co)Gax5bR)3HH&8juDA9S8Q{}}Cc?oP0GcptpwE`O>G`8sO3E2#^q*}~T z3?*-GU1v}ylTlr}u@qQhL$0ci>_hFShV>$YPGqHG=^pKNz>^s*So&gx8K8YX1pFOP zV?tgD`vyt(uOiQMhER7cRn&8?szGfH5> z(Jg>qa2sfcLCW64dVQ^!oI8!0Pzmd9gTu6CmjSaQUOJJu4A=v+V0- zk$%29;B;6q5Eqhgizn6c#c2)Zqo|lA_B~vzeZOZS?G1CQWWQ6iOF@PbG~{aQ2idMV zJ`^XSfTL=D&Sel5iw(#C^)}e^Z<)CqnBH!g1a#WmYHS0pb<>P4U-g?CZCU=m+GgK; z=(re|uXV8NX>()45$)H=3&me%+v3p=IlZK9#Ikh-ZDGt`n*I%d8Gk?x=e{3^#0|&; zV#W`Y&+BuibUH#^2PaG|kiuT&jNy`-MdU?2)pX2N$#>cuHQ#NV9}-1~-#JD&6K`HfcP| zHs4~rxsa5=%hW@e*lV4|Fk$p~po*Z)6nOlon=P)akF%yMf1P? zq!yp<{3qSAr(6}OT%MJ0y@Hudcb^_kI<$bLV^E{$~i0bw1=xqnlUHk?7cTqrqT}>(eM+}W6 z`oD_;g8xkA{(Dj2NWTRcBf??%&^<7!q4tPnSp zL81=SR8_R|r{@G{S1JXi7@uifLyimua+i0;XLr9~)kusf+icIcBHeAtpmVcxcI`;H z&Tq`3V2vw%C&*owbgz0$x$NYUK&DVWYK^bd2^B0N`#yiFEoD6`I|r}XljqZ@)PMjf zSi^$}33HLaJ? zTqDhr0n-^1>8R%jvw*Q^{(=VklnZd9ps=}aF2Iuezy>(*3Vzf2UVk3nDN_8!_fh9V zrIf3Wy~B@(cAXEi?g84>+1GWe_cAwk?~0a-{Uf;hCKuN&9l&sx_8fTmiU3RQp|4!A5u+L0I0nrIITlEtumY+pWJ1r-DbF`XhxXuA3Bk)OU7G|Ubr znj-cqHvcidX!lOH5>&&uWc}td<>PgSkB?CmJimKHed=k64~G0pI5K=JBM?#*Z>E`j zYV=^3ku(4&CG7)0kH?rDj$H5z)oJFC0KNo#UU=UHYt@+Yco6j~JSkE9^%Zk+2;b4% zo#iMUN%P9bbO&HWhODB%3}M^_<875Yf^M*l%rAVUmT46@bZtLPKh^Rh!2;5oE7zS5gK5-s|hXJpt}KkqdtJgBgyh=)D%m{Av&D1arjWS|z){yGFC)9(lme-trb(_5Jd<}$H2 za72$oiZsF(xAY?bPic-&Q58I9dOV)dMxy}@ibT3Nq>zF)$ zsN&zcGd9K}YjcL`4)^iPcO@lEu@r&APKH(69b}8)B-#X6X657e+=gE}9@&HvG{_h! z9%ItKbW$K>z%Jx>wH)`v6xvqUrTGUK(}ER*y9}np?JuJU^l>lLWW?@6 z6b&64!rW6)fkCNo8Kw@U>orHrZTb(!0^th8!HhjC?apMiieKt{%$X`bCy0aU2yJXM zUh?m_BTj{;?JV_N*hc>j*+I8?Lf1|j!-ASian7FzU(1yr>u{*(S9mI%3s7x{??sEBk{WE4_$AJ31^fLjf{Ul1T zmmzSDCpRXjHHyx}R8>JEK%g%^j#qy;lMvD=aI9ClJE}|{xH_2Ybxo0RKNQZ=Q zg&vJ2I5f81usx;s`OIYXxlo6P5%$C$Hm_eOr1-K?@RL}R92EoI; zXV_=J+Q2?UI?WR%dB%oNb5%dHEn#+2SZwQ_f*vW>WG{cIBHL~TXzV%Le5hHbJ-|G3^J8KqS#{#bz% zS1T(;49Bwx-!?oWV=EFFiar=sW7=(uh7}*c>}QidMCt4@Yw?uj>rJ4e|FtqaW+t-9 zp9u&jx1sdNcW``^8A(T7fNTq#yd#=mWheb}Z_w@G@M|RE++&kJQ{Rug6+P~6^Z7dF zo|VuDYYl%{*V#nX!kJx`ocFz}^=m|2?Hbc|j-i%}WTr|;+CGzMW)jIq&X@k(P{J4l z^Xu5s@^qcsvLevLV#zZr8tPKtZFAG^u9c-UMdSg!@p%}ZqeZ56`+6x>m%3PexJN@t zgHf7Gj!*US{Qhb&)#ONaO(BE)y3E=FUsZP=?4I$ZH8muX1OEoQ%*TtJ3Lc_Z3pHy> zn?22f)zVyd|HzpYS@%YbfeqfDoo2qgk`pxJ;6$}k8Jb;_7DwoIL9rps4EZk1XV*+vwyejj~>cA?G{CF3N*!j(ntdxbMt^Z4Bm6=Qxt16 z)G@|vXG?Se9T9C$64;|+q9MY$HMU3TlIRPTQ0z0bu86mCP@S#E@dot<4y+>IlCgfR zl`*<`M@rvf2$1OM@O}sXC=oDNrE&`rYw@~X&|##~iy0i`<;M*+(4Lmg38}`mv(tBg zU)6n}ZY?G`T+LOD)(MidMs&?H2AA%n*yQIZrfsjpm)}O?H$F}UdvY3bfR{$L#tOdV z#1k}auN#r1$cSNnX@Vv*1$?l8=yE5bi66aO{9^rE+9sD}cycsDBYj4u`051R#t0zX z49oA>W(L?dm!B+z^lnMLOmYRMgmOx^!&?wm2Xx_fC_51rvjgdoVH@#A*spEi)0@c_ z7mwh*jfl7g-g+9W+9+d}d>(h;ZT$UK4418#>k&NK6j_(j^x*Na;E0PA^=7M@<laqqI)kvUz(^HtpY@k}83u zb1f(v*LZ{Dif?E*Ht}$=rrtHyvn#=clr#JaN9FCbPHy$^*6KT!m`1#H?+JP9TA`^{ z5m@kU+fv?4IdV3OC9$u#%0|B_hq2l!M^%78TeEQ0<*&`Y+BWl=J{^*GNj^iz@~}1V$Uo3@1?@fI>b&W+ zhUJoeMe?<41w>~hs(ecRCNO~Hc72^;-y`)DRq6xK^R&9 zDH}#=x~Vmq_R$$Xi}FiQU4kLlai@aj6?U34!u|dUv@Bz;jw_3wb3OQE zP=a%vJg)U@xUz8)+MSD=4?l)};Q%r#d+^lylc-yKcI4F+Xz-5fl>@hT4Lm#|ANMW4 z-WjN<+_FbA#g^y@EO6d`D8D&{x-zg%f5cit2zw$NNUfF0}2MV7I-hi2p=DBIT5P1|)Ma z(}_$i;M(b^^CnqBOM8R!^a>|CzfBdjp>@$Q4O@j@Hssx_Z9F45P$qtb=0jh>Jzyn& zM&9S~%ld4->!G3oKtvVQi;9zsR;dFLk9?J)$(1^AL8chrRjY+5Ha92;k`3Pjzlu3E4VC79#9W@>;{6GVtgTj|stWK{j2f)s;kCf5B-r?01=#RR4$5FzkqLYKiZhm5J3l9av_k}l{o`mWI zoCtOYNuE=h(ntMmJA39HT(~<>gZvs$N!br)a}WfM^pTx}gyu702?lMYogM2y!$Wph zD(RmfKj@jtp9cAmSw^*!yi@O)l#d6rMhb`?Pmgc=%FI`}#9c=qcW?=o$%_8F?l>@9 zSZ%h{YWRWVJmZ0I$A_yTsn_p??45MYWjTJ>t}YD9i(F{l%%h|x-}Y_Pxul8+n3@>4 zXPi{hYBgD$A*?9??{ZhNT=ItsUu3N!1*<5ZN3DLqBg+1gw7bst2&sf7`!H#Bhxc5A zHqp!MtF;m+Vv+^bph90UlOcOpw!#K|qr*RBPEew=3^8Tq58)8x-DzT=VZeEtl52R> zfzzRPZ=1QtR?^{L??YN*8dvuAdQcbkyGDz@wFlC5se9zA3##^$_=rGtd?&0n4|V8FKD;D@Gu~beJ-)Y3|F>2VBZOFWllt#wA1i zcF~Y9_SNP!H+4ZUm-|_`)X3T_J^b?5&T;8p?Vy6qHVUFTsF&wnFq_-1`DWCIf<5fJ zT#ByQCr@^A|9x)an6Ho!__Mbk`eUg4_wXAFJ0*JqH(L|O|8aNUCBaT^lL28wFHz3n zoSI(;$h>xhWj-0M!R1PL0>SHqEcH;-(5qz;0pX9&2@3;4VTCb=?{)j1uW+I?s%=av zN$}1^9(GQ4Pxwlty-KR$V5efcuvsYyY?c&laO7(00T|z;?Bb;X?W$x%*>t$$2MYv| z&rptH3ffAn^nAe45M)|K-jGzr&qrU$0R?w}PF)7998s^)odPA;r2^n`92T_oK$?G& znrtFA042m#rWpiFAZXhgW|`Z9b`Csm<*mro>Dc)nX#MCw_Q1^lQfDvX7nIp-%0Kqy zGl?kM{4DQ}F0K=$cN?7&o+_%E(_(xs#_a{H;zIn%SFXZrMxvzvE?^k5ll3@Vi6hpC z>aW;p9>{AQALA6d80kJ;Cni$+*6!0mMiNZ~**6BsC6~yG>K<3=;?&cv*|l_ZVAQDE z*8rM?PNPFFy`$2ldwm8ai?n)0qn91^-(s8zY~6T;!bm*#ZSuK58bbexY9i8zraVe= zv}iCF{w+_T3pMrr_l2JHfG5Ifgy+q&Nng| zLL&IiaV0hr_my$u54=9U;o00F(NoxfKnRD#VV1m+CX__WwFfO066*HT+t=E-fsK&s zEg5-%1L@Bnb0?c2^o<7Eq499(E*%T$8!y>pPMQ&G7MdIIp+CxeY5)0id$g9)pJNS? zdK|ucjb6xFH^{ZGBKUr562FsC?E{zj-59rry!a`hYhbm z0euYmm8w`YmY2a$F@UlpQWgri+JoTH;sj#tum~t*WCt?%pfeS3M z@Hz$g5ZA%etbMPpGTEas8t!ys{O5|avl%GH<(5wRs0%}naHxpG~ zh~5uT+mxCkVun>RH(=Ao9!}igp06quPmhHaXI1TArwO6p4eT{@K+)5T8#VtjeX0u? zUH|&BZjZS(RNo%-ZtjX+nYWav^wV$ogt#d@_NXAmKu|)u4c?1@|4!0d?ChfI# zYLl+e-@$f60uwt2=dC5u!2+IDqp63}4+Bm=s#v9i`15 z=El!TK5h;HYs2_sIe!UUejv6X69$sIwv?3>wnSx(>RLv~-yJ74YOMh>cpr;W7=oMg z>+X}Rk9s_~Gr^N&g~_#ryubYzxxbEPsbqj(cH3E0?Jd;Oz6J9n+RdhHpI`9XPUe@PM26oO4MVD8|P4q)hnT zb>sDe1sTDYgsa4{k)(+4W-P(FnIiWUP6i2fnAwowctuHYAWY_I2O_6emJ3&kh2T3z3 z|6r7)$W$<~1w)jp?kM#~2YqKp>C#NZ z0^o3qnEk*$x^-b!Zu}i4ue04&>Ycb|W^N!_uX$enf+`?I@BeVwR-y@>1}68={@O9h z*W1v#uZ!>TdXc_9_z6PDoc1h?X(9n0YZR%Wj0aDgdzb}l=w8HW?f8fe4p2#`*NUW* zpn4QDTnL}&)98qkgHyY=;lWlQ;x4`$%V*<&UX#7Ye<+J#h(4L?P+SxQrLnH)?`#s~ zmA5?MJk)&&ptGUI4?PKrP$dk>knJ@I`}>nY2k^n@=5VB0Y!yQwT};6bsAA}vU6zL+ zzk9R-hO^h8jeUKvqKg(8%RDJe;Ph`@KcCDbwc*8!=S(hebJ$hMF2M+fBwH30<%AM7 zvOcGRBk2gO>xIc_ig2$8dDIs>o{Cx)zWK8aw#N6=NFR>#Q2!O~Ie(J)5U)N0cv}t! z>7hL;smRoQ_5-3{<`E{@PT?3Rb5zE^ZZK`|bc32@IB*VpzcbRyVi!pdgz zQd_HZuXXg=BIjMEM%}9%23qwvoB*tg zidJi=1F&XNMkk7>3LvFp7@2M*OK3TaT-2%R7!pF^c1@9=4cxL6JvouDONt5{HUdbM zv1*z9WpTn({sEtHhfeSqek1G`>4eSH8nE4N?y?ie`n5#jT{!zbL^-#);hxMDBAXFY zTXAGXZ1FUpv0PMYHD2!dY}ndnJN#v)-Kjh)PmZn7SrsebaHzm>u=p(nDvBMYvtU-% zgj<`#x20;+sxn%=X!r>AWC|2O4>qGsn>u~C-x@=hc3)&exIz1%_L}pt%9W*i7V2|2 zqgCA%3XmaHY_gGUYFjm7PJhU3`Rkml!kfQH7WzhcAJ6j&xU;z9mrTU_ftH+ z@p@=Y(0fbNnyI(6&kgi4YkgJW#@nGeGQ@zFjp6QP>o4}dH&$|iTBYe9_YEk@e>PTO z180-}sAqpt<$P zjS#D8VgQY5ph4sgfv*&d6d#1mlp2mw2Ho1yZUG1Wg#SgfUoKkC9o-HT2qlIYdAsN4 zdHd@ukuvKkbdK=1neT?W->s-k9ra(ZuyvPpDQmdk?O5 ztl5!PWN$C%!g1hF)*S68f1{x}xW5UM_W%YF%`(-n+er^lLBY;(&LhP22#6gibWSaY zbxGpUC_DjCmCbWWD$xK&iiu&(3{z{xJiYHWFn4B^#N(@9TVaP?+oC8x>!G1MOr1-7 z1H&}AM$&|m7=qn~A%6~JKdd^BexVVWK$~ zs73e-k=4h9y+x3lKgT1}Bm8D^#RdlFjy`+oX?~{i!c_Hm)wqCTRBdnSBgWH%UfX+=j)gOOw%)#S+j!ZJ?UW z28^*Fo3?KkTE&WKBuWi|(`{LOcYmAd2IC9i#3e{)SIwV5B47Z@9Wf8%tX6N}G4e+s zB7Estezn#*8R=Ee6|tD2v9|~QirD$}h$kKF>#Kbq{*~r;0zxn0X9=H(mJq>S8Rd1= zD;qJDeXp*I$>-wuxD>`GVLUS9Y=6`2OdhCUu5PwUnib%QqrYqg#6te~@c zox!m^r0=a70tW5=T&;xBs<~g@1|^49vpqAy3EHLgS4WB7Uuc zR(XUpiRUKrt~cE|er*S<6Ibz+gibbIfK1e^9a;CwQGDF6#kq#-43>+Zr_v z^n>tY@@N^$yO+0T&sU|{Y~ZDvr-WGU`d7>po{{7VKx3Iq~2)CTsIHhfA>7CDn{Puv-JWM9H*m>F9t-DlI*Km{&vj zvQ6DVbUD>S#DoIhjgPaMQoNb&idLCgBcRAMI){c&%B-O5{n3dFC zkX~w4EbG9J-HEJ6`6Vb79ePE}e+co9-6kwEQd3P&C{->P&F^i1!|qJiDB<+m(T55} z1<^WuggaSpe(FZEPLasPeUazceDECYGsOKx9j$o|unMuPjra|Y;qFh^#d6NAJGXp5 z$hv~b?FUzL)Ts%7J`*cXSdw_&-KOW$pI&z_h&wu0j`&a}8Z=RbtlJX32-gT2ngc21 zr<^ABPS)zzR2F@7!mlnN4H~EzA&@t}lVem-m%PQtox|ryiUxY-EmZmlZ}xb_1uaSv zDZ5AxAxnSVe_H?X$;Hk;2jpd0pl^FjBpE+(%jA%2%I4x5d1J@eJAAH%m$7}bXdW=1 zIUduzFdV`Pqkw}EuS12!;!t|_q?F;VqSSfK=kplkcWm{I&qah4?>D%a zNoce{NM5EpT`wasMrmgyzZxhIM;alub#%Y{Whi%3>5_xJwz;J|ZIitbM0t-}$zTs2F^H624{nF{=nPnb zL=&yR=P%GKR{|Fxs!@XoX%8Ea2;j6?AQS;tS>p{NpifCsdS?BRIHF1cg>K>$I12+p zRNq#G_1WWGbCK%bFh_M(MW&Ernke_r(<~1}I5X)R-`t3JP;694t3Zjrn-}7Cb>OEs zhSp=sRI7k({82^aVVa4zF~b#6mFhT0V=(vhpBD<^JJDvjKk~#UddcFfr28qx38qEu z6OAp{TnJ*%j(wZA`-~cN$tPtvXZ4#y)-63UZ5NgkRNdlea(CT2y0QZZM$2N^Sw?K#CqObYG;Z~{zH6oRE+?_{H zTRe=YEZq{IiC>>3JUxf7GrEKKv|i!Fga(78G6$F7*Z1XoPOlY+(#^eh>_r?z$0CpN&0 zl~X!PK=?bmt+}yN4gp$8fDkP&I&?x}NFI)iDwUv?NyAefi%BVuQP2#@^2p3vR#*b^ zKxv49m|k8xPcXcfOhy2K7fZ&E`DU`)wzcL$xvsywe$f_BSm&k8&)%Jh-8({5tk@}A z>K}2C%4Gg0?FWQ0Dg4Lh8j1B2Xx@zU(nVNkRoS($2*=KR*IbvNKQiG_DVqFe*V=-} za|eZgB`73&2jD#r*L@ND5gZq&{S^LBb`Jbrft%|J7v?iOxVi762oVYQ}Y`j`ADg2zYioJPy$L}O;GHGds@nj|?xDf8*F6uXfIp^5A9Lsj=iN>*u@%z8p6iYXvl!J76d;9UU7 zLmmCDp1!nH&KrWZl5eFge0!R$I;zA~IlFXtueSL+Z(wH1s;aW}yhLVf zoXDT0$uIb^e)gMet*1?@+%pOZruG&~7(?#dVFfqxOd1KmhHp)qw)q^@`nazJQF8PG zlbl%DRmCp~XnyW0VSihi`MS&}rz?(^(OIz=ajCO{^Ce*_E$|m8XA;rFcB(8HNR@QnZufiES z8D*J_PTY$FTSV6zZ0btDjM;KI3e-AC@70VnIH|b9@gjb19w=u_AY`@=FiKNke(%qO z^O<~8XA7i5aElS?vjP*(4qs4d&vSaaNBLOUSn1R%6FhCdJi7yp`K`2F`L(UnlrvvU zch^#Ys%z=dnMdzyYcq4;4D3}o*`i5YWHS@H`WUakK+qm-S(CE;#Q69is(7^na1i4cc>UnDkMyC)=DXh;bb!SB=nXM3AM;PWXS=1x9Ul?joXmd zvD8Vboo&JlQ7-ZIadE6)T8@eB8z(W3aM7EGr*l*W?euMy+??=1kSoEXt2TTG05#=& z6lWvzLI+}4x9VI>S*NnM9gV*KCt5PR8^~@rxQ{&Cq=rl&*04O@q_9*yXif93`ZR~i z-jL``GAT~9t_B!p$UdHTr)pRmj`4A9TYt~IPWZ3+T^i3mN8+0?&a92w{+Ksfx-(isUAb+l_i>oexW>}wL1OfVnLag-^4?X zC)~JSF?O^BG16?k<23Z8{ULwRi<9L3gs;H0LLrRqM@+ig)6c@z-^{%7L???X;@RSC zrTLSg$g#@R1~QYB7pW~WvVBqeA%4uYlDjwl39gxAuUnTM4+PIV`+mQ!Pw=aU#auIy zXCo+JE8aaj=kD=tNkOA-jliy`(lc3fvC}dL!Hm5m!PcZV@Sxp*V$>7wS(?0f_M^c4 z4rcu1Ki*~m^;eG;X0MJCV&s?rGr za9&jFS++v+4ZlBT6HUTgAcU9)FFAj_(g^(3D+G*GwAy0qcImpjz&)n-BCCRbdzI(J zk7W1dYGXG63rn(&!QWQn!K8q#RK;azl!L3+8qq_XhOnk+V+gwJ_pz!YMWIkzq^Ta? zZl6g2y7@w^!ZBo~nErw$XB>mmWb=(i60p~>@CY%vZAPY{Z2!Fx$p z_Aaf@Wid9u?8ihznT{#t*LP*Qs@c$dB7}JFd097KOYMNlP%DF6k?4|aT*hYNiFYm7 z8}UEp<6J^%!}!TnmQ;;f4tXMAt&?=pY+OjcX4OPl#);zI$g#LKhBL1kDQGmy{%RH< zmjhhFLS~3$5^uotj%XmsHTx*=pNOwNim(M0+vZTls7GCBcT*P)g!6p~~1N_LG>fw@>s+D1rOx1JU~TG}U71`8i8eFx6tx{KmDA z@9J4Ntl;nSH^9Hw6U3n3Z|R>0h2qbH0_T6-zbu?hZ0IF^enebNY@L-o>`ng1n#xw3 z{mG(0;r-<|T$E+J{(GUI*l1&3LJ!$0+{U_8ga7k}yA;sIo?jDXyy0u- z<$@!oey*gop^94;t+b83*FDfi99V!&&9BN>V8TJA($#nmlYy~d*Q6_b&#G?`3jM(;D=6U1ot38>2BF(_J`7LN zt0@LUi1X4!CT}z#DpZ-mdC)M33L^V)xsSPE3V7rzEZ>Q~P1`4P>tn~!#PDQJLYJkC_9ORj+>6Ev>V*{i#K zd!MeoU1DX^Io-f1aM;u**>-U{!wo;K>gJA8M{k-x3*%v~u)ZH~GHIG&gRmp&G(D)B zSJE*Asy!7{OUZ;!f@fe*-6xLA(XR+6D7nKW5111E4|WeW6^ByW3Wkl3eIVdXwzKjy&i8kGOZ+-|Q zMCw%A*b1s}C6mbPd|ZD4G9z{~D~f`xit$1G?26q4Ahbck>Bz-`|q-W++-K}z+?+HWTRVnSZ0 ziOlOLd%+auI?EGImP5wix|uMWiyh*21N?~vs?% zQmRUHu0$vE#_NU>@>IT93FJUBC5S73Dog@{N0$l^;kN6Z{%AkM5&L3bxNJ@Z7n&l5 z%%536l-_zPK)(S<+$EF1!e0h^%>%CBKJz-PVwHH}T`{`p*Au!Ab(k=~L_u5Y43Ah9 zpGjoAbfE`#Y~Mfe>k`X#8yj7V8;$v6CCuPSh0Yr63hgO$1=SnmMY}&z6XNn!{vJ36 zcDl(jPh($y+!jr7ih9y1nF>C? zECaLL<^Az2MRS|((WQmWvB!SyymjwoFE^?bRU6bUZx{^y zXi4NFgK&j2RJlsP0=!*hAP&uxNuaM5(G;#IK)h5~v<$+4bbH`~QUPe>Ron2i9NC!8 zTtHN>(MAZNhGe=o*lE4f)~#6+bO0HyDcvn`V97N!@OzNL`!!rVH8MvfU?rfx*U^Gv z_Y4b!8YP@kL1qZ536SxZxRWgNS#rE=g$H<5PJ1%L7{iYXXUyW-m8FtG@o*EYC_~QGt~B_rd+Fot1O87qP)07iz2)@&K6Kg|Fs zU6MQtpEHcwJYk3)k9d5MpHsRvQ^qhqI*{IwGGJ?*W7(sXYoYz$?$=cHWyA?IHryfe_+o`X z$4`aaJng)7_t-5n1S+U-1|ee~PzOeRS}rb<(}KxB`cC)m-~nR)4`uKCWqF(=fp*!p zZQIplTV1woqsz8!+qSxF+qSE#tKRL|cXnsyop*NjhkO5m{Ny(>GBV;fnI&DYbYplv z%e8v&QvWM=1)cPnWWQRthff1ydvUZ`wWp6_o9gD0y*lzL7jn1=-Yb0gEob--E1y_l zb^BW#5yj5&?C)h|4A`<-nQrVZF4x4OW`tT_-8B_cEo~cac{QtJ+=G1=+jeEO*mXgR{q0K{JqRM2Z-96e-`!4( zv0$TgxHYCgF5z?f_xkswF=vprGuxkUr&oC-5290EPP<^GPqivZ<@PLl9vdDRQBe*= z7;T{j_Xx+B-}@C(%J@XtJR{(Nfu5@=EdU+`dn5-~aG_^FmyQCp8|QpHhGE4PdFax! zC(Faq(%Mm5X8T0T(zir@=qXApZCa{$415OGJ=;1)?+(20>6|1VO62T;Igx z0=AzLlx{)h5m}rymQZz8frKVxQpU#01sqwEl2dsuxtnB=RAqulME_@>MwW8e4M~{I zg&Jg8TvCWS3g%X0GqMRvsTiaXH6^NkyLh}bnJIZ7sib+1b*PNB_JZ909tQT}RYHM{ zxml<%LPdDoJY(T-JUxL~A2-;{%M0pKum+?fostWJxDVA#sW29q(x%0#K}&XwEsIZl@qmIe zMB1;L`KQgG+VT(IMm9)wMaw_(sfERi}8P z9xjBDgduHGp?p2i&$uOUHL1L}7*x?R{L&i4sCU>nE=jaz_SBo>pCkS%@c?_o8QaKy z(%RZdB>2~-W?%BCXbr%};+ zhB#YDcpylP*kOXH5|EeLgMT4I?J(Px%j&!rKNyb!>vyFK0sIcd&D;EPY?ScmLvR3z zYo$dtMYKp(X8JQOWvb3c9)SQYJE&I(XR>%6q; zRR3A_&74vkRq=xwdbZ!e8Va%7C-+YsVX#KlP}5yDZN}Got?oIYaccU!_hW)K5QIreVMhGLcHpU@NL4&N1!n*WJe0H z){+W%ngFSXIfYFm%!X=bgNhiS3BrY6YMze=^L{?VNLn9^maIBB((+fLsAQ$3NLfN9 zylZ$7KmGBemb^3)yuguGS;0z$r3=K!ba-s<9H88d#u(2Cp^5T{F&b^-dWNP~a5v0_ zdy{--tR#&)0!l|fJgGAsjveL4Ygav$j0<<>6o2vY>ea+_$qcg5#qp{M<%C~uUyb2BwfL1DC53Y{l2>4U_7BUHa((FO7aE+Eg@pD=0 z=zERH7ZjGCbf=IhS2X0Gl*DpCgoN9F8jiA49z5vvRL|v}{@e6+sHhF(38-RmKo$Qh zDwec0a{NCHZ;6V3Blkvg_ms2llUg)RD*(eAYKAL~R+0>v&XPC?2IpVas@P)briW#g zY6}s0py&6_jh~q~5^eG}4mAzL!Vwa{rNAe^CiqrIb$$R4H$AkYB^hf@+s-$nxe^5S zQT3eS2tyN3MUutrrCyz4fO-c*Gp?u^V`<_KjteuQdH8|lW_mkuK?P;@pq%#pbF7Vx zi~9xHYe|UYnJ_tUnb;~W7;g#kHL1{50NHVgF)EHrNWA9!B8u9n*5SKl*(4M^MpoXN zof{_PK=kZqdDcR1VcA7T`>FrQc#gRJzunfOpFXE@O>rzSNl6gcr3B)bes73hiD%=$^+f zI^R>a^Op3)!DC+7!h-|Em^~B4MDL$PuOZ04L7t&?L_LPPU8@cF659L0i*LvkfMN@5 z=cc*VP8dWks6%R!N6akv?=?}+*$?IfbYWbx>_7dz?Rr-lLsbV;8D~TAgAceQAdqvl zl^D%-1wm?(xY=oB`hQ5?$E|Og-HG+K&klrv4}qmf&ecZ6_8PvypRsX&{OS{Bqx6l1D zeU!aF9Lj_TuOYHT^n?5QH^vME*41efP|-mE2=s3i{Xa9_f3&u&R@wQl{MOG1<=pjh z&W#}y5Rp@G7{Jn86wJzHP?8@&X%d{8`m^{xi`PJ9^#mDU=gec0TnW;dX z3o|Oz-qZUX^_V8e&9j(R``nTa;0wQx9*t%TqNpnAkL9gxQzbzs^Qwe%-L%$c%nUye z*TaZJMJ=t^JU;uAq_1qBijR8+lHY^3dN&Dgun*#q=ZByBF%f%5NP?Y!1w^yx03y00 z5z;OF@VLK1+sM@LPecL!3lq_QIueW15$BzdxWX$ z(kmsDL}Xw<2A3=;6hTt67T5V!q(GYDi$y4LUTZGMcc=@{>9%4v(5Kt0=YRp#&yMt) z`A%qD1sOwi5d6*kZlWh;_M~er2u(`kb`=ran^XKI-n39XR*M0*5%@YqH&^X!sznve zZYAHYlbHtXw;K@#$eZZ->fUDy#)<0~s2$IWql2yRlq?KZaXequT@R1~-3iF4-6=dA z7L=Ty#7j;gL98o&F@{Ff5kEm7HxR!fH#u? z7#;p?Z1}&>cgi|;nF5I3?7fCZrKFBT&F_m{N!+%=R(s>C(f95lY*oN+slOSCQ~~zX zVGHEcLPBmFJUlF3q)@a;_^?aSfsMmllMDb5vq9*s9NlDJ_{ewXMsXH4l7K_F)InCY zE6$=$O;A_CG^A}r)hf{8*t%GKzb2$vcY5jaa6UV*# z^K0&%o;-9*6_pGF14&ze94{k~G7xr80LP^WIvtan7KdoewXh7E?EVkTpYjH#)R1%e zfzA+lt>U}Cv|Xym`i%!8Zy?5T)%&}4wag<;M$j}0{dV3%2pB{;G67y#VV)~4rRv?Q zR~phlV;;11q4z|hxT^$0P9t;0!MBijnP{r7VFJ;MwO7*9^beFFcJ1lvhV!Uezek?; ze;Z&mae0tf0F{UXz$5>?Kl^V~E$(RNV*kIQ>F9)h+kc?xJ7q+!W>HhOez^-1-HhsO zV6(6VbUtc$Ba~z^*@TOZ!EK+U10Xoha)UlbQQwa@Wl3Nd8&7EfoVIp=f0%fqSY3wv z?p%$#Y5pMb(XILzv>bm!#3qJ(z4v2_hZ?VHNKR|7`3gWI|G$-Z#9688)wGM1hOK!2 zmqymvwE%l~MV=NKaOTa((E}5y)iM`Bkn%FeV-Cc{l_1`A3LgQXqICJY?Lgwbf0{Kb z8uGtmWPY56k;Bl|f4#ZoM*6$$D1F}cmP@F)Fl2&^X|6{0kZ~EK)}nndeDfEL6>XI& zaMHhXlTLxyej*GIE?%uyq2iuJ7cq&S`Md3q&QW0lDMjc(K?gCFpVnG${v2xjLiV^{ zfx664V3dXV6+z`K8(EbG_RFWGTVuy|kAKU0-aC^HM%_-oe!8~Nre*r=2B+u^MjS>bv~< zq-=-51VVPl5hN|I4*I`Z!pDr9&_u_EML{v@RBS+WWKH0_c;kdPlGM~%$*UNRJM}H< zIuSz5ufI3#n@@%byb){aH9C)lGK~8ksw1bnj=})L;xUhSBTSykvqDu2uLeV&aRd?ja3p`bAKB;*eHn?&8 z;EU;TcRnD4xq{EKrZB?V>~bV$5bmtC%WQv8DjDB+_NMb1JHx^C4$ z0sZHsrmw~2mfj+Z6&ENNC<>_?<+;!d=XAPjwT(^!sE4Cv|DKJQTI z6y=Hlmpsoaf&Sq+xAWQ{<3jbnZi)2_WF#IY5udd7u525gKsX;l9*GiaW>+& zzCBYd$_V#N8$RkgH@gSkjp?2T5v z=Te52cfx&$HIi)^!%RncO~Esleda{%CMng~HrZR6(N*bu2)~xy`r(HJN*5v{G3f~l zh3W*?SiJgsw;<^WcDXhSTh)$4C~FnuJWV8oBgs#V70E$Qne5UU*+ik1_J5T76*RNj z>c}Yx6Jfg89|d6sC|jL*Tj*ID1wz!$+lEHhXQDo)F zOx|7;+QQbT&^%B}clLvCGFel*ESb{azHT3WnxNlO2850Ob zE>fv#rmugBe;?)I@rKSy@VT7A5gGI?l@IkE(c8z^L z=_kWD3;)O$@W0}mm@|(AWq_?86WzCODF5`ZW$X-${{x!lO7lV)hYR@&)=#wd#nS2< zi=5-j(QZN{2>FE$1SL+wgaJF*NJBMa3V8zff%WT7tyV_8S(FF(GkbG>06~V@ci%q> zm6FxqTGZ+0OqiOGEo!@KiDq9v0|9v!iwfAU8f2xwI|#*`2kVeL{+b zvxXE>ER~O-n$^`uns8r&(jwZ}MN%fQR&~GXa_S&=8YooVr9YB_%=SH2H~K5 zFEF6|*JLc-u{1NS7cCF&IF04@6wUQ6-`KLnXMJ(K+kjtm+Zn8#+Pb2) z2iUdTK8PfOm6c#}0o-f3vr<(~X=)QB-AUhxg%%D8O+es5g7b;yh$Y(ap@enW%JgGb zhYU$+8*R@LboQ8qywBZ-&L~kWojkBy6xIjCT}Oo2TlUa9)FtV!OwZyK_AEU_9dYnm zw2p=sBhnxvBTw?N3S0IVcrazOgo|NUG9w`=SBbxPNi{#3MKS;^zRV*Hb&@jynCx+L(16Y1VOUMuvX%`a}JTAM1|Hj;i)z}ln_wjK? zEsvgJR+;K(zf=W@2pn$D zwpU;r`>w8}bQ{WTlA#~O2BQxfwwH?tS`Y;5(JKVlU;ZZwP`q!(&2z87j8I$Kxc2TQiWuDZ<-94d_T zfLjaYf)m3$TVf*091u?+vDziB6A&6dJq=wU2>Rq!GoBNPO2RkH@qWCdE4_Z`Ugx_y zY&Em~biRVv^ldZPi)l)#8}ob|sc`HHUq-u^V^uXh(;sx>-Ld|Su%AEDpJ3`FLpLt6 zb|LZM-ZNP{(LXL8vylp~EpWg5*Y~KG<+b}(WRxo@UZryH!?+fi#KIE;!QSe&VAVFg zXzb&e%rbZCW;q!`p0#)FMCb$Z_OP)kR;wIxsty~@q|@}b>hK~@4*X4#W)5A*NA4BN zlrEqQu~gPK%T$bDM6#9kZBDx3%K9=^Htm)+OEgWfH=&JKQPNmW)rR*36DJSa2uJZ5 z2h5(g7#<>eK6}etQky!d8E=wY^eWc_vIYa&sNfP8tMohZviCDi1C=pzzQ4Sq9jx4p z9Ri8*<1b5E`n(GXDE_)#JhB!^kJJ{jmMzljr_C^3!{yJ}eQuZTbWPq;TwT(QALSso z$_2zTMnus6TF};Xx`AiTh>#CgMJ;xw!$K(6H>KNcCbmO^lnvk&*7$|ni8BuRtfUzY zhZ?1UTEXHQyQZ-@8bI>^XLMsF+vz69pGBm~Tojl0$_)Om_t)&$e6Dh(=tOrI9uwckR4yt`U``QwIWglS&p1#X?X)wx}E)!DV?I2-S zId#`sI}9-p8LXPQb2u(yLf9)-sm`^rqr~TK|Ib|C`@E2WMLdl#RCt6*-?S z!+AXPX1CiQbGumd2J~1F8A<3co@z-OUKf#_9oGpa{nPy~#q8bSAKW&nr|SgT)L_5e zb*473-}{s62ue*Qun(2id5}G-HD27gwnip6Ks_@Oz>fJ&e}xYj^&nIBMp*rwz`b?R^tJUejhnY5kZ zK8lX=X{4qofxd!d=5jS?Vh|apQS6tKGZeqmTX&0Vv#0TIlt2gJ=;O1fNfeik<&`;b zUXvX8xJ1f%q&h)dj;{(%&LpP%892)`5UxvZJ6M|RKc)pd6a(s@DWnX}l1#i!TeCx0 zSK|lM1`%@$nvsph@W_VUA1P!7PxciRG~LX0t%)l4Yy<3wWRJJL^Q61Alt54>@|Adj zRS@2&rr;Www^fTTQZF93o6-(!z<6J$N_3CGx9daeryt5x(xT#wcYm)rjqHeC^VqN2cNL%dfG8FDN3b^%7|l(|uCq2* z@n)#>{450YXJYc(#)^TOd!jGmUj#?aMIt>dx9UER&`!$VNj?{^qT2Polh|6D^&r0- z0?szLv=YVbEK5iddm_%huDow7(V1UOO$mNa!V`3pe1+h|XgMPd>931)De~bb!RKC? zcGkPpt=O`26Fj?W-P|yrw%MTEcVoON!{XAXd_nvxKAk!*0vQ9aAL9V_1LvRcsfwMI ziLHX8oso%?(|@9)16+`00WQcwYca1ZP;7{zJuT^rGgb0+uEZuv6jh$|C%-LYXReb6 zsr`A0fwMqyX+{Q)-SK+ZmPzQ-bSp%1InZzC4~~3@bh9@{Eu&R^st^H*`L+<_;5kV_^Ll>Q6cG7} zo6@7tL_jCUz&BLpWxfU}#dAuB98#wM75-Bmmj^-!XoW7^Ac6#TfX&}bESzs-Fe)Er zv@B~bIA*&dHMnn+cKr+cq~k@B8CG~QU{^hpJ6^e5mVNErGgGgZXzrnyhwLvGSBMdU zO)sXuO*9RiK)v>D{aYp6eENd!qka%dg|7cnGESNxf->|)6s70Ocq1Tq_-z8%F`7!f zh|Bm5n7_m$rIz=Xk89i77TzvD;P~%~sB_w&HT22WD;ck0D`lHsu*2sk!`ibYm_!T` z0>8_Mn*g&YP66EVQCKahif4i`&tU;_mZq}HjAfAKf;r_mBsXhy9lxqHurvaRC=IUD z-ooFFqJ}ptW`mtkK0_fT7f<9aEc4NZQR*UEI?=PU*RF#dl;YncJK_kP97QS;?(Ed6Vt=R?_qLfP%uLoHc~c>8qe> z*+&}gJ7D?KPw;G?F&PDMw4pfNJ#)fh*g-8q`+3QBCbpJ!eHj?B@D_=W;u$p>uFvZ7 zu=N)(GACGUQ>bsMd91&rz6&PL+2kFsE4XH>72jmOi!6`SE!pa(rW+;%prS0E=A;ib zf#Wak;D?RXI$|;&;7z*vyJ&LsR%ATnKw1dMb!kvEYQ<_MDU5BiOKWcV!C%cig*dvE zh|i$22M86uN^!}qppIF!30Z}UeE;1sDeS$|0|H}EG*aa)(8n&c*J8(aa&wCCudm}| z0(+?k@G|TH3ci1)Wt1&!?5$1y=ClHS&i}z2*9~xXIbcWik@Fpn<$qY9Lw#O^!O+55Is8C7U6(*hIHgsx}%l3ff0qqCTPJFp>fTa6MGI} z(l}^*zDj7=ybo!Y69dUdmBj3p=D-3BV-E+?eL#`T+1YUn(MzXK>=Gd03;rwMTV@vS zD}{7|P!cTS_=kXxLg)NKg_6j~yN2;`94Z4K;FJ4taV>&aLp+tc*#5C)-n4NgviT8B7}|HwH~M+N?cD|k-qBBPKX@Hdd!-*jdfNe zHQjgKya5#miz0myrHxi|q9Gff|fV@yftZYszEmNsT$)!k+;QB z+s9iewk6R{Imh@>KBQbnhj`Q%>5KlZ_i(yN{!>DnOUxmGwE9<(BQIAn@bY)XXx=YIhM!DeaseCt3C^H}33j4c*lZ~LT zCshYkHen9>#V#lBGhRsZQey4yhs}>kO=I+n0q+b0Gs`BU4GXp&24a@Ahqo%9=dVX9ZH)Qv9j?HZ^>ry#*;!vk*!)^$k7S7hxw5VX64l*QP|-B9iZ{ zitD=~>agS3fAH+AE4KAO#&`QpZu@#`jT`R+UE3C(0;$u`guXTfIA;dG|C_A2MEE2c z2GA5&0p@i7b0A{Jpk!hu>i&P|iPfso08yV{WK==eV`(r^pdy1w2RcPh{*a@Knn*jaLHriPU&8W`vK+_=3z|kT* z{$d1L8V}ud{Z}O zm1_%GVmJESU!Z<7$1g^Jb~xSdWAnEiY7tbo+Jh*f0UP&vQgB!^$;e5G{_AfIw_4sYRUVSZQ1g=!4?<1k@$#V@D>isf;A`ZK6x{2PLnbT9Qw|C;LBa zc?qe z39UtgvKCn~ThbZ1Hbsec{`P?I@Id~gh`z(MJv@YWj^^+Vl69siL0}pllSl^T+{^CV z3)_G$3ACZap$Z>frs+^eqE6Cv8z~Hd`5|NCElwQ60`1|OXpDS$gTuscI-khK@8z@4 z89@@kcJAfO@~xn#z}K+}_fz_fj}%$*CJZSxH^I2AnxVkEU^xa@YPMAAo5fmSD|>MKpv#<%yMq3fI36sqef+&q*a(XATx6s zggP9yW5nC(52mlbF~fC-8`nZeNAEJV+325>0%R@z0DR!@ISX;BZwt`QymDgVb!eJ9 zU+6x+Sq?1{Z|J{fiGotb`@gpf3ike)uURJ@y((lh@}rF$%@M)06EV3ftqF}Ge(ye( z3^)QZc>RSmZT0bQmKFo`A4~**WtbE|PxC)IM>_^(X9HVf14rY(Jx18v&d%vS0A4Oh zihmD$Msoqyt_S5Rv6{yPrNPd#kT(wL!Qrp) z&6tza#!RkF_?lXXg*W%4zDw+o#{IB;2p_bR4 zCCUp`(kBZ{=0JAPUWaHTcGRaABZPRHHkHQ%ZNu`11&$Ybcd3xzWi?EGyqDiZj|N51 z9F$nRImt>}gSN{k%0xgZ#kRCOLYs&wymaaIscIHE-W-)N-FW>~)%f)_iTr~Z(9fld z$*+}@6fwgFstoSXgmm#!vSEJ&60a7mC(0(T{exl+`f7Y?$*Sg78Ox%yNuW;G!PZ;; zbU73k6X!h-PJ!2K0P^=PIn%k1LeG|&a!-G53!IVhYI^&f8vc+NHo#<*(6{QD+m>ED zgTM#s389X#JcgWDA(<6t(+hr%uxkjMfV#8hY8_ zuwqZe=wxTL$nVzD8@L`}tQD2QyuR=#MOUkRKO-&v3b3iV4(Qv-Lt~w@;K}2^a7kTT zzUzami1vi)x^8@Mt2P1eY|yc}9O}f+!4vkg!(YJII(n?ahIU>0%)n<3h2)BQ}JIF5M<+1o`$wf z8!xUQ_6px_vUPZf;1!;b%}k%WvpF7G47^9EgQVU)h@AHxVV5KU<|u=xA@=#jBG*A` zd8NDAaRYnBo;rOYXMA{YC zUwNoF?Z>KuF9e)~SBhpVkUb?JUBjnwO!}7~qj+YF#bLE+LdP}gqRxA}UClv^lgRO^ zT^5l;E1$iCuM_Afe|3Tu-Vn;3HW;%8)JC`m0FKFW73E}z5X5&cVUto5V#ve8uZGgV z(fn7;@nj@5%#lvu6r1pD?NO0ckIucnmEIa8QlkD4sa}*=kn~n;85_1TPJEGlC#D)1sRUR7lLu?TF(w=kXm*5`T@vU=t7=KPh zzkAKuGIU&tiZXa!i%#f1mSV_~-%w@CEGxB2eN-bQahm9M-1!lm?l8;lS9%?#ZdElZ zdgs1XTsm9zESm&rm-zh~qD8BIEmi~6{SKgj|9|S1MvO)5%A#ZpSxY^`1@n;GNfzjk2rAXn+cn^0vmuMDj=jNCVHpB2 zilCon*zT)QyIoscAB?U?Al=cDjUD&c$5*n#QG$5Ye93`EpxML;wbn+cTw<0&_An3) zp4V;Q_ECUW2UU&?_Abc=^M>&h8x-}1e~4quY%wiO6)fPuYg!Ftht2*`Ac!wziCR`F zqyav!kfx@ufB_vd1dstKg$IuODKdDDN{CU?H&Ok}g#3l#>~3lLn_V(y)F=wX-da1E z)u=*VZSCGOTdx=SwER?!W8BOBTwGiZeEa2Re?IU_!1^vy6i8K80v|ij`)Qcu518AM zLlSN@WB9|hxt;@zoIip|V_&j}D++_OQ$!PHw@g!w-bf&kt`Kf#u9-D%YcBAa zbhI27mDZ^?%MAdtuy_0({R)j_BH)lSAiS$*h3@9pA8ig}8>uk6(v_wSw{A^!g2zS~rC*$Fbex+xB~{3g zI8$>t2_TYXndFC#ouqSQ+SZJurN)*rTPI@F2lprCPRIW9w5!7}&=V?kH@h%13aO7i zZcOx*fBNI`-`p?}i%lIj0ChSIPzn+K6Jh&bJsMy&Y-IkAM*W`^tcs%3K0Bf>z;IYD zM~1w)Hl3fXTMjaHdd|@vY(CQq_1SQ7H9;=Hp)K>{<$8R?e}EArL2bu(r?qpOInCUu zu3^(df}Ety$L%*vl^dkrdA8%NVy2m7ue{Xt9QPRlBYR1Ldf&-b|JY`~jCB*5?gq%D zX(H#uDGW<`p(E;288DqYFfLB~g1nz2$QNUdntf(ZU=T>ndtv1qo-nR6NHgcpd3@~9 zAR;_#li;5WT-XRof;CpeBq}kpQoS%9o=HheJulbKP4?Ewf zh87!TB*@bB6pf|e4r}&o-svB)-;vHf=MyVtx$0i4<5NKG;0agrwXMS}$ut4-HH-M^ zF4OtyWZjB;@T~23HFe`OA#S^)k~^~;JgS8Ig>Qf7FYH?Vj*fuPJIjs^gE)qTr_tdB z?4GF9i92sTAMJiasKnuqL1#XlNsA|ls1v=t+Ov;AI6^KlkK18&H33!=JSm(iEutL7 z)$^DHmb8iLNy!Xq97W=!3{By9Vn%bTv$l#DNOV}`F-_0C!R6o@R`SSP5V57j^2PF% znX>Esv^4XJJ-vDiivyG7m}<08EDw7(57YXg)W^o#9QWCy%XjV&tF#2{Dn*yG_Ve%8 zH03#af}Gk;#BX?4G)Tz!U1;O+BN^$+$ubB2a1^Raid(=;qkwa(8MiJ5KwQR2d1aV@ zGSq~m7YCJrl=OBxM)Xd+M>6}X&RVVoL!@kDKDvJw^5F-C7%n7Ikhp4_Q^s8^=YB;T zW>9w>eN0rcm+sR*K0hovtQk%<4XNOeV68;VTjv};UOH;MG}0Z3ges)gtgwKkb>af$ zgCJ)Y$3O)35c0%mYTbh0Yxt>2uKBSboXxe@wwYW<5)!lFO8NJd>?-cgPVGWY;$d13 zA(m<786P8Ve`m(9;1_X_`L0MAPf&EMKk}TSi6W$-y7F`aH^9EtFo$9mZkVQpvvBfv zt=G0RmC)>%lSTA-NVOHhv!<}{-x zj=)*A#ncH%2To&Hx!Ift6IZvc`Ar>6lBE?m>=lxAG-ucb&X=GGeF*&AGcGrzIx2T` zcBt8;u!cFWTf};)1;sod7yK`TTxg;Wo|wTgoa?MXF4_0k|Gwtg#4oGT0d$Rne;cX( zt84r{NB_^8Mzo4-?79RZ-+`Xu1iqoVAxk0Olz9sP%&h(^KWkX31r66 z)D&R~IYey*0m0HMF2o=!(Fd~|Z3mPPRaLM8bX@aBm_~5RK@Zk!y8SgPnXzlFc~&OIkmm0%aQGm$A~5t_w`X!w)}j z1yoU>-xDzl+7Rq098hD^4xO5Od%e{MPsS|SG^z?UU?9myG z4Y6QOm!{DEa|)s7FHYuL#nr%!Pz0alE8=qDv1fnUk4O%cj$R%p5*Ku1$6StYFopRB zHzh5A8PbhZyph-=gYhbxPUDI0DVnZ;A%IIYFB3P>Dzb>Q7{VlebQ!+%`}#>p7sezz z52tp8S>ri=9+EmI^SN|+qccFptD3XL`&dJd-0RrdY(oHSq%F+4D z*N=pj{8H2iy+V=6#6j`*iR@P*nv+hx2uuCmt^SKjF_F;Tux69_rb&X>4B9}acyTye zJ7+Lx8ujGyFbR~H)46t`fPOjkt@n@&mS^d_`P zl}Kv$%S1na!;=k%&rLDvD|GR`be<^4A!lrqICX5o$ex@yzdhe$Xx^lMHhCSKCE8rG z`QAkN>Yp?_C$Y;Gdr0TLWUmC52Cu7^4t2z!M|vcYzRn9_So2nBHkyb7_ctkm`Ho~` z`9OIt&zgLsl;rPPZ*ueWLL+C}l&j%k(JXp~ggiGbprL$6--NUf4z30&l<9Y#ZMgXz z1-SftWw%_7rc9#2HLqLmrY9vMaeloN&Cp9cwC5bEfQYH4NwlPV8Y9l!p?%2Y_=%6O zl^)SeG5XcCZm=sWPR*%O2B$#dLUd3r6I9 zE6w~k(2*r@|K}n8p=*PjAY&vB+f0m24#cKp$NY;^7VZ)C5Q>$GWIpR^>%pC9uTV3| z#U>nWd}``onR`MH(tx14wRh!dfXR>lTzS;Wpm^fQ83frc864F)&hJZGktitYg6)K) zM=h@~->R)(Xy^%L#6lLX7rfInw;Vkl-0@{+IaqTJ@L=ssel=rZ>mRH}`TBu}Mdmwl zQ)`PRQp9Q3+WTZ}-`C$TUF?LU+>{^s4-ybHAcCUTa#D*4*enEQ9X1&pLU5WzBL1Xu z*roi&U-l9Tadj5y%4m6?w~5GHr{IlCd;Q>Bo->!}#z3(t(bb1E82W>yI~V?iO^v^* zbY}geKMrcTneocqoAHe59pisqS|xz`Fq3fC_JR5KZI}FCb-I#v|H^pm1K5tgzkg#q zj<7A9P`I8O|Ih?|ud}gVmkh}cM6?uw3^o%II#RcEjX-eJYGG?C#+Sq!W&C*Yxsf~{ zWE!>pKb_S0e~e9wow-p>32q_tugGx7`!tCfL`je&`gb8}ogd`6+A z6!PL2g$pRJo+I#bz6Rf8<#OQDzC{D!Ip|;}eOpa}VWum;)(%65QNGe>2>x}bVoI2i zk?c*rDidf$85f2&KwBhQsi|)tVD8u+7sG?yLG)uC46T<4*P~scc>BWDAHoLrceI`Z zro_!vvsjWnN60toOs8TI+XdmT`4GzNNe5M2Qp1=Hjx(aVVkZZn8!rwsJNRBO&99X6 zO{{(loD%1=eQh`Q8{O2cz^mjFVqvhn9UTV}7pRJFDfTRML75lfvOviGeureffB~5gJB3u&Y%jFr*>0{+jKyr1F5n|+bvT}7jcjD>iA522~9%zjs znwAV9V~``+A{=i6QbRkfrxP%EA;dMqp!#q{ThdNzjewp9#$UEH7tPje!z1yBa}qZY#%DDY4fOWPXFXa?_tnsqlbB1%kY4OB z6WDI;JQ?io4ZFRhCto?o;eVbK7+U{{JAvH6OiOCZ2$s&oh$qQF!oM?m9}fPc2#WK zwry5y+qP||V%s*VVxwZ)HY!eLoIStQ|L)!0|3R;_9OY=fyn76dhG~lg?(pN7ZhAymD1)8XN{VvlKFf! zqFcpziA{CTU2n>*;)}d!icb@JBLv-#OhYamA2ssO^$aFXM+nSKQsk1bQAC$}rofpD zL2qdbfBhBv2!_>==faeMzA40w2di61QP`0H6Acz^fCAHH(@=Q;L95Hb?%lr)Q~aiA zlK?faK(Qtb9%YTubbbwnAwiiyTF862>FaCHf1bHQb@fO^hc5OiD-ngN4PfWUy+PKCTIuC323WDCz_7_s)e1pUioC+6GCrUQp zPE)jV82#~ld>yXwzFKC1Gq(fi#PYQN)1p-AS6WB``g zEx!lLiFm)7<0^|_bcb-aBLyZre5c^5$qnb+28^*$SLiJj3*@-sI98%F`09CfK__&M zC?ewqHA=@O2(l4{;GHz2!k~~n?et5PHaKxEZwDe*{-Uoe3c;x6+07q{;qX&}uyfqlIyh z&#`_HhO&~AxaF=L4uK3${nE}qyj7vmQS*F;_p~9wGExJcbIXWC#xl3ylrPRI`j`l( zFohW;li8xwGqD(ezh>A$@~S$QdhS!~7kv*gMeYw#rCLU%g4IT*{!tkPCRZ{~py=Cy zT)zW;fvQ?(VPTwBU4l8ta_kJ(h=^SVu1rk<9m$FzLZV5L(76DsJOn00_CEV&|NMx( zbdH0IzA8!*U*MCyi!}&!9@LjC02TT!SVIPFNLZ8LFd7UZBs_lK6~!6LB8bCFcJE!p zVwKxrk`>C=4L~RRBbd1KnJqgUoiGusy^h6P1rIVESl9gU1k+XONodkD#4&Wz8`90p zDl`YF{93H@l7`T!B?XVH_9$*3B@}K^iMlH}KoDr?Y!z0>0fl|Q*xoE-NAba^^Q0Ql z5~RtmjQoHT(IpAe$99x3iuZ%6cY>7fsuSl1I;C?*ps&WYgoPiO;;cM)o1v*Z1}IX}izb!ayeu%;M{@l;=MGXsW#ytGaZnGE7k#uiJTt=P8e!kNXWm z0=S<%&r{2)CrDH+4Yr#D7wz#l)A#Re&U^G4%u8JF)!4CrU8`*(hG77t>PhUT2!UKy z$sN??(aaU1(B9pkeI)A2k1+8P1Qd9heLxUYtq4*h@}K%UcGcKsxJCsi$6Pn;gCb4H z9suHZ`8j<}BLG&?Q?Pm?R!&7>S)2KmM?-2+cKUVeaMF6{IT<5C_COl*tng(xM4!(@ zl$Th6!;4t9i9#fS=5PeuZ3=JCnfCaOkv0cUt-EpqXGZ`2+J^rSN@0j6v>sOHL`!{Z zYkt21pVwXA+mdI%pqdG69W+whkcf}Kck}FSZU!$osmA^>*1mRWq!iitHXL}812nR_ ziD(O*9l$cZP%9FrTwvmr3|8RuVkN2dX~4o?P5d+FQvX?;S)Sb8vRk$whmZ(L>!deN zk;y%7`VtRqmE0xH8Aa`pyFBh+=4OrE$5#H4F`oLrsR|lC7fMu4$B0KMx(VEzYDIhG zbZyCYo}jjAoN>zRI%o%m1Ly2=xs+BP&nYzdKu3DV9f=fgo00_#zY&^>IK{Q4`L zQrfAM`zN4V=FR8AaB10U35;)MJm+)qJ2X}g+=!KQM7wdvMoU7)4Le@%d^3 z{h`e3C5loSBOsrO?+bPApn$LO9b*Af)+eoBu)pE)!FRlNKK_SibKXemMGiRTIz##| zxF{WSh z(bbc2!smVh_$7Yf4^i70N$bg+yyjNF`C*M47ymJ~BEoau8@k5~=pRZNlxUx1_e~f| z?K?4JsB51eur*Xf%iM*|q36{8gEMT)fX=FHXUIq3{_6K0)EMbimsmB4TGv<5C){uB ztIEm$EPH2)z*uyGgwikU<~GYuf{d#*5M@V{4k10I6h4~}6$9G))5}Qc5NQG(XYej# zP&=b^Nk!_XqmYIJ@Q30huQj{-f3{un@p4`+bP6RrM|!$SGc9H2RRieEOmy=Uodooudv;Dt<`XU@klM`@qNvc|qoF>mug|0?Mm zAw$isApt-}!pb1i=QfPI>$-Oc>5FYV{i}wZ@~2vpI)RP<9qDT zP%}6~nima5>90tT1?}NrCF#`%3rC-vpYJffCf6iMfGST>#HveG^Gvd2C8j%NXD#?K z85vFUv^zD5?Hs8ZPvubbWT><;F+WyR!RJqEtFYkAQvOq3sRsZvEwUDPCf#$q?#EAy z4|%2OMp>fiS$Hg&@G<6b6|3I~CbO{+q2NKMo9-avk~V+YRXR}7q;(f`ijfmZvjC-* zN|U9NTDyw-uBV2{{uR472{ogrWhj@~-0G&-q^^f@itWGM z$eM;5S)1xMpl2fj;lfs~A`ii1l{~Y4$tF7q+acy1nWfDsnG8K$*0qda*h%BRI7QR& zR-}fLmFvKs4-&;S!V;Z~V+6HCjA%a6n!{-@nq~2XUAH^6XDqKu^l)4}1moT@Uk=c_ zi>p*^f8FwUP^Jv@ z3dD_XcS&?ZPi0`J#fK~ zI65Gh-;=~IpDGe}DcGB#_H&U?>TLvy+yQ$*5{|Q=CswAE^7LcHkw5(D>peBC{(2>- zE_yt1Qdo2WCqGnyx!Y)Zgs41`jv0p#BVxX8g_@J12E%$lLs*8p+x|*w_}UptEDGuy zH^8`AW2I%>(-<2jovk@e+I{F{fvrl%MEPV##re6s84$W}le&N^Nl>#=j&WJPxWysj zvCWm~<+Iv-Pp0PlxXL$onI-6y=NT>3nUMuOZ&wd#C%y4PpW17CsmP9ruKE~hC$&CU zy-g5T{`|e*$AAa_(9u)QZ?bm^=Nh7EBpkpcNvL!kyQfb3eH%$%SPuCT!05TFvz6w> z)WNCO<|!`GAl-0_{SB0nRFg8zkVQ`0oVwtFmp){^!i8gZi5b8qx77Y8 zB}!Lx{p)!8jM1q}mw{9rH7U70^EKbbzha^eagrTp^MzQr1M5Ge zSgd>!I!(RVdvXW#zK+WTvnbM0v3B|l`8p)a#yo#iJIoSp4aS?g&OwlkR3Hf09H1k~ z=`YvT)@Bp+6em=(lUFkzaVtn=(b1?Ap+$*{CXviqO56Ks&?8Us#q5%MOfKnsokfZK z@>DX3tcqzn4<~j~9IoYr!_a@E!J*2$-=|zaLn6cva|DBS8bUe3*#tZs={mLtR%#b? zanO(}jd#7QRpzcpYXozj11OxMUonu2Ao(p)vy&AI**+7?M17npsC~LW5doUz`;tSApSZf-g%P`4kj8>F5az`NXW|EQI0%)hTtUW+$}}#jquYC8x_uCSyIaekr^v z(nM3m`V$Uvmhxt|H&8Y1r4F*$!`t6+oqrZr*}@?ClF%oAW{Uvm^kxZW3Y({b^$|_U z!Mb{`3P^%}6=eG_hai0Ss;y%|ai*a2#@#Wo9e_SxjOeXVaew;J;oNx>kv#HLw=8;+ zC?NKI3;~4fS+1uShQ6NaZ$f8oT|nN4#GC^m;R@c4hN)H>7oA{Dmi^(}|Kg8OG;((N_h@KC>i_IXko?p9hx!>4RTQE^_}QnJ zR-}|1@>KrFM-`od(&y6`rBo=X;yL^c*uj^gbkBmvO0o|^PV^YLzUtxqnQq3k_&7YO zW<-qbzW03kERlf@)q7fGy<1x+DC4cKb_XFx`WFJit(J^oSFh2^bh}S;vKd)D8$M=a z5NM8oG|4nsN>J0GpA97M6Q+0e29raq!&cC6%pnjSfXFR4cU8bLF4mzMf}pW0v`b(z z1(m5py5SgE53V6vAqOX|!ZV_`+j|ECnj-|j;P_jEC&BS6tXenO!Hh&_-!(LKdMAH9 zdgsDmI#x~5sBAZl4Hxgdn0kMLg5x9TsljAJzHvQ6mESQt&#DKsyC&slo>}vMOu@fzs^nj0KR}!{h3Y%*J1sX=@nnsAOC4RSJ zYOs0=wsqmjnit*@~N7&~)h#WGMd`GAIuK|Z^&abJ8#F*$^Euo=Ww%=qP6>b59{ z3=q+@CRA22VMu2wU#wE2RfnRLs|6C&6^p6O2G#7S`oIUNw4`ZkQ2mlMa8QTx@x!er zRL_&SrV$WLxJv3&VO#`8j;w1{H+{(0MHq1~W$7ydM$cju>61oJQRudv4Hj$VphKPf zG3fD`3tVFZRLc~8(ba8mqeD59Q(b$6hG-lTeX_=_`urGSP)fl4?ox}6vVx%ZEj>`T zLw}*RR5wQd_m9xB5>FL~(S0f|E3UV9gi)j$n_l@c;HKlZhXqkx*!eK>5Xu-iSxxAI zN`>~&J{60l&cReK1ZYa+qF=Ht790#vt<$$$Wbjd`5->`{b;%^Mm*J~ou z)=(8pt?}_7xwzBX7?325{DJ<f53DqggpnwG}$qj!!OfVq1M7=bl>7$oq|sw{Nox#|RA*5l8x`X*=ryDs~x` z(Q(xKy}=@zM7=p`I@6VFl^(by@<-Pu_es4PrH%xdOUHBa(9Dj-IqgUO`CyOo*_X4& zokJ^AhP6N9q{>&$gv$>mFfDKTsCz}Qa=5JXlP5~&2mCE)zJkQB^R#>itNl>A@2v26 zG!+T~H=A(k59B^EeP7{%Pg9q%RgjyF`&N_4WOcKiS6E+bJ_=SafG^`|Zj1Bbrqz?_ z$~dhL8aeo`^DwEaA&W^``D zDP0R|e zG3F`0;N<*66amXsBV-O*)K*S3bv2124~H_S5U*C1TFQUS^skiFxGXq}h(!{|c)e&Y z0=ZF)0)DubcaURHD2_w*HK^2c9NSk-cYOeUJZH8X3CP}`Rb$ znU0lG*SMPZY9)-|bRCpSMT)cn?I?I`nW6?1k8P&2ST+5#{e&E?m)M3RBj=+owTa@~3tNe6S! zw_J+f3J=V=dU#2pCKaT;i)rYAWx!*d5H}ZU09B2w%-9l(3T>)z76XowBn_Ons3`K% zy!yD420qj=oJy6n*WTb9Vn-4dh9G3juSyg%`F$+Bn8ydLuE>(xD%mn=!HIY2w#ViQ z1^+I@^4sTKqB2nFLLqhKR`T`*gBj^>vjcoGel~ItmBT%EKf?S|%K9F#odq~!36Wp> zm36Drlkw~fRcwZ(wN5rnCR0Z|g{W4x+V4olAv&lUxKU#bK+D;h-*q+&9691H%zvI0 z)Sxka6wbC!}(fvzRaKs%KwGZvs6eG@wh}2cq3$|sx`0d91z{I zU>A{`aNF~Vl*bB+xfJ!Mnbvd6HH2>XuMJqn3_R918T56-A@2L^07mg6vOC{Z?RoMi z1n|RYO*Ui?k$VZWxaJ7G%7q0*f_^2_QiqyjjH~s;#aP0p-S5r6KQAFJEYuhP z)V1TowQqTONKvAf9X68opR^uUvedUuXRmXW-o7NrWzNP*dR7V`F2ojcj{)niH35kd zv}`I6iFh5TS=dtsHUK62-XlQ*{#YzfR66XCXy+x^qjMAk&%B_P=u?Vi3YFmAl)YY& z3mi&AzfleLk=!c|a_W*DM%RJH$;xPO?3pi9rp#JGVKKHH9>(g>av0o7zsiV^k?$W( ztdIc+c|b;effCZjgax(kPu$yK6ak-)|6%flkmTs|6~GM>cs1X_TExedK__qM&k+E+0oqULmVi;0+JpJNCFS%F=!R&% z14XqjUDQI%-Y6{dAJ7djD-+CUl(FWA{JgUoDJ;t_7SlWsbkmkO5DRP=j(@XeJO|%8 zvI}qnkaDdHh7n4fXElxCjEop*K)gq#c#%+?)qZ28a0}CJ7M@@$niCw@_bFzIGDz6Z z`mPHyg1LrRe&OXTI1Dz}ZD2nITa2d|J!)DHsnoZX>E6J)vnBC+Y81&CvD|_ilJBqTwwX(apbm8tH;wg4Tuc7p{@E3%A_{##6~=`> z_(m1GPzl){+q!bGg<)SjMIqgC%M$hofpKw2vpswfsz692eFenf=*k6gF&`H~+I4}Z zQJ?!a`MFZh!9x2yQjJ-U59uG;%B8`lftBtrC0s(WWol~zqmn^j5mkO_bGd%h_xetO z#69WypL2HGy&$qf%xTpTopjx<{B~KoUW{K8s8;t z#yVs&nHUVgzGhRBTYPBpBRWHZex)a4?h>*|ki#?tB+2HV@>f{AW55581hb6cAgWd1 zZ?6b=?n&`~3+n&>?@!dh&e7=KgDx$p$+`SvAS^bR8&Wsx7YmK`i-NXE<3jie`wLU!{F``2c>#T^4MTgrmc72xjD!1P^ z>_(|RB@D5{GvT32%{gRPT4SJr%K0a?uYRgJ1xDSF2oPpNz4z*k=GOUuxWXcAHa1?3 z2G0e8he1*(;kOXP2t56K*f+6k6#m^~-7 zN5w8g39WUlMk*KoiG5%bTZk#2&`uN)EI;7~WZ6(m1<;;Eac*6?cgBhl;n^g+-H$K2 z*N+rWj9uQbC)VZKtHl8$yal`2Qf`XC`eDe8g;iY|!R>@hAgm2e)pH&ABq;87-K) zgXUcCfVY(jl&;>0gJT-cYNUNHl?8IBQii@G*cHhJpmOvLZ$N;3Ywrh<&uY<+ZBM2+;7+BnrBVaJaoGhjH zplQrT%$s31_F`Jc9euqi+=U)n6*sU|V%S*qs#JID^7~T!#H?go+uq8is(-IBJ&)`hn}yiFVNyd9)4e- z;5Lx0ybS-b37#?@_Om3-K9Md-BtHResa_x7ffUa6$JK49NK2m0NZ~tT=*n5_@MW8z17R_ZvK== z(M*Mqrlt+Uy3VH8ot_uq9@24}){(dJ7pIbX@LUqknKx22?5J5q&U`FnlwQA?u33^D zc{A8a=n6CBCg)yc%56CHw@~#r;>XcMf%(-B>3X)M^w!lnn?a(5PhF9482H++Z!eoJ zX>us12Dt=TQLK4Uzw^tD1JL9lQzBQZmLp#xenSa{R2$}ZBwO>N+an?#Q~SVar%nV^ zUUj!JNaC@D{d9j^sT7y7 zwVHvZ#RVR;k^F20r^RyRR4|G>tb#E6NTGCjImrDvDR~su5rmfCbp?JNak&V~dVkNi zNy?-ulU@7;zP`BK(fW1Wbfzwb2TVV~;_~pj1KmPygxTl&e3tZ_y!EC1W7i!!!`d#u zMo<{CYWKo|z$a48)z9eZFz$(>P6;u(gsiBl(#fqGz+!mSXH5WR5czltQd}_k{eQq? zJo1MX;_!{Dy)D-;N`B4al0k4PIt2xCg1ck1g^Dz*!X<+bgl`OA|1J)VyEPk8Y&S!j zrYbB}a}z|$*h2sNiDT25uGF+5rkXlE|9GYdBI5{(ac}b1lEz^+#68k-N$Gc9- zu?PGZk496z6sF~SY0whRS+-i;Y3y;@gg1n^n)@IdkL+0N65sXTJPQx8Pe zWAca{wF~chC=3Bm<#RyLh_vV!ZD$dCAu*Q_BTQ{)?1Y7Iw}CDyDz?Y{VjxqR%lua0 zz;7LwcTXf?Lf+6I8Ww6Q4^B&$OA^_ai?t$wd7TmGG2p$CFe2jitapC$zyQ-BP%afL z!ep(;V(F6PTizfhMY75Bj2uJak+w0#tc3t-tTn7?=?T+ko)*mU2j}NMrO~Vpf7QRWV#?(Q6GnOPSb&o>r zli*+L$^HEJOV4T=|jJ_2pLK(6HjPzC=Knp#dy8Oyy0NIfckRGs>$_5c& zgJ?~=4tg@anH{s$(9nq)Ab&Uyk`2%fFgAwpcbm4ibYk)jLr=^`U-Po35aH&H80sI5 zxeDBVx1Mg`%V;t88ty$H;{Ur5bLH_4ir2ai*nM8={cRtT|KA(c?ToVQY~cP>9_4?2 zxBt);9c<11t?#x}pVYyc48qtpma@oKHj_( zznlH2b^s8o?lI4E#K}f8p*sft(Y4N%%F}`7ViLybE}EWxvbBd4;999%iLb8aO-%3g zElxUdzd8->l>Y!*jxkx~{61Kp{yaB_`OmPbY9zeXL8UVa*7%(AQPN?#YFb`O*&>>v z_^Il=_KO-8Y zmv10jhPfkj=DiR*UFu?bVe1dQX|uE+xTsdsf~59)+%K# zn>GO^2wR-o-j_zLIh(mr3#&t0>&sFZ)!qc97VxBqRo*#hG1Ku7Gu)txp)9e)Me^yH zO6w=XdU}(&Oau{>c#@J$XR@#i6T&??LhwYYlQ=*H`uJ@$qyEQ{h9Na)>AT(rW(KbN zgms)|0UKt)GoSq~E69p4DCYF#=iFbW3q{HK+=jXh>e`NjxlKdF_u5UUOo*?1{FG7x z>U9))*&$?2?c~M)qc?Rl?vf$#Xpz!u-KXKG%S+^!QpX4kBZ^`A*Ug$Js)W!qO za)x`4qe<&$<_1jFf*lCi?1Y@19xH|tK8yG!w=W<9T{YW0`RaBU%^4=q1b1rvSuN|A zL`HVwVrmx~D{?oy%7$^1Q(POzOk+s5q63UOHFxYX7fE02#mozcl7}?p6*92}yY#_@ zaP_{EOc9+gu~QO(62_xEr476JsFjN)H$nugV3pZI4Bm`G>+{w3tqGIahyTTx;{3foh9- z&>b2mq7uk_90Vm@P)11OYd!LK%NqnN8nkvhzBio~e(n9Jk*2jN4!ViDCE00?VuVjo z3M;^f7IW-tp&RrHufh^-4q$E%?NuRIn<8fXQ#^Cd#H-6;!rhTh`1rXk=wtXxFCTTW zz)~Wh9w*mO?|SBoM1Lm-XX>R8f!v&ClUoRhjCVgH6DvyFa?s@i700>zL;>_(ohxZw zb&ZB7VN&d;&r1UAU7Uwub6~F(;EHES+om^Q_Vui~eqFz$w-}3wxpA1i%9rU01JEn7 zpVo9L&|Gu*;9b+hhf$ig@arq!@o{8VZzA7q5P9l{pCzmYt}802jxO4Q*>JvQyiI zdhG>>J`Y9Li>_KYZ5I)y`T=#J)5?|WG!!od1OU1Y1>zz;S3q=2qjzGI>`GFFkJr@Y z{V7SMm#)o15-p3M(4w=s^{hpHaY?s8^52)3Phot6h!^4La!4UU9ii zH*!(q#=pX|tK0u#|EEVeoTM#Ja(ce6FDvX-a?C_4rwv~e$1hSWH!e|JSW)yIjj3!p z(76s8^Iudm?M9sBy8|$oKHN02)jL-PLL$kwZf@e2O?$gFA4^gUZpWgu!^-|u5C-Lp zBCmG*a0AW&`WZWLQ)X?ly$k*i!|X zlBiOk!#baUrsDww%m}?sU2tW>T!C^O>)>w?G#gfVb#zUTD~=1DFW>IbLwXmbah^+l zQOf7Tj&aK3+I!5-u=H!eEi6snjP^mLOBbtoLl)2V!GgR-AjfC0g^rs|vT2fBT42pd zpq~v`jFgjYKJ#1h_Nk$Fbo98P@um*qcmT7xH<|W#Ay62w8crh3fMu#=trXmNWPjR* zT4K5JE&j+#lqfwt3f`HJcy$RhSW~Jcp!FBW8$VRbMs0kUp}`p&68R!n@!qaK-u9Pt zQMD*6bp~_6S}*sCsb@Q=D|(cplrp%zuvMQbYn0Z}4+r*E-7-BUnns<)$+ zYY7SjKVbq8M+iInDvVPi_$}7Ci86ilnp&vzMpuJEf21bK4wK~FMAnS4jid-yomxve zgJCb&*z0P*L&xC9?%LSTVdzly_{K}Wt2##d{?%bqXwAcPSM^cVVvIgIw2? zxx3MWq88!H)VKrDHh@#^;MXGqo~?0wzNMJQajBt;VVja#6@$6eME<@|q+WLME^Ge?0t<~`t!8Tqhml2NO^ z^B-&G&!}tzV~e~Ta35_ZBKfQ$eHCE*83y>P$VAg*7zuk))B(oJ4nV00kGpq;9?DRO z&mXY0ofV)vRAvqc!s8WTsAN}sA*?=Z!RIZTEm#6NUfv32F>pGxb z?r{iYewF0dNm@yi9`zxmrHMH2b5b+nL5o#R3x`*xENHi>9Z?nlVI4Akvo0fs(~bpw z)@vMO#~@Z;twaHz=NmWdR%`n_UK0Y#@tia~^ zPO6^vqu)L>Vcf)6`?ltBn2Uy$lNPm(wMa6lX?3m6qnNVNC&)RW`}nq*X}4hdrS)zc z#^0knLfWWKsHjVmh*c<4yCdpZ)VW(zJD+{k7Pf|u8IR2*(Tl@Du}eVId=-3&n1b5kY>;;{iV2Koiioc1|q_QH&m}3 zc>+$6Rf{^DR+nT^ib#!$=!N;(i~+kyh=6nA2J#8Ho}3k5zNxqG8jsiOWJ*lSI2)CC z9n0UDdXCVncoWm9XAP`QK`w<3gGfjk|D3{au)E;J79|w$1phh18NH9o-!91yDM#A6 z1s@JZhV97~3D9X9SzMqhCC?LAj2{eJ7Fs0?X~S6+YFuKAC*?aG^ZE8y-w(NveNCxxSwrLi!&vK`El zRMH-lAQ^m=!3`e^xlmo@+JtW9EiE*24s$mNSo)G_clIaM7Y~DycQg(i-#o1Pmq^+w z-R<+QawST1>QQw?xLvmFqO$&|TD+U-(!Gb?|NIUut+528O2@UhoEw(1kGz!Vj#iMm7qS=Oy9rqbBTiG2a6<7S&7z;M@wg>8}}~z_xF> zlVpYH>2KZx%w#51`R%bzZT=Ym$LIgpR$cbKkdgwW@fYxx@<07;|B}Z4xK{qRW`00r z1nvj?hXaNpeTb1QPKtc2*J<$N)at-!WVIii>=>W)mpiXMp$gSL0ojf4SZefp@?uJt zTe&aC*$(EAMNq>X>sX1H2oMLXZC!2esA=F4y>5kcT?` z*+5M;mz=3^X>>;xJ$mE+%5tobBN!WA6kiy7Hpkd`k1NqB=LO25;WH*997T?8V4?`M zOhT2Z%`AkbJy#yLT*-{y!>e1Fvi!mU$!pofj;e46HzVVU z5Rxh>ZdNvD#?y#YHLjv#>U`+5iN@A60J@U|8(a7=Y~dj}YC5$glKvRO5~?thy6snn zdM2Tt*EVox6SK@d0q9_(BESqrc$Zq2F;QRo=7ES5KDWIRx^7ah@(O64A15c@a|Thi zh!4~ctLCz3&k@7hQ)TT69Q@`ZnvhTw=30&=y^P>l46Q_y-W#s)I}`d0)a%cfDUUdB zhFoj`Nu44V2`tq8-Va6?J$f>QS9`5ForvNmC6~LGR+<*wD8n5HH|jE`(RRWZ)GUs} z<(}CMg~%LxXiL1rLmF&@-%QncN#|12X@3=UXQ3#ON7x)EW__=u{Zz{obYz5~$v4yy zp^-|&qWx<eY&NIK29-5#NKg!RxlC<2o1M*Two z&aT_NLlc6FAg}Lb?L~pffaKjgeRz;pXUE!m8_O?Z`l2CRST7w1^TxmZiRYe4z0!?F zvo-wIbM*{xMmWqPdN4+l1LjE?Q$3DqY*Z z`fZNX$P%%g0r^H?kSCBMVJ&KuDw(*(RZ}_#;~!@YeYji*iHjHld+nW`sA3`cRKNt% zd9;5(vo^Uj6y-Ad$1mo`k6TMm?o7Q;Nuc21suTes^z{|^Wac%F;!;Y`l_5m27N$S`r1)=w5MR8h z9lf2&y#qrYeA(mT32a>jBTs%W)~xtC$|i5vkTJ?t-(se zG|=NaJ(sSp<>mNXVTsVE&Z*4s=mqXi@{F?~@osi{yTR(vE#wJ4Km9<_Imo`bOJi{hX}SrT2)G$yCj(dg;|5Vb$QMn!Xs@| zI`SY{&t1jo9z>q6f_Ct#kfXL$HLt>$mXnHZCh0e9Mgp7C@FcGOO@xt61EH@Gsf}>Z zo?o<2#vkjSEQv|HtwW-Ra&AvSDmF4?<1WDJeHrnrqt<C{#dE}Zkk45MscFHZX7jqTk~DRq1y3C zilcr(kQr7gDX?fI$mI)?A1kmQeJs-oN8WhuVJ%7`xf9BZ0Gd|L6RubI{`1as4;X+qc_%7p-Iq&< zx`}e_j!3==TgL%cImc-tq8=J$m#P;TZFGp3VDNMkvI+|JI<=#G-Yd>?a!xNS`2^ zz0CN{%|+Sw=xEB64WidAn|H^q%vC$yMlFHNx}R#&#j+K7{;f@*n5r;q%G}Dz>l=sC z=mmsg!hd!hPury}URgQR^8Z*xW|07NQTgsWMVfRlD`w6rpW(O6@pN!czD8NGek00tJ7!;oBVCUw0-Qw3ix+k*d-fmF z^{Hj*UHxfqzQlj>{;P>SX-DHLCrd`Ze}Yg%}YZUfoNn!O-A|2ecVo3NVSX| zY(#V1gkg_9UD@?xd*p3ALZ1u54I%`u+Ds=gqdNFWkoM$mCjz+P!EZ!zERo|$DXqfS zmuJwIwP5rYn@ydIP1@9Ec~>EZL(#X5^a?d@5voY4mBKZy+)Lp88={Z97b*>D`Z!;k zxwkDx0;E?fwc$^mBPDw2e6L^@>%NVl_)YM^ohN4wdVRQM`gwkCE`L3|vT|_dduJ@J zvEu5)?kA9`P*QD2gtHLx}Uhmol`JLG%}Q{E+e= zNx4#R8SYY?+QJ0nqfOI+*D7tW3{OsF(*b787KFQ~j=*P*=L9hfZgYp34!s|RvBmM1 zabhA84l#=M$8Z*F>X8EymCKcf$ONplMc^TV6~qXgOR)a1Sv5M<{Zya=0sL6s>YIcN(hz?V@h9nEHX$4KFe@VK^ zXxx&w|AX&Rk)|lyQae{L0rA6u)kWW7#k#Qh@RZJIB*V&eeN9R0QV4=xsR(xGVwCGK zcQGmDk!S?N^A7SFIw{sTGtyw=AxUe6=9jVrkdAF>h zS;FSRnSuqhZ*MXbwGy2wr1AWws4do^4eWOpD3&#<#AY{5(T=2RSr8?SQ!7pQiT?GR zL|*_~Hk{eBpPXC}9#C(V*gSw*X(2f41YEY}7V_GHb~m~V1!hO@Jp&B(5yLw7@4X8o z)Uj`Gqhd=>+ldt~$|c~zclS7v(>`^(1#tbr9yJa{A{0%=eM^|tjy!{_BXak1!Psz! zqRE}7lB#K)CQuK(rSF2@&R)<*mjgB3i*K$CHB1(+@;X$sr&Kr>ojjJ9$~TvgZHYa? zl>RacUhM^nJ>fO29VsCapNc}VgY$ob&N|I1|A?|$otQL&f^cnq9yP%fLuTfSi>>qx zK{2pKP2ZyasJVQMqI7={(&s`vNqY4uepKPsaa_5Y-c%XPv&Jg_!j_e^o}YAvBBokF z4hPL$b3myaMCAA+QErJxBxDo1rBU>WY$^(K4~+%f5wq~6(;a_oLq;?*!sD#WgoU}H z`uY@AAe|9VXnVs#+l$F)t*WsWBBseyIWTCd?MTP_7g7=~CUelm=c~t_do)S@AI9GC zJ=1qt6Yg|u+cvwCj&0kvZQEui9dvBlcw%&H+qU1Q_sr~f)^Cnuul3>n59+SE>Z}v6 zdz}wY34Qp-fio=-zHpo*sbu#tdB$X4Dod$IEyCekRJk8$>q|#{Gj%0pp=9jsJuhc{cd z_x;5IeeV#97T%7-S1=~~w5%N>TxMWVrGtw}b~5n3esn-XJ38xc==+XdFB|)idK8@A z`O74T&6c8U>RTt(_JGd>sDs~x*CA=YcU4&cW?!p?%rDsIO*WM{ObsC-PQ2q1RnX@QU8fx%Gx|ju!GXX~>KE3fojh#J*2iAl_s?l6?;LU_2=7S6Yzom<5KFj^fKW z1$FVbv7EsHA>!;;)x?u@P|&%O)7TV32($)ie_|I=;$tUu!!oo&ci<3>pBhSFzt(S9 zyl80tdgsNfQZ4uA2m>wLHBb!C*nWPxvu5G(3Y2{nT4dQC^gqyZ%w^ts#=#;+tK(?P zs6nTKr(3{#tRN{?0|t}sf?ze)&=zr&Mg-StjytUa9UqWMt6_;OrTco#VRnx}I$!#F`lNy?b`AvKm z@wbhfD_juxvEQ-XCy36F9vx;#EC?qv$PvZS4-yQ1tX-vdZaIGiIM;_R-v^L zNqa=n|3g+Ru{glw;6S9Ai<|qbk2rc7JKIqsv#7wwFAK^8^cz}x5caBY(3`D;e>(%apxziU&AIdV0Nia}dqYx8bxH{t~a9715puxsIdiQy202+G)8s-zq0}^zqX`RVw zaAku_aD4cmL&BYCzDEv2-b8IbMDAMDcL?rd&g$kFuXPJ$6+EzxoukaAl|=g`y--)d zw`=eYk%TZAgr~x~%ec4T*w^5O$vt{C2U!T8+mAIIyRy*RPD8ln8AqV@uIP zg_JtYnjtPWeRPpQI}Fe8Itx5zOdCfy)_^aW5&y55!x_MuL$C&|;VPC{sbfXF-gTkd%``USvev2^cNT{y+~ zadhFz2m0gE`SDrbvhf%;BL$Xz@+1#8T_-qB>aIz1ZKi6dztKf8bEj(;=H|ly6+Hbn zim!$u^xMMT$_i`VF~HC5+4IZjS4dbc5mZ7AmM@Raya#XwvtNzZ*Tx)K@kqmyf)u zH`k1WZm-QE49E^&V$fW+{c{*amuBod0#ERcz!UtxV#Y#tF1F79(JZP|)v?=QNAscR z-YJutV#(DypH)o>HPM7`r;mopn&!^v2eS*xYRpnJW&QMt!>8n5O?NF95%299I?R6E z#)SaIDRnFt>8hK`8oM-Ed+54lq36G;Rs>2CcQDl6Q5PVqCHXc%xSsF^hAyN zxQZF-RUFo;9Mib;74aT9(V8ppvuo4Fed_cWDLlg(q{s=Epz*8}$L2q$*g5hx!$#PT zxe^(7pNEdmGrQo->bH$~yQIxI^MRalgTI_|b)9MZn-t&x;=PL?HM#;2N}iMWOV>UW z-j_RFC?|>Kv8$y z;)yiI(tmS1RQS%$8+-KgKpkY9u6DGHm0eL4O{;7)TEmuGu2kh>fbl1^hd>0ag;7RZ zk^Rqx8PC0kqn*H16@@w2p9Rfce0}U`)XFgCU@4b$w!{e?8IMS{f-o>DH+G7jft>=% zs<3DoGl2qu#g!v#)2wL=?l(s{B|*~TPl*@lYzoD5I|RhP)5&QqKQkB)UG5Mv)XjLo zV9jmka|kb*iC=DqhGO|mrOlAYr`c2@PmjUe0(Vrc7QM{sS9~_>Ys&{^7*F`UD8uuk zFrYrObZ8hKV%f)Z2anNkH?33@AU*KLlwa4l%VaPE6P#l#QzcE0iZ-g+T7Kjb9OW-v zp9!|Akp!LfoQ$*yxQ+QbI(EU5vaR{TjIU$NY?3!)l6B|gRkOZqPo?3yRYrw=v;Md; zE~5E?aKp1mA5_^;kcGmTJNwii78K;jC)X}twuJ@W2bc9Fo5#>*?|fMgkfJI5N~i>W zJ2|Ud;yrk48<-n1(o?nYFsOeZgXwoYH|QV@#}}r_Cuz*Su!B2im5F{6b76lagJAbg z<(WTg|HQ{BdU{n1Pr$J9IluEdBy?D3O4>4*?D&b2+%LSb1%Ik%(-J&>1sI`|wp!Kk z;>+uGl-)|F>R3tO>tVUK)bUk}L0UCa4Q_P-5Y#|@eXL!FXld7ie;N?h-w7~c6{@tA zZkDf_y5CVy$Arkp=8n5YaA0C< z5T&#GI{&So0Ih;`|MBP_cDdFOo~eSsMiK_3od4@xUI`c(^dEMT#$>&}Hn{^im2!V= za_`9)k7fhEPOqWOA1sCJ18s82r&%x~=fr7Zwf5RCTOXD6i zEYEuL!sbEgWkhAtMwI~V|KdTJsx)GAE{p{)dE{t05sSY+fKaYaj~s`How*31uO`jJ z;*9korLqB!@{b(V_|PJ)Rr;Z&nX|GjHs!Mlf|E~6%+3c`eb@vgwJUI-5v_k3KCQFL z{i5>LZ87T>fX6575uuWpgOM}K1l{+qv+F`XBpt0ruDWpz;_aI#4``F?;Z(_26WIDC z0S*TZKRUnlL956PpTw*(19*y%7O)doFUm9SF%q7czNi>Q%I+j5-Ua){&5~ug4~H4Q zyL!|YAp$Aqj0+O@&e% zpQC-EgmlXphlZq%1x2HwDVdTn)AGaNmbNqoK#v7D3<6RSay+N=h5U}bFXUY|*mb3X zLucnANWx}jAXy4uzjf@KTs7FO97<8o(N`ir@N>C~T^5j)j?I+ki{~k?Jb{&nXx9ze zY-^ol_$eN!orwsn8y_L;ClVi{-4k@-M>fD)aVHinl zcgymOV_jnvg`fFE?8`s-fMu$9af7Zr9`f&E-K2spvK$z5h?ELjHngL0vvDv>Th`-t zGr&RtY%cwb^%1~I4H=YznQl1@`2cBCo*e5gg@xO2viKdf#->d_ z;GJEWO3Zoz>o$7?y0P5KHBLl~a4`0j-&0`L-cxd#`KdjSwhP3lU68iwT-|ar60mc4 zf>7W`%lfdi1h^@-uu3i5LFGd;o>r#b7=82G3Qp1stGus(0gSc~lC*54cliV_HEoQsrW)F}L=(tdc_Dl9 zjo%4~Mz{4>KNXdS?mp>_?CXPYGMS?O)?-=^lNTNm}#;hXnp-7ps zT9d<}aRX*}foJ5zUZR4mgPq5%aJ;id0vS>cUWG2r z#T;2ITXw=P@4+zR*p0me$siSQ4&&5_>p)bo0z-d(`rdqaEE#wOzWL+xa~YS!?SnU< zI27f;0YZW;%+4Xz7?#m7?aZz@jAw12T_^J*$pvB+zoO|_rcSPjn$IVUy)!IF!n6L% zPDCvtkL50xzVvi#IlfpLvJXFdnHpJnDUz6iy;a>w4M_=_A5J3ISD7O81-p3`{bLiH zX_zou?|VHxgC0`e7#x|!P=xbi8mPRes4ome39;kIuyFwh4GM^PWUq(?Jw+p}c$zep z$$d>m;e42JKQv!t+}ebPOt`&*4^KshQO}l-$c<4iono@eTWb_DvJ0NaFZ1}_II82D z?+3S}*gC!@8V4Tg9}t^qXLU7OMWl?X>Bb%52IA1~2|JQ(AOGn9Sl7AB=i#9u8a<8U zg1pdvLi9>q{lgbzHB?*a0C*$J{C_xG61K+w>1>Em-E>-ENAeBrHVg~HK(S05=V47t zcWlUVB%cDAkv6*_u{Ll{AkQHSoBH*3*-SgOMMg#j=of!^She#rm#KyX&9aLYS3_6{ zLdeX@do%@%urT-o0yBE(} zk9fbfhA=wKHUu8ZgA>LsX{Pfw<+TO4*)((m^c0L(=uu*m4tmsB!S=yJkm*&!yAK#nW() z-UK!axm5X8TzE*m!FonQd1t0J#Y8RG&`&L>AP`2j9x5Sbt>4NcKwFTSIeTuW4B_e+ zd%;4T8qec}ar-<8prV!z=8?)QML4xMaJ_p`RhoqI@Pq$3ff&F@p z?Cb9H)fgOw5|;E}PT1|Xaddh06yay!CE)sy!G0X;W&5!;7fQZp*xSPA#59vHv1YE2 z?ANV2ok(n6i--RWy{NEsH^E83s6oFsTRyo|EsXhPDE#lJ_Zxae_$f^6IT= zy_@q~FzL@-7!+HPve7vQk$qFDd$gUD_!bEy=U@$)7RR0fFrAhTd*#p!O)`0rCjLuh zucjJgPa?`3^=(zG^f`{t%5mz8`wWlTo)-bSk;N$`FXm)|?Cnuchl$S&_~@ETK^^P* z1YCz9JPIi)iZQ5;nbx?ok!!efCJQRaZi6*pHYaD_A?X-vAaJd2XK)w_%|YOCHjNyh z(kC6Lrf%Sb#;hwivk3)qR?ysu##Ykc%xv_t${``;r)+&d@AoFn?K}tyw-`z5)ZHK4 zwv9~L5Ue7p=kTvp6>VvI7-gw-eFop7d^Mc;OXlSZz-40(Z1ai z>B8Bex2^@$zT#+n>By!iou!sr%?&=YI$)rW%^BGtBxt9K^xt0WV(y)+nu7vxCZ9rC zWic;qWT=UD*)Fx247^ur!5HDUhQOhUJ>pWP@=Yn8u+|P~lJ~`h#1%e6elGMXI8!Mor2fGxv5Ij* z`SelH5MHN~U9?}==3<%q43e*z?dYb`hfc+8*T$gO`P>I~;}57A#d5>kt7$Ih?p63S zjY}zyg7IVrOx7cNU$jnD9}g%DKaQ`}yth?W%FHqaYcyU**CK4>Zd~+qZ$pGI^QFUG zwoAFjMJ@8!(9_%r#3^+N&(5PId04C}DuIj*@XEzaqhI|-eKC+?U~&eD!1?VA zY@X=7YTG+eYL02{V ze(2Cb>NU2Cb!||sNu9<+h(}w7;=V{eXc6(0f<&u^XBuR&+8=kM-jNFmk*Xd&b?emf;%AL)n1fIZ8L3;Dyqtk+@X z!a(yV;y*+ux0n0MB3gD3!xA3)%7@ZGh|O4cCmWvt5F`$~SvYvCf(qBS~jt z=F1}yU{1{0+w+%V3N+~?xEX7hoJ6__Qv%0dw%t{BP4>U6*+wjX>6+DS^cZ%wNj4vQ z4!GqIYpnM^I=irfjznA&JnL7u^{~{!i{a*t6?hXH6eA{PCV*em=v$SY2$Sc|Xu}W0 z_K2_M-4>sy>mM`2H;D)K<@$5Qy- z2J^Kgaw*S%ulQOYFQqe9B=PL4E*TJnZl)t{x6SmEC(EBqEvc>%-f%5+?75=N!PSU` zZD2t5yQt=E6Zj*DA){UI5d1or^6G;iT*m#em{!Yu0!{m>S*D+cMMI>3Mm8|rufFC) zvE_XSZoG!ao1LHh$h80QXR}!_?kfw-202t?z7MpeUJwVQ~{f&W7B{Ri70_R)`dZ zmTwKlu-F{Jrp`56i-XHb(0A*_gu8TQ#21*NTJiHDal#R?Sq;s@#Iw{Ao!>giQ>p7v zFUGXfxN|>NRhi+axls#BMg7Bx)Fs$Dtp30A)5urFy{0Qg1XUP%j`17&B;%;35(X-A z)mXh!hts>Ax5}WwXW2u@vigSDWoAOc*2q`KCeAS~c7FYG@04LsTw3dKtml<$8Zn({ zmqV()+_tLY)$`wrbijKyubA{VFBmzB0|9neAL<>!Bi-C8*T#hAJOXuQl;?*C@` zod%|J$m_P3acoeNO=X>lm|hkA%^EMD_H0?NnyEhUl14#_v4W2q?)FeYK|do}3&MlE z1%-P%M?>z4>!&+2DuUyE+!qQc9N^>MS%|UJcaR&ISn<7G0!!nBBB^pnj%bu5bOeyr@Rs9LR1f)~# zjH*QZm26VKiTGVCCZE^caZ$jLsdI*5JQu3lY8h z-4M1w{B5{5)Dmn>;yZzOz#M!OvLuP~UU}{x@d(ge1LLC<%uI$S|Lyq$8xknKNCdh4 z&gTp=oy7q|Bs?oEihHRtse|P+7DfiX+N0~Ktg>lf@?SCe7<{!SQZF&JDqSN6zBz1O z!!UDo(9xenN)_4Oy`zr$r+;9kWRpo0oSVF9AS9AIaWfIj=Bbuv{g zs-1kTxVhP)Yn%aAeXY+rLC<*1I^p(&tKb>*ZTKdNnuu%A#oo;N$lpZqTeNFU6~8y7 zLB1XtM^GXgSu3kbQ=Tvts~-FSet^JgJgkxnojpDiE9+heVH58ix|sE~tNxq~>S7z2 z2ZjipkQQS1K>pG|m64;jPf*zeHyfU8Y#b?jsI*4|Zg0LFKm_;T(^)rZrH|X+RHeLW zve${&#GDC@&AR#KwL|U1X}tdn4`yX`%d95uTltyUh7`$cm3uaKn@{h1ZLaVg8a|i= zq&N3sc4u#z<|p#E4essCRI)xQJ}T%;GEOWjUQ5j&p>9ONE{edGkteZ;?EL@hjqul_ z$c||9b{1HQ-9U3Q(tj;SV>=@&Mo~vQTi}{+tYmKICL{50O&@%e|J#9aUasy`!D6Zy z3uK1_y6jIa?3jjvwbySG%^m3k90`r<$hSv)Z0gjsyifzBo$L0?*9&(CxNj=>tdp>? z)7DUFm}!ItI!sR$Fcw=$We<3tgyeioHZ44T4P{_vT2G32@RM-k$D$UNDZG6^0B#qn zo9Pfc2_oOJ)dYv`t1NBCwqEvVAinsCHQGtJh`2JdafCFw9%bz8Ly-uVefGdw65d+3a@?rf$&RShc{O7caYNK#ZD7ZL)}W3Rlh z^zl`#AtB3Tu#__hO9lcRCBX0S0TdfqYqvv+r-GA+MN47o)&f7Iw>)o5Aq_AZj0p_C zJ7PIX-D6P32%nz7nT04}G|c=F5y=AZx6ikL7CEIkVL_BoA0>pGDlv_u%P^}ml^f5c ziFJe-xxd;Tl|YoBoYtE;mNg39jX+^Ut(x2K7bzlL;r}UP3x%R^!sEQG+D73351{{^ zs#OP}+St%x?B*+Pa48lfOi;45in*xEn9z;&VVQLz_L01$q}Tgp!!3gc@W;`3G2v-< zr+Op``_IkCAY}7(^pI_;J0I(2o!Ss0O#0OLOPNSzj;^o9dXr3qJWp6w`MT^J1sC=V zp9_2)MwKYRckws~u7r{mlrt0ya8Cz0hsal066J%J17~zuBW=S_OvsmG`I5>;XQ^b2 zAu}6(7&PqZ*fD#@4f1~dp!<;*{|zU~8`wX_7Wu@?<1c{f(NDK0%8NFwDrvI|xv$J( zZhHBPAdq*&7Gr_5Lq-$X)^qdS6YqHcREbD7idd8WeQp6MDCGY`zmPGoH83;z4?0Fj zaRR&bP%Iyh57tth3aI!AKZC8QyHh9Lmo zn>f5Z4H6&7RzNABQLu60eQ*QvMz~Ej>w+B@C z8;U{|n|{ZHt^6Hksc2t$3)MY{;7h4BqUWdug}RO&j;TqQy6)lXuM+{}j3rMtUN}%Q zk9mOVtF9N?*k47+b*OaF6GkpjF4x@Y>kO*Rm+H>lM*_6Y2y`E6N&CS+=!duDX@1bD zj6g(#r)%wT*aaR9CQ@d}^`uwY6%6=~40}lZTFLQnO&(6qfvB-k1lYF)8nXs>7nd8e zLdRE+yzJf>!0Q!1KF$tsw(9iW5CFS{%-?R|+aC0{TO`rhQuv=?T?Waez+=}))3ndH zE>t2-l37m(B$hK3(idiC{IN zk|vm*shE)N?1nBb%{Ri zF<{dlaCoVo>o^@hqTkmm5OpE≪Vvtj}9$ztxEHC=sE(c!g9&iyK0Y-X3Zs=?q#- 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]|)q8DS z&(u@*QWtkjU%Zqcum#LaNolS|n{n7s2`{s|nn$PMG%Db@et_>81SaNUP!@J2nRFB( zQDzmksNsm-P(V`K9QRvRHj#@HY$`p>v4JQ=Vn|}-?VM%i3FhxjIkVZ&JlrobWz3_} zLf0yUphEE6C3qb) z3u`7g|4=hu6$%wqy5v*n)(OuGRXeXfHPaMXQ@>;f`iFGg^1sj~YU5ry*@LaIN)gO2 zs2Ac?38kQvKJSpD@ewPNAZDR@mImRR_H=PpvrYe6^(O|gZ_(E9b9}}Zpp>q%hz{EQ zc{sZJlFqK2`e{w2t(@e10!s5;T)a>z6OmduFtITvR?(*VwZW2?tYk@HQdEi#`L=LA zDOss$-f1HNa1;n*=j}J{yLW&kZ0OZH2A-=zJ5}3qiwdjcPAXOXwxqlYecu6&r;lA9 z@<_Z|u$*YDI^`2Vnex=YIg_tz!1@Vyc$M#4?1#7Tw);(I>k*rR(Uxa7*z;Q^e!Zm3 zBscxVchPjMLbtwFg--QnYMjCF*{_qUtSjDgbZPRpLjt&HG?j-Nkn!)0*osBHGv^Kh zLoq*fIpXSM;5m?4(W_-})yRMMU#6eZ7x6O0AEkCGMZeRK6Lvo8B3uhzae-L`j8yql zSy2c%QxVK}6_2Np?}o+CTb&}O6hTCOx}{r)ePMrdEhy%^2t&V^EVVdgRd||8G@+ykxzgiCac$hBef&dwn-c*4bPn>G#zpB`i_)sW`ym z7(YcP+U{79t3w(jp<8eHPIk1FBAJ;np5s=5VL&f;H?v&FA{q_{0 zJ3wj8MoRAm8&Ij4PPREJt?w7Yv!iS`VvR!Au!>htNv{=4IguqCv%Sj&y-qrn#M8+$ zl#0V(FU#??Em5`5DT5?43yH$%f$N~s=`v)(Dv>|NxWy+aB9STDqa%Ol@wO$;W}Pi~ z?}eyRl7+0Ml{UD̰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

9mT7KQ3<~}Cnxy{Xi1i@J3vInX?#Pp8yl=5d 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� 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

    C`1RUh|zWY-;tP%3pLKyj^ZoGaT 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

    )enxT8gscd)U` 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%0AQ!rz8fsiZT)(T9wv<#so79~|l^}6~ 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 z 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 wIf$&bQ)`K^NQ3Y{28Zlb6 + * Currently this will just print the library name and the version. + */ +public class AppGlum +{ + /** + * Main entry point that will print out the version of Glum to stdout. + */ + public static void main(String[] aArgArr) + { + System.out.println("Glum Library. Version: " + AppInfo.getVersion()); + System.exit(0); + } + +} diff --git a/src/glum/app/AppInfo.java b/src/glum/app/AppInfo.java new file mode 100644 index 0000000..3c68852 --- /dev/null +++ b/src/glum/app/AppInfo.java @@ -0,0 +1,39 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.app; + +/** + * Immutable class that provides information related to the build. + * + * @author lopeznr1 + */ +public class AppInfo +{ + /** + * Defines the version of the Glum library. + *

    + * Note we do not make the Version directly visible and final so other classes will not utilized a cached version + * when built via Ant. + */ + private static String Version = "2.0.0"; + + /** + * Returns the version of the application + */ + public static String getVersion() + { + return Version; + } + +} diff --git a/src/glum/color/ColorHSBL.java b/src/glum/color/ColorHSBL.java new file mode 100644 index 0000000..21a1d99 --- /dev/null +++ b/src/glum/color/ColorHSBL.java @@ -0,0 +1,121 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.color; + +import java.awt.Color; + +/** + * Immutable object which provides access to attributes of a color. + *

    + * The attributes are: hue, saturation, brightness, luminance + * + * @author lopeznr1 + */ +public class ColorHSBL +{ + // Attributes + private final Color refColor; + private final float hue; + private final float saturation; + private final float brightness; + private final float luminance; + + /** Standard Constructor */ + public ColorHSBL(Color aColor) + { + refColor = aColor; + + float[] tmpArr = convertColorToHSBL(aColor); + hue = tmpArr[0]; + saturation = tmpArr[1]; + brightness = tmpArr[2]; + luminance = tmpArr[3]; + } + + /** + * Returns the associated {@link Color}. + */ + public Color getColor() + { + return refColor; + } + + /** + * Returns the hue component. + */ + public float getHue() + { + return hue; + } + + /** + * Returns the saturation component. + */ + public float getSaturation() + { + return saturation; + } + + /** + * Returns the brightness component. + */ + public float getBrightness() + { + return brightness; + } + + /** + * Returns the luminance component. + */ + public float getLuminance() + { + return luminance; + } + + /** + * Utility method that returns a 4 element array that has the following in the elements + *

      + *
    • hue + *
    • saturation + *
    • brightness + *
    • luminosity + *
    + * The source for this conversion originated from:
    + * https://stackoverflow.com/questions/596216/formula-to-determine-perceived-brightness-of-rgb-color + *

    + * Note the above source has changed overtime and the conversion below is not authoritative. + */ + public static float[] convertColorToHSBL(Color aColor) + { + int rI = aColor.getRed(); + int gI = aColor.getGreen(); + int bI = aColor.getBlue(); + + float rF = rI / 255.0f; + float gF = gI / 255.0f; + float bF = bI / 255.0f; + // Formula: broken +// float lum = (float) Math.sqrt(0.241f * rF + 0.691f * gF + 0.068f * bF); + // Formula: corrected +// float lum = (0.299f * rF + 0.587f * gF + 0.114f * bF); + float lum = (float) Math.sqrt(0.299f * rF * rF + 0.587f * gF * gF + 0.114f * bF * bF); + + float[] hsblArr = new float[4]; + Color.RGBtoHSB(rI, gI, bI, hsblArr); + + hsblArr[3] = lum; + return hsblArr; + } + +} diff --git a/src/glum/color/ColorHSBLCompartor.java b/src/glum/color/ColorHSBLCompartor.java new file mode 100644 index 0000000..33835d8 --- /dev/null +++ b/src/glum/color/ColorHSBLCompartor.java @@ -0,0 +1,125 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.color; + +import java.util.Comparator; +import java.util.List; + +import com.google.common.collect.ImmutableList; + +/** + * Flexible Color Comparator that provides color comparisons using any combination of the attributes HLSB (hue, lumance, + * saturation, brightness) rather than RGB (red,green, blue). + * + * @author lopeznr1 + */ +public class ColorHSBLCompartor implements Comparator +{ + // Constants + private static ImmutableList SortKey_HLB = ImmutableList.of(KeyAttr.Hue, KeyAttr.Luminance, + KeyAttr.Brightness); + + // Attributes + private final List keyL; + private final int numGroups; + + /** + * Standard Constructor + * + * @param aKeyL + * Defines an ordered list of {@link KeyAttr}s that will define how colors are sorted. + * @param aNumGroups + * Defines the number of groups for which the various color attributes will be grouped to. + */ + public ColorHSBLCompartor(List aKeyL, int aNumGroups) + { + keyL = ImmutableList.copyOf(aKeyL); + numGroups = aNumGroups; + } + + /** + * Simplified Constructor + * + * Colors will be sorted by the following ordered attributes: hue, lumance, and brightness. + * + * @param aNumGroups + * Defines the number of groups for which the various color attributes will be grouped to. + */ + public ColorHSBLCompartor(int aNumGroups) + { + this(SortKey_HLB, aNumGroups); + } + + @Override + public int compare(ColorHSBL aHsbl1, ColorHSBL aHsbl2) + { + int prevStep = 0; + + // Iterate through all of the keys + int tmpIdx = -1; + for (KeyAttr aKey : keyL) + { + tmpIdx++; + double fVal1, fVal2; + switch (aKey) + { + case Hue: + fVal1 = aHsbl1.getHue(); + fVal2 = aHsbl2.getHue(); + break; + case Saturation: + fVal1 = aHsbl1.getSaturation(); + fVal2 = aHsbl2.getSaturation(); + break; + case Brightness: + fVal1 = aHsbl1.getBrightness(); + fVal2 = aHsbl2.getBrightness(); + break; + case Luminance: + fVal1 = aHsbl1.getLuminance(); + fVal2 = aHsbl2.getLuminance(); + break; + + default: + throw new UnsupportedOperationException("Unrecognized key: " + aKey); + } + + // Compare the corresponding normalized (group) values + int iVal1 = (int) (fVal1 * numGroups); + int iVal2 = (int) (fVal2 * numGroups); + int cmpVal = Integer.compare(iVal1, iVal2); + + // If we are at the last key - do not use the normalize (group) values + // but rather utilize the non-normalized raw float values so as to + // reduce (eliminate?) ambiguities due to values being in the same bin. + if (tmpIdx == keyL.size() - 1) + cmpVal = Double.compare(fVal1, fVal2); + + // Note we flip the comparison if the previous (key) comparison + // resulted in an odd value. We do this so that rather than having + // a number of step function we get more of an oscillating smooth + // transition between the comparison keys. + if (prevStep % 2 == 1) + cmpVal = -cmpVal; + + if (cmpVal != 0) + return cmpVal; + + prevStep = iVal1; + } + + return 0; + } + +} diff --git a/src/glum/color/ColorTestApp.java b/src/glum/color/ColorTestApp.java new file mode 100644 index 0000000..283c034 --- /dev/null +++ b/src/glum/color/ColorTestApp.java @@ -0,0 +1,261 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.color; + +import java.awt.Color; +import java.util.*; + +import javax.swing.JFrame; +import javax.swing.JPanel; + +import com.google.common.collect.ImmutableList; + +import glum.gui.misc.BooleanCellEditor; +import glum.gui.misc.BooleanCellRenderer; +import glum.gui.panel.itemList.*; +import glum.gui.panel.itemList.query.QueryComposer; +import glum.gui.table.ColorHSBLCellRenderer; +import glum.gui.table.PrePendRenderer; +import net.miginfocom.swing.MigLayout; + +/** + * Demo application that demonstrates a number of features: + *

      + *
    • Showing color attributes via a table + *
    • Alternative color sorting + *
    • Multicolmun sorting + *
    + * + * @author lopeznr1 + */ +public class ColorTestApp +{ + // Constants + private static final int DefNumGroups = 8; +// private static final int DefNumItems = 100; // 3100 + private static final int DefNumItems = 3100; + + /** + * Main entry point. + */ + public static void main(String[] args) + { + JFrame frame = new JFrame("TableColorTest"); + frame.setContentPane(formTestPanel(DefNumGroups, DefNumItems)); + + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.pack(); + frame.setVisible(true); + } + + private static JPanel formTestPanel(int aNumGroups, int aNumItems) + { + ItemListPanel tmpILP; + + JPanel retPanel = new JPanel(new MigLayout()); + + // Table Content + QueryComposer tmpComposer = new QueryComposer<>(); + tmpComposer.addAttribute(LookUp.IsVisible, Boolean.class, "Show", 50); + tmpComposer.addAttribute(LookUp.Index, Integer.class, "ID", "Trk: 99"); + + tmpComposer.addAttribute(LookUp.HSBL, Color.class, "HBL", 50); + tmpComposer.addAttribute(LookUp.HSBL, Color.class, "HLB", 50); + tmpComposer.addAttribute(LookUp.HSBL, Color.class, "HLS", 50); + tmpComposer.addAttribute(LookUp.HSBL, Color.class, "HSB", 50); + tmpComposer.addAttribute(LookUp.HSBL, Color.class, "HSL", 50); + tmpComposer.addAttribute(LookUp.HSBL, Color.class, "SBH", 50); + tmpComposer.addAttribute(LookUp.HSBL, Color.class, "SBL", 50); + tmpComposer.addAttribute(LookUp.HSBL, Color.class, "SLB", 50); + + tmpComposer.addAttribute(LookUp.Message, String.class, "Message", null); + tmpComposer.addAttribute(LookUp.Red, Integer.class, "Red", 40); + tmpComposer.addAttribute(LookUp.Green, Integer.class, "Green", 40); + tmpComposer.addAttribute(LookUp.Blue, Integer.class, "Blue", 40); + tmpComposer.addAttribute(LookUp.Hue, Integer.class, "Hue:" + aNumGroups, 60); + tmpComposer.addAttribute(LookUp.Sat, Integer.class, "Sat:" + aNumGroups, 60); + tmpComposer.addAttribute(LookUp.Lum, Integer.class, "Lum:" + aNumGroups, 60); + tmpComposer.addAttribute(LookUp.Bri, Integer.class, "Bri:" + aNumGroups, 60); + + tmpComposer.setEditor(LookUp.IsVisible, new BooleanCellEditor()); + tmpComposer.setRenderer(LookUp.IsVisible, new BooleanCellRenderer()); + tmpComposer.setRenderer(LookUp.Index, new PrePendRenderer("Trk: ")); + tmpComposer.setRenderer(LookUp.HSBL, new ColorHSBLCellRenderer(false)); + +// KeyType[] sortKeyArr = {KeyType.Hue, KeyType.Saturation, KeyType.Luminance}; + List sortKeyL_HBL = ImmutableList.of(KeyAttr.Hue, KeyAttr.Brightness, KeyAttr.Luminance); + List sortKeyL_HLB = ImmutableList.of(KeyAttr.Hue, KeyAttr.Luminance, KeyAttr.Brightness); + List sortKeyL_HLS = ImmutableList.of(KeyAttr.Hue, KeyAttr.Luminance, KeyAttr.Saturation); + List sortKeyL_HSB = ImmutableList.of(KeyAttr.Hue, KeyAttr.Saturation, KeyAttr.Brightness); + List sortKeyL_HSL = ImmutableList.of(KeyAttr.Hue, KeyAttr.Saturation, KeyAttr.Luminance); + List sortKeyL_SBH = ImmutableList.of(KeyAttr.Saturation, KeyAttr.Brightness, KeyAttr.Hue); + List sortKeyL_SBL = ImmutableList.of(KeyAttr.Saturation, KeyAttr.Brightness, KeyAttr.Luminance); + List sortKeyL_SLB = ImmutableList.of(KeyAttr.Saturation, KeyAttr.Luminance, KeyAttr.Brightness); + + ItemHandler tmpIH = new TestItemHandler(aNumGroups); + StaticItemProcessor tmpIP = new StaticItemProcessor<>(formTestItemList(aNumItems)); + tmpILP = new ItemListPanel<>(tmpIH, tmpIP, tmpComposer, true); + tmpILP.setSortingEnabled(true); +// tmpILP.setSortComparator(ColorHSBL.class, new ColorStepSortCompartor(numGroups)); + tmpILP.setSortComparator(2, new ColorHSBLCompartor(sortKeyL_HBL, aNumGroups)); + tmpILP.setSortComparator(3, new ColorHSBLCompartor(sortKeyL_HLB, aNumGroups)); + tmpILP.setSortComparator(4, new ColorHSBLCompartor(sortKeyL_HLS, aNumGroups)); + tmpILP.setSortComparator(5, new ColorHSBLCompartor(sortKeyL_HSB, aNumGroups)); + tmpILP.setSortComparator(6, new ColorHSBLCompartor(sortKeyL_HSL, aNumGroups)); + tmpILP.setSortComparator(7, new ColorHSBLCompartor(sortKeyL_SBH, aNumGroups)); + tmpILP.setSortComparator(8, new ColorHSBLCompartor(sortKeyL_SBL, aNumGroups)); + tmpILP.setSortComparator(9, new ColorHSBLCompartor(sortKeyL_SLB, aNumGroups)); + +// JTable tmpTable = lidarILP.getTable(); +// tmpTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); +// retPanel.add(new JScrollPane(tmpTable), "growx,growy,pushx,pushy,span,wrap"); + retPanel.add(tmpILP, "growx,growy,pushx,pushy,span,wrap"); + + tmpIP.setItems(formTestItemList(aNumItems)); + + return retPanel; + } + + /** + * Helper method that forms the list of TestItems. + */ + private static List formTestItemList(int aNumItems) + { + List retL = new ArrayList<>(); + + Random tmpRandom = new Random(100); + for (int c1 = 0; c1 < aNumItems; c1++) + { + int r = tmpRandom.nextInt(256); + int g = tmpRandom.nextInt(256); + int b = tmpRandom.nextInt(256); + Color tmpColor = new Color(r, g, b); + String tmpMsg = String.format("Color: %3d %3d %3d", r, g, b); + retL.add(new TestItem(c1, tmpColor, tmpMsg)); + } + + return retL; + } + + enum LookUp + { + IsVisible, + + Index, + + Color, + + HSBL, + + Message, + + Red, + + Green, + + Blue, + + Hue, + + Sat, + + Lum, + + Bri + } + + static class TestItem + { + // Attributes + private final int index; + private final Color color; + private final String message; + + // Cache vars + private final ColorHSBL cHSBL; + + // State vars + private boolean isVisible; + + public TestItem(int aIndex, Color aColor, String aMessage) + { + index = aIndex; + color = aColor; + message = aMessage; + + cHSBL = new ColorHSBL(color); + + isVisible = true; + } + } + + static class TestItemHandler implements ItemHandler + { + // Attributes + private final int numGroups; + + /** Standard Constructor */ + public TestItemHandler(int aNumGroups) + { + numGroups = aNumGroups; + } + + @Override + public Object getValue(TestItem aItem, LookUp aEnum) + { + switch (aEnum) + { + case IsVisible: + return aItem.isVisible; + case Index: + return aItem.index; + case Color: + return aItem.color; + case HSBL: + return aItem.cHSBL; + case Message: + return aItem.message; + case Red: + return aItem.color.getRed(); + case Green: + return aItem.color.getGreen(); + case Blue: + return aItem.color.getBlue(); + case Hue: + return (int) (aItem.cHSBL.getHue() * numGroups); + case Sat: + return (int) (aItem.cHSBL.getSaturation() * numGroups); + case Bri: + return (int) (aItem.cHSBL.getBrightness() * numGroups); + case Lum: + return (int) (aItem.cHSBL.getLuminance() * numGroups); + default: + break; + } + + throw new UnsupportedOperationException("Column is not supported. Enum: " + aEnum); + } + + @Override + public void setValue(TestItem aItem, LookUp aEnum, Object aValue) + { + if (aEnum == LookUp.IsVisible) + aItem.isVisible = (Boolean) aValue; + else + throw new UnsupportedOperationException("Column is not supported. Enum: " + aEnum); + } + + } + +} diff --git a/src/glum/color/KeyAttr.java b/src/glum/color/KeyAttr.java new file mode 100644 index 0000000..44c2312 --- /dev/null +++ b/src/glum/color/KeyAttr.java @@ -0,0 +1,31 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.color; + +/** + * Enum that defines the different attributes associated with Colors. + * + * @author lopeznr1 + */ +public enum KeyAttr +{ + Hue, + + Saturation, + + Brightness, + + Luminance, + +} \ No newline at end of file diff --git a/src/glum/coord/Convert.java b/src/glum/coord/Convert.java deleted file mode 100644 index ad5af9b..0000000 --- a/src/glum/coord/Convert.java +++ /dev/null @@ -1,95 +0,0 @@ -package glum.coord; - -/** - * Contains conversion multipliers to/from feet, yards, meters, data miles, and nautical miles, as well as angular - * values to/from degrees and radians. To convert a value X in units of U to units of - * V, use X * Convert.U_TO_V. - */ -public class Convert -{ - public static final double FEET_TO_METERS = 0.3048; - public static final double DM_TO_METERS = 1828.8; - public static final double NM_TO_METERS = 1852.0; - public static final double MILES_TO_METERS = 1609.344; - public static final double YARDS_TO_METERS = 0.9144; // 3 * FEET_TO_METERS - - public static final double METERS_TO_FEET = 1.0 / FEET_TO_METERS; - public static final double DM_TO_FEET = 6000.0; - public static final double NM_TO_FEET = NM_TO_METERS * METERS_TO_FEET; - public static final double MILES_TO_FEET = 5280.0; - public static final double YARDS_TO_FEET = 3.0; - - public static final double METERS_TO_DM = 1.0 / DM_TO_METERS; - public static final double FEET_TO_DM = FEET_TO_METERS * METERS_TO_DM; - public static final double NM_TO_DM = NM_TO_METERS * METERS_TO_DM; - public static final double MILES_TO_DM = MILES_TO_METERS * METERS_TO_DM; - public static final double YARDS_TO_DM = YARDS_TO_METERS * METERS_TO_DM; - - public static final double METERS_TO_NM = 1.0 / NM_TO_METERS; - public static final double FEET_TO_NM = FEET_TO_METERS * METERS_TO_NM; - public static final double DM_TO_NM = DM_TO_METERS * METERS_TO_NM; - public static final double MILES_TO_NM = MILES_TO_METERS * METERS_TO_NM; - public static final double YARDS_TO_NM = YARDS_TO_METERS * NM_TO_METERS; - - public static final double METERS_TO_MILES = 1.0 / MILES_TO_METERS; - public static final double FEET_TO_MILES = FEET_TO_METERS * METERS_TO_MILES; - public static final double DM_TO_MILES = DM_TO_METERS * METERS_TO_MILES; - public static final double NM_TO_MILES = NM_TO_METERS * METERS_TO_MILES; - public static final double YARDS_TO_MILES = YARDS_TO_METERS * METERS_TO_MILES; - - public static final double METERS_TO_YARDS = 1.0 / YARDS_TO_METERS; - public static final double FEET_TO_YARDS = 1.0 / 3.0; - public static final double DM_TO_YARDS = 2000.0; - public static final double NM_TO_YARDS = NM_TO_METERS * METERS_TO_YARDS; - public static final double MILES_TO_YARDS = 1760.0; - - public static final double RAD_TO_DEG = 180.0 / Math.PI; - public static final double DEG_TO_RAD = Math.PI / 180.0; - - public static final double SECS_TO_MSECS = 1000.0; - public static final double MSECS_TO_SECS = 1.0 / SECS_TO_MSECS; - - public static final int MINS_TO_SECS = 60; - public static final int HOURS_TO_MINS = 60; - public static final int HOURS_TO_SECS = HOURS_TO_MINS * MINS_TO_SECS; - - public static final double SECS_TO_MINS = 1 / MINS_TO_SECS; - public static final double MINS_TO_HOURS = 1 / HOURS_TO_MINS; - public static final double SECS_TO_HOURS = 1 / HOURS_TO_SECS; - - /** - * Constructor - */ - private Convert() - { - } - - /** - * Converts an angle to a bearing - */ - public static double angleToBearing(double aAngle) - { - double bearing; - - bearing = 180 - (aAngle + 90); - if (bearing < 0) - bearing += 360; - - return bearing; - } - - /** - * Converts a bearing to an angle - */ - public static double bearingToAngle(double aBearing) - { - double angle; - - angle = 180 - (aBearing + 90); - if (angle < 0) - angle += 360; - - return angle; - } - -} diff --git a/src/glum/coord/CoordUtil.java b/src/glum/coord/CoordUtil.java deleted file mode 100644 index c0fa1a6..0000000 --- a/src/glum/coord/CoordUtil.java +++ /dev/null @@ -1,197 +0,0 @@ -package glum.coord; - - -/** Provides a few useful functions on coordinates, such as converting -* to a user-presentable string. -*/ -public class CoordUtil -{ - /** Convert a Lat/Lon to a pair of DEG:MM:SS H strings. H is the - * hemisphere, N or S for lat, E or W for lon. The default separator - * string LL_SEP is used. - */ - public static String LatLonToString (LatLon ll) - { - return ll == null ? "" : LatLonToString (ll, LL_SEP); - } - - /** Same as the other LatLonToString, excepts this one uses the - * given sep string to separate the Lat and Lon. - */ - public static String LatLonToString (LatLon ll, String sep) - { - return ll == null ? "" : - LatToString (ll.lat) + LL_SEP + LonToString (ll.lon); - } - - /** Converts the given lat to DD:MM:SS H. */ - - public static String LatToString (double lat) - { - return LatToString (lat, true); - } - - /** Converts the given lat to DD:MM:SS H if - * include_seconds is true. If it's false, then the - * :SS part is left off. - */ - - public static String LatToString (double lat, boolean include_seconds) - { - DMS dms = new DMS (lat); - StringBuffer s = new StringBuffer(); - - if ( dms.degrees < 10 ) - s.append ("0"); - s.append (dms.degrees); - s.append (":"); - if ( dms.minutes < 10 ) - s.append ("0"); - s.append (dms.minutes); - if ( include_seconds ) - { - s.append (":"); - if ( dms.seconds < 10 ) - s.append ("0"); - s.append (dms.seconds); - } - s.append (lat >= 0 ? " N" : " S"); - return s.toString(); - } - - /** Similar to LatToString except that the degrees - * part is DDD instead of DD. */ - - public static String LonToString (double lon) - { - return LonToString (lon, true); - } - - /** Similar to LatToString except that the degrees - * part is DDD instead of DD. */ - - public static String LonToString (double lon, boolean include_seconds) - { - DMS dms = new DMS (lon); - StringBuffer s = new StringBuffer(); - - if ( dms.degrees < 100 ) - s.append ("0"); - if ( dms.degrees < 10 ) - s.append ("0"); - s.append (dms.degrees); - s.append (":"); - if ( dms.minutes < 10 ) - s.append ("0"); - s.append (dms.minutes); - if ( include_seconds ) - { - s.append (":"); - if ( dms.seconds < 10 ) - s.append ("0"); - s.append (dms.seconds); - } - s.append (lon >= 0 ? " E" : " W"); - return s.toString(); - } - - /** Converts dmsh_string to a double value. - * The string format should match the output of the - * LatToString formats, including hemisphere. - * If a hemisphere character is not part of the string, the - * returned value will be non-negative. - */ - public static double StringToLat (String dmsh_string) - { - if ( dmsh_string == null || dmsh_string.length() == 0 ) - return 0.0; - - int dms [] = StringToDMS (dmsh_string); - - if ( dms.length == 3 ) - return new Degrees (dms[0], dms[1], dms[2]).degrees; - else return 0.0; - } - - /** {@see StringToLat} */ - - public static double StringToLon (String dmsh_string) - { - // Because we aren't doing any range or hemisphere error - // checking, a lon value is identical to a lat value. - return StringToLat (dmsh_string); - } - - /** Converts dmsh_string to a an array of - * 3 ints representing degrees, minutes, and seconds. - * if a hemisphere character is present (one of NSEW or - * nsew), and it represents a souther or western hemisphere, - * then the degrees value, in index 0 of the returned array, - * will be a non-positive number. - */ - public static int [] StringToDMS (String dmsh_string) - { - if ( dmsh_string == null || dmsh_string.length() == 0 ) - return null; - - char chars [] = dmsh_string.toCharArray(); - int dms [] = new int [ 3 ]; - - dms[0] = 0; - for ( int i = 0, j = 0; i < chars.length; i++ ) - { - char c = chars[i]; - - if ( c == ' ' || c == ' ' ) // Space or tab. - continue; - else if ( c >= '0' && c <= '9' && j < 3 ) - dms[j] = dms[j] * 10 + c - '0'; - else if ( c == ':' ) - { - j++; - dms[j] = 0; - } - else if ( c == 'S' || c == 's' || c == 'W' || c == 'w' ) - dms[0] = -dms[0]; - } - - return dms; - } - - public static class DMS - { - public DMS (double deg) - { - if ( deg < 0 ) deg = -deg; - degrees = (int) deg; - minutes = (int) (deg * 60) % 60; - seconds = (int) (deg * 3600) % 60; - } - public int degrees, minutes, seconds; - } - - public static class Degrees - { - public Degrees (int deg, int min, int sec) - { - degrees = Math.abs (deg) + - Math.abs(min) / 60.0 + - Math.abs(sec) / 3600.0; - if ( deg < 0 || min < 0 || sec < 0 ) - degrees = -degrees; - } - public Degrees (int deg, int min, int sec, char hemisphere) - { - this (deg, min, sec); - if ( hemisphere == 'N' || hemisphere == 'n' || - hemisphere == 'E' || hemisphere == 'e' ) - degrees = Math.abs (degrees); - else if ( hemisphere == 'S' || hemisphere == 's' || - hemisphere == 'W' || hemisphere == 'w' ) - degrees = -Math.abs (degrees); - } - public double degrees; - } - - public static String LL_SEP = " / "; -} diff --git a/src/glum/coord/Epsilon.java b/src/glum/coord/Epsilon.java deleted file mode 100644 index 3c4012a..0000000 --- a/src/glum/coord/Epsilon.java +++ /dev/null @@ -1,43 +0,0 @@ -package glum.coord; - -/** Determines if two numbers are close, usually as a way to say -* that they are equal. Close is defined to mean that their difference -* is less than some small number, which is either supplied by the caller -* or is EPSILON. -*

    For longitude near the equator, a difference of EPSILON is about -* 3.65 feet (where the earth's circumference is about 21913.3 DM, or -* about 60.87 DM per degree longitude). For DataMile measurements, it's -* about 0.72 inches. -*/ - -public class Epsilon -{ - /** The measure of closeness; set to 0.00001. */ - public static final double EPSILON = 0.00001; - - public static boolean close (float a, float b) - { - float diff = a - b; - return diff < EPSILON && diff > -EPSILON; - } - - public static boolean close (float a, float b, float epsilon) - { - float diff = a - b; - return diff < epsilon && diff > -epsilon; - } - - public static boolean close (double a, double b) - { - double diff = a - b; - return diff < EPSILON && diff > -EPSILON; - } - - public static boolean close (double a, double b, float epsilon) - { - double diff = a - b; - return diff < EPSILON && diff > -EPSILON; - } - - private Epsilon () { } -} diff --git a/src/glum/coord/GeoUtil.java b/src/glum/coord/GeoUtil.java deleted file mode 100644 index 4f76522..0000000 --- a/src/glum/coord/GeoUtil.java +++ /dev/null @@ -1,97 +0,0 @@ -package glum.coord; - -/** - * Contains a collection of utility methods to perform linear algebra using the objects from this package. - */ -public class GeoUtil -{ - /** - * realSqr returns aNum*aNum - */ - public static double realSqr(double aNum) - { - return aNum * aNum; - } - - /** - * computeDotProduct - Returns the dot product of vector1 and vector2 - */ - public static double computeDotProduct(Point3D vector1, Point3D vector2) - { - return vector1.x * vector2.x + vector1.y * vector2.y + vector1.z * vector2.z; - } - - /** - * computeDistance - Returns the distance between pt1 and pt2 - */ - public static double computeDistance(Point3D pt1, Point3D pt2) - { - return Math.sqrt(realSqr(pt1.x - pt2.x) + realSqr(pt1.y - pt2.y) + realSqr(pt1.z - pt2.z)); - } - - /** - * computeDistanceSquare - Returns the squared distance between pt1 and pt2 - */ - public static double computeDistanceSquare(Point3D pt1, Point3D pt2) - { - return realSqr(pt1.x - pt2.x) + realSqr(pt1.y - pt2.y) + realSqr(pt1.z - pt2.z); - } - - /** - * computeLength - Returns the magnitude of aVector - */ - public static double computeLength(Point3D aVector) - { - return Math.sqrt(realSqr(aVector.x) + realSqr(aVector.y) + realSqr(aVector.z)); - } - - /** - * computeNormal - Returns the R.H.R normal defined by the 3 points - */ - public static void computeNormal(Point3D pt1, Point3D pt2, Point3D pt3, Point3D aNormal) - { - Point3D vector1, vector2; - - vector1 = new Point3D(); - vector2 = new Point3D(); - computeVector(pt1, pt3, vector1); - computeVector(pt3, pt2, vector2); - - // ! Not sure why I have to negate all the values; Need to refer to linear alg. -//! aNormal.x = vector1.y*vector2.z - vector1.z*vector2.y; -//! aNormal.y = vector1.z*vector2.x - vector1.x*vector2.z; -//! aNormal.z = vector1.x*vector2.y - vector1.y*vector2.x; - aNormal.x = -(vector1.y * vector2.z - vector1.z * vector2.y); - aNormal.y = -(vector1.z * vector2.x - vector1.x * vector2.z); - aNormal.z = -(vector1.x * vector2.y - vector1.y * vector2.x); - - // Normalize the vector - normalizeVector(aNormal); - } - - /** - * computeVector - Returns the vector defined by the 2 points - */ - public static void computeVector(Point3D pt1, Point3D pt2, Point3D aVector) - { - aVector.x = pt2.x - pt1.x; - aVector.y = pt2.y - pt1.y; - aVector.z = pt2.z - pt1.z; - } - - /** - * normalizeVector - Normalizes aVector so that its length is 1 - */ - public static void normalizeVector(Point3D aVector) - { - double length; - - length = computeLength(aVector); - - // Normalize the vector - aVector.x = aVector.x / length; - aVector.y = aVector.y / length; - aVector.z = aVector.z / length; - } - -} diff --git a/src/glum/coord/LatLon.java b/src/glum/coord/LatLon.java deleted file mode 100644 index c9a954c..0000000 --- a/src/glum/coord/LatLon.java +++ /dev/null @@ -1,111 +0,0 @@ -package glum.coord; - -/** Simple class for Lat/Lon values. */ -public class LatLon -{ - public double lat; - public double lon; - - public LatLon() - { - } - - public LatLon(LatLon latlon) - { - if (latlon != null) - { - lat = latlon.lat; - lon = latlon.lon; - } - } - - public LatLon(double lat, double lon) - { - this.lat = lat; - this.lon = lon; - } - - public LatLon(String lat_string, String lon_string) - { - set(lat_string, lon_string); - } - - public void set(double lat, double lon) - { - this.lat = lat; - this.lon = lon; - } - - public void set(LatLon latlon) - { - if (latlon != null) - { - lat = latlon.lat; - lon = latlon.lon; - } - } - - public void set(String lat_string, String lon_string) - { - lat = CoordUtil.StringToLat(lat_string); - lon = CoordUtil.StringToLon(lon_string); - } - - public void normalize() - { - if (lat > 90) - lat = 90; - else if (lat < -90) - lat = -90; - - if (lon > 180) - lon -= 360; - else if (lon < -180) - lon += 360; - } - - /** - * Tests to see if the given object is the same lat/lon as this position. - * "Same" really means "very, very close," as defined by {@link Epsilon}. - * - * @return True if obj is a LatLon and is very close to our lat/lon position. - * False otherwise. - */ - @Override - public boolean equals(Object obj) - { - return (obj instanceof LatLon) && Epsilon.close(lat, ((LatLon)obj).lat) && Epsilon.close(lon, ((LatLon)obj).lon); - } - - @Override - public String toString() - { - return CoordUtil.LatLonToString(this); - } - - /** - * Returns the change in latitude - */ - static public double computeDeltaLat(double lat1, double lat2) - { - return lat2 - lat1; - } - - /** - * Returns the change in longitude - */ - static public double computeDeltaLon(double lon1, double lon2) - { - double dLon; - - dLon = lon2 - lon1; - if (Math.abs(dLon) < 180) - return dLon; - - if (dLon > 180) - return dLon - 360; - else - return dLon + 360; - } - -} diff --git a/src/glum/coord/Point2D.java b/src/glum/coord/Point2D.java deleted file mode 100644 index 7bff1fe..0000000 --- a/src/glum/coord/Point2D.java +++ /dev/null @@ -1,37 +0,0 @@ -package glum.coord; - -public class Point2D -{ - public double x; - public double y; - - public Point2D () { } - - public Point2D (Point2D pt) - { if ( pt != null ) { x = pt.x; y = pt.y; } } - - public Point2D (double x, double y) - { this.x = x; this.y = y; } - - public void set (double x, double y) - { this.x = x; this.y = y; } - - public void set (Point2D pt) - { if ( pt != null ) { x = pt.x; y = pt.y; } } - - public double distance (Point2D aPt) - { - if (aPt == null) - return 0; - - return Math.sqrt((aPt.x - x)*(aPt.x - x) + (aPt.y - y)*(aPt.y - y)); - } - - @Override - public boolean equals (Object obj) - { - return (obj instanceof Point2D) && - Epsilon.close (x, ((Point2D) obj).x) && - Epsilon.close (y, ((Point2D) obj).y); - } -} diff --git a/src/glum/coord/Point2Di.java b/src/glum/coord/Point2Di.java deleted file mode 100644 index f7f09d5..0000000 --- a/src/glum/coord/Point2Di.java +++ /dev/null @@ -1,27 +0,0 @@ -package glum.coord; - -public class Point2Di -{ - public int x; - public int y; - - public Point2Di () { } - - public Point2Di (Point2Di pt) - { if ( pt != null ) { x = pt.x; y = pt.y; } } - - public Point2Di (int x, int y) { this.x = x; this.y = y; } - - public void set (int x, int y) { this.x = x; this.y = y; } - - public void set (Point2Di pt) - { if ( pt != null ) { x = pt.x; y = pt.y; } } - - @Override - public boolean equals (Object obj) - { - return (obj instanceof Point2Di) && - x == ((Point2Di) obj).x && - y == ((Point2Di) obj).y; - } -} diff --git a/src/glum/coord/Point3D.java b/src/glum/coord/Point3D.java deleted file mode 100644 index 7fabc60..0000000 --- a/src/glum/coord/Point3D.java +++ /dev/null @@ -1,42 +0,0 @@ -package glum.coord; - -/** A class for representing any 3-Dimensional vector, which could be -* a position, a velocity, or a rotation. No information about units -* is assumed or implied. -*/ - -public class Point3D -{ - public double x; - public double y; - public double z; - - public Point3D () { } - - public Point3D (Point3D pt) - { if ( pt != null ) { x = pt.x; y = pt.y; z = pt.z; } } - - public Point3D (double x, double y, double z) - { this.x = x; this.y = y; this.z = z; } - - public void set (double x, double y, double z) - { this.x = x; this.y = y; this.z = z; } - - public void set (Point3D pt) - { if ( pt != null ) { x = pt.x; y = pt.y; z = pt.z; } } - - @Override - public boolean equals (Object obj) - { - return (obj instanceof Point3D) && - Epsilon.close (x, ((Point3D) obj).x) && - Epsilon.close (y, ((Point3D) obj).y) && - Epsilon.close (z, ((Point3D) obj).z); - } - - @Override - public String toString() - { - return new String("(" + x + ", " + y + ", " + z + ")"); - } -} diff --git a/src/glum/coord/RngBrg.java b/src/glum/coord/RngBrg.java deleted file mode 100644 index cc79fe2..0000000 --- a/src/glum/coord/RngBrg.java +++ /dev/null @@ -1,47 +0,0 @@ -package glum.coord; - -public class RngBrg -{ - public double rng; - public double brg; - - public RngBrg() { } - - public RngBrg(RngBrg pt) - { - if ( pt != null ) - { - rng = pt.rng; - brg = pt.brg; - } - } - - public RngBrg (double rng, double brg) - { - this.rng = rng; - this.brg = brg; - } - - public void set (double rng, double brg) - { - this.rng = rng; - this.brg = brg; - } - - public void set (RngBrg pt) - { - if ( pt != null ) - { - rng = pt.rng; - brg = pt.brg; - } - } - - @Override - public boolean equals (Object obj) - { - return (obj instanceof RngBrg) && - Epsilon.close (rng, ((RngBrg) obj).rng) && - Epsilon.close (brg, ((RngBrg) obj).brg); - } -} diff --git a/src/glum/coord/UV.java b/src/glum/coord/UV.java deleted file mode 100644 index 8c7150d..0000000 --- a/src/glum/coord/UV.java +++ /dev/null @@ -1,26 +0,0 @@ -package glum.coord; - -public class UV extends Point2D -{ - public UV () - { - x = 0; - y = 0; - } - - public UV (Point2D pt) - { - if (pt != null) - { - x = pt.x; - y = pt.y; - } - } - - public UV (double x, double y) - { - this.x = x; - this.y = y; - } - -} diff --git a/src/glum/database/QueryItem.java b/src/glum/database/QueryItem.java index 6a8aa95..719d4f6 100644 --- a/src/glum/database/QueryItem.java +++ b/src/glum/database/QueryItem.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.database; public abstract interface QueryItem> diff --git a/src/glum/database/QueryItemComparator.java b/src/glum/database/QueryItemComparator.java index 3365316..58a0e1c 100644 --- a/src/glum/database/QueryItemComparator.java +++ b/src/glum/database/QueryItemComparator.java @@ -1,11 +1,31 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.database; import java.util.Comparator; +/** + * {@link Comparable} to sort {@link QueryItem}s based on the provided key. + * + * @author lopeznr1 + */ public class QueryItemComparator, G2 extends Enum> implements Comparator { - private G2 sortKey; + // Attributes + private final G2 sortKey; + /** Standard Constructor */ public QueryItemComparator(G2 aSortKey) { sortKey = aSortKey; @@ -15,10 +35,8 @@ public class QueryItemComparator, G2 extends Enum> i @Override public int compare(G1 item1, G1 item2) { - Comparable value1, value2; - - value1 = (Comparable)item1.getValue(sortKey); - value2 = (Comparable)item2.getValue(sortKey); + var value1 = (Comparable) item1.getValue(sortKey); + var value2 = (Comparable) item2.getValue(sortKey); if (value1 == null && value2 == null) return 0; @@ -34,29 +52,23 @@ public class QueryItemComparator, G2 extends Enum> i /** * Utility method to create a QueryItemComparator by specifying the class and sort Enum. - *

    + *

    * This logic is here due to Java's horrible implementation off generics. */ public static , G4 extends Enum> Comparator spawn(Class aClass, G4 aEnum) { - QueryItemComparator retComparator; - - retComparator = new QueryItemComparator(aEnum); - return retComparator; + return new QueryItemComparator(aEnum); } /** * Utility method to create a QueryItemComparator by specifying just the Enum. Note this method can not be used in a * argument to another method; instead use: {@link #spawn(Class, Enum)} - *

    + *

    * This logic is here due to Java's horrible implementation off generics. */ public static , G4 extends Enum> Comparator spawn(G4 aEnum) { - QueryItemComparator retComparator; - - retComparator = new QueryItemComparator(aEnum); - return retComparator; + return new QueryItemComparator(aEnum); } } diff --git a/src/glum/digest/Digest.java b/src/glum/digest/Digest.java new file mode 100644 index 0000000..34c0953 --- /dev/null +++ b/src/glum/digest/Digest.java @@ -0,0 +1,101 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.digest; + +import java.util.Arrays; + +/** + * Immutable object that stores a single digest. + * + * @author lopeznr1 + */ +public class Digest +{ + private final DigestType digestType; + private final byte[] digestValueArr; + + public Digest(DigestType aDigestType, byte[] aDigestValueArr) + { + digestType = aDigestType; + digestValueArr = Arrays.copyOf(aDigestValueArr, aDigestValueArr.length); + } + + public Digest(DigestType aDigestType, String aHexStr) + { + digestType = aDigestType; + digestValueArr = DigestUtils.hexStr2ByteArr(aHexStr); + } + + /** + * Returns a user friendly description (string) of this digest result. + *

    + * The result will be DigestType:hexDigestValue + */ + public String getDescr() + { + return "" + digestType + ":" + getValueAsString(); + } + + /** + * Returns the DigestType associated with this Digest. + */ + public DigestType getType() + { + return digestType; + } + + /** + * Returns the actual digest (as a string) associated with this Digest. + */ + public byte[] getValue() + { + return Arrays.copyOf(digestValueArr, digestValueArr.length); + } + + /** + * Returns the actual digest (as a string) associated with this Digest. + */ + public String getValueAsString() + { + return DigestUtils.byteArr2HexStr(digestValueArr); + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((digestType == null) ? 0 : digestType.hashCode()); + result = prime * result + Arrays.hashCode(digestValueArr); + return result; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Digest other = (Digest) obj; + if (digestType != other.digestType) + return false; + if (!Arrays.equals(digestValueArr, other.digestValueArr)) + return false; + return true; + } + +} diff --git a/src/glum/digest/DigestType.java b/src/glum/digest/DigestType.java new file mode 100644 index 0000000..c9091d1 --- /dev/null +++ b/src/glum/digest/DigestType.java @@ -0,0 +1,64 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.digest; + +/** + * Enum that defines the supported digest types. + * + * @author lopeznr1 + */ +public enum DigestType +{ + // Weak digest - but very fast + MD5("MD5"), + + // Fairly strong digest type (with good performance on 32 bit machines) + SHA256("SHA-256"), + + // Very strong digest type + SHA512("SHA-512"); + + // State vars + private String algName; + + private DigestType(String aAlgName) + { + algName = aAlgName; + } + + /** + * Returns the official digest algorithm name. + * + * @see http://docs.oracle.com/javase/1.5.0/docs/guide/security/CryptoSpec.html#AppA + */ + public String getAlgName() + { + return algName; + } + + /** + * Returns the corresponding DigestType. + */ + public static DigestType parse(String aStr) + { + if (aStr.equalsIgnoreCase("MD5") == true) + return MD5; + if (aStr.equalsIgnoreCase("SHA256") == true) + return SHA256; + if (aStr.equalsIgnoreCase("SHA512") == true) + return SHA512; + + return null; + } +} \ No newline at end of file diff --git a/src/glum/digest/DigestUtils.java b/src/glum/digest/DigestUtils.java new file mode 100644 index 0000000..a35082a --- /dev/null +++ b/src/glum/digest/DigestUtils.java @@ -0,0 +1,83 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.digest; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import com.google.common.io.BaseEncoding; + +/** + * Collection of utility methods to ease working with the MessageDigest and associated classes. + * + * @author lopeznr1 + */ +public class DigestUtils +{ + /** + * Utility method that will throw a RuntimeExcepption if the specified digest function is not found. + *

    + * Algorithm should be MD5, SHA-256, SHA-512, ... + *

    + * See: http://docs.oracle.com/javase/1.8.0/docs/guide/security/CryptoSpec.html#AppA + */ + public static MessageDigest getDigest(String aAlgorithm) + { + MessageDigest retDigest; + + try + { + retDigest = MessageDigest.getInstance(aAlgorithm); + } + catch (NoSuchAlgorithmException aExp) + { + throw new RuntimeException("Digest not found. Digest algorith not found: " + aAlgorithm); + } + + return retDigest; + } + + /** + * Utility method that will throw a RuntimeExcepption if the specified digest function is not found. + *

    + * See: http://docs.oracle.com/javase/1.8.0/docs/guide/security/CryptoSpec.html#AppA + */ + public static MessageDigest getDigest(DigestType aDigestType) + { + return getDigest(aDigestType.getAlgName()); + } + + /** + * Utility method that returns the (lower case) hex string corresponding to the byte array. + *

    + * Delegates to {@link BaseEncoding.base16().lowerCase().encode(CharSequence)} + */ + public static String byteArr2HexStr(byte[] aByteArr) + { + String retStr = BaseEncoding.base16().lowerCase().encode(aByteArr); + return retStr; + } + + /** + * Utility method that returns a byte array corresponding to the hex string. + *

    + * Delegates to {@link BaseEncoding.base16().lowerCase().decode(CharSequence)} + */ + public static byte[] hexStr2ByteArr(String aHexStr) + { + byte[] retArr = BaseEncoding.base16().lowerCase().decode(aHexStr); + return retArr; + } + +} diff --git a/src/glum/filter/EnumFilter.java b/src/glum/filter/EnumFilter.java index 2782748..e852807 100644 --- a/src/glum/filter/EnumFilter.java +++ b/src/glum/filter/EnumFilter.java @@ -1,35 +1,50 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.filter; +import java.io.IOException; +import java.util.*; + import glum.zio.*; -import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; - +/** + * A {@link Filter} which performs filtering based on a enam values. + * + * @author lopeznr1 + * + * @param + */ public abstract class EnumFilter> implements ZioObj, Filter { // Static config vars private Map> fullMap; - + // State vars - private Set> validSet; + private Set> validS; private boolean isEnabled; - + + /** Standard Constructor */ public EnumFilter(Class> enumClass) { - fullMap = Maps.newLinkedHashMap(); + fullMap = new LinkedHashMap<>(); for (Enum aEnum : enumClass.getEnumConstants()) fullMap.put(aEnum.ordinal(), aEnum); - - validSet = Sets.newLinkedHashSet(); + + validS = new LinkedHashSet<>(); isEnabled = false; } - + /** * Returns true if the filter is active. */ @@ -43,7 +58,7 @@ public abstract class EnumFilter> implements ZioObj, Filt */ public List> getSelectedItems() { - return Lists.newArrayList(validSet); + return new ArrayList<>(validS); } /** @@ -51,10 +66,10 @@ public abstract class EnumFilter> implements ZioObj, Filt */ public void set(EnumFilter aFilter) { - validSet = Sets.newLinkedHashSet(aFilter.validSet); + validS = new LinkedHashSet<>(aFilter.validS); isEnabled = aFilter.getIsEnabled(); } - + /** * Sets whether the filter is active. */ @@ -62,57 +77,52 @@ public abstract class EnumFilter> implements ZioObj, Filt { isEnabled = aBool; } - + /** * Sets the list of valid enums for this filter. */ - public void setSetSelectedItems(List> selectedItems) + public void setSetSelectedItems(List> aItemL) { - validSet.clear(); - validSet.addAll(selectedItems); + validS.clear(); + validS.addAll(aItemL); } - + @Override public void zioRead(ZinStream aStream) throws IOException { - int numItems; - aStream.readVersion(0); - + // Read the payload isEnabled = aStream.readBool(); - - validSet.clear(); - numItems = aStream.readInt(); + + validS.clear(); + var numItems = aStream.readInt(); for (int c1 = 0; c1 < numItems; c1++) - validSet.add(fullMap.get(aStream.readInt())); + validS.add(fullMap.get(aStream.readInt())); } @Override public void zioWrite(ZoutStream aStream) throws IOException { - int numItems; - aStream.writeVersion(0); - + aStream.writeBool(isEnabled); - numItems = validSet.size(); + var numItems = validS.size(); aStream.writeInt(numItems); - for (Enum aEnum : validSet) + for (Enum aEnum : validS) aStream.writeInt(aEnum.ordinal()); } /** - * Utility method that returns whether aValue is within the constraints - * specified by this filter. + * Utility method that returns whether aValue is within the constraints specified by this filter. */ protected boolean testIsValid(G2 aEnum) { if (isEnabled == false) return true; - - return validSet.contains(aEnum); + + return validS.contains(aEnum); } } diff --git a/src/glum/filter/Filter.java b/src/glum/filter/Filter.java index dd1cd69..9761fd3 100644 --- a/src/glum/filter/Filter.java +++ b/src/glum/filter/Filter.java @@ -1,7 +1,27 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.filter; import glum.zio.ZioObj; +/** + * Interface that provides a mechanism for filtering items. + * + * @author lopeznr1 + * + * @param + */ public interface Filter extends ZioObj { /** diff --git a/src/glum/filter/FilterUtil.java b/src/glum/filter/FilterUtil.java index f5b5bb9..65d9c53 100644 --- a/src/glum/filter/FilterUtil.java +++ b/src/glum/filter/FilterUtil.java @@ -1,14 +1,31 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.filter; +import java.util.ArrayList; import java.util.List; import javax.swing.JCheckBox; -import com.google.common.collect.Lists; - import glum.gui.component.GList; import glum.gui.component.GNumberField; +/** + * Collection of utility methods used to aide with working with {@link Filter}s and (swing) UI components. + * + * @author lopeznr1 + */ public class FilterUtil { /** @@ -16,16 +33,14 @@ public class FilterUtil */ public static List applyFilter(List itemList, Filter aFilter) { - List retList; - - retList = Lists.newArrayList(); + var retItemL = new ArrayList(); for (G1 aItem : itemList) { if (aFilter.isValid(aItem) == true) - retList.add(aItem); + retItemL.add(aItem); } - return retList; + return retItemL; } /** @@ -38,18 +53,19 @@ public class FilterUtil } /** - * Utility method to synchronize the associated GUI controls with the specified filter. + * Utility method to synchronize the associated GUI controls with the specified filter. */ public static void setEnumFilter(EnumFilter> aFilter, JCheckBox mainCB, GList> mainList) { mainCB.setSelected(aFilter.getIsEnabled()); mainList.setSelectedItems(aFilter.getSelectedItems()); } - + /** * Utility method to synchronize the specified filter with the associated GUI controls. */ - public static void getRangeFilter(RangeFilter aFilter, JCheckBox mainCB, JCheckBox minCB, JCheckBox maxCB, GNumberField minNF, GNumberField maxNF) + public static void getRangeFilter(RangeFilter aFilter, JCheckBox mainCB, JCheckBox minCB, JCheckBox maxCB, + GNumberField minNF, GNumberField maxNF) { aFilter.setIsEnabled(mainCB.isSelected()); aFilter.setUseMin(minCB.isSelected()); @@ -59,9 +75,10 @@ public class FilterUtil } /** - * Utility method to synchronize the associated GUI controls with the specified filter. + * Utility method to synchronize the associated GUI controls with the specified filter. */ - public static void setRangeGui(RangeFilter aFilter, JCheckBox mainCB, JCheckBox minCB, JCheckBox maxCB, GNumberField minNF, GNumberField maxNF) + public static void setRangeGui(RangeFilter aFilter, JCheckBox mainCB, JCheckBox minCB, JCheckBox maxCB, + GNumberField minNF, GNumberField maxNF) { mainCB.setSelected(aFilter.getIsEnabled()); minCB.setSelected(aFilter.getUseMin()); @@ -71,26 +88,23 @@ public class FilterUtil } /** - * Utility method to keep the various GUI components associated with an EnumFilter synchronized. - * The mainList will be enabled/disabled based on the selection state of mainCB. + * Utility method to keep the various GUI components associated with an EnumFilter synchronized. The mainList will be + * enabled/disabled based on the selection state of mainCB. */ public static void syncEnumGui(JCheckBox mainCB, GList> mainList) { - boolean isEnabled; - - isEnabled = mainCB.isSelected(); + var isEnabled = mainCB.isSelected(); mainList.setEnabled(isEnabled); } /** - * Utility method to keep the various GUI components associated with an RangeFilter synchronized. - * Gui components will be enabled/disabled based on the various check boxes. + * Utility method to keep the various GUI components associated with an RangeFilter synchronized. Gui components will + * be enabled/disabled based on the various check boxes. */ - public static void syncRangeGui(JCheckBox mainCB, JCheckBox minCB, JCheckBox maxCB, GNumberField minNF, GNumberField maxNF) + public static void syncRangeGui(JCheckBox mainCB, JCheckBox minCB, JCheckBox maxCB, GNumberField minNF, + GNumberField maxNF) { - boolean isEnabled; - - isEnabled = mainCB.isSelected(); + var isEnabled = mainCB.isSelected(); minCB.setEnabled(isEnabled); maxCB.setEnabled(isEnabled); minNF.setEnabled(isEnabled & minCB.isSelected()); diff --git a/src/glum/filter/NullFilter.java b/src/glum/filter/NullFilter.java index 35a22a5..39eaac0 100644 --- a/src/glum/filter/NullFilter.java +++ b/src/glum/filter/NullFilter.java @@ -1,12 +1,29 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.filter; +import java.io.IOException; + import glum.zio.ZinStream; import glum.zio.ZoutStream; -import java.io.IOException; - /** - * A Filter which does not filter anything. Thus the method isValid() always returns true. + * A {@link Filter} which does not filter anything. Thus the method isValid() always returns true. + * + * @author lopeznr1 + * + * @param */ public class NullFilter implements Filter { diff --git a/src/glum/filter/RangeFilter.java b/src/glum/filter/RangeFilter.java index a544a9e..f0d3c6a 100644 --- a/src/glum/filter/RangeFilter.java +++ b/src/glum/filter/RangeFilter.java @@ -1,25 +1,39 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.filter; -import glum.zio.*; - import java.io.IOException; +import glum.zio.*; + /** - * Abstract filter which is used to filter a single value between the specified min/max ranges. The only code to write - * is the isValid() method and to call the appropriate Constructor. In the isValid() method, you should delegate filter - * logic to the method testIsValid() with the quantity of interest, and return the result from the method call. + * Abstract {@link Filter} which is used to filter a single value between the specified min/max ranges. The only code to + * write is the isValid() method. In the isValid() method, you should delegate filter logic to the method testIsValid() + * with the quantity of interest, and return the result from the method call. + * + * @author lopeznr1 + * + * @param */ -public abstract class RangeFilter implements ZioObj, Filter +public abstract class RangeFilter implements Filter, ZioObj { + // State vars private boolean isEnabled; private boolean useMin, useMax; private double minValue, maxValue; - /** - * @param aBinCode - * Unique identifier used during serialization. The value specified here should not collide with any other - * codes for which there is serialization. - */ + /** Standard Constructor */ public RangeFilter() { isEnabled = false; @@ -38,7 +52,7 @@ public abstract class RangeFilter implements ZioObj, Filter public boolean getUseMax() { return useMax; } public double getMinValue() { return minValue; } public double getMaxValue() { return maxValue; } - + public void setIsEnabled(boolean aBool) { isEnabled = aBool; } public void setUseMin(boolean aBool) { useMin = aBool; } public void setUseMax(boolean aBool) { useMax = aBool; } @@ -61,11 +75,9 @@ public abstract class RangeFilter implements ZioObj, Filter @Override public void zioRead(ZinStream aStream) throws IOException { - byte bSwitch; - aStream.readVersion(0); - bSwitch = aStream.readByte(); + byte bSwitch = aStream.readByte(); isEnabled = (bSwitch & 0x1) != 0; useMin = (bSwitch & 0x2) != 0; useMax = (bSwitch & 0x4) != 0; @@ -80,11 +92,9 @@ public abstract class RangeFilter implements ZioObj, Filter @Override public void zioWrite(ZoutStream aStream) throws IOException { - byte bSwitch; - aStream.writeVersion(0); - bSwitch = 0; + byte bSwitch = 0; if (isEnabled == true) bSwitch |= 0x1; if (useMin == true) diff --git a/src/glum/gui/FocusUtil.java b/src/glum/gui/FocusUtil.java index e37c1d5..e31add8 100644 --- a/src/glum/gui/FocusUtil.java +++ b/src/glum/gui/FocusUtil.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui; import javax.swing.*; diff --git a/src/glum/gui/GuiExeUtil.java b/src/glum/gui/GuiExeUtil.java new file mode 100644 index 0000000..7fc6215 --- /dev/null +++ b/src/glum/gui/GuiExeUtil.java @@ -0,0 +1,48 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui; + +import java.awt.Component; +import java.awt.event.HierarchyEvent; +import java.awt.event.HierarchyListener; + +/** + * Collection of execution utilities used to execute behavior at a specific time frame associated with the life cycle of + * AWT/Swing components. + * + * @author lopeznr1 + */ +public class GuiExeUtil +{ + /** + * Utility method to execute the Runnable once the specified Component is "showing" on the screen. + */ + public static void executeOnceWhenShowing(Component aComp, Runnable aRunnable) + { + aComp.addHierarchyListener(new HierarchyListener() { + + @Override + public void hierarchyChanged(HierarchyEvent aEvent) + { + if (aComp.isShowing() == false) + return; + + aRunnable.run(); + aComp.removeHierarchyListener(this); + } + }); + + } + +} diff --git a/src/glum/gui/GuiPaneUtil.java b/src/glum/gui/GuiPaneUtil.java new file mode 100644 index 0000000..fc4298b --- /dev/null +++ b/src/glum/gui/GuiPaneUtil.java @@ -0,0 +1,91 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui; + +import java.awt.Component; + +import javax.swing.RootPaneContainer; + +import glum.gui.panel.generic.MessagePanel; +import glum.util.ThreadUtil; + +/** + * Collection of AWT/Swing utilities for showing various message panes. + * + * @author lopeznr1 + */ +public class GuiPaneUtil +{ + /** + * Utility method to display an alert messages. + *

    + * Alert panel will have a nominal size of 750, 300. + */ + public static void showAlertMessage(Component aParent, String aTitle, String aInfoMsg) + { + // Delegate + showFailMessage(aParent, aTitle, aInfoMsg, null, 750, 300); + } + + /** + * Utility method to display failure messages. + *

    + * If a stack trace is specified (not null) then the {@link Throwable} will be displayed below aInfoMsg. + */ + public static void showFailMessage(Component aParent, String aTitle, String aInfoMsg, Throwable aExp, int aMaxW, + int aMaxH) + { + // Compute the panel size + int compW = aMaxW; + int compH = aMaxH; + RootPaneContainer tmpRPC = GuiUtil.getRootPaneContainer(aParent); + if (tmpRPC instanceof Component) + { + Component tmpComp = (Component) tmpRPC; + compW = (int) (tmpComp.getWidth() * 0.80); + compH = (int) (tmpComp.getHeight() * 0.80); + } + if (compW > aMaxW) + compW = aMaxW; + if (compH > aMaxH) + compH = aMaxH; + + // Setup the info message + String infoMsg = ""; + if (aInfoMsg != null) + infoMsg = aInfoMsg; + + if (aExp != null) + infoMsg += "\n" + ThreadUtil.getStackTraceClassic(aExp); + + MessagePanel tmpPanel = new MessagePanel(aParent, aTitle, compW, compH); + tmpPanel.setInfo(infoMsg, 0); + tmpPanel.setTabSize(2); + tmpPanel.setVisibleAsModal(); + } + + /** + * Utility method to display failure messages. + *

    + * The stack trace of the {@link Throwable} will be displayed below aInfoMsg. + *

    + * Error panel will have a nominal size of 750, 300. + */ + public static void showFailMessage(Component aParent, String aTitle, String aInfoMsg, Throwable aExp) + { + // Delegate + showFailMessage(aParent, aTitle, aInfoMsg, aExp, 750, 300); + } + +} diff --git a/src/glum/gui/GuiUtil.java b/src/glum/gui/GuiUtil.java index 35e17a9..58943fd 100644 --- a/src/glum/gui/GuiUtil.java +++ b/src/glum/gui/GuiUtil.java @@ -1,7 +1,22 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui; import java.awt.*; import java.awt.event.ActionListener; +import java.awt.event.ItemListener; +import java.awt.image.BufferedImage; import java.util.Collection; import javax.swing.*; @@ -11,28 +26,29 @@ import javax.swing.event.ChangeListener; import glum.gui.icon.IconUtil; import glum.reflect.Function; +/** + * Collection of AWT/Swing utilities. + * + * @author lopeznr1 + */ public class GuiUtil { /** * Method to examine the labels and returns the size of the largest button. */ - public static Dimension computePreferredJButtonSize(String... labels) + public static Dimension computePreferredJButtonSize(String... aLabelArr) { - Dimension tmpDim, maxDim; - JButton tmpB; - - maxDim = null; - tmpB = new JButton(""); + Dimension maxDim = null; + var tmpB = new JButton(""); // Find the label that requires the largest dimension - for (String aStr : labels) + for (String aStr : aLabelArr) { if (aStr == null) aStr = ""; tmpB.setText(aStr); - tmpDim = tmpB.getPreferredSize(); - + var tmpDim = tmpB.getPreferredSize(); if (maxDim == null || maxDim.getWidth() < tmpDim.getWidth()) maxDim = tmpDim; } @@ -45,30 +61,29 @@ public class GuiUtil */ public static JButton createJButton(String aTitle, ActionListener aActionListener) { - JButton tmpB; - - tmpB = new JButton(aTitle); + var tmpB = new JButton(aTitle); tmpB.addActionListener(aActionListener); + return tmpB; } + /** + * Creates the JRadioButton with the following attributes. + *

    + * TODO: Do not remove this method until DistMaker has been updated... + */ public static JButton createJButton(String aTitle, ActionListener aActionListener, Font aFont) { - JButton tmpB; - - tmpB = new JButton(aTitle); + var tmpB = new JButton(aTitle); tmpB.addActionListener(aActionListener); - if (aFont != null) - tmpB.setFont(aFont); + tmpB.setFont(aFont); return tmpB; } public static JButton createJButton(String aTitle, ActionListener aActionListener, Dimension aDimension) { - JButton tmpB; - - tmpB = new JButton(aTitle); + var tmpB = new JButton(aTitle); tmpB.addActionListener(aActionListener); // Force a dimension @@ -89,9 +104,7 @@ public class GuiUtil public static JButton createJButton(Icon aIcon, ActionListener aActionListener, String aToolTip) { - JButton tmpB; - - tmpB = new JButton(aIcon); + var tmpB = new JButton(aIcon); tmpB.addActionListener(aActionListener); if (aToolTip != null) @@ -110,9 +123,7 @@ public class GuiUtil public static JButton createJButtonViaResource(ActionListener aHandler, String aResourcePath, String aToolTip) { - JButton tmpB; - - tmpB = new JButton(IconUtil.loadIcon(aResourcePath)); + var tmpB = new JButton(IconUtil.loadIcon(aResourcePath)); tmpB.addActionListener(aHandler); if (aToolTip != null) @@ -126,47 +137,12 @@ public class GuiUtil */ public static JCheckBox createJCheckBox(String aTitle, ActionListener aActionListener) { - return createJCheckBox(aTitle, aActionListener, null); - } - - public static JCheckBox createJCheckBox(String aTitle, ActionListener aActionListener, Font aFont) - { - JCheckBox tmpCB; - - tmpCB = new JCheckBox(aTitle); + var tmpCB = new JCheckBox(aTitle); tmpCB.addActionListener(aActionListener); - if (aFont != null) - tmpCB.setFont(aFont); return tmpCB; } - /** - * Creates a JComboBox with the specified settings - */ - public static JComboBox createJComboBox(ActionListener aListener, Font aFont, Collection aItemL) - { - JComboBox tmpBox; - - tmpBox = new JComboBox(); - for (G1 aItem : aItemL) - tmpBox.addItem(aItem); - - if (aFont != null) - tmpBox.setFont(aFont); - - tmpBox.addActionListener(aListener); - return tmpBox; - } - -// /** -// * Creates a JComboBox with the specified settings -// */ -// public static JComboBox createJComboBox(ActionListener aListener, Object... itemArr) -// { -// return createJComboBox(aListener, null, itemArr); -// } - /** * Creates a JLabel with the specified settings */ @@ -177,10 +153,7 @@ public class GuiUtil public static JLabel createJLabel(String aTitle, int aAlignment, Font aFont) { - JLabel tmpL; - - tmpL = new JLabel(aTitle, aAlignment); - + var tmpL = new JLabel(aTitle, aAlignment); if (aFont != null) tmpL.setFont(aFont); @@ -188,48 +161,63 @@ public class GuiUtil } /** - * Creates the JRadioButton with the following attributes. + * Creates the {@link JComboBox} with the following attributes. */ - public static JRadioButton createJRadioButton(String aLabel, ActionListener aListener) + public static JComboBox createComboBox(ActionListener aListener, Collection aItemC) { - return createJRadioButton(aLabel, aListener, null); + var retBox = new JComboBox(); + for (G1 aItem : aItemC) + retBox.addItem(aItem); + + retBox.addActionListener(aListener); + + return retBox; } - public static JRadioButton createJRadioButton(String aLabel, ActionListener aListener, Font aFont) + /** + * Creates the JRadioButton with the following attributes. + */ + public static JRadioButton createJRadioButton(ItemListener aListener, String aLabel) { - JRadioButton tmpRB; + var retRB = new JRadioButton(aLabel); + retRB.addItemListener(aListener); - tmpRB = new JRadioButton(aLabel); - tmpRB.addActionListener(aListener); - if (aFont != null) - tmpRB.setFont(aFont); + return retRB; + } - return tmpRB; + /** + * Creates the JRadioButton with the following attributes. + *

    + * TODO: Do not remove this method until DistMaker has been updated... + */ + public static JRadioButton createJRadioButton(ItemListener aListener, String aLabel, Font aFont) + { + var retRB = new JRadioButton(aLabel); + retRB.addItemListener(aListener); + retRB.setFont(aFont); + + return retRB; } /** * Utility method for creating a visual thin divider - *

    + *

    * Typically added to MigLayout (or like manager) with: add(aComp, "growx,h 4!,span,wrap"); */ public static JPanel createDivider() { - JPanel tmpPanel; + var retPanel = new JPanel(); + retPanel.setBorder(new BevelBorder(BevelBorder.RAISED)); - tmpPanel = new JPanel(); - tmpPanel.setBorder(new BevelBorder(BevelBorder.RAISED)); - - return tmpPanel; + return retPanel; } /** - * Creates an uneditable JTextArea with no border, non-opaque, line wrap enabled and word wrap enabled. + * Creates an uneditable JTextArea with the settings: non-opaque, line wrap enabled and word wrap enabled. */ public static JTextArea createUneditableTextArea(int rows, int cols) { - JTextArea tmpTA; - - tmpTA = new JTextArea("", rows, cols); + var tmpTA = new JTextArea("", rows, cols); tmpTA.setEditable(false); tmpTA.setOpaque(false); tmpTA.setLineWrap(true); @@ -243,9 +231,7 @@ public class GuiUtil */ public static JTextPane createUneditableTextPane() { - JTextPane tmpTP; - - tmpTP = new JTextPane(); + var tmpTP = new JTextPane(); tmpTP.setEditable(false); tmpTP.setOpaque(false); tmpTP.setContentType("text/html"); @@ -258,9 +244,7 @@ public class GuiUtil */ public static JTextField createUneditableTextField(String aTitle) { - JTextField tmpTF; - - tmpTF = new JTextField(aTitle); + var tmpTF = new JTextField(aTitle); tmpTF.setBorder(null); tmpTF.setEditable(false); tmpTF.setOpaque(false); @@ -268,134 +252,103 @@ public class GuiUtil return tmpTF; } + /** + * Utility helper method to create a JButton with the specified configuration. + * + * @param aListener + * A Listener registered with the JButton. + * @param aIcon + * The icon associated with the button + */ + public static JButton formButton(ActionListener aListener, Icon aIcon) + { + var retB = new JButton(); + retB.addActionListener(aListener); + retB.setIcon(aIcon); + return retB; + } + + /** + * Utility helper method to create a JButton with the specified configuration. + * + * @param aListener + * A Listener registered with the JButton. + * @param aImage + * The image to be used as an icon. + * @param aToolTip + * The tool tip associated with the JButton. + */ + public static JButton formButton(ActionListener aListener, BufferedImage aImage, String aToolTip) + { + var tmpIcon = new ImageIcon(aImage); + + var retB = new JButton(); + retB.setIcon(tmpIcon); + retB.addActionListener(aListener); + retB.setToolTipText(aToolTip); + + return retB; + } + + /** + * Utility helper method to create a JButton with the specified configuration. + * + * @param aListener + * A Listener registered with the JButton. + * @param aTitle + * The text title of the JButton. + */ + public static JButton formButton(ActionListener aListener, String aTitle) + { + var retB = new JButton(); + retB.addActionListener(aListener); + retB.setText(aTitle); + return retB; + } + + /** + * Utility helper method to create a JToggleButton with the specified configuration. + * + * @param aListener + * A Listener registered with the JButton. + * @param aPriIcon + * The icon to be used as the primary (unselected) icon. + * @param aSecIcon + * The icon to be used when the secondary (selected) icon. + * @param aToolTip + * The tool tip associated with the JToggleButton. + */ + public static JToggleButton formToggleButton(ActionListener aListener, Icon aPriIcon, Icon aSecIcon) + { + var retTB = new JToggleButton(aPriIcon, false); + retTB.setSelectedIcon(aSecIcon); + retTB.addActionListener(aListener); + + return retTB; + } + /** * Utility method to link a set of radio buttons together */ - public static void linkRadioButtons(JRadioButton... buttonArr) + public static ButtonGroup linkRadioButtons(JRadioButton... aButtonArr) { - ButtonGroup tmpGroup; + var retGroup = new ButtonGroup(); + for (var aItem : aButtonArr) + retGroup.add(aItem); - tmpGroup = new ButtonGroup(); - for (JRadioButton aItem : buttonArr) - tmpGroup.add(aItem); + return retGroup; } /** - * Reads a boolean from a string with out throwing a exception + * Utility method to link a set of radio buttons together */ - public static boolean readBoolean(String aStr, boolean aVal) + public static ButtonGroup linkRadioButtons(Collection aButtonC) { - if (aStr == null) - return aVal; + var retGroup = new ButtonGroup(); + for (var aItem : aButtonC) + retGroup.add(aItem); - // Special case for 1 char strings - if (aStr.length() == 1) - { - char aChar; - - aChar = aStr.charAt(0); - if (aChar == 'T' || aChar == 't' || aChar == '1') - return true; - - return false; - } - - try - { - return Boolean.valueOf(aStr).booleanValue(); - } - catch(Exception e) - { - return aVal; - } - } - - /** - * Reads a double from a string with out throwing a exception. Note aStr can have an number of separators: comma - * chars - */ - public static double readDouble(String aStr, double aVal) - { - try - { - aStr = aStr.replace(",", ""); - return Double.parseDouble(aStr); - } - catch(Exception e) - { - return aVal; - } - } - - /** - * Reads a float from a string with out throwing a exception. Note aStr can have an number of separators: comma chars - */ - public static float readFloat(String aStr, float aVal) - { - try - { - aStr = aStr.replace(",", ""); - return Float.parseFloat(aStr); - } - catch(Exception e) - { - return aVal; - } - } - - /** - * Reads an int from a string without throwing a exception Note aStr can have an number of separators: comma chars - */ - public static int readInt(String aStr, int aVal) - { - try - { - aStr = aStr.replace(",", ""); - return Integer.parseInt(aStr); - } - catch(Exception e) - { - return aVal; - } - } - - /** - * Reads a long from a string without throwing a exception Note aStr can have an number of separators: comma chars - */ - public static long readLong(String aStr, long aVal) - { - try - { - aStr = aStr.replace(",", ""); - return Long.parseLong(aStr); - } - catch(Exception e) - { - return aVal; - } - } - - /** - * Reads an int (forced to fit within a range) from a string with out throwing a exception - */ - public static int readRangeInt(String aStr, int minVal, int maxVal, int aVal) - { - int aInt; - - try - { - aInt = Integer.parseInt(aStr); - if (aInt < minVal) - aInt = minVal; - else if (aInt > maxVal) - aInt = maxVal; - - return aInt; - } - catch(Exception e) - { - return aVal; - } + return retGroup; } /** @@ -403,28 +356,27 @@ public class GuiUtil */ public static RootPaneContainer getRootPaneContainer(Component aComponent) { - Container aParent; - // Check to see if the Component is an actual RootPaneContainer if (aComponent instanceof RootPaneContainer) - return (RootPaneContainer)aComponent; + return (RootPaneContainer) aComponent; // Attempt to locate the RootPaneContainer (through our stack) - aParent = aComponent.getParent(); - while (aParent != null && (aParent instanceof RootPaneContainer) == false) - aParent = aParent.getParent(); + var retParent = aComponent.getParent(); + while (retParent != null && (retParent instanceof RootPaneContainer) == false) + retParent = retParent.getParent(); // Bail if we failed to find the RootPaneContainer - if (aParent instanceof RootPaneContainer == false) + if (retParent instanceof RootPaneContainer == false) throw new RuntimeException("No valid (grand)parent associated with GlassPane."); - return (RootPaneContainer)aParent; + return (RootPaneContainer) retParent; } /** * Utility method to locate all of the subcomponents contained in aContainer which are an instance of searchClass */ - public static void locateAllSubComponents(Container aContainer, Collection itemList, Class... searchClassArr) + public static void locateAllSubComponents(Container aContainer, Collection itemList, + Class... searchClassArr) { for (Component aComponent : aContainer.getComponents()) { @@ -438,13 +390,14 @@ public class GuiUtil } if (aComponent instanceof Container) - locateAllSubComponents((Container)aComponent, itemList, searchClassArr); + locateAllSubComponents((Container) aComponent, itemList, searchClassArr); } } /** - * Utility method to force a Component to act as modal while it is visible Source: - * http://stackoverflow.com/questions/804023/how-do-i-simulate-a-modal-dialog-from-within-an-applet + * Utility method to force a Component to act as modal while it is visible + *

    + * Source: http://stackoverflow.com/questions/804023/how-do-i-simulate-a-modal-dialog-from-within-an-applet */ public static void modalWhileVisible(Component aComponent) { @@ -452,27 +405,27 @@ public class GuiUtil if (SwingUtilities.isEventDispatchThread() == false) throw new RuntimeException("Visibility for modal components must be changed via the Event thread."); - synchronized(aComponent) + synchronized (aComponent) { try { - EventQueue theQueue = aComponent.getToolkit().getSystemEventQueue(); + var theQueue = aComponent.getToolkit().getSystemEventQueue(); while (aComponent.isVisible()) { //System.out.println("About to dispatch event... component.isVisible():" + aComponent.isVisible()); - AWTEvent event = theQueue.getNextEvent(); - Object source = event.getSource(); + var event = theQueue.getNextEvent(); + var source = event.getSource(); if (event instanceof ActiveEvent) { - ((ActiveEvent)event).dispatch(); + ((ActiveEvent) event).dispatch(); } else if (source instanceof Component) { - ((Component)source).dispatchEvent(event); + ((Component) source).dispatchEvent(event); } else if (source instanceof MenuComponent) { - ((MenuComponent)source).dispatchEvent(event); + ((MenuComponent) source).dispatchEvent(event); } else { @@ -480,7 +433,7 @@ public class GuiUtil } } } - catch(InterruptedException ignored) + catch (InterruptedException ignored) { } } @@ -489,18 +442,16 @@ public class GuiUtil /** * Utility to call a specific method (methodName) with specific parameters (aParamArr) on aComp and on all of the * child subcomponents. The method will only be called if the components are an instance of refMatchClass. - *

    - * This is useful so that a component and all of its children can be disabled, hidden, etc
    + *

    + * This is useful so that a component and all of its children can be disabled, hidden, etc
    * Example: GuiUtil.callMethod(myPanel, setEnabled, false); - *

    + *

    * Be aware, this is rather expensive, so do not call in time critical applications. */ public static void callMethod(Component aComp, Class refMatchClass, String aMethodName, Object... aParamArr) { - Class[] typeArr; - // Construct the associated type array - typeArr = new Class[0]; + var typeArr = new Class[0]; if (aParamArr.length > 0) { // Determine the types of the specified arguments @@ -520,24 +471,22 @@ public class GuiUtil /** * Helper method to callMethod */ - private static void callMethodHelper(Component aComp, Class refMatchClass, String aMethodName, Class[] aTypeArr, Object[] aParamArr) + private static void callMethodHelper(Component aComp, Class refMatchClass, String aMethodName, + Class[] aTypeArr, Object[] aParamArr) { - Component[] subCompArr; - Function aFunction; - // Locate and call the actual method if (refMatchClass.isInstance(aComp) == true) { try { - aFunction = new Function(aComp, aMethodName, aTypeArr); - aFunction.invoke(aParamArr); + var tmpFunction = new Function(aComp, aMethodName, aTypeArr); + tmpFunction.invoke(aParamArr); } - catch(NoSuchMethodException aExp1) + catch (NoSuchMethodException aExp1) { throw new RuntimeException("Failed to locate valid function. Method:" + aMethodName, aExp1); } - catch(Exception aExp2) + catch (Exception aExp2) { throw new RuntimeException("Failed to execute function. Method:" + aMethodName, aExp2); } @@ -548,34 +497,44 @@ public class GuiUtil return; // Recurse down our children - subCompArr = ((Container)aComp).getComponents(); + var subCompArr = ((Container) aComp).getComponents(); for (Component aSubComp : subCompArr) callMethodHelper(aSubComp, refMatchClass, aMethodName, aTypeArr, aParamArr); } /** - * Utility method to set all subcomponents to the specified enabled mode. + * Utility method to recursively change the enable state of all Components contained by the specified Container. + * + * @param aContainer + * The Container of interest. + * @param aBool + * Boolean used to define the enable state. */ - // TODO: Phase this method out, replace with callMethod() - public static void setEnabled(Component aComp, boolean aBool) + public static void setEnabled(Container aContainer, boolean aBool) { - Component[] subCompArr; - - aComp.setEnabled(aBool); - if (aComp instanceof Container == false) - return; - - subCompArr = ((Container)aComp).getComponents(); - for (Component aSubComp : subCompArr) - GuiUtil.setEnabled(aSubComp, aBool); + for (Component aComp : aContainer.getComponents()) + { + aComp.setEnabled(aBool); + if (aComp instanceof Container) + setEnabled((Container) aComp, aBool); + } } /** - * Utility method to set the enabled switch on all of the specified components. + * Utility method to set the enable state on all of the specified components. */ - public static void setEnabled(boolean aBool, Component... componentArr) + public static void setEnabled(boolean aBool, Component... aComponentArr) { - for (Component aComp : componentArr) + for (Component aComp : aComponentArr) + aComp.setEnabled(aBool); + } + + /** + * Utility method to set the enable state on all of the specified components. + */ + public static void setEnabled(boolean aBool, Collection aComponentC) + { + for (Component aComp : aComponentC) aComp.setEnabled(aBool); } @@ -592,43 +551,48 @@ public class GuiUtil } /** - * Utility method that takes up to 8 buttons and converts all of their selection states to a single byte. + * Utility method that returns a bitmask composed of the selection state of the specified buttons. + *

    + * The array is transformed such that the least significant bit is assumed to have index position 0 with the higher + * bits corresponding to a larger (array) index. + *

    + * A maximum of 32 buttons is support. Passing in more than 32 buttons will result in an exception. */ - public static byte getSelectionStateAsByte(AbstractButton... buttonArr) + public static int getSelectionStateAsBitMask(AbstractButton... aButtonArr) { - byte retByte; + if (aButtonArr.length > 32) + throw new RuntimeException("Improper API call. Max of 32 buttons supported. Passed: " + aButtonArr.length); - if (buttonArr.length > 8) - throw new RuntimeException("Improper API call. Max of 8 buttons supported. Passed: " + buttonArr.length); - - retByte = 0; - for (int c1 = 0; c1 < buttonArr.length; c1++) + int retBitMask = 0; + for (int c1 = 0; c1 < aButtonArr.length; c1++) { - if (buttonArr[c1].isSelected() == true) - retByte |= 1 << c1; + if (aButtonArr[c1].isSelected() == true) + retBitMask |= 1 << c1; } - return retByte; + return retBitMask; } /** - * Utility method that takes up to 8 buttons and configures the selection state of the buttons to match the bit - * pattern of aByte. + * Utility method that updates the selection state of the provided buttons to reflect the specified bitmask. + *

    + * The array is transformed such that the least significant bit is assumed to have index position 0 with the higher + * bits corresponding to a larger (array) index. + *

    + * A maximum of 32 buttons is support. Passing in more than 32 buttons will result in an exception. */ - public static void setSelectionState(byte aByte, AbstractButton... buttonArr) + public static void setSelectionStateFromBitMask(int aBitMask, AbstractButton... aButtonArr) { - boolean aBool; + if (aButtonArr.length > 32) + throw new RuntimeException("Improper API call. Max of 32 buttons supported. Passed: " + aButtonArr.length); - if (buttonArr.length > 8) - throw new RuntimeException("Improper API call. Max of 8 buttons supported. Passed: " + buttonArr.length); - - for (int c1 = 0; c1 < buttonArr.length; c1++) + for (int c1 = 0; c1 < aButtonArr.length; c1++) { - aBool = false; - if (((0x01 << c1) & aByte) != 0) - aBool = true; + var tmpBool = false; + if (((0x01 << c1) & aBitMask) != 0) + tmpBool = true; - buttonArr[c1].setSelected(aBool); + aButtonArr[c1].setSelected(tmpBool); } } @@ -637,9 +601,7 @@ public class GuiUtil */ public static void updateSlider(JSlider aSlider, int aVal) { - ChangeListener[] tmpArr; - - tmpArr = aSlider.getChangeListeners(); + var tmpArr = aSlider.getChangeListeners(); for (ChangeListener aListener : tmpArr) aSlider.removeChangeListener(aListener); @@ -654,13 +616,13 @@ public class GuiUtil * Utility method that checks to ensure the current thread is running on the ATW thread. If it is NOT then the * specified Runnable will be posted so that it is called on the AWT thread. If it is running on the AWT thread then * nothing will happen and this method will return false. - *

    + *

    * Typically this utility method is called at the start of a function to ensure it is on the AWT thread, and if not * then schedule the function onto the AWT thread. Thus it is strongly advisable that if this method returns true the * caller should immediately exit. - *

    + *

    * Typical usage within a method: - * + * *

     	 * public void actionPerformed(aEvent)
     	 * {
    @@ -668,7 +630,7 @@ public class GuiUtil
     	 *    Runnable tmpRunnable = ()-> actionPerformed(aEvent);
     	 *    if (redispatchOnAwtIfNeeded(this, "actionPerformed", aEvent) = true)
     	 *       return;
    -	 *       
    +	 *
     	 *    // Do normal work ...
     	 * }
     	 * 
    diff --git a/src/glum/gui/GuiUtilEx.java b/src/glum/gui/GuiUtilEx.java new file mode 100644 index 0000000..6dbae23 --- /dev/null +++ b/src/glum/gui/GuiUtilEx.java @@ -0,0 +1,88 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui; + +import java.awt.*; + +/** + * Collection of additional AWT/Swing based utility methods. + * + * @author lopeznr1 + */ +public class GuiUtilEx +{ + /** + * Utility method that returns the default dimension for the main window. + *

    + * The returned {@link Dimension} will have a minimum size of [aMinX,aMinY] or [80%,80%] of the screen resolution + * (which ever is smaller). + */ + public static Dimension getDimensionDefaultMain(int aMinX, int aMinY) + { + var screenDim = Toolkit.getDefaultToolkit().getScreenSize(); + int winW = Math.min(aMinX, (int) (screenDim.getWidth() * 0.80)); + int winH = Math.min(aMinY, (int) (screenDim.getHeight() * 0.80)); + return new Dimension(winW, winH); + } + + /** + * Utility method that returns the default dimension for the main window. + *

    + * The returned {@link Dimension} will have a minimum size of [950, 750] or 80% of the screen resolution. + */ + public static Dimension getDimensionDefaultMain() + { + // Delegate + return getDimensionDefaultMain(950, 750); + } + + /** + * Utility method that returns a new Dimension where the size has been scaled by the specified scalars. + * + * @param aDimension + * The original dimension + * @param aScalerX + * Percent to scale the dimensions width. + * @param aScalerY + * Percent to scale the dimensions height. + */ + public static Dimension getDimensionScaled(Dimension aDimension, double aScalerX, double aScalerY) + { + int winW = Math.min(950, (int) (aDimension.getWidth() * aScalerX)); + int winH = Math.min(750, (int) (aDimension.getHeight() * aScalerY)); + return new Dimension(winW, winH); + } + + /** + * Utility method that returns the (first) parent that matches the specified Class. If there is no such component + * then null will be returned. + */ + public static Component getParent(Component aComp, Class aClass) + { + // Search through all the (grand)parents + var parent = aComp.getParent(); + while (parent != null) + { + // Bail once we have a matching class + if (parent.getClass() == aClass) + return parent; + + // Next parent + parent = parent.getParent(); + } + + return null; + } + +} diff --git a/src/glum/gui/TableUtil.java b/src/glum/gui/TableUtil.java new file mode 100644 index 0000000..e959018 --- /dev/null +++ b/src/glum/gui/TableUtil.java @@ -0,0 +1,185 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui; + +import java.util.*; + +import javax.swing.JTable; +import javax.swing.event.*; + +import com.google.common.primitives.Ints; + +import glum.gui.table.TableSorter; +import glum.item.ItemManager; + +/** + * Collection of utilities associated with JTables. + * + * @author lopeznr1 + */ +public class TableUtil +{ + + /** + * Utility method to invert the selection on the specified table. The selection will be inverted with the specified + * ListSelectionListener being ignored. + * + * @param aTable + * The table for which the selection should be inverted. + * @param aIgnoreListener + * An ListSelectionListener that will not receive any intermediate events. It is important that this listener + * be registered with the table's selection model as it will be deregistered and then later registered. If + * null then this argument will be ignored. + */ + public static void invertSelection(JTable aTable, ListSelectionListener aIgnoreListener) + { + Set oldSet = new HashSet<>(); + for (int aId : aTable.getSelectedRows()) + oldSet.add(aId); + + int numRows = aTable.getRowCount(); + int[] tmpArr = new int[numRows - oldSet.size()]; + + // Determine the rows that are to be selected + int idx = 0; + for (int aId = 0; aId < numRows; aId++) + { + // Skip to next if row was previously selected + if (oldSet.contains(aId) == true) + continue; + + tmpArr[idx] = aId; + idx++; + } + + // Delegate + setSortedSelection(aTable, aIgnoreListener, tmpArr); + + // Send out a single event of the change + if (aIgnoreListener != null) + aIgnoreListener.valueChanged(new ListSelectionEvent(aTable, 0, aTable.getRowCount() - 1, false)); + } + + /** + * Utility method to select the rows at the specified indexes. The selection will be updated with the specified + * ListSelectionListener being ignored. + * + * @param aTable + * The table for which the selection should be updated. + * @param aIgnoreListener + * A ListSelectionListener that will not receive any intermediate events. It is important that this listener + * be registered with the table's selection model as it will be deregistered and then later registered. If + * null then this argument will be ignored. + * @param aRowL + * A list of indexes corresponding to the rows that are to be selected. All other rows will be unselected. + */ + public static void setSelection(JTable aTable, ListSelectionListener aIgnoreListener, List aRowL) + { + // Transform to a sorted array + int[] rowArr = Ints.toArray(aRowL); + Arrays.parallelSort(rowArr); + + // Delegate + setSortedSelection(aTable, aIgnoreListener, rowArr); + } + + /** + * Utility method that will synchronize the table selection to match the selected items in the ItemManager. If new + * items were selected then the table will be scrolled (to the first newly selected row). + */ + public static void updateTableSelection(ListSelectionListener aIgnoreListener, ItemManager aManager, + JTable aTable, TableSorter aSortTableModel) + { + // Form a reverse lookup map of item to (view) index + List fullItemL = aManager.getAllItems(); + Map revLookM = new HashMap<>(); + for (int aIdx = 0; aIdx < fullItemL.size(); aIdx++) + { + int tmpIdx = aSortTableModel.viewIndex(aIdx); + revLookM.put(fullItemL.get(aIdx), tmpIdx); + } + + int[] idxArr = aTable.getSelectedRows(); + List oldL = Ints.asList(idxArr); + Set oldS = new LinkedHashSet<>(oldL); + + List newL = new ArrayList<>(); + for (G1 aItem : aManager.getSelectedItems()) + newL.add(revLookM.get(aItem)); + Set newS = new LinkedHashSet<>(newL); + + // Bail if nothing has changed + if (newS.equals(oldS) == true) + return; + + // Update the table's selection + setSelection(aTable, aIgnoreListener, newL); + aTable.repaint(); + } + + /** + * Utility helper method that selects the specified rows. + *

    + * The rows must be in sorted order. + * + * @param aTable + * The table for which the selection should be updated. + * @param aIgnoreListener + * An ListSelectionListener that will not receive any intermediate events. It is important that this listener + * be registered with the table's selection model as it will be deregistered and then later registered. If + * null then this argument will be ignored. + * @param aSortedRowArr + * An array of indexes corresponding to the rows that are to be selected. All other rows will be unselected. + */ + private static void setSortedSelection(JTable aTable, ListSelectionListener aIgnoreListener, int[] aSortedRowArr) + { + // Initial range + int begIdx = -1; + int endIdx = -1; + if (aSortedRowArr.length >= 1) + { + begIdx = aSortedRowArr[0]; + endIdx = begIdx; + } + + if (aIgnoreListener != null) + aTable.getSelectionModel().removeListSelectionListener(aIgnoreListener); + + aTable.clearSelection(); + + for (int aRow : aSortedRowArr) + { + // Expand the range by: +1 + if (aRow == endIdx + 1) + { + endIdx++; + continue; + } + + // Add the current interval + aTable.addRowSelectionInterval(begIdx, endIdx); + + // Start a new range + begIdx = endIdx = aRow; + } + + // Ensure the last interval gets added + if (begIdx != -1 && endIdx != -1) + aTable.addRowSelectionInterval(begIdx, endIdx); + + if (aIgnoreListener != null) + aTable.getSelectionModel().addListSelectionListener(aIgnoreListener); + } + +} diff --git a/src/glum/gui/action/ActionComponentProvider.java b/src/glum/gui/action/ActionComponentProvider.java new file mode 100644 index 0000000..7ca2657 --- /dev/null +++ b/src/glum/gui/action/ActionComponentProvider.java @@ -0,0 +1,33 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.action; + +import java.awt.Component; +import java.util.Collection; + +/** + * Interface that provides a mechanism for a component to declare that it will provide action {@link Component}s + * (typically buttons). + * + * @author lopeznr1 + */ +public interface ActionComponentProvider +{ + + /** + * Returns a list of {@link Component}s that should be placed in the action region. + */ + public Collection getActionButtons(); + +} diff --git a/src/glum/gui/action/ClickAction.java b/src/glum/gui/action/ClickAction.java index 1a230db..a75e16d 100644 --- a/src/glum/gui/action/ClickAction.java +++ b/src/glum/gui/action/ClickAction.java @@ -1,22 +1,44 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.action; -import java.awt.event.*; -import javax.swing.*; +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import javax.swing.AbstractButton; + +/** + * Action used to "click" on the target {@link AbstractButton}. + *

    + * Clicking will be done via the {@link AbstractButton#doClick()} method. + * + * @author lopeznr1 + */ public class ClickAction extends AbstractAction { - // State vars - protected AbstractButton target; + // Reference vars + private final AbstractButton refTarget; public ClickAction(AbstractButton aTarget) { - target = aTarget; + refTarget = aTarget; } @Override public void actionPerformed(ActionEvent e) { - target.doClick(); + refTarget.doClick(); } } diff --git a/src/glum/gui/action/CloseDialog.java b/src/glum/gui/action/CloseDialog.java new file mode 100644 index 0000000..7aed8e9 --- /dev/null +++ b/src/glum/gui/action/CloseDialog.java @@ -0,0 +1,82 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.action; + +import java.awt.Component; +import java.awt.Frame; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; + +import javax.swing.*; + +import glum.gui.GuiUtil; +import net.miginfocom.swing.MigLayout; + +/** + * JDialog that holds a single {@link Component} and a close button. + * + * @author lopeznr1 + */ +public class CloseDialog extends JDialog implements ActionListener +{ + // Gui vars + private final JButton closeB; + + /** + * Standard Constructor + */ + public CloseDialog(Frame aParent, Component aMainComp) + { + super(aParent); + + var tmpPanel = new JPanel(); + tmpPanel.setLayout(new MigLayout("", "[]", "0[]0[]")); + + tmpPanel.add(aMainComp, "growx,growy,pushx,pushy,span,wrap"); + + // Form a unified list of buttons + var tmpActionCompL = new ArrayList(); + if (aMainComp instanceof ActionComponentProvider) + tmpActionCompL.addAll(((ActionComponentProvider) aMainComp).getActionButtons()); + closeB = GuiUtil.formButton(this, "Close"); + tmpActionCompL.add(closeB); + + // Add the components in + var isFirst = true; + for (Component aComp : tmpActionCompL) + { + if (isFirst == true) + tmpPanel.add(aComp, "span,split,ax right"); + else + tmpPanel.add(aComp, ""); + + isFirst = false; + } + + setContentPane(tmpPanel); + setDefaultCloseOperation(JDialog.HIDE_ON_CLOSE); + setModal(false); + pack(); + } + + @Override + public void actionPerformed(ActionEvent aEvent) + { + var source = aEvent.getSource(); + if (source == closeB) + setVisible(false); + } + +} diff --git a/src/glum/gui/action/MakeVisibleAction.java b/src/glum/gui/action/MakeVisibleAction.java index c63fead..1933288 100644 --- a/src/glum/gui/action/MakeVisibleAction.java +++ b/src/glum/gui/action/MakeVisibleAction.java @@ -1,23 +1,55 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.action; -import java.awt.*; -import java.awt.event.*; -import javax.swing.*; +import java.awt.Component; +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; + +/** + * Action used to change the target {@link Component} to be visible. + * + * @author lopeznr1 + */ public class MakeVisibleAction extends AbstractAction { - // State vars - protected Component target; + // Reference vars + private final Component refTarget; + /** + * Standard Constructor + * + * @param aTarget + */ public MakeVisibleAction(Component aTarget) { - target = aTarget; + refTarget = aTarget; + } + + /** + * Returns the reference target. + */ + public Component getTarget() + { + return refTarget; } @Override public void actionPerformed(ActionEvent e) { - target.setVisible(true); + refTarget.setVisible(true); } } diff --git a/src/glum/gui/action/PopAction.java b/src/glum/gui/action/PopAction.java new file mode 100644 index 0000000..aceca5e --- /dev/null +++ b/src/glum/gui/action/PopAction.java @@ -0,0 +1,66 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.action; + +import java.awt.event.ActionEvent; +import java.util.Collection; +import java.util.List; + +import javax.swing.AbstractAction; +import javax.swing.JMenuItem; + +import com.google.common.collect.ImmutableList; + +/** + * Base action specific to popup menus. + *

    + * Whenever the list of selected objects changes this PopAction will be notified. + * + * @author lopeznr1 + */ +public abstract class PopAction extends AbstractAction +{ + // State vars + private ImmutableList itemL; + + /** + * Standard Constructor + */ + public PopAction() + { + itemL = ImmutableList.of(); + } + + /** + * Notification that the {@link PopAction} should be executed on the specified items. + * + * @param aItemL + */ + public abstract void executeAction(List aItemL); + + /** + * Sets in the items that are currently selected. + */ + public void setChosenItems(Collection aItemC, JMenuItem aAssocMI) + { + itemL = ImmutableList.copyOf(aItemC); + } + + @Override + public void actionPerformed(ActionEvent aAction) + { + executeAction(itemL); + } + +} diff --git a/src/glum/gui/action/PopupMenu.java b/src/glum/gui/action/PopupMenu.java new file mode 100644 index 0000000..a391fc4 --- /dev/null +++ b/src/glum/gui/action/PopupMenu.java @@ -0,0 +1,93 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.action; + +import java.awt.Component; +import java.util.*; + +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; + +import glum.item.ItemManager; + +/** + * UI component that allows a custom popup menu to be built. + * + * @author lopeznr1 + */ +public class PopupMenu extends JPopupMenu +{ + // Reference vars + private ItemManager refManager; + + // State vars + private Map> actionM; + + /** + * Standard Constructor + * + * @param aManager + */ + public PopupMenu(ItemManager aManager) + { + refManager = aManager; + + actionM = new HashMap<>(); + } + + /** + * Registers the specified {@link PopAction} into this {@link PopupMenu}. + *

    + * A simple menu item will be created and associated with the specified action. + */ + public void installPopAction(PopAction aAction, String aTitle) + { + JMenuItem tmpMI = new JMenuItem(aAction); + tmpMI.setText(aTitle); + + // Delegate + installPopAction(aAction, tmpMI); + } + + /** + * Registers the specified {@link PopAction} into this {@link PopupMenu}. + *

    + * The action will be associated with the specified menu item. + */ + public void installPopAction(PopAction aAction, JMenuItem aTargMI) + { + add(aTargMI); + actionM.put(aTargMI, aAction); + } + + @Override + public void show(Component aParent, int aX, int aY) + { + // Bail if we do not have selected items + Set tmpS = refManager.getSelectedItems(); + if (tmpS.size() == 0) + return; + + // Update our PopActions + for (JMenuItem aMI : actionM.keySet()) + { + PopAction tmpPA = actionM.get(aMI); + tmpPA.setChosenItems(tmpS, aMI); + } + + // Delegate + super.show(aParent, aX, aY); + } + +} diff --git a/src/glum/gui/component/GBaseTextField.java b/src/glum/gui/component/GBaseTextField.java new file mode 100644 index 0000000..07657d2 --- /dev/null +++ b/src/glum/gui/component/GBaseTextField.java @@ -0,0 +1,80 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.component; + +import java.awt.*; + +import javax.swing.JTextField; + +/** + * User interface input used to capture an input string. + *

    + * This object provides the following additional features: + *

      + *
    • Display of passive hint text + *
    + * + * @author lopeznr1 + */ +public class GBaseTextField extends JTextField +{ + // State vars + private String mHint; + + /** Standard Constructor */ + public GBaseTextField(String aText, int aNumColumns) + { + super(aText, aNumColumns); + + mHint = null; + } + + /** + * Sets a hint that will be shown whenever the text field is empty. + */ + public void setHint(String aHint) + { + mHint = aHint; + repaint(); + } + + @Override + public void paint(Graphics g) + { + super.paint(g); + + // Bail if there is already input + if (getText().length() != 0) + return; + + // Bail if there is no hint + if (mHint == null || mHint.length() == 0) + return; + + // Draw the textual hint + // Source: + // https://stackoverflow.com/questions/1738966/java-jtextfield-with-input-hint + int h = getHeight(); + ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + Insets ins = getInsets(); + FontMetrics fm = g.getFontMetrics(); + int c0 = getBackground().getRGB(); + int c1 = getForeground().getRGB(); + int m = 0xfefefefe; + int c2 = ((c0 & m) >>> 1) + ((c1 & m) >>> 1); + g.setColor(new Color(c2, true)); + g.drawString(mHint, ins.left, h / 2 + fm.getAscent() / 2 - 2); + } + +} diff --git a/src/glum/gui/component/GComboBox.java b/src/glum/gui/component/GComboBox.java index 4682a0e..e756a73 100644 --- a/src/glum/gui/component/GComboBox.java +++ b/src/glum/gui/component/GComboBox.java @@ -1,58 +1,81 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.component; -import glum.gui.component.model.GComboBoxModel; - import java.awt.event.ActionListener; -import java.util.*; +import java.util.Collection; +import java.util.List; + import javax.swing.JComboBox; import javax.swing.ListCellRenderer; +import com.google.common.collect.ImmutableList; + +import glum.gui.component.model.GComboBoxModel; + +/** + * Enhanced implementation of {@link JComboBox}. This implementation provides for better generics support. Please + * utilize the {@link #getChosenItem()}, {@link #setChosenItem(Object)}, etc... + * + * @author lopeznr1 + */ public class GComboBox extends JComboBox { // State vars - protected GComboBoxModel itemModel; - + private GComboBoxModel itemModel; + public GComboBox() { - itemModel = new GComboBoxModel(new LinkedList()); + itemModel = new GComboBoxModel(ImmutableList.of()); setModel(itemModel); } - + public GComboBox(ActionListener aListener, ListCellRenderer aRenderer) { this(); - + addActionListener(aListener); setRenderer(aRenderer); } - + public GComboBox(ActionListener aListener) { this(); - + addActionListener(aListener); } - - public GComboBox(ActionListener aListener, List aItemList) + + public GComboBox(ActionListener aListener, Collection aItemC) { - itemModel = new GComboBoxModel(aItemList); + itemModel = new GComboBoxModel<>(aItemC); setModel(itemModel); addActionListener(aListener); } - + public GComboBox(ActionListener aListener, G1... aItemArr) { - itemModel = new GComboBoxModel(aItemArr); + itemModel = new GComboBoxModel<>(aItemArr); setModel(itemModel); - + addActionListener(aListener); } /** * Returns the list of all items stored in the GComboBox */ - public ArrayList getAllItems() + public List getAllItems() { return itemModel.getAllItems(); } @@ -64,43 +87,43 @@ public class GComboBox extends JComboBox { return itemModel.getSelectedItem(); } - + /** * Sets in the currently selected item. This method will not trigger an ActionEvent. + * * @see JComboBox#setSelectedItem */ public void setChosenItem(G1 aItem) { - ActionListener[] listenerArr; - - listenerArr = getActionListeners(); + var listenerArr = getActionListeners(); for (ActionListener aListener : listenerArr) removeActionListener(aListener); - + itemModel.setSelectedItem(aItem); super.setSelectedItem(aItem); for (ActionListener aListener : listenerArr) addActionListener(aListener); - // We must force a repaint since any ActionListener responsible will never get the update + // We must force a repaint since any ActionListener responsible will never get the update repaint(); } - + /** * Note aItem must be of the generified type. This method will not trigger an ActionEvent. */ - @Override @SuppressWarnings("unchecked") + @Override + @SuppressWarnings("unchecked") public void addItem(Object aItem) { ActionListener[] listenerArr; - + listenerArr = getActionListeners(); for (ActionListener aListener : listenerArr) removeActionListener(aListener); - - itemModel.addItem((G1)aItem); - + + itemModel.addItem((G1) aItem); + for (ActionListener aListener : listenerArr) addActionListener(aListener); } @@ -110,15 +133,29 @@ public class GComboBox extends JComboBox */ public void addItems(G1... aItemArr) { - ActionListener[] listenerArr; - - listenerArr = getActionListeners(); + var listenerArr = getActionListeners(); for (ActionListener aListener : listenerArr) removeActionListener(aListener); - + for (G1 aItem : aItemArr) itemModel.addItem(aItem); - + + for (ActionListener aListener : listenerArr) + addActionListener(aListener); + } + + /** + * Adds all of the specified items to the model. This method will not trigger an ActionEvent. + */ + public void addItems(Collection aItemC) + { + var listenerArr = getActionListeners(); + for (ActionListener aListener : listenerArr) + removeActionListener(aListener); + + for (G1 aItem : aItemC) + itemModel.addItem(aItem); + for (ActionListener aListener : listenerArr) addActionListener(aListener); } @@ -126,17 +163,68 @@ public class GComboBox extends JComboBox /** * Note aItem must be of the generified type. This method will not trigger an ActionEvent. */ - @Override @SuppressWarnings("unchecked") + @Override + @SuppressWarnings("unchecked") public void removeItem(Object aItem) { - ActionListener[] listenerArr; - - listenerArr = getActionListeners(); + var listenerArr = getActionListeners(); for (ActionListener aListener : listenerArr) removeActionListener(aListener); - - itemModel.removeItem((G1)aItem); - + + itemModel.removeItem((G1) aItem); + + for (ActionListener aListener : listenerArr) + addActionListener(aListener); + } + + /** + * Method to replace all of the items with the specified list of items. + *

    + * If the current selection is in the replacement list then the selection will be maintained otherwise it will be + * replaced with the first item in the list. + *

    + * This method will not trigger an ActionEvent. + */ + public void replaceAllItems(Collection aItemC) + { + G1 pickItem = getChosenItem(); + + var listenerArr = getActionListeners(); + for (ActionListener aListener : listenerArr) + removeActionListener(aListener); + + // Replace the items + itemModel.removeAllItems(); + + for (G1 aItem : aItemC) + itemModel.addItem(aItem); + + // Update the selection after the replacement + if (aItemC.contains(pickItem) == false) + pickItem = null; + if (pickItem == null && aItemC.size() > 0) + pickItem = aItemC.iterator().next(); + + setChosenItem(pickItem); + + for (ActionListener aListener : listenerArr) + addActionListener(aListener); + } + + /** + * Method to replace the original item with the provided replacement. + *

    + * This method will not trigger an ActionEvent. + */ + public void replaceItem(G1 aOrigItem, G1 aReplItem) + { + var listenerArr = getActionListeners(); + for (ActionListener aListener : listenerArr) + removeActionListener(aListener); + + // Delegate + itemModel.replaceItem(aOrigItem, aReplItem); + for (ActionListener aListener : listenerArr) addActionListener(aListener); } @@ -147,14 +235,12 @@ public class GComboBox extends JComboBox @Override public void removeAllItems() { - ActionListener[] listenerArr; - - listenerArr = getActionListeners(); + var listenerArr = getActionListeners(); for (ActionListener aListener : listenerArr) removeActionListener(aListener); - + itemModel.removeAllItems(); - + for (ActionListener aListener : listenerArr) addActionListener(aListener); } @@ -166,7 +252,7 @@ public class GComboBox extends JComboBox // { // throw new RuntimeException("Unsupported operation. Call getChosenItem()"); // } -// +// // @Override // public void setSelectedItem(Object aObj) // { diff --git a/src/glum/gui/component/GComponent.java b/src/glum/gui/component/GComponent.java index 6b26ae1..0bf23e9 100644 --- a/src/glum/gui/component/GComponent.java +++ b/src/glum/gui/component/GComponent.java @@ -1,32 +1,49 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.component; -import glum.gui.panel.generic.GenericCodes; - import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.util.ArrayList; import java.util.List; + import javax.swing.JComponent; -import com.google.common.collect.Lists; +import glum.gui.panel.generic.GenericCodes; +/** + * Base component that provides support for {@link ActionListener} event mechanism. + * + * @author lopeznr1 + */ public class GComponent extends JComponent implements GenericCodes { // State vars - protected List myListeners; + protected List listenerL; + /** Standard Constructor */ public GComponent() { - super(); - - myListeners = Lists.newLinkedList(); + listenerL = new ArrayList<>(); } - + /** * Add an ActionListener to this GPanel */ public void addActionListener(ActionListener aListener) { - myListeners.add(aListener); + listenerL.add(aListener); } /** @@ -34,16 +51,16 @@ public class GComponent extends JComponent implements GenericCodes */ public void removeActionListener(ActionListener aListener) { - myListeners.remove(aListener); + listenerL.remove(aListener); } - + /** * Send out notification to all of the ActionListeners */ public void notifyListeners(Object aSource, int aId, String aCommand) { - for (ActionListener aListener : myListeners) + for (var aListener : listenerL) aListener.actionPerformed(new ActionEvent(aSource, aId, aCommand)); } - + } diff --git a/src/glum/gui/component/GFancyLabel.java b/src/glum/gui/component/GFancyLabel.java index c787cc0..2f6e06c 100644 --- a/src/glum/gui/component/GFancyLabel.java +++ b/src/glum/gui/component/GFancyLabel.java @@ -1,25 +1,37 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.component; import java.awt.Dimension; import java.awt.Font; +import javax.swing.JLabel; + import glum.unit.UnitListener; import glum.unit.UnitProvider; -import javax.swing.JLabel; - public class GFancyLabel extends JLabel implements UnitListener { // State vars - protected UnitProvider[] unitProviderArr; - protected String[] formatStrArr; - protected Object[] refValueArr; + private UnitProvider[] unitProviderArr; + private String[] formatStrArr; + private Object[] refValueArr; /** - * Fancy JLabel which provides auto formatting of objects. The constructor - * is provided with a formatStr which has unit place holders specified by "%u" - * There must be a corresponding UnitProvider for each occurrence of "%u". This - * is provided via the variable arguments of UnitProvider. + * Fancy JLabel which provides auto formatting of objects. The constructor is provided with a formatStr which has + * unit place holders specified by "%u" There must be a corresponding UnitProvider for each occurrence of "%u". This + * is provided via the variable arguments of UnitProvider. */ public GFancyLabel(String formatStr, UnitProvider... aUnitProviderArr) { @@ -27,27 +39,25 @@ public class GFancyLabel extends JLabel implements UnitListener } /** - * Fancy JLabel which provides auto formatting of objects. The constructor - * is provided with a formatStr which has unit place holders specified by "%u" - * There must be a corresponding UnitProvider for each occurrence of "%u". This - * is provided via the variable arguments of UnitProvider. + * Fancy JLabel which provides auto formatting of objects. The constructor is provided with a formatStr which has + * unit place holders specified by "%u" There must be a corresponding UnitProvider for each occurrence of "%u". This + * is provided via the variable arguments of UnitProvider. */ public GFancyLabel(Font aFont, String aFormatStr, UnitProvider... aUnitProviderArr) { - super(); - if (aFont != null) setFont(aFont); - + formatStrArr = aFormatStr.split("%u", -1); - + unitProviderArr = aUnitProviderArr; for (UnitProvider aUnitProvider : unitProviderArr) aUnitProvider.addListener(this); - + // Insanity check if (unitProviderArr.length != formatStrArr.length - 1) - throw new RuntimeException("Num place holders: " + (formatStrArr.length - 1) + " Num units: " + unitProviderArr.length); + throw new RuntimeException( + "Num place holders: " + (formatStrArr.length - 1) + " Num units: " + unitProviderArr.length); refValueArr = new Object[unitProviderArr.length]; for (int c1 = 0; c1 < unitProviderArr.length; c1++) @@ -63,31 +73,30 @@ public class GFancyLabel extends JLabel implements UnitListener } /** - * Method to set in the set of values which will be formatted with the associated UnitProviders - * which were specified via the constructor. + * Method to set in the set of values which will be formatted with the associated UnitProviders which were specified + * via the constructor. */ public void setValues(Object... aValueArr) { - String aStr; - // Ensure the number of objects matches the number of units if (unitProviderArr.length != aValueArr.length) - throw new RuntimeException("Inproper number of arguments. Expected: " + unitProviderArr.length + " Recieved:" + aValueArr.length); - + throw new RuntimeException("Inproper number of arguments. Expected: " + unitProviderArr.length // + + " Recieved:" + aValueArr.length); + for (int c1 = 0; c1 < aValueArr.length; c1++) refValueArr[c1] = aValueArr[c1]; - - aStr = ""; + + var tmpStr = ""; for (int c1 = 0; c1 < aValueArr.length; c1++) { - aStr += formatStrArr[c1]; - aStr += unitProviderArr[c1].getUnit().getString(aValueArr[c1], false); + tmpStr += formatStrArr[c1]; + tmpStr += unitProviderArr[c1].getUnit().getString(aValueArr[c1], false); } - - if (formatStrArr.length > aValueArr.length) - aStr += formatStrArr[formatStrArr.length - 1]; - setText(aStr); + if (formatStrArr.length > aValueArr.length) + tmpStr += formatStrArr[formatStrArr.length - 1]; + + setText(tmpStr); } } diff --git a/src/glum/gui/component/GLabel.java b/src/glum/gui/component/GLabel.java index 005ba91..0feb674 100644 --- a/src/glum/gui/component/GLabel.java +++ b/src/glum/gui/component/GLabel.java @@ -1,15 +1,25 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.component; import java.awt.Dimension; import java.awt.Font; -import glum.unit.ConstUnitProvider; -import glum.unit.Unit; -import glum.unit.UnitListener; -import glum.unit.UnitProvider; - import javax.swing.JLabel; +import glum.unit.*; + public class GLabel extends JLabel implements UnitListener { // State vars @@ -34,8 +44,6 @@ public class GLabel extends JLabel implements UnitListener public GLabel(UnitProvider aUnitProvider, Font aFont, boolean aShowLabel) { - super(); - refUnitProvider = aUnitProvider; if (refUnitProvider != null) refUnitProvider.addListener(this); @@ -62,9 +70,9 @@ public class GLabel extends JLabel implements UnitListener { String aStr; Unit aUnit; - + refValue = aValue; - + aUnit = null; if (refUnitProvider != null) aUnit = refUnitProvider.getUnit(); diff --git a/src/glum/gui/component/GList.java b/src/glum/gui/component/GList.java index fbe2491..ad30bc8 100644 --- a/src/glum/gui/component/GList.java +++ b/src/glum/gui/component/GList.java @@ -1,22 +1,30 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.component; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import javax.swing.JComponent; -import javax.swing.JList; -import javax.swing.SwingUtilities; +import java.util.*; + +import javax.swing.*; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import glum.gui.component.model.GListModel; import glum.gui.panel.generic.GenericCodes; -import com.google.common.collect.Lists; - public class GList extends JComponent implements GenericCodes, ListSelectionListener { // Gui vars @@ -52,9 +60,7 @@ public class GList extends JComponent implements GenericCodes, ListSelection */ public G1 getSelectedItem() { - int selectedIndex; - - selectedIndex = refList.getSelectedIndex(); + var selectedIndex = refList.getSelectedIndex(); if (selectedIndex == -1) return null; @@ -66,17 +72,14 @@ public class GList extends JComponent implements GenericCodes, ListSelection */ public List getSelectedItems() { - ArrayList retList; - int[] indexArr; + var indexArr = refList.getSelectedIndices(); - indexArr = refList.getSelectedIndices(); - - retList = Lists.newArrayList(); + var retItemL = new ArrayList(); for (int aIndex : indexArr) - retList.add(refModel.getElementAt(aIndex)); + retItemL.add(refModel.getElementAt(aIndex)); - retList.trimToSize(); - return retList; + retItemL.trimToSize(); + return retItemL; } /** @@ -104,17 +107,14 @@ public class GList extends JComponent implements GenericCodes, ListSelection */ public void setSelectedItems(List aItemList) { - int[] idArr; - int c1; - // Ensure we are executed only on the proper thread if (SwingUtilities.isEventDispatchThread() == false) throw new RuntimeException("GList.selectItems() not executed on the AWT event dispatch thread."); refList.removeListSelectionListener(this); - c1 = 0; - idArr = new int[aItemList.size()]; + var c1 = 0; + var idArr = new int[aItemList.size()]; for (G1 aItem : aItemList) { idArr[c1] = refModel.indexOf(aItem); diff --git a/src/glum/gui/component/GNumberField.java b/src/glum/gui/component/GNumberField.java index 5ed1f85..f79f740 100644 --- a/src/glum/gui/component/GNumberField.java +++ b/src/glum/gui/component/GNumberField.java @@ -1,56 +1,86 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.component; import java.awt.Color; import java.awt.event.ActionListener; -import javax.swing.*; -import javax.swing.event.*; -import javax.swing.text.*; +import java.text.DecimalFormat; +import java.text.NumberFormat; + +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.Document; + +import com.google.common.collect.Range; import glum.gui.document.NumberDocument; -import glum.unit.ConstUnitProvider; -import glum.unit.NumberUnit; -import glum.unit.Unit; -import glum.unit.UnitListener; -import glum.unit.UnitProvider; +import glum.unit.*; -public class GNumberField extends JTextField implements DocumentListener, UnitListener +/** + * User interface input used to capture an individual numerical input (type: double). + *

    + * Unlike JTextField, users of this class should not use getText() / setText() but rather getValue() / setValue() + * methods. Also it should not be necessary to register DocumentListeners - rather an ActionListener should be + * sufficient. + *

    + * This class provides two modes of converting model values to textual input: + *

      + *
    • {@link NumberFormat} mechanism + *
    • {@link UnitProvider} mechanism + *
    + * + * @author lopeznr1 + */ +public class GNumberField extends GBaseTextField implements DocumentListener, UnitListener { + // Attributes + private final NumberFormat refFormat; + private final UnitProvider refUnitProvider; + // State vars - protected UnitProvider refUnitProvider; - protected double currValue, minValue, maxValue; - protected boolean isMutating; + private Range minMaxRange; + private double currValue; + private boolean isMutating; // Gui vars - protected Color failColor, passColor; - protected NumberDocument myDocument; + private Color colorFail, colorPass; + private NumberDocument myDocument; /** - * Constructor - * + * Standard Constructor + * * @param aListener - * : Default ActionListener + * An ActionListener that will be notified when ever the user makes any input changes. * @param aUnit - * : Object used to format programatic entered values. Note aUnitProvider will also be used to determine if - * Floating or only Integral input is allowed. - * @param inputType - * : Type of input to accept (Integer, Double, etc...) - * @param aMinVal - * : Minimum value to accept - * @param aMaxVal - * : Maximum value to accept + * Object used to format programmatic entered values. Note aUnitProvider will also be used to determine if + * Floating or only Integral input is allowed. + * @param aMinMaxRange + * The range of values to accept. */ - public GNumberField(ActionListener aListener, UnitProvider aUnitProvider, double aMinVal, double aMaxVal) + public GNumberField(ActionListener aListener, UnitProvider aUnitProvider, Range aMinMaxRange) { super("", 0); + refFormat = null; refUnitProvider = aUnitProvider; - currValue = 0; - minValue = aMinVal; - maxValue = aMaxVal; + + minMaxRange = aMinMaxRange; + currValue = Double.NaN; isMutating = false; - failColor = Color.RED.darker(); - passColor = getForeground(); + colorFail = Color.RED.darker(); + colorPass = getForeground(); // Register the ActionListener if (aListener != null) @@ -69,31 +99,82 @@ public class GNumberField extends JTextField implements DocumentListener, UnitLi installUnit(); } - public GNumberField(ActionListener aListener, Unit aUnit, double aMinVal, double aMaxVal) + /** + * Standard Constructor + * + * @param aListener + * An ActionListener that will be notified when ever the user makes any input changes. + * @param aFormat + * NumberFormat used to transform the numerical value to a string. + * @param aMinMaxRange + * The range of values to accept. + */ + public GNumberField(ActionListener aListener, NumberFormat aFormat, Range aMinMaxRange) { - this(aListener, new ConstUnitProvider(aUnit), aMinVal, aMaxVal); + super("", 0); + + refFormat = aFormat; + refUnitProvider = null; + + minMaxRange = aMinMaxRange; + currValue = Double.NaN; + isMutating = false; + + colorFail = Color.RED.darker(); + colorPass = getForeground(); + + // Form the appropriate Document and initialize + myDocument = new NumberDocument(this, false); + super.setDocument(myDocument); + + // Register the ActionListener + if (aListener != null) + addActionListener(aListener); + + // Register for events of interest + myDocument.addDocumentListener(this); + + // Force the UI component to reflect the currValue. Note this is done last since this method + // assumes the GNumberField is already registered with myDocument. + forceTF(currValue); } /** - * Returns whether the current input is valid + * Simplified Constructor + * + * @param aListener + * An ActionListener that will be notified when ever the user makes any input changes. + * @param aUnit + * @param aMinMaxRange + * The range of values to accept. */ - public boolean isValidInput() + public GNumberField(ActionListener aListener, Unit aUnit, Range aMinMaxRange) { - Unit aUnit; - double modelVal; + this(aListener, new ConstUnitProvider(aUnit), aMinMaxRange); + } - // Ensure we have valid input - aUnit = refUnitProvider.getUnit(); + /** + * Simplified Constructor + * + * @param aListener + * An ActionListener that will be notified when ever the user makes any input changes. + * @param aFormat + * NumberFormat used to transform the numerical value to a string. + */ + public GNumberField(ActionListener aListener, NumberFormat aFormat) + { + this(aListener, aFormat, Range.closed(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY)); + } - modelVal = aUnit.parseString(this.getText(), Double.NaN); - if (Double.isNaN(modelVal) == true) - return false; - - // Ensure the value is within range - if (modelVal < minValue || modelVal > maxValue) - return false; - - return true; + /** + * Simplified Constructor + * + * @param aListener + * An ActionListener that will be notified when ever the user makes any input changes. + */ + public GNumberField(ActionListener aListener) + { + this(aListener, new DecimalFormat("#.###"), Range.closed(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY)); } /** @@ -110,6 +191,30 @@ public class GNumberField extends JTextField implements DocumentListener, UnitLi myDocument.addDocumentListener(this); } + /** + * Returns the {@link Color} used to change foreground text whenever invalid input is entered. + */ + public Color getColorFail() + { + return colorFail; + } + + /** + * Returns the {@link Color} used to change foreground text whenever valid input is entered. + */ + public Color getColorPass() + { + return colorPass; + } + + /** + * Returns the range of valid values. + */ + public Range getMinMaxRange() + { + return minMaxRange; + } + /** * Returns the currently stored model value */ @@ -120,20 +225,65 @@ public class GNumberField extends JTextField implements DocumentListener, UnitLi /** * Returns the currently stored model value as an integer. If the modelValue is NaN, then errorVal will be returned. - * The values MaxInt, MinInt are returned for Infinity. + * The values MAX_VALUE will be returned for +Infinity. The value MIN_VALUE will be returned for -Infinity. */ - public int getValueAsInt(int errorVal) + public long getValueAsLong(long aErrorVal) { if (Double.isNaN(currValue) == true) - return errorVal; + return aErrorVal; - return (int)currValue; + return (long) currValue; + } + + /** + * Returns the currently stored model value as an integer. If the modelValue is NaN, then errorVal will be returned. + * The values MAX_VALUE will be returned for +Infinity. The value MIN_VALUE will be returned for -Infinity. + */ + public int getValueAsInt(int aErrorVal) + { + if (Double.isNaN(currValue) == true) + return aErrorVal; + + return (int) currValue; + } + + /** + * Returns whether the current input is valid + */ + public boolean isValidInput() + { + // Ensure we have valid input + double modelVal = transformToModel(this.getText()); + if (Double.isNaN(modelVal) == true) + return false; + + // Ensure the value is within range + if (minMaxRange.contains(modelVal) == false) + return false; + + return true; + } + + /** + * Sets the {@link Color} used to indicate invalid input is entered. + */ + public void setColorFail(Color aColor) + { + colorFail = aColor; + } + + /** + * Sets the {@link Color} used to indicate valid input is entered. + */ + public void setColorPass(Color aColor) + { + colorPass = aColor; } /** * Takes in a model value and will display it with respect to the active unit. This method will not trigger an * ActionEvent. - *

    + *

    * Note this method will do nothing if the UI is being "mutated" when this method is called. */ public void setValue(final double aValue) @@ -143,6 +293,16 @@ public class GNumberField extends JTextField implements DocumentListener, UnitLi if (isMutating == true) return; + // Bail if the value has not changed. We do this so that user + // entered input will not change if the model value has not changed. + double ulp = Math.ulp(aValue); + boolean ignoreInput = true; + ignoreInput &= Double.isNaN(ulp) == false; + ignoreInput &= Double.isFinite(ulp) == true; + ignoreInput &= Math.abs(currValue - aValue) < ulp; + if (ignoreInput == true) + return; + // Simple edit if we are not currently being mutated forceTF(aValue); updateGui(); @@ -151,28 +311,26 @@ public class GNumberField extends JTextField implements DocumentListener, UnitLi /** * Changes the range of acceptable values (in model units). Note the current value will be force to fit this range. */ - public void setMinMaxValue(double aMinValue, double aMaxValue) + public void setMinMaxRange(Range aMinMaxRange) { - Unit aUnit; - - minValue = aMinValue; - maxValue = aMaxValue; - if (currValue < minValue || currValue > maxValue) - currValue = minValue; + minMaxRange = aMinMaxRange; + if (minMaxRange.hasLowerBound() == true && currValue < minMaxRange.lowerEndpoint()) + currValue = minMaxRange.lowerEndpoint(); + else if (minMaxRange.hasUpperBound() == true && currValue > minMaxRange.upperEndpoint()) + currValue = minMaxRange.upperEndpoint(); // Update our document - aUnit = refUnitProvider.getUnit(); - myDocument.setMinMaxValue(aUnit.toUnit(minValue), aUnit.toUnit(maxValue)); - } - - @Override - public void setDocument(Document aDoc) - { -// throw new UnsupportedOperationException(); - if (aDoc != null) - aDoc.addDocumentListener(this); - - super.setDocument(aDoc); + double minValue = minMaxRange.lowerEndpoint(); + double maxValue = minMaxRange.upperEndpoint(); + if (refUnitProvider != null) + { + Unit tmpUnit = refUnitProvider.getUnit(); + myDocument.setMinMaxValue(tmpUnit.toUnit(minValue), tmpUnit.toUnit(maxValue)); + } + else + { + myDocument.setMinMaxValue(minValue, maxValue); + } } @Override @@ -193,6 +351,16 @@ public class GNumberField extends JTextField implements DocumentListener, UnitLi syncValue(aEvent); } + @Override + public void setDocument(Document aDoc) + { +// throw new UnsupportedOperationException(); + if (aDoc != null) + aDoc.addDocumentListener(this); + + super.setDocument(aDoc); + } + @Override public void unitChanged(UnitProvider aProvider, String aKey) { @@ -204,14 +372,11 @@ public class GNumberField extends JTextField implements DocumentListener, UnitLi */ protected void forceTF(double aValue) { - Unit aUnit; - String aStr; - // Save off the new model value, and check the validity currValue = aValue; - if (currValue < minValue || currValue > maxValue) + if (minMaxRange.contains(currValue) == false) currValue = Double.NaN; -// throw new RuntimeException("Programatic input is invalid. Is unit compatible? Input: " + aValue); +// throw new RuntimeException("Programmatic input is invalid. Is unit compatible? Input: " + aValue); // Invalid values shall just clear the text field and bail if (Double.isNaN(currValue) == true) @@ -220,13 +385,12 @@ public class GNumberField extends JTextField implements DocumentListener, UnitLi return; } - // Convert from model value to (unit) textual format - aUnit = refUnitProvider.getUnit(); - aStr = aUnit.getString(currValue); + // Convert from model value to text + String tmpStr = transformToString(currValue); // Update the GUI internals myDocument.removeDocumentListener(this); - setText(aStr); + setText(tmpStr); setCaretPosition(0); myDocument.addDocumentListener(this); } @@ -236,20 +400,17 @@ public class GNumberField extends JTextField implements DocumentListener, UnitLi */ protected void installUnit() { - Unit aUnit; - boolean aBool; - // Ensure that we have a valid Unit - aUnit = refUnitProvider.getUnit(); - if (aUnit instanceof NumberUnit == false) - throw new RuntimeException("refUnitProvider must return a Unit of type NumberUnit. Unit: " + aUnit); + Unit tmpUnit = refUnitProvider.getUnit(); + if (tmpUnit instanceof NumberUnit == false) + throw new RuntimeException("refUnitProvider must return a Unit of type NumberUnit. Unit: " + tmpUnit); // Update our Document to reflect whether this Unit supports floating point numbers - aBool = (aUnit instanceof NumberUnit) && (((NumberUnit)aUnit).isFloating() == true); - myDocument.setAllowFloats(aBool); + boolean tmpBool = (tmpUnit instanceof NumberUnit) && (((NumberUnit) tmpUnit).isFloating() == true); + myDocument.setAllowFloats(tmpBool); // Update the Document's MinMax values reflect the new Unit - setMinMaxValue(minValue, maxValue); + setMinMaxRange(minMaxRange); // Force myDocument's text to match the new unit forceTF(currValue); @@ -258,19 +419,16 @@ public class GNumberField extends JTextField implements DocumentListener, UnitLi /** * Keeps the "model" value conceptually linked to the GUI component. It will also trigger the actionEventListeners. */ - protected void syncValue(DocumentEvent e) + protected void syncValue(DocumentEvent aEvent) { - Unit aUnit; - // Mark ourself as mutating isMutating = true; - // Convert the textual (unit) value to the model value - aUnit = refUnitProvider.getUnit(); - currValue = aUnit.parseString(this.getText(), Double.NaN); + // Convert the string to the model value + currValue = transformToModel(this.getText()); // If the value is not in range then, it is invalid - if (currValue < minValue || currValue > maxValue) + if (minMaxRange.contains(currValue) == false) currValue = Double.NaN; // Notify our listeners and update the GUI @@ -286,13 +444,55 @@ public class GNumberField extends JTextField implements DocumentListener, UnitLi */ protected void updateGui() { - Color aColor; - - aColor = passColor; + Color tmpColor = colorPass; if (isValidInput() == false) - aColor = failColor; + tmpColor = colorFail; - setForeground(aColor); + setForeground(tmpColor); + } + + /** + * Helper method that will take a given value and convert it to a string. + * + * @param aValue + */ + private String transformToString(double aValue) + { + // Convert from model value to (unit) textual format + if (refUnitProvider != null) + { + Unit tmpUnit = refUnitProvider.getUnit(); + String tmpStr = tmpUnit.getString(aValue); + return tmpStr; + } + + return refFormat.format(aValue); + } + + /** + * Helper method that will take a String and convert it to the equivalent numerical value. On failure Double.NaN will + * be returned. + * + * @param aValue + */ + private double transformToModel(String aStr) + { + // Convert the textual (unit) value to the model value + if (refUnitProvider != null) + { + Unit tmpUnit = refUnitProvider.getUnit(); + double retValue = tmpUnit.parseString(aStr, Double.NaN); + return retValue; + } + + try + { + return Double.parseDouble(aStr); + } + catch (NumberFormatException aExp) + { + return Double.NaN; + } } } diff --git a/src/glum/gui/component/GNumberFieldSlider.java b/src/glum/gui/component/GNumberFieldSlider.java new file mode 100644 index 0000000..c1b8eb4 --- /dev/null +++ b/src/glum/gui/component/GNumberFieldSlider.java @@ -0,0 +1,218 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.component; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.text.NumberFormat; + +import javax.swing.JPanel; + +import com.google.common.collect.Range; + +import glum.gui.GuiUtil; +import net.miginfocom.swing.MigLayout; + +/** + * User interface component that combines a {@link GNumberField} and a {@link GSlider} into a single unified component. + * + * @author lopeznr1 + */ +public class GNumberFieldSlider extends JPanel implements ActionListener +{ + // Ref vars + private final ActionListener refLister; + + // Gui vars + private GNumberField valueNF; + private GSlider valueS; + + /** + * Standard Constructor + * + * @param aListener + * An ActionListener that will be notified when ever the user makes any input changes. + * @param aFormat + * NumberFormat used to transform the numerical value to a string. + * @param aMinMaxRange + * The range of values to accept. + * @param aNumColumns + * The number of columns associated with the {@link GNumberField}. + */ + public GNumberFieldSlider(ActionListener aListener, NumberFormat aFormat, Range aMinMaxRange, + int aNumColumns) + { + refLister = aListener; + + buildGui(aFormat, aMinMaxRange); + + setNumColumns(aNumColumns); + setNumSteps(100); + } + + /** + * Simplified Constructor + * + * @param aListener + * An ActionListener that will be notified when ever the user makes any input changes. + * @param aFormat + * NumberFormat used to transform the numerical value to a string. + * @param aMinMaxRange + * The range of values to accept. + */ + public GNumberFieldSlider(ActionListener aListener, NumberFormat aFormat, Range aMinMaxRange) + { + this(aListener, aFormat, aMinMaxRange, 4); + } + + /** + * Returns whether the current input is valid + */ + public boolean isValidInput() + { + // Delegate + return valueNF.isValidInput(); + } + + /** + * Returns the selected value. + */ + public double getValue() + { + return valueNF.getValue(); + } + + /** + * Returns the currently stored model value as an integer. + *

    + * See {@link GNumberField#getValueAsInt(int)} + */ + public int getValueAsInt(int aErrorVal) + { + return valueNF.getValueAsInt(aErrorVal); + } + + /** + * Returns true if the value is being actively adjusted. + *

    + * See also {@link GSlider#getValueIsAdjusting()} + */ + public boolean getValueIsAdjusting() + { + // Delegate + return valueS.getValueIsAdjusting(); + } + + /** + * Sets in the selected value. Note no events will be fired. + */ + public void setValue(double aVal) + { + valueNF.setValue(aVal); + valueS.setModelValue(aVal); + } + + /** + * Set the editable state of the UI component. + */ + public void setEditable(boolean aBool) + { + valueNF.setEditable(aBool); + valueS.setEnabled(aBool); + } + + /** + * Set the enable state of the UI component. + */ + @Override + public void setEnabled(boolean aBool) + { + GuiUtil.setEnabled(this, aBool); + } + + /** + * Sets in the steps to be an integral value. + *

    + * The minVal and maxVal must be integers otherwise this method will throw an exception. The number of steps will be: + * (maxVal - minVal). + */ + public void setIntegralSteps() + { + var tmpMinMaxRange = valueNF.getMinMaxRange(); + double tmpMinVal = tmpMinMaxRange.lowerEndpoint(); + double tmpMaxVal = tmpMinMaxRange.upperEndpoint(); + + int intMaxVal = (int) tmpMaxVal; + int intMinVal = (int) tmpMinVal; + + var isIntegral = true; + isIntegral &= tmpMinVal - intMinVal == 0; + isIntegral &= tmpMaxVal - intMaxVal == 0; + if (isIntegral == false) + throw new RuntimeException("Min,Max values are not integral: [" + tmpMinVal + ", " + tmpMaxVal + "]"); + + // Delegate + int numSteps = intMaxVal - intMinVal; + setNumSteps(numSteps); + } + + /** + * Sets in the number of columns for the associated GTextField. + */ + public void setNumColumns(int aNumColumns) + { + valueNF.setColumns(aNumColumns); + } + + /** + * Sets in the number of steps associated with the slider. + */ + public void setNumSteps(int aNumSteps) + { + valueS.setNumSteps(aNumSteps); + } + + @Override + public void actionPerformed(ActionEvent aEvent) + { + var source = aEvent.getSource(); + if (source == valueNF) + { + double tmpVal = valueNF.getValue(); + valueS.setModelValue(tmpVal); + } + else if (source == valueS) + { + double tmpVal = valueS.getModelValue(); + valueNF.setValue(tmpVal); + } + + var tmpEvent = new ActionEvent(this, 0, ""); + refLister.actionPerformed(tmpEvent); + } + + /** + * Helper method that builds the unified GUI + */ + private void buildGui(NumberFormat aNumberFormat, Range aMinMaxRange) + { + valueNF = new GNumberField(this, aNumberFormat, aMinMaxRange); + valueS = new GSlider(this, aMinMaxRange); + + setLayout(new MigLayout("", "0[]0", "0[]0")); + add(valueNF, "w 40:"); + add(valueS, "growx,pushx"); + } + +} diff --git a/src/glum/gui/component/GPasswordField.java b/src/glum/gui/component/GPasswordField.java index 2d172e9..66a7dc1 100644 --- a/src/glum/gui/component/GPasswordField.java +++ b/src/glum/gui/component/GPasswordField.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.component; import java.awt.event.ActionListener; diff --git a/src/glum/gui/component/GSlider.java b/src/glum/gui/component/GSlider.java index 2299e86..dcf634e 100644 --- a/src/glum/gui/component/GSlider.java +++ b/src/glum/gui/component/GSlider.java @@ -1,46 +1,91 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; + import javax.swing.JSlider; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; +import com.google.common.collect.Range; + +/** + * User interface input used to capture an individual numerical input (type: double). + *

    + * Unlike JSlider, users of this class should not use getValue() / setValue() but rather getModelValue() / + * setModelValue() methods. Also it should not be necessary to register ChangeListeners - rather an ActionListener + * should be sufficient. + * + * @author lopeznr1 + */ public class GSlider extends JSlider implements ChangeListener { - private ActionListener myListener; - private double minVal, maxVal, rngVal; + // Attributes + private final ActionListener refListener; + + // State vars + private Range minMaxRange; private int maxSteps; - public GSlider(ActionListener aListener, int aMaxSteps, double aMinVal, double aMaxVal) + /** + * Standard Constructor + * + * @param aListener + * An ActionListener that will be notified when ever the user makes any input changes. + * @param aMinMaxRange + * The range of values to accept. + * @param aMaxSteps + * The number of steps associated with the slider. + */ + public GSlider(ActionListener aListener, Range aMinMaxRange, int aMaxSteps) { super(0, aMaxSteps); addChangeListener(this); - myListener = aListener; + refListener = aListener; + minMaxRange = aMinMaxRange; maxSteps = aMaxSteps; - - minVal = aMinVal; - maxVal = aMaxVal; - rngVal = maxVal - minVal; } - public GSlider(ActionListener aListener, double aMinVal, double aMaxVal) + /** + * Simplified Constructor + * + * @param aListener + * An ActionListener that will be notified when ever the user makes any input changes. + * @param aMinMaxRange + * The range of values to accept. + */ + public GSlider(ActionListener aListener, Range aMinMaxRange) { - this(aListener, 1000, aMinVal, aMaxVal); + this(aListener, aMinMaxRange, 1000); } /** * Returns the model value for which this slider is currently set to. - *

    + *

    * Use this method over {@link JSlider#getValue()} */ public double getModelValue() { - double retVal; + double minVal = minMaxRange.lowerEndpoint(); + double maxVal = minMaxRange.upperEndpoint(); + double rngVal = maxVal - minVal; - retVal = minVal + ((super.getValue() / (double)maxSteps) * rngVal); + double retVal = minVal + ((super.getValue() / (double) maxSteps) * rngVal); return retVal; } @@ -48,11 +93,13 @@ public class GSlider extends JSlider implements ChangeListener * Takes in the model's minVal and maxVal range. The current chosen model value will be adjusted to be in the middle * of the range. */ - public void setModelRange(double aMinVal, double aMaxVal) + public void setModelRange(Range aMinMaxRange) { - minVal = aMinVal; - maxVal = aMaxVal; - rngVal = maxVal - minVal; + minMaxRange = aMinMaxRange; + + double minVal = minMaxRange.lowerEndpoint(); + double maxVal = minMaxRange.upperEndpoint(); + double rngVal = maxVal - minVal; setModelValue(minVal + rngVal / 2); } @@ -60,17 +107,37 @@ public class GSlider extends JSlider implements ChangeListener /** * Takes in a model value and will adjust the slider to display the value. Note this method will not trigger an * ActionEvent. - *

    + *

    * Use this method over {@link JSlider#setValue} */ public void setModelValue(double aVal) { - double guiVal; - - guiVal = ((aVal - minVal) / rngVal) * maxSteps; + double minVal = minMaxRange.lowerEndpoint(); + double maxVal = minMaxRange.upperEndpoint(); + double rngVal = maxVal - minVal; removeChangeListener(this); - setValue((int)guiVal); + double guiVal = ((aVal - minVal) / rngVal) * maxSteps; + setValue((int) guiVal); + addChangeListener(this); + } + + /** + * Sets in the number of steps associated with the GSlider. + *

    + * Values will be uniformly distributed over the range / numSteps + * + * @param aNumSteps + * The number of steps the slider should have. + */ + public void setNumSteps(int aNumSteps) + { + removeChangeListener(this); + + setMinimum(0); + setMaximum(aNumSteps); + maxSteps = aNumSteps; + addChangeListener(this); } @@ -80,12 +147,26 @@ public class GSlider extends JSlider implements ChangeListener notifyLisener(); } + @Override + @Deprecated + public int getValue() + { + return super.getValue(); + } + + @Override + @Deprecated + public void setValue(int n) + { + super.setValue(n); + } + /** * Helper method to notify our listener */ private void notifyLisener() { - myListener.actionPerformed(new ActionEvent(this, 0, "update")); + refListener.actionPerformed(new ActionEvent(this, 0, "update")); } } diff --git a/src/glum/gui/component/GTextField.java b/src/glum/gui/component/GTextField.java index 61e96b7..646c8fb 100644 --- a/src/glum/gui/component/GTextField.java +++ b/src/glum/gui/component/GTextField.java @@ -1,21 +1,49 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.component; import java.awt.event.ActionListener; -import javax.swing.*; -import javax.swing.event.*; -import javax.swing.text.*; -public class GTextField extends JTextField implements DocumentListener +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.Document; +import javax.swing.text.PlainDocument; + +/** + * User interface input used to capture an input string. + *

    + * Unlike JTextField, users of this class should not use getText() / setText() but rather getValue() / setValue() + * methods. Also it should not be necessary to register DocumentListeners - rather an ActionListener should be + * sufficient. + * + * @author lopeznr1 + */ +public class GTextField extends GBaseTextField implements DocumentListener { /** - * Constructor - * + * Standard Constructor + * * @param aListener - * : Default ActionListener + * An ActionListener that will be notified when ever the user makes any input changes. + * @param aLabel + * The text value to use as the initial input. + * @param aHint + * The hint to show when no input has been entered. */ - public GTextField(ActionListener aListener) + public GTextField(ActionListener aListener, String aLabel, String aHint) { - super("", 0); + super(aLabel, 0); if (aListener != null) addActionListener(aListener); @@ -27,6 +55,35 @@ public class GTextField extends JTextField implements DocumentListener // Register for events of interest doc.addDocumentListener(this); + + if (aHint != null) + setHint(aHint); + + forceTF(aLabel); + } + + /** + * Simplified Constructor + * + * @param aListener + * An ActionListener that will be notified when ever the user makes any input changes. + * @param aLabel + * The text value to use as the initial input. + */ + public GTextField(ActionListener aListener, String aLabel) + { + this(aListener, aLabel, null); + } + + /** + * Simplified Constructor + * + * @param aListener + * An ActionListener that will be notified when ever the user makes any input changes. + */ + public GTextField(ActionListener aListener) + { + this(aListener, "", null); } /** diff --git a/src/glum/gui/component/GToggle.java b/src/glum/gui/component/GToggle.java deleted file mode 100644 index 5043c60..0000000 --- a/src/glum/gui/component/GToggle.java +++ /dev/null @@ -1,76 +0,0 @@ -package glum.gui.component; - -import javax.swing.Icon; -import javax.swing.JToggleButton; - -public class GToggle extends JToggleButton -{ - // State vars - protected boolean isActive; - - // Gui vars - protected Icon falseIcon, trueIcon; - - public GToggle(Icon aFalseIcon, Icon aTrueIcon, boolean aIsActive) - { - super(); - - falseIcon = aFalseIcon; - trueIcon = aTrueIcon; - setSelected(aIsActive); - - setModel(new GToggleButtonModel()); - } - - @Override - public void setSelected(boolean b) - { - super.setSelected(b); - updateGui(); - } - - @Override - public void doClick(int pressTime) - { - super.doClick(pressTime); - updateGui(); - } - - /** - * Utility method - */ - private void updateGui() - { - if (isSelected() == true) - setIcon(trueIcon); - else - setIcon(falseIcon); - } - - /** - * The ToggleButton model - *

    - * Warning: Serialized objects of this class will not be compatible with future Swing releases. The - * current serialization support is appropriate for short term storage or RMI between applications running the same - * version of Swing. As of 1.4, support for long term storage of all JavaBeansTM - * has been added to the java.beans package. Please see {@link java.beans.XMLEncoder}. - */ - public class GToggleButtonModel extends ToggleButtonModel - { - /** - * Creates a new ToggleButton Model - */ - public GToggleButtonModel() - { - } - - @Override - public void setSelected(boolean b) - { - super.setSelected(b); - - updateGui(); - } - } - -} diff --git a/src/glum/gui/component/banner/Banner.java b/src/glum/gui/component/banner/Banner.java index 566cac7..c33e21e 100644 --- a/src/glum/gui/component/banner/Banner.java +++ b/src/glum/gui/component/banner/Banner.java @@ -1,11 +1,25 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.component.banner; import java.awt.*; -import java.awt.geom.*; -import java.util.*; -import javax.swing.*; +import java.awt.geom.Rectangle2D; +import java.util.Collection; -import glum.gui.*; +import javax.swing.JComponent; + +import glum.io.ParseUtil; import glum.io.token.MatchTokenizer; import glum.io.token.Tokenizer; @@ -37,7 +51,7 @@ public class Banner extends JComponent double sX, sY, offSetY; super.paintComponent(g); - g2d = (Graphics2D)g; + g2d = (Graphics2D) g; // Determine the window boundaries winW = getWidth(); @@ -72,7 +86,7 @@ public class Banner extends JComponent sY = winH / 2.0 - msgH / 2.0; g2d.setFont(myConfig.font); g2d.setColor(myConfig.fgColor); - g2d.drawString(displayMsg, (int)sX, (int)(sY + offSetY)); + g2d.drawString(displayMsg, (int) sX, (int) (sY + offSetY)); } /** @@ -102,8 +116,8 @@ public class Banner extends JComponent } /** - * Utility method that converts a string to a BannerConfig. Eventually this method class should be - * moved to a utility class of sorts. + * Utility method that converts a string to a BannerConfig. Eventually this method class should be moved to a utility + * class of sorts. */ public static BannerConfig readBannerConfig(String strLine) { @@ -128,7 +142,7 @@ public class Banner extends JComponent for (String aInstr : instrSet) { parmSet = parmTokenizer.getTokens(aInstr); - parms = parmSet.toArray(new String[] {""}); + parms = parmSet.toArray(new String[] { "" }); if (parms.length == 0) { @@ -152,15 +166,15 @@ public class Banner extends JComponent size = 12; if (parms.length > 2) - size = GuiUtil.readInt(parms[2], 12); + size = ParseUtil.readInt(parms[2], 12); bold = false; if (parms.length > 3) - bold = GuiUtil.readBoolean(parms[3], false); + bold = ParseUtil.readBoolean(parms[3], false); italic = false; if (parms.length > 4) - italic = GuiUtil.readBoolean(parms[4], false); + italic = ParseUtil.readBoolean(parms[4], false); style = 0; if (bold == false && italic == false) @@ -176,22 +190,22 @@ public class Banner extends JComponent { int r, g, b; - r = GuiUtil.readRangeInt(parms[1], 0, 255, 255); - g = GuiUtil.readRangeInt(parms[2], 0, 255, 255); - b = GuiUtil.readRangeInt(parms[3], 0, 255, 255); + r = ParseUtil.readRangeInt(parms[1], 0, 255, 255); + g = ParseUtil.readRangeInt(parms[2], 0, 255, 255); + b = ParseUtil.readRangeInt(parms[3], 0, 255, 255); aConfig.fgColor = new Color(r, g, b); } else if (parms[0].equalsIgnoreCase("bgColor") == true && parms.length >= 4) { int r, g, b, a; - r = GuiUtil.readRangeInt(parms[1], 0, 255, 255); - g = GuiUtil.readRangeInt(parms[2], 0, 255, 255); - b = GuiUtil.readRangeInt(parms[3], 0, 255, 255); + r = ParseUtil.readRangeInt(parms[1], 0, 255, 255); + g = ParseUtil.readRangeInt(parms[2], 0, 255, 255); + b = ParseUtil.readRangeInt(parms[3], 0, 255, 255); a = 255; if (parms.length > 4) - a = GuiUtil.readRangeInt(parms[4], 0, 255, 255); + a = ParseUtil.readRangeInt(parms[4], 0, 255, 255); aConfig.bgColor = new Color(r, g, b, a); } @@ -199,20 +213,20 @@ public class Banner extends JComponent { int r, g, b; - r = GuiUtil.readRangeInt(parms[1], 0, 255, 255); - g = GuiUtil.readRangeInt(parms[2], 0, 255, 255); - b = GuiUtil.readRangeInt(parms[3], 0, 255, 255); + r = ParseUtil.readRangeInt(parms[1], 0, 255, 255); + g = ParseUtil.readRangeInt(parms[2], 0, 255, 255); + b = ParseUtil.readRangeInt(parms[3], 0, 255, 255); aConfig.borderColor = new Color(r, g, b); if (parms.length > 4) - aConfig.borderWidth = GuiUtil.readRangeInt(parms[4], 0, 10, 0); + aConfig.borderWidth = ParseUtil.readRangeInt(parms[4], 0, 10, 0); if (parms.length > 5) - aConfig.borderPad = GuiUtil.readRangeInt(parms[5], -20, 20, 0); + aConfig.borderPad = ParseUtil.readRangeInt(parms[5], -20, 20, 0); } else if (parms[0].equalsIgnoreCase("repeatMsg") == true && parms.length == 2) { - aConfig.numRepeats = GuiUtil.readRangeInt(parms[1], -1, 100, 0); + aConfig.numRepeats = ParseUtil.readRangeInt(parms[1], -1, 100, 0); } } diff --git a/src/glum/gui/component/banner/BannerConfig.java b/src/glum/gui/component/banner/BannerConfig.java index 11a8a4d..ebcda7b 100644 --- a/src/glum/gui/component/banner/BannerConfig.java +++ b/src/glum/gui/component/banner/BannerConfig.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.component.banner; import java.awt.*; diff --git a/src/glum/gui/component/model/GComboBoxModel.java b/src/glum/gui/component/model/GComboBoxModel.java index a35352e..deefa41 100644 --- a/src/glum/gui/component/model/GComboBoxModel.java +++ b/src/glum/gui/component/model/GComboBoxModel.java @@ -1,26 +1,51 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.component.model; import java.util.Collection; + import javax.swing.ComboBoxModel; -import com.google.common.collect.Lists; +import com.google.common.collect.ImmutableList; +import glum.gui.component.GComboBox; + +/** + * Implementation of {@link ComboBoxModel} that is used with {@link GComboBox}. + * + * @author lopeznr1 + */ public class GComboBoxModel extends GListModel implements ComboBoxModel { - protected G1 chosenItem; + private G1 chosenItem; - public GComboBoxModel(G1... aItemArr) + /** + * Standard Constructor + */ + public GComboBoxModel(Collection aItemC) { - this(Lists.newArrayList(aItemArr)); - } - - public GComboBoxModel(Collection aItemList) - { - super(aItemList); + super(aItemC); chosenItem = null; - if (itemList.size() > 0) - chosenItem = itemList.get(0); + if (itemL.size() > 0) + chosenItem = itemL.get(0); + } + + @SafeVarargs + public GComboBoxModel(G1... aItemArr) + { + this(ImmutableList.copyOf(aItemArr)); } @Override @@ -38,17 +63,18 @@ public class GComboBoxModel extends GListModel implements ComboBoxModel< super.removeItem(aItem); chosenItem = null; - if (itemList.size() > 0) - chosenItem = itemList.get(0); + if (itemL.size() > 0) + chosenItem = itemL.get(0); } /** * Note aItem must be of the Generified type */ - @Override @SuppressWarnings("unchecked") + @Override + @SuppressWarnings("unchecked") public void setSelectedItem(Object aItem) { - chosenItem = (G1)aItem; + chosenItem = (G1) aItem; } @Override diff --git a/src/glum/gui/component/model/GListModel.java b/src/glum/gui/component/model/GListModel.java index 9a44ee9..555847f 100644 --- a/src/glum/gui/component/model/GListModel.java +++ b/src/glum/gui/component/model/GListModel.java @@ -1,79 +1,98 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.component.model; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; +import java.util.*; import javax.swing.AbstractListModel; -import com.google.common.collect.Lists; - /** * Generified mutable ListModel + * + * @author lopeznr1 */ public class GListModel extends AbstractListModel { - protected List itemList; + protected final List itemL; - public GListModel(Collection aItemList) + /** + * Standard Constructor + */ + public GListModel(Collection aItemC) { - itemList = Lists.newArrayList(aItemList); + itemL = new ArrayList<>(aItemC); } -// public GListModel(G1... aItemArr) -// { -// itemList = Lists.newArrayList(aItemArr); -// } -// /** * Adds aItem to this model */ public void addItem(G1 aItem) { - int index; - - itemList.add(aItem); - - index = itemList.size() - 1; + itemL.add(aItem); + + var index = itemL.size() - 1; fireIntervalAdded(this, index, index); } - + /** * Removes aItem from this model */ public void removeItem(G1 aItem) { - int index; - - index = itemList.indexOf(aItem); - itemList.remove(aItem); - + var index = itemL.indexOf(aItem); + itemL.remove(aItem); + fireIntervalRemoved(this, index, index); } - + /** * Removes all the items from this model */ public void removeAllItems() { - int lastIndex; - // Bail if the list is empty - if (itemList.isEmpty() == true) + if (itemL.isEmpty() == true) return; - - lastIndex = itemList.size() - 1; - itemList.clear(); - + + var lastIndex = itemL.size() - 1; + itemL.clear(); + fireIntervalRemoved(this, 0, lastIndex); } - + + /** + * Replaces the original item with the provided replacement. + *

    + * Throws an exception if the original item is not installed. + */ + public void replaceItem(G1 aOrigItem, G1 aReplItem) + { + int tmpIdx = itemL.indexOf(aOrigItem); + if (tmpIdx == -1) + throw new RuntimeException("The original item is not in the list"); + + itemL.set(tmpIdx, aReplItem); + + fireContentsChanged(this, tmpIdx, tmpIdx); + } + /** * Returns a list of all the items */ - public ArrayList getAllItems() + public List getAllItems() { - return Lists.newArrayList(itemList); + return new ArrayList<>(itemL); } /** @@ -81,18 +100,18 @@ public class GListModel extends AbstractListModel */ public int indexOf(G1 aItem) { - return itemList.indexOf(aItem); + return itemL.indexOf(aItem); } @Override public int getSize() { - return itemList.size(); + return itemL.size(); } @Override public G1 getElementAt(int index) { - return itemList.get(index); + return itemL.get(index); } } diff --git a/src/glum/gui/dnd/PlainTransferHandler.java b/src/glum/gui/dnd/PlainTransferHandler.java index 38d7ec9..11f3a2f 100644 --- a/src/glum/gui/dnd/PlainTransferHandler.java +++ b/src/glum/gui/dnd/PlainTransferHandler.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dnd; import java.awt.datatransfer.Transferable; @@ -6,19 +19,20 @@ import javax.swing.JComponent; import javax.swing.TransferHandler; /** - * Generic TransferHandler that supports transferring an arbitrary Transferable. Note that before the DnD transfer - * mechanism is triggered via the method exportAsDrag(), the developer must set in the Transferable via the method - * setWorkTransferable(). + * Generic {@link TransferHandler} that supports transferring an arbitrary {@link Transferable}. Note that before the + * DnD transfer mechanism is triggered via the method exportAsDrag(), the developer must set in the {@link Transferable} + * via the method {@link #setWorkTransferable(Transferable)}. + * + * @author lopeznr1 */ public class PlainTransferHandler extends TransferHandler { // State vars protected Transferable workTransferable; + /** Standard Constructor */ public PlainTransferHandler() { - super(); - workTransferable = null; } diff --git a/src/glum/gui/dock/BaseDockable.java b/src/glum/gui/dock/BaseDockable.java index 6d7e266..ddd183d 100644 --- a/src/glum/gui/dock/BaseDockable.java +++ b/src/glum/gui/dock/BaseDockable.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock; import javax.swing.Icon; @@ -11,18 +24,16 @@ public class BaseDockable extends DefaultDockable // Tells whether this Dockable can be dragged and dropped to another station private DockStation homeStation; private boolean isTransferable; - + public BaseDockable() { - super(); - isTransferable = true; } - + public BaseDockable(JComponent aComp, String aTitle, Icon aIcon) { super(aComp, aTitle, aIcon); - + isTransferable = true; } @@ -30,21 +41,19 @@ public class BaseDockable extends DefaultDockable { if (isTransferable == true) return true; - + // We can only be transfered to our homeStation when we are not transferable return aStation == homeStation; } - + public void setTransferable(boolean aBool) { homeStation = null; isTransferable = aBool; - - + // Record our parent when we become non transferable if (isTransferable == false) homeStation = getDockParent(); } - } diff --git a/src/glum/gui/dock/CloseableDockable.java b/src/glum/gui/dock/CloseableDockable.java index ff99df5..10f5a71 100644 --- a/src/glum/gui/dock/CloseableDockable.java +++ b/src/glum/gui/dock/CloseableDockable.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock; import glum.gui.dock.action.Closeable; diff --git a/src/glum/gui/dock/CustomPlaceholderStrategy.java b/src/glum/gui/dock/CustomPlaceholderStrategy.java index eb1ab6e..873f802 100644 --- a/src/glum/gui/dock/CustomPlaceholderStrategy.java +++ b/src/glum/gui/dock/CustomPlaceholderStrategy.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock; import bibliothek.gui.DockStation; diff --git a/src/glum/gui/dock/DockUtil.java b/src/glum/gui/dock/DockUtil.java index a0f4445..e87e881 100644 --- a/src/glum/gui/dock/DockUtil.java +++ b/src/glum/gui/dock/DockUtil.java @@ -1,14 +1,26 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock; import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; import javax.swing.Icon; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; - import bibliothek.gui.DockController; import bibliothek.gui.DockFrontend; import bibliothek.gui.DockStation; @@ -20,6 +32,11 @@ import bibliothek.gui.dock.action.actions.SimpleButtonAction; import bibliothek.gui.dock.station.screen.ScreenFullscreenAction; import bibliothek.gui.dock.station.split.SplitFullScreenAction; +/** + * Collection of utility methods useful for working dockables / dock stations. + * + * @author lopeznr1 + */ public class DockUtil { /** @@ -27,14 +44,14 @@ public class DockUtil */ public static SimpleButtonAction createAction(String aText, Icon aIcon, ActionListener aListener) { - SimpleButtonAction aAction; + SimpleButtonAction retAction; - aAction = new SimpleButtonAction(); - aAction.setText(aText); - aAction.setIcon(aIcon); - aAction.addActionListener(aListener); + retAction = new SimpleButtonAction(); + retAction.setText(aText); + retAction.setIcon(aIcon); + retAction.addActionListener(aListener); - return aAction; + return retAction; } /** @@ -44,24 +61,24 @@ public class DockUtil */ public static DockAction createFullScreenAction(DockStation aStation, DockController aController) { - DockAction fullScreenAction; + DockAction retAction; if (aStation instanceof SplitDockStation) { - fullScreenAction = new SplitFullScreenAction((SplitDockStation)aStation); - ((SplitFullScreenAction)fullScreenAction).setController(aController); + retAction = new SplitFullScreenAction((SplitDockStation) aStation); + ((SplitFullScreenAction) retAction).setController(aController); } else if (aStation instanceof ScreenDockStation) { - fullScreenAction = new ScreenFullscreenAction((ScreenDockStation)aStation); - ((ScreenFullscreenAction)fullScreenAction).setController(aController); + retAction = new ScreenFullscreenAction((ScreenDockStation) aStation); + ((ScreenFullscreenAction) retAction).setController(aController); } else { throw new RuntimeException("Unsupported Dockable type: " + aStation); } - return fullScreenAction; + return retAction; } /** @@ -75,7 +92,7 @@ public class DockUtil { evalDockable = aStation.getDockable(c1); if (evalDockable instanceof DockStation) - evalDockable = findDockable((DockStation)evalDockable, aClass); + evalDockable = findDockable((DockStation) evalDockable, aClass); if (evalDockable.getClass() == aClass) return aClass.cast(evalDockable); @@ -90,19 +107,19 @@ public class DockUtil */ public static List findDockableList(DockFrontend aFrontend, Class... aClassArr) { - Set> classSet; - List itemList; + Set> tmpClassS; + List retItemL; // Transform the match class array to a set - classSet = Sets.newHashSet(); + tmpClassS = new HashSet<>(); for (Class aClass : aClassArr) - classSet.add(aClass); + tmpClassS.add(aClass); - itemList = Lists.newLinkedList(); + retItemL = new ArrayList<>(); for (DockStation aStation : aFrontend.getRoots()) - findDockableList(aStation, classSet, itemList); + findDockableList(aStation, tmpClassS, retItemL); - return itemList; + return retItemL; } /** @@ -111,18 +128,18 @@ public class DockUtil */ public static List findDockableList(DockFrontend aFrontend, Class aClass) { - Set> classSet; - List itemList; + Set> tmpClassS; + List retItemL; // Transform the match class array to a set - classSet = Sets.newHashSet(); - classSet.add(aClass); + tmpClassS = new HashSet<>(); + tmpClassS.add(aClass); - itemList = Lists.newLinkedList(); + retItemL = new ArrayList<>(); for (DockStation aStation : aFrontend.getRoots()) - findDockableList(aStation, classSet, itemList); + findDockableList(aStation, tmpClassS, retItemL); - return itemList; + return retItemL; } /** @@ -131,18 +148,18 @@ public class DockUtil */ public static List findDockableList(DockStation aStation, Class... aClassArr) { - Set> classSet; - List itemList; + Set> tmpClassS; + List retItemL; // Transform the match class array to a set - classSet = Sets.newHashSet(); + tmpClassS = new HashSet<>(); for (Class aClass : aClassArr) - classSet.add(aClass); + tmpClassS.add(aClass); - itemList = Lists.newLinkedList(); - findDockableList(aStation, classSet, itemList); + retItemL = new ArrayList<>(); + findDockableList(aStation, tmpClassS, retItemL); - return itemList; + return retItemL; } /** @@ -151,32 +168,32 @@ public class DockUtil */ public static List findDockableList(DockStation aStation, Class aClass) { - Set> classSet; - List itemList; + Set> tmpClassS; + List retItemL; // Transform the match class array to a set - classSet = Sets.newHashSet(); - classSet.add(aClass); + tmpClassS = new HashSet<>(); + tmpClassS.add(aClass); - itemList = Lists.newLinkedList(); - findDockableList(aStation, classSet, itemList); + retItemL = new ArrayList<>(); + findDockableList(aStation, tmpClassS, retItemL); - return itemList; + return retItemL; } /** * Helper method to remove all PlotGroupStations and ChartDockables */ - public static void removeAllDockablesOfType(DockFrontend aFrontend, Class... dockTypeArr) + public static void removeAllDockablesOfType(DockFrontend aFrontend, Class... aDockTypeArr) { - List dockList; + List dockL; DockStation dockStation; // Gather all of the Dockables of interest - dockList = DockUtil.findDockableList(aFrontend, dockTypeArr); + dockL = DockUtil.findDockableList(aFrontend, aDockTypeArr); // Remove all of the Dockables - for (Dockable aDock : dockList) + for (Dockable aDock : dockL) { dockStation = aDock.getDockParent(); if (dockStation != null) @@ -189,24 +206,25 @@ public class DockUtil * results will be stored in aItemList */ @SuppressWarnings("unchecked") - private static void findDockableList(DockStation aStation, Set> aClassSet, List aItemList) + private static void findDockableList(DockStation aStation, Set> aClassS, + List aItemL) { Dockable evalDockable; for (int c1 = 0; c1 < aStation.getDockableCount(); c1++) { evalDockable = aStation.getDockable(c1); - for (Class aClass : aClassSet) + for (Class aClass : aClassS) { if (aClass.isAssignableFrom(evalDockable.getClass()) == true) { - aItemList.add((G1)evalDockable); + aItemL.add((G1) evalDockable); break; } } if (evalDockable instanceof DockStation) - findDockableList((DockStation)evalDockable, aClassSet, aItemList); + findDockableList((DockStation) evalDockable, aClassS, aItemL); } } diff --git a/src/glum/gui/dock/FrontendAddConfigPanel.java b/src/glum/gui/dock/FrontendAddConfigPanel.java index 1d512e8..6727eda 100644 --- a/src/glum/gui/dock/FrontendAddConfigPanel.java +++ b/src/glum/gui/dock/FrontendAddConfigPanel.java @@ -1,23 +1,38 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock; -import java.awt.*; +import java.awt.Component; import java.awt.event.ActionEvent; import bibliothek.gui.DockFrontend; +import glum.gui.panel.generic.BaseTextInputPanel; -import glum.gui.panel.generic.TextInputPanel; - -public class FrontendAddConfigPanel extends TextInputPanel +/** + * UI component that allows the creation of a docking configuration. + * + * @author lopeznr1 + */ +public class FrontendAddConfigPanel extends BaseTextInputPanel { // Constants public static final String DEFAULT_NAME = "Default"; - // State vars - protected DockFrontend refFrontend; + // Ref vars + private final DockFrontend refFrontend; - /** - * Constructor - */ + /** Standard Constructor */ public FrontendAddConfigPanel(Component aParent, DockFrontend aFrontend) { super(aParent); @@ -32,14 +47,11 @@ public class FrontendAddConfigPanel extends TextInputPanel @Override public void actionPerformed(ActionEvent e) { - Object source; - String configName; - // Save of the configuration - source = e.getSource(); + var source = e.getSource(); if (source == acceptB) { - configName = getInput(); + var configName = getInput(); refFrontend.save(configName); } @@ -50,16 +62,14 @@ public class FrontendAddConfigPanel extends TextInputPanel @Override protected void updateGui() { - String inputStr, infoMsg; - boolean isEnabled; - // Assume the GUI is invalid - isEnabled = false; + var isEnabled = false; // Retrieve the name - inputStr = inputTF.getText(); + var inputStr = inputTF.getText(); // Determine the validity of specified name + String infoMsg; if (inputStr.equals("") == true) { infoMsg = "Please enter a valid configuration name."; diff --git a/src/glum/gui/dock/FrontendManageConfigPanel.java b/src/glum/gui/dock/FrontendManageConfigPanel.java index 3d6679a..b0c4927 100644 --- a/src/glum/gui/dock/FrontendManageConfigPanel.java +++ b/src/glum/gui/dock/FrontendManageConfigPanel.java @@ -1,21 +1,29 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock; import java.awt.Component; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.util.Collection; -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.SwingUtilities; -import javax.swing.border.BevelBorder; +import java.util.ArrayList; + +import javax.swing.*; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; -//import echo.gui.LookUp; - -import com.google.common.collect.Lists; - +import bibliothek.gui.DockFrontend; import glum.database.QueryItem; import glum.gui.FocusUtil; import glum.gui.GuiUtil; @@ -25,24 +33,27 @@ import glum.gui.panel.itemList.ItemListPanel; import glum.gui.panel.itemList.StaticItemProcessor; import glum.gui.panel.itemList.query.QueryComposer; import glum.gui.panel.itemList.query.QueryItemHandler; - -import bibliothek.gui.DockFrontend; - import net.miginfocom.swing.MigLayout; +/** + * UI component that provides a listing of all docking configurations. + * + * @author lopeznr1 + */ public class FrontendManageConfigPanel extends GlassPanel implements ActionListener, ListSelectionListener { // GUI vars - protected ItemListPanel listPanel; - protected JButton deleteB, closeB; + private ItemListPanel listPanel; + private JButton deleteB, closeB; // State vars - protected DockFrontend refFrontend; - protected StaticItemProcessor myItemProcessor; + private DockFrontend refFrontend; + private StaticItemProcessor myItemProcessor; /** - * Constructor - * @param aFrontend + * Standard Constructor + * + * @param aFrontend */ public FrontendManageConfigPanel(Component aParent, DockFrontend aFrontend) { @@ -53,8 +64,8 @@ public class FrontendManageConfigPanel extends GlassPanel implements ActionListe // Build the actual GUI buildGuiArea(); - setPreferredSize(new Dimension(250, 300));//TODO:getPreferredSize().height)); - + setPreferredSize(new Dimension(250, 300));// TODO:getPreferredSize().height)); + // Set up keyboard short cuts FocusUtil.addAncestorKeyBinding(this, "ESCAPE", new ClickAction(closeB)); FocusUtil.addAncestorKeyBinding(this, "ENTER", new ClickAction(closeB)); @@ -65,19 +76,16 @@ public class FrontendManageConfigPanel extends GlassPanel implements ActionListe public void setVisible(boolean aBool) { resetGui(); - + super.setVisible(aBool); } @Override - public void actionPerformed(ActionEvent e) + public void actionPerformed(ActionEvent aEvent) { - Object source; - ConfigItem chosenItem; + var chosenItem = listPanel.getSelectedItem(); - chosenItem = listPanel.getSelectedItem(); - - source = e.getSource(); + var source = aEvent.getSource(); if (source == deleteB) { refFrontend.delete(chosenItem.getName()); @@ -94,7 +102,7 @@ public class FrontendManageConfigPanel extends GlassPanel implements ActionListe { if (aEvent.getValueIsAdjusting() == true) return; - + updateGui(); } @@ -103,34 +111,27 @@ public class FrontendManageConfigPanel extends GlassPanel implements ActionListe */ private void buildGuiArea() { - JLabel tmpL; - QueryItemHandler aItemHandler; - QueryComposer aComposer; - - setLayout(new MigLayout("", "[grow][][]", "[][grow][]")); - - tmpL = new JLabel("Select configuration:", JLabel.CENTER); - add(tmpL, "growx,span 4,wrap"); - - // Construct the actual table panel - aComposer = new QueryComposer(); - aComposer.addAttribute(LookUp.Name, String.class, "Configuration", null); - myItemProcessor = new StaticItemProcessor(); - aItemHandler = new QueryItemHandler(aComposer); - listPanel = new ItemListPanel(aItemHandler, myItemProcessor, false, true); + var tmpL = new JLabel("Select configuration:", JLabel.CENTER); + add(tmpL, "growx,span 4,wrap"); + + // Construct the actual table panel + var tmpComposer = new QueryComposer(); + tmpComposer.addAttribute(LookUp.Name, String.class, "Configuration", null); + + myItemProcessor = new StaticItemProcessor<>(); + var tmpIH = new QueryItemHandler(); + listPanel = new ItemListPanel<>(tmpIH, myItemProcessor, tmpComposer, true); listPanel.addListSelectionListener(this); add(listPanel, "growx,growy,span 4,wrap"); - + // Control area deleteB = GuiUtil.createJButton("Delete", this); closeB = GuiUtil.createJButton("Close", this); - add(deleteB, "skip 1,span 1"); - add(closeB, "span 1"); - - setBorder(new BevelBorder(BevelBorder.RAISED)); + add(deleteB, "skip 1"); + add(closeB, ""); } /** @@ -138,31 +139,19 @@ public class FrontendManageConfigPanel extends GlassPanel implements ActionListe */ private void resetGui() { - Collection strList; - Collection itemList; - - itemList = Lists.newLinkedList(); - - strList = refFrontend.getSettings(); - for (String aStr : strList) + var itemL = new ArrayList(); + var strL = refFrontend.getSettings(); + for (String aStr : strL) { // Add only non reserved items if (aStr.charAt(0) != '.') - itemList.add(new ConfigItem(aStr)); + itemL.add(new ConfigItem(aStr)); } - - myItemProcessor.setItems(itemList); - - // TODO: Ugly code: Should be able to just call updateGui but can not - SwingUtilities.invokeLater(new Runnable() - { - - @Override - public void run() - { - updateGui(); - } - }); + + myItemProcessor.setItems(itemL); + + // TODO: Should be able to just call updateGui but can not + SwingUtilities.invokeLater(() -> updateGui()); } /** @@ -170,58 +159,46 @@ public class FrontendManageConfigPanel extends GlassPanel implements ActionListe */ private void updateGui() { - ConfigItem chosenItem; - boolean isEnabled; - - chosenItem = listPanel.getSelectedItem(); - + var chosenItem = listPanel.getSelectedItem(); if (chosenItem != null) refFrontend.load(chosenItem.getName()); - - isEnabled = chosenItem != null; + + var isEnabled = chosenItem != null; deleteB.setEnabled(isEnabled); - + // Ensure we have the focus. -//listPanel.requestFocusInWindow(); +//listPanel.requestFocusInWindow(); repaint(); - // TODO: Ugly code: Not sure why need to request focus multiple times to ensure we regrab + // TODO: Ugly code: Not sure why need to request focus multiple times to ensure we regrab // the focus. This is particularly true when there are multiple DockStations located on different // windows - SwingUtilities.invokeLater(new Runnable() - { - + SwingUtilities.invokeLater(new Runnable() { + @Override public void run() { listPanel.getTable().requestFocus(); - - SwingUtilities.invokeLater(new Runnable() - { - - @Override - public void run() - { - listPanel.getTable().requestFocus(); - } - }); - -// listPanel.getTable().getFocusCycleRootAncestor().setFocusCycleRoot(true); + + SwingUtilities.invokeLater(() -> listPanel.getTable().requestFocus()); + +// listPanel.getTable().getFocusCycleRootAncestor().setFocusCycleRoot(true); listPanel.getTable().requestFocusInWindow(); repaint(); } }); } - - + /** - * Internal class only used to wrap named (string) settings into - * a QueryItem + * Internal class only used to wrap named (string) settings into a QueryItem. + * + * @author lopeznr1 */ class ConfigItem implements QueryItem { - private String refName; - + // Attributes + private final String refName; + public ConfigItem(String aName) { refName = aName; @@ -246,7 +223,7 @@ public class FrontendManageConfigPanel extends GlassPanel implements ActionListe { throw new UnsupportedOperationException(); } - + } } diff --git a/src/glum/gui/dock/LookUp.java b/src/glum/gui/dock/LookUp.java index 838d67d..8191f66 100644 --- a/src/glum/gui/dock/LookUp.java +++ b/src/glum/gui/dock/LookUp.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock; public enum LookUp diff --git a/src/glum/gui/dock/PlainDockSituationIgnore.java b/src/glum/gui/dock/PlainDockSituationIgnore.java index 3617a25..d457143 100644 --- a/src/glum/gui/dock/PlainDockSituationIgnore.java +++ b/src/glum/gui/dock/PlainDockSituationIgnore.java @@ -1,9 +1,21 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock; +import java.util.LinkedHashSet; import java.util.Set; -import com.google.common.collect.Sets; - import bibliothek.gui.DockStation; import bibliothek.gui.dock.DockElement; import bibliothek.gui.dock.layout.DockSituationIgnore; @@ -12,50 +24,52 @@ import bibliothek.gui.dock.perspective.PerspectiveStation; public class PlainDockSituationIgnore implements DockSituationIgnore { - private Set> ignoreClassSet; - + // State vars + private Set> ignoreClassS; + + /** Standard Constructor */ public PlainDockSituationIgnore() { - ignoreClassSet = Sets.newLinkedHashSet(); + ignoreClassS = new LinkedHashSet<>(); } - + /** * Method to register a new type of Dockable/Station to ignore */ public void addIgnoreClass(Class aClass) { - ignoreClassSet.add(aClass); + ignoreClassS.add(aClass); } - + @Override public boolean ignoreElement(PerspectiveElement aElement) { // TODO Auto-generated method stub return false; } - + @Override public boolean ignoreElement(DockElement aElement) { // Check to see if aElement is of one of the types in ignoreClassSet - for (Class aClass : ignoreClassSet) + for (var aClass : ignoreClassS) { if (aClass.isAssignableFrom(aElement.getClass()) == true) return true; } - + return false; } - + @Override - public boolean ignoreChildren(PerspectiveStation station) + public boolean ignoreChildren(PerspectiveStation aStation) { // TODO Auto-generated method stub return false; } - + @Override - public boolean ignoreChildren(DockStation station) + public boolean ignoreChildren(DockStation aStation) { // TODO Auto-generated method stub return false; diff --git a/src/glum/gui/dock/PrimConfig.java b/src/glum/gui/dock/PrimConfig.java index 1dfb05a..d246ff5 100644 --- a/src/glum/gui/dock/PrimConfig.java +++ b/src/glum/gui/dock/PrimConfig.java @@ -1,211 +1,196 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; +import java.io.*; +import java.util.LinkedHashMap; import java.util.Map; -import java.util.Set; - -import com.google.common.collect.Maps; import glum.io.IoUtil; +/** + * Container which holds a mapping of keys (String) to a bunch of primitive objects. + *

    + * Serialization of content is supported via {@link #readBin(DataInputStream)} and {@link #writeBin(DataOutputStream)} + * + * @author lopeznr1 + */ public class PrimConfig { // Constants - public static final byte CODE_BOOL = 1; - public static final byte CODE_INT = 10; - public static final byte CODE_LONG = 11; - public static final byte CODE_SHORT = 12; - public static final byte CODE_FLOAT = 13; + public static final byte CODE_BOOL = 1; + public static final byte CODE_INT = 10; + public static final byte CODE_LONG = 11; + public static final byte CODE_SHORT = 12; + public static final byte CODE_FLOAT = 13; public static final byte CODE_DOUBLE = 14; public static final byte CODE_STRING = 20; // State vars - protected Map mySingletonMap; - - /** - * Container which holds a mapping of keys (String) to a bunch - * of primitive objects. - */ + protected Map mySingletonM; + + /** Standard Constructor */ public PrimConfig() { - mySingletonMap = Maps.newLinkedHashMap(); + mySingletonM = new LinkedHashMap<>(); } /** - * Returns the boolean associated with aKey - * If no such value is found then defaultVal is returned. + * Returns the boolean associated with aKey If no such value is found then defaultVal is returned. */ - public boolean getBoolean(String aKey, boolean defaultVal) + public boolean getBoolean(String aKey, boolean aDefaultVal) { - Object aValue; - // If no value associated with the key then return defaultVal - aValue = mySingletonMap.get(aKey); - if (aValue == null || aValue.getClass() != Boolean.class) - return defaultVal; + var tmpVal = mySingletonM.get(aKey); + if (tmpVal == null || tmpVal.getClass() != Boolean.class) + return aDefaultVal; - return (Boolean)aValue; + return (Boolean) tmpVal; } - + /** - * Returns the int associated with aKey - * If no such value is found then defaultVal is returned. + * Returns the int associated with aKey If no such value is found then defaultVal is returned. */ - public int getInt(String aKey, int defaultVal) + public int getInt(String aKey, int aDefaultVal) { - Object aValue; - // If no value associated with the key then return defaultVal - aValue = mySingletonMap.get(aKey); - if (aValue == null || aValue.getClass() != Integer.class) - return defaultVal; + var tmpVal = mySingletonM.get(aKey); + if (tmpVal == null || tmpVal.getClass() != Integer.class) + return aDefaultVal; - return (Integer)aValue; + return (Integer) tmpVal; } - + /** - * Returns the long associated with aKey - * If no such value is found then defaultVal is returned. + * Returns the long associated with aKey If no such value is found then defaultVal is returned. */ - public long getLong(String aKey, long defaultVal) + public long getLong(String aKey, long aDefaultVal) { - Object aValue; - // If no value associated with the key then return defaultVal - aValue = mySingletonMap.get(aKey); - if (aValue == null || aValue.getClass() != Long.class) - return defaultVal; + var tmpVal = mySingletonM.get(aKey); + if (tmpVal == null || tmpVal.getClass() != Long.class) + return aDefaultVal; - return (Long)aValue; + return (Long) tmpVal; } - + /** - * Returns the short associated with aKey - * If no such value is found then defaultVal is returned. + * Returns the short associated with aKey If no such value is found then defaultVal is returned. */ - public short getShort(String aKey, short defaultVal) + public short getShort(String aKey, short aDefaultVal) { - Object aValue; - // If no value associated with the key then return defaultVal - aValue = mySingletonMap.get(aKey); - if (aValue == null || aValue.getClass() != Short.class) - return defaultVal; + var tmpVal = mySingletonM.get(aKey); + if (tmpVal == null || tmpVal.getClass() != Short.class) + return aDefaultVal; - return (Short)aValue; + return (Short) tmpVal; } /** - * Returns the float associated with aKey - * If no such value is found then defaultVal is returned. + * Returns the float associated with aKey If no such value is found then defaultVal is returned. */ - public float getFloat(String aKey, float defaultVal) + public float getFloat(String aKey, float aDefaultVal) { - Object aValue; - // If no value associated with the key then return defaultVal - aValue = mySingletonMap.get(aKey); - if (aValue == null || aValue.getClass() != Float.class) - return defaultVal; + var tmpVal = mySingletonM.get(aKey); + if (tmpVal == null || tmpVal.getClass() != Float.class) + return aDefaultVal; - return (Float)aValue; + return (Float) tmpVal; } /** - * Returns the double associated with aKey - * If no such value is found then defaultVal is returned. + * Returns the double associated with aKey If no such value is found then defaultVal is returned. */ - public double getDouble(String aKey, double defaultVal) + public double getDouble(String aKey, double aDefaultVal) { - Object aValue; - // If no value associated with the key then return defaultVal - aValue = mySingletonMap.get(aKey); - if (aValue == null || aValue.getClass() != Double.class) - return defaultVal; + var tmpVal = mySingletonM.get(aKey); + if (tmpVal == null || tmpVal.getClass() != Double.class) + return aDefaultVal; - return (Double)aValue; + return (Double) tmpVal; } /** - * Returns the String associated with aKey - * If no such value is found then defaultVal is returned. + * Returns the String associated with aKey If no such value is found then defaultVal is returned. */ - public String getString(String aKey, String defaultVal) + public String getString(String aKey, String aDefaultVal) { - Object aValue; - // If no value associated with the key then return defaultVal - aValue = mySingletonMap.get(aKey); - if (aValue == null || aValue.getClass() != String.class) - return defaultVal; + var tmpVal = mySingletonM.get(aKey); + if (tmpVal == null || tmpVal.getClass() != String.class) + return aDefaultVal; - return (String)aValue; + return (String) tmpVal; } /** - * Associates aVal with aKey. Note this will overwrite any - * previous association. + * Associates aVal with aKey. Note this will overwrite any previous association. */ public void setBoolean(String aKey, boolean aValue) { - mySingletonMap.put(aKey, aValue); + mySingletonM.put(aKey, aValue); } /** - * Associates aVal with aKey. Note this will overwrite any - * previous association. + * Associates aVal with aKey. Note this will overwrite any previous association. */ public void setInt(String aKey, int aValue) { - mySingletonMap.put(aKey, aValue); + mySingletonM.put(aKey, aValue); } /** - * Associates aVal with aKey. Note this will overwrite any - * previous association. + * Associates aVal with aKey. Note this will overwrite any previous association. */ public void setLong(String aKey, long aValue) { - mySingletonMap.put(aKey, aValue); + mySingletonM.put(aKey, aValue); } /** - * Associates aVal with aKey. Note this will overwrite any - * previous association. + * Associates aVal with aKey. Note this will overwrite any previous association. */ public void setShort(String aKey, short aValue) { - mySingletonMap.put(aKey, aValue); + mySingletonM.put(aKey, aValue); } /** - * Associates aVal with aKey. Note this will overwrite any - * previous association. + * Associates aVal with aKey. Note this will overwrite any previous association. */ public void setFloat(String aKey, float aValue) { - mySingletonMap.put(aKey, aValue); + mySingletonM.put(aKey, aValue); } /** - * Associates aVal with aKey. Note this will overwrite any - * previous association. + * Associates aVal with aKey. Note this will overwrite any previous association. */ public void setDouble(String aKey, double aValue) { - mySingletonMap.put(aKey, aValue); + mySingletonM.put(aKey, aValue); } /** - * Associates aVal with aKey. Note this will overwrite any - * previous association. + * Associates aVal with aKey. Note this will overwrite any previous association. */ public void setString(String aKey, String aValue) { - mySingletonMap.put(aKey, aValue); + mySingletonM.put(aKey, aValue); } /** @@ -213,50 +198,46 @@ public class PrimConfig */ public void readBin(DataInputStream aStream) throws IOException { - int numItems; - String aKey; - byte aType; + mySingletonM.clear(); - mySingletonMap.clear(); - - numItems = aStream.readInt(); + var numItems = aStream.readInt(); for (int c1 = 0; c1 < numItems; c1++) { - aKey = IoUtil.readString(aStream); - aType = aStream.readByte(); - - switch(aType) + var tmpKey = IoUtil.readString(aStream); + var tmpType = aStream.readByte(); + + switch (tmpType) { case CODE_BOOL: - mySingletonMap.put(aKey, aStream.readBoolean()); + mySingletonM.put(tmpKey, aStream.readBoolean()); break; - + case CODE_INT: - mySingletonMap.put(aKey, aStream.readInt()); + mySingletonM.put(tmpKey, aStream.readInt()); break; - + case CODE_LONG: - mySingletonMap.put(aKey, aStream.readLong()); + mySingletonM.put(tmpKey, aStream.readLong()); break; - + case CODE_SHORT: - mySingletonMap.put(aKey, aStream.readShort()); + mySingletonM.put(tmpKey, aStream.readShort()); break; - + case CODE_FLOAT: - mySingletonMap.put(aKey, aStream.readFloat()); + mySingletonM.put(tmpKey, aStream.readFloat()); break; - + case CODE_DOUBLE: - mySingletonMap.put(aKey, aStream.readDouble()); + mySingletonM.put(tmpKey, aStream.readDouble()); break; - + case CODE_STRING: - mySingletonMap.put(aKey, IoUtil.readString(aStream)); + mySingletonM.put(tmpKey, IoUtil.readString(aStream)); break; - + default: - throw new RuntimeException("Unreconnized type: " + aType); + throw new RuntimeException("Unreconnized type: " + tmpType); } } } @@ -266,55 +247,52 @@ public class PrimConfig */ public void writeBin(DataOutputStream aStream) throws IOException { - Set keySet; - Object aVal; - - keySet = mySingletonMap.keySet(); - - aStream.writeInt(keySet.size()); - for (String aKey : keySet) + var keyS = mySingletonM.keySet(); + + aStream.writeInt(keyS.size()); + for (String aKey : keyS) { IoUtil.writeString(aStream, aKey); - - aVal = mySingletonMap.get(aKey); - if (aVal instanceof Boolean) + + var tmpVal = mySingletonM.get(aKey); + if (tmpVal instanceof Boolean) { aStream.writeByte(CODE_BOOL); - aStream.writeBoolean((Boolean)aVal); + aStream.writeBoolean((Boolean) tmpVal); } - else if (aVal instanceof Integer) + else if (tmpVal instanceof Integer) { aStream.writeByte(CODE_INT); - aStream.writeInt((Integer)aVal); + aStream.writeInt((Integer) tmpVal); } - else if (aVal instanceof Long) + else if (tmpVal instanceof Long) { aStream.writeByte(CODE_LONG); - aStream.writeLong((Long)aVal); + aStream.writeLong((Long) tmpVal); } - else if (aVal instanceof Short) + else if (tmpVal instanceof Short) { aStream.writeByte(CODE_SHORT); - aStream.writeLong((Short)aVal); + aStream.writeLong((Short) tmpVal); } - else if (aVal instanceof Float) + else if (tmpVal instanceof Float) { aStream.writeByte(CODE_FLOAT); - aStream.writeFloat((Float)aVal); + aStream.writeFloat((Float) tmpVal); } - else if (aVal instanceof Double) + else if (tmpVal instanceof Double) { aStream.writeByte(CODE_DOUBLE); - aStream.writeDouble((Double)aVal); + aStream.writeDouble((Double) tmpVal); } - else if (aVal instanceof String) + else if (tmpVal instanceof String) { aStream.writeByte(CODE_STRING); - IoUtil.writeString(aStream, (String)aVal); + IoUtil.writeString(aStream, (String) tmpVal); } else { - throw new RuntimeException("Unsupported Object: " + aVal); + throw new RuntimeException("Unsupported Object: " + tmpVal); } } } diff --git a/src/glum/gui/dock/PrimDock.java b/src/glum/gui/dock/PrimDock.java index ada6192..cd29b15 100644 --- a/src/glum/gui/dock/PrimDock.java +++ b/src/glum/gui/dock/PrimDock.java @@ -1,28 +1,41 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock; import bibliothek.gui.dock.DockElement; /** - * Base class for Dockables which would like to store their configuration via - * the PrimConfig mechanism. Note if the child class will be loaded with the - * PrimDockableFactory, then you should have a constructor with one of the - * following properties:. - *

  1. 1 arguments: Registry - *
  2. 0 arguments: Empty Constructor - *
    - *
    It is also important the method getFactoryID() is overridden so that it returns PrimDockableFactory.ID + * Base class for Dockables which would like to store their configuration via the PrimConfig mechanism. Note if the + * child class will be loaded with the PrimDockableFactory, then you should have a constructor with one of the following + * properties:. + *
      + *
    • 1 arguments: Registry + *
    • 0 arguments: Empty Constructor + *
    + * It is also important the method getFactoryID() is overridden so that it returns PrimDockableFactory.ID + * + * @author lopeznr1 */ public interface PrimDock extends DockElement { /** - * Returns the PrimConfig associated with the Dockable - * @return + * Returns the PrimConfig associated with the Dockable. */ public abstract PrimConfig getConfiguration(); /** - * Configures the Dockable with the aConfig - * @return + * Configures the Dockable with the aConfig. */ public abstract void setConfiguration(PrimConfig aConfig); diff --git a/src/glum/gui/dock/PrimDockFactory.java b/src/glum/gui/dock/PrimDockFactory.java index 2be01d8..6748571 100644 --- a/src/glum/gui/dock/PrimDockFactory.java +++ b/src/glum/gui/dock/PrimDockFactory.java @@ -1,16 +1,25 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock; -import glum.reflect.ReflectUtil; -import glum.registry.Registry; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; +import java.io.*; import java.lang.reflect.Constructor; import java.util.Map; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; + import bibliothek.gui.Dockable; import bibliothek.gui.dock.DockFactory; import bibliothek.gui.dock.dockable.DefaultDockablePerspective; @@ -18,47 +27,52 @@ import bibliothek.gui.dock.layout.LocationEstimationMap; import bibliothek.gui.dock.perspective.PerspectiveDockable; import bibliothek.gui.dock.station.support.PlaceholderStrategy; import bibliothek.util.xml.XElement; +import glum.reflect.ReflectUtil; +import glum.registry.Registry; /** * Generic DockableFactory for creating PrimDocks. - *

    + *

    * Note that before this factory is used all PrimDockable class types must first be associated with a spawnName. This is * used during serialization configuration associated with PrimDock. See method {@link PrimDockFactory#addSpawnMapping} + * + * @author lopeznr1 */ public class PrimDockFactory implements DockFactory { // Constants public static final String ID = "PrimDockFactory"; - public static final String SpawnNameKey = "factory.spawnName"; - + public static final String SpawnNameKey = "factory.spawnName"; + // State var - protected Registry refRegistry; - protected BiMap> spawnMap; - + private Registry refRegistry; + private BiMap> spawnM; + + /** Standard Constructor */ public PrimDockFactory(Registry aRegistry) { refRegistry = aRegistry; - spawnMap = HashBiMap.create(); + spawnM = HashBiMap.create(); } - + /** * Add a mapping for a PrimDockable to a the associated spawnName. It is mandatory * that this mapping is always the same regardless of application executions, as - * this value will be serialized to the disk. + * this value will be serialized to the disk. */ - public void addSpawnMapping(String spawnName, Class spawnClass) + public void addSpawnMapping(String aSpawnName, Class aSpawnClass) { // Ensure the spawnName is not already reserved - if (spawnMap.containsKey(spawnName) == true) - throw new RuntimeException("Previous mapping stored for spawnName:" + spawnName); - + if (spawnM.containsKey(aSpawnName) == true) + throw new RuntimeException("Previous mapping stored for spawnName:" + aSpawnName); + // Ensure the spawnClass is not already stored - if (spawnMap.inverse().containsKey(spawnClass) == true) - throw new RuntimeException("Previous mapping stored for spawnClass:" + spawnClass); - - spawnMap.put(spawnName, spawnClass); + if (spawnM.inverse().containsKey(aSpawnClass) == true) + throw new RuntimeException("Previous mapping stored for spawnClass:" + aSpawnClass); + + spawnM.put(aSpawnName, aSpawnClass); } - + @Override public String getID() @@ -71,18 +85,18 @@ public class PrimDockFactory implements DockFactory children, PlaceholderStrategy placeholders) { PrimDock aDockable; - + aDockable = layout(layout, placeholders); return aDockable; } @@ -157,13 +171,13 @@ public class PrimDockFactory implements DockFactory parmTypes[] = {Registry.class}; Object parmValues[] = {refRegistry}; String spawnName; - + spawnName = aLayout.getString(SpawnNameKey, null); - - spawnClass = spawnMap.get(spawnName); + + spawnClass = spawnM.get(spawnName); if (spawnClass == null) throw new RuntimeException("Factory is not configured properly. Failed to locate associated class for spawn name:" + spawnName); - + try { spawnConstructor = ReflectUtil.getConstructorSafe(spawnClass, parmTypes); @@ -176,7 +190,7 @@ public class PrimDockFactory implements DockFactory children) + public void layoutPerspective(DefaultDockablePerspective perspective, PrimConfig layout, + Map children) { ; // Nothing to do } diff --git a/src/glum/gui/dock/PrimDockable.java b/src/glum/gui/dock/PrimDockable.java index 5e37117..fc34137 100644 --- a/src/glum/gui/dock/PrimDockable.java +++ b/src/glum/gui/dock/PrimDockable.java @@ -1,14 +1,30 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock; import bibliothek.gui.dock.DefaultDockable; /** - * Base class for Dockables which would like to store their configuration via - * the PrimConfig mechanism. Note if the child class will be loaded with the - * PrimDockableFactory, then you should have a constructor with one of the - * following properties:. - *

  3. 1 arguments: Registry - *
  4. 0 arguments: Empty Constructor + * Base class for Dockables which would like to store their configuration via the PrimConfig mechanism. Note if the + * child class will be loaded with the PrimDockableFactory, then you should have a constructor with one of the following + * properties:. + *
      + *
    • 1 arguments: Registry + *
    • 0 arguments: Empty Constructor + *
    + * + * @author lopeznr1 */ public abstract class PrimDockable extends DefaultDockable implements PrimDock { diff --git a/src/glum/gui/dock/action/Closeable.java b/src/glum/gui/dock/action/Closeable.java index c1995dc..1f9d755 100644 --- a/src/glum/gui/dock/action/Closeable.java +++ b/src/glum/gui/dock/action/Closeable.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock.action; public interface Closeable diff --git a/src/glum/gui/dock/action/Destroyable.java b/src/glum/gui/dock/action/Destroyable.java index 7acc10b..69312cc 100644 --- a/src/glum/gui/dock/action/Destroyable.java +++ b/src/glum/gui/dock/action/Destroyable.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock.action; public interface Destroyable diff --git a/src/glum/gui/dock/action/DismissAction.java b/src/glum/gui/dock/action/DismissAction.java index 991a87e..00071bd 100644 --- a/src/glum/gui/dock/action/DismissAction.java +++ b/src/glum/gui/dock/action/DismissAction.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock.action; import javax.swing.Icon; diff --git a/src/glum/gui/dock/action/MakeVisibleAction.java b/src/glum/gui/dock/action/MakeVisibleAction.java index f40dc08..443fffc 100644 --- a/src/glum/gui/dock/action/MakeVisibleAction.java +++ b/src/glum/gui/dock/action/MakeVisibleAction.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock.action; import javax.swing.Icon; diff --git a/src/glum/gui/dock/action/SimpleDockAction.java b/src/glum/gui/dock/action/SimpleDockAction.java index d75e134..e002d8b 100644 --- a/src/glum/gui/dock/action/SimpleDockAction.java +++ b/src/glum/gui/dock/action/SimpleDockAction.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock.action; import java.awt.event.ActionEvent; diff --git a/src/glum/gui/dock/action/ToggleAction.java b/src/glum/gui/dock/action/ToggleAction.java index eed2ec1..bf89008 100644 --- a/src/glum/gui/dock/action/ToggleAction.java +++ b/src/glum/gui/dock/action/ToggleAction.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock.action; import javax.swing.Icon; @@ -7,21 +20,22 @@ import bibliothek.gui.dock.action.actions.SimpleButtonAction; /** * An DockAction that will fire trigger an embedded java.awt Action + * + * @author lopeznr1 */ public class ToggleAction extends SimpleButtonAction { // State vars - protected boolean isActive; - + private boolean isActive; + // Gui vars - protected Icon falseIcon, trueIcon; - + private final Icon falseIcon, trueIcon; + + /** Standard Constructor */ public ToggleAction(String aText, Icon aFalseIcon, Icon aTrueIcon, boolean aIsActive) { - super(); - isActive = aIsActive; - + falseIcon = aFalseIcon; trueIcon = aTrueIcon; @@ -36,24 +50,24 @@ public class ToggleAction extends SimpleButtonAction { return isActive; } - + public void setIsActive(boolean aBool) { isActive = aBool; updateGui(); } - + @Override public void action(Dockable aDockable) { isActive = !isActive; updateGui(); - + super.action(aDockable); } /** - * Utility method + * Utility method */ private void updateGui() { diff --git a/src/glum/gui/dock/alt/AltScreenDockFrame.java b/src/glum/gui/dock/alt/AltScreenDockFrame.java index e161d2f..3aec91b 100644 --- a/src/glum/gui/dock/alt/AltScreenDockFrame.java +++ b/src/glum/gui/dock/alt/AltScreenDockFrame.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock.alt; import glum.gui.dock.action.Destroyable; diff --git a/src/glum/gui/dock/alt/AltScreenDockStation.java b/src/glum/gui/dock/alt/AltScreenDockStation.java index a46b25a..8233213 100644 --- a/src/glum/gui/dock/alt/AltScreenDockStation.java +++ b/src/glum/gui/dock/alt/AltScreenDockStation.java @@ -1,42 +1,58 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock.alt; -import glum.gui.dock.BaseDockable; - import java.awt.Window; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; - import bibliothek.gui.Dockable; import bibliothek.gui.dock.ScreenDockStation; import bibliothek.gui.dock.action.DefaultDockActionSource; import bibliothek.gui.dock.action.DockAction; import bibliothek.gui.dock.action.DockActionSource; import bibliothek.gui.dock.action.LocationHint; +import glum.gui.dock.BaseDockable; /** * Alternative ScreenDockStation which provides no default direct/indirect action offers. + * + * @author lopeznr1 */ public class AltScreenDockStation extends ScreenDockStation { // Action vars - private List directDockActionList; - private List indirectDockActionList; + private List directDockActionL; + private List indirectDockActionL; // Lock vars - private Set lockSet; + private Set lockS; private boolean isLocked; - public AltScreenDockStation(Window owner) + /** + * Standard Constructor + */ + public AltScreenDockStation(Window aOwner) { - super(owner); + super(aOwner); - directDockActionList = Lists.newArrayList(); - indirectDockActionList = Lists.newArrayList(); + directDockActionL = new ArrayList<>(); + indirectDockActionL = new ArrayList<>(); - lockSet = Sets.newHashSet(); + lockS = new HashSet<>(); isLocked = false; } @@ -45,7 +61,7 @@ public class AltScreenDockStation extends ScreenDockStation */ public void addDirectActionOffer(DockAction aDockAction) { - directDockActionList.add(aDockAction); + directDockActionL.add(aDockAction); } /** @@ -53,7 +69,7 @@ public class AltScreenDockStation extends ScreenDockStation */ public void addIndirectActionOffer(DockAction aDockAction) { - indirectDockActionList.add(aDockAction); + indirectDockActionL.add(aDockAction); } /** @@ -66,14 +82,14 @@ public class AltScreenDockStation extends ScreenDockStation if (isLocked == false) { - lockSet.clear(); + lockS.clear(); return; } // Record all of the valid children when the lock is triggered for (int c1 = 0; c1 < getDockableCount(); c1++) { - lockSet.add(getDockable(c1)); + lockS.add(getDockable(c1)); } } @@ -82,34 +98,34 @@ public class AltScreenDockStation extends ScreenDockStation public boolean accept(Dockable aChild) { // If we are locked then never accept any Dockable, which was not recorded as valid when the lock happened - if (isLocked == true && lockSet.contains(aChild) == false) + if (isLocked == true && lockS.contains(aChild) == false) return false; // Never accept any Dockable that has been marked as nontransferable if (aChild instanceof BaseDockable) - return ((BaseDockable)aChild).isTransferable(this); + return ((BaseDockable) aChild).isTransferable(this); return super.accept(aChild); } @Override - public DefaultDockActionSource getDirectActionOffers(Dockable dockable) + public DefaultDockActionSource getDirectActionOffers(Dockable aDockable) { DefaultDockActionSource source; source = new DefaultDockActionSource(new LocationHint(LocationHint.DIRECT_ACTION, LocationHint.VERY_RIGHT)); - source.add(directDockActionList.toArray(new DockAction[0])); + source.add(directDockActionL.toArray(new DockAction[0])); return source; } @Override - public DockActionSource getIndirectActionOffers(Dockable dockable) + public DockActionSource getIndirectActionOffers(Dockable aDockable) { DefaultDockActionSource source; source = new DefaultDockActionSource(new LocationHint(LocationHint.INDIRECT_ACTION, LocationHint.VERY_RIGHT)); - source.add(indirectDockActionList.toArray(new DockAction[0])); + source.add(indirectDockActionL.toArray(new DockAction[0])); return source; } diff --git a/src/glum/gui/dock/alt/AltScreenDockWindowFactory.java b/src/glum/gui/dock/alt/AltScreenDockWindowFactory.java index 6c0aba7..b62b4b9 100644 --- a/src/glum/gui/dock/alt/AltScreenDockWindowFactory.java +++ b/src/glum/gui/dock/alt/AltScreenDockWindowFactory.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock.alt; import java.awt.Dialog; diff --git a/src/glum/gui/dock/alt/AltSplitDockStation.java b/src/glum/gui/dock/alt/AltSplitDockStation.java index c1e3e0b..dc09b51 100644 --- a/src/glum/gui/dock/alt/AltSplitDockStation.java +++ b/src/glum/gui/dock/alt/AltSplitDockStation.java @@ -1,39 +1,49 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.dock.alt; -import glum.gui.dock.BaseDockable; -import java.util.List; -import java.util.Set; - -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; +import java.util.*; import bibliothek.gui.Dockable; import bibliothek.gui.dock.SplitDockStation; -import bibliothek.gui.dock.action.DefaultDockActionSource; -import bibliothek.gui.dock.action.DockAction; -import bibliothek.gui.dock.action.DockActionSource; -import bibliothek.gui.dock.action.LocationHint; +import bibliothek.gui.dock.action.*; +import glum.gui.dock.BaseDockable; +/** + * Alternative implementation of SplitDockStation. + * + * @author lopeznr1 + */ public class AltSplitDockStation extends SplitDockStation { // Action vars - private List directDockActionList; - private List localDockActionList; - private List indirectDockActionList; + private List directDockActionL; + private List localDockActionL; + private List indirectDockActionL; // Lock vars - private Set lockSet; + private Set lockS; private boolean isLocked; + /** Standard Constructor */ public AltSplitDockStation() { - super(); + directDockActionL = new ArrayList<>(); + localDockActionL = new ArrayList<>(); + indirectDockActionL = new ArrayList<>(); - directDockActionList = Lists.newArrayList(); - localDockActionList = Lists.newArrayList(); - indirectDockActionList = Lists.newArrayList(); - - lockSet = Sets.newHashSet(); + lockS = new HashSet<>(); isLocked = false; } @@ -42,7 +52,7 @@ public class AltSplitDockStation extends SplitDockStation */ public void addDirectActionOffer(DockAction aDockAction) { - directDockActionList.add(aDockAction); + directDockActionL.add(aDockAction); } /** @@ -50,7 +60,7 @@ public class AltSplitDockStation extends SplitDockStation */ public void addLocalActionOffer(DockAction aDockAction) { - localDockActionList.add(aDockAction); + localDockActionL.add(aDockAction); } /** @@ -58,7 +68,7 @@ public class AltSplitDockStation extends SplitDockStation */ public void addIndirectActionOffer(DockAction aDockAction) { - indirectDockActionList.add(aDockAction); + indirectDockActionL.add(aDockAction); } /** @@ -71,14 +81,14 @@ public class AltSplitDockStation extends SplitDockStation if (isLocked == false) { - lockSet.clear(); + lockS.clear(); return; } // Record all of the valid children when the lock is triggered for (int c1 = 0; c1 < getDockableCount(); c1++) { - lockSet.add(getDockable(c1)); + lockS.add(getDockable(c1)); } } @@ -87,18 +97,18 @@ public class AltSplitDockStation extends SplitDockStation public boolean accept(Dockable aChild) { // If we are locked then never accept any Dockable, which was not recorded as valid when the lock happened - if (isLocked == true && lockSet.contains(aChild) == false) + if (isLocked == true && lockS.contains(aChild) == false) return false; // Never accept any Dockable that has been marked as nontransferable if (aChild instanceof BaseDockable) - return ((BaseDockable)aChild).isTransferable(this); + return ((BaseDockable) aChild).isTransferable(this); // Default behavior for non BaseDockables return super.accept(aChild); } -// +// // @Override // protected boolean acceptable(Dockable old, Dockable next) // { @@ -112,8 +122,8 @@ public class AltSplitDockStation extends SplitDockStation //// if (((BaseDockable)next).isTransferable() == false) //// return false; //// } -//// -//// +//// +//// //// // TODO Auto-generated method stub //// return super.acceptable(old, next); // } @@ -127,7 +137,7 @@ public class AltSplitDockStation extends SplitDockStation // return super.canDrag(aDockable); // } // -// +// // @Override // public boolean canReplace(Dockable oldDockable, Dockable nextDockable) // { @@ -141,12 +151,10 @@ public class AltSplitDockStation extends SplitDockStation // } @Override - public DefaultDockActionSource getDirectActionOffers(Dockable dockable) + public DefaultDockActionSource getDirectActionOffers(Dockable aDockable) { - DefaultDockActionSource source; - - source = new DefaultDockActionSource(new LocationHint(LocationHint.DIRECT_ACTION, LocationHint.VERY_RIGHT)); - source.add(directDockActionList.toArray(new DockAction[0])); + var source = new DefaultDockActionSource(new LocationHint(LocationHint.DIRECT_ACTION, LocationHint.VERY_RIGHT)); + source.add(directDockActionL.toArray(new DockAction[0])); return source; } @@ -154,21 +162,17 @@ public class AltSplitDockStation extends SplitDockStation @Override public DockActionSource getLocalActionOffers() { - DefaultDockActionSource source; - - source = new DefaultDockActionSource(new LocationHint(LocationHint.DOCKABLE, LocationHint.RIGHT)); - source.add(localDockActionList.toArray(new DockAction[0])); + var source = new DefaultDockActionSource(new LocationHint(LocationHint.DOCKABLE, LocationHint.RIGHT)); + source.add(localDockActionL.toArray(new DockAction[0])); return source; } @Override - public DockActionSource getIndirectActionOffers(Dockable dockable) + public DockActionSource getIndirectActionOffers(Dockable aDockable) { - DefaultDockActionSource source; - - source = new DefaultDockActionSource(new LocationHint(LocationHint.INDIRECT_ACTION, LocationHint.VERY_RIGHT)); - source.add(indirectDockActionList.toArray(new DockAction[0])); + var source = new DefaultDockActionSource(new LocationHint(LocationHint.INDIRECT_ACTION, LocationHint.VERY_RIGHT)); + source.add(indirectDockActionL.toArray(new DockAction[0])); return source; } diff --git a/src/glum/gui/document/BaseDocument.java b/src/glum/gui/document/BaseDocument.java index 64926aa..160a230 100644 --- a/src/glum/gui/document/BaseDocument.java +++ b/src/glum/gui/document/BaseDocument.java @@ -1,24 +1,38 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.document; import java.awt.event.*; -import javax.swing.text.*; + import javax.swing.JTextField; +import javax.swing.text.PlainDocument; public abstract class BaseDocument extends PlainDocument implements ActionListener, FocusListener { // State vars protected JTextField ownerTF; + /** Standard Constructor */ public BaseDocument(JTextField aOwnerTF) { - super(); - ownerTF = aOwnerTF; } /** * Get the owner of this Document model - * Todo: This method should no longer be needed. + *

    + * TODO: This method should no longer be needed. */ public JTextField getOwner() { diff --git a/src/glum/gui/document/BaseNumberDocument.java b/src/glum/gui/document/BaseNumberDocument.java index a1973fe..7bec794 100644 --- a/src/glum/gui/document/BaseNumberDocument.java +++ b/src/glum/gui/document/BaseNumberDocument.java @@ -1,23 +1,40 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.document; import javax.swing.JTextField; +/** + * Implementation of {@link BaseDocument} specific for numerical input. + * + * @author lopeznr1 + */ public abstract class BaseNumberDocument extends BaseDocument { // State vars protected double minVal, maxVal; protected boolean formalizeDoc; - protected int numAvailCols; + /** Standard Constructor */ public BaseNumberDocument(JTextField aOwner, double aMinVal, double aMaxVal) { super(aOwner); - + setMinMaxValue(aMinVal, aMaxVal); formalizeDoc = false; - numAvailCols = -1; } - + /** * Updates the new range of valid numbers. */ @@ -25,12 +42,12 @@ public abstract class BaseNumberDocument extends BaseDocument { minVal = aMinVal; maxVal = aMaxVal; - + // Insanity check if (minVal >= maxVal) throw new RuntimeException("Illogical range. Range: [" + minVal + "," + maxVal + "]"); } - + @Override public void formalizeInput() { @@ -56,6 +73,4 @@ public abstract class BaseNumberDocument extends BaseDocument ownerTF.addFocusListener(this); } - - } diff --git a/src/glum/gui/document/CharDocument.java b/src/glum/gui/document/CharDocument.java index 9a1b9d2..51a3f12 100644 --- a/src/glum/gui/document/CharDocument.java +++ b/src/glum/gui/document/CharDocument.java @@ -1,37 +1,49 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.document; +import java.util.HashSet; import java.util.Set; import javax.swing.JTextField; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; -import com.google.common.collect.Sets; - /** * Specialized Document designed to accept only the specified input chars + * + * @author lopeznr1 */ public class CharDocument extends BaseDocument { - private Set validSet; + private Set validS; - public CharDocument(JTextField aOwner, String validCharStr) - { - this(aOwner, validCharStr, true); - } - - public CharDocument(JTextField aOwner, String validCharStr, boolean isCaseSensitive) + /** + * Standard Constructor + */ + public CharDocument(JTextField aOwner, String aValidCharStr, boolean aIsCaseSensitive) { super(aOwner); - - validSet = Sets.newHashSet(); - for (int c1 = 0; c1 < validCharStr.length(); c1++) + + validS = new HashSet<>(); + for (int c1 = 0; c1 < aValidCharStr.length(); c1++) { - validSet.add(validCharStr.charAt(c1)); - if (isCaseSensitive == false) + validS.add(aValidCharStr.charAt(c1)); + if (aIsCaseSensitive == false) { - validSet.add(Character.toLowerCase(validCharStr.charAt(c1))); - validSet.add(Character.toUpperCase(validCharStr.charAt(c1))); + validS.add(Character.toLowerCase(aValidCharStr.charAt(c1))); + validS.add(Character.toUpperCase(aValidCharStr.charAt(c1))); } } } @@ -41,25 +53,23 @@ public class CharDocument extends BaseDocument { ; // Nothing to do } - - @Override - public void insertString(int offs, String str, AttributeSet a) throws BadLocationException - { - char aChar; + @Override + public void insertString(int aOffs, String aStr, AttributeSet aAS) throws BadLocationException + { // Insanity check - if (str == null) + if (aStr == null) return; - + // Ensure all of the characters in str are in the valid set - for (int c1 = 0; c1 < str.length(); c1++) + for (int c1 = 0; c1 < aStr.length(); c1++) { - aChar = str.charAt(c1); - if (validSet.contains(aChar) == false) - throw new BadLocationException("Invalid character: " + aChar, offs); + char tmpChar = aStr.charAt(c1); + if (validS.contains(tmpChar) == false) + throw new BadLocationException("Invalid character: " + tmpChar, aOffs); } - - super.insertString(offs, str, a); + + super.insertString(aOffs, aStr, aAS); } } diff --git a/src/glum/gui/document/NumberDocument.java b/src/glum/gui/document/NumberDocument.java index ab00b93..9de1956 100644 --- a/src/glum/gui/document/NumberDocument.java +++ b/src/glum/gui/document/NumberDocument.java @@ -1,15 +1,40 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.document; -import javax.swing.text.*; import javax.swing.JTextField; +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; -import glum.gui.GuiUtil; +import glum.io.ParseUtil; +/** + * Implementation of {@link BaseNumberDocument}. + * + * @author lopeznr1 + */ public class NumberDocument extends BaseNumberDocument { - protected boolean allowFloats; -// protected NumberUnit myUnit; + // Constants + private final String ValidPosDigitStr = "0123456789"; + private final String ValidNegDigitStr = "+-0123456789"; + private final String ValidFractStr = "+-0123456789e."; + protected boolean allowFloats; +// protected NumberUnit myUnit; + + /** Standard Constructor */ public NumberDocument(JTextField aOwnerTF, boolean aFormalizeDoc) { super(aOwnerTF, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); @@ -38,6 +63,28 @@ public class NumberDocument extends BaseNumberDocument if (str == null) return; + // Change to allow user to enter any valid single character to be input. Note that the + // NumberDocument is used primary by GNumberField and as such invalid input will be + // colored with the "fail" color. + if (str.length() == 1) + { + if (allowFloats == false && minVal >= 0 && ValidPosDigitStr.contains(str) == true) + { + super.insertString(offs, str, a); + return; + } + else if (allowFloats == false && minVal < 0 && ValidNegDigitStr.contains(str) == true) + { + super.insertString(offs, str, a); + return; + } + else if (allowFloats == true && ValidFractStr.contains(str) == true) + { + super.insertString(offs, str, a); + return; + } + } + // Special cases aChar = str.charAt(0); if (offs == 0) @@ -76,13 +123,6 @@ public class NumberDocument extends BaseNumberDocument if (str.contains(".") == true && allowFloats == false) throw new BadLocationException("Only integers are allowed.", offs); - // Ensure we do not exceed number of columns - if (numAvailCols > 0) - { - if (offs + str.length() >= numAvailCols) - throw new BadLocationException("Too many characters to insert.", offs); - } - // Form the resultant string bStr = ""; eStr = ""; @@ -92,7 +132,7 @@ public class NumberDocument extends BaseNumberDocument resultStr = bStr + str + eStr; // Ensure the resultant is sensical - aVal = GuiUtil.readDouble(resultStr, Double.NaN); + aVal = ParseUtil.readDouble(resultStr, Double.NaN); if (Double.isNaN(aVal) == true) throw new BadLocationException("Nonsensical number.", offs); diff --git a/src/glum/gui/icon/ArrowNorthIcon.java b/src/glum/gui/icon/ArrowNorthIcon.java index 7cb2fbe..abc8c2e 100644 --- a/src/glum/gui/icon/ArrowNorthIcon.java +++ b/src/glum/gui/icon/ArrowNorthIcon.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.icon; import java.awt.Color; diff --git a/src/glum/gui/icon/ArrowSouthIcon.java b/src/glum/gui/icon/ArrowSouthIcon.java index b275f74..280e1ed 100644 --- a/src/glum/gui/icon/ArrowSouthIcon.java +++ b/src/glum/gui/icon/ArrowSouthIcon.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.icon; import java.awt.Color; diff --git a/src/glum/gui/icon/BaseIcon.java b/src/glum/gui/icon/BaseIcon.java index e6b69db..6fa0849 100644 --- a/src/glum/gui/icon/BaseIcon.java +++ b/src/glum/gui/icon/BaseIcon.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.icon; import javax.swing.Icon; diff --git a/src/glum/gui/icon/DeleteIcon.java b/src/glum/gui/icon/DeleteIcon.java index 99a39d5..d503b95 100644 --- a/src/glum/gui/icon/DeleteIcon.java +++ b/src/glum/gui/icon/DeleteIcon.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.icon; import java.awt.BasicStroke; diff --git a/src/glum/gui/icon/EmptyIcon.java b/src/glum/gui/icon/EmptyIcon.java index 3d29e27..63c153a 100644 --- a/src/glum/gui/icon/EmptyIcon.java +++ b/src/glum/gui/icon/EmptyIcon.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.icon; import java.awt.Component; @@ -5,11 +18,28 @@ import java.awt.Graphics; import javax.swing.Icon; +/** + * Immutable icon that renders nothing. + *

    + * This icon will typically be used as a place holder. + * + * @author lopeznr1 + */ public class EmptyIcon implements Icon { - protected int width, height; - - EmptyIcon(int aWidth, int aHeight) + // Attributes + private final int width; + private final int height; + + /** + * Standard Constructor + * + * @param aWidth + * The width of the icon. + * @param aHeight + * The height of the icon. + */ + public EmptyIcon(int aWidth, int aHeight) { width = aWidth; height = aHeight; diff --git a/src/glum/gui/icon/IconUtil.java b/src/glum/gui/icon/IconUtil.java index 0a5d305..9d1df82 100644 --- a/src/glum/gui/icon/IconUtil.java +++ b/src/glum/gui/icon/IconUtil.java @@ -1,24 +1,45 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.icon; import java.awt.Component; +import java.net.URL; + import javax.swing.ImageIcon; import javax.swing.JButton; +/** + * Collection of utility methods for dealing with icons. + * + * @author lopeznr1 + */ public class IconUtil { /** - * Utility method to load the Icon specified by the resource path. + * Utility method to load an icon from the specified resource. */ - public static ImageIcon loadIcon(String iconPath) + public static ImageIcon loadIcon(String aIconPath) { -// URL aURL; -// -// aURL = IconUtil.class.getClassLoader().getResource(iconPath); -// if (aURL == null) -// throw new RuntimeException("Failed to load icon for path: " + iconPath); -// -// return new ImageIcon(aURL); - return new ImageIcon(ClassLoader.getSystemResource(iconPath)); + return new ImageIcon(ClassLoader.getSystemResource(aIconPath)); + } + + /** + * Utility method to load an icon from the specified resource. + */ + public static ImageIcon loadIcon(URL aURL) + { + return new ImageIcon(aURL); } /** @@ -28,8 +49,8 @@ public class IconUtil { if (aComp instanceof JButton == false) return false; - - return ((JButton)aComp).getModel().isPressed(); + + return ((JButton) aComp).getModel().isPressed(); } } diff --git a/src/glum/gui/info/FilePathInfo.java b/src/glum/gui/info/FilePathInfo.java index 3ef7b7a..c1121c0 100644 --- a/src/glum/gui/info/FilePathInfo.java +++ b/src/glum/gui/info/FilePathInfo.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.info; import glum.zio.*; diff --git a/src/glum/gui/info/WindowCfg.java b/src/glum/gui/info/WindowCfg.java new file mode 100644 index 0000000..5c4bd25 --- /dev/null +++ b/src/glum/gui/info/WindowCfg.java @@ -0,0 +1,162 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.info; + +import java.awt.Component; +import java.awt.Dimension; +import java.io.IOException; + +import glum.zio.*; + +/** + * Immutable object used to store the position/dimension/visibility state of a {@link Component}. + *

    + * This is typically used capture the state of a dialog or window. + * + * @author lopeznr1 + */ +public class WindowCfg implements ZioRaw +{ + // Attributes + private final boolean isShown; + private final int posX; + private final int posY; + private final int dimX; + private final int dimY; + + /** Standard Constructor */ + public WindowCfg(boolean aIsShown, int aPosX, int aPosY, int aDimX, int aDimY) + { + isShown = aIsShown; + + posX = aPosX; + posY = aPosY; + + dimX = aDimX; + dimY = aDimY; + } + + /** Serialization Constructor */ + public WindowCfg(ZinStream aStream) throws IOException + { + aStream.readVersion(0); + + isShown = aStream.readBool(); + + posX = aStream.readInt(); + posY = aStream.readInt(); + + dimX = aStream.readInt(); + dimY = aStream.readInt(); + } + + /** + * UI based constructor + * + * The WindowInfo will be setup to the current state of the provided {@link Component}. + */ + public WindowCfg(Component aComponent) + { + isShown = aComponent.isVisible(); + + posX = aComponent.getLocation().x; + posY = aComponent.getLocation().y; + + dimX = aComponent.getSize().width; + dimY = aComponent.getSize().height; + } + + /** + * Syncs the specified {@link Component} to match the state of this {@link WindowCfg}. + */ + public void configure(Component aComponent) + { + aComponent.setLocation(posX, posY); + + aComponent.setPreferredSize(new Dimension(dimX, dimY)); + aComponent.setSize(dimX, dimY); + } + + /** + * Returns a {@link WindowCfg} which matches this WindowInfo but with the isShown flag set to the requested setting. + */ + public WindowCfg withIsShown(boolean aIsShown) + { + // Bail if nothing changes + if (isShown == aIsShown) + return this; + + return new WindowCfg(aIsShown, posX, posY, dimX, dimY); + } + + // @formatter:off + // Accessor methods + public boolean isShown() { return isShown; } + public int dimX() { return dimX; } + public int dimY() { return dimY; } + public int posX() { return posX; } + public int posY() { return posY; } + // @formatter:on + + @Override + public void zioWrite(ZoutStream aStream) throws IOException + { + aStream.writeVersion(0); + + aStream.writeBool(isShown); + + aStream.writeInt(posX); + aStream.writeInt(posY); + + aStream.writeInt(dimX); + aStream.writeInt(dimY); + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + dimX; + result = prime * result + dimY; + result = prime * result + (isShown ? 1231 : 1237); + result = prime * result + posX; + result = prime * result + posY; + return result; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + WindowCfg other = (WindowCfg) obj; + if (dimX != other.dimX) + return false; + if (dimY != other.dimY) + return false; + if (isShown != other.isShown) + return false; + if (posX != other.posX) + return false; + if (posY != other.posY) + return false; + return true; + } + +} diff --git a/src/glum/gui/info/WindowInfo.java b/src/glum/gui/info/WindowInfo.java deleted file mode 100644 index 9247252..0000000 --- a/src/glum/gui/info/WindowInfo.java +++ /dev/null @@ -1,77 +0,0 @@ -package glum.gui.info; - -import glum.zio.*; -import glum.zio.util.ZioUtil; - -import java.awt.*; -import java.io.*; - -public class WindowInfo implements ZioObj -{ - // Raw vars - protected Point position; - protected Dimension size; - protected boolean isVisible; - - /** - * Constructor - */ - public WindowInfo() - { - position = null; - size = null; - isVisible = false; - } - - public WindowInfo(Component aComponent) - { - this(); - - if (aComponent == null) - return; - - position = aComponent.getLocation(); - size = aComponent.getSize(); - isVisible = aComponent.isVisible(); - } - - /** - * configure - Syncs aComponent with parmaters of this WindowInfo - */ - public void configure(Component aComponent) - { - if (position != null) - aComponent.setLocation(position); - - if (size != null) - { - aComponent.setPreferredSize(size); - aComponent.setSize(size); - } - } - - @Override - public void zioRead(ZinStream aStream) throws IOException - { - aStream.readVersion(0); - - isVisible = aStream.readBool(); - - position = ZioUtil.readPoint(aStream); - - size = ZioUtil.readDimension(aStream); - } - - @Override - public void zioWrite(ZoutStream aStream) throws IOException - { - aStream.writeVersion(0); - - aStream.writeBool(isVisible); - - ZioUtil.writePoint(aStream, position); - - ZioUtil.writeDimension(aStream, size); - } - -} diff --git a/src/glum/gui/dialog/MemoryUtilDialog.java b/src/glum/gui/memory/MemoryUtilDialog.java similarity index 59% rename from src/glum/gui/dialog/MemoryUtilDialog.java rename to src/glum/gui/memory/MemoryUtilDialog.java index 968e548..7e000b3 100644 --- a/src/glum/gui/dialog/MemoryUtilDialog.java +++ b/src/glum/gui/memory/MemoryUtilDialog.java @@ -1,20 +1,36 @@ -package glum.gui.dialog; +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.memory; -import java.awt.Font; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.text.DecimalFormat; + import javax.swing.*; -import net.miginfocom.swing.MigLayout; import glum.gui.FocusUtil; import glum.gui.GuiUtil; import glum.gui.action.ClickAction; import glum.gui.component.GLabel; -import glum.unit.ConstUnitProvider; -import glum.unit.NumberUnit; -import glum.unit.UnitProvider; +import glum.unit.*; +import net.miginfocom.swing.MigLayout; +/** + * UI componet that provides a view of the applications memory state. + * + * @author lopeznr1 + */ public class MemoryUtilDialog extends JDialog implements ActionListener { // Gui components @@ -23,7 +39,7 @@ public class MemoryUtilDialog extends JDialog implements ActionListener private JButton closeB, gcRunB, updateB; /** - * Constructor + * Standard Constructor */ public MemoryUtilDialog(JFrame parentFrame) { @@ -33,8 +49,7 @@ public class MemoryUtilDialog extends JDialog implements ActionListener setDefaultCloseOperation(HIDE_ON_CLOSE); setModal(false); - DecimalFormat numFormat; - numFormat = new DecimalFormat(); + var numFormat = new DecimalFormat(); numFormat.setGroupingUsed(true); numFormat.setGroupingSize(3); numFormat.setMaximumFractionDigits(0); @@ -52,9 +67,7 @@ public class MemoryUtilDialog extends JDialog implements ActionListener @Override public void actionPerformed(ActionEvent e) { - Object source; - - source = e.getSource(); + var source = e.getSource(); if (source == gcRunB) { System.gc(); @@ -82,40 +95,36 @@ public class MemoryUtilDialog extends JDialog implements ActionListener */ private void buildGuiArea() { - JPanel aPanel; - JLabel tmpL; - Font tmpFont; - // Form the panel - aPanel = new JPanel(new MigLayout("", "[right][left,grow]", "[][][]10[]10")); - tmpFont = new JTextField().getFont(); + var tmpPanel = new JPanel(new MigLayout("", "[right][left,grow]", "[][][]10[]10")); + var tmpFont = new JTextField().getFont(); // Info area - tmpL = new JLabel("Total Memory:"); + var tmpL = new JLabel("Total Memory:"); totalMemL = new GLabel(byteUP, tmpFont, true); - aPanel.add(tmpL, ""); - aPanel.add(totalMemL, "growx,wrap"); + tmpPanel.add(tmpL, ""); + tmpPanel.add(totalMemL, "growx,wrap"); tmpL = new JLabel("Free Memory:"); freeMemL = new GLabel(byteUP, tmpFont, true); - aPanel.add(tmpL, ""); - aPanel.add(freeMemL, "growx,wrap"); + tmpPanel.add(tmpL, ""); + tmpPanel.add(freeMemL, "growx,wrap"); tmpL = new JLabel("Used Memory:"); usedMemL = new GLabel(byteUP, tmpFont, true); - aPanel.add(tmpL, ""); - aPanel.add(usedMemL, "growx,wrap"); + tmpPanel.add(tmpL, ""); + tmpPanel.add(usedMemL, "growx,wrap"); // Control area updateB = GuiUtil.createJButton("Update", this); gcRunB = GuiUtil.createJButton("Run GC", this); closeB = GuiUtil.createJButton("Close", this); - aPanel.add(updateB, "span 2,split 3"); - aPanel.add(gcRunB, ""); - aPanel.add(closeB, ""); + tmpPanel.add(updateB, "span 2,split 3"); + tmpPanel.add(gcRunB, ""); + tmpPanel.add(closeB, ""); // Add the main panel into the dialog - getContentPane().add(aPanel); + getContentPane().add(tmpPanel); pack(); } @@ -124,15 +133,12 @@ public class MemoryUtilDialog extends JDialog implements ActionListener */ private void updateGui() { - Runtime aRuntime; - long freeMem, usedMem, totalMem; - - aRuntime = Runtime.getRuntime(); + var tmpRuntime = Runtime.getRuntime(); // Update the memory usage - freeMem = aRuntime.freeMemory(); - totalMem = aRuntime.totalMemory(); - usedMem = totalMem - freeMem; + var freeMem = tmpRuntime.freeMemory(); + var totalMem = tmpRuntime.totalMemory(); + var usedMem = totalMem - freeMem; freeMemL.setValue(freeMem); totalMemL.setValue(totalMem); diff --git a/src/glum/gui/misc/BooleanCellEditor.java b/src/glum/gui/misc/BooleanCellEditor.java index 334bc16..bc3d80a 100644 --- a/src/glum/gui/misc/BooleanCellEditor.java +++ b/src/glum/gui/misc/BooleanCellEditor.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.misc; import java.awt.Component; @@ -5,43 +18,47 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Collection; import java.util.LinkedList; -import javax.swing.AbstractCellEditor; -import javax.swing.JCheckBox; -import javax.swing.JTable; + +import javax.swing.*; import javax.swing.table.TableCellEditor; +/** + * Cell editor suitable for editing boolean values. + * + * @author lopeznr1 + */ public class BooleanCellEditor extends AbstractCellEditor implements ActionListener, TableCellEditor { // State vars - protected Collection myListeners; - protected JCheckBox refCheckBox; + private Collection listenerL; + private JCheckBox refCheckBox; - /** - * Constructor - */ + /** Standard Constructor */ + public BooleanCellEditor(ActionListener aListener) + { + listenerL = new LinkedList<>(); + if (aListener != null) + listenerL.add(aListener); + + refCheckBox = new JCheckBox("", false); + refCheckBox.addActionListener(this); + refCheckBox.setHorizontalAlignment(JCheckBox.CENTER); + } + + /** Simplified Constructor */ public BooleanCellEditor() { this(null); } - public BooleanCellEditor(ActionListener aListener) - { - myListeners = new LinkedList(); - if (aListener != null) - myListeners.add(aListener); - - refCheckBox = new JCheckBox("", false); - refCheckBox.addActionListener(this); - } - public void addActionListener(ActionListener aListener) { - myListeners.add(aListener); + listenerL.add(aListener); } public void removeActionListener(ActionListener aListener) { - myListeners.remove(aListener); + listenerL.remove(aListener); } @Override @@ -50,7 +67,7 @@ public class BooleanCellEditor extends AbstractCellEditor implements ActionListe fireEditingStopped(); aEvent = new ActionEvent(this, aEvent.getID(), "BooleanCell edited."); - for (ActionListener aListener : myListeners) + for (ActionListener aListener : listenerL) aListener.actionPerformed(aEvent); } @@ -60,7 +77,7 @@ public class BooleanCellEditor extends AbstractCellEditor implements ActionListe // Update our checkbox with the appropriate state refCheckBox.removeActionListener(this); if (value instanceof Boolean) - refCheckBox.setSelected((Boolean)value); + refCheckBox.setSelected((Boolean) value); refCheckBox.addActionListener(this); return refCheckBox; diff --git a/src/glum/gui/misc/BooleanCellRenderer.java b/src/glum/gui/misc/BooleanCellRenderer.java index 30a5062..dd5b167 100644 --- a/src/glum/gui/misc/BooleanCellRenderer.java +++ b/src/glum/gui/misc/BooleanCellRenderer.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.misc; import java.awt.Component; @@ -6,19 +19,27 @@ import javax.swing.JCheckBox; import javax.swing.JTable; import javax.swing.table.TableCellRenderer; +/** + * {@link TableCellRenderer} used to render boolean values. + * + * @author lopeznr1 + */ public class BooleanCellRenderer extends JCheckBox implements TableCellRenderer { + /** Standard Constructor */ public BooleanCellRenderer() { super("", false); + setHorizontalAlignment(JCheckBox.CENTER); } @Override - public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, + int row, int column) { if (value instanceof Boolean) { - setSelected((Boolean)value); + setSelected((Boolean) value); return this; } diff --git a/src/glum/gui/misc/ColorCellRenderer.java b/src/glum/gui/misc/ColorCellRenderer.java index 2d1ede7..a2440ad 100644 --- a/src/glum/gui/misc/ColorCellRenderer.java +++ b/src/glum/gui/misc/ColorCellRenderer.java @@ -1,35 +1,47 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.misc; -import java.awt.Color; -import java.awt.Component; -import java.awt.Graphics; -import java.awt.Graphics2D; +import java.awt.*; import javax.swing.JPanel; import javax.swing.JTable; import javax.swing.table.TableCellRenderer; +/** + * {@link TableCellRenderer} used to render a color. + * + * @author lopeznr1 + */ public class ColorCellRenderer extends JPanel implements TableCellRenderer { // State vars protected Color activeColor; - /** - * Constructor - */ + /** Standard Constructor */ public ColorCellRenderer() { - super(); - activeColor = null; } @Override - public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, + int row, int column) { activeColor = null; if (value instanceof Color) - activeColor = (Color)value; + activeColor = (Color) value; if (activeColor != null) setBackground(activeColor); @@ -42,10 +54,8 @@ public class ColorCellRenderer extends JPanel implements TableCellRenderer @Override public void paint(Graphics g) { - Graphics2D g2d; - super.paint(g); - g2d = (Graphics2D)g; + var g2d = (Graphics2D) g; // Bail if we have a valid color if (activeColor != null) diff --git a/src/glum/gui/misc/CustomLCR.java b/src/glum/gui/misc/CustomLCR.java new file mode 100644 index 0000000..f8a1fd3 --- /dev/null +++ b/src/glum/gui/misc/CustomLCR.java @@ -0,0 +1,79 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.misc; + +import java.awt.Component; +import java.util.HashMap; +import java.util.Map; + +import javax.swing.*; + +/** + * {@link ListCellRenderer} used to render custom labels corresponding to the provided items. + *

    + * If an item is not registered via {@link #addMapping(Object, String)}, then the string returned by + * {@link Object#toString()} will be utilized instead. + * + * @author lopeznr1 + */ +public class CustomLCR extends DefaultListCellRenderer +{ + // State vars + private final Map labelM; + + /** Standard Constructor */ + public CustomLCR() + { + labelM = new HashMap<>(); + } + + @Override + public Component getListCellRendererComponent(JList list, Object aObj, int index, boolean isSelected, + boolean hasFocus) + { + JLabel retL = (JLabel) super.getListCellRendererComponent(list, aObj, index, isSelected, hasFocus); + + String tmpLabel = labelM.get(aObj); + if (tmpLabel == null) + tmpLabel = "" + aObj; + + retL.setText(tmpLabel); + return retL; + } + + /** + * Registers the label that will be shown when the specified item is selected. + */ + public void addMapping(Object aItem, String aLabel) + { + labelM.put(aItem, aLabel); + } + + /** + * Deregisters the label that will be shown when the specified item is selected. + */ + public void delMapping(Object aItem) + { + labelM.remove(aItem); + } + + /** + * Returns the label associated with the specified mapping. + */ + public String getLabel(Object aItem) + { + return labelM.get(aItem); + } + +} diff --git a/src/glum/gui/misc/MultiState.java b/src/glum/gui/misc/MultiState.java index 4161fcd..1ea064a 100644 --- a/src/glum/gui/misc/MultiState.java +++ b/src/glum/gui/misc/MultiState.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.misc; import java.awt.*; diff --git a/src/glum/gui/misc/MultiStateCheckBox.java b/src/glum/gui/misc/MultiStateCheckBox.java index 428d94e..c632d6f 100644 --- a/src/glum/gui/misc/MultiStateCheckBox.java +++ b/src/glum/gui/misc/MultiStateCheckBox.java @@ -1,9 +1,23 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.misc; import java.awt.event.*; -import java.util.*; +import java.util.HashMap; + import javax.swing.*; -import javax.swing.plaf.*; +import javax.swing.plaf.ActionMapUIResource; public class MultiStateCheckBox extends JCheckBox implements MouseListener { @@ -12,7 +26,7 @@ public class MultiStateCheckBox extends JCheckBox implements MouseListener protected HashMap nextStateMap; /** - * Constructor + * Standard Constructor */ public MultiStateCheckBox(String text, boolean is3StateCycle) { @@ -76,10 +90,8 @@ public class MultiStateCheckBox extends JCheckBox implements MouseListener @Override public void doClick() { - MouseEvent aEvent; - - aEvent = new MouseEvent(this, MouseEvent.MOUSE_PRESSED, 0, 0, 0, 0, 0, false); - handleMouseEvent(aEvent); + var tmpEvent = new MouseEvent(this, MouseEvent.MOUSE_PRESSED, 0, 0, 0, 0, 0, false); + handleMouseEvent(tmpEvent); model.advanceToNextState(); } @@ -138,25 +150,23 @@ public class MultiStateCheckBox extends JCheckBox implements MouseListener */ protected void handleMouseEvent(MouseEvent e) { - int aID; - - aID = e.getID(); - if (aID == MouseEvent.MOUSE_ENTERED) + var tmpID = e.getID(); + if (tmpID == MouseEvent.MOUSE_ENTERED) { model.setArmed(true); } - else if (aID == MouseEvent.MOUSE_EXITED) + else if (tmpID == MouseEvent.MOUSE_EXITED) { model.setArmed(false); } - else if (aID == MouseEvent.MOUSE_RELEASED) + else if (tmpID == MouseEvent.MOUSE_RELEASED) { if (model.isArmed() == true) model.advanceToNextState(); model.setPressed(false); } - else if (aID == MouseEvent.MOUSE_PRESSED) + else if (tmpID == MouseEvent.MOUSE_PRESSED) { grabFocus(); model.setPressed(true); @@ -168,10 +178,7 @@ public class MultiStateCheckBox extends JCheckBox implements MouseListener */ protected void rebuildKeyboardMap() { - ActionMap map; - - map = new ActionMapUIResource(); - + var map = new ActionMapUIResource(); map.put("pressed", new AbstractAction() { @Override diff --git a/src/glum/gui/misc/MultiStateCheckBoxCellEditor.java b/src/glum/gui/misc/MultiStateCheckBoxCellEditor.java index f0cd124..1c79033 100644 --- a/src/glum/gui/misc/MultiStateCheckBoxCellEditor.java +++ b/src/glum/gui/misc/MultiStateCheckBoxCellEditor.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.misc; import java.awt.Component; diff --git a/src/glum/gui/misc/MultiStateCheckBoxCellRenderer.java b/src/glum/gui/misc/MultiStateCheckBoxCellRenderer.java index 2aa4e4f..d37e5f0 100644 --- a/src/glum/gui/misc/MultiStateCheckBoxCellRenderer.java +++ b/src/glum/gui/misc/MultiStateCheckBoxCellRenderer.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.misc; import java.awt.Component; diff --git a/src/glum/gui/misc/MultiStateCheckBoxHeader.java b/src/glum/gui/misc/MultiStateCheckBoxHeader.java index 4f7ef92..8655c68 100644 --- a/src/glum/gui/misc/MultiStateCheckBoxHeader.java +++ b/src/glum/gui/misc/MultiStateCheckBoxHeader.java @@ -1,18 +1,34 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.misc; -import java.awt.*; +import java.awt.Component; import java.awt.event.*; -import javax.swing.*; -import javax.swing.table.*; + +import javax.swing.JTable; +import javax.swing.UIManager; +import javax.swing.table.JTableHeader; +import javax.swing.table.TableCellRenderer; public class MultiStateCheckBoxHeader extends MultiStateCheckBox implements TableCellRenderer, MouseListener, MouseMotionListener { // State vars - protected JTableHeader refHeader; - protected int column; + private JTableHeader refHeader; + private int column; /** - * Constructor + * Standard Constructor */ public MultiStateCheckBoxHeader(JTable aTable, boolean is3StateCycle) { @@ -36,19 +52,14 @@ public class MultiStateCheckBoxHeader extends MultiStateCheckBox implements Tabl */ public int getAssociatedColumn(MouseEvent aEvent) { - JTableHeader aHeader; - JTable aTable; - TableColumnModel aColumnModel; - int viewCol, refCol; - if (aEvent.getSource() instanceof JTableHeader == false) return -1; - aHeader = (JTableHeader)aEvent.getSource(); - aTable = aHeader.getTable(); - aColumnModel = aTable.getColumnModel(); - viewCol = aColumnModel.getColumnIndexAtX(aEvent.getX()); - refCol = aTable.convertColumnIndexToModel(viewCol); + var tmpHeader = (JTableHeader)aEvent.getSource(); + var tmpTable = tmpHeader.getTable(); + var tmpColumnModel = tmpTable.getColumnModel(); + var viewCol = tmpColumnModel.getColumnIndexAtX(aEvent.getX()); + var refCol = tmpTable.convertColumnIndexToModel(viewCol); return viewCol; } diff --git a/src/glum/gui/misc/MultiStateCheckBoxHeaderTest.java b/src/glum/gui/misc/MultiStateCheckBoxHeaderTest.java index d701de4..e0c8807 100644 --- a/src/glum/gui/misc/MultiStateCheckBoxHeaderTest.java +++ b/src/glum/gui/misc/MultiStateCheckBoxHeaderTest.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.misc; import java.awt.event.*; diff --git a/src/glum/gui/misc/MultiStateCheckBoxTest.java b/src/glum/gui/misc/MultiStateCheckBoxTest.java index 089acb2..4c24923 100644 --- a/src/glum/gui/misc/MultiStateCheckBoxTest.java +++ b/src/glum/gui/misc/MultiStateCheckBoxTest.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.misc; import java.awt.*; diff --git a/src/glum/gui/misc/MultiStateIcon.java b/src/glum/gui/misc/MultiStateIcon.java index 14a5b2d..5ef47be 100644 --- a/src/glum/gui/misc/MultiStateIcon.java +++ b/src/glum/gui/misc/MultiStateIcon.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.misc; import java.awt.*; diff --git a/src/glum/gui/misc/MultiStateModel.java b/src/glum/gui/misc/MultiStateModel.java index 888a91c..20043e3 100644 --- a/src/glum/gui/misc/MultiStateModel.java +++ b/src/glum/gui/misc/MultiStateModel.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.misc; import java.awt.event.*; diff --git a/src/glum/gui/misc/SimpleTitledBorder.java b/src/glum/gui/misc/SimpleTitledBorder.java index 9e4c3da..9b999f6 100644 --- a/src/glum/gui/misc/SimpleTitledBorder.java +++ b/src/glum/gui/misc/SimpleTitledBorder.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.misc; import java.awt.*; diff --git a/src/glum/gui/panel/CardPanel.java b/src/glum/gui/panel/CardPanel.java index 73531e3..10793ff 100644 --- a/src/glum/gui/panel/CardPanel.java +++ b/src/glum/gui/panel/CardPanel.java @@ -1,40 +1,54 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel; -import java.awt.*; +import java.awt.CardLayout; +import java.awt.Component; import java.util.*; -import javax.swing.*; - -import com.google.common.collect.*; +import javax.swing.JPanel; /** * Panel that allows you to shuffle between a collection of "card" panels. Each card must be a descendant of JPanel. - *

    - * Unlike CardLayout this class supports mapping of an object (rather than a string) to a card. + *

    + * Unlike {@link CardLayout} this class supports mapping of an object (rather than a string) to a card. + * + * @author lopeznr1 */ public class CardPanel extends JPanel { - protected BiMap keyMap; - protected Map revMap; - protected CardLayout myLayout; - protected G1 activeCard; - protected G1 backupCard; + // State vars + private HashMap keyMap; + private Map revMap; + private CardLayout workCardLayout; + private G1 activeCard; + private G1 backupCard; + /** Standard Constructor */ public CardPanel() { - super(); + workCardLayout = new CardLayout(); + setLayout(workCardLayout); - myLayout = new CardLayout(); - setLayout(myLayout); - - keyMap = HashBiMap.create(); + keyMap = new HashMap<>(); revMap = new HashMap<>(); activeCard = null; backupCard = null; } @Override - public Component add(String name, Component comp) + public Component add(String aName, Component aComp) { throw new RuntimeException("Improper method call. Use addCard() instead of add()"); } @@ -42,29 +56,26 @@ public class CardPanel extends JPanel /** * Adds and associates the key, aKey with the specified card aCard. */ - public void addCard(Object aKey, G1 aCard) { // aCard must be of type Component if ((aCard instanceof Component) == false) - throw new IllegalArgumentException("aCard must be of type Component. Found class: " + aCard.getClass().getName()); + throw new IllegalArgumentException( + "aCard must be of type Component. Found class: " + aCard.getClass().getName()); - // Add the card if no card associated with the key + // Associate the card with the specified key if (keyMap.get(aKey) == null) - { - // Form the 2-way association of aKey and aComponent keyMap.put(aKey, aCard); - - // Form the 1-way mapping of aComponent to a (unique) strKey - String strKey = "" + revMap.size(); - revMap.put(aCard, strKey); - - add((Component)aCard, strKey); - } // If the key is associated, then ensure it is matched to aCard else if (keyMap.get(aKey) != aCard) - { throw new RuntimeException("Attempting to add new card with an already inserted key: " + aKey); + + // Form a mapping of aCard to a unique string key + if (revMap.containsKey(aCard) == false) + { + String strKey = "" + revMap.size(); + revMap.put(aCard, strKey); + add((Component) aCard, strKey); } switchToCard(aKey); @@ -78,17 +89,23 @@ public class CardPanel extends JPanel return activeCard; } - public Collection getAllCards() + public List getAllCards() { - Collection itemList; + var retCardL = new ArrayList(keyMap.values()); + return retCardL; + } - itemList = new ArrayList(keyMap.values()); - return itemList; + /** + * Returns the card associated with the specified key. + */ + public G1 getCardForKey(Object aKey) + { + return keyMap.get(aKey); } /** * Method to specify the backup card to use should no card be found that corresponds to the specified key. - *

    + *

    * This method will throw a RuntimeException if a card has not been associated with the specified key. */ public void setBackupCard(Object aKey) @@ -100,11 +117,13 @@ public class CardPanel extends JPanel /** * Causes this CardPanel to show the card associated with the specified key. - *

    - * A RuntimeException will be thrown if there is no card associated with aKey and the backupCord has not been specified. + *

    + * A RuntimeException will be thrown if there is no card associated with aKey and the backupCord has not been + * specified. */ public void switchToCard(Object aKey) { + // Switch to the proper card activeCard = keyMap.get(aKey); if (activeCard == null) activeCard = backupCard; @@ -113,22 +132,9 @@ public class CardPanel extends JPanel if (activeCard == null) throw new RuntimeException("No mapping found when switching to card: " + aKey); - switchToInstalledCard(activeCard); - } - - /** - * Causes this CardPanel to show the specified card. Throws a RuntimeException if the card was not previously added via addCard(). - */ - public void switchToInstalledCard(G1 aCard) - { - if (keyMap.values().contains(aCard) == false) - throw new RuntimeException("No mapping found when switching to card: " + aCard); - - // Switch to the proper card - activeCard = aCard; - - String strKey = revMap.get(aCard); - myLayout.show(this, strKey); + // Switch the CardLayout to the activeCard + String strKey = revMap.get(activeCard); + workCardLayout.show(this, strKey); } } diff --git a/src/glum/gui/panel/ColorInputPanel.java b/src/glum/gui/panel/ColorInputPanel.java index d6313c1..927e287 100644 --- a/src/glum/gui/panel/ColorInputPanel.java +++ b/src/glum/gui/panel/ColorInputPanel.java @@ -1,10 +1,19 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel; -import glum.gui.component.GNumberField; -import glum.unit.*; - import java.awt.Color; -import java.awt.Font; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -12,26 +21,31 @@ import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; +import com.google.common.collect.Range; + +import glum.gui.component.GNumberField; +import glum.unit.ConstUnitProvider; +import glum.unit.NumberUnit; import net.miginfocom.swing.MigLayout; +/** + * User input component that allows the user to specify the a {@link Color}. + * + * @author lopeznr1 + */ public class ColorInputPanel extends GPanel implements ActionListener, ChangeListener { - // Constants - private static final Font miniFont = new Font("Serif", Font.PLAIN, 10); - // Gui components private ColorPanel colorP; - private JLabel redL, greenL, blueL; - private JSlider redS, greenS, blueS; - private GNumberField redNF, greenNF, blueNF; + private JLabel redL, greenL, blueL, alphaL; + private JSlider redS, greenS, blueS, alphaS; + private GNumberField redNF, greenNF, blueNF, alphaNF; - /** - * Constructor - */ - public ColorInputPanel(boolean isHorizontal, boolean showTF) + /** Standard Constructor */ + public ColorInputPanel(boolean aIsHorizontal, boolean aShowTF, boolean aShowAlpha) { // Build the gui areas - buildGuiArea(isHorizontal, showTF); + buildGuiArea(aShowTF, aIsHorizontal, aShowAlpha); // Set in the default color setColorConfig(Color.BLACK); @@ -42,12 +56,11 @@ public class ColorInputPanel extends GPanel implements ActionListener, ChangeLis */ public Color getColorConfig() { - int redVal, greenVal, blueVal; - - redVal = redS.getValue(); - greenVal = greenS.getValue(); - blueVal = blueS.getValue(); - return new Color(redVal, greenVal, blueVal); + var redVal = redS.getValue(); + var greenVal = greenS.getValue(); + var blueVal = blueS.getValue(); + var alphaVal = alphaS.getValue(); + return new Color(redVal, greenVal, blueVal, alphaVal); } /** @@ -65,14 +78,12 @@ public class ColorInputPanel extends GPanel implements ActionListener, ChangeLis @Override public void actionPerformed(ActionEvent aEvent) { - Object source; - // Perform GUI updates - source = aEvent.getSource(); + var source = aEvent.getSource(); updateGui(source); // Notify the listeners - notifyListeners(source, ID_UPDATE); + notifyListeners(this, ID_UPDATE); } @Override @@ -87,6 +98,9 @@ public class ColorInputPanel extends GPanel implements ActionListener, ChangeLis blueL.setEnabled(aBool); blueS.setEnabled(aBool); blueNF.setEnabled(aBool); + alphaL.setEnabled(aBool); + alphaS.setEnabled(aBool); + alphaNF.setEnabled(aBool); colorP.setEnabled(aBool); } @@ -94,97 +108,106 @@ public class ColorInputPanel extends GPanel implements ActionListener, ChangeLis @Override public void stateChanged(ChangeEvent aEvent) { - Object source; - JSlider aSlider; - // Perform GUI updates - source = aEvent.getSource(); + var source = aEvent.getSource(); updateGui(source); // Notify the listeners if (source instanceof JSlider) { // Fire off an event only if not being updated - aSlider = (JSlider)source; - if (aSlider.getValueIsAdjusting() == false) - notifyListeners(source, ID_UPDATE); + var tmpSlider = (JSlider) source; + if (tmpSlider.getValueIsAdjusting() == false) + notifyListeners(this, ID_UPDATE); else - notifyListeners(source, ID_UPDATING); + notifyListeners(this, ID_UPDATING); } } /** * Forms the actual gui */ - private void buildGuiArea(boolean isHorizontal, boolean showTF) + private void buildGuiArea(boolean aIsHorizontal, boolean aShowTF, boolean aShowAlpha) { - JPanel rPanel, gPanel, bPanel; - UnitProvider countUP; - int sliderStyle; - - sliderStyle = JSlider.HORIZONTAL; - if (isHorizontal == false) + var sliderStyle = JSlider.HORIZONTAL; + if (aIsHorizontal == false) sliderStyle = JSlider.VERTICAL; - countUP = new ConstUnitProvider(new NumberUnit("", "", 1.0, 0)); + var countUP = new ConstUnitProvider(new NumberUnit("", "", 1.0, 0)); // RGB sliders + var rgbRange = Range.closed(0.0, 255.0); + redL = new JLabel("R", JLabel.CENTER); redS = new JSlider(sliderStyle, 0, 255, 0); - redNF = new GNumberField(this, countUP, 0, 255); - rPanel = formColorControl(redS, redL, redNF, isHorizontal, showTF); + redNF = new GNumberField(this, countUP, rgbRange); + var rPanel = formColorControl(redS, redL, redNF, aIsHorizontal, aShowTF); greenL = new JLabel("G", JLabel.CENTER); greenS = new JSlider(sliderStyle, 0, 255, 0); - greenNF = new GNumberField(this, countUP, 0, 255); - gPanel = formColorControl(greenS, greenL, greenNF, isHorizontal, showTF); + greenNF = new GNumberField(this, countUP, rgbRange); + var gPanel = formColorControl(greenS, greenL, greenNF, aIsHorizontal, aShowTF); blueL = new JLabel("B", JLabel.CENTER); blueS = new JSlider(sliderStyle, 0, 255, 0); - blueNF = new GNumberField(this, countUP, 0, 255); - bPanel = formColorControl(blueS, blueL, blueNF, isHorizontal, showTF); + blueNF = new GNumberField(this, countUP, rgbRange); + var bPanel = formColorControl(blueS, blueL, blueNF, aIsHorizontal, aShowTF); + + alphaL = new JLabel("A", JLabel.CENTER); + alphaS = new JSlider(sliderStyle, 0, 255, 0); + alphaNF = new GNumberField(this, countUP, rgbRange); + var aPanel = formColorControl(alphaS, alphaL, alphaNF, aIsHorizontal, aShowTF); // The color area colorP = new ColorPanel(40, 40); + colorP.setOpaque(true); - if (isHorizontal == true) + if (aIsHorizontal == true) { - setLayout(new MigLayout("", "0[grow,75::][]0", "0[][][]0")); + if (aShowAlpha == false) + setLayout(new MigLayout("", "0[grow,75::][]0", "0[][][]0")); + else + setLayout(new MigLayout("", "0[grow,75::][]0", "0[][][][]0")); - add(rPanel, "growx,span 1,wrap"); - add(gPanel, "growx,span 1,wrap"); - add(bPanel, "growx,span 1,wrap"); - add(colorP, "cell 1 0,growy,spanx 1,spany 3"); + add(rPanel, "growx,wrap"); + add(gPanel, "growx,wrap"); + add(bPanel, "growx,wrap"); + if (aShowAlpha == true) + add(aPanel, "growx,wrap"); + add(colorP, "cell 1 0,growy,spany"); } else { - setLayout(new MigLayout("", "0[][][]0", "0[grow,75::][]0")); + if (aShowAlpha == false) + setLayout(new MigLayout("", "0[][][]0", "0[grow,75::][]0")); + else + setLayout(new MigLayout("", "0[][][][]0", "0[grow,75::][]0")); - add(rPanel, "growy,span 1"); - add(gPanel, "growy,span 1"); - add(bPanel, "growy,span 1,wrap"); - add(colorP, "growx,span 3"); + add(rPanel, "growy"); + add(gPanel, "growy"); + add(bPanel, "growy"); + if (aShowAlpha == true) + add(aPanel, "growy"); + add(colorP, "newline,growx,spanx"); } } /** * builds a JSlider with a label */ - private JPanel formColorControl(JSlider aS, JLabel aL, GNumberField aNF, boolean isHorizontal, boolean showTF) + private JPanel formColorControl(JSlider aS, JLabel aL, GNumberField aNF, boolean aIsHorizontal, boolean aShowTF) { - JPanel aPanel; - - aPanel = new JPanel(); - if (isHorizontal == true) + var retPanel = new JPanel(); + if (aIsHorizontal == true) { - aPanel.setLayout(new BoxLayout(aPanel, BoxLayout.X_AXIS)); + retPanel.setLayout(new BoxLayout(retPanel, BoxLayout.X_AXIS)); aL.setAlignmentY(0.5f); aS.setAlignmentY(0.5f); aNF.setAlignmentY(0.5f); } else { - aPanel.setLayout(new BoxLayout(aPanel, BoxLayout.Y_AXIS)); + retPanel.setLayout(new BoxLayout(retPanel, BoxLayout.Y_AXIS)); aL.setAlignmentX(0.5f); aS.setAlignmentX(0.5f); aNF.setAlignmentX(0.5f); @@ -195,22 +218,21 @@ public class ColorInputPanel extends GPanel implements ActionListener, ChangeLis aNF.setHorizontalAlignment(JTextField.CENTER); aNF.setColumns(3); aNF.setValue(0); - aNF.setFont(miniFont); aNF.setMinimumSize(aNF.getPreferredSize()); aNF.setMaximumSize(aNF.getPreferredSize()); - aPanel.add(aL); - aPanel.add(aS); + retPanel.add(aL); + retPanel.add(aS); - if (isHorizontal == true) - aPanel.add(Box.createHorizontalStrut(2)); + if (aIsHorizontal == true) + retPanel.add(Box.createHorizontalStrut(2)); else - aPanel.add(Box.createVerticalStrut(2)); + retPanel.add(Box.createVerticalStrut(2)); - if (showTF == true) - aPanel.add(aNF); + if (aShowTF == true) + retPanel.add(aNF); - return aPanel; + return retPanel; } /** @@ -218,17 +240,17 @@ public class ColorInputPanel extends GPanel implements ActionListener, ChangeLis */ private void synchronizeGui(Color aColor) { - int redVal, greenVal, blueVal; - // Get the rgb values - redVal = aColor.getRed(); - greenVal = aColor.getGreen(); - blueVal = aColor.getBlue(); + var redVal = aColor.getRed(); + var greenVal = aColor.getGreen(); + var blueVal = aColor.getBlue(); + var alphaVal = aColor.getAlpha(); // Stop listening to events while updating redS.removeChangeListener(this); greenS.removeChangeListener(this); blueS.removeChangeListener(this); + alphaS.removeChangeListener(this); // Update the gui components if (redVal != redNF.getValue()) @@ -237,30 +259,34 @@ public class ColorInputPanel extends GPanel implements ActionListener, ChangeLis greenNF.setValue(greenVal); if (blueVal != blueNF.getValue()) blueNF.setValue(blueVal); + if (alphaVal != alphaNF.getValue()) + alphaNF.setValue(alphaVal); redS.setValue(redVal); greenS.setValue(greenVal); blueS.setValue(blueVal); - colorP.setColor(new Color(redVal, greenVal, blueVal)); + alphaS.setValue(alphaVal); + colorP.setColor(new Color(redVal, greenVal, blueVal, 255)); // Proceed with listening to events redS.addChangeListener(this); greenS.addChangeListener(this); blueS.addChangeListener(this); + alphaS.addChangeListener(this); } /** * Updates the gui to reflect the source that has changed */ - private void updateGui(Object source) + private void updateGui(Object aSource) { - int redVal, greenVal, blueVal; - // Determine what values to retrieve based on the source - if (source instanceof GNumberField) + int redVal, greenVal, blueVal, alphaVal; + if (aSource instanceof GNumberField) { redVal = redNF.getValueAsInt(0); greenVal = greenNF.getValueAsInt(0); blueVal = blueNF.getValueAsInt(0); + alphaVal = alphaNF.getValueAsInt(0); } else { @@ -268,42 +294,53 @@ public class ColorInputPanel extends GPanel implements ActionListener, ChangeLis redVal = redS.getValue(); greenVal = greenS.getValue(); blueVal = blueS.getValue(); + alphaVal = alphaS.getValue(); } // Update the appropriate component - if (source == redS) + if (aSource == redS) { redNF.setValue(redVal); } - else if (source == greenS) + else if (aSource == greenS) { greenNF.setValue(greenVal); } - else if (source == blueS) + else if (aSource == blueS) { blueNF.setValue(blueVal); } - else if (source == redNF) + else if (aSource == alphaS) + { + alphaNF.setValue(alphaVal); + } + else if (aSource == redNF) { redS.removeChangeListener(this); redS.setValue(redVal); redS.addChangeListener(this); } - else if (source == greenNF) + else if (aSource == greenNF) { greenS.removeChangeListener(this); greenS.setValue(greenVal); greenS.addChangeListener(this); } - else if (source == blueNF) + else if (aSource == blueNF) { blueS.removeChangeListener(this); blueS.setValue(blueVal); blueS.addChangeListener(this); } + else if (aSource == alphaNF) + { + alphaS.removeChangeListener(this); + alphaS.setValue(alphaVal); + alphaS.addChangeListener(this); + } // Update the preview color - colorP.setColor(new Color(redVal, greenVal, blueVal)); + colorP.setColor(new Color(redVal, greenVal, blueVal, 255)); } } diff --git a/src/glum/gui/panel/ColorPanel.java b/src/glum/gui/panel/ColorPanel.java index aa5eef1..4657fa5 100644 --- a/src/glum/gui/panel/ColorPanel.java +++ b/src/glum/gui/panel/ColorPanel.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel; import java.awt.Color; @@ -10,12 +23,10 @@ import javax.swing.border.LineBorder; public class ColorPanel extends JPanel { // State vars - protected Color dispColor; + private Color dispColor; public ColorPanel() { - super(); - dispColor = Color.BLACK; updateGui(); } @@ -49,9 +60,7 @@ public class ColorPanel extends JPanel */ protected void updateGui() { - boolean isEnabled; - - isEnabled = isEnabled(); + var isEnabled = isEnabled(); if (isEnabled == false) { setBackground(Color.LIGHT_GRAY); diff --git a/src/glum/gui/panel/ComponentTracker.java b/src/glum/gui/panel/ComponentTracker.java index 7fb0dd8..8b4c656 100644 --- a/src/glum/gui/panel/ComponentTracker.java +++ b/src/glum/gui/panel/ComponentTracker.java @@ -1,24 +1,38 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel; import java.awt.Component; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; +import java.util.Arrays; import java.util.List; -import com.google.common.collect.Lists; - /** - * Utility class to allow a component to track other Component so it can keep it's - * properties synchronized with the tracked Components. + * Worker class to allow a component to track other Component so it can keep it's properties synchronized with the + * tracked Components. + * + * @author lopeznr1 */ public class ComponentTracker implements ComponentListener { - protected Component targComp; - protected Component trkHiddenComp; - protected Component trkMovedComp; - protected Component trkResizedComp; - protected Component trkShownComp; - + private Component targComp; + private Component trkHiddenComp; + private Component trkMovedComp; + private Component trkResizedComp; + private Component trkShownComp; + public ComponentTracker(Component aTargComp) { targComp = aTargComp; @@ -27,63 +41,59 @@ public class ComponentTracker implements ComponentListener trkResizedComp = null; trkShownComp = null; } - + /** - * Track aComp so that if it is hidden, then the reference - * targetComponent will be hidden. + * Track aComp so that if it is hidden, then the reference targetComponent will be hidden. */ public void setHiddenTracker(Component aComp) { // Deregister from the old trkShownComp if (trkHiddenComp != null) trkHiddenComp.removeComponentListener(this); - + trkHiddenComp = aComp; updateListenersForTrackedComponents(); } /** - * Track aComp so that if it is moved, then the reference - * targetComponent will be moved. + * Track aComp so that if it is moved, then the reference targetComponent will be moved. */ public void setMovedTracker(Component aComp) { // Deregister from the old trkShownComp if (trkMovedComp != null) trkMovedComp.removeComponentListener(this); - + trkMovedComp = aComp; updateListenersForTrackedComponents(); } /** - * Track aComp so that if it is resized, then the reference - * targetComponent will be resized. + * Track aComp so that if it is resized, then the reference targetComponent will be resized. */ public void setResizedTracker(Component aComp) { // Deregister from the old trkShownComp if (trkResizedComp != null) trkResizedComp.removeComponentListener(this); - + trkResizedComp = aComp; updateListenersForTrackedComponents(); } /** - * Track aComp so that if it is shown, then the reference - * targetComponent will be shown. + * Track aComp so that if it is shown, then the reference targetComponent will be shown. */ public void setShownTracker(Component aComp) { // Deregister from the old trkShownComp if (trkShownComp != null) trkShownComp.removeComponentListener(this); - + trkShownComp = aComp; updateListenersForTrackedComponents(); } - + @Override public void componentHidden(ComponentEvent aEvent) { @@ -114,43 +124,42 @@ public class ComponentTracker implements ComponentListener if (aEvent.getComponent() == trkShownComp) targComp.setVisible(true); } - + /** - * Utility method to ensure that we are registered (Component events) for - * all component which are being tracked. Note that at most we will register - * only only once for each unique Component. + * Utility method to ensure that we are registered (Component events) for all component which are being tracked. Note + * that at most we will register only only once for each unique Component. */ protected void updateListenersForTrackedComponents() { - List listenerList; + List listenerL; if (trkHiddenComp != null) { - listenerList = Lists.newArrayList(trkHiddenComp.getComponentListeners()); - if (listenerList.contains(this) == false) + listenerL = Arrays.asList(trkHiddenComp.getComponentListeners()); + if (listenerL.contains(this) == false) trkHiddenComp.addComponentListener(this); } - + if (trkMovedComp != null) { - listenerList = Lists.newArrayList(trkMovedComp.getComponentListeners()); - if (listenerList.contains(this) == false) + listenerL = Arrays.asList(trkMovedComp.getComponentListeners()); + if (listenerL.contains(this) == false) trkMovedComp.addComponentListener(this); } - + if (trkResizedComp != null) { - listenerList = Lists.newArrayList(trkResizedComp.getComponentListeners()); - if (listenerList.contains(this) == false) + listenerL = Arrays.asList(trkResizedComp.getComponentListeners()); + if (listenerL.contains(this) == false) trkResizedComp.addComponentListener(this); } - + if (trkShownComp != null) { - listenerList = Lists.newArrayList(trkShownComp.getComponentListeners()); - if (listenerList.contains(this) == false) + listenerL = Arrays.asList(trkShownComp.getComponentListeners()); + if (listenerL.contains(this) == false) trkShownComp.addComponentListener(this); } } - + } diff --git a/src/glum/gui/panel/CredentialPanel.java b/src/glum/gui/panel/CredentialPanel.java index 5b9dd89..a6fb21b 100644 --- a/src/glum/gui/panel/CredentialPanel.java +++ b/src/glum/gui/panel/CredentialPanel.java @@ -1,48 +1,56 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel; -import glum.net.Credential; -import glum.net.NetUtil; -import glum.net.Result; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.*; + import glum.gui.FocusUtil; import glum.gui.GuiUtil; import glum.gui.action.ClickAction; import glum.gui.component.GPasswordField; import glum.gui.component.GTextField; -import glum.gui.panel.GlassPanel; - -import java.awt.Color; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.EventQueue; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JPasswordField; -import javax.swing.JTextField; -import javax.swing.border.BevelBorder; - +import glum.net.*; import net.miginfocom.swing.MigLayout; +/** + * Panel that provides functionality that allow the user to enter credentials. + * + * @author lopeznr1 + */ public class CredentialPanel extends GlassPanel implements ActionListener { // Constants private static final Color warnColor = new Color(128, 0, 0); // GUI vars - protected JLabel titleL; - protected JButton ignoreB, acceptB; - protected JPasswordField passTF; - protected GTextField userTF; - protected JTextField sourceTF, warnTA; + private JLabel titleL; + private JButton ignoreB, acceptB; + private JPasswordField passTF; + private GTextField userTF; + private JTextField sourceTF, warnTA; // State vars - protected Credential myCredential; - protected Result eResult; - protected Boolean isReset; + private Credential myCredential; + private Result eResult; + private Boolean isReset; /** - * Constructor + * Standard Constructor */ public CredentialPanel(Component aParent) { @@ -85,14 +93,7 @@ public class CredentialPanel extends GlassPanel implements ActionListener // Reset the dialog isReset = true; - EventQueue.invokeLater(new Runnable() - { - @Override - public void run() - { - updateGui(); - } - }); + EventQueue.invokeLater(() -> updateGui()); } public void setTitle(String aTitle) @@ -101,12 +102,9 @@ public class CredentialPanel extends GlassPanel implements ActionListener } @Override - public void actionPerformed(ActionEvent e) + public void actionPerformed(ActionEvent aEvent) { - Object source; - - source = e.getSource(); - + var source = aEvent.getSource(); if (source == ignoreB) { // Hide the dialog @@ -126,8 +124,7 @@ public class CredentialPanel extends GlassPanel implements ActionListener final Credential aCredential = new Credential(userTF.getText(), passTF.getPassword()); final String uriRoot = sourceTF.getText(); - new Thread() - { + new Thread() { @Override public void run() { @@ -147,8 +144,7 @@ public class CredentialPanel extends GlassPanel implements ActionListener aResult = NetUtil.checkCredentials(uriRoot, aCredential); // Update the Gui - EventQueue.invokeLater(new Runnable() - { + EventQueue.invokeLater(new Runnable() { @Override public void run() { @@ -189,10 +185,6 @@ public class CredentialPanel extends GlassPanel implements ActionListener */ private void buildGuiArea() { - Dimension aDimension; - JLabel tmpL; - String aStr; - // Form the grid bag constraints setLayout(new MigLayout("", "[right][grow][][]")); @@ -201,7 +193,7 @@ public class CredentialPanel extends GlassPanel implements ActionListener add(titleL, "growx,span 4,wrap"); // Source area - tmpL = new JLabel("Source:"); + JLabel tmpL = new JLabel("Source:"); add(tmpL); sourceTF = GuiUtil.createUneditableTextField("http://www.google.edu"); @@ -220,19 +212,17 @@ public class CredentialPanel extends GlassPanel implements ActionListener add(passTF, "growx,span 3,wrap"); // Warn area - aStr = "Please enter the credentials for accessing the data."; - warnTA = GuiUtil.createUneditableTextField(aStr); + String tmpStr = "Please enter the credentials for accessing the data."; + warnTA = GuiUtil.createUneditableTextField(tmpStr); warnTA.setForeground(warnColor); add(warnTA, "growx,span 4,wrap"); // Action area - aDimension = GuiUtil.computePreferredJButtonSize("Ignore", "Accept"); - ignoreB = GuiUtil.createJButton("Ignore", this, aDimension); - acceptB = GuiUtil.createJButton("Accept", this, aDimension); - add(ignoreB, "skip 2,span 1"); - add(acceptB, "span 1"); - - setBorder(new BevelBorder(BevelBorder.RAISED)); + Dimension tmpDim = GuiUtil.computePreferredJButtonSize("Ignore", "Accept"); + ignoreB = GuiUtil.createJButton("Ignore", this, tmpDim); + acceptB = GuiUtil.createJButton("Accept", this, tmpDim); + add(ignoreB, "skip 2"); + add(acceptB, ""); } /** @@ -241,7 +231,6 @@ public class CredentialPanel extends GlassPanel implements ActionListener private void updateGui() { boolean isEnabled; - if (isReset == null) { warnTA.setText("Checking the credentials..."); @@ -262,20 +251,20 @@ public class CredentialPanel extends GlassPanel implements ActionListener switch (eResult) { case BadCredentials: - warnTA.setText("Credentials are invalid."); - break; + warnTA.setText("Credentials are invalid."); + break; case ConnectFailure: - warnTA.setText("Failed to connect to resource."); - break; + warnTA.setText("Failed to connect to resource."); + break; case UnreachableHost: - warnTA.setText("Unreachable host."); - break; + warnTA.setText("Unreachable host."); + break; default: - warnTA.setText("Unreconzied error. Error: " + eResult); - break; + warnTA.setText("Unreconzied error. Error: " + eResult); + break; } } } diff --git a/src/glum/gui/panel/CustomFocusTraversalPolicy.java b/src/glum/gui/panel/CustomFocusTraversalPolicy.java index a0b8d9f..5d5500d 100644 --- a/src/glum/gui/panel/CustomFocusTraversalPolicy.java +++ b/src/glum/gui/panel/CustomFocusTraversalPolicy.java @@ -1,91 +1,103 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel; -import java.awt.Component; -import java.awt.Container; -import java.awt.FocusTraversalPolicy; +import java.awt.*; import java.util.ArrayList; -import com.google.common.collect.Lists; - +/** + * Implementation of {@link FocusTraversalPolicy}. + *

    + * Components will be traversed based on the order they have been added to this object. + * + * @author lopeznr1 + */ public class CustomFocusTraversalPolicy extends FocusTraversalPolicy { - protected ArrayList itemList; - + // State vars + private ArrayList itemL; + + /** Standard Constructor */ public CustomFocusTraversalPolicy() { - itemList = Lists.newArrayList(); + itemL = new ArrayList<>(); } - + /** * Method to add an item to the end of the FocusTraversalPolicy */ public void addComponent(Component aItem) { - itemList.add(aItem); + itemL.add(aItem); } @Override public Component getComponentAfter(Container focusCycleRoot, Component aComponent) { - Component aComp; - int cIndex, tIndex; - // Bail if the component is not in our list - cIndex = itemList.indexOf(aComponent); + var cIndex = itemL.indexOf(aComponent); if (cIndex < 0) return getFirstComponent(focusCycleRoot); - - tIndex = cIndex; + + var tIndex = cIndex; while (true) { // Iterate through the circular loop tIndex++; - if (tIndex == itemList.size()) + if (tIndex == itemL.size()) tIndex = 0; - + // Ensure the item is focusable - aComp = itemList.get(tIndex); - if (checkFocusability(aComp) == true) - return aComp; - + var tmpComp = itemL.get(tIndex); + if (checkFocusability(tmpComp) == true) + return tmpComp; + // Bail if we have made an full loop if (tIndex == cIndex) break; } - - return itemList.get(cIndex); + + return itemL.get(cIndex); } @Override public Component getComponentBefore(Container focusCycleRoot, Component aComponent) { - Component aComp; - int cIndex, tIndex; - // Bail if the component is not in our list - cIndex = itemList.indexOf(aComponent); + var cIndex = itemL.indexOf(aComponent); if (cIndex < 0) return getLastComponent(focusCycleRoot); - - tIndex = cIndex; + + var tIndex = cIndex; while (true) { // Iterate through the circular loop tIndex--; if (tIndex == -1) - tIndex = itemList.size() - 1; - + tIndex = itemL.size() - 1; + // Ensure the item is focusable - aComp = itemList.get(tIndex); - if (checkFocusability(aComp) == true) - return aComp; - + var tmpComp = itemL.get(tIndex); + if (checkFocusability(tmpComp) == true) + return tmpComp; + // Bail if we have made an full loop if (tIndex == cIndex) break; } - - return itemList.get(cIndex); + + return itemL.get(cIndex); } @Override @@ -97,58 +109,49 @@ public class CustomFocusTraversalPolicy extends FocusTraversalPolicy @Override public Component getFirstComponent(Container focusCycleRoot) { - Component aComp; - // Bail if no components to traverse - if (itemList.isEmpty() == true) + if (itemL.isEmpty() == true) return null; - - aComp = itemList.get(0); - if (checkFocusability(aComp) == true) - return aComp; - - return getComponentAfter(focusCycleRoot, aComp); + + var tmpComp = itemL.get(0); + if (checkFocusability(tmpComp) == true) + return tmpComp; + + return getComponentAfter(focusCycleRoot, tmpComp); } @Override public Component getLastComponent(Container focusCycleRoot) { - Component aComp; - // Bail if no components to traverse - if (itemList.isEmpty() == true) + if (itemL.isEmpty() == true) return null; - - aComp = itemList.get(itemList.size()-1); - if (checkFocusability(aComp) == true) - return aComp; - - return getComponentBefore(focusCycleRoot, aComp); + + var tmpComp = itemL.get(itemL.size() - 1); + if (checkFocusability(tmpComp) == true) + return tmpComp; + + return getComponentBefore(focusCycleRoot, tmpComp); } - /** - * Utility method to test to see if a Component can grab the focus. - * To grab the focus a component must be: - * - focusable - * - enabled - * - visible - * - be in a container that is being shown. - */ + * Utility method to test to see if a Component can grab the focus. To grab the focus a component must be: - + * focusable - enabled - visible - be in a container that is being shown. + */ protected boolean checkFocusability(Component aComp) { if (aComp.isFocusable() == false) return false; - + if (aComp.isEnabled() == false) return false; - + if (aComp.isVisible() == false) return false; - + if (aComp.isShowing() == false) return false; - + return true; } diff --git a/src/glum/gui/panel/FontInputPanel.java b/src/glum/gui/panel/FontInputPanel.java new file mode 100644 index 0000000..b835cc1 --- /dev/null +++ b/src/glum/gui/panel/FontInputPanel.java @@ -0,0 +1,209 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.panel; + +import java.awt.Font; +import java.awt.GraphicsEnvironment; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.google.common.collect.Range; + +import glum.gui.component.GComboBox; +import glum.gui.component.GNumberField; +import glum.unit.*; +import net.miginfocom.swing.MigLayout; + +/** + * User input component that allows the user to specify a {@link Font}. + * + * @author lopeznr1 + */ +public class FontInputPanel extends GPanel implements ActionListener, ChangeListener +{ + // Gui components + private GComboBox nameBox; + private JLabel sizeL; + private JSlider sizeS; + private GNumberField sizeNF; + private JCheckBox boldCB, italicCB; + private JTextField previewTF; + + /** + * Standard Constructor + */ + public FontInputPanel() + { + // Build the gui area + buildGuiArea(); + + // Set in the default Font + var defFont = UIManager.getFont("Label.font"); + setFontConfig(defFont); + } + + /** + * Returns the selected Font. + */ + public Font getFontConfig() + { + var name = nameBox.getChosenItem(); + + var size = sizeS.getValue(); + + var style = Font.PLAIN; + if (boldCB.isSelected() == true) + style |= Font.BOLD; + if (italicCB.isSelected() == true) + style |= Font.ITALIC; + + return new Font(name, style, size); + } + + /** + * Sets in the current selected Font. + */ + public void setFontConfig(Font aFont) + { + synchronizeGui(aFont); + } + + @Override + public void actionPerformed(ActionEvent aEvent) + { + // Perform GUI updates + var source = aEvent.getSource(); + updateGui(source); + + // Notify the listeners + notifyListeners(source, ID_UPDATE); + } + + @Override + public void setEnabled(boolean aBool) + { + nameBox.setEnabled(aBool); + boldCB.setEnabled(aBool); + italicCB.setEnabled(aBool); + sizeL.setEnabled(aBool); + sizeS.setEnabled(aBool); + sizeNF.setEnabled(aBool); + + previewTF.setEnabled(aBool); + } + + @Override + public void stateChanged(ChangeEvent aEvent) + { + // Perform GUI updates + var source = aEvent.getSource(); + updateGui(source); + + if (source == sizeS) + { + // Notify the listeners + if (sizeS.getValueIsAdjusting() == false) + notifyListeners(source, ID_UPDATE); + else + notifyListeners(source, ID_UPDATING); + } + } + + /** + * Forms the actual gui + */ + private void buildGuiArea() + { + setLayout(new MigLayout("", "0[right][][fill][grow]0", "0[]0")); + var nameArr = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); + + // Name area + var tmpL = new JLabel("Name:"); + nameBox = new GComboBox<>(this, nameArr); + add("", tmpL); + add("span 2", nameBox); + + // Preview area + previewTF = new JTextField("Sample Text"); + previewTF.setHorizontalAlignment(JTextField.CENTER); + add("growx,growy,pushy,spany,wrap", previewTF); + + // Style area + tmpL = new JLabel("Style:"); + boldCB = new JCheckBox("Bold", false); + boldCB.addActionListener(this); + italicCB = new JCheckBox("Italic", false); + italicCB.addActionListener(this); + add("", tmpL); + add("", boldCB); + add("wrap", italicCB); + + // Size area + Range sizeRange = Range.closed(4.0, 72.0); + UnitProvider countUP = new ConstUnitProvider(new NumberUnit("", "", 1.0, 0)); + sizeL = new JLabel("Size:", JLabel.CENTER); + sizeNF = new GNumberField(this, countUP, sizeRange); + sizeS = new JSlider(JSlider.HORIZONTAL, 4, 72, 12); + add("", sizeL); + add("span 2,split,w 24::", sizeNF); + add("growx", sizeS); + } + + /** + * Syncs the GUI to match aFont. + */ + private void synchronizeGui(Font aFont) + { + nameBox.setChosenItem(aFont.getName()); + boldCB.setSelected(aFont.isBold()); + italicCB.setSelected(aFont.isItalic()); + + // Update the components related to size + int size = aFont.getSize(); + if (size != sizeNF.getValue()) + sizeNF.setValue(size); + + sizeS.removeChangeListener(this); + sizeS.setValue(size); + sizeS.addChangeListener(this); + } + + /** + * Updates the gui to reflect the source that has changed + */ + private void updateGui(Object aSource) + { + if (aSource == sizeS) + { + int size = sizeS.getValue(); + sizeNF.setValue(size); + } + else if (aSource == sizeNF) + { + int size = sizeNF.getValueAsInt(4); + sizeS.removeChangeListener(this); + sizeS.setValue(size); + sizeS.addChangeListener(this); + } + + // Update the preview area + var currFont = getFontConfig(); + previewTF.setFont(currFont); + } + +} diff --git a/src/glum/gui/panel/GPanel.java b/src/glum/gui/panel/GPanel.java index bccb60f..6e7bbb3 100644 --- a/src/glum/gui/panel/GPanel.java +++ b/src/glum/gui/panel/GPanel.java @@ -1,30 +1,46 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel; -import glum.gui.panel.generic.GenericCodes; - import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.util.*; +import java.util.LinkedHashSet; +import java.util.Set; import javax.swing.JPanel; +import glum.gui.panel.generic.GenericCodes; + /** - * JPanel that supports registration of ActionListeners. Derived classes will be responsible for determining when an ActionEvent should be fired. + * JPanel that supports registration of {@link ActionListener}s. Derived classes will be responsible for determining + * when an {@link ActionEvent} should be fired. + * + * @author lopeznr1 */ public class GPanel extends JPanel implements GenericCodes { // State vars - protected Set myListeners; + protected Set myListenerS; + /** Standard Constructor */ public GPanel() { - super(); - - myListeners = new LinkedHashSet<>(); + myListenerS = new LinkedHashSet<>(); } /** - * Add an ActionListener to this GPanel + * Add an {@link ActionListener} from this panel. */ public synchronized void addActionListener(ActionListener aListener) { @@ -32,40 +48,49 @@ public class GPanel extends JPanel implements GenericCodes if (aListener == null) throw new RuntimeException("Listener should not be null."); - myListeners.add(aListener); + myListenerS.add(aListener); } /** - * Remove an ActionListener to this GPanel + * Remove an {@link ActionListener} from this panel. */ - public synchronized void removeActionListener(ActionListener aListener) + public synchronized void delActionListener(ActionListener aListener) { - myListeners.remove(aListener); + myListenerS.remove(aListener); } /** - * Send out notification to all of the ActionListeners + * Send out notification to all of the {@link ActionListener}s. */ public void notifyListeners(Object aSource, int aId, String aCommand) { - Set tmpListeners; - // Get a copy of the current set of listeners + Set tmpListenerS; synchronized (this) { - tmpListeners = new LinkedHashSet<>(myListeners); + tmpListenerS = new LinkedHashSet<>(myListenerS); } // Notify our listeners - for (ActionListener aListener : tmpListeners) + for (ActionListener aListener : tmpListenerS) aListener.actionPerformed(new ActionEvent(aSource, aId, aCommand)); } /** - * Send out notification to all of the ActionListeners + * Send out notification to all of the {@link ActionListener}s. */ public void notifyListeners(Object aSource, int aId) { + // Delegate notifyListeners(aSource, aId, ""); } + + /** + * Send out notification to all of the {@link ActionListener}s. + */ + public void notifyListeners(Object aSource) + { + // Delegate + notifyListeners(aSource, 0, ""); + } } diff --git a/src/glum/gui/panel/GlassPane.java b/src/glum/gui/panel/GlassPane.java index b725353..9f393c1 100644 --- a/src/glum/gui/panel/GlassPane.java +++ b/src/glum/gui/panel/GlassPane.java @@ -1,36 +1,50 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel; -import glum.gui.GuiUtil; -import glum.gui.panel.CustomFocusTraversalPolicy; -import glum.zio.*; - import java.awt.*; -import java.awt.event.ComponentEvent; -import java.awt.event.ComponentListener; -import java.awt.event.MouseEvent; +import java.awt.event.*; import java.io.IOException; -import java.util.Set; +import java.util.LinkedHashSet; import javax.swing.*; import javax.swing.event.MouseInputAdapter; +import glum.gui.GuiUtil; +import glum.zio.*; import net.miginfocom.swing.MigLayout; -import com.google.common.collect.Sets; - +/** + * GUI element useful for holding the provided child UI components. This GUI component will be displayed as a floating + * "modal" glass pane over it's parent component. + * + * @author lopeznr1 + */ public class GlassPane extends JComponent implements ZioObj, ComponentListener { // Communicator vars - protected Component parentComp; - protected WaftPanel childComp; + private final Component parentComp; + protected final WaftPanel childComp; + + private RootPaneContainer rootPane; - protected RootPaneContainer rootPane; - // State vars - protected Color fillColor; - protected CustomFocusTraversalPolicy myFocusPolicy; - protected MigLayout refLayout; + private Color fillColor; + private CustomFocusTraversalPolicy myFocusPolicy; + private MigLayout workLayout; + /** Standard Constructor */ public GlassPane(Component aRefParent, JComponent aDisplayPane) { // Communicator vars @@ -38,26 +52,26 @@ public class GlassPane extends JComponent implements ZioObj, ComponentListener childComp = new WaftPanel(aDisplayPane, this); // Build the GUI - refLayout = new MigLayout(); - setLayout(refLayout); + workLayout = new MigLayout(); + setLayout(workLayout); // add("pos 150px 100px", childComp); add("", childComp); - this.validate(); - this.revalidate(); - + validate(); + revalidate(); + // Register for events of interest - childComp.addComponentListener(this); + childComp.addComponentListener(this); // Set up the GlassPanelListener handler GlassPaneListener aListener = new GlassPaneListener(this, childComp); addMouseListener(aListener); addMouseMotionListener(aListener); - + rootPane = null; fillColor = new Color(96, 96, 96, 96); } - + /** * Set in an alternative shade color. */ @@ -65,10 +79,10 @@ public class GlassPane extends JComponent implements ZioObj, ComponentListener { fillColor = aColor; } - + /** - * Method to change the end users ability to resize a panel. This method call is just a suggestion - * and the GUI toolkit may totally ignore this call. + * Method to change the end users ability to resize a panel. This method call is just a suggestion and the GUI + * toolkit may totally ignore this call. */ public void setResizable(boolean aBool) { @@ -82,145 +96,78 @@ public class GlassPane extends JComponent implements ZioObj, ComponentListener } @Override - public void componentMoved(ComponentEvent e) + public void componentMoved(ComponentEvent aEvent) { - if (e.getSource() == childComp) - refLayout.setComponentConstraints(childComp, "pos " + childComp.getLocation().x + " " + childComp.getLocation().y); + if (aEvent.getSource() == childComp) + workLayout.setComponentConstraints(childComp, + "pos " + childComp.getLocation().x + " " + childComp.getLocation().y); } @Override - public void componentShown(ComponentEvent e) + public void componentShown(ComponentEvent aEvent) { ; // Nothing to do } @Override - public void componentHidden(ComponentEvent e) + public void componentHidden(ComponentEvent aEvent) { ; // Nothing to do } @Override - public void setVisible(boolean isVisible) + public void setVisible(boolean aIsVisible) { - JLayeredPane layeredPane; - KeyboardFocusManager aManager; - // Initialize the GUI if it has not been initialed // Once initialized rootPane != null if (rootPane == null) initializeGui(); - - super.setVisible(isVisible); - + + super.setVisible(aIsVisible); + // Activate/Deactive managing the focus - setFocusCycleRoot(isVisible); - setFocusTraversalPolicyProvider(isVisible); - + setFocusCycleRoot(aIsVisible); + setFocusTraversalPolicyProvider(aIsVisible); + // Bail if this GlassPane is no longer visible - if (isVisible == false) + if (aIsVisible == false) return; // Ensure this GlassPane is at the top - layeredPane = rootPane.getLayeredPane(); + var layeredPane = rootPane.getLayeredPane(); layeredPane.moveToFront(this); // Clear out the old focus (from the underlay component) - aManager = KeyboardFocusManager.getCurrentKeyboardFocusManager(); - aManager.clearGlobalFocusOwner(); - + var tmpKFM = KeyboardFocusManager.getCurrentKeyboardFocusManager(); + tmpKFM.clearGlobalFocusOwner(); + // Wait and ensure that the GlassPane is visible, before we request the focus - SwingUtilities.invokeLater(new Runnable() - { + SwingUtilities.invokeLater(new Runnable() { @Override public void run() { - Component aComponent; - - aComponent = myFocusPolicy.getDefaultComponent(null); - if (aComponent != null) - aComponent.requestFocusInWindow(); - + var tmpComponent = myFocusPolicy.getDefaultComponent(null); + if (tmpComponent != null) + tmpComponent.requestFocusInWindow(); + repaint(); } }); - } - + } + @Override public void zioRead(ZinStream aStream) throws IOException { + aStream.readVersion(0); childComp.zioRead(aStream); } - + @Override public void zioWrite(ZoutStream aStream) throws IOException { + aStream.writeVersion(0); childComp.zioWrite(aStream); } - - /** - * Sets up the GUI for the GlassPane - */ - protected void initializeGui() - { - Set compSet; - JLayeredPane layeredPane; - ComponentTracker aTracker; - Dimension prefDim; - Point prefLoc; - - // Ensure this method is not called twice - if (rootPane != null) - throw new RuntimeException("GlassPane.initializeGui() has been already called."); - - // Retrieve the associated rootPane and layeredPane - rootPane = GuiUtil.getRootPaneContainer(parentComp); - - // Add the GlassPane into the layeredPane - layeredPane = rootPane.getLayeredPane(); - layeredPane.setLayer(this, JLayeredPane.PALETTE_LAYER); - layeredPane.add(this); - setSize(layeredPane.getSize()); -// layeredPane.validate(); - layeredPane.revalidate(); - - // Determine initial location and size of the child component - prefDim = childComp.getPrefDim(); - if (prefDim == null) - prefDim = childComp.getPreferredSize(); - - prefLoc = childComp.getPrefLoc(); - if (prefLoc == null) - prefLoc = new Point(getWidth()/2 - prefDim.width/2, getHeight()/2 - prefDim.height/2); - -// System.out.println("prefLoc: " + prefLoc + " prefDim: " + prefDim); - childComp.setLocation(prefLoc); - childComp.setSize(prefDim); -//childComp.revalidate(); -//layeredPane.revalidate(); -//this.validate(); -//this.revalidate(); - - // Setup the focus policy to consist of any child components of type - // AbstractButton, JComboBox, JTextField, JTable - compSet = Sets.newLinkedHashSet(); - Class[] classArr = {AbstractButton.class, JComboBox.class, JTextField.class, JTable.class}; - GuiUtil.locateAllSubComponents(childComp, compSet, classArr); - - myFocusPolicy = new CustomFocusTraversalPolicy(); - for (Component aItem : compSet) - myFocusPolicy.addComponent(aItem); - - setFocusCycleRoot(true); - setFocusTraversalPolicyProvider(true); - setFocusTraversalPolicy(myFocusPolicy); - - // Set up a ComponentTracker to keep this GlassPane linked to the - // appropriate Components of interest - aTracker = new ComponentTracker(this); - aTracker.setHiddenTracker((Component)rootPane); - aTracker.setResizedTracker(layeredPane); - } @Override protected void paintComponent(Graphics g) @@ -242,12 +189,71 @@ public class GlassPane extends JComponent implements ZioObj, ComponentListener System.out.println("Undefined object being " + aStr + "..."); } + /** + * Sets up the GUI for the GlassPane + */ + private void initializeGui() + { + // Ensure this method is not called twice + if (rootPane != null) + throw new RuntimeException("GlassPane.initializeGui() has been already called."); + + // Retrieve the associated rootPane and layeredPane + rootPane = GuiUtil.getRootPaneContainer(parentComp); + + // Add the GlassPane into the layeredPane + var layeredPane = rootPane.getLayeredPane(); + layeredPane.setLayer(this, JLayeredPane.PALETTE_LAYER); + layeredPane.add(this); + setSize(layeredPane.getSize()); +// layeredPane.validate(); + layeredPane.revalidate(); + + // Determine initial location and size of the child component + var prefDim = childComp.getPrefDim(); + if (prefDim == null) + prefDim = childComp.getPreferredSize(); + + var prefLoc = childComp.getPrefLoc(); + if (prefLoc == null) + prefLoc = new Point(getWidth() / 2 - prefDim.width / 2, getHeight() / 2 - prefDim.height / 2); + +// System.out.println("prefLoc: " + prefLoc + " prefDim: " + prefDim); + childComp.setLocation(prefLoc); + childComp.setSize(prefDim); +//childComp.revalidate(); +//layeredPane.revalidate(); +//this.validate(); +//this.revalidate(); + + // Setup the focus policy to consist of any child components of type + // AbstractButton, JComboBox, JTextField, JTable + var compS = new LinkedHashSet(); + Class[] classArr = { AbstractButton.class, JComboBox.class, JTextField.class, JTable.class }; + GuiUtil.locateAllSubComponents(childComp, compS, classArr); + + myFocusPolicy = new CustomFocusTraversalPolicy(); + for (Component aItem : compS) + myFocusPolicy.addComponent(aItem); + + setFocusCycleRoot(true); + setFocusTraversalPolicyProvider(true); + setFocusTraversalPolicy(myFocusPolicy); + + // Set up a ComponentTracker to keep this GlassPane linked to the + // appropriate Components of interest + var tmpTracker = new ComponentTracker(this); + tmpTracker.setHiddenTracker((Component) rootPane); + tmpTracker.setResizedTracker(layeredPane); + } + } - /** - * Utility class to capture all mouse interactions and redispatch the events only - * to the subcomponents associated with the container. + * Helper class to capture all mouse interactions and redispatch the events only to the subcomponents associated with + * the container. + * + * @author lopeznr1 */ class GlassPaneListener extends MouseInputAdapter { @@ -302,44 +308,39 @@ class GlassPaneListener extends MouseInputAdapter redispatchMouseEvent(e, true); } - //A more finished version of this method would - //handle mouse-dragged events specially. - private void redispatchMouseEvent(MouseEvent e, boolean repaint) + // A more finished version of this method would + // handle mouse-dragged events specially. + private void redispatchMouseEvent(MouseEvent aEvent, boolean aIsRepaint) { + var glassPanePoint = aEvent.getPoint(); + var container = contentPane; + var containerPoint = SwingUtilities.convertPoint(glassPane, glassPanePoint, contentPane); - Point glassPanePoint = e.getPoint(); - Container container = contentPane; - Point containerPoint = SwingUtilities.convertPoint(glassPane, glassPanePoint, contentPane); - - //The mouse event is probably over the content pane. - //Find out exactly which component it's over. - Component component; - - component = SwingUtilities.getDeepestComponentAt(container, containerPoint.x, containerPoint.y); + // The mouse event is probably over the content pane. + // Find out exactly which component it's over. + var component = SwingUtilities.getDeepestComponentAt(container, containerPoint.x, containerPoint.y); if (component != null) { - //Forward events over the to the component - dispatchEvent(component, e); + // Forward events over the to the component + dispatchEvent(component, aEvent); } // Update the glass pane if requested. - if (repaint) + if (aIsRepaint) glassPane.repaint(); } private void dispatchEvent(Component aComponent, MouseEvent aEvent) { - Point aPt; - if (aComponent == null || aEvent == null) return; - aPt = SwingUtilities.convertPoint(glassPane, aEvent.getPoint(), aComponent); - if (aPt == null) + var tmpPt = SwingUtilities.convertPoint(glassPane, aEvent.getPoint(), aComponent); + if (tmpPt == null) return; - aComponent.dispatchEvent(new MouseEvent(aComponent, aEvent.getID(), aEvent.getWhen(), aEvent.getModifiers(), - aPt.x, aPt.y, aEvent.getClickCount(), aEvent.isPopupTrigger())); + aComponent.dispatchEvent(new MouseEvent(aComponent, aEvent.getID(), aEvent.getWhen(), aEvent.getModifiersEx(), + tmpPt.x, tmpPt.y, aEvent.getClickCount(), aEvent.isPopupTrigger())); } } diff --git a/src/glum/gui/panel/GlassPaneAncient.java b/src/glum/gui/panel/GlassPaneAncient.java index abe2e68..31f480a 100644 --- a/src/glum/gui/panel/GlassPaneAncient.java +++ b/src/glum/gui/panel/GlassPaneAncient.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel; import glum.gui.GuiUtil; diff --git a/src/glum/gui/panel/GlassPanel.java b/src/glum/gui/panel/GlassPanel.java index f0248aa..faa35aa 100644 --- a/src/glum/gui/panel/GlassPanel.java +++ b/src/glum/gui/panel/GlassPanel.java @@ -1,34 +1,56 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel; -import glum.gui.GuiUtil; -import glum.zio.*; - import java.awt.Component; import java.awt.Dimension; import java.io.IOException; +import javax.swing.border.BevelBorder; + +import glum.gui.GuiUtil; +import glum.zio.*; + +/** + * Panel that is typically used to display a model panel that pops up over a top level component. + * + * @author lopeznr1 + */ public abstract class GlassPanel extends GPanel implements ZioObj { // State vars - protected Component myGlassPane; + private Component myGlassPane; + /** Standard Constructor */ public GlassPanel(Component aRefParent) { this(aRefParent, PaneType.GlassPanel); } + /** Complex Constructor */ public GlassPanel(Component aRefParent, PaneType aType) { - super(); - if (aType == PaneType.GlassPanel) myGlassPane = new GlassPane(aRefParent, this); else if (aType == PaneType.GlassPanelAncient) myGlassPane = new GlassPaneAncient(aRefParent, this); else myGlassPane = new StandardPane(aRefParent, this); + + setBorder(new BevelBorder(BevelBorder.RAISED)); } - + /** * Call this method to change the end-user's ability to resize this component. Note this call is just a suggestion * and may be totally ignored by the GUI toolkit. @@ -46,13 +68,13 @@ public abstract class GlassPanel extends GPanel implements ZioObj public void setVisibleAsModal() { setVisible(true); - + if (myGlassPane instanceof GlassPane) GuiUtil.modalWhileVisible(myGlassPane); - else + else GuiUtil.modalWhileVisible(getParent()); } - + @Override public void setSize(Dimension aDim) { @@ -61,13 +83,13 @@ public abstract class GlassPanel extends GPanel implements ZioObj else super.setSize(aDim); } - + @Override public void setSize(int width, int height) { setSize(new Dimension(width, height)); } - + @Override public void setVisible(boolean isVisible) { @@ -87,23 +109,23 @@ public abstract class GlassPanel extends GPanel implements ZioObj if (myGlassPane.isVisible() == true) myGlassPane.repaint(); } - + @Override public void zioRead(ZinStream aStream) throws IOException { aStream.readVersion(0); - + if (myGlassPane instanceof ZioObj) ((ZioObj)myGlassPane).zioRead(aStream); } - + @Override public void zioWrite(ZoutStream aStream) throws IOException { aStream.writeVersion(0); - + if (myGlassPane instanceof ZioObj) ((ZioObj)myGlassPane).zioWrite(aStream); } - + } diff --git a/src/glum/gui/panel/NoticePanel.java b/src/glum/gui/panel/NoticePanel.java new file mode 100644 index 0000000..81f54c1 --- /dev/null +++ b/src/glum/gui/panel/NoticePanel.java @@ -0,0 +1,187 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.panel; + +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Map; + +import javax.swing.*; +import javax.swing.border.LineBorder; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +import com.google.common.collect.ImmutableMap; + +import glum.gui.FocusUtil; +import glum.gui.GuiUtil; +import glum.gui.action.ClickAction; +import glum.gui.component.model.GListModel; +import net.miginfocom.swing.MigLayout; + +/** + * {@link GlassPanel} used to provide the user with a listing of notices. + *

    + * The title and list of notices can be customized. + * + * @author lopeznr1 + */ +public class NoticePanel extends GlassPanel implements ActionListener, ListSelectionListener +{ + // GUI vars + private JLabel titleL; + private JList itemJL; + private JTextArea infoTA; + private JButton closeB; + + // State vars + private ImmutableMap notificationM; + + /** Standard Constructor */ + public NoticePanel(Component aParent, String aTitle, int sizeX, int sizeY) + { + super(aParent); + + notificationM = ImmutableMap.of(); + + buildGuiArea(); + setSize(sizeX, sizeY); + setTitle(aTitle); + + // Set up keyboard short cuts + FocusUtil.addAncestorKeyBinding(this, "ESCAPE", new ClickAction(closeB)); + FocusUtil.addAncestorKeyBinding(this, "ENTER", new ClickAction(closeB)); + } + + /** Simplified Constructor */ + public NoticePanel(Component aParent, String aTitle) + { + this(aParent, aTitle, 275, 350); + } + + /** Simplified Constructor */ + public NoticePanel(Component aParent) + { + this(aParent, "Untitled", 275, 350); + } + + /** + * Sets the notifications to be displayed. + */ + public void setNotifications(Map aNotificationM) + { + notificationM = ImmutableMap.copyOf(aNotificationM); + + // Update the item JList + itemJL.removeListSelectionListener(this); + var tmpModel = new GListModel<>(aNotificationM.keySet()); + itemJL.setModel(tmpModel); + itemJL.addListSelectionListener(this); + + String tmpItem = null; + if (notificationM.size() > 0) + tmpItem = notificationM.keySet().iterator().next(); + itemJL.setSelectedValue(tmpItem, true); + + updateGui(); + } + + /** + * Sets the font of the info area. + */ + public void setFontInfo(Font aFont) + { + infoTA.setFont(aFont); + } + + /** + * Sets the tab size associated with the info area. + */ + public void setTabSize(int aSize) + { + infoTA.setTabSize(aSize); + } + + /** + * Sets the title of this panel. + */ + public void setTitle(String aTitle) + { + titleL.setText(aTitle); + } + + @Override + public void actionPerformed(ActionEvent aEvent) + { + var source = aEvent.getSource(); + if (source == closeB) + { + setVisible(false); + notifyListeners(this, ID_CANCEL); + } + + updateGui(); + } + + @Override + public void valueChanged(ListSelectionEvent aEvent) + { + updateGui(); + } + + /** + * Forms the actual GUI + */ + private void buildGuiArea() + { + setLayout(new MigLayout("", "[]", "[]")); + + // Title Area + titleL = new JLabel("Title", JLabel.CENTER); + add(titleL, "growx,span,wrap"); + add(GuiUtil.createDivider(), "growx,h 4!,span,wrap"); + + // Notification area + itemJL = new JList<>(); + itemJL.setBorder(new LineBorder(Color.BLACK)); + itemJL.addListSelectionListener(this); + + infoTA = new JTextArea("No status", 3, 0); + infoTA.setEditable(false); +// infoTA.setOpaque(false); + infoTA.setLineWrap(true); + infoTA.setTabSize(3); + infoTA.setWrapStyleWord(true); + + add(new JScrollPane(itemJL), "w pref+3::,growy,span,split"); + add(new JScrollPane(infoTA), "growx,growy,pushx,pushy,span,wrap"); + + // Action area + closeB = GuiUtil.createJButton("Close", this); + add(closeB, "ax right,span,split"); + } + + /** + * Helper method that keeps various UI components synchronized. + */ + private void updateGui() + { + var tmpKey = itemJL.getSelectedValue(); + var tmpVal = notificationM.get(tmpKey); + + infoTA.setText(tmpVal); + infoTA.setCaretPosition(0); + } +} diff --git a/src/glum/gui/panel/PaneType.java b/src/glum/gui/panel/PaneType.java index 8372b67..9253cc1 100644 --- a/src/glum/gui/panel/PaneType.java +++ b/src/glum/gui/panel/PaneType.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel; public enum PaneType diff --git a/src/glum/gui/panel/ShadePane.java b/src/glum/gui/panel/ShadePane.java index b4054ea..52ea35d 100644 --- a/src/glum/gui/panel/ShadePane.java +++ b/src/glum/gui/panel/ShadePane.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel; import glum.gui.GuiUtil; diff --git a/src/glum/gui/panel/StandardPane.java b/src/glum/gui/panel/StandardPane.java index 8fed0d9..fac36af 100644 --- a/src/glum/gui/panel/StandardPane.java +++ b/src/glum/gui/panel/StandardPane.java @@ -1,30 +1,44 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel; -import java.awt.Color; -import java.awt.Component; -import java.awt.Graphics; +import java.awt.*; import javax.swing.JComponent; import javax.swing.RootPaneContainer; + import glum.gui.GuiUtil; -import glum.gui.panel.CustomFocusTraversalPolicy; import net.miginfocom.swing.MigLayout; +/** + * UI panel for displaying an overlayed {@link Component}. + * + * @author lopeznr1 + */ public class StandardPane extends JComponent { // Communicator vars - protected Component refParent; - protected JComponent displayPane; + private final Component refParent; + private final JComponent displayPane; - protected RootPaneContainer rootPane; + private RootPaneContainer rootPane; // State vars - protected Color fillColor; - protected CustomFocusTraversalPolicy myFocusPolicy; + private Color fillColor; +// private CustomFocusTraversalPolicy myFocusPolicy; - /** - * Constructor - */ + /** Standard Constructor */ public StandardPane(Component aRefParent, JComponent aDisplayPane) { // Communicator vars @@ -77,7 +91,7 @@ public class StandardPane extends JComponent // Set up a ComponentTracker to keep this ShadePane linked to the rootPane aTracker = new ComponentTracker(this); - aTracker.setResizedTracker((Component)rootPane); + aTracker.setResizedTracker((Component) rootPane); } @Override diff --git a/src/glum/gui/panel/WaftPanel.java b/src/glum/gui/panel/WaftPanel.java index a313a2c..cb4b700 100644 --- a/src/glum/gui/panel/WaftPanel.java +++ b/src/glum/gui/panel/WaftPanel.java @@ -1,29 +1,41 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel; -import glum.gui.panel.nub.HorizontalNub; -import glum.util.MathUtil; -import glum.zio.*; -import glum.zio.util.ZioUtil; - -import java.awt.Component; -import java.awt.Dimension; -import java.awt.Point; +import java.awt.*; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.io.IOException; import javax.swing.JComponent; +import glum.gui.panel.nub.HorizontalNub; +import glum.util.MathUtil; +import glum.zio.*; +import glum.zio.util.ZioUtil; import net.miginfocom.swing.MigLayout; /** - * Panel which provides decorative controls to allow a panel to be moved or resized. This is especially useful for - * wrapping a component that will be thrown into a JLayered pane of sorts. Note it is assumed that WaftPanel is - * contained in a LayoutManager of type MigLayout. - *

    + * Panel which provides decorative controls to allow a panel to be moved or resized. This is useful for wrapping a + * component that will be thrown into a JLayered pane of sorts. Note it is assumed that WaftPanel is contained in a + * LayoutManager of type MigLayout. + *

    * Perhaps, this class should be rewritten so that it draws the move, resize borders, and manually listens to them * rather than delegating to the Nub classes. The current implementation is slightly inefficient, but probably more * flexible as it allows other custom Nubs to be swapped in. + * + * @author lopeznr1 */ public class WaftPanel extends JComponent implements ZioObj, ComponentListener { @@ -31,52 +43,51 @@ public class WaftPanel extends JComponent implements ZioObj, ComponentListener protected Component childComp; protected JComponent parentComp; protected HorizontalNub nMoveComp, sMoveComp; - + // State vars protected Dimension prefDim; protected Point prefLoc; protected double aspectRatio; - - public WaftPanel(Component aChildComp, JComponent aParentComp, boolean topNubOnly) - { - super(); + /** Standard Constructor */ + public WaftPanel(Component aChildComp, JComponent aParentComp, boolean aTopNubOnly) + { childComp = aChildComp; parentComp = aParentComp; - + prefDim = null; prefLoc = null; // Embed the childComp into this WaftPanel and surround with the various nub handles setLayout(new MigLayout("", "0[grow,fill]0[]0", "0[]0[grow,fill]0[]0")); - + nMoveComp = new HorizontalNub(parentComp, this, true); sMoveComp = new HorizontalNub(parentComp, this, false); - add("wrap", nMoveComp); - add("growx,growy,wrap", aChildComp); - if (topNubOnly == false) - add("wrap", sMoveComp); - + add(nMoveComp, "wrap"); + add(aChildComp, "growx,growy,wrap"); + if (aTopNubOnly == false) + add(sMoveComp, "wrap"); + // Register to listen to components events associated with the parent parentComp.addComponentListener(this); } - + + /** Simplified Constructor */ public WaftPanel(Component aChildComp, JComponent aParentComp) { this(aChildComp, aParentComp, false); } - - + /** - * Change whether the user can move the WaftPanel. Note this will automatically - * disabling the resizing of the WaftPanel. + * Change whether the user can move the WaftPanel. Note this will automatically disabling the resizing of the + * WaftPanel. */ public void setMovable(boolean aBool) { nMoveComp.setVisible(false); sMoveComp.setVisible(false); } - + /** * Changes whether the associated nubs will provide resize controls. */ @@ -85,7 +96,7 @@ public class WaftPanel extends JComponent implements ZioObj, ComponentListener nMoveComp.setResizable(aBool); sMoveComp.setResizable(aBool); } - + /** * Returns the current targeted and locked dimension */ @@ -104,66 +115,61 @@ public class WaftPanel extends JComponent implements ZioObj, ComponentListener { if (prefLoc == null) return null; - + return new Point(prefLoc); } - + @Override public void setLocation(Point aLoc) { // Update the preferred location prefLoc = new Point(aLoc); - + setLocationAndLock(aLoc); } - + @Override public void setLocation(int x, int y) { setLocation(new Point(x, y)); } - + @Override public void setSize(Dimension aDim) { // Update the preferred location prefDim = new Dimension(aDim); - + super.setSize(aDim); } - + @Override public void componentResized(ComponentEvent aEvent) { - Point targLoc; - Dimension targDim; - Dimension minDim; - - Dimension parentDim; - // Respond only to resizes of our parent if (aEvent.getComponent() != parentComp) return; - + // Record the current WaftPanels as preferences if they have not been recorded if (prefDim == null) prefDim = getSize(); - + if (prefLoc == null) prefLoc = getLocation(); - + // Retrieve the targDim, targLoc, and the minDim - minDim = getMinimumSize(); - targDim = new Dimension(prefDim); - targLoc = new Point(prefLoc); - + var minDim = getMinimumSize(); + var targDim = new Dimension(prefDim); + var targLoc = new Point(prefLoc); + // Ensure the targDim is no larger than parentDim - parentDim = parentComp.getSize(); + var parentDim = parentComp.getSize(); targDim.width = MathUtil.boundRange(0, parentDim.width, targDim.width); targDim.height = MathUtil.boundRange(0, parentDim.height, targDim.height); - + // Attempt to force ourself into the constraint of the parentComp - MathUtil.forceConstraints(targLoc, targDim, 0, 0, parentComp.getWidth(), parentComp.getHeight(), new Dimension(0, 0), true); + MathUtil.forceConstraints(targLoc, targDim, 0, 0, parentComp.getWidth(), parentComp.getHeight(), + new Dimension(0, 0), true); // Ensure the minimum constraints are not violated targDim.width = MathUtil.boundRange(minDim.width, targDim.width, targDim.width); @@ -173,7 +179,7 @@ public class WaftPanel extends JComponent implements ZioObj, ComponentListener setLocationAndLock(targLoc); setPreferredSize(targDim); super.setSize(targDim); - + parentComp.revalidate(); } @@ -194,46 +200,50 @@ public class WaftPanel extends JComponent implements ZioObj, ComponentListener { ; // Nothing to do } - + @Override public void zioRead(ZinStream aStream) throws IOException { aStream.readVersion(0); - + prefDim = ZioUtil.readDimension(aStream); prefLoc = ZioUtil.readPoint(aStream); + + if (prefLoc != null && prefDim != null) + { + setLocation(prefLoc); + setPreferredSize(prefDim); + } } @Override public void zioWrite(ZoutStream aStream) throws IOException { aStream.writeVersion(0); - + ZioUtil.writeDimension(aStream, prefDim); ZioUtil.writePoint(aStream, prefLoc); } /** * Helper method to set the location of this WaftPanel relative to its parent. - *

    + *

    * Adjustments are made to our constraints associated with the layout manager. This is needed so that when the layout * manager is (re)validated any movement or resize changes made will stick. Note we assume that we are placed in a * MigLayout manager. */ private void setLocationAndLock(Point aLoc) { - MigLayout rootLayout; - // Adjust our constraints associated in the layout manager. This is needed so that when the layout manager is // (re)validated any movement or resize changes made will stick. // Note we assume that we are placed in a MigLayout manager. - rootLayout = (MigLayout)parentComp.getLayout(); + var rootLayout = (MigLayout) parentComp.getLayout(); if (rootLayout == null) { super.setLocation(aLoc.x, aLoc.y); return; } - + rootLayout.setComponentConstraints(this, "pos " + aLoc.x + "px " + aLoc.y + "px"); rootLayout.invalidateLayout(null); } diff --git a/src/glum/gui/panel/generic/BaseTextInputPanel.java b/src/glum/gui/panel/generic/BaseTextInputPanel.java new file mode 100644 index 0000000..cfd0d7b --- /dev/null +++ b/src/glum/gui/panel/generic/BaseTextInputPanel.java @@ -0,0 +1,163 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.panel.generic; + +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.*; + +import glum.gui.FocusUtil; +import glum.gui.GuiUtil; +import glum.gui.action.ClickAction; +import glum.gui.component.GTextField; +import glum.gui.panel.GlassPanel; +import net.miginfocom.swing.MigLayout; + +/** + * {@link GlassPanel} used to prompt the user for a text inupt. + *

    + * The title and (prompt) message can be customized. + * + * @author lopeznr1 + */ +public abstract class BaseTextInputPanel extends GlassPanel implements ActionListener, GenericCodes +{ + // Constants + public static final Color warnColor = new Color(128, 0, 0); + + // GUI vars + protected JLabel titleL, inputL, infoL; + protected JButton cancelB, acceptB; + protected GTextField inputTF; + + // State vars + protected boolean isAccepted; + + /** Standard Constructor */ + public BaseTextInputPanel(Component aParent) + { + super(aParent); + + isAccepted = false; + + // Build the actual GUI + buildGuiArea(); + setPreferredSize(new Dimension(300, getPreferredSize().height)); + + // Set up keyboard short cuts + FocusUtil.addAncestorKeyBinding(this, "ESCAPE", new ClickAction(cancelB)); + FocusUtil.addAncestorKeyBinding(this, "ENTER", new ClickAction(acceptB)); + FocusUtil.addFocusKeyBinding(inputTF, "ENTER", new ClickAction(acceptB)); + } + + /** + * Returns the input of the user. + */ + public String getInput() + { + if (isAccepted == false) + return null; + + return inputTF.getText(); + } + + /** + * Sets in aStr as the default input + */ + public void setInput(String aStr) + { + inputTF.setValue(aStr); + } + + @Override + public void actionPerformed(ActionEvent aEvent) + { + var source = aEvent.getSource(); + if (source == cancelB) + { + isAccepted = false; + setVisible(false); + notifyListeners(this, ID_CANCEL, "Cancel"); + } + else if (source == acceptB) + { + isAccepted = true; + setVisible(false); + notifyListeners(this, ID_ACCEPT, "Accept"); + } + else if (source == inputTF) + { + updateGui(); + } + } + + @Override + public void setVisible(boolean aBool) + { + if (aBool == true) + isAccepted = false; +// resetGui(); + + super.setVisible(aBool); + } + + /** + * Forms the actual dialog GUI + */ + protected void buildGuiArea() + { + setLayout(new MigLayout("", "[right][grow][][]", "[][][20!][]")); + var tmpFont = (new JTextField()).getFont(); + + // Title Area + titleL = new JLabel("Title", JLabel.CENTER); + add(titleL, "growx,span 4,wrap"); + + // Source area + inputL = new JLabel("Symbol:"); + inputTF = new GTextField(this); + add(inputL); + add(inputTF, "growx,span 3,wrap"); + + // Warn area + var tmpStr = "Please enter text input."; + infoL = GuiUtil.createJLabel(tmpStr, tmpFont); + infoL.setForeground(warnColor); + add(infoL, "growx,span 4,wrap"); + + // Control area + cancelB = GuiUtil.createJButton("Cancel", this); + acceptB = GuiUtil.createJButton("Accept", this); + add(cancelB, "skip 2"); + add(acceptB, ""); + } + + /** + * Sets the Gui and associated components to the initial state + */ + public void resetGui() + { + isAccepted = false; + inputTF.setText(""); + updateGui(); + } + + /** + * Utility method to update the various GUI components (most likely infoL, acceptB) based on the current inputTF. + */ + protected abstract void updateGui(); + +} diff --git a/src/glum/gui/panel/generic/GenericCodes.java b/src/glum/gui/panel/generic/GenericCodes.java index 32daa92..981daad 100644 --- a/src/glum/gui/panel/generic/GenericCodes.java +++ b/src/glum/gui/panel/generic/GenericCodes.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.generic; public interface GenericCodes diff --git a/src/glum/gui/panel/generic/LocationPanel.java b/src/glum/gui/panel/generic/LocationPanel.java index ae86ac3..1a9bd83 100644 --- a/src/glum/gui/panel/generic/LocationPanel.java +++ b/src/glum/gui/panel/generic/LocationPanel.java @@ -1,5 +1,26 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.generic; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.io.IOException; + +import javax.swing.*; + import glum.gui.FocusUtil; import glum.gui.GuiUtil; import glum.gui.action.ClickAction; @@ -8,44 +29,34 @@ import glum.gui.panel.GlassPanel; import glum.io.Loader; import glum.io.LoaderInfo; import glum.unit.ByteUnit; -import glum.unit.Unit; import glum.zio.ZinStream; import glum.zio.ZoutStream; - -import java.awt.Color; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.io.File; -import java.io.IOException; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; -import javax.swing.JTextField; -import javax.swing.border.BevelBorder; - import net.miginfocom.swing.MigLayout; +/** + * {@link GlassPanel} used to prompt the user for a location on the disk. + *

    + * The title and (prompt) message can be customized. + * + * @author lopeznr1 + */ public class LocationPanel extends GlassPanel implements ActionListener, GenericCodes { // Constants - public static final Color warnColor = new Color(128, 0, 0); + public static final Color ColorWarn = new Color(128, 0, 0); // GUI vars - protected JLabel titleL, locationL, infoL, warnL; - protected JButton cancelB, acceptB, fileB; - protected JTextArea instrTA; - protected GTextField locationTF; + private JLabel titleL, locationL, infoL, warnL; + private JButton cancelB, acceptB, fileB; + private JTextArea instrTA; + private GTextField locationTF; // State vars - protected LoaderInfo loaderInfo; - protected long minFreeSpace; - protected boolean isAccepted; + private LoaderInfo loaderInfo; + private long minFreeSpace; + private boolean isAccepted; + /** Standard Constructor */ public LocationPanel(Component aParent) { super(aParent); @@ -65,14 +76,38 @@ public class LocationPanel extends GlassPanel implements ActionListener, Generic } /** - * Returns the input of the user. + * Returns the file as specified in the location gui. + *

    + * Returns null if the input is empty. */ - public File getInput() + public File getPath() { - if (isAccepted == false) + var inputStr = locationTF.getText(); + if (inputStr.isEmpty() == true) return null; - return new File(locationTF.getText()); + return new File(inputStr); + } + + /** + * Returns true if this panel was accepted. + */ + public boolean isAccepted() + { + return isAccepted; + } + + /** + * Sets the location gui to reflect the specified file. + */ + public void setPath(File aFile) + { + var inputStr = ""; + if (aFile != null) + inputStr = aFile.getAbsolutePath(); + + locationTF.setText(inputStr); + updateGui(); } /** @@ -102,47 +137,24 @@ public class LocationPanel extends GlassPanel implements ActionListener, Generic @Override public void actionPerformed(ActionEvent aEvent) { - Object source; - - source = aEvent.getSource(); - if (source == cancelB) - { - isAccepted = false; - setVisible(false); - notifyListeners(this, ID_CANCEL, "Cancel"); - } - else if (source == acceptB) - { - isAccepted = true; - setVisible(false); - notifyListeners(this, ID_ACCEPT, "Accept"); - } - else if (source == locationTF) - { - updateGui(); - } + var source = aEvent.getSource(); + if (source == acceptB) + doActionAccept(); + else if (source == cancelB) + doActionCancel(); else if (source == fileB) - { - File aFile; - - // Retrieve the path to load - aFile = Loader.queryUserForPath(loaderInfo, getParent(), "Select target folder", true); - if (aFile != null) - locationTF.setValue(aFile.getAbsolutePath()); - - updateGui(); - } + doActionDestPath(); + updateGui(); } @Override public void setVisible(boolean aBool) { - // Reset the GUI + // Reset relevant state vars if (aBool == true) { isAccepted = false; - locationTF.setText(""); updateGui(); } @@ -174,12 +186,8 @@ public class LocationPanel extends GlassPanel implements ActionListener, Generic */ protected void buildGuiArea() { - JScrollPane tmpPane; - Font aFont; - String aStr; - setLayout(new MigLayout("", "[][][grow]", "[][grow,50::][]")); - aFont = (new JTextField()).getFont(); + var tmpFont = (new JTextField()).getFont(); // Title Area titleL = new JLabel("Title", JLabel.CENTER); @@ -187,7 +195,7 @@ public class LocationPanel extends GlassPanel implements ActionListener, Generic // Instruction area instrTA = GuiUtil.createUneditableTextArea(2, 0); - tmpPane = new JScrollPane(instrTA); + var tmpPane = new JScrollPane(instrTA); tmpPane.setBorder(null); add(tmpPane, "growx,growy,span,wrap"); @@ -200,43 +208,74 @@ public class LocationPanel extends GlassPanel implements ActionListener, Generic add(locationTF, "growx,span,wrap"); // Info area - aStr = "Please specify the disk location where the catalog should be constructed.."; - infoL = GuiUtil.createJLabel(aStr, aFont); + var tmpStr = "Please specify the disk location where the catalog should be constructed.."; + infoL = GuiUtil.createJLabel(tmpStr, tmpFont); add(infoL, "growx,span,wrap"); // Warn area - aStr = ""; - warnL = GuiUtil.createJLabel(aStr, aFont); - warnL.setForeground(warnColor); + tmpStr = ""; + warnL = GuiUtil.createJLabel(tmpStr, tmpFont); + warnL.setForeground(ColorWarn); add(warnL, "growx,h 20!,span,wrap"); // Control area - cancelB = GuiUtil.createJButton("Cancel", this, aFont); - acceptB = GuiUtil.createJButton("Accept", this, aFont); - add(cancelB, "align right,span,split 2"); + cancelB = GuiUtil.createJButton("Cancel", this); + acceptB = GuiUtil.createJButton("Accept", this); + add(cancelB, "ax right,span,split"); add(acceptB, ""); - - setBorder(new BevelBorder(BevelBorder.RAISED)); } /** - * Utility method to update the various GUI components (most likely infoL, acceptB) based on the current inputTF. + * Helper method to process the "accept" action. */ - protected void updateGui() + private void doActionAccept() { - File destPath, rootPath; - String infoStr, warnStr; - Unit diskUnit; - boolean isValid; - long freeBytes; + isAccepted = true; + setVisible(false); + notifyListeners(this, ID_ACCEPT, "Cancel"); + } + /** + * Helper method to process the "cancel" action. + */ + private void doActionCancel() + { + isAccepted = false; + setVisible(false); + notifyListeners(this, ID_CANCEL, "Cancel"); + } + + /** + * Helper method to process the "specify destination path" action. + */ + private void doActionDestPath() + { + // Retrieve the path to load + File tmpFile = Loader.queryUserForPath(loaderInfo, getParent(), "Select target folder", true); + if (tmpFile != null) + locationTF.setValue(tmpFile.getAbsolutePath()); + } + + /** + * Helper method that keeps various UI components synchronized. + */ + private void updateGui() + { // Retrieve the folder - destPath = null; - if (locationTF.getText().isEmpty() == false) - destPath = new File(locationTF.getText()); + File destPath = null; + String locationStr = locationTF.getText(); + if (locationStr.isEmpty() == false) + { + if (locationStr.equals("~") == true) + locationStr = System.getProperty("user.home"); + if (locationStr.startsWith("~/") == true) + locationStr = new File(System.getProperty("user.home"), locationStr.substring(2)).getAbsolutePath(); + + destPath = new File(locationStr); + } // Retrieve the root folder - rootPath = destPath; + var rootPath = destPath; while (rootPath != null) { if (rootPath.isDirectory() == true) @@ -246,7 +285,8 @@ public class LocationPanel extends GlassPanel implements ActionListener, Generic } // Test the validity of the location - isValid = false; + String infoStr, warnStr; + var isValid = false; if (rootPath == null) { infoStr = "Free space: ---"; @@ -254,19 +294,23 @@ public class LocationPanel extends GlassPanel implements ActionListener, Generic } else { - diskUnit = new ByteUnit(2); - freeBytes = rootPath.getFreeSpace(); - infoStr = "Free space: " + diskUnit.getString(freeBytes); + var diskBU = new ByteUnit(2); + var freeBytes = rootPath.getFreeSpace(); + infoStr = "Free space: " + diskBU.getString(freeBytes); if (minFreeSpace > 0) - infoStr += " Required space: " + diskUnit.getString(minFreeSpace); + infoStr += " Required space: " + diskBU.getString(minFreeSpace); warnStr = ""; if (rootPath.canWrite() == false) warnStr = "No write permission at location."; else if (freeBytes < minFreeSpace && minFreeSpace > 0) - warnStr = "Not enough free space on disk. Minimun required: " + diskUnit.getString(minFreeSpace); + warnStr = "Not enough free space on disk. Minimun required: " + diskBU.getString(minFreeSpace); else isValid = true; + + // Update the loaderInfo to reflect the user input + if (destPath != null) + loaderInfo.setPath(destPath); } // Update the components diff --git a/src/glum/gui/panel/generic/MessagePanel.java b/src/glum/gui/panel/generic/MessagePanel.java index 38c1f56..d8e4838 100644 --- a/src/glum/gui/panel/generic/MessagePanel.java +++ b/src/glum/gui/panel/generic/MessagePanel.java @@ -1,31 +1,45 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.generic; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.*; + import glum.gui.FocusUtil; import glum.gui.GuiUtil; import glum.gui.action.ClickAction; import glum.gui.panel.GlassPanel; - -import java.awt.Component; -import java.awt.Font; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; -import javax.swing.JTextField; -import javax.swing.border.BevelBorder; -import javax.swing.border.Border; - import net.miginfocom.swing.MigLayout; +/** + * {@link GlassPanel} used to provide the user with a notification. + *

    + * The title and (notification) message can be customized. + * + * @author lopeznr1 + */ public class MessagePanel extends GlassPanel implements ActionListener, GenericCodes { // GUI vars - protected JLabel titleL; - protected JTextArea infoTA; - protected JButton closeB; + private JLabel titleL; + private JTextArea infoTA; + private JButton closeB; + /** Standard Constructor */ public MessagePanel(Component aParent, String aTitle, int sizeX, int sizeY) { super(aParent); @@ -39,26 +53,46 @@ public class MessagePanel extends GlassPanel implements ActionListener, GenericC FocusUtil.addAncestorKeyBinding(this, "ENTER", new ClickAction(closeB)); } - public MessagePanel(Component aParent) - { - this(aParent, "Untitled", 275, 350); - } - + /** Simplified Constructor */ public MessagePanel(Component aParent, String aTitle) { this(aParent, aTitle, 275, 350); } + /** Simplified Constructor */ + public MessagePanel(Component aParent) + { + this(aParent, "Untitled", 275, 350); + } + /** - * Sets the message of the PromptPanel + * Sets the info message and adjusts the caret position. + */ + public void setInfo(String aStr, int aCaretPos) + { + infoTA.setText(aStr); + infoTA.setCaretPosition(aCaretPos); + } + + /** + * Sets the info message. */ public void setInfo(String aStr) { infoTA.setText(aStr); + infoTA.setCaretPosition(0); } /** - * Sets the title of this PromptPanel + * Sets the tab size associated with the info area. + */ + public void setTabSize(int aSize) + { + infoTA.setTabSize(aSize); + } + + /** + * Sets the title of this panel. */ public void setTitle(String aTitle) { @@ -68,9 +102,7 @@ public class MessagePanel extends GlassPanel implements ActionListener, GenericC @Override public void actionPerformed(ActionEvent aEvent) { - Object source; - - source = aEvent.getSource(); + var source = aEvent.getSource(); if (source == closeB) { setVisible(false); @@ -81,14 +113,9 @@ public class MessagePanel extends GlassPanel implements ActionListener, GenericC /** * Forms the actual GUI */ - protected void buildGuiArea() + private void buildGuiArea() { - JScrollPane tmpScrollPane; - Font aFont; - Border aBorder; - setLayout(new MigLayout("", "[right][grow][]", "[][grow][]")); - aFont = (new JTextField()).getFont(); // Title Area titleL = new JLabel("Title", JLabel.CENTER); @@ -99,19 +126,16 @@ public class MessagePanel extends GlassPanel implements ActionListener, GenericC infoTA.setEditable(false); // infoTA.setOpaque(false); infoTA.setLineWrap(true); + infoTA.setTabSize(3); infoTA.setWrapStyleWord(true); - tmpScrollPane = new JScrollPane(infoTA); + JScrollPane tmpScrollPane = new JScrollPane(infoTA); // tmpScrollPane.setBorder(null); add(tmpScrollPane, "growx,growy,span,wrap"); // Control area - closeB = GuiUtil.createJButton("Close", this, aFont); - add(closeB, "skip 2,span 1"); - - // Border - aBorder = new BevelBorder(BevelBorder.RAISED); - setBorder(aBorder); + closeB = GuiUtil.createJButton("Close", this); + add(closeB, "skip 2"); } } diff --git a/src/glum/gui/panel/generic/PromptPanel.java b/src/glum/gui/panel/generic/PromptPanel.java index e373fe3..df6181c 100644 --- a/src/glum/gui/panel/generic/PromptPanel.java +++ b/src/glum/gui/panel/generic/PromptPanel.java @@ -1,59 +1,76 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.generic; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.*; + import glum.gui.FocusUtil; import glum.gui.GuiUtil; import glum.gui.action.ClickAction; import glum.gui.panel.GlassPanel; - -import java.awt.Component; -import java.awt.Font; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; -import javax.swing.JTextField; -import javax.swing.border.BevelBorder; -import javax.swing.border.Border; - import net.miginfocom.swing.MigLayout; +/** + * {@link GlassPanel} used to prompt the user of an action to be taken. A message will be provided via a + * {@link JTextArea}. + *

    + * The title and (prompt) message can be customized. + * + * @author lopeznr1 + */ public class PromptPanel extends GlassPanel implements ActionListener, GenericCodes { // GUI vars - protected JLabel titleL; - protected JTextArea infoTA; - protected JButton cancelB, acceptB; - - // State vars - protected boolean isAccepted; + private JLabel titleL; + private JTextArea infoTA; + private JButton cancelB, acceptB; + // State vars + private boolean isAccepted; + + /** Standard Constructor */ public PromptPanel(Component aParent, String aTitle, int sizeX, int sizeY) { super(aParent); - + isAccepted = false; buildGuiArea(); setSize(sizeX, sizeY); setTitle(aTitle); - + // Set up keyboard short cuts FocusUtil.addAncestorKeyBinding(this, "ESCAPE", new ClickAction(cancelB)); FocusUtil.addAncestorKeyBinding(this, "ENTER", new ClickAction(acceptB)); } - public PromptPanel(Component aParent) - { - this(aParent, "Untitled", 275, 350); - } - + /** Simplified Constructor */ public PromptPanel(Component aParent, String aTitle) { this(aParent, aTitle, 275, 350); } - + + /** Simplified Constructor */ + public PromptPanel(Component aParent) + { + this(aParent, "Untitled", 275, 350); + } + /** * Returns true if the prompt was accepted */ @@ -61,9 +78,18 @@ public class PromptPanel extends GlassPanel implements ActionListener, GenericCo { return isAccepted; } - + /** - * Sets the message of the PromptPanel + * Sets the info message and adjusts the caret position. + */ + public void setInfo(String aStr, int aCaretPos) + { + infoTA.setText(aStr); + infoTA.setCaretPosition(aCaretPos); + } + + /** + * Sets the info message. */ public void setInfo(String aStr) { @@ -71,7 +97,15 @@ public class PromptPanel extends GlassPanel implements ActionListener, GenericCo } /** - * Sets the title of this PromptPanel + * Sets in the tabs size of the info text area. + */ + public void setTabSize(int aNumSpaces) + { + infoTA.setTabSize(aNumSpaces); + } + + /** + * Sets the title of this panel. */ public void setTitle(String aTitle) { @@ -81,9 +115,7 @@ public class PromptPanel extends GlassPanel implements ActionListener, GenericCo @Override public void actionPerformed(ActionEvent aEvent) { - Object source; - - source = aEvent.getSource(); + var source = aEvent.getSource(); if (source == cancelB) { isAccepted = false; @@ -97,14 +129,14 @@ public class PromptPanel extends GlassPanel implements ActionListener, GenericCo notifyListeners(this, ID_ACCEPT, "Accept"); } } - + @Override public void setVisible(boolean isVisible) { // Reset the panel if (isVisible == true) isAccepted = false; - + super.setVisible(isVisible); } @@ -113,17 +145,12 @@ public class PromptPanel extends GlassPanel implements ActionListener, GenericCo */ protected void buildGuiArea() { - JScrollPane tmpScrollPane; - Font aFont; - Border aBorder; - setLayout(new MigLayout("", "[right][grow][][]", "[][grow][]")); - aFont = (new JTextField()).getFont(); - + // Title Area titleL = new JLabel("Title", JLabel.CENTER); add(titleL, "growx,span,wrap"); - + // Info area infoTA = new JTextArea("No status", 3, 0); infoTA.setEditable(false); @@ -131,19 +158,15 @@ public class PromptPanel extends GlassPanel implements ActionListener, GenericCo infoTA.setLineWrap(true); infoTA.setWrapStyleWord(true); - tmpScrollPane = new JScrollPane(infoTA); + var tmpScrollPane = new JScrollPane(infoTA); // tmpScrollPane.setBorder(null); add(tmpScrollPane, "growx,growy,span,wrap"); - + // Control area - cancelB = GuiUtil.createJButton("Cancel", this, aFont); - acceptB = GuiUtil.createJButton("Accept", this, aFont); + cancelB = GuiUtil.createJButton("Cancel", this); + acceptB = GuiUtil.createJButton("Accept", this); add(cancelB, "skip 2"); add(acceptB, ""); - - // Border - aBorder = new BevelBorder(BevelBorder.RAISED); - setBorder(aBorder); } } diff --git a/src/glum/gui/panel/generic/SimplePromptPanel.java b/src/glum/gui/panel/generic/SimplePromptPanel.java index 9aef442..b39d0c6 100644 --- a/src/glum/gui/panel/generic/SimplePromptPanel.java +++ b/src/glum/gui/panel/generic/SimplePromptPanel.java @@ -1,16 +1,38 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.generic; -import java.awt.*; -import java.awt.event.*; -import javax.swing.*; -import javax.swing.border.*; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; -import glum.gui.*; +import javax.swing.*; + +import glum.gui.FocusUtil; +import glum.gui.GuiUtil; import glum.gui.action.ClickAction; import glum.gui.panel.GlassPanel; -import glum.gui.panel.generic.GenericCodes; import net.miginfocom.swing.MigLayout; +/** + * {@link GlassPanel} used to prompt the user of an action to be taken. A message will be provided via a {@link JLabel}. + *

    + * The title and (prompt) message can be customized. + * + * @author lopeznr1 + */ public class SimplePromptPanel extends GlassPanel implements ActionListener, GenericCodes { // GUI vars @@ -20,6 +42,7 @@ public class SimplePromptPanel extends GlassPanel implements ActionListener, Gen // State vars protected boolean isAccepted; + /** Standard Constructor */ public SimplePromptPanel(Component aParent) { super(aParent); @@ -62,9 +85,7 @@ public class SimplePromptPanel extends GlassPanel implements ActionListener, Gen @Override public void actionPerformed(ActionEvent aEvent) { - Object source; - - source = aEvent.getSource(); + var source = aEvent.getSource(); if (source == cancelB) { isAccepted = false; @@ -91,26 +112,22 @@ public class SimplePromptPanel extends GlassPanel implements ActionListener, Gen */ protected void buildGuiArea() { - Font aFont; - setLayout(new MigLayout("", "[right][grow][][]", "[][][20!][]")); - aFont = (new JTextField()).getFont(); + var tmpFont = (new JTextField()).getFont(); // Title Area titleL = new JLabel("Title", JLabel.CENTER); add(titleL, "growx,span,wrap"); // Message area - messageL = GuiUtil.createJLabel("Message", aFont); + messageL = GuiUtil.createJLabel("Message", tmpFont); add(messageL, "growx,span,wrap"); // Control area - cancelB = GuiUtil.createJButton("Cancel", this, aFont); - acceptB = GuiUtil.createJButton("Accept", this, aFont); + cancelB = GuiUtil.createJButton("Cancel", this); + acceptB = GuiUtil.createJButton("Accept", this); add(cancelB, "skip 2"); add(acceptB, ""); - - setBorder(new BevelBorder(BevelBorder.RAISED)); } } diff --git a/src/glum/gui/panel/generic/TextInputPanel.java b/src/glum/gui/panel/generic/TextInputPanel.java index e79c8d3..2d868a3 100644 --- a/src/glum/gui/panel/generic/TextInputPanel.java +++ b/src/glum/gui/panel/generic/TextInputPanel.java @@ -1,153 +1,168 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.generic; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import javax.swing.*; -import javax.swing.border.BevelBorder; -import glum.gui.FocusUtil; -import glum.gui.GuiUtil; -import glum.gui.action.ClickAction; -import glum.gui.component.GTextField; -import glum.gui.panel.GlassPanel; -import net.miginfocom.swing.MigLayout; +import java.awt.Component; +import java.util.Collection; +import java.util.regex.Pattern; -public abstract class TextInputPanel extends GlassPanel implements ActionListener, GenericCodes +import com.google.common.collect.ImmutableSet; + +/** + * UI component that provides the user with a simple prompt to allow for simple textual input. + *

    + * Support is provided for the following: + *

      + *
    • Checking for empty input. This is treated as an error. + *
    • Checking for input that matches reserved input. This is treated as an error. + *
    • Checking for input that matches utilized input. This is treated as a warning. + *
    + * + * @author lopeznr1 + */ +public class TextInputPanel extends BaseTextInputPanel { - // Constants - public static final Color warnColor = new Color(128, 0, 0); - - // GUI vars - protected JLabel titleL, inputL, infoL; - protected JButton cancelB, acceptB; - protected GTextField inputTF; - // State vars - protected boolean isAccepted; + private ImmutableSet reservedS; + private ImmutableSet utilizedS; + private Pattern matchPattern; + + private String issueBadInputMsg; + private String issueNoInputMsg; + private String issueReservedMsg; + private String issueUtilizedMsg; /** - * Constructor + * Standard Constructor */ - public TextInputPanel(Component aParent) + public TextInputPanel(Component aParent, String aTitle, String aNameLabel) { super(aParent); - - isAccepted = false; - // Build the actual GUI - buildGuiArea(); - setPreferredSize(new Dimension(300, getPreferredSize().height)); + // Set in a more specific title and input label + titleL.setText(aTitle); + inputL.setText(aNameLabel); - // Set up keyboard short cuts - FocusUtil.addAncestorKeyBinding(this, "ESCAPE", new ClickAction(cancelB)); - FocusUtil.addAncestorKeyBinding(this, "ENTER", new ClickAction(acceptB)); - FocusUtil.addFocusKeyBinding(inputTF, "ENTER", new ClickAction(acceptB)); - } - - /** - * Returns the input of the user. - */ - public String getInput() - { - if (isAccepted == false) - return null; - - return inputTF.getText(); - } + reservedS = ImmutableSet.of(); + utilizedS = ImmutableSet.of(); + matchPattern = null; - /** - * Sets in aStr as the default input - */ - public void setInput(String aStr) - { - inputTF.setValue(aStr); - } + issueNoInputMsg = "Specified input is invalid."; + issueBadInputMsg = "Please enter a valid name."; + issueReservedMsg = "Name is reserved. Please pick another."; + issueUtilizedMsg = "Name is in use. Item will be overwritten"; - @Override - public void actionPerformed(ActionEvent aEvent) - { - Object source; - - source = aEvent.getSource(); - if (source == cancelB) - { - isAccepted = false; - setVisible(false); - notifyListeners(this, ID_CANCEL, "Cancel"); - } - else if (source == acceptB) - { - isAccepted = true; - setVisible(false); - notifyListeners(this, ID_ACCEPT, "Accept"); - } - else if (source == inputTF) - { - updateGui(); - } - } - - @Override - public void setVisible(boolean aBool) - { - if (aBool == true) - isAccepted = false; -// resetGui(); - - super.setVisible(aBool); - } - - /** - * Forms the actual dialog GUI - */ - protected void buildGuiArea() - { - Font aFont; - String aStr; - - setLayout(new MigLayout("", "[right][grow][][]", "[][][20!][]")); - aFont = (new JTextField()).getFont(); - - // Title Area - titleL = new JLabel("Title", JLabel.CENTER); - add(titleL, "growx,span 4,wrap"); - - // Source area - inputL = new JLabel("Symbol:"); - inputTF = new GTextField(this); - add(inputL); - add(inputTF, "growx,span 3,wrap"); - - // Warn area - aStr = "Please enter text input."; - infoL = GuiUtil.createJLabel(aStr, aFont); - infoL.setForeground(warnColor); - add(infoL, "growx,span 4,wrap"); - - // Control area - cancelB = GuiUtil.createJButton("Cancel", this, aFont); - acceptB = GuiUtil.createJButton("Accept", this, aFont); - add(cancelB, "skip 2,span 1"); - add(acceptB, "span 1"); - - setBorder(new BevelBorder(BevelBorder.RAISED)); - } - - /** - * Sets the Gui and associated components to the initial state - */ - public void resetGui() - { - isAccepted = false; - inputTF.setText(""); updateGui(); } - + /** - * Utility method to update the various GUI components - * (most likely infoL, acceptB) based on the current - * inputTF. + * Simplified Constructor */ - protected abstract void updateGui(); + public TextInputPanel(Component aParent, String aTitle) + { + this(aParent, aTitle, "Name:"); + } + + /** + * Method that allows the user to customize the warning / error message associated with user input. + * + * @param aIssueBadInputMsg + * Message used when the user has entered bad input. + * @param aIssueNoInputMsg + * Message used when the user has not entered any input. + * @param aIssueReservedMsg + * Message used when the user has entered input that is reserved. + * @param aIssueUtilizedMsg + * Message used when the user has entered input that is utilized. + */ + public void setIssueMessages(String aIssueBadInputMsg, String aIssueNoInputMsg, String aIssueReservedMsg, + String aIssueUtilizedMsg) + { + if (aIssueBadInputMsg != null) + issueBadInputMsg = aIssueBadInputMsg; + if (aIssueNoInputMsg != null) + issueNoInputMsg = aIssueNoInputMsg; + if (aIssueReservedMsg != null) + issueReservedMsg = aIssueReservedMsg; + if (aIssueUtilizedMsg != null) + issueUtilizedMsg = aIssueUtilizedMsg; + + updateGui(); + } + + /** + * Sets in a regular expression for which to check for valid input. + *

    + * User specified input must match this regular expression. + *

    + * Passing in null will allow any user input to match. + */ + public void setMatchRegex(String aMatchRegex) + { + matchPattern = null; + if (aMatchRegex != null) + matchPattern = Pattern.compile(aMatchRegex); + + updateGui(); + } + + /** + * Sets in all of the names that have been reserved. + *

    + * Input matching items in this list will not be allowed. + */ + public void setReservedNames(Collection aReservedC) + { + reservedS = ImmutableSet.copyOf(aReservedC); + updateGui(); + } + + /** + * Sets in all of the names that have been utilized. + *

    + * Input matching items in this list will be allowed - but will result in a warning. + */ + public void setUtilizedNames(Collection aUtilizedC) + { + utilizedS = ImmutableSet.copyOf(aUtilizedC); + updateGui(); + } + + @Override + protected void updateGui() + { + String inputStr = inputTF.getText(); + + // Check for errors / warnings + String failMsg = null; + String warnMsg = null; + if (inputStr.equals("") == true) + failMsg = issueNoInputMsg; + else if (matchPattern != null && matchPattern.matcher(inputStr).matches() == false) + failMsg = issueBadInputMsg; + else if (reservedS.contains(inputStr) == true) + failMsg = issueReservedMsg; + else if (utilizedS.contains(inputStr) == true) + warnMsg = issueUtilizedMsg; + + String infoMsg = failMsg; + if (infoMsg == null) + infoMsg = warnMsg; + infoL.setText(infoMsg); + + boolean isEnabled = failMsg == null; + acceptB.setEnabled(isEnabled); + } } diff --git a/src/glum/gui/panel/itemList/BasicItemHandler.java b/src/glum/gui/panel/itemList/BasicItemHandler.java deleted file mode 100644 index ff5671f..0000000 --- a/src/glum/gui/panel/itemList/BasicItemHandler.java +++ /dev/null @@ -1,569 +0,0 @@ -package glum.gui.panel.itemList; - -import glum.gui.panel.itemList.query.QueryAttribute; -import glum.gui.panel.itemList.query.QueryComposer; -import glum.gui.panel.itemList.query.QueryTableCellRenderer; -import glum.unit.Unit; -import glum.unit.UnitListener; -import glum.unit.UnitProvider; -import glum.zio.*; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Map; - -import javax.swing.JTable; -import javax.swing.table.JTableHeader; -import javax.swing.table.TableCellRenderer; -import javax.swing.table.TableColumn; -import javax.swing.table.TableColumnModel; - -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; - -public abstract class BasicItemHandler implements ZioObj, ItemHandler, UnitListener -{ - protected JTable myOwner; - protected ArrayList fullAttributeList; - protected ArrayList sortedAttributeList; - - /** - * Constructor - */ - public BasicItemHandler(QueryComposer aComposer) - { - this(aComposer.getItems()); - } - - public BasicItemHandler(Collection aQueryAttrList) - { - int evalIndex; - - myOwner = null; - - fullAttributeList = new ArrayList(); - sortedAttributeList = new ArrayList(); - - if (aQueryAttrList != null && aQueryAttrList.isEmpty() == false) - { - evalIndex = 0; - for (QueryAttribute aAttr : aQueryAttrList) - { - // Ensure the model index is appropriately initialized - if (evalIndex != aAttr.modelIndex) - throw new RuntimeException("Improper initialization. Expected Index: " + evalIndex + " Received Index: " + aAttr.modelIndex); - - fullAttributeList.add(aAttr); - sortedAttributeList.add(aAttr); - evalIndex++; - - // Register for the appropriate unit events - aAttr.refUnitProvider.addListener(this); - } - } - - fullAttributeList.trimToSize(); - sortedAttributeList.trimToSize(); - } - - @Override - public void zioRead(ZinStream aStream) throws IOException - { - ArrayList newSortedList; - int numItems, index; - - // Header - aStream.readVersion(0); - - // Payload - ZioObjUtil.readList(aStream, fullAttributeList); - - // Reorder the sortedAttributeList based on the serialization - numItems = aStream.readInt(); - newSortedList = Lists.newArrayListWithCapacity(numItems); - for (int c1 = 0; c1 < numItems; c1++) - { - index = aStream.readInt(); - newSortedList.add(fullAttributeList.get(index)); - } - - sortedAttributeList = newSortedList; - sortedAttributeList.trimToSize(); - - // Initialize the table columns - rebuildTableColumns(); - } - - @Override - public void zioWrite(ZoutStream aStream) throws IOException - { - int numItems; - - // Header - aStream.writeVersion(0); - - // Payload - ZioObjUtil.writeList(aStream, fullAttributeList); - - // Output the order of the sortedAttributeList - numItems = sortedAttributeList.size(); - aStream.writeInt(numItems); - for (QueryAttribute aAttr : sortedAttributeList) - aStream.writeInt(aAttr.modelIndex); - } - - @Override - public void initialize(JTable aOwner) - { - JTableHeader aTableHeader; - TableColumnModel aTableColumnModel; - TableColumn aTableColumn; - - // This method is only allowed to be called once! - if (myOwner != null) - throw new RuntimeException("QueryItemHandler already initialized!"); - - myOwner = aOwner; - - aTableHeader = myOwner.getTableHeader(); - aTableColumnModel = aTableHeader.getColumnModel(); - - // Customize overall settings - aTableHeader.setReorderingAllowed(false); - - // Grab all of the precomputed columns from the table - // and store with their associated queryAttributes - for (int c1 = 0; c1 < fullAttributeList.size(); c1++) - { - aTableColumn = aTableColumnModel.getColumn(c1); - fullAttributeList.get(c1).assocTableColumn = aTableColumn; - } - - // Rebuild the table columns; Needed so that only the - // visible ones are displayed. Do this only after you - // have grabbed the table columns as aTableColumnModel - // will strictly contain the "visible" ones - rebuildTableColumns(); - } - - @Override - public int getColumnCount() - { - return fullAttributeList.size(); - } - - @Override - public Class getColumnClass(int colNum) - { - // Insanity check - if (colNum < 0 && colNum >= fullAttributeList.size()) - return String.class; - - return fullAttributeList.get(colNum).refClass; - } - - public int getColumnDefaultWidth(int colNum) - { - int defaultSize, minSize; - - // Insanity check - if (colNum < 0 && colNum >= fullAttributeList.size()) - return -1; - - // Get the default and min size - defaultSize = fullAttributeList.get(colNum).defaultSize; - minSize = fullAttributeList.get(colNum).minSize; - - // Ensure size makes sense - if (defaultSize < minSize) - return minSize; - - return defaultSize; - } - - /** - * getColumnMinWidth - */ -/* public int getColumnMinWidth(int colNum) - { - // Insanity check - if (queryAttributes == null) - return -1; - - if (colNum < 0 && colNum >= queryAttributes.length) - return -1; - - return queryAttributes[colNum].minSize; - } -*/ - /** - * getColumnMaxWidth - */ - public int getColumnMaxWidth(int colNum) - { - int defaultSize, maxSize; - - // Insanity check - if (colNum < 0 && colNum >= fullAttributeList.size()) - return -1; - - // Get the default and max size - defaultSize = fullAttributeList.get(colNum).defaultSize; - maxSize = fullAttributeList.get(colNum).maxSize; - - // Ensure size makes sense - if (defaultSize > maxSize && maxSize != -1) - return defaultSize; - - return maxSize; - } - - @Override - public String getColumnLabel(int colNum) - { - QueryAttribute aAttribute; - Unit aUnit; - String aStr; - - // Insanity check - if (colNum < 0 && colNum >= fullAttributeList.size()) - return ""; - - aAttribute = fullAttributeList.get(colNum); - - // Retrieve the associated unit - aUnit = aAttribute.refUnitProvider.getUnit(); - - // Retrieve the base column label - aStr = aAttribute.label; - - // Append the unit name to the column label - if (aUnit != null && "".equals(aUnit.getLabel(false)) == false) - return aStr + " [" + aUnit.getLabel(false) + "]"; - else - return aStr; - } - - @Override - public Collection getColumnLabels() - { - Collection retSet; - - if (fullAttributeList == null) - return new ArrayList(); - - retSet = new ArrayList(); - for (QueryAttribute aAttribute : fullAttributeList) - { - if (aAttribute != null && aAttribute.label != null) - retSet.add(aAttribute.label); - else - retSet.add(""); - } - - return retSet; - } - - @Override - public boolean isCellEditable(int colNum) - { - // Insanity check - if (colNum < 0 && colNum >= fullAttributeList.size()) - return false; - - if (fullAttributeList.get(colNum).editor == null) - return false; - - return true; - } - - @Override - public boolean isColumnVisible(int colNum) - { - // Insanity check - if (colNum < 0 && colNum >= fullAttributeList.size()) - return false; - - return fullAttributeList.get(colNum).isVisible; - } - - /** - * update - */ - public void update() - { - TableColumn aTableColumn; - QueryTableCellRenderer aRenderer; - Object aObject; - Unit aUnit; - - // Update all the TableColumn renderers with the appropriate unit - for (QueryAttribute aAttribute : fullAttributeList) - { - if (aAttribute.assocTableColumn != null) - { - aTableColumn = aAttribute.assocTableColumn; - aObject = aTableColumn.getCellRenderer(); - if (aObject instanceof QueryTableCellRenderer) - { - aRenderer = (QueryTableCellRenderer)aObject; - - aAttribute.assocTableColumn.setHeaderValue( getColumnLabel(aAttribute.modelIndex) ); - - aUnit = aAttribute.refUnitProvider.getUnit(); - aRenderer.setUnit(aUnit); - } - } - } - } - - @Override - public void unitChanged(UnitProvider aManager, String aKey) - { - JTableHeader aTableHeader; - - update(); - - myOwner.repaint(); - - aTableHeader = myOwner.getTableHeader(); - if (aTableHeader != null) - aTableHeader.repaint(); -/* - for (QueryAttribute aAttribute : queryAttributes) - { - if (aKey.equals(aAttribute.unitKey) == true) - } - - Tile aTile; - - // Update our listPanel to by sync with the active tile - if (aKey.equals("tile.active") == true) - { - aTile = refRegistry.getSingleton(aKey, Tile.class); - - listPanel.removeListSelectionListener(this); - listPanel.selectItem(aTile); - listPanel.addListSelectionListener(this); - } - - updateGui(); -*/ - } - - - -public Unit getUnit(int colNum) -{ - QueryAttribute aAttribute; - - // Insanity check - if (colNum < 0 && colNum >= fullAttributeList.size()) - return null; - - aAttribute = fullAttributeList.get(colNum); - return aAttribute.refUnitProvider.getUnit(); -} - - -public void setColumnAlignment(int colNum, int aAlignment) -{ - // Insanity check - if (colNum < 0 && colNum >= fullAttributeList.size()) - return; - - fullAttributeList.get(colNum).alignment = aAlignment; -} - -public void setColumnLabel(int colNum, String aLabel) -{ - // Insanity check - if (colNum < 0 && colNum >= fullAttributeList.size()) - return; - - fullAttributeList.get(colNum).label = aLabel; -} - -public void setColumnPosition(int colNum, int aPosition) -{ - // Insanity check - if (colNum < 0 || aPosition < 0 - || colNum >= fullAttributeList.size() - || aPosition >= fullAttributeList.size()) - return; - - sortedAttributeList.remove(fullAttributeList.get(colNum)); - sortedAttributeList.add(aPosition, fullAttributeList.get(colNum)); -} - - -public void setColumnSize(int colNum, int aSize) -{ - // Insanity check - if (colNum < 0 && colNum >= fullAttributeList.size()) - return; - -System.out.println("[QueryItemHandler.java] Changing size of colNum: " + aSize); - fullAttributeList.get(colNum).defaultSize = aSize; -} - - -public void setColumnSortDir(int colNum, int aSortDir) -{ - // Insanity check - if (colNum < 0 && colNum >= fullAttributeList.size()) - return; - - fullAttributeList.get(colNum).sortDir = aSortDir; -} - - -public void setColumnVisible(int colNum, boolean isVisible) -{ - // Insanity check - if (colNum < 0 && colNum >= fullAttributeList.size()) - return; - - fullAttributeList.get(colNum).isVisible = isVisible; - - // Update our table - if (myOwner != null) - rebuildTableColumns(); -} - - - - - - - - - - /** - * getSortedAttributes - */ - public ArrayList getSortedAttributes() - { - return new ArrayList(sortedAttributeList); - } - - - /** - * moveSortedAttribute - */ - public void moveSortedAttribute(int currIndex, int newIndex) - { - QueryAttribute aItem; - - aItem = sortedAttributeList.get(currIndex); - sortedAttributeList.remove(currIndex); - sortedAttributeList.add(newIndex, aItem); - } - - /** - * Method to reconfigure the columns of the table. This will update the - * display order of the columns as well as the individual relevant - * attributes of the column. Currently supported relevant attributes are - * those defined in the method {@link QueryAttribute#setConfig}. - * - * Note any non specified columns will appear last according to the previous order. - * - * @param orderSet: Ordered set of QueryAttributes with matching modelIndexes - */ - public void setOrderAndConfig(Collection orderArr) - { - Map itemMap; - QueryAttribute workItem; - - // Form a lookup map (modelIndex to attribute) - itemMap = Maps.newLinkedHashMap(); - for (QueryAttribute aItem : sortedAttributeList) - itemMap.put(aItem.modelIndex, aItem); - - // Rebuild the sortedQueryAttribute list to conform with - // - the specified order of orderList - // - synch up relevant attributes - sortedAttributeList.clear(); - for (QueryAttribute aItem: orderArr) - { - workItem = itemMap.remove(aItem.modelIndex); - if (workItem != null) - { - workItem.setConfig(aItem); - sortedAttributeList.add(workItem); - } - } - - sortedAttributeList.addAll(itemMap.values()); - itemMap.clear(); - } - - /** - * initializeTableColumn - Helper method to initialize aTableColumn - * with the actual QueryAttribute properties. - * - * @param colNum: Index into column model - */ - protected void initializeTableColumn(int colNum) - { - TableCellRenderer aRenderer; - QueryAttribute aAttribute; - TableColumn aTableColumn; - String aLabel; - int defaultWidth, maxWidth, minWidth; - - // Insanity check - if (colNum < 0 && colNum >= fullAttributeList.size()) - return; - - // Get the associated table column - aAttribute = fullAttributeList.get(colNum); - aTableColumn = aAttribute.assocTableColumn; - if (aTableColumn == null) - return; - - // Retrieve settings of interest - aLabel = getColumnLabel(colNum); - defaultWidth = getColumnDefaultWidth(colNum); - maxWidth = getColumnMaxWidth(colNum); - minWidth = aAttribute.minSize; - - // Set up the column's renderer - aRenderer = aAttribute.renderer; - if (aRenderer == null) - aRenderer = new QueryTableCellRenderer(aAttribute); - aTableColumn.setCellRenderer(aRenderer); - - // Set up the column's editor - aTableColumn.setCellEditor(aAttribute.editor); - - // Set up the column's size attributes - aTableColumn.setMinWidth(minWidth); - aTableColumn.setMaxWidth(maxWidth); - aTableColumn.setPreferredWidth(defaultWidth); - - // Set up the column header - aTableColumn.setHeaderValue(aLabel); - } - -// TODO -> This should probably be protected - public void rebuildTableColumns() - { - // Enforce the constraints (QueryAttribute) on the associated columns - for (int c1 = 0; c1 < fullAttributeList.size(); c1++) - initializeTableColumn(c1); - - // Remove all of the columns from the table - for (QueryAttribute aAttribute : fullAttributeList) - myOwner.removeColumn(aAttribute.assocTableColumn); - - // Add in only the columns that are visible - for (QueryAttribute aAttribute : sortedAttributeList) - { - if (aAttribute.isVisible == true) - myOwner.addColumn(aAttribute.assocTableColumn); - } - } - -} diff --git a/src/glum/gui/panel/itemList/BasicItemProcessor.java b/src/glum/gui/panel/itemList/BasicItemProcessor.java index d93441d..c430a61 100644 --- a/src/glum/gui/panel/itemList/BasicItemProcessor.java +++ b/src/glum/gui/panel/itemList/BasicItemProcessor.java @@ -1,26 +1,46 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.itemList; import java.util.*; +import glum.item.*; + +/** + * Base implementation of the interface {@link ItemProcessor}. + * + * @author lopeznr1 + */ public abstract class BasicItemProcessor implements ItemProcessor { - private Collection myListeners; + private final List listenerL; public BasicItemProcessor() { - myListeners = new ArrayList(); + listenerL = new ArrayList<>(); } @Override - public synchronized void addItemChangeListener(ItemChangeListener aListener) + public synchronized void addListener(ItemEventListener aListener) { - myListeners.add(aListener); + listenerL.add(aListener); } @Override - public synchronized void removeItemChangeListener(ItemChangeListener aListener) + public synchronized void delListener(ItemEventListener aListener) { - myListeners.remove(aListener); + listenerL.remove(aListener); } /** @@ -28,17 +48,16 @@ public abstract class BasicItemProcessor implements ItemProcessor */ protected void notifyListeners() { - Collection notifySet; - // Get the listeners - synchronized(this) + Collection tmpListenerL; + synchronized (this) { - notifySet = new ArrayList(myListeners); + tmpListenerL = new ArrayList<>(listenerL); } // Send out the notifications - for (ItemChangeListener aListener : notifySet) - aListener.itemChanged(); + for (ItemEventListener aListener : tmpListenerL) + aListener.handleItemEvent(this, ItemEventType.ItemsChanged); } } diff --git a/src/glum/gui/panel/itemList/FilterItemProcessor.java b/src/glum/gui/panel/itemList/FilterItemProcessor.java index 9977431..a4c9009 100644 --- a/src/glum/gui/panel/itemList/FilterItemProcessor.java +++ b/src/glum/gui/panel/itemList/FilterItemProcessor.java @@ -1,26 +1,45 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.itemList; import java.util.ArrayList; import java.util.Collection; -import com.google.common.collect.Lists; +import com.google.common.collect.ImmutableList; import glum.filter.Filter; import glum.filter.NullFilter; +/** + * Implementation of {@link ItemProcessor} which provides automatic filtering. + *

    + * Only items that pass the specified activeFilter will be returned. + * + * @author lopeznr1 + */ public class FilterItemProcessor extends BasicItemProcessor { // State vars - private ArrayList fullList; - private ArrayList passList; + private ImmutableList fullItemL; + private ImmutableList passItemL; private Filter activeFilter; + /** Standard Constructor */ public FilterItemProcessor() { - super(); - - fullList = Lists.newArrayList(); - passList = Lists.newArrayList(); + fullItemL = ImmutableList.of(); + passItemL = ImmutableList.of(); activeFilter = new NullFilter(); } @@ -51,9 +70,9 @@ public class FilterItemProcessor extends BasicItemProcessor * Replaces the current full list of items stored with aItemList. Note that the number of items available by this * processor may be less than the number of items in aItemList due to the active filter. */ - public void setItems(Collection aItemList) + public void setItems(Collection aItemC) { - fullList = new ArrayList(aItemList); + fullItemL = ImmutableList.copyOf(aItemC); rebuildPassList(); // Notify our listeners @@ -63,13 +82,13 @@ public class FilterItemProcessor extends BasicItemProcessor @Override public int getNumItems() { - return passList.size(); + return passItemL.size(); } @Override - public Collection getItems() + public ImmutableList getAllItems() { - return Lists.newArrayList(passList); + return passItemL; } /** @@ -78,15 +97,14 @@ public class FilterItemProcessor extends BasicItemProcessor */ private void rebuildPassList() { - passList = Lists.newArrayList(); - - for (G1 aItem : fullList) + var tmpItemL = new ArrayList(); + for (var aItem : fullItemL) { if (activeFilter == null || activeFilter.isValid(aItem) == true) - passList.add(aItem); + tmpItemL.add(aItem); } - passList.trimToSize(); + passItemL = ImmutableList.copyOf(tmpItemL); } } diff --git a/src/glum/gui/panel/itemList/ItemChangeListener.java b/src/glum/gui/panel/itemList/ItemChangeListener.java deleted file mode 100644 index e8a90a4..0000000 --- a/src/glum/gui/panel/itemList/ItemChangeListener.java +++ /dev/null @@ -1,10 +0,0 @@ -package glum.gui.panel.itemList; - -public interface ItemChangeListener -{ - /** - * ItemChangeListener interface methods - */ - public void itemChanged(); - -} diff --git a/src/glum/gui/panel/itemList/ItemHandler.java b/src/glum/gui/panel/itemList/ItemHandler.java index 8588bdc..18fbd75 100644 --- a/src/glum/gui/panel/itemList/ItemHandler.java +++ b/src/glum/gui/panel/itemList/ItemHandler.java @@ -1,31 +1,45 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.itemList; -import java.util.*; - -import javax.swing.*; - -public interface ItemHandler +/** + * Interface which allows standardized mechanism for getting and setting of values associated with an item. + * + * @param + * Generic associated with the item. + * @param + * Generic enum that defines the "names" of values to be accessed (get / set). + * + * @author lopeznr1 + */ +public interface ItemHandler> { /** - * ItemHandler interface methods - */ - public int getColumnCount(); - public Class getColumnClass(int colNum); - public String getColumnLabel(int colNum); - public Collection getColumnLabels(); - public Object getColumnValue(G1 aItem, int colNum); - public void setColumnValue(G1 aItem, int colNum, Object aValue); - public boolean isCellEditable(int colNum); - public boolean isColumnVisible(int colNum); + * Returns the appropriate data field within the item for the specified lookup enum. + * + * @param aItem + * @param aEnum + */ + public Object getValue(G1 aItem, G2 aEnum); /** - * Notifies the ItemHandler of its associated JTable This table should be updated or painted whenever - * the internals of this ItemHandler change + * Updates the appropriate data field within the item for the specified lookup enum (with the provided value). + * + * @param aItem + * @param aEnum + * @param aValue */ - public void initialize(JTable aOwner); - - /** - * Notifies the ItemHandler to synchronize the items to match the state of the associated Columns - */ -// public void synchItems(); + public void setValue(G1 aItem, G2 aEnum, Object aValue); + } diff --git a/src/glum/gui/panel/itemList/ItemListPanel.java b/src/glum/gui/panel/itemList/ItemListPanel.java index 2c0df06..d958ed0 100644 --- a/src/glum/gui/panel/itemList/ItemListPanel.java +++ b/src/glum/gui/panel/itemList/ItemListPanel.java @@ -1,69 +1,110 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.itemList; -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Graphics; -import java.awt.event.*; -import java.util.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + import javax.swing.*; -import javax.swing.event.*; -import javax.swing.table.*; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.table.TableColumn; +import glum.gui.GuiExeUtil; +import glum.gui.TableUtil; import glum.gui.component.GComboBox; -import glum.gui.table.*; - -import com.google.common.collect.*; +import glum.gui.panel.itemList.query.QueryComposer; +import glum.gui.table.JTableScrolling; +import glum.gui.table.TableSorter; +import glum.item.*; import net.miginfocom.swing.MigLayout; -public class ItemListPanel extends JPanel implements ActionListener, ListSelectionListener, ItemChangeListener +/** + * UI component that will display a list of items as provided by the specified {@link ItemHandler} and + * {@link ItemProcessor}. + * + * @author lopeznr1 + */ +public class ItemListPanel> extends JPanel + implements ActionListener, ItemEventListener, ListSelectionListener { - // Gui components - protected JTable myTable; - protected JScrollPane tableScrollPane; - protected ItemListTableModel myTableModel; - protected TableSorter sortTableModel; - protected GComboBox searchBox; - protected JTextField searchTF; + // Ref vars + private final ItemProcessor refItemProcessor; + + // Gui vars + private JTable myTable; + private JScrollPane tableScrollPane; + private TableSorter sortTableModel; + private TableColumnHandler workTableColumnHandler; + private ItemListTableModel viewTableModel; + private GComboBox searchBox; + private JTextField searchTF; // State vars - protected ItemHandler myItemHandler; - protected ItemProcessor myItemProcessor; - protected boolean updateNeeded; + private List listenerL; + private boolean updateNeeded; - // Communicator vars - protected List myListeners; - - public ItemListPanel(ItemHandler aItemHandler, ItemProcessor aItemProcessor, boolean hasSearchBox, boolean supportsMultipleSelection) + /** Standard Constructor */ + public ItemListPanel(ItemHandler aItemHandler, ItemProcessor aItemProcessor, QueryComposer aComposer, + boolean aSupportsMultipleSelection) { - // State vars - myItemHandler = aItemHandler; - myItemProcessor = aItemProcessor; + // Delegate + this(aItemHandler, aItemProcessor, aComposer, false, aSupportsMultipleSelection); + } + + private ItemListPanel(ItemHandler aItemHandler, ItemProcessor aItemProcessor, + QueryComposer aComposer, boolean aHasSearchBox, boolean aSupportsMultipleSelection) + { + refItemProcessor = aItemProcessor; + + listenerL = new ArrayList<>(); updateNeeded = true; - // Communicator vars - myListeners = Lists.newLinkedList(); + // Form the gui + buildGuiArea(aItemHandler, aComposer, aHasSearchBox, aSupportsMultipleSelection); + GuiExeUtil.executeOnceWhenShowing(myTable, () -> updateTable()); - // Build the actual GUI - buildGuiArea(hasSearchBox, supportsMultipleSelection); - - // Register for DataChange events and trigger the initial one - myItemProcessor.addItemChangeListener(this); + // Register for events of interest + refItemProcessor.addListener(this); } /** - * addListSelectionListener + * Registers a ListSelectionListener with this ItemListPanel. */ public synchronized void addListSelectionListener(ListSelectionListener aListener) { - myListeners.add(aListener); + listenerL.add(aListener); } /** - * removeListSelectionListener + * Deregisters a ListSelectionListener with this ItemListPanel. */ - public synchronized void removeListSelectionListener(ListSelectionListener aListener) + public synchronized void delListSelectionListener(ListSelectionListener aListener) { - myListeners.remove(aListener); + listenerL.remove(aListener); + } + + /** + * Returns the backing {@link TableColumnHandler}. + */ + public TableColumnHandler getTableColumnHandler() + { + return workTableColumnHandler; } /** @@ -75,35 +116,17 @@ public class ItemListPanel extends JPanel implements ActionListener, ListSel myTable.getActionMap().put(aAction, aAction); } - @Override - public void actionPerformed(ActionEvent e) - { - Object source; - TableColumn selectedItem; - - // Determine the source - source = e.getSource(); - - if (source == searchTF || source == searchBox) - { - selectedItem = searchBox.getChosenItem(); - selectNextItem(selectedItem, searchTF.getText()); - } - } - /** * Returns the object located at the specified (view) row */ public synchronized G1 getItem(int aRow) { - G1 aObj; - aRow = sortTableModel.modelIndex(aRow); if (aRow == -1) return null; - aObj = myTableModel.getRowItem(aRow); - return aObj; + var retObj = viewTableModel.getRowItem(aRow); + return retObj; } /** @@ -111,13 +134,10 @@ public class ItemListPanel extends JPanel implements ActionListener, ListSel */ public synchronized G1 getSelectedItem() { - G1 selectedObj; - int selectedRow; - // Ensure the table is up to date updateTable(); - selectedRow = myTable.getSelectedRow(); + int selectedRow = myTable.getSelectedRow(); if (selectedRow == -1) return null; @@ -125,67 +145,40 @@ public class ItemListPanel extends JPanel implements ActionListener, ListSel if (selectedRow == -1) return null; - selectedObj = myTableModel.getRowItem(selectedRow); + G1 selectedObj = viewTableModel.getRowItem(selectedRow); return selectedObj; } /** - * getSelectedItems - Returns the list of selected items from the table + * Returns the list of selected items from the table */ public synchronized List getSelectedItems() { - List aList; - G1 selectedObj; - int[] selectedRows; - int selectedRow; - // Ensure the table is up to date updateTable(); - aList = Lists.newLinkedList(); - selectedRows = myTable.getSelectedRows(); - if (selectedRows != null) + // Transform from rows to items + var retItemL = new ArrayList(); + int[] idxArr = myTable.getSelectedRows(); + for (int aIdx : idxArr) { - for (int aInt : selectedRows) + int tmpIdx = sortTableModel.modelIndex(aIdx); + if (tmpIdx != -1) { - selectedRow = sortTableModel.modelIndex(aInt); - if (selectedRow != -1) - { - selectedObj = myTableModel.getRowItem(selectedRow); - aList.add(selectedObj); - } + G1 selectedObj = viewTableModel.getRowItem(tmpIdx); + retItemL.add(selectedObj); } } - return aList; - } - - @Override - public void itemChanged() - { - // Mark the table as being outdated - updateNeeded = true; - - // The advantage to the code below (as opposed to SwingUtilities.invokeLater() style) is - // that it is only updated when it absolutely is necessary. Thus if multiple updates come - // in before it is repainted they will be ignored. In the future this method may have an - // argument called isLazy which allow both styles of updating. - repaint(); + return retItemL; } /** - * This may be triggered indirectly via a network call after the method repaint() has been called. Do not call this - * method from a non gui thread + * Returns the associated SortTableModel */ - @Override - public void paint(Graphics g) + public TableSorter getSortTableModel() { - // Ensure the table is up to date - updateTable(); - - // Do the actual paint - if (g != null) - super.paint(g); + return sortTableModel; } /** @@ -195,8 +188,6 @@ public class ItemListPanel extends JPanel implements ActionListener, ListSel */ public synchronized void selectItem(G1 aObj) { - int chosenRow; - // Ensure we are executed only on the proper thread if (SwingUtilities.isEventDispatchThread() == false) throw new RuntimeException("ItemListPanel.selectItem() not executed on the AWT event dispatch thread."); @@ -214,7 +205,7 @@ public class ItemListPanel extends JPanel implements ActionListener, ListSel } else { - chosenRow = myTableModel.getRowIndex(aObj); + int chosenRow = viewTableModel.getRowIndex(aObj); if (chosenRow != -1) { chosenRow = sortTableModel.viewIndex(chosenRow); @@ -233,6 +224,23 @@ public class ItemListPanel extends JPanel implements ActionListener, ListSel myTable.repaint(); } + /** + * Ensures that the specified item is within the view. + */ + public synchronized void scrollToItem(G1 aItem) + { + // Transform from item to row index (view) + int tmpRow = viewTableModel.getRowIndex(aItem); + if (tmpRow == -1) + return; + + tmpRow = sortTableModel.viewIndex(tmpRow); + + // Ensure the row is in the view + if (JTableScrolling.isRowVisible(myTable, tmpRow) == false) + JTableScrolling.centerRow(myTable, tmpRow); + } + /** * Sets the table bodies color to aColor */ @@ -241,6 +249,22 @@ public class ItemListPanel extends JPanel implements ActionListener, ListSel tableScrollPane.getViewport().setBackground(aColor); } + /** + * Sets in the Comparator that will be used to sort the specified column. + */ + public void setSortComparator(int aColNum, Comparator aComparator) + { + sortTableModel.setColumnIndexComparator(aColNum, aComparator); + } + + /** + * Sets in the Comparator that will be used to sort items of type aType. + */ + public void setSortComparator(Class aType, Comparator aComparator) + { + sortTableModel.setColumnClassComparator(aType, aComparator); + } + /** * Sets whether the table can be sorted */ @@ -249,6 +273,54 @@ public class ItemListPanel extends JPanel implements ActionListener, ListSel sortTableModel.setSortingEnabled(aBool); } + @Override + public void actionPerformed(ActionEvent aEvent) + { + // Determine the source + var source = aEvent.getSource(); + if (source == searchTF || source == searchBox) + { + TableColumn selectedItem = searchBox.getChosenItem(); + selectNextItem(selectedItem, searchTF.getText()); + } + } + + @Override + public void handleItemEvent(Object aSource, ItemEventType aEventType) + { + // Time to update our selected items + if (aEventType == ItemEventType.ItemsSelected) + updateTableSelection(); + // Nothing to do just a repaint is needed + else if (aEventType == ItemEventType.ItemsMutated) + myTable.repaint(); + // Mark the table as being outdated + else if (aEventType == ItemEventType.ItemsChanged) + updateNeeded = true; + + // The advantage to the code below (as opposed to SwingUtilities.invokeLater() + // style) is that it is only updated when it absolutely is necessary. Thus if + // multiple updates come in before it is repainted they will be ignored. In + // the future this method may have an argument called isLazy which allow both + // styles of updating. + repaint(); + } + + /** + * This may be triggered indirectly via a network call after the method repaint() has been called. Do not call this + * method from a non gui thread + */ + @Override + public void paint(Graphics g) + { + // Ensure the table is up to date + updateTable(); + + // Do the actual paint + if (g != null) + super.paint(g); + } + @Override public void setEnabled(boolean aBool) { @@ -258,6 +330,20 @@ public class ItemListPanel extends JPanel implements ActionListener, ListSel @Override public void valueChanged(ListSelectionEvent aEvent) { + if (refItemProcessor instanceof ItemManager == false) + { + notifyListeners(aEvent); + return; + } + + // TODO: All ItemProcessor may go away and we interface with ItemManager + var tmpManager = (ItemManager) refItemProcessor; + + // Update the ItemManager's selection + tmpManager.delListener(this); + tmpManager.setSelectedItems(getSelectedItems()); + tmpManager.addListener(this); + notifyListeners(aEvent); } @@ -272,51 +358,50 @@ public class ItemListPanel extends JPanel implements ActionListener, ListSel /** * Forms the actual panel GUI */ - private void buildGuiArea(boolean hasSearchBox, boolean supportsMultipleSelection) + private void buildGuiArea(ItemHandler aItemHandler, QueryComposer aComposer, boolean aHasSearchBox, + boolean aSupportsMultipleSelection) { - JLabel tmpL; - - if (hasSearchBox == true) + if (aHasSearchBox == true) setLayout(new MigLayout("", "0[][][grow]0", "0[grow][]0")); else setLayout(new MigLayout("", "0[][][grow]0", "0[grow]0")); // Form the table - myTableModel = new ItemListTableModel(myItemHandler); - sortTableModel = new TableSorter(myTableModel); + workTableColumnHandler = new TableColumnHandler<>(aComposer.getItems()); + viewTableModel = new ItemListTableModel<>(aItemHandler, workTableColumnHandler); + sortTableModel = new TableSorter(viewTableModel); myTable = new JTable(sortTableModel); sortTableModel.setTableHeader(myTable.getTableHeader()); -//! sortTableModel.setSortingStatus(0, 1); myTable.setBackground(null); myTable.getSelectionModel().addListSelectionListener(this); - if (supportsMultipleSelection == false) + if (aSupportsMultipleSelection == false) myTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION); else myTable.getSelectionModel().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); - // Notify the ItemHandler of its associated table and initialize the table - myItemHandler.initialize(myTable); + // Notify the TableModel of the associated table (and initialize the table) + viewTableModel.initialize(myTable); // Create the scroll pane and add the table to it. tableScrollPane = new JScrollPane(myTable); - add(tableScrollPane, "growx,growy,span 3"); + add(tableScrollPane, "growx,growy,span"); // The search section searchBox = null; searchTF = null; - if (hasSearchBox == true) + if (aHasSearchBox == true) { - tmpL = new JLabel("Find:"); - add(tmpL, "newline,span 1"); + var tmpL = new JLabel("Find:"); + add(tmpL, "newline"); searchBox = new GComboBox(this, new SearchBoxRenderer()); searchBox.setMaximumSize(searchBox.getPreferredSize()); - add(searchBox, "span 1"); + add(searchBox, ""); searchTF = new JTextField(""); searchTF.addActionListener(this); - add(searchTF, "growx,span 1"); + add(searchTF, "growx,span"); // Set in the preferred font tmpL.setFont(searchTF.getFont()); @@ -332,17 +417,16 @@ public class ItemListPanel extends JPanel implements ActionListener, ListSel */ protected void notifyListeners(ListSelectionEvent aEvent) { - List tmpList; - ListSelectionEvent tmpEvent; - - synchronized(this) + List tmpL; + synchronized (this) { - tmpList = Lists.newArrayList(myListeners); + tmpL = new ArrayList<>(listenerL); } // Notify our listeners - tmpEvent = new ListSelectionEvent(this, aEvent.getFirstIndex(), aEvent.getLastIndex(), aEvent.getValueIsAdjusting()); - for (ListSelectionListener aListener : tmpList) + var tmpEvent = new ListSelectionEvent(this, aEvent.getFirstIndex(), aEvent.getLastIndex(), + aEvent.getValueIsAdjusting()); + for (ListSelectionListener aListener : tmpL) aListener.valueChanged(tmpEvent); } @@ -351,47 +435,42 @@ public class ItemListPanel extends JPanel implements ActionListener, ListSel */ protected synchronized void rebuildItemList() { - Collection itemList; - Collection selectedObjSet; - int[] selectedRows; - int aRow; - // Insanity check - if (myItemProcessor == null) + if (refItemProcessor == null) return; // Get the old selected items - selectedObjSet = new LinkedList(); - selectedRows = myTable.getSelectedRows(); - if (selectedRows != null) + var selectedL = new ArrayList(); + int[] idxArr = myTable.getSelectedRows(); + for (int aInt : idxArr) { - for (int aInt : selectedRows) - { - aRow = sortTableModel.modelIndex(aInt); - selectedObjSet.add(myTableModel.getRowItem(aRow)); - } + int tmpIdx = sortTableModel.modelIndex(aInt); + selectedL.add(viewTableModel.getRowItem(tmpIdx)); } // Suspend listening to selection change events myTable.getSelectionModel().removeListSelectionListener(this); // Update our table with the new set of items - itemList = myItemProcessor.getItems(); - myTableModel.clear(); - myTableModel.addItems(itemList); + var itemL = refItemProcessor.getAllItems(); + viewTableModel.clear(); + viewTableModel.addItems(itemL); - // Reselect the old selected items - myTable.getSelectionModel().clearSelection(); - for (G1 aObj : selectedObjSet) + // Determine the row indexes to be selected + var tmpRowL = new ArrayList(); + for (G1 aObj : selectedL) { - aRow = myTableModel.getRowIndex(aObj); - if (aRow != -1) - { - aRow = sortTableModel.viewIndex(aRow); - myTable.getSelectionModel().addSelectionInterval(aRow, aRow); - } + int tmpRow = viewTableModel.getRowIndex(aObj); + if (tmpRow == -1) + continue; + + tmpRow = sortTableModel.viewIndex(tmpRow); + tmpRowL.add(tmpRow); } + // Select the appropriate rows + TableUtil.setSelection(myTable, null, tmpRowL); + // Restore listening to selection change events myTable.getSelectionModel().addListSelectionListener(this); } @@ -401,21 +480,27 @@ public class ItemListPanel extends JPanel implements ActionListener, ListSel */ protected synchronized void rebuildSearchBox() { - TableColumn selectedItem; - int numCols; - // Insanity check if (searchBox == null) return; // Save off the currently selected object - selectedItem = searchBox.getChosenItem(); + var selectedItem = searchBox.getChosenItem(); // Reconstitude searchBox - numCols = myTable.getColumnCount(); + var numCols = myTable.getColumnCount(); searchBox.removeAllItems(); for (int c1 = 0; c1 < numCols; c1++) - searchBox.addItem(myTable.getTableHeader().getColumnModel().getColumn(c1)); + { + TableColumn tmpTC = myTable.getTableHeader().getColumnModel().getColumn(c1); + + // Allow items to be searched if derived from JLabel + // TODO: In the future this should have a searchable flag rather + // this hack that checks if this is a child of JLabel + var tmpTCR = tmpTC.getCellRenderer(); + if (tmpTCR instanceof JLabel) + searchBox.addItem(tmpTC); + } // Set up the searchBox to appropriate state searchBox.removeActionListener(this); @@ -429,97 +514,88 @@ public class ItemListPanel extends JPanel implements ActionListener, ListSel /** * Utility to locate the next item to be selected, and change the section to that such item */ - protected synchronized void selectNextItem(TableColumn aTableColumn, String searchStr) + protected synchronized void selectNextItem(TableColumn aTableColumn, String aSearchStr) { - TableCellRenderer aRenderer; - JLabel tmpL; - Object aObj; - String currStr; - int colNum, chosenRow, startRow, numRows; - int cX; - boolean hardMatchRequired; - - if (aTableColumn == null || searchStr == null) + if (aTableColumn == null || aSearchStr == null) return; // Retrieve the model index and table renderer - aRenderer = aTableColumn.getCellRenderer(); - colNum = aTableColumn.getModelIndex(); + var aRenderer = aTableColumn.getCellRenderer(); + var colNum = aTableColumn.getModelIndex(); // Is a hard match required - hardMatchRequired = false; - if (searchStr.endsWith(" ") == true) + var hardMatchRequired = false; + if (aSearchStr.endsWith(" ") == true) { hardMatchRequired = true; - searchStr = searchStr.substring(0, searchStr.length() - 1); + aSearchStr = aSearchStr.substring(0, aSearchStr.length() - 1); } - startRow = myTable.getSelectedRow(); + var startRow = myTable.getSelectedRow(); if (startRow == -1) startRow = 0; else startRow++; - numRows = sortTableModel.getRowCount(); + var numRows = sortTableModel.getRowCount(); if (numRows == 0) return; // Search lower half of table - chosenRow = -1; - for (cX = startRow; cX < numRows; cX++) + var chosenRow = -1; + for (var cX = startRow; cX < numRows; cX++) { - aObj = sortTableModel.getValueAt(cX, colNum); - if (aObj != null) - { + var tmpObj = sortTableModel.getValueAt(cX, colNum); + if (tmpObj == null) + continue; - tmpL = (JLabel)aRenderer.getTableCellRendererComponent(myTable, aObj, false, false, cX, colNum); - currStr = tmpL.getText(); - if (currStr != null) + var tmpL = (JLabel) aRenderer.getTableCellRendererComponent(myTable, tmpObj, false, false, cX, colNum); + var currStr = tmpL.getText(); + if (currStr == null) + continue; + + if (hardMatchRequired == true) + { + if (currStr.equals(aSearchStr) == true) { - if (hardMatchRequired == true) - { - if (currStr.equals(searchStr) == true) - { - chosenRow = cX; - break; - } - } - else if (currStr.startsWith(searchStr) == true) - { - chosenRow = cX; - break; - } + chosenRow = cX; + break; } } + else if (currStr.contains(aSearchStr) == true) + { + chosenRow = cX; + break; + } } // Search upper half of table if (chosenRow == -1) { - for (cX = 0; cX < startRow; cX++) + for (var cX = 0; cX < startRow; cX++) { - aObj = sortTableModel.getValueAt(cX, colNum); - if (aObj != null) + var tmpObj = sortTableModel.getValueAt(cX, colNum); + if (tmpObj == null) + continue; + + var tmpL = (JLabel) aRenderer.getTableCellRendererComponent(myTable, tmpObj, false, false, cX, colNum); + var currStr = tmpL.getText(); + if (currStr == null) + continue; + + if (hardMatchRequired == true) { - tmpL = (JLabel)aRenderer.getTableCellRendererComponent(myTable, aObj, false, false, cX, colNum); - currStr = tmpL.getText(); - if (currStr != null) + if (currStr.equals(aSearchStr) == true) { - if (hardMatchRequired == true) - { - if (currStr.equals(searchStr) == true) - { - chosenRow = cX; - break; - } - } - else if (currStr.startsWith(searchStr) == true) - { - chosenRow = cX; - break; - } + chosenRow = cX; + break; } } + else if (currStr.contains(aSearchStr) == true) + { + chosenRow = cX; + break; + } } } @@ -542,8 +618,6 @@ public class ItemListPanel extends JPanel implements ActionListener, ListSel protected synchronized void updateTable() { // Bail if an update is no longer needed. - // If an update was originally scheduled, this will be false - // due to coalesion. if (updateNeeded == false) return; //System.out.println("ItemListPanel.updateTable() Addr:" + this.hashCode()); @@ -560,4 +634,18 @@ public class ItemListPanel extends JPanel implements ActionListener, ListSel updateNeeded = false; } + /** + * Helper method to update the table selection to match the state of the ItemManager. + *

    + * TODO: In the future this method may go away ItemProcessor and ItemManager are merged. + */ + private void updateTableSelection() + { + // Ensure the table is up to date + updateTable(); + + if (refItemProcessor instanceof ItemManager) + TableUtil.updateTableSelection(this, (ItemManager) refItemProcessor, myTable, sortTableModel); + } + } diff --git a/src/glum/gui/panel/itemList/ItemListTableModel.java b/src/glum/gui/panel/itemList/ItemListTableModel.java index 46a9aef..2a19ce6 100644 --- a/src/glum/gui/panel/itemList/ItemListTableModel.java +++ b/src/glum/gui/panel/itemList/ItemListTableModel.java @@ -1,78 +1,107 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.itemList; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; + +import javax.swing.JTable; import javax.swing.table.AbstractTableModel; -public class ItemListTableModel extends AbstractTableModel +/** + * TableModel that provides access to a collection of items handled by the provided {@link ItemHandler}. + * + * @param + * @param + * + * @author lopeznr1 + */ +public class ItemListTableModel> extends AbstractTableModel { - private ItemHandler myHandler; - private ArrayList myVector; + // Ref vars + private final ItemHandler refItemHandler; + + // State vars + private final TableColumnHandler workTableColumnHandler; + private final ArrayList myItemL; /** - * Constructor + * Standard Constructor + * + * @param aItemHandler + * @param aTableColumnHandler */ - public ItemListTableModel(ItemHandler aHandler) + public ItemListTableModel(ItemHandler aItemHandler, TableColumnHandler aTableColumnHandler) { - myHandler = aHandler; + refItemHandler = aItemHandler; + workTableColumnHandler = aTableColumnHandler; - myVector = new ArrayList(); + myItemL = new ArrayList<>(); } @Override public int getColumnCount() { - if (myHandler == null) - return 0; - - return myHandler.getColumnCount(); + return workTableColumnHandler.getColumnCount(); } @Override public int getRowCount() { - return myVector.size(); + return myItemL.size(); } @Override - public String getColumnName(int col) + public String getColumnName(int aCol) { - if (myHandler == null) + return workTableColumnHandler.getColumnLabel(aCol); + } + + @Override + public Object getValueAt(int aRow, int aCol) + { + // Locate the lookup corresponding to the specified column + var tmpEnum = workTableColumnHandler.getEnum(aCol); + if (tmpEnum == null) return null; - return myHandler.getColumnLabel(col); + // Retrieve the appropriate data field of the appropriate item + return refItemHandler.getValue(myItemL.get(aRow), tmpEnum); } @Override - public Object getValueAt(int row, int col) + public Class getColumnClass(int aCol) { - if (myHandler == null) - return null; - - return myHandler.getColumnValue(myVector.get(row), col); + return workTableColumnHandler.getColumnClass(aCol); } @Override - public Class getColumnClass(int col) + public boolean isCellEditable(int aRow, int aCol) { - return myHandler.getColumnClass(col); + return workTableColumnHandler.isColumnEditable(aCol); } @Override - public boolean isCellEditable(int row, int col) + public void setValueAt(Object aValue, int aRow, int aCol) { - if (myHandler == null) - return false; - - return myHandler.isCellEditable(col); - } - - @Override - public void setValueAt(Object value, int row, int col) - { - if (myHandler == null) + // Locate the lookup corresponding to the specified column + var tmpEnum = workTableColumnHandler.getEnum(aCol); + if (tmpEnum == null) return; - myHandler.setColumnValue(myVector.get(row), col, value); + // Update the appropriate data field of the appropriate item + refItemHandler.setValue(myItemL.get(aRow), tmpEnum, aValue); } /** @@ -80,15 +109,13 @@ public class ItemListTableModel extends AbstractTableModel */ public void clear() { - int endIndex; - - if (myVector.isEmpty() == true) + if (myItemL.isEmpty() == true) return; - endIndex = myVector.size() - 1; - myVector.clear(); + int endIdx = myItemL.size() - 1; + myItemL.clear(); - fireTableRowsDeleted(0, endIndex); + fireTableRowsDeleted(0, endIdx); } /** @@ -96,18 +123,16 @@ public class ItemListTableModel extends AbstractTableModel */ public int getRowIndex(G1 aItem) { - int aIndex; - if (aItem == null) return -1; - aIndex = 0; - for (G1 aObj : myVector) + int tmpIdx = 0; + for (G1 aObj : myItemL) { if (aObj.equals(aItem) == true) - return aIndex; + return tmpIdx; - aIndex++; + tmpIdx++; } return -1; @@ -116,33 +141,40 @@ public class ItemListTableModel extends AbstractTableModel /** * Returns the item associated with the row */ - public G1 getRowItem(int row) + public G1 getRowItem(int aRow) { - if (row < 0 || row >= myVector.size()) + if (aRow < 0 || aRow >= myItemL.size()) return null; - return myVector.get(row); + return myItemL.get(aRow); } /** * Adds the collection to our TableModel */ - public void addItems(Collection aCollection) + public void addItems(Collection aItemC) { - int startIndex, endIndex; - - if (aCollection == null) + if (aItemC == null) return; - if (aCollection.isEmpty() == true) + if (aItemC.isEmpty() == true) return; - startIndex = myVector.size(); - endIndex = startIndex + aCollection.size(); + var startIndex = myItemL.size(); + var endIndex = startIndex + aItemC.size(); - myVector.addAll(aCollection); + myItemL.addAll(aItemC); fireTableRowsInserted(startIndex, endIndex); } + /** + * Notifies this {@link TableColumnHandler} of the associated {@link JTable}. + */ + public void initialize(JTable aTable) + { + // Delegate + workTableColumnHandler.initialize(aTable); + } + } diff --git a/src/glum/gui/panel/itemList/ItemProcessor.java b/src/glum/gui/panel/itemList/ItemProcessor.java index 6877771..03e7f52 100644 --- a/src/glum/gui/panel/itemList/ItemProcessor.java +++ b/src/glum/gui/panel/itemList/ItemProcessor.java @@ -1,26 +1,48 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.itemList; -import java.util.*; +import com.google.common.collect.ImmutableList; +import glum.item.ItemEventListener; + +/** + * Interface that defines methods for accessing a collection of items. Support is provided to allow notification of when + * the items change via the ItemEventListener mechanism. + * + * @author lopeznr1 + */ public interface ItemProcessor { + /** + * Adds a {@link ItemEventListener} to this ItemProcessor. + */ + public void addListener(ItemEventListener aListener); + + /** + * Removes a {@link ItemEventListener} from this ItemProcessor. + */ + public void delListener(ItemEventListener aListener); + + /** + * Returns a list of all items in this ItemProcessor + */ + public ImmutableList getAllItems(); + /** * Returns the number of items in this ItemProcessor */ public int getNumItems(); - /** - * Returns a list of all items in this ItemProcessor - */ - public Collection getItems(); - - /** - * Registers for notification when items are added/removed from this ItemProcessor. - */ - public void addItemChangeListener(ItemChangeListener aItemChangeListener); - - /** - * Deregisters for notification of item list change events. - */ - public void removeItemChangeListener(ItemChangeListener aItemChangeListener); } diff --git a/src/glum/gui/panel/itemList/RegistryProcessor.java b/src/glum/gui/panel/itemList/RegistryProcessor.java index 673c9a1..006783c 100644 --- a/src/glum/gui/panel/itemList/RegistryProcessor.java +++ b/src/glum/gui/panel/itemList/RegistryProcessor.java @@ -1,52 +1,74 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.itemList; -import java.util.*; +import java.util.List; -import glum.registry.*; +import com.google.common.collect.ImmutableList; +import glum.registry.Registry; +import glum.registry.ResourceListener; + +/** + * Implementation of {@link ItemProcessor} backed by the specified Registry and resource key. + *

    + * Notification will be sent out whenever the registry changes. + * + * @author lopeznr1 + */ public class RegistryProcessor extends BasicItemProcessor implements ResourceListener { - private Registry refRegistry; - private Class resourceClass; - private Object resourceKey; + // Ref vars + private final Registry refRegistry; + private final Class resourceClass; + private final Object resourceKey; - private Collection itemList; + // State vars + private ImmutableList itemL; - /** - * Constructor - */ + /** Standard Constructor */ public RegistryProcessor(Registry aRegistry, Object aResourceKey, Class aResourceClass) { - super(); - refRegistry = aRegistry; resourceClass = aResourceClass; resourceKey = aResourceKey; // Initialize our state vars - itemList = new ArrayList(); + itemL = ImmutableList.of(); // Register for events of interest refRegistry.addResourceListener(resourceKey, this); } @Override - public synchronized Collection getItems() + public ImmutableList getAllItems() { - return new ArrayList(itemList); + return itemL; } @Override public synchronized int getNumItems() { - return itemList.size(); + return itemL.size(); } @Override public void resourceChanged(Registry aRegistry, Object aKey) { // Retrieve the list of tiles - itemList = refRegistry.getResourceItems(resourceKey, resourceClass); + List tmpL = refRegistry.getResourceItems(resourceKey, resourceClass); + itemL = ImmutableList.copyOf(tmpL); // Notify our listeners notifyListeners(); diff --git a/src/glum/gui/panel/itemList/SearchBoxRenderer.java b/src/glum/gui/panel/itemList/SearchBoxRenderer.java index 9334c7d..d369a03 100644 --- a/src/glum/gui/panel/itemList/SearchBoxRenderer.java +++ b/src/glum/gui/panel/itemList/SearchBoxRenderer.java @@ -1,33 +1,43 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.itemList; -import java.awt.*; +import java.awt.Component; + import javax.swing.*; -import javax.swing.table.*; +import javax.swing.table.TableColumn; public class SearchBoxRenderer extends DefaultListCellRenderer { - /** - * Constructor - */ + /*** Standard Constructor */ public SearchBoxRenderer() { - super(); + ; // Nothing to do } @Override - public Component getListCellRendererComponent(JList list, Object aObj, int index, boolean isSelected, boolean hasFocus) + public Component getListCellRendererComponent(JList list, Object aObj, int index, boolean isSelected, + boolean hasFocus) { - JLabel retL; - String aStr; - - retL = (JLabel)super.getListCellRendererComponent(list, aObj, index, isSelected, hasFocus); + var retL = (JLabel) super.getListCellRendererComponent(list, aObj, index, isSelected, hasFocus); if (aObj instanceof TableColumn) { - aStr = "" + ((TableColumn)aObj).getHeaderValue(); - if (aStr.equals("null") == true || aStr.equals("") == true) - aStr = "" + ((TableColumn)aObj).getIdentifier(); + var tmpStr = "" + ((TableColumn) aObj).getHeaderValue(); + if (tmpStr.equals("null") == true || tmpStr.equals("") == true) + tmpStr = "" + ((TableColumn) aObj).getIdentifier(); - retL.setText(aStr); + retL.setText(tmpStr); } return retL; diff --git a/src/glum/gui/panel/itemList/StaticItemProcessor.java b/src/glum/gui/panel/itemList/StaticItemProcessor.java index 141025c..8f19204 100644 --- a/src/glum/gui/panel/itemList/StaticItemProcessor.java +++ b/src/glum/gui/panel/itemList/StaticItemProcessor.java @@ -1,23 +1,45 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.itemList; import java.util.*; +import com.google.common.collect.ImmutableList; + +/** + * Implementation of {@link ItemProcessor} which maintains a static list of items. + * + * @author lopeznr1 + */ public class StaticItemProcessor extends BasicItemProcessor { - private ArrayList itemList; + private ImmutableList itemL; - public StaticItemProcessor() + /** + * Standard Constructor + */ + public StaticItemProcessor(List aItemL) { - super(); - - itemList = new ArrayList(); + itemL = ImmutableList.copyOf(aItemL); } - public StaticItemProcessor(List aList) + /** + * Empty Constructor + */ + public StaticItemProcessor() { - super(); - - itemList = new ArrayList(aList); + this(ImmutableList.of()); } /** @@ -27,30 +49,30 @@ public class StaticItemProcessor extends BasicItemProcessor */ public int indexOf(G1 aItem) { - return itemList.indexOf(aItem); + return itemL.indexOf(aItem); } /** * Replaces the static list of items stored with aItemList */ - public void setItems(Collection aItemList) + public void setItems(Collection aItemC) { - itemList = new ArrayList(aItemList); + itemL = ImmutableList.copyOf(aItemC); // Notify our listeners notifyListeners(); } @Override - public synchronized ArrayList getItems() + public synchronized ImmutableList getAllItems() { - return new ArrayList(itemList); + return itemL; } @Override public synchronized int getNumItems() { - return itemList.size(); + return itemL.size(); } } diff --git a/src/glum/gui/panel/itemList/TableColumnHandler.java b/src/glum/gui/panel/itemList/TableColumnHandler.java new file mode 100644 index 0000000..52cf882 --- /dev/null +++ b/src/glum/gui/panel/itemList/TableColumnHandler.java @@ -0,0 +1,521 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.panel.itemList; + +import java.util.*; + +import javax.swing.JTable; + +import com.google.common.collect.ImmutableList; + +import glum.gui.panel.itemList.query.QueryAttribute; +import glum.gui.panel.itemList.query.QueryTableCellRenderer; +import glum.gui.table.SortDir; +import glum.gui.table.TableSorter; +import glum.unit.*; + +/** + * Support class for {@link ItemListTableModel}. + * + * @author lopeznr1 + */ +public class TableColumnHandler> implements UnitListener +{ + // Attributes + private final ArrayList> fullAttributeL; + + // State vars + private JTable myOwner; + private ArrayList> orderAttributeL; + + /** Standard Constructor */ + public TableColumnHandler(Collection> aQueryAttrC) + { + myOwner = null; + + fullAttributeL = new ArrayList<>(); + orderAttributeL = new ArrayList<>(); + + int evalIndex = 0; + for (QueryAttribute aAttr : aQueryAttrC) + { + // Ensure the model index is appropriately initialized + if (evalIndex != aAttr.modelIndex) + throw new RuntimeException( + "Improper initialization. Expected Index: " + evalIndex + " Received Index: " + aAttr.modelIndex); + + fullAttributeL.add(aAttr); + orderAttributeL.add(aAttr); + evalIndex++; + + // Register for the appropriate unit events + aAttr.refUnitProvider.addListener(this); + } + + fullAttributeL.trimToSize(); + orderAttributeL.trimToSize(); + } + + /** + * Returns the (enum) lookup associated with the specified column index. + *

    + * Returns null if the column index is out of range. + */ + public G2 getEnum(int aColIdx) + { + // Insanity check + if (aColIdx < 0 && aColIdx >= fullAttributeL.size()) + return null; + + return fullAttributeL.get(aColIdx).refKey; + } + + /** + * Notifies the {@link TableColumnHandler} of its associated JTable. This table should be updated or painted whenever + * the internals of this {@link TableColumnHandler} change. + */ + public void initialize(JTable aOwner) + { + // This method is only allowed to be called once! + if (myOwner != null) + throw new RuntimeException("TableColumnHandler already initialized!"); + + myOwner = aOwner; + + var tmpTableHeader = myOwner.getTableHeader(); + var tmpTableColumnModel = tmpTableHeader.getColumnModel(); + + // Customize overall settings + tmpTableHeader.setReorderingAllowed(false); + + // Grab all of the precomputed columns from the table + // and store with their associated queryAttributes + for (int c1 = 0; c1 < fullAttributeL.size(); c1++) + { + var tmpTableColumn = tmpTableColumnModel.getColumn(c1); + fullAttributeL.get(c1).assocTableColumn = tmpTableColumn; + } + + // Rebuild the table columns; Needed so that only the + // visible ones are displayed. Do this only after you + // have grabbed the table columns as aTableColumnModel + // will strictly contain the "visible" ones + rebuildTableColumns(); + } + + /** + * Returns the number of columns associated with the tabular data. + */ + public int getColumnCount() + { + return fullAttributeL.size(); + } + + /** + * Returns the class of data type for each column in the tabular data. + */ + public Class getColumnClass(int colNum) + { + // Insanity check + if (colNum < 0 && colNum >= fullAttributeL.size()) + return String.class; + + return fullAttributeL.get(colNum).refClass; + } + + public int getColumnDefaultWidth(int colNum) + { + // Insanity check + if (colNum < 0 && colNum >= fullAttributeL.size()) + return -1; + + // Get the default and min size + var defaultSize = fullAttributeL.get(colNum).defaultSize; + var minSize = fullAttributeL.get(colNum).minSize; + + // Ensure size makes sense + if (defaultSize < minSize) + return minSize; + + return defaultSize; + } + + /** + * getColumnMinWidth + */ +// public int getColumnMinWidth(int colNum) +// { +// // Insanity check +// if (queryAttributes == null) +// return -1; +// +// if (colNum < 0 && colNum >= queryAttributes.length) +// return -1; +// +// return queryAttributes[colNum].minSize; +// } + + /** + * getColumnMaxWidth + */ + public int getColumnMaxWidth(int colNum) + { + // Insanity check + if (colNum < 0 && colNum >= fullAttributeL.size()) + return -1; + + // Get the default and max size + var defaultSize = fullAttributeL.get(colNum).defaultSize; + var maxSize = fullAttributeL.get(colNum).maxSize; + + // Ensure size makes sense + if (defaultSize > maxSize && maxSize != -1) + return defaultSize; + + return maxSize; + } + + /** + * Returns the text label that should be used for the title of the column. + */ + public String getColumnLabel(int colNum) + { + // Insanity check + if (colNum < 0 && colNum >= fullAttributeL.size()) + return ""; + + var tmpAttribute = fullAttributeL.get(colNum); + + // Retrieve the associated unit + var tmpUnit = tmpAttribute.refUnitProvider.getUnit(); + + // Retrieve the base column label + var tmpStr = tmpAttribute.label; + + // Append the unit name to the column label + if (tmpUnit != null && "".equals(tmpUnit.getLabel(false)) == false) + return tmpStr + " [" + tmpUnit.getLabel(false) + "]"; + else + return tmpStr; + } + + /** + * Returns an ordered list of the {@link QueryAttribute}s. + */ + public ArrayList> getOrderedAttributes() + { + if (myOwner.getModel() instanceof TableSorter aTableSorter) + { + for (var aAttr : orderAttributeL) + { + var tmpIdx = aAttr.modelIndex; + aAttr.sortDir = aTableSorter.getSortDir(tmpIdx); + } + } + + return new ArrayList>(orderAttributeL); + } + + /** + * Returns a list of model indexes which define the priority of the sorted columns. + */ + public List getSortPriorityList() + { + var tmpModel = myOwner.getModel(); + if (tmpModel instanceof TableSorter aTableSorter) + { + var retSortPriorityL = new ArrayList(); + var tmpSortStateM = aTableSorter.getSortState(); + for (var aModelIdx : tmpSortStateM.keySet()) + retSortPriorityL.add(aModelIdx); + + return retSortPriorityL; + } + + return ImmutableList.of(); + } + + /** + * Returns true if the data associated at the specified column can be edited. + */ + public boolean isColumnEditable(int colNum) + { + // Insanity check + if (colNum < 0 && colNum >= fullAttributeL.size()) + return false; + + if (fullAttributeL.get(colNum).editor == null) + return false; + + return true; + } + + @Override + public void unitChanged(UnitProvider aManager, String aKey) + { + update(); + + myOwner.repaint(); + + var tmpTableHeader = myOwner.getTableHeader(); + if (tmpTableHeader != null) + tmpTableHeader.repaint(); + +// for (QueryAttribute aAttribute : queryAttributes) +// { +// if (aKey.equals(aAttribute.unitKey) == true) +// } +// +// Tile aTile; +// +// // Update our listPanel to by sync with the active tile +// if (aKey.equals("tile.active") == true) +// { +// aTile = refRegistry.getSingleton(aKey, Tile.class); +// +// listPanel.removeListSelectionListener(this); +// listPanel.selectItem(aTile); +// listPanel.addListSelectionListener(this); +// } +// +// updateGui(); + } + + public Unit getUnit(int aColNum) + { + // Insanity check + if (aColNum < 0 && aColNum >= fullAttributeL.size()) + return null; + + var tmpAttribute = fullAttributeL.get(aColNum); + return tmpAttribute.refUnitProvider.getUnit(); + } + + public void setColumnAlignment(int colNum, int aAlignment) + { + // Insanity check + if (colNum < 0 && colNum >= fullAttributeL.size()) + return; + + fullAttributeL.get(colNum).alignment = aAlignment; + } + + public void setColumnLabel(int colNum, String aLabel) + { + // Insanity check + if (colNum < 0 && colNum >= fullAttributeL.size()) + return; + + fullAttributeL.get(colNum).label = aLabel; + } + + public void setColumnPosition(int colNum, int aPosition) + { + // Insanity check + if (colNum < 0 || aPosition < 0 || colNum >= fullAttributeL.size() || aPosition >= fullAttributeL.size()) + return; + + orderAttributeL.remove(fullAttributeL.get(colNum)); + orderAttributeL.add(aPosition, fullAttributeL.get(colNum)); + } + + public void setColumnSize(int colNum, int aSize) + { + // Insanity check + if (colNum < 0 && colNum >= fullAttributeL.size()) + return; + + System.out.println("[QueryItemHandler.java] Changing size of colNum: " + aSize); + fullAttributeL.get(colNum).defaultSize = aSize; + } + + public void setColumnSortDir(int colNum, SortDir aSortDir) + { + // Insanity check + if (colNum < 0 && colNum >= fullAttributeL.size()) + return; + + fullAttributeL.get(colNum).sortDir = aSortDir; + } + + public void setColumnVisible(int colNum, boolean isVisible) + { + // Insanity check + if (colNum < 0 && colNum >= fullAttributeL.size()) + return; + + fullAttributeL.get(colNum).isVisible = isVisible; + + // Update our table + if (myOwner != null) + rebuildTableColumns(); + } + + /** + * moveSortedAttribute + */ + public void moveSortedAttribute(int currIndex, int newIndex) + { + var tmpItem = orderAttributeL.get(currIndex); + orderAttributeL.remove(currIndex); + orderAttributeL.add(newIndex, tmpItem); + } + + /** + * Method to reconfigure the columns of the table. This will update the display order of the columns as well as the + * individual relevant attributes of the column. Currently supported relevant attributes are those defined in the + * method {@link QueryAttribute#setConfig}. + * + * Note any non specified columns will appear last according to the previous order. + * + * @param orderSet: + * Ordered set of QueryAttributes with matching modelIndexes + */ + public void setOrderAndConfig(Collection> aOrderArr) + { + // Form a lookup map (modelIndex to attribute) + var tmpItemM = new LinkedHashMap>(); + for (var aItem : orderAttributeL) + tmpItemM.put(aItem.modelIndex, aItem); + + // Rebuild the sortedQueryAttribute list to conform with + // - the specified order of orderList + // - synch up relevant attributes + orderAttributeL.clear(); + for (QueryAttribute aItem : aOrderArr) + { + var workItem = tmpItemM.remove(aItem.modelIndex); + if (workItem != null) + { + workItem.setConfig(aItem); + orderAttributeL.add(workItem); + } + } + + orderAttributeL.addAll(tmpItemM.values()); + tmpItemM.clear(); + } + + /** + * Returns a list of model indexes which define the priority of the sorted columns. + */ + public void setSortPriorityList(List aSortPriorityL) + { + var tmpModel = myOwner.getModel(); + if (tmpModel instanceof TableSorter aTableSorter) + { + var tmpAttributeM = new HashMap>(); + for (var aAttribute: fullAttributeL) + tmpAttributeM.put(aAttribute.modelIndex, aAttribute); + + var tmpSortStateM = new LinkedHashMap(); + for (var aModelIdx : aSortPriorityL) + { + var tmpAttribute = tmpAttributeM.get(aModelIdx); + if (tmpAttribute == null) + continue; + + tmpSortStateM.put(aModelIdx, tmpAttribute.sortDir); + } + + aTableSorter.setSortState(tmpSortStateM); + } + } + + /** + * Helper method to initialize a TableColumn with the actual QueryAttribute properties. + * + * @param colNum: + * Index into column model + */ + protected void initializeTableColumn(int aColNum) + { + // Insanity check + if (aColNum < 0 && aColNum >= fullAttributeL.size()) + return; + + // Get the associated table column + var tmpAttribute = fullAttributeL.get(aColNum); + var tmpTableColumn = tmpAttribute.assocTableColumn; + if (tmpTableColumn == null) + return; + + // Retrieve settings of interest + var label = getColumnLabel(aColNum); + var defaultWidth = getColumnDefaultWidth(aColNum); + var maxWidth = getColumnMaxWidth(aColNum); + var minWidth = tmpAttribute.minSize; + + // Set up the column's renderer + var tmpRenderer = tmpAttribute.renderer; + if (tmpRenderer == null) + tmpRenderer = new QueryTableCellRenderer(tmpAttribute); + tmpTableColumn.setCellRenderer(tmpRenderer); + + // Set up the column's editor + tmpTableColumn.setCellEditor(tmpAttribute.editor); + + // Set up the column's size attributes + tmpTableColumn.setMinWidth(minWidth); + tmpTableColumn.setMaxWidth(maxWidth); + tmpTableColumn.setPreferredWidth(defaultWidth); + tmpTableColumn.setWidth(defaultWidth); + + // Set up the column header + tmpTableColumn.setHeaderValue(label); + } + +// TODO -> This should probably be protected + public void rebuildTableColumns() + { + // Enforce the constraints (QueryAttribute) on the associated columns + for (int c1 = 0; c1 < fullAttributeL.size(); c1++) + initializeTableColumn(c1); + + // Remove all of the columns from the table + for (var aAttribute : fullAttributeL) + myOwner.removeColumn(aAttribute.assocTableColumn); + + // Add in only the columns that are visible + for (var aAttribute : orderAttributeL) + { + if (aAttribute.isVisible == false) + continue; + myOwner.addColumn(aAttribute.assocTableColumn); + } + } + + /** + * Helper method that updates all the TableColumn renders with the appropriate unit. + */ + private void update() + { + for (var aAttribute : fullAttributeL) + { + if (aAttribute.assocTableColumn != null) + { + var tmpTableColumn = aAttribute.assocTableColumn; + var tmpObject = tmpTableColumn.getCellRenderer(); + if (tmpObject instanceof QueryTableCellRenderer aRenderer) + { + aAttribute.assocTableColumn.setHeaderValue(getColumnLabel(aAttribute.modelIndex)); + + var tmpUnit = aAttribute.refUnitProvider.getUnit(); + aRenderer.setUnit(tmpUnit); + } + } + } + } + +} diff --git a/src/glum/gui/panel/itemList/config/AddProfilePanel.java b/src/glum/gui/panel/itemList/config/AddProfilePanel.java index 3063e86..331bd4e 100644 --- a/src/glum/gui/panel/itemList/config/AddProfilePanel.java +++ b/src/glum/gui/panel/itemList/config/AddProfilePanel.java @@ -1,16 +1,26 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.itemList.config; -import glum.gui.panel.generic.TextInputPanel; +import glum.gui.panel.generic.BaseTextInputPanel; import java.awt.*; import java.util.*; import com.google.common.collect.Sets; -public class AddProfilePanel extends TextInputPanel +public class AddProfilePanel extends BaseTextInputPanel { - // Constants - public static final String DEFAULT_NAME = "Default"; - // State vars protected Set reservedSet; @@ -51,7 +61,7 @@ public class AddProfilePanel extends TextInputPanel { infoMsg = "Please enter a valid profile name."; } - else if (inputStr.equals(DEFAULT_NAME) == true) + else if (inputStr.equals(ProfileConfig.DEFAULT_NAME) == true) { infoMsg = "Name is reserved. Please pick another."; } diff --git a/src/glum/gui/panel/itemList/config/ConfigHandler.java b/src/glum/gui/panel/itemList/config/ConfigHandler.java index 2521c06..6a8957b 100644 --- a/src/glum/gui/panel/itemList/config/ConfigHandler.java +++ b/src/glum/gui/panel/itemList/config/ConfigHandler.java @@ -1,77 +1,60 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.itemList.config; -import glum.gui.panel.itemList.BasicItemHandler; +import glum.gui.panel.itemList.ItemHandler; +import glum.gui.panel.itemList.ItemListPanel; import glum.gui.panel.itemList.query.QueryAttribute; -import glum.gui.panel.itemList.query.QueryComposer; -public class ConfigHandler extends BasicItemHandler +/** + * Implementation of {@link ItemHandler} that allows for tabular access of columns shown in a {@link ItemListPanel}. + * + * @author lopeznr1 + */ +public class ConfigHandler> implements ItemHandler, ConfigLookUp> { - public ConfigHandler(QueryComposer aComposer) - { - super(aComposer); - } - @Override - public Object getColumnValue(QueryAttribute aObj, int colNum) + public Object getValue(QueryAttribute aItem, ConfigLookUp aEnum) { - Enum refKey; - - // Insanity check - if (colNum < 0 && colNum >= fullAttributeList.size()) - return null; - - refKey = fullAttributeList.get(colNum).refKey; - return getColumnValue(aObj, refKey); - } - - @Override - public void setColumnValue(QueryAttribute aObj, int colNum, Object aValue) - { - Enum refKey; - - // Insanity check - if (colNum < 0 && colNum >= fullAttributeList.size()) - return; - - refKey = fullAttributeList.get(colNum).refKey; - setColumnValue(aObj, refKey, aValue); - } - - /** - * Method to get the value from aObj described by aRefKey - */ - public Object getColumnValue(QueryAttribute aItem, Enum aRefKey) - { - switch ((ConfigLookUp)aRefKey) + switch (aEnum) { case IsVisible: - return aItem.isVisible; + return aItem.isVisible; case Name: - return aItem.refKey; + return aItem.refKey; case Label: - return aItem.label; + return aItem.label; default: - break; + break; } - return null; + throw new RuntimeException("Unsupported enum:" + aEnum); } - /** - * Method to get the value from aObj described by aRefKey - */ - public void setColumnValue(QueryAttribute aItem, Enum aRefKey, Object aValue) + @Override + public void setValue(QueryAttribute aItem, ConfigLookUp aEnum, Object aValue) { - if (aRefKey == ConfigLookUp.IsVisible) + if (aEnum == ConfigLookUp.IsVisible) { - aItem.isVisible = (Boolean)aValue; + aItem.isVisible = (Boolean) aValue; return; } - throw new RuntimeException("Unsupported Operation."); + throw new RuntimeException("Unsupported enum:" + aEnum); } } diff --git a/src/glum/gui/panel/itemList/config/ConfigLookUp.java b/src/glum/gui/panel/itemList/config/ConfigLookUp.java index 05ffd72..e049064 100644 --- a/src/glum/gui/panel/itemList/config/ConfigLookUp.java +++ b/src/glum/gui/panel/itemList/config/ConfigLookUp.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.itemList.config; public enum ConfigLookUp diff --git a/src/glum/gui/panel/itemList/config/EditTablePanel.java b/src/glum/gui/panel/itemList/config/EditTablePanel.java index c8a21b5..27e8e5d 100644 --- a/src/glum/gui/panel/itemList/config/EditTablePanel.java +++ b/src/glum/gui/panel/itemList/config/EditTablePanel.java @@ -1,116 +1,129 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.itemList.config; -import java.awt.*; +import java.awt.Component; +import java.awt.Dimension; import java.awt.event.*; import java.io.IOException; -import java.util.*; +import java.util.HashSet; +import java.util.List; import javax.swing.*; -import javax.swing.border.*; +import javax.swing.border.EmptyBorder; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.DefaultTableCellRenderer; -import com.google.common.collect.Sets; - -import glum.gui.*; -import glum.gui.action.*; +import glum.gui.FocusUtil; +import glum.gui.GuiUtil; +import glum.gui.action.ClickAction; import glum.gui.component.GComboBox; import glum.gui.component.GTextField; -import glum.gui.icon.ArrowNorthIcon; -import glum.gui.icon.ArrowSouthIcon; -import glum.gui.icon.DeleteIcon; -import glum.gui.misc.*; -import glum.gui.panel.*; -import glum.gui.panel.itemList.BasicItemHandler; -import glum.gui.panel.itemList.ItemHandler; -import glum.gui.panel.itemList.ItemListPanel; -import glum.gui.panel.itemList.StaticItemProcessor; -import glum.gui.panel.itemList.query.*; +import glum.gui.icon.*; +import glum.gui.misc.BooleanCellEditor; +import glum.gui.misc.BooleanCellRenderer; +import glum.gui.panel.GlassPanel; +import glum.gui.panel.generic.MessagePanel; +import glum.gui.panel.itemList.*; +import glum.gui.panel.itemList.query.QueryAttribute; +import glum.gui.panel.itemList.query.QueryComposer; import glum.zio.*; import net.miginfocom.swing.MigLayout; -public class EditTablePanel extends GlassPanel implements ActionListener, ZioObj, ListSelectionListener +/** + * UI component that allows for the configuration of the table contained by an {@link ItemListPanel}. + *

    + * Construction of this panel involves passing a reference to the {@link ItemListPanel}'s {@link TableColumnHandler}. + * This handler is retrieved via {@link ItemListPanel#getTableColumnHandler()}. + * + * @author lopeznr1 + */ +public class EditTablePanel> extends GlassPanel + implements ActionListener, ItemListener, ListSelectionListener, ZioObj { + // Attributes + private final TableColumnHandler refTableColumnHandler; + // GUI vars - protected JLabel titleL; - protected JRadioButton profileRB, customRB; - protected GComboBox profileBox; - protected ItemListPanel listPanel; - protected BooleanCellEditor col0Editor; - protected BooleanCellRenderer col0Renderer; - protected DefaultTableCellRenderer col1Renderer; - protected JButton closeB, saveB, upB, downB, deleteB; - protected JLabel labelL; - protected GTextField labelTF; - protected Font smallFont; - protected AddProfilePanel profilePanel; + private JLabel titleL; + private JRadioButton profileRB, customRB; + private GComboBox> profileBox; + private ItemListPanel, ConfigLookUp> listPanel; + private BooleanCellEditor col0Editor; + private BooleanCellRenderer col0Renderer; + private DefaultTableCellRenderer col1Renderer; + private JButton closeB, saveB, upB, downB, deleteB; + private JLabel labelL; + private GTextField labelTF; + private AddProfilePanel profilePanel; // State vars - protected BasicItemHandler refItemHandler; - protected StaticItemProcessor myItemProcessor; + protected StaticItemProcessor> myItemProcessor; - public EditTablePanel(Component aParent, BasicItemHandler aItemHandler) + /** Standard Constructor */ + public EditTablePanel(Component aParent, TableColumnHandler aTableColumnHandler) { super(aParent); // State vars - refItemHandler = aItemHandler; + refTableColumnHandler = aTableColumnHandler; // Build the actual GUI - smallFont = (new JTextField()).getFont(); buildGuiArea(); setPreferredSize(new Dimension(250, getPreferredSize().height)); - + profilePanel = new AddProfilePanel(this); profilePanel.setSize(375, 140); profilePanel.addActionListener(this); - + syncGui(); updateGui(); - + // Set up some keyboard shortcuts FocusUtil.addAncestorKeyBinding(this, "ESCAPE", new ClickAction(closeB)); } /** - * Adds a predefined profile (as specified in aConfig) to the - * list of available profiles. + * Adds a predefined profile (as specified in aConfig) to the list of available profiles. + * * @param aConfig */ - public void addConfig(ProfileConfig aConfig) + public void addConfig(ProfileConfig aConfig) { profileBox.addItem(aConfig); } - - public ArrayList getAllConfig() + + public List> getAllConfig() { return profileBox.getAllItems(); } - - public BasicItemHandler getItemHandler() - { - return refItemHandler; - } - + /** - * Synchronizes the gui to match the model + * Synchronizes the gui to match the model */ public void syncGui() { - myItemProcessor.setItems(refItemHandler.getSortedAttributes()); + myItemProcessor.setItems(refTableColumnHandler.getOrderedAttributes()); } - + @Override public void actionPerformed(ActionEvent aEvent) { - Object source; - QueryAttribute aItem; - int index; + var tmpItem = listPanel.getSelectedItem(); - aItem = listPanel.getSelectedItem(); - - source = aEvent.getSource(); + var source = aEvent.getSource(); if (source == labelTF) { updateLayerAttribute(); @@ -118,8 +131,7 @@ public class EditTablePanel extends GlassPanel implements ActionListener, ZioObj } else if (source == deleteB) { - profileBox.removeItem(profileBox.getChosenItem()); - actionPerformed(new ActionEvent(profileBox, ID_UPDATE, null)); + doActionDel(); } else if (source == closeB) { @@ -128,145 +140,157 @@ public class EditTablePanel extends GlassPanel implements ActionListener, ZioObj } else if (source == saveB) { - Set nameSet; - - nameSet = Sets.newHashSet(); - for (ProfileConfig aProfile : profileBox.getAllItems()) - nameSet.add(aProfile.getName()); - + var nameS = new HashSet(); + for (ProfileConfig aProfile : profileBox.getAllItems()) + nameS.add(aProfile.name()); + profilePanel.resetGui(); - profilePanel.setReservedNames(nameSet); + profilePanel.setReservedNames(nameS); profilePanel.setVisible(true); } else if (source == upB) { - index = myItemProcessor.indexOf(aItem); - refItemHandler.moveSortedAttribute(index, index - 1); - refItemHandler.rebuildTableColumns(); - - myItemProcessor.setItems(refItemHandler.getSortedAttributes()); + var index = myItemProcessor.indexOf(tmpItem); + refTableColumnHandler.moveSortedAttribute(index, index - 1); + refTableColumnHandler.rebuildTableColumns(); + + myItemProcessor.setItems(refTableColumnHandler.getOrderedAttributes()); } else if (source == downB) { - index = myItemProcessor.indexOf(aItem); - refItemHandler.moveSortedAttribute(index, index + 1); - refItemHandler.rebuildTableColumns(); - - myItemProcessor.setItems(refItemHandler.getSortedAttributes()); + var index = myItemProcessor.indexOf(tmpItem); + refTableColumnHandler.moveSortedAttribute(index, index + 1); + refTableColumnHandler.rebuildTableColumns(); + + myItemProcessor.setItems(refTableColumnHandler.getOrderedAttributes()); } else if (source == col0Editor) { - refItemHandler.rebuildTableColumns(); + refTableColumnHandler.rebuildTableColumns(); } else if (source == profileBox || source == profileRB) { - ProfileConfig aConfig; - - aConfig = profileBox.getChosenItem(); - refItemHandler.setOrderAndConfig(aConfig.getItems()); - refItemHandler.rebuildTableColumns(); - - myItemProcessor.setItems(refItemHandler.getSortedAttributes()); + var tmpConfig = profileBox.getChosenItem(); + refTableColumnHandler.setOrderAndConfig(tmpConfig.getItems()); + refTableColumnHandler.rebuildTableColumns(); + + myItemProcessor.setItems(refTableColumnHandler.getOrderedAttributes()); } else if (source == profilePanel && aEvent.getID() == AddProfilePanel.ID_ACCEPT) { - ProfileConfig aProfile; - Collection aItemList; - String aName; - - aName = profilePanel.getInput(); - aItemList = myItemProcessor.getItems(); - for (QueryAttribute aAttribute : aItemList) + var tmpName = profilePanel.getInput(); + var tmpItemL = myItemProcessor.getAllItems(); + for (var aAttribute : myItemProcessor.getAllItems()) aAttribute.synchronizeAttribute(); - - aProfile = new ProfileConfig(aName, aItemList); - profileBox.addItem(aProfile); + + // Determine if this will result in a Profile being replaced + ProfileConfig repProfile = null; + for (ProfileConfig aProfile : profileBox.getAllItems()) + { + if (aProfile.name().equals(tmpName) == true) + repProfile = aProfile; + } + + var tmpProfile = new ProfileConfig<>(tmpName, tmpItemL); + if (repProfile == null) + profileBox.addItem(tmpProfile); + else + profileBox.replaceItem(repProfile, tmpProfile); } updateGui(); } - + + @Override + public void itemStateChanged(ItemEvent aEvent) + { + var source = aEvent.getSource(); + if (source == profileRB) + { + var tmpConfig = profileBox.getChosenItem(); + refTableColumnHandler.setOrderAndConfig(tmpConfig.getItems()); + refTableColumnHandler.rebuildTableColumns(); + + myItemProcessor.setItems(refTableColumnHandler.getOrderedAttributes()); + } + + updateGui(); + } + + @Override + public void valueChanged(ListSelectionEvent aEvent) + { + // Update only after the user has released the mouse + if (aEvent.getValueIsAdjusting() == true) + return; + + updateGui(); + } + @Override public void zioRead(ZinStream aStream) throws IOException { - ProfileConfig aProfile; - int numItems, profileIndex; - boolean aBool; - super.zioRead(aStream); - + aStream.readVersion(0); - - aBool = aStream.readBool(); - customRB.setSelected(aBool); - profileRB.setSelected(!aBool); - - numItems = aStream.readInt(); + + var tmpBool = aStream.readBool(); + customRB.setSelected(tmpBool); + profileRB.setSelected(!tmpBool); + + var numItems = aStream.readInt(); profileBox.removeAllItems(); for (int c1 = 0; c1 < numItems; c1++) { - aProfile = new ProfileConfig("unnamed", myItemProcessor.getItems()); - aProfile.zioRead(aStream); - - profileBox.addItem(aProfile); + var tmpProfileConfig = ProfileConfig.zioRead(aStream, myItemProcessor.getAllItems()); + profileBox.addItem(tmpProfileConfig); } - - profileIndex = aStream.readInt(); + + var profileIndex = aStream.readInt(); profileBox.removeActionListener(this); - if (profileIndex >= 0) + if (profileIndex >= 0) profileBox.setSelectedIndex(profileIndex); profileBox.addActionListener(this); - - refItemHandler.zioRead(aStream); - + + var tmpProfileConfig = ProfileConfig.zioRead(aStream, myItemProcessor.getAllItems()); + refTableColumnHandler.setOrderAndConfig(tmpProfileConfig.getItems()); + refTableColumnHandler.rebuildTableColumns(); + refTableColumnHandler.setSortPriorityList(tmpProfileConfig.sortPriorityList()); + + myItemProcessor.setItems(refTableColumnHandler.getOrderedAttributes()); + updateGui(); } @Override public void zioWrite(ZoutStream aStream) throws IOException { - int numItems, profileIndex; - boolean aBool; - super.zioWrite(aStream); aStream.writeVersion(0); - - aBool = customRB.isSelected(); - aStream.writeBool(aBool); - - numItems = profileBox.getAllItems().size(); + + var tmpBool = customRB.isSelected(); + aStream.writeBool(tmpBool); + + var numItems = profileBox.getAllItems().size(); aStream.writeInt(numItems); - - for (ProfileConfig aProfile : profileBox.getAllItems()) - { - aProfile.zioWrite(aStream); - } - - profileIndex = profileBox.getSelectedIndex(); + + for (var aProfile : profileBox.getAllItems()) + ProfileConfig.zioWrite(aStream, aProfile); + + var profileIndex = profileBox.getSelectedIndex(); aStream.writeInt(profileIndex); - - refItemHandler.zioWrite(aStream); - } - @Override - public void valueChanged(ListSelectionEvent e) - { - // Update only after the user has released the mouse - if (e.getValueIsAdjusting() == true) - return; - - updateGui(); + var tmpProfileConfig = new ProfileConfig<>("", refTableColumnHandler.getOrderedAttributes(), + refTableColumnHandler.getSortPriorityList()); + ProfileConfig.zioWrite(aStream, tmpProfileConfig); } /** - * Builds the main GUI area + * Helper method that builds the main GUI area */ - protected void buildGuiArea() + private void buildGuiArea() { - JPanel tmpPanel; - ProfileConfig aConfig; - // Form the layout setLayout(new MigLayout("", "[left][grow][]", "[][][]3[grow][]")); @@ -275,121 +299,130 @@ public class EditTablePanel extends GlassPanel implements ActionListener, ZioObj add(titleL, "growx,span 2,wrap"); // Profile Area - profileRB = GuiUtil.createJRadioButton("Profile:", this, smallFont); + profileRB = GuiUtil.createJRadioButton(this, "Profile:"); add(profileRB, "span 1"); - profileBox = new GComboBox(); + profileBox = new GComboBox<>(); profileBox.addActionListener(this); - profileBox.setFont(smallFont); add(profileBox, "growx,span 1"); - - deleteB = GuiUtil.createJButton(new DeleteIcon(14), this); - add(deleteB, "align right,span 1,w 20!, h 20!,wrap"); - - // Custom Area - customRB = GuiUtil.createJRadioButton("Custom:", this, smallFont); - customRB.setSelected(true); - add(customRB, "span 1"); - //saveB = GuiUtil.createJButton(new ArrowNorthIcon(14), this); - //add(saveB, "align right,span 1,w 18!, h 18!"); - saveB = GuiUtil.createJButton("Save", this, smallFont); + deleteB = GuiUtil.createJButton(new DeleteIcon(14), this); + add(deleteB, "align right,span 1,w 20!,h 20!,wrap"); + + // Custom Area + customRB = GuiUtil.createJRadioButton(this, "Custom:"); + add(customRB, ""); + + // saveB = GuiUtil.createJButton(new ArrowNorthIcon(14), this); + // add(saveB, "align right,span 1,w 18!, h 18!"); + saveB = GuiUtil.createJButton("Save", this); add(saveB, "align right,span 2,wrap"); - - tmpPanel = buildItemListTablePanel(); + + var tmpPanel = buildItemListTablePanel(); tmpPanel.setBorder(new EmptyBorder(0, 15, 0, 0)); add(tmpPanel, "growx,growy,span,wrap"); // Link the radio buttons GuiUtil.linkRadioButtons(profileRB, customRB); - + // Build the config area tmpPanel = buildConfigPanel(); add(tmpPanel, "growx,span,wrap"); // Action area - closeB = GuiUtil.createJButton("Close", this, smallFont); + closeB = GuiUtil.createJButton("Close", this); add(closeB, "align right,span"); // Add in the default profile - aConfig = new ProfileConfig(AddProfilePanel.DEFAULT_NAME, refItemHandler.getSortedAttributes()); - profileBox.addItem(aConfig); + var tmpConfig = new ProfileConfig<>(ProfileConfig.DEFAULT_NAME, refTableColumnHandler.getOrderedAttributes()); + profileBox.addItem(tmpConfig); - setBorder(new BevelBorder(BevelBorder.RAISED)); + customRB.setSelected(true); } /** - * Utility method to build the configuration area for the individual attributes + * Helper method to build the configuration area for the individual attributes */ - protected JPanel buildConfigPanel() + private JPanel buildConfigPanel() { - JPanel tmpPanel; - // Form the layout - tmpPanel = new JPanel(); + var tmpPanel = new JPanel(); tmpPanel.setLayout(new MigLayout("", "0[grow][][]0", "0[][]10")); // Title Area - labelL = GuiUtil.createJLabel("Label:", smallFont); + labelL = new JLabel("Label:", JLabel.LEADING); tmpPanel.add(labelL, "growx,span 1"); - + upB = GuiUtil.createJButton(new ArrowNorthIcon(14), this); tmpPanel.add(upB, "align right,span 1,w 18!, h 18!"); - + downB = GuiUtil.createJButton(new ArrowSouthIcon(14), this); tmpPanel.add(downB, "align right,span 1,w 18!, h 18!,wrap"); - - labelTF = new GTextField(this); + + labelTF = new GTextField(this); tmpPanel.add(labelTF, "growx,span 3,wrap"); - + return tmpPanel; } /** - * Utility method to build the query item list table + * Helper method to build the query item list table */ - protected JPanel buildItemListTablePanel() + private JPanel buildItemListTablePanel() { - QueryComposer aComposer; - ItemHandler aItemHandler; - - aComposer = new QueryComposer(); - aComposer.addAttribute(ConfigLookUp.IsVisible, Boolean.class, "", ""); - aComposer.addAttribute(ConfigLookUp.Name, String.class, "Name", null); - aComposer.addAttribute(ConfigLookUp.Label, String.class, "Label", null); - + var tmpComposer = new QueryComposer(); + tmpComposer.addAttribute(ConfigLookUp.IsVisible, Boolean.class, "", ""); + tmpComposer.addAttribute(ConfigLookUp.Name, String.class, "Name", null); + tmpComposer.addAttribute(ConfigLookUp.Label, String.class, "Label", null); + col0Editor = new BooleanCellEditor(); col0Editor.addActionListener(this); col0Renderer = new BooleanCellRenderer(); col1Renderer = new DefaultTableCellRenderer(); - aComposer.setEditor(ConfigLookUp.IsVisible, col0Editor); - aComposer.setRenderer(ConfigLookUp.IsVisible, col0Renderer); - aComposer.setRenderer(ConfigLookUp.Name, col1Renderer); - aComposer.setRenderer(ConfigLookUp.Label, col1Renderer); - - aItemHandler = new ConfigHandler(aComposer); - - myItemProcessor = new StaticItemProcessor(); - myItemProcessor.setItems(refItemHandler.getSortedAttributes()); - - listPanel = new ItemListPanel(aItemHandler, myItemProcessor, false, false); + tmpComposer.setEditor(ConfigLookUp.IsVisible, col0Editor); + tmpComposer.setRenderer(ConfigLookUp.IsVisible, col0Renderer); + tmpComposer.setRenderer(ConfigLookUp.Name, col1Renderer); + tmpComposer.setRenderer(ConfigLookUp.Label, col1Renderer); + + var tmpIH = new ConfigHandler(); + myItemProcessor = new StaticItemProcessor<>(); + myItemProcessor.setItems(refTableColumnHandler.getOrderedAttributes()); + + listPanel = new ItemListPanel<>(tmpIH, myItemProcessor, tmpComposer, false); listPanel.setSortingEnabled(false); listPanel.addListSelectionListener(this); return listPanel; } - + + /** + * Helper method that executes the delete profile action. + */ + private void doActionDel() + { + var tmpPick = profileBox.getChosenItem(); + if (tmpPick.name() == ProfileConfig.DEFAULT_NAME) + { + var msgPanel = new MessagePanel(this, "Reserved Profile"); + msgPanel.setInfo("The Default profile can not be deleted."); + msgPanel.setSize(400, 140); + msgPanel.setVisibleAsModal(); + return; + } + + profileBox.removeItem(profileBox.getChosenItem()); + actionPerformed(new ActionEvent(profileBox, ID_UPDATE, null)); + } + /** * Synchronizes the model to match the label gui */ - protected void updateLayerAttribute() + private void updateLayerAttribute() { - QueryAttribute chosenItem; - - chosenItem = listPanel.getSelectedItem(); + var chosenItem = listPanel.getSelectedItem(); chosenItem.label = labelTF.getText(); - + chosenItem.assocTableColumn.setHeaderValue(chosenItem.label); - + listPanel.repaint(); //getParent().repaint(); } @@ -397,16 +430,12 @@ public class EditTablePanel extends GlassPanel implements ActionListener, ZioObj /** * Synchronizes our GUI vars */ - protected void updateGui() + private void updateGui() { - QueryAttribute chosenItem; - boolean isEnabled; - int chosenIndex; - // Update the profile area - isEnabled = profileRB.isSelected(); + var isEnabled = profileRB.isSelected(); profileBox.setEnabled(isEnabled); - + isEnabled = isEnabled & (profileBox.getAllItems().size() > 1); deleteB.setEnabled(isEnabled); @@ -416,20 +445,20 @@ public class EditTablePanel extends GlassPanel implements ActionListener, ZioObj GuiUtil.setEnabled(listPanel, isEnabled); col0Renderer.setEnabled(isEnabled); col1Renderer.setEnabled(isEnabled); - - chosenItem = listPanel.getSelectedItem(); - chosenIndex = myItemProcessor.indexOf(chosenItem); + + var chosenItem = listPanel.getSelectedItem(); + var chosenIndex = myItemProcessor.indexOf(chosenItem); if (chosenItem != null) labelTF.setText(chosenItem.label); isEnabled = isEnabled & (chosenItem != null); labelL.setEnabled(isEnabled); labelTF.setEnabled(isEnabled); - - upB.setEnabled(isEnabled & (chosenIndex > 0)); - downB.setEnabled(isEnabled & (chosenIndex+1 < myItemProcessor.getNumItems())); - listPanel.repaint(); + upB.setEnabled(isEnabled & (chosenIndex > 0)); + downB.setEnabled(isEnabled & (chosenIndex + 1 < myItemProcessor.getNumItems())); + + listPanel.repaint(); } } diff --git a/src/glum/gui/panel/itemList/config/EditTablePanelClassic.java b/src/glum/gui/panel/itemList/config/EditTablePanelClassic.java index 5cacc6f..83301bf 100644 --- a/src/glum/gui/panel/itemList/config/EditTablePanelClassic.java +++ b/src/glum/gui/panel/itemList/config/EditTablePanelClassic.java @@ -1,44 +1,74 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.itemList.config; -import java.awt.*; -import java.awt.event.*; -import java.util.*; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.HashMap; +import java.util.Map; + import javax.swing.*; import javax.swing.border.*; -import glum.gui.*; -import glum.gui.action.*; +import glum.gui.FocusUtil; +import glum.gui.GuiUtil; +import glum.gui.action.ClickAction; import glum.gui.component.GComboBox; -import glum.gui.panel.*; -import glum.gui.panel.itemList.query.*; - +import glum.gui.panel.GlassPanel; +import glum.gui.panel.itemList.TableColumnHandler; +import glum.gui.panel.itemList.query.QueryAttribute; +import glum.gui.panel.itemList.query.QueryItemHandler; import net.miginfocom.swing.MigLayout; -public class EditTablePanelClassic extends GlassPanel implements ActionListener +/** + * UI component that allows for the configuration of ItemListPanels that utilize the {@link QueryAttribute} and + * {@link QueryItemHandler} interfaces. + *

    + * New code should make use of {@link EditTablePanel} rather than {@link EditTablePanelClassic}. + * + * @author lopeznr1 + */ +public class EditTablePanelClassic> extends GlassPanel implements ActionListener { + // Attributes + private final TableColumnHandler refTableColumnHandler; + // GUI vars - protected JLabel titleL; - protected JRadioButton profileRB, customRB; - protected GComboBox profileBox; - protected JButton closeB; + private JLabel titleL; + private JRadioButton profileRB, customRB; + private GComboBox> profileBox; + private JButton closeB; // State vars - protected QueryItemHandler refItemHandler; - protected Map actionMap; + private Map> actionMap; - public EditTablePanelClassic(Component aParent, QueryItemHandler aItemHandler) + /** Standard Constructor */ + public EditTablePanelClassic(Component aParent, TableColumnHandler aTableColumnHandler) { super(aParent); // State vars - refItemHandler = aItemHandler; - actionMap = new HashMap(); + refTableColumnHandler = aTableColumnHandler; + actionMap = new HashMap<>(); // Build the actual GUI buildGuiArea(); setPreferredSize(new Dimension(200, getPreferredSize().height)); updateGui(); - + // Set up some keyboard shortcuts FocusUtil.addAncestorKeyBinding(this, "ESCAPE", new ClickAction(closeB)); } @@ -46,10 +76,7 @@ public class EditTablePanelClassic extends GlassPanel implements ActionListener @Override public void actionPerformed(ActionEvent e) { - Object source; - - source = e.getSource(); - + var source = e.getSource(); if (source == closeB) { setVisible(false); @@ -57,35 +84,29 @@ public class EditTablePanelClassic extends GlassPanel implements ActionListener } else if (source == profileBox || source == profileRB) { - ProfileConfig aConfig; - QueryAttribute aAttribute; - - aConfig = profileBox.getChosenItem(); - refItemHandler.setOrderAndConfig(aConfig.getItems()); - refItemHandler.rebuildTableColumns(); - + var tmpConfig = profileBox.getChosenItem(); + refTableColumnHandler.setOrderAndConfig(tmpConfig.getItems()); + refTableColumnHandler.rebuildTableColumns(); + for (JCheckBox itemCB : actionMap.keySet()) { - aAttribute = actionMap.get(itemCB); - itemCB.setSelected(aAttribute.isVisible); + var tmpAttribute = actionMap.get(itemCB); + itemCB.setSelected(tmpAttribute.isVisible); } } else if (source instanceof JCheckBox) { - QueryAttribute aAttribute; - JCheckBox tmpCB; - - tmpCB = (JCheckBox)source; - aAttribute = actionMap.get(tmpCB); - aAttribute.isVisible = tmpCB.isSelected(); - - refItemHandler.rebuildTableColumns(); + var tmpCB = (JCheckBox) source; + var tmpAttribute = actionMap.get(tmpCB); + tmpAttribute.isVisible = tmpCB.isSelected(); + + refTableColumnHandler.rebuildTableColumns(); } updateGui(); } - public void addConfig(ProfileConfig aConfig) + public void addConfig(ProfileConfig aConfig) { profileBox.addItem(aConfig); } @@ -95,9 +116,6 @@ public class EditTablePanelClassic extends GlassPanel implements ActionListener */ protected void buildGuiArea() { - JPanel tmpPanel; - Border border1, border2; - // Form the grid bag constraints setLayout(new MigLayout("", "[left][grow]", "[]")); @@ -110,7 +128,7 @@ public class EditTablePanelClassic extends GlassPanel implements ActionListener profileRB.addActionListener(this); add(profileRB, "span 1"); - profileBox = new GComboBox(); + profileBox = new GComboBox<>(); profileBox.addActionListener(this); add(profileBox, "growx,span 1,wrap"); @@ -119,9 +137,9 @@ public class EditTablePanelClassic extends GlassPanel implements ActionListener customRB.addActionListener(this); add(customRB, "span 1,wrap"); - tmpPanel = buildQueryItemPanel(); - border1 = new EmptyBorder(0, 10, 0, 10); - border2 = new BevelBorder(BevelBorder.RAISED); + var tmpPanel = buildQueryItemPanel(); + var border1 = new EmptyBorder(0, 10, 0, 10); + var border2 = new BevelBorder(BevelBorder.RAISED); tmpPanel.setBorder(new CompoundBorder(border2, border1)); tmpPanel.setEnabled(false); add(tmpPanel, "growx,span 2,wrap"); @@ -130,45 +148,36 @@ public class EditTablePanelClassic extends GlassPanel implements ActionListener GuiUtil.linkRadioButtons(profileRB, customRB); // Build the default profile box - ProfileConfig aConfig; - aConfig = new ProfileConfig(AddProfilePanel.DEFAULT_NAME, refItemHandler.getSortedAttributes()); - addConfig(aConfig); + var tmpConfig = new ProfileConfig<>(ProfileConfig.DEFAULT_NAME, refTableColumnHandler.getOrderedAttributes()); + addConfig(tmpConfig); // Action area closeB = GuiUtil.createJButton("Close", this); - add(closeB, "align right,span 2"); - - setBorder(new BevelBorder(BevelBorder.RAISED)); + add(closeB, "ax right,span,split"); } protected JPanel buildQueryItemPanel() { - Collection attrList; - JPanel aPanel; - JCheckBox tmpCB; + var retPanel = new JPanel(); + retPanel.setLayout(new BoxLayout(retPanel, BoxLayout.Y_AXIS)); - aPanel = new JPanel(); - aPanel.setLayout(new BoxLayout(aPanel, BoxLayout.Y_AXIS)); - - attrList = refItemHandler.getSortedAttributes(); - for (QueryAttribute aAttr : attrList) + var tmpAttrL = refTableColumnHandler.getOrderedAttributes(); + for (QueryAttribute aAttr : tmpAttrL) { - tmpCB = new JCheckBox(aAttr.label, aAttr.isVisible); + var tmpCB = new JCheckBox(aAttr.label, aAttr.isVisible); tmpCB.addActionListener(this); - aPanel.add(tmpCB); + retPanel.add(tmpCB); actionMap.put(tmpCB, aAttr); } - return aPanel; + return retPanel; } protected void updateGui() { - boolean isEnabled; - // Update the profile area - isEnabled = profileRB.isSelected(); + var isEnabled = profileRB.isSelected(); profileBox.setEnabled(isEnabled); // Update the custom area diff --git a/src/glum/gui/panel/itemList/config/ProfileConfig.java b/src/glum/gui/panel/itemList/config/ProfileConfig.java index 4dec909..8b14cae 100644 --- a/src/glum/gui/panel/itemList/config/ProfileConfig.java +++ b/src/glum/gui/panel/itemList/config/ProfileConfig.java @@ -1,150 +1,186 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.itemList.config; -import glum.gui.panel.itemList.query.QueryAttribute; -import glum.gui.panel.itemList.query.QueryComposer; -import glum.zio.*; - import java.io.IOException; import java.util.*; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; +import com.google.common.collect.ImmutableList; + +import glum.gui.panel.itemList.query.QueryAttribute; +import glum.zio.ZinStream; +import glum.zio.ZoutStream; /** + * Class that stores an ordered list of {@link QueryAttribute}s and the corresponding sort priorities. * - * Class that stores a list of QuerryAttributes used to describe a specific configuration - * of ItemListPanel and associated table. - * + * @author lopeznr1 */ -public class ProfileConfig implements ZioObj +public class ProfileConfig> { - // State vars - protected String myName; - protected Map itemMap; + // Constants + /** String constant that defines the reserved profile name. */ + public static final String DEFAULT_NAME = "Default"; + + // Attributes + private final String name; + private Map> itemM; + private final ImmutableList sortPriorityL; /** - * Constructor. Form a deep copy of the list of QueryAttributes. This is to - * ensure that this profile will not share QueryAttributes with other profiles - * leading to inadvertent tampering. - * - * @param aName: Name of the profile - * @param aItemSet: Set of all QuerryAttributes associated with this profile - */ - public ProfileConfig(String aName, Collection aItemList) + * Standard Constructor. Form a deep copy of the list of {@link QueryAttribute}s. This is to ensure that this profile + * will not share {@link QueryAttribute}s with other profiles leading to inadvertent tampering. + * + * @param aName: + * Name of the profile + * @param aItemSet: + * Set of all QuerryAttributes associated with this profile + */ + public ProfileConfig(String aName, Collection> aItemC, Collection aSortPriorityC) { - myName = aName; - - itemMap = Maps.newLinkedHashMap(); - for (QueryAttribute aItem : aItemList) - itemMap.put(aItem.modelIndex, new QueryAttribute(aItem)); - } - - /** - * Constructor. All of the QueryAttribute will be turned off by default. - * It is imperative that you call setVisibleItems() to allow this profile - * to display columns when activated. - * - * @param aName: Name of the profile - * @param aComposer: The composer which provides all QuerryAttributes - * associated with this profile - */ - public ProfileConfig(String aName, QueryComposer aComposer) - { - this(aName, aComposer.getItems()); + name = aName; - for (QueryAttribute aItem : itemMap.values()) - aItem.isVisible = false; + itemM = new LinkedHashMap<>(); + for (QueryAttribute aItem : aItemC) + itemM.put(aItem.modelIndex, new QueryAttribute<>(aItem)); + + sortPriorityL = ImmutableList.copyOf(aSortPriorityC); + + // TODO: Make this object fully immutable } - + /** - * Sets the specified QueryAttributes as visible for this ProfileConfig + * Simplified Constructor. Form a deep copy of the list of {@link QueryAttribute}s. This is to ensure that this + * profile will not share {@link QueryAttribute}s with other profiles leading to inadvertent tampering. + * + * @param aName: + * Name of the profile + * @param aItemSet: + * Set of all QuerryAttributes associated with this profile */ - public void setVisibleItems(Collection aItemList) + public ProfileConfig(String aName, Collection> aItemL) { - for (QueryAttribute aItem : aItemList) - itemMap.get(aItem.modelIndex).isVisible = true; - } - - /** - * Sets the specified QueryAttributes as visible for this ProfileConfig - */ - public void setVisibleItems(QueryAttribute... aItemList) - { - for (QueryAttribute aItem : aItemList) - itemMap.get(aItem.modelIndex).isVisible = true; + this(aName, aItemL, ImmutableList.of()); } /** * Returns the (display) name of this ProfileConfig */ - public String getName() + public String name() { - return myName; + return name; } /** - * Returns the QueryAttributes associated with this ProfileConfig + * Returns the {@link QueryAttribute}s associated with this ProfileConfig */ - public ArrayList getItems() + public ArrayList> getItems() { - return Lists.newArrayList(itemMap.values()); + return new ArrayList<>(itemM.values()); } - @Override - public void zioRead(ZinStream aStream) throws IOException + /** + * Returns a list of model indexes which define the priority of the associated {@link QueryAttribute}s. + */ + public ImmutableList sortPriorityList() { - Map newMap; - QueryAttribute queryAttr; - int numItems, modelId; - - aStream.readVersion(0); - - myName = aStream.readString(); - - numItems = aStream.readInt(); - if (numItems != itemMap.size()) - throw new IOException("Mismatched attribute count. Expected: " + itemMap.size() + " Read: " + numItems); - - newMap = Maps.newLinkedHashMap(); - for (int c1 = 0; c1 < numItems; c1++) - { - modelId = aStream.readInt(); - - queryAttr = itemMap.get(modelId); - queryAttr.zioRead(aStream); - - newMap.put(modelId, queryAttr); - } - - itemMap = newMap; - } - - @Override - public void zioWrite(ZoutStream aStream) throws IOException - { - QueryAttribute queryAttr; - int numItems; - - aStream.writeVersion(0); - - aStream.writeString(myName); - - numItems = itemMap.size(); - aStream.writeInt(numItems); - - for (int aModelId : itemMap.keySet()) - { - queryAttr = itemMap.get(aModelId); - - aStream.writeInt(aModelId); - queryAttr.zioWrite(aStream); - } + return sortPriorityL; } @Override public String toString() { - return myName; + return name; + } + + /** + * Utility method to read the {@link ProfileConfig} from the specified stream. + */ + public static > ProfileConfig zioRead(ZinStream aStream, Collection> aItemC) + throws IOException + { + var itemM = new LinkedHashMap>(); + for (QueryAttribute aItem : aItemC) + itemM.put(aItem.modelIndex, new QueryAttribute<>(aItem)); + + aStream.readVersion(1); + + var name = aStream.readString(); + + var doRestore = true; + var numItems = aStream.readInt(); + if (numItems != itemM.size()) + { + var tmpMsg = "Mismatched attribute count. Expected: " + itemM.size() + " Read: " + numItems; + tmpMsg += "\n\tTable configuration, " + name + ", will not be restored..."; + System.err.println(tmpMsg); + doRestore = false; + } + + var newItemL = new ArrayList>(); + for (int c1 = 0; c1 < numItems; c1++) + { + var modelId = aStream.readInt(); + + var queryAttr = itemM.get(modelId); + if (queryAttr == null || doRestore == false) + { + // Skip over unsupported QueryAttributes + var tmpAttr = new QueryAttribute(null, null, -1); + tmpAttr.zioRead(aStream); + continue; + } + queryAttr.zioRead(aStream); + + newItemL.add(queryAttr); + } + + var priCnt = aStream.readInt(); + var tmpSortPriL = new ArrayList(priCnt); + for (var c1 = 0; c1 < priCnt; c1++) + tmpSortPriL.add(aStream.readInt()); + + if (doRestore == false) + { + newItemL = new ArrayList>(aItemC); + tmpSortPriL = new ArrayList(priCnt); + } + + return new ProfileConfig<>(name, newItemL, tmpSortPriL); + } + + /** + * Utility method to write the {@link ProfileConfig} to the specified stream. + */ + public static void zioWrite(ZoutStream aStream, ProfileConfig aItem) throws IOException + { + aStream.writeVersion(1); + + aStream.writeString(aItem.name); + + aStream.writeInt(aItem.itemM.size()); + for (int aModelId : aItem.itemM.keySet()) + { + var queryAttr = aItem.itemM.get(aModelId); + + aStream.writeInt(aModelId); + queryAttr.zioWrite(aStream); + } + + aStream.writeInt(aItem.sortPriorityL.size()); + for (var aModelId : aItem.sortPriorityL) + aStream.writeInt(aModelId); } } diff --git a/src/glum/gui/panel/itemList/query/QueryAttribute.java b/src/glum/gui/panel/itemList/query/QueryAttribute.java index 95f822c..9e1d94d 100644 --- a/src/glum/gui/panel/itemList/query/QueryAttribute.java +++ b/src/glum/gui/panel/itemList/query/QueryAttribute.java @@ -1,22 +1,45 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.itemList.query; import java.io.IOException; +import java.util.Comparator; import javax.swing.JLabel; import javax.swing.table.*; +import glum.gui.table.SortDir; import glum.unit.EmptyUnitProvider; import glum.unit.UnitProvider; import glum.zio.*; -public class QueryAttribute implements ZioObj +/** + * Class that provides for the definition of attributes associated with a specific data field. + * + * @author lopeznr1 + */ +public class QueryAttribute> implements ZioObj { - // State vars + // Attributes + public final G1 refKey; public final int modelIndex; - public Enum refKey; - public Class refClass; + public final Class refClass; + + // State vars + public Comparator refComparator; public UnitProvider refUnitProvider; - + // Config vars public String label; public boolean isVisible; @@ -24,38 +47,52 @@ public class QueryAttribute implements ZioObj public int defaultSize; public int minSize; public int maxSize; - public int sortDir; + public SortDir sortDir; // Helper vars public TableColumn assocTableColumn; public TableCellRenderer renderer; public TableCellEditor editor; - public QueryAttribute(int aModelIndex) + /** + * Standard Constructor. + * + * @param aRefKey + * @param aRefClass + * @param aModelIndex + */ + public QueryAttribute(G1 aRefKey, Class aRefClass, int aModelIndex) { + refKey = aRefKey; + refClass = aRefClass; modelIndex = aModelIndex; - refKey = null; - refClass = String.class; - label = ""; + refComparator = null; + refUnitProvider = new EmptyUnitProvider(); + + label = ""; isVisible = true; alignment = JLabel.LEFT; defaultSize = 100; maxSize = -1; minSize = -1; - sortDir = 0; - refUnitProvider = new EmptyUnitProvider(); + sortDir = SortDir.NotSorted; assocTableColumn = null; renderer = null; editor = null; } - - public QueryAttribute(QueryAttribute aAttribute) + + /** + * Copy Constructor. + * + * @param aAttribute + */ + public QueryAttribute(QueryAttribute aAttribute) { // Synchronize the attribute before copying the configuration aAttribute.synchronizeAttribute(); - + modelIndex = aAttribute.modelIndex; refKey = aAttribute.refKey; refClass = aAttribute.refClass; @@ -70,17 +107,16 @@ public class QueryAttribute implements ZioObj refUnitProvider = aAttribute.refUnitProvider; assocTableColumn = null; - renderer = null; //aAttribute.renderer; - editor = null; //aAttribute.editor; + renderer = null; // aAttribute.renderer; + editor = null; // aAttribute.editor; } - + /** * Sets this QueryAttribute to match aAttribute. - *

    - * Currently only the following config vars are matched: - * label, isVisible, alignment, defaultSize, sortDir + *

    + * Currently only the following config vars are matched: label, isVisible, alignment, defaultSize, sortDir */ - public void setConfig(QueryAttribute aAttribute) + public void setConfig(QueryAttribute aAttribute) { label = aAttribute.label; isVisible = aAttribute.isVisible; @@ -96,7 +132,7 @@ public class QueryAttribute implements ZioObj { if (assocTableColumn == null) return; - + defaultSize = assocTableColumn.getWidth(); // sortDir = assocTableColumn. } @@ -109,7 +145,7 @@ public class QueryAttribute implements ZioObj if (assocTableColumn == null) return; -System.out.println("Are we ready for this ???"); + System.out.println("Are we ready for this ???"); assocTableColumn.setWidth(defaultSize); // sortDir = assocTableColumn. } @@ -118,14 +154,12 @@ System.out.println("Are we ready for this ???"); public void zioRead(ZinStream aStream) throws IOException { aStream.readVersion(0); - + label = aStream.readString(); isVisible = aStream.readBool(); alignment = aStream.readInt(); defaultSize = aStream.readInt(); - minSize = aStream.readInt(); - maxSize = aStream.readInt(); - sortDir = aStream.readInt(); + sortDir = aStream.readEnum(SortDir.values()); } @Override @@ -133,16 +167,14 @@ System.out.println("Are we ready for this ???"); { // Synchronize the attribute before serialization synchronizeAttribute(); - + aStream.writeVersion(0); - + aStream.writeString(label); aStream.writeBool(isVisible); aStream.writeInt(alignment); aStream.writeInt(defaultSize); - aStream.writeInt(minSize); - aStream.writeInt(maxSize); - aStream.writeInt(sortDir); + aStream.writeEnum(sortDir); } - + } diff --git a/src/glum/gui/panel/itemList/query/QueryComposer.java b/src/glum/gui/panel/itemList/query/QueryComposer.java index a9d6dfa..6284e15 100644 --- a/src/glum/gui/panel/itemList/query/QueryComposer.java +++ b/src/glum/gui/panel/itemList/query/QueryComposer.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.itemList.query; import java.util.*; @@ -9,34 +22,44 @@ import javax.swing.table.TableCellRenderer; import com.google.common.base.Preconditions; +import glum.gui.panel.itemList.ItemListPanel; import glum.unit.UnitProvider; +/** + * Object which allows a collection of QueryAttributes to be programmatically constructed. This object provides for a + * simplified and uniform method for construction the {@link QueryAttribute}s that will be associated with a + * {@link ItemListPanel}. + * + * @param + * + * @author lopeznr1 + */ public class QueryComposer> { // State vars - protected ArrayList itemList; -// protected Map itemMap; + protected ArrayList> itemL; +// protected Map> itemM; public QueryComposer() { - itemList = new ArrayList<>(); -// itemMap = Maps.newLinkedHashMap(); + itemL = new ArrayList<>(); +// itemM = new LinkedHashMap<>(); } /** * Return the QueryAttribute located at aIndex */ - public QueryAttribute get(int aIndex) + public QueryAttribute get(int aIndex) { - return itemList.get(aIndex); + return itemL.get(aIndex); } /** * Return the QueryAttribute associated with aRefKey */ - public QueryAttribute getItem(G1 aRefKey) + public QueryAttribute getItem(G1 aRefKey) { - for (QueryAttribute aItem : itemList) + for (QueryAttribute aItem : itemL) { if (aItem.refKey == aRefKey) return aItem; @@ -48,39 +71,39 @@ public class QueryComposer> /** * Returns a listing of all the QueryAttributes that were composed */ - public Collection getItems() + public Collection> getItems() { - return new ArrayList<>(itemList); + return new ArrayList<>(itemL); } /** * Returns a listing of the items found between sIndex, eIndex (inclusive) */ - public Collection getItemsFrom(int sIndex, int eIndex) + public Collection> getItemsFrom(int sIndex, int eIndex) { - List rList; + List> retL; - rList = new ArrayList<>((eIndex - sIndex) + 1); + retL = new ArrayList<>((eIndex - sIndex) + 1); for (int c1 = sIndex; c1 <= eIndex; c1++) - rList.add(itemList.get(c1)); + retL.add(itemL.get(c1)); - return rList; + return retL; } /** * Returns a listing of the items found between sKey, eKey (inclusive) */ - public Collection getItemsFrom(G1 sKey, G1 eKey) + public Collection> getItemsFrom(G1 sKey, G1 eKey) { int sIndex, eIndex; - // Locate the indexes for the coresponding elements + // Locate the indexes for the corresponding elements sIndex = eIndex = -1; - for (int c1 = 0; c1 < itemList.size(); c1++) + for (int c1 = 0; c1 < itemL.size(); c1++) { - if (itemList.get(c1).refKey == sKey) + if (itemL.get(c1).refKey == sKey) sIndex = c1; - if (itemList.get(c1).refKey == eKey) + if (itemL.get(c1).refKey == eKey) eIndex = c1; } @@ -100,35 +123,36 @@ public class QueryComposer> */ public int size() { - return itemList.size(); + return itemL.size(); } /** * Method to add a QueryAttribute to this container */ - public QueryAttribute addAttribute(G1 aRefKey, UnitProvider aUnitProvider, String aName, String maxValue) + public QueryAttribute addAttribute(G1 aRefKey, UnitProvider aUnitProvider, String aName, String maxValue) { return addAttribute(aRefKey, aUnitProvider, aName, maxValue, true); } - public QueryAttribute addAttribute(G1 aRefKey, UnitProvider aUnitProvider, String aName, String maxValue, boolean isVisible) + public QueryAttribute addAttribute(G1 aRefKey, UnitProvider aUnitProvider, String aName, String maxValue, + boolean isVisible) { - QueryAttribute aAttribute; + QueryAttribute retAttribute; // Insanity check Preconditions.checkNotNull(aUnitProvider); - aAttribute = addAttribute(aRefKey, Double.class, aName, maxValue, isVisible); - aAttribute.refUnitProvider = aUnitProvider; - return aAttribute; + retAttribute = addAttribute(aRefKey, Double.class, aName, maxValue, isVisible); + retAttribute.refUnitProvider = aUnitProvider; + return retAttribute; } - public QueryAttribute addAttribute(G1 aRefKey, Class aClass, String aName, String maxValue) + public QueryAttribute addAttribute(G1 aRefKey, Class aClass, String aName, String maxValue) { return addAttribute(aRefKey, aClass, aName, maxValue, true); } - public QueryAttribute addAttribute(G1 aRefKey, Class aClass, String aName, String maxValue, boolean isVisible) + public QueryAttribute addAttribute(G1 aRefKey, Class aClass, String aName, String maxValue, boolean isVisible) { int maxSize; @@ -140,14 +164,14 @@ public class QueryComposer> return addAttribute(aRefKey, aClass, aName, maxSize, isVisible); } - public QueryAttribute addAttribute(G1 aRefKey, Class aClass, String aName, int aMaxSize) + public QueryAttribute addAttribute(G1 aRefKey, Class aClass, String aName, int aMaxSize) { return addAttribute(aRefKey, aClass, aName, aMaxSize, true); } - public QueryAttribute addAttribute(G1 aRefKey, Class aClass, String aLabel, int aMaxSize, boolean isVisible) + public QueryAttribute addAttribute(G1 aRefKey, Class aClass, String aLabel, int aMaxSize, boolean isVisible) { - QueryAttribute aAttribute; + QueryAttribute retAttribute; int defaultSize, minSize, maxSize; // Get the defaultSize @@ -167,28 +191,26 @@ public class QueryComposer> defaultSize = maxSize; // Form the attribute - aAttribute = new QueryAttribute(itemList.size()); - aAttribute.refKey = aRefKey; - aAttribute.refClass = aClass; - aAttribute.label = aLabel; - aAttribute.defaultSize = defaultSize; - aAttribute.maxSize = maxSize; - aAttribute.minSize = minSize; - aAttribute.isVisible = isVisible; + retAttribute = new QueryAttribute<>(aRefKey, aClass, itemL.size()); + retAttribute.label = aLabel; + retAttribute.defaultSize = defaultSize; + retAttribute.maxSize = maxSize; + retAttribute.minSize = minSize; + retAttribute.isVisible = isVisible; - itemList.add(aAttribute); - return aAttribute; + itemL.add(retAttribute); + return retAttribute; } - public QueryAttribute addAttribute(G1 aRefKey, Enum enumSet[], String aName) + public QueryAttribute addAttribute(G1 aRefKey, Enum enumSet[], String aName) { return addAttribute(aRefKey, enumSet, aName, true); } - public QueryAttribute addAttribute(G1 aRefKey, Enum enumSet[], String aLabel, boolean isVisible) + public QueryAttribute addAttribute(G1 aRefKey, Enum enumSet[], String aLabel, boolean isVisible) { - QueryAttribute aAttribute; - int defaultSize, aSize; + QueryAttribute retAttribute; + int defaultSize, tmpSize; // Get the defaultSize defaultSize = 10; @@ -197,23 +219,21 @@ public class QueryComposer> for (Enum aEnum : enumSet) { - aSize = computeStringWidth(aEnum.toString()); - if (aSize > defaultSize) - defaultSize = aSize; + tmpSize = computeStringWidth(aEnum.toString()); + if (tmpSize > defaultSize) + defaultSize = tmpSize; } // Form the attribute - aAttribute = new QueryAttribute(itemList.size()); - aAttribute.refKey = aRefKey; - aAttribute.refClass = Enum.class; - aAttribute.label = aLabel; - aAttribute.defaultSize = defaultSize; - aAttribute.maxSize = defaultSize; - aAttribute.minSize = 15; - aAttribute.isVisible = isVisible; + retAttribute = new QueryAttribute<>(aRefKey, Enum.class, itemL.size()); + retAttribute.label = aLabel; + retAttribute.defaultSize = defaultSize; + retAttribute.maxSize = defaultSize; + retAttribute.minSize = 15; + retAttribute.isVisible = isVisible; - itemList.add(aAttribute); - return aAttribute; + itemL.add(retAttribute); + return retAttribute; } /** @@ -221,7 +241,7 @@ public class QueryComposer> */ public void setEditor(G1 aRefKey, TableCellEditor aEditor) { - for (QueryAttribute aItem : itemList) + for (QueryAttribute aItem : itemL) { if (aItem.refKey == aRefKey) { @@ -238,16 +258,18 @@ public class QueryComposer> */ public void setRenderer(G1 aRefKey, TableCellRenderer aRenderer) { - for (QueryAttribute aItem : itemList) + boolean isPass = false; + for (QueryAttribute aItem : itemL) { if (aItem.refKey == aRefKey) { aItem.renderer = aRenderer; - return; + isPass = true; } } - throw new RuntimeException("No item found with the key:" + aRefKey); + if (isPass == false) + throw new RuntimeException("No item found with the key:" + aRefKey); } /** diff --git a/src/glum/gui/panel/itemList/query/QueryItemHandler.java b/src/glum/gui/panel/itemList/query/QueryItemHandler.java index 61de190..10c6c34 100644 --- a/src/glum/gui/panel/itemList/query/QueryItemHandler.java +++ b/src/glum/gui/panel/itemList/query/QueryItemHandler.java @@ -1,58 +1,41 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.itemList.query; -import java.util.*; - import glum.database.QueryItem; -import glum.gui.panel.itemList.BasicItemHandler; +import glum.gui.panel.itemList.ItemHandler; -public class QueryItemHandler> extends BasicItemHandler +/** + * Implementation of {@link ItemHandler} that allows for tabular access of items that implement the {@link QueryItem} + * interface. + * + * @author lopeznr1 + */ +public class QueryItemHandler, G2 extends Enum> implements ItemHandler { - public QueryItemHandler(QueryComposer aComposer) + @Override + public Object getValue(G1 aItem, G2 aEnum) { - super(aComposer.getItems()); - } - - public QueryItemHandler(Collection aQueryAttrList) - { - super(aQueryAttrList); + // Delegate + return aItem.getValue(aEnum); } @Override - public Object getColumnValue(G1 aObj, int colNum) + public void setValue(G1 aItem, G2 aEnum, Object aValue) { - QueryItem> aItem; - - // Insanity check - if (colNum < 0 && colNum >= fullAttributeList.size()) - return null; - -// return aObj.getValue(fullAttributeList.get(colNum).refKey); - aItem = null; - if (aObj instanceof QueryItem) - { - aItem = (QueryItem>)aObj; - return aItem.getValue(fullAttributeList.get(colNum).refKey); - } - - return null; - } - - @Override - public void setColumnValue(G1 aObj, int colNum, Object aValue) - { - QueryItem> aItem; - - // Insanity check - if (colNum < 0 && colNum >= fullAttributeList.size()) - return; - -// aObj.setValue(fullAttributeList.get(colNum).refKey, aValue); - aItem = null; - if (aObj instanceof QueryItem) - { - aItem = (QueryItem>)aObj; - aItem.setValue(fullAttributeList.get(colNum).refKey, aValue); - } + // Delegate + aItem.setValue(aEnum, aValue); } } diff --git a/src/glum/gui/panel/itemList/query/QueryTableCellRenderer.java b/src/glum/gui/panel/itemList/query/QueryTableCellRenderer.java index cf8e7cc..3781fc4 100644 --- a/src/glum/gui/panel/itemList/query/QueryTableCellRenderer.java +++ b/src/glum/gui/panel/itemList/query/QueryTableCellRenderer.java @@ -1,20 +1,38 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.itemList.query; import java.awt.Component; + +import javax.swing.JLabel; +import javax.swing.JTable; import javax.swing.table.DefaultTableCellRenderer; -import javax.swing.*; import glum.unit.Unit; +/** + * TableCellRenderer used to render a QueryAttribute with the appropriate unit. + * + * @author lopeznr1 + */ public class QueryTableCellRenderer extends DefaultTableCellRenderer { // State vars - protected Unit myUnit; + private Unit myUnit; - public QueryTableCellRenderer(QueryAttribute aAttribute) + public QueryTableCellRenderer(QueryAttribute aAttribute) { - super(); - myUnit = null; if (aAttribute != null) { @@ -37,12 +55,11 @@ public class QueryTableCellRenderer extends DefaultTableCellRenderer } @Override - public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, + int row, int column) { - JLabel retLabel; - // No special processing is needed if no unit specified - retLabel = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + var retLabel = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); if (myUnit == null) return retLabel; diff --git a/src/glum/gui/panel/nub/Action.java b/src/glum/gui/panel/nub/Action.java index 001f3de..29b8794 100644 --- a/src/glum/gui/panel/nub/Action.java +++ b/src/glum/gui/panel/nub/Action.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.nub; public enum Action diff --git a/src/glum/gui/panel/nub/HorizontalNub.java b/src/glum/gui/panel/nub/HorizontalNub.java index 37a78c5..10f4008 100644 --- a/src/glum/gui/panel/nub/HorizontalNub.java +++ b/src/glum/gui/panel/nub/HorizontalNub.java @@ -1,29 +1,40 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.nub; +import java.awt.*; +import java.awt.event.*; + +import javax.swing.*; + import glum.gui.panel.WaftPanel; import glum.util.MathUtil; -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Point; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.awt.event.MouseMotionListener; -import javax.swing.JComponent; -import javax.swing.JLayeredPane; -import javax.swing.SwingUtilities; - +/** + * UI component that provides the (top and south) control used to drag glass panels. + * + * @author lopeznr1 + */ public class HorizontalNub extends JComponent implements MouseListener, MouseMotionListener { // Constants private static final int DEFAULT_CORNER_DIM = 20; - + // Gui vars protected JComponent rootComp; protected WaftPanel waftPanel; - + // Hook vars protected Dimension hookDim; protected Point hookLoc; @@ -33,40 +44,39 @@ public class HorizontalNub extends JComponent implements MouseListener, MouseMot protected Action action; protected boolean isNorth; protected int cornerDim; - + // Color vars protected Color plainColor; protected Color hookColor; protected Color armedColor; - + + /** Standard Constructor */ public HorizontalNub(JComponent aRootComp, WaftPanel aWaftPanel, boolean aIsNorth) { - super(); - rootComp = aRootComp; waftPanel = aWaftPanel; - + hookDim = null; hookLoc = null; hookPt = null; - + action = Action.None; isNorth = aIsNorth; cornerDim = DEFAULT_CORNER_DIM; - + plainColor = Color.GRAY; hookColor = Color.RED.darker(); armedColor = Color.RED.darker().darker(); - + // Register for events of interest addMouseMotionListener(this); addMouseListener(this); - + // Set the dimensions of this component setMinimumSize(new Dimension(50, 6)); setPreferredSize(new Dimension(50, 6)); } - + /** * Sets in whether there will be a sub nub to grab hold that will allow the end user to resize the associated * WaftPanel. @@ -76,10 +86,10 @@ public class HorizontalNub extends JComponent implements MouseListener, MouseMot cornerDim = DEFAULT_CORNER_DIM; if (aBool == false) cornerDim = 0; - + repaint(); } - + @Override public void mouseClicked(MouseEvent aEvent) { @@ -89,19 +99,17 @@ public class HorizontalNub extends JComponent implements MouseListener, MouseMot @Override public void mousePressed(MouseEvent aEvent) { - Point mousePt; - // Push the waftPanel to the top of the (stack) view if (rootComp instanceof JLayeredPane) - ((JLayeredPane)rootComp).moveToFront(waftPanel); + ((JLayeredPane) rootComp).moveToFront(waftPanel); - mousePt = aEvent.getPoint(); + var mousePt = aEvent.getPoint(); action = computeMode(mousePt); - + hookDim = new Dimension(waftPanel.getSize()); hookLoc = new Point(waftPanel.getLocation()); hookPt = SwingUtilities.convertPoint(this, mousePt, rootComp); - + repaint(); } @@ -111,7 +119,7 @@ public class HorizontalNub extends JComponent implements MouseListener, MouseMot hookDim = null; hookLoc = null; hookPt = null; - + action = computeMode(aEvent.getPoint()); repaint(); } @@ -128,18 +136,18 @@ public class HorizontalNub extends JComponent implements MouseListener, MouseMot // Bail if we are in an active state if (hookPt != null) return; - + action = Action.None; repaint(); } - + @Override public void mouseDragged(MouseEvent aEvent) { // Bail if we are not in an active state if (hookPt == null) return; - + // Transform the reference WaftPanel tranformWaftPanel(aEvent); } @@ -150,49 +158,43 @@ public class HorizontalNub extends JComponent implements MouseListener, MouseMot action = computeMode(aEvent.getPoint()); repaint(); } - + @Override public void paint(Graphics g) { - Graphics2D g2d; - int width, height; - boolean isArmed; - - height = getHeight(); - width = getWidth(); - - g2d = (Graphics2D)g; - + var height = getHeight(); + var width = getWidth(); + + var g2d = (Graphics2D) g; + // Left nub if (cornerDim > 0) { - isArmed = action == Action.ResizeNW || action == Action.ResizeSW; - drawHandle(g2d, 0, 0, cornerDim, height, isArmed); - + var isArmed = action == Action.ResizeNW || action == Action.ResizeSW; + drawHandle(g2d, 0, 0, cornerDim, height, isArmed); + // Right nub isArmed = action == Action.ResizeNE || action == Action.ResizeSE; - drawHandle(g2d, width-cornerDim, 0, cornerDim, height, isArmed); + drawHandle(g2d, width - cornerDim, 0, cornerDim, height, isArmed); } // Middle nub - drawHandle(g2d, cornerDim, 0, width - (2 * cornerDim), height, action == Action.Move); + drawHandle(g2d, cornerDim, 0, width - (2 * cornerDim), height, action == Action.Move); } - + /** * Helper method to update the mode based on the mouse position */ private Action computeMode(Point mousePt) { - int mouseX, mouseY; - - mouseX = mousePt.x; - mouseY = mousePt.y; - + var mouseX = mousePt.x; + var mouseY = mousePt.y; + if (mouseX < 0 || mouseX >= getWidth()) return Action.None; if (mouseY < 0 || mouseY >= getHeight()) return Action.None; - + if (isNorth == true) { if (mouseX < cornerDim) @@ -216,80 +218,73 @@ public class HorizontalNub extends JComponent implements MouseListener, MouseMot */ protected void drawHandle(Graphics2D g2d, int x, int y, int width, int height, boolean isArmed) { - Color aColor; - - aColor = plainColor; + var tmpColor = plainColor; if (isArmed == true) { - aColor = armedColor; + tmpColor = armedColor; if (hookPt != null) - aColor = hookColor; + tmpColor = hookColor; } - - g2d.setColor(aColor); - g2d.fill3DRect(x, y, width, height, true); + + g2d.setColor(tmpColor); + g2d.fill3DRect(x, y, width, height, true); } - + /** * Helper method to transform the associated WaftPanel based on the drag event and the current action. */ protected void tranformWaftPanel(MouseEvent aEvent) { - Dimension minDim, targDim; - Point targLoc; - Point mousePt; - Point nwPt, sePt; - int maxX, maxY; - int diffX, diffY; - - maxX = rootComp.getWidth(); - maxY = rootComp.getHeight(); - - minDim = waftPanel.getMinimumSize(); - - // Retrieve the mousePt in the rootComp coordinate system and force it to be in the bounds of the rootComp. - mousePt = aEvent.getPoint(); + var maxX = rootComp.getWidth(); + var maxY = rootComp.getHeight(); + + var minDim = waftPanel.getMinimumSize(); + + // Retrieve the mousePt in the rootComp coordinate system and force it to be in the bounds of the rootComp. + var mousePt = aEvent.getPoint(); mousePt = SwingUtilities.convertPoint(this, mousePt, rootComp); - mousePt.x = MathUtil.boundRange(0, maxX, mousePt.x); - mousePt.y = MathUtil.boundRange(0, maxY, mousePt.y); - + mousePt.x = MathUtil.boundRange(0, maxX, mousePt.x); + mousePt.y = MathUtil.boundRange(0, maxY, mousePt.y); + // Compute the total change in the mouse movement from the hooked point - diffX = -(mousePt.x - hookPt.x); - diffY = -(mousePt.y - hookPt.y); - - // Retrieve the northwest and southeast points in the root coordinate system - nwPt = new Point(0, 0); + var diffX = -(mousePt.x - hookPt.x); + var diffY = -(mousePt.y - hookPt.y); + + // Retrieve the northwest and southeast points in the root coordinate system + var nwPt = new Point(0, 0); nwPt = SwingUtilities.convertPoint(waftPanel, nwPt, rootComp); - sePt = new Point(waftPanel.getWidth(), waftPanel.getHeight()); + var sePt = new Point(waftPanel.getWidth(), waftPanel.getHeight()); sePt = SwingUtilities.convertPoint(waftPanel, sePt, rootComp); // Determine the target transformations based on the action + Point targLoc; + Dimension targDim; if (action == Action.ResizeNW) { targLoc = new Point(hookLoc.x - diffX, hookLoc.y - diffY); targDim = new Dimension(hookDim.width + diffX, hookDim.height + diffY); - + MathUtil.forceConstraints(targLoc, targDim, 0, 0, sePt.x, sePt.y, minDim, false); } else if (action == Action.ResizeNE) { targLoc = new Point(hookLoc.x, hookLoc.y - diffY); targDim = new Dimension(hookDim.width - diffX, hookDim.height + diffY); - + MathUtil.forceConstraints(targLoc, targDim, nwPt.x, 0, maxX, sePt.y, minDim, false); } else if (action == Action.ResizeSW) { targLoc = new Point(hookLoc.x - diffX, hookLoc.y); targDim = new Dimension(hookDim.width + diffX, hookDim.height - diffY); - + MathUtil.forceConstraints(targLoc, targDim, 0, nwPt.y, sePt.x, maxY, minDim, false); } else if (action == Action.ResizeSE) { targLoc = new Point(hookLoc.x, hookLoc.y); targDim = new Dimension(hookDim.width - diffX, hookDim.height - diffY); - + MathUtil.forceConstraints(targLoc, targDim, nwPt.x, nwPt.y, maxX, maxY, minDim, false); } else if (action == Action.Move) @@ -304,7 +299,7 @@ public class HorizontalNub extends JComponent implements MouseListener, MouseMot { throw new RuntimeException("Unsupported action: " + action); } - + // Update the waftPanel with the target transformations waftPanel.setLocation(targLoc); waftPanel.setPreferredSize(targDim); diff --git a/src/glum/gui/panel/task/AutoTaskPanel.java b/src/glum/gui/panel/task/AutoTaskPanel.java index 2044665..8ba6dd0 100644 --- a/src/glum/gui/panel/task/AutoTaskPanel.java +++ b/src/glum/gui/panel/task/AutoTaskPanel.java @@ -1,105 +1,112 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.task; +import java.awt.Component; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.*; + import glum.gui.FocusUtil; import glum.gui.GuiUtil; import glum.gui.action.ClickAction; import glum.gui.component.GLabel; import glum.gui.panel.generic.GenericCodes; -import glum.unit.ConstUnitProvider; -import glum.unit.NumberUnit; -import glum.unit.TimeCountUnit; -import glum.unit.UnitProvider; -import java.awt.Component; -import java.awt.Font; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JScrollPane; -import javax.swing.JTextField; -import javax.swing.border.BevelBorder; -import javax.swing.border.Border; +import glum.task.Task; +import glum.unit.*; import net.miginfocom.swing.MigLayout; /** - * TaskPanel component that shows the progress of a Task. The panel itself is a Task. - * This TaskPanel will typically disappear once the progress reaches 100% or the task is - * no longer valid. This task may be stopped prematurely if the user hits the abort button. + * GUI component that shows the progress of a "task". The panel itself is a {@link Task}. This TaskPanel will typically + * disappear once the progress reaches 100% or the task is no longer valid. This task may be stopped prematurely if the + * user hits the abort button. + * + * @author lopeznr1 */ public class AutoTaskPanel extends BaseTaskPanel implements ActionListener, GenericCodes { // GUI vars private JButton abortB; - + + /** + * Standard Constructor + */ + public AutoTaskPanel(Component aParent, boolean aHasInfoArea, boolean aHasStatusArea) + { + super(aParent); + + buildGuiArea(aHasInfoArea, aHasStatusArea); + setSize(450, getPreferredSize().height); + setTitle("Task Progress"); + + // Set up keyboard short cuts + FocusUtil.addAncestorKeyBinding(this, "ESCAPE", new ClickAction(abortB)); + + updateGui(); + } + + /** + * Simplified Constructor + */ public AutoTaskPanel(Component aParent) { this(aParent, false, true); } - public AutoTaskPanel(Component aParent, boolean hasInfoArea, boolean hasStatusArea) - { - super(aParent); - - buildGuiArea(hasInfoArea, hasStatusArea); - setSize(450, getPreferredSize().height); - setTitle("Task Progress"); - - // Set up keyboard short cuts - FocusUtil.addAncestorKeyBinding(this, "ESCAPE", new ClickAction(abortB)); - - updateGui(); - } - @Override public void actionPerformed(ActionEvent aEvent) { - Object source; - - source = aEvent.getSource(); + var source = aEvent.getSource(); if (source == abortB) { abort(); - notifyListeners(this, ID_CANCEL, "Abort"); + notifyListeners(this, ID_CANCEL, "Abort"); setVisible(false); } - + updateGui(); } - + /** * Forms the actual GUI */ protected void buildGuiArea(boolean hasInfoArea, boolean hasStatusArea) { - JLabel tmpL; - JScrollPane tmpScrollPane; - UnitProvider percentUP, timerUP; - String colConstraints; - Font aFont; - Border aBorder; - - colConstraints = "[][]"; + var colConstraints = "[][]"; if (hasStatusArea == true) colConstraints += "[]"; if (hasInfoArea == true) colConstraints += "[grow][]"; setLayout(new MigLayout("", "[right][pref!][grow][right][pref!]", colConstraints)); - aFont = (new JTextField()).getFont(); - + var tmpFont = (new JTextField()).getFont(); + // Title area titleL = new JLabel("Title", JLabel.CENTER); add(titleL, "growx,span,wrap"); - + // Progress + Timer area - percentUP = new ConstUnitProvider(new NumberUnit("%", "%", 100.0, 2)); - tmpL = new JLabel("Progress:"); - progressL = new GLabel(percentUP, aFont, true); + var percentUP = new ConstUnitProvider(new NumberUnit("%", "%", 100.0, 2)); + var tmpL = new JLabel("Progress:"); + progressL = new GLabel(percentUP, tmpFont, true); add(tmpL, ""); add(progressL, ""); - - timerUP = new ConstUnitProvider(new TimeCountUnit(2)); + + var timerUP = new ConstUnitProvider(new TimeCountUnit(2)); tmpL = new JLabel("Time:"); - timerL = new GLabel(timerUP, aFont, true); + timerL = new GLabel(timerUP, tmpFont, true); add(tmpL, "skip 1"); add(timerL, "wrap"); @@ -107,30 +114,26 @@ public class AutoTaskPanel extends BaseTaskPanel implements ActionListener, Gene if (hasStatusArea == true) { tmpL = new JLabel("Status:"); - statusL = new GLabel(aFont); + statusL = new GLabel(tmpFont); add(tmpL, ""); add(statusL, "growx,span,wrap"); } - + // Info area infoTA = null; if (hasInfoArea == true) { - infoTA = GuiUtil.createUneditableTextArea(3, 0); - infoTA.setFont(new Font("Monospaced", Font.PLAIN, aFont.getSize()-2)); + infoTA = GuiUtil.createUneditableTextArea(3, 0); + infoTA.setFont(new Font("Monospaced", Font.PLAIN, tmpFont.getSize() - 2)); infoTA.setOpaque(true); - - tmpScrollPane = new JScrollPane(infoTA); + + var tmpScrollPane = new JScrollPane(infoTA); add(tmpScrollPane, "growx,growy,span,wrap"); } - - // Control area - abortB = GuiUtil.createJButton("Abort", this, aFont); - add(abortB, "align right,span,split 1"); - // Border - aBorder = new BevelBorder(BevelBorder.RAISED); - setBorder(aBorder); + // Control area + abortB = GuiUtil.createJButton("Abort", this); + add(abortB, "ax right,span,split"); } @Override @@ -143,7 +146,7 @@ public class AutoTaskPanel extends BaseTaskPanel implements ActionListener, Gene isActive = false; setVisible(false); } - + progressL.setValue(mProgress); if (statusL != null) statusL.setValue(mStatus); diff --git a/src/glum/gui/panel/task/BaseTaskPanel.java b/src/glum/gui/panel/task/BaseTaskPanel.java index 81df0d8..b858836 100644 --- a/src/glum/gui/panel/task/BaseTaskPanel.java +++ b/src/glum/gui/panel/task/BaseTaskPanel.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.task; import java.awt.Component; @@ -11,11 +24,14 @@ import glum.task.Task; import glum.util.WallTimer; /** - * Abstract TaskPanel that handles all of the state vars used to maintain the Task interface. + * Abstract GUI component that provides an implementation of the {@link Task} interface. + * + * @author lopeznr1 */ public abstract class BaseTaskPanel extends GlassPanel implements Task { // State vars + protected boolean isAborted; protected boolean isActive; protected String infoMsgFrag; protected double mProgress; @@ -31,10 +47,14 @@ public abstract class BaseTaskPanel extends GlassPanel implements Task protected GLabel statusL; protected JTextArea infoTA; + /** + * Standard Constructor + */ public BaseTaskPanel(Component aParent) { super(aParent); + isAborted = false; isActive = true; infoMsgFrag = null; mProgress = 0; @@ -66,15 +86,27 @@ public abstract class BaseTaskPanel extends GlassPanel implements Task public void abort() { mTimer.stop(); + isAborted = true; isActive = false; - Runnable tmpRunnable = () -> updateGui(); - SwingUtilities.invokeLater(tmpRunnable); + SwingUtilities.invokeLater(() -> updateGui()); } @Override - public void infoAppend(String aMsg) + public boolean isAborted() { - infoUpdateForce(aMsg); + return isAborted; + } + + @Override + public boolean isActive() + { + return isActive; + } + + @Override + public void logReg(String aFmtMsg, Object... aObjArr) + { + logRegUpdateForce(aFmtMsg, aObjArr); // Reset the dynamic vars infoMsgFrag = null; @@ -82,19 +114,19 @@ public abstract class BaseTaskPanel extends GlassPanel implements Task } @Override - public void infoAppendln(String aMsg) + public void logRegln(String aFmtMsg, Object... aObjArr) { - infoAppend(aMsg + '\n'); + logReg(aFmtMsg + '\n', aObjArr); } @Override - public void infoUpdate(String aMsg) + public void logRegUpdate(String aFmtMsg, Object... aObjArr) { // Bail if it is not time to update our UI if (isTimeForUpdate() == false) return; - infoUpdateForce(aMsg); + logRegUpdateForce(aFmtMsg, aObjArr); } @Override @@ -106,6 +138,7 @@ public abstract class BaseTaskPanel extends GlassPanel implements Task @Override public void reset() { + isAborted = false; isActive = true; infoMsgFrag = null; mProgress = 0; @@ -117,8 +150,7 @@ public abstract class BaseTaskPanel extends GlassPanel implements Task if (infoTA != null) infoTA.setText(""); - Runnable tmpRunnable = () -> updateGui(); - SwingUtilities.invokeLater(tmpRunnable); + SwingUtilities.invokeLater(() -> updateGui()); } @Override @@ -130,14 +162,13 @@ public abstract class BaseTaskPanel extends GlassPanel implements Task if (isTimeForUpdate() == false && aProgress < 1.0) return; - Runnable tmpRunnable = () -> updateGui(); - SwingUtilities.invokeLater(tmpRunnable); + SwingUtilities.invokeLater(() -> updateGui()); } @Override - public void setProgress(int currVal, int maxVal) + public void setProgress(int aCurrVal, int aMaxVal) { - setProgress((currVal + 0.0) / maxVal); + setProgress((aCurrVal + 0.0) / aMaxVal); } @Override @@ -147,10 +178,10 @@ public abstract class BaseTaskPanel extends GlassPanel implements Task } @Override - public void setTabSize(int numSpaces) + public void setTabSize(int aNumSpaces) { if (infoTA != null) - infoTA.setTabSize(numSpaces); + infoTA.setTabSize(aNumSpaces); } @Override @@ -168,14 +199,7 @@ public abstract class BaseTaskPanel extends GlassPanel implements Task if (isTimeForUpdate() == false) return; - Runnable tmpRunnable = () -> updateGui(); - SwingUtilities.invokeLater(tmpRunnable); - } - - @Override - public boolean isActive() - { - return isActive; + SwingUtilities.invokeLater(() -> updateGui()); } /** @@ -201,7 +225,7 @@ public abstract class BaseTaskPanel extends GlassPanel implements Task /** * Utility method that does the actual updating of the previous info text with aMsg */ - protected void infoUpdateForce(String aMsg) + protected void logRegUpdateForce(String aFmtMsg, Object... aObjArr) { int end, start; int currLC; @@ -210,6 +234,8 @@ public abstract class BaseTaskPanel extends GlassPanel implements Task if (infoTA == null) return; + String tmpMsg = String.format(aFmtMsg, aObjArr); + // Update the old message if (infoMsgFrag != null) { @@ -219,18 +245,18 @@ public abstract class BaseTaskPanel extends GlassPanel implements Task { end = infoTA.getLineEndOffset(infoTA.getLineCount() - 1); start = end - infoMsgFrag.length(); - infoTA.replaceRange(aMsg, start, end); + infoTA.replaceRange(tmpMsg, start, end); } - catch(Exception aExp) + catch (Exception aExp) { - System.out.println("infoMsgFrag:" + infoMsgFrag.length() + " start: " + start + " end:" + end); + System.err.println("infoMsgFrag:" + infoMsgFrag.length() + " start: " + start + " end:" + end); throw new RuntimeException(aExp); } } // Just append the message else { - infoTA.append(aMsg); + infoTA.append(tmpMsg); // Trim the buffer if we exceed our maxLC if (maxLC > 0) @@ -245,9 +271,9 @@ public abstract class BaseTaskPanel extends GlassPanel implements Task end = infoTA.getLineEndOffset(currLC - maxLC); infoTA.replaceRange("", start, end); } - catch(Exception aExp) + catch (Exception aExp) { - System.out.println("currLC:" + currLC + " maxLC:" + maxLC + " start: " + start + " end:" + end); + System.err.println("currLC:" + currLC + " maxLC:" + maxLC + " start: " + start + " end:" + end); throw new RuntimeException(aExp); } } @@ -255,7 +281,7 @@ public abstract class BaseTaskPanel extends GlassPanel implements Task } // Save off the new dynamic message fragment - infoMsgFrag = aMsg; + infoMsgFrag = tmpMsg; // timerL.setValue(mTimer.getTotal()); // SwingUtilities.invokeLater(new FunctionRunnable(timerL, "updateGui")); diff --git a/src/glum/gui/panel/task/DualTaskPanel.java b/src/glum/gui/panel/task/DualTaskPanel.java index 256c0db..f95c632 100644 --- a/src/glum/gui/panel/task/DualTaskPanel.java +++ b/src/glum/gui/panel/task/DualTaskPanel.java @@ -1,41 +1,57 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.task; import java.awt.Component; -import java.awt.Font; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; + import javax.swing.JButton; import javax.swing.JSplitPane; -import javax.swing.JTextField; - -import net.miginfocom.swing.MigLayout; import glum.gui.GuiUtil; import glum.gui.panel.GlassPanel; +import net.miginfocom.swing.MigLayout; +/** + * Panel used to display content from 2 separet {@link PlainTaskPanel}s. + * + * @author lopeznr1 + */ public class DualTaskPanel extends GlassPanel implements ActionListener { // Gui vars - private PlainTaskPanel priTask, secTask; + private final PlainTaskPanel priTask, secTask; private JButton abortB, closeB; - public DualTaskPanel(Component parentFrame, PlainTaskPanel aPriTask, PlainTaskPanel aSecTask, boolean showControlArea) + /** Standard Constructor */ + public DualTaskPanel(Component parentFrame, PlainTaskPanel aPriTask, PlainTaskPanel aSecTask, + boolean showControlArea) { super(parentFrame); - + priTask = aPriTask; secTask = aSecTask; - + buildGui(showControlArea); - updateGui(); + updateGui(); } @Override public void actionPerformed(ActionEvent aEvent) { - Object source; - - source = aEvent.getSource(); + var source = aEvent.getSource(); if (source == abortB) { priTask.abort(); @@ -45,31 +61,28 @@ public class DualTaskPanel extends GlassPanel implements ActionListener { setVisible(false); } - + updateGui(); } - + @Override - public void setVisible(boolean isVisible) + public void setVisible(boolean isVisible) { updateGui(); super.setVisible(isVisible); } - + /** * Forms the GUI */ private void buildGui(boolean showControlArea) { - JSplitPane mainPane; - Font smallFont; - - mainPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, priTask, secTask); + var mainPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, priTask, secTask); mainPane.setResizeWeight(0.40); - + setLayout(new MigLayout("", "[grow]", "[grow][]")); - + // Main area add(mainPane, "growx,growy,span 1,wrap"); @@ -78,24 +91,20 @@ public class DualTaskPanel extends GlassPanel implements ActionListener closeB = null; if (showControlArea == true) { - smallFont = (new JTextField()).getFont(); - - abortB = GuiUtil.createJButton("Abort", this, smallFont); + abortB = GuiUtil.createJButton("Abort", this); add(abortB, "align right,split 2"); - closeB = GuiUtil.createJButton("Close", this, smallFont); + closeB = GuiUtil.createJButton("Close", this); add(closeB); } } - + /** * Keeps the GUI synchronized */ private void updateGui() { - boolean isActive; - - isActive = priTask.isActive | secTask.isActive(); + var isActive = priTask.isActive | secTask.isActive(); if (abortB != null && closeB != null) { abortB.setEnabled(isActive); diff --git a/src/glum/gui/panel/task/FullTaskPanel.java b/src/glum/gui/panel/task/FullTaskPanel.java index c130166..e3bd710 100644 --- a/src/glum/gui/panel/task/FullTaskPanel.java +++ b/src/glum/gui/panel/task/FullTaskPanel.java @@ -1,72 +1,86 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.task; +import java.awt.Component; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.*; + import glum.gui.FocusUtil; import glum.gui.GuiUtil; import glum.gui.action.ClickAction; import glum.gui.component.GLabel; import glum.gui.panel.generic.GenericCodes; -import glum.unit.ConstUnitProvider; -import glum.unit.NumberUnit; -import glum.unit.TimeCountUnit; -import glum.unit.UnitProvider; -import java.awt.Component; -import java.awt.Font; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JScrollPane; -import javax.swing.JTextField; -import javax.swing.border.BevelBorder; -import javax.swing.border.Border; +import glum.task.Task; +import glum.unit.*; import net.miginfocom.swing.MigLayout; /** - * TaskPanel component that shows the progress of a Task. The panel itself is a Task. - * This TaskPanel must be manually closed by the user. + * GUI component that shows the progress of a "task". The panel itself is a {@link Task}. This panel must be manually + * closed by the user. + * + * @author lopeznr1 */ public class FullTaskPanel extends BaseTaskPanel implements ActionListener, GenericCodes { // GUI vars private JButton abortB, closeB; + /** + * Standard Constructor + */ + public FullTaskPanel(Component aParent, boolean hasInfoArea, boolean hasStatusArea) + { + super(aParent); + + buildGuiArea(hasInfoArea, hasStatusArea); + setSize(450, getPreferredSize().height); + setTitle("Task Progress"); + + // Set up keyboard short cuts + FocusUtil.addAncestorKeyBinding(this, "ESCAPE", new ClickAction(abortB)); + FocusUtil.addAncestorKeyBinding(this, "ENTER", new ClickAction(closeB)); + + updateGui(); + } + + /** + * Simplified Constructor + */ public FullTaskPanel(Component aParent) { this(aParent, true, true); } - public FullTaskPanel(Component aParent, boolean hasInfoArea, boolean hasStatusArea) - { - super(aParent); - - buildGuiArea(hasInfoArea, hasStatusArea); - setSize(450, getPreferredSize().height); - setTitle("Task Progress"); - - // Set up keyboard short cuts - FocusUtil.addAncestorKeyBinding(this, "ESCAPE", new ClickAction(abortB)); - FocusUtil.addAncestorKeyBinding(this, "ENTER", new ClickAction(closeB)); - - updateGui(); - } - @Override public void actionPerformed(ActionEvent aEvent) { - Object source; - - source = aEvent.getSource(); + var source = aEvent.getSource(); if (source == abortB) { abort(); - notifyListeners(this, ID_CANCEL, "Abort"); + notifyListeners(this, ID_CANCEL, "Abort"); } else if (source == closeB) { setVisible(false); notifyListeners(this, ID_ACCEPT, "Close"); } - + updateGui(); } @@ -75,35 +89,28 @@ public class FullTaskPanel extends BaseTaskPanel implements ActionListener, Gene */ protected void buildGuiArea(boolean hasInfoArea, boolean hasStatusArea) { - JLabel tmpL; - JScrollPane tmpScrollPane; - UnitProvider percentUP, timerUP; - String colConstraints; - Font aFont; - Border aBorder; - - colConstraints = "[][]"; + var colConstraints = "[][]"; if (hasStatusArea == true) colConstraints += "[]"; if (hasInfoArea == true) colConstraints += "[grow][]"; setLayout(new MigLayout("", "[right][pref!][grow][right][pref!]", colConstraints)); - aFont = (new JTextField()).getFont(); - + var tmpFont = (new JTextField()).getFont(); + // Title area titleL = new JLabel("Title", JLabel.CENTER); add(titleL, "growx,span,wrap"); - + // Progress + Timer area - percentUP = new ConstUnitProvider(new NumberUnit("%", "%", 100.0, 2)); - tmpL = new JLabel("Progress:"); - progressL = new GLabel(percentUP, aFont, true); + var percentUP = new ConstUnitProvider(new NumberUnit("%", "%", 100.0, 2)); + var tmpL = new JLabel("Progress:"); + progressL = new GLabel(percentUP, tmpFont, true); add(tmpL, ""); add(progressL, ""); - - timerUP = new ConstUnitProvider(new TimeCountUnit(2)); + + var timerUP = new ConstUnitProvider(new TimeCountUnit(2)); tmpL = new JLabel("Time:"); - timerL = new GLabel(timerUP, aFont, true); + timerL = new GLabel(timerUP, tmpFont, true); add(tmpL, "skip 1"); add(timerL, "wrap"); @@ -111,44 +118,40 @@ public class FullTaskPanel extends BaseTaskPanel implements ActionListener, Gene if (hasStatusArea == true) { tmpL = new JLabel("Status:"); - statusL = new GLabel(aFont); + statusL = new GLabel(tmpFont); add(tmpL, ""); add(statusL, "growx,span,wrap"); } - + // Info area infoTA = null; if (hasInfoArea == true) { - infoTA = GuiUtil.createUneditableTextArea(7, 0); - infoTA.setFont(new Font("Monospaced", Font.PLAIN, aFont.getSize()-1)); + infoTA = GuiUtil.createUneditableTextArea(7, 0); + infoTA.setFont(new Font("Monospaced", Font.PLAIN, tmpFont.getSize() - 1)); infoTA.setOpaque(true); - - tmpScrollPane = new JScrollPane(infoTA); + + var tmpScrollPane = new JScrollPane(infoTA); add(tmpScrollPane, "growx,growy,span,wrap"); } - - // Control area - abortB = GuiUtil.createJButton("Abort", this, aFont); - closeB = GuiUtil.createJButton("Close", this, aFont); - add(abortB, "align right,span,split 2"); - add(closeB, "span 1"); - // Border - aBorder = new BevelBorder(BevelBorder.RAISED); - setBorder(aBorder); + // Control area + abortB = GuiUtil.createJButton("Abort", this); + closeB = GuiUtil.createJButton("Close", this); + add(abortB, "ax right,span,split"); + add(closeB, ""); } - + @Override protected void updateGui() { // If progress >= 1.0, then we are done if (mProgress >= 1.0) isActive = false; - + abortB.setEnabled(isActive); closeB.setEnabled(!isActive); - + progressL.setValue(mProgress); if (statusL != null) statusL.setValue(mStatus); diff --git a/src/glum/gui/panel/task/PlainTaskPanel.java b/src/glum/gui/panel/task/PlainTaskPanel.java index 1829e27..9cef093 100644 --- a/src/glum/gui/panel/task/PlainTaskPanel.java +++ b/src/glum/gui/panel/task/PlainTaskPanel.java @@ -1,42 +1,59 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.panel.task; +import java.awt.Component; +import java.awt.Font; + +import javax.swing.*; + import glum.gui.GuiUtil; import glum.gui.component.GLabel; -import glum.unit.ConstUnitProvider; -import glum.unit.NumberUnit; -import glum.unit.TimeCountUnit; -import glum.unit.UnitProvider; -import java.awt.Component; -import java.awt.Font; -import javax.swing.JLabel; -import javax.swing.JScrollPane; -import javax.swing.JTextField; -import javax.swing.border.BevelBorder; +import glum.task.Task; +import glum.unit.*; import net.miginfocom.swing.MigLayout; /** - * TaskPanel component that shows the progress of a Task. The panel itself is a Task. - * This TaskPanel is typically embedded into other Components, thus it has no built in - * abort or close button. + * GUI component that shows the progress of a "task". The panel itself is a {@link Task}. This panel is typically + * embedded into other components, thus it has no built in abort or close button. + * + * @author lopeznr1 */ public class PlainTaskPanel extends BaseTaskPanel { + /** + * Standard Constructor + */ + public PlainTaskPanel(Component aParent, boolean hasInfoArea, boolean hasStatusArea) + { + super(aParent); + + buildGuiArea(hasInfoArea, hasStatusArea); + setSize(450, getPreferredSize().height); + setTitle("Task Progress"); + + updateGui(); + } + + /** + * Simplified Constructor + */ public PlainTaskPanel(Component aParent) { this(aParent, true, true); } - public PlainTaskPanel(Component aParent, boolean hasInfoArea, boolean hasStatusArea) - { - super(aParent); - - buildGuiArea(hasInfoArea, hasStatusArea); - setSize(450, getPreferredSize().height); - setTitle("Task Progress"); - - updateGui(); - } - /** * Forms the actual GUI */ @@ -47,7 +64,7 @@ public class PlainTaskPanel extends BaseTaskPanel UnitProvider percentUP, timerUP; String colConstraints; Font aFont; - + colConstraints = "[][]"; if (hasStatusArea == true) colConstraints += "[]"; @@ -55,18 +72,18 @@ public class PlainTaskPanel extends BaseTaskPanel colConstraints += "[grow]"; setLayout(new MigLayout("", "[right][pref!][grow][right][pref!]", colConstraints)); aFont = (new JTextField()).getFont(); - + // Title area titleL = new JLabel("Title", JLabel.CENTER); add(titleL, "growx,span,wrap"); - + // Progress + Timer area - percentUP = new ConstUnitProvider(new NumberUnit("%", "%", 100.0, 2)); + percentUP = new ConstUnitProvider(new NumberUnit("%", "%", 100.0, 2)); tmpL = new JLabel("Progress:"); progressL = new GLabel(percentUP, aFont, true); add(tmpL, ""); add(progressL, ""); - + timerUP = new ConstUnitProvider(new TimeCountUnit(2)); tmpL = new JLabel("Time:"); timerL = new GLabel(timerUP, aFont, true); @@ -81,30 +98,27 @@ public class PlainTaskPanel extends BaseTaskPanel add(tmpL, "newline"); add(statusL, "growx,span"); } - + // Info area infoTA = null; if (hasInfoArea == true) { - infoTA = GuiUtil.createUneditableTextArea(7, 0); -infoTA.setFont(new Font(aFont.getName(), Font.PLAIN, aFont.getSize()-2)); + infoTA = GuiUtil.createUneditableTextArea(7, 0); +infoTA.setFont(new Font(aFont.getName(), Font.PLAIN, aFont.getSize() - 2)); infoTA.setOpaque(true); - + tmpScrollPane = new JScrollPane(infoTA); add(tmpScrollPane, "growx,growy,newline,span"); } - - // Border - setBorder(new BevelBorder(BevelBorder.RAISED)); } - + @Override protected void updateGui() { // If progress >= 1.0, then we are done if (mProgress >= 1.0) isActive = false; - + progressL.setValue(mProgress); if (statusL != null) statusL.setValue(mStatus); diff --git a/src/glum/gui/table/BaseTableModel.java b/src/glum/gui/table/BaseTableModel.java new file mode 100644 index 0000000..b1fe7bc --- /dev/null +++ b/src/glum/gui/table/BaseTableModel.java @@ -0,0 +1,90 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.table; + +import java.util.ArrayList; +import java.util.List; + +import javax.swing.event.TableModelEvent; +import javax.swing.event.TableModelListener; +import javax.swing.table.TableModel; + +/** + * Base class used to define a TableModel. This class provides for handling of each columns type and title. + * + * @author lopeznr1 + */ +public abstract class BaseTableModel implements TableModel +{ + // Attributes + private final Class[] classArr; + private final String[] nameArr; + + // State vars + private List listenerL; + + /** + * Standard Constructor + */ + public BaseTableModel(Class[] aClassArr, String[] aNameArr) + { + classArr = aClassArr; + nameArr = aNameArr; + if (classArr.length != nameArr.length) + throw new RuntimeException("Parameters aClassArr and aNameArr must be the same length!"); + + listenerL = new ArrayList<>(); + } + + @Override + public void addTableModelListener(TableModelListener aListener) + { + listenerL.add(aListener); + } + + @Override + public void removeTableModelListener(TableModelListener aListener) + { + listenerL.remove(aListener); + } + + @Override + public Class getColumnClass(int aColIndex) + { + return classArr[aColIndex]; + } + + @Override + public int getColumnCount() + { + return classArr.length; + } + + @Override + public String getColumnName(int aColIndex) + { + return nameArr[aColIndex]; + } + + /** + * Helper method to send out notification to the registered listeners. + */ + protected void notifyListeners() + { + TableModelEvent tmpEvent = new TableModelEvent(this); + for (TableModelListener aListener : listenerL) + aListener.tableChanged(tmpEvent); + } + +} diff --git a/src/glum/gui/table/ColorCellRenderer.java b/src/glum/gui/table/ColorCellRenderer.java new file mode 100644 index 0000000..c9fd813 --- /dev/null +++ b/src/glum/gui/table/ColorCellRenderer.java @@ -0,0 +1,74 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.table; + +import java.awt.Color; +import java.awt.Component; + +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.border.Border; +import javax.swing.table.TableCellRenderer; + +/** + * Class that used to display a filled color rectangle for a table cell where the data model is a {@link Color} + */ +public class ColorCellRenderer extends JLabel implements TableCellRenderer +{ + // Constants + private static final long serialVersionUID = 1L; + + // State vars + private boolean showToolTips; + + // Cache vars + private Border cUnselectedBorder = null; + private Border cSelectedBorder = null; + + public ColorCellRenderer(boolean aShowToolTips) + { + showToolTips = aShowToolTips; + + setOpaque(true); // MUST do this for background to show up. + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object color, boolean isSelected, boolean hasFocus, + int row, int column) + { + Color newColor = (Color) color; + setBackground(newColor); + + if (isSelected) + { + if (cSelectedBorder == null) + cSelectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, table.getSelectionBackground()); + setBorder(cSelectedBorder); + } + else + { + if (cUnselectedBorder == null) + cUnselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, table.getBackground()); + setBorder(cUnselectedBorder); + } + + String toolTipStr = null; + if (showToolTips == true && newColor != null) + toolTipStr = "RGB value: " + newColor.getRed() + ", " + newColor.getGreen() + ", " + newColor.getBlue(); + setToolTipText(toolTipStr); + + return this; + } +} diff --git a/src/glum/gui/table/ColorHSBLCellRenderer.java b/src/glum/gui/table/ColorHSBLCellRenderer.java new file mode 100644 index 0000000..479b725 --- /dev/null +++ b/src/glum/gui/table/ColorHSBLCellRenderer.java @@ -0,0 +1,76 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.table; + +import java.awt.Color; +import java.awt.Component; + +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.border.Border; +import javax.swing.table.TableCellRenderer; + +import glum.color.ColorHSBL; + +/** + * Class that used to display a filled color rectangle for a table cell where the data model is a {@link Color} + */ +public class ColorHSBLCellRenderer extends JLabel implements TableCellRenderer +{ + // Constants + private static final long serialVersionUID = 1L; + + // State vars + private boolean showToolTips; + + // Cache vars + private Border cUnselectedBorder = null; + private Border cSelectedBorder = null; + + public ColorHSBLCellRenderer(boolean aShowToolTips) + { + showToolTips = aShowToolTips; + + setOpaque(true); // MUST do this for background to show up. + } + + @Override + public Component getTableCellRendererComponent(JTable aTable, Object aObj, boolean isSelected, boolean hasFocus, + int row, int column) + { + Color tmpColor = ((ColorHSBL) aObj).getColor(); + setBackground(tmpColor); + + if (isSelected) + { + if (cSelectedBorder == null) + cSelectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, aTable.getSelectionBackground()); + setBorder(cSelectedBorder); + } + else + { + if (cUnselectedBorder == null) + cUnselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, aTable.getBackground()); + setBorder(cUnselectedBorder); + } + + String toolTipStr = null; + if (showToolTips == true && tmpColor != null) + toolTipStr = "RGB value: " + tmpColor.getRed() + ", " + tmpColor.getGreen() + ", " + tmpColor.getBlue(); + setToolTipText(toolTipStr); + + return this; + } +} diff --git a/src/glum/gui/table/KeyValueTableModel.java b/src/glum/gui/table/KeyValueTableModel.java index 6c51b33..583653f 100644 --- a/src/glum/gui/table/KeyValueTableModel.java +++ b/src/glum/gui/table/KeyValueTableModel.java @@ -1,36 +1,50 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.table; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + import javax.swing.table.AbstractTableModel; -import com.google.common.collect.Lists; - +/** + * Implementation of {@link AbstractTableModel} for displaying key / value pairs. + * + * @author lopeznr1 + */ public class KeyValueTableModel extends AbstractTableModel { - private String[] columnNames; - private List> myList; + private String[] columnNameArr; + private List> workL; /** - * Constructor + * Standard Constructor */ - public KeyValueTableModel() + public KeyValueTableModel(String aKeyHeader, String aValueHeader) { - this(null, null); - } + columnNameArr = new String[2]; + columnNameArr[0] = ""; + columnNameArr[1] = ""; - public KeyValueTableModel(String keyHeader, String valueHeader) - { - columnNames = new String[2]; - columnNames[0] = ""; - columnNames[1] = ""; + if (aKeyHeader != null) + columnNameArr[0] = aKeyHeader; - if (keyHeader != null) - columnNames[0] = keyHeader; + if (aValueHeader != null) + columnNameArr[1] = aValueHeader; - if (valueHeader != null) - columnNames[1] = valueHeader; - - myList = Lists.newArrayList(); + workL = new ArrayList<>(); } /** @@ -43,10 +57,10 @@ public class KeyValueTableModel extends AbstractTableModel if (aMap.isEmpty() == true) return; - startIndex = myList.size(); + startIndex = workL.size(); endIndex = startIndex + aMap.size(); - myList.addAll(aMap.entrySet()); + workL.addAll(aMap.entrySet()); fireTableRowsInserted(startIndex, endIndex); } @@ -58,11 +72,11 @@ public class KeyValueTableModel extends AbstractTableModel { int endIndex; - if (myList.isEmpty() == true) + if (workL.isEmpty() == true) return; - endIndex = myList.size() - 1; - myList.clear(); + endIndex = workL.size() - 1; + workL.clear(); fireTableRowsDeleted(0, endIndex); } @@ -70,44 +84,44 @@ public class KeyValueTableModel extends AbstractTableModel @Override public int getColumnCount() { - return columnNames.length; + return columnNameArr.length; } @Override public int getRowCount() { - return myList.size(); + return workL.size(); } @Override - public String getColumnName(int col) + public String getColumnName(int aCol) { - return columnNames[col]; + return columnNameArr[aCol]; } @Override - public Object getValueAt(int row, int col) + public Object getValueAt(int aRow, int aCol) { - if (row < 0 || row >= myList.size()) + if (aRow < 0 || aRow >= workL.size()) return null; // DataProviderShell Enabled - if (col == 0) - return myList.get(row).getKey(); - else if (col == 1) - return myList.get(row).getValue(); + if (aCol == 0) + return workL.get(aRow).getKey(); + else if (aCol == 1) + return workL.get(aRow).getValue(); return null; } @Override - public Class getColumnClass(int col) + public Class getColumnClass(int aCol) { return String.class; } @Override - public boolean isCellEditable(int row, int col) + public boolean isCellEditable(int aRow, int aCol) { // Note that the data/cell address is constant, // no matter where the cell appears on screen. diff --git a/src/glum/gui/table/NumberRenderer.java b/src/glum/gui/table/NumberRenderer.java new file mode 100644 index 0000000..e6a275d --- /dev/null +++ b/src/glum/gui/table/NumberRenderer.java @@ -0,0 +1,77 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.table; + +import java.awt.Component; +import java.text.DecimalFormat; +import java.text.NumberFormat; + +import javax.swing.JTable; +import javax.swing.table.DefaultTableCellRenderer; + +/** + * TableCellRenderer used to display numerical values. + * + * @author lopeznr1 + */ +public class NumberRenderer extends DefaultTableCellRenderer +{ + // Constants + private static final long serialVersionUID = 1L; + + // Attributes + private final NumberFormat numberFormat; + private final String nanStr; + + /** + * Standard Constructor + * + * @param aPrependLabel: + * String that will be prepended to the table cell's value. + */ + public NumberRenderer(NumberFormat aNumberFormat, String aNanStr) + { + numberFormat = aNumberFormat; + nanStr = aNanStr; + } + + /** + * Simplified Constructor + * + * @param aNumberFmtStr + * The number string format to be sent to a DecimalFormat. + * @param aNanStr + * The String to be used as NaN. + */ + public NumberRenderer(String aNumberFmtStr, String aNanStr) + { + this(new DecimalFormat(aNumberFmtStr), aNanStr); + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, + int row, int column) + { + String tmpStr = nanStr; + if (value instanceof Number) + { + double tmpVal = ((Number) value).doubleValue(); + if (Double.isNaN(tmpVal) == false) + tmpStr = numberFormat.format(value); + } + + return super.getTableCellRendererComponent(table, tmpStr, isSelected, hasFocus, row, column); + } + +} diff --git a/src/glum/gui/table/PrePendRenderer.java b/src/glum/gui/table/PrePendRenderer.java new file mode 100644 index 0000000..973d0b0 --- /dev/null +++ b/src/glum/gui/table/PrePendRenderer.java @@ -0,0 +1,53 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.table; + +import java.awt.Component; + +import javax.swing.JTable; +import javax.swing.table.DefaultTableCellRenderer; + +/** + * TableCellRenderer used to display values prepended with a text string. + * + * @author lopeznr1 + */ +public class PrePendRenderer extends DefaultTableCellRenderer +{ + // Constants + private static final long serialVersionUID = 1L; + + // Attributes + private final String prependLabel; + + /** + * Standard Constructor + * + * @param aPrependLabel: + * String that will be prepended to the table cell's value. + */ + public PrePendRenderer(String aPrependLabel) + { + prependLabel = aPrependLabel; + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, + int row, int column) + { + String tmpStr = prependLabel + value; + return super.getTableCellRendererComponent(table, tmpStr, isSelected, hasFocus, row, column); + } + +} diff --git a/src/glum/gui/table/Scrolling.java b/src/glum/gui/table/Scrolling.java index 3432b98..af72384 100644 --- a/src/glum/gui/table/Scrolling.java +++ b/src/glum/gui/table/Scrolling.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /* * Source from: http://www.chka.de/swing/table/JTableScrolling.java * diff --git a/src/glum/gui/table/SortDir.java b/src/glum/gui/table/SortDir.java new file mode 100644 index 0000000..330b2d8 --- /dev/null +++ b/src/glum/gui/table/SortDir.java @@ -0,0 +1,28 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.table; + +/** + * Enum which defines the possible sort states. + * + * @author lopeznr1 + */ +public enum SortDir +{ + NotSorted, + + Ascending, + + Descending, +} \ No newline at end of file diff --git a/src/glum/gui/table/TablePopupHandler.java b/src/glum/gui/table/TablePopupHandler.java new file mode 100644 index 0000000..34aebd7 --- /dev/null +++ b/src/glum/gui/table/TablePopupHandler.java @@ -0,0 +1,74 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.table; + +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import javax.swing.JPopupMenu; + +import glum.item.ItemManager; + +/** + * Object that provides the logic used to determine when a {@link JPopupMenu} should be displayed. + *

    + * In order for a popup menu to be shown at least one item must be selected in provided refManager. When a popup menu is + * triggered the current selection will not be changed. + * + * @author lopeznr1 + */ +public class TablePopupHandler extends MouseAdapter +{ + // Ref vars + private final ItemManager refManager; + private final JPopupMenu refPopupMenu; + + /** + * Standard Constructor + */ + public TablePopupHandler(ItemManager aManager, JPopupMenu aPopupMenu) + { + refManager = aManager; + refPopupMenu = aPopupMenu; + } + + @Override + public void mousePressed(MouseEvent aEvent) + { + maybeShowPopup(aEvent); + } + + @Override + public void mouseReleased(MouseEvent aEvent) + { + maybeShowPopup(aEvent); + } + + /** + * Helper method to handle the showing of the table popup menu. + */ + private void maybeShowPopup(MouseEvent aEvent) + { + // Bail if this is not a valid popup action + if (aEvent.isPopupTrigger() == false) + return; + + // Bail if no items are selected + if (refManager.getSelectedItems().isEmpty() == true) + return; + + refPopupMenu.show(aEvent.getComponent(), aEvent.getX(), aEvent.getY()); + } + +} diff --git a/src/glum/gui/table/TableSorter.java b/src/glum/gui/table/TableSorter.java index 83f99ea..c16af71 100644 --- a/src/glum/gui/table/TableSorter.java +++ b/src/glum/gui/table/TableSorter.java @@ -1,120 +1,363 @@ +/* + * Copyright (c) 1995, 2008, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of Oracle or the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ package glum.gui.table; -import java.awt.*; +import java.awt.Component; import java.awt.event.*; import java.util.*; -import java.util.List; import javax.swing.*; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.*; +import com.google.common.collect.ImmutableList; + +import glum.gui.table.sort.DefaultSortIconProvider; +import glum.gui.table.sort.SortIconProvider; + /** - * TableSorter is a decorator for TableModels; adding sorting functionality to a - * supplied TableModel. TableSorter does not store or copy the data in its - * TableModel; instead it maintains a map from the row indexes of the view to - * the row indexes of the model. As requests are made of the sorter (like - * getValueAt(row, col)) they are passed to the underlying model after the row - * numbers have been translated via the internal mapping array. This way, the - * TableSorter appears to hold another copy of the table with the rows in a - * different order. - *

    - * TableSorter registers itself as a listener to the underlying model, just as - * the JTable itself would. Events recieved from the model are examined, - * sometimes manipulated (typically widened), and then passed on to the - * TableSorter's listeners (typically the JTable). If a change to the model has - * invalidated the order of TableSorter's rows, a note of this is made and the - * sorter will resort the rows the next time a value is requested. - *

    - * When the tableHeader property is set, either by using the setTableHeader() - * method or the two argument constructor, the table header may be used as a - * complete UI for TableSorter. The default renderer of the tableHeader is - * decorated with a renderer that indicates the sorting status of each column. - * In addition, a mouse listener is installed with the following behavior: + * {@link TableSorter} is a decorator for TableModels; adding sorting functionality to a supplied TableModel. + * TableSorter does not store or copy the data in its TableModel; instead it maintains a map from the row indexes of the + * view to the row indexes of the model. As requests are made of the sorter (like getValueAt(row, col)) they are passed + * to the underlying model after the row numbers have been translated via the internal mapping array. This way, the + * TableSorter appears to hold another copy of the table with the rows in a different order. + *

    + * TableSorter registers itself as a listener to the underlying model, just as the JTable itself would. Events received + * from the model are examined, sometimes manipulated (typically widened), and then passed on to the TableSorter's + * listeners (typically the JTable). If a change to the model has invalidated the order of TableSorter's rows, a note of + * this is made and the sorter will resort the rows the next time a value is requested. + *

    + * When the tableHeader property is set, either by using the setTableHeader() method or the two argument constructor, + * the table header may be used as a complete UI for TableSorter. The default renderer of the tableHeader is decorated + * with a renderer that indicates the sorting status of each column. In addition, a mouse listener is installed with the + * following behavior: *

      - *
    • - * Mouse-click: Clears the sorting status of all other columns and advances the - * sorting status of that column through three values: {NOT_SORTED, ASCENDING, - * DESCENDING} (then back to NOT_SORTED again). - *
    • - * SHIFT-mouse-click: Clears the sorting status of all other columns and cycles - * the sorting status of the column through the same three values, in the - * opposite order: {NOT_SORTED, DESCENDING, ASCENDING}. - *
    • - * CONTROL-mouse-click and CONTROL-SHIFT-mouse-click: as above except that the - * changes to the column do not cancel the statuses of columns that are already - * sorting - giving a way to initiate a compound sort. + *
    • Mouse-Click: Clears the sorting status of all other columns and sets the current column as the primary sort + * column. + *
    • CONTROL-Mouse-Click: Adds a secondary (or lower rank) column to sort upon. + *
    • SHIFT-Mouse-Click: Clears the sort status of the selected column. The column will no longer influence the sort. *
    - *

    - * This is a long overdue rewrite of a class of the same name that first - * appeared in the swing table demos in 1997. - * + * Please note that mouse-clicking on the columns will toggle the sort directive of that column from ascending to + * descending. + *

    + * Column headers that have been selected for sorting provide the following (default) visual indicators: + *

      + *
    • The 1st (primary) sort column is displayed with a bright red icon. + *
    • The 2nd sort column is displayed with a dark red icon. + *
    • The 3rd sort column is displayed with a black icon. + *
    • The 4th (and all lower level) sort column is displayed with a gray icon. + *
    + * Please note that the icons used for painting the sort indicators can be customized via the method + * {@link #setSortIconProvider(SortIconProvider)}. + *

    + * This class is an incompatible rewrite of the TableSorter class (~2004Feb27). The original class can be currently be + * sourced (as of date 2019Aug20) from:
    + * https://docs.oracle.com/javase/tutorial/uiswing/examples/components/TableSorterDemoProject/src/components/TableSorter.java
    + * or
    + * https://web.archive.org/web/20171019235101/http://docs.oracle.com/javase/tutorial/uiswing/examples/components/TableSorterDemoProject/src/components/TableSorter.java + * * @author Philip Milne * @author Brendon McLean * @author Dan van Enckevort * @author Parwinder Sekhon - * @version 2.0 02/27/04 + * @author lopeznr1 (Rewrite author as used in Glum library) */ -@SuppressWarnings("unchecked") -// This class is no longer supported. It should be removed as of Java 1.7 public class TableSorter extends AbstractTableModel { - protected TableModel tableModel; - // Constants - private static final long serialVersionUID = -1999L; - public static final int DESCENDING = -1; - public static final int NOT_SORTED = 0; - public static final int ASCENDING = 1; + private static final Comparator LEXICAL_COMPARATOR = Comparator.comparing(Object::toString); - private static Directive EMPTY_DIRECTIVE = new Directive(-1, NOT_SORTED); - - public static final Comparator COMPARABLE_COMAPRATOR = new Comparator() - { - @Override - public int compare(Object o1, Object o2) - { - return ((Comparable)o1).compareTo(o2); - } - }; - public static final Comparator LEXICAL_COMPARATOR = new Comparator() - { - @Override - public int compare(Object o1, Object o2) - { - return o1.toString().compareTo(o2.toString()); - } - }; + // Ref vars + private final TableModel refTableModel; + private JTableHeader refTableHeader; + // State vars private Row[] viewToModel; private int[] modelToView; - private JTableHeader tableHeader; - private MouseListener mouseListener; - private TableModelListener tableModelListener; - private Map columnComparators = new HashMap(); - private List sortingColumns = new ArrayList(); + private final MouseListener mouseListener; + private final TableModelListener tableModelListener; + private final Map, Comparator> columnComparatorClassM; + private final Map> columnComparatorIndexM; + private final List sortingColumnL; private boolean isSortEnabled; - public TableSorter() + // Render vars + private SortIconProvider refSortIconProvider; + private ImmutableList sortIconAsceL; + private ImmutableList sortIconDescL; + private int cSize; + + /** + * Standard Constructor + * + * @param aTableModel + * The table model which will be used as the backing table model. + * @param aTableHeader + * The table header asociated with the JTable. + */ + public TableSorter(TableModel aTableModel, JTableHeader aTableHeader) { - this.mouseListener = new MouseHandler(); - this.tableModelListener = new TableModelHandler(); + refTableModel = aTableModel; + + viewToModel = null; + modelToView = null; + + mouseListener = new MouseHandler(); + tableModelListener = new TableModelHandler(); + columnComparatorClassM = new HashMap<>(); + columnComparatorIndexM = new HashMap<>(); + sortingColumnL = new ArrayList<>(); isSortEnabled = true; + + refSortIconProvider = DefaultSortIconProvider.Default; + sortIconAsceL = ImmutableList.of(); + sortIconDescL = ImmutableList.of(); + cSize = -1; + + if (aTableHeader != null) + setTableHeader(aTableHeader); + + // Register for events of interest + refTableModel.addTableModelListener(tableModelListener); + fireTableStructureChanged(); } - public TableSorter(TableModel tableModel) + /** Simplified Constructor */ + public TableSorter(TableModel aTableModel) { - this(); - setTableModel(tableModel); + this(aTableModel, null); } - public TableSorter(TableModel tableModel, JTableHeader tableHeader) + /** + * Returns the {@link SortDir} of the specified column. + */ + public SortDir getSortDir(int aColNum) { - this(); - setTableHeader(tableHeader); - setTableModel(tableModel); + var tmpDirective = getDirective(aColNum); + if (tmpDirective != null) + return tmpDirective.sortDir; + + return SortDir.NotSorted; + } + + /** + * Returns an ordered map of column model index to the {@link SortDir}. + *

    + * The returned map is a snapshot of the columns that are sorted and their ordered priority. + */ + public Map getSortState() + { + var retSortStateM = new LinkedHashMap(); + for (var aDirective : sortingColumnL) + retSortStateM.put(aDirective.column, aDirective.sortDir); + + return retSortStateM; + } + + public JTableHeader getTableHeader() + { + return refTableHeader; + } + + public TableModel getTableModel() + { + return refTableModel; + } + + /** + * Returns true if any columns have a sort directive set. + */ + public boolean isSorting() + { + return sortingColumnL.size() != 0; + } + + /** + * Sets in the Comparator that will be associated with columns of data type, aType. + *

    + * This Comparator will only be used if the column does not have a specific Comparator associated with it. + * + * @param aType + * @param aComparator + */ + public void setColumnClassComparator(Class aType, Comparator aComparator) + { + if (aComparator == null) + columnComparatorClassM.remove(aType); + else + columnComparatorClassM.put(aType, aComparator); + } + + /** + * Sets in the Comparator that will be associated with the specified column. + *

    + * This Comparator will be used before other (criteria matching) Comparators. + * + * @param aColNum + * @param aComparator + */ + public void setColumnIndexComparator(int aColNum, Comparator aComparator) + { + if (aComparator == null) + columnComparatorIndexM.remove(aColNum); + else + columnComparatorIndexM.put(aColNum, aComparator); + } + + /** + * Sets the {@link SortIconProvider}. + */ + public void setSortIconProvider(SortIconProvider aSortIconProvider) + { + refSortIconProvider = aSortIconProvider; + + // Invalidate the sort icons cache + sortIconAsceL = ImmutableList.of(); + sortIconDescL = ImmutableList.of(); + cSize = -1; + + sortingStatusChanged(); + } + + /** + * Installs an ordered map of column model index to the {@link SortDir}. + *

    + * The installed map will set the columns that are sorted and their ordered sort priority. + */ + public void setSortState(Map aSortDirM) + { + sortingColumnL.clear(); + for (var aCol : aSortDirM.keySet()) + sortingColumnL.add(new Directive(aCol, aSortDirM.get(aCol))); + + sortingStatusChanged(); + } + + /** + * Sets whether sorting is enabled or disabled. + */ + public void setSortingEnabled(boolean aBool) + { + isSortEnabled = aBool; + + clearSortingState(); + } + + /** + * Sets in the table header associated with the backing TableModel. A mouse listener will be installed that provides + * a fully functional UI for configuring the rank and sort directive of the various columns. + * + * @param aTableHeader + */ + public void setTableHeader(JTableHeader aTableHeader) + { + if (refTableHeader != null) + { + refTableHeader.removeMouseListener(mouseListener); + TableCellRenderer defaultRenderer = refTableHeader.getDefaultRenderer(); + if (defaultRenderer instanceof SortableHeaderRenderer) + { + refTableHeader.setDefaultRenderer(((SortableHeaderRenderer) defaultRenderer).tableCellRenderer); + } + } + refTableHeader = aTableHeader; + if (refTableHeader != null) + { + refTableHeader.addMouseListener(mouseListener); + refTableHeader.setDefaultRenderer(new SortableHeaderRenderer(refTableHeader.getDefaultRenderer())); + } + } + + public int modelIndex(int viewIndex) + { + return getViewToModel()[viewIndex].modelIndex; + } + + public int viewIndex(int modelIndex) + { + return getModelToView()[modelIndex]; + } + + // TableModel interface methods + @Override + public int getRowCount() + { + return (refTableModel == null) ? 0 : refTableModel.getRowCount(); + } + + @Override + public int getColumnCount() + { + return (refTableModel == null) ? 0 : refTableModel.getColumnCount(); + } + + @Override + public String getColumnName(int column) + { + return refTableModel.getColumnName(column); + } + + @Override + public Class getColumnClass(int column) + { + return refTableModel.getColumnClass(column); + } + + @Override + public boolean isCellEditable(int row, int column) + { + return refTableModel.isCellEditable(modelIndex(row), column); + } + + @Override + public Object getValueAt(int row, int column) + { + return refTableModel.getValueAt(modelIndex(row), column); + } + + @Override + public void setValueAt(Object aValue, int row, int column) + { + refTableModel.setValueAt(aValue, modelIndex(row), column); + } + + /** + * Helper method that clears all sorting directives and sends out the proper notification. + */ + private void cancelSorting() + { + sortingColumnL.clear(); + sortingStatusChanged(); } private void clearSortingState() @@ -123,166 +366,87 @@ public class TableSorter extends AbstractTableModel modelToView = null; } - public TableModel getTableModel() + /** + * Helper method that returns the appropriate Comparator for the specified column. + */ + private Comparator getComparator(int aColNum) { - return tableModel; - } + Comparator retComparator; - public void setTableModel(TableModel tableModel) - { - if (this.tableModel != null) - { - this.tableModel.removeTableModelListener(tableModelListener); - } + // Utilize the column index Comparator (if specified) + retComparator = columnComparatorIndexM.get(aColNum); + if (retComparator != null) + return retComparator; - this.tableModel = tableModel; - if (this.tableModel != null) - { - this.tableModel.addTableModelListener(tableModelListener); - } + // Utilize the class type Comparator (if specified) + Class columnType = refTableModel.getColumnClass(aColNum); + retComparator = columnComparatorClassM.get(columnType); + if (retComparator != null) + return retComparator; - clearSortingState(); - fireTableStructureChanged(); - } - - public JTableHeader getTableHeader() - { - return tableHeader; - } - - public void setTableHeader(JTableHeader tableHeader) - { - if (this.tableHeader != null) - { - this.tableHeader.removeMouseListener(mouseListener); - TableCellRenderer defaultRenderer = this.tableHeader.getDefaultRenderer(); - if (defaultRenderer instanceof SortableHeaderRenderer) - { - this.tableHeader.setDefaultRenderer(((SortableHeaderRenderer)defaultRenderer).tableCellRenderer); - } - } - this.tableHeader = tableHeader; - if (this.tableHeader != null) - { - this.tableHeader.addMouseListener(mouseListener); - this.tableHeader.setDefaultRenderer(new SortableHeaderRenderer(this.tableHeader.getDefaultRenderer())); - } - } - - public boolean isSorting() - { - return sortingColumns.size() != 0; - } - - private Directive getDirective(int column) - { - for (int i = 0; i < sortingColumns.size(); i++) - { - Directive directive = (Directive)sortingColumns.get(i); - if (directive.column == column) - { - return directive; - } - } - return EMPTY_DIRECTIVE; - } - - public int getSortingStatus(int column) - { - return getDirective(column).direction; - } - - private void sortingStatusChanged() - { - clearSortingState(); - fireTableDataChanged(); - if (tableHeader != null) - { - tableHeader.repaint(); - } - } - - public void setSortingStatus(int column, int status) - { - Directive directive = getDirective(column); - if (directive != EMPTY_DIRECTIVE) - { - sortingColumns.remove(directive); - } - if (status != NOT_SORTED) - { - sortingColumns.add(new Directive(column, status)); - } - sortingStatusChanged(); - } - - protected Icon getHeaderRendererIcon(int column, int size) - { - Directive directive = getDirective(column); - if (directive == EMPTY_DIRECTIVE) - { - return null; - } - // return new Arrow(directive.direction == DESCENDING, size, - // sortingColumns.indexOf(directive)); - return null; - } - - private void cancelSorting() - { - sortingColumns.clear(); - sortingStatusChanged(); - } - - public void setColumnComparator(Class type, Comparator comparator) - { - if (comparator == null) - { - columnComparators.remove(type); - } - else - { - columnComparators.put(type, comparator); - } - } - - protected Comparator getComparator(int column) - { - Class columnType = tableModel.getColumnClass(column); - Comparator comparator = (Comparator)columnComparators.get(columnType); - if (comparator != null) - { - return comparator; - } if (Comparable.class.isAssignableFrom(columnType)) - { - return COMPARABLE_COMAPRATOR; - } + return Comparator.naturalOrder(); + return LEXICAL_COMPARATOR; } - private Row[] getViewToModel() + /** + * Helper method that returns the sort directive of the specified column. + *

    + * Returns null if the column is not sorted. + */ + private Directive getDirective(int aColNum) { - if (viewToModel == null) + for (Directive aDirective : sortingColumnL) { - int tableModelRowCount = tableModel.getRowCount(); - viewToModel = new Row[tableModelRowCount]; - for (int row = 0; row < tableModelRowCount; row++) - { - viewToModel[row] = new Row(row); - } - - if (isSorting()) - { - Arrays.sort(viewToModel); - } + if (aDirective.column == aColNum) + return aDirective; } - return viewToModel; + return null; } - public int modelIndex(int viewIndex) + /** + * Helper method that returns the icon that should be used for the specified column. + *

    + * If the column is not sorted then null will be returned. + * + * @param aColumn + * @param aSize + * @return + */ + private Icon getHeaderRendererIcon(int aColumn, int aSize) { - return getViewToModel()[viewIndex].modelIndex; + // Bail if this column is not sorted + Directive directive = getDirective(aColumn); + if (directive == null) + return null; + + // Synthesize new icons if the size differs from the cache + if (aSize != cSize) + { + sortIconAsceL = ImmutableList.copyOf(refSortIconProvider.getIconsForSortAsce(aSize)); + sortIconDescL = ImmutableList.copyOf(refSortIconProvider.getIconsForSortDesc(aSize)); + } + cSize = aSize; + + // Locate the proper sort icon list + List sortIconL = sortIconAsceL; + if (directive.sortDir == SortDir.Descending) + sortIconL = sortIconDescL; + + // Bail if there are no available sort icons + if (sortIconL.size() == 0) + return null; + + // Retrieve the priority + int priority = sortingColumnL.indexOf(directive); + if (priority < 0) + priority = 0; + else if (priority >= sortIconL.size()) + priority = sortIconL.size() - 1; + + // Return the proper icon corresponding to the priority + return sortIconL.get(priority); } private int[] getModelToView() @@ -299,111 +463,94 @@ public class TableSorter extends AbstractTableModel return modelToView; } - public int viewIndex(int modelIndex) + private Row[] getViewToModel() { - return getModelToView()[modelIndex]; + if (viewToModel == null) + { + int tableModelRowCount = refTableModel.getRowCount(); + viewToModel = new Row[tableModelRowCount]; + for (int row = 0; row < tableModelRowCount; row++) + { + viewToModel[row] = new Row(row); + } + + if (isSorting()) + { + Arrays.sort(viewToModel); + } + } + return viewToModel; } - // TableModel interface methods - - @Override - public int getRowCount() + /** + * Sets the {@link SortDir} for a specific column. + *

    + * Note that this column will be sorted after any previously sorted column. + */ + private void setSortDir(int aColNum, SortDir aSortDir) { - return (tableModel == null) ? 0 : tableModel.getRowCount(); + var directive = getDirective(aColNum); + if (directive != null) + sortingColumnL.remove(directive); + + if (aSortDir != SortDir.NotSorted) + sortingColumnL.add(new Directive(aColNum, aSortDir)); + + sortingStatusChanged(); } - @Override - public int getColumnCount() + /** + * Helper method that is triggered whenever any of the sort state changes. + *

    + * Event notification will be sent out. + */ + private void sortingStatusChanged() { - return (tableModel == null) ? 0 : tableModel.getColumnCount(); - } - - @Override - public String getColumnName(int column) - { - return tableModel.getColumnName(column); - } - - @Override - public Class getColumnClass(int column) - { - return tableModel.getColumnClass(column); - } - - @Override - public boolean isCellEditable(int row, int column) - { - return tableModel.isCellEditable(modelIndex(row), column); - } - - @Override - public Object getValueAt(int row, int column) - { - return tableModel.getValueAt(modelIndex(row), column); - } - - @Override - public void setValueAt(Object aValue, int row, int column) - { - tableModel.setValueAt(aValue, modelIndex(row), column); - } - - - public void setSortingEnabled(boolean aBool) - { - isSortEnabled = aBool; - clearSortingState(); + fireTableDataChanged(); + if (refTableHeader != null) + refTableHeader.repaint(); } - - // Helper classes - private class Row implements Comparable + private class Row implements Comparable { - private int modelIndex; + // Attributes + private final int modelIndex; - public Row(int index) + /** Standard Constructor */ + public Row(int aIndex) { - this.modelIndex = index; + modelIndex = aIndex; } @Override - public int compareTo(Object o) + @SuppressWarnings("unchecked") + public int compareTo(Row aRow) { int row1 = modelIndex; - int row2 = ((Row)o).modelIndex; + int row2 = aRow.modelIndex; - for (Iterator it = sortingColumns.iterator(); it.hasNext();) + for (Directive aDirective : sortingColumnL) { - Directive directive = (Directive)it.next(); - int column = directive.column; - Object o1 = tableModel.getValueAt(row1, column); - Object o2 = tableModel.getValueAt(row2, column); + int column = aDirective.column; + Object o1 = refTableModel.getValueAt(row1, column); + Object o2 = refTableModel.getValueAt(row2, column); int comparison = 0; // Define null less than everything, except null. if (o1 == null && o2 == null) - { comparison = 0; - } else if (o1 == null) - { comparison = -1; - } else if (o2 == null) - { comparison = 1; - } else - { - comparison = getComparator(column).compare(o1, o2); - } + comparison = ((Comparator) getComparator(column)).compare(o1, o2); + if (comparison != 0) - { - return directive.direction == DESCENDING ? -comparison : comparison; - } + return aDirective.sortDir == SortDir.Descending ? -comparison : comparison; } return 0; } @@ -455,7 +602,8 @@ public class TableSorter extends AbstractTableModel // which can be a performance problem for large tables. The last // clause avoids this problem. int column = e.getColumn(); - if (e.getFirstRow() == e.getLastRow() && column != TableModelEvent.ALL_COLUMNS && getSortingStatus(column) == NOT_SORTED && modelToView != null) + if (e.getFirstRow() == e.getLastRow() && column != TableModelEvent.ALL_COLUMNS + && getSortDir(column) == SortDir.NotSorted && modelToView != null) { int viewIndex = getModelToView()[e.getFirstRow()]; fireTableChanged(new TableModelEvent(TableSorter.this, viewIndex, viewIndex, column, e.getType())); @@ -473,145 +621,76 @@ public class TableSorter extends AbstractTableModel private class MouseHandler extends MouseAdapter { @Override - public void mouseClicked(MouseEvent e) + public void mouseClicked(MouseEvent aEvent) { - JTableHeader h = (JTableHeader)e.getSource(); - TableColumnModel columnModel = h.getColumnModel(); - int viewColumn = columnModel.getColumnIndexAtX(e.getX()); - int column = columnModel.getColumn(viewColumn).getModelIndex(); - // Bail if sorting is disabled if (isSortEnabled == false) return; - - if (column != -1) - { - int status = getSortingStatus(column); - if (!e.isControlDown()) - { - cancelSorting(); - } - // // Cycle the sorting states through {NOT_SORTED, ASCENDING, - // DESCENDING} or - // // {NOT_SORTED, DESCENDING, ASCENDING} depending on whether shift - // is pressed. - // status = status + (e.isShiftDown() ? -1 : 1); - // status = (status + 4) % 3 - 1; // signed mod, returning {-1, 0, - // 1} - if (status == ASCENDING) - status = DESCENDING; - else - status = ASCENDING; - setSortingStatus(column, status); - } - } - } + // Bail if no view column + JTableHeader h = (JTableHeader) aEvent.getSource(); + TableColumnModel columnModel = h.getColumnModel(); + int viewColumn = columnModel.getColumnIndexAtX(aEvent.getX()); + if (viewColumn == -1) + return; - private static class Arrow implements Icon - { - private boolean descending; - private int size; - private int priority; + // Bail if no model column + int column = columnModel.getColumn(viewColumn).getModelIndex(); + if (column == -1) + return; - public Arrow(boolean descending, int size, int priority) - { - this.descending = descending; - this.size = size; - this.priority = priority; - } + // Save off the current sort direction + var prevSortDir = getSortDir(column); - @Override - public void paintIcon(Component c, Graphics g, int x, int y) - { - Color color = c == null ? Color.GRAY : c.getBackground(); - // In a compound sort, make each succesive triangle 20% - // smaller than the previous one. - int dx = (int)(size / 2 * Math.pow(0.8, priority)); - int dy = descending ? dx : -dx; - // Align icon (roughly) with font baseline. - y = y + 5 * size / 6 + (descending ? -dy : 0); - int shift = descending ? 1 : -1; - g.translate(x, y); + // Clear all sort status if CTRL or SHIFT are not pressed + if (aEvent.isControlDown() == false && aEvent.isShiftDown() == false) + cancelSorting(); - // Right diagonal. - g.setColor(color.darker()); - g.drawLine(dx / 2, dy, 0, 0); - g.drawLine(dx / 2, dy + shift, 0, shift); + // Alternate between Ascending, Descending sorting + var nextSortDir = SortDir.Ascending; + if (prevSortDir == SortDir.Ascending) + nextSortDir = SortDir.Descending; - // Left diagonal. - g.setColor(color.brighter()); - g.drawLine(dx / 2, dy, dx, 0); - g.drawLine(dx / 2, dy + shift, dx, shift); + // If SHIFT is pressed then clear the sorting status + if (aEvent.isShiftDown() == true) + nextSortDir = SortDir.NotSorted; - // Horizontal line. - if (descending) - { - g.setColor(color.darker().darker()); - } - else - { - g.setColor(color.brighter().brighter()); - } - g.drawLine(dx, 0, 0, 0); - - g.setColor(color); - g.translate(-x, -y); - } - - @Override - public int getIconWidth() - { - return size; - } - - @Override - public int getIconHeight() - { - return size; + setSortDir(column, nextSortDir); } } private class SortableHeaderRenderer implements TableCellRenderer { - private TableCellRenderer tableCellRenderer; + // Attributes + private final TableCellRenderer tableCellRenderer; - public SortableHeaderRenderer(TableCellRenderer tableCellRenderer) + /** Standard Constructor */ + public SortableHeaderRenderer(TableCellRenderer aTableCellRenderer) { - this.tableCellRenderer = tableCellRenderer; + tableCellRenderer = aTableCellRenderer; } @Override - public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, + int row, int column) { - Component c = tableCellRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); - if (c instanceof JLabel) + var tmpComp = tableCellRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + if (tmpComp instanceof JLabel aLabel) { - JLabel l = (JLabel)c; - l.setHorizontalTextPosition(JLabel.LEFT); + aLabel.setHorizontalTextPosition(JLabel.LEFT); int modelColumn = table.convertColumnIndexToModel(column); - l.setIcon(getHeaderRendererIcon(modelColumn, l.getFont().getSize())); + aLabel.setIcon(getHeaderRendererIcon(modelColumn, aLabel.getFont().getSize())); } - return c; + return tmpComp; } - /* - * public void setEnabled(boolean aBool) { - * getTableCellRendererComponent.setEnabled(aBool); - * - * } - */ } - private static class Directive + /** + * Record that holds the state of a specific column and it's associated sort direction. + */ + private static record Directive(int column, SortDir sortDir) { - private int column; - private int direction; - - public Directive(int column, int direction) - { - this.column = column; - this.direction = direction; - } } + } diff --git a/src/glum/gui/table/sort/DefaultSortIconProvider.java b/src/glum/gui/table/sort/DefaultSortIconProvider.java new file mode 100644 index 0000000..56d9327 --- /dev/null +++ b/src/glum/gui/table/sort/DefaultSortIconProvider.java @@ -0,0 +1,75 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.table.sort; + +import java.awt.Color; +import java.util.*; + +import javax.swing.Icon; + +import com.google.common.collect.ImmutableList; + +/** + * Default implementation of the SortIconProvider interface. + *

    + * This SortIconProvider provides a flexible number of "priorities" based on the list of colors provided at construction + * time. + * + * @author lopeznr1 + */ +public class DefaultSortIconProvider implements SortIconProvider +{ + // Constants + private static final List DefaultColorL = ImmutableList.of(Color.RED, Color.RED.darker(), Color.BLACK, + Color.LIGHT_GRAY); + public static final DefaultSortIconProvider Default = new DefaultSortIconProvider(DefaultColorL); + + // Attributes + private final ImmutableList refColorL; + + /** + * Standard Constructor + * + * @param aColorL + * The list of colors for each priority. Colors at the front of the list are associated with a higher + * priority. + */ + public DefaultSortIconProvider(List aColorL) + { + refColorL = ImmutableList.copyOf(aColorL); + } + + @Override + public List getIconsForSortAsce(int aSize) + { + List retL = new ArrayList<>(); + + for (Color aColor : refColorL) + retL.add(new SortArrow(false, aSize, aColor)); + + return retL; + } + + @Override + public List getIconsForSortDesc(int aSize) + { + List retL = new ArrayList<>(); + + for (Color aColor : refColorL) + retL.add(new SortArrow(true, aSize, aColor)); + + return retL; + } + +} diff --git a/src/glum/gui/table/sort/SortArrow.java b/src/glum/gui/table/sort/SortArrow.java new file mode 100644 index 0000000..2fa76b9 --- /dev/null +++ b/src/glum/gui/table/sort/SortArrow.java @@ -0,0 +1,100 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.table.sort; + +import java.awt.*; + +import javax.swing.Icon; + +/** + * Icon used to render the sort indicator. + * + * @author lopeznr1 + */ +public class SortArrow implements Icon +{ + // Attributes + private final boolean descending; + private final int size; + private final Color fillColor; + + /** + * Standard Constructor + * + * @param aDescending + * @param aSize + * @param aPriority + */ + public SortArrow(boolean aDescending, int aSize, Color aColor) + { + descending = aDescending; + size = aSize; + fillColor = aColor; + } + + @Override + public void paintIcon(Component aComp, Graphics g, int x, int y) + { + int dx = (int) (size / 2.0); + int dy = descending ? dx : -dx; + + // Align icon (roughly) with font baseline. + y = y + 5 * size / 6 + (descending ? -dy : 0); + int shift = descending ? 1 : -1; + g.translate(x, y); + + int[] xArr = { dx / 2, 0, dx }; + int[] yArr = { dy + shift, shift, 0 }; + + // Draw the solid area + g.setColor(fillColor); + g.fillPolygon(xArr, yArr, 3); + + // Draw the border area + Color tmpColor = aComp == null ? Color.GRAY : aComp.getBackground(); + + // Right diagonal. + g.setColor(tmpColor.darker()); + g.drawLine(dx / 2, dy, 0, 0); + g.drawLine(dx / 2, dy + shift, 0, shift); + + // Left diagonal. + g.setColor(tmpColor.brighter()); + g.drawLine(dx / 2, dy, dx, 0); + g.drawLine(dx / 2, dy + shift, dx, shift); + + // Horizontal line. + if (descending) + g.setColor(tmpColor.darker().darker()); + else + g.setColor(tmpColor.brighter().brighter()); + g.drawLine(dx, 0, 0, 0); + + g.setColor(tmpColor); + g.translate(-x, -y); + } + + @Override + public int getIconWidth() + { + return size; + } + + @Override + public int getIconHeight() + { + return size; + } + +} diff --git a/src/glum/gui/table/sort/SortArrowLegacy.java b/src/glum/gui/table/sort/SortArrowLegacy.java new file mode 100644 index 0000000..da0dacc --- /dev/null +++ b/src/glum/gui/table/sort/SortArrowLegacy.java @@ -0,0 +1,95 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.table.sort; + +import java.awt.*; + +import javax.swing.Icon; + +/** + * Legacy Icon used to render the sort indicator. + * + * @author lopeznr1 + */ +class SortArrowLegacy implements Icon +{ + // Attributes + private final boolean descending; + private final int size; + private final int priority; + + /** + * Standard Constructor + * + * @param aDescending + * @param aSize + * @param aPriority + */ + public SortArrowLegacy(boolean aDescending, int aSize, int aPriority) + { + descending = aDescending; + size = aSize; + priority = aPriority; + } + + @Override + public void paintIcon(Component aComp, Graphics g, int x, int y) + { + Color color = aComp == null ? Color.GRAY : aComp.getBackground(); + // In a compound sort, make each succesive triangle 20% + // smaller than the previous one. + int dx = (int) (size / 2 * Math.pow(0.8, priority)); + int dy = descending ? dx : -dx; + // Align icon (roughly) with font baseline. + y = y + 5 * size / 6 + (descending ? -dy : 0); + int shift = descending ? 1 : -1; + g.translate(x, y); + + // Right diagonal. + g.setColor(color.darker()); + g.drawLine(dx / 2, dy, 0, 0); + g.drawLine(dx / 2, dy + shift, 0, shift); + + // Left diagonal. + g.setColor(color.brighter()); + g.drawLine(dx / 2, dy, dx, 0); + g.drawLine(dx / 2, dy + shift, dx, shift); + + // Horizontal line. + if (descending) + { + g.setColor(color.darker().darker()); + } + else + { + g.setColor(color.brighter().brighter()); + } + g.drawLine(dx, 0, 0, 0); + + g.setColor(color); + g.translate(-x, -y); + } + + @Override + public int getIconWidth() + { + return size; + } + + @Override + public int getIconHeight() + { + return size; + } +} \ No newline at end of file diff --git a/src/glum/gui/table/sort/SortIconProvider.java b/src/glum/gui/table/sort/SortIconProvider.java new file mode 100644 index 0000000..75ee924 --- /dev/null +++ b/src/glum/gui/table/sort/SortIconProvider.java @@ -0,0 +1,46 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.gui.table.sort; + +import java.util.List; + +import javax.swing.Icon; + +/** + * Interface that allows customization of the sort icons. + * + * @author lopeznr1 + */ +public interface SortIconProvider +{ + /** + * Returns a list of icons that will be utilized as the sort icon indicators (when the items are sorted ascending). + * Icons at the front of the list will be associated with columns of data that have a higher sort priority. + * + * @param aSize + * The size of the sort icons in pixels. + * @return + */ + public List getIconsForSortAsce(int aSize); + + /** + * Returns a list of icons that will be utilized as the sort icon indicators (when the items are sorted descending). + * Icons at the front of the list will be associated with columns of data that have a higher sort priority. + * + * @param aSize + * The size of the sort icons in pixels. + * @return + */ + public List getIconsForSortDesc(int aSize); +} diff --git a/src/glum/gui/unit/DateUnitPanel.java b/src/glum/gui/unit/DateUnitPanel.java index d88e756..2c76c19 100644 --- a/src/glum/gui/unit/DateUnitPanel.java +++ b/src/glum/gui/unit/DateUnitPanel.java @@ -1,39 +1,48 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.unit; import java.awt.Dimension; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.util.List; -import java.util.TimeZone; +import java.awt.event.*; +import java.util.*; -import javax.swing.*; +import javax.swing.JLabel; +import javax.swing.JRadioButton; import javax.swing.table.DefaultTableCellRenderer; -import com.google.common.collect.Lists; - import glum.database.QueryItem; import glum.gui.GuiUtil; -import glum.gui.component.GComboBox; -import glum.gui.component.GLabel; -import glum.gui.component.GTextField; +import glum.gui.component.*; import glum.gui.document.CharDocument; -import glum.gui.panel.itemList.ItemHandler; import glum.gui.panel.itemList.ItemListPanel; import glum.gui.panel.itemList.StaticItemProcessor; import glum.gui.panel.itemList.query.QueryComposer; import glum.gui.panel.itemList.query.QueryItemHandler; -import glum.unit.DateUnit; -import glum.unit.DateUnitProvider; -import glum.unit.UnitProvider; - +import glum.unit.*; import net.miginfocom.swing.MigLayout; -public class DateUnitPanel extends EditorPanel implements ActionListener +/** + * Panel that allows the user to view and configure a {@link DateUnitProvider}. + * + * @author lopeznr1 + */ +public class DateUnitPanel extends EditorPanel implements ActionListener, ItemListener { // Constants // @formatter:off public static final String[][] DescriptionArr = { - {"G", "Era", "AD"}, + {"G", "Era", "AD"}, {"y", "Year", "1996"}, {"M", "Month in year", "Jul; 07"}, {"w", "Week in year", "27"}, @@ -56,7 +65,7 @@ public class DateUnitPanel extends EditorPanel implements ActionListener private GComboBox protoBox; private GTextField custTF; private GLabel exampleL; - private ItemListPanel ruleLP; + private ItemListPanel ruleLP; private DefaultTableCellRenderer plainRenderer; private JLabel timeZoneL; private GComboBox timeZoneBox; @@ -64,10 +73,9 @@ public class DateUnitPanel extends EditorPanel implements ActionListener // State vars private DateUnitProvider myUnitProvider; + /** Standard Constructor */ public DateUnitPanel() { - super(); - myUnitProvider = null; buildGuiArea(); @@ -93,29 +101,34 @@ public class DateUnitPanel extends EditorPanel implements ActionListener notifyListeners(this, ID_UPDATE, "unit.update"); } + @Override + public void itemStateChanged(ItemEvent aEvent) + { + updateModel(); + updateGui(); + + notifyListeners(this, ID_UPDATE, "unit.update"); + } + @Override public void setUnitProvider(UnitProvider aUnitProvider) { - List protoNameList; - DateUnit activeUnit; - TimeZone activeTimeZone; - // Update our UnitProvider myUnitProvider = null; if (aUnitProvider instanceof DateUnitProvider) - myUnitProvider = (DateUnitProvider)aUnitProvider; + myUnitProvider = (DateUnitProvider) aUnitProvider; // Sync the GUI to the state of the aEditable - protoNameList = myUnitProvider.getProtoNameList(); + var protoNameL = myUnitProvider.getProtoNameList(); - activeUnit = myUnitProvider.getUnit(); - activeTimeZone = activeUnit.getTimeZone(); + var activeUnit = myUnitProvider.getUnit(); + var activeTimeZone = activeUnit.getTimeZone(); // Synch the GUI with the UnitProvider custTF.setValue(myUnitProvider.getCustomPattern()); protoBox.removeAllItems(); - for (String aName : protoNameList) + for (String aName : protoNameL) protoBox.addItem(aName); protoBox.setChosenItem(myUnitProvider.getProtoUnit().getConfigName()); @@ -142,12 +155,6 @@ public class DateUnitPanel extends EditorPanel implements ActionListener */ private void buildGuiArea() { - CharDocument charDoc; - QueryComposer aComposer; - ItemHandler itemHandler; - StaticItemProcessor itemProcessor; - int targH; - setLayout(new MigLayout("", "0[left][grow]0", "0[][][][][][]0[grow]0")); // Example area @@ -158,19 +165,22 @@ public class DateUnitPanel extends EditorPanel implements ActionListener add(GuiUtil.createDivider(), "growx,h 4!,span,wrap"); // Specification area - typeRB = GuiUtil.createJRadioButton("Named:", this); + typeRB = GuiUtil.createJRadioButton(this, "Named:"); protoBox = new GComboBox(this); add("span 1", typeRB); add("growx,span,wrap", protoBox); // Custom area - custRB = GuiUtil.createJRadioButton("Custom:", this); + custRB = GuiUtil.createJRadioButton(this, "Custom:"); custTF = new GTextField(this); - charDoc = new CharDocument(custTF, "GyMwWDdEaHhmsSzZ :|\\/-,[](){}<>;.", true); + var charDoc = new CharDocument(custTF, "GyMwWDdEaHhmsSzZ :|\\/-,[](){}<>;.", true); custTF.setDocument(charDoc); add("span 1", custRB); add("growx,span,wrap", custTF); + // Link the radio buttons + GuiUtil.linkRadioButtons(custRB, typeRB); + // TimeZone area timeZoneL = new JLabel("TimeZone:"); timeZoneBox = new GComboBox(this, new TimeZoneCellRenderer()); @@ -180,29 +190,25 @@ public class DateUnitPanel extends EditorPanel implements ActionListener add("span 1", timeZoneL); add("growx,span,w 0:100:,wrap", timeZoneBox); - // Link the radio buttons - GuiUtil.linkRadioButtons(custRB, typeRB); - // Rules table - List itemList; - itemList = Lists.newLinkedList(); + var tmpItemL = new ArrayList(); for (String[] aRow : DescriptionArr) - itemList.add(new PlainRow(aRow[0], aRow[1], aRow[2])); + tmpItemL.add(new PlainRow(aRow[0], aRow[1], aRow[2])); - aComposer = new QueryComposer(); - aComposer.addAttribute(Lookup.Key, String.class, "Key", "Key"); - aComposer.addAttribute(Lookup.Comp, String.class, "Date Comp.", "Time Zone: General"); - aComposer.addAttribute(Lookup.Example, String.class, "Example", "Eastern; EST"); - aComposer.getItem(Lookup.Example).maxSize = 5000; + var tmpComposer = new QueryComposer(); + tmpComposer.addAttribute(Lookup.Key, String.class, "Key", "Key"); + tmpComposer.addAttribute(Lookup.Comp, String.class, "Date Comp.", "Time Zone: General"); + tmpComposer.addAttribute(Lookup.Example, String.class, "Example", "Eastern; EST"); + tmpComposer.getItem(Lookup.Example).maxSize = 5000; plainRenderer = new DefaultTableCellRenderer(); for (Lookup aEnum : Lookup.values()) - aComposer.setRenderer(aEnum, plainRenderer); + tmpComposer.setRenderer(aEnum, plainRenderer); - itemHandler = new QueryItemHandler(aComposer); - itemProcessor = new StaticItemProcessor(itemList); + var tmpIH = new QueryItemHandler(); + var tmpIP = new StaticItemProcessor<>(tmpItemL); - targH = ((custTF.getPreferredSize().height - 2) * DescriptionArr.length) + 2; - ruleLP = new ItemListPanel(itemHandler, itemProcessor, false, false); + var targH = ((custTF.getPreferredSize().height - 2) * DescriptionArr.length) + 2; + ruleLP = new ItemListPanel<>(tmpIH, tmpIP, tmpComposer, false); ruleLP.setPreferredSize(new Dimension(ruleLP.getPreferredSize().width, targH)); ruleLP.setSortingEnabled(false); ruleLP.setEnabled(false); @@ -214,13 +220,11 @@ public class DateUnitPanel extends EditorPanel implements ActionListener */ private void updateModel() { - TimeZone timeZone; - // Need a valid UnitProvider if (myUnitProvider == null) return; - timeZone = timeZoneBox.getChosenItem(); + var timeZone = timeZoneBox.getChosenItem(); if (typeRB.isSelected() == true) myUnitProvider.activateProto(timeZone, protoBox.getChosenItem()); else @@ -232,24 +236,19 @@ public class DateUnitPanel extends EditorPanel implements ActionListener */ private void updateGui() { - DateUnit activeUnit; - String exampleStr; - boolean isEnabled; - long currTime; - - isEnabled = custRB.isSelected(); + var isEnabled = custRB.isSelected(); custTF.setEnabled(isEnabled); protoBox.setEnabled(!isEnabled); // itemLP.repaint(); // Retrieve the activeUnit - activeUnit = null; + var activeUnit = (DateUnit) null; if (myUnitProvider != null) activeUnit = myUnitProvider.getUnit(); // Update the example area - currTime = System.currentTimeMillis(); - exampleStr = ""; + var currTime = System.currentTimeMillis(); + var exampleStr = ""; if (activeUnit != null) exampleStr = activeUnit.getString(currTime); @@ -258,15 +257,22 @@ public class DateUnitPanel extends EditorPanel implements ActionListener /** * Helper classes to aide with setting up the info table + * + * @author lopeznr1 */ enum Lookup { Key, Comp, Example }; + /** + * Helper classes to aide with setting up the info table + * + * @author lopeznr1 + */ class PlainRow implements QueryItem { - private String key, comp, example; + private final String key, comp, example; public PlainRow(String aKey, String aComp, String aExample) { @@ -281,16 +287,16 @@ public class DateUnitPanel extends EditorPanel implements ActionListener switch (aEnum) { case Key: - return key; + return key; case Comp: - return comp; + return comp; case Example: - return example; + return example; default: - return null; + return null; } } diff --git a/src/glum/gui/unit/DecimalUnitPanel.java b/src/glum/gui/unit/DecimalUnitPanel.java index af93d62..cb82aa3 100644 --- a/src/glum/gui/unit/DecimalUnitPanel.java +++ b/src/glum/gui/unit/DecimalUnitPanel.java @@ -1,22 +1,39 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.unit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.text.DecimalFormat; import java.util.List; -import javax.swing.*; + +import javax.swing.JCheckBox; +import javax.swing.JLabel; + +import com.google.common.collect.Range; import glum.gui.GuiUtil; import glum.gui.component.GComboBox; import glum.gui.component.GNumberField; -import glum.unit.ConstUnitProvider; -import glum.unit.DecimalUnitProvider; -import glum.unit.NumberUnit; -import glum.unit.Unit; -import glum.unit.UnitProvider; - +import glum.unit.*; import net.miginfocom.swing.MigLayout; +/** + * User input component that allows the user to specify a {@link DecimalUnitProvider}. + * + * @author lopeznr1 + */ public class DecimalUnitPanel extends EditorPanel implements ActionListener { // Gui components @@ -28,10 +45,9 @@ public class DecimalUnitPanel extends EditorPanel implements ActionListener // State vars private DecimalUnitProvider myUnitProvider; + /** Standard Constructor */ public DecimalUnitPanel() { - super(); - myUnitProvider = null; buildGuiArea(); @@ -50,26 +66,21 @@ public class DecimalUnitPanel extends EditorPanel implements ActionListener @Override public void setUnitProvider(UnitProvider aUnitProvider) { - int decimalPlaces; - boolean forceFullLabel; - List unitList; - Unit protoUnit, chosenUnit; - // Update our UnitProvider myUnitProvider = null; if (aUnitProvider instanceof DecimalUnitProvider) - myUnitProvider = (DecimalUnitProvider)aUnitProvider; + myUnitProvider = (DecimalUnitProvider) aUnitProvider; // Sync the GUI to the state of the aEditable - decimalPlaces = 0; - forceFullLabel = false; - unitList = null; - protoUnit = null; + var decimalPlaces = 0; + var forceFullLabel = false; + List unitL = null; + Unit protoUnit = null; if (myUnitProvider != null) { decimalPlaces = myUnitProvider.getDecimalPlaces(); forceFullLabel = myUnitProvider.getForceFullLabel(); - unitList = myUnitProvider.getProtoUnitList(); + unitL = myUnitProvider.getProtoUnitList(); protoUnit = myUnitProvider.getProtoUnit(); } @@ -77,9 +88,9 @@ public class DecimalUnitPanel extends EditorPanel implements ActionListener decimalPlacesNF.setValue(decimalPlaces); forceLongUnitsCB.setSelected(forceFullLabel); - chosenUnit = null; + Unit chosenUnit = null; unitBox.removeAllItems(); - for (Unit aUnit : unitList) + for (Unit aUnit : unitL) { unitBox.addItem(aUnit); if (aUnit == protoUnit) @@ -97,10 +108,8 @@ public class DecimalUnitPanel extends EditorPanel implements ActionListener */ private void buildGuiArea() { - UnitProvider countUP; - setLayout(new MigLayout("", "0[right][][grow]0", "0[][]0[]0")); - countUP = new ConstUnitProvider(new NumberUnit("", "", 1.0, new DecimalFormat("###,###,###,###,##0"))); + var countUP = new ConstUnitProvider(new NumberUnit("", "", 1.0, new DecimalFormat("###,###,###,###,##0"))); // Unit area unitL = new JLabel("Unit:"); @@ -109,8 +118,9 @@ public class DecimalUnitPanel extends EditorPanel implements ActionListener add("growx,span,wrap", unitBox); // DecimalPlaces area + var tmpRange = Range.closed(0.0, 9.0); decimalPlacesL = new JLabel("Decimal Places:"); - decimalPlacesNF = new GNumberField(this, countUP, 0, 9); + decimalPlacesNF = new GNumberField(this, countUP, tmpRange); add("span 2", decimalPlacesL); add("growx,span 1,wrap", decimalPlacesNF); @@ -124,16 +134,13 @@ public class DecimalUnitPanel extends EditorPanel implements ActionListener */ private void updateGui() { - Unit protoUnit; - boolean isEnabled; - // Need a valid UnitProvider if (myUnitProvider == null) return; // Synch the gui components - protoUnit = unitBox.getChosenItem(); - isEnabled = (protoUnit instanceof NumberUnit); + var protoUnit = unitBox.getChosenItem(); + var isEnabled = (protoUnit instanceof NumberUnit); forceLongUnitsCB.setEnabled(isEnabled); } @@ -142,14 +149,10 @@ public class DecimalUnitPanel extends EditorPanel implements ActionListener */ private void updateModel() { - Unit protoUnit; - int decimalPlaces; - boolean forceLongUnits; - // Get the gui configuration - protoUnit = unitBox.getChosenItem(); - decimalPlaces = decimalPlacesNF.getValueAsInt(0); - forceLongUnits = forceLongUnitsCB.isSelected(); + var protoUnit = unitBox.getChosenItem(); + var decimalPlaces = decimalPlacesNF.getValueAsInt(0); + var forceLongUnits = forceLongUnitsCB.isSelected(); // Update the UnitProvider myUnitProvider.activate(protoUnit, decimalPlaces, forceLongUnits); diff --git a/src/glum/gui/unit/EditorPanel.java b/src/glum/gui/unit/EditorPanel.java index 75abeea..7f9701c 100644 --- a/src/glum/gui/unit/EditorPanel.java +++ b/src/glum/gui/unit/EditorPanel.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.unit; import glum.gui.panel.GPanel; diff --git a/src/glum/gui/unit/LatLonUnitPanel.java b/src/glum/gui/unit/LatLonUnitPanel.java deleted file mode 100644 index 4f933d1..0000000 --- a/src/glum/gui/unit/LatLonUnitPanel.java +++ /dev/null @@ -1,196 +0,0 @@ -package glum.gui.unit; - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.text.DecimalFormat; -import javax.swing.*; - -import net.miginfocom.swing.MigLayout; - -import glum.gui.GuiUtil; -import glum.gui.component.GComboBox; -import glum.gui.component.GNumberField; -import glum.gui.panel.CardPanel; -import glum.unit.ConstUnitProvider; -import glum.unit.LatLonUnitProvider; -import glum.unit.NumberUnit; -import glum.unit.UnitProvider; - -public class LatLonUnitPanel extends EditorPanel implements ActionListener -{ - // Gui components - private GComboBox unitBox; - private CardPanel editPanel; - private GNumberField decimalPlacesNF; - private JCheckBox isZeroCenteredCB; - private JCheckBox isSecondsShownCB; - - // State vars - private LatLonUnitProvider myUnitProvider; - - public LatLonUnitPanel() - { - super(); - - myUnitProvider = null; - buildGuiArea(); - updateGui(); - } - - @Override - public void actionPerformed(ActionEvent aEvent) - { - Object source; - - source = aEvent.getSource(); - if (source == unitBox) - updateGui(); - - updateEditable(); - notifyListeners(this, ID_UPDATE, "unit.update"); - } - - @Override - public void setUnitProvider(UnitProvider aUnitProvider) - { - String unitTypeStr; - int decimalPlaces; - boolean isSecondsShown; - boolean isZeroCentered; - - // Update our UnitProvider - myUnitProvider = null; - if (aUnitProvider instanceof LatLonUnitProvider) - myUnitProvider = (LatLonUnitProvider)aUnitProvider; - - // Sync the GUI to the state of the UnitProvider - decimalPlaces = decimalPlacesNF.getValueAsInt(0); - isSecondsShown = isSecondsShownCB.isSelected(); - isZeroCentered = isZeroCenteredCB.isSelected(); - unitTypeStr = "Raw"; - if (myUnitProvider != null) - { - if (myUnitProvider.isRawUnitActive() == true) - { - decimalPlaces = myUnitProvider.getDecimalPlaces(); - isZeroCentered = myUnitProvider.isZeroCentered(); - unitTypeStr = "Raw"; - } - else - { - isSecondsShown = myUnitProvider.isSecondsShown(); - unitTypeStr = "Standard"; - } - } - - // Synch the GUI with the UnitProvider - decimalPlacesNF.setValue(decimalPlaces); - isSecondsShownCB.setSelected(isSecondsShown); - isZeroCenteredCB.setSelected(isZeroCentered); - - unitBox.removeActionListener(this); - unitBox.setSelectedItem(unitTypeStr); - unitBox.addActionListener(this); - - updateGui(); - } - - /** - * Forms the GUI - */ - private void buildGuiArea() - { - JLabel unitL; - - setLayout(new MigLayout("", "0[right][][grow]0", "0[][grow]0")); - - // Unit area - unitL = new JLabel("Unit:", JLabel.CENTER); - unitBox = new GComboBox(this, "Raw", "Standard"); - add("span 1", unitL); - add("growx,span,wrap", unitBox); - - // Edit area - editPanel = new CardPanel(); - editPanel.addCard("Raw", formRawPanel()); - editPanel.addCard("Standard", formStandardPanel()); - add("growx,growy,span", editPanel); - } - - /** - * Forms the panel to configure the raw unit for lat/lon - */ - private JPanel formRawPanel() - { - JPanel tmpPanel; - JLabel tmpL; - UnitProvider countUP; - - tmpPanel = new JPanel(new MigLayout("", "0[right][][grow]0", "0[]0")); - countUP = new ConstUnitProvider(new NumberUnit("", "", 1.0, new DecimalFormat("###,###,###,###,##0"))); - - // DecimalPlaces area - tmpL = new JLabel("Decimal Places:", JLabel.CENTER); - decimalPlacesNF = new GNumberField(this, countUP, 0, 9); - tmpPanel.add("span 2", tmpL); - tmpPanel.add("growx,span 1,wrap", decimalPlacesNF); - - // Zero centered area - isZeroCenteredCB = GuiUtil.createJCheckBox("Zero Centered", this); - tmpPanel.add("align left,span", isZeroCenteredCB); - - return tmpPanel; - } - - /** - * Forms the panel to configure the standard unit for lat/lon - */ - private JPanel formStandardPanel() - { - JPanel tmpPanel; - - tmpPanel = new JPanel(new MigLayout("", "0[grow]0", "0[]0")); - - isSecondsShownCB = GuiUtil.createJCheckBox("Show Seconds", this); - tmpPanel.add("span 1", isSecondsShownCB); - - return tmpPanel; - } - - /** - * Updates the GUI - */ - private void updateGui() - { - String unitTypeStr; - - unitTypeStr = unitBox.getChosenItem(); - editPanel.switchToCard(unitTypeStr); - } - - /** - * Updates the UnitProvider - */ - private void updateEditable() - { - String unitTypeStr; - boolean isSecondsShown; - boolean isZeroCentered; - int decimalPlaces; - - // Bail if no UnitProvider - if (myUnitProvider == null) - return; - - isSecondsShown = isSecondsShownCB.isSelected(); - isZeroCentered = isZeroCenteredCB.isSelected(); - decimalPlaces = decimalPlacesNF.getValueAsInt(0); - - unitTypeStr = unitBox.getChosenItem(); - if (unitTypeStr.equals("Raw") == true) - myUnitProvider.activateRaw(decimalPlaces, isZeroCentered); - else - myUnitProvider.activateStandard(isSecondsShown); - } - -} diff --git a/src/glum/gui/unit/TimeZoneCellRenderer.java b/src/glum/gui/unit/TimeZoneCellRenderer.java index 6a3dc75..339945f 100644 --- a/src/glum/gui/unit/TimeZoneCellRenderer.java +++ b/src/glum/gui/unit/TimeZoneCellRenderer.java @@ -1,30 +1,45 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.unit; import java.awt.Component; import java.util.TimeZone; -import javax.swing.DefaultListCellRenderer; -import javax.swing.JLabel; -import javax.swing.JList; +import javax.swing.*; +/** + * ListCellRenderer that displays the ID of the specified {@link TimeZone}. + * + * @author lopeznr1 + */ public class TimeZoneCellRenderer extends DefaultListCellRenderer { + /** Standard Constructor */ public TimeZoneCellRenderer() { - super(); + ; // Nothing to do } @Override - public Component getListCellRendererComponent(JList list, Object aObj, int index, boolean isSelected, boolean hasFocus) + public Component getListCellRendererComponent(JList list, Object aObj, int index, boolean isSelected, + boolean hasFocus) { - JLabel retL; - String aStr; - - retL = (JLabel)super.getListCellRendererComponent(list, aObj, index, isSelected, hasFocus); + var retL = (JLabel) super.getListCellRendererComponent(list, aObj, index, isSelected, hasFocus); if (aObj instanceof TimeZone) { - aStr = ((TimeZone)aObj).getID(); - retL.setText(aStr); + var tmpStr = ((TimeZone) aObj).getID(); + retL.setText(tmpStr); } return retL; diff --git a/src/glum/gui/unit/UnitConfigurationDialog.java b/src/glum/gui/unit/UnitConfigurationDialog.java index 178f317..f83eb40 100644 --- a/src/glum/gui/unit/UnitConfigurationDialog.java +++ b/src/glum/gui/unit/UnitConfigurationDialog.java @@ -1,14 +1,26 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.unit; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.util.List; +import java.util.ArrayList; import javax.swing.*; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; -import javax.swing.table.*; import glum.gui.FocusUtil; import glum.gui.GuiUtil; @@ -20,10 +32,15 @@ import glum.unit.UnitListener; import glum.unit.UnitProvider; import net.miginfocom.swing.MigLayout; +/** + * JDialog that allows for the configuration of UnitProviders. + * + * @author lopeznr1 + */ public class UnitConfigurationDialog extends JDialog implements ActionListener, ListSelectionListener, UnitListener { // Gui Components - private ItemListPanel itemLP; + private ItemListPanel itemLP; private CardPanel editorPanel; private JLabel titleL; private JButton closeB; @@ -31,6 +48,7 @@ public class UnitConfigurationDialog extends JDialog implements ActionListener, // State vars private StaticItemProcessor itemProcessor; + /** Standard Constructor */ public UnitConfigurationDialog(JFrame aParentFrame) { // Make sure we call the parent @@ -59,8 +77,6 @@ public class UnitConfigurationDialog extends JDialog implements ActionListener, */ public void addEditorPanel(Class aClass, EditorPanel aPanel) { - Dimension aDim; - // Insanity check if (aClass == null || aPanel == null) return; @@ -70,15 +86,15 @@ public class UnitConfigurationDialog extends JDialog implements ActionListener, editorPanel.switchToCard("null"); updateGui(); - aDim = editorPanel.getMinimumSize(); + var tmpDim = editorPanel.getMinimumSize(); for (EditorPanel evalPanel : editorPanel.getAllCards()) { - if (evalPanel.getMinimumSize().height > aDim.height) - aDim.height = evalPanel.getMinimumSize().height; + if (evalPanel.getMinimumSize().height > tmpDim.height) + tmpDim.height = evalPanel.getMinimumSize().height; } // Set the dialog to the best initial size - setSize(getPreferredSize().width, itemLP.getHeight() + closeB.getHeight() + titleL.getHeight() + aDim.height); + setSize(getPreferredSize().width, itemLP.getHeight() + closeB.getHeight() + titleL.getHeight() + tmpDim.height); } /** @@ -86,8 +102,6 @@ public class UnitConfigurationDialog extends JDialog implements ActionListener, */ public void addUnitProvider(UnitProvider aUnitProvider) { - List itemList; - // Insanity check if (aUnitProvider == null) return; @@ -96,10 +110,9 @@ public class UnitConfigurationDialog extends JDialog implements ActionListener, aUnitProvider.addListener(this); // Update the table processor - itemList = itemProcessor.getItems(); - itemList.add(aUnitProvider); - ; - itemProcessor.setItems(itemList); + var itemL = new ArrayList<>(itemProcessor.getAllItems()); + itemL.add(aUnitProvider); + itemProcessor.setItems(itemL); // Update the dialog updateTable(); @@ -108,9 +121,7 @@ public class UnitConfigurationDialog extends JDialog implements ActionListener, @Override public void actionPerformed(ActionEvent aEvent) { - Object source; - - source = aEvent.getSource(); + var source = aEvent.getSource(); if (source == closeB) { setVisible(false); @@ -126,9 +137,7 @@ public class UnitConfigurationDialog extends JDialog implements ActionListener, @Override public void valueChanged(ListSelectionEvent aEvent) { - Object source; - - source = aEvent.getSource(); + var source = aEvent.getSource(); if (source == itemLP) { if (aEvent.getValueIsAdjusting() == true) @@ -143,10 +152,6 @@ public class UnitConfigurationDialog extends JDialog implements ActionListener, */ private void buildGuiArea() { - QueryComposer aComposer; - ItemHandler itemHandler; - EditorPanel nullPanel; - setLayout(new MigLayout("", "[center,grow]", "2[]3[grow][fill,growprio 200][]")); // Build the title label @@ -154,20 +159,20 @@ public class UnitConfigurationDialog extends JDialog implements ActionListener, add("span,wrap", titleL); // UnitProvider table - aComposer = new QueryComposer(); - aComposer.addAttribute(Lookup.Key, String.class, "Type", null); - aComposer.addAttribute(Lookup.Value, String.class, "Value", null); + var tmpComposer = new QueryComposer(); + tmpComposer.addAttribute(LookUp.Key, String.class, "Type", null); + tmpComposer.addAttribute(LookUp.Value, String.class, "Value", null); - itemHandler = new UnitProviderHandler(aComposer); - itemProcessor = new StaticItemProcessor(); + var tmpIH = new UnitProviderHandler(); + itemProcessor = new StaticItemProcessor<>(); - itemLP = new ItemListPanel(itemHandler, itemProcessor, false, false); + itemLP = new ItemListPanel<>(tmpIH, itemProcessor, tmpComposer, false); itemLP.addListSelectionListener(this); itemLP.setSortingEnabled(false); add("growx,growy,h 70::,span,wrap", itemLP); // Form the editor area - nullPanel = new EditorPanel(); + var nullPanel = new EditorPanel(); nullPanel.setPreferredSize(new Dimension(1, 1)); editorPanel = new CardPanel(); @@ -184,35 +189,29 @@ public class UnitConfigurationDialog extends JDialog implements ActionListener, */ private void updateGui() { - EditorPanel aPanel; - UnitProvider aUnitProvider; - Dimension aDim; - String cardName; - // Get the selected UnitProvider - aUnitProvider = itemLP.getSelectedItem(); + var tmpUnitProvider = itemLP.getSelectedItem(); // Switch to the appropriate Editor - cardName = "null"; - if (aUnitProvider != null) - cardName = "" + aUnitProvider.getClass(); + var cardName = "null"; + if (tmpUnitProvider != null) + cardName = "" + tmpUnitProvider.getClass(); editorPanel.switchToCard(cardName); // Resize the editorPanel to be as compact as the active card - aPanel = editorPanel.getActiveCard(); - aPanel.setUnitProvider(aUnitProvider); - aDim = aPanel.getPreferredSize(); + var tmpPanel = editorPanel.getActiveCard(); + tmpPanel.setUnitProvider(tmpUnitProvider); + var tmpDim = tmpPanel.getPreferredSize(); // System.out.println("minHeight: " + aDim.getHeight() + " hmm: " + aPanel); - editorPanel.setMaximumSize(new Dimension(5000, aDim.height)); - // Hack to get the editorPanel resize properly. Not sure why invalidate(), validate() do not work + editorPanel.setMaximumSize(new Dimension(5000, tmpDim.height)); + // Hack to get the editorPanel resize properly. Not sure why invalidate(), + // validate() do not work int aHeight = getHeight(); - Runnable tmpRunnable1 = () -> setSize(getWidth(), aHeight - 1); - Runnable tmpRunnable2 = () -> setSize(getWidth(), aHeight); - SwingUtilities.invokeLater(tmpRunnable1); - SwingUtilities.invokeLater(tmpRunnable2); -// Runnable tmpRunnable1 = () -> editorPanel.invalidate(); -// Runnable tmpRunnable2 = () -> editorPanel.validate(); + SwingUtilities.invokeLater(() -> setSize(getWidth(), aHeight - 1)); + SwingUtilities.invokeLater(() -> setSize(getWidth(), aHeight)); +// SwingUtilities.invokeLater(() -> editorPanel.invalidate()); +// SwingUtilities.invokeLater(() -> editorPanel.validate()); // invalidate(); // validate(); } @@ -222,80 +221,81 @@ public class UnitConfigurationDialog extends JDialog implements ActionListener, */ private void updateTable() { - List itemList; - JTableHeader aTableHeader; - TableColumnModel aTableColumnModel; - TableColumn aTableColumn; - int aWidth, tmpWidth; - JLabel aLabel; - // Update myTable column[0] width - aWidth = 10; - aLabel = new JLabel(""); - itemList = itemProcessor.getItems(); - for (UnitProvider aUnitProvider : itemList) + var aWidth = 10; + var tmpLabel = new JLabel(""); + var itemL = itemProcessor.getAllItems(); + for (UnitProvider aUnitProvider : itemL) { - aLabel.setText(aUnitProvider.getDisplayName()); - tmpWidth = aLabel.getPreferredSize().width + 5; + tmpLabel.setText(aUnitProvider.getDisplayName()); + var tmpWidth = tmpLabel.getPreferredSize().width + 5; if (aWidth < tmpWidth) aWidth = tmpWidth; } // Set sizing attributes of the column 1 - aTableHeader = itemLP.getTable().getTableHeader(); - aTableColumnModel = aTableHeader.getColumnModel(); - aTableColumn = aTableColumnModel.getColumn(0); - aTableColumn.setResizable(false); - aTableColumn.setMinWidth(aWidth); - aTableColumn.setMaxWidth(aWidth); - aTableColumn.setPreferredWidth(aWidth); + var tmpTableHeader = itemLP.getTable().getTableHeader(); + var tmpTableColumnModel = tmpTableHeader.getColumnModel(); + var tmpTableColumn = tmpTableColumnModel.getColumn(0); + tmpTableColumn.setResizable(false); + tmpTableColumn.setMinWidth(aWidth); + tmpTableColumn.setMaxWidth(aWidth); + tmpTableColumn.setPreferredWidth(aWidth); } /** - * Helper classes to aide with setting up the UnitProvider table + * Enum which defines the types used to show a listing of configurable units. + * + * @author lopeznr1 */ - enum Lookup + enum LookUp { Key, + Value, }; - public class UnitProviderHandler extends BasicItemHandler + /** + * Implementation of {@link ItemHandler} that allows for tabular access of {@link UnitProvider}s. + * + * @author lopeznr1 + */ + public class UnitProviderHandler implements ItemHandler { - public UnitProviderHandler(QueryComposer aComposer) - { - super(aComposer); - } - @Override - public Object getColumnValue(UnitProvider aItem, int aColNum) + public Object getValue(UnitProvider aItem, LookUp aEnum) { - Enum refKey; - - // Insanity check - if (aColNum < 0 && aColNum >= fullAttributeList.size()) - return null; - - refKey = fullAttributeList.get(aColNum).refKey; - switch ((Lookup)refKey) + switch (aEnum) { case Key: return aItem.getDisplayName(); case Value: - return aItem.getConfigName(); + return getConfigName(aItem); default: break; } - return null; + throw new RuntimeException("Unsupported enum:" + aEnum); } @Override - public void setColumnValue(UnitProvider aItem, int colNum, Object aValue) + public void setValue(UnitProvider aItem, LookUp aEnum, Object aValue) { - throw new RuntimeException("Unsupported Operation."); + throw new RuntimeException("Unsupported enum:" + aEnum); + } + + /** + * Helper method that returns the configuration name to utilize. + */ + public String getConfigName(UnitProvider aUnitProvider) + { + var tmpUnit = aUnitProvider.getUnit(); + if (tmpUnit == null) + return "None"; + + return tmpUnit.getConfigName(); } } diff --git a/src/glum/gui/unit/UnitLabelRenderer.java b/src/glum/gui/unit/UnitLabelRenderer.java index b566e3f..186a960 100644 --- a/src/glum/gui/unit/UnitLabelRenderer.java +++ b/src/glum/gui/unit/UnitLabelRenderer.java @@ -1,28 +1,46 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.gui.unit; import java.awt.Component; + import javax.swing.*; import glum.unit.Unit; +/** + * ListCellRenderer that displays the {@link Unit}'s configuration name. + * + * @author lopeznr1 + */ public class UnitLabelRenderer extends DefaultListCellRenderer { + /** Standard Constructor */ public UnitLabelRenderer() { - super(); + ; // Nothing to do } @Override - public Component getListCellRendererComponent(JList list, Object aObj, int index, boolean isSelected, boolean hasFocus) + public Component getListCellRendererComponent(JList list, Object aObj, int index, boolean isSelected, + boolean hasFocus) { - JLabel retL; - String aStr; - - retL = (JLabel)super.getListCellRendererComponent(list, aObj, index, isSelected, hasFocus); + var retL = (JLabel) super.getListCellRendererComponent(list, aObj, index, isSelected, hasFocus); if (aObj instanceof Unit) { - aStr = ((Unit)aObj).getConfigName(); - retL.setText(aStr); + var tmpStr = ((Unit) aObj).getConfigName(); + retL.setText(tmpStr); } return retL; diff --git a/src/glum/io/ConfigMapTP.java b/src/glum/io/ConfigMapTP.java index 1091c9b..77d1431 100644 --- a/src/glum/io/ConfigMapTP.java +++ b/src/glum/io/ConfigMapTP.java @@ -1,12 +1,31 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.io; import glum.registry.ConfigMap; +/** + * Implementation of {@link TokenProcessor} used to populate a {@link ConfigMap}. + * + * @author lopeznr1 + */ public class ConfigMapTP implements TokenProcessor { // State vars - ConfigMap refConfigMap; + private ConfigMap refConfigMap; + /** Standard Constructor */ public ConfigMapTP(ConfigMap aConfigMap) { refConfigMap = aConfigMap; @@ -19,20 +38,20 @@ public class ConfigMapTP implements TokenProcessor } @Override - public boolean process(String[] tokens, int lineNum) + public boolean process(String[] aTokenArr, int aLineNum) { // Insanity check - if (tokens == null || tokens.length <= 1) + if (aTokenArr == null || aTokenArr.length <= 1) return false; - if (tokens.length == 2) + if (aTokenArr.length == 2) { - refConfigMap.put(tokens[0], tokens[1]); + refConfigMap.put(aTokenArr[0], aTokenArr[1]); } else { - for (int c1 = 1; c1 < tokens.length; c1++) - refConfigMap.addItem(tokens[0], tokens[c1]); + for (int c1 = 1; c1 < aTokenArr.length; c1++) + refConfigMap.addItem(aTokenArr[0], aTokenArr[c1]); } return true; diff --git a/src/glum/io/IoUtil.java b/src/glum/io/IoUtil.java index 169fe8e..8f53946 100644 --- a/src/glum/io/IoUtil.java +++ b/src/glum/io/IoUtil.java @@ -1,17 +1,33 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.io; -import glum.task.*; -import glum.zio.ZinStream; -import glum.zio.ZoutStream; - import java.io.*; import java.net.*; import java.nio.channels.Channel; -import java.util.Base64; -import java.util.Map; +import java.util.*; -import com.google.common.collect.Maps; +import glum.task.ConsoleTask; +import glum.task.Task; +import glum.zio.ZinStream; +import glum.zio.ZoutStream; +/** + * Collection of various utility methods. + * + * @author lopeznr1 + */ public class IoUtil { /** @@ -19,27 +35,14 @@ public class IoUtil */ public static URL createURL(String aUrlStr) { - URL retURL; - try { - retURL = new URL(aUrlStr); + return new URL(aUrlStr); } - catch(MalformedURLException e) + catch (MalformedURLException e) { - retURL = null; + return null; } - - return retURL; - } - - /** - * Prints out the error msg followed by the stack trace. - */ - public static void dumpTrace(Exception aExp, String aMsg) - { - System.out.println(aMsg); - aExp.printStackTrace(); } /** @@ -54,7 +57,7 @@ public class IoUtil { aChannel.close(); } - catch(Exception aExp) + catch (Exception aExp) { aExp.printStackTrace(); } @@ -72,7 +75,7 @@ public class IoUtil { aStream.close(); } - catch(Exception aExp) + catch (Exception aExp) { aExp.printStackTrace(); } @@ -90,7 +93,7 @@ public class IoUtil { aStream.close(); } - catch(Exception aExp) + catch (Exception aExp) { aExp.printStackTrace(); } @@ -108,7 +111,7 @@ public class IoUtil { aReader.close(); } - catch(Exception aExp) + catch (Exception aExp) { aExp.printStackTrace(); } @@ -126,7 +129,7 @@ public class IoUtil { aWriter.close(); } - catch(Exception aExp) + catch (Exception aExp) { aExp.printStackTrace(); } @@ -145,7 +148,7 @@ public class IoUtil { aStream.close(); } - catch(Exception aExp) + catch (Exception aExp) { aExp.printStackTrace(); } @@ -163,17 +166,55 @@ public class IoUtil { aStream.close(); } - catch(Exception aExp) + catch (Exception aExp) { aExp.printStackTrace(); } } + /** + * Utility helper that will locate the next available {@link File} with a name closest to that of aFile. + *

    + * The next available {@link File} is defined as a file that does not exist. + *

    + * Names will be searched by appending an increasing index (starting from zero) onto the file name until a + * {@link File} is located that does not exist (on the local file system). + */ + public static File locateNextAvailableFile(File aFile) + { + // If the File does not exist then just utilize it as is + if (aFile.exists() == false) + return aFile; + + // Extract the path's components of interest + File basePath = aFile.getParentFile(); + String baseName = aFile.getName(); + String fileExt = ""; + + String tmpFileName = aFile.getName(); + int tmpIdx = tmpFileName.lastIndexOf("."); + if (tmpIdx != -1 && tmpIdx != tmpFileName.length() - 1) + { + baseName = tmpFileName.substring(0, tmpIdx); + fileExt = tmpFileName.substring(tmpIdx); + } + + int currIdx = 0; + while (true) + { + File tmpFile = new File(basePath, baseName + "." + currIdx + fileExt); + if (tmpFile.exists() == false) + return tmpFile; + + currIdx++; + } + } + /** * Downloads the content at aUrl and saves it to aFile. Before the file is downloaded, the connection is configured * with the specified property map. The download will be aborted if aTask is no longer active. */ - public static boolean copyUrlToFile(Task aTask, URL aUrl, File aFile, Map aPropertyMap) + public static boolean copyUrlToFile(Task aTask, URL aUrl, File aFile, Map aPropertyM) { // Ensure we have a valid aTask if (aTask == null) @@ -193,10 +234,10 @@ public class IoUtil aConnection.setReadTimeout(90 * 1000); // Setup the various properties - if (aPropertyMap != null) + if (aPropertyM != null) { - for (String aKey : aPropertyMap.keySet()) - aConnection.setRequestProperty(aKey, aPropertyMap.get(aKey)); + for (String aKey : aPropertyM.keySet()) + aConnection.setRequestProperty(aKey, aPropertyM.get(aKey)); } inStream = aConnection.getInputStream(); @@ -216,16 +257,16 @@ public class IoUtil // Bail if aTask is aborted if (aTask.isActive() == false) { - aTask.infoAppendln("Download of file: " + aFile + " has been aborted!"); + aTask.logRegln("Download of file: " + aFile + " has been aborted!"); return false; } } } - catch(Exception aExp) + catch (Exception aExp) { - aTask.infoAppendln("Exception:" + aExp); - aTask.infoAppendln(" URL:" + aUrl); - aTask.infoAppendln(" File:" + aFile); + aTask.logRegln("Exception:" + aExp); + aTask.logRegln(" URL:" + aUrl); + aTask.logRegln(" File:" + aFile); aExp.printStackTrace(); return false; } @@ -261,13 +302,13 @@ public class IoUtil */ public static boolean copyUrlToFile(Task aTask, URL aUrl, File aFile, String aUsername, String aPassword) { - String authStr = aUsername + ":" + aPassword; + var authStr = aUsername + ":" + aPassword; authStr = Base64.getEncoder().encodeToString(authStr.getBytes()); - Map plainMap = Maps.newHashMap(); - plainMap.put("Authorization", "Basic " + authStr); + var plainM = new HashMap(); + plainM.put("Authorization", "Basic " + authStr); - return copyUrlToFile(aTask, aUrl, aFile, plainMap); + return copyUrlToFile(aTask, aUrl, aFile, plainM); } /** @@ -289,9 +330,8 @@ public class IoUtil { tmpUrl = aFile1.toURI().toURL(); } - catch(Exception aExp) + catch (Exception aExp) { - System.out.println("Exception:" + aExp); aExp.printStackTrace(); return false; } @@ -301,9 +341,8 @@ public class IoUtil /** * Method to recursively delete all of the contents located in the specified directory. - *

    + *

    * Source: http://stackoverflow.com/questions/3775694/deleting-folder-from-java - *

    */ public static boolean deleteDirectory(File directory) { @@ -368,7 +407,8 @@ public class IoUtil // Ensure the string size is less than 0x00FFFF if (size >= 0x00FFFF) - throw new RuntimeException("Transformed UTF-8 string is too large! Max size: " + (0x00FFFF - 1) + " Curr size:" + size); + throw new IOException( + "Transformed UTF-8 string is too large! Max size: " + (0x00FFFF - 1) + " Curr size:" + size); // Write out the string aStream.writeShort(size & 0x00FFFF); @@ -390,15 +430,16 @@ public class IoUtil data = aStr.getBytes("UTF-8"); size = data.length; } - catch(Exception aExp) + catch (Exception aExp) { throw new RuntimeException("UTF-8 Transform error.", aExp); } if (size >= 0x00FFFF) - throw new RuntimeException("Transformed UTF-8 string is too large! Max size: " + (0x00FFFF - 1) + " Curr size:" + size); + throw new RuntimeException( + "Transformed UTF-8 string is too large! Max size: " + (0x00FFFF - 1) + " Curr size:" + size); - return (short)(2 + data.length); + return (short) (2 + data.length); } } diff --git a/src/glum/io/Loader.java b/src/glum/io/Loader.java index 0fa41c3..a90afb0 100644 --- a/src/glum/io/Loader.java +++ b/src/glum/io/Loader.java @@ -1,19 +1,37 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.io; -import java.awt.*; +import java.awt.Component; import java.io.*; -import java.net.*; +import java.net.URL; import java.util.*; -import java.util.List; -import javax.swing.*; + +import javax.swing.JFileChooser; import javax.swing.filechooser.FileFilter; -import com.google.common.collect.Lists; +import com.google.common.collect.ImmutableList; import glum.io.token.MatchTokenizer; import glum.io.token.Tokenizer; import glum.task.Task; +/** + * Collection of utility method for loading a text file using {@link TokenProcessor}s and and a {@link Tokenizer}. + * + * @author lopeznr1 + */ public class Loader { // Constants @@ -30,11 +48,8 @@ public class Loader public static void loadAsciiFile(File aFile, TokenProcessor aTokenProcessor, Tokenizer aTokenizer) { - Collection tpSet; - - tpSet = new LinkedList(); - tpSet.add(aTokenProcessor); - loadAsciiFile(aFile, tpSet, aTokenizer); + var tmpTokenProcessorL = ImmutableList.of(aTokenProcessor); + loadAsciiFile(aFile, tmpTokenProcessorL, aTokenizer); } public static void loadAsciiFile(File aFile, Collection tpSet) @@ -49,16 +64,14 @@ public class Loader public static void loadAsciiFile(File aFile, Collection tpSet, Tokenizer aTokenizer, Task aTask) { - URL aUrl; - try { - aUrl = aFile.toURI().toURL(); - loadAsciiFile(aUrl, tpSet, aTokenizer, aTask); + var tmpUrl = aFile.toURI().toURL(); + loadAsciiFile(tmpUrl, tpSet, aTokenizer, aTask); } - catch (Exception e) + catch (Exception aExp) { - e.printStackTrace(); + aExp.printStackTrace(); System.out.println("Resource not processed: " + aFile); return; } @@ -66,11 +79,8 @@ public class Loader public static void loadAsciiFile(URL aUrl, TokenProcessor aTokenProcessor) { - Collection tpSet; - - tpSet = new LinkedList(); - tpSet.add(aTokenProcessor); - loadAsciiFile(aUrl, tpSet, new MatchTokenizer(DEFAULT_REG_EX)); + var tmpTokenProcessorL = ImmutableList.of(aTokenProcessor); + loadAsciiFile(aUrl, tmpTokenProcessorL, new MatchTokenizer(DEFAULT_REG_EX)); } public static void loadAsciiFile(URL aUrl, Collection tpSet) @@ -80,38 +90,30 @@ public class Loader public static void loadAsciiFile(URL aUrl, Collection tpSet, Tokenizer aTokenizer) { - loadAsciiFile(aUrl, tpSet, aTokenizer, null); + loadAsciiFile(aUrl, tpSet, aTokenizer, null); } - + public static void loadAsciiFile(URL aUrl, Collection tpSet, Tokenizer aTokenizer, Task aTask) { - InputStream inStream; - // Insanity check if (aUrl == null) return; // Process our input - inStream = null; - try + try (var aStream = aUrl.openStream();) { - inStream = aUrl.openStream(); - loadAsciiFile(inStream, tpSet, aTokenizer, aTask); + loadAsciiFile(aStream, tpSet, aTokenizer, aTask); } - catch (FileNotFoundException e) + catch (FileNotFoundException aExp) { System.out.println("Resource not found: " + aUrl); return; } - catch (IOException e) + catch (IOException aExp) { System.out.println("Ioexception occured while loading: " + aUrl); return; } - finally - { - IoUtil.forceClose(inStream); - } } public static void loadAsciiFile(InputStream inStream, TokenProcessor aTokenProcessor) throws IOException @@ -119,18 +121,17 @@ public class Loader loadAsciiFile(inStream, aTokenProcessor, new MatchTokenizer(DEFAULT_REG_EX)); } - public static void loadAsciiFile(InputStream inStream, TokenProcessor aTokenProcessor, Tokenizer aTokenizer) throws IOException + public static void loadAsciiFile(InputStream inStream, TokenProcessor aTokenProcessor, Tokenizer aTokenizer) + throws IOException { loadAsciiFile(inStream, aTokenProcessor, aTokenizer, null); } - public static void loadAsciiFile(InputStream inStream, TokenProcessor aTokenProcessor, Tokenizer aTokenizer, Task aTask) throws IOException + public static void loadAsciiFile(InputStream inStream, TokenProcessor aTokenProcessor, Tokenizer aTokenizer, + Task aTask) throws IOException { - Collection tpSet; - - tpSet = new LinkedList(); - tpSet.add(aTokenProcessor); - loadAsciiFile(inStream, tpSet, aTokenizer, aTask); + var tmpTokenProcessorL = ImmutableList.of(aTokenProcessor); + loadAsciiFile(inStream, tmpTokenProcessorL, aTokenizer, aTask); } public static void loadAsciiFile(InputStream inStream, Collection tpSet) throws IOException @@ -138,23 +139,17 @@ public class Loader loadAsciiFile(inStream, tpSet, new MatchTokenizer(DEFAULT_REG_EX), null); } - public static void loadAsciiFile(InputStream inStream, Collection tpSet, Tokenizer aTokenizer, Task aTask) throws IOException + public static void loadAsciiFile(InputStream inStream, Collection tpSet, Tokenizer aTokenizer, + Task aTask) throws IOException { - BufferedReader br; - String strLine; - int lineNum; - ArrayList aList; - String tokens[], dummyVar[]; - boolean isProcessed; - // Insanity check if (tpSet == null) return; // Process our input - br = new BufferedReader( new InputStreamReader(inStream) ); - lineNum = 0; - dummyVar = new String[1]; + var tmpBR = new BufferedReader(new InputStreamReader(inStream)); + var lineNum = 0; + var dummyVar = new String[1]; // Read the lines while (true) @@ -162,8 +157,8 @@ public class Loader // Bail if the associated task is no longer active if (aTask != null && aTask.isActive() == false) return; - - strLine = br.readLine(); + + var strLine = tmpBR.readLine(); if (strLine == null) { // Notify the TokenProcessors of job done @@ -171,25 +166,24 @@ public class Loader aTP.flush(); // Release the various streams - br.close(); + tmpBR.close(); inStream.close(); break; } lineNum++; // Get the tokens out of our string - tokens = null; - aList = aTokenizer.getTokens(strLine); - if (aList.size() > 0) + var tokenL = aTokenizer.getTokens(strLine); + if (tokenL.size() > 0) { // Transform from a list to an array - tokens = aList.toArray(dummyVar); + var tokenArr = tokenL.toArray(dummyVar); // Process the tokens - isProcessed = false; + var isProcessed = false; for (TokenProcessor aTP : tpSet) { - isProcessed = aTP.process(tokens, lineNum); + isProcessed = aTP.process(tokenArr, lineNum); if (isProcessed == true) break; } @@ -205,188 +199,184 @@ public class Loader /** * Prompts the user to select a single File - * + * * @param aLoaderInfo - * : A object that stores the configuration from method call to method call. - * @param parentComp - * The parent component for the associated FileChooser GUI + * : A object that stores the configuration from method call to method call. + * @param aParentComp + * The parent component for the associated FileChooser GUI * @param aTitleStr - * The title of the FileChooser GUI - * @param ffList - * A List of FileFilters - * @param isSaveDialog - * Whether this FileChooser displays a GUI appropriate for saving a file + * The title of the FileChooser GUI + * @param aFileFilterC + * A List of FileFilters + * @param aIsSaveDialog + * Whether this FileChooser displays a GUI appropriate for saving a file * @return The selected file or null */ - public static File queryUserForFile(LoaderInfo aLoaderInfo, Component parentComp, String aTitleStr, Collection ffList, boolean isSaveDialog) + public static File queryUserForFile(LoaderInfo aLoaderInfo, Component aParentComp, String aTitleStr, + Collection aFileFilterC, boolean aIsSaveDialog) { - JFileChooser aFC; - int aVal; - // Ensure we have a non null LoaderInfo if (aLoaderInfo == null) aLoaderInfo = new LoaderInfo(); // Set up the FileChooser - aFC = new StandardFileChooser(null); - aFC.setAcceptAllFileFilterUsed(false); - aFC.setDialogTitle(aTitleStr); - aFC.setMultiSelectionEnabled(false); - aLoaderInfo.loadConfig(aFC); + var tmpFC = new StandardFileChooser(null); + tmpFC.setAcceptAllFileFilterUsed(false); + tmpFC.setDialogTitle(aTitleStr); + tmpFC.setMultiSelectionEnabled(false); + aLoaderInfo.loadConfig(tmpFC); // Set in the FileFilters - for (FileFilter aFileFilter : ffList) - aFC.addChoosableFileFilter(aFileFilter); + for (FileFilter aFileFilter : aFileFilterC) + tmpFC.addChoosableFileFilter(aFileFilter); // Let the user choose a file - if (isSaveDialog == true) - aVal = aFC.showSaveDialog(parentComp); + int tmpVal; + if (aIsSaveDialog == true) + tmpVal = tmpFC.showSaveDialog(aParentComp); else - aVal = aFC.showOpenDialog(parentComp); + tmpVal = tmpFC.showOpenDialog(aParentComp); // Store off the current settings - aLoaderInfo.saveConfig(aFC); + aLoaderInfo.saveConfig(tmpFC); // Bail if no file chosen - if (aVal != JFileChooser.APPROVE_OPTION) + if (tmpVal != JFileChooser.APPROVE_OPTION) return null; // Return the file - return aFC.getSelectedFile(); + return tmpFC.getSelectedFile(); } - public static File queryUserForFile(LoaderInfo aLoaderInfo, Component parentComp, String aTitleStr, FileFilter aFileFilter, boolean isSaveDialog) + public static File queryUserForFile(LoaderInfo aLoaderInfo, Component aParentComp, String aTitleStr, + FileFilter aFileFilter, boolean aIsSaveDialog) { - List ffList; - - ffList = Lists.newArrayList(); + var tmpFileFilterL = new ArrayList(); if (aFileFilter != null) - ffList.add(aFileFilter); - - return queryUserForFile(aLoaderInfo, parentComp, aTitleStr, ffList, isSaveDialog); + tmpFileFilterL.add(aFileFilter); + + return queryUserForFile(aLoaderInfo, aParentComp, aTitleStr, tmpFileFilterL, aIsSaveDialog); } - - public static File queryUserForFile(LoaderInfo aLoaderInfo, Component parentComp, String aTitleStr, boolean isSaveDialog) + + public static File queryUserForFile(LoaderInfo aLoaderInfo, Component aParentComp, String aTitleStr, + boolean aIsSaveDialog) { - return queryUserForFile(aLoaderInfo, parentComp, aTitleStr, (FileFilter)null, isSaveDialog); + return queryUserForFile(aLoaderInfo, aParentComp, aTitleStr, (FileFilter) null, aIsSaveDialog); } - + /** * Prompts the user to select multiple Files - * + * * @param aLoaderInfo - * : A object that stores the configuration from method call to method call. - * @param parentComp - * The parent component for the associated FileChooser GUI + * : A object that stores the configuration from method call to method call. + * @param aParentComp + * The parent component for the associated FileChooser GUI * @param aTitleStr - * The title of the FileChooser GUI - * @param ffList - * A List of FileFilters - * @param isSaveDialog - * Whether this FileChooser displays a GUI appropriate for saving a file + * The title of the FileChooser GUI + * @param aFileFilterC + * A List of FileFilters + * @param aIsSaveDialog + * Whether this FileChooser displays a GUI appropriate for saving a file * @return The selected file or null */ - public static List queryUserForFiles(LoaderInfo aLoaderInfo, Component parentComp, String aTitleStr, Collection ffList, boolean isSaveDialog) + public static List queryUserForFiles(LoaderInfo aLoaderInfo, Component aParentComp, String aTitleStr, + Collection aFileFilterC, boolean aIsSaveDialog) { - JFileChooser aFC; - List retList; - int aVal; - // Ensure we have a non null LoaderInfo if (aLoaderInfo == null) aLoaderInfo = new LoaderInfo(); // Set up the FileChooser - aFC = new StandardFileChooser(null); - aFC.setAcceptAllFileFilterUsed(false); - aFC.setDialogTitle(aTitleStr); - aFC.setMultiSelectionEnabled(true); - aLoaderInfo.loadConfig(aFC); + var tmpFC = new StandardFileChooser(null); + tmpFC.setAcceptAllFileFilterUsed(false); + tmpFC.setDialogTitle(aTitleStr); + tmpFC.setMultiSelectionEnabled(true); + aLoaderInfo.loadConfig(tmpFC); // Set in the FileFilters - for (FileFilter aFileFilter : ffList) - aFC.addChoosableFileFilter(aFileFilter); + for (FileFilter aFileFilter : aFileFilterC) + tmpFC.addChoosableFileFilter(aFileFilter); // Let the user choose a file - if (isSaveDialog == true) - aVal = aFC.showSaveDialog(parentComp); + int tmpVal; + if (aIsSaveDialog == true) + tmpVal = tmpFC.showSaveDialog(aParentComp); else - aVal = aFC.showOpenDialog(parentComp); + tmpVal = tmpFC.showOpenDialog(aParentComp); // Store off the current settings - aLoaderInfo.saveConfig(aFC); + aLoaderInfo.saveConfig(tmpFC); // Bail if no file chosen - if (aVal != JFileChooser.APPROVE_OPTION) + if (tmpVal != JFileChooser.APPROVE_OPTION) return null; // Return a list that is modifiable - retList = Arrays.asList(aFC.getSelectedFiles()); - retList = Lists.newArrayList(retList); - return retList; + var retFileL = Arrays.asList(tmpFC.getSelectedFiles()); + retFileL = new ArrayList<>(retFileL); + return retFileL; } - - public static List queryUserForFiles(LoaderInfo aLoaderInfo, Component parentComp, String aTitleStr, FileFilter aFileFilter, boolean isSaveDialog) + + public static List queryUserForFiles(LoaderInfo aLoaderInfo, Component aParentComp, String aTitleStr, + FileFilter aFileFilter, boolean isSaveDialog) { - List ffList; - - ffList = Lists.newArrayList(); + var tmpFileFilterL = new ArrayList(); if (aFileFilter != null) - ffList.add(aFileFilter); - - return queryUserForFiles(aLoaderInfo, parentComp, aTitleStr, ffList, isSaveDialog); + tmpFileFilterL.add(aFileFilter); + + return queryUserForFiles(aLoaderInfo, aParentComp, aTitleStr, tmpFileFilterL, isSaveDialog); } - - public static List queryUserForFiles(LoaderInfo aLoaderInfo, Component parentComp, String aTitleStr, boolean isSaveDialog) + + public static List queryUserForFiles(LoaderInfo aLoaderInfo, Component parentComp, String aTitleStr, + boolean aIsSaveDialog) { - return queryUserForFiles(aLoaderInfo, parentComp, aTitleStr, (FileFilter)null, isSaveDialog); + return queryUserForFiles(aLoaderInfo, parentComp, aTitleStr, (FileFilter) null, aIsSaveDialog); } - + /** * Prompts the user to select a single folder - * + * * @param aLoaderInfo - * : A object that stores the configuration from method call to method call. - * @param parentComp - * The parent component for the associated FileChooser GUI + * : A object that stores the configuration from method call to method call. + * @param aParentComp + * The parent component for the associated FileChooser GUI * @param aTitleStr - * The title of the FileChooser GUI - * @param isSaveDialog - * Whether this FileChooser displays a GUI appropriate for saving a file + * The title of the FileChooser GUI + * @param aIsSaveDialog + * Whether this FileChooser displays a GUI appropriate for saving a file * @return The selected file or null */ - public static File queryUserForPath(LoaderInfo aLoaderInfo, Component parentComp, String aTitleStr, boolean isSaveDialog) + public static File queryUserForPath(LoaderInfo aLoaderInfo, Component aParentComp, String aTitleStr, + boolean aIsSaveDialog) { - JFileChooser aFC; - int aVal; - // Ensure we have a non null LoaderInfo if (aLoaderInfo == null) aLoaderInfo = new LoaderInfo(); // Set up the FileChooser - aFC = new StandardFileChooser(null); - aFC.setDialogTitle(aTitleStr); - aFC.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); - aFC.setMultiSelectionEnabled(false); - - aLoaderInfo.loadConfig(aFC); + var tmpFC = new StandardFileChooser(null); + tmpFC.setDialogTitle(aTitleStr); + tmpFC.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + tmpFC.setMultiSelectionEnabled(false); + + aLoaderInfo.loadConfig(tmpFC); // Let the user choose a file - if (isSaveDialog == true) - aVal = aFC.showSaveDialog(parentComp); + int tmpVal; + if (aIsSaveDialog == true) + tmpVal = tmpFC.showSaveDialog(aParentComp); else - aVal = aFC.showOpenDialog(parentComp); + tmpVal = tmpFC.showOpenDialog(aParentComp); // Store off the current settings - aLoaderInfo.saveConfig(aFC); + aLoaderInfo.saveConfig(tmpFC); // Bail if no file chosen - if (aVal != JFileChooser.APPROVE_OPTION) + if (tmpVal != JFileChooser.APPROVE_OPTION) return null; // Return the file - return aFC.getSelectedFile(); + return tmpFC.getSelectedFile(); } - + } diff --git a/src/glum/io/LoaderInfo.java b/src/glum/io/LoaderInfo.java index 87969f7..4828551 100644 --- a/src/glum/io/LoaderInfo.java +++ b/src/glum/io/LoaderInfo.java @@ -1,8 +1,18 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.io; -import glum.zio.*; -import glum.zio.util.ZioUtil; - import java.awt.Dimension; import java.awt.Point; import java.io.File; @@ -12,36 +22,56 @@ import java.lang.reflect.Method; import javax.swing.JFileChooser; import javax.swing.plaf.FileChooserUI; +import glum.zio.*; +import glum.zio.util.ZioUtil; + /** - * Mutable object that contains the details needed to display a load or save GUI component. This object will typically be changed whenever the component is - * modified. + * Object that stores the configuration associated with a {@link JFileChooser}. + *

    + * The following state is stored: + *

      + *
    • Position of dialog + *
    • Size of dialog + *
    • File path + *
    + * + * @author lopeznr1 */ public class LoaderInfo implements ZioObj { + // State vars + private File path; + // Gui vars private boolean isVisible; private Point position; private Dimension dimension; - // Path vars - private String filePath; - - public LoaderInfo(File aFilePath) + /** Standard Constructor */ + public LoaderInfo(File aPath) { + path = aPath; + isVisible = false; position = null; dimension = null; - - filePath = null; - if (aFilePath != null) - filePath = aFilePath.getAbsolutePath(); } + /** Simplified Constructor */ public LoaderInfo() { this(null); } + // Accessor methods + // @formatter:off + /** Gets the file path. */ + public File getPath() { return path; } + + /** Sets the file path. */ + public void setPath(File aPath) { path = aPath; } + // @formatter:on + /** * Loads the current configuration into aFileChooser */ @@ -57,26 +87,23 @@ public class LoaderInfo implements ZioObj } // Bail if there is no filePath - if (filePath == null) + if (path == null) return; if (aFileChooser.getFileSelectionMode() == JFileChooser.DIRECTORIES_ONLY) { - File tmpFile = new File(filePath); - // Locate the first folder that exists -// File workPath = tmpFile.getParentFile(); - File workPath = tmpFile; - while (workPath != null && workPath.isDirectory() == false) - workPath = workPath.getParentFile(); + File tmpPath = path; + while (tmpPath != null && tmpPath.isDirectory() == false) + tmpPath = tmpPath.getParentFile(); // Set the FileChooser to the first folder that exists - aFileChooser.setCurrentDirectory(workPath); + aFileChooser.setCurrentDirectory(tmpPath); // Set the FileChooser's name area to reflect the absolute path - if (workPath != null) + if (tmpPath != null) { - String absPathStr = tmpFile.getAbsolutePath(); + String absPathStr = path.getAbsolutePath(); try { FileChooserUI tmpFCUI = aFileChooser.getUI(); @@ -84,7 +111,7 @@ public class LoaderInfo implements ZioObj Method setFileName = fcClass.getMethod("setFileName", String.class); setFileName.invoke(tmpFCUI, absPathStr); } - catch(Exception aExp) + catch (Exception aExp) { aExp.printStackTrace(); } @@ -92,15 +119,15 @@ public class LoaderInfo implements ZioObj } else { - File tmpPath = new File(filePath); - if (tmpPath.isFile() == true) + if (path.isFile() == true) { - aFileChooser.setSelectedFile(tmpPath); + aFileChooser.setSelectedFile(path); } else { // Set the FileChooser's current directory to the first folder that exists - while (tmpPath != null && tmpPath.getParentFile().isDirectory() == false) + File tmpPath = path; + while (tmpPath != null && tmpPath.getParentFile() != null && tmpPath.getParentFile().isDirectory() == false) tmpPath = tmpPath.getParentFile(); aFileChooser.setCurrentDirectory(tmpPath); @@ -117,15 +144,7 @@ public class LoaderInfo implements ZioObj dimension = aFileChooser.getSize(); isVisible = aFileChooser.isVisible(); - filePath = aFileChooser.getCurrentDirectory().getAbsolutePath(); - } - - /** - * Sets the filePath of the LoaderInfo. - */ - public void setFilePath(String aFilePath) - { - filePath = aFilePath; + path = aFileChooser.getCurrentDirectory(); } @Override @@ -134,11 +153,13 @@ public class LoaderInfo implements ZioObj aStream.readVersion(0); isVisible = aStream.readBool(); - position = ZioUtil.readPoint(aStream); dimension = ZioUtil.readDimension(aStream); - filePath = aStream.readString(); + String pathStr = aStream.readString(); + path = null; + if (pathStr != null) + path = new File(pathStr); } @Override @@ -147,11 +168,13 @@ public class LoaderInfo implements ZioObj aStream.writeVersion(0); aStream.writeBool(isVisible); - ZioUtil.writePoint(aStream, position); ZioUtil.writeDimension(aStream, dimension); - aStream.writeString(filePath); + String pathStr = null; + if (path != null) + pathStr = path.getAbsolutePath(); + aStream.writeString(pathStr); } } diff --git a/src/glum/io/NullOutputStream.java b/src/glum/io/NullOutputStream.java index 4047c23..9e85f81 100644 --- a/src/glum/io/NullOutputStream.java +++ b/src/glum/io/NullOutputStream.java @@ -1,15 +1,31 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.io; import java.io.DataOutputStream; import java.io.IOException; +/** + * Implementation of {@link DataOutputStream} useful for counting bytes written. + * + * @author lopeznr1 + */ public class NullOutputStream extends DataOutputStream { - protected int byteCount; + private int byteCount; - /** - * OutputStream used to count bytes that are to be written. - */ + /** Standard Constructor */ public NullOutputStream() { super(null); diff --git a/src/glum/io/ParseUtil.java b/src/glum/io/ParseUtil.java new file mode 100644 index 0000000..cd6b710 --- /dev/null +++ b/src/glum/io/ParseUtil.java @@ -0,0 +1,148 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.io; + +/** + * Collection of utility methods for parsing values from text input. + * + * @author lopeznr1 + */ +public class ParseUtil +{ + /** + * Reads a boolean from a string with out throwing a exception. + */ + public static boolean readBoolean(String aStr, boolean aVal) + { + if (aStr == null) + return aVal; + + // Special case for 1 char strings + if (aStr.length() == 1) + { + char aChar; + + aChar = aStr.charAt(0); + if (aChar == 'T' || aChar == 't' || aChar == '1') + return true; + + return false; + } + + try + { + return Boolean.valueOf(aStr).booleanValue(); + } + catch (Exception aExp) + { + return aVal; + } + } + + /** + * Reads a double from a string with out throwing a exception. Note aStr can have an number of separators: comma + * chars + */ + public static double readDouble(String aStr, double aVal) + { + try + { + aStr = aStr.replace(",", ""); + return Double.parseDouble(aStr); + } + catch (Exception aExp) + { + return aVal; + } + } + + /** + * Reads a float from a string with out throwing a exception. Note aStr can have an number of separators: comma chars + */ + public static float readFloat(String aStr, float aVal) + { + try + { + aStr = aStr.replace(",", ""); + return Float.parseFloat(aStr); + } + catch (Exception aExp) + { + return aVal; + } + } + + /** + * Reads an int from a string without throwing a exception. Note aStr can have an number of separators: comma chars + */ + public static int readInt(String aStr, int aVal) + { + try + { + aStr = aStr.replace(",", ""); + return Integer.parseInt(aStr); + } + catch (Exception aExp) + { + return aVal; + } + } + + /** + * Reads a long from a string without throwing a exception. Note aStr can have an number of separators: comma chars + */ + public static long readLong(String aStr, long aVal) + { + try + { + aStr = aStr.replace(",", ""); + return Long.parseLong(aStr); + } + catch (Exception aExp) + { + return aVal; + } + } + + /** + * Reads an int (forced to fit within a range) from a string with out throwing a exception. + */ + public static int readRangeInt(String aStr, int aMinVal, int aMaxVal, int aVal) + { + try + { + int tmpInt = Integer.parseInt(aStr); + if (tmpInt < aMinVal) + tmpInt = aMinVal; + else if (tmpInt > aMaxVal) + tmpInt = aMaxVal; + + return tmpInt; + } + catch (Exception aExp) + { + return aVal; + } + } + + /** + * Utility method to strip the white space from an array of tokens. + */ + public static void cleanTokens(String[] aTokenArr) + { + for (int c1 = 0; c1 < aTokenArr.length; c1++) + aTokenArr[c1] = aTokenArr[c1].strip(); + } + +} diff --git a/src/glum/io/RegExFileFilter.java b/src/glum/io/RegExFileFilter.java index 6aaeaf5..d4b30dc 100644 --- a/src/glum/io/RegExFileFilter.java +++ b/src/glum/io/RegExFileFilter.java @@ -1,9 +1,28 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.io; import java.io.File; import java.util.regex.Pattern; + import javax.swing.filechooser.FileFilter; +/** + * Implementation of {@link FileFilter} used to gather files that match a regular expression. + * + * @author lopeznr1 + */ public class RegExFileFilter extends FileFilter implements java.io.FileFilter { // State vars @@ -11,6 +30,16 @@ public class RegExFileFilter extends FileFilter implements java.io.FileFilter private Pattern matchPattern; private boolean allowDirs; + /** + * Standard Constructor + * + * @param aDescription + * Textual description of this filter. + * @param aMatchRegEx + * Regular expression used for matching. + * @param aAllowDirs + * Set to true if directories should (always) be kept or rejected. + */ public RegExFileFilter(String aDescription, String aMatchRegEx, boolean aAllowDirs) { description = aDescription; @@ -18,6 +47,7 @@ public class RegExFileFilter extends FileFilter implements java.io.FileFilter allowDirs = aAllowDirs; } + /** Simplified Constructor */ public RegExFileFilter(String aDescription, String aMatchRegEx) { this(aDescription, aMatchRegEx, true); @@ -34,15 +64,13 @@ public class RegExFileFilter extends FileFilter implements java.io.FileFilter @Override public boolean accept(File aFile) { - String fileName; - // Allow directories if appropriate if (aFile.isDirectory() == true) return allowDirs; // Retrieve the corresponding file name - fileName = aFile.getName(); - + var fileName = aFile.getName(); + // Test to see if the fileName matches the compiled regex if (matchPattern.matcher(fileName).matches() == true) return true; diff --git a/src/glum/io/SimpleFileFilter.java b/src/glum/io/SimpleFileFilter.java index b9f522a..bc5f044 100644 --- a/src/glum/io/SimpleFileFilter.java +++ b/src/glum/io/SimpleFileFilter.java @@ -1,23 +1,57 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.io; import java.io.File; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; + import javax.swing.filechooser.FileFilter; +/** + * Implementation of {@link FileFilter} used to gather files that have a matching file extension. + * + * @author lopeznr1 + */ public class SimpleFileFilter extends FileFilter implements java.io.FileFilter { // State vars - private Collection extensionList; + private ArrayList extensionL; private String description; private boolean allowDirs; + /** + * Standard Constructor + * + * @param aDescription + * Textual description of this filter. + */ public SimpleFileFilter(String aDescription) { - allowDirs = true; + extensionL = new ArrayList(); description = aDescription; - extensionList = new LinkedList(); + allowDirs = true; } + /** + * Convenience Constructor + * + * @param aDescription + * Textual description of this filter. + * @param aExtension + * A file extension to match against. + */ public SimpleFileFilter(String aDescription, String aExtension) { this(aDescription); @@ -30,15 +64,24 @@ public class SimpleFileFilter extends FileFilter implements java.io.FileFilter public void addExtension(String aExtension) { if (aExtension != null) - extensionList.add(aExtension); + extensionL.add(aExtension); } - + /** * Adds the collections of file extensions which should be allowed through this filter */ - public void addExtensions(String... extArr) + public void addExtensions(String... aExtensionArr) { - for (String aExtension : extArr) + for (String aExtension : aExtensionArr) + addExtension(aExtension); + } + + /** + * Adds the collections of file extensions which should be allowed through this filter + */ + public void addExtensions(Collection aExtensionC) + { + for (String aExtension : aExtensionC) addExtension(aExtension); } @@ -53,28 +96,25 @@ public class SimpleFileFilter extends FileFilter implements java.io.FileFilter @Override public boolean accept(File aFile) { - String aStr, aFileName; - int aIndex; - // Allow directories if appropriate if (aFile.isDirectory() == true) return allowDirs; // Retrieve the corresponding file name - aFileName = aFile.getName(); + var tmpFileName = aFile.getName(); // Ensure the file has an extension - aIndex = aFileName.lastIndexOf('.'); - if (aIndex == -1) + var tmpIdx = tmpFileName.lastIndexOf('.'); + if (tmpIdx == -1) return false; // See if aFileName's extension matches any in extensionList - for (String aExt : extensionList) + for (String aExt : extensionL) { - if (aFileName.length() > aExt.length()) + if (tmpFileName.length() > aExt.length()) { - aStr = aFileName.substring(aFileName.length() - aExt.length()); - if (aExt.equalsIgnoreCase(aStr) == true) + var tmpStr = tmpFileName.substring(tmpFileName.length() - aExt.length()); + if (aExt.equalsIgnoreCase(tmpStr) == true) return true; } } diff --git a/src/glum/io/StandardFileChooser.java b/src/glum/io/StandardFileChooser.java index 2340c54..31fcb2d 100644 --- a/src/glum/io/StandardFileChooser.java +++ b/src/glum/io/StandardFileChooser.java @@ -1,8 +1,28 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.io; import java.awt.*; -import javax.swing.*; +import javax.swing.JDialog; +import javax.swing.JFileChooser; + +/** + * Enhanced {@link JFileChooser} that allows external code to position the file dialog. + * + * @author lopeznr1 + */ public class StandardFileChooser extends JFileChooser { // Gui vars diff --git a/src/glum/io/TokenProcessor.java b/src/glum/io/TokenProcessor.java index 3d1dc75..0d86005 100644 --- a/src/glum/io/TokenProcessor.java +++ b/src/glum/io/TokenProcessor.java @@ -1,18 +1,35 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.io; +/** + * Interface that provides a mechanism to allow the processing of an array of (string) tokens. + * + * @author lopeznr1 + */ public interface TokenProcessor { /** - * Lets the processor know that it will not be called anymore. This method is - * only called after all data has been read in from the corresponding stream. - * Note this method will never get called if the reading was aborted or an IO - * exception occured. + * Lets the processor know that it will not be called anymore. This method is only called after all data has been + * read in from the corresponding stream. Note this method will never get called if the reading was aborted or an IO + * exception occurred. */ public void flush(); /** * Returns true if able to handle the tokens */ - public boolean process(String[] tokens, int lineNum); + public boolean process(String[] aTokenArr, int aLineNum); } diff --git a/src/glum/io/WarningTP.java b/src/glum/io/WarningTP.java index a46255d..8258559 100644 --- a/src/glum/io/WarningTP.java +++ b/src/glum/io/WarningTP.java @@ -1,30 +1,51 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.io; -import java.util.*; +import java.util.LinkedHashMap; +import java.util.Map; +/** + * Implementation of {@link TokenProcessor} used to log (to stdout) warnings for tokens where the first token matches a + * specific value. + * + * @author lopeznr1 + */ public class WarningTP implements TokenProcessor { - private Map warningSet; + // State vars + private Map warningM; - /** - * Constructor - */ + /** Standard Constructor */ public WarningTP() { - warningSet = new LinkedHashMap(); + warningM = new LinkedHashMap(); } /** - * add - Adds a new warning to the set of warnings; Note if aMsg is null - * then aInstr will just be ignored - */ + * Adds a new warning to the set of warnings. If during the processing of tokens the first token equals aInstr then + * the warning message will be logged to stdout. + *

    + * Note if aMsg is null then aInstr will just be ignored + */ public void add(String aInstr, String aMsg) { // Insanity check if (aInstr == null) return; - warningSet.put(aInstr, aMsg); + warningM.put(aInstr, aMsg); } @Override @@ -34,20 +55,18 @@ public class WarningTP implements TokenProcessor } @Override - public boolean process(String[] tokens, int lineNum) + public boolean process(String[] aTokenArr, int aLineNum) { // Insanity check - if (tokens == null) + if (aTokenArr == null) return false; - if (warningSet.containsKey(tokens[0]) == true) + if (warningM.containsKey(aTokenArr[0]) == true) { - String aMsg; - // Display the aMsg if associated with aInstr - aMsg = warningSet.get(tokens[0]); - if (aMsg instanceof String) - System.out.println("[" + lineNum + "] " + aMsg); + var tmpMsg = warningM.get(aTokenArr[0]); + if (tmpMsg instanceof String) + System.out.println("[" + aLineNum + "] " + tmpMsg); return true; } diff --git a/src/glum/io/token/BaseTokenizer.java b/src/glum/io/token/BaseTokenizer.java index d7e2fc2..e6b9996 100644 --- a/src/glum/io/token/BaseTokenizer.java +++ b/src/glum/io/token/BaseTokenizer.java @@ -1,5 +1,23 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.io.token; +/** + * Abstract base {@link Tokenizer}. + * + * @author lopeznr1 + */ public abstract class BaseTokenizer implements Tokenizer { // Constants @@ -10,6 +28,7 @@ public abstract class BaseTokenizer implements Tokenizer // State vars protected int commentMode; + /** Standard Constructor */ public BaseTokenizer() { commentMode = MODE_ANY_POS; @@ -26,31 +45,31 @@ public abstract class BaseTokenizer implements Tokenizer /** * Returns the string with the comments stripped based on the allowable comment style */ - protected String getCleanString(String inputStr) + protected String getCleanString(String aInputStr) { if (commentMode == MODE_NONE) { - return inputStr; + return aInputStr; } else if (commentMode == MODE_FIRST_POS) { - if (inputStr.indexOf(0) == '#') - return ""; + if (aInputStr.indexOf(0) == '#') + return ""; } else if (commentMode == MODE_ANY_POS) { int index; - index = inputStr.indexOf('#'); + index = aInputStr.indexOf('#'); if (index != -1) - inputStr = inputStr.substring(0, index); + aInputStr = aInputStr.substring(0, index); } else { throw new RuntimeException("Error: commentMode:" + commentMode); } - - return inputStr; + + return aInputStr; } } diff --git a/src/glum/io/token/MatchTokenizer.java b/src/glum/io/token/MatchTokenizer.java index 8dfa1b2..98c642e 100644 --- a/src/glum/io/token/MatchTokenizer.java +++ b/src/glum/io/token/MatchTokenizer.java @@ -1,13 +1,39 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.io.token; -import java.util.*; -import java.util.regex.*; +import java.util.ArrayList; +import java.util.regex.Pattern; +/** + * Implementation of {@link BaseTokenizer} that transforms an input string into tokens by matching against a regular + * expression. + * + * @author lopeznr1 + */ public class MatchTokenizer extends BaseTokenizer { + // State vars private Pattern myPattern; private boolean autoStripQuotes; + /** + * Standard Constructor + * + * @param aRegEx + * The regular expression used to tokenize input strings. + */ public MatchTokenizer(String aRegEx) { // Compile the pattern @@ -25,32 +51,28 @@ public class MatchTokenizer extends BaseTokenizer } @Override - public ArrayList getTokens(String inputStr) + public ArrayList getTokens(String aInputStr) { - Matcher aMatcher; - ArrayList aList; - String aMatch; - // Clean up the input string before processing - inputStr = getCleanString(inputStr); + aInputStr = getCleanString(aInputStr); - aMatcher = myPattern.matcher(inputStr); - if (aMatcher == null) + var tmpMatcher = myPattern.matcher(aInputStr); + if (tmpMatcher == null) return null; - aList = new ArrayList(); - while (aMatcher.find() == true) + var retTokenL = new ArrayList(); + while (tmpMatcher.find() == true) { - aMatch = aMatcher.group(); + var tmpMatch = tmpMatcher.group(); // Strip the (double) quotes if requested if (autoStripQuotes == true) - aMatch = TokenUtil.getRawStr(aMatch); + tmpMatch = TokenUtil.getRawStr(tmpMatch); - aList.add(aMatch); + retTokenL.add(tmpMatch); } - return aList; + return retTokenL; } } diff --git a/src/glum/io/token/SplitTokenizer.java b/src/glum/io/token/SplitTokenizer.java index 9470cca..3610b2b 100644 --- a/src/glum/io/token/SplitTokenizer.java +++ b/src/glum/io/token/SplitTokenizer.java @@ -1,14 +1,34 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.io.token; -import java.util.*; -import java.util.regex.*; - -import com.google.common.collect.Lists; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.regex.Pattern; +/** + * Implementation of {@link BaseTokenizer} that transforms an input string into tokens by splitting against a regular + * expression. + * + * @author lopeznr1 + */ public class SplitTokenizer extends BaseTokenizer { - private Pattern myPattern; + // Attributes + private final Pattern myPattern; + /** Standard Construcotr */ public SplitTokenizer(String aRegEx) { // Compile the pattern @@ -16,18 +36,15 @@ public class SplitTokenizer extends BaseTokenizer } @Override - public ArrayList getTokens(String inputStr) + public ArrayList getTokens(String aInputStr) { - String[] tokenArr; - ArrayList retList; - // Clean up the input string before processing - inputStr = getCleanString(inputStr); + aInputStr = getCleanString(aInputStr); - tokenArr = myPattern.split(inputStr, -1); + var tokenArr = myPattern.split(aInputStr, -1); - retList = Lists.newArrayList(tokenArr); - return retList; + var retTokenL = new ArrayList<>(Arrays.asList(tokenArr)); + return retTokenL; } } diff --git a/src/glum/io/token/TokenUtil.java b/src/glum/io/token/TokenUtil.java index b521854..1074023 100644 --- a/src/glum/io/token/TokenUtil.java +++ b/src/glum/io/token/TokenUtil.java @@ -1,5 +1,10 @@ package glum.io.token; +/** + * Collection of utility method to aid with tokenization of input strings. + * + * @author lopeznr1 + */ public class TokenUtil { /** @@ -24,29 +29,26 @@ public class TokenUtil } /** - * Utility method to convert a wild card exression to a regular - * expression. Currently only the special chars '?', '*' are supported. - * Source: http://www.rgagnon.com/javadetails/java-0515.html + * Utility method to convert a wild card exression to a regular expression. Currently only the special chars '?', '*' + * are supported. Source: http://www.rgagnon.com/javadetails/java-0515.html */ - public static String convertWildCardToRegEx(String wildcard) + public static String convertWildCardToRegEx(String aWildcard) { - StringBuffer regex; - - regex = new StringBuffer(wildcard.length()); - regex.append('^'); - for (int i = 0, is = wildcard.length(); i < is; i++) + var regexSB = new StringBuffer(aWildcard.length()); + regexSB.append('^'); + for (int i = 0, is = aWildcard.length(); i < is; i++) { - char c = wildcard.charAt(i); + char c = aWildcard.charAt(i); switch (c) { case '*': - regex.append(".*"); - break; - + regexSB.append(".*"); + break; + case '?': - regex.append("."); - break; - + regexSB.append("."); + break; + // escape special regexp-characters case '(': case ')': @@ -59,18 +61,18 @@ public class TokenUtil case '}': case '|': case '\\': - regex.append("\\"); - regex.append(c); - break; - + regexSB.append("\\"); + regexSB.append(c); + break; + default: - regex.append(c); - break; + regexSB.append(c); + break; } } - - regex.append('$'); - return (regex.toString()); - } + + regexSB.append('$'); + return (regexSB.toString()); + } } diff --git a/src/glum/io/token/Tokenizer.java b/src/glum/io/token/Tokenizer.java index 10150eb..690ed17 100644 --- a/src/glum/io/token/Tokenizer.java +++ b/src/glum/io/token/Tokenizer.java @@ -1,12 +1,30 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.io.token; import java.util.ArrayList; +/** + * Interface that provide a mechanism for transforming an input string into an array of (string) tokens. + * + * @author lopeznr1 + */ public interface Tokenizer { /** * Returns all the tokens that match the pattern from the input */ - public ArrayList getTokens(String inputStr); + public ArrayList getTokens(String aInputStr); } diff --git a/src/glum/item/BaseItemManager.java b/src/glum/item/BaseItemManager.java new file mode 100644 index 0000000..5fee8cd --- /dev/null +++ b/src/glum/item/BaseItemManager.java @@ -0,0 +1,133 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.item; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; + +/** + * Base implementation of the ItemManager interface. + * + * @author lopeznr1 + */ +public class BaseItemManager implements ItemManager +{ + // State vars + private List listenerL; + + private ImmutableList fullItemL; + private ImmutableSet pickItemS; + + /** + * Standard Constructor + */ + public BaseItemManager() + { + listenerL = new ArrayList<>(); + + fullItemL = ImmutableList.of(); + pickItemS = ImmutableSet.of(); + } + + @Override + public void addListener(ItemEventListener aListener) + { + listenerL.add(aListener); + } + + @Override + public void delListener(ItemEventListener aListener) + { + listenerL.remove(aListener); + } + + @Override + public ImmutableList getAllItems() + { + return fullItemL; + } + + @Override + public int getNumItems() + { + return fullItemL.size(); + } + + @Override + public ImmutableSet getSelectedItems() + { + return pickItemS; + } + + @Override + public void removeItems(Collection aItemC) + { + List tmpTrackL = new ArrayList<>(fullItemL); + tmpTrackL.removeAll(aItemC); + fullItemL = ImmutableList.copyOf(tmpTrackL); + + Set tmpTrackS = new LinkedHashSet<>(pickItemS); + tmpTrackS.removeAll(aItemC); + pickItemS = ImmutableSet.copyOf(tmpTrackS); + + // Send out the appropriate notifications + notifyListeners(this, ItemEventType.ItemsChanged); + notifyListeners(this, ItemEventType.ItemsSelected); + } + + @Override + public void setAllItems(Collection aItemC) + { + fullItemL = ImmutableList.copyOf(aItemC); + + // Update the picked items to contain items only in fullItemL + Set tmpS = new LinkedHashSet<>(fullItemL); + tmpS = Sets.intersection(pickItemS, tmpS); + pickItemS = ImmutableSet.copyOf(tmpS); + + notifyListeners(this, ItemEventType.ItemsChanged); + notifyListeners(this, ItemEventType.ItemsSelected); + } + + @Override + public void setSelectedItems(Collection aItemC) + { + // Bail if the selection has not changed + if (aItemC.equals(pickItemS.asList()) == true) + return; + + // Update our selection + pickItemS = ImmutableSet.copyOf(aItemC); + + // Send out the appropriate notifications + notifyListeners(this, ItemEventType.ItemsSelected); + } + + /** + * Sends out notification to all the listeners of the specified event. + */ + protected void notifyListeners(Object aSource, ItemEventType aEventType) + { + for (ItemEventListener aListener : listenerL) + aListener.handleItemEvent(aSource, aEventType); + } + +} diff --git a/src/glum/item/ConstIdGenerator.java b/src/glum/item/ConstIdGenerator.java new file mode 100644 index 0000000..0ae611e --- /dev/null +++ b/src/glum/item/ConstIdGenerator.java @@ -0,0 +1,38 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.item; + +/** + * {@link IdGenerator} that always returns the exact same value. + * + * @author lopeznr1 + */ +public class ConstIdGenerator implements IdGenerator +{ + // State vars + private int constId; + + /** Standard Constructor */ + public ConstIdGenerator(int aConstId) + { + constId = aConstId; + } + + @Override + public int getNextId() + { + return constId; + } + +} diff --git a/src/glum/item/IdGenerator.java b/src/glum/item/IdGenerator.java new file mode 100644 index 0000000..732f875 --- /dev/null +++ b/src/glum/item/IdGenerator.java @@ -0,0 +1,29 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.item; + +/** + * Interface that provides a mechanism for generating ids. + * + * @author lopeznr1 + */ +public interface IdGenerator +{ + /** + * Returns the next available id. The returned id should be unique from any + * other returned value. + */ + public int getNextId(); + +} diff --git a/src/glum/item/IncrIdGenerator.java b/src/glum/item/IncrIdGenerator.java new file mode 100644 index 0000000..eaa2bc7 --- /dev/null +++ b/src/glum/item/IncrIdGenerator.java @@ -0,0 +1,45 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.item; + +/** + * {@link IdGenerator} which returns incrementing id values. + * + * @author lopeznr1 + */ +public class IncrIdGenerator implements IdGenerator +{ + private int nextId; + + /** + * Standard Constructor + * + * @param aBegId + * The id of the first value to be returned. + */ + public IncrIdGenerator(int aBegId) + { + nextId = aBegId; + } + + @Override + public int getNextId() + { + int retId = nextId; + nextId++; + + return retId; + } + +} diff --git a/src/glum/item/ItemEventListener.java b/src/glum/item/ItemEventListener.java new file mode 100644 index 0000000..ee4f692 --- /dev/null +++ b/src/glum/item/ItemEventListener.java @@ -0,0 +1,33 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.item; + +/** + * Interface that provides for the callback mechanism for notification of item changes. + * + * @author lopeznr1 + */ +public interface ItemEventListener +{ + /** + * Notification method that the state of items has changed + * + * @param aSource + * The object that generated this event. + * @param aEvent + * The type that describes the event. + */ + public void handleItemEvent(Object aSource, ItemEventType aEventType); + +} diff --git a/src/glum/item/ItemEventType.java b/src/glum/item/ItemEventType.java new file mode 100644 index 0000000..82e51cb --- /dev/null +++ b/src/glum/item/ItemEventType.java @@ -0,0 +1,38 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.item; + +/** + * Enumeration that provides for the various event types that are supported. + * + * @author lopeznr1 + */ +public enum ItemEventType +{ + /** + * Type which corresponds to an event where the list of items have changed. + */ + ItemsChanged, + + /** + * Type which corresponds to an event where any items have been mutated. + */ + ItemsMutated, + + /** + * Type which corresponds to an event where the selected items have changed. + */ + ItemsSelected; + +} diff --git a/src/glum/item/ItemGroup.java b/src/glum/item/ItemGroup.java new file mode 100644 index 0000000..e5594f7 --- /dev/null +++ b/src/glum/item/ItemGroup.java @@ -0,0 +1,42 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.item; + +/** + * Enum which defines the group of items to apply an operation to. + * + * @author lopeznr1 + */ +public enum ItemGroup +{ + /** + * Type which corresponds to no items. + */ + None, + + /** + * Type which corresponds to all available items. + */ + All, + + /** + * Type which corresponds to the selected items. + */ + Selected, + + /** + * Type which corresponds to the visible items. + */ + Visible, +} diff --git a/src/glum/item/ItemManager.java b/src/glum/item/ItemManager.java new file mode 100644 index 0000000..ebdd170 --- /dev/null +++ b/src/glum/item/ItemManager.java @@ -0,0 +1,60 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.item; + +import java.util.Collection; + +import com.google.common.collect.ImmutableSet; + +import glum.gui.panel.itemList.ItemProcessor; + +/** + * Interface that defines a collection of methods used to manage and handle notification of a set of items. + * + * @author lopeznr1 + */ +public interface ItemManager extends ItemProcessor +{ + /** + * Returns the set of selected items. + */ + public ImmutableSet getSelectedItems(); + + /** + * Removes the specified lists of items from this {@link ItemManager}. + * + * @param aItemC + * The list of items to be removed. + */ + public void removeItems(Collection aItemC); + + /** + * Installs the specified items into this {@link ItemManager}. + *

    + * All prior items will be cleared out. + * + * @param aItemC + * The list of items to be installed. + */ + public void setAllItems(Collection aItemC); + + /** + * Method that sets in the list of selected items. + * + * @param aItemC + * The list of items to be selected. + */ + public void setSelectedItems(Collection aItemC); + +} diff --git a/src/glum/item/ItemManagerUtil.java b/src/glum/item/ItemManagerUtil.java new file mode 100644 index 0000000..5eeac65 --- /dev/null +++ b/src/glum/item/ItemManagerUtil.java @@ -0,0 +1,55 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.item; + +import java.util.Set; + +import com.google.common.collect.*; + +/** + * Collection of utility methods for working with ItemManagers. + * + * @author lopeznr1 + */ +public class ItemManagerUtil +{ + /** + * Utility method that will select all of the items in the specified ItemManager. + */ + public static void selectAll(ItemManager aManager) + { + aManager.setSelectedItems(aManager.getAllItems()); + } + + /** + * Utility method that will invert the selection of the specified ItemManager. + */ + public static void selectInvert(ItemManager aManager) + { + Set fullS = ImmutableSet.copyOf(aManager.getAllItems()); + Set pickS = ImmutableSet.copyOf(aManager.getSelectedItems()); + + Set tmpS = Sets.difference(fullS, pickS); + aManager.setSelectedItems(ImmutableList.copyOf(tmpS)); + } + + /** + * Utility method that will clear the selection of the specified ItemManager. + */ + public static void selectNone(ItemManager aManager) + { + aManager.setSelectedItems(ImmutableList.of()); + } + +} diff --git a/src/glum/logic/LogicChunk.java b/src/glum/logic/LogicChunk.java index f42e4b3..3efecca 100644 --- a/src/glum/logic/LogicChunk.java +++ b/src/glum/logic/LogicChunk.java @@ -1,13 +1,32 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.logic; +/** + * Interface that defines a specific block of logic. + * + * @author lopeznr1 + */ public interface LogicChunk { /** - * LogicChunk interface methods - */ + * Runs the logic asssociated with this {@link LogicChunk}. + */ public void activate(); - public void dispose(); - public String getName(); - public String getVersion(); + /** + * Notifies the {@link LogicChunk} that it will no longer be utilized. + */ + public void dispose(); } diff --git a/src/glum/logic/LogicChunkEngine.java b/src/glum/logic/LogicChunkEngine.java index 14613f4..2365467 100644 --- a/src/glum/logic/LogicChunkEngine.java +++ b/src/glum/logic/LogicChunkEngine.java @@ -1,37 +1,55 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.logic; -import glum.io.token.MatchTokenizer; -import glum.reflect.ReflectUtil; -import glum.registry.Registry; - import java.awt.GraphicsEnvironment; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.*; -import java.lang.reflect.Constructor; import java.net.URL; import java.util.*; import javax.swing.*; +import glum.io.token.MatchTokenizer; +import glum.reflect.ReflectUtil; +import glum.registry.Registry; + +/** + * Mechanism for loading {@link LogicChunk}s into an application. + * + * @author lopeznr1 + */ public class LogicChunkEngine implements ActionListener { + // Ref vars + private final Registry refRegistry; + // State vars - private Registry refRegistry; private JMenuBar menuBar; private Map menuMap; private ShutdownHook shutdownHook; + /** Standard Constructor */ public LogicChunkEngine(Registry aRegistry, URL aURL, String aAppName) { - boolean isHeadless; - refRegistry = aRegistry; menuMap = null; menuBar = null; // Are we headless - isHeadless = GraphicsEnvironment.isHeadless(); + var isHeadless = GraphicsEnvironment.isHeadless(); // Install our custom shutdown logic shutdownHook = new ShutdownHook(this, aAppName); @@ -53,7 +71,7 @@ public class LogicChunkEngine implements ActionListener if (aLogicChunk != null) aLogicChunk.dispose(); } - catch(Exception aExp) + catch (Exception aExp) { System.out.println("Failed to dispose LogicChunk. Exception:"); aExp.printStackTrace(); @@ -91,109 +109,85 @@ public class LogicChunkEngine implements ActionListener @Override public void actionPerformed(ActionEvent aEvent) { - LogicChunk aLogicChunk; - // Insanity check if (aEvent == null || menuMap == null) return; // Find the associated LogicChunk - aLogicChunk = (LogicChunk)menuMap.get(aEvent.getSource()); + var tmpLogicChunk = menuMap.get(aEvent.getSource()); // Activate the MenuItem - if (aLogicChunk != null) - aLogicChunk.activate(); + if (tmpLogicChunk != null) + tmpLogicChunk.activate(); } /** * Helper method to load up the LogicChunk described at aURL */ - private void loadLogicChunks(URL aURL, boolean isHeadless) + private void loadLogicChunks(URL aURL, boolean aIsHeadless) { - InputStream inStream; - BufferedReader br; - MatchTokenizer aTokenizer; - String regEx; - ArrayList tokenList; - String[] tokens; - int numTokens; - LinkedList currMenuList; - JMenuBar aMenuBar; - JMenu currMenu; - JMenuItem aMenuItem; - String strLine; - String aLoc; - LogicChunk logicChunk; + var aLoc = aURL.toString(); - aLoc = aURL.toString(); - - currMenuList = new LinkedList(); - aMenuItem = null; + var currMenuL = new LinkedList(); menuMap = new LinkedHashMap(); // Build our tokenizer - regEx = "(#.*)|([a-zA-Z0-9\\.]+)|(\"[^\"\\r\\n]*\")|([a-zA-Z0-9\\.]+)"; - aTokenizer = new MatchTokenizer(regEx); + var regEx = "(#.*)|([a-zA-Z0-9\\.]+)|(\"[^\"\\r\\n]*\")|([a-zA-Z0-9\\.]+)"; + var tmpTokenizer = new MatchTokenizer(regEx); // Create the MenuBar only if we are not headless - aMenuBar = null; - if (isHeadless == false) - aMenuBar = new JMenuBar(); + var tmpMenuBar = (JMenuBar) null; + if (aIsHeadless == false) + tmpMenuBar = new JMenuBar(); // Process our input - try + JMenu currMenu; + try (var br = new BufferedReader(new InputStreamReader(aURL.openStream()))) { - inStream = aURL.openStream(); - br = new BufferedReader(new InputStreamReader(inStream)); - // Read the lines while (true) { - strLine = br.readLine(); + var strLine = br.readLine(); if (strLine == null) { br.close(); - inStream.close(); break; } // Get the tokens out of strLine - tokenList = aTokenizer.getTokens(strLine); + var tokenL = tmpTokenizer.getTokens(strLine); - tokens = tokenList.toArray(new String[0]); - numTokens = tokens.length; + var tokenArr = tokenL.toArray(new String[0]); + var numTokens = tokenArr.length; + + // Skip to the next line if the line is empty + if (numTokens == 0) + continue; // Process the tokens - if (numTokens == 0) + if ((aIsHeadless == true) && (tokenArr[0].equals("Menu") == true || tokenArr[0].equals("MenuItem") == true + || tokenArr[0].equals("SubMenu") || tokenArr[0].equals("EndSubMenu"))) { - ; // Empty line + System.out.println("Ignoring:" + tokenArr[0] + " command. Running in headless environment."); + System.out.println("\tTokens: " + tokenArr); } - else if ((isHeadless == true) && (tokens[0].equals("Menu") == true || tokens[0].equals("MenuItem") == true || tokens[0].equals("SubMenu") || tokens[0].equals("EndSubMenu"))) + else if (tokenArr[0].equals("Menu") == true && numTokens == 2) { - System.out.println("Ignoring:" + tokens[0] + " command. Running in headless environment."); - System.out.println("\tTokens: " + tokens); - } - else if (tokens[0].equals("Menu") == true && numTokens == 2) - { - JMenu aMenu; - // Create a new menu - aMenu = new JMenu(tokens[1]); - aMenuBar.add(aMenu); + var tmpMenu = new JMenu(tokenArr[1]); + tmpMenuBar.add(tmpMenu); // Reset the menu list - currMenuList.clear(); - currMenuList.addLast(aMenu); + currMenuL.clear(); + currMenuL.addLast(tmpMenu); } - else if (tokens[0].equals("SubMenu") == true && numTokens == 2) + else if (tokenArr[0].equals("SubMenu") == true && numTokens == 2) { - JMenu aMenu; - // Get the current menu - if (currMenuList.isEmpty() == true) + if (currMenuL.isEmpty() == true) currMenu = null; else - currMenu = currMenuList.getLast(); + currMenu = currMenuL.getLast(); if (currMenu == null) { @@ -202,113 +196,108 @@ public class LogicChunkEngine implements ActionListener else { // Create and add into the current working menu - aMenu = new JMenu(tokens[1]); - currMenu.add(aMenu); + var tmpMenu = new JMenu(tokenArr[1]); + currMenu.add(tmpMenu); - currMenuList.addLast(aMenu); + currMenuL.addLast(tmpMenu); } } - else if (tokens[0].equals("EndSubMenu") == true && numTokens == 1) + else if (tokenArr[0].equals("EndSubMenu") == true && numTokens == 1) { - if (currMenuList.isEmpty() == true) + if (currMenuL.isEmpty() == true) System.out.println("[" + aLoc + "]: Warning no parent sub menu found. Instr: EndSubMenu\n"); else - currMenuList.removeLast(); + currMenuL.removeLast(); } // Process the various types of MenuItems - else if (tokens[0].equals("AutoItem") == true && numTokens == 3) + else if (tokenArr[0].equals("AutoItem") == true && numTokens == 3) { // Build the auto item and add it only into our MenuMap - logicChunk = loadLogicChunkInstance(refRegistry, tokens[2], tokens[1], aLoc); + var logicChunk = loadLogicChunkInstance(refRegistry, tokenArr[2], tokenArr[1], aLoc); if (logicChunk != null) - { menuMap.put(logicChunk, logicChunk); - } } - else if (tokens[0].equals("MenuItem") == true && (numTokens == 2 || numTokens == 3)) + else if (tokenArr[0].equals("MenuItem") == true && (numTokens == 2 || numTokens == 3)) { // Get the current menu - if (currMenuList.isEmpty() == true) + if (currMenuL.isEmpty() == true) currMenu = null; else - currMenu = currMenuList.getLast(); + currMenu = currMenuL.getLast(); if (currMenu == null) { System.out.println("[" + aLoc + "]: Warning no parent sub menu found. Instr: MenuItem.\n"); } - else if (tokens[1].equals("Break") == true) + else if (tokenArr[1].equals("Break") == true) { currMenu.addSeparator(); } else { + JMenuItem tmpMenuItem; + // Build the menu item if (numTokens == 2) { - aMenuItem = new JMenuItem(tokens[1]); - aMenuItem.setEnabled(false); + tmpMenuItem = new JMenuItem(tokenArr[1]); + tmpMenuItem.setEnabled(false); } else { // Try to build the LogicChunk and load it into our MenuMap - logicChunk = loadLogicChunkInstance(refRegistry, tokens[2], tokens[1], aLoc); + var logicChunk = loadLogicChunkInstance(refRegistry, tokenArr[2], tokenArr[1], aLoc); // Form the MenuItem or Menu if (logicChunk instanceof SubMenuChunk) - aMenuItem = new JMenu(tokens[1]); + tmpMenuItem = new JMenu(tokenArr[1]); else - aMenuItem = new JMenuItem(tokens[1]); - aMenuItem.addActionListener(this); + tmpMenuItem = new JMenuItem(tokenArr[1]); + tmpMenuItem.addActionListener(this); // Associate the MenuItem with the LogicChunk if (logicChunk != null) - menuMap.put(aMenuItem, logicChunk); + menuMap.put(tmpMenuItem, logicChunk); // Notify MenuItemChunk/SubMenuChunk of the associated menu item if (logicChunk instanceof MenuItemChunk) - ((MenuItemChunk)logicChunk).setMenuItem(aMenuItem); + ((MenuItemChunk) logicChunk).setMenuItem(tmpMenuItem); if (logicChunk instanceof SubMenuChunk) - ((SubMenuChunk)logicChunk).setMenu((JMenu)aMenuItem); + ((SubMenuChunk) logicChunk).setMenu((JMenu) tmpMenuItem); } - currMenu.add(aMenuItem); + currMenu.add(tmpMenuItem); } } else { - System.out.println("Unreconized line. Instruction: -" + tokens[0] + "-"); - System.out.println("\tTokens: " + tokens); + System.err.println("Unreconized line. Instruction: -" + tokenArr[0] + "-"); + System.err.println("\tTokens: " + tokenArr); } } } - catch(FileNotFoundException e) + catch (FileNotFoundException aExp) { - System.out.println("File not found: " + aLoc); + System.err.println("File not found: " + aLoc); return; } - catch(IOException e) + catch (IOException aExp) { - System.out.println("Ioexception occured in: LogicChunkEngine.loadLogicChunks()"); + System.err.println("Ioexception occured in: LogicChunkEngine.loadLogicChunks()"); return; } - menuBar = aMenuBar; + menuBar = tmpMenuBar; } /** * Attempts to load up the logicChunk with the specified aLabel. If that fails then will attempt to construct the * LogicChunk using the default constructor. */ - private static LogicChunk loadLogicChunkInstance(Registry aRegistry, String aFullClassPath, String aLabel, String aLoc) + private static LogicChunk loadLogicChunkInstance(Registry aRegistry, String aFullClassPath, String aLabel, + String aLoc) { - Class rawClass; - Constructor rawConstructor; - Class parmTypes1[] = {Registry.class, String.class}; - Class parmTypes2[] = {String.class}; - Object parmValues[]; - // Insanity check if (aFullClassPath == null) return null; @@ -316,7 +305,7 @@ public class LogicChunkEngine implements ActionListener try { // Retrieve the class and ensure it is a LogicChunk - rawClass = Class.forName(aFullClassPath); + var rawClass = Class.forName(aFullClassPath); if (LogicChunk.class.isAssignableFrom(rawClass) == false) { System.out.println("Failure: " + aFullClassPath + " is not a LogicChunk!"); @@ -325,38 +314,40 @@ public class LogicChunkEngine implements ActionListener } // Try the 1st preferred constructor - rawConstructor = ReflectUtil.getConstructorSafe(rawClass, parmTypes1); + Class parmTypes1[] = { Registry.class, String.class }; + var rawConstructor = ReflectUtil.getConstructorSafe(rawClass, parmTypes1); if (rawConstructor != null) { - parmValues = new Object[2]; + var parmValues = new Object[2]; parmValues[0] = aRegistry; parmValues[1] = aLabel; - return (LogicChunk)rawConstructor.newInstance(parmValues); + return (LogicChunk) rawConstructor.newInstance(parmValues); } // Try the 2nd preferred constructor + Class parmTypes2[] = { String.class }; rawConstructor = ReflectUtil.getConstructorSafe(rawClass, parmTypes2); if (rawConstructor != null) { - parmValues = new Object[1]; + var parmValues = new Object[1]; parmValues[0] = aLabel; - return (LogicChunk)rawConstructor.newInstance(parmValues); + return (LogicChunk) rawConstructor.newInstance(parmValues); } // Just use the default constructor else { - return (LogicChunk)rawClass.getDeclaredConstructor().newInstance(); + return (LogicChunk) rawClass.getDeclaredConstructor().newInstance(); } } - catch(ClassNotFoundException aExp) + catch (ClassNotFoundException aExp) { - System.out.println("Failure: " + aFullClassPath + " not found."); - System.out.println("\tLocation: " + aLoc + "\n"); + System.err.println("Failure: " + aFullClassPath + " not found."); + System.err.println("\tLocation: " + aLoc + "\n"); } - catch(Exception aExp) + catch (Exception aExp) { // Unknown Exception aExp.printStackTrace(); diff --git a/src/glum/logic/MenuItemChunk.java b/src/glum/logic/MenuItemChunk.java index 0f8fce7..8e5ac0c 100644 --- a/src/glum/logic/MenuItemChunk.java +++ b/src/glum/logic/MenuItemChunk.java @@ -1,11 +1,29 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.logic; -import javax.swing.*; +import javax.swing.JMenuItem; +/** + * Interface used with {@link LogicChunk} to access the parent {@link JMenuItem}. + * + * @author lopeznr1 + */ public interface MenuItemChunk { /** - * MenuItemChunk interface methods + * Notifies the {@link LogicChunk} of the associated {@link JMenuItem} */ public void setMenuItem(JMenuItem aMI); diff --git a/src/glum/logic/ShutdownHook.java b/src/glum/logic/ShutdownHook.java index 63a4d50..67d1120 100644 --- a/src/glum/logic/ShutdownHook.java +++ b/src/glum/logic/ShutdownHook.java @@ -1,60 +1,75 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.logic; +import java.util.*; + import glum.util.ThreadUtil; -import java.util.List; -import java.util.Set; - -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; - /** - * Shutdown logic that attempts to properly shutdown the refLogicChunkEngine. This logic assumes - * that any non daemon threads that appear after the creation of this shutdown hook are a direct - * result of the LogicChunkEngine. Any threads that are started due to a LogicChunkEngine should - * be properly stopped whenever the dispose() method is called on the corresponding LogicChunk. - * a - *

  5. This shutdown hook should be installed via Runtime.getRuntime().addShutdownHook(). - *
  6. This shutdown hook should be installed before aLogicChunkEngine.loadLogicChunks() is called. - *
  7. This class should remain package visible + * Shutdown logic that attempts to properly shutdown the refLogicChunkEngine. This logic assumes that any non daemon + * threads that appear after the creation of this shutdown hook are a direct result of the LogicChunkEngine. Any threads + * that are started due to a LogicChunkEngine should be properly stopped whenever the dispose() method is called on the + * corresponding LogicChunk. + *
      + *
    • This shutdown hook should be installed via Runtime.getRuntime().addShutdownHook(). + *
    • This shutdown hook should be installed before aLogicChunkEngine.loadLogicChunks() is called. + *
    • This class should remain package visible + *
    + * + * @author lopeznr1 */ class ShutdownHook extends Thread { // State vars - private Set ignoreThreadSet; + private Set ignoreThreadS; private LogicChunkEngine refLogicChunkEngine; private String appName; private boolean doAbortExit; private boolean doQuickExit; - + + /** + * Standard Constructor + */ public ShutdownHook(LogicChunkEngine aLogicChunkEngine, String aAppName) { refLogicChunkEngine = aLogicChunkEngine; appName = aAppName; doAbortExit = false; doQuickExit = false; - + // Form the list of currently running non daemon threads that can be ignored // Any non daemon threads that appear are assumed to be started via a LogicChunk and thus // should be properly stopped whenever the dispose() method is called on the LogicChunk. - ignoreThreadSet = Sets.newHashSet(); - ignoreThreadSet.addAll(Thread.getAllStackTraces().keySet()); - ignoreThreadSet.add(this); - + ignoreThreadS = new HashSet<>(); + ignoreThreadS.addAll(Thread.getAllStackTraces().keySet()); + ignoreThreadS.add(this); + // Set in the preferred name of this thread setName("thread-" + getClass().getSimpleName()); } /** * Configures the shutdown hook to exit immediately by skipping the shutdown of the refLogicChunkEngine. - *

    + *

    * Note the associated LogicChunks dispose method will never be called. */ public void setAbortExit(boolean aBool) { doAbortExit = aBool; } - + /** * Configures the shutdown hook to exit quickly by not waiting for daemon threads. */ @@ -62,110 +77,105 @@ class ShutdownHook extends Thread { doQuickExit = aBool; } - + @Override public void run() { - List origList, currList; - ThreadGroup aThreadGroup; - int cntAttempts; - // Bail if we have been marked to abort if (doAbortExit == true) { System.out.println(appName + " has been aborted!"); return; } - + // Shutdown the LogicChunkEngine refLogicChunkEngine.dispose(); - + // Bail if we are configured to exit quickly if (doQuickExit == true) return; - + // Ensure all non daemon threads are dead - origList = Lists.newArrayList(); - currList = getBlockList(); - - cntAttempts = 1; - while (currList.isEmpty() == false) + List origL = new ArrayList<>(); + var currL = getBlockList(); + + var cntAttempts = 1; + while (currL.isEmpty() == false) { // Dump out the blocking threads, whenever there is a change - if (origList.size() != currList.size()) + if (origL.size() != currL.size()) { - System.out.println("Shutdown logic blocked by " + currList.size() + " threads.."); - for (Thread aThread : currList) + System.out.println("Shutdown logic blocked by " + currL.size() + " threads.."); + for (Thread aThread : currL) { - aThreadGroup = aThread.getThreadGroup(); - if (aThreadGroup != null) - System.out.println(" Waiting for non daemon thread to die: " + aThread.getName() + " threadGroup: " + aThreadGroup.getName()); + var tmpThreadGroup = aThread.getThreadGroup(); + if (tmpThreadGroup != null) + System.out.println(" Waiting for non daemon thread to die: " + aThread.getName() + + " threadGroup: " + tmpThreadGroup.getName()); else - System.out.println(" Waiting for non daemon thread to die: " + aThread.getName() + " threadGroup: null"); - + System.out.println( + " Waiting for non daemon thread to die: " + aThread.getName() + " threadGroup: null"); + } System.out.println(); - - origList = currList; + + origL = currL; } - + // Give the blocked threads some time to exit ThreadUtil.safeSleep(1000); // Retrieve the updated list of blocking threads - currList = getBlockList(); + currL = getBlockList(); // Bail if too many failed attempts - if (cntAttempts >= 7 & currList.isEmpty() == false) + if (cntAttempts >= 7 & currL.isEmpty() == false) { // Print out the stack trace of all active threads - System.out.println("Shutdown logic blocked by " + currList.size() + " threads.."); - for (Thread aThread : currList) + System.out.println("Shutdown logic blocked by " + currL.size() + " threads.."); + for (Thread aThread : currL) { System.out.println(" [" + aThread.getName() + "] StackTrace:"); for (StackTraceElement aItem : aThread.getStackTrace()) System.out.println(" " + aItem.toString()); } - + System.out.println("\nAborting " + appName + ". Waited too long..."); break; } - + cntAttempts++; } - + // Let the user know that we were properly terminated System.out.println(appName + " has been shutdown."); } - + /** - * Helper method to retrieve the set of non daemon threads that we are waiting on to finish executing - *
    Note any thread with the following name pattern will be ignored: + * Helper method to retrieve the set of non daemon threads that we are waiting on to finish executing + *

    + * Note any thread with the following name pattern will be ignored:
    * AWT-*, DestroyJavaVM */ private List getBlockList() { - List fullList; - List blockList; - String name; - - blockList = Lists.newLinkedList(); - - fullList = Lists.newLinkedList(Thread.getAllStackTraces().keySet()); - for (Thread aThread : fullList) + var blockL = new ArrayList(); + + var fullL = new ArrayList<>(Thread.getAllStackTraces().keySet()); + for (Thread aThread : fullL) { - name = aThread.getName(); - + var name = aThread.getName(); + // Record the non daemon threads that are still alive and not in the ignoreThreadSet or with a known name - if (aThread.isDaemon() == false && aThread.isAlive() == true && ignoreThreadSet.contains(aThread) == false) + if (aThread.isDaemon() == false && aThread.isAlive() == true && ignoreThreadS.contains(aThread) == false) { // Only add the thread if the name is not one of a well defined name such as: AWT-*, DestroyJavaVM if ((name.startsWith("AWT-") == true || name.startsWith("DestroyJavaVM") == true) == false) - blockList.add(aThread); + blockL.add(aThread); } } - - return blockList; + + return blockL; } } diff --git a/src/glum/logic/SubMenuChunk.java b/src/glum/logic/SubMenuChunk.java index d2f2b54..618e15c 100644 --- a/src/glum/logic/SubMenuChunk.java +++ b/src/glum/logic/SubMenuChunk.java @@ -1,12 +1,30 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.logic; -import javax.swing.*; +import javax.swing.JMenu; +/** + * Interface used with {@link LogicChunk} to access the parent {@link JMenu}. + * + * @author lopeznr1 + */ public interface SubMenuChunk { /** - * MenuItemChunk interface methods - */ + * Notifies the {@link LogicChunk} of the associated {@link JMenu} + */ public void setMenu(JMenu aMenu); } diff --git a/src/glum/logic/dock/FrontendLoadMI.java b/src/glum/logic/dock/FrontendLoadMI.java index 50c5d8a..32ff5fb 100644 --- a/src/glum/logic/dock/FrontendLoadMI.java +++ b/src/glum/logic/dock/FrontendLoadMI.java @@ -1,24 +1,45 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.logic.dock; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.util.Set; + import javax.swing.JMenu; import javax.swing.JMenuItem; -import glum.logic.LogicChunk; -import glum.logic.SubMenuChunk; -import glum.registry.Registry; - import bibliothek.gui.DockFrontend; import bibliothek.gui.Dockable; import bibliothek.gui.dock.event.DockFrontendListener; +import glum.logic.LogicChunk; +import glum.logic.SubMenuChunk; +import glum.registry.Registry; +/** + * {@link LogicChunk} used to allow an end user to load a dockable configuration. + * + * @author lopeznr1 + */ public class FrontendLoadMI implements LogicChunk, SubMenuChunk, DockFrontendListener, ActionListener { - protected DockFrontend refFrontend; - protected JMenu refMenu; + // Ref vars + private final DockFrontend refFrontend; + // State vars + private JMenu refMenu; + + /** Standard Constructor */ public FrontendLoadMI(Registry aRegistry, String aLabel) { refFrontend = aRegistry.getSingleton(DockFrontend.class, DockFrontend.class); @@ -28,14 +49,11 @@ public class FrontendLoadMI implements LogicChunk, SubMenuChunk, DockFrontendLis @Override public void actionPerformed(ActionEvent aEvent) { - Object aSource; - String aName; - - aSource = aEvent.getSource(); - if (aSource instanceof JMenuItem) + var source = aEvent.getSource(); + if (source instanceof JMenuItem) { - aName = ((JMenuItem)aSource).getText(); - refFrontend.load(aName); + var name = ((JMenuItem) source).getText(); + refFrontend.load(name); } } @@ -50,18 +68,6 @@ public class FrontendLoadMI implements LogicChunk, SubMenuChunk, DockFrontendLis { } - @Override - public String getName() - { - return "DockFrontend Configuration Loader"; - } - - @Override - public String getVersion() - { - return "0.1"; - } - @Override public void setMenu(JMenu aMenu) { @@ -124,32 +130,28 @@ public class FrontendLoadMI implements LogicChunk, SubMenuChunk, DockFrontendLis } /** - * Utility method to keep the refMenu in sync with the available dock - * configurations + * Helper method that keeps various UI components synchronized. */ protected void updateGui() { - Set currSet; - JMenuItem tmpMI; - // Remove the old items refMenu.removeAll(); // Add all of the current configurations - currSet = refFrontend.getSettings(); - for (String aStr : currSet) + var currS = refFrontend.getSettings(); + for (String aStr : currS) { // Do not add hidden configurations if (aStr.charAt(0) != '.') { - tmpMI = new JMenuItem(aStr); + var tmpMI = new JMenuItem(aStr); tmpMI.addActionListener(this); refMenu.add(tmpMI); } } // Ensure we have items (to be enabled) - refMenu.setEnabled(currSet.size() > 0); + refMenu.setEnabled(currS.size() > 0); } } diff --git a/src/glum/logic/dock/FrontendManageMI.java b/src/glum/logic/dock/FrontendManageMI.java index 37e2a7e..946d896 100644 --- a/src/glum/logic/dock/FrontendManageMI.java +++ b/src/glum/logic/dock/FrontendManageMI.java @@ -1,27 +1,43 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.logic.dock; import javax.swing.JFrame; - import bibliothek.gui.DockFrontend; - import glum.gui.dock.FrontendManageConfigPanel; import glum.logic.LogicChunk; import glum.registry.Registry; +/** + * {@link LogicChunk} used to display the various dockable configurations and allow the user to switch to a specific + * configuration.. + * + * @author lopeznr1 + */ public class FrontendManageMI implements LogicChunk { - private FrontendManageConfigPanel myPanel; - + // State vars + private final FrontendManageConfigPanel myPanel; + + /** Standard Constructor */ public FrontendManageMI(Registry aRegistry, String aLabel) { - JFrame aFrame; - DockFrontend aFrontend; - - aFrame = aRegistry.getSingleton("root.window", JFrame.class); - aFrontend = aRegistry.getSingleton(DockFrontend.class, DockFrontend.class); - - myPanel = new FrontendManageConfigPanel(aFrame, aFrontend); + var tmpFrame = aRegistry.getSingleton("root.window", JFrame.class); + var tmpFrontend = aRegistry.getSingleton(DockFrontend.class, DockFrontend.class); + + myPanel = new FrontendManageConfigPanel(tmpFrame, tmpFrontend); } @Override @@ -35,16 +51,4 @@ public class FrontendManageMI implements LogicChunk { } - @Override - public String getName() - { - return "DockFrontend Configuration Manager"; - } - - @Override - public String getVersion() - { - return "0.1"; - } - } diff --git a/src/glum/logic/dock/FrontendSaveMI.java b/src/glum/logic/dock/FrontendSaveMI.java index c556d6d..3f3e4c9 100644 --- a/src/glum/logic/dock/FrontendSaveMI.java +++ b/src/glum/logic/dock/FrontendSaveMI.java @@ -1,26 +1,42 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.logic.dock; import javax.swing.JFrame; +import bibliothek.gui.DockFrontend; import glum.gui.dock.FrontendAddConfigPanel; import glum.logic.LogicChunk; import glum.registry.Registry; -import bibliothek.gui.DockFrontend; - +/** + * {@link LogicChunk} used to allow an end user to save a dockable configuration. + * + * @author lopeznr1 + */ public class FrontendSaveMI implements LogicChunk { - protected FrontendAddConfigPanel myPanel; - + // State vars + private FrontendAddConfigPanel myPanel; + + /** Standard Constructor */ public FrontendSaveMI(Registry aRegistry, String aLabel) { - JFrame aFrame; - DockFrontend aFrontend; - - aFrame = aRegistry.getSingleton("root.window", JFrame.class); - aFrontend = aRegistry.getSingleton(DockFrontend.class, DockFrontend.class); + var tmpFrame = aRegistry.getSingleton("root.window", JFrame.class); + var tmpFrontend = aRegistry.getSingleton(DockFrontend.class, DockFrontend.class); - myPanel = new FrontendAddConfigPanel(aFrame, aFrontend); + myPanel = new FrontendAddConfigPanel(tmpFrame, tmpFrontend); } @Override @@ -34,16 +50,4 @@ public class FrontendSaveMI implements LogicChunk { } - @Override - public String getName() - { - return "DockFrontend Configuration Saver"; - } - - @Override - public String getVersion() - { - return "0.1"; - } - } diff --git a/src/glum/logic/dock/FrontendShowMI.java b/src/glum/logic/dock/FrontendShowMI.java index 337e676..1334776 100644 --- a/src/glum/logic/dock/FrontendShowMI.java +++ b/src/glum/logic/dock/FrontendShowMI.java @@ -1,50 +1,65 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.logic.dock; import javax.swing.JMenuItem; +import bibliothek.gui.DockFrontend; import glum.logic.LogicChunk; import glum.logic.MenuItemChunk; import glum.registry.Registry; -import bibliothek.gui.DockFrontend; -import bibliothek.gui.Dockable; - +/** + * {@link LogicChunk} used to allow an end user to display a dockable. + * + * @author lopeznr1 + */ public class FrontendShowMI implements LogicChunk, MenuItemChunk { + // Ref vars + private final DockFrontend refFrontend; + // State vars - protected DockFrontend refFrontend; - protected String labelStr; - protected String refName; - + private String labelStr; + private String refName; + + /** Standard Constructor */ public FrontendShowMI(Registry aRegistry, String aLabel) { - String[] tokens; - refFrontend = aRegistry.getSingleton(DockFrontend.class, DockFrontend.class); - - tokens = aLabel.split(":"); - if (tokens.length != 2) + + var tokenArr = aLabel.split(":"); + if (tokenArr.length != 2) throw new RuntimeException("Invalid label specification for LogicChunk.\n Label: " + aLabel); - - labelStr = tokens[0]; - refName = tokens[1]; + + labelStr = tokenArr[0]; + refName = tokenArr[1]; } @Override public void activate() { - Dockable aDockable; - - aDockable = refFrontend.getDockable(refName); - if (aDockable == null) + var tmpDockable = refFrontend.getDockable(refName); + if (tmpDockable == null) { System.out.println("Failed to locate a dockable with the name: " + refName); return; } - + // refFrontend.hide(aDockable); - refFrontend.show(aDockable); - aDockable.getDockParent().setFrontDockable(aDockable); + refFrontend.show(tmpDockable); + tmpDockable.getDockParent().setFrontDockable(tmpDockable); } @Override @@ -52,18 +67,6 @@ public class FrontendShowMI implements LogicChunk, MenuItemChunk { } - @Override - public String getName() - { - return "DockFrontend Dock Shower"; - } - - @Override - public String getVersion() - { - return "0.1"; - } - @Override public void setMenuItem(JMenuItem aMI) { diff --git a/src/glum/logic/misc/ConsoleEchoMI.java b/src/glum/logic/misc/ConsoleEchoMI.java index 324c41f..a6a0812 100644 --- a/src/glum/logic/misc/ConsoleEchoMI.java +++ b/src/glum/logic/misc/ConsoleEchoMI.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.logic.misc; import javax.swing.JMenuItem; @@ -7,27 +20,31 @@ import glum.logic.MenuItemChunk; import glum.registry.Registry; /** - * LogicChunk used to echo a message to the console. + * {@link LogicChunk} used to echo a message to the console. + * + * @author lopeznr1 */ public class ConsoleEchoMI implements LogicChunk, MenuItemChunk { - private String message; - private String title; + // Attributes + private final String message; + private final String title; + /** Standard Constructor */ public ConsoleEchoMI(Registry aRegistry, String aLabel) { - String[] strArr; - - title = aLabel; - message = "No message specified."; - // Customize the message and MenuItem title - strArr = aLabel.split(":"); + var strArr = aLabel.split(":"); if (strArr.length == 2) { title = strArr[0]; message = strArr[1]; } + else + { + title = aLabel; + message = "No message specified."; + } } @Override @@ -42,18 +59,6 @@ public class ConsoleEchoMI implements LogicChunk, MenuItemChunk ; // Nothing to do } - @Override - public String getName() - { - return "Console Echo MI"; - } - - @Override - public String getVersion() - { - return "0.1"; - } - @Override public void setMenuItem(JMenuItem aMI) { diff --git a/src/glum/logic/misc/MemoryDialogMI.java b/src/glum/logic/misc/MemoryDialogMI.java index 15dde61..20dfdce 100644 --- a/src/glum/logic/misc/MemoryDialogMI.java +++ b/src/glum/logic/misc/MemoryDialogMI.java @@ -1,16 +1,38 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.logic.misc; -import glum.gui.dialog.MemoryUtilDialog; -import glum.logic.LogicChunk; -import glum.registry.Registry; - import javax.swing.JFrame; +import glum.gui.memory.MemoryUtilDialog; +import glum.logic.LogicChunk; +import glum.registry.Registry; + +/** + * {@link LogicChunk} used to show a UI component of the memory state of the application. + * + * @author lopeznr1 + */ public class MemoryDialogMI implements LogicChunk { - private JFrame refMainFrame; + // Ref vars + private final JFrame refMainFrame; + + // State vars private MemoryUtilDialog myDialog; - + + /** Standard Constructor */ public MemoryDialogMI(Registry aRegistry, String aLabel) { refMainFrame = aRegistry.getSingleton("root.window", JFrame.class); @@ -23,7 +45,7 @@ public class MemoryDialogMI implements LogicChunk // Lazy initialization if (myDialog == null) myDialog = new MemoryUtilDialog(refMainFrame); - + myDialog.setVisible(true); } @@ -32,16 +54,4 @@ public class MemoryDialogMI implements LogicChunk { } - @Override - public String getName() - { - return "Memory Util Dialog"; - } - - @Override - public String getVersion() - { - return "0.1"; - } - } diff --git a/src/glum/math/FastMath.java b/src/glum/math/FastMath.java deleted file mode 100644 index a4475fd..0000000 --- a/src/glum/math/FastMath.java +++ /dev/null @@ -1,614 +0,0 @@ -package glum.math; - -final public class FastMath -{ - - private FastMath(){} - - - /** A "close to zero" double epsilon value for use*/ - public static final double DBL_EPSILON = 2.220446049250313E-16d; - - - /** A "close to zero" double epsilon value for use*/ - public static final double FLT_EPSILON = 1.1920928955078125E-7d; - - - /** A "close to zero" double epsilon value for use*/ - public static final double ZERO_TOLERANCE = 0.0001d; - - public static final double ONE_THIRD = 1.0/3.0; - - - /** The value PI as a double. (180 degrees) */ - public static final double PI = Math.PI; - - - /** The value 2PI as a double. (360 degrees) */ - public static final double TWO_PI = 2.0 * PI; - - - /** The value PI/2 as a double. (90 degrees) */ - public static final double HALF_PI = 0.5 * PI; - - - /** The value PI/4 as a double. (45 degrees) */ - public static final double QUARTER_PI = 0.25 * PI; - - - /** The value 1/PI as a double. */ - public static final double INV_PI = 1.0 / PI; - - - /** The value 1/(2PI) as a double. */ - public static final double INV_TWO_PI = 1.0 / TWO_PI; - - - /** A value to multiply a degree value by, to convert it to radians. */ - public static final double DEG_TO_RAD = PI / 180.0; - - - /** A value to multiply a radian value by, to convert it to degrees. */ - public static final double RAD_TO_DEG = 180.0 / PI; - - - /** A precreated random object for random numbers. */ - //! public static final Random rand = new Random(System.currentTimeMillis()); - - - - - /** - * Returns true if the number is a power of 2 (2,4,8,16...) - * - * A good implementation found on the Java boards. note: a number is a power - * of two if and only if it is the smallest number with that number of - * significant bits. Therefore, if you subtract 1, you know that the new - * number will have fewer bits, so ANDing the original number with anything - * less than it will give 0. - * - * @param number - * The number to test. - * @return True if it is a power of two. - */ - public static boolean isPowerOfTwo(int number) { - return (number > 0) && (number & (number - 1)) == 0; - } - - public static int nearestPowerOfTwo(int number) { - return (int)Math.pow(2, Math.ceil(Math.log(number) / Math.log(2))); - } - - - /** - * Linear interpolation from startValue to endValue by the given percent. - * Basically: ((1 - percent) * startValue) + (percent * endValue) - * - * @param percent - * Percent value to use. - * @param startValue - * Begining value. 0% of f - * @param endValue - * ending value. 100% of f - * @return The interpolated value between startValue and endValue. - */ - public static double LERP(double percent, double startValue, double endValue) { - if (startValue == endValue) return startValue; - return ((1 - percent) * startValue) + (percent * endValue); - } - - - - - /** - * Returns the arc cosine of an angle given in radians.
    - * Special cases: - *

    • If fValue is smaller than -1, then the result is PI. - *
    • If the argument is greater than 1, then the result is 0.
    - * @param fValue The angle, in radians. - * @return fValue's acos - * @see java.lang.Math#acos(double) - */ - public static double acos(double fValue) { - if (-1.0f < fValue) { - if (fValue < 1.0f) - return Math.acos(fValue); - - return 0.0f; - } - - return PI; - } - - - /** - * Returns the arc sine of an angle given in radians.
    - * Special cases: - *
    • If fValue is smaller than -1, then the result is -HALF_PI. - *
    • If the argument is greater than 1, then the result is HALF_PI.
    - * @param fValue The angle, in radians. - * @return fValue's asin - * @see java.lang.Math#asin(double) - */ - public static double asin(double fValue) { - if (-1.0f < fValue) { - if (fValue < 1.0f) - return Math.asin(fValue); - - return HALF_PI; - } - - return -HALF_PI; - } - - - /** - * Returns the arc tangent of an angle given in radians.
    - * @param fValue The angle, in radians. - * @return fValue's asin - * @see java.lang.Math#atan(double) - */ - public static double atan(double fValue) { - return Math.atan(fValue); - } - - - /** - * A direct call to Math.atan2. - * @param fY - * @param fX - * @return Math.atan2(fY,fX) - * @see java.lang.Math#atan2(double, double) - */ - public static double atan2(double fY, double fX) { - return Math.atan2(fY, fX); - } - - - /** - * Rounds a fValue up. A call to Math.ceil - * @param fValue The value. - * @return The fValue rounded up - * @see java.lang.Math#ceil(double) - */ - public static double ceil(double fValue) { - return Math.ceil(fValue); - } - - /** - * Fast Trig functions for x86. This forces the trig functiosn to stay - * within the safe area on the x86 processor (-45 degrees to +45 degrees) - * The results may be very slightly off from what the Math and StrictMath - * trig functions give due to rounding in the angle reduction but it will be - * very very close. - * - * note: code from wiki posting on java.net by jeffpk - */ - public static double reduceSinAngle(double radians) { - radians %= TWO_PI; // put us in -2PI to +2PI space - if (Math.abs(radians) > PI) { // put us in -PI to +PI space - radians = radians - (TWO_PI); - } - if (Math.abs(radians) > HALF_PI) {// put us in -PI/2 to +PI/2 space - radians = PI - radians; - } - - - return radians; - } - - - /** - * Returns sine of a value. - * - * note: code from wiki posting on java.net by jeffpk - * - * @param fValue - * The value to sine, in radians. - * @return The sine of fValue. - * @see java.lang.Math#sin(double) - */ -/*! public static double sin(double fValue) { - fValue = reduceSinAngle(fValue); // limits angle to between -PI/2 and +PI/2 - if (Math.abs(fValue)<=Math.PI/4){ - return Math.sin(fValue); - } - - return Math.cos(Math.PI/2-fValue); - } -*/ - - /** - * Returns cos of a value. - * - * @param fValue - * The value to cosine, in radians. - * @return The cosine of fValue. - * @see java.lang.Math#cos(double) - */ -/*! public static double cos(double fValue) { - return sin(fValue+HALF_PI); - } -*/ - - /** - * Returns E^fValue - * @param fValue Value to raise to a power. - * @return The value E^fValue - * @see java.lang.Math#exp(double) - */ -/*! public static double exp(double fValue) { - return Math.exp(fValue); - } -*/ - - /** - * Returns Absolute value of a double. - * @param fValue The value to abs. - * @return The abs of the value. - * @see java.lang.Math#abs(double) - */ - public static double abs(double fValue) { - if (fValue < 0) return -fValue; - return fValue; - } - - - /** - * Returns a number rounded down. - * @param fValue The value to round - * @return The given number rounded down - * @see java.lang.Math#floor(double) - */ - public static double floor(double fValue) { - return Math.floor(fValue); - } - - - /** - * Returns 1/sqrt(fValue) - * @param fValue The value to process. - * @return 1/sqrt(fValue) - * @see java.lang.Math#sqrt(double) - */ - public static double invSqrt(double fValue) { - return (1.0f / Math.sqrt(fValue)); - } - - - /** - * Returns the log base E of a value. - * @param fValue The value to log. - * @return The log of fValue base E - * @see java.lang.Math#log(double) - */ - public static double log(double fValue) { - return Math.log(fValue); - } - - /** - * Returns the logarithm of value with given base, calculated as log(value)/log(base), - * so that pow(base, return)==value (contributed by vear) - * @param value The value to log. - * @param base Base of logarithm. - * @return The logarithm of value with given base - */ - public static double log(double value, double base) { - return (Math.log(value)/Math.log(base)); - } - - - /** - * Returns a number raised to an exponent power. fBase^fExponent - * @param fBase The base value (IE 2) - * @param fExponent The exponent value (IE 3) - * @return base raised to exponent (IE 8) - * @see java.lang.Math#pow(double, double) - */ -/*! public static double pow(double fBase, double fExponent) { - return Math.pow(fBase, fExponent); - } -*/ - - /** - * Returns the value squared. fValue ^ 2 - * @param fValue The vaule to square. - * @return The square of the given value. - */ - public static double sqr(double fValue) { - return fValue * fValue; - } - - - /** - * Returns the square root of a given value. - * @param fValue The value to sqrt. - * @return The square root of the given value. - * @see java.lang.Math#sqrt(double) - */ - public static double sqrt(double fValue) { - return Math.sqrt(fValue); - } - - - /** - * Returns the tangent of a value. If USE_FAST_TRIG is enabled, an approximate value - * is returned. Otherwise, a direct value is used. - * @param fValue The value to tangent, in radians. - * @return The tangent of fValue. - * @see java.lang.Math#tan(double) - */ - public static double tan(double fValue) { - return Math.tan(fValue); - } - - - - - - - - /** - * Returns the integral value of a given value. - * @param fValue The value to round. - * @return The square root of the given value. - * @see java.lang.Math#round(double) - */ - public static double round(double fValue) { - return Math.round(fValue); - } - - -//compute sine -/*public static double sin(double x) -{ - double aAns; - - if (x < -3.14159265) - x += 6.28318531; - else if (x > 3.14159265) - x -= 6.28318531; - - if (x < 0) - { - aAns = 1.27323954 * x + .405284735 * x * x; - - if (aAns < 0) - aAns = .225 * (aAns *-aAns - aAns) + aAns; - else - aAns = .225 * (aAns * aAns - aAns) + aAns; - } - else - { - aAns = 1.27323954 * x - 0.405284735 * x * x; - - if (aAns < 0) - aAns = .225 * (aAns *-aAns - aAns) + aAns; - else - aAns = .225 * (aAns * aAns - aAns) + aAns; - } - - return aAns; -} - - -public static double cos(double x) -{ - return sin(x + 1.57079632); -} -*/ - -//compute cosine: sin(x + PI/2) = cos(x) -/*public static double cos(double x) -{ - double aAns; - - x += 1.57079632; - if (x > 3.14159265) - x -= 6.28318531; - - if (x < 0) - { - aAns = 1.27323954 * x + 0.405284735 * x * x; - - if (aAns < 0) - aAns = .225 * (aAns *-aAns - aAns) + aAns; - else - aAns = .225 * (aAns * aAns - aAns) + aAns; - } - else - { - aAns = 1.27323954 * x - 0.405284735 * x * x; - - if (aAns < 0) - aAns = .225 * (aAns *-aAns - aAns) + aAns; - else - aAns = .225 * (aAns * aAns - aAns) + aAns; - } - - return aAns; -} -*/ - - - - -public static final double invFact2 = 1.0/2.0; -public static final double invFact3 = 1.0/6.0; -public static final double invFact4 = 1.0/24.0; -public static final double invFact5 = 1.0/120.0; -public static final double invFact6 = 1.0/720.0; -public static final double invFact7 = 1.0/5040.0; -public static final double invFact8 = 1.0/40320.0; -public static final double invFact9 = 1.0/362880.0; -public static final double invFact11 = 1.0/39916800.0; -public static final double invFact13 = 1.0/6227020800.0; -public static final double invFact15 = 1.0/1307674368000.0; - -public static final double invFact2n = -1.0/2.0; -public static final double invFact3n = -1.0/6.0; -public static final double invFact4n = -1.0/24.0; -public static final double invFact5n = -1.0/120.0; -public static final double invFact6n = -1.0/720.0; -public static final double invFact7n = -1.0/5040.0; -public static final double invFact8n = -1.0/40320.0; -public static final double invFact9n = -1.0/362880.0; -public static final double invFact11n = -1.0/39916800.0; - - -/*public static double cos(double x) -{ - double x2, x4, x6; - - while (x < -Math.PI) - x += TWO_PI; - - while (x > Math.PI) - x -= TWO_PI; - - x2 = x * x; - x4 = x2 * x2; - x6 = x4 * x2; - - return 1 - invFact2*x2 + invFact4*x4 - invFact6*x6; -}*/ -public static double cos(double x) -{ - return sin(x + HALF_PI); -} - - -public static double sin(double x) -{ -// double x2, x3, x5, x7; -// double x2, x3, x4; - double x2, x3; - - while (x < -Math.PI) - x += TWO_PI; - - while (x > Math.PI) - x -= TWO_PI; - -// x2 = x * x; -// x3 = x2 * x; -// x5 = x3 * x2; -// x7 = x5 * x2; -// return x - invFact3*x3 + invFact5*x5 - invFact7*x7; - - -// x2 = x * x; -// x3 = x2 * x; -// x4 = x3 * x; -// return x + x3*(-invFact3 + invFact5*x2 - invFact7*x4) - -// x2 = x * x; -// x3 = x2 * x; -// return x + x3*(-invFact3 + x2*(invFact5 - invFact7*x2)); - -// x2 = x * x; -// x3 = x2 * x; -// return x + x3*(-invFact3 + x2*(invFact5 + x2*(-invFact7 + x2*(invFact9 - x2*invFact11)))); - - x2 = x * x; - x3 = x2 * x; - return x + x3*(-invFact3 + x2*(invFact5 + x2*(-invFact7 - + x2*(invFact9 + x2*(-invFact11 + x2*invFact13))))); -} -//public static double sin(double x) -//{ -// return cos(x - 1.57079632679); //! This does not work -//} - - - - -/*//compute sine -public static double sin(double x) -{ - double aAns; - - if (x < -3.14159265) - x += 6.28318531; - else if (x > 3.14159265) - x -= 6.28318531; - - if (x < 0) - aAns = 1.27323954 * x + 0.405284735 * x * x; - else - aAns = 1.27323954 * x - 0.405284735 * x * x; - - return aAns; -} - - -//compute cosine: sin(x + PI/2) = cos(x) -public static double cos(double x) -{ - double aAns; - - x += 1.57079632; - if (x > 3.14159265) - x -= 6.28318531; - - if (x < 0) - aAns = 1.27323954 * x + 0.405284735 * x * x; - else - aAns = 1.27323954 * x - 0.405284735 * x * x; - - return aAns; -} -*/ - - - - - - - - - - - - - - - - /** - * pow - Method to replace Math.pow(). - * THis method should be faster but results in a coarser solution. - * Grabed from: http://martin.ankerl.com/2007/10/04/optimized-pow-approximation-for-java-and-c-c/ - */ - public static double pow(final double a, final double b) - { - final int x = (int) (Double.doubleToLongBits(a) >> 32); - final int y = (int) (b * (x - 1072632447) + 1072632447); - return Double.longBitsToDouble(((long) y) << 32); - } - - - - /** - * exp - Method to replace Math.exp(). - * THis method should be faster but results in a coarser solution. - * Grabed from: http://martin.ankerl.com/2007/10/04/optimized-pow-approximation-for-java-and-c-c/ - */ - public static double exp(double val) - { - final long tmp = (long) (1512775 * val + (1072693248 - 60801)); - return Double.longBitsToDouble(tmp << 32); - } - - - - public static double min(double aVal, double bVal) - { -// return Math.min(aVal, bVal); - if (aVal < bVal) - return aVal; - - return bVal; - } - - -} \ No newline at end of file diff --git a/src/glum/math/FractionTransformer.java b/src/glum/math/FractionTransformer.java deleted file mode 100644 index 1d2db02..0000000 --- a/src/glum/math/FractionTransformer.java +++ /dev/null @@ -1,93 +0,0 @@ -package glum.math; - -public enum FractionTransformer -{ - Linear("Linear", "LINEAR") - { - @Override - public double transform(double aFrac) - { - return aFrac; - } - }, - - Cosine("Cosine", "COSINE") - { - @Override - public double transform(double aFrac) - { - aFrac = aFrac * Math.PI; - aFrac = (1 - Math.cos(aFrac)) * 0.5; - - return aFrac; - } - }, - - Cubic("Cubic", "CUBIC") - { - @Override - public double transform(double aFrac) - { - double t, t2; - - t = aFrac; - t2 = aFrac * aFrac; - aFrac = 3*t2 - 2*t2*t; - - return aFrac; - } - }; - - - // Vars - private final String userStr; - private final String encodeStr; - - - /** - * Constructor - */ - FractionTransformer(String aUserStr, String aEncodeStr) - { - userStr = aUserStr; - encodeStr = aEncodeStr; - } - - - /** - * getReferenceName - */ - public String getEncodeString() - { - return encodeStr; - } - - /** - * transform - */ - public abstract double transform(double aFrac); - - /** - * parse - Returns the associated enum type - */ - public static FractionTransformer parse(String aStr) - { - if (aStr == null) - return null; - - for (FractionTransformer aEnum : FractionTransformer.values()) - { - if (aStr.equals(aEnum.getEncodeString()) == true) - return aEnum; - } - - return null; - } - - @Override - public String toString() - { - return userStr; - } - -} diff --git a/src/glum/math/GradientMesh.java b/src/glum/math/GradientMesh.java deleted file mode 100644 index 561c1fc..0000000 --- a/src/glum/math/GradientMesh.java +++ /dev/null @@ -1,170 +0,0 @@ -package glum.math; - -import static java.lang.Math.*; -import java.util.*; - -import glum.coord.*; - -public class GradientMesh -{ - // Level of detail - int lod; - int bitMask; - - // Reference gradients - double[] grad1D; - Point2D[] grad2D; - Point3D[] grad3D; - - /** - * Constructor - */ - public GradientMesh(long aSeed, int aLod) - { - Random aRand; - - // lod: Level of detail. The number of reference gradients for - // each dimension is equal to 2^lod. Thus we support [64, 1048576] - // reference gradients. Erroneous input will be silently clamped. - lod = aLod; - if (lod < 6) - lod = 6; - else if (lod > 20) - lod = 20; - - // Compute the bit mask associated with lod - bitMask = 0; - for (int c1 = 0; c1 < lod; c1++) - bitMask = (bitMask << 1) + 1; - - // Construct our reference items - aRand = new Random(aSeed); - constructMesh1D(aRand); - constructMesh2D(aRand); -//aRand = new Random(3432); - constructMesh3D(aRand); - } - - public double getGrad1D(int randVal) - { - return grad1D[randVal & bitMask]; - } - - public Point2D getGrad2D(int randVal) - { - return grad2D[randVal & bitMask]; - } - - public Point3D getGrad3D(int randVal) - { - return grad3D[randVal & bitMask]; - } - - /** - * constructMesh1D - */ - protected void constructMesh1D(Random aRand) - { - int numItems; - - numItems = 1 << lod; - grad1D = new double[numItems]; - for (int c1 = 0; c1 < numItems; c1++) - { - if (aRand.nextDouble() < 0.5) - grad1D[c1] = -1; - else - grad1D[c1] = 1; - } - } - - /** - * constructMesh2D - */ - protected void constructMesh2D(Random aRand) - { - Point2D aPt; - double theta; - int numItems; - - numItems = 1 << lod; - grad2D = new Point2D[numItems]; - for (int c1 = 0; c1 < numItems; c1++) - { - // Compute a random point on the unit circle. - aPt = new Point2D(); - - theta = 2 * PI * aRand.nextDouble(); - aPt.x = cos(theta); - aPt.y = sin(theta); - grad2D[c1] = aPt; -/* - while (true) - { - aPt.x = 1.0 - aRand.nextDouble()*2; - aPt.z = 1.0 - aRand.nextDouble()*2; - - if (aPt.x * aPt.x + aPt.y * aPt.y <= 1.0) - break; - } - - len = sqrt((aPt.x * aPt.x) + (aPt.y * aPt.y)); - aPt.x = aPt.x / len; - aPt.y = aPt.y / len; - grad3D[c1] = aPt; -*/ - } - } - - /** - * constructMesh3D - */ - protected void constructMesh3D(Random aRand) - { - Point3D aPt; - double u, v, theta, phi; - int numItems; - - numItems = 1 << lod; - grad3D = new Point3D[numItems]; - for (int c1 = 0; c1 < numItems; c1++) - { - // Compute a random point on a sphere. The logic is taken from the site: - // http://mathworld.wolfram.com/SpherePointPicking.html - // Note you can not pick 3 points x,y, and z and then normalize them to - // the unit vector because these points are constrained by the equation: - // x^2 + y^2 + z^2 = 1 - aPt = new Point3D(); - - u = aRand.nextDouble(); - v = aRand.nextDouble(); - - theta = 2 * PI * u; - phi = acos(2*v - 1); - - // Go from phi, theta to x,y,z on the unit sphere - aPt.x = 1.0*cos(theta)*sin(phi); - aPt.y = 1.0*sin(theta)*sin(phi); - aPt.z = 1.0*cos(phi); - grad3D[c1] = aPt; -/* - while (true) - { - aPt.x = 1.0 - aRand.nextDouble()*2; - aPt.y = 1.0 - aRand.nextDouble()*2; - aPt.z = 1.0 - aRand.nextDouble()*2; - - if (aPt.x * aPt.x + aPt.y * aPt.y + aPt.z * aPt.z <= 1.0) - break; - } - - len = sqrt((aPt.x * aPt.x) + (aPt.y * aPt.y) + (aPt.z * aPt.z)); - aPt.x = aPt.x / len; - aPt.y = aPt.y / len; - aPt.z = aPt.z / len; - grad3D[c1] = aPt; -*/ - } - } - -} diff --git a/src/glum/math/GroupNoise.java b/src/glum/math/GroupNoise.java deleted file mode 100644 index a975a20..0000000 --- a/src/glum/math/GroupNoise.java +++ /dev/null @@ -1,94 +0,0 @@ -package glum.math; - -import java.util.*; - -public class GroupNoise implements Noise -{ - // Collection of individual noise objects - protected Collection myNoiseList; - - /** - * Constructor - */ - public GroupNoise() - { - myNoiseList = new LinkedList(); - } - - public GroupNoise(Collection aNoiseList) - { - myNoiseList = new LinkedList(aNoiseList); - } - - /** - * Adds a Noise object to this GroupNoise - */ - public void add(Noise aNoise) - { - myNoiseList.add(aNoise); - } - - @Override - public double getAmplitude() - { - double maxAmp, aAmp; - - maxAmp = 0; - for (Noise aNoise : myNoiseList) - { - aAmp = aNoise.getAmplitude(); - if (aAmp > maxAmp) - maxAmp = aAmp; - } - - return maxAmp; - } - - /** - * getNoiseList - Returns the list of individual noises - */ - public Collection getNoiseList() - { - List aNoiseList; - - aNoiseList = new LinkedList(myNoiseList); - return aNoiseList; - } - - @Override - public double getValue1D(double x) - { - double total; - - total = 0; - for (Noise aNoiseGen : myNoiseList) - total += aNoiseGen.getValue1D(x); - - return total; - } - - @Override - public double getValue2D(double x, double y) - { - double total; - - total = 0; - for (Noise aNoiseGen : myNoiseList) - total += aNoiseGen.getValue2D(x, y); - - return total; - } - - @Override - public double getValue3D(double x, double y, double z) - { - double total; - - total = 0; - for (Noise aNoiseGen : myNoiseList) - total += aNoiseGen.getValue3D(x, y, z); - - return total; - } - -} diff --git a/src/glum/math/Noise.java b/src/glum/math/Noise.java deleted file mode 100644 index 11ecedd..0000000 --- a/src/glum/math/Noise.java +++ /dev/null @@ -1,28 +0,0 @@ -package glum.math; - -public interface Noise -{ - /** - * getAmplitude - Returns the maximum offset produced by this noise - */ - public double getAmplitude(); - - /** - * getValue1D - Returns a continous random value in the range of [0-1] in 1D space. It is repeatable for the same x - * values. - */ - public double getValue1D(double x); - - /** - * getValue2D - Returns a continous random value in the range of [0-1] in 2D space. It is repeatable for the same x,y - * values. - */ - public double getValue2D(double x, double y); - - /** - * getValue3D - Returns a continous random value in the range of [0-1] in 3D space. It is repeatable for the same - * x,y,z values. - */ - public double getValue3D(double x, double y, double z); - -} diff --git a/src/glum/math/PerlinNoise.java b/src/glum/math/PerlinNoise.java deleted file mode 100644 index eff99f0..0000000 --- a/src/glum/math/PerlinNoise.java +++ /dev/null @@ -1,367 +0,0 @@ -package glum.math; - -import static java.lang.Math.*; -import java.util.*; - -import glum.coord.*; - -public class PerlinNoise implements Noise -{ - // Seed key values - protected GradientMesh myGradientMesh; - protected FractionTransformer myFracTransformer; - protected double amp, freq; - - protected int[] p0Table, p1Table, p2Table, p3Table; - protected int[] p4Table, p5Table, p6Table, p7Table; - protected int[] p8Table, p9Table, p10Table, p11Table; - - /** - * Constructor - */ - public PerlinNoise(long aSeed) - { - this(aSeed, null); - } - - public PerlinNoise(long aSeed, GradientMesh aGradientMesh) - { - Vector aList; - Random aRandom; - - // Set up our rand generater - aRandom = new Random(aSeed); - - // Set up the fraction tranform and function - myFracTransformer = FractionTransformer.Cubic; - - // If no GradientMesh specified then construct our own reference gradient - // mesh with 2^12 reference points. - myGradientMesh = aGradientMesh; - if (myGradientMesh == null) - myGradientMesh = new GradientMesh(aRandom.nextLong(), 12); - - // Construct the words to use in the permutation - aList = new Vector(); - for (int c1 = 0; c1 < 256; c1++) - aList.add(c1); - - Collections.shuffle(aList, aRandom); - p0Table = new int[256]; - for (int c1 = 0; c1 < 256; c1++) - p0Table[c1] = aList.get(c1) << 0; - - Collections.shuffle(aList, aRandom); - p1Table = new int[256]; - for (int c1 = 0; c1 < 256; c1++) - p1Table[c1] = aList.get(c1) << 8; - - Collections.shuffle(aList, aRandom); - p2Table = new int[256]; - for (int c1 = 0; c1 < 256; c1++) - p2Table[c1] = aList.get(c1) << 16; - - Collections.shuffle(aList, aRandom); - p4Table = new int[256]; - for (int c1 = 0; c1 < 256; c1++) - p4Table[c1] = aList.get(c1) << 0; - - Collections.shuffle(aList, aRandom); - p5Table = new int[256]; - for (int c1 = 0; c1 < 256; c1++) - p5Table[c1] = aList.get(c1) << 8; - - Collections.shuffle(aList, aRandom); - p6Table = new int[256]; - for (int c1 = 0; c1 < 256; c1++) - p6Table[c1] = aList.get(c1) << 16; - - Collections.shuffle(aList, aRandom); - p8Table = new int[256]; - for (int c1 = 0; c1 < 256; c1++) - p8Table[c1] = aList.get(c1) << 0; - - Collections.shuffle(aList, aRandom); - p9Table = new int[256]; - for (int c1 = 0; c1 < 256; c1++) - p9Table[c1] = aList.get(c1) << 8; - - Collections.shuffle(aList, aRandom); - p10Table = new int[256]; - for (int c1 = 0; c1 < 256; c1++) - p10Table[c1] = aList.get(c1) << 16; - -/* -int x = 0xF0F0F0F0; -System.out.print(" x[0]:" + ((x & 0x000000ff) >> 0) ); -System.out.print(" x[1]:" + ((x & 0x0000ff00) >> 8) ); -System.out.print(" x[2]:" + ((x & 0x00ff0000) >> 16) ); -System.out.println(" x[3]:" + ((x & 0xff000000) >> 24) ); - -System.out.println("Hmm..."); -System.out.print(" x[0]:" + ((x >> 0) & 0xff) ); -System.out.print(" x[1]:" + ((x >> 8) & 0xff) ); -System.out.print(" x[2]:" + ((x >> 16) & 0xff) ); -System.out.println(" x[3]:" + ((x >> 24) & 0xff) ); -System.exit(0); -*/ - } - - - /** - * setPersistence - Set the amplitude and frequency associated - * with the noise function. - */ - public void setPersistence(double aAmp, double aFreq) - { - amp = aAmp; - freq = aFreq; - } - - - @Override - public double getAmplitude() - { - return amp; - } - - - - - @Override - public double getValue1D(double x) - { - return getInterpolatedValue1D(x * freq) * amp; - } - - - public double getInterpolatedValue1D(double x) - { - int iX; - double fracX; - double left, right; - - iX = (int)floor(x); - fracX = x - iX; - fracX = myFracTransformer.transform(fracX); - - left = getSmoothValue1D(x, iX); - right = getSmoothValue1D(x, iX + 1); - - return interpolate(left, right, fracX); - } - - - public double getSmoothValue1D(double x, int qX) - { - int vx; -// int w1, w2, w3, w4; - int w1, w2, w3; - double gradPt; - double value; - - vx = qX; - w1 = (vx >> 0) & 0xFF; - w2 = (vx >> 8) & 0xFF; - w3 = (vx >> 16) & 0xFF; -// w4 = (vx >> 24) & 0xFF; - w2 = w2 + w1; - w3 = w3 + w2; -// w4 = w4 + w3; - vx = (p0Table[w1 & 0xff] ) | (p1Table[w2 & 0xff] ) | (p2Table[w3 & 0xff] ); - - // Return a value from -1 to 1 - gradPt = myGradientMesh.getGrad1D(vx); - value = gradPt * (x - qX); - return value; - } - - - - - - - - - - - @Override - public double getValue2D(double x, double y) - { - return getInterpolatedValue2D(x * freq, y * freq) * amp; - } - - - public double getInterpolatedValue2D(double x, double y) - { - int iX, iY; - double fracX, fracY; - double tL, tR, bL, bR, topRow, botRow; - - iX = (int)floor(x); - fracX = x - iX; - fracX = myFracTransformer.transform(fracX); - - iY = (int)floor(y); - fracY = y - iY; - fracY = myFracTransformer.transform(fracY); - - tL = getSmoothValue2D(x, y, iX, iY); - tR = getSmoothValue2D(x, y, iX + 1, iY); - bL = getSmoothValue2D(x, y, iX, iY + 1); - bR = getSmoothValue2D(x, y, iX + 1, iY + 1); - - topRow = interpolate(tL, tR, fracX); - botRow = interpolate(bL, bR, fracX); - - return interpolate(topRow, botRow, fracY); - } - - - public double getSmoothValue2D(double x, double y, int qX, int qY) - { - int vx, vy; -// int w1, w2, w3, w4; - int w1, w2, w3; - Point2D gradPt; - double value; - - vx = qX; - w1 = (vx >> 0) & 0xFF; - w2 = (vx >> 8) & 0xFF; - w3 = (vx >> 16) & 0xFF; -// w4 = (vx >> 24) & 0xFF; - w2 = w2 + w1; - w3 = w3 + w2; -// w4 = w4 + w3; - vx = (p0Table[w1 & 0xff] ) | (p1Table[w2 & 0xff] ) | (p2Table[w3 & 0xff] ); - - vy = vx + qY; - w1 = (vy >> 0) & 0xFF; - w2 = (vy >> 8) & 0xFF; - w3 = (vy >> 16) & 0xFF; -// w4 = (vy >> 24) & 0xFF; - w2 = w2 + w1; - w3 = w3 + w2; -// w4 = w4 + w3; - vy = (p4Table[w1 & 0xff] ) | (p5Table[w2 & 0xff] ) | (p6Table[w3 & 0xff] ); - - // Return a value from -1 to 1 - gradPt = myGradientMesh.getGrad2D(vy); - value = (gradPt.x * (x - qX)) + (gradPt.y * (y - qY)); - return value; - } - - - - - - - - - - - - - - @Override - public double getValue3D(double x, double y, double z) - { - return getInterpolatedValue3D(x * freq, y * freq, z * freq) * amp; - } - - - public double getInterpolatedValue3D(double x, double y, double z) - { - int iX, iY, iZ; - double fracX, fracY, fracZ; - double tL, tR, bL, bR, topRow, botRow; - double planeZ1, planeZ2; - - iX = (int)floor(x); - fracX = x - iX; - fracX = myFracTransformer.transform(fracX); - - iY = (int)floor(y); - fracY = y - iY; - fracY = myFracTransformer.transform(fracY); - - iZ = (int)floor(z); - fracZ = z - iZ; - fracZ = myFracTransformer.transform(fracZ); - - tL = getSmoothValue3D(x, y, z, iX, iY, iZ); - tR = getSmoothValue3D(x, y, z, iX + 1, iY, iZ); - bL = getSmoothValue3D(x, y, z, iX, iY + 1, iZ); - bR = getSmoothValue3D(x, y, z, iX + 1, iY + 1, iZ); - topRow = interpolate(tL, tR, fracX); - botRow = interpolate(bL, bR, fracX); - planeZ1 = interpolate(topRow, botRow, fracY); - - tL = getSmoothValue3D(x, y, z, iX, iY, iZ + 1); - tR = getSmoothValue3D(x, y, z, iX + 1, iY, iZ + 1); - bL = getSmoothValue3D(x, y, z, iX, iY + 1, iZ + 1); - bR = getSmoothValue3D(x, y, z, iX + 1, iY + 1, iZ + 1); - topRow = interpolate(tL, tR, fracX); - botRow = interpolate(bL, bR, fracX); - planeZ2 = interpolate(topRow, botRow, fracY); - - return interpolate(planeZ1, planeZ2, fracZ); - } - - - public double getSmoothValue3D(double x, double y, double z, int qX, int qY, int qZ) - { - int vx, vy, vz; -// int w1, w2, w3, w4; - int w1, w2, w3; - Point3D gradPt; - double value; - - vx = qX; - w1 = (vx >> 0) & 0xFF; - w2 = (vx >> 8) & 0xFF; - w3 = (vx >> 16) & 0xFF; -// w4 = (vx >> 24) & 0xFF; - w2 = w2 + w1; - w3 = w3 + w2; -// w4 = w4 + w3; - vx = (p0Table[w1 & 0xff] ) | (p1Table[w2 & 0xff] ) | (p2Table[w3 & 0xff] ); - - vy = vx + qY; - w1 = (vy >> 0) & 0xFF; - w2 = (vy >> 8) & 0xFF; - w3 = (vy >> 16) & 0xFF; -// w4 = (vy >> 24) & 0xFF; - w2 = w2 + w1; - w3 = w3 + w2; -// w4 = w4 + w3; - vy = (p4Table[w1 & 0xff] ) | (p5Table[w2 & 0xff] ) | (p6Table[w3 & 0xff] ); - - vz = vy + qZ; - w1 = (vz >> 0) & 0xFF; - w2 = (vz >> 8) & 0xFF; - w3 = (vz >> 16) & 0xFF; -// w4 = (vz >> 24) & 0xFF; - w2 = w2 + w1; - w3 = w3 + w2; -// w4 = w4 + w3; - vz = (p8Table[w1 & 0xff] ) | (p9Table[w2 & 0xff] ) | (p10Table[w3 & 0xff] ); - - // Return a value from -1 to 1 - gradPt = myGradientMesh.getGrad3D(vz); - value = (gradPt.x * (x - qX)) + (gradPt.y * (y - qY)) + (gradPt.z * (z - qZ)); - return value; - } - - - /** - * interpolate - Returns a value between v1 and v2 wrt to frac. - */ - protected double interpolate(double v1, double v2, double frac) - { - return v1 + frac*(v2 - v1); - } - -} - diff --git a/src/glum/math/TransformNoise.java b/src/glum/math/TransformNoise.java deleted file mode 100644 index 2c840fb..0000000 --- a/src/glum/math/TransformNoise.java +++ /dev/null @@ -1,44 +0,0 @@ -package glum.math; - -public class TransformNoise implements Noise -{ - // State vars - protected Noise refNoise; - protected double offsetVal; - protected double scalarVal; - - /** - * Constructor - */ - public TransformNoise(Noise aRefNoise, double aOffsetVal, double aScalarVal) - { - refNoise = aRefNoise; - offsetVal = aOffsetVal; - scalarVal = aScalarVal; - } - - @Override - public double getAmplitude() - { - return scalarVal * refNoise.getAmplitude(); - } - - @Override - public double getValue1D(double x) - { - return offsetVal + (scalarVal * refNoise.getValue1D(x)); - } - - @Override - public double getValue2D(double x, double y) - { - return offsetVal + (scalarVal * refNoise.getValue2D(x, y)); - } - - @Override - public double getValue3D(double x, double y, double z) - { - return offsetVal + (scalarVal * refNoise.getValue3D(x, y, z)); - } - -} diff --git a/src/glum/math/TurbulentNoise.java b/src/glum/math/TurbulentNoise.java deleted file mode 100644 index 9f21a38..0000000 --- a/src/glum/math/TurbulentNoise.java +++ /dev/null @@ -1,40 +0,0 @@ -package glum.math; - -public class TurbulentNoise implements Noise -{ - // State vars - protected Noise refNoise; - - /** - * Constructor - */ - public TurbulentNoise(Noise aRefNoise) - { - refNoise = aRefNoise; - } - - @Override - public double getAmplitude() - { - return refNoise.getAmplitude(); - } - - @Override - public double getValue1D(double x) - { - return Math.abs(refNoise.getValue1D(x)); - } - - @Override - public double getValue2D(double x, double y) - { - return Math.abs(refNoise.getValue2D(x, y)); - } - - @Override - public double getValue3D(double x, double y, double z) - { - return Math.abs(refNoise.getValue3D(x, y, z)); - } - -} diff --git a/src/glum/math/ValueNoise.java b/src/glum/math/ValueNoise.java deleted file mode 100644 index 7f1f64f..0000000 --- a/src/glum/math/ValueNoise.java +++ /dev/null @@ -1,422 +0,0 @@ -package glum.math; - -import static java.lang.Math.*; -import java.util.*; - - -public class ValueNoise implements Noise -{ - // Seed key values - protected FractionTransformer myFracTransformer; - protected double amp, freq; - protected int p0, p1, p2, p3, p4; - protected int[] p0Table, p1Table, p2Table; - protected int[] p4Table, p5Table, p6Table, p7Table; - protected int[] p8Table, p9Table, p10Table, p11Table; - - /** - * Constructor - */ - public ValueNoise(long aSeed) - { - p0 = 57; - p1 = 15731; - p2 = 789221; - p3 = 1376312589; - - Vector aList; - Random aRandom; - - // Set up our rand generater - aRandom = new Random(aSeed); - - // Set up the fraction tranform function - myFracTransformer = FractionTransformer.Cubic; - - // Construct the words to use in the permutation - aList = new Vector(); - for (int c1 = 0; c1 < 256; c1++) - aList.add(c1); - - Collections.shuffle(aList, aRandom); - p0Table = new int[256]; - for (int c1 = 0; c1 < 256; c1++) - p0Table[c1] = aList.get(c1) << 0; - - Collections.shuffle(aList, aRandom); - p1Table = new int[256]; - for (int c1 = 0; c1 < 256; c1++) - p1Table[c1] = aList.get(c1) << 8; - - Collections.shuffle(aList, aRandom); - p2Table = new int[256]; - for (int c1 = 0; c1 < 256; c1++) - p2Table[c1] = aList.get(c1) << 16; - - Collections.shuffle(aList, aRandom); - p4Table = new int[256]; - for (int c1 = 0; c1 < 256; c1++) - p4Table[c1] = aList.get(c1) << 0; - - Collections.shuffle(aList, aRandom); - p5Table = new int[256]; - for (int c1 = 0; c1 < 256; c1++) - p5Table[c1] = aList.get(c1) << 8; - - Collections.shuffle(aList, aRandom); - p6Table = new int[256]; - for (int c1 = 0; c1 < 256; c1++) - p6Table[c1] = aList.get(c1) << 16; - - Collections.shuffle(aList, aRandom); - p8Table = new int[256]; - for (int c1 = 0; c1 < 256; c1++) - p8Table[c1] = aList.get(c1) << 0; - - Collections.shuffle(aList, aRandom); - p9Table = new int[256]; - for (int c1 = 0; c1 < 256; c1++) - p9Table[c1] = aList.get(c1) << 8; - - Collections.shuffle(aList, aRandom); - p10Table = new int[256]; - for (int c1 = 0; c1 < 256; c1++) - p10Table[c1] = aList.get(c1) << 16; - -/* -int x = 0xF0F0F0F0; -System.out.print(" x[0]:" + ((x & 0x000000ff) >> 0) ); -System.out.print(" x[1]:" + ((x & 0x0000ff00) >> 8) ); -System.out.print(" x[2]:" + ((x & 0x00ff0000) >> 16) ); -System.out.println(" x[3]:" + ((x & 0xff000000) >> 24) ); - -System.out.println("Hmm..."); -System.out.print(" x[0]:" + ((x >> 0) & 0xff) ); -System.out.print(" x[1]:" + ((x >> 8) & 0xff) ); -System.out.print(" x[2]:" + ((x >> 16) & 0xff) ); -System.out.println(" x[3]:" + ((x >> 24) & 0xff) ); -System.exit(0); -*/ - } - - public ValueNoise(int aP0, int aP1, int aP2, int aP3) - { - this(0); - - amp = 1.0; - freq = 1.0; - p0 = aP0; - p1 = aP1; - p2 = aP2; - p3 = aP3; - } - - - /** - * setPersistence - Set the amplitude and frequency associated - * with the noise function. - */ - public void setPersistence(double aAmp, double aFreq) - { - amp = aAmp; - freq = aFreq; - } - - - @Override - public double getAmplitude() - { - return amp; - } - - - @Override - public double getValue1D(double x) - { - return getInterpolatedValue1D(x * freq) * amp; - } - - - public double getInterpolatedValue1D(double x) - { - int iX; - double fracX; - double left, right; - - iX = (int)floor(x); - fracX = x - iX; - fracX = myFracTransformer.transform(fracX); - - left = getSmoothValue1D(iX); - right = getSmoothValue1D(iX + 1); - - return interpolate(left, right, fracX); - } - - - public double getSmoothValue1D(int x) - { - double corners, center; - - corners = (getRawValue1D(x-1) + getRawValue1D(x+1)) / 2; - center = getRawValue1D(x) / 2; - return corners + center; - } - - - public double getRawValue1D(int x) - { - int vx; -// int w1, w2, w3, w4; - int w1, w2, w3; - -/* long aVal; - - aVal = x + (y * p0); - aVal = (aVal <<13) ^ aVal; - - return (1.0 - ((aVal * (aVal * aVal * p1 + p2) + p3) & 0x7fffffff) / 1073741824.0); -*/ - - vx = x; - w1 = (vx >> 0) & 0xFF; - w2 = (vx >> 8) & 0xFF; - w3 = (vx >> 16) & 0xFF; -// w4 = (vx >> 24) & 0xFF; - w2 = w2 + w1; - w3 = w3 + w2; -// w4 = w4 + w3; - vx = (p0Table[w1 & 0xff] ) | (p1Table[w2 & 0xff] ) | (p2Table[w3 & 0xff] ); - - // Return a value from -1 to 1. The value of vy is in the range: [0, 2^24 - 1] - return 1.0 - (vx/8388607.5); - } - - - - - - - - - - - @Override - public double getValue2D(double x, double y) - { - return getInterpolatedValue2D(x * freq, y * freq) * amp; - } - - - public double getInterpolatedValue2D(double x, double y) - { - int iX, iY; - double fracX, fracY; - double tL, tR, bL, bR, topRow, botRow; - - iX = (int)floor(x); - fracX = x - iX; - fracX = myFracTransformer.transform(fracX); - - iY = (int)floor(y); - fracY = y - iY; - fracY = myFracTransformer.transform(fracY); - - tL = getSmoothValue2D(iX, iY); - tR = getSmoothValue2D(iX + 1, iY); - bL = getSmoothValue2D(iX, iY + 1); - bR = getSmoothValue2D(iX + 1, iY + 1); - - topRow = interpolate(tL, tR, fracX); - botRow = interpolate(bL, bR, fracX); - - return interpolate(topRow, botRow, fracY); - } - - - public double getSmoothValue2D(int x, int y) - { -return getRawValue2D(x, y); -/* double corners, sides, center; - - corners = (getRawValue2D(x-1, y-1) + getRawValue2D(x+1, y-1) + getRawValue2D(x-1, y+1) + getRawValue2D(x+1, y+1)) / 16; - sides = (getRawValue2D(x-1, y) + getRawValue2D(x+1, y) + getRawValue2D(x, y-1) + getRawValue2D(x, y+1)) / 8; - center = getRawValue2D(x, y) / 4; - - return corners + sides + center; -*/ } - - - public double getRawValue2D(int x, int y) - { -// int vx, vy; -// int w1, w2, w3, w4; - - long aVal; - aVal = x + (y * p0); - aVal = (aVal <<13) ^ aVal; - - return (1.0 - ((aVal * (aVal * aVal * p1 + p2) + p3) & 0x7fffffff) / 1073741824.0); - - -// vx = x; -// w1 = (vx >> 0) & 0xFF; -// w2 = (vx >> 8) & 0xFF; -// w3 = (vx >> 16) & 0xFF; -//// w4 = (vx >> 24) & 0xFF; -// w2 = w2 + w1; -// w3 = w3 + w2; -//// w4 = w4 + w3; -// vx = (p0Table[w1 & 0xff] ) | (p1Table[w2 & 0xff] ) | (p2Table[w3 & 0xff] ); -// -// vy = vx + y; -// w1 = (vy >> 0) & 0xFF; -// w2 = (vy >> 8) & 0xFF; -// w3 = (vy >> 16) & 0xFF; -//// w4 = (vy >> 24) & 0xFF; -// w2 = w2 + w1; -// w3 = w3 + w2; -//// w4 = w4 + w3; -// vy = (p4Table[w1 & 0xff] ) | (p5Table[w2 & 0xff] ) | (p6Table[w3 & 0xff] ); -// -// // Return a value from -1 to 1. The value of vy is in the range: [0, 2^24 - 1] -// return 1.0 - (vy/8388607.5); - } - - - - - - - - - - - - - - @Override - public double getValue3D(double x, double y, double z) - { - return getInterpolatedValue3D(x * freq, y * freq, z * freq) * amp; - } - - - public double getInterpolatedValue3D(double x, double y, double z) - { - int iX, iY, iZ; - double fracX, fracY, fracZ; - double tL, tR, bL, bR, topRow, botRow; - double planeZ1, planeZ2; - - iX = (int)floor(x); - fracX = x - iX; - fracX = myFracTransformer.transform(fracX); - - iY = (int)floor(y); - fracY = y - iY; - fracY = myFracTransformer.transform(fracY); - - iZ = (int)floor(z); - fracZ = z - iZ; - fracZ = myFracTransformer.transform(fracZ); - - tL = getSmoothValue3D(iX, iY, iZ); - tR = getSmoothValue3D(iX + 1, iY, iZ); - bL = getSmoothValue3D(iX, iY + 1, iZ); - bR = getSmoothValue3D(iX + 1, iY + 1, iZ); - topRow = interpolate(tL, tR, fracX); - botRow = interpolate(bL, bR, fracX); - planeZ1 = interpolate(topRow, botRow, fracY); - - tL = getSmoothValue3D(iX, iY, iZ + 1); - tR = getSmoothValue3D(iX + 1, iY, iZ + 1); - bL = getSmoothValue3D(iX, iY + 1, iZ + 1); - bR = getSmoothValue3D(iX + 1, iY + 1, iZ + 1); - topRow = interpolate(tL, tR, fracX); - botRow = interpolate(bL, bR, fracX); - planeZ2 = interpolate(topRow, botRow, fracY); - - return interpolate(planeZ1, planeZ2, fracZ); - } - - - public double getSmoothValue3D(int x, int y, int z) - { -return getRawValue3D(x, y, z); -/* double center, surface, side, corner; - - center = getRawValue3D(x, y, z); - center = center / 8; - - surface = getRawValue3D(x - 1, y, z) + getRawValue3D(x + 1, y, z) - + getRawValue3D(x, y - 1, z) + getRawValue3D(x, y + 1, z) - + getRawValue3D(x, y, z - 1) + getRawValue3D(x, y, z + 1); - surface = surface / 16; - - side = getRawValue3D(x, y-1, z-1) + getRawValue3D(x, y-1, z+1) + getRawValue3D(x, y+1, z-1) + getRawValue3D(x, y+1, z+1) - + getRawValue3D(x-1, y, z-1) + getRawValue3D(x-1, y, z+1) + getRawValue3D(x+1, y, z-1) + getRawValue3D(x+1, y, z+1) - + getRawValue3D(x-1, y-1, z) + getRawValue3D(x-1, y+1, z) + getRawValue3D(x+1, y-1, z) + getRawValue3D(x+1, y+1, z); - side = side / 32; - - corner = getRawValue3D(x-1, y-1, z-1) + getRawValue3D(x+1, y-1, z-1) - + getRawValue3D(x-1, y+1, z-1) + getRawValue3D(x+1, y+1, z-1) - + getRawValue3D(x-1, y-1, z+1) + getRawValue3D(x+1, y-1, z+1) - + getRawValue3D(x-1, y+1, z+1) + getRawValue3D(x+1, y+1, z+1); - corner = corner / 64; - - return center + surface + side + corner; -*/ } - - - public double getRawValue3D(int x, int y, int z) - { - int vx, vy, vz; -// int w1, w2, w3, w4; - int w1, w2, w3; - - vx = x; - w1 = (vx >> 0) & 0xFF; - w2 = (vx >> 8) & 0xFF; - w3 = (vx >> 16) & 0xFF; -// w4 = (vx >> 24) & 0xFF; - w2 = w2 + w1; - w3 = w3 + w2; -// w4 = w4 + w3; - vx = (p0Table[w1 & 0xff] ) | (p1Table[w2 & 0xff] ) | (p2Table[w3 & 0xff] ); - - vy = vx + y; - w1 = (vy >> 0) & 0xFF; - w2 = (vy >> 8) & 0xFF; - w3 = (vy >> 16) & 0xFF; -// w4 = (vy >> 24) & 0xFF; - w2 = w2 + w1; - w3 = w3 + w2; -// w4 = w4 + w3; - vy = (p4Table[w1 & 0xff] ) | (p5Table[w2 & 0xff] ) | (p6Table[w3 & 0xff] ); - - vz = vy + z; - w1 = (vz >> 0) & 0xFF; - w2 = (vz >> 8) & 0xFF; - w3 = (vz >> 16) & 0xFF; -// w4 = (vz >> 24) & 0xFF; - w2 = w2 + w1; - w3 = w3 + w2; -// w4 = w4 + w3; - vz = (p8Table[w1 & 0xff] ) | (p9Table[w2 & 0xff] ) | (p10Table[w3 & 0xff] ); - - // Return a value from -1 to 1. The value of vy is in the range: [0, 2^24 - 1] - return 1.0 - (vz/8388607.5); - } - - - /** - * interpolate - Returns a value between v1 and v2 wrt to frac. - */ - protected double interpolate(double v1, double v2, double frac) - { - return v1 + frac*(v2 - v1); - } - -} - diff --git a/src/glum/math/ViolentNoise1.java.misc b/src/glum/math/ViolentNoise1.java.misc deleted file mode 100644 index 8b3f2a3..0000000 --- a/src/glum/math/ViolentNoise1.java.misc +++ /dev/null @@ -1,79 +0,0 @@ -package glum.math; - -import java.util.*; - -public class ViolentNoise1 implements Noise -{ - // Seed key values - protected Collection myNoiseList; - - - /** - * Constructor - */ - public ViolentNoise1() - { - myNoiseList = new LinkedList(); - } - - - /** - * add - */ - public void add(Noise aNoise) - { - myNoiseList.add(aNoise); - } - - - /** - * getValue1D - Returns a continous random value in the range of [0-1] in - * 1D space. It is repeatable for the same x values. - */ - public double getValue1D(double x) - { - double total; - - total = 0; - for (Noise aNoiseGen : myNoiseList) - total += aNoiseGen.getValue1D(x); - - total = Math.sin(x + total); - return total; - } - - - /** - * getValue2D - Returns a continous random value in the range of [0-1] in - * 2D space. It is repeatable for the same x,y values. - */ - public double getValue2D(double x, double y) - { - double total; - - total = 0; - for (Noise aNoiseGen : myNoiseList) - total += aNoiseGen.getValue2D(x, y); - - total = Math.sin(x + total); - return total; - } - - - /** - * getValue3D - Returns a continous random value in the range of [0-1] in - * 3D space. It is repeatable for the same x,y,z values. - */ - public double getValue3D(double x, double y, double z) - { - double total; - - total = 0; - for (Noise aNoiseGen : myNoiseList) - total += aNoiseGen.getValue3D(x, y, z); - - total = Math.sin(x + total); - return total; - } - -} diff --git a/src/glum/math/ViolentNoise2.java.misc b/src/glum/math/ViolentNoise2.java.misc deleted file mode 100644 index a2c75ec..0000000 --- a/src/glum/math/ViolentNoise2.java.misc +++ /dev/null @@ -1,79 +0,0 @@ -package glum.math; - -import java.util.*; - -public class ViolentNoise2 implements Noise -{ - // Seed key values - protected Collection myNoiseList; - - - /** - * Constructor - */ - public ViolentNoise2() - { - myNoiseList = new LinkedList(); - } - - - /** - * add - */ - public void add(Noise aNoise) - { - myNoiseList.add(aNoise); - } - - - /** - * getValue1D - Returns a continous random value in the range of [0-1] in - * 1D space. It is repeatable for the same x values. - */ - public double getValue1D(double x) - { - double total; - - total = 0; - for (Noise aNoiseGen : myNoiseList) - total += Math.abs(aNoiseGen.getValue1D(x)); - - total = Math.sin(x + total); - return total; - } - - - /** - * getValue2D - Returns a continous random value in the range of [0-1] in - * 2D space. It is repeatable for the same x,y values. - */ - public double getValue2D(double x, double y) - { - double total; - - total = 0; - for (Noise aNoiseGen : myNoiseList) - total += Math.abs(aNoiseGen.getValue2D(x, y)); - - total = Math.sin(x + total); - return total; - } - - - /** - * getValue3D - Returns a continous random value in the range of [0-1] in - * 3D space. It is repeatable for the same x,y,z values. - */ - public double getValue3D(double x, double y, double z) - { - double total; - - total = 0; - for (Noise aNoiseGen : myNoiseList) - total += Math.abs(aNoiseGen.getValue3D(x, y, z)); - - total = Math.tan(x + total); - return total; - } - -} diff --git a/src/glum/misc/InitListener.java b/src/glum/misc/InitListener.java new file mode 100644 index 0000000..0f6044b --- /dev/null +++ b/src/glum/misc/InitListener.java @@ -0,0 +1,31 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.misc; + +/** + * Listener that provides notification whenever a source has completed initialization. + * + * @author lopeznr1 + */ +public interface InitListener +{ + + /** + * Method that is called whenever the specified source has been initialized. + * + * @param aSource + */ + public void handleInitAction(Object aSource); + +} diff --git a/src/glum/net/Credential.java b/src/glum/net/Credential.java index 1e6d94d..b433a30 100644 --- a/src/glum/net/Credential.java +++ b/src/glum/net/Credential.java @@ -1,5 +1,23 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.net; +/** + * Defines credentials needed to access a resource. + * + * @author lopeznr1 + */ public class Credential { // Constants @@ -11,12 +29,14 @@ public class Credential protected final char[] password; /** + * Standard Constructor + *

    * Creates a domainless credential. - * + * * @param aUsername * @param aPassword - * the password to utilize. Note: a reference to this memory is retained by design to minimize distribution - * of the password content through the application. + * the password to utilize. Note: a reference to this memory is retained by design to minimize distribution of + * the password content through the application. */ public Credential(String aUsername, char[] aPassword) { @@ -43,7 +63,7 @@ public class Credential /** * Returns the password character array. - * + * * @return a reference to the internal array held by the instance. */ public char[] getPassword() diff --git a/src/glum/net/FetchError.java b/src/glum/net/FetchError.java new file mode 100644 index 0000000..8d44561 --- /dev/null +++ b/src/glum/net/FetchError.java @@ -0,0 +1,47 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.net; + +/** + * Custom {@link Exception} that is thrown whenever a download fails. + * + * @author lopeznr1 + * + */ +public class FetchError extends Exception +{ + // Attributes + private final Result refResult; + + /** + * Standard Constructor + */ + public FetchError(Throwable aCause, Result aResult) + { + super(aCause); + + refResult = aResult; + } + + /** + * Returns the {@link Result} + * + * @return + */ + public Result getResult() + { + return refResult; + } + +} diff --git a/src/glum/net/HostUtil.java b/src/glum/net/HostUtil.java new file mode 100644 index 0000000..7b91d50 --- /dev/null +++ b/src/glum/net/HostUtil.java @@ -0,0 +1,144 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.net; + +import java.io.InputStream; +import java.io.InputStreamReader; + +import com.google.common.base.Charsets; +import com.google.common.io.CharStreams; + +/** + * Collection of utility methods to support determining the system's host name. + * + * @author lopeznr1 + */ +public class HostUtil +{ + // Constants + /** + * Regex for host name. Source: + * https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address + */ + private static final String ValidHostnameRegex = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$"; + + // Cache vars (static) + private static String cHostName = null; + + /** + * Returns the system host name as specified via the following: + *

      + *
    • First try the relevant system environment variable. + *
    • Query the system via the hostname command + *
    + * Returns null on failure. + *

    + * Source: https://stackoverflow.com/questions/7348711/recommended-way-to-get-hostname-in-java + * + * @param aAllowCache + * If set to true, then the cached version will be utilized (if available) otherwise a system call may be + * required. + */ + public static String getHostName(boolean aAllowCache) + { + String hostName = cHostName; + if (aAllowCache == true && hostName != null) + return hostName; + + String osName = System.getProperty("os.name").toLowerCase(); + if (osName.contains("win")) + { + hostName = System.getenv("COMPUTERNAME"); + if (hostName == null) + hostName = execReadToString("hostname"); + } + else if (osName.contains("nix") || osName.contains("nux") || osName.contains("mac os x")) + { + hostName = System.getenv("HOSTNAME"); + if (hostName == null) + hostName = execReadToString("hostname"); + if (hostName == null) + hostName = execReadToString("cat /etc/hostname"); + } + + // Remove extra whitespace + if (hostName != null) + hostName = hostName.trim(); + + // Transform empty string to null + if (hostName == null || hostName.isEmpty() == true) + hostName = null; + + // Update the cache + cHostName = hostName; + + return hostName; + } + + /** + * Returns the system host name as specified via the following: + *

      + *
    • First try the relevant system environment variable. + *
    • Query the system via the hostname command + *
    + * Returns null on failure. + *

    + * Note that this method will return cached values. + */ + public static String getHostName() + { + // Delegate + return getHostName(true); + } + + /** + * Utility method that returns true if the specified host name is valid. + */ + public static boolean isValidHostName(String aName) + { + if (aName == null) + return false; + + return aName.matches(ValidHostnameRegex); + } + + /** + * Return the string corresponding to the executed command. + *

    + * On success returns the corresponding string with whitespace stripped. + *

    + * Any failure will result in null. + */ + private static String execReadToString(String execCommand) + { + try + { + Process tmpProcess = Runtime.getRuntime().exec(execCommand); + try (InputStream aStream = tmpProcess.getInputStream()) + { + String result = CharStreams.toString(new InputStreamReader(aStream, Charsets.UTF_8)); + return result.trim(); + } + catch (Exception aExp) + { + return null; + } + } + catch (Exception aExp) + { + return null; + } + } + +} diff --git a/src/glum/net/NetUtil.java b/src/glum/net/NetUtil.java index 51ef553..852293f 100644 --- a/src/glum/net/NetUtil.java +++ b/src/glum/net/NetUtil.java @@ -1,17 +1,250 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.net; -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.ConnectException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLConnection; -import java.net.UnknownHostException; +import java.io.*; +import java.net.*; +import java.security.DigestInputStream; +import java.security.MessageDigest; import java.util.Base64; +import glum.digest.Digest; +import glum.digest.DigestUtils; +import glum.io.IoUtil; +import glum.task.ConsoleTask; +import glum.task.Task; +import glum.unit.NumberUnit; + +/** + * Collection of utility methods for working with remote resources. + * + * @author lopeznr1 + */ public class NetUtil { + /** + * Utility method to download the specified file from aSrcUrl to aDestFile. Returns true on success. + *

    + * Note the passed in {@link Task}'s progress will be updated from 0% to 100% at file download completion, If the + * specified parameter aFileSize is invalid (aFileSize <= 0) or the download turns out to be bigger than the + * specified size then there will be no progress update while the file is being downloaded - only at completion. + *

    + * This method can be aborted via {@link Task#abort()}. + *

    + *

    + * Returns true if the download is successfully completed. Returns false if the provided task is aborted. + */ + public static boolean download(Task aTask, URL aSrcUrl, File aDstFile, Credential aCredential, long aFileSize, + MessageDigest aDigest, String aUpdateMsg) throws FetchError + { + + NumberUnit perNU = new NumberUnit("", "", 1.0, "0.00 %"); + + // Ensure we have a valid aTask + if (aTask == null) + aTask = new ConsoleTask(); + + // Allocate space for the byte buffer + byte[] byteArr = new byte[10000]; + + // Determine if the file download should be "resumed" + long cntByteCurr = 0L; + if (aDstFile.exists() == true) + cntByteCurr = aDstFile.length(); + + // Perform the actual copying + InputStream inStream = null; + OutputStream outStream = null; + URLConnection connection = null; + try + { + // Open the src stream (with a 30 sec connect timeout) + connection = aSrcUrl.openConnection(); + if (cntByteCurr > 0) + connection.setRequestProperty("Range", "bytes=" + cntByteCurr + "-"); +// connection.setConnectTimeout(30 * 1000); +// connection.setReadTimeout(90 * 1000); + + // Open the InputStream + inStream = NetUtil.getInputStream(connection, aCredential); + if (aDigest != null) + inStream = new DigestInputStream(inStream, aDigest); + + // Open the OutputStream + boolean isAppend = false; + if (cntByteCurr > 0) + isAppend = true; + outStream = new FileOutputStream(aDstFile, isAppend); + + // Copy the bytes from the instream to the outstream + long cntByteFull = aFileSize; + int numBytes = 0; + while (numBytes != -1) + { + numBytes = inStream.read(byteArr); + if (numBytes > 0) + { + outStream.write(byteArr, 0, numBytes); + cntByteCurr += numBytes; + } + + // Update the progressVal to reflect the download progress. Note however that we do update the + // progress to 100% since that would change the task to be flagged as inactive and thus cause + // the download to be aborted prematurely. + // TODO: In the future Tasks should not be marked as inactive based on progress values + double progressVal = 0; + if (cntByteFull > 0) + { + progressVal = (cntByteCurr + 0.0) / cntByteFull; + if (progressVal >= 1.0) + progressVal = 0.99; + } + + if (aUpdateMsg != null) + aTask.logRegUpdate(aUpdateMsg + perNU.getString(progressVal) + "\n"); + aTask.setProgress(progressVal); + + // Bail if the Task has been aborted + if (aTask.isAborted() == true) + return false; + } + + // Mark aTask's progress as complete since the file was downloaded. + if (aUpdateMsg != null) + aTask.logRegUpdate(aUpdateMsg + perNU.getString(1.0) + "\n"); + aTask.setProgress(1.0); + } + catch (IOException aExp) + { + Result tmpResult = getResult(aExp, connection); + throw new FetchError(aExp, tmpResult); + } + finally + { + IoUtil.forceClose(inStream); + IoUtil.forceClose(outStream); + } + + return true; + } + + /** + * Utility method to download the specified file from aSrcUrl to aDestFile. Returns true on success. + *

    + * Note the passed in {@link Task}'s progress will be updated from 0% to 100% at file download completion, If the + * specified parameter aFileSize is invalid (aFileSize <= 0) or the download turns out to be bigger than the + * specified size then there will be no progress update while the file is being downloaded - only at completion. + *

    + * On any error logging will be sent to the provided Task. + *

    + * Validation of the download will be performed if a valid {@link Digest} (aTargDigest) is provided. + *

    + * This method can be aborted via {@link Task#abort()}. + *

    + * Returns true if the download is successfully completed. Returns false if the provided task is aborted. + */ + public static boolean download(Task aTask, URL aSrcUrl, File aDstFile, Credential aCredential, long aFileLen, + Digest aTargDigest) + { + // Form the message digest of interest + MessageDigest tmpMessageDigest = null; + if (aTargDigest != null) + tmpMessageDigest = DigestUtils.getDigest(aTargDigest.getType()); + + try + { + NetUtil.download(aTask, aSrcUrl, aDstFile, aCredential, aFileLen, tmpMessageDigest, null); + if (aTask.isAborted() == true) + { + aTask.logRegln("File download has been aborted..."); + aTask.logRegln("\tSource: " + aSrcUrl); + aTask.logRegln("\tFile: " + aDstFile + "\n"); +// aTask.logRegln("\tFile: " + dstFile + " Bytes transferred: " + cntByteCurr); + return false; + } + } + catch (FetchError aExp) + { + aTask.logRegln("File download has failed..."); + aTask.logRegln("\tReason: " + aExp.getResult()); + aTask.logRegln("\tSource: " + aSrcUrl); + aTask.logRegln("\tFile: " + aDstFile + "\n"); + return false; + } + + // Success if there is no targDigest to validate against + if (aTargDigest == null) + return true; + + // Validate that the file was downloaded successfully + Digest testDigest = new Digest(aTargDigest.getType(), tmpMessageDigest.digest()); + if (aTargDigest.equals(testDigest) == false) + { + aTask.logRegln("File download is corrupted..."); + aTask.logRegln("\tFile: " + aDstFile); + aTask.logRegln("\t\tExpected " + aTargDigest.getDescr()); + aTask.logRegln("\t\tReceived " + testDigest.getDescr() + "\n"); + return false; + } + + return true; + } + + /** + * Utility method that converts an IOException to an understandable message + */ + public static String getErrorCodeMessage(URL aUpdateUrl, URLConnection aConnection, IOException aExp) + { + // Form a user friendly exception + String errMsg; + errMsg = "The update site, " + aUpdateUrl + ", is not available.\n\t"; + + Result result; + result = NetUtil.getResult(aExp, aConnection); + switch (result) + { + case BadCredentials: + errMsg += "The update site is password protected and bad credentials were provided.\n"; + break; + + case ConnectFailure: + case UnreachableHost: + case UnsupportedConnection: + errMsg += "The update site appears to be unreachable.\n"; + break; + + case Interrupted: + errMsg += "The retrival of the remote file has been interrupted.\n"; + break; + + case InvalidResource: + errMsg += "The remote file does not appear to be valid.\n"; + break; + + default: + errMsg += "An undefined error occurred while retrieving the remote file.\n"; + break; + } + + // Log the URL which we failed on + URL fetchUrl; + fetchUrl = aConnection.getURL(); + errMsg += "\tURL: " + fetchUrl + "\n"; + + return errMsg; + } + /** * Utility method for retrieving the response code */ @@ -21,7 +254,7 @@ public class NetUtil { return aConnection.getResponseCode(); } - catch(IOException aExp) + catch (IOException aExp) { return -1; } @@ -65,7 +298,7 @@ public class NetUtil // See if there was a problem with the HTTP Connection if (aConnection instanceof HttpURLConnection) { - int responseCode = getResponseCode((HttpURLConnection)aConnection); + int responseCode = getResponseCode((HttpURLConnection) aConnection); switch (responseCode) { case HttpURLConnection.HTTP_UNAUTHORIZED: @@ -104,7 +337,7 @@ public class NetUtil /** * Checks to see whether the supplied aCredential is valid for the specified root URI. - * + * * @return The result of testing the credentials. */ public static Result checkCredentials(String uriRoot, Credential aCredential) @@ -129,7 +362,7 @@ public class NetUtil inStream.close(); return Result.Success; } - catch(Exception aExp) + catch (Exception aExp) { // aExp.printStackTrace(); return getResult(aExp, aConnection); diff --git a/src/glum/net/Result.java b/src/glum/net/Result.java index c59da4e..128c8c3 100644 --- a/src/glum/net/Result.java +++ b/src/glum/net/Result.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.net; public enum Result diff --git a/src/glum/net/UrlUtil.java b/src/glum/net/UrlUtil.java new file mode 100644 index 0000000..7b6bcf5 --- /dev/null +++ b/src/glum/net/UrlUtil.java @@ -0,0 +1,123 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.net; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +import com.google.common.io.ByteStreams; + +import glum.task.Task; +import glum.util.ThreadUtil; + +/** + * Collection of utility methods for working with {@link URL}s. + * + * @author lopeznr1 + */ +public class UrlUtil +{ + /** + * utility method that returns a the textual content corresponding to the resource at the specified URL. + *

    + * Returns null on failure, and logs the exception to aTask. + */ + public static String readText(Task aTask, URL aUrl) + { + try (InputStream aStream = aUrl.openStream()) + { + // Utilize Java 9 Stream read facility +// return new String(aStream.readAllBytes(), StandardCharsets.UTF_8); + byte[] tmpByteArr = ByteStreams.toByteArray(aStream); + return new String(tmpByteArr, StandardCharsets.UTF_8); + } + catch (IOException aExp) + { + aTask.logRegln(ThreadUtil.getStackTrace(aExp)); + } + + return null; + } + + /** + * Transforms a string representation of url to a {@link URL}. + *

    + * On any error a wrapped {@link RuntimeException} will be thrown. + */ + public static URL toUrl(String aUrlStr) + { + try + { + return new URL(aUrlStr); + } + catch (Exception aExp) + { + throw new RuntimeException(aExp); + } + } + + /** + * Transforms a {@link URI} to a {@link URL}. + *

    + * On any error a wrapped {@link RuntimeException} will be thrown. + */ + public static URL toUrl(URI aURI) + { + try + { + return aURI.toURL(); + } + catch (Exception aExp) + { + throw new RuntimeException(aExp); + } + } + + /** + * Transforms and resolves a list of strings into a URL. + *

    + * The first string should be absolute and reference some top level folder. All intermediate strings will be + * interpreted as relative folders. Resolving is done via {@link URI#resolve(String)} + *

    + * Minimal error checking is done so it is the responsibility of the caller to ensure input is valid. Wrapped + * {@link RuntimeException}s will be thrown. + */ + public static URL resolve(String... aStrArr) + { + String tmpStr = aStrArr[0]; + if (tmpStr.endsWith("/") == false) + tmpStr += "/"; + URI tmpURI = URI.create(tmpStr); + + for (int c1 = 0; c1 < aStrArr.length - 1; c1++) + { + tmpStr = aStrArr[c1]; + if (tmpStr.endsWith("/") == false) + tmpStr = tmpStr += '/'; + + tmpURI = tmpURI.resolve(tmpStr); + } + + int lastIdx = aStrArr.length - 1; + if (lastIdx > 0) + tmpURI = tmpURI.resolve(aStrArr[lastIdx]); + + // Delegate + return toUrl(tmpURI); + } + +} diff --git a/src/glum/reflect/Function.java b/src/glum/reflect/Function.java index 31f56fd..1490383 100644 --- a/src/glum/reflect/Function.java +++ b/src/glum/reflect/Function.java @@ -5,9 +5,9 @@ import java.lang.reflect.Method; /** * Class to provide easy access to function and allow them to be manipulated as objects. - *

    - * Source: http://stackoverflow.com/questions/1073358/function-pointers-in-java
    - * Source: http://tutorials.jenkov.com/java-reflection/private-fields-and-methods.html
    + *

    + * Source: http://stackoverflow.com/questions/1073358/function-pointers-in-java
    + * Source: http://tutorials.jenkov.com/java-reflection/private-fields-and-methods.html
    * Source: http://java.sun.com/developer/technicalArticles/ALT/Reflection/ */ public class Function diff --git a/src/glum/reflect/ReflectUtil.java b/src/glum/reflect/ReflectUtil.java index 18cbb80..6ebae3f 100644 --- a/src/glum/reflect/ReflectUtil.java +++ b/src/glum/reflect/ReflectUtil.java @@ -1,66 +1,85 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.reflect; import java.io.File; -import java.lang.reflect.*; -import java.net.URL; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.net.URLDecoder; import javax.lang.model.type.NullType; import com.google.common.primitives.Primitives; +/** + * Collection of reflect related utility methods. + * + * @author lopeznr1 + */ public class ReflectUtil { /** * Utility method to check if an object of type rightType can be assigned to leftType - *

    + *

    * If the leftType is a non primitive then rightType must be null or of a child class. - *

    + *

    * If the leftType is a primitive then this method will only return true if the rightType is a primitive and does not - * need to be cast so that loss of information will occur. I.E:
    - * checkAssignability(long, Int) -> true
    + * need to be cast so that loss of information will occur. I.E:
    + * checkAssignability(long, Int) -> true
    * checkAssignability(int, Long) -> false */ - static boolean checkAssignability(Class leftType, Class rightType) + static boolean checkAssignability(Class aLeftType, Class aRightType) { // Evaluate non primitive assignments - if (leftType.isPrimitive() == false) + if (aLeftType.isPrimitive() == false) { - if (rightType == null || rightType == NullType.class) + if (aRightType == null || aRightType == NullType.class) return true; else - return leftType.isAssignableFrom(rightType); + return aLeftType.isAssignableFrom(aRightType); } // Evaluate primitive assignments - if (rightType == null) + if (aRightType == null) return false; - leftType = Primitives.wrap(leftType); - rightType = Primitives.wrap(rightType); + aLeftType = Primitives.wrap(aLeftType); + aRightType = Primitives.wrap(aRightType); // Bail if same types - if (leftType == rightType) + if (aLeftType == aRightType) return true; - if (leftType == Double.class) + if (aLeftType == Double.class) { - if (rightType == Float.class) + if (aRightType == Float.class) return true; } - else if (leftType == Long.class) + else if (aLeftType == Long.class) { - if (rightType == Integer.class || rightType == Short.class || rightType == Character.TYPE || rightType == Byte.class) + if (aRightType == Integer.class || aRightType == Short.class || aRightType == Character.TYPE + || aRightType == Byte.class) return true; } - else if (leftType == Integer.class) + else if (aLeftType == Integer.class) { - if (rightType == Short.class || rightType == Character.TYPE || rightType == Byte.class) + if (aRightType == Short.class || aRightType == Character.TYPE || aRightType == Byte.class) return true; } - else if (leftType == Short.class) + else if (aLeftType == Short.class) { - if (rightType == Character.TYPE || rightType == Byte.class) + if (aRightType == Character.TYPE || aRightType == Byte.class) return true; } @@ -71,38 +90,32 @@ public class ReflectUtil * Returns the Constructor of aClass with arguments matching parmTypes. On failure it returns null rather than throw * an exception. */ - public static Constructor getConstructorSafe(Class aClass, Class... parmTypes) + public static Constructor getConstructorSafe(Class aClass, Class... aParmTypeArr) { - Constructor aConstructor; - try { - aConstructor = aClass.getConstructor(parmTypes); + return aClass.getConstructor(aParmTypeArr); } - catch(Exception aExp) + catch (Exception aExp) { return null; } - - return aConstructor; } /** * Returns a neatly formatted string corresponding to classArr */ - public static String getStringForClassArray(Class... classArr) + public static String getStringForClassArray(Class... aClassArr) { - String retStr; - - retStr = "["; - for (int c1 = 0; c1 < classArr.length; c1++) + var retStr = "["; + for (int c1 = 0; c1 < aClassArr.length; c1++) { - if (classArr[c1] == null) + if (aClassArr[c1] == null) retStr += "null"; else - retStr += classArr[c1].getName(); + retStr += aClassArr[c1].getName(); - if (c1 + 1 != classArr.length) + if (c1 + 1 != aClassArr.length) retStr += ", "; } retStr += "]"; @@ -110,33 +123,58 @@ public class ReflectUtil } + /** + * Utility method to return the root directory of the specified class. + */ + public static File getInstalledRootDir(Class aClass) + { + File rootDir; + + // Delegate the retrieval of the (schema) root directory + var dataPath = getPartialRootDir(aClass); + + // Remove the jar file component from the path "!*.jar" + var tmpIdx = dataPath.lastIndexOf("!"); + if (tmpIdx != -1) + { + dataPath = dataPath.substring(0, tmpIdx); + rootDir = new File(dataPath); + rootDir = rootDir.getParentFile(); + } + else + { + System.out.println("Warning: " + aClass.getSimpleName() + + " class does not appear to be packaged. Assuming developers environment."); + rootDir = new File(dataPath); + rootDir = rootDir.getParentFile().getParentFile().getParentFile(); + } + + return rootDir; + } + /** * Attempts to return an object of type G1 from the fully qualified path */ - public static G1 loadObject(String aFullClassPath, Class retType) + public static G1 loadObject(String aFullClassPath, Class aRetType) { - Class aClass; - Object aObject; - // Insanity check if (aFullClassPath == null) return null; - aObject = null; try { - aClass = Class.forName(aFullClassPath); - if (retType.isAssignableFrom(aClass) == false) + var tmpClass = Class.forName(aFullClassPath); + if (aRetType.isAssignableFrom(tmpClass) == false) return null; - aObject = aClass.getDeclaredConstructor().newInstance(); - return retType.cast(aObject); + var tmpObject = tmpClass.getDeclaredConstructor().newInstance(); + return aRetType.cast(tmpObject); } - catch(ClassNotFoundException aExp) + catch (ClassNotFoundException aExp) { System.out.println("Failure: " + aFullClassPath + " not found."); } - catch(Exception aExp) + catch (Exception aExp) { // Unknown Exception aExp.printStackTrace(); @@ -149,37 +187,33 @@ public class ReflectUtil * Attempts to return an object of type G1 from the fully qualified path. The object will be constructed using the * vars parmTypes and parmValues. */ - public static G1 loadObject(String aFullClassPath, Class retType, Class[] parmTypes, Object[] parmValues) + public static G1 loadObject(String aFullClassPath, Class aRetType, Class[] aParmTypeArr, + Object[] aParmValueArr) { - Class aClass; - Constructor aConstructor; - Object aObject; - // Insanity check if (aFullClassPath == null) return null; - aObject = null; try { - aClass = Class.forName(aFullClassPath); - if (retType.isAssignableFrom(aClass) == false) + var tmpClass = Class.forName(aFullClassPath); + if (aRetType.isAssignableFrom(tmpClass) == false) return null; // Try to obtain the preferred constructor - aConstructor = aClass.getConstructor(parmTypes); - if (aConstructor == null) + var tmpConstructor = tmpClass.getConstructor(aParmTypeArr); + if (tmpConstructor == null) return null; // Construct the object - aObject = aConstructor.newInstance(parmValues); - return retType.cast(aObject); + var tmpObject = tmpConstructor.newInstance(aParmValueArr); + return aRetType.cast(tmpObject); } - catch(ClassNotFoundException aExp) + catch (ClassNotFoundException aExp) { System.out.println("Failure: " + aFullClassPath + " not found."); } - catch(Exception aExp) + catch (Exception aExp) { // Unknown Exception aExp.printStackTrace(); @@ -188,90 +222,26 @@ public class ReflectUtil return null; } - /** - * Utility method to determine where refClass is installed - */ - public static File getInstalledRootDir(Class refClass) - { - String dataPath; - File rootDir; - URL aUrl; - int aIndex; - - // Attempt to determine the default data directory - aUrl = refClass.getResource(refClass.getSimpleName() + ".class"); - // System.out.println("URL:" + aUrl); - try - { - dataPath = aUrl.toURI().toString(); - dataPath = URLDecoder.decode(dataPath, "UTF-8"); - } - catch(Exception aExp) - { - dataPath = aUrl.getPath(); - try - { - dataPath = URLDecoder.decode(dataPath, "UTF-8"); - } - catch(Exception aExp2) - { - ; - } - } - - // Remove the "file:" protocol specification - aIndex = dataPath.indexOf("file:"); - if (aIndex != -1) - dataPath = dataPath.substring(aIndex + 5); - else - System.out.println("Warning: Protocol \"file:\" not found. Run from http???"); - - // Remove the jar file component from the path "!*.jar" - aIndex = dataPath.lastIndexOf("!"); - if (aIndex != -1) - { - dataPath = dataPath.substring(0, aIndex); - rootDir = new File(dataPath); - rootDir = rootDir.getParentFile(); - } - else - { - System.out.println("Warning: " + refClass.getSimpleName() + " class does not appear to be packaged. Assuming developers environment."); - rootDir = new File(dataPath); - rootDir = rootDir.getParentFile().getParentFile().getParentFile(); - } - - return rootDir; - } - /** * Utility method that searches for a matching method from the specified class, refClass. If null is specified as any * of the parameter types, then any non primitive object will be considered a match for that parameter. */ - public static Method locateMatchingMethod(Class refClass, String methodName, Class... methodParamTypeArr) + public static Method locateMatchingMethod(Class aClass, String aMethodName, Class... aMethodParamTypeArr) { - Method[] methodArr; - boolean isMatch; - Class aClass; - // Search all of the available methods - methodArr = refClass.getDeclaredMethods(); + var methodArr = aClass.getDeclaredMethods(); for (Method aItem : methodArr) { // Ensure the name matches - if (methodName.equals(aItem.getName()) == true) + if (aMethodName.equals(aItem.getName()) == true) { - Class[] evalParamTypeArr; - // Ensure the number of arguments matches - evalParamTypeArr = aItem.getParameterTypes(); - if (evalParamTypeArr.length == methodParamTypeArr.length) + var evalParamTypeArr = aItem.getParameterTypes(); + if (evalParamTypeArr.length == aMethodParamTypeArr.length) { - isMatch = true; + var isMatch = true; for (int c1 = 0; c1 < evalParamTypeArr.length; c1++) - { - isMatch &= checkAssignability(evalParamTypeArr[c1], methodParamTypeArr[c1]); - } + isMatch &= checkAssignability(evalParamTypeArr[c1], aMethodParamTypeArr[c1]); // Found a matching method if (isMatch == true) @@ -281,12 +251,70 @@ public class ReflectUtil } // Try the super classes - aClass = refClass.getSuperclass(); - if (aClass != null) - return locateMatchingMethod(aClass, methodName, methodParamTypeArr); + var tmpClass = aClass.getSuperclass(); + if (tmpClass != null) + return locateMatchingMethod(tmpClass, aMethodName, aMethodParamTypeArr); // No method found return null; } + /** + * Utility method to determine if the JVM is running from a developers environment. + *

    + * Pass the class in of the calling code. + */ + public static boolean isDeveloperEnvironment(Class aClass) + { + // Delegate the retrieval of the (schema) root directory + var dataPath = getPartialRootDir(aClass); + + // If there is a jar ("!*.jar") component then we are packaged up + var tmpIdx = dataPath.lastIndexOf("!"); + if (tmpIdx != -1) + return false; + + return true; + } + + /** + * Utility helper method that returns a string representing the the (schema) root directory of the specified class. + *

    + * If the software is packaged into a jar this string will contain the reference to the jar component. + */ + private static String getPartialRootDir(Class aClass) + { + String dataPath; + + // Attempt to determine the default data directory + var tmpUrl = aClass.getResource(aClass.getSimpleName() + ".class"); + // System.out.println("URL:" + aUrl); + try + { + dataPath = tmpUrl.toURI().toString(); + dataPath = URLDecoder.decode(dataPath, "UTF-8"); + } + catch (Exception aExp) + { + dataPath = tmpUrl.getPath(); + try + { + dataPath = URLDecoder.decode(dataPath, "UTF-8"); + } + catch (Exception aExp2) + { + ; + } + } + + // Remove the "file:" protocol specification + var tmpIndex = dataPath.indexOf("file:"); + if (tmpIndex != -1) + dataPath = dataPath.substring(tmpIndex + 5); + else + System.out.println("Warning: Protocol \"file:\" not found. Run from http???"); + + return dataPath; + } + } diff --git a/src/glum/registry/ConfigMap.java b/src/glum/registry/ConfigMap.java index 5e02358..7df9044 100644 --- a/src/glum/registry/ConfigMap.java +++ b/src/glum/registry/ConfigMap.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.registry; import java.util.*; diff --git a/src/glum/registry/Registry.java b/src/glum/registry/Registry.java index 27217c4..0af6ca8 100644 --- a/src/glum/registry/Registry.java +++ b/src/glum/registry/Registry.java @@ -1,31 +1,47 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.registry; import java.util.*; /** * The Registry class allows for various types of entities to be linked together by a common key. Outside operators can: - *

      - *
    • 1. Add items associated with the key - *
    • 2. Retrieve a collection of all items associated with the key - *
    • 3. Register to receive notification of any changes associated with a key - *
    • 4. Send notification with regard to a key - *
    + *
      + *
    • 1. Add items associated with the key + *
    • 2. Retrieve a collection of all items associated with the key + *
    • 3. Register to receive notification of any changes associated with a key + *
    • 4. Send notification with regard to a key + *
    + * + * @author lopeznr1 */ public class Registry { // State var - private Map> mySetMap; - private Map> mySetListeners; - private Map mySingletonMap; - private Map> mySingletonListeners; + private Map> myGroupM; + private Map> myGroupListenerM; + private Map mySingletonM; + private Map> mySingletonListenerM; + /** Standard Constructor */ public Registry() { - mySetMap = new LinkedHashMap>(); - mySetListeners = new LinkedHashMap>(); + myGroupM = new LinkedHashMap>(); + myGroupListenerM = new LinkedHashMap>(); - mySingletonMap = new LinkedHashMap(); - mySingletonListeners = new LinkedHashMap>(); + mySingletonM = new LinkedHashMap(); + mySingletonListenerM = new LinkedHashMap>(); } /** @@ -33,26 +49,26 @@ public class Registry */ public synchronized void dispose() { - if (mySetMap != null) + if (myGroupM != null) { - mySetMap.clear(); - mySetMap = null; + myGroupM.clear(); + myGroupM = null; } - if (mySetListeners != null) + if (myGroupListenerM != null) { - mySetListeners.clear(); - mySetListeners = null; + myGroupListenerM.clear(); + myGroupListenerM = null; } - if (mySingletonMap != null) + if (mySingletonM != null) { - mySingletonMap.clear(); - mySingletonMap = null; + mySingletonM.clear(); + mySingletonM = null; } - if (mySingletonListeners != null) + if (mySingletonListenerM != null) { - mySingletonListeners.clear(); - mySingletonListeners = null; + mySingletonListenerM.clear(); + mySingletonListenerM = null; } } @@ -61,17 +77,17 @@ public class Registry */ public void addAllResourceItems(Object aKey, Collection aList) { - Collection aCollection; + Collection tmpGroupC; - synchronized(this) + synchronized (this) { - aCollection = mySetMap.get(aKey); - if (aCollection == null) + tmpGroupC = myGroupM.get(aKey); + if (tmpGroupC == null) { - aCollection = new LinkedHashSet(); - mySetMap.put(aKey, aCollection); + tmpGroupC = new LinkedHashSet(); + myGroupM.put(aKey, tmpGroupC); } - aCollection.addAll(aList); + tmpGroupC.addAll(aList); } // Notify the listeners @@ -83,17 +99,17 @@ public class Registry */ public void addResourceItem(Object aKey, Object aObject) { - Collection aCollection; + Collection tmpGroupC; - synchronized(this) + synchronized (this) { - aCollection = mySetMap.get(aKey); - if (aCollection == null) + tmpGroupC = myGroupM.get(aKey); + if (tmpGroupC == null) { - aCollection = new LinkedHashSet(); - mySetMap.put(aKey, aCollection); + tmpGroupC = new LinkedHashSet(); + myGroupM.put(aKey, tmpGroupC); } - aCollection.add(aObject); + tmpGroupC.add(aObject); } // Notify the listeners @@ -105,17 +121,17 @@ public class Registry */ public void addResourceListener(Object aKey, ResourceListener aListener) { - Collection aSet; + Collection tmpListenerC; - synchronized(this) + synchronized (this) { - aSet = mySetListeners.get(aKey); - if (aSet == null) + tmpListenerC = myGroupListenerM.get(aKey); + if (tmpListenerC == null) { - aSet = new ArrayList(); - mySetListeners.put(aKey, aSet); + tmpListenerC = new ArrayList(); + myGroupListenerM.put(aKey, tmpListenerC); } - aSet.add(aListener); + tmpListenerC.add(aListener); } } @@ -124,17 +140,17 @@ public class Registry */ public void addSingletonListener(Object aKey, ResourceListener aListener) { - Collection aSet; + Collection tmpListenerC; - synchronized(this) + synchronized (this) { - aSet = mySingletonListeners.get(aKey); - if (aSet == null) + tmpListenerC = mySingletonListenerM.get(aKey); + if (tmpListenerC == null) { - aSet = new ArrayList(); - mySingletonListeners.put(aKey, aSet); + tmpListenerC = new ArrayList(); + mySingletonListenerM.put(aKey, tmpListenerC); } - aSet.add(aListener); + tmpListenerC.add(aListener); } } @@ -143,30 +159,30 @@ public class Registry */ public synchronized List getResourceItems(Object aKey) { - Collection aCollection; + Collection tmpGroupC; - aCollection = mySetMap.get(aKey); - if (aCollection == null) + tmpGroupC = myGroupM.get(aKey); + if (tmpGroupC == null) return new ArrayList(); - return new ArrayList(aCollection); + return new ArrayList(tmpGroupC); } - public synchronized List getResourceItems(Object aKey, Class retType) + public synchronized List getResourceItems(Object aKey, Class aRetType) { - Collection aCollection; + Collection tmpGroupC; List retList; retList = new ArrayList(); - aCollection = mySetMap.get(aKey); - if (aCollection == null) + tmpGroupC = myGroupM.get(aKey); + if (tmpGroupC == null) return retList; - for (Object aObj : aCollection) + for (Object aObj : tmpGroupC) { - if (retType.isInstance(aObj) == true) - retList.add(retType.cast(aObj)); + if (aRetType.isInstance(aObj) == true) + retList.add(aRetType.cast(aObj)); } return retList; @@ -177,27 +193,27 @@ public class Registry */ public synchronized Object getSingleton(Object aKey) { - Object aSingleton; + Object tmpSingleton; // Insanity check if (aKey == null) return null; - aSingleton = mySingletonMap.get(aKey); - return aSingleton; + tmpSingleton = mySingletonM.get(aKey); + return tmpSingleton; } - public synchronized G1 getSingleton(Object aKey, Class retType) + public synchronized G1 getSingleton(Object aKey, Class aRetType) { - Object aSingleton; + Object tmpSingleton; // Insanity check if (aKey == null) return null; - aSingleton = mySingletonMap.get(aKey); - if (retType.isInstance(aSingleton) == true) - return retType.cast(aSingleton); + tmpSingleton = mySingletonM.get(aKey); + if (aRetType.isInstance(tmpSingleton) == true) + return aRetType.cast(tmpSingleton); return null; } @@ -207,18 +223,18 @@ public class Registry */ public void removeAllResourceItems(Object aKey) { - Collection aCollection; + Collection tmpGroupC; // Remove the item - synchronized(this) + synchronized (this) { // Get the associated collection - aCollection = mySetMap.get(aKey); - if (aCollection == null) + tmpGroupC = myGroupM.get(aKey); + if (tmpGroupC == null) return; // Remove all the items from the collection - aCollection.clear(); + tmpGroupC.clear(); } // Notify the listeners @@ -230,18 +246,18 @@ public class Registry */ public void removeResourceItem(Object aKey, Object aObject) { - Collection aCollection; + Collection tmpGroupC; // Remove the item - synchronized(this) + synchronized (this) { // Get the associated collection - aCollection = mySetMap.get(aKey); - if (aCollection == null) + tmpGroupC = myGroupM.get(aKey); + if (tmpGroupC == null) return; // Remove the item from the collection - aCollection.remove(aObject); + tmpGroupC.remove(aObject); } // Notify the listeners @@ -253,18 +269,18 @@ public class Registry */ public void replaceResourceItems(Object aKey, Collection aList) { - Collection aCollection; + Collection tmpGroupC; - synchronized(this) + synchronized (this) { - aCollection = mySetMap.get(aKey); - if (aCollection == null) + tmpGroupC = myGroupM.get(aKey); + if (tmpGroupC == null) { - aCollection = new LinkedHashSet(); - mySetMap.put(aKey, aCollection); + tmpGroupC = new LinkedHashSet(); + myGroupM.put(aKey, tmpGroupC); } - aCollection.clear(); - aCollection.addAll(aList); + tmpGroupC.clear(); + tmpGroupC.addAll(aList); } // Notify the listeners @@ -276,19 +292,19 @@ public class Registry */ public void removeResourceListener(Object aKey, ResourceListener aListener) { - Collection aSet; + Collection tmpListenerC; // Remove the listenr - synchronized(this) + synchronized (this) { - if (mySetListeners == null) + if (myGroupListenerM == null) return; - aSet = mySetListeners.get(aKey); - if (aSet == null) + tmpListenerC = myGroupListenerM.get(aKey); + if (tmpListenerC == null) return; - aSet.remove(aListener); + tmpListenerC.remove(aListener); } } @@ -297,19 +313,19 @@ public class Registry */ public void removeSingletonListener(Object aKey, ResourceListener aListener) { - Collection aSet; + Collection tmpListenerC; // Remove the listenr - synchronized(this) + synchronized (this) { - if (mySingletonListeners == null) + if (mySingletonListenerM == null) return; - aSet = mySingletonListeners.get(aKey); - if (aSet == null) + tmpListenerC = mySingletonListenerM.get(aKey); + if (tmpListenerC == null) return; - aSet.remove(aListener); + tmpListenerC.remove(aListener); } } @@ -318,14 +334,14 @@ public class Registry */ public void setSingleton(Object aKey, Object aObject) { - synchronized(this) + synchronized (this) { // Remove the entry from the hashtable if null if (aObject == null) - mySingletonMap.remove(aKey); + mySingletonM.remove(aKey); // Set in the entry for the corresponding aKey else - mySingletonMap.put(aKey, aObject); + mySingletonM.put(aKey, aObject); } // Notify the listeners @@ -337,20 +353,20 @@ public class Registry */ public void notifyResourceListeners(Object aKey) { - Collection aSet, notifySet; + Collection tmpListenerC; // Get the listeners - synchronized(this) + synchronized (this) { - aSet = mySetListeners.get(aKey); - if (aSet == null) + tmpListenerC = myGroupListenerM.get(aKey); + if (tmpListenerC == null) return; - notifySet = new ArrayList(aSet); + tmpListenerC = new ArrayList(tmpListenerC); } // Send out the notifications - for (ResourceListener aListener : notifySet) + for (ResourceListener aListener : tmpListenerC) aListener.resourceChanged(this, aKey); } @@ -359,20 +375,20 @@ public class Registry */ public void notifySingletonListeners(Object aKey) { - Collection aSet, notifySet; + Collection tmpListenerC; // Get the listeners - synchronized(this) + synchronized (this) { - aSet = mySingletonListeners.get(aKey); - if (aSet == null) + tmpListenerC = mySingletonListenerM.get(aKey); + if (tmpListenerC == null) return; - notifySet = new ArrayList(aSet); + tmpListenerC = new ArrayList(tmpListenerC); } // Send out the notifications - for (ResourceListener aListener : notifySet) + for (ResourceListener aListener : tmpListenerC) aListener.resourceChanged(this, aKey); } diff --git a/src/glum/registry/ResourceListener.java b/src/glum/registry/ResourceListener.java index ac28bdc..9467887 100644 --- a/src/glum/registry/ResourceListener.java +++ b/src/glum/registry/ResourceListener.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.registry; public interface ResourceListener diff --git a/src/glum/registry/SelectionListener.java b/src/glum/registry/SelectionListener.java index 9c8381d..957eb06 100644 --- a/src/glum/registry/SelectionListener.java +++ b/src/glum/registry/SelectionListener.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.registry; public interface SelectionListener diff --git a/src/glum/registry/SelectionManager.java b/src/glum/registry/SelectionManager.java index aa285fc..a494b39 100644 --- a/src/glum/registry/SelectionManager.java +++ b/src/glum/registry/SelectionManager.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.registry; import java.util.*; @@ -8,23 +21,28 @@ import com.google.common.collect.*; /** * Class that support arbitrary item selection. All valid types of selected items must be registered in the Constructor. + * + * @author lopeznr1 */ public class SelectionManager { // State vars - private Multimap, Object> selectionMap; - private Multimap, SelectionListener> listenerMap; - private Set> registerSet; + private Multimap, Object> selectionM; + private Multimap, SelectionListener> listenerM; + private Set> registerS; private boolean notifyViaAwtThread; - public SelectionManager(Class... classArr) + /** + * Standard Constructor + */ + public SelectionManager(Class... aClassArr) { - selectionMap = ArrayListMultimap.create(); - listenerMap = HashMultimap.create(); + selectionM = ArrayListMultimap.create(); + listenerM = HashMultimap.create(); - registerSet = Sets.newHashSet(); - for (Class aClass : classArr) - registerSet.add(aClass); + registerS = new HashSet<>(); + for (Class aClass : aClassArr) + registerS.add(aClass); notifyViaAwtThread = false; } @@ -35,8 +53,8 @@ public class SelectionManager */ public synchronized void addListener(SelectionListener aListener, Class aClass) { - if (registerSet.contains(aClass) == true) - listenerMap.put(aClass, aListener); + if (registerS.contains(aClass) == true) + listenerM.put(aClass, aListener); else throw new RuntimeException("Unregistered selection class: " + aClass); } @@ -54,13 +72,13 @@ public class SelectionManager */ public void addItem(Class aClass, G1 aItem, SelectionListener aSkipListener) { - if (registerSet.contains(aClass) == false) + if (registerS.contains(aClass) == false) throw new RuntimeException("Unregistered selection class: " + aClass); // Replace the old selections with the new item list - synchronized(this) + synchronized (this) { - selectionMap.put(aClass, aItem); + selectionM.put(aClass, aItem); } notifyListeners(aClass, aSkipListener); @@ -69,23 +87,23 @@ public class SelectionManager /** * Add to the list of selected items associated with aClass */ - public void addItems(Class aClass, List aItemList) + public void addItems(Class aClass, List aItemL) { - addItems(aClass, aItemList, null); + addItems(aClass, aItemL, null); } /** * Adds to the list of selected items and notifies all listeners but the specified skipListener */ - public void addItems(Class aClass, List aItemList, SelectionListener aSkipListener) + public void addItems(Class aClass, List aItemL, SelectionListener aSkipListener) { - if (registerSet.contains(aClass) == false) + if (registerS.contains(aClass) == false) throw new RuntimeException("Unregistered selection class: " + aClass); // Replace the old selections with the new item list - synchronized(this) + synchronized (this) { - selectionMap.putAll(aClass, aItemList); + selectionM.putAll(aClass, aItemL); } notifyListeners(aClass, aSkipListener); @@ -97,36 +115,36 @@ public class SelectionManager @SuppressWarnings("unchecked") public synchronized List getSelectedItems(Class aClass) { - if (registerSet.contains(aClass) == false) + if (registerS.contains(aClass) == false) throw new RuntimeException("Unregistered selection class: " + aClass); - return (List)Lists.newArrayList(selectionMap.get(aClass)); + return (List) new ArrayList<>(selectionM.get(aClass)); } /** * Removes from the list of selected items associated with aClass */ - public void removeItems(Class aClass, List aItemList) + public void removeItems(Class aClass, List aItemL) { - removeItems(aClass, aItemList, null); + removeItems(aClass, aItemL, null); } /** * Removes from the list of selected items and notifies all listeners but the specified skipListener */ - public void removeItems(Class aClass, List aItemList, SelectionListener skipListener) + public void removeItems(Class aClass, List aItemL, SelectionListener skipListener) { - if (registerSet.contains(aClass) == false) + if (registerS.contains(aClass) == false) throw new RuntimeException("Unregistered selection class: " + aClass); // Replace the old selections with the new item list - synchronized(this) + synchronized (this) { Set replaceSet; replaceSet = new LinkedHashSet(getSelectedItems(aClass)); - replaceSet.removeAll(aItemList); - selectionMap.replaceValues(aClass, replaceSet); + replaceSet.removeAll(aItemL); + selectionM.replaceValues(aClass, replaceSet); } notifyListeners(aClass, skipListener); @@ -135,23 +153,23 @@ public class SelectionManager /** * Sets in the selected items and notifies all listeners */ - public void setItems(Class aClass, List aItemList) + public void setItems(Class aClass, List aItemL) { - setItems(aClass, aItemList, null); + setItems(aClass, aItemL, null); } /** * Sets in the selected items and notifies all listeners but the specified skipListener */ - public void setItems(Class aClass, List aItemList, SelectionListener aSkipListener) + public void setItems(Class aClass, List aItemL, SelectionListener aSkipListener) { - if (registerSet.contains(aClass) == false) + if (registerS.contains(aClass) == false) throw new RuntimeException("Unregistered selection class: " + aClass); // Replace the old selections with the new item list - synchronized(this) + synchronized (this) { - selectionMap.replaceValues(aClass, aItemList); + selectionM.replaceValues(aClass, aItemL); } notifyListeners(aClass, aSkipListener); @@ -174,19 +192,18 @@ public class SelectionManager // Ensure this logic is always executed on the AWT thread (if notifyViaAwtThread == true) if (notifyViaAwtThread == true && SwingUtilities.isEventDispatchThread() == false) { - Runnable tmpRunnable = () -> notifyListeners(aClass, aSkipListener); - SwingUtilities.invokeLater(tmpRunnable); + SwingUtilities.invokeLater(() -> notifyListeners(aClass, aSkipListener)); return; } - List listenerList; - synchronized(this) + List listenerL; + synchronized (this) { - listenerList = new ArrayList<>(listenerMap.get(aClass)); - listenerList.remove(aSkipListener); + listenerL = new ArrayList<>(listenerM.get(aClass)); + listenerL.remove(aSkipListener); } - for (SelectionListener aListener : listenerList) + for (SelectionListener aListener : listenerL) aListener.selectionChanged(this, aClass); } diff --git a/src/glum/source/LocalSource.java b/src/glum/source/LocalSource.java new file mode 100644 index 0000000..17b1cad --- /dev/null +++ b/src/glum/source/LocalSource.java @@ -0,0 +1,118 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.source; + +import java.io.File; +import java.net.URL; + +/** + * Local (immutable) implementation of {@link Source}. + *

    + * This object supports only 1 attributes: + *

      + *
    • localFile: The local location where the file can be retrieved (or stored). + *
    + * Note that localFile can not be null. + * + * @author lopeznr1 + */ +public class LocalSource implements Source +{ + // Attributes + private final File localFile; + private final boolean isAuthoritative; + + /** Standard constructor. */ + public LocalSource(File aLocalFile, boolean aIsAuthoritative) + { + localFile = aLocalFile; + isAuthoritative = aIsAuthoritative; + + // Insanity check + if (localFile == null) + throw new NullPointerException("Parameter aLocalFile must not be null."); + } + + /** Simplified constructor. */ + public LocalSource(File aLocalFile) + { + this(aLocalFile, true); + } + + @Override + public long getDiskSize() + { + // If flagged as authoritative then trust the (local) file system and use + // the use the size stored there otherwise no way to know if the value is + // authoritative. + if (isAuthoritative == true) + return localFile.length(); + + return -1; + } + + @Override + public String getName() + { + return localFile.getName(); + } + + @Override + public String getPath() + { + return localFile.getPath(); + } + + @Override + public File getLocalFile() + { + return localFile; + } + + @Override + public URL getRemoteUrl() + { + return null; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + LocalSource other = (LocalSource) obj; + if (localFile == null) + { + if (other.localFile != null) + return false; + } + else if (!localFile.equals(other.localFile)) + return false; + return true; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((localFile == null) ? 0 : localFile.hashCode()); + return result; + } + +} diff --git a/src/glum/source/PlainSource.java b/src/glum/source/PlainSource.java new file mode 100644 index 0000000..57cb430 --- /dev/null +++ b/src/glum/source/PlainSource.java @@ -0,0 +1,140 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.source; + +import java.io.File; +import java.net.URL; + +/** + * Simple (immutable) implementation of {@link Source}. + *

    + * This object supports 2 attributes: + *

      + *
    • remoteUrl: The remote location where the resource can be retrieved. + *
    • localFile: The local location where the file can be retrieved (or stored). + *
    + * Note that either the remoteUrl or localFile can be null but not both. If both are specified then the remoteUrl takes + * precedence. + * + * @author lopeznr1 + */ +public class PlainSource implements Source +{ + // Attributes + private final File localFile; + private final URL remoteUrl; + private final long diskSize; + + /** + * Standard constructor. + *

    + * Either the local or remote resource may be null but not both! + */ + public PlainSource(File aLocalFile, URL aRemoteUrl, long aDiskSize) + { + localFile = aLocalFile; + remoteUrl = aRemoteUrl; + diskSize = aDiskSize; + + // Insanity check + if (localFile == null && remoteUrl == null) + throw new NullPointerException("Both aLocalFile and aRemoteUrl may not be null."); + } + + /** + * Simplified Constructor + */ + public PlainSource(File aLocalFile, URL aRemoteUrl) + { + this(aLocalFile, aRemoteUrl, -1); + } + + @Override + public long getDiskSize() + { + return diskSize; + } + + @Override + public String getName() + { + if (remoteUrl != null) + { + String tmpPath = remoteUrl.getPath(); + int tmpIdx = tmpPath.lastIndexOf("/"); + return tmpPath.substring(tmpIdx + 1); + } + + return localFile.getName(); + } + + @Override + public String getPath() + { + if (remoteUrl != null) + return "" + remoteUrl; + + return localFile.getPath(); + } + + @Override + public File getLocalFile() + { + return localFile; + } + + @Override + public URL getRemoteUrl() + { + return remoteUrl; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + PlainSource other = (PlainSource) obj; + if (localFile == null) + { + if (other.localFile != null) + return false; + } + else if (!localFile.equals(other.localFile)) + return false; + if (remoteUrl == null) + { + if (other.remoteUrl != null) + return false; + } + else if (!remoteUrl.equals(other.remoteUrl)) + return false; + return true; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((localFile == null) ? 0 : localFile.hashCode()); + result = prime * result + ((remoteUrl == null) ? 0 : remoteUrl.hashCode()); + return result; + } + +} diff --git a/src/glum/source/Source.java b/src/glum/source/Source.java new file mode 100644 index 0000000..9ed939a --- /dev/null +++ b/src/glum/source/Source.java @@ -0,0 +1,61 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.source; + +import java.io.File; +import java.net.URL; + +/** + * Interface that defines the location of a resource. + *

    + * Implementations of this interface should be immutable. + * + * @author lopeznr1 + */ +public interface Source +{ + /** + * Returns the (expected) disk size (in bytes) of the source. + *

    + * If the size is unknown then -1 should be returned. + */ + public long getDiskSize(); + + /** + * Returns the file name associated with the source. + *

    + * This is just the last element in the path. + */ + public String getName(); + + /** + * Returns the path as a string associated with the source. + */ + public String getPath(); + + /** + * Returns the (local) source. + *

    + * May return null. + */ + public File getLocalFile(); + + /** + * Returns the (remote) source. + *

    + * May return null. + */ + public URL getRemoteUrl(); + +} diff --git a/src/glum/source/SourceState.java b/src/glum/source/SourceState.java new file mode 100644 index 0000000..e4bc705 --- /dev/null +++ b/src/glum/source/SourceState.java @@ -0,0 +1,35 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.source; + +/** + * Enum that defines the current (availability) state of a {@link Source}. + * + * @author lopeznr1 + */ +public enum SourceState +{ + /** Defines the source as being located on a local file system. */ + Local, + + /** Defines the source as being located on a remote system. */ + Remote, + + /** Defines the source as being partially fetched to a local file system. */ + Partial, + + /** Defines the source as being unavailable. */ + Unavailable; + +} diff --git a/src/glum/source/SourceUtil.java b/src/glum/source/SourceUtil.java new file mode 100644 index 0000000..2bf9617 --- /dev/null +++ b/src/glum/source/SourceUtil.java @@ -0,0 +1,277 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.source; + +import java.io.File; +import java.io.IOException; +import java.net.*; + +import glum.net.*; +import glum.task.Task; +import glum.unit.ByteUnit; +import glum.unit.NumberUnit; + +/** + * Collection of utility methods for working with {@link Source} objects. + * + * @author lopeznr1 + */ +public class SourceUtil +{ + /** + * Utility method that will download the contents of the {@link Source}. + *

    + * The contents will be downloaded from the remote location to local file. + * + * @param aTask + * {@link Task} used to monitor or abort the download process. + * @param aSource + * {@link Source} to be downloaded. + * @param aCredential + * User {@link Credential} for the resource. May be null if credentials are not needed. + */ + public static void download(Task aTask, Source aSource, Credential aCredential) throws IOException, FetchError + { + URL remoteUrl = aSource.getRemoteUrl(); + File localFile = aSource.getLocalFile(); + long diskSize = aSource.getDiskSize(); + + // Log the start of the download + File partFile = getTempFile(aSource); + if (partFile.exists() == false) + aTask.logRegln("Downloading file..."); + else + aTask.logRegln("Resuming download..."); + + // Ensure the source has both a local and remote end points + if (localFile == null) + throw new IOException("Local file has not been specified."); + if (remoteUrl == null) + throw new IOException("Remote URL has not been specified."); + + // Ensure the parent folder exists + File parentDir = localFile.getParentFile(); + parentDir.mkdirs(); + + if (parentDir.exists() == false) + throw new IOException("Parent folder does not exist! \n\tFolder: " + parentDir); + if (parentDir.canWrite() == false) + throw new IOException("Parent folder is read only! \n\tFolder: " + parentDir); + + // Fetch the remote file + boolean isPass; + try + { + // Delegate the actual download + String updateMsg = "\tProgress: "; + isPass = NetUtil.download(aTask, remoteUrl, partFile, aCredential, diskSize, null, updateMsg); + } + catch (FetchError aExp) + { + aTask.logRegln("File download has failed..."); + + Result tmpResult = aExp.getResult(); + switch (tmpResult) + { + case BadCredentials: + aTask.logRegln("\tReason: The site is password protected and bad credentials were provided.\n"); + break; + + case ConnectFailure: + case UnreachableHost: + case UnsupportedConnection: + aTask.logRegln("\tReason: The site appears to be unreachable.\n"); + break; + + case Interrupted: + aTask.logRegln("\tReason: The retrival of the remote file has been interrupted.\n"); + break; + + case InvalidResource: + aTask.logRegln("\tReason: The remote file does not appear to be valid..\n"); + break; + + default: + aTask.logRegln("\tReason: Unknown ---> " + tmpResult + "..\n"); + break; + } + + // Rethrow the error + throw aExp; + } + + // Log the results + String resultMsg = "\tDownload has been completed.\n"; + if (isPass == false) + { + resultMsg = "\tDownload has failed!\n"; + if (aTask.isAborted() == true) + resultMsg = "\tDownload has been aborted.\n"; + aTask.abort(); + } + aTask.logRegln(resultMsg); + + // Move the (completed) tmpFile to the proper location + if (isPass == true) + partFile.renameTo(localFile); + } + + /** + * Forms a {@link Source} relevant to the specified parameters. + * + * @param aCacheDir + * The folder where content will be cached + * @param aBasePath + * The base path of the source + * @param aTargPath + * The path to the resource. If basePath is null then this will be interpreted as absolute. + * @param aDiskSize + * The disk size in bytes of the resource. + * + * @throws MalformedURLException + */ + public static Source formSource(File aCacheDir, String aBasePath, String aTargPath, long aDiskSize) + throws MalformedURLException + { + if (aBasePath != null) + { + if (aBasePath.startsWith("http") == true) + { + File localFile = null; + if (aCacheDir != null) + localFile = new File(aCacheDir, aTargPath); + + if (aBasePath.endsWith("/") == false) + aBasePath += "/"; + + URL remotePath = new URL(aBasePath + aTargPath); + return new PlainSource(localFile, remotePath, aDiskSize); + } + + return new LocalSource(new File(aBasePath, aTargPath)); + } + + return new LocalSource(new File(aTargPath)); + } + + /** + * Utility method that returns the file that should be used as the temporal file when it is being fetched. Currently + * this just appends the text ".part" to the file. + */ + public static File getTempFile(Source aSource) + { + File localFile = aSource.getLocalFile(); + if (localFile == null) + return null; + + File retFile = new File(localFile.getParentFile(), localFile.getName() + ".part"); + return retFile; + } + + /** + * Utility method that returns the base path associated with the specified {@link Source}. + *

    + * If both the remote {@link URL} and local {@link File} are specified then the base path will be relative to the + * remote {@link URL}. + */ + public static String getBasePath(Source aSource) + { + URL remoteUrl = aSource.getRemoteUrl(); + if (remoteUrl != null) + { + try + { + return "" + remoteUrl.toURI().resolve("."); + } + catch (URISyntaxException aExp) + { + throw new RuntimeException(aExp); + } + } + + File localFile = aSource.getLocalFile(); + return localFile.getParent(); + } + + /** + * Returns the {@link SourceState} associated with the source. + *

    + * Depending on underlying file system changes the returned state may out dated. + */ + public static SourceState getState(Source aSource) + { + File localFile = aSource.getLocalFile(); + if (localFile != null && localFile.isFile() == true) + return SourceState.Local; + + File partFile = getTempFile(aSource); + if (partFile != null && partFile.isFile() == true) + return SourceState.Partial; + + URL remoteUrl = aSource.getRemoteUrl(); + if (remoteUrl != null) + return SourceState.Remote; + + return SourceState.Unavailable; + } + + /** + * Utility method that returns a detailed message describing the overall status of the {@link Source}. + */ + public static String getStatusMsg(Source aSource) + { + URL remoteUrl = aSource.getRemoteUrl(); + File localFile = aSource.getLocalFile(); + long diskSize = aSource.getDiskSize(); + + File partFile = getTempFile(aSource); + double perDone = partFile.length() / (diskSize + 0.0); + + NumberUnit perNU = new NumberUnit("", "", 1.0, "0.00 %"); + ByteUnit tmpBU = new ByteUnit(2); + + // Log the header + String retMsg = "File has not been fetched."; + if (remoteUrl == null) + retMsg = "File is local."; + else if (localFile.exists() == true) + retMsg = "File has been downloaded and is local."; + else if (partFile.exists() == true) + { + retMsg = "File has been partially downloaded. [Fetched: " + perNU.getString(perDone) + "]"; + if (diskSize < 0) + retMsg = "File has been partially downloaded. [Fetched: " + tmpBU.getString(partFile.length()) + "]"; + } + else + retMsg = "File is remote. A download will be required."; + + // Log the stats + retMsg += "\n"; + if (remoteUrl != null) + { + retMsg += "\tSRC: " + remoteUrl + "\n"; + retMsg += "\tDST: " + localFile + "\n"; + } + else + { + retMsg += "\tName: " + localFile.getName() + "\n"; + retMsg += "\tPath: " + localFile.getParent() + "\n"; + } + if (diskSize > 0) + retMsg += "\tSize: " + tmpBU.getString(diskSize) + "\n"; + + return retMsg; + } + +} diff --git a/src/glum/task/BufferTask.java b/src/glum/task/BufferTask.java new file mode 100644 index 0000000..f244147 --- /dev/null +++ b/src/glum/task/BufferTask.java @@ -0,0 +1,172 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.task; + +import com.google.common.base.Strings; + +/** + * Implementation of {@link Task} where all logging is captured to a buffer. + * + * @author lopeznr1 + */ +public class BufferTask implements Task +{ + // State vars + private boolean isAborted; + private boolean isActive; + private double progress; + private String tabStr; + + private String dynamicMsgLast; + private StringBuffer workSB; + + /** Standard Constructor */ + public BufferTask() + { + isAborted = false; + isActive = true; + progress = 0; + tabStr = null; + + dynamicMsgLast = null; + workSB = new StringBuffer(); + } + + /** + * Clears out the buffer. The buffer will return an empty string after this call. + */ + public void clearBuffer() + { + dynamicMsgLast = null; + workSB = new StringBuffer(); + } + + /** + * Return the (full) text sent to this task's buffer. + */ + public String getBuffer() + { + String tmpMsg = dynamicMsgLast; + if (tmpMsg == null) + return workSB.toString(); + + return workSB.toString() + tmpMsg; + } + + @Override + public void abort() + { + isAborted = true; + isActive = false; + } + + @Override + public double getProgress() + { + return progress; + } + + @Override + public boolean isAborted() + { + return isAborted; + } + + @Override + public boolean isActive() + { + return isActive; + } + + @Override + public void logReg(String aFmtMsg, Object... aObjArr) + { + // Force the last dynamic message to output + String dynMsg = dynamicMsgLast; + if (dynMsg != null) + { + if (tabStr != null) + dynMsg = dynMsg.replace("\t", tabStr); + workSB.append(dynMsg); + } + dynamicMsgLast = null; + + String tmpMsg = String.format(aFmtMsg, aObjArr); + + // Update the tab chars + if (tabStr != null) + tmpMsg = tmpMsg.replace("\t", tabStr); + + workSB.append(tmpMsg); + } + + @Override + public void logRegln(String aFmtMsg, Object... aObjArr) + { + logReg(aFmtMsg + '\n', aObjArr); + } + + @Override + public void logRegUpdate(String aFmtMsg, Object... aObjArr) + { + // Update the dynamic message + String tmpMsg = String.format(aFmtMsg, aObjArr); + dynamicMsgLast = tmpMsg; + } + + @Override + public void reset() + { + isAborted = false; + isActive = true; + progress = 0; + } + + @Override + public void setProgress(double aProgress) + { + progress = aProgress; + } + + @Override + public void setProgress(int currVal, int maxVal) + { + setProgress((currVal + 0.0) / maxVal); + } + + @Override + public void setRefreshRateMs(long aRateMs) + { + ; // Nothing to do + } + + @Override + public void setStatus(String aStatus) + { + ; // Nothing to do + } + + @Override + public void setTabSize(int numSpaces) + { + tabStr = Strings.repeat(" ", numSpaces); + } + + @Override + public void setTitle(String aTitle) + { + ; // Nothing to do + } + +} diff --git a/src/glum/task/ConsoleTask.java b/src/glum/task/ConsoleTask.java index dc87bdd..8e3259d 100644 --- a/src/glum/task/ConsoleTask.java +++ b/src/glum/task/ConsoleTask.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.task; import com.google.common.base.Strings; @@ -5,11 +18,18 @@ import com.google.common.base.Strings; import glum.unit.NumberUnit; import glum.unit.Unit; +/** + * Implementation of {@link Task} where all logging will be sent to the console. + * + * @author lopeznr1 + */ public class ConsoleTask implements Task { + // State vars + private boolean isAborted; private boolean isActive; private double progress; - + private String dynamicMsgFrag; private String dynamicMsgLast; private long dynamicMsgRateMs; @@ -17,25 +37,28 @@ public class ConsoleTask implements Task private Unit progressUnit; private boolean showProgressInUpdate; - + private String tabStr; + /** Standard Constructor */ public ConsoleTask(boolean aShowProgressInUpdate) { + isAborted = false; isActive = true; progress = 0; - + dynamicMsgFrag = null; dynamicMsgLast = null; dynamicMsgRateMs = 37; oldTimeMs = Long.MIN_VALUE; - progressUnit = new NumberUnit("", "", 100, 2); + progressUnit = new NumberUnit("", "", 100, 2); showProgressInUpdate = aShowProgressInUpdate; - + tabStr = null; } + /** Simplified Constructor */ public ConsoleTask() { this(false); @@ -44,23 +67,44 @@ public class ConsoleTask implements Task @Override public void abort() { + isAborted = true; isActive = false; } - + @Override - public void infoAppend(String aMsg) + public double getProgress() + { + return progress; + } + + @Override + public boolean isAborted() + { + return isAborted; + } + + @Override + public boolean isActive() + { + return isActive; + } + + @Override + public void logReg(String aFmtMsg, Object... aObjArr) { // Force the last dynamic message to be shown if (dynamicMsgLast != null) dynamicMsgUpdateForce(dynamicMsgLast, 0); dynamicMsgLast = null; - + + String tmpMsg = String.format(aFmtMsg, aObjArr); + // Update the tab chars if (tabStr != null) - aMsg = aMsg.replace("\t", tabStr); - + tmpMsg = tmpMsg.replace("\t", tabStr); + // Display the new message - System.out.print(aMsg); + System.out.print(tmpMsg); System.out.flush(); // Reset the dynamic vars @@ -70,20 +114,22 @@ public class ConsoleTask implements Task } @Override - public void infoAppendln(String aMsg) + public void logRegln(String aFmtMsg, Object... aObjArr) { - infoAppend(aMsg + '\n'); + logReg(aFmtMsg + '\n', aObjArr); } - + @Override - public void infoUpdate(String aMsg) + public void logRegUpdate(String aFmtMsg, Object... aObjArr) { long currTimeMs, totalTimeMs; - + + String tmpMsg = String.format(aFmtMsg, aObjArr); + // Auto add the progress into update messages if necessary - dynamicMsgLast = aMsg; + dynamicMsgLast = tmpMsg; if (showProgressInUpdate == true) - dynamicMsgLast = "[" + progressUnit.getString(getProgress()) +"%] " + aMsg; + dynamicMsgLast = "[" + progressUnit.getString(getProgress()) + "%] " + tmpMsg; // Get the current time currTimeMs = System.nanoTime() / 1000000; @@ -98,15 +144,10 @@ public class ConsoleTask implements Task dynamicMsgLast = null; } - @Override - public double getProgress() - { - return progress; - } - @Override public void reset() { + isAborted = false; isActive = true; progress = 0; } @@ -118,9 +159,9 @@ public class ConsoleTask implements Task } @Override - public void setProgress(int currVal, int maxVal) + public void setProgress(int aCurrVal, int aMaxVal) { - setProgress((currVal + 0.0) / maxVal); + setProgress((aCurrVal + 0.0) / aMaxVal); } @Override @@ -137,9 +178,9 @@ public class ConsoleTask implements Task } @Override - public void setTabSize(int numSpaces) + public void setTabSize(int aNumSpaces) { - tabStr = Strings.repeat(" ", numSpaces); + tabStr = Strings.repeat(" ", aNumSpaces); } @Override @@ -149,12 +190,6 @@ public class ConsoleTask implements Task ; // Nothing to do } - @Override - public boolean isActive() - { - return isActive; - } - /** * Helper method that does the actual updating of the previous dynamic text with aMsg. */ @@ -169,7 +204,7 @@ public class ConsoleTask implements Task for (int c1 = 0; c1 < numTempChars; c1++) System.out.print("\b \b"); } - + // Update the tab chars if (tabStr != null) aMsg = aMsg.replace("\t", tabStr); diff --git a/src/glum/task/CountTask.java b/src/glum/task/CountTask.java index 28f3f8b..346967a 100644 --- a/src/glum/task/CountTask.java +++ b/src/glum/task/CountTask.java @@ -1,25 +1,43 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.task; /** - * Task that automatically updates the progress based on a count. - * Once the currCount == fullCount then the Task is Complete. + * Task that automatically updates the progress based on a count. Once the currCount == fullCount then the Task is + * considered complete. + * + * @author lopeznr1 */ public class CountTask implements Task { - protected Task refTask; - protected int fullCount; - protected int currCount; + // Ref vars + private final Task refTask; + // State vars + private int fullCount; + private int currCount; + + /** Standard Constructor */ public CountTask(Task aRefTask, int aFullCount) { refTask = aRefTask; fullCount = aFullCount; currCount = 0; } - + /** - * Increment the currCount (and corresponding) progress to - * completion. + * Increment the currCount (and corresponding) progress to completion. */ public void incrementCount() { @@ -32,24 +50,6 @@ public class CountTask implements Task { refTask.abort(); } - - @Override - public void infoAppend(String aMsg) - { - refTask.infoAppend(aMsg); - } - - @Override - public void infoAppendln(String aMsg) - { - refTask.infoAppendln(aMsg); - } - - @Override - public void infoUpdate(String aMsg) - { - refTask.infoUpdate(aMsg); - } @Override public double getProgress() @@ -57,6 +57,36 @@ public class CountTask implements Task return (currCount + 0.0) / fullCount; } + @Override + public boolean isAborted() + { + return refTask.isAborted(); + } + + @Override + public boolean isActive() + { + return refTask.isActive(); + } + + @Override + public void logReg(String aFmtMsg, Object... aObjArr) + { + refTask.logReg(aFmtMsg, aObjArr); + } + + @Override + public void logRegln(String aFmtMsg, Object... aObjArr) + { + refTask.logRegln(aFmtMsg, aObjArr); + } + + @Override + public void logRegUpdate(String aFmtMsg, Object... aObjArr) + { + refTask.logRegUpdate(aFmtMsg, aObjArr); + } + @Override public void reset() { @@ -72,9 +102,9 @@ public class CountTask implements Task } @Override - public void setProgress(int currVal, int maxVal) + public void setProgress(int aCurrVal, int aMaxVal) { - setProgress((currVal + 0.0)/ maxVal); + setProgress((aCurrVal + 0.0) / aMaxVal); } @Override @@ -90,9 +120,9 @@ public class CountTask implements Task } @Override - public void setTabSize(int numSpaces) + public void setTabSize(int aNumSpaces) { - refTask.setTabSize(numSpaces); + refTask.setTabSize(aNumSpaces); } @Override @@ -101,10 +131,4 @@ public class CountTask implements Task refTask.setTitle(aTitle); } - @Override - public boolean isActive() - { - return refTask.isActive(); - } - } diff --git a/src/glum/task/FileLogTask.java b/src/glum/task/FileLogTask.java deleted file mode 100644 index b1188eb..0000000 --- a/src/glum/task/FileLogTask.java +++ /dev/null @@ -1,166 +0,0 @@ -package glum.task; - -import glum.unit.NumberUnit; -import glum.unit.Unit; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.PrintStream; - -import com.google.common.base.Strings; - -public class FileLogTask implements Task -{ - private boolean isActive; - private double progress; - private String tabStr; - - private String dynamicMsgLast; - private Unit progressUnit; - private boolean showProgressInUpdate; - - private PrintStream printStream; - - public FileLogTask(File aFile, boolean isAppend, boolean aShowProgressInUpdate) - { - isActive = true; - progress = 0; - tabStr = null; - - dynamicMsgLast = null; - progressUnit = new NumberUnit("", "", 100, 2); - showProgressInUpdate = aShowProgressInUpdate; - - // Open the file stream for writing - try - { - printStream = new PrintStream(new FileOutputStream(aFile, isAppend)); - } - catch (FileNotFoundException aExp) - { - throw new RuntimeException(aExp); - } - - } - - public FileLogTask(File aFile, boolean isAppend) - { - this(aFile, isAppend, false); - } - - /** - * Properly close the associated file used for the log - */ - public void close() - { - printStream.close(); - } - - /** - * Configures whether the dynamic messages should have an automatic progress bar readout. - */ - public void setShowProgressInUpdate(boolean aShowProgressInUpdate) - { - showProgressInUpdate = aShowProgressInUpdate; - } - - @Override - public void abort() - { - isActive = false; - } - - @Override - public void infoAppend(String aMsg) - { - // Force the last dynamic message to output - if (dynamicMsgLast != null) - printStream.print(dynamicMsgLast); - dynamicMsgLast = null; - - // Update the tab chars - if (tabStr != null) - aMsg = aMsg.replace("\t", tabStr); - - // Display the new message - printStream.print(aMsg); - printStream.flush(); - } - - @Override - public void infoAppendln(String aMsg) - { - infoAppend(aMsg + '\n'); - } - - @Override - public void infoUpdate(String aMsg) - { - // Update the dynamic message - dynamicMsgLast = aMsg; - - // Auto add the progress into update messages if necessary - if (showProgressInUpdate == true) - dynamicMsgLast = "[" + progressUnit.getString(getProgress()) + "%] " + aMsg; - } - - @Override - public double getProgress() - { - return progress; - } - - @Override - public void reset() - { - isActive = true; - progress = 0; - } - - @Override - public void setProgress(double aProgress) - { - progress = aProgress; - } - - @Override - public void setProgress(int currVal, int maxVal) - { - setProgress((currVal + 0.0) / maxVal); - } - - @Override - public void setRefreshRateMs(long aRateMs) - { - // Refresh rate is nonsensical for a file log - ; // Nothing to do - } - - @Override - public void setStatus(String aStatus) - { - // FileLogTasks do not support a status line. - ; // Nothing to do - } - - @Override - public void setTabSize(int numSpaces) - { - tabStr = Strings.repeat(" ", numSpaces); - } - - @Override - public void setTitle(String aTitle) - { - // FileLogTasks do not support a title line. - ; // Nothing to do - } - - @Override - public boolean isActive() - { - return isActive; - } - -} diff --git a/src/glum/task/IndentTask.java b/src/glum/task/IndentTask.java new file mode 100644 index 0000000..4968395 --- /dev/null +++ b/src/glum/task/IndentTask.java @@ -0,0 +1,153 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.task; + +/** + * Task designed to encompass 1 child task and indent all output by the specified number of tabs. + * + * @author lopeznr1 + */ +public class IndentTask implements Task +{ + // Attributes + private final Task childTask; + private final String tabStr; + + // State vars + private boolean isTrailNL; + + /** Standard Constructor */ + public IndentTask(Task aChildTask, int aNumIndent) + { + childTask = aChildTask; + + String tmpStr = ""; + for (int c1 = 0; c1 < aNumIndent; c1++) + tmpStr += "\t"; + tabStr = tmpStr; + + isTrailNL = true; + } + + @Override + public void abort() + { + childTask.abort(); + } + + @Override + public double getProgress() + { + return childTask.getProgress(); + } + + @Override + public boolean isAborted() + { + return childTask.isAborted(); + } + + @Override + public boolean isActive() + { + return childTask.isActive(); + } + + @Override + public void logReg(String aFmtMsg, Object... aObjArr) + { + String tmpMsg = String.format(aFmtMsg, aObjArr); + + // Indent the first log message on the following conditions: + // - The previous output had the last char == '\n' (a trailing NL) + // - If aMsg does NOT start with a newline + if (isTrailNL == true && tmpMsg.startsWith("\n") == false) + { + isTrailNL = false; + tmpMsg = tabStr + tmpMsg; + } + + // Keep track of any trailing newline and strip it from aMsg + if (tmpMsg.endsWith("\n") == true) + { + isTrailNL = true; + tmpMsg = tmpMsg.substring(0, tmpMsg.length() - 1); + } + + // Indent any text that is after a newline + tmpMsg = tmpMsg.replaceAll("\n", "\n" + tabStr); + + // Add back in the trailing newline + if (isTrailNL == true) + tmpMsg += "\n"; + + childTask.logReg(tmpMsg); + } + + @Override + public void logRegln(String aFmtMsg, Object... aObjArr) + { + logReg(aFmtMsg + "\n", aObjArr); + } + + @Override + public void logRegUpdate(String aFmtMsg, Object... aObjArr) + { + aFmtMsg = aFmtMsg.replaceAll("\n", "\n" + tabStr); + childTask.logRegUpdate(aFmtMsg, aObjArr); + } + + @Override + public void reset() + { + childTask.reset(); + } + + @Override + public void setProgress(double aProgress) + { + childTask.setProgress(aProgress); + } + + @Override + public void setProgress(int aCurrVal, int aMaxVal) + { + childTask.setProgress(aCurrVal, aMaxVal); + } + + @Override + public void setRefreshRateMs(long aRateMs) + { + childTask.setRefreshRateMs(aRateMs); + } + + @Override + public void setStatus(String aStatus) + { + childTask.setStatus(aStatus); + } + + @Override + public void setTabSize(int aNumSpaces) + { + childTask.setTabSize(aNumSpaces); + } + + @Override + public void setTitle(String aTitle) + { + childTask.setTitle(aTitle); + } + +} diff --git a/src/glum/task/InvalidTask.java b/src/glum/task/InvalidTask.java new file mode 100644 index 0000000..6bacb3f --- /dev/null +++ b/src/glum/task/InvalidTask.java @@ -0,0 +1,121 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.task; + +/** + * Singleton instance of {@link Task} that defines the "invalid" {@link Task}. + *

    + * All attempts to change the state of this task will be ignored. + *

    + * All queries to this task will reflect that of an inactive / aborted task. + * + * @author lopeznr1 + */ +public class InvalidTask implements Task +{ + // Constants + /** The "invalid" {@link Task}. */ + static final InvalidTask Instance = new InvalidTask(); + + /** Private Singleton Constructor */ + private InvalidTask() + { + ; // Nothing to do + } + + @Override + public void abort() + { + ; // Nothing to do + } + + @Override + public double getProgress() + { + return Double.NaN; + } + + @Override + public boolean isAborted() + { + return true; + } + + @Override + public boolean isActive() + { + return false; + } + + @Override + public void logReg(String aFmtMsg, Object... aObjArr) + { + ; // Nothing to do + } + + @Override + public void logRegln(String aFmtMsg, Object... aObjArr) + { + ; // Nothing to do + } + + @Override + public void logRegUpdate(String aFmtMsg, Object... aObjArr) + { + ; // Nothing to do + } + + @Override + public void reset() + { + ; // Nothing to do + } + + @Override + public void setProgress(double aProgress) + { + ; // Nothing to do + } + + @Override + public void setProgress(int aCurrVal, int aMaxVal) + { + ; // Nothing to do + } + + @Override + public void setRefreshRateMs(long aRateMs) + { + ; // Nothing to do + } + + @Override + public void setStatus(String aStatus) + { + ; // Nothing to do + } + + @Override + public void setTabSize(int aNumSpaces) + { + ; // Nothing to do + } + + @Override + public void setTitle(String aTitle) + { + ; // Nothing to do + } + +} diff --git a/src/glum/task/NotifyTask.java b/src/glum/task/NotifyTask.java new file mode 100644 index 0000000..0baacf7 --- /dev/null +++ b/src/glum/task/NotifyTask.java @@ -0,0 +1,255 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.task; + +/** + * Implementation of {@link Task} where notification of progress updates is made via a single {@link TaskListener}. + *

    + * Notifications will be sent out once no more frequently than once every 47 milliseconds (~21 updates per second). The + * frequency of notifications can be configured via {@link #setRefreshRateMS(long)} + * + * @author lopeznr1 + */ +public class NotifyTask implements Task +{ + // Ref vars + private final Task refTask; + private final TaskListener refListener; + + // State vars + private boolean isInit; + private long begTime; + private long endTime; + + private long refreshRateMs; + + /** Standard Constructor. */ + public NotifyTask(Task aTask, TaskListener aListener) + { + refTask = aTask; + refListener = aListener; + + isInit = true; + begTime = 0L; + endTime = 0L; + + refreshRateMs = 47; + } + + /** Simplified Constructor. */ + public NotifyTask(TaskListener aListener) + { + this(new SilentTask(), aListener); + } + + /** + * Forces an update to this task. + *

    + * If the task is active then the end time will be updated. + *

    + * The {@link TaskListener} will be notified. + */ + public void forceUpdate() + { + if (isActive() == true) + endTime = System.currentTimeMillis(); + + refListener.taskUpdate(this); + } + + /** + * Return true if the task has reached completion or has been aborted. + */ + public boolean isDone() + { + if (isAborted() == true) + return true; + + if (getProgress() >= 1.0) + return true; + + return false; + } + + /** + * Return true if the task has not left the initial state. + *

    + * A task should be considered in the initial state if the progress has not moved past 0% and / or if it has not been + * manually transitioned out. + */ + public boolean isInit() + { + return isInit; + } + + /** + * Moves the task from the init state to the in-progress state + */ + public void markInitDone() + { + begTime = System.currentTimeMillis(); + isInit = false; + } + + /** + * Returns the reference {@link Task}. + */ + public Task getRefTask() + { + return refTask; + } + + /** + * Returns the rate (in milliseconds) at which notifications will be sent out. + */ + public long getRefreshRateMS() + { + return refreshRateMs; + } + + /** + * Returns the (system) time when this Task was reset or initialization was completed. + */ + public long getTimeBeg() + { + return begTime; + } + + /** + * Returns the (system) time when this Task was last updated. + */ + public long getTimeEnd() + { + return endTime; + } + + @Override + public void abort() + { + if (isActive() == true) + endTime = System.currentTimeMillis(); + + refTask.abort(); + + isInit = false; + refListener.taskUpdate(this); + } + + @Override + public double getProgress() + { + return refTask.getProgress(); + } + + @Override + public boolean isAborted() + { + return refTask.isAborted(); + } + + @Override + public boolean isActive() + { + return refTask.isActive(); + } + + @Override + public void logReg(String aFmtMsg, Object... aObjArr) + { + refTask.logReg(aFmtMsg, aObjArr); + } + + @Override + public void logRegln(String aFmtMsg, Object... aObjArr) + { + refTask.logRegln(aFmtMsg, aObjArr); + } + + @Override + public void logRegUpdate(String aFmtMsg, Object... aObjArr) + { + refTask.logRegUpdate(aFmtMsg, aObjArr); + } + + @Override + public void reset() + { + refTask.reset(); + + isInit = true; + begTime = System.currentTimeMillis(); + endTime = 0L; + } + + @Override + public void setProgress(double aProgress) + { + refTask.setProgress(aProgress); + isInit = false; + + // Update UI at refreshRateMS + long currTime = System.currentTimeMillis(); + if (currTime < endTime + refreshRateMs) + return; + + // Update + if (isActive() == true) + endTime = System.currentTimeMillis(); + + refListener.taskUpdate(this); + } + + @Override + public void setProgress(int aCurrVal, int aMaxVal) + { + refTask.setProgress(aCurrVal, aMaxVal); + isInit = false; + + // Update UI at refreshRateMS + long currTime = System.currentTimeMillis(); + if (currTime < endTime + refreshRateMs) + return; + + // Update + if (isActive() == true) + endTime = System.currentTimeMillis(); + + refListener.taskUpdate(this); + } + + @Override + public void setRefreshRateMs(long aRateMs) + { + refreshRateMs = aRateMs; + } + + @Override + public void setStatus(String aStatus) + { + refTask.setStatus(aStatus); + } + + @Override + public void setTabSize(int aNumSpaces) + { + refTask.setTabSize(aNumSpaces); + } + + @Override + public void setTitle(String aTitle) + { + refTask.setTitle(aTitle); + } + +} diff --git a/src/glum/task/PartialTask.java b/src/glum/task/PartialTask.java index 12557a9..ab92aec 100644 --- a/src/glum/task/PartialTask.java +++ b/src/glum/task/PartialTask.java @@ -1,12 +1,35 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.task; +/** + * Task designed to be a partial of a larger {@link Task}. Utilization of this task is focused on the smaller partial + * task at hand. + * + * @author lopeznr1 + */ public class PartialTask implements Task { - protected Task refTask; - protected double progressOffset; - protected double progressTotalFragment; - protected double internalProgress; + // Ref vars + private final Task refTask; + // State vars + private double progressOffset; + private double progressTotalFragment; + private double internalProgress; + + /** Standard Constructor */ public PartialTask(Task aRefTask, double aProgressOffset, double aProgressTotalFragment) { refTask = aRefTask; @@ -21,30 +44,42 @@ public class PartialTask implements Task refTask.abort(); } - @Override - public void infoAppend(String aMsg) - { - refTask.infoAppend(aMsg); - } - - @Override - public void infoAppendln(String aMsg) - { - refTask.infoAppendln(aMsg); - } - - @Override - public void infoUpdate(String aMsg) - { - refTask.infoUpdate(aMsg); - } - @Override public double getProgress() { return progressOffset + (progressTotalFragment * internalProgress); } + @Override + public boolean isAborted() + { + return refTask.isAborted(); + } + + @Override + public boolean isActive() + { + return refTask.isActive(); + } + + @Override + public void logReg(String aFmtMsg, Object... aObjArr) + { + refTask.logReg(aFmtMsg, aObjArr); + } + + @Override + public void logRegln(String aFmtMsg, Object... aObjArr) + { + refTask.logRegln(aFmtMsg, aObjArr); + } + + @Override + public void logRegUpdate(String aFmtMsg, Object... aObjArr) + { + refTask.logRegUpdate(aFmtMsg, aObjArr); + } + @Override public void reset() { @@ -61,9 +96,9 @@ public class PartialTask implements Task } @Override - public void setProgress(int currVal, int maxVal) + public void setProgress(int aCurrVal, int aMaxVal) { - setProgress((currVal + 0.0) / maxVal); + setProgress((aCurrVal + 0.0) / aMaxVal); } @Override @@ -79,9 +114,9 @@ public class PartialTask implements Task } @Override - public void setTabSize(int numSpaces) + public void setTabSize(int aNumSpaces) { - refTask.setTabSize(numSpaces); + refTask.setTabSize(aNumSpaces); } @Override @@ -90,10 +125,4 @@ public class PartialTask implements Task refTask.setTitle(aTitle); } - @Override - public boolean isActive() - { - return refTask.isActive(); - } - } diff --git a/src/glum/task/PrintStreamTask.java b/src/glum/task/PrintStreamTask.java new file mode 100644 index 0000000..4de841b --- /dev/null +++ b/src/glum/task/PrintStreamTask.java @@ -0,0 +1,188 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.task; + +import java.io.PrintStream; + +import com.google.common.base.Strings; + +import glum.unit.NumberUnit; +import glum.unit.Unit; + +/** + * Implementation of {@link Task} where all logging will be sent to the provided {@link PrintStream}. + * + * @author lopeznr1 + */ +public class PrintStreamTask implements Task +{ + // Ref vars + private final PrintStream refStream; + + // State vars + private boolean isAborted; + private boolean isActive; + private double progress; + private String tabStr; + + private String dynamicMsgLast; + private Unit progressUnit; + private boolean showProgressInUpdate; + + /** + * Standard Constructor + * + * @param aStream + * @param aShowProgressInUpdate + */ + public PrintStreamTask(PrintStream aStream, boolean aShowProgressInUpdate) + { + refStream = aStream; + + isAborted = false; + isActive = true; + progress = 0; + tabStr = null; + + dynamicMsgLast = null; + progressUnit = new NumberUnit("", "", 100, 2); + showProgressInUpdate = aShowProgressInUpdate; + } + + /** + * Properly close the associated file used for the log + */ + public void close() + { + refStream.close(); + } + + /** + * Configures whether the dynamic messages should have an automatic progress bar readout. + */ + public void setShowProgressInUpdate(boolean aShowProgressInUpdate) + { + showProgressInUpdate = aShowProgressInUpdate; + } + + @Override + public void abort() + { + isAborted = true; + isActive = false; + } + + @Override + public double getProgress() + { + return progress; + } + + @Override + public boolean isAborted() + { + return isAborted; + } + + @Override + public boolean isActive() + { + return isActive; + } + + @Override + public void logReg(String aFmtMsg, Object... aObjArr) + { + // Force the last dynamic message to output + if (dynamicMsgLast != null) + refStream.print(dynamicMsgLast); + dynamicMsgLast = null; + + String tmpMsg = String.format(aFmtMsg, aObjArr); + + // Update the tab chars + if (tabStr != null) + tmpMsg = tmpMsg.replace("\t", tabStr); + + // Display the new message + refStream.print(tmpMsg); + refStream.flush(); + } + + @Override + public void logRegln(String aFmtMsg, Object... aObjArr) + { + logReg(aFmtMsg + '\n', aObjArr); + } + + @Override + public void logRegUpdate(String aFmtMsg, Object... aObjArr) + { + // Update the dynamic message + String tmpMsg = String.format(aFmtMsg, aObjArr); + dynamicMsgLast = tmpMsg; + + // Auto add the progress into update messages if necessary + if (showProgressInUpdate == true) + dynamicMsgLast = "[" + progressUnit.getString(getProgress()) + "%] " + tmpMsg; + } + + @Override + public void reset() + { + isAborted = false; + isActive = true; + progress = 0; + } + + @Override + public void setProgress(double aProgress) + { + progress = aProgress; + } + + @Override + public void setProgress(int aCurrVal, int aMaxVal) + { + setProgress((aCurrVal + 0.0) / aMaxVal); + } + + @Override + public void setRefreshRateMs(long aRateMs) + { + // Refresh rate is nonsensical for a file log + ; // Nothing to do + } + + @Override + public void setStatus(String aStatus) + { + // PrintStreamTasks do not support a status line. + ; // Nothing to do + } + + @Override + public void setTabSize(int aNumSpaces) + { + tabStr = Strings.repeat(" ", aNumSpaces); + } + + @Override + public void setTitle(String aTitle) + { + // PrintStreamTasks do not support a title line. + ; // Nothing to do + } + +} diff --git a/src/glum/task/SilentTask.java b/src/glum/task/SilentTask.java index e6d5a66..4c1e2aa 100644 --- a/src/glum/task/SilentTask.java +++ b/src/glum/task/SilentTask.java @@ -1,12 +1,34 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.task; +/** + * Implementation of {@link Task} where all logging is ignored. + * + * @author lopeznr1 + */ public class SilentTask implements Task { + // State vars + private boolean isAborted; private boolean isActive; private double progress; + /** Standard Constructor */ public SilentTask() { + isAborted = false; isActive = true; progress = 0; } @@ -14,36 +36,50 @@ public class SilentTask implements Task @Override public void abort() { + isAborted = true; isActive = false; } - @Override - public void infoAppend(String aMsg) - { - ; // Nothing to do - } - - @Override - public void infoAppendln(String aMsg) - { - ; // Nothing to do - } - - @Override - public void infoUpdate(String aMsg) - { - ; // Nothing to do - } - @Override public double getProgress() { return progress; } + @Override + public boolean isAborted() + { + return isAborted; + } + + @Override + public boolean isActive() + { + return isActive; + } + + @Override + public void logReg(String aFmtMsg, Object... aObjArr) + { + ; // Nothing to do + } + + @Override + public void logRegln(String aFmtMsg, Object... aObjArr) + { + ; // Nothing to do + } + + @Override + public void logRegUpdate(String aFmtMsg, Object... aObjArr) + { + ; // Nothing to do + } + @Override public void reset() { + isAborted = false; isActive = true; progress = 0; } @@ -55,9 +91,9 @@ public class SilentTask implements Task } @Override - public void setProgress(int currVal, int maxVal) + public void setProgress(int aCurrVal, int aMaxVal) { - setProgress((currVal + 0.0) / maxVal); + setProgress((aCurrVal + 0.0) / aMaxVal); } @Override @@ -73,7 +109,7 @@ public class SilentTask implements Task } @Override - public void setTabSize(int numSpaces) + public void setTabSize(int aNumSpaces) { ; // Nothing to do } @@ -84,10 +120,4 @@ public class SilentTask implements Task ; // Nothing to do } - @Override - public boolean isActive() - { - return isActive; - } - } diff --git a/src/glum/task/SplitTask.java b/src/glum/task/SplitTask.java index dffb83e..0617798 100644 --- a/src/glum/task/SplitTask.java +++ b/src/glum/task/SplitTask.java @@ -1,117 +1,137 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.task; -import java.util.List; - -import com.google.common.collect.Lists; +import com.google.common.collect.ImmutableList; /** * Task designed to encompass many "sub" child tasks and forward actions to each child task. - *

    + *

    * On any query method call such as {@link #getProgress()}, the first child task specified in the constructor will be * utilized as the task to fulfill the query. + * + * @author lopeznr1 */ public class SplitTask implements Task { - private List childTaskList; + // Attributes + private final ImmutableList childTaskL; /** + * Standard Constructor + * * @param aTaskArr - * The list of child tasks for which this SplitTask will automatically forward the method calls to. + * The list of child tasks for which this SplitTask will automatically forward the method calls to. */ public SplitTask(Task... aTaskArr) { - childTaskList = Lists.newLinkedList(); - for (Task aTask : aTaskArr) - childTaskList.add(aTask); + childTaskL = ImmutableList.copyOf(aTaskArr); } @Override public void abort() { - for (Task aTask : childTaskList) + for (Task aTask : childTaskL) aTask.abort(); } - @Override - public void infoAppend(String aMsg) - { - for (Task aTask : childTaskList) - aTask.infoAppend(aMsg); - } - - @Override - public void infoAppendln(String aMsg) - { - for (Task aTask : childTaskList) - aTask.infoAppendln(aMsg); - } - - @Override - public void infoUpdate(String aMsg) - { - for (Task aTask : childTaskList) - aTask.infoUpdate(aMsg); - } - @Override public double getProgress() { - return childTaskList.get(0).getProgress(); + return childTaskL.get(0).getProgress(); + } + + @Override + public boolean isAborted() + { + return childTaskL.get(0).isAborted(); + } + + @Override + public boolean isActive() + { + return childTaskL.get(0).isActive(); + } + + @Override + public void logReg(String aFmtMsg, Object... aObjArr) + { + for (Task aTask : childTaskL) + aTask.logReg(aFmtMsg, aObjArr); + } + + @Override + public void logRegln(String aFmtMsg, Object... aObjArr) + { + for (Task aTask : childTaskL) + aTask.logRegln(aFmtMsg, aObjArr); + } + + @Override + public void logRegUpdate(String aFmtMsg, Object... aObjArr) + { + for (Task aTask : childTaskL) + aTask.logRegUpdate(aFmtMsg, aObjArr); } @Override public void reset() { - for (Task aTask : childTaskList) + for (Task aTask : childTaskL) aTask.reset(); } @Override public void setProgress(double aProgress) { - for (Task aTask : childTaskList) + for (Task aTask : childTaskL) aTask.setProgress(aProgress); } @Override - public void setProgress(int currVal, int maxVal) + public void setProgress(int aCurrVal, int aMaxVal) { - for (Task aTask : childTaskList) - aTask.setProgress(currVal, maxVal); + for (Task aTask : childTaskL) + aTask.setProgress(aCurrVal, aMaxVal); } @Override public void setRefreshRateMs(long aRateMs) { - for (Task aTask : childTaskList) + for (Task aTask : childTaskL) aTask.setRefreshRateMs(aRateMs); } @Override public void setStatus(String aStatus) { - for (Task aTask : childTaskList) + for (Task aTask : childTaskL) aTask.setStatus(aStatus); } @Override - public void setTabSize(int numSpaces) + public void setTabSize(int aNumSpaces) { - for (Task aTask : childTaskList) - aTask.setTabSize(numSpaces); + for (Task aTask : childTaskL) + aTask.setTabSize(aNumSpaces); } @Override public void setTitle(String aTitle) { - for (Task aTask : childTaskList) + for (Task aTask : childTaskL) aTask.setTitle(aTitle); } - @Override - public boolean isActive() - { - return childTaskList.get(0).isActive(); - } - } diff --git a/src/glum/task/Task.java b/src/glum/task/Task.java index 9b5fca4..6293d6c 100644 --- a/src/glum/task/Task.java +++ b/src/glum/task/Task.java @@ -1,78 +1,106 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.task; +/** + * Interface that provides support to monitor a "task". The following features are provided: + *

      + *
    • Checking the state of the task. + *
    • Querying + updating of progress. + *
    • Logging of regular messages associated with the task. + *
    • Configuration of refresh rates. + *
    + * + * @author lopeznr1 + */ public interface Task { + // Constants + /** The "invalid" {@link Task}. */ + public static final Task Invalid = InvalidTask.Instance; + /** - * Method to allow the Task to be properly aborted. After this method - * call, the method call isActive will return false. + * Method to allow the Task to be properly aborted. After this method call, the method call isActive will return + * false. */ public void abort(); - - /** - * Appends aMsg to the info buffer. The text aMsg can not be updated. - */ - public void infoAppend(String aMsg); /** - * Appends aMsg to the info buffer. The text aMsg can not be updated. - * A new line will automatically be added to the buffer after aMsg. - */ - public void infoAppendln(String aMsg); - - /** - * Updates the previous update message with the text aMsg. The text - * aMsg can be updated as long as the method infoAppend is not called. - */ - public void infoUpdate(String aMsg); - - /** - * Returns the percent of progress that has been completed. - * 0.0: Not started - * 1.0: Complete + * Returns the percent of progress that has been completed. 0.0: Not started 1.0: Complete */ public double getProgress(); - + + /** + * Returns whether this task has been aborted. + */ + public boolean isAborted(); + + /** + * Returns whether this task is still active. + */ + public boolean isActive(); + + /** + * Appends a formatted string to the regular buffer. The appended text can not be updated. + */ + public void logReg(String aFmtMsg, Object... aObjArr); + + /** + * Appends a formatted string to the regular buffer. The appended text can not be updated. A new line will + * automatically be added to the buffer after the appended text. + */ + public void logRegln(String aFmtMsg, Object... aObjArr); + + /** + * Updates the previous regular message with a formatted string. The text can be updated as long as the method + * logReg() and logRegln() are not called. + */ + public void logRegUpdate(String aFmtMsg, Object... aObjArr); + /** * Method to reset a Task to its initial state. */ public void reset(); /** - * Sets in the percent of progress that has been completed. - * 0.0: Not started - * 1.0: Complete - */ - public void setProgress(double aProgress); - - /** - * Sets it the progress of a task so that its completion - * value is a ratio of currVal to maxVal. + * Sets in the percent of progress that has been completed. 0.0: Not started 1.0: Complete */ - public void setProgress(int currVal, int maxVal); + public void setProgress(double aProgress); /** - * Sets in the maximum rate the UI will be refreshed at. + * Sets it the progress of a task so that its completion value is a ratio of aCurrVal to aMaxVal. + */ + public void setProgress(int aCurrVal, int aMaxVal); + + /** + * Sets in the maximum rate (in milliseconds) the UI will be refreshed at. */ public void setRefreshRateMs(long aRateMs); /** - * Method that sets the single line status of the task + * Method that sets the single line status of the task */ public void setStatus(String aStatus); /** * Sets in the number of spaces the tab character will be converted to */ - public void setTabSize(int numSpaces); + public void setTabSize(int aNumSpaces); /** - * Method that sets the title of the task + * Method that sets the title of the task */ public void setTitle(String aTitle); - /** - * Returns whether this task is still active - */ - public boolean isActive(); - } diff --git a/src/glum/task/TaskListener.java b/src/glum/task/TaskListener.java new file mode 100644 index 0000000..76098f3 --- /dev/null +++ b/src/glum/task/TaskListener.java @@ -0,0 +1,28 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.task; + +/** + * Interface that provides for the callback mechanism for notification of updates to a {@link Task}. + * + * @author lopeznr1 + */ +public interface TaskListener +{ + /** + * Notification method that the task has been updated. + */ + public void taskUpdate(Task aTask); + +} diff --git a/src/glum/task/TaskState.java b/src/glum/task/TaskState.java deleted file mode 100644 index 39c694f..0000000 --- a/src/glum/task/TaskState.java +++ /dev/null @@ -1,20 +0,0 @@ -package glum.task; - -public enum TaskState -{ - // Initial state - Inactive, - - // Default normal state - Active, - - // Transitional states - TransAbort, - TransComplete, - TransSuspend, - - // Terminal states - Aborted, - Completed, - Suspended -} diff --git a/src/glum/text/SigFigNumberFormat.java b/src/glum/text/SigFigNumberFormat.java new file mode 100644 index 0000000..4c10eed --- /dev/null +++ b/src/glum/text/SigFigNumberFormat.java @@ -0,0 +1,115 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.text; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.text.*; + +/** + * NumberFormat used to display values with a desired number of significant figures. + *

    + * TODO: This class is incomplete and was rushed just to support simple display of values with a requested number of + * significant digits. + * + * @author lopeznr1 + */ +public class SigFigNumberFormat extends NumberFormat +{ + // Attributes + private final String nanStr; + private final int numSigFigs; + + /** + * Standard Constructor + * + * @param aNumSigFigs + * The number of significant figures to display + * @param aNaNStr + * The string to show for values of NaN. + */ + public SigFigNumberFormat(int aNumSigFigs, String aNaNStr) + { + nanStr = aNaNStr; + numSigFigs = aNumSigFigs; + } + + /** + * Simplified Constructor + * + * @param aNumSigFigs + * The number of significant figures to display + */ + public SigFigNumberFormat(int aNumSigFigs) + { + this(aNumSigFigs, null); + } + + @Override + public StringBuffer format(double number, StringBuffer result, FieldPosition fieldPosition) + { + // Special handling for infinite + if (Double.isInfinite(number) == true) + return result.append("" + number); + + // Special handling for NaN + if (Double.isNaN(number) == true) + { + if (nanStr != null) + return result.append(nanStr); + + result.append("-."); + for (int c1 = 0; c1 < numSigFigs - 1; c1++) + result.append("-"); + + return result; + } + + BigDecimal tmpBD = getValueRoundedToSigFigs(number, numSigFigs); + return result.append("" + tmpBD); + } + + @Override + public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) + { + throw new RuntimeException("NOT IMPLEMENTED"); + } + + @Override + public Number parse(String source, ParsePosition parsePosition) + { + throw new RuntimeException("NOT IMPLEMENTED"); + } + + /** + * Utility method that returns a BigDecimal rounded to the specified number of significant figures. + *

    + * Source: https://stackoverflow.com/questions/7548841 + *

    + * TODO: Consider promoting this method to a utility class. + * + * @param aValue + * The double value to be rounded + * @param aNumSigFigs + * The number of significant figures of interest. + */ + private static BigDecimal getValueRoundedToSigFigs(double aValue, int aNumSigFigs) + { + BigDecimal retBD = new BigDecimal(aValue); + retBD = retBD.round(new MathContext(aNumSigFigs)); + + return retBD; + } + +} \ No newline at end of file diff --git a/src/glum/unit/BaseUnitProvider.java b/src/glum/unit/BaseUnitProvider.java index 67a4ddf..d7fdee5 100644 --- a/src/glum/unit/BaseUnitProvider.java +++ b/src/glum/unit/BaseUnitProvider.java @@ -1,20 +1,35 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.unit; +import java.util.ArrayList; import java.util.List; -import com.google.common.collect.Lists; - /** - * Base UnitProvider class that provides the functionality of listener registration and notification. + * Base implementation of {@link UnitProvider} class that provides the functionality of listener registration and + * notification. + * + * @author lopeznr1 */ public abstract class BaseUnitProvider implements UnitProvider { - private List myListeners; + private List listenerL; private String refName; public BaseUnitProvider(String aRefName) { - myListeners = Lists.newLinkedList(); + listenerL = new ArrayList<>(); refName = aRefName; } @@ -27,13 +42,13 @@ public abstract class BaseUnitProvider implements UnitProvider @Override public void addListener(UnitListener aListener) { - myListeners.add(aListener); + listenerL.add(aListener); } @Override - public void removeListener(UnitListener aListener) + public void delListener(UnitListener aListener) { - myListeners.remove(aListener); + listenerL.remove(aListener); } /** @@ -41,7 +56,7 @@ public abstract class BaseUnitProvider implements UnitProvider */ protected void notifyListeners() { - for (UnitListener aListener : myListeners) + for (UnitListener aListener : listenerL) aListener.unitChanged(this, refName); } diff --git a/src/glum/unit/ByteUnit.java b/src/glum/unit/ByteUnit.java index cf14be4..35a9c70 100644 --- a/src/glum/unit/ByteUnit.java +++ b/src/glum/unit/ByteUnit.java @@ -1,5 +1,25 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.unit; +import java.text.DecimalFormat; + +/** + * {@link HeuristicUnit} used to display a total count of bytes. + * + * @author lopeznr1 + */ public class ByteUnit extends HeuristicUnit { // Constants @@ -8,24 +28,44 @@ public class ByteUnit extends HeuristicUnit public static final double Gigabyte = 1024 * 1024 * 1024; public static final double Terabyte = 1024 * 1024 * 1024L * 1024L; - public ByteUnit(int numDecimalPlaces) + // Attributes + private final String nanStr; + + /** Standard Constructor */ + public ByteUnit(DecimalFormat aDispFormat, String aNaNStr) { - super("Heuristic", numDecimalPlaces); + super("Heuristic", aDispFormat); + nanStr = aNaNStr; + } + + /** Simplified Constructor */ + public ByteUnit(DecimalFormat aDispFormat) + { + this(aDispFormat, "---"); + } + + /** Alternative Constructor */ + public ByteUnit(int aNumDecimalPlaces, String aNaNStr) + { + this(UnitUtil.formFormatWithNumDecimalPlaces(aNumDecimalPlaces), aNaNStr); + } + + /** Alternative Constructor */ + public ByteUnit(int aNumDecimalPlaces) + { + this(aNumDecimalPlaces, "---"); } @Override public String getString(Object aVal) { - String aStr, unitStr; - long numBytes; - double dVal; - - aStr = "N/A"; if (aVal instanceof Number == false) - return aStr; - - numBytes = ((Number)aVal).longValue(); + return nanStr; + var numBytes = ((Number) aVal).longValue(); + + String unitStr; + double dVal; if (numBytes < Kilobyte) { unitStr = "B"; @@ -52,9 +92,9 @@ public class ByteUnit extends HeuristicUnit dVal = (numBytes + 0.0) / Terabyte; } - synchronized (format) + synchronized (dispFormat) { - return format.format(dVal) + " " + unitStr; + return dispFormat.format(dVal) + " " + unitStr; } } diff --git a/src/glum/unit/ConstUnitProvider.java b/src/glum/unit/ConstUnitProvider.java index 9854494..30360ef 100644 --- a/src/glum/unit/ConstUnitProvider.java +++ b/src/glum/unit/ConstUnitProvider.java @@ -1,21 +1,37 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.unit; +import java.io.IOException; + import glum.zio.ZinStream; import glum.zio.ZoutStream; -import java.io.IOException; - /** - * UnitProvider that always returns the same Unit. + * {@link UnitProvider} that always returns the same {@link Unit}. + * + * @author lopeznr1 */ public class ConstUnitProvider implements UnitProvider { // State vars - private Unit activeUnit; + private final Unit refUnit; + /** Standard Constructor */ public ConstUnitProvider(Unit aUnit) { - activeUnit = aUnit; + refUnit = aUnit; } @Override @@ -25,17 +41,11 @@ public class ConstUnitProvider implements UnitProvider } @Override - public void removeListener(UnitListener aListener) + public void delListener(UnitListener aListener) { ; // Nothing to do } - @Override - public String getConfigName() - { - return "Const"; - } - @Override public String getDisplayName() { @@ -45,7 +55,7 @@ public class ConstUnitProvider implements UnitProvider @Override public Unit getUnit() { - return activeUnit; + return refUnit; } @Override diff --git a/src/glum/unit/Convert.java b/src/glum/unit/Convert.java index 349fc1c..9a6f24e 100644 --- a/src/glum/unit/Convert.java +++ b/src/glum/unit/Convert.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.unit; /** Contains conversion multipliers to/from feet, yards, meters, data miles, diff --git a/src/glum/unit/DateTimeUnit.java b/src/glum/unit/DateTimeUnit.java new file mode 100644 index 0000000..239f627 --- /dev/null +++ b/src/glum/unit/DateTimeUnit.java @@ -0,0 +1,113 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.unit; + +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; + +/** + * Implementation of {@link Unit} for displaying temporal objects such as date, time, etc... + * + * @author lopeznr1 + */ +public class DateTimeUnit implements Unit +{ + // Attributes + private final String configName; + private final DateTimeFormatter formatter; + + // State vars + private String nullStr; + + /** Standard Constructor */ + public DateTimeUnit(String aConfigName, DateTimeFormatter aFormatter) + { + configName = aConfigName; + formatter = aFormatter; + + nullStr = "---"; + } + + /** Alternative Constructor */ + public DateTimeUnit(String aConfigName, String aFmtStr) + { + configName = aConfigName; + formatter = DateTimeFormatter.ofPattern(aFmtStr); + + nullStr = "---"; + } + + /** + * Sets in the string representation for null (or invalid) values + */ + public void setNaNString(String aStr) + { + nullStr = aStr; + } + + @Override + public String getConfigName() + { + return configName; + } + + @Override + public String getLabel(boolean aIsDetailed) + { + return ""; + } + + @Override + public String getString(Object aVal) + { + TemporalAccessor tmpAccessor; + + // Note we format a TemporalAccessor rather than to LocalDate,LocalTime,... + // since this is much more generic + tmpAccessor = null; + if (aVal instanceof TemporalAccessor) + tmpAccessor = (TemporalAccessor) aVal; + + // Bail if unrecognized object + if (tmpAccessor == null) + return nullStr; + + return formatter.format(tmpAccessor); + } + + @Override + public String getString(Object aVal, boolean aIsDetailed) + { + return getString(aVal); + } + + @Override + public double parseString(String aDateStr, double aErrorVal) + { + throw new UnsupportedOperationException(); + } + + @Override + public double toModel(double aVal) + { + throw new UnsupportedOperationException(); + } + + @Override + public double toUnit(double aVal) + { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/glum/unit/DateUnit.java b/src/glum/unit/DateUnit.java index a46882b..c95b975 100644 --- a/src/glum/unit/DateUnit.java +++ b/src/glum/unit/DateUnit.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.unit; import java.text.*; @@ -102,7 +115,7 @@ public class DateUnit implements Unit } @Override - public String getLabel(boolean isDetailed) + public String getLabel(boolean aIsDetailed) { return ""; } @@ -138,7 +151,7 @@ public class DateUnit implements Unit } @Override - public String getString(Object aVal, boolean isDetailed) + public String getString(Object aVal, boolean aIsDetailed) { return getString(aVal); } diff --git a/src/glum/unit/DateUnitProvider.java b/src/glum/unit/DateUnitProvider.java index e4386a6..50e6ae8 100644 --- a/src/glum/unit/DateUnitProvider.java +++ b/src/glum/unit/DateUnitProvider.java @@ -1,16 +1,29 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.unit; +import java.io.IOException; +import java.util.*; + import glum.zio.ZinStream; import glum.zio.ZoutStream; -import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.TimeZone; - -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; - +/** + * Implementation of {@link UnitProvider} that provides a unit compatible for processing chronological values. + * + * @author lopeznr1 + */ public class DateUnitProvider extends BaseUnitProvider { // Config vars @@ -20,9 +33,10 @@ public class DateUnitProvider extends BaseUnitProvider private String cfgCustomPattern; // State vars - private Map protoMap; + private Map protoM; private DateUnit activeUnit; + /** Standard Constructor */ public DateUnitProvider(String aRefName, DateUnit aActiveUnit) { super(aRefName); @@ -34,7 +48,7 @@ public class DateUnitProvider extends BaseUnitProvider cfgCustomPattern = aActiveUnit.getFormat().toPattern(); // State vars - protoMap = Maps.newHashMap(); + protoM = new HashMap<>(); activeUnit = null; // Activate the default (prototype) unit @@ -52,7 +66,7 @@ public class DateUnitProvider extends BaseUnitProvider if (cfgProtoName == null) cfgProtoName = aUnit.getConfigName(); - protoMap.put(aUnit.getConfigName(), aUnit); + protoM.put(aUnit.getConfigName(), aUnit); } /** @@ -76,7 +90,7 @@ public class DateUnitProvider extends BaseUnitProvider DateUnit protoUnit; // Ensure this proto unit is installed - protoUnit = protoMap.get(aProtoName); + protoUnit = protoM.get(aProtoName); if (protoUnit == null) throw new RuntimeException("Specified name is not installed as a prototype! aProtoName: " + aProtoName); @@ -109,7 +123,7 @@ public class DateUnitProvider extends BaseUnitProvider */ public DateUnit getProtoUnit() { - return protoMap.get(cfgProtoName); + return protoM.get(cfgProtoName); } /** @@ -117,13 +131,7 @@ public class DateUnitProvider extends BaseUnitProvider */ public List getProtoNameList() { - return Lists.newArrayList(protoMap.keySet()); - } - - @Override - public String getConfigName() - { - return activeUnit.getConfigName(); + return new ArrayList<>(protoM.keySet()); } @Override diff --git a/src/glum/unit/DecimalUnitProvider.java b/src/glum/unit/DecimalUnitProvider.java index 04e9fc4..34d2d00 100644 --- a/src/glum/unit/DecimalUnitProvider.java +++ b/src/glum/unit/DecimalUnitProvider.java @@ -1,70 +1,89 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.unit; -import glum.zio.ZinStream; -import glum.zio.ZoutStream; - import java.io.IOException; import java.text.Format; import java.text.NumberFormat; +import java.util.ArrayList; import java.util.List; -import com.google.common.collect.Lists; +import glum.zio.ZinStream; +import glum.zio.ZoutStream; +/** + * Implementation of {@link UnitProvider} that provides a unit compatible for processing numeric values. + * + * @author lopeznr1 + */ public class DecimalUnitProvider extends BaseUnitProvider { // State vars - private List protoUnitList; + private List protoUnitL; private Unit activeUnit; - + + /** Standard Constructor */ public DecimalUnitProvider(String aRefName, Unit aActiveUnit) { super(aRefName); - - protoUnitList = Lists.newLinkedList(); + + protoUnitL = new ArrayList<>(); activeUnit = aActiveUnit; } /** * Updates the activeUnit with the specified configuration */ - public void activate(Unit protoUnit, int decimalPlaces, boolean forceFullLabel) + public void activate(Unit aProtoUnit, int aDecimalPlaces, boolean aForceFullLabel) { String formatStr; // Insanity check - if (protoUnit == null) + if (aProtoUnit == null) return; // Build the format formatStr = "###,###,###,###,###,##0"; - if (decimalPlaces > 0) + if (aDecimalPlaces > 0) { formatStr += "."; - for (int c1 = 0; c1 < decimalPlaces; c1++) + for (int c1 = 0; c1 < aDecimalPlaces; c1++) formatStr += "0"; } - if (protoUnit instanceof HeuristicUnit) - activeUnit = ((HeuristicUnit)protoUnit).spawnClone(decimalPlaces); - else if (forceFullLabel == true) - activeUnit = new NumberUnit(protoUnit.getLabel(true), protoUnit.getLabel(true), protoUnit.toUnit(1), formatStr); + if (aProtoUnit instanceof HeuristicUnit) + activeUnit = ((HeuristicUnit) aProtoUnit).spawnClone(aDecimalPlaces); + else if (aForceFullLabel == true) + activeUnit = new NumberUnit(aProtoUnit.getLabel(true), aProtoUnit.getLabel(true), aProtoUnit.toUnit(1), + formatStr); else - activeUnit = new NumberUnit(protoUnit.getLabel(true), protoUnit.getLabel(false), protoUnit.toUnit(1), formatStr); - + activeUnit = new NumberUnit(aProtoUnit.getLabel(true), aProtoUnit.getLabel(false), aProtoUnit.toUnit(1), + formatStr); + notifyListeners(); } /** - * Adds in the specified Unit as a prototype (which can be used to configure - * the active unit) + * Adds in the specified Unit as a prototype (which can be used to configure the active unit) */ public void addProtoUnit(Unit aProtoUnit) { // Insanity check if (aProtoUnit instanceof HeuristicUnit == false && aProtoUnit instanceof NumberUnit == false) throw new RuntimeException("ProtoUnit must either be of type HeuristicUnit or NumberUnit"); - - protoUnitList.add(aProtoUnit); + + protoUnitL.add(aProtoUnit); } /** @@ -76,7 +95,7 @@ public class DecimalUnitProvider extends BaseUnitProvider aFormat = activeUnit.getFormat(); if (aFormat instanceof NumberFormat) - return ((NumberFormat)aFormat).getMaximumFractionDigits(); + return ((NumberFormat) aFormat).getMaximumFractionDigits(); return 0; } @@ -87,10 +106,10 @@ public class DecimalUnitProvider extends BaseUnitProvider public boolean getForceFullLabel() { String fullLabel, shortLabel; - + if (activeUnit instanceof HeuristicUnit) return false; - + fullLabel = activeUnit.getLabel(true); shortLabel = activeUnit.getLabel(false); return fullLabel.equals(shortLabel); @@ -102,11 +121,11 @@ public class DecimalUnitProvider extends BaseUnitProvider public Unit getProtoUnit() { String activeLabel, protoLabel; - + activeLabel = activeUnit.getLabel(true); - for (Unit aUnit : protoUnitList) + for (Unit aUnit : protoUnitL) { - protoLabel = aUnit.getLabel(true); + protoLabel = aUnit.getLabel(true); if (protoLabel.equals(activeLabel) == true) return aUnit; } @@ -119,13 +138,7 @@ public class DecimalUnitProvider extends BaseUnitProvider */ public List getProtoUnitList() { - return Lists.newArrayList(protoUnitList); - } - - @Override - public String getConfigName() - { - return activeUnit.getConfigName(); + return new ArrayList<>(protoUnitL); } @Override @@ -141,18 +154,18 @@ public class DecimalUnitProvider extends BaseUnitProvider int protoUnitIdx; int decimalPlaces; boolean forceFullLabel; - + // Read the stream's content aStream.readVersion(0); - + protoUnitIdx = aStream.readInt(); decimalPlaces = aStream.readInt(); forceFullLabel = aStream.readBool(); - + // Install the configuration protoUnit = null; - if (protoUnitIdx >=0 && protoUnitIdx < protoUnitList.size()) - protoUnit = protoUnitList.get(protoUnitIdx); + if (protoUnitIdx >= 0 && protoUnitIdx < protoUnitL.size()) + protoUnit = protoUnitL.get(protoUnitIdx); activate(protoUnit, decimalPlaces, forceFullLabel); } @@ -162,15 +175,15 @@ public class DecimalUnitProvider extends BaseUnitProvider int protoUnitIdx; int decimalPlaces; boolean forceFulLabel; - + // Retrieve the configuration - protoUnitIdx = protoUnitList.indexOf(getProtoUnit()); + protoUnitIdx = protoUnitL.indexOf(getProtoUnit()); decimalPlaces = getDecimalPlaces(); forceFulLabel = getForceFullLabel(); // Write the stream's contents aStream.writeVersion(0); - + aStream.writeInt(protoUnitIdx); aStream.writeInt(decimalPlaces); aStream.writeBool(forceFulLabel); diff --git a/src/glum/unit/EmptyUnitProvider.java b/src/glum/unit/EmptyUnitProvider.java index a32586b..e19bab2 100644 --- a/src/glum/unit/EmptyUnitProvider.java +++ b/src/glum/unit/EmptyUnitProvider.java @@ -1,10 +1,28 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.unit; +import java.io.IOException; + import glum.zio.ZinStream; import glum.zio.ZoutStream; -import java.io.IOException; - +/** + * {@link UnitProvider} that always returns null for the {@link Unit}. + * + * @author lopeznr1 + */ public class EmptyUnitProvider implements UnitProvider { @Override @@ -14,17 +32,11 @@ public class EmptyUnitProvider implements UnitProvider } @Override - public void removeListener(UnitListener aListener) + public void delListener(UnitListener aListener) { ; // Nothing to do } - @Override - public String getConfigName() - { - return "None"; - } - @Override public String getDisplayName() { diff --git a/src/glum/unit/HeuristicUnit.java b/src/glum/unit/HeuristicUnit.java index bb2ae28..2e560df 100644 --- a/src/glum/unit/HeuristicUnit.java +++ b/src/glum/unit/HeuristicUnit.java @@ -1,41 +1,48 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.unit; import java.text.DecimalFormat; import java.text.Format; /** - * A special kind of Unit that converts the value to the most human readable format. Thus, this Unit does not support - * any internal model, and calling associated model conversion routines will throw an - * {@link UnsupportedOperationException}.
    - *
    + * Implementation of {@link Unit} that converts the value to the most human readable format. + *

    + * This Unit does not support any internal model, and calling associated model conversion routines will throw an + * {@link UnsupportedOperationException}. + *

    * The primary method to override is getString(Object aVal) + * + * @author lopeznr1 */ public abstract class HeuristicUnit implements Unit { - // State vars - private String configName; - protected DecimalFormat format; + // Attributes + private final String configName; + protected final DecimalFormat dispFormat; - public HeuristicUnit(String aConfigName, int numDecimalPlaces) + /** Standard Constructor */ + public HeuristicUnit(String aConfigName, DecimalFormat aDispFormat) { - String aStr; - configName = aConfigName; - - aStr = "#0"; - if (numDecimalPlaces > 0) - { - aStr = "#0."; - for (int c1 = 0; c1 < numDecimalPlaces; c1++) - aStr += "0"; - } - format = new DecimalFormat(aStr); + dispFormat = aDispFormat; } /** * Spawns a near exact copy of this HeuristicUnit (only different number of decimal places) */ - public abstract HeuristicUnit spawnClone(int numDecimalPlaces); + public abstract HeuristicUnit spawnClone(int aNumDecimalPlaces); @Override public String getConfigName() @@ -46,20 +53,20 @@ public abstract class HeuristicUnit implements Unit @Override public Format getFormat() { - if (format == null) + if (dispFormat == null) return null; - return (Format)format.clone(); + return (Format) dispFormat.clone(); } @Override - public String getLabel(boolean isDetailed) + public String getLabel(boolean aIsDetailed) { return ""; } @Override - public String getString(Object aVal, boolean isDetailed) + public String getString(Object aVal, boolean aIsDetailed) { return getString(aVal); } diff --git a/src/glum/unit/LatLonUnitProvider.java b/src/glum/unit/LatLonUnitProvider.java deleted file mode 100644 index 1ebfa3a..0000000 --- a/src/glum/unit/LatLonUnitProvider.java +++ /dev/null @@ -1,173 +0,0 @@ -package glum.unit; - -import glum.zio.ZinStream; -import glum.zio.ZoutStream; - -import java.io.IOException; -import java.text.Format; -import java.text.NumberFormat; - -public class LatLonUnitProvider extends BaseUnitProvider -{ - // State vars - private Unit latUnit, lonUnit; - - public LatLonUnitProvider(String aRefName) - { - super(aRefName); - - activateStandard(false); - } - - /** - * Updates the active lat,lon unit to be configured in raw format - */ - public void activateRaw(int aDecimalPlaces, boolean aIsZeroCentered) - { - // Set in the unit - if (aIsZeroCentered == true) - { - latUnit = new ShiftedUnit("", "", 0, aDecimalPlaces); - lonUnit = new ShiftedUnit("", "", 0, aDecimalPlaces); - } - else - { - latUnit = new ShiftedUnit("", "", 90, aDecimalPlaces); - lonUnit = new ShiftedUnit("", "", 180, aDecimalPlaces); - } - - notifyListeners(); - } - - /** - * Updates the active lat,lon unit to be configured in standard format - */ - public void activateStandard(boolean aIsSecondsShown) - { - latUnit = new LatUnit(aIsSecondsShown); - lonUnit = new LonUnit(aIsSecondsShown); - notifyListeners(); - } - - /** - * Returns the number of decimal places used in the raw unit. - */ - public int getDecimalPlaces() - { - Format aFormat; - - aFormat = latUnit.getFormat(); - if (aFormat instanceof NumberFormat) - return ((NumberFormat)aFormat).getMaximumFractionDigits(); - - return 0; - } - - /** - * Returns the active lat unit - */ - public Unit getLatUnit() - { - return latUnit; - } - - /** - * Returns the active lon unit - */ - public Unit getLonUnit() - { - return lonUnit; - } - - /** - * Returns whether the raw unit is zero centered. This is true only for properly configured active raw units. - */ - public boolean isZeroCentered() - { - if (latUnit instanceof LatUnit) - return false; - - return (latUnit.toModel(0) == 0); - } - - /** - * Returns whether the active lat,lon unit is set to display in the raw format - */ - public boolean isRawUnitActive() - { - if (latUnit instanceof LatUnit) - return false; - - return true; - } - - /** - * Returns whether the active lat,lon unit will display seconds. This is true only for properly configured active - * standard units. - */ - public boolean isSecondsShown() - { - if (latUnit instanceof LatUnit) - return ((LatUnit)latUnit).isSecondsShown(); - - return false; - } - - @Override - public String getConfigName() - { - if (latUnit instanceof LatUnit) - return "Standard"; - - return "Raw"; - } - - @Override - public Unit getUnit() - { - throw new RuntimeException("Please use getLatUnit() or getLonUnit() instead."); - } - - @Override - public void zioRead(ZinStream aStream) throws IOException - { - int decimalPlaces; - boolean isRawUnit, isZeroCentered, isSecondsShown; - - // Read the stream's content - aStream.readVersion(0); - - isRawUnit = aStream.readBool(); - decimalPlaces = aStream.readInt(); - isZeroCentered = aStream.readBool(); - isSecondsShown = aStream.readBool(); - - // Install the configuration - if (isRawUnit == true) - activateRaw(decimalPlaces, isZeroCentered); - else - activateStandard(isSecondsShown); - } - - @Override - public void zioWrite(ZoutStream aStream) throws IOException - { - int decimalPlaces; - boolean isRawUnit, isZeroCentered, isSecondsShown; - - // Retrieve the configuration - isRawUnit = isRawUnitActive(); - decimalPlaces = getDecimalPlaces(); - isZeroCentered = isZeroCentered(); - isSecondsShown = isSecondsShown(); - - // Write the stream's contents - aStream.writeVersion(0); - - aStream.writeBool(isRawUnit); - aStream.writeInt(decimalPlaces); - aStream.writeBool(isZeroCentered); - aStream.writeBool(isSecondsShown); - } - -} diff --git a/src/glum/unit/LatUnit.java b/src/glum/unit/LatUnit.java deleted file mode 100644 index 6228e79..0000000 --- a/src/glum/unit/LatUnit.java +++ /dev/null @@ -1,94 +0,0 @@ -package glum.unit; - -import java.text.*; - -import glum.coord.*; - -public class LatUnit implements Unit -{ - // State vars - private boolean isSecondsShown; - - /** - * Constructor - */ - public LatUnit(boolean aIsSecondsShown) - { - isSecondsShown = aIsSecondsShown; - } - - /** - * isSecondsShown - */ - public boolean isSecondsShown() - { - return isSecondsShown; - } - - @Override - public String getConfigName() - { - return "Lat"; - } - - @Override - public Format getFormat() - { - return null; - } - - @Override - public String getLabel(boolean isDetailed) - { - return ""; - } - - @Override - public String getString(Object aObj) - { - Double aVal; - - if (aObj instanceof Number) - aVal = ((Number)aObj).doubleValue(); -/* else if (aObj instanceof LatLon) - aVal = ((LatLon)aVal).lat; -*/ else - return "N/A"; - - return CoordUtil.LatToString(aVal, isSecondsShown); - } - - @Override - public String getString(Object aObj, boolean isDetailed) - { - Double aVal; - - if (aObj instanceof Number) - aVal = ((Number)aObj).doubleValue(); -/* else if (aObj instanceof LatLon) - aVal = ((LatLon)aVal).lat; -*/ else - return "N/A"; - - return CoordUtil.LatToString(aVal, isSecondsShown); - } - - @Override - public double parseString(String aStr, double eVal) - { - return CoordUtil.StringToLat(aStr); - } - - @Override - public double toModel(double aVal) - { - return aVal; - } - - @Override - public double toUnit(double aVal) - { - return aVal; - } - -} diff --git a/src/glum/unit/LonUnit.java b/src/glum/unit/LonUnit.java deleted file mode 100644 index eb1caca..0000000 --- a/src/glum/unit/LonUnit.java +++ /dev/null @@ -1,94 +0,0 @@ -package glum.unit; - -import java.text.*; - -import glum.coord.*; - -public class LonUnit implements Unit -{ - // State vars - private boolean isSecondsShown; - - /** - * Constructor - */ - public LonUnit(boolean aIsSecondsShown) - { - isSecondsShown = aIsSecondsShown; - } - - /** - * isSecondsShown - */ - public boolean isSecondsShown() - { - return isSecondsShown; - } - - @Override - public String getConfigName() - { - return "Lon"; - } - - @Override - public Format getFormat() - { - return null; - } - - @Override - public String getLabel(boolean isDetailed) - { - return ""; - } - - @Override - public String getString(Object aObj) - { - Double aVal; - - if (aObj instanceof Number) - aVal = ((Number)aObj).doubleValue(); -/* else if (aObj instanceof LatLon) - aVal = ((LatLon)aVal).lon; -*/ else - return "N/A"; - - return CoordUtil.LonToString(aVal, isSecondsShown); - } - - @Override - public String getString(Object aObj, boolean isDetailed) - { - Double aVal; - - if (aObj instanceof Number) - aVal = ((Number)aObj).doubleValue(); -/* else if (aObj instanceof LatLon) - aVal = ((LatLon)aVal).lon; -*/ else - return "N/A"; - - return CoordUtil.LonToString(aVal, isSecondsShown); - } - - @Override - public double parseString(String aStr, double eVal) - { - return CoordUtil.StringToLon(aStr); - } - - @Override - public double toModel(double aVal) - { - return aVal; - } - - @Override - public double toUnit(double aVal) - { - return aVal; - } - -} diff --git a/src/glum/unit/NumberInverseUnit.java b/src/glum/unit/NumberInverseUnit.java index 7235eb8..e492e3b 100644 --- a/src/glum/unit/NumberInverseUnit.java +++ b/src/glum/unit/NumberInverseUnit.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.unit; import java.text.DecimalFormat;; @@ -28,17 +41,17 @@ public class NumberInverseUnit extends NumberUnit if (aVal instanceof Number == false) return aStr; - if (format == null) + if (dispFormat == null) return "" + ((1.0/((Number)aVal).doubleValue()) * conversionFactor); - synchronized (format) + synchronized (dispFormat) { - return format.format((1.0/((Number)aVal).doubleValue()) * conversionFactor); + return dispFormat.format((1.0/((Number)aVal).doubleValue()) * conversionFactor); } } @Override - public String getString(Object aVal, boolean isDetailed) + public String getString(Object aVal, boolean aIsDetailed) { if (aVal instanceof Number == false) return "N/A"; diff --git a/src/glum/unit/NumberUnit.java b/src/glum/unit/NumberUnit.java index 3c00119..b345e76 100644 --- a/src/glum/unit/NumberUnit.java +++ b/src/glum/unit/NumberUnit.java @@ -1,57 +1,60 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.unit; -import glum.gui.GuiUtil; +import java.text.DecimalFormat; +import java.text.Format; -import java.text.*; +import glum.io.ParseUtil; +/** + * Implementation of {@link Unit} for displaying decimal values. + * + * @author lopeznr1 + */ public class NumberUnit implements Unit { // State vars - protected DecimalFormat format; + protected DecimalFormat dispFormat; protected String nanStr; protected String fullLabel; protected String shortLabel; protected double conversionFactor; - /** - * Constructor - */ + /** Standard Constructor */ + public NumberUnit(String aFullLabel, String aShortLabel, double aConversionFactor, DecimalFormat aDispFormat) + { + nanStr = "---"; + fullLabel = aFullLabel; + shortLabel = aShortLabel; + conversionFactor = aConversionFactor; + dispFormat = aDispFormat; + } + + public NumberUnit(String aFullLabel, String aShortLabel, double aConversionFactor, String aFormatStr) + { + this(aFullLabel, aShortLabel, aConversionFactor, new DecimalFormat(aFormatStr)); + } + public NumberUnit(String aFullLabel, String aShortLabel, double aConversionFactor) { - this(aFullLabel, aShortLabel, aConversionFactor, (DecimalFormat)null); + this(aFullLabel, aShortLabel, aConversionFactor, (DecimalFormat) null); } - public NumberUnit(String aFullLabel, String aShortLabel, double aConversionFactor, String aDecimalFormatStr) + public NumberUnit(String aFullLabel, String aShortLabel, double aConversionFactor, int aNumDecimalPlaces) { - this(aFullLabel, aShortLabel, aConversionFactor, new DecimalFormat(aDecimalFormatStr)); - } - - public NumberUnit(String aFullLabel, String aShortLabel, double aConversionFactor, DecimalFormat aFormat) - { - nanStr = "---"; - fullLabel = aFullLabel; - shortLabel = aShortLabel; - conversionFactor = aConversionFactor; - format = aFormat; - } - - public NumberUnit(String aFullLabel, String aShortLabel, double aConversionFactor, int numDecimalPlaces) - { - String aStr; - - nanStr = "---"; - fullLabel = aFullLabel; - shortLabel = aShortLabel; - conversionFactor = aConversionFactor; - - aStr = "#0"; - if (numDecimalPlaces > 0) - { - aStr = "#0."; - for (int c1 = 0; c1 < numDecimalPlaces; c1++) - aStr += "0"; - } - format = new DecimalFormat(aStr); + this(aFullLabel, aShortLabel, aConversionFactor, UnitUtil.formFormatWithNumDecimalPlaces(aNumDecimalPlaces)); } /** @@ -59,12 +62,12 @@ public class NumberUnit implements Unit */ public boolean isFloating() { - if (format != null && format.getMaximumFractionDigits() == 0) + if (dispFormat != null && dispFormat.getMaximumFractionDigits() == 0) return false; return true; -//System.out.println("NumFracDigits:" + format.getMaximumFractionDigits()); +//System.out.println("NumFracDigits:" + format.getMaximumFractionDigits()); // return format.isParseIntegerOnly(); } @@ -79,10 +82,10 @@ public class NumberUnit implements Unit @Override public Format getFormat() { - if (format == null) + if (dispFormat == null) return null; - return (Format)format.clone(); + return (Format) dispFormat.clone(); } @Override @@ -92,9 +95,9 @@ public class NumberUnit implements Unit } @Override - public String getLabel(boolean isDetailed) + public String getLabel(boolean aIsDetailed) { - if (isDetailed == true) + if (aIsDetailed == true) return fullLabel; else return shortLabel; @@ -106,27 +109,27 @@ public class NumberUnit implements Unit if (aVal instanceof Number == false) return nanStr; - double doubleVal = ((Number)aVal).doubleValue(); + double doubleVal = ((Number) aVal).doubleValue(); if (Double.isNaN(doubleVal) == true) return nanStr; - if (format == null) + if (dispFormat == null) return "" + doubleVal * conversionFactor; - synchronized (format) + synchronized (dispFormat) { - return format.format(doubleVal * conversionFactor); + return dispFormat.format(doubleVal * conversionFactor); } } @Override - public String getString(Object aVal, boolean isDetailed) + public String getString(Object aVal, boolean aIsDetailed) { // Delegate String retStr = getString(aVal); // Add the label component - if (isDetailed == true) + if (aIsDetailed == true) retStr += " " + fullLabel; else retStr += " " + shortLabel; @@ -139,7 +142,7 @@ public class NumberUnit implements Unit { double aVal; - aVal = GuiUtil.readDouble(aStr, Double.NaN); + aVal = ParseUtil.readDouble(aStr, Double.NaN); return toModel(aVal); } diff --git a/src/glum/unit/ShiftedUnit.java b/src/glum/unit/ShiftedUnit.java index e06aa3d..0a9cea2 100644 --- a/src/glum/unit/ShiftedUnit.java +++ b/src/glum/unit/ShiftedUnit.java @@ -1,7 +1,25 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.unit; -import java.text.*; +import java.text.Format; +/** + * Implementation of {@link Unit} for displaying values shifted by a constant value. + * + * @author lopeznr1 + */ public class ShiftedUnit implements Unit { // State vars @@ -11,9 +29,7 @@ public class ShiftedUnit implements Unit private String shortLabel; private double deltaValue; - /** - * Constructor - */ + /** Standard Constructor */ public ShiftedUnit(String aFullLabel, String aShortLabel, double aDeltaValue, Format aFormat) { nanStr = "---"; @@ -22,23 +38,9 @@ public class ShiftedUnit implements Unit deltaValue = aDeltaValue; format = aFormat; } - public ShiftedUnit(String aFullLabel, String aShortLabel, double aDeltaValue, int numDecimalPlaces) + public ShiftedUnit(String aFullLabel, String aShortLabel, double aDeltaValue, int aNumDecimalPlaces) { - String aStr; - - aStr = "#0"; - if (numDecimalPlaces > 0) - { - aStr = "#0."; - for (int c1 = 0; c1 < numDecimalPlaces; c1++) - aStr += "0"; - } - - nanStr = "---"; - fullLabel = aFullLabel; - shortLabel = aShortLabel; - deltaValue = aDeltaValue; - format = new DecimalFormat(aStr); + this(aFullLabel, aShortLabel, aDeltaValue, UnitUtil.formFormatWithNumDecimalPlaces(aNumDecimalPlaces)); } @Override @@ -57,9 +59,9 @@ public class ShiftedUnit implements Unit } @Override - public String getLabel(boolean isDetailed) + public String getLabel(boolean aIsDetailed) { - if (isDetailed == true) + if (aIsDetailed == true) return fullLabel; else return shortLabel; @@ -88,7 +90,7 @@ public class ShiftedUnit implements Unit } @Override - public String getString(Object aObj, boolean isDetailed) + public String getString(Object aObj, boolean aIsDetailed) { double aVal; @@ -102,7 +104,7 @@ public class ShiftedUnit implements Unit if (format == null) { - if (isDetailed == true) + if (aIsDetailed == true) return "" + (aVal + deltaValue) + " " + fullLabel; else return "" + (aVal + deltaValue) + " " + shortLabel; @@ -110,7 +112,7 @@ public class ShiftedUnit implements Unit synchronized (format) { - if (isDetailed == true) + if (aIsDetailed == true) return format.format(Double.valueOf(aVal + deltaValue)) + " " + fullLabel; else return format.format(Double.valueOf(aVal + deltaValue)) + " " + shortLabel; diff --git a/src/glum/unit/TimeCountUnit.java b/src/glum/unit/TimeCountUnit.java index d84c573..78aacc9 100644 --- a/src/glum/unit/TimeCountUnit.java +++ b/src/glum/unit/TimeCountUnit.java @@ -1,12 +1,39 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.unit; -import static glum.util.TimeConst.*; +import static glum.util.TimeConst.MS_IN_DAY; +import static glum.util.TimeConst.MS_IN_HOUR; +import static glum.util.TimeConst.MS_IN_MIN; +import static glum.util.TimeConst.MS_IN_SEC; + +import java.text.DecimalFormat; +import java.time.Duration; + import glum.util.WallTimer; /** - * Unit used to display a total count of time. This Unit is not configurable and only numerical values should be passed - * in where each increment represents 1 millisecond. If a WallTimer is passed in then this Unit will display it's total - * time. + * {@link HeuristicUnit} used to display a total count of time. + *

    + * This unit supports three types of inputs: + *

      + *
    • Numerical values - where each increment represents 1 millisecond. + *
    • {@link WallTimer} - where this unit will display it's total time. + *
    • {@link Duration} + *
    + * + * @author lopeznr1 */ public class TimeCountUnit extends HeuristicUnit { @@ -15,43 +42,53 @@ public class TimeCountUnit extends HeuristicUnit private static final long MAX_SEC_BEFORE_FMT_HOUR = 2 * MS_IN_HOUR; private static final long MAX_SEC_BEFORE_FMT_MIN = 2 * MS_IN_MIN; - // State vars - private String nanStr; + // Attributes + private final String nanStr; - public TimeCountUnit(int numDecimalPlaces) + /** Standard Constructor */ + public TimeCountUnit(DecimalFormat aDispFormat, String aNanStr) { - super("Heuristic", numDecimalPlaces); - nanStr = "---"; + super("Heuristic", aDispFormat); + nanStr = aNanStr; + } + + /** Alternative Constructor */ + public TimeCountUnit(int aNumDecimalPlaces, String aNanStr) + { + this(UnitUtil.formFormatWithNumDecimalPlaces(aNumDecimalPlaces), aNanStr); + } + + /** Simplified Constructor */ + public TimeCountUnit(int aNumDecimalPlaces) + { + this(UnitUtil.formFormatWithNumDecimalPlaces(aNumDecimalPlaces), "---"); } @Override public String getString(Object aObj) { - double numMS; - String aStr; - // Transform WallTimers to their total count if (aObj instanceof WallTimer) - aObj = ((WallTimer)aObj).getTotal(); + aObj = ((WallTimer) aObj).getTotal(); // We need a number - if (aObj instanceof Number == false) - return "N/A"; + if (aObj instanceof Duration) + aObj = ((Duration) aObj).toMillis(); + else if (aObj instanceof Number == false) + return nanStr; - numMS = ((Number)aObj).doubleValue(); + var numMS = ((Number) aObj).doubleValue(); if (Double.isNaN(numMS) == true) return nanStr; if (numMS > MAX_SEC_BEFORE_FMT_DAY) - aStr = format.format(numMS / MS_IN_DAY) + " days"; + return dispFormat.format(numMS / MS_IN_DAY) + " days"; else if (numMS > MAX_SEC_BEFORE_FMT_HOUR) - aStr = format.format(numMS / MS_IN_HOUR) + " hrs"; + return dispFormat.format(numMS / MS_IN_HOUR) + " hrs"; else if (numMS > MAX_SEC_BEFORE_FMT_MIN) - aStr = format.format(numMS / MS_IN_MIN) + " min"; + return dispFormat.format(numMS / MS_IN_MIN) + " min"; else - aStr = format.format(numMS / MS_IN_SEC) + " sec"; - - return aStr; + return dispFormat.format(numMS / MS_IN_SEC) + " sec"; } @Override diff --git a/src/glum/unit/Unit.java b/src/glum/unit/Unit.java index 793474a..d546a42 100644 --- a/src/glum/unit/Unit.java +++ b/src/glum/unit/Unit.java @@ -1,7 +1,28 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.unit; -import java.text.*; +import java.text.Format; +/** + * Interface that provides a mechanism of transforming model values to a user oriented string. + *

    + * Transforming a user oriented string back to a (numerical) model value is supported via + * {@link #parseString(String, double)}. + * + * @author lopeznr1 + */ public interface Unit { /** @@ -12,29 +33,29 @@ public interface Unit /** * Returns Format object associated with the unit. */ - public Format getFormat(); + public default Format getFormat() + { + return null; + } /** * Returns the label associated with the unit. */ - public String getLabel(boolean isDetailed); - + public String getLabel(boolean aIsDetailed); + /** - * Returns string representation of aVal w.r.t this unit without the - * associated label. + * Returns string representation of aVal w.r.t this unit without the associated label. */ public String getString(Object aVal); /** - * Returns string representation of aVal w.r.t this unit with the associated - * (detailed if isDetailed == true) label. + * Returns string representation of aVal w.r.t this unit with the associated (detailed if isDetailed == true) label. */ - public String getString(Object aVal, boolean isDetailed); + public String getString(Object aVal, boolean aIsDetailed); /** - * Returns the model value which corresponds to the specified input string. - * The input string should be in this unit. If no value can be parsed then - * eVal is returned. + * Returns the model value which corresponds to the specified input string. The input string should be in this unit. + * If no value can be parsed then eVal is returned. */ public double parseString(String aStr, double eVal); @@ -44,8 +65,7 @@ public interface Unit public double toModel(double aVal); /** - * Returns aVal in terms of this unit. Note aVal is assumed to be in model - * units. + * Returns aVal in terms of this unit. Note aVal is assumed to be in model units. */ public double toUnit(double aVal); diff --git a/src/glum/unit/UnitListener.java b/src/glum/unit/UnitListener.java index 48d6986..f781b75 100644 --- a/src/glum/unit/UnitListener.java +++ b/src/glum/unit/UnitListener.java @@ -1,9 +1,27 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.unit; +/** + * Interface that provides for the callback mechanism for notification of unit changes. + * + * @author lopeznr1 + */ public interface UnitListener { /** - * Event callback for notification of whenever a Unit changes in aManager + * Notification method that the unit has changed. */ public void unitChanged(UnitProvider aProvider, String aKey); diff --git a/src/glum/unit/UnitProvider.java b/src/glum/unit/UnitProvider.java index be070d6..db2700a 100644 --- a/src/glum/unit/UnitProvider.java +++ b/src/glum/unit/UnitProvider.java @@ -1,23 +1,37 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.unit; import glum.zio.ZioObj; +/** + * Interface that provides a mechanism for accessing a {@link Unit} and getting notification when that {@link Unit} has + * changed. + * + * @author lopeznr1 + */ public interface UnitProvider extends ZioObj { /** * Adds a Listener for Unit changes */ public void addListener(UnitListener aListener); - + /** * Removes a Listener for Unit changes */ - public void removeListener(UnitListener aListener); - - /** - * Returns the name of the current configuration - */ - public String getConfigName(); + public void delListener(UnitListener aListener); /** * Returns the official name name associated with the Unit @@ -25,7 +39,7 @@ public interface UnitProvider extends ZioObj public String getDisplayName(); /** - * Returns the Unit associated with this provider + * Returns the {@link Unit} associated with this provider */ public Unit getUnit(); diff --git a/src/glum/unit/UnitUtil.java b/src/glum/unit/UnitUtil.java new file mode 100644 index 0000000..b7faba4 --- /dev/null +++ b/src/glum/unit/UnitUtil.java @@ -0,0 +1,43 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.unit; + +import java.text.DecimalFormat; + +/** + * Collection of utility methods to support working with {@link Unit}s. + * + * @author lopeznr1 + */ +public class UnitUtil +{ + /** + * Utility method to create a {@link DecimalFormat} that has the specified number of decimal places + */ + public static DecimalFormat formFormatWithNumDecimalPlaces(int aNumDecimalPlaces) + { + String tmpStr; + + tmpStr = "#0"; + if (aNumDecimalPlaces > 0) + { + tmpStr = "#0."; + for (int c1 = 0; c1 < aNumDecimalPlaces; c1++) + tmpStr += "0"; + } + + return new DecimalFormat(tmpStr); + } + +} diff --git a/src/glum/util/DateUtil.java b/src/glum/util/DateUtil.java index abed8c9..422157b 100644 --- a/src/glum/util/DateUtil.java +++ b/src/glum/util/DateUtil.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.util; import java.util.*; @@ -9,7 +22,7 @@ public class DateUtil /** * Returns the date as a double that best describes the currDate position relative to startDate and endDate. - *

    + *

    * Return should be between 0 - 1. */ public static double computeFractionalPosition(Calendar startDate, Calendar currDate, Calendar endDate) diff --git a/src/glum/util/ImageUtil.java b/src/glum/util/ImageUtil.java index c0c580f..bd722aa 100644 --- a/src/glum/util/ImageUtil.java +++ b/src/glum/util/ImageUtil.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.util; import java.awt.image.BufferedImage; diff --git a/src/glum/util/MathUtil.java b/src/glum/util/MathUtil.java index 67d865b..814c3c0 100644 --- a/src/glum/util/MathUtil.java +++ b/src/glum/util/MathUtil.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.util; import java.awt.Dimension; diff --git a/src/glum/util/ThreadUtil.java b/src/glum/util/ThreadUtil.java index 0216f11..f2a01ab 100644 --- a/src/glum/util/ThreadUtil.java +++ b/src/glum/util/ThreadUtil.java @@ -1,15 +1,80 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.util; -import glum.task.Task; - import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.InvocationTargetException; import javax.swing.SwingUtilities; +import glum.task.Task; + +/** + * Collection of thread utility methods. + *

    + * The provided utility methods fall under the following classes: + *

      + *
    • Transforming a stack trace into a string. + *
    • Scheduling a {@link Runnable} on a separate thread, the AWT, or as a shutdown hook. + *
    • Sleeping / waiting on a thread. + *
    + * + * @author lopeznr1 + */ public class ThreadUtil { + /** + * Utility method to register the runnable into the JVM's shutdown hook. + */ + public static void addShutdownHook(Runnable aRunnable) + { + Thread tmpThread = new Thread(aRunnable); + Runtime.getRuntime().addShutdownHook(tmpThread); + } + + /** + * Utility method to print the stack trace of aExp to a string + */ + public static String getStackTrace(Throwable aThrowable) + { + StringBuilder tmpSB = new StringBuilder(); + tmpSB.append(aThrowable.getClass().getName() + "\n"); + tmpSB.append("Msg: " + aThrowable.getMessage() + "\n"); + + // Print out the stack trace + for (StackTraceElement aItem : aThrowable.getStackTrace()) + tmpSB.append(" " + aItem.toString() + "\n"); + + // Print out any cause + // TODO + + return tmpSB.toString(); + } + + /** + * Utility method to print the stack trace of aExp to a string exactly as {@link Throwable#printStackTrace} + */ + public static String getStackTraceClassic(Throwable aThrowable) + { + StringWriter tmpSW = new StringWriter(); + PrintWriter tmpPW = new PrintWriter(tmpSW); + aThrowable.printStackTrace(tmpPW); + + tmpPW.close(); + return tmpSW.toString(); + } /** * Utility method to execute aRunnable synchronously on the AWT event dispatching thread with out throwing an @@ -25,11 +90,11 @@ public class ThreadUtil SwingUtilities.invokeAndWait(aRunnable); break; } - catch(InterruptedException aExp) + catch (InterruptedException aExp) { aExp.printStackTrace(); } - catch(InvocationTargetException aExp) + catch (InvocationTargetException aExp) { // This is an unrecoverable exception throw new RuntimeException(aExp); @@ -42,29 +107,25 @@ public class ThreadUtil */ public static void launchRunnable(Runnable aRunnable, String threadName) { - Thread aThread; - - aThread = new Thread(aRunnable, threadName); - aThread.start(); + Thread tmpThread = new Thread(aRunnable, threadName); + tmpThread.start(); } /** * Utility method to suspend the current thread for numMS milliseconds. - * + * * @param aTask - * This method will return prematurely if aTask is no longer active. + * This method will return prematurely if aTask is no longer active. */ public static void safeSleep(long numMS, Task aTask) { - long wakeTime; - - wakeTime = System.currentTimeMillis() + numMS; + long wakeTime = System.currentTimeMillis() + numMS; sleepUntilTime(wakeTime, aTask); } /** * Utility method to sleep for numMS milliseconds without throwing an {@link InterruptedException}. - * + * * @return True: if the thread was interrupted */ public static boolean safeSleep(long numMS) @@ -73,18 +134,18 @@ public class ThreadUtil { Thread.sleep(numMS); } - catch(InterruptedException aExp) + catch (InterruptedException aExp) { return true; } return false; } - + /** * Utility method to wait for a signal without throwing an {@link InterruptedException}. - * + * * @see Thread#wait() - * + * * @return True: if the thread was interrupted */ public static boolean safeWait(Object aLock) @@ -93,7 +154,7 @@ public class ThreadUtil { aLock.wait(); } - catch(InterruptedException aExp) + catch (InterruptedException aExp) { return true; } @@ -102,9 +163,9 @@ public class ThreadUtil /** * Utility method to wait for a signal without throwing an {@link InterruptedException}. - * + * * @see Thread#wait(long) - * + * * @return True: if the thread was interrupted */ public static boolean safeWait(Object aLock, long aMaxTimeMS) @@ -113,7 +174,7 @@ public class ThreadUtil { aLock.wait(aMaxTimeMS); } - catch(InterruptedException aExp) + catch (InterruptedException aExp) { return true; } @@ -122,20 +183,17 @@ public class ThreadUtil /** * Utility method to suspend the current thread until the system time is at or has passed nextWakeTime - * + * * @return True: if the thread was interrupted */ public static boolean sleepUntilTime(long nextWakeTime) { - long currTime, sleepTime; - boolean isInterrupt; - - currTime = System.currentTimeMillis(); + long currTime = System.currentTimeMillis(); while (currTime < nextWakeTime) { - sleepTime = nextWakeTime - currTime; + long sleepTime = nextWakeTime - currTime; - isInterrupt = ThreadUtil.safeSleep(sleepTime); + boolean isInterrupt = ThreadUtil.safeSleep(sleepTime); if (isInterrupt == true) return true; @@ -147,59 +205,20 @@ public class ThreadUtil /** * Utility method to suspend the current thread until the system time is at or has passed nextWakeTime - * + * * @param aTask - * This method will return prematurely if aTask is no longer active. + * This method will return prematurely if aTask is no longer active. */ public static void sleepUntilTime(long nextWakeTime, Task aTask) { - long currTime, sleepTime; - - currTime = System.currentTimeMillis(); + long currTime = System.currentTimeMillis(); while (currTime < nextWakeTime && aTask.isActive() == true) { - sleepTime = nextWakeTime - currTime; + long sleepTime = nextWakeTime - currTime; ThreadUtil.safeSleep(sleepTime); currTime = System.currentTimeMillis(); } } - /** - * Utility method to print the stack trace of aExp to a string - */ - public static String getStackTrace(Throwable aThrowable) - { - StringBuilder strBuf; - - strBuf = new StringBuilder(); - strBuf.append(aThrowable.getClass().getName() + "\n"); - strBuf.append("Msg: " + aThrowable.getMessage() + "\n"); - - // Print out the stack trace - for (StackTraceElement aItem : aThrowable.getStackTrace()) - strBuf.append(" " + aItem.toString() + "\n"); - - // Print out any cause - // TODO - - return strBuf.toString(); - } - - /** - * Utility method to print the stack trace of aExp to a string exactly as {@link Throwable#printStackTrace} - */ - public static String getStackTraceClassic(Throwable aThrowable) - { - StringWriter stringWriter; - PrintWriter printWriter; - - stringWriter = new StringWriter(); - printWriter = new PrintWriter(stringWriter); - aThrowable.printStackTrace(printWriter); - - printWriter.close(); - return stringWriter.toString(); - } - } diff --git a/src/glum/util/TimeConst.java b/src/glum/util/TimeConst.java index 280423b..dc67972 100644 --- a/src/glum/util/TimeConst.java +++ b/src/glum/util/TimeConst.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.util; public class TimeConst diff --git a/src/glum/util/WallTimer.java b/src/glum/util/WallTimer.java index f70d381..2181ae5 100644 --- a/src/glum/util/WallTimer.java +++ b/src/glum/util/WallTimer.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.util; public class WallTimer diff --git a/src/glum/version/PlainVersion.java b/src/glum/version/PlainVersion.java new file mode 100644 index 0000000..d7447f7 --- /dev/null +++ b/src/glum/version/PlainVersion.java @@ -0,0 +1,126 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.version; + +/** + * Provides the standard implementation of the {@link Version} interface. + * + * @author lopeznr1 + */ +public class PlainVersion implements Version +{ + // Constants + public static PlainVersion AbsMin = new PlainVersion(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); + public static PlainVersion AbsMax = new PlainVersion(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); + public static PlainVersion Zero = new PlainVersion(0, 0, 0); + + // Attributes + private final int major; + private final int minor; + private final int patch; + + /** Standard Constructor */ + public PlainVersion(int aMajor, int aMinor, int aPatch) + { + major = aMajor; + minor = aMinor; + patch = aPatch; + } + + /** + * Forms a PlainVersion from the specified string. The version should have have at most 3 integer components + * separated by the char: '.'. Any extra components after the first 3 will be ignored. + *

    + * If any of the first 3 components are not integers then null will be returned. + */ + public static PlainVersion parse(String aStr) + { + var tokenArr = aStr.split("\\."); + + int major = 0, minor = 0, patch = 0; + try + { + major = Integer.parseInt(tokenArr[0]); + if (tokenArr.length >= 2) + minor = Integer.parseInt(tokenArr[1]); + if (tokenArr.length >= 3) + patch = Integer.parseInt(tokenArr[2]); + } + catch (NumberFormatException aExp) + { + return null; + } + + return new PlainVersion(major, minor, patch); + } + + @Override + public int major() + { + return major; + } + + @Override + public int minor() + { + return minor; + } + + @Override + public int patch() + { + return patch; + } + + @Override + public String toString() + { + var retStr = "" + major + "." + minor; + if (patch != 0) + retStr += "." + patch; + + return retStr; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + PlainVersion other = (PlainVersion) obj; + if (major != other.major) + return false; + if (minor != other.minor) + return false; + if (patch != other.patch) + return false; + return true; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + major; + result = prime * result + minor; + result = prime * result + patch; + return result; + } + +} diff --git a/src/glum/version/Version.java b/src/glum/version/Version.java new file mode 100644 index 0000000..d80ffdd --- /dev/null +++ b/src/glum/version/Version.java @@ -0,0 +1,49 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.version; + +/** + * Interface which provides access to version components (major, minor, patch). + *

    + * Each component is modeled as an integer and it is assumed that higher values correspond to more developed software. + *

    + * Reference: https://semver.org/ + *

    + * Implementors of this interface should be immutable. + * + * @author lopeznr1 + */ +public interface Version +{ + // Constants + public static Version AbsMin = PlainVersion.AbsMin; + public static Version AbsMax = PlainVersion.AbsMax; + public static Version Zero = PlainVersion.Zero; + + /** + * Returns the major version component. + */ + public int major(); + + /** + * Returns the minor version component. + */ + public int minor(); + + /** + * Returns the patch version component. + */ + public int patch(); + +} diff --git a/src/glum/version/VersionUtils.java b/src/glum/version/VersionUtils.java new file mode 100644 index 0000000..64cd50b --- /dev/null +++ b/src/glum/version/VersionUtils.java @@ -0,0 +1,109 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.version; + +/** + * Utility class that allows for comparing Versions. + *

    + * Eventually when Java allows operator overloading then this class can go away since the standard mathematical + * comparison symbols would be much clearer. + * + * @author lopeznr1 + */ +public class VersionUtils +{ + /** + * Utility method that returns true if aVerA occurs after aVerB + */ + public static boolean isAfter(Version aVerA, Version aVerB) + { + int majorA = aVerA.major(); + int minorA = aVerA.minor(); + int patchA = aVerA.patch(); + int majorB = aVerB.major(); + int minorB = aVerB.minor(); + int patchB = aVerB.patch(); + + if (majorA > majorB) + return true; + if (majorA == majorB && minorA > minorB) + return true; + if (majorA == majorB && minorA == minorB && patchA > patchB) + return true; + + return false; + } + + /** + * Utility method that returns true if aVerA occurs after aVerB + */ + public static boolean isAfterOrEqual(Version aVerA, Version aVerB) + { + // Delegate to isAfter + return isAfter(aVerB, aVerA) == false; + } + + /** + * Utility method that returns true if the following statement is true: + *

    + * aVerEval >= aVerMin && aVerEval <= aVerMax + *

    + * A LogicError will be thrown if the aVerMin and aVerMax are inverted (aVerMin > aVerMax) + */ + public static boolean isInRange(Version aVerEval, Version aVerMin, Version aVerMax) + { + // Ensure the endpoints are not inverted + if (isAfter(aVerMin, aVerMax) == true) + throw new RuntimeException("Min/Max versions appear to be swapped. min: " + aVerMin + " max: " + aVerMax); + + // Decompose and delegate + if (isAfter(aVerMin, aVerEval) == true) + return false; + if (isAfter(aVerEval, aVerMax) == true) + return false; + + return true; + } + + /** + * Utility method to allow the comparison of two versions. + * + * @param aVerA + * @param aVerB + * @return + */ + public static int compare(Version aVerA, Version aVerB) + { + + int majorA = aVerA.major(); + int minorA = aVerA.minor(); + int patchA = aVerA.patch(); + int majorB = aVerB.major(); + int minorB = aVerB.minor(); + int patchB = aVerB.patch(); + + int cmpVal; + cmpVal = majorA - majorB; + if (cmpVal != 0) + return cmpVal; + cmpVal = minorA - minorB; + if (cmpVal != 0) + return cmpVal; + cmpVal = patchA - patchB; + if (cmpVal != 0) + return cmpVal; + + return 0; + } +} diff --git a/src/glum/zio/NullableZRS.java b/src/glum/zio/NullableZRS.java new file mode 100644 index 0000000..397e5b7 --- /dev/null +++ b/src/glum/zio/NullableZRS.java @@ -0,0 +1,108 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.zio; + +import java.io.IOException; +import java.lang.reflect.Constructor; + +/** + * Generic implementation of {@link ZioSpawner} that supports "null" items. + *

    + * This implementation will support serialization of null (or invalid) items. When the deserialization process occurs a + * previously serialized null item will result in the corresponding null (or invalid) item being returned. + * + * @author lopeznr1 + */ +public class NullableZRS implements ZioSpawner +{ + // Attributes + private final Class refClass; + private final G1 refInvalidItem; + + /** + * Standard Constructor + * + * @param aClass + * Reference class associated with the items to be serialized by this {@link ZioSpawner}. + * + * @param aInvalidItem + * Null equivalent object. This may be null or an (immutable) object that stands for "invalid" values. + */ + public NullableZRS(Class aClass, G1 aInvalidItem) + { + refClass = aClass; + refInvalidItem = aInvalidItem; + } + + /** + * Simplified Constructor + *

    + * Serialized values of null (or a previously invalid item) will result in null being returned. + */ + public NullableZRS(Class aClass) + { + this(aClass, null); + } + + @Override + public G1 readInstance(ZinStream aStream) throws IOException + { + aStream.readVersion(0); + + byte type = aStream.readByte(); + if (type == 0) + return refInvalidItem; + + // Locate an appropriate Constructor + Constructor tmpConstructor = null; + try + { + tmpConstructor = refClass.getConstructor(ZinStream.class); + } + catch (NoSuchMethodException | SecurityException aExp) + { + throw new IOException("Failed to locate a proper constructor. Constuctor requires " // + + "a single argument - ZioStream", aExp); + } + + // Serialize the class + try + { + G1 retItem = tmpConstructor.newInstance(aStream); + return retItem; + } + catch (Exception aExp) + { + throw new IOException("Failed to instantiate: " + refClass, aExp); + } + + } + + @Override + public void writeInstance(ZoutStream aStream, G1 aItem) throws IOException + { + aStream.writeVersion(0); + + byte type = 0; + if (aItem != null && aItem != refInvalidItem) + type = 1; + + aStream.writeByte(type); + if (type == 0) + return; + + aItem.zioWrite(aStream); + } + +} diff --git a/src/glum/zio/ZinStream.java b/src/glum/zio/ZinStream.java index 4bd5aef..a11d986 100644 --- a/src/glum/zio/ZinStream.java +++ b/src/glum/zio/ZinStream.java @@ -1,12 +1,31 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.zio; import java.io.IOException; +/** + * Interface that defines methods used to deserialize data using the glum.zio framework. + * + * @author lopeznr1 + */ public interface ZinStream extends AutoCloseable { /** * Releases any resources associated with the ZioInStream */ + @Override public void close() throws IOException; /** @@ -16,7 +35,7 @@ public interface ZinStream extends AutoCloseable /** * Returns the checksum (as a string) of the stream. - *

    + *

    * Note, if the stream is still open, then the returned value will be the checksum evaluated as of the last byte * grabbed from this stream (with no buffering effects - closing the stream immediately will not result in a * different value). @@ -25,9 +44,9 @@ public interface ZinStream extends AutoCloseable /** * Returns the virtual position of this stream - * + * * @throws IOException - * If the stream has been closed. + * If the stream has been closed. */ public long getPosition() throws IOException; @@ -46,6 +65,11 @@ public interface ZinStream extends AutoCloseable */ public char readChar() throws IOException; + /** + * Returns an enum from the stream. + */ + public > G1 readEnum(G1[] aValueArr) throws IOException; + /** * Returns the next int */ @@ -85,17 +109,17 @@ public interface ZinStream extends AutoCloseable /** * Returns the contents of dstArr, starting from offset to length fully. - * + * * @throws IOException - * Will be thrown if not enough data in the stream to fulfill request + * Will be thrown if not enough data in the stream to fulfill request */ public void readFully(byte[] dstArr, int offset, int length) throws IOException; /** * Reads in the contents of dstArr fully. - * + * * @throws IOException - * Will be thrown if not enough data in the stream to fulfill request + * Will be thrown if not enough data in the stream to fulfill request */ public void readFully(byte[] dstArr) throws IOException; @@ -110,16 +134,24 @@ public interface ZinStream extends AutoCloseable * Method to read the recorded version, and validate that it matches one of the values in validArr. If there is a * mismatch, then an IOException will be thrown. The recorded version should have been written with the inverse * method {@link ZoutStream#writeVersion}. - * + * * @return The version that was read in. */ public int readVersion(int... validArr) throws IOException; + /** + * Method to read the recorded version. Any value will due. The previously recorded version should have been written + * with the inverse method {@link ZoutStream#writeVersion}. + * + * @return The version that was read in. + */ + public int readVersionAny() throws IOException; + /** * Method to skip numBytes. - * + * * @throws IOException - * Will be thrown if not enough data in the stream to fulfill request + * Will be thrown if not enough data in the stream to fulfill request */ public void skipBytes(int numBytes) throws IOException; diff --git a/src/glum/zio/ZioObj.java b/src/glum/zio/ZioObj.java index e5c02d2..b34471d 100644 --- a/src/glum/zio/ZioObj.java +++ b/src/glum/zio/ZioObj.java @@ -1,19 +1,34 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.zio; import java.io.IOException; /** * Interface to allow for serialization of (mutable) objects. + * + * @author lopeznr1 */ public interface ZioObj { /** - * Deserialization method to read data from the ZinStream. + * Deserialization method to read data from the {@link ZinStream}. */ public void zioRead(ZinStream aStream) throws IOException; /** - * Serialization method to write data to the ZoutStream. + * Serialization method to write data to the {@link ZoutStream}. */ public void zioWrite(ZoutStream aStream) throws IOException; diff --git a/src/glum/zio/ZioObjUtil.java b/src/glum/zio/ZioObjUtil.java index 672903f..1d3d1ec 100644 --- a/src/glum/zio/ZioObjUtil.java +++ b/src/glum/zio/ZioObjUtil.java @@ -1,17 +1,32 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.zio; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.*; +/** + * Collection of utility methods for working with {@link ZioObj} objects. + * + * @author lopeznr1 + */ public class ZioObjUtil { /** - * Utility method to read a list of ZioObj items. The objects are assumed to be of the same type. - *

    - * Format: ()* + * Utility method to read a list of {@link ZioObj} items. The objects are assumed to be of the same type. + *

    + * Format: <numItems> (<ZioObj>)* */ public static ArrayList readList(ZinStream aStream, Class aClass) throws IOException { @@ -19,7 +34,7 @@ public class ZioObjUtil int numItems = aStream.readInt(); // Read the actual objects - ArrayList itemList = new ArrayList(); + ArrayList retItemL = new ArrayList(); for (int c1 = 0; c1 < numItems; c1++) { // Serialize the class @@ -27,7 +42,7 @@ public class ZioObjUtil { G1 aItem = aClass.getDeclaredConstructor().newInstance(); aItem.zioRead(aStream); - itemList.add(aItem); + retItemL.add(aItem); } catch(Exception aException) { @@ -35,46 +50,46 @@ public class ZioObjUtil } } - return itemList; + return retItemL; } /** * Utility method to read a preloaded list of ZioObj items. The passed in list must contain the exact number of items * as that stored on disk and in the correct order. - *

    - * Format: ()* + *

    + * Format: <numItems> (<ZioObj>)* */ - public static void readList(ZinStream aStream, Collection aItemList) throws IOException + public static void readList(ZinStream aStream, Collection aItemL) throws IOException { // Read the item count int numItems = aStream.readInt(); - if (numItems != aItemList.size()) - throw new IOException("Items stored: " + numItems + ". Expected: " + aItemList.size()); + if (numItems != aItemL.size()) + throw new IOException("Items stored: " + numItems + ". Expected: " + aItemL.size()); // Read the actual BinRaw items - for (ZioObj aItem : aItemList) + for (ZioObj aItem : aItemL) aItem.zioRead(aStream); } /** * Utility method to write out a list of ZioObj items. - *

    - * Format: ()* + *

    + * Format: <numItems> (<ZioObj>)* */ - public static void writeList(ZoutStream aStream, Collection aItemList) throws IOException + public static void writeList(ZoutStream aStream, Collection aItemL) throws IOException { // Write the item count - aStream.writeInt(aItemList.size()); + aStream.writeInt(aItemL.size()); // Write the actual objects - for (ZioObj aItem : aItemList) + for (ZioObj aItem : aItemL) aItem.zioWrite(aStream); } /** * Utility method to read a map of binary objects. The ZioObj items are assumed to be of the same type. - *

    - * Format: ()* + *

    + * Format: <numItems> (<String, ZioObj>)* */ public static Map readMap(ZinStream aStream, Class aClass) throws IOException { @@ -82,7 +97,7 @@ public class ZioObjUtil int numItems = aStream.readInt(); // Read the actual objects - Map itemMap = new LinkedHashMap(); + Map retItemM = new LinkedHashMap(); for (int c1 = 0; c1 < numItems; c1++) { // Read the key @@ -90,10 +105,10 @@ public class ZioObjUtil // Read the value G1 aItem = read(aStream, aClass); - itemMap.put(aKey, aItem); + retItemM.put(aKey, aItem); } - return itemMap; + return retItemM; } /** @@ -101,15 +116,15 @@ public class ZioObjUtil * contain as many items (and in the order) as that which will be read in from the disk. It is therefore advisable * that only LinkedHashMaps be used with this method. */ - public static void readMap(ZinStream aStream, Map aItemMap) throws IOException + public static void readMap(ZinStream aStream, Map aItemM) throws IOException { // Read the item count int numItems = aStream.readInt(); - if (numItems != aItemMap.size()) - throw new IOException("Items stored: " + numItems + ". Expected: " + aItemMap.size()); + if (numItems != aItemM.size()) + throw new IOException("Items stored: " + numItems + ". Expected: " + aItemM.size()); // Read the actual key,value pairings - String[] keyArr = aItemMap.keySet().toArray(new String[0]); + String[] keyArr = aItemM.keySet().toArray(new String[0]); for (int c1 = 0; c1 < numItems; c1++) { // Read the key and ensure the proper key was read @@ -118,29 +133,29 @@ public class ZioObjUtil throw new IOException("Key read: " + aKey + ". Expected: " + keyArr[c1]); // Read the value - ZioObj aItem = aItemMap.get(aKey); + ZioObj aItem = aItemM.get(aKey); aItem.zioRead(aStream); } } /** * Utility method to write out a map of ZioObj items. - *

    - * Format: ()* + *

    + * Format: <numItems> (<String, ZioObj>)* */ - public static void writeMap(ZoutStream aStream, Map aItemMap) throws IOException + public static void writeMap(ZoutStream aStream, Map aItemM) throws IOException { // Write the item count - aStream.writeInt(aItemMap.size()); + aStream.writeInt(aItemM.size()); // Write the actual objects - for (String aKey : aItemMap.keySet()) + for (String aKey : aItemM.keySet()) { // Write the key aStream.writeString(aKey); // Write the value - ZioObj aZioObj = aItemMap.get(aKey); + ZioObj aZioObj = aItemM.get(aKey); aZioObj.zioWrite(aStream); } } diff --git a/src/glum/zio/ZioRaw.java b/src/glum/zio/ZioRaw.java index f5c14a7..8ee160d 100644 --- a/src/glum/zio/ZioRaw.java +++ b/src/glum/zio/ZioRaw.java @@ -1,18 +1,31 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.zio; import java.io.IOException; /** * Interface to allow for serialization of (immutable) objects. - *

    - * The method zioWrite() is provided to allow immutable objects to be serialized. - *

    - * The method zioRead() is not provided since that would imply mutability. Any deserialization should be done via the constructor. + *

    + * Implementations of this interface would be expected to handle deserialization via the constructor. + * + * @author lopeznr1 */ public interface ZioRaw { /** - * Serialization method to write data to the ZoutStream. + * Serialization method to write data to the {@link ZoutStream}. */ public void zioWrite(ZoutStream aStream) throws IOException; diff --git a/src/glum/zio/ZioRawUtil.java b/src/glum/zio/ZioRawUtil.java deleted file mode 100644 index 704d7a8..0000000 --- a/src/glum/zio/ZioRawUtil.java +++ /dev/null @@ -1,69 +0,0 @@ -package glum.zio; - -import java.io.IOException; -import java.lang.reflect.Constructor; -import java.util.ArrayList; -import java.util.Collection; - -public class ZioRawUtil -{ - /** - * Utility method to read a list of ZioRaw items. The following requirements must be met: - *

      - *
    • The objects are assumed to be of the same type. - *
    • The class must define a Constructor that takes one argument - a ZinStream. - *
    - *

    - * Format: ()* - */ - public static ArrayList readList(ZinStream aStream, Class aClass) throws IOException - { - // Locate an appropriate Constructor - Constructor tmpConstructor = null; - try - { - tmpConstructor = aClass.getConstructor(ZinStream.class); - } - catch(NoSuchMethodException | SecurityException aExp) - { - throw new IOException("Failed to locate a proper constructor. Constuctor requires a single argument - ZioStream", aExp); - } - - // Read the item count - int numItems = aStream.readInt(); - - // Read the actual objects - ArrayList itemList = new ArrayList(); - for (int c1 = 0; c1 < numItems; c1++) - { - // Serialize the class - try - { - G1 aItem = tmpConstructor.newInstance(aStream); - itemList.add(aItem); - } - catch(Exception aException) - { - throw new IOException("Failed to instantiate: " + aClass, aException); - } - } - - return itemList; - } - - /** - * Utility method to write out a list of ZioRaw items. - *

    - * Format: ()* - */ - public static void writeList(ZoutStream aStream, Collection aItemList) throws IOException - { - // Write the item count - aStream.writeInt(aItemList.size()); - - // Write the actual objects - for (ZioRaw aItem : aItemList) - aItem.zioWrite(aStream); - } - -} diff --git a/src/glum/zio/ZioSpawner.java b/src/glum/zio/ZioSpawner.java new file mode 100644 index 0000000..0a82096 --- /dev/null +++ b/src/glum/zio/ZioSpawner.java @@ -0,0 +1,35 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.zio; + +import java.io.IOException; + +/** + * Interface that defines a mechanism for serialization of items. + * + * @author lopeznr1 + */ +public interface ZioSpawner +{ + /** + * Method that returns the appropriate item that is read from the specified {@link ZinStream}. + */ + public G1 readInstance(ZinStream aStream) throws IOException; + + /** + * Method to serialized the provided item to the specified {@link ZoutStream}. + */ + public void writeInstance(ZoutStream aStream, G1 aItem) throws IOException; + +} diff --git a/src/glum/zio/ZoutStream.java b/src/glum/zio/ZoutStream.java index 86b9941..7396bb0 100644 --- a/src/glum/zio/ZoutStream.java +++ b/src/glum/zio/ZoutStream.java @@ -1,27 +1,46 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.zio; import java.io.IOException; +/** + * Interface that defines methods used to serialize data using the glum.zio framework. + * + * @author lopeznr1 + */ public interface ZoutStream extends AutoCloseable { /** * Releases any resources associated with the stream */ + @Override public void close() throws IOException; /** * Returns the checksum (as a string) of the stream. - *

    - * Note, if the stream is still open, then the returned value will be the checksum evaluated as of the last byte sent to this stream (with no buffering - * effects - closing the stream immediately will not result in a different value). + *

    + * Note, if the stream is still open, then the returned value will be the checksum evaluated as of the last byte sent + * to this stream (with no buffering effects - closing the stream immediately will not result in a different value). */ public String getCheckSum() throws IOException; /** * Returns the virtual position of this stream. - * + * * @throws IOException - * If the stream has been closed. + * If the stream has been closed. */ public long getPosition() throws IOException; @@ -40,6 +59,11 @@ public interface ZoutStream extends AutoCloseable */ public void writeChar(char aChar) throws IOException; + /** + * Outputs an enum to the stream. + */ + public > void writeEnum(G1 aEnum) throws IOException; + /** * Outputs the next int */ @@ -71,29 +95,30 @@ public interface ZoutStream extends AutoCloseable public void writeString(String aStr) throws IOException; /** - * Utility method to write out a raw string. Note the inverse function is {@link ZinStream#readRawStringAndValidate}. The string will be interpreted as a - * US-ASCII string. + * Utility method to write out a raw string. Note the inverse function is {@link ZinStream#readRawStringAndValidate}. + * The string will be interpreted as a US-ASCII string. */ public void writeRawString(String aStr) throws IOException; /** * Writes the contents of dstArr, starting from offset to length fully. - * + * * @throws IOException - * Will be thrown if not enough space in the stream to fulfill request + * Will be thrown if not enough space in the stream to fulfill request */ public void writeFully(byte[] dstArr, int offset, int length) throws IOException; /** * Writes the contents of dstArr fully. - * + * * @throws IOException - * Will be thrown if not enough space in the stream to fulfill request + * Will be thrown if not enough space in the stream to fulfill request */ public void writeFully(byte[] dstArr) throws IOException; /** - * Method to write the version to the stream. To properly read the version, use the inverse function {@link ZinStream#readVersion}. + * Method to write the version to the stream. To properly read the version, use the inverse function + * {@link ZinStream#readVersion}. */ public void writeVersion(int aVersion) throws IOException; diff --git a/src/glum/zio/stream/BaseZinStream.java b/src/glum/zio/stream/BaseZinStream.java index 7fd1d0d..6612df5 100644 --- a/src/glum/zio/stream/BaseZinStream.java +++ b/src/glum/zio/stream/BaseZinStream.java @@ -1,3 +1,16 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.zio.stream; import java.io.IOException; @@ -14,6 +27,11 @@ import glum.util.WallTimer; import glum.zio.ZinStream; import glum.zio.util.ZioUtil; +/** + * Base implementation of the {@link ZinStream}. + * + * @author lopeznr1 + */ public abstract class BaseZinStream implements ZinStream { // Work vars @@ -47,13 +65,13 @@ public abstract class BaseZinStream implements ZinStream digest = MessageDigest.getInstance("MD5"); digestPos = 0; } - catch(NoSuchAlgorithmException aExp) + catch (NoSuchAlgorithmException aExp) { throw new IOException("Unreconized Algorithm", aExp); } } - // Allocate our work vars + // Allocate the work vars allocateWorkVars(streamSizeHint); } @@ -65,7 +83,8 @@ public abstract class BaseZinStream implements ZinStream */ public BaseZinStream(ByteBuffer aWorkBuffer, boolean computeCheckSum) throws IOException { - // Allocate the checksum digest worker + // Allocate the stat vars + wallTimer = new WallTimer(true); digest = null; checkSumStr = null; if (computeCheckSum == true) @@ -75,13 +94,13 @@ public abstract class BaseZinStream implements ZinStream digest = MessageDigest.getInstance("MD5"); digestPos = 0; } - catch(NoSuchAlgorithmException aExp) + catch (NoSuchAlgorithmException aExp) { throw new IOException("Unreconized Algorithm", aExp); } } - // Allocate our work vars + // Allocate the work vars workBuffer = aWorkBuffer; if (workBuffer == null) throw new NullPointerException(); @@ -159,6 +178,17 @@ public abstract class BaseZinStream implements ZinStream return workBuffer.getChar(); } + @Override + public > G1 readEnum(G1[] aValueArr) throws IOException + { + int ver = readVersion(0, 1); + if (ver == 0) + return null; + + int ordinal = ZioUtil.readCompactInt(this); + return aValueArr[ordinal]; + } + @Override public int readInt() throws IOException { @@ -240,7 +270,8 @@ public abstract class BaseZinStream implements ZinStream // Ensure the two arrays are equal if (Arrays.equals(absByteArr, readByteArr) == false) - throw new IOException("Mismatched string. Needed:" + absStr + " Found:" + new String(readByteArr, Charsets.US_ASCII)); + throw new IOException( + "Mismatched string. Needed:" + absStr + " Found:" + new String(readByteArr, Charsets.US_ASCII)); } @Override @@ -293,9 +324,7 @@ public abstract class BaseZinStream implements ZinStream int readVersion; // Read the version - readVersion = readByte() & 0x00FF; - if (readVersion == 255) - readVersion = readInt(); + readVersion = ZioUtil.readCompactInt(this); // Ensure the version is one of the valid versions if (readVersion == aValidVer) @@ -324,7 +353,18 @@ public abstract class BaseZinStream implements ZinStream if (validArr.length == 1) throw new IOException("Unreconized version... Read: " + readVersion + " Expected: " + validArr[0]); - throw new IOException("Unreconized version... Read: " + readVersion + " Expected one of the following: " + Arrays.toString(validArr)); + throw new IOException("Unreconized version... Read: " + readVersion + " Expected one of the following: " + + Arrays.toString(validArr)); + } + + @Override + public int readVersionAny() throws IOException + { + int readVersion; + + // Read the version + readVersion = ZioUtil.readCompactInt(this); + return readVersion; } @Override @@ -351,7 +391,7 @@ public abstract class BaseZinStream implements ZinStream /** * Helper method to refresh the workBuffer with new data from the stream. This method ensures that workBuffer will * always have enough data to support reading. - *

    + *

    * If there is no more data on the stream then this method should throw an IOException */ protected abstract void refreshWorkBuffer() throws IOException; @@ -369,14 +409,12 @@ public abstract class BaseZinStream implements ZinStream */ protected void updateDigest() throws IOException { - ByteBuffer tmpBuffer; - // Bail if the there is no digest if (digest == null) return; // Retrieve a duplicate of the workBuffer (to preserve its configuration) - tmpBuffer = workBuffer.duplicate(); + var tmpBuffer = workBuffer.duplicate(); // Evaluate the digest from the digestPos to the limit (workBuffer's current position) tmpBuffer.flip(); @@ -392,11 +430,8 @@ public abstract class BaseZinStream implements ZinStream */ private void allocateWorkVars(long streamSizeHint) throws IOException { - int workCap; - boolean isDirect; - // Determine if we should use a direct buffer for our workBuffer (stream > 25 MB) - isDirect = false; + var isDirect = false; if (streamSizeHint > 25 * 1024 * 1024) isDirect = true; @@ -404,7 +439,7 @@ public abstract class BaseZinStream implements ZinStream if (isDirect == false) { // [1K, 16K], indirect buffer - workCap = (int)streamSizeHint; + var workCap = (int) streamSizeHint; if (workCap < 1024) workCap = 1024; else if (workCap > 16 * 1024) @@ -415,10 +450,10 @@ public abstract class BaseZinStream implements ZinStream else { // 512K, direct buffer - workCap = 512 * 1024; + var workCap = 512 * 1024; workBuffer = ByteBuffer.allocateDirect(workCap); } -//System.out.println("Is direct buffer: " + workBuffer.isDirect() + " bufferCap: " + workCap); +//System.out.println("Is direct buffer: " + workBuffer.isDirect() + " bufferCap: " + workCap); // Mark the contents in workBuffer as completely empty workBuffer.limit(0); diff --git a/src/glum/zio/stream/BaseZoutStream.java b/src/glum/zio/stream/BaseZoutStream.java index 104af6a..dbb9f70 100644 --- a/src/glum/zio/stream/BaseZoutStream.java +++ b/src/glum/zio/stream/BaseZoutStream.java @@ -1,9 +1,18 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.zio.stream; -import glum.util.WallTimer; -import glum.zio.ZoutStream; -import glum.zio.util.ZioUtil; - import java.io.IOException; import java.math.BigInteger; import java.nio.ByteBuffer; @@ -13,6 +22,15 @@ import java.security.NoSuchAlgorithmException; import com.google.common.base.Charsets; import com.google.common.base.Strings; +import glum.util.WallTimer; +import glum.zio.ZoutStream; +import glum.zio.util.ZioUtil; + +/** + * Base implementation of the {@link ZoutStream}. + * + * @author lopeznr1 + */ public abstract class BaseZoutStream implements ZoutStream { // Work vars @@ -24,11 +42,13 @@ public abstract class BaseZoutStream implements ZoutStream private String checkSumStr; /** + * Standard Constructor + * * @param computeCheckSum - * True if a checksum (md5sum) is desired to be computed as the stream is written + * True if a checksum (md5sum) is desired to be computed as the stream is written * @param isDirect - * True if a direct buffer is desired. This should only be true if the stream is going to a physical I/O component (disk, network) and the size of - * the final stream will be at least ~50 MB. + * True if a direct buffer is desired. This should only be true if the stream is going to a physical I/O + * component (disk, network) and the size of the final stream will be at least ~50 MB. */ public BaseZoutStream(boolean computeCheckSum, boolean isDirect) throws IOException { @@ -41,7 +61,7 @@ public abstract class BaseZoutStream implements ZoutStream if (computeCheckSum == true) digest = MessageDigest.getInstance("MD5"); } - catch(NoSuchAlgorithmException aExp) + catch (NoSuchAlgorithmException aExp) { throw new IOException("Unreconized Algorithm", aExp); } @@ -110,9 +130,9 @@ public abstract class BaseZoutStream implements ZoutStream public void writeBool(boolean aBool) throws IOException { if (aBool == false) - writeByte((byte)0); + writeByte((byte) 0); else - writeByte((byte)1); + writeByte((byte) 1); } @Override @@ -125,6 +145,20 @@ public abstract class BaseZoutStream implements ZoutStream workBuffer.putChar(aChar); } + @Override + public > void writeEnum(G1 aEnum) throws IOException + { + if (aEnum == null) + { + writeVersion(0); + return; + } + + writeVersion(1); + int ordinal = aEnum.ordinal(); + ZioUtil.writeCompactInt(this, ordinal); + } + @Override public void writeInt(int aInt) throws IOException { @@ -184,14 +218,14 @@ public abstract class BaseZoutStream implements ZoutStream // Null strings are handled in special fashion if (aStr == null) { - writeShort((short)0x00FFFF); + writeShort((short) 0x00FFFF); return; } // Empty strings are handled in special fashion if (aStr.equals("") == true) { - writeShort((short)0); + writeShort((short) 0); return; } @@ -201,10 +235,11 @@ public abstract class BaseZoutStream implements ZoutStream // Ensure the string size is less than 0x00FFFF if (size >= 0x00FFFF) - throw new RuntimeException("Transformed UTF-8 string is too large! Max size: " + (0x00FFFF - 1) + " Curr size:" + size); + throw new RuntimeException( + "Transformed UTF-8 string is too large! Max size: " + (0x00FFFF - 1) + " Curr size:" + size); // Write out the string - writeShort((short)(size & 0x00FFFF)); + writeShort((short) (size & 0x00FFFF)); writeFully(data); } @@ -269,8 +304,9 @@ public abstract class BaseZoutStream implements ZoutStream } /** - * Helper method that ensures the digest has been updated with any buffered data. The buffer will be cleared after the digest has been updated. - *

    + * Helper method that ensures the digest has been updated with any buffered data. The buffer will be cleared after + * the digest has been updated. + *

    * The method shall be called exclusively from {@link BaseZoutStream#emptyWorkBuffer()}. */ protected void clearWorkBuffer() @@ -287,19 +323,20 @@ public abstract class BaseZoutStream implements ZoutStream } /** - * Helper method to empty the workBuffer and copy the contents to the stream. The contents of the workBuffer will be output to the "stream". This method - * ensures that workBuffer will always have enough data to support writing + * Helper method to empty the workBuffer and copy the contents to the stream. The contents of the workBuffer will be + * output to the "stream". This method ensures that workBuffer will always have enough data to support writing */ protected abstract void emptyWorkBuffer() throws IOException; /** - * Helper method to release any stream related vars. This method will only be called once, the very first time the method {@link #close()} is called. + * Helper method to release any stream related vars. This method will only be called once, the very first time the + * method {@link #close()} is called. */ protected abstract void releaseStreamVars() throws IOException; /** * Helper method to allocate our work vars. - * + * * @throws IOException */ private void allocateWorkVars(boolean isDirect) throws IOException @@ -319,7 +356,7 @@ public abstract class BaseZoutStream implements ZoutStream workCap = 512 * 1024; workBuffer = ByteBuffer.allocateDirect(workCap); } -//System.out.println("Is direct buffer: " + workBuffer.isDirect() + " bufferCap: " + workCap); +//System.out.println("Is direct buffer: " + workBuffer.isDirect() + " bufferCap: " + workCap); // Mark the buffers as empty workBuffer.clear(); diff --git a/src/glum/zio/stream/ByteArrayZinStream.java b/src/glum/zio/stream/ByteArrayZinStream.java index 51a10f9..dae5a64 100644 --- a/src/glum/zio/stream/ByteArrayZinStream.java +++ b/src/glum/zio/stream/ByteArrayZinStream.java @@ -1,19 +1,41 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.zio.stream; import java.io.EOFException; import java.io.IOException; import java.nio.ByteBuffer; +import glum.zio.ZinStream; + +/** + * Implementation of {@link ZinStream} backed by a byte array. + * + * @author lopeznr1 + */ public class ByteArrayZinStream extends BaseZinStream { + /** Standard Constructor */ public ByteArrayZinStream(byte[] aDataArr, boolean computeCheckSum) throws IOException { super(ByteBuffer.wrap(aDataArr), computeCheckSum); - + // Move the position to the start workBuffer.rewind(); } + /** Simplified Constructor */ public ByteArrayZinStream(byte[] aDataArr) throws IOException { this(aDataArr, false); @@ -24,7 +46,7 @@ public class ByteArrayZinStream extends BaseZinStream { if (workBuffer == null) return 0; - + return workBuffer.remaining(); } @@ -34,7 +56,7 @@ public class ByteArrayZinStream extends BaseZinStream // There is no virtual position if the stream has been closed if (workBuffer == null) throw new IOException("Stream has been closed."); - + return workBuffer.position(); } @@ -44,7 +66,7 @@ public class ByteArrayZinStream extends BaseZinStream // There will never be new fresh data for a ByteArrayZinStream throw new EOFException("EOF reached on stream."); } - + @Override protected void releaseStreamVars() throws IOException { diff --git a/src/glum/zio/stream/ByteArrayZoutStream.java b/src/glum/zio/stream/ByteArrayZoutStream.java index 9ac976b..2b2e298 100644 --- a/src/glum/zio/stream/ByteArrayZoutStream.java +++ b/src/glum/zio/stream/ByteArrayZoutStream.java @@ -1,23 +1,45 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.zio.stream; import java.io.IOException; import java.util.Arrays; +import glum.zio.ZoutStream; + +/** + * Implementation of {@link ZoutStream} backed by a byte array. + * + * @author lopeznr1 + */ public class ByteArrayZoutStream extends BaseZoutStream { // Stream vars private byte[] dataArr; private int dataPos; + /** Standard Constructor */ public ByteArrayZoutStream(int initCap, boolean computeCheckSum) throws IOException { super(computeCheckSum, false); - + // Set up the stream vars dataArr = new byte[initCap]; dataPos = 0; } + /** Simplified Constructor */ public ByteArrayZoutStream(int initCap) throws IOException { this(initCap, false); @@ -34,7 +56,7 @@ public class ByteArrayZoutStream extends BaseZoutStream emptyWorkBuffer(); return Arrays.copyOf(dataArr, dataPos); } - + // Return the final byte array return dataArr; } @@ -75,7 +97,7 @@ public class ByteArrayZoutStream extends BaseZoutStream // Clear the workBuffer clearWorkBuffer(); } - + @Override protected void releaseStreamVars() throws IOException { diff --git a/src/glum/zio/stream/DebugZoutStream.java b/src/glum/zio/stream/DebugZoutStream.java index 1344cbf..a050f0f 100644 --- a/src/glum/zio/stream/DebugZoutStream.java +++ b/src/glum/zio/stream/DebugZoutStream.java @@ -1,15 +1,32 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.zio.stream; -import glum.zio.ZoutStream; - import java.io.IOException; import com.google.common.base.Charsets; +import glum.zio.ZoutStream; +import glum.zio.util.ZioUtil; + /** - * ZoutStream used for debugging. This is useful when trying to determine when and where a specific byte is being written out. - *

    - * When the specified nth byte has been written this object will throw an IOException. + * {@link ZoutStream} used for debugging. This is useful when trying to determine when and where a specific byte is + * being written out. + *

    + * When the specified nth byte has been written this object will throw an {@link IOException}. + * + * @author lopeznr1 */ public class DebugZoutStream implements ZoutStream { @@ -17,8 +34,10 @@ public class DebugZoutStream implements ZoutStream private int failByteCnt; /** + * Standard Constructor + * * @param aFailByteCnt - * The nth byte that when written will cause an Exception to be raised. + * The nth byte that when written will cause an {@link IOException} to be raised. */ public DebugZoutStream(int aFailByteCnt) { @@ -76,6 +95,20 @@ public class DebugZoutStream implements ZoutStream throwBadByteWrittenException(); } + @Override + public > void writeEnum(G1 aEnum) throws IOException + { + if (aEnum == null) + { + writeVersion(0); + return; + } + + writeVersion(1); + int ordinal = aEnum.ordinal(); + ZioUtil.writeCompactInt(this, ordinal); + } + @Override public void writeInt(int aInt) throws IOException { @@ -146,7 +179,8 @@ public class DebugZoutStream implements ZoutStream // Ensure the string size is less than 0x00FFFF if (size >= 0x00FFFF) - throw new RuntimeException("Transformed UTF-8 string is too large! Max size: " + (0x00FFFF - 1) + " Curr size:" + size); + throw new RuntimeException( + "Transformed UTF-8 string is too large! Max size: " + (0x00FFFF - 1) + " Curr size:" + size); byteCnt += 2 + size; if (byteCnt >= failByteCnt) diff --git a/src/glum/zio/stream/FileZinStream.java b/src/glum/zio/stream/FileZinStream.java index 57ce585..7d3839d 100644 --- a/src/glum/zio/stream/FileZinStream.java +++ b/src/glum/zio/stream/FileZinStream.java @@ -1,26 +1,45 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.zio.stream; -import java.io.EOFException; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; +import java.io.*; import java.nio.channels.FileChannel; +import glum.zio.ZinStream; + +/** + * Implementation of {@link ZinStream} backed by a file. + * + * @author lopeznr1 + */ public class FileZinStream extends BaseZinStream { // Stream vars private FileChannel fileCh; private byte[] staleArr; - public FileZinStream(File aFile, boolean computeCheckSum) throws IOException + /** Standard Constructor */ + public FileZinStream(File aFile, boolean aComputeCheckSum) throws IOException { - super(computeCheckSum, aFile.length()); + super(aComputeCheckSum, aFile.length()); // Set up the stream vars fileCh = new FileInputStream(aFile).getChannel(); staleArr = new byte[256]; } + /** Simplified Constructor */ public FileZinStream(File aFile) throws IOException { this(aFile, false); @@ -48,13 +67,11 @@ public class FileZinStream extends BaseZinStream @Override protected void refreshWorkBuffer() throws IOException { - int numReadBytes, numStaleBytes; - // Ensure the digest has been updated before refreshing the buffer updateDigest(); // Copies the remaining data from workBuffer to a byte (stale) array - numStaleBytes = workBuffer.remaining(); + var numStaleBytes = workBuffer.remaining(); if (numStaleBytes > 0) workBuffer.get(staleArr, 0, numStaleBytes); @@ -64,7 +81,7 @@ public class FileZinStream extends BaseZinStream workBuffer.put(staleArr, 0, numStaleBytes); // Fill the remaining workBuffer with data from the "stream" - numReadBytes = fileCh.read(workBuffer); + var numReadBytes = fileCh.read(workBuffer); if (numReadBytes == 0) System.out.println("Failed to read any buffer bytes!!! Bytes formerly read: " + numReadBytes); if (numReadBytes == -1) diff --git a/src/glum/zio/stream/FileZoutStream.java b/src/glum/zio/stream/FileZoutStream.java index ce96b98..64b3b5f 100644 --- a/src/glum/zio/stream/FileZoutStream.java +++ b/src/glum/zio/stream/FileZoutStream.java @@ -1,23 +1,43 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.zio.stream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; +import java.io.*; import java.nio.channels.FileChannel; +import glum.zio.ZoutStream; + +/** + * Implementation of {@link ZoutStream} backed by a file. + * + * @author lopeznr1 + */ public class FileZoutStream extends BaseZoutStream { // Stream vars private FileChannel fileCh; - public FileZoutStream(File aFile, boolean computeCheckSum, boolean isDirect) throws IOException + /** Standard Constructor */ + public FileZoutStream(File aFile, boolean aComputeCheckSum, boolean aIsDirect) throws IOException { - super(computeCheckSum, isDirect); + super(aComputeCheckSum, aIsDirect); // Set up the stream vars fileCh = new FileOutputStream(aFile).getChannel(); } + /** Simplified Constructor */ public FileZoutStream(File aFile) throws IOException { this(aFile, false, false); @@ -36,18 +56,17 @@ public class FileZoutStream extends BaseZoutStream @Override protected void emptyWorkBuffer() throws IOException { - int bytesWritten; - // Prepare the buffer for a dump of its contents from the start workBuffer.flip(); // Copy the contents of workBuffer to the stream (fileCh) - bytesWritten = 0; + var bytesWritten = 0; while (workBuffer.remaining() > 0) { bytesWritten = fileCh.write(workBuffer); if (workBuffer.remaining() > 0) - System.out.println("Failed to write buffer all at once. bytesToWrite: " + workBuffer.remaining() + ". Bytes formerly written: " + bytesWritten); + System.out.println("Failed to write buffer all at once. bytesToWrite: " + workBuffer.remaining() // + + ". Bytes formerly written: " + bytesWritten); } // Clear the workBuffer diff --git a/src/glum/zio/stream/NullZoutStream.java b/src/glum/zio/stream/NullZoutStream.java index 338fa9b..75683fe 100644 --- a/src/glum/zio/stream/NullZoutStream.java +++ b/src/glum/zio/stream/NullZoutStream.java @@ -1,18 +1,37 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.zio.stream; -import glum.zio.ZoutStream; - import java.io.IOException; import com.google.common.base.Charsets; +import glum.zio.ZoutStream; +import glum.zio.util.ZioUtil; + /** - * ZoutStream used to count bytes that would be written. + * {@link ZoutStream} used to count bytes that would be written. + *

    + * Content is not written to any resource. + * + * @author lopeznr1 */ public class NullZoutStream implements ZoutStream { private int byteCount; + /** Standard Constructor */ public NullZoutStream() { byteCount = 0; @@ -62,6 +81,20 @@ public class NullZoutStream implements ZoutStream byteCount += 2; } + @Override + public > void writeEnum(G1 aEnum) throws IOException + { + if (aEnum == null) + { + writeVersion(0); + return; + } + + writeVersion(1); + int ordinal = aEnum.ordinal(); + ZioUtil.writeCompactInt(this, ordinal); + } + @Override public void writeInt(int aInt) throws IOException { @@ -118,7 +151,8 @@ public class NullZoutStream implements ZoutStream // Ensure the string size is less than 0x00FFFF if (size >= 0x00FFFF) - throw new RuntimeException("Transformed UTF-8 string is too large! Max size: " + (0x00FFFF - 1) + " Curr size:" + size); + throw new RuntimeException( + "Transformed UTF-8 string is too large! Max size: " + (0x00FFFF - 1) + " Curr size:" + size); byteCount += 2 + size; } diff --git a/src/glum/zio/util/WrapInputStream.java b/src/glum/zio/util/WrapInputStream.java index 648c833..5588499 100644 --- a/src/glum/zio/util/WrapInputStream.java +++ b/src/glum/zio/util/WrapInputStream.java @@ -1,15 +1,29 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.zio.util; -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; +import glum.zio.ZinStream; import glum.zio.stream.BaseZinStream; /** - * Package private class to transform an InputStream to a ZinStream (view). + * Package private class to transform an {@link InputStream} to a {@link ZinStream} (view). + * + * @author lopeznr1 */ class WrapInputStream extends BaseZinStream { @@ -18,9 +32,10 @@ class WrapInputStream extends BaseZinStream private byte[] staleArr; private long streamPos; - protected WrapInputStream(InputStream aStream, boolean computeCheckSum) throws IOException + /** Standard Constructor */ + protected WrapInputStream(InputStream aStream, boolean aComputeCheckSum) throws IOException { - super(computeCheckSum, 0); + super(aComputeCheckSum, 0); // Set up the stream vars refCh = Channels.newChannel(aStream); @@ -28,6 +43,7 @@ class WrapInputStream extends BaseZinStream streamPos = 0; } + /** Simplified Constructor */ protected WrapInputStream(InputStream aStream) throws IOException { this(aStream, false); diff --git a/src/glum/zio/util/WrapOutputStream.java b/src/glum/zio/util/WrapOutputStream.java index 20ccf27..f8004a2 100644 --- a/src/glum/zio/util/WrapOutputStream.java +++ b/src/glum/zio/util/WrapOutputStream.java @@ -1,30 +1,48 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.zio.util; -import glum.zio.stream.BaseZoutStream; - import java.io.IOException; import java.io.OutputStream; import java.nio.channels.Channels; import java.nio.channels.WritableByteChannel; +import glum.zio.ZoutStream; +import glum.zio.stream.BaseZoutStream; + /** - * Package private class to transform an OutputStream to a ZoutStream (view). + * Package private class to transform an {@link OutputStream} to a {@link ZoutStream} (view). + * + * @author lopeznr1 */ -public class WrapOutputStream extends BaseZoutStream +class WrapOutputStream extends BaseZoutStream { // Stream vars private WritableByteChannel refCh; private long streamPos; - protected WrapOutputStream(OutputStream aStream, boolean computeCheckSum, boolean isDirect) throws IOException + /** Standard Constructor */ + protected WrapOutputStream(OutputStream aStream, boolean aComputeCheckSum, boolean aIsDirect) throws IOException { - super(computeCheckSum, isDirect); + super(aComputeCheckSum, aIsDirect); // Set up the stream vars refCh = Channels.newChannel(aStream); streamPos = 0; } + /** Simplified Constructor */ protected WrapOutputStream(OutputStream aStream) throws IOException { this(aStream, false, false); @@ -54,7 +72,8 @@ public class WrapOutputStream extends BaseZoutStream { bytesWritten = refCh.write(workBuffer); if (workBuffer.remaining() > 0) - System.out.println("Failed to write buffer all at once. bytesToWrite: " + workBuffer.remaining() + ". Bytes formerly written: " + bytesWritten); + System.out.println("Failed to write buffer all at once. bytesToWrite: " + workBuffer.remaining() + + ". Bytes formerly written: " + bytesWritten); } // Clear the workBuffer @@ -65,7 +84,7 @@ public class WrapOutputStream extends BaseZoutStream protected void releaseStreamVars() throws IOException { refCh.close(); - + refCh = null; streamPos = -1; } diff --git a/src/glum/zio/util/WrapZinStream.java b/src/glum/zio/util/WrapZinStream.java index acb800c..4e518e4 100644 --- a/src/glum/zio/util/WrapZinStream.java +++ b/src/glum/zio/util/WrapZinStream.java @@ -1,17 +1,34 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.zio.util; -import glum.zio.ZinStream; - import java.io.IOException; import java.io.InputStream; +import glum.zio.ZinStream; + /** - * Package private class to transform a ZinStream to an InputStream (view). + * Package private class to transform a {@link ZinStream} to an {@link InputStream} (view). + * + * @author lopeznr1 */ class WrapZinStream extends InputStream { - private ZinStream refStream; + // Attributes + private final ZinStream refStream; + /** Standard Constructor */ protected WrapZinStream(ZinStream aStream) { refStream = aStream; @@ -32,36 +49,36 @@ class WrapZinStream extends InputStream return 0x00FF & refStream.readByte(); } - + @Override public int read(byte[] b, int off, int len) throws IOException { - long availBytes; - - availBytes = refStream.getAvailable(); + long availBytes; + + availBytes = refStream.getAvailable(); if (availBytes == 0) return -1; - + if (len > availBytes) - len = (int)availBytes; + len = (int) availBytes; refStream.readFully(b, off, len); return len; } - + @Override public long skip(long len) throws IOException { - long availBytes; - - availBytes = refStream.getAvailable(); + long availBytes; + + availBytes = refStream.getAvailable(); if (availBytes == 0) return -1; - + if (len > availBytes) len = availBytes; - - refStream.skipBytes((int)len); + + refStream.skipBytes((int) len); return len; } } \ No newline at end of file diff --git a/src/glum/zio/util/WrapZoutStream.java b/src/glum/zio/util/WrapZoutStream.java index 8bb4c21..169c6f7 100644 --- a/src/glum/zio/util/WrapZoutStream.java +++ b/src/glum/zio/util/WrapZoutStream.java @@ -1,17 +1,34 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.zio.util; -import glum.zio.ZoutStream; - import java.io.IOException; import java.io.OutputStream; +import glum.zio.ZoutStream; + /** - * Package private class to transform a ZoutStream to an OutputStream (view). + * Package private class to transform a {@link ZoutStream} to an {@link OutputStream} (view). + * + * @author lopeznr1 */ class WrapZoutStream extends OutputStream { - private ZoutStream refStream; + // Attributes + private final ZoutStream refStream; + /** Standard Constructor */ protected WrapZoutStream(ZoutStream aStream) { refStream = aStream; @@ -27,9 +44,9 @@ class WrapZoutStream extends OutputStream @Override public void write(int b) throws IOException { - refStream.writeByte((byte)(b & 0x00FF)); + refStream.writeByte((byte) (b & 0x00FF)); } - + @Override public void write(byte[] b, int off, int len) throws IOException { diff --git a/src/glum/zio/util/ZioRawUtil.java b/src/glum/zio/util/ZioRawUtil.java new file mode 100644 index 0000000..6559a23 --- /dev/null +++ b/src/glum/zio/util/ZioRawUtil.java @@ -0,0 +1,157 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package glum.zio.util; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Collection; + +import glum.zio.*; + +/** + * Collection of utility methods for working with {@link ZioRaw} objects. + * + * @author lopeznr1 + */ +public class ZioRawUtil +{ + /** + * Utility method to read a list of {@link ZioRaw} items. The following requirements must be met: + *

      + *
    • The objects are assumed to be of the same type. + *
    • The class must define a Constructor that takes one argument - a {@link ZinStream}. + *
    + *

    + * Format: <numItems> (<ZioRaw>)* + */ + public static ArrayList readList(ZinStream aStream, Class aClass) throws IOException + { + // Locate an appropriate Constructor + Constructor tmpConstructor = null; + try + { + tmpConstructor = aClass.getConstructor(ZinStream.class); + } + catch (NoSuchMethodException | SecurityException aExp) + { + throw new IOException("Failed to locate a proper constructor. Constuctor requires " // + + "a single argument - ZioStream", aExp); + } + + // Read the item count + int numItems = aStream.readInt(); + + // Read the actual objects + ArrayList retItemL = new ArrayList(); + for (int c1 = 0; c1 < numItems; c1++) + { + // Serialize the class + try + { + G1 aItem = tmpConstructor.newInstance(aStream); + retItemL.add(aItem); + } + catch (Exception aException) + { + throw new IOException("Failed to instantiate: " + aClass, aException); + } + } + + return retItemL; + } + + /** + * Utility method to write out a list of {@link ZioRaw} items. + *

    + * Format: <numItems> (<ZioObj>)* + */ + public static void writeList(ZoutStream aStream, Collection aItemC) throws IOException + { + // Write the item count + aStream.writeInt(aItemC.size()); + + // Write the actual objects + for (ZioRaw aItem : aItemC) + aItem.zioWrite(aStream); + } + + /** + * Utility method to write an individual {@link ZioRaw} item. + */ + public static G1 readRaw(ZinStream aStream, ZioSpawner aSpawner) throws IOException + { + aStream.readVersion(0); + return aSpawner.readInstance(aStream); + } + + /** + * Utility method to write an individual {@link ZioRaw} item. + */ + public static void writeRaw(ZoutStream aStream, ZioSpawner aSpawner, G1 aItem) throws IOException + { + aStream.writeVersion(0); + aSpawner.writeInstance(aStream, aItem); + } + + /** + * Utility method to read a list of {@link ZioRaw} items. + *

    + * Format: (<ZioRaw>)* + */ + public static ArrayList readRawList(ZinStream aStream, ZioSpawner aSpawner) throws IOException + { + ArrayList retItemL; + G1 aItem; + int numItems; + + // Read the item count + numItems = aStream.readInt(); + + // Read the actual objects + retItemL = new ArrayList<>(numItems + 1); + for (int c1 = 0; c1 < numItems; c1++) + { + // Serialize the object + try + { + aItem = aSpawner.readInstance(aStream); + retItemL.add(aItem); + } + catch (Exception aException) + { + throw new IOException("Failed to instantiate: " + aSpawner, aException); + } + } + + return retItemL; + } + + /** + * Utility method to write out a list of {@link ZioRaw} items. + *

    + * Format: (<ZioRaw>)* + */ + public static void writeRawList(ZoutStream aStream, ZioSpawner aSpawner, Collection aItemC) + throws IOException + { + // Write the item count + aStream.writeInt(aItemC.size()); + + // Write the actual objects + for (G1 aItem : aItemC) + aSpawner.writeInstance(aStream, aItem); + } + +} diff --git a/src/glum/zio/util/ZioUtil.java b/src/glum/zio/util/ZioUtil.java index 80677e8..3273104 100644 --- a/src/glum/zio/util/ZioUtil.java +++ b/src/glum/zio/util/ZioUtil.java @@ -1,5 +1,22 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.zio.util; +import java.awt.*; +import java.io.IOException; +import java.time.*; + import glum.task.Task; import glum.unit.TimeCountUnit; import glum.zio.ZinStream; @@ -7,11 +24,11 @@ import glum.zio.ZoutStream; import glum.zio.stream.FileZinStream; import glum.zio.stream.FileZoutStream; -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Point; -import java.io.IOException; - +/** + * Collection of utility methods for working with zio streams ({@link ZinStream} and {@link ZoutStream}). + * + * @author lopeznr1 + */ public class ZioUtil { /** @@ -32,7 +49,7 @@ public class ZioUtil checksum = aStream.getCheckSum(); runTime = aStream.getRunTime(); message = "[Read] MD5: " + checksum + " Source: " + aSource + " Time: " + aUnit.getString(runTime); - aTask.infoAppendln(message); + aTask.logRegln(message); } /** @@ -53,7 +70,7 @@ public class ZioUtil checksum = aStream.getCheckSum(); runTime = aStream.getRunTime(); message = "[Read] MD5: " + checksum + " Source: " + aSource + " Time: " + aUnit.getString(runTime); - aTask.infoAppendln(message); + aTask.logRegln(message); } /** @@ -81,9 +98,9 @@ public class ZioUtil byte byteVal; // Takes up one byte if in the range of (0, 254) - byteVal = (byte)0x00FF; + byteVal = (byte) 0x00FF; if (aValue >= 0 && aValue < 255) - byteVal = (byte)(0x00FF & aValue); + byteVal = (byte) (0x00FF & aValue); aStream.writeByte(byteVal); // Takes up 5 bytes otherwise @@ -186,20 +203,119 @@ public class ZioUtil if (aColor == null) { - aStream.writeByte((byte)0); + aStream.writeByte((byte) 0); return; } - aStream.writeByte((byte)1); + aStream.writeByte((byte) 1); - byteVal = (byte)(0x00FF & aColor.getRed()); + byteVal = (byte) (0x00FF & aColor.getRed()); aStream.writeByte(byteVal); - byteVal = (byte)(0x00FF & aColor.getGreen()); + byteVal = (byte) (0x00FF & aColor.getGreen()); aStream.writeByte(byteVal); - byteVal = (byte)(0x00FF & aColor.getBlue()); + byteVal = (byte) (0x00FF & aColor.getBlue()); aStream.writeByte(byteVal); } + /** + * Utility method to read a {@link LocalDate} from the specified stream. + */ + public static LocalDate readDate(ZinStream aStream) throws IOException + { + int ver = aStream.readVersion(0, 1); + if (ver == 0) + return null; + + int year = aStream.readShort(); + int dayOfYear = aStream.readShort(); + return LocalDate.ofYearDay(year, dayOfYear); + } + + /** + * Utility method to write a {@link LocalDate} to the specified stream. + */ + public static void writeDate(ZoutStream aStream, LocalDate aDate) throws IOException + { + if (aDate == null) + { + aStream.writeVersion(0); + return; + } + + short year = (short) aDate.getYear(); + short dayOfYear = (short) aDate.getDayOfYear(); + + aStream.writeVersion(1); + aStream.writeShort(year); + aStream.writeShort(dayOfYear); + } + + /** + * Utility method to read a {@link LocalTime} from the specified stream. + */ + public static LocalTime readTime(ZinStream aStream) throws IOException + { + int ver = aStream.readVersion(0, 1); + if (ver == 0) + return null; + + int hour = aStream.readByte(); + int min = aStream.readByte(); + int sec = aStream.readByte(); + return LocalTime.of(hour, min, sec); + } + + /** + * Utility method to write a {@link LocalTime} to the specified stream. + */ + public static void writeTime(ZoutStream aStream, LocalTime aTime) throws IOException + { + if (aTime == null) + { + aStream.writeVersion(0); + return; + } + + byte hour = (byte) aTime.getHour(); + byte min = (byte) aTime.getMinute(); + byte sec = (byte) aTime.getSecond(); + + aStream.writeVersion(1); + aStream.writeByte(hour); + aStream.writeByte(min); + aStream.writeByte(sec); + } + + /** + * Utility method to read a {@link LocalDateTime}from the specified stream. + */ + public static LocalDateTime readDateTime(ZinStream aStream) throws IOException + { + int ver = aStream.readVersion(0, 1); + if (ver == 0) + return null; + + LocalDate tmpDate = readDate(aStream); + LocalTime tmpTime = readTime(aStream); + return LocalDateTime.of(tmpDate, tmpTime); + } + + /** + * Utility method to write a {@link LocalDateTime} to the specified stream. + */ + public static void writeDateTime(ZoutStream aStream, LocalDateTime aDateTime) throws IOException + { + if (aDateTime == null) + { + aStream.writeVersion(0); + return; + } + + aStream.writeVersion(1); + writeDate(aStream, aDateTime.toLocalDate()); + writeTime(aStream, aDateTime.toLocalTime()); + } + } diff --git a/src/glum/zio/util/ZioWrapUtil.java b/src/glum/zio/util/ZioWrapUtil.java index c50c08b..f86520d 100644 --- a/src/glum/zio/util/ZioWrapUtil.java +++ b/src/glum/zio/util/ZioWrapUtil.java @@ -1,21 +1,33 @@ +// Copyright (C) 2024 The Johns Hopkins University Applied Physics Laboratory LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package glum.zio.util; +import java.io.*; + import glum.zio.ZinStream; import glum.zio.ZoutStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - /** - * Utility class that takes a ZinStream/ZoutStream and presents various InputStream/OutputStream views. + * Utility class that takes a {@link ZinStream} / {@link ZoutStream} and presents various {@link InputStream} / + * {@link OutputStream} views. + * + * @author lopeznr1 */ public class ZioWrapUtil { /** - * Utility method to return the ZinStream as a DataInputStream view + * Utility method to return the {@link ZinStream} as a {@link DataInputStream} view */ public static DataInputStream asDataInputStream(ZinStream aStream) { @@ -23,7 +35,7 @@ public class ZioWrapUtil } /** - * Utility method to return the ZinStream as a DataInputStream view + * Utility method to return the {@link ZinStream} as a {@link DataInputStream} view */ public static DataOutputStream asDataOutputStream(ZoutStream aStream) { @@ -31,7 +43,7 @@ public class ZioWrapUtil } /** - * Utility method to return the ZinStream as an InputStream view + * Utility method to return the {@link ZinStream} as an {@link InputStream} view */ public static InputStream asInputStream(ZinStream aStream) { @@ -39,15 +51,15 @@ public class ZioWrapUtil } /** - * Utility method to return the ZoutStream as an OutputStream view + * Utility method to return the {@link ZoutStream} as an {@link OutputStream} view */ public static OutputStream asOutputStream(ZoutStream aStream) { return new WrapZoutStream(aStream); } - + /** - * Utility method to return InputStream the as a ZinStream view + * Utility method to return {@link InputStream} the as a {@link ZinStream} view */ public static ZinStream asZinStream(InputStream aStream) throws IOException { diff --git a/src/module-info.java.unused b/src/module-info.java.unused new file mode 100644 index 0000000..50bb9a2 --- /dev/null +++ b/src/module-info.java.unused @@ -0,0 +1,38 @@ +module glum +{ + requires java.base; + requires java.compiler; + requires transitive java.desktop; +// requires static java.desktop; + requires guava; + requires miglayout; +// requires miglayout; +// requires net.miginfocom; +// requires net.miginfocom.layout; + + exports glum.gui; + exports glum.io; + exports glum.io.token; + exports glum.net; + exports glum.unit; + exports glum.util; + exports glum.reflect; + exports glum.registry; + exports glum.task; + + exports glum.database; + exports glum.gui.action; + exports glum.gui.component; + exports glum.gui.panel; + exports glum.gui.panel.generic; + exports glum.gui.panel.itemList; + exports glum.gui.panel.itemList.query; + exports glum.gui.panel.task; + exports glum.zio; + exports glum.zio.stream; + exports glum.zio.util; + +// uses java.awt.event.ActionListener; + uses java.awt.Font; + uses java.awt.Component; +} diff --git a/tools/build.xml b/tools/build.xml new file mode 100644 index 0000000..bc3bfe4 --- /dev/null +++ b/tools/build.xml @@ -0,0 +1,66 @@ + + + + + + + Glum Library + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/buildRelease b/tools/buildRelease new file mode 100755 index 0000000..5cb03d2 --- /dev/null +++ b/tools/buildRelease @@ -0,0 +1,305 @@ +#! /usr/bin/env python3 + +import argparse +import datetime +import fileinput +import glob +import os +import re +import shutil +import signal +import subprocess +import sys +import time +import traceback + + +# Define the (baseline) version +baseVersion = "2.0.0" + +# Define relevant base names +appBaseName = 'Glum' +libBaseName = 'glum' + +# Define the libraries Glum depends on +libList = ['guava-18.0.jar', 'miglayout-3.7.2-swing.jar'] + +# Define the paths to various executables +antPath = '/spare/apache/apache-ant-1.10.8' +antExe = os.path.join(antPath, 'bin/ant') +jdkPath = '/spare/jdk-17' +jarExe = os.path.join(jdkPath, 'bin/jar') +javaExe = os.path.join(jdkPath, 'bin/java') + + +def checkForInstalledApps(): + """Checks for installed applications needed to build a release of the + Glum library. + """ + # Ensure required applications are installed + errList = [] + for aPath in [antExe, jarExe, javaExe]: + if os.path.exists(aPath) == False: + errList.append('System executable is missing: ' + aPath) + if len(errList) > 0: + print('There are configuration errors with the environment or system.') +# print('System Path:' + str(sys.path)) + print('Please correct the following:') + for aError in errList: + print('\t' + aError) + sys.exit(0) + + +def buildLibraryJar(aVersion): + """Method that builds the library jars. Upon sucessful execution + jar files (binary + source) will be generated. + """ + # Note it is assumed that this is run from the path /proj/glum/ + + # Compile the java files + # Run the Ant script to build the class files + os.environ["JAVA_HOME"] = jdkPath + exeCmd = [antExe, '-f', 'tools/build.xml', 'compile'] + retCode = subprocess.call(exeCmd) + if retCode != 0: + print('Failed while trying to run ant script. Exiting...\n') + sys.exit(0) + print('Finished executing Ant script.\n') + + # Generate the manifest file + manifestFN = 'release/Manifest.txt' +# buildManifestFile(manifestFN) + + # Build the (bin) jar file + jarBinFN = 'release/' + libBaseName + '-' + aVersion + '.jar' + exeCmd = [jarExe, 'cfM', jarBinFN, '-C', 'bin', '.'] + retCode = subprocess.call(exeCmd) + if retCode != 0: + print('Failed to build jar file: ' + jarBinFN) + exit(-1) + + # Build the (src) jar file + jarSrcFN = 'release/' + libBaseName + '-' + aVersion + '-src.jar' + exeCmd = ['zip', '-D9q', '../' + jarSrcFN, '-r', '.'] + retCode = subprocess.call(exeCmd, cwd='./src') + if retCode != 0: + print('Failed to build jar file: ' + jarSrcFN) + exit(-1) + + # Remove the manifest file +# os.remove(manifestFN) + + +def buildManifestFile(aManifestFN): + """Generates the manifest file that is placed within the application jar. + """ + manifestFile = open(aManifestFN, 'w') + manifestFile.write('Manifest-Version: 1.0\n') + manifestFile.write('Comment-Info: Auto generated by buildRelease\n') + + exeDate = time.localtime() + buildDateStr = time.strftime('%Y%b%d %H:%M:%S', exeDate) + manifestFile.write('Comment-Time: ' + buildDateStr + '\n') +# libStr = ' '.join('lib/' + aName for aName in libList) +# manifestFile.write('Class-Path: ' + libStr + '\n') +# manifestFile.write('Main-Class: glum.app.AppGlum\n') + manifestFile.close() + + +def buildRelease(aExtraTag, aDoNotClean=False): + """Method that builds a release of Glum. Upon sucessful execution + the following will be created: + - glum-.jar + - glum--src.jar + """ + # Define the version to build + # Retrieve the date stamp to utilize as the base version for this release +# zios_2021Jan: Should we utilize chrono versioning or major.minor.path versioning + exeDate = datetime.date.today() +# baseVersion = exeDate.strftime('%Y.%m.%d') + version = baseVersion + if aExtraTag != None: + if aExtraTag.startswith('.') or aExtraTag.startswith('-'): + version = baseVersion + aExtraTag + else: + version = baseVersion + '-' + aExtraTag + + # Retrieve the install path + installPath = getInstallRoot() + installPath = os.path.dirname(installPath) + + # Define the paths of interest + jarBinPathFN = os.path.join(installPath, 'release', libBaseName + '-' + version + '.jar') + jarSrcPathFN = os.path.join(installPath, 'release', libBaseName + '-' + version + '-src.jar') + + # Let the user know of the version we are building + print('Building ' + appBaseName + ' release ' + version + '...\n') + + # Bail if there is a prior build + failMsgL = [] + if os.path.exists(jarBinPathFN) == True: + failMsgL += ['Library binary file already exists: ' + jarBinPathFN] + if os.path.exists(jarSrcPathFN) == True: + failMsgL += ['Library source file already exists: ' + jarSrcPathFN] + + if len(failMsgL) > 0: + errPrintln('Aborting ' + appBaseName + ' release build. Reasons:') + for aFailMsg in failMsgL: + errPrintln(' - ' + aFailMsg) + errPrintln('') + exit(-1) + +# # Bail if the documents have not been updated to reflect the version to be built +# isPass = True +# dstPath = os.path.join(workPath, 'doc') +# for aDoc in ['ChangeLog.txt', 'ReadMe.txt']: +# srcPath = os.path.join(installPath, 'doc', aDoc) +# isPass &= checkDocVersion(srcPath, version) +# +# # Bail if any documents have versions that do not match. They should be updated. +# if isPass == False: +# errPrintln('Please update the documentation files before a release is built.') +# errPrintln('Build has been aborted.\n') +# exit(-1) + + # Auto update the AppInfo file with the version info + appInfoSrcFile = 'src/glum/app/AppInfo.java' + isVersionFound = False + for line in fileinput.FileInput(appInfoSrcFile, inplace=1): + if re.match('^\s*private\s*static\s*String\s*Version\s*=.*', line) != None: + line = '\tprivate static String Version = "' + version + '";\n' + isVersionFound = True + print(line, end='') + + if isVersionFound == False: + errPrintln('\nFailed to locate version declaration in library!') + errPrintln('\tVersion has NOT have been updated.') + errPrintln('\tSearched file: ' + appInfoSrcFile + '\n') + + + # Build the library jar + buildLibraryJar(version) + + # Confirm that we have built the class files properly by retrieving the + # version from the built application jar and comparing it to our version. + tmpVersion = getLibraryBinVersion(jarBinPathFN) + if tmpVersion != version: + print('Failure: Embedded release: {} Expected: {}\n\tAborting...'.format(tmpVersion, version)) + exit(-1) + + print(appBaseName + ' release ' + version + ' built. Assets:') + for aFilePath in [jarBinPathFN, jarSrcPathFN]: + print(' - ' + aFilePath) + print('') + + +def checkDocVersion(aFile, aVersion): + """Method that returns true if the version in the documentation file, aFile + matches the provided version. The documentation file should be a simple text + file where the version is recorded on the 2nd line. The 2nd line is expected + to match the following pattern: + Version: + + If the version does not match then the expected input vs the actual input + will be logged to stderr. This method is useful to allow a release to be + built and ensuring that the documentation files have been updated. + """ + verStr = 'Version: ' + aVersion + + workFileInput = fileinput.FileInput(aFile, mode = 'rb') + for line in workFileInput: + lineNum = workFileInput.lineno() + + # Convert from bytes to string. If there is an issue then just assume + # the empty string. + try: + lineStr = line.decode("utf-8") + lineStr = lineStr.rstrip() + except: + lineStr = '' + + if lineNum == 2 and lineStr != verStr: + errPrintln('Documentation file does not match version.') + errPrintln(' File: ' + aFile) + errPrintln(' [Line-2] Actual: ' + lineStr) + errPrintln(' [Line-2] Expected: ' + verStr) + errPrintln('') + return False + + return True + + +def errPrintln(aMessage=''): + """Print the specified string with a trailing newline to stderr. Each tab + character will be replaced with: 3 spaces""" + aMessage = aMessage.replace('\t', ' ') + sys.stderr.write(aMessage + '\n') + + +def getLibraryBinVersion(aJarBinPath): + """Method that will return the version of the specified binary jar file. + The binary jar file will be queried (package glum.app.AppGlum) and the + output will be parsed. Any failures will result in the abrupt exit of this + script.""" + try: + cpStr = aJarBinPath + exeCmd = [javaExe, '-cp', cpStr, 'glum.app.AppGlum', '--version'] + output = subprocess.check_output(exeCmd).decode('utf-8') + version = output.split(':')[-1].strip() + return version + except Exception as aExp: + traceback.print_exc() + exit(-1) + + +def getInstallRoot(): + """Returns the root path where the running script is installed.""" + argv = sys.argv; + installRoot = os.path.dirname(argv[0]) +# print('appInstallRoot: ' + appInstallRoot) + return installRoot + + +def handleSignal(signal, frame): + """Signal handler, typically used to capture ctrl-c.""" + print('User aborted processing!') + sys.exit(0) + + +if __name__ == "__main__": + # Logic to capture Ctrl-C and bail + signal.signal(signal.SIGINT, handleSignal) + + # Require python version 3.6 or later + targVer = (3, 6) + if sys.version_info < targVer: + print('The installed version of python is too old. Please upgrade.') + print(' Current version: ' + '.'.join(str(i) for i in sys.version_info)) + print(' Require version: ' + '.'.join(str(i) for i in targVer)) + sys.exit(-1) + + tmpDescr = 'Utility to build a ' + appBaseName + ' release\n' + parser = argparse.ArgumentParser(prefix_chars='-', description=tmpDescr, add_help=False, fromfile_prefix_chars='@') + parser.add_argument('--help', '-h', help='Show this help message and exit.', action='help') + parser.add_argument('--doNotClean', default=False, action='store_true', help='Do NOT remove temporary work folder created while generating release.') + parser.add_argument('--doFullBuild', default=False, action='store_true', help='Force a full build of the main jar file. (Unsupported action)') + parser.add_argument('--extraTag', default=None, action='store', help='Specify an extra tag for to the version.') + + # Intercept any request for a help message and bail + argv = sys.argv; + if '-h' in argv or '-help' in argv or '--help' in argv: + parser.print_help() + exit() + + # Parse the args + parser.formatter_class.max_help_position = 50 + args = parser.parse_args() + + # TODO: Finish this functionality + if args.doFullBuild == True: + print("Unsupported action: [--doFullBuild]. Skipping...") + + # Ensure required applications are installed + checkForInstalledApps() + + buildRelease(args.extraTag, args.doNotClean)