Bugfixes from zOS testing
This commit is contained in:
@@ -104,6 +104,29 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ensure clicking anywhere inside the frame brings focus to window and terminalPanel
|
||||||
|
Toolkit.getDefaultToolkit().addAWTEventListener(event -> {
|
||||||
|
if (event instanceof MouseEvent && event.getID() == MouseEvent.MOUSE_PRESSED) {
|
||||||
|
MouseEvent me = (MouseEvent) event;
|
||||||
|
Object src = me.getSource();
|
||||||
|
if (src instanceof Component) {
|
||||||
|
Window win = SwingUtilities.getWindowAncestor((Component) src);
|
||||||
|
if (win == J3270App.this && !isAnyChildDialogActive()) {
|
||||||
|
if (!J3270App.this.isFocused() || !J3270App.this.isActive()) {
|
||||||
|
J3270App.this.toFront();
|
||||||
|
J3270App.this.requestFocus();
|
||||||
|
}
|
||||||
|
boolean isMenu = (src instanceof JMenuBar) || (src instanceof JMenuItem) || (src instanceof JPopupMenu)
|
||||||
|
|| SwingUtilities.getAncestorOfClass(JMenuBar.class, (Component) src) != null
|
||||||
|
|| SwingUtilities.getAncestorOfClass(JPopupMenu.class, (Component) src) != null;
|
||||||
|
if (!isMenu && terminalPanel != null && !terminalPanel.hasFocus()) {
|
||||||
|
terminalPanel.requestFocusInWindow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, AWTEvent.MOUSE_EVENT_MASK);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isAnyChildDialogActive() {
|
private boolean isAnyChildDialogActive() {
|
||||||
@@ -694,6 +717,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
dialog.setInitialTls(haus.nightmare.j3270.config.Settings.getAutoConnectTls());
|
dialog.setInitialTls(haus.nightmare.j3270.config.Settings.getAutoConnectTls());
|
||||||
dialog.setInitialVerifyCert(haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert());
|
dialog.setInitialVerifyCert(haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert());
|
||||||
dialog.setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage());
|
dialog.setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage());
|
||||||
|
dialog.setInitialGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||||
dialog.setVisible(true);
|
dialog.setVisible(true);
|
||||||
|
|
||||||
if (dialog.isConfirmed()) {
|
if (dialog.isConfirmed()) {
|
||||||
@@ -730,6 +754,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
|
haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
|
||||||
haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
|
haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
|
||||||
haus.nightmare.j3270.config.Settings.setCodePage(config.getCodePage());
|
haus.nightmare.j3270.config.Settings.setCodePage(config.getCodePage());
|
||||||
|
if (config.getGraphicsMode() != null) {
|
||||||
|
haus.nightmare.j3270.config.Settings.setGraphicsMode(config.getGraphicsMode());
|
||||||
|
}
|
||||||
|
|
||||||
disconnect();
|
disconnect();
|
||||||
|
|
||||||
|
|||||||
@@ -234,7 +234,6 @@ public class HostStorage {
|
|||||||
if (map.containsKey("tn3270e")) h.setTn3270e(parseBool(map.get("tn3270e"), true));
|
if (map.containsKey("tn3270e")) h.setTn3270e(parseBool(map.get("tn3270e"), true));
|
||||||
if (map.containsKey("graphicsMode")) {
|
if (map.containsKey("graphicsMode")) {
|
||||||
String gm = String.valueOf(map.get("graphicsMode"));
|
String gm = String.valueOf(map.get("graphicsMode"));
|
||||||
if ("PROGRAMMED_SYMBOLS".equalsIgnoreCase(gm)) gm = "BOTH";
|
|
||||||
h.setGraphicsMode(gm);
|
h.setGraphicsMode(gm);
|
||||||
}
|
}
|
||||||
if (map.containsKey("codePage")) h.setCodePage(String.valueOf(map.get("codePage")));
|
if (map.containsKey("codePage")) h.setCodePage(String.valueOf(map.get("codePage")));
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import haus.nightmare.lib3270j.ConnectionConfig;
|
|||||||
import haus.nightmare.lib3270j.TerminalModel;
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
|
import javax.swing.border.EmptyBorder;
|
||||||
import javax.swing.border.TitledBorder;
|
import javax.swing.border.TitledBorder;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -226,6 +227,24 @@ public class ConnectDialog extends JDialog {
|
|||||||
graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||||
graphicsCombo.setFont(ThemeManager.getUiFont());
|
graphicsCombo.setFont(ThemeManager.getUiFont());
|
||||||
ThemeManager.styleComboBox(graphicsCombo);
|
ThemeManager.styleComboBox(graphicsCombo);
|
||||||
|
graphicsCombo.setRenderer(new DefaultListCellRenderer() {
|
||||||
|
@Override
|
||||||
|
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
|
||||||
|
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
||||||
|
setBorder(new EmptyBorder(4, 8, 4, 8));
|
||||||
|
if (value instanceof haus.nightmare.lib3270j.graphics.GraphicsMode) {
|
||||||
|
setText(((haus.nightmare.lib3270j.graphics.GraphicsMode) value).getDescription());
|
||||||
|
}
|
||||||
|
if (isSelected) {
|
||||||
|
setBackground(ThemeManager.getSelectionBg());
|
||||||
|
setForeground(ThemeManager.getSelectionFg());
|
||||||
|
} else {
|
||||||
|
setBackground(ThemeManager.getBgComponent());
|
||||||
|
setForeground(ThemeManager.getFgMain());
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
});
|
||||||
contentPanel.add(graphicsCombo, gbc);
|
contentPanel.add(graphicsCombo, gbc);
|
||||||
|
|
||||||
gridy++;
|
gridy++;
|
||||||
@@ -664,7 +683,7 @@ public class ConnectDialog extends JDialog {
|
|||||||
|
|
||||||
String gfx = "BOTH";
|
String gfx = "BOTH";
|
||||||
if (graphicsCombo.getSelectedItem() != null) {
|
if (graphicsCombo.getSelectedItem() != null) {
|
||||||
gfx = graphicsCombo.getSelectedItem().toString();
|
gfx = ((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem()).name();
|
||||||
}
|
}
|
||||||
|
|
||||||
String cp = "037";
|
String cp = "037";
|
||||||
@@ -731,7 +750,9 @@ public class ConnectDialog extends JDialog {
|
|||||||
result.setLuName(lu);
|
result.setLuName(lu);
|
||||||
}
|
}
|
||||||
if (graphicsCombo.getSelectedItem() != null) {
|
if (graphicsCombo.getSelectedItem() != null) {
|
||||||
result.setGraphicsMode((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem());
|
haus.nightmare.lib3270j.graphics.GraphicsMode gm = (haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem();
|
||||||
|
result.setGraphicsMode(gm);
|
||||||
|
haus.nightmare.j3270.config.Settings.setGraphicsMode(gm);
|
||||||
}
|
}
|
||||||
|
|
||||||
Object cpSelection = codePageCombo.getSelectedItem();
|
Object cpSelection = codePageCombo.getSelectedItem();
|
||||||
@@ -776,6 +797,14 @@ public class ConnectDialog extends JDialog {
|
|||||||
result.setProxy(pType, pHost, pPort, pUser.isEmpty() ? null : pUser, pPass.isEmpty() ? null : pPass);
|
result.setProxy(pType, pHost, pPort, pUser.isEmpty() ? null : pUser, pPass.isEmpty() ? null : pPass);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedProfileId != null) {
|
||||||
|
SavedHost existing = HostStorage.findById(selectedProfileId);
|
||||||
|
String pName = (existing != null && existing.getName() != null && !existing.getName().trim().isEmpty())
|
||||||
|
? existing.getName() : host;
|
||||||
|
SavedHost updated = buildCurrentSavedHost(pName);
|
||||||
|
HostStorage.saveHost(updated);
|
||||||
|
}
|
||||||
|
|
||||||
confirmed = true;
|
confirmed = true;
|
||||||
dispose();
|
dispose();
|
||||||
}
|
}
|
||||||
@@ -810,6 +839,12 @@ public class ConnectDialog extends JDialog {
|
|||||||
tn3270eCheckBox.setSelected(tn3270e);
|
tn3270eCheckBox.setSelected(tn3270e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setInitialGraphicsMode(haus.nightmare.lib3270j.graphics.GraphicsMode mode) {
|
||||||
|
if (mode != null && selectedProfileId == null && graphicsCombo != null) {
|
||||||
|
graphicsCombo.setSelectedItem(mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void setInitialCodePage(String cp) {
|
public void setInitialCodePage(String cp) {
|
||||||
if (cp != null && codePageCombo != null) {
|
if (cp != null && codePageCombo != null) {
|
||||||
for (int i = 0; i < codePageCombo.getItemCount(); i++) {
|
for (int i = 0; i < codePageCombo.getItemCount(); i++) {
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
|||||||
// Font and cell dimensions
|
// Font and cell dimensions
|
||||||
private Font terminalFont;
|
private Font terminalFont;
|
||||||
private Font boldTerminalFont;
|
private Font boldTerminalFont;
|
||||||
private int cellWidth;
|
int cellWidth;
|
||||||
private int cellHeight;
|
int cellHeight;
|
||||||
private int fontAscent;
|
private int fontAscent;
|
||||||
private int fontDescent;
|
private int fontDescent;
|
||||||
private int padding = 4;
|
private int padding = 4;
|
||||||
@@ -74,6 +74,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
|||||||
private int selectionStartRow = -1, selectionStartCol = -1;
|
private int selectionStartRow = -1, selectionStartCol = -1;
|
||||||
private int selectionEndRow = -1, selectionEndCol = -1;
|
private int selectionEndRow = -1, selectionEndCol = -1;
|
||||||
private boolean isDragging = false;
|
private boolean isDragging = false;
|
||||||
|
private boolean isActivationClick = false;
|
||||||
private static final Color SELECTION_COLOR = new Color(75, 110, 175, 102); // 40% alpha blend
|
private static final Color SELECTION_COLOR = new Color(75, 110, 175, 102); // 40% alpha blend
|
||||||
|
|
||||||
// ========== Search Highlight state ==========
|
// ========== Search Highlight state ==========
|
||||||
@@ -117,7 +118,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
|||||||
/**
|
/**
|
||||||
* Compute the horizontal render offset to center the grid within the panel.
|
* Compute the horizontal render offset to center the grid within the panel.
|
||||||
*/
|
*/
|
||||||
private int getRenderOffsetX() {
|
int getRenderOffsetX() {
|
||||||
int termCols = 80;
|
int termCols = 80;
|
||||||
if (client != null) termCols = client.getScreenBuffer().getDisplayCols();
|
if (client != null) termCols = client.getScreenBuffer().getDisplayCols();
|
||||||
int gridW = termCols * cellWidth;
|
int gridW = termCols * cellWidth;
|
||||||
@@ -128,7 +129,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
|||||||
/**
|
/**
|
||||||
* Compute the vertical render offset to center the grid within the panel.
|
* Compute the vertical render offset to center the grid within the panel.
|
||||||
*/
|
*/
|
||||||
private int getRenderOffsetY() {
|
int getRenderOffsetY() {
|
||||||
int termRows = 24;
|
int termRows = 24;
|
||||||
if (client != null) termRows = client.getScreenBuffer().getDisplayRows();
|
if (client != null) termRows = client.getScreenBuffer().getDisplayRows();
|
||||||
int gridH = termRows * cellHeight;
|
int gridH = termRows * cellHeight;
|
||||||
@@ -186,7 +187,16 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
|||||||
MouseAdapter mouseHandler = new MouseAdapter() {
|
MouseAdapter mouseHandler = new MouseAdapter() {
|
||||||
@Override
|
@Override
|
||||||
public void mousePressed(MouseEvent e) {
|
public void mousePressed(MouseEvent e) {
|
||||||
requestFocusInWindow();
|
Window win = SwingUtilities.getWindowAncestor(TerminalPanel.this);
|
||||||
|
boolean wasWindowInactive = (win != null && (!win.isActive() || !win.isFocused()));
|
||||||
|
boolean wasPanelUnfocused = !hasFocus();
|
||||||
|
isActivationClick = wasWindowInactive || wasPanelUnfocused;
|
||||||
|
|
||||||
|
if (wasWindowInactive) {
|
||||||
|
win.toFront();
|
||||||
|
win.requestFocus();
|
||||||
|
}
|
||||||
|
requestFocus();
|
||||||
|
|
||||||
// Right-click context menu
|
// Right-click context menu
|
||||||
if (SwingUtilities.isRightMouseButton(e)) {
|
if (SwingUtilities.isRightMouseButton(e)) {
|
||||||
@@ -206,16 +216,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
|||||||
row = Math.max(0, Math.min(row, displayRows - 1));
|
row = Math.max(0, Math.min(row, displayRows - 1));
|
||||||
|
|
||||||
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive();
|
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive();
|
||||||
boolean isSelectableField = false;
|
if (isGraphic || lightPenMode) {
|
||||||
if (sb.isFormatted()) {
|
|
||||||
int faPos = sb.findFieldAttribute(row * displayCols + col);
|
|
||||||
if (faPos >= 0) {
|
|
||||||
int fa = sb.getCell(faPos).fa & 0xFF;
|
|
||||||
isSelectableField = haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) &&
|
|
||||||
!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isGraphic || lightPenMode || isSelectableField) {
|
|
||||||
clearSelection();
|
clearSelection();
|
||||||
} else {
|
} else {
|
||||||
selectionStartRow = row;
|
selectionStartRow = row;
|
||||||
@@ -292,7 +293,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
|||||||
row = Math.max(0, Math.min(row, displayRows - 1));
|
row = Math.max(0, Math.min(row, displayRows - 1));
|
||||||
int clickAddr = row * displayCols + col;
|
int clickAddr = row * displayCols + col;
|
||||||
|
|
||||||
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null;
|
boolean isGraphic = client.getGocaDecoder() != null &&
|
||||||
|
(client.getGocaDecoder().isGraphicsCursorActive() ||
|
||||||
|
(client.getGraphicsPlane() != null && client.getGraphicsPlane().hasContent()));
|
||||||
|
|
||||||
if (isGraphic) {
|
if (isGraphic) {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
@@ -307,8 +310,11 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
|||||||
py = Math.max(0, Math.min(py, gHeight - 1));
|
py = Math.max(0, Math.min(py, gHeight - 1));
|
||||||
client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
|
client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
|
||||||
|
|
||||||
|
int gx = client.getGocaDecoder().getGraphicCursorX();
|
||||||
|
int gy = client.getGocaDecoder().getGraphicCursorY();
|
||||||
int button = javax.swing.SwingUtilities.isRightMouseButton(e) ? 2 : 1;
|
int button = javax.swing.SwingUtilities.isRightMouseButton(e) ? 2 : 1;
|
||||||
client.getInputProcessor().sendGraphicMouseAid(
|
client.getInputProcessor().sendGraphicMouseAid(
|
||||||
|
gx, gy,
|
||||||
haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER,
|
haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER,
|
||||||
button,
|
button,
|
||||||
e.isShiftDown(),
|
e.isShiftDown(),
|
||||||
@@ -318,25 +324,57 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatic Light-Pen / Selectable Field detection on mouse click:
|
boolean wasDrag = (selectionStartRow != -1 && selectionEndRow != -1 &&
|
||||||
|
!(selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol));
|
||||||
|
boolean activation = isActivationClick;
|
||||||
|
isActivationClick = false;
|
||||||
|
|
||||||
|
if (wasDrag) {
|
||||||
|
// Preserve drag/block selection so user can interact with menus or press Ctrl+C
|
||||||
|
repaint();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single click (not a drag):
|
||||||
|
clearSelection();
|
||||||
|
sb.setCursorAddress(clickAddr);
|
||||||
|
|
||||||
|
// If this was an activation click (e.g. clicking to resume focus),
|
||||||
|
// do not fire field actions or send AIDs
|
||||||
|
if (activation) {
|
||||||
|
refreshScreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Automatic Light-Pen / Selectable Field detection on mouse click (HoD parity):
|
||||||
if (sb.isFormatted()) {
|
if (sb.isFormatted()) {
|
||||||
int faPos = sb.findFieldAttribute(clickAddr);
|
int faPos = sb.findFieldAttribute(clickAddr);
|
||||||
if (faPos >= 0) {
|
if (faPos >= 0) {
|
||||||
int fa = sb.getCell(faPos).fa & 0xFF;
|
int fa = sb.getCell(faPos).fa & 0xFF;
|
||||||
if (haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) &&
|
if (haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) &&
|
||||||
!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa)) {
|
!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa)) {
|
||||||
clearSelection();
|
int size = displayRows * displayCols;
|
||||||
if (client.lightPenSelect(clickAddr)) {
|
int desPos = (faPos + 1) % size;
|
||||||
refreshScreen();
|
char desAscii = (char) sb.getCell(desPos).ucs4;
|
||||||
return;
|
int desEbcdic = sb.getCell(desPos).ec & 0xFF;
|
||||||
|
|
||||||
|
boolean isToggleField = (desEbcdic == 0x6F || desAscii == '?' || desAscii == '\uFF1F' ||
|
||||||
|
desEbcdic == 0x6E || desAscii == '>' || desAscii == '\uFF1E');
|
||||||
|
boolean isEnterField = (desEbcdic == 0x50 || desAscii == '&' || desAscii == '\uFF06');
|
||||||
|
boolean isProtected = haus.nightmare.lib3270j.protocol.DS3270Constants.faIsProtected(fa);
|
||||||
|
|
||||||
|
// Only auto-select toggle fields (?/>), enter fields (&), or unprotected space fields
|
||||||
|
if (lightPenMode || isToggleField || isEnterField || !isProtected) {
|
||||||
|
if (client.lightPenSelect(clickAddr)) {
|
||||||
|
refreshScreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lightPenMode) {
|
if (lightPenMode) {
|
||||||
clearSelection();
|
|
||||||
sb.setCursorAddress(clickAddr);
|
|
||||||
boolean result = client.lightPenSelect(clickAddr);
|
boolean result = client.lightPenSelect(clickAddr);
|
||||||
if (result) {
|
if (result) {
|
||||||
refreshScreen();
|
refreshScreen();
|
||||||
@@ -346,12 +384,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol) {
|
refreshScreen();
|
||||||
sb.setCursorAddress(clickAddr);
|
return;
|
||||||
clearSelection();
|
|
||||||
refreshScreen();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
repaint();
|
repaint();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -304,4 +304,40 @@ public class HostStorageAndProxyTest {
|
|||||||
dialog.dispose();
|
dialog.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("SavedHost preserves all GraphicsMode settings across JSON serialization without clobbering to BOTH")
|
||||||
|
public void testSavedHostGraphicsModeSerializationRoundtrip() {
|
||||||
|
for (haus.nightmare.lib3270j.graphics.GraphicsMode gm : haus.nightmare.lib3270j.graphics.GraphicsMode.values()) {
|
||||||
|
SavedHost host = new SavedHost(
|
||||||
|
UUID.randomUUID().toString(),
|
||||||
|
"Gfx " + gm.name(),
|
||||||
|
"test.host.com",
|
||||||
|
23,
|
||||||
|
4,
|
||||||
|
24,
|
||||||
|
80,
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
"TSO",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
gm.name(),
|
||||||
|
"037",
|
||||||
|
"NONE",
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
"",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
String json = HostStorage.serializeHostsJson(List.of(host));
|
||||||
|
List<SavedHost> parsed = HostStorage.parseHostsJson(json);
|
||||||
|
assertEquals(1, parsed.size());
|
||||||
|
assertEquals(gm.name(), parsed.get(0).getGraphicsMode(),
|
||||||
|
"Graphics mode " + gm.name() + " must be preserved and not coerced to BOTH");
|
||||||
|
assertEquals(gm, parsed.get(0).toConnectionConfig().getGraphicsMode(),
|
||||||
|
"ConnectionConfig must reflect GraphicsMode." + gm.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package haus.nightmare.j3270.ui;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||||
|
import haus.nightmare.lib3270j.ConnectionState;
|
||||||
|
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.awt.HeadlessException;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.awt.event.MouseListener;
|
||||||
|
import java.awt.event.MouseMotionListener;
|
||||||
|
|
||||||
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class TerminalPanelSelectionTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Selection drag over protected/intensified fields is preserved on mouse release (Bugs 3 & 4)")
|
||||||
|
public void testSelectionOverProtectedFieldsAndReleasePreservation() {
|
||||||
|
try {
|
||||||
|
TerminalPanel panel = new TerminalPanel();
|
||||||
|
panel.setSize(800, 600);
|
||||||
|
|
||||||
|
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||||
|
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||||
|
|
||||||
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
|
sb.erase(false);
|
||||||
|
|
||||||
|
// Put a protected, intensified selectable field at pos 0 (ISPF header simulation)
|
||||||
|
sb.setCellFA(0, (byte) (FA_PRINTABLE | FA_PROTECT | FA_INT_HIGH_SEL));
|
||||||
|
String headerText = " ISPF PRIMARY OPTION MENU";
|
||||||
|
for (int i = 0; i < headerText.length(); i++) {
|
||||||
|
sb.getCell(1 + i).ucs4 = headerText.charAt(i);
|
||||||
|
sb.getCell(1 + i).ec = (byte) (0x40 + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
panel.setClient(client);
|
||||||
|
|
||||||
|
// Cell dimensions in panel
|
||||||
|
int cellW = panel.cellWidth > 0 ? panel.cellWidth : 10;
|
||||||
|
int cellH = panel.cellHeight > 0 ? panel.cellHeight : 18;
|
||||||
|
int ox = panel.getRenderOffsetX();
|
||||||
|
int oy = panel.getRenderOffsetY();
|
||||||
|
|
||||||
|
MouseListener[] mouseListeners = panel.getMouseListeners();
|
||||||
|
MouseMotionListener[] motionListeners = panel.getMouseMotionListeners();
|
||||||
|
assertTrue(mouseListeners.length > 0);
|
||||||
|
assertTrue(motionListeners.length > 0);
|
||||||
|
|
||||||
|
MouseListener ml = mouseListeners[0];
|
||||||
|
MouseMotionListener mml = motionListeners[0];
|
||||||
|
|
||||||
|
// 1. Mouse Pressed at cell (row 0, col 2)
|
||||||
|
int startX = ox + 2 * cellW + cellW / 2;
|
||||||
|
int startY = oy + cellH / 2;
|
||||||
|
MouseEvent pressEvent = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
|
||||||
|
0, startX, startY, 1, false, MouseEvent.BUTTON1);
|
||||||
|
ml.mousePressed(pressEvent);
|
||||||
|
|
||||||
|
// 2. Drag to cell (row 0, col 15)
|
||||||
|
int dragX = ox + 15 * cellW + cellW / 2;
|
||||||
|
int dragY = oy + cellH / 2;
|
||||||
|
MouseEvent dragEvent = new MouseEvent(panel, MouseEvent.MOUSE_DRAGGED, System.currentTimeMillis(),
|
||||||
|
0, dragX, dragY, 1, false, MouseEvent.BUTTON1);
|
||||||
|
mml.mouseDragged(dragEvent);
|
||||||
|
|
||||||
|
assertTrue(panel.hasSelection(), "Drag over protected field must create active selection (Bug 3)");
|
||||||
|
|
||||||
|
// 3. Mouse Released at cell (row 0, col 15)
|
||||||
|
MouseEvent releaseEvent = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
|
||||||
|
0, dragX, dragY, 1, false, MouseEvent.BUTTON1);
|
||||||
|
ml.mouseReleased(releaseEvent);
|
||||||
|
|
||||||
|
// Selection MUST NOT be dropped upon mouse release! (Bug 4)
|
||||||
|
assertTrue(panel.hasSelection(), "Selection must remain active after mouse release (Bug 4)");
|
||||||
|
String selected = panel.getSelectedText();
|
||||||
|
assertNotNull(selected);
|
||||||
|
assertTrue(selected.length() > 0, "Selected text must not be empty");
|
||||||
|
|
||||||
|
// 4. A subsequent single click without drag should clear the selection and move the cursor
|
||||||
|
int clickX = ox + 5 * cellW + cellW / 2;
|
||||||
|
int clickY = oy + cellH / 2;
|
||||||
|
MouseEvent clickPress = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
|
||||||
|
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
|
||||||
|
ml.mousePressed(clickPress);
|
||||||
|
MouseEvent clickRelease = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
|
||||||
|
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
|
||||||
|
ml.mouseReleased(clickRelease);
|
||||||
|
|
||||||
|
assertFalse(panel.hasSelection(), "Single click must clear previous selection");
|
||||||
|
assertEquals(5, sb.getCursorAddress(), "Single click must position cursor at clicked cell");
|
||||||
|
|
||||||
|
} catch (HeadlessException e) {
|
||||||
|
// Handled gracefully in headless environments
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -456,11 +456,26 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
|
|||||||
this.bytesTransferred = 0;
|
this.bytesTransferred = 0;
|
||||||
this.totalBytes = (config.isSend() && localFile.exists()) ? localFile.length() : 0;
|
this.totalBytes = (config.isSend() && localFile.exists()) ? localFile.length() : 0;
|
||||||
|
|
||||||
|
// Auto-detect DBCS from translator codepage if not explicitly set
|
||||||
|
if (!config.isDbcs() && translator != null && translator.getCodePage() != null && translator.getCodePage().isDBCS()) {
|
||||||
|
config.setDbcs(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for keyboard unlock before typing command
|
||||||
|
waitForKeyboardUnlock(2000);
|
||||||
|
|
||||||
|
// If clear is requested, send Clear AID first to wipe intermediate screen status
|
||||||
|
if (config.isClear() && input != null && !input.isNvtMode()) {
|
||||||
|
input.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR);
|
||||||
|
try { Thread.sleep(150); } catch (InterruptedException ignored) {}
|
||||||
|
waitForKeyboardUnlock(2000);
|
||||||
|
}
|
||||||
|
|
||||||
String command = config.buildCommand();
|
String command = config.buildCommand();
|
||||||
log.info("ECLXfer: initiating transfer: " + command);
|
log.info("ECLXfer: initiating transfer: " + command);
|
||||||
|
|
||||||
int capacity = input.kybdPrime();
|
int capacity = input != null ? input.kybdPrime() : 0;
|
||||||
if (capacity < command.length()) {
|
if (input != null && capacity < command.length()) {
|
||||||
String err = "Input field capacity insufficient for command (" + capacity + " chars)";
|
String err = "Input field capacity insufficient for command (" + capacity + " chars)";
|
||||||
fireEvent(ECLXferEvent.XFER_ABORTED, FTConstants.ECL_ERR_XFER_ABORT, err);
|
fireEvent(ECLXferEvent.XFER_ABORTED, FTConstants.ECL_ERR_XFER_ABORT, err);
|
||||||
return FTConstants.ECL_ERR_XFER_ABORT;
|
return FTConstants.ECL_ERR_XFER_ABORT;
|
||||||
@@ -469,10 +484,25 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
|
|||||||
setState(FTState.AWAIT_ACK);
|
setState(FTState.AWAIT_ACK);
|
||||||
fireEvent(ECLXferEvent.XFER_STARTED, 0, "Transfer initiated");
|
fireEvent(ECLXferEvent.XFER_STARTED, 0, "Transfer initiated");
|
||||||
|
|
||||||
input.emulateInput(command + "\n");
|
if (input != null) {
|
||||||
|
input.emulateInput(command + "\n");
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void waitForKeyboardUnlock(long timeoutMs) {
|
||||||
|
if (input == null) return;
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
while (input.isKeyboardLocked() && (System.currentTimeMillis() - start) < timeoutMs) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(20);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void parseOptionsIntoConfig(FTConfig config, String options) {
|
private void parseOptionsIntoConfig(FTConfig config, String options) {
|
||||||
if (options == null || options.trim().isEmpty()) return;
|
if (options == null || options.trim().isEmpty()) return;
|
||||||
config.parseOptions(options);
|
config.parseOptions(options);
|
||||||
|
|||||||
@@ -74,6 +74,11 @@ public class FTConfig {
|
|||||||
private String codePage = null;
|
private String codePage = null;
|
||||||
private boolean unicode = false;
|
private boolean unicode = false;
|
||||||
private int unicodeType = 0; // 0 = UCS2, 1 = UTF8
|
private int unicodeType = 0; // 0 = UCS2, 1 = UTF8
|
||||||
|
private String programName = null;
|
||||||
|
private boolean dbcs = false;
|
||||||
|
private boolean optionNew = false;
|
||||||
|
private boolean clear = true;
|
||||||
|
private boolean optionNoso = false;
|
||||||
|
|
||||||
// ========== Derived convenience getters ==========
|
// ========== Derived convenience getters ==========
|
||||||
|
|
||||||
@@ -91,6 +96,37 @@ public class FTConfig {
|
|||||||
}
|
}
|
||||||
public boolean isAppend() { return existAction == ExistAction.APPEND; }
|
public boolean isAppend() { return existAction == ExistAction.APPEND; }
|
||||||
public boolean isOverwrite() { return existAction == ExistAction.REPLACE; }
|
public boolean isOverwrite() { return existAction == ExistAction.REPLACE; }
|
||||||
|
public boolean isOptionNew() { return optionNew; }
|
||||||
|
public void setOptionNew(boolean optionNew) { this.optionNew = optionNew; }
|
||||||
|
public boolean isOptionNoso() { return optionNoso; }
|
||||||
|
public void setOptionNoso(boolean optionNoso) { this.optionNoso = optionNoso; }
|
||||||
|
public boolean isDbcs() { return dbcs; }
|
||||||
|
public void setDbcs(boolean dbcs) { this.dbcs = dbcs; }
|
||||||
|
public boolean isClear() { return clear; }
|
||||||
|
public void setClear(boolean clear) { this.clear = clear; }
|
||||||
|
|
||||||
|
public String getProgramName() {
|
||||||
|
if (programName != null && !programName.trim().isEmpty()) {
|
||||||
|
return programName.trim();
|
||||||
|
}
|
||||||
|
if (dbcs || isDbcsCodePage(codePage)) {
|
||||||
|
return "APVUFILE";
|
||||||
|
}
|
||||||
|
return "IND$FILE";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProgramName(String programName) {
|
||||||
|
this.programName = programName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isDbcsCodePage(String cp) {
|
||||||
|
if (cp == null || cp.trim().isEmpty()) return false;
|
||||||
|
String clean = cp.trim().toUpperCase();
|
||||||
|
if (clean.startsWith("CP")) clean = clean.substring(2);
|
||||||
|
return clean.equals("930") || clean.equals("939") || clean.equals("935") ||
|
||||||
|
clean.equals("1388") || clean.equals("933") || clean.equals("937") ||
|
||||||
|
clean.equals("1371");
|
||||||
|
}
|
||||||
|
|
||||||
// ========== Standard getters and setters ==========
|
// ========== Standard getters and setters ==========
|
||||||
|
|
||||||
@@ -303,10 +339,22 @@ public class FTConfig {
|
|||||||
|
|
||||||
if (upper.contains("APPEND")) {
|
if (upper.contains("APPEND")) {
|
||||||
this.existAction = ExistAction.APPEND;
|
this.existAction = ExistAction.APPEND;
|
||||||
|
} else if (upper.contains("NEW") && !upper.contains("NONEW")) {
|
||||||
|
this.optionNew = true;
|
||||||
} else if (upper.contains("REPLACE") || upper.contains("OVERWRITE")) {
|
} else if (upper.contains("REPLACE") || upper.contains("OVERWRITE")) {
|
||||||
this.existAction = ExistAction.REPLACE;
|
this.existAction = ExistAction.REPLACE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (upper.contains("NOSO")) {
|
||||||
|
this.optionNoso = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upper.contains("APVUFILE")) {
|
||||||
|
this.programName = "APVUFILE";
|
||||||
|
} else if (upper.contains("IND$FILE")) {
|
||||||
|
this.programName = "IND$FILE";
|
||||||
|
}
|
||||||
|
|
||||||
if (upper.contains("CICS")) {
|
if (upper.contains("CICS")) {
|
||||||
this.hostType = HostType.CICS;
|
this.hostType = HostType.CICS;
|
||||||
} else if (upper.contains("CMS") || upper.contains("VM")) {
|
} else if (upper.contains("CMS") || upper.contains("VM")) {
|
||||||
@@ -332,6 +380,10 @@ public class FTConfig {
|
|||||||
|
|
||||||
String hostTrimmed = hostFilename.trim();
|
String hostTrimmed = hostFilename.trim();
|
||||||
|
|
||||||
|
if (hostType == HostType.TSO && hostTrimmed.contains(" ") && validateCmsFilename(hostTrimmed) == null) {
|
||||||
|
hostType = HostType.CMS;
|
||||||
|
}
|
||||||
|
|
||||||
if (hostType == HostType.TSO) {
|
if (hostType == HostType.TSO) {
|
||||||
String tsoError = validateTsoDatasetName(hostTrimmed);
|
String tsoError = validateTsoDatasetName(hostTrimmed);
|
||||||
if (tsoError != null) return tsoError;
|
if (tsoError != null) return tsoError;
|
||||||
@@ -448,8 +500,8 @@ public class FTConfig {
|
|||||||
public String buildCommand() {
|
public String buildCommand() {
|
||||||
StringBuilder cmd = new StringBuilder();
|
StringBuilder cmd = new StringBuilder();
|
||||||
|
|
||||||
// IND$FILE GET/PUT hostfile
|
// Host program selection: IND$FILE or APVUFILE
|
||||||
cmd.append("IND$FILE ");
|
cmd.append(getProgramName()).append(" ");
|
||||||
cmd.append(isReceive() ? "GET " : "PUT ");
|
cmd.append(isReceive() ? "GET " : "PUT ");
|
||||||
cmd.append(hostFilename != null ? hostFilename.trim() : "");
|
cmd.append(hostFilename != null ? hostFilename.trim() : "");
|
||||||
|
|
||||||
@@ -477,16 +529,23 @@ public class FTConfig {
|
|||||||
opts.append("NOCRLF");
|
opts.append("NOCRLF");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append (send only)
|
if (isOptionNoso()) {
|
||||||
if (isAppend() && isSend()) {
|
|
||||||
if (opts.length() > 0) opts.append(" ");
|
if (opts.length() > 0) opts.append(" ");
|
||||||
opts.append("APPEND");
|
opts.append("NOSO");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overwrite / Replace (if send or explicitly requested)
|
// Append / New / Replace (send only)
|
||||||
if (isOverwrite() && isSend()) {
|
if (isSend()) {
|
||||||
if (opts.length() > 0) opts.append(" ");
|
if (isAppend()) {
|
||||||
opts.append("REPLACE");
|
if (opts.length() > 0) opts.append(" ");
|
||||||
|
opts.append("APPEND");
|
||||||
|
} else if (isOptionNew()) {
|
||||||
|
if (opts.length() > 0) opts.append(" ");
|
||||||
|
opts.append("NEW");
|
||||||
|
} else if (isOverwrite()) {
|
||||||
|
if (opts.length() > 0) opts.append(" ");
|
||||||
|
opts.append("REPLACE");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Host-specific send options
|
// Host-specific send options
|
||||||
|
|||||||
@@ -123,15 +123,69 @@ public final class FTConstants {
|
|||||||
// Error codes
|
// Error codes
|
||||||
public static final int TR_ERR_EOF = 0x2200;
|
public static final int TR_ERR_EOF = 0x2200;
|
||||||
public static final int TR_ERR_CMDFAIL = 0x0100;
|
public static final int TR_ERR_CMDFAIL = 0x0100;
|
||||||
|
public static final int TR_ERR_OPEN_FAILED = 0x0100;
|
||||||
|
public static final int TR_ERR_SEQ_NOT_ALLOWED = 0x0200;
|
||||||
|
public static final int TR_ERR_NOT_AUTHORIZED = 0x3E00;
|
||||||
|
public static final int TR_ERR_SYNTAX_ERROR = 0x6000;
|
||||||
|
public static final int TR_ERR_GET_PAST_EOF = 0x2200;
|
||||||
|
|
||||||
// DFT buffer size limits
|
// DFT buffer size limits (Default matching IBM HoD bmtuSize = 2500)
|
||||||
public static final int DFT_MIN_BUF = 256;
|
public static final int DFT_MIN_BUF = 256;
|
||||||
public static final int DFT_MAX_BUF = 32768;
|
public static final int DFT_MAX_BUF = 32768;
|
||||||
public static final int DFT_BUF = 4096;
|
public static final int DFT_BUF = 2500;
|
||||||
|
|
||||||
// AID for structured field response
|
// AID for structured field response
|
||||||
public static final int AID_SF = 0x88;
|
public static final int AID_SF = 0x88;
|
||||||
|
|
||||||
|
// DDM Stream Names (ASCII and EBCDIC)
|
||||||
|
public static final String STREAM_NAME_FT_DATA = "FT:DATA";
|
||||||
|
public static final String STREAM_NAME_FT_MSG = "FT:MSG";
|
||||||
|
public static final byte[] STREAM_NAME_FT_DATA_BYTES_ASCII = {
|
||||||
|
0x46, 0x54, 0x3A, 0x44, 0x41, 0x54, 0x41 // 'F','T',':','D','A','T','A'
|
||||||
|
};
|
||||||
|
public static final byte[] STREAM_NAME_FT_DATA_BYTES_EBCDIC = {
|
||||||
|
(byte) 0xC6, (byte) 0xE3, 0x7A, (byte) 0xC4, (byte) 0xC1, (byte) 0xE3, (byte) 0xC1
|
||||||
|
};
|
||||||
|
public static final byte[] STREAM_NAME_FT_MSG_BYTES_ASCII = {
|
||||||
|
0x46, 0x54, 0x3A, 0x4D, 0x53, 0x47, 0x20 // 'F','T',':','M','S','G',' '
|
||||||
|
};
|
||||||
|
public static final byte[] STREAM_NAME_FT_MSG_BYTES_EBCDIC = {
|
||||||
|
(byte) 0xC6, (byte) 0xE3, 0x7A, (byte) 0xD4, (byte) 0xE2, (byte) 0xC7, 0x40
|
||||||
|
};
|
||||||
|
|
||||||
|
// HoD Binary Packet Templates (Xfer3270.java)
|
||||||
|
public static final byte[] RB_OPEN_ACK = {
|
||||||
|
(byte) 0x88, 0x00, 0x05, (byte) 0xD0, 0x00, 0x09, (byte) 0xFF, (byte) 0xEF
|
||||||
|
};
|
||||||
|
public static final byte[] RB_CLOSE_ACK = {
|
||||||
|
(byte) 0x88, 0x00, 0x05, (byte) 0xD0, 0x41, 0x09, (byte) 0xFF, (byte) 0xEF
|
||||||
|
};
|
||||||
|
public static final byte[] RB_OPEN_FAILED = {
|
||||||
|
0x00, 0x09, (byte) 0xD0, 0x00, 0x08, 0x69, 0x04, 0x01, 0x00, (byte) 0xFF, (byte) 0xEF
|
||||||
|
};
|
||||||
|
public static final byte[] RB_GET_PAST_EOF = {
|
||||||
|
(byte) 0x88, 0x00, 0x09, (byte) 0xD0, 0x46, 0x08, 0x69, 0x04, 0x22, 0x00, (byte) 0xFF, (byte) 0xEF
|
||||||
|
};
|
||||||
|
public static final byte[] RB_UPLOAD_DATA_HEADER = {
|
||||||
|
0x11, 0x11, (byte) 0xD0, 0x46, 0x05, 0x63, 0x06, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
(byte) 0xC0, (byte) 0x80, 0x61, 0x00, 0x00
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds exact 14-byte Data Acknowledgement matching HoD Xfer3270:
|
||||||
|
* [0x88, 0x00, 0x0B, 0xD0, 0x47, 0x05, 0x63, 0x06, rec_b0, rec_b1, rec_b2, rec_b3, 0xFF, 0xEF]
|
||||||
|
*/
|
||||||
|
public static byte[] createDataAck(long recnum) {
|
||||||
|
return new byte[] {
|
||||||
|
(byte) 0x88, 0x00, 0x0B, (byte) 0xD0, 0x47, 0x05, 0x63, 0x06,
|
||||||
|
(byte) ((recnum >> 24) & 0xFF),
|
||||||
|
(byte) ((recnum >> 16) & 0xFF),
|
||||||
|
(byte) ((recnum >> 8) & 0xFF),
|
||||||
|
(byte) (recnum & 0xFF),
|
||||||
|
(byte) 0xFF, (byte) 0xEF
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ========== Transfer State ==========
|
// ========== Transfer State ==========
|
||||||
|
|
||||||
public enum FTState {
|
public enum FTState {
|
||||||
|
|||||||
@@ -215,11 +215,13 @@ public class FTCut {
|
|||||||
// CUT frames MUST have a skip field attribute at O_SF (1919)
|
// CUT frames MUST have a skip field attribute at O_SF (1919)
|
||||||
byte sfAttr = screen.getCellFAByte(O_SF);
|
byte sfAttr = screen.getCellFAByte(O_SF);
|
||||||
if (sfAttr == 0 || !isSkip(sfAttr)) {
|
if (sfAttr == 0 || !isSkip(sfAttr)) {
|
||||||
// Not a CUT frame (likely local echo, menu return, or intermediate screen)
|
// Not a CUT frame (likely local echo, menu return, or host error screen)
|
||||||
if (xferInProgress) {
|
if (xferInProgress) {
|
||||||
log.warning("CUT: Received non-CUT frame while transfer in progress. Host aborted.");
|
log.warning("CUT: Received non-CUT frame while transfer in progress. Host aborted.");
|
||||||
xferInProgress = false;
|
xferInProgress = false;
|
||||||
listener.onTransferAborted("Host aborted transfer (returned to menu)");
|
String hostErr = scanScreenForErrors();
|
||||||
|
String abortMsg = hostErr != null ? hostErr : "Host aborted transfer (returned to menu)";
|
||||||
|
listener.onTransferAborted(abortMsg);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -245,6 +247,38 @@ public class FTCut {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans rows 1-24 of the 3270 screen buffer for host error indicators.
|
||||||
|
* Checks for TSO (IKJ*), CMS (DMS*), CICS (DFH*), and generic transfer abort messages.
|
||||||
|
*/
|
||||||
|
public String scanScreenForErrors() {
|
||||||
|
if (screen == null) return null;
|
||||||
|
int rows = screen.getRows();
|
||||||
|
int cols = screen.getCols();
|
||||||
|
String[] errorPrefixes = {
|
||||||
|
"IKJ", "DMS", "DFH", "TRANS", "NOT FOUND", "ERROR", "REJECTED", "INVALID", "COMMAND NOT FOUND", "FILE NOT FOUND"
|
||||||
|
};
|
||||||
|
for (int r = 0; r < rows; r++) {
|
||||||
|
StringBuilder lineSb = new StringBuilder();
|
||||||
|
for (int c = 0; c < cols; c++) {
|
||||||
|
int pos = r * cols + c;
|
||||||
|
int ec = screen.getCellEC(pos);
|
||||||
|
if (ec != 0) {
|
||||||
|
lineSb.append(translator.ebcdicToUnicode(ec));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String line = lineSb.toString().trim();
|
||||||
|
if (line.isEmpty()) continue;
|
||||||
|
String lineUpper = line.toUpperCase();
|
||||||
|
for (String prefix : errorPrefixes) {
|
||||||
|
if (lineUpper.contains(prefix)) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private void checkForHostPrompts() {
|
private void checkForHostPrompts() {
|
||||||
if (screen == null) return;
|
if (screen == null) return;
|
||||||
int size = screen.getRows() * screen.getCols();
|
int size = screen.getRows() * screen.getCols();
|
||||||
@@ -299,6 +333,10 @@ public class FTCut {
|
|||||||
cutAck();
|
cutAck();
|
||||||
|
|
||||||
String msg = extractHostMessage();
|
String msg = extractHostMessage();
|
||||||
|
if ("Host cancelled transfer".equals(msg)) {
|
||||||
|
String hostErr = scanScreenForErrors();
|
||||||
|
if (hostErr != null) msg = hostErr;
|
||||||
|
}
|
||||||
listener.onTransferAborted(msg);
|
listener.onTransferAborted(msg);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -388,7 +426,7 @@ public class FTCut {
|
|||||||
log.fine("CUT: > DATA seq=" + seq + " len=" + count);
|
log.fine("CUT: > DATA seq=" + seq + " len=" + count);
|
||||||
expandedLength += count;
|
expandedLength += count;
|
||||||
listener.onBytesTransferred(expandedLength);
|
listener.onBytesTransferred(expandedLength);
|
||||||
input.sendAidForFT(AID_ENTER);
|
paceAndSendAid(AID_ENTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Data (Download) ==========
|
// ========== Data (Download) ==========
|
||||||
@@ -467,11 +505,11 @@ public class FTCut {
|
|||||||
screen.setCellFA(O_DR_SF, attr);
|
screen.setCellFA(O_DR_SF, attr);
|
||||||
|
|
||||||
log.fine("CUT: Retransmitting last upload frame (len=" + lastUploadCount + ")");
|
log.fine("CUT: Retransmitting last upload frame (len=" + lastUploadCount + ")");
|
||||||
input.sendAidForFT(AID_ENTER);
|
paceAndSendAid(AID_ENTER);
|
||||||
} else {
|
} else {
|
||||||
// In download mode, send ACK_RETRANSMIT (PF1)
|
// In download mode, send ACK_RETRANSMIT (PF1)
|
||||||
log.fine("CUT: Requesting frame retransmit from host via PF1");
|
log.fine("CUT: Requesting frame retransmit from host via PF1");
|
||||||
input.sendAidForFT(ACK_RETRANSMIT);
|
paceAndSendAid(ACK_RETRANSMIT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,7 +517,7 @@ public class FTCut {
|
|||||||
|
|
||||||
private void cutAck() {
|
private void cutAck() {
|
||||||
log.fine("CUT: > ACK (Enter)");
|
log.fine("CUT: > ACK (Enter)");
|
||||||
input.sendAidForFT(AID_ENTER);
|
paceAndSendAid(AID_ENTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Abort ==========
|
// ========== Abort ==========
|
||||||
@@ -492,10 +530,24 @@ public class FTCut {
|
|||||||
screen.setCell(RO_REASON_CODE, (reason >> 8) & 0xFF);
|
screen.setCell(RO_REASON_CODE, (reason >> 8) & 0xFF);
|
||||||
screen.setCell(RO_REASON_CODE + 1, reason & 0xFF);
|
screen.setCell(RO_REASON_CODE + 1, reason & 0xFF);
|
||||||
|
|
||||||
input.sendAidForFT(AID_PF2);
|
paceAndSendAid(AID_PF2);
|
||||||
listener.onTransferAborted(message);
|
listener.onTransferAborted(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void paceAndSendAid(int aid) {
|
||||||
|
if (input == null) return;
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
while (input.isKeyboardLocked() && System.currentTimeMillis() - start < 1000) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(10);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input.sendAidForFT((byte) aid);
|
||||||
|
}
|
||||||
|
|
||||||
// ========== Character Translation ==========
|
// ========== Character Translation ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ public class FTDft {
|
|||||||
private int customMtuSize = 0;
|
private int customMtuSize = 0;
|
||||||
private int pendingByte = -1;
|
private int pendingByte = -1;
|
||||||
|
|
||||||
|
// Record formatting buffers and SO/SI tracking
|
||||||
|
private final ByteArrayOutputStream downloadRecordBuffer = new ByteArrayOutputStream();
|
||||||
|
private boolean dbcsMode = false;
|
||||||
|
private boolean hasPendingByteInDBCS = false;
|
||||||
|
private byte pendingByteDBCS = 0;
|
||||||
|
private byte[] pendingUploadRecord = null;
|
||||||
|
private int pendingUploadRecordOffset = 0;
|
||||||
|
|
||||||
// Savebuf for Read Modified retransmit
|
// Savebuf for Read Modified retransmit
|
||||||
private byte[] dftSaveBuf = null;
|
private byte[] dftSaveBuf = null;
|
||||||
private int dftSaveBufLen = 0;
|
private int dftSaveBufLen = 0;
|
||||||
@@ -159,6 +167,12 @@ public class FTDft {
|
|||||||
dftSaveBufLen = 0;
|
dftSaveBufLen = 0;
|
||||||
lastCr = false;
|
lastCr = false;
|
||||||
pendingByte = -1;
|
pendingByte = -1;
|
||||||
|
downloadRecordBuffer.reset();
|
||||||
|
dbcsMode = false;
|
||||||
|
hasPendingByteInDBCS = false;
|
||||||
|
pendingByteDBCS = 0;
|
||||||
|
pendingUploadRecord = null;
|
||||||
|
pendingUploadRecordOffset = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -201,6 +215,12 @@ public class FTDft {
|
|||||||
|
|
||||||
log.fine("DFT: request code 0x" + Integer.toHexString(requestCode));
|
log.fine("DFT: request code 0x" + Integer.toHexString(requestCode));
|
||||||
|
|
||||||
|
// Check if this is a host error reply (e.g. TR_ERROR_REPLY 0x08, or contains TR_ERROR_HDR 0x6904)
|
||||||
|
if ((requestCode & 0xFF) == TR_ERROR_REPLY || isErrorField(data, payloadStart, offset + length - payloadStart)) {
|
||||||
|
handleHostErrorReply(data, payloadStart, offset + length - payloadStart);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (requestCode) {
|
switch (requestCode) {
|
||||||
case TR_OPEN_REQ:
|
case TR_OPEN_REQ:
|
||||||
dftOpenRequest(data, offset, length);
|
dftOpenRequest(data, offset, length);
|
||||||
@@ -228,8 +248,74 @@ public class FTDft {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isErrorField(byte[] data, int start, int length) {
|
||||||
|
int end = Math.min(start + length, data.length);
|
||||||
|
for (int i = start; i + 3 < end; i++) {
|
||||||
|
int hdr = ((data[i] & 0xFF) << 8) | (data[i + 1] & 0xFF);
|
||||||
|
if (hdr == TR_ERROR_HDR) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleHostErrorReply(byte[] data, int payloadStart, int length) {
|
||||||
|
int end = Math.min(payloadStart + length, data.length);
|
||||||
|
int errCode = 0;
|
||||||
|
for (int i = payloadStart; i + 3 < end; i++) {
|
||||||
|
int hdr = ((data[i] & 0xFF) << 8) | (data[i + 1] & 0xFF);
|
||||||
|
if (hdr == TR_ERROR_HDR) {
|
||||||
|
errCode = ((data[i + 2] & 0xFF) << 8) | (data[i + 3] & 0xFF);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String message;
|
||||||
|
switch (errCode) {
|
||||||
|
case TR_ERR_OPEN_FAILED:
|
||||||
|
message = "Host file open failed (file not found, cannot open, or dataset does not exist)";
|
||||||
|
break;
|
||||||
|
case TR_ERR_SEQ_NOT_ALLOWED:
|
||||||
|
message = "Host transfer sequence not allowed (state violation)";
|
||||||
|
break;
|
||||||
|
case TR_ERR_GET_PAST_EOF:
|
||||||
|
if (listener != null && listener.getCurrentState() == FTState.RUNNING) {
|
||||||
|
log.info("DFT: Host reported end of file (0x2200)");
|
||||||
|
listener.onTransferComplete(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
message = "Host reached end of file";
|
||||||
|
break;
|
||||||
|
case TR_ERR_NOT_AUTHORIZED:
|
||||||
|
message = "Host file access not authorized (security violation)";
|
||||||
|
break;
|
||||||
|
case TR_ERR_SYNTAX_ERROR:
|
||||||
|
message = "Host command syntax error or invalid transfer options";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
message = "Host transfer error (code 0x" + Integer.toHexString(errCode) + ")";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
log.warning("DFT host error reply: " + message + " (errCode=0x" + Integer.toHexString(errCode) + ")");
|
||||||
|
dftAbort(message, TR_DATA_INSERT);
|
||||||
|
}
|
||||||
|
|
||||||
// ========== Open Request ==========
|
// ========== Open Request ==========
|
||||||
|
|
||||||
|
private static boolean containsBytes(byte[] data, int start, int end, byte[] pattern) {
|
||||||
|
if (data == null || pattern == null || pattern.length == 0) return false;
|
||||||
|
int max = Math.min(end, data.length) - pattern.length;
|
||||||
|
for (int i = Math.max(0, start); i <= max; i++) {
|
||||||
|
boolean match = true;
|
||||||
|
for (int j = 0; j < pattern.length; j++) {
|
||||||
|
if (data[i + j] != pattern[j]) {
|
||||||
|
match = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (match) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private void dftOpenRequest(byte[] data, int sfOffset, int sfLength) {
|
private void dftOpenRequest(byte[] data, int sfOffset, int sfLength) {
|
||||||
log.fine("DFT: Open request");
|
log.fine("DFT: Open request");
|
||||||
|
|
||||||
@@ -247,7 +333,10 @@ public class FTDft {
|
|||||||
// Check for host DDM file attributes embedded in open request
|
// Check for host DDM file attributes embedded in open request
|
||||||
parseDdmAttributes(data, sfOffset + 3, sfLength - 3);
|
parseDdmAttributes(data, sfOffset + 3, sfLength - 3);
|
||||||
|
|
||||||
if (isMessageStream(nameBuf)) {
|
int sfEnd = Math.min(sfOffset + sfLength, data.length);
|
||||||
|
if (containsBytes(data, sfOffset, sfEnd, STREAM_NAME_FT_MSG_BYTES_ASCII) ||
|
||||||
|
containsBytes(data, sfOffset, sfEnd, STREAM_NAME_FT_MSG_BYTES_EBCDIC) ||
|
||||||
|
isMessageStream(nameBuf)) {
|
||||||
messageFlag = true;
|
messageFlag = true;
|
||||||
log.info("DFT: Open request for message stream (" + nameBuf + ")");
|
log.info("DFT: Open request for message stream (" + nameBuf + ")");
|
||||||
} else {
|
} else {
|
||||||
@@ -258,7 +347,7 @@ public class FTDft {
|
|||||||
dftEof = false;
|
dftEof = false;
|
||||||
recnum = 1;
|
recnum = 1;
|
||||||
|
|
||||||
// Acknowledge Open matching x3270 (SF_TRANSFER_DATA + 0x0009)
|
// Acknowledge Open matching IBM Host On-Demand
|
||||||
dftOpenAck();
|
dftOpenAck();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,12 +411,7 @@ public class FTDft {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void dftOpenAck() {
|
private void dftOpenAck() {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(6);
|
input.sendStructuredFieldData(RB_OPEN_ACK.clone());
|
||||||
out.write(AID_SF);
|
|
||||||
out.write(0); out.write(5); // SF length
|
|
||||||
out.write(SF_TRANSFER_DATA);
|
|
||||||
out.write(0); out.write(9); // OpenAck response code 0x0009
|
|
||||||
input.sendStructuredFieldData(out.toByteArray());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Insert Request (host sending data for download) ==========
|
// ========== Insert Request (host sending data for download) ==========
|
||||||
@@ -441,31 +525,125 @@ public class FTDft {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write download data to the local file, handling ASCII conversion.
|
* Write download data to the local file, handling ASCII conversion, RECFM F/V formatting, and EOF stripping.
|
||||||
* Matching x3270 upload_convert logic.
|
* Conforms to IBM Host On-Demand specification.
|
||||||
*/
|
*/
|
||||||
private void writeDownloadData(byte[] data, int offset, int length,
|
private void writeDownloadData(byte[] data, int offset, int length,
|
||||||
FTConfig config) throws IOException {
|
FTConfig config) throws IOException {
|
||||||
if (outputStream == null) return;
|
if (outputStream == null || length <= 0) return;
|
||||||
|
|
||||||
if (config != null && config.isUnicode()) {
|
if (config != null && config.isUnicode()) {
|
||||||
outputStream.write(data, offset, length);
|
outputStream.write(data, offset, length);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.isAscii()) {
|
if (config != null && !config.isAscii()) {
|
||||||
outputStream.write(data, offset, length);
|
outputStream.write(data, offset, length);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean isRecfmV = (config != null && config.getRecfm() == FTConfig.RecordFormat.VARIABLE) ||
|
||||||
|
"V".equalsIgnoreCase(hostRecfm);
|
||||||
|
boolean isRecfmF = (config != null && config.getRecfm() == FTConfig.RecordFormat.FIXED) ||
|
||||||
|
"F".equalsIgnoreCase(hostRecfm);
|
||||||
|
int lrecl = (config != null && config.getLrecl() > 0) ? config.getLrecl() : hostLrecl;
|
||||||
|
|
||||||
|
if (isRecfmV) {
|
||||||
|
downloadRecordBuffer.write(data, offset, length);
|
||||||
|
processVariableDownloadRecords(config);
|
||||||
|
} else if (isRecfmF && lrecl > 0) {
|
||||||
|
downloadRecordBuffer.write(data, offset, length);
|
||||||
|
processFixedDownloadRecords(lrecl, config);
|
||||||
|
} else {
|
||||||
|
writeConvertedChunk(data, offset, length, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processVariableDownloadRecords(FTConfig config) throws IOException {
|
||||||
|
while (downloadRecordBuffer.size() >= 4) {
|
||||||
|
byte[] buf = downloadRecordBuffer.toByteArray();
|
||||||
|
int rdwLen = ((buf[0] & 0xFF) << 8) | (buf[1] & 0xFF);
|
||||||
|
if (rdwLen < 4) {
|
||||||
|
downloadRecordBuffer.reset();
|
||||||
|
if (buf.length > 1) {
|
||||||
|
downloadRecordBuffer.write(buf, 1, buf.length - 1);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (buf.length < rdwLen) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
int recDataLen = rdwLen - 4;
|
||||||
|
writeConvertedChunk(buf, 4, recDataLen, config);
|
||||||
|
outputStream.write(System.lineSeparator().getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
downloadRecordBuffer.reset();
|
||||||
|
if (buf.length > rdwLen) {
|
||||||
|
downloadRecordBuffer.write(buf, rdwLen, buf.length - rdwLen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processFixedDownloadRecords(int lrecl, FTConfig config) throws IOException {
|
||||||
|
while (downloadRecordBuffer.size() >= lrecl) {
|
||||||
|
byte[] buf = downloadRecordBuffer.toByteArray();
|
||||||
|
int end = lrecl;
|
||||||
|
while (end > 0) {
|
||||||
|
int b = buf[end - 1] & 0xFF;
|
||||||
|
if (b == 0x20 || b == 0x40 || (config != null && config.isRemapFlag() && b == FT2ASC[0x40])) {
|
||||||
|
end--;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeConvertedChunk(buf, 0, end, config);
|
||||||
|
outputStream.write(System.lineSeparator().getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
downloadRecordBuffer.reset();
|
||||||
|
if (buf.length > lrecl) {
|
||||||
|
downloadRecordBuffer.write(buf, lrecl, buf.length - lrecl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flushRemainingDownloadRecords() throws IOException {
|
||||||
|
if (outputStream == null || downloadRecordBuffer.size() == 0) return;
|
||||||
|
FTConfig config = listener != null ? listener.getConfig() : null;
|
||||||
|
byte[] remaining = downloadRecordBuffer.toByteArray();
|
||||||
|
downloadRecordBuffer.reset();
|
||||||
|
if (config != null && config.isAscii()) {
|
||||||
|
int end = remaining.length;
|
||||||
|
while (end > 0) {
|
||||||
|
int b = remaining[end - 1] & 0xFF;
|
||||||
|
if (b == 0x20 || b == 0x40 || (config.isRemapFlag() && b == FT2ASC[0x40])) {
|
||||||
|
end--;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (end > 0) {
|
||||||
|
writeConvertedChunk(remaining, 0, end, config);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outputStream.write(remaining);
|
||||||
|
}
|
||||||
|
outputStream.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeConvertedChunk(byte[] data, int offset, int length, FTConfig config) throws IOException {
|
||||||
|
if (outputStream == null || length <= 0) return;
|
||||||
for (int i = 0; i < length; i++) {
|
for (int i = 0; i < length; i++) {
|
||||||
int b = data[offset + i] & 0xFF;
|
int b = data[offset + i] & 0xFF;
|
||||||
|
|
||||||
if (config.isCrFlag() && (b == '\r' || b == EOF_CTRL_Z || b == EOF_CTRL_D)) {
|
if (b == EOF_CTRL_Z || b == EOF_CTRL_D) {
|
||||||
continue; // Strip CR and EOF (^Z / ^D)
|
continue; // Strip EOF (^Z / ^D)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.isRemapFlag()) {
|
if (config != null && config.isCrFlag() && b == '\r') {
|
||||||
|
continue; // Strip CR
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config == null || !config.isRemapFlag()) {
|
||||||
outputStream.write(b);
|
outputStream.write(b);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -506,30 +684,42 @@ public class FTDft {
|
|||||||
byte[] readBuf = new byte[numbytes];
|
byte[] readBuf = new byte[numbytes];
|
||||||
int totalRead = 0;
|
int totalRead = 0;
|
||||||
|
|
||||||
|
boolean isRecfmF = (config != null && config.getRecfm() == FTConfig.RecordFormat.FIXED) ||
|
||||||
|
"F".equalsIgnoreCase(hostRecfm);
|
||||||
|
boolean isRecfmV = (config != null && config.getRecfm() == FTConfig.RecordFormat.VARIABLE) ||
|
||||||
|
"V".equalsIgnoreCase(hostRecfm);
|
||||||
|
int lrecl = (config != null && config.getLrecl() > 0) ? config.getLrecl() : hostLrecl;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (!dftEof && totalRead < numbytes) {
|
if (config != null && config.isAscii() && isRecfmF && lrecl > 0) {
|
||||||
if (config != null && config.isUnicode() && inputStream instanceof haus.nightmare.lib3270j.xfer.XferFileInputUnicode) {
|
totalRead = readFixedRecordUpload(readBuf, numbytes, lrecl, config);
|
||||||
int n = ((haus.nightmare.lib3270j.xfer.XferFileInputUnicode) inputStream).readData(readBuf, totalRead, numbytes - totalRead);
|
} else if (config != null && config.isAscii() && isRecfmV) {
|
||||||
if (n <= 0) {
|
totalRead = readVariableRecordUpload(readBuf, numbytes, config);
|
||||||
dftEof = true;
|
} else {
|
||||||
break;
|
while (!dftEof && totalRead < numbytes) {
|
||||||
|
if (config != null && config.isUnicode() && inputStream instanceof haus.nightmare.lib3270j.xfer.XferFileInputUnicode) {
|
||||||
|
int n = ((haus.nightmare.lib3270j.xfer.XferFileInputUnicode) inputStream).readData(readBuf, totalRead, numbytes - totalRead);
|
||||||
|
if (n <= 0) {
|
||||||
|
dftEof = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
totalRead += n;
|
||||||
|
} else if (config != null && config.isAscii()) {
|
||||||
|
int b = dftAsciiRead(config);
|
||||||
|
if (b == -1) {
|
||||||
|
dftEof = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
readBuf[totalRead++] = (byte) b;
|
||||||
|
} else {
|
||||||
|
if (inputStream == null) { dftEof = true; break; }
|
||||||
|
int n = inputStream.read(readBuf, totalRead, numbytes - totalRead);
|
||||||
|
if (n <= 0) {
|
||||||
|
dftEof = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
totalRead += n;
|
||||||
}
|
}
|
||||||
totalRead += n;
|
|
||||||
} else if (config.isAscii()) {
|
|
||||||
int b = dftAsciiRead(config);
|
|
||||||
if (b == -1) {
|
|
||||||
dftEof = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
readBuf[totalRead++] = (byte) b;
|
|
||||||
} else {
|
|
||||||
if (inputStream == null) { dftEof = true; break; }
|
|
||||||
int n = inputStream.read(readBuf, totalRead, numbytes - totalRead);
|
|
||||||
if (n <= 0) {
|
|
||||||
dftEof = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
totalRead += n;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -537,17 +727,17 @@ public class FTDft {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(bufferSize);
|
|
||||||
out.write(AID_SF);
|
|
||||||
|
|
||||||
int sfLenPos = out.size();
|
|
||||||
out.write(0); out.write(0);
|
|
||||||
|
|
||||||
out.write(SF_TRANSFER_DATA);
|
|
||||||
|
|
||||||
if (totalRead > 0) {
|
if (totalRead > 0) {
|
||||||
log.fine("DFT: > GetReply rec=" + recnum + " " + totalRead + " bytes");
|
log.fine("DFT: > GetReply rec=" + recnum + " " + totalRead + " bytes");
|
||||||
|
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream(bufferSize);
|
||||||
|
out.write(AID_SF);
|
||||||
|
|
||||||
|
int sfLenPos = out.size();
|
||||||
|
out.write(0); out.write(0);
|
||||||
|
|
||||||
|
out.write(SF_TRANSFER_DATA);
|
||||||
|
|
||||||
out.write((TR_GET_REPLY >> 8) & 0xFF);
|
out.write((TR_GET_REPLY >> 8) & 0xFF);
|
||||||
out.write(TR_GET_REPLY & 0xFF);
|
out.write(TR_GET_REPLY & 0xFF);
|
||||||
|
|
||||||
@@ -570,31 +760,167 @@ public class FTDft {
|
|||||||
out.write(readBuf, 0, totalRead);
|
out.write(readBuf, 0, totalRead);
|
||||||
|
|
||||||
bytesTransferred += totalRead;
|
bytesTransferred += totalRead;
|
||||||
|
|
||||||
|
byte[] result = out.toByteArray();
|
||||||
|
int sfLen = result.length - 1;
|
||||||
|
result[sfLenPos] = (byte) ((sfLen >> 8) & 0xFF);
|
||||||
|
result[sfLenPos + 1] = (byte) (sfLen & 0xFF);
|
||||||
|
|
||||||
|
dftSaveBuf = result.clone();
|
||||||
|
dftSaveBufLen = result.length;
|
||||||
|
|
||||||
|
input.sendStructuredFieldData(result);
|
||||||
|
listener.onBytesTransferred(bytesTransferred);
|
||||||
} else {
|
} else {
|
||||||
log.fine("DFT: > GetReply EOF");
|
log.fine("DFT: > GetReply EOF");
|
||||||
|
|
||||||
out.write((TR_GET_REQ >> 8) & 0xFF);
|
|
||||||
out.write(TR_ERROR_REPLY & 0xFF);
|
|
||||||
|
|
||||||
out.write((TR_ERROR_HDR >> 8) & 0xFF);
|
|
||||||
out.write(TR_ERROR_HDR & 0xFF);
|
|
||||||
|
|
||||||
out.write((TR_ERR_EOF >> 8) & 0xFF);
|
|
||||||
out.write(TR_ERR_EOF & 0xFF);
|
|
||||||
|
|
||||||
dftEof = true;
|
dftEof = true;
|
||||||
|
dftSaveBuf = RB_GET_PAST_EOF.clone();
|
||||||
|
dftSaveBufLen = RB_GET_PAST_EOF.length;
|
||||||
|
input.sendStructuredFieldData(RB_GET_PAST_EOF.clone());
|
||||||
|
listener.onBytesTransferred(bytesTransferred);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] readNextUploadLineConverted(FTConfig config) throws IOException {
|
||||||
|
if (inputStream == null || dftEof) return null;
|
||||||
|
ByteArrayOutputStream lineBytes = new ByteArrayOutputStream();
|
||||||
|
int c;
|
||||||
|
boolean hasData = false;
|
||||||
|
while ((c = inputStream.read()) != -1) {
|
||||||
|
hasData = true;
|
||||||
|
if (c == '\r') {
|
||||||
|
continue; // ignore CR
|
||||||
|
}
|
||||||
|
if (c == '\n') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (c == EOF_CTRL_Z || c == EOF_CTRL_D) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
lineBytes.write(c);
|
||||||
|
}
|
||||||
|
if (!hasData && c == -1) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] result = out.toByteArray();
|
byte[] raw = lineBytes.toByteArray();
|
||||||
int sfLen = result.length - 1;
|
return convertLineForUpload(raw, config);
|
||||||
result[sfLenPos] = (byte) ((sfLen >> 8) & 0xFF);
|
}
|
||||||
result[sfLenPos + 1] = (byte) (sfLen & 0xFF);
|
|
||||||
|
|
||||||
dftSaveBuf = result.clone();
|
private byte[] convertLineForUpload(byte[] raw, FTConfig config) {
|
||||||
dftSaveBufLen = result.length;
|
String lineStr = new String(raw, StandardCharsets.UTF_8);
|
||||||
|
ByteArrayOutputStream converted = new ByteArrayOutputStream(raw.length + 8);
|
||||||
|
boolean isDbcs = (config != null && config.isDbcs()) || (translator != null && translator.getCodePage() != null && translator.getCodePage().isDBCS());
|
||||||
|
boolean noSo = config != null && config.isOptionNoso();
|
||||||
|
boolean inDbcsState = false;
|
||||||
|
|
||||||
input.sendStructuredFieldData(result);
|
for (int i = 0; i < lineStr.length(); i++) {
|
||||||
listener.onBytesTransferred(bytesTransferred);
|
char ch = lineStr.charAt(i);
|
||||||
|
int ebc = translator != null ? translator.unicodeToEbcdic(ch) : -1;
|
||||||
|
boolean charIsDbcs = isDbcs && (ch > 0xFF || ebc > 0xFF);
|
||||||
|
if (charIsDbcs) {
|
||||||
|
if (!inDbcsState && !noSo) {
|
||||||
|
converted.write(0x0E); // Shift-Out
|
||||||
|
inDbcsState = true;
|
||||||
|
}
|
||||||
|
converted.write((ebc >> 8) & 0xFF);
|
||||||
|
converted.write(ebc & 0xFF);
|
||||||
|
} else {
|
||||||
|
if (inDbcsState && !noSo) {
|
||||||
|
converted.write(0x0F); // Shift-In
|
||||||
|
inDbcsState = false;
|
||||||
|
}
|
||||||
|
converted.write(convertAsciiByte(ch, config));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inDbcsState && !noSo) {
|
||||||
|
converted.write(0x0F); // Shift-In at end of record
|
||||||
|
}
|
||||||
|
return converted.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int readFixedRecordUpload(byte[] outBuf, int maxBytes, int lrecl, FTConfig config) throws IOException {
|
||||||
|
int totalRead = 0;
|
||||||
|
int padByte = (config != null && config.isRemapFlag()) ? FT2ASC[0x40] : 0x20;
|
||||||
|
|
||||||
|
while (totalRead + lrecl <= maxBytes && !dftEof) {
|
||||||
|
byte[] rec;
|
||||||
|
if (pendingUploadRecord != null) {
|
||||||
|
rec = pendingUploadRecord;
|
||||||
|
pendingUploadRecord = null;
|
||||||
|
} else {
|
||||||
|
rec = readNextUploadLineConverted(config);
|
||||||
|
if (rec == null) {
|
||||||
|
dftEof = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rec.length <= lrecl) {
|
||||||
|
System.arraycopy(rec, 0, outBuf, totalRead, rec.length);
|
||||||
|
totalRead += rec.length;
|
||||||
|
int padCount = lrecl - rec.length;
|
||||||
|
for (int p = 0; p < padCount; p++) {
|
||||||
|
outBuf[totalRead++] = (byte) padByte;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Longer than lrecl: split
|
||||||
|
System.arraycopy(rec, 0, outBuf, totalRead, lrecl);
|
||||||
|
totalRead += lrecl;
|
||||||
|
int rem = rec.length - lrecl;
|
||||||
|
pendingUploadRecord = new byte[rem];
|
||||||
|
System.arraycopy(rec, lrecl, pendingUploadRecord, 0, rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return totalRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int readVariableRecordUpload(byte[] outBuf, int maxBytes, FTConfig config) throws IOException {
|
||||||
|
int totalRead = 0;
|
||||||
|
while (totalRead + 4 < maxBytes && !dftEof) {
|
||||||
|
byte[] rec;
|
||||||
|
if (pendingUploadRecord != null) {
|
||||||
|
rec = pendingUploadRecord;
|
||||||
|
pendingUploadRecord = null;
|
||||||
|
} else {
|
||||||
|
rec = readNextUploadLineConverted(config);
|
||||||
|
if (rec == null) {
|
||||||
|
dftEof = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int needed = rec.length + 4;
|
||||||
|
if (totalRead + needed <= maxBytes) {
|
||||||
|
outBuf[totalRead++] = (byte) ((needed >> 8) & 0xFF);
|
||||||
|
outBuf[totalRead++] = (byte) (needed & 0xFF);
|
||||||
|
outBuf[totalRead++] = 0;
|
||||||
|
outBuf[totalRead++] = 0;
|
||||||
|
System.arraycopy(rec, 0, outBuf, totalRead, rec.length);
|
||||||
|
totalRead += rec.length;
|
||||||
|
} else {
|
||||||
|
if (totalRead > 0) {
|
||||||
|
// Save for next MTU buffer
|
||||||
|
pendingUploadRecord = rec;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
// Single record exceeds entire MTU: split
|
||||||
|
int chunk = maxBytes - 4;
|
||||||
|
int rdwLen = chunk + 4;
|
||||||
|
outBuf[totalRead++] = (byte) ((rdwLen >> 8) & 0xFF);
|
||||||
|
outBuf[totalRead++] = (byte) (rdwLen & 0xFF);
|
||||||
|
outBuf[totalRead++] = 0;
|
||||||
|
outBuf[totalRead++] = 0;
|
||||||
|
System.arraycopy(rec, 0, outBuf, totalRead, chunk);
|
||||||
|
totalRead += chunk;
|
||||||
|
int rem = rec.length - chunk;
|
||||||
|
pendingUploadRecord = new byte[rem];
|
||||||
|
System.arraycopy(rec, chunk, pendingUploadRecord, 0, rem);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return totalRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -717,14 +1043,13 @@ public class FTDft {
|
|||||||
private void dftCloseRequest() {
|
private void dftCloseRequest() {
|
||||||
log.fine("DFT: Close");
|
log.fine("DFT: Close");
|
||||||
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(6);
|
try {
|
||||||
out.write(AID_SF);
|
flushRemainingDownloadRecords();
|
||||||
out.write(0); out.write(5);
|
} catch (IOException e) {
|
||||||
out.write(SF_TRANSFER_DATA);
|
log.warning("DFT: Error flushing download buffer on close: " + e.getMessage());
|
||||||
out.write((TR_CLOSE_REPLY >> 8) & 0xFF);
|
}
|
||||||
out.write(TR_CLOSE_REPLY & 0xFF);
|
|
||||||
|
|
||||||
input.sendStructuredFieldData(out.toByteArray());
|
input.sendStructuredFieldData(RB_CLOSE_ACK.clone());
|
||||||
|
|
||||||
if (listener.getCurrentState() != FTState.NONE) {
|
if (listener.getCurrentState() != FTState.NONE) {
|
||||||
log.info("DFT: File transfer completed on close request (" + bytesTransferred + " bytes)");
|
log.info("DFT: File transfer completed on close request (" + bytesTransferred + " bytes)");
|
||||||
@@ -735,21 +1060,7 @@ public class FTDft {
|
|||||||
// ========== Data Acknowledgement ==========
|
// ========== Data Acknowledgement ==========
|
||||||
|
|
||||||
private void dftDataAck() {
|
private void dftDataAck() {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(12);
|
input.sendStructuredFieldData(createDataAck(recnum++));
|
||||||
out.write(AID_SF);
|
|
||||||
out.write(0); out.write(11); // SF length = 11 (0x000B)
|
|
||||||
out.write(SF_TRANSFER_DATA);
|
|
||||||
out.write((TR_NORMAL_REPLY >> 8) & 0xFF);
|
|
||||||
out.write(TR_NORMAL_REPLY & 0xFF);
|
|
||||||
out.write((TR_RECNUM_HDR >> 8) & 0xFF);
|
|
||||||
out.write(TR_RECNUM_HDR & 0xFF);
|
|
||||||
out.write((int) ((recnum >> 24) & 0xFF));
|
|
||||||
out.write((int) ((recnum >> 16) & 0xFF));
|
|
||||||
out.write((int) ((recnum >> 8) & 0xFF));
|
|
||||||
out.write((int) (recnum & 0xFF));
|
|
||||||
|
|
||||||
recnum++;
|
|
||||||
input.sendStructuredFieldData(out.toByteArray());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Abort ==========
|
// ========== Abort ==========
|
||||||
@@ -757,18 +1068,22 @@ public class FTDft {
|
|||||||
private void dftAbort(String message, int code) {
|
private void dftAbort(String message, int code) {
|
||||||
log.warning("DFT: ABORT — " + message);
|
log.warning("DFT: ABORT — " + message);
|
||||||
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(10);
|
if (code == TR_OPEN_REQ) {
|
||||||
out.write(AID_SF);
|
input.sendStructuredFieldData(RB_OPEN_FAILED.clone());
|
||||||
out.write(0); out.write(9);
|
} else {
|
||||||
out.write(SF_TRANSFER_DATA);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(10);
|
||||||
out.write((code >> 8) & 0xFF);
|
out.write(AID_SF);
|
||||||
out.write(TR_ERROR_REPLY & 0xFF);
|
out.write(0); out.write(9);
|
||||||
out.write((TR_ERROR_HDR >> 8) & 0xFF);
|
out.write(SF_TRANSFER_DATA);
|
||||||
out.write(TR_ERROR_HDR & 0xFF);
|
out.write((code >> 8) & 0xFF);
|
||||||
out.write((TR_ERR_CMDFAIL >> 8) & 0xFF);
|
out.write(TR_ERROR_REPLY & 0xFF);
|
||||||
out.write(TR_ERR_CMDFAIL & 0xFF);
|
out.write((TR_ERROR_HDR >> 8) & 0xFF);
|
||||||
|
out.write(TR_ERROR_HDR & 0xFF);
|
||||||
|
out.write((TR_ERR_CMDFAIL >> 8) & 0xFF);
|
||||||
|
out.write(TR_ERR_CMDFAIL & 0xFF);
|
||||||
|
|
||||||
input.sendStructuredFieldData(out.toByteArray());
|
input.sendStructuredFieldData(out.toByteArray());
|
||||||
|
}
|
||||||
listener.onTransferAborted(message);
|
listener.onTransferAborted(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,14 @@ import java.util.List;
|
|||||||
* <p>A GOCA fillet curve passes from the initial point P_0 to the terminal point P_{N-1},
|
* <p>A GOCA fillet curve passes from the initial point P_0 to the terminal point P_{N-1},
|
||||||
* bending tangentially toward each intermediate control point P_i.
|
* bending tangentially toward each intermediate control point P_i.
|
||||||
*/
|
*/
|
||||||
public class FilletPts {
|
public class FilletPts extends HODEllipse {
|
||||||
|
|
||||||
public static final int DEFAULT_STEPS_PER_SEGMENT = 24;
|
public static final int DEFAULT_STEPS_PER_SEGMENT = 24;
|
||||||
|
|
||||||
|
public FilletPts() {
|
||||||
|
super(0.0, 0.0, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates interpolated spline vertices across control points using double precision.
|
* Calculates interpolated spline vertices across control points using double precision.
|
||||||
*
|
*
|
||||||
@@ -116,17 +120,115 @@ public class FilletPts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interpolates fillet points into provided output buffers.
|
* Interpolates fillet points into provided output buffers using HoD's exact rational quadratic
|
||||||
|
* polynomial spline algorithm.
|
||||||
* Matches IBM Host On-Demand FilletPts.getFilletPoints.
|
* Matches IBM Host On-Demand FilletPts.getFilletPoints.
|
||||||
*/
|
*/
|
||||||
public int getFilletPoints(int[] xIn, int[] yIn, int n, int[] xOut, int[] yOut) {
|
public int getFilletPoints(int[] nArray, int[] nArray2, int n, int[] nArray3, int[] nArray4) {
|
||||||
if (xIn == null || yIn == null || n <= 0 || xOut == null || yOut == null) {
|
if (nArray == null || nArray2 == null || n <= 0 || nArray3 == null || nArray4 == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
int[][] pts = calculate(xIn, yIn, n, 16);
|
|
||||||
int count = Math.min(pts[0].length, Math.min(xOut.length, yOut.length));
|
int n2;
|
||||||
System.arraycopy(pts[0], 0, xOut, 0, count);
|
int n3 = 0;
|
||||||
System.arraycopy(pts[1], 0, yOut, 0, count);
|
int n4 = 0;
|
||||||
return count;
|
boolean bl = false;
|
||||||
|
int n5 = 0;
|
||||||
|
for (n2 = 1; n2 < n; ++n2) {
|
||||||
|
if (nArray[n2] != nArray[n5] || nArray2[n2] != nArray2[n5]) continue;
|
||||||
|
bl = true;
|
||||||
|
}
|
||||||
|
if (bl) {
|
||||||
|
int[] nArray5 = new int[n];
|
||||||
|
int[] nArray6 = new int[n];
|
||||||
|
nArray5[0] = nArray[0];
|
||||||
|
nArray6[0] = nArray2[0];
|
||||||
|
n5 = 0;
|
||||||
|
for (n2 = 1; n2 < n; ++n2) {
|
||||||
|
if (nArray[n2] == nArray[n5] && nArray2[n2] == nArray2[n5]) continue;
|
||||||
|
nArray5[++n5] = nArray[n2];
|
||||||
|
nArray6[n5] = nArray2[n2];
|
||||||
|
}
|
||||||
|
nArray = nArray5;
|
||||||
|
nArray2 = nArray6;
|
||||||
|
n = n5 + 1;
|
||||||
|
}
|
||||||
|
nArray3[n3] = nArray[0];
|
||||||
|
nArray4[n3] = nArray2[0];
|
||||||
|
int n6 = nArray[0];
|
||||||
|
int n9 = nArray2[0];
|
||||||
|
n3++;
|
||||||
|
|
||||||
|
if (n == 1) {
|
||||||
|
nArray3[n3] = nArray3[0];
|
||||||
|
nArray4[n3++] = nArray4[0];
|
||||||
|
return n3;
|
||||||
|
}
|
||||||
|
if (n == 2) {
|
||||||
|
nArray3[n3] = nArray[1];
|
||||||
|
nArray4[n3++] = nArray2[1];
|
||||||
|
return n3;
|
||||||
|
}
|
||||||
|
int[] nArray7 = new int[3];
|
||||||
|
int[] nArray8 = new int[3];
|
||||||
|
nArray7[0] = nArray[n4];
|
||||||
|
nArray8[0] = nArray2[n4++];
|
||||||
|
--n;
|
||||||
|
nArray7[1] = nArray[n4];
|
||||||
|
nArray8[1] = nArray2[n4++];
|
||||||
|
--n;
|
||||||
|
while (true) {
|
||||||
|
if (n == 0) {
|
||||||
|
nArray3[n3] = nArray7[1];
|
||||||
|
nArray4[n3++] = nArray8[1];
|
||||||
|
return n3;
|
||||||
|
}
|
||||||
|
nArray7[2] = nArray[n4];
|
||||||
|
nArray8[2] = nArray2[n4];
|
||||||
|
if (n > 1) {
|
||||||
|
nArray7[2] = (nArray7[2] + nArray7[1]) / 2;
|
||||||
|
nArray8[2] = (nArray8[2] + nArray8[1]) / 2;
|
||||||
|
}
|
||||||
|
--n;
|
||||||
|
this.dCenterX = nArray7[0] + nArray7[2] - nArray7[1];
|
||||||
|
this.dCenterY = nArray8[0] + nArray8[2] - nArray8[1];
|
||||||
|
this.dP = (double) nArray7[0] - this.dCenterX;
|
||||||
|
this.dQ = (double) nArray8[2] - this.dCenterY;
|
||||||
|
this.dR = (double) nArray7[2] - this.dCenterX;
|
||||||
|
this.dS = (double) nArray8[0] - this.dCenterY;
|
||||||
|
int n10 = (int) this.dP;
|
||||||
|
int n11 = (int) this.dQ;
|
||||||
|
int n12 = (int) this.dR;
|
||||||
|
int n13 = (int) this.dS;
|
||||||
|
int n14 = Math.max(Math.abs(n10) + Math.abs(n13), Math.abs(n12) + Math.abs(n11));
|
||||||
|
this.dRad = n14;
|
||||||
|
if (n14 == 0) {
|
||||||
|
return n3;
|
||||||
|
}
|
||||||
|
int n15 = this.bres(this.dRad);
|
||||||
|
for (n2 = 0; n2 <= n15; ++n2) {
|
||||||
|
this.filletHODSegment2(nArray3, nArray4, n3, this.dXq[n2], this.dYq[n2]);
|
||||||
|
if (n6 == nArray3[n3] && n9 == nArray4[n3]) continue;
|
||||||
|
n6 = nArray3[n3];
|
||||||
|
n9 = nArray4[n3++];
|
||||||
|
}
|
||||||
|
for (n2 = n15; n2 >= 0; --n2) {
|
||||||
|
this.filletHODSegment2(nArray3, nArray4, n3, this.dYq[n2], this.dXq[n2]);
|
||||||
|
if (n6 == nArray3[n3] && n9 == nArray4[n3]) continue;
|
||||||
|
n6 = nArray3[n3];
|
||||||
|
n9 = nArray4[n3++];
|
||||||
|
}
|
||||||
|
if (n6 != nArray7[2] || n9 != nArray8[2]) {
|
||||||
|
nArray3[n3] = nArray7[2];
|
||||||
|
nArray4[n3++] = nArray8[2];
|
||||||
|
}
|
||||||
|
if (n == 0) {
|
||||||
|
return n3;
|
||||||
|
}
|
||||||
|
nArray7[0] = n6 = nArray7[2];
|
||||||
|
nArray8[0] = n9 = nArray8[2];
|
||||||
|
nArray7[1] = nArray[n4];
|
||||||
|
nArray8[1] = nArray2[n4++];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,19 @@ public class GddmCoordinateTransform {
|
|||||||
private double transformX = 1.0;
|
private double transformX = 1.0;
|
||||||
private double transformY = 1.0;
|
private double transformY = 1.0;
|
||||||
|
|
||||||
|
// Aspect Ratio parameters (P, Q, R, S) per IBM Host On-Demand (HODDecoder / GSAP)
|
||||||
|
public static final int DEFAULT_P = 1;
|
||||||
|
public static final int DEFAULT_Q = 1;
|
||||||
|
public static final int DEFAULT_R = 0;
|
||||||
|
public static final int DEFAULT_S = 0;
|
||||||
|
|
||||||
|
private int paramP = DEFAULT_P;
|
||||||
|
private int paramQ = DEFAULT_Q;
|
||||||
|
private int paramR = DEFAULT_R;
|
||||||
|
private int paramS = DEFAULT_S;
|
||||||
|
private double scaleX = 1.0;
|
||||||
|
private double scaleY = 1.0;
|
||||||
|
|
||||||
public GddmCoordinateTransform() {
|
public GddmCoordinateTransform() {
|
||||||
this(80, 24, DEFAULT_CHAR_WIDTH, DEFAULT_CHAR_HEIGHT_16);
|
this(80, 24, DEFAULT_CHAR_WIDTH, DEFAULT_CHAR_HEIGHT_16);
|
||||||
}
|
}
|
||||||
@@ -270,4 +283,68 @@ public class GddmCoordinateTransform {
|
|||||||
public int convertYtoRow(int y) {
|
public int convertYtoRow(int y) {
|
||||||
return (defaultCharHeight > 0) ? (y / defaultCharHeight) + 1 : 1;
|
return (defaultCharHeight > 0) ? (y / defaultCharHeight) + 1 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Aspect Ratio & Coordinate Inversion (Phase 17.2)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets GOCA aspect ratio parameters P, Q, R, S and updates scaling factors.
|
||||||
|
* ScaleX = P/Q, ScaleY = R/S. Defaults: P=1, Q=1, R=0, S=0.
|
||||||
|
*/
|
||||||
|
public void setAspectRatio(int p, int q, int r, int s) {
|
||||||
|
this.paramP = p;
|
||||||
|
this.paramQ = q;
|
||||||
|
this.paramR = r;
|
||||||
|
this.paramS = s;
|
||||||
|
this.scaleX = (q != 0) ? ((double) p / (double) q) : 1.0;
|
||||||
|
this.scaleY = (s != 0) ? ((double) r / (double) s) : 1.0;
|
||||||
|
if (this.scaleX <= 0.0) this.scaleX = 1.0;
|
||||||
|
if (this.scaleY <= 0.0) this.scaleY = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets aspect ratio parameters to defaults: P=1, Q=1, R=0, S=0.
|
||||||
|
*/
|
||||||
|
public void resetAspectRatio() {
|
||||||
|
setAspectRatio(DEFAULT_P, DEFAULT_Q, DEFAULT_R, DEFAULT_S);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getParamP() { return paramP; }
|
||||||
|
public int getParamQ() { return paramQ; }
|
||||||
|
public int getParamR() { return paramR; }
|
||||||
|
public int getParamS() { return paramS; }
|
||||||
|
|
||||||
|
public double getScaleX() { return scaleX; }
|
||||||
|
public double getScaleY() { return scaleY; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inverts GOCA Cartesian Y (positive upward from bottom-left) to raster Y (top-down):
|
||||||
|
* Y_screen = (originY - gocaY) * ScaleY.
|
||||||
|
*/
|
||||||
|
public int mapGocaYToScreen(int originY, int gocaY) {
|
||||||
|
return (int) Math.round((originY - gocaY) * scaleY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inverts GOCA Cartesian Y to raster screen Y using the transform's default origin YMax.
|
||||||
|
*/
|
||||||
|
public int mapGocaYToScreen(int gocaY) {
|
||||||
|
return mapGocaYToScreen(getYMax(), gocaY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inverts raster Y (top-down) back to GOCA Cartesian Y (positive upward from bottom-left):
|
||||||
|
* Y_goca = originY - (screenY / ScaleY).
|
||||||
|
*/
|
||||||
|
public int mapScreenYToGoca(int originY, int screenY) {
|
||||||
|
return originY - (scaleY > 0 ? (int) Math.round(screenY / scaleY) : screenY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inverts raster screen Y back to GOCA Cartesian Y using the transform's default origin YMax.
|
||||||
|
*/
|
||||||
|
public int mapScreenYToGoca(int screenY) {
|
||||||
|
return mapScreenYToGoca(getYMax(), screenY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,10 +40,15 @@ public class GocaDecoder {
|
|||||||
private double charHeight = 16.0;
|
private double charHeight = 16.0;
|
||||||
private int charSet = 0;
|
private int charSet = 0;
|
||||||
private int charPrecision = GocaConstants.CP_STRING;
|
private int charPrecision = GocaConstants.CP_STRING;
|
||||||
private int arcParamP = 1;
|
public static final int DEFAULT_ARC_P = 1;
|
||||||
private int arcParamQ = 0;
|
public static final int DEFAULT_ARC_Q = 1;
|
||||||
private int arcParamR = 0;
|
public static final int DEFAULT_ARC_R = 0;
|
||||||
private int arcParamS = 1;
|
public static final int DEFAULT_ARC_S = 0;
|
||||||
|
|
||||||
|
private int arcParamP = DEFAULT_ARC_P;
|
||||||
|
private int arcParamQ = DEFAULT_ARC_Q;
|
||||||
|
private int arcParamR = DEFAULT_ARC_R;
|
||||||
|
private int arcParamS = DEFAULT_ARC_S;
|
||||||
|
|
||||||
private boolean segChained = false;
|
private boolean segChained = false;
|
||||||
private boolean segDynamic = false;
|
private boolean segDynamic = false;
|
||||||
@@ -77,6 +82,7 @@ public class GocaDecoder {
|
|||||||
private int imgHeight = 0;
|
private int imgHeight = 0;
|
||||||
private int imgBitDepth = GocaConstants.BPP_1;
|
private int imgBitDepth = GocaConstants.BPP_1;
|
||||||
private int imgCompression = GocaConstants.IMG_UNCOMPRESSED;
|
private int imgCompression = GocaConstants.IMG_UNCOMPRESSED;
|
||||||
|
private int imgMultiplier = 1;
|
||||||
private final List<Byte> imgBuffer = new ArrayList<>();
|
private final List<Byte> imgBuffer = new ArrayList<>();
|
||||||
|
|
||||||
// Segment Store for retained graphics / segment calling (G_GCALL 0x2A)
|
// Segment Store for retained graphics / segment calling (G_GCALL 0x2A)
|
||||||
@@ -368,7 +374,15 @@ public class GocaDecoder {
|
|||||||
inImage = false;
|
inImage = false;
|
||||||
imgBitDepth = GocaConstants.BPP_1;
|
imgBitDepth = GocaConstants.BPP_1;
|
||||||
imgCompression = GocaConstants.IMG_UNCOMPRESSED;
|
imgCompression = GocaConstants.IMG_UNCOMPRESSED;
|
||||||
|
imgMultiplier = 1;
|
||||||
imgBuffer.clear();
|
imgBuffer.clear();
|
||||||
|
arcParamP = DEFAULT_ARC_P;
|
||||||
|
arcParamQ = DEFAULT_ARC_Q;
|
||||||
|
arcParamR = DEFAULT_ARC_R;
|
||||||
|
arcParamS = DEFAULT_ARC_S;
|
||||||
|
if (plane != null && plane.getTransform() != null) {
|
||||||
|
plane.getTransform().setAspectRatio(arcParamP, arcParamQ, arcParamR, arcParamS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] partialOrderBuffer = new byte[0];
|
private byte[] partialOrderBuffer = new byte[0];
|
||||||
@@ -567,7 +581,10 @@ public class GocaDecoder {
|
|||||||
int w = 0, h = 0;
|
int w = 0, h = 0;
|
||||||
int bitDepth = GocaConstants.BPP_1;
|
int bitDepth = GocaConstants.BPP_1;
|
||||||
int compression = GocaConstants.IMG_UNCOMPRESSED;
|
int compression = GocaConstants.IMG_UNCOMPRESSED;
|
||||||
|
int imult = 1;
|
||||||
if (payloadLen >= 6 && idx + 2 + payloadLen <= end) {
|
if (payloadLen >= 6 && idx + 2 + payloadLen <= end) {
|
||||||
|
imult = readCoord(inputData, idx + 2);
|
||||||
|
if (imult <= 0) imult = 1;
|
||||||
w = readCoord(inputData, idx + 4);
|
w = readCoord(inputData, idx + 4);
|
||||||
h = readCoord(inputData, idx + 6);
|
h = readCoord(inputData, idx + 6);
|
||||||
if (payloadLen >= 7) {
|
if (payloadLen >= 7) {
|
||||||
@@ -592,7 +609,7 @@ public class GocaDecoder {
|
|||||||
compression = inputData[idx + 7] & 0xFF;
|
compression = inputData[idx + 7] & 0xFF;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
beginImage(curX, curY, w, h, bitDepth, compression);
|
beginImage(curX, curY, w, h, bitDepth, compression, imult);
|
||||||
idx += orderLen;
|
idx += orderLen;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -672,13 +689,25 @@ public class GocaDecoder {
|
|||||||
idx += orderLen;
|
idx += orderLen;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GocaConstants.G_GSAP: { // Arc Parameters (0x22)
|
case GocaConstants.G_GSAP: { // Arc Parameters / Set Aspect Ratio (0x22)
|
||||||
if (idx + 9 < end) {
|
if (idx + 9 < end) {
|
||||||
arcParamP = readCoord(inputData, idx + 2);
|
arcParamP = readCoord(inputData, idx + 2);
|
||||||
arcParamQ = readCoord(inputData, idx + 4);
|
arcParamQ = readCoord(inputData, idx + 4);
|
||||||
arcParamR = readCoord(inputData, idx + 6);
|
arcParamR = readCoord(inputData, idx + 6);
|
||||||
arcParamS = readCoord(inputData, idx + 8);
|
arcParamS = readCoord(inputData, idx + 8);
|
||||||
|
} else {
|
||||||
|
arcParamP = DEFAULT_ARC_P;
|
||||||
|
arcParamQ = DEFAULT_ARC_Q;
|
||||||
|
arcParamR = DEFAULT_ARC_R;
|
||||||
|
arcParamS = DEFAULT_ARC_S;
|
||||||
}
|
}
|
||||||
|
if (plane != null && plane.getTransform() != null) {
|
||||||
|
plane.getTransform().setAspectRatio(arcParamP, arcParamQ, arcParamR, arcParamS);
|
||||||
|
}
|
||||||
|
logger.info(String.format("GOCA GSAP: P=%d Q=%d R=%d S=%d (ScaleX=%.2f ScaleY=%.2f)",
|
||||||
|
arcParamP, arcParamQ, arcParamR, arcParamS,
|
||||||
|
(arcParamQ != 0 ? (double) arcParamP / arcParamQ : 1.0),
|
||||||
|
(arcParamS != 0 ? (double) arcParamR / arcParamS : 1.0)));
|
||||||
idx += orderLen;
|
idx += orderLen;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1012,7 +1041,10 @@ public class GocaDecoder {
|
|||||||
int w, h;
|
int w, h;
|
||||||
int bitDepth = GocaConstants.BPP_1;
|
int bitDepth = GocaConstants.BPP_1;
|
||||||
int compression = GocaConstants.IMG_UNCOMPRESSED;
|
int compression = GocaConstants.IMG_UNCOMPRESSED;
|
||||||
|
int imult = 1;
|
||||||
if (payloadLen >= 10) {
|
if (payloadLen >= 10) {
|
||||||
|
imult = readCoord(inputData, idx + 6);
|
||||||
|
if (imult <= 0) imult = 1;
|
||||||
w = readCoord(inputData, idx + 8);
|
w = readCoord(inputData, idx + 8);
|
||||||
h = readCoord(inputData, idx + 10);
|
h = readCoord(inputData, idx + 10);
|
||||||
if (payloadLen >= 11) {
|
if (payloadLen >= 11) {
|
||||||
@@ -1037,7 +1069,7 @@ public class GocaDecoder {
|
|||||||
compression = inputData[idx + 11] & 0xFF;
|
compression = inputData[idx + 11] & 0xFF;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
beginImage(x, y, w, h, bitDepth, compression);
|
beginImage(x, y, w, h, bitDepth, compression, imult);
|
||||||
}
|
}
|
||||||
idx += orderLen;
|
idx += orderLen;
|
||||||
break;
|
break;
|
||||||
@@ -1309,10 +1341,14 @@ public class GocaDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void beginImage(int x, int y, int w, int h) {
|
private void beginImage(int x, int y, int w, int h) {
|
||||||
beginImage(x, y, w, h, GocaConstants.BPP_1, GocaConstants.IMG_UNCOMPRESSED);
|
beginImage(x, y, w, h, GocaConstants.BPP_1, GocaConstants.IMG_UNCOMPRESSED, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void beginImage(int x, int y, int w, int h, int bitDepth, int compression) {
|
private void beginImage(int x, int y, int w, int h, int bitDepth, int compression) {
|
||||||
|
beginImage(x, y, w, h, bitDepth, compression, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void beginImage(int x, int y, int w, int h, int bitDepth, int compression, int multiplier) {
|
||||||
this.inImage = true;
|
this.inImage = true;
|
||||||
this.imgX = x;
|
this.imgX = x;
|
||||||
this.imgY = y;
|
this.imgY = y;
|
||||||
@@ -1320,6 +1356,7 @@ public class GocaDecoder {
|
|||||||
this.imgHeight = h;
|
this.imgHeight = h;
|
||||||
this.imgBitDepth = bitDepth;
|
this.imgBitDepth = bitDepth;
|
||||||
this.imgCompression = compression;
|
this.imgCompression = compression;
|
||||||
|
this.imgMultiplier = (multiplier > 0) ? multiplier : 1;
|
||||||
this.imgBuffer.clear();
|
this.imgBuffer.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1335,7 +1372,7 @@ public class GocaDecoder {
|
|||||||
|
|
||||||
int px = plane.mapX(imgX);
|
int px = plane.mapX(imgX);
|
||||||
int py = plane.mapY(imgY);
|
int py = plane.mapY(imgY);
|
||||||
plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor, imgBitDepth, imgCompression);
|
plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor, imgBitDepth, imgCompression, imgMultiplier);
|
||||||
inImage = false;
|
inImage = false;
|
||||||
imgBuffer.clear();
|
imgBuffer.clear();
|
||||||
}
|
}
|
||||||
@@ -1429,38 +1466,191 @@ public class GocaDecoder {
|
|||||||
|
|
||||||
private void processArc(byte[] data, int off, int len, boolean fromCurPos, boolean isFull) {
|
private void processArc(byte[] data, int off, int len, boolean fromCurPos, boolean isFull) {
|
||||||
int pos = off;
|
int pos = off;
|
||||||
|
int end = off + len;
|
||||||
|
|
||||||
|
if (isFull) {
|
||||||
|
// GFARC (0xC7) or GCFARC (0x87) Full Arc / Ellipse
|
||||||
|
int centerX = curX;
|
||||||
|
int centerY = curY;
|
||||||
|
if (!fromCurPos && pos + 4 <= end) {
|
||||||
|
centerX = readCoord(data, pos);
|
||||||
|
centerY = readCoord(data, pos + 2);
|
||||||
|
pos += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
double multiplier = 1.0;
|
||||||
|
if (pos + 2 <= end) {
|
||||||
|
int intPart = data[pos];
|
||||||
|
int fracPart = data[pos + 1] & 0xFF;
|
||||||
|
multiplier = intPart + fracPart / 256.0;
|
||||||
|
if (multiplier <= 0.0) multiplier = 1.0;
|
||||||
|
pos += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
double dP = arcParamP * multiplier;
|
||||||
|
double dQ = arcParamQ * multiplier;
|
||||||
|
double dR = arcParamR * multiplier;
|
||||||
|
double dS = arcParamS * multiplier;
|
||||||
|
|
||||||
|
int[] arcX = new int[256];
|
||||||
|
int[] arcY = new int[256];
|
||||||
|
int nPts = HODEllipse.getHODEllipse(dP, dQ, dR, dS, arcX, arcY, centerX, centerY);
|
||||||
|
|
||||||
|
for (int i = 0; i < nPts; i++) {
|
||||||
|
trackPoint(arcX[i], arcY[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inArea) {
|
||||||
|
for (int i = 0; i < nPts; i++) {
|
||||||
|
if (i == 0 && currentPolyPts == 0) {
|
||||||
|
addAreaLineStart(arcX[i], arcY[i]);
|
||||||
|
} else {
|
||||||
|
addAreaPoint(arcX[i], arcY[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < nPts - 1; i++) {
|
||||||
|
plane.drawLine(plane.mapXDouble(arcX[i]), plane.mapYDouble(arcY[i]),
|
||||||
|
plane.mapXDouble(arcX[i + 1]), plane.mapYDouble(arcY[i + 1]),
|
||||||
|
curColor, lineType, lineWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
curX = centerX;
|
||||||
|
curY = centerY;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Partial Arc: GARC (0xC6) or GCARC (0x86)
|
||||||
|
// 3-point circular arc definition per GOCA specs: start, intermediate, end
|
||||||
|
int x0 = curX;
|
||||||
|
int y0 = curY;
|
||||||
|
boolean has3Point = false;
|
||||||
|
int x1 = 0, y1 = 0, x2 = 0, y2 = 0;
|
||||||
|
|
||||||
|
if (!fromCurPos && len >= 12 && pos + 12 <= end) {
|
||||||
|
x0 = readCoord(data, pos);
|
||||||
|
y0 = readCoord(data, pos + 2);
|
||||||
|
x1 = readCoord(data, pos + 4);
|
||||||
|
y1 = readCoord(data, pos + 6);
|
||||||
|
x2 = readCoord(data, pos + 8);
|
||||||
|
y2 = readCoord(data, pos + 10);
|
||||||
|
has3Point = true;
|
||||||
|
} else if (fromCurPos && len >= 8 && pos + 8 <= end) {
|
||||||
|
x1 = readCoord(data, pos);
|
||||||
|
y1 = readCoord(data, pos + 2);
|
||||||
|
x2 = readCoord(data, pos + 4);
|
||||||
|
y2 = readCoord(data, pos + 6);
|
||||||
|
has3Point = true;
|
||||||
|
} else if (!fromCurPos && len >= 8 && pos + 8 <= end) {
|
||||||
|
x1 = readCoord(data, pos);
|
||||||
|
y1 = readCoord(data, pos + 2);
|
||||||
|
x2 = readCoord(data, pos + 4);
|
||||||
|
y2 = readCoord(data, pos + 6);
|
||||||
|
has3Point = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has3Point) {
|
||||||
|
double d = 2.0 * (x0 * (y1 - y2) + x1 * (y2 - y0) + x2 * (y0 - y1));
|
||||||
|
if (Math.abs(d) < 1e-6) {
|
||||||
|
// Collinear points: draw straight line segments P0 -> P1 -> P2
|
||||||
|
trackPoint(x0, y0);
|
||||||
|
trackPoint(x1, y1);
|
||||||
|
trackPoint(x2, y2);
|
||||||
|
if (inArea) {
|
||||||
|
if (currentPolyPts == 0) addAreaLineStart(x0, y0);
|
||||||
|
addAreaPoint(x1, y1);
|
||||||
|
addAreaPoint(x2, y2);
|
||||||
|
} else {
|
||||||
|
plane.drawLine(plane.mapXDouble(x0), plane.mapYDouble(y0),
|
||||||
|
plane.mapXDouble(x1), plane.mapYDouble(y1),
|
||||||
|
curColor, lineType, lineWidth);
|
||||||
|
plane.drawLine(plane.mapXDouble(x1), plane.mapYDouble(y1),
|
||||||
|
plane.mapXDouble(x2), plane.mapYDouble(y2),
|
||||||
|
curColor, lineType, lineWidth);
|
||||||
|
}
|
||||||
|
curX = x2;
|
||||||
|
curY = y2;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double x0sq = (double) x0 * x0 + (double) y0 * y0;
|
||||||
|
double x1sq = (double) x1 * x1 + (double) y1 * y1;
|
||||||
|
double x2sq = (double) x2 * x2 + (double) y2 * y2;
|
||||||
|
double ccx = (x0sq * (y1 - y2) + x1sq * (y2 - y0) + x2sq * (y0 - y1)) / d;
|
||||||
|
double ccy = (x0sq * (x2 - x1) + x1sq * (x0 - x2) + x2sq * (x1 - x0)) / d;
|
||||||
|
double rad = Math.hypot(x0 - ccx, y0 - ccy);
|
||||||
|
|
||||||
|
double theta0 = Math.atan2(y0 - ccy, x0 - ccx);
|
||||||
|
double theta1 = Math.atan2(y1 - ccy, x1 - ccx);
|
||||||
|
double theta2 = Math.atan2(y2 - ccy, x2 - ccx);
|
||||||
|
if (theta0 < 0) theta0 += 2 * Math.PI;
|
||||||
|
if (theta1 < 0) theta1 += 2 * Math.PI;
|
||||||
|
if (theta2 < 0) theta2 += 2 * Math.PI;
|
||||||
|
|
||||||
|
double d01 = theta1 - theta0;
|
||||||
|
while (d01 < 0) d01 += 2 * Math.PI;
|
||||||
|
double d02 = theta2 - theta0;
|
||||||
|
while (d02 < 0) d02 += 2 * Math.PI;
|
||||||
|
|
||||||
|
double sweep = (d01 < d02) ? d02 : (d02 - 2 * Math.PI);
|
||||||
|
|
||||||
|
int steps = Math.max(8, (int) Math.round(Math.abs(Math.toDegrees(sweep)) / 5.0));
|
||||||
|
int prevX = x0;
|
||||||
|
int prevY = y0;
|
||||||
|
trackPoint(x0, y0);
|
||||||
|
if (inArea && currentPolyPts == 0) {
|
||||||
|
addAreaLineStart(x0, y0);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int s = 1; s <= steps; s++) {
|
||||||
|
double t = (double) s / steps;
|
||||||
|
double angle = theta0 + t * sweep;
|
||||||
|
int px = (s == steps) ? x2 : (int) Math.round(ccx + rad * Math.cos(angle));
|
||||||
|
int py = (s == steps) ? y2 : (int) Math.round(ccy + rad * Math.sin(angle));
|
||||||
|
trackPoint(px, py);
|
||||||
|
if (inArea) {
|
||||||
|
addAreaPoint(px, py);
|
||||||
|
} else {
|
||||||
|
plane.drawLine(plane.mapXDouble(prevX), plane.mapYDouble(prevY),
|
||||||
|
plane.mapXDouble(px), plane.mapYDouble(py),
|
||||||
|
curColor, lineType, lineWidth);
|
||||||
|
}
|
||||||
|
prevX = px;
|
||||||
|
prevY = py;
|
||||||
|
}
|
||||||
|
|
||||||
|
curX = x2;
|
||||||
|
curY = y2;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: sweep angle parameter decoding
|
||||||
int centerX = curX;
|
int centerX = curX;
|
||||||
int centerY = curY;
|
int centerY = curY;
|
||||||
|
if (!fromCurPos && pos + 4 <= end) {
|
||||||
// For absolute variants, read the center point from data
|
|
||||||
if (!fromCurPos && pos + 4 <= off + len) {
|
|
||||||
centerX = readCoord(data, pos);
|
centerX = readCoord(data, pos);
|
||||||
centerY = readCoord(data, pos + 2);
|
centerY = readCoord(data, pos + 2);
|
||||||
pos += 4;
|
pos += 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the multiplier (integer.fraction format)
|
|
||||||
double multiplier = 1.0;
|
double multiplier = 1.0;
|
||||||
if (pos + 2 <= off + len) {
|
if (pos + 2 <= end) {
|
||||||
int intPart = data[pos]; // signed byte
|
int intPart = data[pos];
|
||||||
int fracPart = data[pos + 1] & 0xFF;
|
int fracPart = data[pos + 1] & 0xFF;
|
||||||
multiplier = intPart + fracPart / 256.0;
|
multiplier = intPart + fracPart / 256.0;
|
||||||
if (multiplier == 0.0) multiplier = 1.0;
|
if (multiplier == 0.0) multiplier = 1.0;
|
||||||
pos += 2;
|
pos += 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute semi-axes from arc parameters scaled by multiplier
|
|
||||||
// P,Q and R,S are conjugate semi-diameter vectors
|
|
||||||
double pScaled = arcParamP * multiplier;
|
double pScaled = arcParamP * multiplier;
|
||||||
double qScaled = arcParamQ * multiplier;
|
double qScaled = arcParamQ * multiplier;
|
||||||
double rScaled = arcParamR * multiplier;
|
double rScaled = arcParamR * multiplier;
|
||||||
double sScaled = arcParamS * multiplier;
|
double sScaled = arcParamS * multiplier;
|
||||||
|
|
||||||
// Semi-axis lengths in GOCA coordinate space
|
|
||||||
double semiAxis1 = Math.sqrt(pScaled * pScaled + qScaled * qScaled);
|
double semiAxis1 = Math.sqrt(pScaled * pScaled + qScaled * qScaled);
|
||||||
double semiAxis2 = Math.sqrt(rScaled * rScaled + sScaled * sScaled);
|
double semiAxis2 = Math.sqrt(rScaled * rScaled + sScaled * sScaled);
|
||||||
|
|
||||||
// If arc parameters are at default (1,0,0,1) and curPos != center, use distance to center as radius
|
|
||||||
double dist = Math.hypot(curX - centerX, curY - centerY);
|
double dist = Math.hypot(curX - centerX, curY - centerY);
|
||||||
if (semiAxis1 <= 1.01 && dist > 1.0) {
|
if (semiAxis1 <= 1.01 && dist > 1.0) {
|
||||||
semiAxis1 = dist * multiplier;
|
semiAxis1 = dist * multiplier;
|
||||||
@@ -1470,7 +1660,6 @@ public class GocaDecoder {
|
|||||||
if (semiAxis1 < 1.0) semiAxis1 = 1.0;
|
if (semiAxis1 < 1.0) semiAxis1 = 1.0;
|
||||||
if (semiAxis2 < 1.0) semiAxis2 = 1.0;
|
if (semiAxis2 < 1.0) semiAxis2 = 1.0;
|
||||||
|
|
||||||
// Map to pixel space with double precision
|
|
||||||
double cx = plane.mapXDouble(centerX);
|
double cx = plane.mapXDouble(centerX);
|
||||||
double cy = plane.mapYDouble(centerY);
|
double cy = plane.mapYDouble(centerY);
|
||||||
double rx = Math.abs(plane.mapXDouble(semiAxis1) - plane.mapXDouble(0));
|
double rx = Math.abs(plane.mapXDouble(semiAxis1) - plane.mapXDouble(0));
|
||||||
@@ -1478,38 +1667,24 @@ public class GocaDecoder {
|
|||||||
if (rx <= 0.0) rx = Math.max(1.0, semiAxis1);
|
if (rx <= 0.0) rx = Math.max(1.0, semiAxis1);
|
||||||
if (ry <= 0.0) ry = Math.max(1.0, semiAxis2);
|
if (ry <= 0.0) ry = Math.max(1.0, semiAxis2);
|
||||||
|
|
||||||
// For partial arcs, determine start angle and sweep angle
|
|
||||||
double startAngleDeg = 0.0;
|
double startAngleDeg = 0.0;
|
||||||
double sweepAngleDeg = 360.0;
|
double sweepAngleDeg = 360.0;
|
||||||
if (!isFull) {
|
if (pos + 4 <= end) {
|
||||||
if (pos + 4 <= off + len) {
|
int startInt = data[pos];
|
||||||
// Sweep start: integer.fraction of full revolution
|
int startFrac = data[pos + 1] & 0xFF;
|
||||||
int startInt = data[pos];
|
startAngleDeg = (startInt + startFrac / 256.0) * 360.0;
|
||||||
int startFrac = data[pos + 1] & 0xFF;
|
pos += 2;
|
||||||
startAngleDeg = (startInt + startFrac / 256.0) * 360.0;
|
int sweepInt = data[pos];
|
||||||
pos += 2;
|
int sweepFrac = data[pos + 1] & 0xFF;
|
||||||
// Sweep angle: integer.fraction of full revolution
|
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
|
||||||
int sweepInt = data[pos];
|
} else if (pos + 2 <= end) {
|
||||||
int sweepFrac = data[pos + 1] & 0xFF;
|
int sweepInt = data[pos];
|
||||||
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
|
int sweepFrac = data[pos + 1] & 0xFF;
|
||||||
} else if (pos + 2 <= off + len) {
|
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
|
||||||
// Single sweep parameter
|
if (curX != centerX || curY != centerY) {
|
||||||
int sweepInt = data[pos];
|
double startRad = Math.atan2(plane.mapYDouble(centerY) - plane.mapYDouble(curY), plane.mapXDouble(curX) - plane.mapXDouble(centerX));
|
||||||
int sweepFrac = data[pos + 1] & 0xFF;
|
startAngleDeg = Math.toDegrees(startRad);
|
||||||
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
|
if (startAngleDeg < 0) startAngleDeg += 360.0;
|
||||||
if (curX != centerX || curY != centerY) {
|
|
||||||
double startRad = Math.atan2(plane.mapYDouble(centerY) - plane.mapYDouble(curY), plane.mapXDouble(curX) - plane.mapXDouble(centerX));
|
|
||||||
startAngleDeg = Math.toDegrees(startRad);
|
|
||||||
if (startAngleDeg < 0) startAngleDeg += 360.0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No sweep data — if we have a current point, start there and sweep full circle
|
|
||||||
if (curX != centerX || curY != centerY) {
|
|
||||||
double startRad = Math.atan2(plane.mapYDouble(centerY) - plane.mapYDouble(curY), plane.mapXDouble(curX) - plane.mapXDouble(centerX));
|
|
||||||
startAngleDeg = Math.toDegrees(startRad);
|
|
||||||
if (startAngleDeg < 0) startAngleDeg += 360.0;
|
|
||||||
}
|
|
||||||
sweepAngleDeg = 360.0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1533,7 +1708,7 @@ public class GocaDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
plane.drawArc(cx, cy, rx, ry, startAngleDeg, sweepAngleDeg,
|
plane.drawArc(cx, cy, rx, ry, startAngleDeg, sweepAngleDeg,
|
||||||
curColor, lineType, lineWidth, isFull);
|
curColor, lineType, lineWidth, false);
|
||||||
|
|
||||||
curX = centerX;
|
curX = centerX;
|
||||||
curY = centerY;
|
curY = centerY;
|
||||||
@@ -1543,63 +1718,62 @@ public class GocaDecoder {
|
|||||||
int pos = off;
|
int pos = off;
|
||||||
int end = off + len;
|
int end = off + len;
|
||||||
|
|
||||||
List<Double> ptsX = new ArrayList<>();
|
|
||||||
List<Double> ptsY = new ArrayList<>();
|
|
||||||
List<Integer> gocaPtsX = new ArrayList<>();
|
List<Integer> gocaPtsX = new ArrayList<>();
|
||||||
List<Integer> gocaPtsY = new ArrayList<>();
|
List<Integer> gocaPtsY = new ArrayList<>();
|
||||||
|
|
||||||
if (fromCurPos) {
|
if (fromCurPos) {
|
||||||
trackPoint(curX, curY);
|
|
||||||
ptsX.add(plane.mapXDouble(curX));
|
|
||||||
ptsY.add(plane.mapYDouble(curY));
|
|
||||||
gocaPtsX.add(curX);
|
gocaPtsX.add(curX);
|
||||||
gocaPtsY.add(curY);
|
gocaPtsY.add(curY);
|
||||||
if (inArea && currentPolyPts == 0) {
|
|
||||||
addAreaLineStart(curX, curY);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
while (pos + 4 <= end) {
|
while (pos + 4 <= end) {
|
||||||
int x = readCoord(data, pos);
|
int x = readCoord(data, pos);
|
||||||
int y = readCoord(data, pos + 2);
|
int y = readCoord(data, pos + 2);
|
||||||
trackPoint(x, y);
|
|
||||||
ptsX.add(plane.mapXDouble(x));
|
|
||||||
ptsY.add(plane.mapYDouble(y));
|
|
||||||
gocaPtsX.add(x);
|
gocaPtsX.add(x);
|
||||||
gocaPtsY.add(y);
|
gocaPtsY.add(y);
|
||||||
if (inArea && ptsX.size() == 1 && currentPolyPts == 0) {
|
|
||||||
addAreaLineStart(x, y);
|
|
||||||
}
|
|
||||||
curX = x;
|
|
||||||
curY = y;
|
|
||||||
pos += 4;
|
pos += 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ptsX.size() >= 2) {
|
int n = gocaPtsX.size();
|
||||||
int n = ptsX.size();
|
if (n < 2) return;
|
||||||
double[] px = new double[n];
|
|
||||||
double[] py = new double[n];
|
|
||||||
for (int i = 0; i < n; i++) {
|
|
||||||
px[i] = ptsX.get(i);
|
|
||||||
py[i] = ptsY.get(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
int[] ctrlX = new int[n];
|
||||||
|
int[] ctrlY = new int[n];
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
ctrlX[i] = gocaPtsX.get(i);
|
||||||
|
ctrlY[i] = gocaPtsY.get(i);
|
||||||
|
trackPoint(ctrlX[i], ctrlY[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
FilletPts fillet = new FilletPts();
|
||||||
|
int req = fillet.getPointsRequired(n);
|
||||||
|
int[] outX = new int[Math.max(req, n * 32)];
|
||||||
|
int[] outY = new int[Math.max(req, n * 32)];
|
||||||
|
int nPoints = fillet.getFilletPoints(ctrlX, ctrlY, n, outX, outY);
|
||||||
|
|
||||||
|
if (nPoints > 0) {
|
||||||
|
for (int i = 0; i < nPoints; i++) {
|
||||||
|
trackPoint(outX[i], outY[i]);
|
||||||
|
}
|
||||||
if (inArea) {
|
if (inArea) {
|
||||||
int gn = gocaPtsX.size();
|
for (int i = 0; i < nPoints; i++) {
|
||||||
int[] gx = new int[gn];
|
if (i == 0 && currentPolyPts == 0) {
|
||||||
int[] gy = new int[gn];
|
addAreaLineStart(outX[0], outY[0]);
|
||||||
for (int i = 0; i < gn; i++) {
|
} else {
|
||||||
gx[i] = gocaPtsX.get(i);
|
addAreaPoint(outX[i], outY[i]);
|
||||||
gy[i] = gocaPtsY.get(i);
|
}
|
||||||
}
|
|
||||||
int[][] curve = FilletPts.calculate(gx, gy, gn, 16);
|
|
||||||
for (int i = 1; i < curve[0].length; i++) {
|
|
||||||
addAreaPoint(curve[0][i], curve[1][i]);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
plane.drawFillet(px, py, n, curColor, lineType, lineWidth);
|
for (int i = 0; i < nPoints - 1; i++) {
|
||||||
|
plane.drawLine(plane.mapXDouble(outX[i]), plane.mapYDouble(outY[i]),
|
||||||
|
plane.mapXDouble(outX[i + 1]), plane.mapYDouble(outY[i + 1]),
|
||||||
|
curColor, lineType, lineWidth);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
curX = ctrlX[n - 1];
|
||||||
|
curY = ctrlY[n - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processMarker(byte[] data, int off, int len, boolean fromCurPos) {
|
private void processMarker(byte[] data, int off, int len, boolean fromCurPos) {
|
||||||
@@ -1824,6 +1998,15 @@ public class GocaDecoder {
|
|||||||
resetDefaults();
|
resetDefaults();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized int getArcParamP() { return arcParamP; }
|
||||||
|
public synchronized int getArcParamQ() { return arcParamQ; }
|
||||||
|
public synchronized int getArcParamR() { return arcParamR; }
|
||||||
|
public synchronized int getArcParamS() { return arcParamS; }
|
||||||
|
public synchronized int getImageMultiplier() { return imgMultiplier; }
|
||||||
|
public synchronized boolean hasActiveGraphics() {
|
||||||
|
return graphicsCursorActive || (plane != null && plane.hasContent());
|
||||||
|
}
|
||||||
|
|
||||||
private int readCoord(byte[] data, int off) {
|
private int readCoord(byte[] data, int off) {
|
||||||
return (short) (((data[off] & 0xFF) << 8) | (data[off + 1] & 0xFF));
|
return (short) (((data[off] & 0xFF) << 8) | (data[off + 1] & 0xFF));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1221,12 +1221,12 @@ public class GraphicsPlane implements PixelBuffer {
|
|||||||
for (int i = 0; i < pLen - 1; i++) {
|
for (int i = 0; i < pLen - 1; i++) {
|
||||||
drawLine((double) px[offset + i], (double) py[offset + i],
|
drawLine((double) px[offset + i], (double) py[offset + i],
|
||||||
(double) px[offset + i + 1], (double) py[offset + i + 1],
|
(double) px[offset + i + 1], (double) py[offset + i + 1],
|
||||||
boundaryColorArgb, lineType, GocaConstants.LW_NORMAL);
|
boundaryColorArgb, lineType, lineWidth);
|
||||||
}
|
}
|
||||||
if (pLen >= 3 && (px[offset] != px[offset + pLen - 1] || py[offset] != py[offset + pLen - 1])) {
|
if (pLen >= 3 && (px[offset] != px[offset + pLen - 1] || py[offset] != py[offset + pLen - 1])) {
|
||||||
drawLine((double) px[offset + pLen - 1], (double) py[offset + pLen - 1],
|
drawLine((double) px[offset + pLen - 1], (double) py[offset + pLen - 1],
|
||||||
(double) px[offset], (double) py[offset],
|
(double) px[offset], (double) py[offset],
|
||||||
boundaryColorArgb, lineType, GocaConstants.LW_NORMAL);
|
boundaryColorArgb, lineType, lineWidth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
offset += pLen;
|
offset += pLen;
|
||||||
@@ -1417,6 +1417,16 @@ public class GraphicsPlane implements PixelBuffer {
|
|||||||
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, shearAngle);
|
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, shearAngle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized void drawVectorString(double x, double y, String text, int colorArgb,
|
||||||
|
double cellWidth, double cellHeight, int dir, double angle, double shearAngle) {
|
||||||
|
drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle, shearAngle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void drawVectorString(int x, int y, String text, int colorArgb,
|
||||||
|
int cellWidth, int cellHeight, int dir, double angle) {
|
||||||
|
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
private void drawVssChar(double x, double y, char c, int color, double cw, double ch) {
|
private void drawVssChar(double x, double y, char c, int color, double cw, double ch) {
|
||||||
drawVssChar(x, y, c, color, cw, ch, 0.0, 0.0);
|
drawVssChar(x, y, c, color, cw, ch, 0.0, 0.0);
|
||||||
}
|
}
|
||||||
@@ -1530,8 +1540,17 @@ public class GraphicsPlane implements PixelBuffer {
|
|||||||
*/
|
*/
|
||||||
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb,
|
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb,
|
||||||
int bitDepth, int compressionMode) {
|
int bitDepth, int compressionMode) {
|
||||||
|
drawImage(x, y, width, height, imageData, fgColorArgb, bitDepth, compressionMode, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws bitmap image with support for bit depth, RLE decompression, and image multiplier (imult).
|
||||||
|
*/
|
||||||
|
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb,
|
||||||
|
int bitDepth, int compressionMode, int multiplier) {
|
||||||
if (imageData == null || width <= 0 || height <= 0) return;
|
if (imageData == null || width <= 0 || height <= 0) return;
|
||||||
int fgColor = (fgColorArgb != 0) ? fgColorArgb : GocaConstants.GOCA_COLORS[0];
|
int fgColor = (fgColorArgb != 0) ? fgColorArgb : GocaConstants.GOCA_COLORS[0];
|
||||||
|
int m = (multiplier > 0) ? multiplier : 1;
|
||||||
|
|
||||||
byte[] rawData = imageData;
|
byte[] rawData = imageData;
|
||||||
if (compressionMode == GocaConstants.IMG_RLE) {
|
if (compressionMode == GocaConstants.IMG_RLE) {
|
||||||
@@ -1549,7 +1568,15 @@ public class GraphicsPlane implements PixelBuffer {
|
|||||||
if (byteIdx < rawData.length) {
|
if (byteIdx < rawData.length) {
|
||||||
boolean bit = ((rawData[byteIdx] >> (7 - (col % 8))) & 1) != 0;
|
boolean bit = ((rawData[byteIdx] >> (7 - (col % 8))) & 1) != 0;
|
||||||
if (bit) {
|
if (bit) {
|
||||||
setPixel(x + col, y + row, fgColor);
|
if (m <= 1) {
|
||||||
|
setPixel(x + col, y + row, fgColor);
|
||||||
|
} else {
|
||||||
|
for (int dy = 0; dy < m; dy++) {
|
||||||
|
for (int dx = 0; dx < m; dx++) {
|
||||||
|
setPixel(x + col * m + dx, y + row * m + dy, fgColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1572,7 +1599,15 @@ public class GraphicsPlane implements PixelBuffer {
|
|||||||
case 3: pixelColor = GocaConstants.GOCA_COLORS[4]; break; // Green
|
case 3: pixelColor = GocaConstants.GOCA_COLORS[4]; break; // Green
|
||||||
default: pixelColor = fgColor; break;
|
default: pixelColor = fgColor; break;
|
||||||
}
|
}
|
||||||
setPixel(x + col, y + row, pixelColor);
|
if (m <= 1) {
|
||||||
|
setPixel(x + col, y + row, pixelColor);
|
||||||
|
} else {
|
||||||
|
for (int dy = 0; dy < m; dy++) {
|
||||||
|
for (int dx = 0; dx < m; dx++) {
|
||||||
|
setPixel(x + col * m + dx, y + row * m + dy, pixelColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1588,7 +1623,16 @@ public class GraphicsPlane implements PixelBuffer {
|
|||||||
int shift = (1 - (col % 2)) * 4;
|
int shift = (1 - (col % 2)) * 4;
|
||||||
int val = (rawData[byteIdx] >> shift) & 0x0F;
|
int val = (rawData[byteIdx] >> shift) & 0x0F;
|
||||||
if (val != 0) {
|
if (val != 0) {
|
||||||
setPixel(x + col, y + row, GocaConstants.getGocaColorArgb(val));
|
int pixelColor = GocaConstants.getGocaColorArgb(val);
|
||||||
|
if (m <= 1) {
|
||||||
|
setPixel(x + col, y + row, pixelColor);
|
||||||
|
} else {
|
||||||
|
for (int dy = 0; dy < m; dy++) {
|
||||||
|
for (int dx = 0; dx < m; dx++) {
|
||||||
|
setPixel(x + col * m + dx, y + row * m + dy, pixelColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1602,7 +1646,16 @@ public class GraphicsPlane implements PixelBuffer {
|
|||||||
if (byteIdx < rawData.length) {
|
if (byteIdx < rawData.length) {
|
||||||
int val = rawData[byteIdx] & 0xFF;
|
int val = rawData[byteIdx] & 0xFF;
|
||||||
if (val != 0) {
|
if (val != 0) {
|
||||||
setPixel(x + col, y + row, GocaConstants.getGocaColorArgb(val));
|
int pixelColor = GocaConstants.getGocaColorArgb(val);
|
||||||
|
if (m <= 1) {
|
||||||
|
setPixel(x + col, y + row, pixelColor);
|
||||||
|
} else {
|
||||||
|
for (int dy = 0; dy < m; dy++) {
|
||||||
|
for (int dx = 0; dx < m; dx++) {
|
||||||
|
setPixel(x + col * m + dx, y + row * m + dy, pixelColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
package haus.nightmare.lib3270j.graphics;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates intermediate points for IBM GOCA ellipses and arcs using conjugate semi-axes P, Q, R, S.
|
||||||
|
* Matches IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODEllipse and HODGraphUtil).
|
||||||
|
*/
|
||||||
|
public class HODEllipse {
|
||||||
|
|
||||||
|
protected static final double[] dXgxdq = new double[] {
|
||||||
|
1.0, 0.999847695, 0.999390827, 0.998629534, 0.99756405, 0.996194698,
|
||||||
|
0.994521895, 0.992546151, 0.990268068, 0.98768834, 0.984807753, 0.981627183,
|
||||||
|
0.9781476, 0.974370064, 0.970295726, 0.965925826, 0.961261695, 0.956304756,
|
||||||
|
0.951056516, 0.945518575, 0.93969262, 0.933580426, 0.927183854, 0.920504853,
|
||||||
|
0.913545457, 0.906307787, 0.898794046, 0.891006524, 0.882947592, 0.874619707,
|
||||||
|
0.866025403, 0.8571673, 0.848048096, 0.838670567, 0.829037572, 0.819152044,
|
||||||
|
0.809016994, 0.79863551, 0.788010753, 0.777145961, 0.766044443, 0.75470958,
|
||||||
|
0.743144825, 0.731353701, 0.7193398, 0.707106781
|
||||||
|
};
|
||||||
|
|
||||||
|
protected static final double[] dXgydq = new double[] {
|
||||||
|
0.0, 0.017452406, 0.034899496, 0.052335956, 0.069756473, 0.087155742,
|
||||||
|
0.104528463, 0.121869343, 0.139173101, 0.156434465, 0.173648177, 0.190808995,
|
||||||
|
0.20791169, 0.224951054, 0.241921895, 0.258819045, 0.275637355, 0.292371704,
|
||||||
|
0.309016994, 0.325568154, 0.342020143, 0.358367949, 0.374606593, 0.390731128,
|
||||||
|
0.406736643, 0.422618261, 0.438371146, 0.453990499, 0.469471562, 0.48480962,
|
||||||
|
0.5, 0.515038074, 0.529919264, 0.544639035, 0.559192903, 0.573576436,
|
||||||
|
0.587785252, 0.601815023, 0.615661475, 0.629320391, 0.642787609, 0.656059029,
|
||||||
|
0.669130606, 0.68199836, 0.69465837, 0.707106781
|
||||||
|
};
|
||||||
|
|
||||||
|
protected double dP;
|
||||||
|
protected double dQ;
|
||||||
|
protected double dR;
|
||||||
|
protected double dS;
|
||||||
|
protected int iP;
|
||||||
|
protected int iQ;
|
||||||
|
protected int iR;
|
||||||
|
protected int iS;
|
||||||
|
protected double dRad;
|
||||||
|
protected double dCenterX;
|
||||||
|
protected double dCenterY;
|
||||||
|
protected double[] dXq = new double[9];
|
||||||
|
protected double[] dYq = new double[9];
|
||||||
|
|
||||||
|
public HODEllipse(double p, double q, double r, double s) {
|
||||||
|
this.dP = p;
|
||||||
|
this.dQ = q;
|
||||||
|
this.dR = r;
|
||||||
|
this.dS = s;
|
||||||
|
this.iP = (int) Math.round(p);
|
||||||
|
this.iQ = (int) Math.round(q);
|
||||||
|
this.iR = (int) Math.round(r);
|
||||||
|
this.iS = (int) Math.round(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHODPoints(int[] outX, int[] outY, int centerX, int centerY) {
|
||||||
|
if (outX == null || outY == null || outX.length < 128 || outY.length < 128) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int ptIdx = 0;
|
||||||
|
int n5 = Math.max(Math.abs(this.iP) + Math.abs(this.iS), Math.abs(this.iR) + Math.abs(this.iQ));
|
||||||
|
this.dRad = n5;
|
||||||
|
this.dCenterX = centerX;
|
||||||
|
this.dCenterY = centerY;
|
||||||
|
if (n5 == 0) {
|
||||||
|
outX[0] = centerX;
|
||||||
|
outY[0] = centerY;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int n6 = this.bres(this.dRad);
|
||||||
|
int lastX = this.iP + centerX;
|
||||||
|
int lastY = this.iS + centerY;
|
||||||
|
|
||||||
|
for (int i = 0; i <= n6; ++i) {
|
||||||
|
this.filletHODSegment(outX, outY, ptIdx, this.dXq[i], this.dYq[i]);
|
||||||
|
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
|
||||||
|
lastX = outX[ptIdx];
|
||||||
|
lastY = outY[ptIdx++];
|
||||||
|
}
|
||||||
|
for (int i = n6; i >= 0; --i) {
|
||||||
|
this.filletHODSegment(outX, outY, ptIdx, this.dYq[i], this.dXq[i]);
|
||||||
|
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
|
||||||
|
lastX = outX[ptIdx];
|
||||||
|
lastY = outY[ptIdx++];
|
||||||
|
}
|
||||||
|
for (int i = 0; i <= n6; ++i) {
|
||||||
|
this.filletHODSegment(outX, outY, ptIdx, -this.dYq[i], this.dXq[i]);
|
||||||
|
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
|
||||||
|
lastX = outX[ptIdx];
|
||||||
|
lastY = outY[ptIdx++];
|
||||||
|
}
|
||||||
|
for (int i = n6; i >= 0; --i) {
|
||||||
|
this.filletHODSegment(outX, outY, ptIdx, -this.dXq[i], this.dYq[i]);
|
||||||
|
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
|
||||||
|
lastX = outX[ptIdx];
|
||||||
|
lastY = outY[ptIdx++];
|
||||||
|
}
|
||||||
|
for (int i = 0; i <= n6; ++i) {
|
||||||
|
this.filletHODSegment(outX, outY, ptIdx, -this.dXq[i], -this.dYq[i]);
|
||||||
|
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
|
||||||
|
lastX = outX[ptIdx];
|
||||||
|
lastY = outY[ptIdx++];
|
||||||
|
}
|
||||||
|
for (int i = n6; i >= 0; --i) {
|
||||||
|
this.filletHODSegment(outX, outY, ptIdx, -this.dYq[i], -this.dXq[i]);
|
||||||
|
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
|
||||||
|
lastX = outX[ptIdx];
|
||||||
|
lastY = outY[ptIdx++];
|
||||||
|
}
|
||||||
|
for (int i = 0; i <= n6; ++i) {
|
||||||
|
this.filletHODSegment(outX, outY, ptIdx, this.dYq[i], -this.dXq[i]);
|
||||||
|
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
|
||||||
|
lastX = outX[ptIdx];
|
||||||
|
lastY = outY[ptIdx++];
|
||||||
|
}
|
||||||
|
for (int i = n6; i >= 0; --i) {
|
||||||
|
this.filletHODSegment(outX, outY, ptIdx, this.dXq[i], -this.dYq[i]);
|
||||||
|
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
|
||||||
|
lastX = outX[ptIdx];
|
||||||
|
lastY = outY[ptIdx++];
|
||||||
|
}
|
||||||
|
|
||||||
|
outX[ptIdx] = outX[0];
|
||||||
|
outY[ptIdx++] = outY[0];
|
||||||
|
return ptIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int bres(double rad) {
|
||||||
|
for (int i = 0; i < 9; ++i) {
|
||||||
|
this.dXq[i] = rad * dXgxdq[i * 5 + 2];
|
||||||
|
this.dYq[i] = rad * dXgydq[i * 5 + 2];
|
||||||
|
}
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void filletHODSegment(int[] outX, int[] outY, int idx, double d, double d2) {
|
||||||
|
outX[idx] = (int) Math.round(this.dP * d / this.dRad + this.dR * d2 / this.dRad + this.dCenterX);
|
||||||
|
outY[idx] = (int) Math.round(-(this.dS * d / this.dRad + this.dQ * d2 / this.dRad) + this.dCenterY);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void filletHODSegment2(int[] outX, int[] outY, int idx, double d, double d2) {
|
||||||
|
outX[idx] = (int) Math.round(this.dP * d / this.dRad + this.dR * d2 / this.dRad + this.dCenterX);
|
||||||
|
outY[idx] = (int) Math.round(this.dS * d / this.dRad + this.dQ * d2 / this.dRad + this.dCenterY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getHODEllipse(double p, double q, double r, double s, int[] outX, int[] outY, int cx, int cy) {
|
||||||
|
HODEllipse ellipse = new HODEllipse(p, q, r, s);
|
||||||
|
return ellipse.getHODPoints(outX, outY, cx, cy);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,11 +18,12 @@ public class ProgramSymbolManager {
|
|||||||
public static final int NUMBER_GRAPHICS_SYMBOL_SETS = 4; // RWS 8..11 (Sets 6..9)
|
public static final int NUMBER_GRAPHICS_SYMBOL_SETS = 4; // RWS 8..11 (Sets 6..9)
|
||||||
|
|
||||||
private int defaultCellWidth = 9;
|
private int defaultCellWidth = 9;
|
||||||
private int defaultCellHeight = 12; // Standard IBM 3279 PS Slot Default Height (SDH = 0x0C = 12)
|
private int defaultCellHeight = 16; // Standard IBM 9x16 Programmed Symbol cell matrix
|
||||||
|
|
||||||
|
|
||||||
public void setDefaultCellDimensions(int width, int height) {
|
public void setDefaultCellDimensions(int width, int height) {
|
||||||
this.defaultCellWidth = (width > 0) ? width : 9;
|
this.defaultCellWidth = (width > 0) ? width : 9;
|
||||||
this.defaultCellHeight = (height > 0) ? height : 12;
|
this.defaultCellHeight = (height > 0) ? height : 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getDefaultCellWidth() {
|
public int getDefaultCellWidth() {
|
||||||
@@ -194,20 +195,24 @@ public class ProgramSymbolManager {
|
|||||||
boolean isTriplePlane = (rws >= 4 && rws <= 7);
|
boolean isTriplePlane = (rws >= 4 && rws <= 7);
|
||||||
ProgramSymbolSet set = sets[setIndex];
|
ProgramSymbolSet set = sets[setIndex];
|
||||||
|
|
||||||
|
if (clearAll) {
|
||||||
|
set.clear();
|
||||||
|
}
|
||||||
|
|
||||||
int extHeaderLen = 0;
|
int extHeaderLen = 0;
|
||||||
int cellWidth = defaultCellWidth;
|
int cellWidth = (loadFormat == 1 || rws < 8) ? 9 : defaultCellWidth;
|
||||||
int cellHeight = defaultCellHeight;
|
int cellHeight = (loadFormat == 1 || rws < 8) ? 16 : defaultCellHeight;
|
||||||
int colorPlane = 0; // 0 = all planes, 1 = Red, 2 = Green, 4 = Blue
|
int colorPlane = 0; // 0 = all planes, 1 = Blue, 2 = Red, 4 = Green
|
||||||
|
|
||||||
if (hasExtHeader && data.length > 4) {
|
if (hasExtHeader && data.length > 4) {
|
||||||
extHeaderLen = data[4] & 0xFF;
|
extHeaderLen = data[4] & 0xFF;
|
||||||
if (extHeaderLen > 3 && data.length > 6) {
|
if (extHeaderLen > 3 && data.length > 6) {
|
||||||
int lw = data[6] & 0xFF;
|
int lw = data[6] & 0xFF;
|
||||||
if (lw > 0) cellWidth = lw;
|
if (lw > 0 && loadFormat != 1) cellWidth = lw;
|
||||||
}
|
}
|
||||||
if (extHeaderLen > 4 && data.length > 7) {
|
if (extHeaderLen > 4 && data.length > 7) {
|
||||||
int lh = data[7] & 0xFF;
|
int lh = data[7] & 0xFF;
|
||||||
if (lh > 0) cellHeight = lh;
|
if (lh > 0 && loadFormat != 1) cellHeight = lh;
|
||||||
}
|
}
|
||||||
if (extHeaderLen >= 6 && data.length > 9) {
|
if (extHeaderLen >= 6 && data.length > 9) {
|
||||||
colorPlane = data[9] & 0xFF;
|
colorPlane = data[9] & 0xFF;
|
||||||
@@ -223,55 +228,34 @@ public class ProgramSymbolManager {
|
|||||||
int remaining = data.length - offset;
|
int remaining = data.length - offset;
|
||||||
int codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint;
|
int codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint;
|
||||||
|
|
||||||
int sliceBytes = (loadFormat == 1) ? 18 : (cellWidth * cellHeight + 7) / 8;
|
int bytesPerSymbol = (loadFormat == 1) ? 18 : (cellWidth * cellHeight + 7) / 8;
|
||||||
if (sliceBytes <= 0) sliceBytes = 18;
|
if (bytesPerSymbol <= 0) bytesPerSymbol = 18;
|
||||||
|
|
||||||
int bytesPerSymbol;
|
|
||||||
if (isTriplePlane && colorPlane == 0 && remaining >= sliceBytes * 3) {
|
|
||||||
bytesPerSymbol = sliceBytes * 3;
|
|
||||||
} else {
|
|
||||||
bytesPerSymbol = sliceBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) {
|
while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) {
|
||||||
byte[] pixelData = new byte[cellWidth * cellHeight];
|
byte[] pixelData = new byte[cellWidth * cellHeight];
|
||||||
ProgramSymbolSet.SymbolSlot existing = set.getSlot(codeIndex);
|
ProgramSymbolSet.SymbolSlot existing = set.getSlot(codeIndex);
|
||||||
if (!clearAll && existing != null && existing.getWidth() == cellWidth && existing.getHeight() == cellHeight) {
|
|
||||||
System.arraycopy(existing.getPixelData(), 0, pixelData, 0, Math.min(existing.getPixelData().length, pixelData.length));
|
// If loading a specific color plane (colorPlane != 0), merge with existing ONLY IF existing was not all-planes
|
||||||
|
if (isTriplePlane && colorPlane != 0 && existing != null && !existing.isAllPlanesLoaded()
|
||||||
|
&& existing.getWidth() == cellWidth && existing.getHeight() == cellHeight) {
|
||||||
|
System.arraycopy(existing.getPixelData(), 0, pixelData, 0,
|
||||||
|
Math.min(existing.getPixelData().length, pixelData.length));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTriplePlane && colorPlane == 0 && bytesPerSymbol == sliceBytes * 3) {
|
if (loadFormat == 1) {
|
||||||
// 3 consecutive slices: Red (plane 1), Green (plane 2), Blue (plane 4)
|
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
||||||
if (loadFormat == 1) {
|
|
||||||
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, true, 1);
|
|
||||||
unpackFormat1(data, offset + sliceBytes, pixelData, cellWidth, cellHeight, true, 2);
|
|
||||||
unpackFormat1(data, offset + sliceBytes * 2, pixelData, cellWidth, cellHeight, true, 4);
|
|
||||||
} else {
|
|
||||||
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, true, 1);
|
|
||||||
unpackFormat3(data, offset + sliceBytes, pixelData, cellWidth, cellHeight, true, 2);
|
|
||||||
unpackFormat3(data, offset + sliceBytes * 2, pixelData, cellWidth, cellHeight, true, 4);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (loadFormat == 1) {
|
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
||||||
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
|
||||||
} else {
|
|
||||||
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane));
|
boolean allPlanes = (colorPlane == 0);
|
||||||
|
set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane, allPlanes));
|
||||||
|
|
||||||
codeIndex++;
|
codeIndex++;
|
||||||
offset += bytesPerSymbol;
|
offset += bytesPerSymbol;
|
||||||
remaining -= bytesPerSymbol;
|
remaining -= bytesPerSymbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clearAll) {
|
|
||||||
for (int i = codeIndex; i < ProgramSymbolSet.NUM_SLOTS; i++) {
|
|
||||||
set.clearSlot(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logger.isLoggable(Level.FINE)) {
|
if (logger.isLoggable(Level.FINE)) {
|
||||||
logger.fine(String.format("LOADPS: Loaded PS Set LCID=0x%02X (RWS=%d, %s, %dx%d, %d glyphs)",
|
logger.fine(String.format("LOADPS: Loaded PS Set LCID=0x%02X (RWS=%d, %s, %dx%d, %d glyphs)",
|
||||||
lcid, rws, isTriplePlane ? "Triple-Plane" : "Single-Plane",
|
lcid, rws, isTriplePlane ? "Triple-Plane" : "Single-Plane",
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ public class ProgramSymbolSet {
|
|||||||
private final int height;
|
private final int height;
|
||||||
private final byte[] pixelData; // 1 byte per pixel: 0 = background, 1..7 = color index (or 1 for monochrome)
|
private final byte[] pixelData; // 1 byte per pixel: 0 = background, 1..7 = color index (or 1 for monochrome)
|
||||||
private final boolean isTriplePlane;
|
private final boolean isTriplePlane;
|
||||||
|
private final boolean allPlanesLoaded;
|
||||||
private int[] cachedRgbArray;
|
private int[] cachedRgbArray;
|
||||||
private int cachedFgRgb = -1;
|
private int cachedFgRgb = -1;
|
||||||
private int cachedBgRgb = -1;
|
private int cachedBgRgb = -1;
|
||||||
@@ -73,10 +74,19 @@ public class ProgramSymbolSet {
|
|||||||
private int cachedScaledBgRgb = -1;
|
private int cachedScaledBgRgb = -1;
|
||||||
|
|
||||||
public SymbolSlot(int width, int height, byte[] pixelData, boolean isTriplePlane) {
|
public SymbolSlot(int width, int height, byte[] pixelData, boolean isTriplePlane) {
|
||||||
|
this(width, height, pixelData, isTriplePlane, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SymbolSlot(int width, int height, byte[] pixelData, boolean isTriplePlane, boolean allPlanesLoaded) {
|
||||||
this.width = width > 0 ? width : 9;
|
this.width = width > 0 ? width : 9;
|
||||||
this.height = height > 0 ? height : 16;
|
this.height = height > 0 ? height : 16;
|
||||||
this.pixelData = pixelData;
|
this.pixelData = pixelData;
|
||||||
this.isTriplePlane = isTriplePlane;
|
this.isTriplePlane = isTriplePlane;
|
||||||
|
this.allPlanesLoaded = allPlanesLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAllPlanesLoaded() {
|
||||||
|
return allPlanesLoaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getWidth() {
|
public int getWidth() {
|
||||||
@@ -183,18 +193,18 @@ public class ProgramSymbolSet {
|
|||||||
} else if (!isTriplePlane) {
|
} else if (!isTriplePlane) {
|
||||||
rgbArray[idx] = fgArgb;
|
rgbArray[idx] = fgArgb;
|
||||||
} else {
|
} else {
|
||||||
// Triple-Plane RGB composite aligned with IBM Host On-Demand 16-color GOCA palette:
|
// Triple-Plane RGB composite aligned with IBM 3179G / HoD GOCA palette:
|
||||||
// val is bitmask: bit 0 (0x01) = Red, bit 1 (0x02) = Green, bit 2 (0x04) = Blue
|
// val bitmask: bit 0 (0x01) = Red, bit 1 (0x02) = Green, bit 2 (0x04) = Blue
|
||||||
int colorMask = val & 0x07;
|
int colorMask = val & 0x07;
|
||||||
int colorArgb;
|
int colorArgb;
|
||||||
switch (colorMask) {
|
switch (colorMask) {
|
||||||
case 1: colorArgb = GocaConstants.GOCA_COLORS[2]; break; // Red
|
case 1: colorArgb = GocaConstants.GOCA_COLORS[2]; break; // Red
|
||||||
case 2: colorArgb = GocaConstants.GOCA_COLORS[4]; break; // Green
|
case 2: colorArgb = GocaConstants.GOCA_COLORS[4]; break; // Green
|
||||||
case 3: colorArgb = GocaConstants.GOCA_COLORS[6]; break; // Yellow
|
case 3: colorArgb = GocaConstants.GOCA_COLORS[6]; break; // Yellow (Red + Green)
|
||||||
case 4: colorArgb = GocaConstants.GOCA_COLORS[1]; break; // Blue (CUSTOMBLUE)
|
case 4: colorArgb = GocaConstants.GOCA_COLORS[1]; break; // Blue
|
||||||
case 5: colorArgb = GocaConstants.GOCA_COLORS[3]; break; // Pink
|
case 5: colorArgb = GocaConstants.GOCA_COLORS[3]; break; // Pink (Red + Blue)
|
||||||
case 6: colorArgb = GocaConstants.GOCA_COLORS[5]; break; // Turquoise
|
case 6: colorArgb = GocaConstants.GOCA_COLORS[5]; break; // Turquoise (Green + Blue)
|
||||||
case 7: colorArgb = GocaConstants.GOCA_COLORS[7]; break; // White
|
case 7: colorArgb = GocaConstants.GOCA_COLORS[7]; break; // White (Red + Green + Blue)
|
||||||
default: colorArgb = bgArgb; break;
|
default: colorArgb = bgArgb; break;
|
||||||
}
|
}
|
||||||
rgbArray[idx] = colorArgb;
|
rgbArray[idx] = colorArgb;
|
||||||
|
|||||||
@@ -846,6 +846,8 @@ public class InputProcessor {
|
|||||||
if (graphicsPlane != null) {
|
if (graphicsPlane != null) {
|
||||||
graphicsPlane.clear();
|
graphicsPlane.clear();
|
||||||
}
|
}
|
||||||
|
lastAid = aidCode;
|
||||||
|
setKeyboardLocked(true);
|
||||||
byte[] data = new byte[] { (byte) aidCode };
|
byte[] data = new byte[] { (byte) aidCode };
|
||||||
sendAidResponse(data);
|
sendAidResponse(data);
|
||||||
return;
|
return;
|
||||||
@@ -948,6 +950,15 @@ public class InputProcessor {
|
|||||||
* Transmit a mouse / light-pen graphic input AID matching IBM Host On-Demand PS3179G and DS3270.
|
* Transmit a mouse / light-pen graphic input AID matching IBM Host On-Demand PS3179G and DS3270.
|
||||||
*/
|
*/
|
||||||
public void sendGraphicMouseAid(int aidCode, int button, boolean isShift, boolean isCtrl) {
|
public void sendGraphicMouseAid(int aidCode, int button, boolean isShift, boolean isCtrl) {
|
||||||
|
boolean hasGraphics = gocaDecoder != null && (gocaDecoder.isGraphicsCursorActive() ||
|
||||||
|
(gocaDecoder.getGraphicsPlane() != null && gocaDecoder.getGraphicsPlane().hasContent()));
|
||||||
|
if (hasGraphics) {
|
||||||
|
int gx = gocaDecoder.getGraphicCursorX();
|
||||||
|
int gy = gocaDecoder.getGraphicCursorY();
|
||||||
|
sendGraphicMouseAid(gx, gy, aidCode, button, isShift, isCtrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isFullSession()) {
|
if (fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isFullSession()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -957,41 +968,51 @@ public class InputProcessor {
|
|||||||
setKeyboardLocked(true);
|
setKeyboardLocked(true);
|
||||||
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive()) {
|
byte[] rma = buildReadModifiedInboundData(aidCode, false);
|
||||||
int gx = gocaDecoder.getGraphicCursorX();
|
try {
|
||||||
int gy = gocaDecoder.getGraphicCursorY();
|
out.write(rma);
|
||||||
int cols = (screen != null && screen.getCols() > 0) ? screen.getCols() : 80;
|
} catch (java.io.IOException ignored) {}
|
||||||
int numRows = (screen != null && screen.getRows() > 0) ? screen.getRows() : 24;
|
sendAidResponse(out.toByteArray());
|
||||||
int cursorAddr = screen != null ? screen.getCursorAddress() : 0;
|
}
|
||||||
int row = cursorAddr / cols;
|
|
||||||
int col = cursorAddr % cols;
|
|
||||||
|
|
||||||
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
|
/**
|
||||||
gx, gy, row, col, button, true, isShift, isCtrl
|
* Transmits a mouse / light-pen graphic input structured field AID with explicit GOCA coordinates.
|
||||||
);
|
*/
|
||||||
|
public void sendGraphicMouseAid(int gx, int gy, int aidCode, int button, boolean isShift, boolean isCtrl) {
|
||||||
StringBuilder sfHex = new StringBuilder();
|
if (fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isFullSession()) {
|
||||||
for (byte b : sf) {
|
return;
|
||||||
sfHex.append(String.format("%02X ", b & 0xFF));
|
|
||||||
}
|
|
||||||
log.info(String.format(
|
|
||||||
"sendGraphicMouseAid: goca=(%d, %d) row=%d col=%d cursorAddr=%d btn=%d shift=%b ctrl=%b SF_HEX=[%s]",
|
|
||||||
gx, gy, row, col, cursorAddr, button, isShift, isCtrl, sfHex.toString().trim()
|
|
||||||
));
|
|
||||||
|
|
||||||
// Structured Field AID (0x88) + 56-byte Graphic Input SF
|
|
||||||
out.write(AID_SF);
|
|
||||||
try {
|
|
||||||
out.write(sf);
|
|
||||||
byte[] rma = buildReadModifiedInboundData(aidCode, false);
|
|
||||||
out.write(rma);
|
|
||||||
} catch (java.io.IOException ignored) {}
|
|
||||||
} else {
|
|
||||||
byte[] rma = buildReadModifiedInboundData(aidCode, false);
|
|
||||||
try {
|
|
||||||
out.write(rma);
|
|
||||||
} catch (java.io.IOException ignored) {}
|
|
||||||
}
|
}
|
||||||
|
if (isKeyboardLocked()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setKeyboardLocked(true);
|
||||||
|
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
int cols = (screen != null && screen.getCols() > 0) ? screen.getCols() : 80;
|
||||||
|
int cursorAddr = screen != null ? screen.getCursorAddress() : 0;
|
||||||
|
int row = cursorAddr / cols;
|
||||||
|
int col = cursorAddr % cols;
|
||||||
|
|
||||||
|
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
|
||||||
|
gx, gy, row, col, button, true, isShift, isCtrl
|
||||||
|
);
|
||||||
|
|
||||||
|
StringBuilder sfHex = new StringBuilder();
|
||||||
|
for (byte b : sf) {
|
||||||
|
sfHex.append(String.format("%02X ", b & 0xFF));
|
||||||
|
}
|
||||||
|
log.info(String.format(
|
||||||
|
"sendGraphicMouseAid: goca=(%d, %d) row=%d col=%d cursorAddr=%d btn=%d shift=%b ctrl=%b SF_HEX=[%s]",
|
||||||
|
gx, gy, row, col, cursorAddr, button, isShift, isCtrl, sfHex.toString().trim()
|
||||||
|
));
|
||||||
|
|
||||||
|
// Structured Field AID (0x88) + 56-byte Graphic Input SF
|
||||||
|
out.write(AID_SF);
|
||||||
|
try {
|
||||||
|
out.write(sf);
|
||||||
|
byte[] rma = buildReadModifiedInboundData(aidCode, false);
|
||||||
|
out.write(rma);
|
||||||
|
} catch (java.io.IOException ignored) {}
|
||||||
|
|
||||||
sendAidResponse(out.toByteArray());
|
sendAidResponse(out.toByteArray());
|
||||||
}
|
}
|
||||||
@@ -1030,7 +1051,12 @@ public class InputProcessor {
|
|||||||
screen.setCursorAddress(address);
|
screen.setCursorAddress(address);
|
||||||
|
|
||||||
// Immediate Selection (Space or Null) -> AID_SELECT (0x7E)
|
// Immediate Selection (Space or Null) -> AID_SELECT (0x7E)
|
||||||
|
// Per IBM 3270 Reference (GA23-0059), light-pen selection has no effect
|
||||||
|
// on protected fields with a space or null designator character.
|
||||||
if (ebcdic == 0x00 || ebcdic == 0x40 || ascii == ' ' || ascii == 0 || ascii == '\u3000') {
|
if (ebcdic == 0x00 || ebcdic == 0x40 || ascii == ' ' || ascii == 0 || ascii == '\u3000') {
|
||||||
|
if (haus.nightmare.lib3270j.protocol.DS3270Constants.faIsProtected(fa)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||||
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_SELECT);
|
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_SELECT);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1448,6 +1448,11 @@ public class TelnetFSM {
|
|||||||
* Send a 3270 data record, with TN3270E header if in TN3270E mode.
|
* Send a 3270 data record, with TN3270E header if in TN3270E mode.
|
||||||
*/
|
*/
|
||||||
public void send3270Data(byte[] data) {
|
public void send3270Data(byte[] data) {
|
||||||
|
if (data != null && data.length >= 2 && (data[data.length - 2] & 0xFF) == IAC && (data[data.length - 1] & 0xFF) == EOR) {
|
||||||
|
byte[] trimmed = new byte[data.length - 2];
|
||||||
|
System.arraycopy(data, 0, trimmed, 0, trimmed.length);
|
||||||
|
data = trimmed;
|
||||||
|
}
|
||||||
if (tn3270eNegotiated) {
|
if (tn3270eNegotiated) {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(data.length + EH_SIZE);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(data.length + EH_SIZE);
|
||||||
// TN3270E header
|
// TN3270E header
|
||||||
|
|||||||
@@ -376,6 +376,14 @@ public class Xfer3270 implements FileTransferInterface {
|
|||||||
config.setTransferMode(this.option_ASCII ? FTConfig.TransferMode.ASCII : FTConfig.TransferMode.BINARY);
|
config.setTransferMode(this.option_ASCII ? FTConfig.TransferMode.ASCII : FTConfig.TransferMode.BINARY);
|
||||||
config.setCrAction(this.option_CRLF ? FTConfig.CrAction.REMOVE : FTConfig.CrAction.KEEP);
|
config.setCrAction(this.option_CRLF ? FTConfig.CrAction.REMOVE : FTConfig.CrAction.KEEP);
|
||||||
config.setExistAction(this.option_APPEND ? FTConfig.ExistAction.APPEND : FTConfig.ExistAction.REPLACE);
|
config.setExistAction(this.option_APPEND ? FTConfig.ExistAction.APPEND : FTConfig.ExistAction.REPLACE);
|
||||||
|
config.setClear(this.bSendClear);
|
||||||
|
config.setDbcs(this.currentCodePage != null && this.currentCodePage.isDBCS());
|
||||||
|
config.setOptionNew(this.option_NEW);
|
||||||
|
config.setOptionNoso(this.option_NOSO);
|
||||||
|
if (this.option_UNICODE) {
|
||||||
|
config.setUnicode(true);
|
||||||
|
config.setUnicodeType(this.unicodeType);
|
||||||
|
}
|
||||||
config.setDftBufferSize(bmtuSize);
|
config.setDftBufferSize(bmtuSize);
|
||||||
if (options != null && !options.isEmpty()) {
|
if (options != null && !options.isEmpty()) {
|
||||||
config.parseOptions(options);
|
config.parseOptions(options);
|
||||||
@@ -387,7 +395,38 @@ public class Xfer3270 implements FileTransferInterface {
|
|||||||
public void getFile(String command, String hostFile, String localFile, int mode,
|
public void getFile(String command, String hostFile, String localFile, int mode,
|
||||||
String options, ECLXferListener listener) throws ECLErr {
|
String options, ECLXferListener listener) throws ECLErr {
|
||||||
FileTransferStatusInterface statusAdapter = createStatusAdapter(listener, localFile, hostFile);
|
FileTransferStatusInterface statusAdapter = createStatusAdapter(listener, localFile, hostFile);
|
||||||
getFile(hostFile, MVS_TSO, localFile, mode, statusAdapter, options);
|
setOptionFlags(options);
|
||||||
|
if (mode == BINARY) {
|
||||||
|
this.option_ASCII = false;
|
||||||
|
} else {
|
||||||
|
this.option_ASCII = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FTConfig config = new FTConfig();
|
||||||
|
config.setDirection(FTConfig.Direction.RECEIVE);
|
||||||
|
config.setLocalFilename(localFile);
|
||||||
|
config.setHostFilename(hostFile);
|
||||||
|
config.setHostType(FTConfig.HostType.TSO);
|
||||||
|
config.setTransferMode(this.option_ASCII ? FTConfig.TransferMode.ASCII : FTConfig.TransferMode.BINARY);
|
||||||
|
config.setCrAction(this.option_CRLF ? FTConfig.CrAction.REMOVE : FTConfig.CrAction.KEEP);
|
||||||
|
config.setExistAction(this.option_APPEND ? FTConfig.ExistAction.APPEND : FTConfig.ExistAction.REPLACE);
|
||||||
|
config.setClear(this.bSendClear);
|
||||||
|
config.setDbcs(this.currentCodePage != null && this.currentCodePage.isDBCS());
|
||||||
|
config.setOptionNew(this.option_NEW);
|
||||||
|
config.setOptionNoso(this.option_NOSO);
|
||||||
|
if (command != null && !command.trim().isEmpty()) {
|
||||||
|
config.setProgramName(command.trim());
|
||||||
|
}
|
||||||
|
if (this.option_UNICODE) {
|
||||||
|
config.setUnicode(true);
|
||||||
|
config.setUnicodeType(this.unicodeType);
|
||||||
|
}
|
||||||
|
config.setDftBufferSize(bmtuSize);
|
||||||
|
if (options != null && !options.isEmpty()) {
|
||||||
|
config.parseOptions(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
executeTransfer(config, statusAdapter, localFile, hostFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -408,6 +447,15 @@ public class Xfer3270 implements FileTransferInterface {
|
|||||||
(hostType == CICS ? FTConfig.HostType.CICS : FTConfig.HostType.TSO));
|
(hostType == CICS ? FTConfig.HostType.CICS : FTConfig.HostType.TSO));
|
||||||
config.setTransferMode(this.option_ASCII ? FTConfig.TransferMode.ASCII : FTConfig.TransferMode.BINARY);
|
config.setTransferMode(this.option_ASCII ? FTConfig.TransferMode.ASCII : FTConfig.TransferMode.BINARY);
|
||||||
config.setCrAction(this.option_CRLF ? FTConfig.CrAction.REMOVE : FTConfig.CrAction.KEEP);
|
config.setCrAction(this.option_CRLF ? FTConfig.CrAction.REMOVE : FTConfig.CrAction.KEEP);
|
||||||
|
config.setExistAction(this.option_APPEND ? FTConfig.ExistAction.APPEND : FTConfig.ExistAction.REPLACE);
|
||||||
|
config.setClear(this.bSendClear);
|
||||||
|
config.setDbcs(this.currentCodePage != null && this.currentCodePage.isDBCS());
|
||||||
|
config.setOptionNew(this.option_NEW);
|
||||||
|
config.setOptionNoso(this.option_NOSO);
|
||||||
|
if (this.option_UNICODE) {
|
||||||
|
config.setUnicode(true);
|
||||||
|
config.setUnicodeType(this.unicodeType);
|
||||||
|
}
|
||||||
config.setRecfm(recfm);
|
config.setRecfm(recfm);
|
||||||
config.setLrecl(String.valueOf(lrecl));
|
config.setLrecl(String.valueOf(lrecl));
|
||||||
config.setBlksize(String.valueOf(blksize));
|
config.setBlksize(String.valueOf(blksize));
|
||||||
@@ -429,7 +477,48 @@ public class Xfer3270 implements FileTransferInterface {
|
|||||||
public void putFile(String command, String hostFile, String localFile, int mode,
|
public void putFile(String command, String hostFile, String localFile, int mode,
|
||||||
String options, ECLXferListener listener) throws ECLErr {
|
String options, ECLXferListener listener) throws ECLErr {
|
||||||
FileTransferStatusInterface statusAdapter = createStatusAdapter(listener, localFile, hostFile);
|
FileTransferStatusInterface statusAdapter = createStatusAdapter(listener, localFile, hostFile);
|
||||||
putFile(localFile, MVS_TSO, hostFile, mode, statusAdapter, options);
|
setOptionFlags(options);
|
||||||
|
if (mode == BINARY) {
|
||||||
|
this.option_ASCII = false;
|
||||||
|
} else {
|
||||||
|
this.option_ASCII = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FTConfig config = new FTConfig();
|
||||||
|
config.setDirection(FTConfig.Direction.SEND);
|
||||||
|
config.setLocalFilename(localFile);
|
||||||
|
config.setHostFilename(hostFile);
|
||||||
|
config.setHostType(FTConfig.HostType.TSO);
|
||||||
|
config.setTransferMode(this.option_ASCII ? FTConfig.TransferMode.ASCII : FTConfig.TransferMode.BINARY);
|
||||||
|
config.setCrAction(this.option_CRLF ? FTConfig.CrAction.REMOVE : FTConfig.CrAction.KEEP);
|
||||||
|
config.setExistAction(this.option_APPEND ? FTConfig.ExistAction.APPEND : FTConfig.ExistAction.REPLACE);
|
||||||
|
config.setClear(this.bSendClear);
|
||||||
|
config.setDbcs(this.currentCodePage != null && this.currentCodePage.isDBCS());
|
||||||
|
config.setOptionNew(this.option_NEW);
|
||||||
|
config.setOptionNoso(this.option_NOSO);
|
||||||
|
if (command != null && !command.trim().isEmpty()) {
|
||||||
|
config.setProgramName(command.trim());
|
||||||
|
}
|
||||||
|
if (this.option_UNICODE) {
|
||||||
|
config.setUnicode(true);
|
||||||
|
config.setUnicodeType(this.unicodeType);
|
||||||
|
}
|
||||||
|
config.setRecfm(recfm);
|
||||||
|
config.setLrecl(String.valueOf(lrecl));
|
||||||
|
config.setBlksize(String.valueOf(blksize));
|
||||||
|
config.setDftBufferSize(bmtuSize);
|
||||||
|
if (primarySpace > 0) {
|
||||||
|
config.setSpace(primarySpace + (secondarySpace > 0 ? "," + secondarySpace : ""));
|
||||||
|
}
|
||||||
|
if ("TRACKS".equalsIgnoreCase(spaceUnits)) config.setUnits(FTConfig.AllocationUnit.TRACKS);
|
||||||
|
else if ("CYLINDERS".equalsIgnoreCase(spaceUnits)) config.setUnits(FTConfig.AllocationUnit.CYLINDERS);
|
||||||
|
else if ("AVBLOCK".equalsIgnoreCase(spaceUnits)) config.setUnits(FTConfig.AllocationUnit.AVBLOCK);
|
||||||
|
|
||||||
|
if (options != null && !options.isEmpty()) {
|
||||||
|
config.parseOptions(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
executeTransfer(config, statusAdapter, localFile, hostFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void executeTransfer(FTConfig config, FileTransferStatusInterface status,
|
private void executeTransfer(FTConfig config, FileTransferStatusInterface status,
|
||||||
|
|||||||
+476
@@ -0,0 +1,476 @@
|
|||||||
|
package haus.nightmare.lib3270j.ft;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
|
import haus.nightmare.lib3270j.charset.CodePageRegistry;
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
import static haus.nightmare.lib3270j.ft.FTConstants.*;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 16 Test Suite: IND$FILE Host File Transfer Protocol Hardening.
|
||||||
|
* Validates 1:1 packet, timing, and record-layout compatibility with IBM Host On-Demand v14.
|
||||||
|
*/
|
||||||
|
public class FTProtocolHardeningPhase16Test {
|
||||||
|
|
||||||
|
private EbcdicTranslator translator;
|
||||||
|
private ScreenBuffer screen;
|
||||||
|
private MockInputProcessor input;
|
||||||
|
private MockDftListener listener;
|
||||||
|
private FTDft ftDft;
|
||||||
|
|
||||||
|
static class MockInputProcessor extends InputProcessor {
|
||||||
|
final List<byte[]> sentStructuredFields = new ArrayList<>();
|
||||||
|
|
||||||
|
public MockInputProcessor(ScreenBuffer screen, EbcdicTranslator translator) {
|
||||||
|
super(screen, translator, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendStructuredFieldData(byte[] data) {
|
||||||
|
sentStructuredFields.add(data.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class MockDftListener implements FTDft.FTDftListener {
|
||||||
|
FTConfig config = new FTConfig();
|
||||||
|
FTState state = FTState.RUNNING;
|
||||||
|
long bytesTransferred = 0;
|
||||||
|
boolean completeCalled = false;
|
||||||
|
boolean abortCalled = false;
|
||||||
|
String abortMessage = null;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDftRunning() {
|
||||||
|
state = FTState.RUNNING;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTransferComplete(String errorMessage) {
|
||||||
|
state = FTState.NONE;
|
||||||
|
completeCalled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTransferAborted(String errorMessage) {
|
||||||
|
state = FTState.NONE;
|
||||||
|
abortCalled = true;
|
||||||
|
abortMessage = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBytesTransferred(long bytes) {
|
||||||
|
this.bytesTransferred = bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FTState getCurrentState() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setState(FTState state) {
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FTConfig getConfig() {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File getLocalFile() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
translator = new EbcdicTranslator();
|
||||||
|
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
|
||||||
|
input = new MockInputProcessor(screen, translator);
|
||||||
|
listener = new MockDftListener();
|
||||||
|
ftDft = new FTDft(input, translator, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHoDBinaryPacketTemplates() {
|
||||||
|
// 1. RB_OPEN_ACK: 8 bytes matching HoD Xfer3270 lines 553-561
|
||||||
|
byte[] expectedOpenAck = {
|
||||||
|
(byte) 0x88, 0x00, 0x05, (byte) 0xD0, 0x00, 0x09, (byte) 0xFF, (byte) 0xEF
|
||||||
|
};
|
||||||
|
assertArrayEquals(expectedOpenAck, RB_OPEN_ACK);
|
||||||
|
|
||||||
|
// 2. RB_CLOSE_ACK: 8 bytes matching HoD Xfer3270 lines 578-586
|
||||||
|
byte[] expectedCloseAck = {
|
||||||
|
(byte) 0x88, 0x00, 0x05, (byte) 0xD0, 0x41, 0x09, (byte) 0xFF, (byte) 0xEF
|
||||||
|
};
|
||||||
|
assertArrayEquals(expectedCloseAck, RB_CLOSE_ACK);
|
||||||
|
|
||||||
|
// 3. RB_GET_PAST_EOF: 12 bytes matching HoD Xfer3270 lines 795-806
|
||||||
|
byte[] expectedEof = {
|
||||||
|
(byte) 0x88, 0x00, 0x09, (byte) 0xD0, 0x46, 0x08, 0x69, 0x04, 0x22, 0x00, (byte) 0xFF, (byte) 0xEF
|
||||||
|
};
|
||||||
|
assertArrayEquals(expectedEof, RB_GET_PAST_EOF);
|
||||||
|
|
||||||
|
// 4. RB_OPEN_FAILED: 11 bytes matching HoD Xfer3270 lines 588-599
|
||||||
|
byte[] expectedOpenFailed = {
|
||||||
|
0x00, 0x09, (byte) 0xD0, 0x00, 0x08, 0x69, 0x04, 0x01, 0x00, (byte) 0xFF, (byte) 0xEF
|
||||||
|
};
|
||||||
|
assertArrayEquals(expectedOpenFailed, RB_OPEN_FAILED);
|
||||||
|
|
||||||
|
// 5. createDataAck(recnum): 14 bytes matching HoD Xfer3270 lines 563-576 + doubleFoxFox
|
||||||
|
byte[] ack1 = createDataAck(1);
|
||||||
|
assertEquals(14, ack1.length);
|
||||||
|
assertEquals((byte) 0x88, ack1[0]);
|
||||||
|
assertEquals(0x00, ack1[1]);
|
||||||
|
assertEquals(0x0B, ack1[2]);
|
||||||
|
assertEquals((byte) 0xD0, ack1[3]);
|
||||||
|
assertEquals(0x47, ack1[4]);
|
||||||
|
assertEquals(0x05, ack1[5]);
|
||||||
|
assertEquals(0x63, ack1[6]);
|
||||||
|
assertEquals(0x06, ack1[7]);
|
||||||
|
assertEquals(0x00, ack1[8]);
|
||||||
|
assertEquals(0x00, ack1[9]);
|
||||||
|
assertEquals(0x00, ack1[10]);
|
||||||
|
assertEquals(0x01, ack1[11]);
|
||||||
|
assertEquals((byte) 0xFF, ack1[12]);
|
||||||
|
assertEquals((byte) 0xEF, ack1[13]);
|
||||||
|
|
||||||
|
byte[] ackLarge = createDataAck(0x12345678L);
|
||||||
|
assertEquals(0x12, ackLarge[8] & 0xFF);
|
||||||
|
assertEquals(0x34, ackLarge[9] & 0xFF);
|
||||||
|
assertEquals(0x56, ackLarge[10] & 0xFF);
|
||||||
|
assertEquals(0x78, ackLarge[11] & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenRequestSendsExactHoDOpenAck() {
|
||||||
|
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
ftDft.initTransfer(in, out);
|
||||||
|
|
||||||
|
// Build Open Request SF
|
||||||
|
byte[] openSf = {
|
||||||
|
0x00, 0x05, (byte) 0xD0, 0x00, 0x12
|
||||||
|
};
|
||||||
|
|
||||||
|
ftDft.processStructuredField(openSf, 0, openSf.length);
|
||||||
|
|
||||||
|
assertEquals(1, input.sentStructuredFields.size());
|
||||||
|
assertArrayEquals(RB_OPEN_ACK, input.sentStructuredFields.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCloseRequestFlushesAndSendsExactHoDCloseAck() {
|
||||||
|
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
ftDft.initTransfer(in, out);
|
||||||
|
|
||||||
|
byte[] closeSf = {
|
||||||
|
0x00, 0x05, (byte) 0xD0, 0x41, 0x12
|
||||||
|
};
|
||||||
|
|
||||||
|
ftDft.processStructuredField(closeSf, 0, closeSf.length);
|
||||||
|
|
||||||
|
assertEquals(1, input.sentStructuredFields.size());
|
||||||
|
assertArrayEquals(RB_CLOSE_ACK, input.sentStructuredFields.get(0));
|
||||||
|
assertTrue(listener.completeCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUploadEofSendsExactHoDGetPastEof() {
|
||||||
|
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
ftDft.initTransfer(in, out);
|
||||||
|
|
||||||
|
// Host sends TR_GET_REQ (0x4611)
|
||||||
|
byte[] getSf = {
|
||||||
|
0x00, 0x05, (byte) 0xD0, 0x46, 0x11
|
||||||
|
};
|
||||||
|
|
||||||
|
ftDft.processStructuredField(getSf, 0, getSf.length);
|
||||||
|
|
||||||
|
assertEquals(1, input.sentStructuredFields.size());
|
||||||
|
assertArrayEquals(RB_GET_PAST_EOF, input.sentStructuredFields.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDataAckLayoutOnDownload() {
|
||||||
|
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
ftDft.initTransfer(in, out);
|
||||||
|
|
||||||
|
// Host sends Data Insert SF with 4 data bytes
|
||||||
|
byte[] dataInsert = {
|
||||||
|
0x00, 0x0C, (byte) 0xD0, 0x47, 0x04,
|
||||||
|
0x61, 0x00, 0x09, // TR_BEGIN_DATA + len (9 = 4 data + 5)
|
||||||
|
'T', 'E', 'S', 'T'
|
||||||
|
};
|
||||||
|
|
||||||
|
ftDft.processStructuredField(dataInsert, 0, dataInsert.length);
|
||||||
|
|
||||||
|
assertEquals(1, input.sentStructuredFields.size());
|
||||||
|
byte[] expectedAck = createDataAck(1);
|
||||||
|
assertArrayEquals(expectedAck, input.sentStructuredFields.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHostErrorRepliesMappedToDescriptions() {
|
||||||
|
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
ftDft.initTransfer(in, out);
|
||||||
|
|
||||||
|
// 1. TR_ERR_OPEN_FAILED (0x0100)
|
||||||
|
byte[] errOpenFailed = {
|
||||||
|
0x00, 0x09, (byte) 0xD0, 0x00, 0x08, 0x69, 0x04, 0x01, 0x00
|
||||||
|
};
|
||||||
|
ftDft.processStructuredField(errOpenFailed, 0, errOpenFailed.length);
|
||||||
|
assertTrue(listener.abortCalled);
|
||||||
|
assertTrue(listener.abortMessage.toLowerCase().contains("file not found") ||
|
||||||
|
listener.abortMessage.toLowerCase().contains("open failed"));
|
||||||
|
|
||||||
|
// Reset listener
|
||||||
|
listener.abortCalled = false;
|
||||||
|
listener.abortMessage = null;
|
||||||
|
|
||||||
|
// 2. TR_ERR_NOT_AUTHORIZED (0x3E00)
|
||||||
|
byte[] errNotAuth = {
|
||||||
|
0x00, 0x09, (byte) 0xD0, 0x00, 0x08, 0x69, 0x04, 0x3E, 0x00
|
||||||
|
};
|
||||||
|
ftDft.processStructuredField(errNotAuth, 0, errNotAuth.length);
|
||||||
|
assertTrue(listener.abortCalled);
|
||||||
|
assertTrue(listener.abortMessage.toLowerCase().contains("not authorized") ||
|
||||||
|
listener.abortMessage.toLowerCase().contains("security"));
|
||||||
|
|
||||||
|
// 3. TR_ERR_SYNTAX_ERROR (0x6000)
|
||||||
|
listener.abortCalled = false;
|
||||||
|
byte[] errSyntax = {
|
||||||
|
0x00, 0x09, (byte) 0xD0, 0x00, 0x08, 0x69, 0x04, 0x60, 0x00
|
||||||
|
};
|
||||||
|
ftDft.processStructuredField(errSyntax, 0, errSyntax.length);
|
||||||
|
assertTrue(listener.abortCalled);
|
||||||
|
assertTrue(listener.abortMessage.toLowerCase().contains("syntax error") ||
|
||||||
|
listener.abortMessage.toLowerCase().contains("options"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStreamNameDetectionAsciiAndEbcdic() {
|
||||||
|
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
ftDft.initTransfer(in, out);
|
||||||
|
|
||||||
|
// Open request containing EBCDIC FT:MSG
|
||||||
|
ByteArrayOutputStream sfMsg = new ByteArrayOutputStream();
|
||||||
|
sfMsg.write(0x00); sfMsg.write(0x10);
|
||||||
|
sfMsg.write(SF_TRANSFER_DATA);
|
||||||
|
sfMsg.write(0x00); sfMsg.write(0x12);
|
||||||
|
sfMsg.write(STREAM_NAME_FT_MSG_BYTES_EBCDIC, 0, STREAM_NAME_FT_MSG_BYTES_EBCDIC.length);
|
||||||
|
|
||||||
|
byte[] msgPacket = sfMsg.toByteArray();
|
||||||
|
msgPacket[0] = (byte) ((msgPacket.length >> 8) & 0xFF);
|
||||||
|
msgPacket[1] = (byte) (msgPacket.length & 0xFF);
|
||||||
|
|
||||||
|
ftDft.processStructuredField(msgPacket, 0, msgPacket.length);
|
||||||
|
assertEquals(FTState.RUNNING, listener.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRecfmFPaddingAndSplittingUpload() {
|
||||||
|
FTConfig config = new FTConfig();
|
||||||
|
config.setDirection(FTConfig.Direction.SEND);
|
||||||
|
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||||
|
config.setRecfm(FTConfig.RecordFormat.FIXED);
|
||||||
|
config.setLrecl("10");
|
||||||
|
listener.config = config;
|
||||||
|
|
||||||
|
// Local content has two lines: "SHORT" (5 chars) and "LONGER LINE THAN TEN" (20 chars)
|
||||||
|
String localContent = "SHORT\nLONGER LINE THAN TEN\n";
|
||||||
|
ByteArrayInputStream in = new ByteArrayInputStream(localContent.getBytes(StandardCharsets.UTF_8));
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
ftDft.initTransfer(in, out);
|
||||||
|
|
||||||
|
// Host sends TR_GET_REQ
|
||||||
|
byte[] getSf = { 0x00, 0x05, (byte) 0xD0, 0x46, 0x11 };
|
||||||
|
ftDft.processStructuredField(getSf, 0, getSf.length);
|
||||||
|
|
||||||
|
assertEquals(1, input.sentStructuredFields.size());
|
||||||
|
byte[] reply = input.sentStructuredFields.get(0);
|
||||||
|
int headerLen = 17;
|
||||||
|
int dataLen = reply.length - headerLen;
|
||||||
|
assertTrue(dataLen >= 30, "Should have packed at least 3 fixed records of 10 bytes each");
|
||||||
|
|
||||||
|
String rec1 = new String(reply, headerLen, 10, StandardCharsets.UTF_8);
|
||||||
|
assertEquals("SHORT ", rec1);
|
||||||
|
|
||||||
|
String rec2 = new String(reply, headerLen + 10, 10, StandardCharsets.UTF_8);
|
||||||
|
assertEquals("LONGER LIN", rec2);
|
||||||
|
|
||||||
|
String rec3 = new String(reply, headerLen + 20, 10, StandardCharsets.UTF_8);
|
||||||
|
assertEquals("E THAN TEN", rec3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRecfmFDownloadTrimmingAndNewline() throws Exception {
|
||||||
|
FTConfig config = new FTConfig();
|
||||||
|
config.setDirection(FTConfig.Direction.RECEIVE);
|
||||||
|
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||||
|
config.setRecfm(FTConfig.RecordFormat.FIXED);
|
||||||
|
config.setLrecl("10");
|
||||||
|
listener.config = config;
|
||||||
|
|
||||||
|
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
ftDft.initTransfer(in, out);
|
||||||
|
|
||||||
|
// Host sends 10-byte fixed record "HELLO "
|
||||||
|
byte[] hostRecord = "HELLO ".getBytes(StandardCharsets.UTF_8);
|
||||||
|
ByteArrayOutputStream sf = new ByteArrayOutputStream();
|
||||||
|
sf.write(0x00); sf.write(hostRecord.length + 8);
|
||||||
|
sf.write(SF_TRANSFER_DATA);
|
||||||
|
sf.write(0x47); sf.write(0x04);
|
||||||
|
sf.write(TR_BEGIN_DATA);
|
||||||
|
int fieldLen = hostRecord.length + 5;
|
||||||
|
sf.write((fieldLen >> 8) & 0xFF); sf.write(fieldLen & 0xFF);
|
||||||
|
sf.write(hostRecord);
|
||||||
|
|
||||||
|
byte[] pkt = sf.toByteArray();
|
||||||
|
pkt[0] = (byte) ((pkt.length >> 8) & 0xFF);
|
||||||
|
pkt[1] = (byte) (pkt.length & 0xFF);
|
||||||
|
|
||||||
|
ftDft.processStructuredField(pkt, 0, pkt.length);
|
||||||
|
|
||||||
|
// Verify output has "HELLO" stripped of trailing spaces plus local line separator
|
||||||
|
String result = new String(out.toByteArray(), StandardCharsets.UTF_8);
|
||||||
|
assertEquals("HELLO" + System.lineSeparator(), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRecfmVRdwUploadAndDownload() throws Exception {
|
||||||
|
// 1. Upload RECFM V
|
||||||
|
FTConfig sendConfig = new FTConfig();
|
||||||
|
sendConfig.setDirection(FTConfig.Direction.SEND);
|
||||||
|
sendConfig.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||||
|
sendConfig.setRecfm(FTConfig.RecordFormat.VARIABLE);
|
||||||
|
listener.config = sendConfig;
|
||||||
|
|
||||||
|
String testLine = "VARIABLE RECORD TEST";
|
||||||
|
ByteArrayInputStream in = new ByteArrayInputStream((testLine + "\n").getBytes(StandardCharsets.UTF_8));
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
ftDft.initTransfer(in, out);
|
||||||
|
|
||||||
|
byte[] getSf = { 0x00, 0x05, (byte) 0xD0, 0x46, 0x11 };
|
||||||
|
ftDft.processStructuredField(getSf, 0, getSf.length);
|
||||||
|
|
||||||
|
assertEquals(1, input.sentStructuredFields.size());
|
||||||
|
byte[] reply = input.sentStructuredFields.get(0);
|
||||||
|
int headerLen = 17;
|
||||||
|
|
||||||
|
// Verify 4-byte RDW header: length should be testLine.length() + 4
|
||||||
|
int rdwLen = ((reply[headerLen] & 0xFF) << 8) | (reply[headerLen + 1] & 0xFF);
|
||||||
|
assertEquals(testLine.length() + 4, rdwLen);
|
||||||
|
assertEquals(0, reply[headerLen + 2]);
|
||||||
|
assertEquals(0, reply[headerLen + 3]);
|
||||||
|
String payload = new String(reply, headerLen + 4, testLine.length(), StandardCharsets.UTF_8);
|
||||||
|
assertEquals(testLine, payload);
|
||||||
|
|
||||||
|
// 2. Download RECFM V
|
||||||
|
FTConfig recvConfig = new FTConfig();
|
||||||
|
recvConfig.setDirection(FTConfig.Direction.RECEIVE);
|
||||||
|
recvConfig.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||||
|
recvConfig.setRecfm(FTConfig.RecordFormat.VARIABLE);
|
||||||
|
listener.config = recvConfig;
|
||||||
|
|
||||||
|
ByteArrayOutputStream downOut = new ByteArrayOutputStream();
|
||||||
|
ftDft.initTransfer(new ByteArrayInputStream(new byte[0]), downOut);
|
||||||
|
|
||||||
|
// Host sends RDW header + "TEST V DATA"
|
||||||
|
byte[] vData = "TEST V DATA".getBytes(StandardCharsets.UTF_8);
|
||||||
|
int vRdwLen = vData.length + 4;
|
||||||
|
ByteArrayOutputStream sfDown = new ByteArrayOutputStream();
|
||||||
|
sfDown.write(0x00); sfDown.write(0x00);
|
||||||
|
sfDown.write(SF_TRANSFER_DATA);
|
||||||
|
sfDown.write(0x47); sfDown.write(0x04);
|
||||||
|
sfDown.write(TR_BEGIN_DATA);
|
||||||
|
int fLen = vRdwLen + 5;
|
||||||
|
sfDown.write((fLen >> 8) & 0xFF); sfDown.write(fLen & 0xFF);
|
||||||
|
sfDown.write((vRdwLen >> 8) & 0xFF); sfDown.write(vRdwLen & 0xFF);
|
||||||
|
sfDown.write(0x00); sfDown.write(0x00);
|
||||||
|
sfDown.write(vData);
|
||||||
|
|
||||||
|
byte[] downPkt = sfDown.toByteArray();
|
||||||
|
downPkt[0] = (byte) ((downPkt.length >> 8) & 0xFF);
|
||||||
|
downPkt[1] = (byte) (downPkt.length & 0xFF);
|
||||||
|
|
||||||
|
ftDft.processStructuredField(downPkt, 0, downPkt.length);
|
||||||
|
|
||||||
|
String downResult = new String(downOut.toByteArray(), StandardCharsets.UTF_8);
|
||||||
|
assertEquals("TEST V DATA" + System.lineSeparator(), downResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDbcsProgramSelectionAndOptions() {
|
||||||
|
FTConfig cfgDbcs = new FTConfig();
|
||||||
|
cfgDbcs.setCodePage("930"); // Japanese DBCS
|
||||||
|
assertEquals("APVUFILE", cfgDbcs.getProgramName());
|
||||||
|
|
||||||
|
FTConfig cfgSbcs = new FTConfig();
|
||||||
|
cfgSbcs.setCodePage("037"); // US English SBCS
|
||||||
|
assertEquals("IND$FILE", cfgSbcs.getProgramName());
|
||||||
|
|
||||||
|
// Option NEW and NOSO parsing
|
||||||
|
FTConfig optionsCfg = new FTConfig();
|
||||||
|
optionsCfg.setHostType(FTConfig.HostType.TSO);
|
||||||
|
optionsCfg.setDirection(FTConfig.Direction.SEND);
|
||||||
|
optionsCfg.setHostFilename("'TEST.FILE'");
|
||||||
|
optionsCfg.parseOptions("NEW NOSO APVUFILE");
|
||||||
|
|
||||||
|
assertTrue(optionsCfg.isOptionNew());
|
||||||
|
assertTrue(optionsCfg.isOptionNoso());
|
||||||
|
assertEquals("APVUFILE", optionsCfg.getProgramName());
|
||||||
|
|
||||||
|
String cmd = optionsCfg.buildCommand();
|
||||||
|
assertTrue(cmd.startsWith("APVUFILE PUT 'TEST.FILE'"));
|
||||||
|
assertTrue(cmd.contains("NEW"));
|
||||||
|
assertTrue(cmd.contains("NOSO"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCutErrorScreenScanning() {
|
||||||
|
FTCut cut = new FTCut(screen, input, translator, new FTCut.FTCutListener() {
|
||||||
|
@Override public void onCutRunning() {}
|
||||||
|
@Override public void onTransferComplete(String err) {}
|
||||||
|
@Override public void onTransferAborted(String err) {}
|
||||||
|
@Override public void onBytesTransferred(long b) {}
|
||||||
|
@Override public FTConstants.FTState getCurrentState() { return FTConstants.FTState.AWAIT_ACK; }
|
||||||
|
@Override public void setState(FTConstants.FTState s) {}
|
||||||
|
@Override public FTConfig getConfig() { return new FTConfig(); }
|
||||||
|
@Override public File getLocalFile() { return null; }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write an error line on row 2 of screen
|
||||||
|
String errLine = "IKJ56700I DATASET 'NONEXIST.FILE' NOT FOUND";
|
||||||
|
int startPos = 80; // row 2, col 1
|
||||||
|
for (int i = 0; i < errLine.length(); i++) {
|
||||||
|
screen.setCell(startPos + i, translator.unicodeToEbcdic(errLine.charAt(i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
String detected = cut.scanScreenForErrors();
|
||||||
|
assertNotNull(detected);
|
||||||
|
assertTrue(detected.contains("IKJ56700I"));
|
||||||
|
assertTrue(detected.contains("NOT FOUND"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+303
@@ -0,0 +1,303 @@
|
|||||||
|
package haus.nightmare.lib3270j.graphics;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||||
|
import haus.nightmare.lib3270j.protocol.DS3270Constants;
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Targeted verification tests for Phase 17:
|
||||||
|
* - Multi-polygon area fills with boundary enforcement & winding/even-odd rules
|
||||||
|
* - GFARC/GCFARC full arc & ellipse generation with HODEllipse Bresenham tables
|
||||||
|
* - GARC/GCARC 3-point circular arc interpolation
|
||||||
|
* - FilletPts polynomial interpolation (HoD getFilletPoints) and area accumulation
|
||||||
|
* - GRLINE/GCRLIN relative lines with signed 8-bit deltas
|
||||||
|
* - GSAP (P/Q/R/S) aspect ratio and coordinate transformations
|
||||||
|
* - GBIMG image multiplier (imult) support
|
||||||
|
* - 56-byte Graphic Input structured field layout and sendGraphicMouseAid dispatch
|
||||||
|
*/
|
||||||
|
public class Phase17GocaGraphicsParityTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMultiPolygonAreaFillWithBoundaryAndFillRules() {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||||
|
GocaDecoder decoder = new GocaDecoder(plane);
|
||||||
|
|
||||||
|
// 1. Begin Area with boundary flag set (0x40) and Winding rule (0x20)
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
out.write(GocaConstants.G_GSCOL); out.write(0x02); // Red
|
||||||
|
out.write(GocaConstants.G_GSLW); out.write(0x02); // Line width 2
|
||||||
|
out.write(GocaConstants.G_GBAR); out.write(0x40 | 0x20); // flags: drawBoundary=true, winding=true
|
||||||
|
|
||||||
|
// First polygon: Triangle
|
||||||
|
out.write(GocaConstants.G_GLINE); out.write(12);
|
||||||
|
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
|
||||||
|
out.write(0x00); out.write(50); out.write(0x00); out.write(10);
|
||||||
|
out.write(0x00); out.write(30); out.write(0x00); out.write(50);
|
||||||
|
|
||||||
|
// End Area
|
||||||
|
out.write(GocaConstants.G_GEAR);
|
||||||
|
|
||||||
|
byte[] stream = out.toByteArray();
|
||||||
|
decoder.decodeGoca(stream, 0, stream.length);
|
||||||
|
|
||||||
|
assertTrue(plane.hasContent(), "GraphicsPlane must have content after area fill");
|
||||||
|
assertTrue(decoder.hasActiveGraphics(), "Decoder must report active graphics when plane has content");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHODEllipseAndFullArcGeneration() {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||||
|
GocaDecoder decoder = new GocaDecoder(plane);
|
||||||
|
|
||||||
|
// Set GSAP aspect ratio / semi-axes: P=20, Q=1, R=10, S=1
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
out.write(GocaConstants.G_GSAP); out.write(8);
|
||||||
|
out.write(0x00); out.write(20); // P
|
||||||
|
out.write(0x00); out.write(1); // Q
|
||||||
|
out.write(0x00); out.write(10); // R
|
||||||
|
out.write(0x00); out.write(1); // S
|
||||||
|
|
||||||
|
// Full Arc Absolute (0xC7): Center=(100, 100), Multiplier=1.0 (1, 0)
|
||||||
|
out.write(GocaConstants.G_GFARC); out.write(6);
|
||||||
|
out.write(0x00); out.write(100); // centerX
|
||||||
|
out.write(0x00); out.write(100); // centerY
|
||||||
|
out.write(0x01); out.write(0x00); // Multiplier 1.00
|
||||||
|
|
||||||
|
byte[] stream = out.toByteArray();
|
||||||
|
decoder.decodeGoca(stream, 0, stream.length);
|
||||||
|
|
||||||
|
assertEquals(20, decoder.getArcParamP());
|
||||||
|
assertEquals(1, decoder.getArcParamQ());
|
||||||
|
assertEquals(10, decoder.getArcParamR());
|
||||||
|
assertEquals(1, decoder.getArcParamS());
|
||||||
|
assertTrue(plane.hasContent(), "Full arc must render on graphics plane");
|
||||||
|
|
||||||
|
// Verify direct HODEllipse Bresenham generation
|
||||||
|
int[] ptsX = new int[256];
|
||||||
|
int[] ptsY = new int[256];
|
||||||
|
int nPts = HODEllipse.getHODEllipse(20.0, 1.0, 10.0, 1.0, ptsX, ptsY, 50, 50);
|
||||||
|
assertTrue(nPts > 8, "HODEllipse must generate valid polygon vertices");
|
||||||
|
assertEquals(ptsX[0], ptsX[nPts - 1], "HODEllipse polygon must close to initial vertex");
|
||||||
|
assertEquals(ptsY[0], ptsY[nPts - 1], "HODEllipse polygon must close to initial vertex");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testThreePointCircularArcInterpolation() {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||||
|
GocaDecoder decoder = new GocaDecoder(plane);
|
||||||
|
|
||||||
|
// Current Position Arc (GCARC 0x86):
|
||||||
|
// Start at (0, 0), intermediate at (50, 50), end at (100, 0) -> upper semicircle
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
out.write(GocaConstants.G_GSCP); out.write(4);
|
||||||
|
out.write(0x00); out.write(0);
|
||||||
|
out.write(0x00); out.write(0);
|
||||||
|
|
||||||
|
out.write(GocaConstants.G_GCARC); out.write(8);
|
||||||
|
out.write(0x00); out.write(50); // intermediate X
|
||||||
|
out.write(0x00); out.write(50); // intermediate Y
|
||||||
|
out.write(0x00); out.write(100); // end X
|
||||||
|
out.write(0x00); out.write(0); // end Y
|
||||||
|
|
||||||
|
byte[] stream = out.toByteArray();
|
||||||
|
decoder.decodeGoca(stream, 0, stream.length);
|
||||||
|
|
||||||
|
assertTrue(plane.hasContent(), "3-point circular arc must render on graphics plane");
|
||||||
|
assertEquals(100, decoder.getCurX(), "Terminal X must update to arc end point");
|
||||||
|
assertEquals(0, decoder.getCurY(), "Terminal Y must update to arc end point");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFilletPolynomialInterpolationAndAreaAccumulation() {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||||
|
GocaDecoder decoder = new GocaDecoder(plane);
|
||||||
|
|
||||||
|
// Direct test of FilletPts polynomial interpolation
|
||||||
|
FilletPts fillet = new FilletPts();
|
||||||
|
int[] ctrlX = new int[] { 10, 50, 90 };
|
||||||
|
int[] ctrlY = new int[] { 10, 80, 10 };
|
||||||
|
int req = fillet.getPointsRequired(3);
|
||||||
|
int[] outX = new int[req];
|
||||||
|
int[] outY = new int[req];
|
||||||
|
int nPts = fillet.getFilletPoints(ctrlX, ctrlY, 3, outX, outY);
|
||||||
|
assertTrue(nPts >= 3, "FilletPts must interpolate curve points across control vertices");
|
||||||
|
|
||||||
|
// GFLT (0xC5 Fillet Absolute) inside an Area
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
out.write(GocaConstants.G_GBAR); out.write(0x40); // drawBoundary=true
|
||||||
|
out.write(GocaConstants.G_GFLT); out.write(12);
|
||||||
|
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
|
||||||
|
out.write(0x00); out.write(50); out.write(0x00); out.write(80);
|
||||||
|
out.write(0x00); out.write(90); out.write(0x00); out.write(10);
|
||||||
|
out.write(GocaConstants.G_GEAR);
|
||||||
|
|
||||||
|
byte[] stream = out.toByteArray();
|
||||||
|
decoder.decodeGoca(stream, 0, stream.length);
|
||||||
|
|
||||||
|
assertTrue(plane.hasContent(), "Area with fillet must render on graphics plane");
|
||||||
|
assertEquals(90, decoder.getCurX(), "Current X must update to last fillet vertex");
|
||||||
|
assertEquals(10, decoder.getCurY(), "Current Y must update to last fillet vertex");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRelativeLinesWithSignedDeltas() {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||||
|
GocaDecoder decoder = new GocaDecoder(plane);
|
||||||
|
|
||||||
|
// GRLINE (0xE1): Start at (100, 100), dx=-20, dy=+30, dx=+40, dy=-15
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
out.write(GocaConstants.G_GRLINE); out.write(8);
|
||||||
|
out.write(0x00); out.write(100); // startX
|
||||||
|
out.write(0x00); out.write(100); // startY
|
||||||
|
out.write((byte) -20); // dx1 = -20
|
||||||
|
out.write((byte) 30); // dy1 = +30
|
||||||
|
out.write((byte) 40); // dx2 = +40
|
||||||
|
out.write((byte) -15); // dy2 = -15
|
||||||
|
|
||||||
|
byte[] stream = out.toByteArray();
|
||||||
|
decoder.decodeGoca(stream, 0, stream.length);
|
||||||
|
|
||||||
|
assertTrue(plane.hasContent());
|
||||||
|
// 100 - 20 + 40 = 120
|
||||||
|
assertEquals(120, decoder.getCurX(), "Current X should be 120");
|
||||||
|
// 100 + 30 - 15 = 115
|
||||||
|
assertEquals(115, decoder.getCurY(), "Current Y should be 115");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGsapAspectRatioAndCoordinateTransform() {
|
||||||
|
GddmCoordinateTransform transform = new GddmCoordinateTransform(400, 300);
|
||||||
|
transform.setAspectRatio(2, 1, 3, 1);
|
||||||
|
|
||||||
|
assertEquals(2.0, transform.getScaleX(), 0.001);
|
||||||
|
assertEquals(3.0, transform.getScaleY(), 0.001);
|
||||||
|
|
||||||
|
// Coordinate inversion tests
|
||||||
|
int gocaY = 50;
|
||||||
|
int screenY = transform.mapGocaYToScreen(gocaY);
|
||||||
|
int invertedGocaY = transform.mapScreenYToGoca(screenY);
|
||||||
|
assertEquals(gocaY, invertedGocaY, "Inversion round-trip must preserve GOCA Y");
|
||||||
|
|
||||||
|
transform.resetAspectRatio();
|
||||||
|
assertEquals(1.0, transform.getScaleX(), 0.001);
|
||||||
|
assertEquals(1.0, transform.getScaleY(), 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testImageMultiplierSupport() {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||||
|
GocaDecoder decoder = new GocaDecoder(plane);
|
||||||
|
|
||||||
|
// GBIMG (0xD1): origin=(10, 10), imult=2, w=8, h=8, 1 bpp uncompressed
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
out.write(GocaConstants.G_GBIMG); out.write(10);
|
||||||
|
out.write(0x00); out.write(10); // X
|
||||||
|
out.write(0x00); out.write(10); // Y
|
||||||
|
out.write(0x00); out.write(2); // imult = 2
|
||||||
|
out.write(0x00); out.write(8); // w = 8
|
||||||
|
out.write(0x00); out.write(8); // h = 8
|
||||||
|
|
||||||
|
// GIMD (0x92): 8 bytes (8 rows of 8 bits each)
|
||||||
|
out.write(GocaConstants.G_GIMD); out.write(8);
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
out.write((byte) 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GEIMG (0x93)
|
||||||
|
out.write(GocaConstants.G_GEIMG);
|
||||||
|
|
||||||
|
byte[] stream = out.toByteArray();
|
||||||
|
decoder.decodeGoca(stream, 0, stream.length);
|
||||||
|
|
||||||
|
assertEquals(2, decoder.getImageMultiplier(), "Image multiplier must be parsed as 2");
|
||||||
|
assertTrue(plane.hasContent(), "Image must render on graphics plane");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGraphicInputStructuredField56BytesLayout() {
|
||||||
|
int gocaX = 1234;
|
||||||
|
int gocaY = -5678;
|
||||||
|
int row = 10;
|
||||||
|
int col = 20;
|
||||||
|
|
||||||
|
// Button 1 (Pick), Shift=true, Ctrl=false
|
||||||
|
byte[] sf = GraphicInputBuilder.buildGraphicInput(gocaX, gocaY, row, col, 1, true, true, false);
|
||||||
|
|
||||||
|
assertEquals(56, sf.length, "Structured field length must be 56 bytes");
|
||||||
|
// Bytes 0-1: 0x0034 (52 decimal)
|
||||||
|
assertEquals(0x00, sf[0]);
|
||||||
|
assertEquals(0x34, sf[1]);
|
||||||
|
// Bytes 16-19: Class descriptor (0x23, 0x00, 0x23, 0x00)
|
||||||
|
assertEquals(0x23, sf[16]);
|
||||||
|
assertEquals(0x00, sf[17]);
|
||||||
|
assertEquals(0x23, sf[18]);
|
||||||
|
assertEquals(0x00, sf[19]);
|
||||||
|
// Bytes 24-25: 16-bit signed GOCA X
|
||||||
|
short x = (short) (((sf[24] & 0xFF) << 8) | (sf[25] & 0xFF));
|
||||||
|
assertEquals(gocaX, x);
|
||||||
|
// Bytes 26-27: 16-bit signed GOCA Y
|
||||||
|
short y = (short) (((sf[26] & 0xFF) << 8) | (sf[27] & 0xFF));
|
||||||
|
assertEquals(gocaY, y);
|
||||||
|
// Bytes 28-31: Fixed mouse trigger class (0x00000004)
|
||||||
|
assertEquals(0x00, sf[28]);
|
||||||
|
assertEquals(0x00, sf[29]);
|
||||||
|
assertEquals(0x00, sf[30]);
|
||||||
|
assertEquals(0x04, sf[31]);
|
||||||
|
// Bytes 32-33: Fixed correlation class (0x0004)
|
||||||
|
assertEquals(0x00, sf[32]);
|
||||||
|
assertEquals(0x04, sf[33]);
|
||||||
|
// Byte 34: Shift flag 0x80
|
||||||
|
assertEquals((byte) 0x80, sf[34]);
|
||||||
|
// Byte 35: Button 1 (0x01)
|
||||||
|
assertEquals(0x01, sf[35]);
|
||||||
|
|
||||||
|
// Button 2 (Action), Shift=false, Ctrl=true
|
||||||
|
byte[] sf2 = GraphicInputBuilder.buildGraphicInput(gocaX, gocaY, row, col, 2, true, false, true);
|
||||||
|
assertEquals((byte) 0x40, sf2[34]);
|
||||||
|
assertEquals(0x02, sf2[35]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSendGraphicMouseAidInteractiveDispatch() {
|
||||||
|
EbcdicTranslator translator = new EbcdicTranslator();
|
||||||
|
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator);
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||||
|
GocaDecoder decoder = new GocaDecoder(plane);
|
||||||
|
|
||||||
|
AtomicReference<byte[]> sentData = new AtomicReference<>();
|
||||||
|
InputProcessor inputProcessor = new InputProcessor(screen, translator, null) {
|
||||||
|
@Override
|
||||||
|
protected void sendAidResponse(byte[] data) {
|
||||||
|
sentData.set(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
inputProcessor.setGocaDecoder(decoder);
|
||||||
|
|
||||||
|
// When no graphics are active, regular RMA is sent
|
||||||
|
inputProcessor.sendGraphicMouseAid(DS3270Constants.AID_ENTER, 1, false, false);
|
||||||
|
assertNotNull(sentData.get());
|
||||||
|
assertNotEquals(DS3270Constants.AID_SF, sentData.get()[0], "Without active graphics, AID_SF must not be sent");
|
||||||
|
|
||||||
|
// Activate graphics cursor
|
||||||
|
sentData.set(null);
|
||||||
|
inputProcessor.setKeyboardLocked(false);
|
||||||
|
decoder.attachGraphicCursor(100, 200);
|
||||||
|
assertTrue(decoder.hasActiveGraphics());
|
||||||
|
|
||||||
|
inputProcessor.sendGraphicMouseAid(100, 200, DS3270Constants.AID_ENTER, 1, false, false);
|
||||||
|
byte[] out = sentData.get();
|
||||||
|
assertNotNull(out);
|
||||||
|
assertEquals(DS3270Constants.AID_SF, out[0] & 0xFF, "With active graphics, Structured Field AID (0x88) must be sent");
|
||||||
|
assertTrue(out.length >= 57, "Output must contain AID byte + 56-byte Graphic Input SF");
|
||||||
|
assertEquals(0x00, out[1]);
|
||||||
|
assertEquals(0x34, out[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
+126
-2
@@ -51,7 +51,7 @@ public class ProgramSymbolManagerTest {
|
|||||||
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x40, 0x41);
|
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x40, 0x41);
|
||||||
assertNotNull(slot);
|
assertNotNull(slot);
|
||||||
assertEquals(9, slot.getWidth());
|
assertEquals(9, slot.getWidth());
|
||||||
assertEquals(12, slot.getHeight());
|
assertEquals(16, slot.getHeight());
|
||||||
assertFalse(slot.isTriplePlane());
|
assertFalse(slot.isTriplePlane());
|
||||||
|
|
||||||
// Verify pixel data (Row 0 Col 0 is 1)
|
// Verify pixel data (Row 0 Col 0 is 1)
|
||||||
@@ -64,7 +64,7 @@ public class ProgramSymbolManagerTest {
|
|||||||
int bg = 0xFF000000; // Black
|
int bg = 0xFF000000; // Black
|
||||||
int[] rgb = slot.getRgbPixels(fg, bg);
|
int[] rgb = slot.getRgbPixels(fg, bg);
|
||||||
assertNotNull(rgb);
|
assertNotNull(rgb);
|
||||||
assertEquals(9 * 12, rgb.length);
|
assertEquals(9 * 16, rgb.length);
|
||||||
assertEquals(fg, rgb[0]); // Row 0 Col 0 pixel should be foreground Green
|
assertEquals(fg, rgb[0]); // Row 0 Col 0 pixel should be foreground Green
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,4 +164,128 @@ public class ProgramSymbolManagerTest {
|
|||||||
assertNotNull(slot);
|
assertNotNull(slot);
|
||||||
assertEquals(1, slot.getPixelData()[0]);
|
assertEquals(1, slot.getPixelData()[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMultiSymbolLoadPsTriplePlane190Symbols() {
|
||||||
|
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||||
|
|
||||||
|
// Simulate bits image loader connect payload: 190 glyphs * 18 bytes = 3420 bytes
|
||||||
|
// Sent to a triple plane set (RWS 4, LCID 0x42) with colorPlane = 0 (no ext header)
|
||||||
|
byte[] payload = new byte[4 + 190 * 18];
|
||||||
|
payload[0] = 0x41; // Format 1, clearAll = true
|
||||||
|
payload[1] = 0x42; // LCID 0x42
|
||||||
|
payload[2] = 0x41; // Start codepoint 0x41
|
||||||
|
payload[3] = 0x04; // RWS 4 (Triple Plane)
|
||||||
|
|
||||||
|
for (int i = 0; i < 190; i++) {
|
||||||
|
// Set Row 0 Col 0 for each symbol
|
||||||
|
payload[4 + i * 18] = (byte) 0x80;
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.loadps(payload);
|
||||||
|
|
||||||
|
ProgramSymbolSet set = manager.getSymbolSet(0x42);
|
||||||
|
assertNotNull(set);
|
||||||
|
|
||||||
|
// Verify ALL 190 glyphs (from 0x41 to 0xFE) were loaded, NOT just 63!
|
||||||
|
for (int cp = 0x41; cp <= 0xFE; cp++) {
|
||||||
|
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x42, cp);
|
||||||
|
assertNotNull(slot, "Glyph for codepoint 0x" + Integer.toHexString(cp) + " must not be null!");
|
||||||
|
assertEquals(9, slot.getWidth());
|
||||||
|
assertEquals(16, slot.getHeight());
|
||||||
|
assertTrue(slot.isTriplePlane());
|
||||||
|
assertEquals(7, slot.getPixelData()[0] & 0xFF, "Pixel (0,0) must have all 3 planes set (7)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testColorPlaneCompositeMergingAndPalette() {
|
||||||
|
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||||
|
|
||||||
|
// 1. Send Plane 1 (Red) for codepoint 0x41
|
||||||
|
byte[] sf1 = new byte[4 + 6 + 18];
|
||||||
|
sf1[0] = (byte) 0x81; // Format 1, extHeader=true
|
||||||
|
sf1[1] = 0x42; // LCID 0x42
|
||||||
|
sf1[2] = 0x41; // Codepoint 0x41
|
||||||
|
sf1[3] = 0x04; // RWS 4
|
||||||
|
sf1[4] = 0x06; // Ext header len = 6
|
||||||
|
sf1[9] = 0x01; // Plane 1 = Red
|
||||||
|
sf1[10] = (byte) 0x80;// Row 0 Col 0 = bit 1
|
||||||
|
manager.loadps(sf1);
|
||||||
|
|
||||||
|
ProgramSymbolSet.SymbolSlot slot1 = manager.getSymbol(0x42, 0x41);
|
||||||
|
assertNotNull(slot1);
|
||||||
|
assertEquals(1, slot1.getPixelData()[0] & 0xFF);
|
||||||
|
// Plane 1 alone must be Red (GOCA_COLORS[2] = 0xFFF01818)
|
||||||
|
assertEquals(GocaConstants.GOCA_COLORS[2], slot1.getRgbPixels(0, 0)[0]);
|
||||||
|
|
||||||
|
// 2. Send Plane 2 (Green) for codepoint 0x41
|
||||||
|
byte[] sf2 = new byte[4 + 6 + 18];
|
||||||
|
sf2[0] = (byte) 0x81;
|
||||||
|
sf2[1] = 0x42;
|
||||||
|
sf2[2] = 0x41;
|
||||||
|
sf2[3] = 0x04;
|
||||||
|
sf2[4] = 0x06;
|
||||||
|
sf2[9] = 0x02; // Plane 2 = Green
|
||||||
|
sf2[10] = (byte) 0x80;// Row 0 Col 0 = bit 1
|
||||||
|
manager.loadps(sf2);
|
||||||
|
|
||||||
|
ProgramSymbolSet.SymbolSlot slot2 = manager.getSymbol(0x42, 0x41);
|
||||||
|
assertNotNull(slot2);
|
||||||
|
// Red (1) | Green (2) = 3 (Yellow)
|
||||||
|
assertEquals(3, slot2.getPixelData()[0] & 0xFF);
|
||||||
|
assertEquals(GocaConstants.GOCA_COLORS[6], slot2.getRgbPixels(0, 0)[0]);
|
||||||
|
|
||||||
|
// 3. Send Plane 4 (Blue) for codepoint 0x41
|
||||||
|
byte[] sf3 = new byte[4 + 6 + 18];
|
||||||
|
sf3[0] = (byte) 0x81;
|
||||||
|
sf3[1] = 0x42;
|
||||||
|
sf3[2] = 0x41;
|
||||||
|
sf3[3] = 0x04;
|
||||||
|
sf3[4] = 0x06;
|
||||||
|
sf3[9] = 0x04; // Plane 4 = Blue
|
||||||
|
sf3[10] = (byte) 0x80;// Row 0 Col 0 = bit 1
|
||||||
|
manager.loadps(sf3);
|
||||||
|
|
||||||
|
ProgramSymbolSet.SymbolSlot slot3 = manager.getSymbol(0x42, 0x41);
|
||||||
|
assertNotNull(slot3);
|
||||||
|
// Red (1) | Green (2) | Blue (4) = 7 (White)
|
||||||
|
assertEquals(7, slot3.getPixelData()[0] & 0xFF);
|
||||||
|
assertEquals(GocaConstants.GOCA_COLORS[7], slot3.getRgbPixels(0, 0)[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransitionMonochromeToColor() {
|
||||||
|
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||||
|
|
||||||
|
// 1. Initial monochrome load (colorPlane = 0)
|
||||||
|
byte[] sfMono = new byte[4 + 18];
|
||||||
|
sfMono[0] = 0x01; // Format 1
|
||||||
|
sfMono[1] = 0x42;
|
||||||
|
sfMono[2] = 0x41;
|
||||||
|
sfMono[3] = 0x04; // RWS 4
|
||||||
|
sfMono[4] = (byte) 0x80; // Row 0 Col 0 set
|
||||||
|
manager.loadps(sfMono);
|
||||||
|
|
||||||
|
ProgramSymbolSet.SymbolSlot monoSlot = manager.getSymbol(0x42, 0x41);
|
||||||
|
assertEquals(7, monoSlot.getPixelData()[0] & 0xFF);
|
||||||
|
assertTrue(monoSlot.isAllPlanesLoaded());
|
||||||
|
|
||||||
|
// 2. Host now sends color plane 1 (Red only)
|
||||||
|
byte[] sfRed = new byte[4 + 6 + 18];
|
||||||
|
sfRed[0] = (byte) 0x81;
|
||||||
|
sfRed[1] = 0x42;
|
||||||
|
sfRed[2] = 0x41;
|
||||||
|
sfRed[3] = 0x04;
|
||||||
|
sfRed[4] = 0x06;
|
||||||
|
sfRed[9] = 0x01; // Plane 1 (Red)
|
||||||
|
sfRed[10] = (byte) 0x80;
|
||||||
|
manager.loadps(sfRed);
|
||||||
|
|
||||||
|
ProgramSymbolSet.SymbolSlot colorSlot = manager.getSymbol(0x42, 0x41);
|
||||||
|
// Previous monochrome bits (7) must NOT bleed in! Only Red (1) should be present!
|
||||||
|
assertEquals(1, colorSlot.getPixelData()[0] & 0xFF);
|
||||||
|
assertEquals(GocaConstants.GOCA_COLORS[2], colorSlot.getRgbPixels(0, 0)[0]);
|
||||||
|
assertFalse(colorSlot.isAllPlanesLoaded());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,6 +218,32 @@ public class InputProcessorTest {
|
|||||||
assertEquals(2, screen.getCursorAddress());
|
assertEquals(2, screen.getCursorAddress());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProtectedFieldSpaceDesignatorNoLightPenSelection() {
|
||||||
|
screen.erase(false);
|
||||||
|
// Field attribute at pos 0: PROTECTED, intensified selectable (0x28) as found in ISPF screen titles
|
||||||
|
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_PROTECT | FA_INT_HIGH_SEL));
|
||||||
|
// Designator character at pos 1: space
|
||||||
|
screen.getCell(1).ec = 0x40;
|
||||||
|
screen.getCell(1).ucs4 = ' ';
|
||||||
|
screen.getCell(2).ec = (byte) 0xC1;
|
||||||
|
screen.getCell(2).ucs4 = 'A';
|
||||||
|
|
||||||
|
java.util.concurrent.atomic.AtomicInteger sentAid = new java.util.concurrent.atomic.AtomicInteger(-1);
|
||||||
|
InputProcessor input = new InputProcessor(screen, translator, null) {
|
||||||
|
@Override
|
||||||
|
public void sendAid(int aidCode) {
|
||||||
|
sentAid.set(aidCode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Per IBM GA23-0059, clicking/light-pen on protected field with space/null must have no effect
|
||||||
|
boolean result = input.lightPenSelect(2);
|
||||||
|
assertFalse(result, "Protected field with space designator must not trigger light-pen selection");
|
||||||
|
assertEquals(-1, sentAid.get(), "Must not send AID_SELECT");
|
||||||
|
assertFalse((screen.getCell(0).fa & FA_MODIFY) != 0, "Must not set FA_MODIFY");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testLightPenSelectEnterImmediateAmpersand() {
|
public void testLightPenSelectEnterImmediateAmpersand() {
|
||||||
screen.erase(false);
|
screen.erase(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user