/*
* Copyright (C) 2011 Alasdair C. Hamilton
*
* This program 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.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package ket; // Move to ket.math?
import java.io.*;
import java.util.Vector;
import ket.math.*;
import ket.math.convert.*;
import ket.math.purpose.Text;
import ket.display.*;
import ketUI.Ket;
/**
* There are various levels of abstraction between low level graphical and
* keyboard interactions and the high level, abstract mathematical model. This
* class provides a boundary between the two providing practical tools for
* loading and saving equations, but also provides an abstract equation model
* as a graph of lists of equations. Additionally, searching and substitution
* methods are provided.
*/
public class MathCollection implements SelectionState {
final CursorSelection cursorSelection;
final RangeSelection rangeSelection;
final ColumnSelection columnSelection;
final Drag drag;
Selection selection;
Selection past;
Address from;
Address to;
final KnownArguments knownArguments;
// If this is null then the value has changed and so no longer needs to
// be compared.
Integer fileHashCode;
Undo undo;
/**
* Check to see if the equation list has been changed since the last save.
*/
public boolean containsUnsavedChanges() {
if (fileHashCode==null) {
return true;
}
int currentHashCode = getEquationList().hashCode();
if (fileHashCode.intValue()!=currentHashCode) {
// Set the original value to null to avoid having to
// check every time a new change is made. This also
// avoids unlikely but possible hashCode coincidences.
fileHashCode = null;
return true;
} else {
return false;
}
}
public int hashCode() {
return getEquationList().hashCode();
}
public KnownArguments getKnownArguments() {
return knownArguments;
}
/////////////
// FILE IO //
/////////////
/**
* Given the location in which a file is stored, read in and parse a
* given text file and append each equation to this list. The current
* element is set as the last equation to be loaded.
* The program handles exceptions by aborting, displaying an
* error message to standard output and returning false.
* @return Returns <I>true</I> if the program successfully opened and
* read to the end of the file. No equations need be read. In
* contrast, if the file is missing or cannot be opened or read from,
* then exceptions are caught and <I>false</I> is instead returned.
*/
public boolean read(String filename) {
try {
readStream(new FileReader(filename));
return true;
} catch (IOException e) {
Ket.out.println(" !!! Failure to read file: " + filename + ". !!!");
Ket.out.println("exception: " + e);
e.printStackTrace();
return false;
}
}
private void readStream(Reader reader) throws IOException {
EquationList equationList = getEquationList();
BufferedReader bufferedReader = new BufferedReader(reader);
Vector<Argument> rows = new Vector<Argument>();
String line = bufferedReader.readLine();
while (line!=null) {
line = readAppendedLines(bufferedReader, line);
if (line.trim().indexOf('\t')!=-1) { // A table of tab-separated values
Ket.out.println("<row>");
Ket.out.println("tsv: " + line);
String[] elms = line.trim().split("\t");
Branch matrix = new Branch(Function.MATRIX);
Ket.out.println("<LOOP>");
for (String element : elms) {
Ket.out.print("\t\telement = '" + element + "'");
Ket.out.println("<PARSE>");
Argument next = ArgumentParser.parseArgument(element, knownArguments, null, this);
Ket.out.println("</PARSE>");
Ket.out.print(" -> ");
Ket.out.println(next);
if (next!=null) {
matrix.append(next);
}
}
Ket.out.println("</LOOP>");
Ket.out.println("\trow: " + matrix);
rows.add(matrix);
Ket.out.println("</row>");
} else {
appendTable(rows);
Equation equation = processLine(line);
equationList.addLast(equation);
setCurrent(equation.getRoot());
}
line = bufferedReader.readLine();
}
appendTable(rows);
fileHashCode = new Integer(equationList.hashCode());
undo = new Undo(equationList);
bufferedReader.close();
}
/**
* Append an existing table, if complete.
*/
private void appendTable(Vector<Argument> rows) {
if (rows==null || rows.size()==0) return;
Equation equation = new Equation(new Branch(Function.VECTOR, rows));
getEquationList().addLast(equation);
setCurrent(equation.getRoot());
rows.clear();
}
private boolean isWrappedLine(String line) {
int len = line.length();
return len>0 && line.charAt(len-1)=='\\';
}
/**
* Keep reading from the buffer reader while the lines are wrapped,
* that is they end with a backslash. The resulting line (without the
* backslash) is then returned.
*/
private String readAppendedLines(BufferedReader bufferedReader, String line) throws IOException {
if (!isWrappedLine(line)) {
return line;
}
int len = line.length();
line = len>1 ? line.substring(0, len-1) : "";
String next = "";
while (true) {
next = bufferedReader.readLine();
if (next==null) {
return line;
} else if (!isWrappedLine(next)) {
return line + next;
} else if (next.length()>1) {
line += next.substring(0, next.length()-1);
}
}
}
/**
* Template files are stored within Ket.jar. Note that all filenames
* should be absolute paths relative to the root of the jar file!
*/
public boolean readJar(String filename) { // TODO: Move to a more general location such as Ket.
// Note: Jar filenames always start with a slash.
try {
InputStream inputStream = MathCollection.class.getResourceAsStream(filename);
if (inputStream!=null) {
readStream(new InputStreamReader(inputStream));
return true;
} else {
Ket.out.println(" !!! File not found within Jar !!! ");
Ket.out.println(filename);
return false;
}
} catch (IOException e) {
Ket.out.println(" !!! Failure to read jar file: '" + filename + "'. !!!");
Ket.out.println("exception: " + e);
e.printStackTrace();
return false;
}
}
/**
* Process the next line from a text file and add it to the mathList as
* an equation or as a line of plain text.
*/
public Equation processLine(String line) {
// An equation is denoted by a tab or at least for spaces of indentation.
boolean equationLike = line.matches("[ {4}\t].*");
// Labels follow a # symbol.
int labelIndex = line.lastIndexOf('#');
if (labelIndex!=-1) { // The line ends in a label.
String content = line.substring(0, labelIndex).trim();
String label = line.substring(labelIndex+1).trim();
Argument root = getRoot(content, equationLike);
return new Equation(root, label);
} else { // The line is not labelled.
Argument root = getRoot(line, equationLike);
return new Equation(root);
}
}
private Argument getRoot(String content, boolean equationLike) {
if (equationLike && !content.matches("\\s*")) {
return ArgumentParser.parseArgument(content, knownArguments, null, this);
}
String text = content.trim();
if (text.matches("\\*.+\\*")) { // *heading* or *a longer title*
text = text.substring(1, text.length()-1); // ish
Token token = new Token(new Text(text));
token.setBold(true);
return token;
} else {
return new Token(new Text(text));
}
}
private boolean saveRoot(Equation equation, BufferedWriter bufferedWriter) throws IOException {
Argument root = equation.getRoot();
Argument[][] table = null;
if (root instanceof Branch) {
Vector<Argument> kids = root.asBranch().getChildren();
if (root.getFunction()==Function.VECTOR) {
table = ArgumentTools.asTable(kids, false);
} else if (root.getFunction()==Function.MATRIX) {
table = ArgumentTools.asTable(kids, true);
}
}
if (table!=null) {
// A table
for (Argument[] row : table) {
for (Argument element : row) {
bufferedWriter.write("\t" + element);
}
bufferedWriter.write("\n");
}
return true;
} else {
// A regular equation.
bufferedWriter.write("" + equation.toString() + "\n");
return false;
}
}
/**
* Save text to a given filename, returning true if the file was saved correctly.
*/
public boolean writeText(String filename) {
try {
FileWriter fileWriter = new FileWriter(filename, false);
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
for (Equation equation : getEquationList().getEquations()) {
saveRoot(equation, bufferedWriter);
}
bufferedWriter.close();
fileHashCode = new Integer(getEquationList().hashCode());
return true;
} catch (IOException e) {
Ket.out.println(" !!! Exception thrown during saving to file !!! ");
Ket.out.println(e);
e.printStackTrace();
return false;
}
}
/**
* Save text to a given filename, returning true if the file was saved correctly.
*/
public boolean writeLatex(String filename) {
try {
FileWriter fileWriter = new FileWriter(filename, false);
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
bufferedWriter.write(LatexTools.LATEX_FILE_START);
boolean wasPlainText = false;
for (Equation equation : getEquationList().getEquations()) {
if (wasPlainText && equation.isPlainText()) {
// Add a blank line between lines of
// text which are assumed to represent
// paragraphs.
bufferedWriter.write("\n");
}
bufferedWriter.write(equation.toLatex() + "\n");
wasPlainText = equation.isPlainText();
}
bufferedWriter.write(LatexTools.LATEX_FILE_END);
bufferedWriter.close();
return true;
} catch (IOException e) {
Ket.out.println(" !!! Exception thrown during saving to file !!! ");
Ket.out.println(e);
e.printStackTrace();
return false;
}
}
/**
* Save text to a given filename, returning true if the file was saved correctly.
*/
public boolean writeClojure(String filename) {
try {
FileWriter fileWriter = new FileWriter(filename, false);
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
for (Equation equation : getEquationList().getEquations()) {
bufferedWriter.write(equation.toPrefixNotation()+"\n");
}
bufferedWriter.close();
return true;
} catch (IOException e) {
Ket.out.println(" !!! Exception thrown during saving to file !!! ");
Ket.out.println(e);
e.printStackTrace();
return false;
}
}
/**
* Save text to a given filename, returning true if the file was saved correctly.
*/
public boolean writeHTML(String filename) {
try {
FileWriter fileWriter = new FileWriter(filename, false);
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
bufferedWriter.write(HTMLTools.HTML_FILE_START);
boolean wasPlainText = false;
for (Equation equation : getEquationList().getEquations()) {
if (wasPlainText && equation.isPlainText()) {
// Only add a blank line between paragraphs.
bufferedWriter.write("</P>\n");
bufferedWriter.write("\n");
bufferedWriter.write("<P>\n");
}
bufferedWriter.write(equation.toHTML());
wasPlainText = equation.isPlainText();
}
bufferedWriter.write(HTMLTools.HTML_FILE_END);
bufferedWriter.close();
return true;
} catch (IOException e) {
Ket.out.println(" !!! Exception thrown during saving to file !!! ");
Ket.out.println(e);
e.printStackTrace();
return false;
}
}
// ---------------- SELECTION RELATED STUFF ----------------
public MathCollection(KnownArguments knownArguments) {
this.knownArguments = knownArguments;
cursorSelection = new CursorSelection(this); // This implicitly creates a new equation list.
rangeSelection = new RangeSelection(cursorSelection);
columnSelection = new ColumnSelection(cursorSelection);
selection = cursorSelection;
fileHashCode = new Integer(-1); // Default to 'has unsaved changes'.
undo = new Undo();
drag = new Drag(cursorSelection);
}
public Drag getDrag() {
return drag;
}
public EquationList getEquationList() {
return selection.getEquationList();
}
public Undo getUndo() {
return undo;
}
public boolean undoStackIsUpToDate() {
return undo.isUnchanged(selection.getEquationList());
}
public void updateUndoStack() {
Address cursor = getCurrent().getAddress();
undo.recordChange(selection.getEquationList(), cursor);
}
public boolean undo() {
//- int index = getCurrent().getEquationIndex();
EquationList el = getEquationList();
Address address = undo.undo(el);
if (address==null) {
Ket.out.println(" !!! NULL UNDO CURSOR LOCATION !!! ");
return false;
}
Argument current = el.relativeAddress(address);
if (current==null) {
int equationIndex = bound(0, address.getEquationIndex(), el.size()-1);
selection.setCurrent(el.getEquation(equationIndex).getRoot());
return true;
}
selection.setCurrent(current);
return true;
}
private int bound(int from, int index, int to) {
if (index<from) {
return from;
} else if (to<index) {
return to;
} else {
return index;
}
}
public boolean redo() {
EquationList el = getEquationList();
Address address = undo.redo(el);
if (address==null) {
Ket.out.println(" !!! NULL REDO CURSOR LOCATION !!! ");
return false;
}
Argument current = el.relativeAddress(address);
selection.setCurrent(current!=null ? current : el.getEquation(0).getRoot());
return false; // Return type: fail or unchanged?
}
public void setColumnSelection() {
past = columnSelection;
selection = columnSelection;
columnSelection.selectFromCurrent();
from = selection.getCursor().getAddress();
}
public void setRangeSelection() {
past = rangeSelection;
selection = rangeSelection;
rangeSelection.selectFromCurrent();
from = selection.getCursor().getAddress();
}
public void setCursorSelection() {
selection = cursorSelection;
to = selection.getCursor().getAddress();
}
private Argument getAddress(Address address) { // TODO: Use in undo.
if (address==null) {
return null;
}
return getEquationList().relativeAddress(address);
}
public void reselection() {
if (to==null || from==null) {
Ket.out.println(" !!! No region !!! ");
} else if (past==rangeSelection) {
setCurrent(getAddress(from));
selection = rangeSelection;
rangeSelection.selectFromCurrent();
setCurrent(getAddress(to));
} else if (past==columnSelection) {
setCurrent(getAddress(from));
selection = columnSelection;
columnSelection.selectFromCurrent();
setCurrent(getAddress(to));
} else {
Ket.out.println(" !!! No known past !!! ");
}
}
public void toggleColumnSelection() {
if (isSingleSelectionSet()) {
setColumnSelection();
} else {
setCursorSelection();
}
}
public void toggleRangeSelection() {
if (isSingleSelectionSet()) {
setRangeSelection();
} else {
setCursorSelection();
}
}
//- public void reselect() {
//- }
public boolean isSingleSelectionSet() {
return selection==cursorSelection;
}
public boolean isRangeSelectionSet() {
return selection==rangeSelection;
}
public boolean isColumnSelectionSet() {
return selection==columnSelection;
}
public Selection getSelection() {
return selection;
}
public Cursor getCursor() {
return selection.getCursor();
}
public CursorSelection getCursorSelection() {
return cursorSelection;
}
public RangeSelection getRangeSelection() {
return rangeSelection;
}
/**
* Provides access to information about the current selection without the ability to change anything.
*/
public Highlight getHighlight() {
return getSelection();
}
private Argument getCurrent() {
return selection.getCurrent();
}
private void setCurrent(Argument argument) {
selection.setCurrent(argument);
}
}