InfinitePlan.java
/* ========================================================================
* PlantUML : a free UML diagram generator
* ========================================================================
*
* (C) Copyright 2009-2024, Arnaud Roques
*
* Project Info: https://plantuml.com
*
* If you like this project or if you find it useful, you can support us at:
*
* https://plantuml.com/patreon (only 1$ per month!)
* https://plantuml.com/paypal
*
* This file is part of PlantUML.
*
* PlantUML is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PlantUML distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
* License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
*
* Original Author: Arnaud Roques
*
*
*/
package net.sourceforge.plantuml.asciiverse;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import net.sourceforge.plantuml.FileFormat;
import net.sourceforge.plantuml.asciiart.TextStringBounder;
import net.sourceforge.plantuml.security.SecurityUtils;
public final class InfinitePlan {
private final TextStringBounder stringBounder;
private final List<InfiniteString> plan;
private final int dx;
private final int dy;
public InfinitePlan(FileFormat fileFormat) {
this(fileFormat, 0, 0, new ArrayList<>());
}
private InfinitePlan(FileFormat fileFormat, int dx, int dy, List<InfiniteString> plan) {
this.stringBounder = new TextStringBounder(fileFormat);
this.dx = dx;
this.dy = dy;
this.plan = plan;
}
public InfinitePlan move(int movex, int movey) {
return new InfinitePlan(stringBounder.getFileFormat(), dx + movex, dy + movey, plan);
}
public void drawChar(char c) {
ensureSize(dy);
plan.get(dy).setCharAt(dx, c);
}
public void muteChar(char... values) {
if (values.length % 2 != 0)
throw new IllegalArgumentException();
ensureSize(dy);
final char existing = plan.get(dy).getCharAt(dx);
for (int i = 0; i < values.length; i += 2)
if (existing == values[i]) {
plan.get(dy).setCharAt(dx, values[i + 1]);
return;
}
}
public void drawString(String s) {
ensureSize(dy);
plan.get(dy).setStringAt(dx, s);
}
// public void fillRect(char c, int x, int y, int width, int height) {
// for (int i = 0; i < width; i++)
// for (int j = 0; j < height; j++)
// drawChar(c, x + i, y + j);
// }
private void ensureSize(int yAbsolute) {
while (yAbsolute >= plan.size())
plan.add(new InfiniteString());
}
public char getCharAt(int x, int y) {
x += dx;
y += dy;
if (y < 0 || y >= plan.size())
return ' ';
return plan.get(y).getCharAt(x);
}
// Every line is rendered from the same startingPosition, found here as
// the leftmost position any line actually used (InfiniteString.
// getLeftmostPosition(), <= 0). Using each line's own extent instead
// would misalign the output: a line that never drew left of 0 would start
// one column further right than a neighboring line that did. If no line
// ever went negative, this is 0, exactly the historical behavior.
public void exportTxt(OutputStream os) {
final PrintStream ps = SecurityUtils.createPrintStream(os);
int startingPosition = 0;
for (InfiniteString line : plan)
startingPosition = Math.min(startingPosition, line.getLeftmostPosition());
for (InfiniteString line : plan)
ps.println(line.getString(startingPosition));
}
public TextStringBounder getStringBounder() {
return stringBounder;
}
// Character set: centralizes the ASCII vs Unicode (box-drawing) choice for
// every drawing method below, so that callers (Participant.asciiDraw,
// CommunicationTile.asciiDraw, the lifeline-drawing pass...) never hardcode
// a character themselves and stay correct regardless of FileFormat.
// The Unicode code points below are copy-pasted from the legacy
// asciiart.UmlCharAreaImpl (drawBoxSimpleUnicode) rather than reused from
// it, since that package is slated for removal.
public boolean isUnicode() {
return stringBounder.getFileFormat() == FileFormat.UTXT;
}
public char getHLineChar() {
return isUnicode() ? '\u2500' : '-'; // ─ or -
}
public char getVLineChar() {
return isUnicode() ? '\u2502' : '|'; // │ or |
}
// The two lifeline/box-border junction characters: a T pointing down
// (horizontal bar above, vertical stub below — a box's bottom border with
// a lifeline continuing under it) and a T pointing up (the symmetric
// case, a lifeline arriving into a box's top border, or into nothing if
// there is no footbox). ASCII has no distinct up/down T, hence the same
// '+' for both; Unicode does, hence the two different code points below,
// copied for the same reason as every other Unicode glyph in this class:
// read for the character, not reused as code, since the source
// (legacy asciiart) is slated for removal.
private char getTDownChar() {
return isUnicode() ? '┬' : '+';
}
private char getTUpChar() {
return isUnicode() ? '┴' : '+';
}
// Mutes the cell at the current position into the correct lifeline
// crossing, format-aware: a box border character (getHLineChar()) becomes
// a T pointing away from the box, a blank cell becomes a plain lifeline
// segment (getVLineChar()). Centralizes the ASCII '+' vs Unicode '┬'/'┴'
// choice the same way getHLineChar()/getVLineChar() already centralize
// plain borders and lines, so callers (PlayingSpaceWithParticipants's
// lifeline-fill pass) never hardcode a junction character themselves.
public void muteLifelineBelowBox() {
muteChar(getHLineChar(), getTDownChar(), ' ', getVLineChar());
}
public void muteLifelineAboveBox() {
muteChar(getHLineChar(), getTUpChar(), ' ', getVLineChar());
}
// Straight, unconditional fills over an inclusive range — the ASCII
// counterpart of a UGraphic hline/vline. Callers that need to preserve
// existing content (e.g. not overwrite an arrowhead) should keep checking
// getCharAt() themselves before calling drawChar() directly, as
// PlayingSpaceWithParticipants's lifeline-fill pass currently does.
public void drawHLine(int xFrom, int xTo, int y) {
fillHLine(getHLineChar(), xFrom, xTo, y);
}
public void drawVLine(int yFrom, int yTo, int x) {
fillVLine(getVLineChar(), yFrom, yTo, x);
}
// Dashed variant, for dotted arrows (return messages, async replies...).
// Idea copied from the legacy asciiart.ComponentTextArrow.drawU(), which
// draws the full line then blanks every other cell when
// ArrowConfiguration.isDotted(); reimplemented here as "only draw every
// other cell" in the first place, same visual result, no code reused.
public void drawHLine(int xFrom, int xTo, int y, boolean dotted) {
if (dotted == false) {
drawHLine(xFrom, xTo, y);
return;
}
final char c = getHLineChar();
int idx = 0;
for (int i = xFrom; i <= xTo; i++, idx++)
if (idx % 2 == 0)
move(i, y).drawChar(c);
}
// Char-parameterized versions of the two above: used internally wherever
// a border needs a character other than the plain box/lifeline one
// (namely the note box's double lines in Unicode mode, see
// drawNoteBoxUnicode()/ANote). Package-private rather than private so
// that ANote (the AsciiBlock that owns the note box shape) can call them
// directly instead of every InfinitePlan drawing primitive needing a
// public wrapper.
void fillHLine(char c, int xFrom, int xTo, int y) {
for (int i = xFrom; i <= xTo; i++)
move(i, y).drawChar(c);
}
void fillVLine(char c, int yFrom, int yTo, int x) {
for (int j = yFrom; j <= yTo; j++)
move(x, j).drawChar(c);
}
// Public (not package-private): originally relaxed from `private` only for
// AGroupFrame's own custom-drawn top border (its bottom border and sides
// use different characters entirely — tildes and '!' — so it cannot reuse
// drawBox() wholesale). Widened further, from
// package-private to public, for CommunicationTileSelf (package
// sequencediagram.teoz, not asciiverse): a self-message's little loop is a
// small box with only two corners on the lifeline side, and needs the same
// Unicode-aware corner choice drawBox() already centralizes here — same
// principle as getHLineChar()/getVLineChar(): InfinitePlan owns every
// format-dependent character choice, callers never hardcode one.
public char getTopLeftChar() {
return isUnicode() ? '\u250c' : ','; // ┌ or ,
}
public char getTopRightChar() {
return isUnicode() ? '\u2510' : '.'; // ┐ or .
}
// Public, not package-private, for the same reason as
// getTopLeftChar()/getTopRightChar() above: originally relaxed for
// AGroupFrame's plain (non-tab) frame shape (the one a partition uses),
// widened for CommunicationTileSelf's loop-box corners.
public char getBottomLeftChar() {
return isUnicode() ? '\u2514' : '`'; // └ or `
}
public char getBottomRightChar() {
return isUnicode() ? '\u2518' : '\''; // ┘ or '
}
public void drawBox(int width, int height) {
if (width <= 0 || height <= 0)
throw new IllegalArgumentException();
// top border
drawChar(getTopLeftChar());
if (width > 2)
drawHLine(1, width - 2, 0);
if (width > 1)
move(width - 1, 0).drawChar(getTopRightChar());
// sides
if (height > 2) {
drawVLine(1, height - 2, 0);
if (width > 1)
drawVLine(1, height - 2, width - 1);
}
// bottom border
if (height > 1) {
move(0, height - 1).drawChar(getBottomLeftChar());
if (width > 2)
drawHLine(1, width - 2, height - 1);
if (width > 1)
move(width - 1, height - 1).drawChar(getBottomRightChar());
}
}
// Returns an AsciiBlock rather than drawing eagerly: the actual shape
// (ASCII dog-ear vs. Unicode double-line box) lives in ANote, so that the
// note box's drawing code sits next to the object that owns it instead of
// as anonymous classes here. ANote.asciiDraw(plan) is called later,
// against whatever InfinitePlan the caller has positioned at the block's
// top-left corner (see AsciiBlock.asciiDraw's contract) — isUnicode isn't
// even needed here: ANote reads it off that same plan when it draws,
// rather than being told at construction time (see ANote.asciiDraw()).
//
// The box is sized around `text` rather than taking explicit width/height:
// `text` is itself an AsciiBlock (e.g. a Display, which now prints its own
// lines — see Display.asciiDraw()), and ANote derives its own width/height
// from text.asciiDimension() internally — nothing to compute here.
public AsciiBlock createNoteBox(AsciiBlock text) {
return new ANote(text);
}
}