/* GeoGebra - Dynamic Mathematics for Everyone http://www.geogebra.org This file is part of GeoGebra. 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. */ package org.geogebra.common.kernel; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.Set; import java.util.TreeSet; import org.geogebra.common.GeoGebraConstants; import org.geogebra.common.kernel.algos.AlgoElement; import org.geogebra.common.kernel.algos.AlgoMacroInterface; import org.geogebra.common.kernel.algos.ConstructionElement; import org.geogebra.common.kernel.geos.GeoElement; import org.geogebra.common.kernel.geos.GeoVector; import org.geogebra.common.kernel.geos.Test; import org.geogebra.common.kernel.kernelND.GeoPointND; import org.geogebra.common.main.MyError; import org.geogebra.common.util.StringUtil; import org.geogebra.common.util.debug.Log; /** * A macro is a user defined commmand. It has its own macro construction that is * used by all using AlgoMacro instances. * * @author Markus Hohenwarter */ public class Macro { private Kernel kernel; private String cmdName = "", toolName = "", toolHelp = ""; private String iconFileName = ""; // image file private boolean showInToolBar = true; private Construction macroCons; // macro construction private StringBuilder macroConsXML; private GeoElement[] macroInput, macroOutput; // input and output objects private String[] macroInputLabels, macroOutputLabels; private Test[] inputTypes; private Integer viewId = null; private LinkedList<AlgoElement> usingAlgos = new LinkedList<AlgoElement>(); /** * Creates a new macro using the given input and output GeoElements. * * @param kernel * Kernel * @param cmdName * Command name * @param input * Array of input objects * @param output * Array of output objects * @throws Exception * if macro initialization fails (unnecessary input, independent * output) */ public Macro(Kernel kernel, String cmdName, GeoElement[] input, GeoElement[] output) throws Exception { this(kernel, cmdName); initMacro(input, output); } /** * Creates a new macro. Note: you need to call initMacro() when using this * constructor. * * @param kernel * Kernel * @param cmdName * Command name */ public Macro(Kernel kernel, String cmdName) { this.kernel = kernel; setCommandName(cmdName); copyCaptions = true; } /** * Returns all input geos from the macro construction. * * @return all input geos from the macro construction. */ public GeoElement[] getMacroInput() { return macroInput; } /** * Returns kernel * * @return kernel */ public Kernel getKernel() { return kernel; } /** * Returns all output geos from the macro construction. * * @return Array of output elements */ public GeoElement[] getMacroOutput() { return macroOutput; } /** * Returns whether geo is part of this macro's construction. * * @param geo * Geo to be found in construction * @return true iff geo is part of this macro's construction. */ final public boolean isInMacroConstruction(GeoElement geo) { return geo.cons == macroCons; } /** * Returns the construction object of this macro. * * @return construction object of this macro. */ public Construction getMacroConstruction() { return macroCons; } /** * Initiates macro * * @param macroCons1 * macro construction * @param inputLabels * labels for input * @param outputLabels * labels for output */ public void initMacro(Construction macroCons1, String[] inputLabels, String[] outputLabels) { this.macroCons = macroCons1; this.macroConsXML = new StringBuilder(); macroCons.getConstructionXML(macroConsXML, false); this.macroInputLabels = inputLabels; this.macroOutputLabels = outputLabels; initInputOutput(); // init inputTypes array inputTypes = new Test[macroInput.length]; for (int i = 0; i < macroInput.length; i++) { inputTypes[i] = Test.getSpecificTest(macroInput[i]); } // after initing we turn global variable lookup on again, // so we can use for example functions with parameters in macros too. // Such parameters are global variables if (macroCons1 instanceof MacroConstruction) { ((MacroConstruction) macroCons1).setGlobalVariableLookup(true); } } private void initInputOutput() { // get the input and output geos from the macro construction macroInput = new GeoElement[macroInputLabels.length]; macroOutput = new GeoElement[macroOutputLabels.length]; for (int i = 0; i < macroInputLabels.length; i++) { macroInput[i] = macroCons.lookupLabel(macroInputLabels[i]); macroInput[i].setFixed(false); } for (int i = 0; i < macroOutputLabels.length; i++) { macroOutput[i] = macroCons.lookupLabel(macroOutputLabels[i]); } } private void initMacro(GeoElement[] input, GeoElement[] output) throws Exception { // check that every output object depends on an input object // and that all input objects are really needed for (int i = 0; i < output.length; i++) { boolean dependsOnInput = false; for (int k = 0; k < input.length; k++) { boolean dependencyFound = output[i].isChildOf(input[k]); if (dependencyFound) { dependsOnInput = true; } } if (!dependsOnInput) { throw new Exception(kernel.getApplication().getLocalization() .getError("Tool.OutputNotDependent") + ": " + output[i].getNameDescription()); } } // steps to create a macro // 1) outputAndParents = set of all predecessors of output objects // 2) inputChildren = set of all children of input objects // 3) macroElements = intersection of outputParents and inputChildren // 4) add input and output objects to macroElements // 5) create XML representation for macro-construction // 6) create a new macro-construction from this XML representation // 1) create the set of all parents of this macro's output objects TreeSet<GeoElement> outputParents = new TreeSet<GeoElement>(); for (int i = 0; i < output.length; i++) { output[i].addPredecessorsToSet(outputParents, false); // note: Locateables (like Texts, Images, Vectors) may depend on // points, // these points must be part of the macro construction if (output[i] instanceof Locateable) { Locateable loc = (Locateable) output[i]; GeoPointND[] points = loc.getStartPoints(); if (points != null) { for (int k = 0; k < points.length; k++) { outputParents.add((GeoElement) points[k]); ((GeoElement) points[k]) .addPredecessorsToSet(outputParents, false); } } } } // 2) and 3) get intersection of inputChildren and outputParents TreeSet<ConstructionElement> macroConsOrigElements = new TreeSet<ConstructionElement>(); TreeSet<Long> usedAlgoIds = new TreeSet<Long>(); Iterator<GeoElement> it = outputParents.iterator(); while (it.hasNext()) { GeoElement outputParent = it.next(); if (outputParent.isLabelSet()) { for (int i = 0; i < input.length; i++) { if (outputParent.isChildOf(input[i])) { addDependentElement(outputParent, macroConsOrigElements, usedAlgoIds); // add parent only once: get out of loop i = input.length; } } } } // 4) add input and output objects to macroElements // ensure that all input and all output objects have labels set // Note: we have to undo this at the end of this method !!! boolean[] isInputLabeled = new boolean[input.length]; boolean[] isOutputLabeled = new boolean[output.length]; String[] inputLabels = new String[input.length]; String[] outputLabels = new String[output.length]; GeoPointND[] startPoints = new GeoPointND[input.length]; for (int i = 0; i < input.length; i++) { isInputLabeled[i] = input[i].isLabelSet(); if (!isInputLabeled[i]) { input[i].setLabelSimple(input[i].getDefaultLabel()); input[i].setLabelSet(true); } if (input[i] instanceof GeoVector) { startPoints[i] = ((GeoVector) input[i]).getStartPoint(); ((GeoVector) input[i]).setStartPoint(null); } inputLabels[i] = input[i].getLabelSimple(); // add input element to macroConsOrigElements // we handle some special cases for input types like segment, // polygons, etc. switch (input[i].getGeoClassType()) { case SEGMENT: case RAY: case POLYGON: case FUNCTION: case INTERVAL: case POLYHEDRON: case CURVE_CARTESIAN:// needed for // http://www.geogebra.org/forum/viewtopic.php?t=7275 // add parent algo and its input objects to // macroConsOrigElements addSpecialInputElement(input[i], macroConsOrigElements); break; default: // add input element to macroConsOrigElements macroConsOrigElements.add(input[i]); // make sure we don't have any parent algorithms of input[i] in // our construction AlgoElement algo = input[i].getParentAlgorithm(); if (algo != null) { macroConsOrigElements.remove(algo); } } } for (int i = 0; i < output.length; i++) { isOutputLabeled[i] = output[i].isLabelSet(); if (!isOutputLabeled[i]) { output[i].setLabelSimple(output[i].getDefaultLabel()); output[i].setLabelSet(true); } outputLabels[i] = output[i].getLabelSimple(); // add output element and its algorithm to macroConsOrigElements addDependentElement(output[i], macroConsOrigElements, usedAlgoIds); } // 5) create XML representation for macro-construction macroConsXML = buildMacroXML( input.length == 0 ? kernel : input[0].kernel, macroConsOrigElements); // if we used temp labels in step (4) remove them again for (int i = 0; i < input.length; i++) { if (!isInputLabeled[i]) { input[i].setLabelSet(false); } if (input[i] instanceof GeoVector) { ((GeoVector) input[i]).setStartPoint(startPoints[i]); } } for (int i = 0; i < output.length; i++) { if (!isOutputLabeled[i]) { output[i].setLabelSet(false); } } Log.debug(macroConsXML); // 6) create a new macro-construction from this XML representation Construction macroCons2 = createMacroConstruction( macroConsXML.toString()); // init macro initMacro(macroCons2, inputLabels, outputLabels); } /** * Adds the geo, its parent algorithm and all its siblings to the * consElementSet and its id to used AlgoIds * * @param geo * Element to be added (with parent and siblings) * @param consElementSet * Set of geos & algos used in macro construction * @param usedAlgoIds * Set of IDs of algorithms used in macro construction */ public static void addDependentElement(GeoElement geo, Set<ConstructionElement> consElementSet, Set<Long> usedAlgoIds) { AlgoElement algo = geo.getParentAlgorithm(); if (algo.isInConstructionList()) { addDependentAlgo(algo, consElementSet, usedAlgoIds); } else { // HELPER algorithm, e.g. segment of polygon // we only add the geo because it is output // of some other algorithm in construction list consElementSet.add(geo); } } /** * Adds the geo, its parent algorithm and all its siblings to the * consElementSet and its id to used AlgoIds * * @param algo * Element to be added * @param consElementSet * Set of geos & algos used in macro construction * @param usedAlgoIds * Set of IDs of algorithms used in macro construction */ public static void addDependentAlgo(AlgoElement algo, Set<ConstructionElement> consElementSet, Set<Long> usedAlgoIds) { // STANDARD case // add algorithm Long algoID = Long.valueOf(algo.getID()); if (!usedAlgoIds.contains(algoID)) { consElementSet.add(algo); } usedAlgoIds.add(algoID); // add all output elements including geo GeoElement[] algoOutput = algo.getOutput(); for (int i = 0; i < algoOutput.length; i++) { consElementSet.add(algoOutput[i]); } } /** * Adds the geo, its parent algorithm and all input of the parent algorithm * to the consElementSet. This is used for e.g. a segment that is used as an * input object of a macro. We also need to have the segment's start and * endpoint. * * @param geo * special element * @param consElementSet * set to add this element elements */ public static void addSpecialInputElement(GeoElement geo, Set<ConstructionElement> consElementSet) { // add geo consElementSet.add(geo); // add parent algo and input objects AlgoElement algo = geo.getParentAlgorithm(); if (algo != null && algo.isInConstructionList()) { // STANDARD case // add algorithm consElementSet.add(algo); // add all output elements including geo GeoElement[] algoInput = algo.getInput(); for (int i = 0; i < algoInput.length; i++) { if (algoInput[i].isLabelSet()) { consElementSet.add(algoInput[i]); } } } } /** * Note: changes macroConsElements * * @param kernel * Kernel * @param macroConsElements * elements involved in macro (input, internal, output) * @return XML string of macro construction */ public static StringBuilder buildMacroXML(Kernel kernel, Set<ConstructionElement> macroConsElements) { // get the XML for all macro construction elements StringBuilder macroConsXML = new StringBuilder(500); macroConsXML.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); macroConsXML.append("<geogebra format=\"" + GeoGebraConstants.XML_FILE_FORMAT + "\">\n"); macroConsXML .append("<construction author=\"\" title=\"\" date=\"\">\n"); Iterator<ConstructionElement> it = macroConsElements.iterator(); while (it.hasNext()) { ConstructionElement ce = it.next(); if (ce.isGeoElement()) { ce.getXML(false, macroConsXML); } else if (ce.isAlgoElement()) { AlgoElement algo = (AlgoElement) ce; algo.getXML(macroConsXML, false); } } macroConsXML.append("</construction>\n"); macroConsXML.append("</geogebra>"); return macroConsXML; } /** * Creates a macro construction from a given xml string. The names of the * input and output objects within this construction are given by * inputLabels and outputLabels * * @param macroXML */ private Construction createMacroConstruction(String macroConstructionXML) throws Exception { // build macro construction MacroKernel mk = kernel.newMacroKernel(); mk.setContinuous(false); // during initing we turn global variable lookup off, so we can be sure // that the macro construction only dependes on it's input mk.setGlobalVariableLookup(false); try { mk.loadXML(macroConstructionXML); } catch (MyError e) { String msg = e.getLocalizedMessage(); Log.debug(msg); e.printStackTrace(); throw new Exception(msg); } catch (Exception e) { e.printStackTrace(); throw new Exception(e.getMessage()); } return mk.getConstruction(); } /** * Add link to algo using this macro * * @param algoMacro * macro algorithm */ public void registerAlgorithm(AlgoMacroInterface algoMacro) { usingAlgos.add((AlgoElement) algoMacro); } /** * Remove link to algo using this macro * * @param algoMacro * macro algorithm */ public void unregisterAlgorithm(AlgoMacroInterface algoMacro) { usingAlgos.remove(algoMacro); } /** * Returns whether this macro is being used by algorithms in the current * construction. * * @return true iff this macro is being used by algorithms in the current * construction */ final public boolean isUsed() { return usingAlgos.size() > 0; } /** * Removes links to all algos using this macro */ final public void setUnused() { usingAlgos.clear(); } /** * Returns the types of input objects of the default macro construction. * This can be used to check whether a given GeoElement array can be used as * input for this macro. * * @return types of input objects */ final public Test[] getInputTypes() { return inputTypes; } /** * Returns the tool help * * @return tool help */ public String getToolHelp() { if (toolHelp == null || "".equals(toolHelp)) { return toString(); } return toolHelp; } /** * Returns a String showing all needed types of this macro. eg [ <Text>, * <Number> ] * * @return string showing all needed types of this macro. */ public String getNeededTypesString() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < macroInput.length; ++i) { sb.append(macroInput[i].translatedTypeString()); if (i != macroInput.length - 1) { sb.append(", "); } } return sb.toString(); } /** * Sets tool help. * * @param toolHelp * Tool help. Either "","null" or null for empty. */ public void setToolHelp(String toolHelp) { if (toolHelp == null || "null".equals(toolHelp)) { this.toolHelp = ""; } else { this.toolHelp = toolHelp; } } /** * Returns command name * * @return Command name */ public String getCommandName() { return cmdName; } /** * Sets commandd name * * @param name * Command name */ public void setCommandName(String name) { if (name != null) { this.cmdName = name; } } /** * Returns tool name * * @return Tool name */ public String getToolName() { return toolName; } /** * Returns toolname, if empty, returns command name. * * @return Toolname, if empty, returns command name. */ public String getToolOrCommandName() { if (!"".equals(toolName)) { return toolName; } return cmdName; } /** * Sets tool name * * @param name * new tool name */ public void setToolName(String name) { if (name == null || "null".equals(name) || name.length() == 0) { this.toolName = cmdName; } else { this.toolName = name; } } /** * Sets icon filename * * @param name * Icon filename, "" or null for empty */ public void setIconFileName(String name) { if (name == null) { this.iconFileName = ""; } else { this.iconFileName = name; } } /** * Returns icon filename * * @return icon filename */ public String getIconFileName() { return iconFileName; } /** * Returns the syntax descriptiont of this macro. */ @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(cmdName); sb.append("[ "); // input types sb.append('<'); sb.append(macroInput[0].translatedTypeString()); sb.append('>'); for (int i = 1; i < macroInput.length; ++i) { sb.append(", "); sb.append('<'); sb.append(macroInput[i].translatedTypeString()); sb.append('>'); } sb.append(" ]"); return sb.toString(); } /** * Adds XML representation of this macro for saving in a ggb file to given * string builder. * * @param sb * StringBuilder for adding the macro representation */ public void getXML(StringBuilder sb) { sb.append("<macro cmdName=\""); StringUtil.encodeXML(sb, cmdName); sb.append("\" toolName=\""); StringUtil.encodeXML(sb, toolName); sb.append("\" toolHelp=\""); StringUtil.encodeXML(sb, toolHelp); sb.append("\" iconFile=\""); StringUtil.encodeXML(sb, iconFileName); sb.append("\" showInToolBar=\""); sb.append(showInToolBar); sb.append("\" copyCaptions=\""); sb.append(copyCaptions); if (viewId != null) { sb.append("\" viewId=\""); sb.append(viewId); } sb.append("\">\n"); // add input labels sb.append("<macroInput"); for (int i = 0; i < macroInputLabels.length; i++) { // attribute name is input no. sb.append(" a"); sb.append(i); sb.append("=\""); StringUtil.encodeXML(sb, macroInputLabels[i]); sb.append("\""); } sb.append("/>\n"); // add output labels sb.append("<macroOutput"); for (int i = 0; i < macroOutputLabels.length; i++) { // attribute name is output no. sb.append(" a"); sb.append(i); sb.append("=\""); StringUtil.encodeXML(sb, macroOutputLabels[i]); sb.append("\""); } sb.append("/>\n"); // macro construction XML if (macroConsXML != null && macroConsXML.length() > 0) { sb.append(macroConsXML.toString()); } else { macroCons.getConstructionXML(sb, false); } sb.append("</macro>\n"); } /** * Returns whether this macro should be shown in toolbar * * @return true iff this macro should be shown in toolbar */ public final boolean isShowInToolBar() { return showInToolBar; } /** * Sets whether this macro should be shown in toolbar * * @param showInToolBar * true iff this macro should be shown in toolbar */ public final void setShowInToolBar(boolean showInToolBar) { this.showInToolBar = showInToolBar; } /** * Returns list of macros used by this one * * @return list of macros used by this one */ public ArrayList<Macro> getUsedMacros() { return macroCons.getUsedMacros(); } /** * Returns list of geos created using this macro * * @return list of geos created using this macro */ public ArrayList<GeoElement> getDependentGeos() { ArrayList<GeoElement> geos = new ArrayList<GeoElement>(); Iterator<AlgoElement> curr = usingAlgos.iterator(); while (curr.hasNext()) { AlgoElement algo = curr.next(); // seek for the first visible geo GeoElement geo = algo.getOutput(0); while (!geo.isLabelSet() && geo.getAllChildren().size() > 0) { geo = geo.getAllChildren().first(); } // add that geo and its siblings algo = geo.getParentAlgorithm(); for (int i = 0; i < algo.getOutputLength(); i++) { geos.add(algo.getOutput(i)); } } return geos; } private boolean copyCaptions; /** * Set whether the macro should copy captions of resulting objects * * @param copyCaptions * true to copy */ public void setCopyCaptionsAndVisibility(boolean copyCaptions) { this.copyCaptions = copyCaptions; } /** * @return true if the macro copies captions of resulting objects */ public boolean isCopyCaptionsAndVisibility() { return copyCaptions; } /** * @return to which view's toolbar this belongs */ public Integer getViewId() { return viewId; } /** * @param viewId * to which view's toolbar this belongs */ public void setViewId(Integer viewId) { this.viewId = viewId; } }