package org.geogebra.common.gui.view.spreadsheet;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.TreeSet;
import org.geogebra.common.awt.GPoint;
import org.geogebra.common.kernel.CircularDefinitionException;
import org.geogebra.common.kernel.Construction;
import org.geogebra.common.kernel.Kernel;
import org.geogebra.common.kernel.Locateable;
import org.geogebra.common.kernel.StringTemplate;
import org.geogebra.common.kernel.arithmetic.Traversing;
import org.geogebra.common.kernel.arithmetic.Traversing.SpreadsheetVariableRenamer;
import org.geogebra.common.kernel.arithmetic.ValidExpression;
import org.geogebra.common.kernel.commands.EvalInfo;
import org.geogebra.common.kernel.geos.GeoBoolean;
import org.geogebra.common.kernel.geos.GeoElement;
import org.geogebra.common.kernel.geos.GeoElementSpreadsheet;
import org.geogebra.common.kernel.geos.GeoFunction;
import org.geogebra.common.kernel.geos.GeoImage;
import org.geogebra.common.kernel.geos.GeoList;
import org.geogebra.common.kernel.geos.GeoNumeric;
import org.geogebra.common.kernel.geos.GeoText;
import org.geogebra.common.kernel.kernelND.GeoElementND;
import org.geogebra.common.kernel.kernelND.GeoPointND;
import org.geogebra.common.main.App;
import org.geogebra.common.main.SpreadsheetTableModel;
import org.geogebra.common.main.error.ErrorHandler;
import org.geogebra.common.main.error.ErrorHelper;
import org.geogebra.common.plugin.EventType;
import org.geogebra.common.util.AsyncOperation;
import org.geogebra.common.util.StringUtil;
import org.geogebra.common.util.debug.Log;
import org.geogebra.common.util.lang.Unicode;
import com.google.gwt.regexp.shared.MatchResult;
import com.google.gwt.regexp.shared.RegExp;
public class RelativeCopy {
protected Kernel kernel;
App app;
// protected MyTable table;
public RelativeCopy(Kernel kernel0) {
kernel = kernel0;
app = kernel.getApplication();
}
/**
* Performs spreadsheet drag-copy operation.
*
* @param sx1
* source minimum column
* @param sy1
* source minimum row
* @param sx2
* source maximum column
* @param sy2
* source maximum row
* @param dx1
* destination minimum column
* @param dy1
* destination minimum row
* @param dx2
* destination maximum column
* @param dy2
* destination maximum row
* @return
*/
public boolean doDragCopy(int sx1, int sy1, int sx2, int sy2, int dx1,
int dy1, int dx2, int dy2) {
// -|1|-
// 2|-|3
// -|4|-
app.setWaitCursor();
Construction cons = kernel.getConstruction();
try {
boolean success = false;
// collect all redefine operations
cons.startCollectingRedefineCalls();
boolean patternOK = isPatternSource(
new CellRange(app, sx1, sy1, sx2, sy2));
// ==============================================
// vertical drag
// ==============================================
if ((sx1 == dx1) && (sx2 == dx2)) {
if (dy2 < sy1) { // 1 ----- drag up
if (((sy1 + 1) == sy2) && patternOK) {
// two row source, so drag copy a linear pattern
for (int x = sx1; x <= sx2; ++x) {
GeoElement v1 = getValue(app, x, sy1);
GeoElement v2 = getValue(app, x, sy2);
if ((v1 == null) || (v2 == null)) {
continue;
}
for (int y = dy2; y >= dy1; --y) {
// quick solution: stop on fixed cell
// this may be improved later
GeoElement vOld = getValue(app, x, y);
if (vOld != null
&& vOld.isProtected(EventType.UPDATE)) {
break;
}
GeoElement v3 = getValue(app, x, y + 2);
GeoElement v4 = getValue(app, x, y + 1);
String vs1 = v3.isGeoFunction() ? "(x)" : "";
String vs2 = v4.isGeoFunction() ? "(x)" : "";
String d0 = GeoElementSpreadsheet
.getSpreadsheetCellName(x, y + 2) + vs1;
String d1 = GeoElementSpreadsheet
.getSpreadsheetCellName(x, y + 1) + vs2;
String text = "=CopyFreeObject[2*" + d1 + "-"
+ d0 + "]";
doCopyNoStoringUndoInfo1(kernel, app, text, v4,
x, y);
}
}
} else { // not two row source, so drag-copy the first row
// of the source
doCopyVerticalNoStoringUndoInfo1(sx1, sx2, sy1, dy1,
dy2);
}
success = true;
}
else if (dy1 > sy2) { // 4 ---- drag down
if (((sy1 + 1) == sy2) && patternOK) {
// two row source, so drag copy a linear pattern
for (int x = sx1; x <= sx2; ++x) {
GeoElement v1 = getValue(app, x, sy1);
GeoElement v2 = getValue(app, x, sy2);
if ((v1 == null) || (v2 == null)) {
continue;
}
for (int y = dy1; y <= dy2; ++y) {
// quick solution: stop on fixed cell
// this may be improved later
GeoElement vOld = getValue(app, x, y);
if (vOld != null
&& vOld.isProtected(EventType.UPDATE)) {
break;
}
GeoElement v3 = getValue(app, x, y - 2);
GeoElement v4 = getValue(app, x, y - 1);
String vs1 = v3.isGeoFunction() ? "(x)" : "";
String vs2 = v4.isGeoFunction() ? "(x)" : "";
String d0 = GeoElementSpreadsheet
.getSpreadsheetCellName(x, y - 2) + vs1;
String d1 = GeoElementSpreadsheet
.getSpreadsheetCellName(x, y - 1) + vs2;
String text = "=CopyFreeObject[2*" + d1 + "-"
+ d0 + "]";
doCopyNoStoringUndoInfo1(kernel, app, text, v4,
x, y);
}
}
} else {
// not two row source, so drag-copy the last row of the
// source
doCopyVerticalNoStoringUndoInfo1(sx1, sx2, sy2, dy1,
dy2);
}
success = true;
}
}
// ==============================================
// horizontal drag
// ==============================================
else if ((sy1 == dy1) && (sy2 == dy2)) {
if (dx2 < sx1) { // 2 ---- drag left
if (((sx1 + 1) == sx2) && patternOK) {
// two column source, so drag copy a linear pattern
for (int y = sy1; y <= sy2; ++y) {
GeoElement v1 = getValue(app, sx1, y);
GeoElement v2 = getValue(app, sx2, y);
if ((v1 == null) || (v2 == null)) {
continue;
}
for (int x = dx2; x >= dx1; --x) {
// quick solution: stop on fixed cell
// this may be improved later
GeoElement vOld = getValue(app, x, y);
if (vOld != null
&& vOld.isProtected(EventType.UPDATE)) {
break;
}
GeoElement v3 = getValue(app, x + 2, y);
GeoElement v4 = getValue(app, x + 1, y);
String vs1 = v3.isGeoFunction() ? "(x)" : "";
String vs2 = v4.isGeoFunction() ? "(x)" : "";
String d0 = GeoElementSpreadsheet
.getSpreadsheetCellName(x + 2, y) + vs1;
String d1 = GeoElementSpreadsheet
.getSpreadsheetCellName(x + 1, y) + vs2;
String text = "=CopyFreeObject[2*" + d1 + "-"
+ d0 + "]";
doCopyNoStoringUndoInfo1(kernel, app, text, v4,
x, y);
}
}
} else {
// not two column source, so drag-copy the first column
// of the source
doCopyHorizontalNoStoringUndoInfo1(sy1, sy2, sx1, dx1,
dx2);
}
success = true;
} else if (dx1 > sx2) { // 4 --- drag right
if (((sx1 + 1) == sx2) && patternOK) {
// two column source, so drag copy a linear pattern
for (int y = sy1; y <= sy2; ++y) {
GeoElement v1 = getValue(app, sx1, y);
GeoElement v2 = getValue(app, sx2, y);
if ((v1 == null) || (v2 == null)) {
continue;
}
for (int x = dx1; x <= dx2; ++x) {
// quick solution: stop on fixed cell
// this may be improved later
GeoElement vOld = getValue(app, x, y);
if (vOld != null
&& vOld.isProtected(EventType.UPDATE)) {
break;
}
GeoElement v3 = getValue(app, x - 2, y);
GeoElement v4 = getValue(app, x - 1, y);
String vs1 = v3.isGeoFunction() ? "(x)" : "";
String vs2 = v4.isGeoFunction() ? "(x)" : "";
String d0 = GeoElementSpreadsheet
.getSpreadsheetCellName(x - 2, y) + vs1;
String d1 = GeoElementSpreadsheet
.getSpreadsheetCellName(x - 1, y) + vs2;
String text = "=CopyFreeObject[2*" + d1 + "-"
+ d0 + "]";
doCopyNoStoringUndoInfo1(kernel, app, text, v4,
x, y);
}
}
} else {
// not two column source, so drag-copy the last column
// of the source
doCopyHorizontalNoStoringUndoInfo1(sy1, sy2, sx2, dx1,
dx2);
}
success = true;
}
}
// now do all redefining and build new construction
cons.processCollectedRedefineCalls();
if (success) {
return true;
}
String msg = "sx1 = " + sx1 + "\r\n" + "sy1 = " + sy1 + "\r\n"
+ "sx2 = " + sx2 + "\r\n" + "sy2 = " + sy2 + "\r\n"
+ "dx1 = " + dx1 + "\r\n" + "dy1 = " + dy1 + "\r\n"
+ "dx2 = " + dx2 + "\r\n" + "dy2 = " + dy2 + "\r\n";
throw new RuntimeException(
"Error from RelativeCopy.doCopy:\r\n" + msg);
} catch (Exception ex) {
// kernel.getApplication().showError(ex.getMessage());
ex.printStackTrace();
return false;
} finally {
cons.stopCollectingRedefineCalls();
app.setDefaultCursor();
}
}
/**
* Tests if a cell range can be used as the source for a pattern drag-copy.
*
* @param cellRange
* @return
*/
private static boolean isPatternSource(CellRange cellRange) {
// don't allow empty cells
if (cellRange.hasEmptyCells()) {
return false;
}
// test for any unacceptable geos in the range
ArrayList<GeoElement> list = cellRange.toGeoList();
for (GeoElement geo : list) {
if (!(geo.isGeoNumeric() || geo.isGeoFunction()
|| geo.isGeoPoint())) {
return false;
}
}
return true;
}
/**
* Performs a vertical spreadsheet drag-copy. Cells are copied vertically
* row by row using a single given row as the copy source.
*
* @param x1
* minimum column of the drag-copy region
* @param x2
* maximum column of the drag-copy region
* @param sy
* source row
* @param dy1
* destination minimum row
* @param dy2
* destination maximum row
* @throws Exception
*/
public void doCopyVerticalNoStoringUndoInfo1(int x1, int x2, int sy,
int dy1, int dy2) throws Exception {
// create a treeset, ordered by construction index
// so that when we relative copy A1=1 B1=(A1+C1)/2 C1=3
// B2 is done last
TreeSet<GeoElement> tree = new TreeSet<GeoElement>();
for (int x = x1; x <= x2; ++x) {
int ix = x - x1;
GeoElement cell = getValue(app, x1 + ix, sy);
if (cell != null) {
tree.add(cell);
}
}
for (int y = dy1; y <= dy2; ++y) {
int iy = y - dy1;
Iterator<GeoElement> iterator = tree.iterator();
while (iterator.hasNext()) {
GeoElement geo = (iterator.next());
if (geo != null) {
GPoint p = geo.getSpreadsheetCoords();
GeoElement vOld = getValue(app, p.x, dy1 + iy);
if (vOld != null && vOld.isProtected(EventType.UPDATE)) {
continue;
}
doCopyNoStoringUndoInfo0(kernel, app, geo,
getValue(app, p.x, dy1 + iy), 0, y - sy);
// Application.debug(p.x+"");
}
}
}
}
/**
* Performs a horizontal spreadsheet drag-copy. Cells are copied
* horizontally column by column using a single given column as the copy
* source.
*
* @param y1
* minimum row of the drag-copy region
* @param y2
* maximum row of the drag-copy region
* @param sx
* source column
* @param dx1
* destination minimum column
* @param dx2
* destination maximum column
* @throws Exception
*/
public void doCopyHorizontalNoStoringUndoInfo1(int y1, int y2, int sx,
int dx1, int dx2) throws Exception {
// create a treeset, ordered by construction index
// so that when we relative copy A1=1 A2=(A1+A3)/2 A3=3
// B2 is done last
TreeSet<GeoElement> tree = new TreeSet<GeoElement>();
for (int y = y1; y <= y2; ++y) {
int iy = y - y1;
GeoElement cell = getValue(app, sx, y1 + iy);
if (cell != null) {
tree.add(cell);
}
}
for (int x = dx1; x <= dx2; ++x) {
int ix = x - dx1;
Iterator<GeoElement> iterator = tree.iterator();
while (iterator.hasNext()) {
GeoElement geo = (iterator.next());
if (geo != null) {
GPoint p = geo.getSpreadsheetCoords();
GeoElement vOld = getValue(app, dx1 + ix, p.y);
if (vOld != null && vOld.isProtected(EventType.UPDATE)) {
continue;
}
doCopyNoStoringUndoInfo0(kernel, app, geo,
getValue(app, dx1 + ix, p.y), x - sx, 0);
// Application.debug(p.y+"");
}
}
}
}
protected static final RegExp pattern2 = RegExp
.compile("(::|\\$)([A-Z]+)(::|\\$)([0-9]+)");
public static GeoElementND doCopyNoStoringUndoInfo0(Kernel kernel, App app,
GeoElement value, GeoElementND oldValue, int dx, int dy)
throws Exception {
return doCopyNoStoringUndoInfo0(kernel, app, value, oldValue, dx, dy,
-1, -1);
}
public static GeoElementND doCopyNoStoringUndoInfo0(Kernel kernel, App app,
GeoElement value, GeoElementND oldValue, int dx, int dy,
int rowStart, int columnStart) throws Exception {
if (value == null) {
if (oldValue != null) {
MatchResult matcher = GeoElementSpreadsheet.spreadsheetPattern
.exec(oldValue
.getLabel(StringTemplate.defaultTemplate));
int column = GeoElementSpreadsheet
.getSpreadsheetColumn(matcher);
int row = GeoElementSpreadsheet.getSpreadsheetRow(matcher);
prepareAddingValueToTableNoStoringUndoInfo(kernel, app, null,
oldValue, column, row, true);
}
return null;
}
String text = null;
// make sure a/0.001 doesn't become a/0
StringTemplate highPrecision = StringTemplate.maxPrecision;
if (value.isPointOnPath() || value.isPointInRegion()) {
text = value.getDefinition(highPrecision);
} else if (value.isChangeable()) {
text = value.toValueString(highPrecision);
} else {
text = value.getDefinition(highPrecision);
}
// handle GeoText source value
if (value.isGeoText() && !((GeoText) value).isTextCommand()) {
// enclose text in quotes if we are copying an independent GeoText,
// e.g. "2+3"
if (value.isIndependent()) {
text = "\"" + text + "\"";
} else {
// check if 'text' parses to a GeoText
GeoText testGeoText = kernel.getAlgebraProcessor()
.evaluateToText(text, false, false);
// if it doesn't then force it to by adding +"" on the end
if (testGeoText == null) {
text = text + "+\"\"";
}
}
}
// for E1 = Polynomial[D1] we need value.getCommandDescription();
// even though it's a GeoFunction
if (value.isGeoFunction() && "".equals(text)) {
// we need the definition without A1(x)= on the front
text = ((GeoFunction) value).toSymbolicString(highPrecision);
}
boolean freeImage = false;
if (value.isGeoImage()) {
GeoImage image = (GeoImage) value;
if (image.getParentAlgorithm() == null) {
freeImage = true;
}
}
boolean oldFlag = kernel.isUsingInternalCommandNames();
kernel.setUseInternalCommandNames(true);
// FIXME maybe try-catch this?
ValidExpression exp = kernel.getParser().parseGeoGebraExpression(text);
kernel.setUseInternalCommandNames(oldFlag);
updateCellReferences(exp, dx, dy);
text = exp.toString(highPrecision);
// condition to show object
GeoBoolean bool = value.getShowObjectCondition();
String boolText = null, oldBoolText = null;
if (bool != null) {
if (bool.isChangeable()) {
oldBoolText = bool.toValueString(highPrecision);
} else {
oldBoolText = bool.getDefinition(highPrecision);
}
}
// dynamic color function
GeoList dynamicColorList = value.getColorFunction();
String colorText = null, oldColorText = null;
if (dynamicColorList != null) {
if (dynamicColorList.isChangeable()) {
oldColorText = dynamicColorList.toValueString(highPrecision);
} else {
oldColorText = dynamicColorList.getDefinition(highPrecision);
}
}
// allow pasting blank strings
if ("".equals(text)) {
text = "\"\"";
}
// make sure that non-GeoText elements are copied when the
// equalsRequired option is set
if (!value.isGeoText()
&& app.getSettings().getSpreadsheet().equalsRequired()) {
text = "=" + text;
}
// Application.debug("add text = " + text + ", name = " + (char)('A' +
// column + dx) + (row + dy + 1));
// get location of source cell
// TODO: Why not always use getSpreadsheetCoords()?
int row0 = rowStart;
int column0 = columnStart;
if (row0 > -1 && column0 > -1) {
// nothing to do, already set
} else if (value.isLabelSet()) {
MatchResult matcher = GeoElementSpreadsheet.spreadsheetPattern
.exec(value.getLabel(StringTemplate.defaultTemplate));
column0 = GeoElementSpreadsheet.getSpreadsheetColumn(matcher);
row0 = GeoElementSpreadsheet.getSpreadsheetRow(matcher);
} else if (value.getSpreadsheetCoords() != null) {
// the cell has been deleted but still exists in clipboard memory
column0 = value.getSpreadsheetCoords().x;
row0 = value.getSpreadsheetCoords().y;
}
// create the new cell geo
GeoElementND value2;
if (freeImage || value.isGeoButton()) {
value2 = value.copy();
if (oldValue != null) {
oldValue.remove();
}
// value2.setLabel(table.getModel().getColumnName(column0 + dx)
// + (row0 + dy + 1));
value2.setLabel(GeoElementSpreadsheet
.getSpreadsheetCellName(column0 + dx, row0 + dy));
value2.updateRepaint();
} else {
value2 = prepareAddingValueToTableNoStoringUndoInfo(kernel, app,
text, oldValue, column0 + dx, row0 + dy, true);
}
if (value2 == null) {
return null;
}
value2.setAllVisualProperties(value, false);
value2.setAuxiliaryObject(true);
String startPoints[] = null;
if (value instanceof Locateable) {
Locateable loc = (Locateable) value;
GeoPointND[] pts = loc.getStartPoints();
if (pts != null) {
startPoints = new String[pts.length];
for (int i = 0; i < pts.length; i++) {
startPoints[i] = ((GeoElement) pts[i])
.getLabel(highPrecision);
if (GeoElementSpreadsheet.spreadsheetPattern
.test(startPoints[i])) {
startPoints[i] = updateCellNameWithOffset(
startPoints[i], dx, dy);
}
}
}
}
if (oldBoolText != null) {
exp = kernel.getParser().parseGeoGebraExpression(oldBoolText);
updateCellReferences(exp, dx, dy);
boolText = exp.toString(StringTemplate.maxPrecision);
}
// attempt to set updated condition to show object (if it's changed)
if ((boolText != null)) {
// removed as doesn't work for eg "random()<0.5" #388
// && !boolText.equals(oldBoolText)) {
GeoBoolean newConditionToShowObject = kernel.getAlgebraProcessor()
.evaluateToBoolean(boolText, ErrorHelper.silent());
if (newConditionToShowObject != null) {
value2.setShowObjectCondition(newConditionToShowObject);
value2.update(); // needed to hide/show object as
// appropriate
} else {
return null;
}
}
if (oldColorText != null) {
exp = kernel.getParser().parseGeoGebraExpression(oldColorText);
updateCellReferences(exp, dx, dy);
colorText = exp.toString(StringTemplate.maxPrecision);
}
// copy the scripts from the old GeoElement
value2.setScripting(value);
// attempt to set updated dynamic color function (if it's changed)
if ((colorText != null)) {
// removed as doesn't work for eg "random()" #388
// && !colorText.equals(oldColorText)) {
try {
// Application.debug("new color function: "+colorText);
GeoList newColorFunction = kernel.getAlgebraProcessor()
.evaluateToList(colorText);
value2.setColorFunction(newColorFunction);
// value2.update();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
if (startPoints != null) {
for (int i = 0; i < startPoints.length; i++) {
((Locateable) value2).setStartPoint(kernel.getAlgebraProcessor()
.evaluateToPoint(startPoints[i],
app.getDefaultErrorHandler(), true),
i);
}
value2.update();
}
// Application.debug((row + dy) + "," + column);
// Application.debug("isGeoFunction()=" + value2.isGeoFunction());
// Application.debug("row0 ="+row0+" dy="+dy+" column0= "+column0+"
// dx="+dx);
return value2;
}
/**
* Updates the cell references in text according to a relative copy in the
* spreadsheet of offset (dx,dy) (changes only dependents of value) eg
* change A1 < 3 to A2 < 3 for a vertical copy
*/
private static void updateCellReferences(ValidExpression exp, int dx,
int dy) {
SpreadsheetVariableRenamer replacer = new Traversing.SpreadsheetVariableRenamer(
dx, dy);
exp.traverse(replacer);
}
public static String updateCellNameWithOffset(String name, int dx, int dy) {
MatchResult m = GeoElementSpreadsheet.spreadsheetPattern.exec(name);
// $ or ""
String m1 = m.getGroup(GeoElementSpreadsheet.MATCH_COLUMN_$);
// column eg A
String m2 = m.getGroup(GeoElementSpreadsheet.MATCH_COLUMN);
// $ or ""
String m3 = m.getGroup(GeoElementSpreadsheet.MATCH_ROW_$);
// row eg 23
String m4 = m.getGroup(GeoElementSpreadsheet.MATCH_ROW);
if ("".equals(m1)) {
int column = GeoElementSpreadsheet.getSpreadsheetColumn(m);
if (column > -1 && dx + column > 0) {
m2 = GeoElementSpreadsheet
.getSpreadsheetColumnName(dx + column);
}
}
if ("".equals(m3)) {
int row = GeoElementSpreadsheet.getSpreadsheetRow(m);
if (row > -1 && dy + row + 1 >= 1) {
m4 = "" + (dy + row + 1);
}
}
// preserve $ eg A$3 -> A$4
StringBuilder newName = new StringBuilder();
newName.append(m1);
newName.append(m2);
newName.append(m3);
newName.append(m4);
return newName.toString();
}
/**
* @param kernel
* kernel
* @param app
* application
* @param text
* definition text
* @param geoForStyle
* geo to be used for style of output
* @param column
* column
* @param row
* row
* @throws Exception
* if definition of new geo fails
*/
public static void doCopyNoStoringUndoInfo1(Kernel kernel, App app,
String text, GeoElement geoForStyle, int column, int row)
throws Exception {
GeoElement oldValue = getValue(app, column, row);
if (text == null) {
if (oldValue != null) {
prepareAddingValueToTableNoStoringUndoInfo(kernel, app, null,
oldValue, column, row, true);
}
return;
}
GeoElementND value2 = prepareAddingValueToTableNoStoringUndoInfo(kernel,
app, text, oldValue, column, row, true);
if (geoForStyle != null) {
value2.setVisualStyle(geoForStyle);
}
}
public static String replaceAll(RegExp spreadsheetpattern, String text,
String before, String after) {
StringBuilder pre = new StringBuilder();
String post = text;
int end = 0;
// Application.debug(text + " " + before + " " + after + " ");
MatchResult matcher;
do {
matcher = spreadsheetpattern.exec(post);
if (matcher != null) {
String s = matcher.getGroup(0);
// Application.debug("match: " + s);
if (s.equals(before)) {
int start = post.indexOf(s);
pre.append(post.substring(0, start));
pre.append(after); // replace 's' with 'after'
end = start + s.length();
post = post.substring(end);
} else {
int start = post.indexOf(s);
pre.append(post.substring(0, start));
pre.append(s); // leave 's' alone
end = start + s.length();
post = post.substring(end);
}
}
} while (matcher != null);
pre.append(post);
// Application.debug("returning: " + pre.toString());
return pre.toString();
}
/**
* Returns array of GeoElements that depend on given GeoElement geo
*
* @param geo
* @return
*/
public static GeoElement[] getDependentObjects(GeoElement geo) {
if (geo.isIndependent()) {
return new GeoElement[0];
}
TreeSet<GeoElement> geoTree = geo.getAllPredecessors();
return geoTree.toArray(new GeoElement[0]);
}
/**
* Returns 2D array, GeoElement[columns][rows], containing GeoElements found
* in the cell range with upper left corner (column1, row1) and lower right
* corner (column2, row2).
*
* @param app
* application
* @param column1
* start column
* @param row1
* start row
* @param column2
* end column
* @param row2
* end row
* @return array of geos in given range
*/
public static GeoElement[][] getValues(App app, int column1, int row1,
int column2, int row2) {
GeoElement[][] values = new GeoElement[(column2 - column1)
+ 1][(row2 - row1) + 1];
for (int r = row1; r <= row2; ++r) {
for (int c = column1; c <= column2; ++c) {
values[c - column1][r - row1] = getValue(app, c, r);
}
}
return values;
}
/**
* Returns the GeoElement for the cell with the given column and row values.
*/
/*
* public static GeoElement getValue(AbstractApplication app, int column,
* int row) {
*
* String cellName = GeoElementSpreadsheet.getSpreadsheetCellName(column,
* row);
*
* return app.getKernel().getConstruction().lookupLabel(cellName);
*
* }
*/
public static GeoElement getValue(App app, GPoint point) {
return getValue(app, point.getX(), point.getY());
}
/**
* Returns the GeoElement for the cell with the given column and row values.
*/
public static GeoElement getValue(App app, int column, int row) {
SpreadsheetTableModel tableModel = app.getSpreadsheetTableModel();
if ((row < 0) || (row >= tableModel.getRowCount())) {
return null;
}
if ((column < 0) || (column >= tableModel.getColumnCount())) {
return null;
}
return (GeoElement) tableModel.getValueAt(row, column);
}
// =========================================================================
// Cell Editing Methods
// =========================================================================
private static GeoElementND prepareNewValue(Kernel kernel, String name,
String inputText) throws Exception {
String text = inputText;
if (text == null) {
return null;
}
// remove leading equal sign, e.g. "= A1 + A2"
if (text.length() > 0 && text.charAt(0) == '=') {
text = text.substring(1);
}
text = text.trim();
// no equal sign in input
GeoElementND[] newValues = null;
try {
// check if input is same as name: circular definition
if (text.equals(name)) {
// circular definition
throw new CircularDefinitionException();
}
// evaluate input text without an error dialog in case of unquoted
// text
newValues = kernel.getAlgebraProcessor()
.processAlgebraCommandNoExceptionsOrErrors(text, false);
// check if text was the label of an existing geo
// toUpperCase() added to fix bug A1=1, enter just 'a1' or 'A1' into
// cell B1 -> A1 disappears
if (StringUtil.toLowerCase(text)
.equals(newValues[0]
.getLabel(StringTemplate.defaultTemplate))
// also need eg =a to work
|| text.equals(newValues[0]
.getLabel(StringTemplate.defaultTemplate))) {
// make sure we create a copy of this existing or auto-created
// geo
// by providing the new cell name in the beginning
text = name + " = " + text;
newValues = kernel.getAlgebraProcessor()
.processAlgebraCommandNoExceptions(text, false);
}
// check if name was auto-created: if yes we could have a circular
// definition
GeoElement autoCreateGeo = kernel.lookupLabel(name);
if (autoCreateGeo != null) {
// check for circular definition: if newValue depends on
// autoCreateGeo
boolean circularDefinition = false;
for (int i = 0; i < newValues.length; i++) {
if (newValues[i].isChildOf(autoCreateGeo)) {
circularDefinition = true;
break;
}
}
if (circularDefinition) {
// remove the auto-created object and the result
autoCreateGeo.remove();
newValues[0].remove();
// circular definition
throw new CircularDefinitionException();
}
}
for (int i = 0; i < newValues.length; i++) {
newValues[i].setAuxiliaryObject(true);
if (newValues[i].isGeoText()) {
newValues[i].setEuclidianVisible(false);
}
}
GeoElement.setLabels(name, newValues); // set names to be D1,
// E1,
// F1, etc for multiple
// objects
} catch (CircularDefinitionException ce) {
// circular definition
kernel.getApplication().showError("CircularDefinition");
return null;
} catch (Exception e) {
// create text if something went wrong
if (text.startsWith("\"")) {
text = text.substring(1, text.length() - 2);
}
text = "\"" + (text.replace("\"", "\"+UnicodeToLetter[34]+\""))
+ "\"";
newValues = kernel.getAlgebraProcessor()
.processAlgebraCommandNoExceptions(text, false);
newValues[0].setLabel(name);
newValues[0].setEuclidianVisible(false);
newValues[0].update();
}
return newValues[0];
}
private static void updateOldValue(final Kernel kernel,
final GeoElementND oldValue, String name, String text0,
final AsyncOperation<GeoElementND> callback) throws Exception {
String text = text0;
if (text.charAt(0) == '=') {
text = text.substring(1);
}
// always redefine objects in spreadsheet, don't store undo info
// here
EvalInfo info = new EvalInfo(
!kernel.getConstruction().isSuppressLabelsActive(), true);
kernel.getAlgebraProcessor().changeGeoElementNoExceptionHandling(
oldValue, text, info, false,
new AsyncOperation<GeoElementND>() {
@Override
public void callback(GeoElementND newValue) {
Log.debug("REDEFINED" + newValue);
// newValue.setConstructionDefaults();
newValue.setAllVisualProperties(oldValue.toGeoElement(),
true);
if (oldValue.isAuxiliaryObject()) {
newValue.setAuxiliaryObject(true);
}
// Application.debug("GeoClassType = " +
// newValue.getGeoClassType()+" " +
// newValue.getGeoClassType());
if (newValue.getGeoClassType() == oldValue
.getGeoClassType()) {
// newValue.setVisualStyle(oldValue);
} else {
kernel.getApplication().refreshViews();
}
callback.callback(newValue);
}
}, getErrorHandler(kernel, oldValue, name, text0, callback));
}
private static ErrorHandler getErrorHandler(final Kernel kernel,
final GeoElementND oldValue, final String name, final String text0,
final AsyncOperation<GeoElementND> callback) {
return new ErrorHandler() {
@Override
public void showError(String msg) {
Log.debug(msg);
if (kernel.getLocalization().getError("CircularDefinition")
.equals(msg)) {
kernel.getApplication().getDefaultErrorHandler()
.showError(msg);
} else {
handleThrowable();
}
}
@Override
public void resetError() {
showError(null);
}
public void handleThrowable() {
// if exception is thrown treat the input as text and try to
// update
// the cell as a GeoText
// reset the text string if old value is GeoText
if (oldValue.isGeoText()) {
((GeoText) oldValue).setTextString(text0);
oldValue.updateCascade();
}
// if not currently a GeoText and no children, redefine the cell
// as new GeoText
else if (!oldValue.hasChildren()) {
oldValue.remove();
GeoElementND newValue;
// add input as text
try {
newValue = prepareNewValue(kernel, name,
"\"" + text0 + "\"");
} catch (Throwable t) {
try {
newValue = prepareNewValue(kernel, name, "");
} catch (Throwable tt) {
newValue = new GeoNumeric(kernel.getConstruction(),
Double.NaN);
}
}
newValue.setEuclidianVisible(false);
newValue.update();
callback.callback(newValue);
}
// otherwise throw an exception and let the cell revert to the
// old value
else {
// throw new Exception(e);
}
}
@Override
public void showCommandError(String command, String message) {
handleThrowable();
}
@Override
public boolean onUndefinedVariables(String string,
AsyncOperation<String[]> callback1) {
return false;
}
@Override
public String getCurrentCommand() {
// TODO Auto-generated method stub
return null;
}
};
}
/**
* Prepares a spreadsheet cell editor string for processing in the kernel
* and returns either (1) a new GeoElement for the cell or (2) null.
*
* @param kernel
* kernel
* @param app
* application
* @param inputText
* string representation of the new GeoElement
* @param oldValue
* current cell GeoElement
* @param column
* cell column
* @param row
* cell row
* @param internal
* whether to force internal command names
* @return either (1) a new GeoElement for the cell or (2) null
* @throws Exception
*/
public static GeoElementND prepareAddingValueToTableNoStoringUndoInfo(
Kernel kernel, App app, String inputText, GeoElementND oldValue,
int column, int row, boolean internal) throws Exception {
String text = inputText;
// get the cell name
String name = GeoElementSpreadsheet.getSpreadsheetCellName(column, row);
// trim the text
if (text != null) {
text = text.trim();
if (text.length() == 0) {
text = null;
}
}
// if "=" is required before commands and text is not a number
// or does not begin with "=" then surround it with quotes.
// This will force the cell to become GeoText.
if (app.getSettings().getSpreadsheet().equalsRequired()
&& text != null) {
if (!((text.charAt(0) == '=') || isNumber(text))) {
text = "\"" + text + "\"";
}
}
// if the cell is currently GeoText then prepare it for changes
// make sure it can be changed to something else
// eg (2,3 can be overwritten as (2,3)
// if (oldValue != null && oldValue.isGeoText() &&
// !oldValue.hasChildren()) {
// oldValue.remove();
// oldValue = null;
// }
// if the text is null then remove the current cell geo and return null
if (text == null) {
if (oldValue != null) {
oldValue.remove();
}
return null;
// else if the target cell is empty, try to create a new GeoElement
// for this cell
}
boolean oldFlag = kernel.isUsingInternalCommandNames();
try {
// this will be a new geo
kernel.setUseInternalCommandNames(internal);
if (oldValue == null) {
GeoElementND ret = prepareNewValue(kernel, name, text);
kernel.setUseInternalCommandNames(oldFlag);
return ret;
}
updateOldValue(kernel, oldValue, name, text,
new AsyncOperation<GeoElementND>() {
@Override
public void callback(GeoElementND obj) {
redefinedElement = obj;
}
});
kernel.setUseInternalCommandNames(oldFlag);
return redefinedElement;
} catch (Throwable t) {
kernel.setUseInternalCommandNames(oldFlag);
return prepareNewValue(kernel, name, "");
}
}
private static GeoElementND redefinedElement;
/**
* Tests if a string represents a number.
*
* @param s
* @return true if the given string represents a number.
*/
public static boolean isNumber(String str) {
String s = str;
// trim and return false if empty string
s = s.trim();
if (s == null || s.length() == 0) {
return false;
}
// remove degree/% char from end of string
if (s.charAt(s.length() - 1) == Unicode.DEGREE_CHAR
|| s.charAt(s.length() - 1) == '%') {
s = s.substring(0, s.length() - 1);
}
// split the string using the exponentiation char
// and test for possible number strings
String[] s2 = s.split("E");
if (s2.length == 1) {
return isStandardNumber(s2[0]);
} else if (s2.length == 2) {
return isStandardNumber(s2[0]) && isStandardNumber(s2[1]);
} else {
return false;
}
}
/**
* Returns true if a string is a standard number, i.e not in scientific
* notation
*
* @param s
* @return
*/
private static boolean isStandardNumber(String s) {
// return if empty string
if (s == null || s.length() == 0) {
return false;
}
// test the first char for a digit, sign or decimal point.
Character c = s.charAt(0);
if (!(StringUtil.isDigit(c) || c == '.' || c == '-' || c == '+'
|| c == '\u2212')) {
return false;
}
// test the remaining chars for digits or decimal point
int decimalCount = 0;
for (int i = 1; i < s.length(); i++) {
c = s.charAt(i);
if (StringUtil.isDigit(c)) {
continue;
}
if (c == '.' && decimalCount == 0) {
decimalCount++;
} else {
return false;
}
}
return true;
}
}