package org.geogebra.common.main; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Map.Entry; import java.util.TreeSet; import org.geogebra.common.gui.view.spreadsheet.CopyPasteCut; import org.geogebra.common.gui.view.spreadsheet.MyTableInterface; import org.geogebra.common.gui.view.spreadsheet.RelativeCopy; import org.geogebra.common.kernel.Construction; import org.geogebra.common.kernel.Kernel; import org.geogebra.common.kernel.geos.GeoAngle; import org.geogebra.common.kernel.geos.GeoElement; import org.geogebra.common.kernel.geos.GeoElementSpreadsheet; 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.geos.SpreadsheetTraceable; import org.geogebra.common.plugin.GeoClass; import org.geogebra.common.util.SpreadsheetTraceSettings; import org.geogebra.common.util.debug.Log; /** * This class manages tracing of GeoElements to the spreadsheet. A trace is a * spreadsheet cell, or set of cells in the same row, that holds the numeric * value(s) of a GeoElement. * * Specifically, the class: * * (1) maintains TraceGeoCollection, a hash table that matches all GeoElements * that trace to the spreadsheet with their trace settings * * (2) creates and updates spreadsheet cell traces based on type of geo (e.g. * angles, polar points, lists) * * (3) determines the column/row location for a geo trace * * * @author G. Sturr 2010-4-22 * */ public class SpreadsheetTraceManager { // external components private App app; private Kernel kernel; // collection of all geos currently traced protected HashMap<GeoElement, SpreadsheetTraceSettings> traceGeoCollection; // temporary collection of trace geos, held during construction updates private HashSet<GeoElement> storedTraces; private boolean collectingTraces = false; // misc variables private boolean doShiftCellsUp = true; public SpreadsheetTraceManager(App app) { this.app = app; kernel = app.getKernel(); traceGeoCollection = new HashMap<GeoElement, SpreadsheetTraceSettings>(); storedTraces = new HashSet<GeoElement>(); } // ============================================= // Add/Remove Geo Trace // ============================================= /** Add a geo to the trace collection */ public void addSpreadsheetTraceGeo(GeoElement spreadsheetTraceable) { SpreadsheetTraceSettings t = spreadsheetTraceable.getTraceSettings(); Construction cons = app.getKernel().getConstruction(); // Set trace columns if (t.traceColumn1 == -1) { t.traceColumn1 = getNextTraceColumn(); } if (t.doTraceGeoCopy) { t.traceColumn2 = t.traceColumn1; } else { ArrayList<GeoNumeric> traceList = new ArrayList<GeoNumeric>(); spreadsheetTraceable.addToSpreadsheetTraceList(traceList); t.traceColumn2 = t.traceColumn1 + traceList.size() - 1; } // Set trace rows if (t.traceRow1 == -1) { t.traceRow1 = 0; // default to first row } t.headerOffset = 0; if (t.showLabel) { ++t.headerOffset; } if (t.showTraceList) { ++t.headerOffset; } if (t.doRowLimit) { t.traceRow2 = t.traceRow1 + t.numRows - 1 + t.headerOffset; } else { t.traceRow2 = app.getMaxSpreadsheetRowsVisible(); } t.tracingRow = t.traceRow1; t.lastTrace.clear(); // add the geo and its settings to the hash table traceGeoCollection.put(spreadsheetTraceable, t); // set the tracing flag for this geo spreadsheetTraceable.setSpreadsheetTrace(true); // clear the trace columns and put the current trace into the // spreadsheet clearGeoTraceColumns(spreadsheetTraceable); if (t.showTraceList) { for (int column = t.traceColumn1; column <= t.traceColumn2; column++) { createTraceListCell(cons, column, t.traceRow1); } } // traceToSpreadsheet(geo); setHeader(spreadsheetTraceable); app.repaintSpreadsheet(); } public void updateTraceSettings(GeoElement geo) { if (geo == null) { return; } SpreadsheetTraceSettings t = geo.getTraceSettings(); // clearGeoTraceColumns(geo); CopyPasteCut.delete(app, t.traceColumn1, t.traceRow1, t.traceColumn2, app.getMaxSpreadsheetRowsVisible(), MyTableInterface.CELL_SELECT); addSpreadsheetTraceGeo(geo); } /** * clear geo trace for the geo * * @param geo */ public void clearGeoTrace(GeoElement geo) { if (geo == null) { return; } SpreadsheetTraceSettings t = geo.getTraceSettings(); // prevent removing headings clearGeoTraceColumns(geo, true); // reset the trace lists for (int column = t.traceColumn1; column <= t.traceColumn2; column++) { clearTraceListCell(column, t.traceRow1); } // reset the tracing row t.tracingRow = t.traceRow1; } /** Remove a geo from the trace collection */ public void removeSpreadsheetTraceGeo(GeoElement geo) { if (!traceGeoCollection.containsKey(geo)) { return; } traceGeoCollection.remove(geo); app.repaintSpreadsheet(); if (app.has(Feature.IMPROVE_CONTEXT_MENU)) { geo.setSpreadsheetTrace(false); } } /** Remove all geos from the trace collection. */ public void removeAllSpreadsheetTraceGeos() { /* * for(GeoElement geo:traceGeoCollection.keySet()){ * geo.setSpreadsheetTrace(false); } */ traceGeoCollection.clear(); app.repaintSpreadsheet(); // Application.printStacktrace("remove all traces "); } /** * Load all tracing geos into the TraceGeoCollection. Called by * SpreadsheetView after undo or load file. */ public void loadTraceGeoCollection() { // Application.debug("loading trace geos"); traceGeoCollection.clear(); TreeSet<GeoElement> ts = app.getKernel().getConstruction() .getGeoSetConstructionOrder(); for (GeoElement geo : ts) { if (geo.getSpreadsheetTrace()) { traceGeoCollection.put(geo, geo.getTraceSettings()); // System.out.println("load this geo: " + geo.toString()); } } app.repaintSpreadsheet(); } // ============================================= // Utility Methods // ============================================= public int getNextTraceColumn() { return Math.max(app.getSpreadsheetTableModel().getHighestUsedColumn(), getHighestTraceColumn()) + 1; } private int getHighestTraceColumn() { int max = -1; for (GeoElement geo : traceGeoCollection.keySet()) { if (geo.getTraceSettings().traceColumn2 > max) { max = geo.getTraceSettings().traceColumn2; } } return max; } public boolean isTraceGeo(GeoElement geo) { return traceGeoCollection.containsKey(geo); } public boolean isTraceColumn(int column) { SpreadsheetTraceSettings t; for (Entry<GeoElement, SpreadsheetTraceSettings> entry : traceGeoCollection .entrySet()) { t = entry.getValue(); if (column >= t.traceColumn1 && column <= t.traceColumn2) { return true; } } return false; } /** * * @param column * column spreadsheet * @return trace settings for the column */ public SpreadsheetTraceSettings getTraceSettings(int column) { SpreadsheetTraceSettings t; for (Entry<GeoElement, SpreadsheetTraceSettings> entry : traceGeoCollection .entrySet()) { t = entry.getValue(); if (column >= t.traceColumn1 && column <= t.traceColumn2) { return t; } } return null; } public ArrayList<GeoElement> getTraceGeoList() { ArrayList<GeoElement> traceGeoList = new ArrayList<GeoElement>(); for (GeoElement geo : traceGeoCollection.keySet()) { traceGeoList.add(geo); } return traceGeoList; } public GeoElement getTraceGeo(int column) { SpreadsheetTraceSettings t; for (Entry<GeoElement, SpreadsheetTraceSettings> entry : traceGeoCollection .entrySet()) { GeoElement geo = entry.getKey(); t = entry.getValue(); if (column >= t.traceColumn1 && column <= t.traceColumn2) { return geo; } } return null; } /** * toggle trace geo in the column paused/recorded * * @param column * spreadsheet column */ public void togglePauseTraceGeo(int column) { SpreadsheetTraceSettings t; for (Entry<GeoElement, SpreadsheetTraceSettings> entry : traceGeoCollection .entrySet()) { GeoElement geo = entry.getKey(); t = entry.getValue(); if (column >= t.traceColumn1 && column <= t.traceColumn2) { togglePauseTraceGeo(geo, t); return; } } } private void togglePauseTraceGeo(GeoElement geo, SpreadsheetTraceSettings t) { t.pause = !t.pause; if (!t.pause) { traceToSpreadsheet(geo, t); } } /** * set all record to spreadsheet pause/run * * @param pause * true to pause, false to run */ public void pauseAllTraces(boolean pause) { SpreadsheetTraceSettings t; for (Entry<GeoElement, SpreadsheetTraceSettings> entry : traceGeoCollection .entrySet()) { GeoElement geo = entry.getKey(); t = entry.getValue(); t.pause = pause; if (!t.pause) { traceToSpreadsheet(geo, t); } } } /** * * @return first trace geo */ public GeoElement getFirstTraceGeo() { if (traceGeoCollection.isEmpty()) { return null; } return (GeoElement) traceGeoCollection.keySet().toArray()[0]; } public SpreadsheetTraceSettings getTraceSettings(GeoElement geo) { return traceGeoCollection.get(geo); } /** * Delete the elements in the trace columns of a single geo. */ public void clearGeoTraceColumns(GeoElement geo) { clearGeoTraceColumns(geo, false); } /** * Delete the elements in the trace columns of a single geo. * * @param geo * geo * @param keepHeader * say if headers have to be removed or not */ public void clearGeoTraceColumns(GeoElement geo, boolean keepHeader) { SpreadsheetTraceSettings t = geo.getTraceSettings(); if (t == null) { return; } int row1 = t.traceRow1; if (keepHeader) { row1 += t.headerOffset; } CopyPasteCut.delete(app, t.traceColumn1, row1, t.traceColumn2, app.getMaxSpreadsheetRowsVisible(), MyTableInterface.CELL_SELECT); // t.tracingRow = t.traceRow1; // t.lastTrace.clear(); } public void handleColumnDelete(int column1, int column2) { SpreadsheetTraceSettings t; for (GeoElement geo : traceGeoCollection.keySet()) { t = geo.getTraceSettings(); if (column2 >= t.traceColumn1 && column1 <= t.traceColumn2) { setHeader(geo); t.tracingRow = 0; } } app.repaintSpreadsheet(); } public void handleColumnDelete(int column1, int row1, int column2, int row2) { SpreadsheetTraceSettings t; for (GeoElement geo : traceGeoCollection.keySet()) { t = geo.getTraceSettings(); if (column2 >= t.traceColumn1 && column1 <= t.traceColumn2) { // re create header if needed and if more than headers deleted if (row1 < row2 && row1 < t.headerOffset) { setHeader(geo); } // restart from last deleted row if (row2 >= t.tracingRow - 1 + t.headerOffset) { t.tracingRow = row1 - t.headerOffset; if (t.tracingRow <= 0) { t.tracingRow = 0; } else { // check empty rows boolean emptyCells = true; int row = t.tracingRow + t.headerOffset; do { row--; int col = t.traceColumn1; do { GeoElement cell = RelativeCopy.getValue(app, col, row); if (cell != null) { emptyCells = false; } col++; } while (emptyCells && col <= t.traceColumn2); } while (emptyCells && row >= t.headerOffset); t.tracingRow = row + 1 - t.headerOffset; } } } } app.repaintSpreadsheet(); } public SpreadsheetTraceSettings getDefaultTraceSettings() { return new SpreadsheetTraceSettings(); } public void setNeedsColumnReset(GeoElement geo, boolean flag) { if (!traceGeoCollection.containsKey(geo)) { return; } traceGeoCollection.get(geo).needsColumnReset = flag; } // ============================================= // XML // ============================================= public String getTraceXML(GeoElement geo) { SpreadsheetTraceSettings t = geo.getTraceSettings(); StringBuilder sb = new StringBuilder(); sb.append("\t<spreadsheetTrace val=\"true\""); sb.append(" traceColumn1=\""); sb.append(t.traceColumn1); sb.append("\""); sb.append(" traceColumn2=\""); sb.append(t.traceColumn2); sb.append("\""); sb.append(" traceRow1=\""); sb.append(t.traceRow1); sb.append("\""); sb.append(" traceRow2=\""); sb.append(t.traceRow2); sb.append("\""); sb.append(" tracingRow=\""); sb.append(t.tracingRow); sb.append("\""); sb.append(" numRows=\""); sb.append(t.numRows); sb.append("\""); sb.append(" headerOffset=\""); sb.append(t.headerOffset); sb.append("\""); sb.append(" doColumnReset=\""); sb.append(t.doColumnReset ? "true" : "false"); sb.append("\""); sb.append(" doRowLimit=\""); sb.append(t.doRowLimit ? "true" : "false"); sb.append("\""); sb.append(" showLabel=\""); sb.append(t.showLabel ? "true" : "false"); sb.append("\""); sb.append(" showTraceList=\""); sb.append(t.showTraceList ? "true" : "false"); sb.append("\""); sb.append(" doTraceGeoCopy=\""); sb.append(t.doTraceGeoCopy ? "true" : "false"); sb.append("\""); if (t.pause) { sb.append(" pause=\"true\""); } sb.append("/>\n"); /* * this param is not included: * * public ArrayList<Double> lastTrace = new ArrayList<Double>(); * * do we need it? */ return sb.toString(); } // ============================================= // Tracing // ============================================= /** * Turns on the collectingTraces flag and clears the storedTraces collection * so that trace geos will be recorded but not traced while a construction * updates. */ public void startCollectingSpreadsheetTraces() { collectingTraces = true; storedTraces.clear(); } /** * Turns off the collectingTraces flag after a construction has updated and * then calls traceToSpreadsheet to trace all geos that have been put on * hold during the update. */ public void stopCollectingSpreadsheetTraces() { collectingTraces = false; for (GeoElement geo : storedTraces) { traceToSpreadsheet(geo); } storedTraces.clear(); } /** * Trace the current value(s) of a geo into the spreadsheet. * * This will either create new cells to display trace values for the geo or * update old trace cells with new trace values. Placement in the * spreadsheet is determined by geo.traceSettings. * * Traces are displayed according to geo type. An optional header cell of * type GeoText can be placed in the first tracing row. This displays the * geo name, or the name of each element in a GeoList. * */ public void traceToSpreadsheet(GeoElement geo) { // stop spurious numbers after undo if (kernel.isViewReiniting()) { return; } if (!traceGeoCollection.containsKey(geo)) { return; } SpreadsheetTraceSettings t = traceGeoCollection.get(geo); if (t.pause) { // trace is paused return; } traceToSpreadsheet(geo, t); } private void traceToSpreadsheet(GeoElement geo, SpreadsheetTraceSettings t) { Construction cons = app.getKernel().getConstruction(); // TODO // 1) test if equals is working here // 2) add code to override this test if grouping of trace elements is // allowed // 3) improve selection rectangle handling // get the current trace for this geo // TRACE ALSO IF EQUAL TO LAST TRACE if (!t.doTraceGeoCopy) { getCurrentTrace(geo, t.lastTrace); } // if only collecting traces, then record this geo for later tracing and // exit. if (collectingTraces) { storedTraces.add(geo); return; } // handle column reset if (t.needsColumnReset && t.doColumnReset) { t.traceColumn1 = getNextTraceColumn(); t.tracingRow = t.traceRow1; t.needsColumnReset = false; } // allow autoscrolling to keep the current trace in view app.setScrollToShow(true); // set the headers /* * if (t.tracingRow == t.traceRow1 && t.headerOffset > 0) { * setHeader(geo, cons); } */ // 'row' is the temporary row counter actually used for creating traces. // 't.tracingRow' is the row counter kept in memory. // When row size is limited then tracingRow = -1 serves as a flag to // keep our row counter from going past traceRow2 int row = t.tracingRow + t.headerOffset; if (t.tracingRow == -1) { row = t.traceRow2; } // add geo traces if we are NOT in the last row if (t.tracingRow != -1) { setGeoTraceRow(geo, cons, t.lastTrace, row); } // if in the last row, shift cells up and put new trace in last row else if (doShiftCellsUp) { GeoElement sourceCell; // GeoElement targetCell; int minTraceRow = t.traceRow1 + t.headerOffset + 1; if (t.numRows == 1) { --minTraceRow; } for (int c = t.traceColumn1; c <= t.traceColumn2; c++) { for (int r = minTraceRow; r <= t.traceRow2; r++) { // get the source cell sourceCell = RelativeCopy.getValue(app, c, r); // copy the value from the source cell into the target cell // below // (don't do this if there is only one row) if (t.numRows > 1) { if (sourceCell != null) { if (t.doTraceGeoCopy) { setTraceCellAsGeoCopy(sourceCell, c, r - 1); } else { setTraceCell(cons, c, r - 1, ((GeoNumeric) sourceCell).getValue(), GeoClass.NUMERIC); } } } // if the source cell is in the last row, update it with a // new trace if (r == t.traceRow2) { if (t.doTraceGeoCopy) { setTraceCellAsGeoCopy(geo, c, r); } else { setTraceCell(cons, c, r, t.lastTrace.get(c - t.traceColumn1), GeoClass.NUMERIC); } } } } } // draw the selection rectangle around the last trace row // table.setSelectionRectangle(new CellRange(table.app,t.traceColumn1, // row, // t.traceColumn2, row)); // update geo.traceRow counter t.tracingRow = (row < t.traceRow2) ? t.tracingRow + 1 : -1; // update trace lists if (t.showTraceList) { int traceIndex = 0; for (int column = t.traceColumn1; column <= t.traceColumn2; column++) { updateTraceListCell(cons, geo, column, t.traceRow1, t.lastTrace, traceIndex); ++traceIndex; } } app.setScrollToShow(false); } /** Create a row of trace cell(s) in the trace column(s) of a geo. */ protected boolean setGeoTraceRow(GeoElement geo, Construction cons, ArrayList<Double> traceArray0, int row) { SpreadsheetTraceSettings t = traceGeoCollection.get(geo); int column = t.traceColumn1; int traceIndex = 0; GeoElement[] geos = geo.getGeoElements(); if (t.doTraceGeoCopy) { setTraceCellAsGeoCopy(geo, t.traceColumn1, row); return true; } // handle null trace (when shifting cells a null trace is sometimes // needed) ArrayList<Double> traceArray = traceArray0; if (traceArray == null) { traceArray = new ArrayList<Double>(); traceArray.add(Double.NaN); traceArray.add(Double.NaN); } // trace ArrayList<GeoNumeric> traceList = new ArrayList<GeoNumeric>(); for (int i = 0; i < geos.length; i++) { if (geos[i] instanceof SpreadsheetTraceable) { // AbstractApplication.debug("SpreadsheetTraceable"); SpreadsheetTraceable traceGeo = (SpreadsheetTraceable) geos[i]; traceList.clear(); traceGeo.addToSpreadsheetTraceList(traceList); for (int j = 0; j < traceList.size(); j++) { setTraceCell(cons, column, row, traceArray.get(traceIndex), traceList.get(j).isGeoAngle() ? GeoClass.ANGLE : GeoClass.NUMERIC); ++column; ++traceIndex; } } else { Log.warn("not SpreadsheetTraceable " + geos[i].getGeoClassType()); } } return false; } protected final void setTraceCellAsGeoCopy(GeoElement geo, int column, int row) { GeoElement cell = RelativeCopy.getValue(app, column, row); try { if (cell == null || !cell.getGeoClassType().equals(geo.getGeoClassType())) { // cell is null or type has changed: deep copy cell = geo.deepCopyGeo(); cell.setLabel(GeoElementSpreadsheet .getSpreadsheetCellName(column, row)); cell.setAllVisualProperties(geo, true); cell.setSpreadsheetTrace(false); cell.setTraceSettings(null); cell.setAuxiliaryObject(true); cell.setLabelVisible(false); if (cell.isGeoText()) { cell.setEuclidianVisible(false); } } else { // just copy - so children are not removed // properties not changed cell.set(geo); } cell.updateCascade(); } catch (Exception e) { e.printStackTrace(); } } protected void setTraceCell(Construction cons, int column, int row, Object value, GeoClass geoClassType) { GeoElement cell = RelativeCopy.getValue(app, column, row); boolean isUpdateCell = cell != null && cell.getGeoClassType().equals(geoClassType); if (isUpdateCell) { switch (geoClassType) { default: // do nothing break; case NUMERIC: ((GeoNumeric) cell).setValue((Double) value); break; case ANGLE: ((GeoAngle) cell).setValue((Double) value); break; case TEXT: ((GeoText) cell).setTextString((String) value); break; } cell.updateCascade(); } else { // delete old cell geo if (cell != null) { CopyPasteCut.delete(app, column, row, column, row, MyTableInterface.CELL_SELECT); } String cellName = GeoElementSpreadsheet .getSpreadsheetCellName(column, row); switch (geoClassType) { default: // do nothing break; case NUMERIC: cell = new GeoNumeric(cons, (Double) value); break; case ANGLE: cell = new GeoAngle(cons, (Double) value); break; case TEXT: cell = new GeoText(cons, (String) value); break; } cell.setLabel(cellName); cell.setEuclidianVisible(false); } cell.setAuxiliaryObject(true); // cell.updateCascade(); } // ====================================== // List Tracing private void createTraceListCell(Construction cons, int column, int row) { GeoElement cell = RelativeCopy.getValue(app, column, row); if (cell != null) { CopyPasteCut.delete(app, column, row, column, row, MyTableInterface.CELL_SELECT); } try { cell = new GeoList(cons); putCell(cell, column, row); cell.setEuclidianVisible(false); cell.setAuxiliaryObject(true); cell.updateCascade(); } catch (Exception e) { e.printStackTrace(); } } private static void putCell(GeoElement cell, int column, int row) { String label = GeoElementSpreadsheet.getSpreadsheetCellName(column, row); GeoElement old = cell.getKernel().lookupLabel(label); if (old != null) { old.remove(); } cell.setLabel(label); } private void updateTraceListCell(Construction cons, GeoElement geo, int column, int row, ArrayList<Double> values, int index) { GeoElement cell = RelativeCopy.getValue(app, column, row); if (cell == null || !cell.isGeoList()) { return; } if (geo.getTraceSettings().doTraceGeoCopy) { // add a copy of the trace ((GeoList) cell).add(geo.copyInternal(cons)); } else { // add the numeric value of the trace ((GeoList) cell).add(new GeoNumeric(cons, values.get(index))); } cell.updateCascade(); } private void clearTraceListCell(int column, int row) { GeoElement cell = RelativeCopy.getValue(app, column, row); if (cell == null || !cell.isGeoList()) { return; } // clear the list ((GeoList) cell).clear(); // update dependent objects cell.updateCascade(); } // End List Tracing // ====================================== private void getCurrentTrace(GeoElement geo, ArrayList<Double> trace) { trace.clear(); if (geo.isGeoList()) { for (int elem = 0; elem < ((GeoList) geo).size(); elem++) { addElementTrace(((GeoList) geo).get(elem), trace); } } else { addElementTrace(geo, trace); } } protected boolean addElementTrace(GeoElement geo, ArrayList<Double> currentTrace) { if (geo instanceof SpreadsheetTraceable) { SpreadsheetTraceable traceGeo = (SpreadsheetTraceable) geo; ArrayList<GeoNumeric> traceList = new ArrayList<GeoNumeric>(); traceGeo.addToSpreadsheetTraceList(traceList); for (int i = 0; i < traceList.size(); i++) { currentTrace.add(traceList.get(i).getValue()); } } return false; } /** Create header cell(s) for each trace column of a geo. */ private void setHeader(GeoElement geo) { SpreadsheetTraceSettings t = traceGeoCollection.get(geo); int column, row; GeoText headerText = null; GeoElement[] geos = geo.getGeoElements(); if (t.showLabel) { row = t.traceRow1 + t.headerOffset - 1; column = t.traceColumn1; for (int i = 0; i < geos.length; i++) { if (geos[i] instanceof SpreadsheetTraceable) { ArrayList<GeoText> strings = ((SpreadsheetTraceable) geos[i]) .getColumnHeadings(); for (int j = 0; j < strings.size(); j++) { headerText = strings.get(j); putCell(headerText, column, row); column++; } } } } } /** * * @return true if at least one geo is traced */ public boolean hasGeoTraced() { return !traceGeoCollection.isEmpty(); } }