InfiniteString.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;

// A single "infinite" line of characters, extending both to the right
// (position >= 0, backed by positiveChars) and to the left (position < 0,
// backed by negativeChars). This mirrors InfinitePlan itself, whose move()
// can shift dx/dy negative (e.g. a note box drawn to the left of the
// leftmost participant): a single line must be able
// to grow in either direction from an arbitrary starting position, not just
// rightward from 0.
//
// negativeChars stores position -1 at index 0, position -2 at index 1, and
// so on (index = -position - 1): appending to this StringBuilder always
// extends further left, away from zero, exactly the way appending to
// positiveChars always extends further right. The two halves never need to
// "meet in the middle" or resize each other; position 0 itself always lives
// in positiveChars.
public class InfiniteString {

	private final StringBuilder positiveChars = new StringBuilder();
	private final StringBuilder negativeChars = new StringBuilder();

	private void ensureLength(int length) {
		while (positiveChars.length() < length)
			positiveChars.append(' ');

	}

	private void ensureNegativeLength(int length) {
		while (negativeChars.length() < length)
			negativeChars.append(' ');

	}

	public void setCharAt(int position, char ch) {
		if (position >= 0) {
			ensureLength(position + 1);
			positiveChars.setCharAt(position, ch);
		} else {
			final int negIndex = -position - 1;
			ensureNegativeLength(negIndex + 1);
			negativeChars.setCharAt(negIndex, ch);
		}
	}

	public void setStringAt(int position, String s) {
		// Delegates to setCharAt() one character at a time rather than
		// pre-sizing positiveChars/negativeChars in bulk: a string can start
		// negative and cross into positive territory (or vice versa) in a
		// single call, and reusing setCharAt() keeps that split in exactly
		// one place instead of duplicating the position >= 0 test here.
		for (int i = 0; i < s.length(); i++)
			setCharAt(position + i, s.charAt(i));
	}

	public char getCharAt(int position) {
		if (position >= 0) {
			if (position >= positiveChars.length())
				return ' ';

			return positiveChars.charAt(position);
		}

		final int negIndex = -position - 1;
		if (negIndex >= negativeChars.length())
			return ' ';

		return negativeChars.charAt(negIndex);
	}

	// The leftmost position ever written to on this line, as a value <= 0:
	// -negativeChars.length() (0 if setCharAt()/setStringAt() never touched a
	// negative position). Lets a caller that renders many lines together
	// (InfinitePlan.exportTxt()) find the leftmost column across *all* of them
	// before picking a single common startingPosition to pass to getString()
	// — needed because each InfiniteString only knows its own extent, not its
	// neighbors', and every line must be rendered from the same column for the
	// output to stay aligned.
	public int getLeftmostPosition() {
		return -negativeChars.length();
	}

	// Renders the line from startingPosition (inclusive) to the end of
	// positiveChars. startingPosition may be negative: the caller (typically
	// InfinitePlan.exportTxt(), once it tracks the leftmost column used
	// across every line) decides how far left the export should reach, and
	// any position not actually written to (either because this particular
	// line never drew that far left, or because startingPosition reaches
	// further left than any line did) reads back as a space, same as
	// getCharAt() does.
	public String getString(int startingPosition) {
		if (startingPosition >= 0)
			return positiveChars.substring(startingPosition);

		final int negLength = -startingPosition;
		final StringBuilder result = new StringBuilder(negLength + positiveChars.length());
		// Walk from the leftmost requested position (highest negIndex) up to
		// -1 (negIndex 0), i.e. left to right, matching reading order.
		for (int negIndex = negLength - 1; negIndex >= 0; negIndex--)
			result.append(negIndex < negativeChars.length() ? negativeChars.charAt(negIndex) : ' ');

		result.append(positiveChars);
		return result.toString();
	}
}