diff options
Diffstat (limited to 'src/de/lmu/ifi/dbs/elki/visualization/svg')
15 files changed, 1423 insertions, 241 deletions
diff --git a/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGButton.java b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGButton.java new file mode 100644 index 00000000..9d71c3d6 --- /dev/null +++ b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGButton.java @@ -0,0 +1,165 @@ +package de.lmu.ifi.dbs.elki.visualization.svg; + +/* + This file is part of ELKI: + Environment for Developing KDD-Applications Supported by Index-Structures + + Copyright (C) 2012 + Ludwig-Maximilians-Universität München + Lehr- und Forschungseinheit für Datenbanksysteme + ELKI Development Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; + +/** + * Class to draw a button as SVG. + * + * @author Erich Schubert + */ +public class SVGButton { + /** + * Default button color + */ + public static final String DEFAULT_BUTTON_COLOR = SVGConstants.CSS_LIGHTGRAY_VALUE; + + /** + * Default text color + */ + public static final String DEFAULT_TEXT_COLOR = SVGConstants.CSS_BLACK_VALUE; + + /** + * X position + */ + private double x; + + /** + * Y position + */ + private double y; + + /** + * Width + */ + private double w; + + /** + * Height + */ + private double h; + + /** + * Corner rounding factor. NaN = no rounding + */ + private double r = Double.NaN; + + /** + * Class for the buttons main CSS + */ + private CSSClass butcss; + + /** + * Button title, optional + */ + private String title = null; + + /** + * Title styling + */ + private CSSClass titlecss = null; + + /** + * Constructor. + * + * @param x Position X + * @param y Position Y + * @param w Width + * @param h Height + * @param r Rounded radius + */ + public SVGButton(double x, double y, double w, double h, double r) { + super(); + this.x = x; + this.y = y; + this.w = w; + this.h = h; + this.r = r; + this.butcss = new CSSClass(this, "button"); + butcss.setStatement(SVGConstants.CSS_FILL_PROPERTY, DEFAULT_BUTTON_COLOR); + butcss.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_BLACK_VALUE); + butcss.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, ".01"); + } + + /** + * Produce the actual SVG elements for the button. + * + * @param svgp Plot to draw to + * @return Button wrapper element + */ + public Element render(SVGPlot svgp) { + Element tag = svgp.svgElement(SVGConstants.SVG_G_TAG); + Element button = svgp.svgRect(x, y, w, h); + if(!Double.isNaN(r)) { + SVGUtil.setAtt(button, SVGConstants.SVG_RX_ATTRIBUTE, r); + SVGUtil.setAtt(button, SVGConstants.SVG_RY_ATTRIBUTE, r); + } + SVGUtil.setAtt(button, SVGConstants.SVG_STYLE_ATTRIBUTE, butcss.inlineCSS()); + tag.appendChild(button); + // Add light effect: + if (svgp.getIdElement(SVGEffects.LIGHT_GRADIENT_ID) != null) { + Element light = svgp.svgRect(x, y, w, h); + if(!Double.isNaN(r)) { + SVGUtil.setAtt(light, SVGConstants.SVG_RX_ATTRIBUTE, r); + SVGUtil.setAtt(light, SVGConstants.SVG_RY_ATTRIBUTE, r); + } + SVGUtil.setAtt(light, SVGConstants.SVG_STYLE_ATTRIBUTE, "fill:url(#"+SVGEffects.LIGHT_GRADIENT_ID+");fill-opacity:.5"); + tag.appendChild(light); + } + + // Add shadow effect: + if(svgp.getIdElement(SVGEffects.SHADOW_ID) != null) { + //Element shadow = svgp.svgRect(x + (w * .05), y + (h * .05), w, h); + //SVGUtil.setAtt(button, SVGConstants.SVG_STYLE_ATTRIBUTE, SVGConstants.CSS_FILL_PROPERTY + ":" + SVGConstants.CSS_BLACK_VALUE); + button.setAttribute(SVGConstants.SVG_FILTER_ATTRIBUTE, "url(#" + SVGEffects.SHADOW_ID + ")"); + //tag.appendChild(shadow); + } + + if(title != null) { + Element label = svgp.svgText(x + w * .5, y + h * .7, title); + label.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, titlecss.inlineCSS()); + tag.appendChild(label); + } + return tag; + } + + /** + * Set the button title + * + * @param title Button title + * @param textcolor Color + */ + public void setTitle(String title, String textcolor) { + this.title = title; + if(titlecss == null) { + titlecss = new CSSClass(this, "text"); + titlecss.setStatement(SVGConstants.CSS_TEXT_ANCHOR_PROPERTY, SVGConstants.CSS_MIDDLE_VALUE); + titlecss.setStatement(SVGConstants.CSS_FILL_PROPERTY, textcolor); + titlecss.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, .6 * h); + } + } +}
\ No newline at end of file diff --git a/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGCheckbox.java b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGCheckbox.java new file mode 100644 index 00000000..b6cf1165 --- /dev/null +++ b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGCheckbox.java @@ -0,0 +1,187 @@ +package de.lmu.ifi.dbs.elki.visualization.svg; + +/* + This file is part of ELKI: + Environment for Developing KDD-Applications Supported by Index-Structures + + Copyright (C) 2012 + Ludwig-Maximilians-Universität München + Lehr- und Forschungseinheit für Datenbanksysteme + ELKI Development Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.EventListenerList; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; +import org.w3c.dom.events.Event; +import org.w3c.dom.events.EventListener; +import org.w3c.dom.events.EventTarget; + +/** + * SVG checkbox. + * + * @author Sascha Goldhofer + */ +public class SVGCheckbox { + /** + * Status flag + */ + protected boolean checked; + + /** + * Event listeners + */ + protected EventListenerList listenerList = new EventListenerList(); + + /** + * Checkbox label + */ + protected String label = null; + + /** + * Constructor, without label + * + * @param checked Checked status + */ + public SVGCheckbox(boolean checked) { + this(checked, null); + } + + /** + * Constructor, with label + * + * @param checked Checked status + * @param label Label + */ + public SVGCheckbox(boolean checked, String label) { + this.checked = checked; + this.label = label; + } + + /** + * Render the SVG checkbox to a plot + * + * @param svgp Plot to draw to + * @param x X offset + * @param y Y offset + * @param size Size factor + * @return Container element + */ + public Element renderCheckBox(SVGPlot svgp, double x, double y, double size) { + // create check + final Element checkmark = SVGEffects.makeCheckmark(svgp); + checkmark.setAttribute(SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "scale(" + (size / 11) + ") translate(" + x + " " + y + ")"); + if(!checked) { + checkmark.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, SVGConstants.CSS_DISPLAY_PROPERTY + ":" + SVGConstants.CSS_NONE_VALUE); + } + + // create box + Element checkbox_box = SVGUtil.svgRect(svgp.getDocument(), x, y, size, size); + checkbox_box.setAttribute(SVGConstants.SVG_FILL_ATTRIBUTE, "#d4e4f1"); + checkbox_box.setAttribute(SVGConstants.SVG_STROKE_ATTRIBUTE, "#a0a0a0"); + checkbox_box.setAttribute(SVGConstants.SVG_STROKE_WIDTH_ATTRIBUTE, "0.5"); + + // create checkbox + final Element checkbox = svgp.svgElement(SVGConstants.SVG_G_TAG); + checkbox.appendChild(checkbox_box); + checkbox.appendChild(checkmark); + + // create Label + if(label != null) { + Element labele = svgp.svgText(x + 2 * size, y + size, label); + // TODO: font size! + checkbox.appendChild(labele); + } + + // add click event listener + EventTarget targ = (EventTarget) checkbox; + targ.addEventListener(SVGConstants.SVG_CLICK_EVENT_TYPE, new EventListener() { + /** + * Set the checkmark, and fire the event. + */ + protected void check() { + checkmark.removeAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE); + checked = true; + fireSwitchEvent(new ChangeEvent(SVGCheckbox.this)); + } + + /** + * Remove the checkmark and fire the event. + */ + protected void uncheck() { + checkmark.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, SVGConstants.CSS_DISPLAY_PROPERTY + ":" + SVGConstants.CSS_NONE_VALUE); + checked = false; + fireSwitchEvent(new ChangeEvent(SVGCheckbox.this)); + } + + @Override + public void handleEvent(Event evt) { + if(checked) { + uncheck(); + } + else { + check(); + } + } + }, false); + + return checkbox; + } + + /** + * Register a listener for this checkbox. + * + * @param listener Listener to add + */ + public void addCheckBoxListener(ChangeListener listener) { + listenerList.add(ChangeListener.class, listener); + } + + /** + * Remove a listener for this checkbox. + * + * @param listener Listener to remove + */ + public void removeCheckBoxListener(ChangeListener listener) { + listenerList.remove(ChangeListener.class, listener); + } + + /** + * Return the checkmark status. + * + * @return true, when checked + */ + public boolean isChecked() { + return checked; + } + + /** + * Fire the event to listeners + * + * @param evt Event to fire + */ + protected void fireSwitchEvent(ChangeEvent evt) { + Object[] listeners = listenerList.getListenerList(); + for(int i = 0; i < listeners.length; i += 2) { + if(listeners[i] == ChangeListener.class) { + ((ChangeListener) listeners[i + 1]).stateChanged(evt); + } + } + } +}
\ No newline at end of file diff --git a/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGCloneVisible.java b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGCloneVisible.java new file mode 100644 index 00000000..749720ee --- /dev/null +++ b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGCloneVisible.java @@ -0,0 +1,60 @@ +package de.lmu.ifi.dbs.elki.visualization.svg; + +/* + This file is part of ELKI: + Environment for Developing KDD-Applications Supported by Index-Structures + + Copyright (C) 2012 + Ludwig-Maximilians-Universität München + Lehr- und Forschungseinheit für Datenbanksysteme + ELKI Development Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import de.lmu.ifi.dbs.elki.utilities.xml.DOMCloner; + +/** + * Clone visible parts of an SVG document. + * + * @author Erich Schubert + */ +public class SVGCloneVisible extends DOMCloner { + @Override + public Node cloneNode(Document doc, Node eold) { + // Skip elements with visibility=hidden + if(eold instanceof Element) { + Element eeold = (Element) eold; + String vis = eeold.getAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY); + if(SVGConstants.CSS_HIDDEN_VALUE.equals(vis)) { + return null; + } + } + // Perform clone flat + Node enew = doc.importNode(eold, false); + // Recurse: + for(Node n = eold.getFirstChild(); n != null; n = n.getNextSibling()) { + final Node clone = cloneNode(doc, n); + if (clone != null) { + enew.appendChild(clone); + } + } + return enew; + } +} diff --git a/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGEffects.java b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGEffects.java new file mode 100644 index 00000000..79565902 --- /dev/null +++ b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGEffects.java @@ -0,0 +1,147 @@ +package de.lmu.ifi.dbs.elki.visualization.svg; + +/* + This file is part of ELKI: + Environment for Developing KDD-Applications Supported by Index-Structures + + Copyright (C) 2012 + Ludwig-Maximilians-Universität München + Lehr- und Forschungseinheit für Datenbanksysteme + ELKI Development Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +/** + * Class containing some popular SVG effects. + * + * @author Erich Schubert + */ +public class SVGEffects { + /** + * ID for the drop shadow effect + */ + public static final String SHADOW_ID = "shadow-effect"; + + /** + * ID for the light gradient fill + */ + public static final String LIGHT_GRADIENT_ID = "light-gradient"; + + /** + * Static method to prepare a SVG document for drop shadow effects. + * + * Invoke this from an appropriate update thread! + * + * @param svgp Plot to prepare + */ + public static void addShadowFilter(SVGPlot svgp) { + Element shadow = svgp.getIdElement(SHADOW_ID); + if(shadow == null) { + shadow = svgp.svgElement(SVGConstants.SVG_FILTER_TAG); + shadow.setAttribute(SVGConstants.SVG_ID_ATTRIBUTE, SHADOW_ID); + shadow.setAttribute(SVGConstants.SVG_WIDTH_ATTRIBUTE, "140%"); + shadow.setAttribute(SVGConstants.SVG_HEIGHT_ATTRIBUTE, "140%"); + + Element offset = svgp.svgElement(SVGConstants.SVG_FE_OFFSET_TAG); + offset.setAttribute(SVGConstants.SVG_IN_ATTRIBUTE, SVGConstants.SVG_SOURCE_ALPHA_VALUE); + offset.setAttribute(SVGConstants.SVG_RESULT_ATTRIBUTE, "off"); + offset.setAttribute(SVGConstants.SVG_DX_ATTRIBUTE, "0.1"); + offset.setAttribute(SVGConstants.SVG_DY_ATTRIBUTE, "0.1"); + shadow.appendChild(offset); + + Element gauss = svgp.svgElement(SVGConstants.SVG_FE_GAUSSIAN_BLUR_TAG); + gauss.setAttribute(SVGConstants.SVG_IN_ATTRIBUTE, "off"); + gauss.setAttribute(SVGConstants.SVG_RESULT_ATTRIBUTE, "blur"); + gauss.setAttribute(SVGConstants.SVG_STD_DEVIATION_ATTRIBUTE, "0.1"); + shadow.appendChild(gauss); + + Element blend = svgp.svgElement(SVGConstants.SVG_FE_BLEND_TAG); + blend.setAttribute(SVGConstants.SVG_IN_ATTRIBUTE, SVGConstants.SVG_SOURCE_GRAPHIC_VALUE); + blend.setAttribute(SVGConstants.SVG_IN2_ATTRIBUTE, "blur"); + blend.setAttribute(SVGConstants.SVG_MODE_ATTRIBUTE, SVGConstants.SVG_NORMAL_VALUE); + shadow.appendChild(blend); + + svgp.getDefs().appendChild(shadow); + svgp.putIdElement(SHADOW_ID, shadow); + } + } + + /** + * Static method to prepare a SVG document for light gradient effects. + * + * Invoke this from an appropriate update thread! + * + * @param svgp Plot to prepare + */ + public static void addLightGradient(SVGPlot svgp) { + Element gradient = svgp.getIdElement(LIGHT_GRADIENT_ID); + if(gradient == null) { + gradient = svgp.svgElement(SVGConstants.SVG_LINEAR_GRADIENT_TAG); + gradient.setAttribute(SVGConstants.SVG_ID_ATTRIBUTE, LIGHT_GRADIENT_ID); + gradient.setAttribute(SVGConstants.SVG_X1_ATTRIBUTE, "0"); + gradient.setAttribute(SVGConstants.SVG_Y1_ATTRIBUTE, "0"); + gradient.setAttribute(SVGConstants.SVG_X2_ATTRIBUTE, "0"); + gradient.setAttribute(SVGConstants.SVG_Y2_ATTRIBUTE, "1"); + + Element stop0 = svgp.svgElement(SVGConstants.SVG_STOP_TAG); + stop0.setAttribute(SVGConstants.SVG_STOP_COLOR_ATTRIBUTE, "white"); + stop0.setAttribute(SVGConstants.SVG_STOP_OPACITY_ATTRIBUTE, "1"); + stop0.setAttribute(SVGConstants.SVG_OFFSET_ATTRIBUTE, "0"); + gradient.appendChild(stop0); + + Element stop04 = svgp.svgElement(SVGConstants.SVG_STOP_TAG); + stop04.setAttribute(SVGConstants.SVG_STOP_COLOR_ATTRIBUTE, "white"); + stop04.setAttribute(SVGConstants.SVG_STOP_OPACITY_ATTRIBUTE, "0"); + stop04.setAttribute(SVGConstants.SVG_OFFSET_ATTRIBUTE, ".4"); + gradient.appendChild(stop04); + + Element stop06 = svgp.svgElement(SVGConstants.SVG_STOP_TAG); + stop06.setAttribute(SVGConstants.SVG_STOP_COLOR_ATTRIBUTE, "black"); + stop06.setAttribute(SVGConstants.SVG_STOP_OPACITY_ATTRIBUTE, "0"); + stop06.setAttribute(SVGConstants.SVG_OFFSET_ATTRIBUTE, ".6"); + gradient.appendChild(stop06); + + Element stop1 = svgp.svgElement(SVGConstants.SVG_STOP_TAG); + stop1.setAttribute(SVGConstants.SVG_STOP_COLOR_ATTRIBUTE, "black"); + stop1.setAttribute(SVGConstants.SVG_STOP_OPACITY_ATTRIBUTE, ".5"); + stop1.setAttribute(SVGConstants.SVG_OFFSET_ATTRIBUTE, "1"); + gradient.appendChild(stop1); + + svgp.getDefs().appendChild(gradient); + svgp.putIdElement(LIGHT_GRADIENT_ID, gradient); + } + } + + /** + * Checkmark path, sized approx. 15x15 + */ + public static final String SVG_CHECKMARK_PATH = "M0 6.458 L2.047 4.426 5.442 7.721 12.795 0 15 2.117 5.66 11.922 Z"; + + /** + * Creates a 15x15 big checkmark + * + * @param svgp Plot to create the element for + * @return Element + */ + public static Element makeCheckmark(SVGPlot svgp) { + Element checkmark = svgp.svgElement(SVGConstants.SVG_PATH_TAG); + checkmark.setAttribute(SVGConstants.SVG_D_ATTRIBUTE, SVG_CHECKMARK_PATH); + checkmark.setAttribute(SVGConstants.SVG_FILL_ATTRIBUTE, SVGConstants.CSS_BLACK_VALUE); + checkmark.setAttribute(SVGConstants.SVG_STROKE_ATTRIBUTE, SVGConstants.CSS_NONE_VALUE); + return checkmark; + } +}
\ No newline at end of file diff --git a/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperCube.java b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperCube.java index 18bbeb06..723f13df 100644 --- a/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperCube.java +++ b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperCube.java @@ -4,7 +4,7 @@ package de.lmu.ifi.dbs.elki.visualization.svg; This file is part of ELKI: Environment for Developing KDD-Applications Supported by Index-Structures - Copyright (C) 2011 + Copyright (C) 2012 Ludwig-Maximilians-Universität München Lehr- und Forschungseinheit für Datenbanksysteme ELKI Development Team @@ -31,7 +31,7 @@ import org.apache.batik.util.SVGConstants; import org.w3c.dom.Element; import de.lmu.ifi.dbs.elki.data.NumberVector; -import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector; +import de.lmu.ifi.dbs.elki.math.linearalgebra.VMath; import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D; /** @@ -52,7 +52,7 @@ public class SVGHyperCube { * @param max Opposite corner * @return path element */ - public static Element drawFrame(SVGPlot svgp, Projection2D proj, Vector min, Vector max) { + public static Element drawFrame(SVGPlot svgp, Projection2D proj, double[] min, double[] max) { SVGPath path = new SVGPath(); ArrayList<double[]> edges = getVisibleEdges(proj, min, max); double[] rv_min = proj.fastProjectDataToRenderSpace(min); @@ -72,7 +72,7 @@ public class SVGHyperCube { */ public static <V extends NumberVector<V, ?>> Element drawFrame(SVGPlot svgp, Projection2D proj, V min, V max) { SVGPath path = new SVGPath(); - ArrayList<double[]> edges = getVisibleEdges(proj, min.getColumnVector(), max.getColumnVector()); + ArrayList<double[]> edges = getVisibleEdges(proj, min.getColumnVector().getArrayRef(), max.getColumnVector().getArrayRef()); double[] rv_min = proj.fastProjectDataToRenderSpace(min); recDrawEdges(path, rv_min, edges, 0, new BitSet()); return path.makeElement(svgp); @@ -88,7 +88,7 @@ public class SVGHyperCube { * @param max Opposite corner * @return group element */ - public static Element drawFilled(SVGPlot svgp, String cls, Projection2D proj, Vector min, Vector max) { + public static Element drawFilled(SVGPlot svgp, String cls, Projection2D proj, double[] min, double[] max) { Element group = svgp.svgElement(SVGConstants.SVG_G_TAG); ArrayList<double[]> edges = getVisibleEdges(proj, min, max); double[] rv_min = proj.fastProjectDataToRenderSpace(min); @@ -109,7 +109,7 @@ public class SVGHyperCube { */ public static <V extends NumberVector<V, ?>> Element drawFilled(SVGPlot svgp, String cls, Projection2D proj, V min, V max) { Element group = svgp.svgElement(SVGConstants.SVG_G_TAG); - ArrayList<double[]> edges = getVisibleEdges(proj, min.getColumnVector(), max.getColumnVector()); + ArrayList<double[]> edges = getVisibleEdges(proj, min.getColumnVector().getArrayRef(), max.getColumnVector().getArrayRef()); double[] rv_min = proj.fastProjectDataToRenderSpace(min); recDrawSides(svgp, group, cls, rv_min, edges, 0, new BitSet()); return group; @@ -123,12 +123,12 @@ public class SVGHyperCube { * @param s_max Maximum value (in scaled space) * @return Edge list */ - private static ArrayList<double[]> getVisibleEdges(Projection2D proj, Vector s_min, Vector s_max) { - Vector s_deltas = s_max.minus(s_min); + private static ArrayList<double[]> getVisibleEdges(Projection2D proj, double[] s_min, double[] s_max) { + double[] s_deltas = VMath.minus(s_max, s_min); ArrayList<double[]> r_edges = new ArrayList<double[]>(); - for(int i = 0; i < s_min.getDimensionality(); i++) { - Vector delta = new Vector(s_min.getDimensionality()); - delta.set(i, s_deltas.get(i)); + for(int i = 0; i < s_min.length; i++) { + double[] delta = new double[s_min.length]; + delta[i] = s_deltas[i]; double[] deltas = proj.fastProjectRelativeDataToRenderSpace(delta); if(deltas[0] != 0 || deltas[1] != 0) { r_edges.add(deltas); diff --git a/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperSphere.java b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperSphere.java index 169c8698..29b437f9 100644 --- a/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperSphere.java +++ b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperSphere.java @@ -4,7 +4,7 @@ package de.lmu.ifi.dbs.elki.visualization.svg; This file is part of ELKI: Environment for Developing KDD-Applications Supported by Index-Structures - Copyright (C) 2011 + Copyright (C) 2012 Ludwig-Maximilians-Universität München Lehr- und Forschungseinheit für Datenbanksysteme ELKI Development Team @@ -29,7 +29,6 @@ import org.w3c.dom.Element; import de.lmu.ifi.dbs.elki.data.NumberVector; import de.lmu.ifi.dbs.elki.distance.distancevalue.NumberDistance; -import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector; import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D; /** @@ -51,7 +50,6 @@ public class SVGHyperSphere { /** * Wireframe "manhattan" hypersphere * - * @param <V> vector type * @param <D> radius * @param svgp SVG Plot * @param proj Visualization projection @@ -59,26 +57,27 @@ public class SVGHyperSphere { * @param rad radius * @return path element */ - public static <V extends NumberVector<V, ?>, D extends NumberDistance<?, ?>> Element drawManhattan(SVGPlot svgp, Projection2D proj, V mid, D rad) { - Vector v_mid = mid.getColumnVector(); - BitSet dims = proj.getVisibleDimensions2D(); + public static <D extends NumberDistance<?, ?>> Element drawManhattan(SVGPlot svgp, Projection2D proj, NumberVector<?, ?> mid, D rad) { + final double radius = rad.doubleValue(); + final double[] v_mid = mid.getColumnVector().getArrayRef(); // a copy + final BitSet dims = proj.getVisibleDimensions2D(); SVGPath path = new SVGPath(); for(int dim = dims.nextSetBit(0); dim >= 0; dim = dims.nextSetBit(dim + 1)) { - Vector v1 = v_mid.copy(); - v1.set(dim, v1.get(dim) + rad.doubleValue()); - Vector v2 = v_mid.copy(); - v2.set(dim, v2.get(dim) - rad.doubleValue()); - double[] p1 = proj.fastProjectDataToRenderSpace(v1); - double[] p2 = proj.fastProjectDataToRenderSpace(v2); + v_mid[dim] += radius; + double[] p1 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim] -= radius; + v_mid[dim] -= radius; + double[] p2 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim] += radius; for(int dim2 = dims.nextSetBit(0); dim2 >= 0; dim2 = dims.nextSetBit(dim2 + 1)) { if(dim < dim2) { - Vector v3 = v_mid.copy(); - v3.set(dim2, v3.get(dim2) + rad.doubleValue()); - Vector v4 = v_mid.copy(); - v4.set(dim2, v4.get(dim2) - rad.doubleValue()); - double[] p3 = proj.fastProjectDataToRenderSpace(v3); - double[] p4 = proj.fastProjectDataToRenderSpace(v4); + v_mid[dim2] += radius; + double[] p3 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim2] -= radius; + v_mid[dim2] -= radius; + double[] p4 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim2] += radius; path.moveTo(p1[0], p1[1]); path.drawTo(p3[0], p3[1]); @@ -98,7 +97,6 @@ public class SVGHyperSphere { /** * Wireframe "euclidean" hypersphere * - * @param <V> vector type * @param <D> radius * @param svgp SVG Plot * @param proj Visualization projection @@ -106,33 +104,34 @@ public class SVGHyperSphere { * @param rad radius * @return path element */ - public static <V extends NumberVector<V, ?>, D extends NumberDistance<?, ?>> Element drawEuclidean(SVGPlot svgp, Projection2D proj, V mid, D rad) { - Vector v_mid = mid.getColumnVector(); + public static <D extends NumberDistance<?, ?>> Element drawEuclidean(SVGPlot svgp, Projection2D proj, NumberVector<?, ?> mid, D rad) { + final double radius = rad.doubleValue(); + double[] v_mid = mid.getColumnVector().getArrayRef(); // a copy BitSet dims = proj.getVisibleDimensions2D(); SVGPath path = new SVGPath(); for(int dim = dims.nextSetBit(0); dim >= 0; dim = dims.nextSetBit(dim + 1)) { - Vector v1 = v_mid.copy(); - v1.set(dim, v1.get(dim) + rad.doubleValue()); - Vector v2 = v_mid.copy(); - v2.set(dim, v2.get(dim) - rad.doubleValue()); - double[] p1 = proj.fastProjectDataToRenderSpace(v1); - double[] p2 = proj.fastProjectDataToRenderSpace(v2); + v_mid[dim] += radius; + double[] p1 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim] -= radius; + v_mid[dim] -= radius; + double[] p2 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim] += radius; // delta vector - Vector dt1 = new Vector(v1.getDimensionality()); - dt1.set(dim, rad.doubleValue()); + double[] dt1 = new double[v_mid.length]; + dt1[dim] = radius; double[] d1 = proj.fastProjectRelativeDataToRenderSpace(dt1); for(int dim2 = dims.nextSetBit(0); dim2 >= 0; dim2 = dims.nextSetBit(dim2 + 1)) { if(dim < dim2) { - Vector v3 = v_mid.copy(); - v3.set(dim2, v3.get(dim2) + rad.doubleValue()); - Vector v4 = v_mid.copy(); - v4.set(dim2, v4.get(dim2) - rad.doubleValue()); - double[] p3 = proj.fastProjectDataToRenderSpace(v3); - double[] p4 = proj.fastProjectDataToRenderSpace(v4); + v_mid[dim2] += radius; + double[] p3 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim2] -= radius; + v_mid[dim2] -= radius; + double[] p4 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim2] += radius; // delta vector - Vector dt2 = new Vector(v2.getDimensionality()); - dt2.set(dim2, rad.doubleValue()); + double[] dt2 = new double[v_mid.length]; + dt2[dim2] = radius; double[] d2 = proj.fastProjectRelativeDataToRenderSpace(dt2); path.moveTo(p1[0], p1[1]); @@ -150,7 +149,6 @@ public class SVGHyperSphere { /** * Wireframe "Lp" hypersphere * - * @param <V> vector type * @param <D> radius * @param svgp SVG Plot * @param proj Visualization projection @@ -159,18 +157,19 @@ public class SVGHyperSphere { * @param p L_p value * @return path element */ - public static <V extends NumberVector<V, ?>, D extends NumberDistance<?, ?>> Element drawLp(SVGPlot svgp, Projection2D proj, V mid, D rad, double p) { - Vector v_mid = mid.getColumnVector(); - BitSet dims = proj.getVisibleDimensions2D(); + public static <D extends NumberDistance<?, ?>> Element drawLp(SVGPlot svgp, Projection2D proj, NumberVector<?, ?> mid, D rad, double p) { + final double radius = rad.doubleValue(); + final double[] v_mid = mid.getColumnVector().getArrayRef(); + final BitSet dims = proj.getVisibleDimensions2D(); final double kappax, kappay; if(p > 1.) { - double kappal = Math.pow(0.5, 1. / p); + final double kappal = Math.pow(0.5, 1. / p); kappax = Math.min(1.3, 4. * (2 * kappal - 1) / 3.); kappay = 0; } else if(p < 1.) { - double kappal = 1 - Math.pow(0.5, 1. / p); + final double kappal = 1 - Math.pow(0.5, 1. / p); kappax = 0; kappay = Math.min(1.3, 4. * (2 * kappal - 1) / 3.); } @@ -182,27 +181,27 @@ public class SVGHyperSphere { SVGPath path = new SVGPath(); for(int dim = dims.nextSetBit(0); dim >= 0; dim = dims.nextSetBit(dim + 1)) { - Vector vp0 = v_mid.copy(); - vp0.set(dim, vp0.get(dim) + rad.doubleValue()); - Vector vm0 = v_mid.copy(); - vm0.set(dim, vm0.get(dim) - rad.doubleValue()); - double[] pvp0 = proj.fastProjectDataToRenderSpace(vp0); - double[] pvm0 = proj.fastProjectDataToRenderSpace(vm0); + v_mid[dim] += radius; + double[] pvp0 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim] -= radius; + v_mid[dim] -= radius; + double[] pvm0 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim] += radius; // delta vector - Vector tvd0 = new Vector(vp0.getDimensionality()); - tvd0.set(dim, rad.doubleValue()); + double[] tvd0 = new double[v_mid.length]; + tvd0[dim] = radius; double[] vd0 = proj.fastProjectRelativeDataToRenderSpace(tvd0); for(int dim2 = dims.nextSetBit(0); dim2 >= 0; dim2 = dims.nextSetBit(dim2 + 1)) { if(dim < dim2) { - Vector v0p = v_mid.copy(); - v0p.set(dim2, v0p.get(dim2) + rad.doubleValue()); - Vector v0m = v_mid.copy(); - v0m.set(dim2, v0m.get(dim2) - rad.doubleValue()); - double[] pv0p = proj.fastProjectDataToRenderSpace(v0p); - double[] pv0m = proj.fastProjectDataToRenderSpace(v0m); + v_mid[dim2] += radius; + double[] pv0p = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim2] -= radius; + v_mid[dim2] -= radius; + double[] pv0m = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim2] += radius; // delta vector - Vector tv0d = new Vector(vm0.getDimensionality()); - tv0d.set(dim2, rad.doubleValue()); + double[] tv0d = new double[v_mid.length]; + tv0d[dim2] = radius; double[] v0d = proj.fastProjectRelativeDataToRenderSpace(tv0d); if(p > 1) { @@ -271,7 +270,6 @@ public class SVGHyperSphere { /** * Wireframe "cross" hypersphere * - * @param <V> vector type * @param <D> radius * @param svgp SVG Plot * @param proj Visualization projection @@ -279,19 +277,20 @@ public class SVGHyperSphere { * @param rad radius * @return path element */ - public static <V extends NumberVector<V, ?>, D extends NumberDistance<?, ?>> Element drawCross(SVGPlot svgp, Projection2D proj, V mid, D rad) { - Vector v_mid = mid.getColumnVector(); - BitSet dims = proj.getVisibleDimensions2D(); + public static <D extends NumberDistance<?, ?>> Element drawCross(SVGPlot svgp, Projection2D proj, NumberVector<?, ?> mid, D rad) { + final double radius = rad.doubleValue(); + final double[] v_mid = mid.getColumnVector().getArrayRef(); + final BitSet dims = proj.getVisibleDimensions2D(); SVGPath path = new SVGPath(); for(int dim = dims.nextSetBit(0); dim >= 0; dim = dims.nextSetBit(dim + 1)) { - Vector v1 = v_mid.copy(); - v1.set(dim, v1.get(dim) + rad.doubleValue()); - Vector v2 = v_mid.copy(); - v2.set(dim, v2.get(dim) - rad.doubleValue()); - double[] p1 = proj.fastProjectDataToRenderSpace(v1); - double[] p2 = proj.fastProjectDataToRenderSpace(v2); + v_mid[dim] += radius; + double[] p1 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim] -= radius; path.moveTo(p1[0], p1[1]); + v_mid[dim] -= radius; + double[] p2 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim] += radius; path.drawTo(p2[0], p2[1]); path.close(); } diff --git a/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGPath.java b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGPath.java index 2be331ca..2b86870b 100644 --- a/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGPath.java +++ b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGPath.java @@ -4,7 +4,7 @@ package de.lmu.ifi.dbs.elki.visualization.svg; This file is part of ELKI: Environment for Developing KDD-Applications Supported by Index-Structures - Copyright (C) 2011 + Copyright (C) 2012 Ludwig-Maximilians-Universität München Lehr- und Forschungseinheit für Datenbanksysteme ELKI Development Team @@ -118,8 +118,18 @@ public class SVGPath { } /** + * Constructor with initial point. + * + * @param xy initial coordinates + */ + public SVGPath(double[] xy) { + this(); + this.moveTo(xy); + } + + /** * Constructor from a vector collection (e.g. a polygon) - * + * * @param vectors vectors */ public SVGPath(Iterable<Vector> vectors) { @@ -159,6 +169,25 @@ public class SVGPath { * @param xy new coordinates * @return path object, for compact syntax. */ + public SVGPath drawTo(double[] xy) { + if(!isStarted()) { + moveTo(xy); + } + else { + lineTo(xy); + } + return this; + } + + /** + * Draw a line given a series of coordinates. + * + * Helper function that will use "move" for the first point, "lineto" for the + * remaining. + * + * @param xy new coordinates + * @return path object, for compact syntax. + */ public SVGPath drawTo(Vector xy) { if(!isStarted()) { moveTo(xy); @@ -196,6 +225,17 @@ public class SVGPath { * @param xy new coordinates * @return path object, for compact syntax. */ + public SVGPath lineTo(double[] xy) { + append(SVGConstants.PATH_LINE_TO, xy[0], xy[1]); + return this; + } + + /** + * Draw a line to the given coordinates. + * + * @param xy new coordinates + * @return path object, for compact syntax. + */ public SVGPath lineTo(Vector xy) { append(SVGConstants.PATH_LINE_TO, xy.get(0), xy.get(1)); return this; @@ -219,6 +259,17 @@ public class SVGPath { * @param xy new coordinates * @return path object, for compact syntax. */ + public SVGPath relativeLineTo(double[] xy) { + append(PATH_LINE_TO_RELATIVE, xy[0], xy[1]); + return this; + } + + /** + * Draw a line to the given relative coordinates. + * + * @param xy new coordinates + * @return path object, for compact syntax. + */ public SVGPath relativeLineTo(Vector xy) { append(PATH_LINE_TO_RELATIVE, xy.get(0), xy.get(1)); return this; @@ -286,6 +337,17 @@ public class SVGPath { * @param xy new coordinates * @return path object, for compact syntax. */ + public SVGPath moveTo(double[] xy) { + append(SVGConstants.PATH_MOVE, xy[0], xy[1]); + return this; + } + + /** + * Move to the given coordinates. + * + * @param xy new coordinates + * @return path object, for compact syntax. + */ public SVGPath moveTo(Vector xy) { append(SVGConstants.PATH_MOVE, xy.get(0), xy.get(1)); return this; @@ -309,6 +371,17 @@ public class SVGPath { * @param xy new coordinates * @return path object, for compact syntax. */ + public SVGPath relativeMoveTo(double[] xy) { + append(PATH_MOVE_RELATIVE, xy[0], xy[1]); + return this; + } + + /** + * Move to the given relative coordinates. + * + * @param xy new coordinates + * @return path object, for compact syntax. + */ public SVGPath relativeMoveTo(Vector xy) { append(PATH_MOVE_RELATIVE, xy.get(0), xy.get(1)); return this; @@ -338,6 +411,19 @@ public class SVGPath { * @param xy new coordinates * @return path object, for compact syntax. */ + public SVGPath cubicTo(double[] c1xy, double[] c2xy, double[] xy) { + append(SVGConstants.PATH_CUBIC_TO, c1xy[0], c1xy[1], c2xy[0], c2xy[1], xy[0], xy[1]); + return this; + } + + /** + * Cubic Bezier line to the given coordinates. + * + * @param c1xy first control point + * @param c2xy second control point + * @param xy new coordinates + * @return path object, for compact syntax. + */ public SVGPath cubicTo(Vector c1xy, Vector c2xy, Vector xy) { append(SVGConstants.PATH_CUBIC_TO, c1xy.get(0), c1xy.get(1), c2xy.get(0), c2xy.get(1), xy.get(0), xy.get(1)); return this; @@ -367,6 +453,19 @@ public class SVGPath { * @param xy new coordinates * @return path object, for compact syntax. */ + public SVGPath relativeCubicTo(double[] c1xy, double[] c2xy, double[] xy) { + append(PATH_CUBIC_TO_RELATIVE, c1xy[0], c1xy[1], c2xy[0], c2xy[1], xy[0], xy[1]); + return this; + } + + /** + * Cubic Bezier line to the given relative coordinates. + * + * @param c1xy first control point + * @param c2xy second control point + * @param xy new coordinates + * @return path object, for compact syntax. + */ public SVGPath relativeCubicTo(Vector c1xy, Vector c2xy, Vector xy) { append(PATH_CUBIC_TO_RELATIVE, c1xy.get(0), c1xy.get(1), c2xy.get(0), c2xy.get(1), xy.get(0), xy.get(1)); return this; @@ -393,6 +492,18 @@ public class SVGPath { * @param xy new coordinates * @return path object, for compact syntax. */ + public SVGPath smoothCubicTo(double[] c2xy, double[] xy) { + append(PATH_SMOOTH_CUBIC_TO, c2xy[0], c2xy[1], xy[0], xy[1]); + return this; + } + + /** + * Smooth Cubic Bezier line to the given coordinates. + * + * @param c2xy second control point + * @param xy new coordinates + * @return path object, for compact syntax. + */ public SVGPath smoothCubicTo(Vector c2xy, Vector xy) { append(PATH_SMOOTH_CUBIC_TO, c2xy.get(0), c2xy.get(1), xy.get(0), xy.get(1)); return this; @@ -419,6 +530,18 @@ public class SVGPath { * @param xy new coordinates * @return path object, for compact syntax. */ + public SVGPath relativeSmoothCubicTo(double[] c2xy, double[] xy) { + append(PATH_SMOOTH_CUBIC_TO_RELATIVE, c2xy[0], c2xy[1], xy[0], xy[1]); + return this; + } + + /** + * Smooth Cubic Bezier line to the given relative coordinates. + * + * @param c2xy second control point + * @param xy new coordinates + * @return path object, for compact syntax. + */ public SVGPath relativeSmoothCubicTo(Vector c2xy, Vector xy) { append(PATH_SMOOTH_CUBIC_TO_RELATIVE, c2xy.get(0), c2xy.get(1), xy.get(0), xy.get(1)); return this; @@ -445,6 +568,18 @@ public class SVGPath { * @param xy new coordinates * @return path object, for compact syntax. */ + public SVGPath quadTo(double[] c1xy, double[] xy) { + append(SVGConstants.PATH_QUAD_TO, c1xy[0], c1xy[1], xy[0], xy[1]); + return this; + } + + /** + * Quadratic Bezier line to the given coordinates. + * + * @param c1xy first control point + * @param xy new coordinates + * @return path object, for compact syntax. + */ public SVGPath quadTo(Vector c1xy, Vector xy) { append(SVGConstants.PATH_QUAD_TO, c1xy.get(0), c1xy.get(1), xy.get(0), xy.get(1)); return this; @@ -471,6 +606,18 @@ public class SVGPath { * @param xy new coordinates * @return path object, for compact syntax. */ + public SVGPath relativeQuadTo(double[] c1xy, double[] xy) { + append(PATH_QUAD_TO_RELATIVE, c1xy[0], c1xy[1], xy[0], xy[1]); + return this; + } + + /** + * Quadratic Bezier line to the given relative coordinates. + * + * @param c1xy first control point + * @param xy new coordinates + * @return path object, for compact syntax. + */ public SVGPath relativeQuadTo(Vector c1xy, Vector xy) { append(PATH_QUAD_TO_RELATIVE, c1xy.get(0), c1xy.get(1), xy.get(0), xy.get(1)); return this; @@ -494,6 +641,17 @@ public class SVGPath { * @param xy new coordinates * @return path object, for compact syntax. */ + public SVGPath smoothQuadTo(double[] xy) { + append(SVGConstants.PATH_SMOOTH_QUAD_TO, xy[0], xy[1]); + return this; + } + + /** + * Smooth quadratic Bezier line to the given coordinates. + * + * @param xy new coordinates + * @return path object, for compact syntax. + */ public SVGPath smoothQuadTo(Vector xy) { append(SVGConstants.PATH_SMOOTH_QUAD_TO, xy.get(0), xy.get(1)); return this; @@ -517,6 +675,17 @@ public class SVGPath { * @param xy new coordinates * @return path object, for compact syntax. */ + public SVGPath relativeSmoothQuadTo(double[] xy) { + append(PATH_SMOOTH_QUAD_TO_RELATIVE, xy[0], xy[1]); + return this; + } + + /** + * Smooth quadratic Bezier line to the given relative coordinates. + * + * @param xy new coordinates + * @return path object, for compact syntax. + */ public SVGPath relativeSmoothQuadTo(Vector xy) { append(PATH_SMOOTH_QUAD_TO_RELATIVE, xy.get(0), xy.get(1)); return this; @@ -528,7 +697,7 @@ public class SVGPath { * @param rx x radius * @param ry y radius * @param ar x-axis-rotation - * @param la large arc flag, if angle >= 180¬∞ + * @param la large arc flag, if angle >= 180 deg * @param sp sweep flag, if arc will be drawn in positive-angle direction * @param x new coordinates * @param y new coordinates @@ -541,9 +710,24 @@ public class SVGPath { /** * Elliptical arc curve to the given coordinates. * + * @param rx x radius + * @param ry y radius + * @param ar x-axis-rotation + * @param la large arc flag, if angle >= 180 deg + * @param sp sweep flag, if arc will be drawn in positive-angle direction + * @param xy new coordinates + */ + public SVGPath ellipticalArc(double rx, double ry, double ar, double la, double sp, double[] xy) { + append(SVGConstants.PATH_ARC, rx, ry, ar, la, sp, xy[0], xy[1]); + return this; + } + + /** + * Elliptical arc curve to the given coordinates. + * * @param rxy radius * @param ar x-axis-rotation - * @param la large arc flag, if angle >= 180¬∞ + * @param la large arc flag, if angle >= 180 deg * @param sp sweep flag, if arc will be drawn in positive-angle direction * @param xy new coordinates */ @@ -558,7 +742,7 @@ public class SVGPath { * @param rx x radius * @param ry y radius * @param ar x-axis-rotation - * @param la large arc flag, if angle >= 180¬∞ + * @param la large arc flag, if angle >= 180 deg * @param sp sweep flag, if arc will be drawn in positive-angle direction * @param x new coordinates * @param y new coordinates @@ -571,9 +755,24 @@ public class SVGPath { /** * Elliptical arc curve to the given relative coordinates. * + * @param rx x radius + * @param ry y radius + * @param ar x-axis-rotation + * @param la large arc flag, if angle >= 180 deg + * @param sp sweep flag, if arc will be drawn in positive-angle direction + * @param xy new coordinates + */ + public SVGPath relativeEllipticalArc(double rx, double ry, double ar, double la, double sp, double[] xy) { + append(PATH_ARC_RELATIVE, rx, ry, ar, la, sp, xy[0], xy[1]); + return this; + } + + /** + * Elliptical arc curve to the given relative coordinates. + * * @param rxy radius * @param ar x-axis-rotation - * @param la large arc flag, if angle >= 180¬∞ + * @param la large arc flag, if angle >= 180 deg * @param sp sweep flag, if arc will be drawn in positive-angle direction * @param xy new coordinates */ diff --git a/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGPlot.java b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGPlot.java index 14889fb0..81299b10 100644 --- a/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGPlot.java +++ b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGPlot.java @@ -4,7 +4,7 @@ package de.lmu.ifi.dbs.elki.visualization.svg; This file is part of ELKI: Environment for Developing KDD-Applications Supported by Index-Structures - Copyright (C) 2011 + Copyright (C) 2012 Ludwig-Maximilians-Universität München Lehr- und Forschungseinheit für Datenbanksysteme ELKI Development Team @@ -23,15 +23,12 @@ package de.lmu.ifi.dbs.elki.visualization.svg; along with this program. If not, see <http://www.gnu.org/licenses/>. */ -import java.io.ByteArrayOutputStream; +import java.awt.image.BufferedImage; import java.io.File; -import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.lang.ref.WeakReference; -import java.net.URI; -import java.net.URISyntaxException; import java.util.Collection; import java.util.HashMap; @@ -44,31 +41,25 @@ import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.apache.batik.dom.svg.SVGDOMImplementation; -import org.apache.batik.dom.util.DOMUtilities; -import org.apache.batik.svggen.SVGSyntax; import org.apache.batik.transcoder.Transcoder; import org.apache.batik.transcoder.TranscoderException; import org.apache.batik.transcoder.TranscoderInput; import org.apache.batik.transcoder.TranscoderOutput; +import org.apache.batik.transcoder.XMLAbstractTranscoder; import org.apache.batik.transcoder.image.JPEGTranscoder; import org.apache.batik.transcoder.image.PNGTranscoder; -import org.apache.batik.util.Base64EncoderStream; import org.apache.batik.util.SVGConstants; -import org.apache.fop.render.ps.EPSTranscoder; -import org.apache.fop.render.ps.PSTranscoder; -import org.apache.fop.svg.PDFTranscoder; import org.w3c.dom.DOMImplementation; import org.w3c.dom.DocumentType; import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; import org.w3c.dom.events.Event; import org.w3c.dom.svg.SVGDocument; import org.w3c.dom.svg.SVGPoint; import de.lmu.ifi.dbs.elki.logging.LoggingUtil; import de.lmu.ifi.dbs.elki.utilities.FileUtil; -import de.lmu.ifi.dbs.elki.utilities.xml.XMLNodeListIterator; +import de.lmu.ifi.dbs.elki.visualization.batikutil.CloneInlineImages; +import de.lmu.ifi.dbs.elki.visualization.batikutil.ThumbnailTranscoder; import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager; import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict; @@ -165,12 +156,12 @@ public class SVGPlot { // create a CSS class manager. cssman = new CSSClassManager(); } - + /** * Clean up the plot. */ public void dispose() { - getUpdateRunner().clear(); + runner.clear(); } /** @@ -296,6 +287,7 @@ public class SVGPlot { /** * Convenience method to add a CSS class or log an error. + * * @param cls CSS class to add. */ public void addCSSClassOrLogError(CSSClass cls) { @@ -303,7 +295,7 @@ public class SVGPlot { cssman.addClass(cls); } catch(CSSNamingConflict e) { - de.lmu.ifi.dbs.elki.logging.LoggingUtil.exception(e); + //de.lmu.ifi.dbs.elki.logging.LoggingUtil.exception(e); } } @@ -313,8 +305,7 @@ public class SVGPlot { */ public void updateStyleElement() { // TODO: this should be sufficient - why does Batik occasionally not pick up - // the - // changes unless we actually replace the style element itself? + // the changes unless we actually replace the style element itself? // cssman.updateStyleElement(document, style); Element newstyle = cssman.makeStyleElement(document); style.getParentNode().replaceChild(newstyle, style); @@ -336,40 +327,7 @@ public class SVGPlot { OutputStream out = new FileOutputStream(file); // TODO embed linked images. javax.xml.transform.Result result = new StreamResult(out); - // deep clone document - SVGDocument doc = (SVGDocument) DOMUtilities.deepCloneDocument(getDocument(), getDocument().getImplementation()); - NodeList imgs = doc.getElementsByTagNameNS(SVGConstants.SVG_NAMESPACE_URI, SVGConstants.SVG_IMAGE_TAG); - final String tmpurl = new File(System.getProperty("java.io.tmpdir") + File.separator).toURI().toString(); - for(Node img : new XMLNodeListIterator(imgs)) { - if(img instanceof Element) { - try { - Element i = (Element) img; - String href = i.getAttributeNS(SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_ATTRIBUTE); - if(href.startsWith(tmpurl) && href.endsWith(".png")) { - // need to convert the image into an inline image. - ByteArrayOutputStream os = new ByteArrayOutputStream(); - Base64EncoderStream encoder = new Base64EncoderStream(os); - File in = new File(new URI(href)); - FileInputStream instream = new FileInputStream(in); - byte[] buf = new byte[4096]; - while(true) { - int read = instream.read(buf, 0, buf.length); - if(read <= 0) { - break; - } - encoder.write(buf, 0, read); - } - instream.close(); - encoder.close(); - // replace HREF with inlined image data. - i.setAttributeNS(SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_ATTRIBUTE, SVGSyntax.DATA_PROTOCOL_PNG_PREFIX + os.toString()); - } - } - catch(URISyntaxException e) { - LoggingUtil.warning("Error in embedding PNG image."); - } - } - } + SVGDocument doc = cloneDocument(); // Use a transformer for pretty printing Transformer xformer = TransformerFactory.newInstance().newTransformer(); xformer.setOutputProperty(OutputKeys.INDENT, "yes"); @@ -387,11 +345,9 @@ public class SVGPlot { * @throws TranscoderException On input/parsing errors */ protected void transcode(File file, Transcoder transcoder) throws IOException, TranscoderException { - // Since the Transcoder is Batik-based, it will replace the rendering tree, - // which would then break display. Thus we need to deep clone the document - // first. - // -- found by Simon. - SVGDocument doc = (SVGDocument) DOMUtilities.deepCloneDocument(getDocument(), getDocument().getImplementation()); + // Disable validation, performance is more important here (thumbnails!) + transcoder.addTranscodingHint(XMLAbstractTranscoder.KEY_XML_PARSER_VALIDATING, Boolean.FALSE); + SVGDocument doc = cloneDocument(); TranscoderInput input = new TranscoderInput(doc); OutputStream out = new FileOutputStream(file); TranscoderOutput output = new TranscoderOutput(out); @@ -401,14 +357,37 @@ public class SVGPlot { } /** + * Clone the SVGPlot document for transcoding. + * + * This will usually be necessary for exporting the SVG document if it is + * currently being displayed: otherwise, we break the Batik rendering trees. + * (Discovered by Simon). + * + * @return cloned document + */ + protected SVGDocument cloneDocument() { + return (SVGDocument) new CloneInlineImages().cloneDocument(SVGDOMImplementation.getDOMImplementation(), document); + } + + /** * Transcode file to PDF. * * @param file Output filename * @throws IOException On write errors * @throws TranscoderException On input/parsing errors. + * @throws ClassNotFoundException PDF transcoder not installed */ - public void saveAsPDF(File file) throws IOException, TranscoderException { - transcode(file, new PDFTranscoder()); + public void saveAsPDF(File file) throws IOException, TranscoderException, ClassNotFoundException { + try { + Object t = Class.forName("org.apache.fop.svg.PDFTranscoder").newInstance(); + transcode(file, (Transcoder) t); + } + catch(InstantiationException e) { + throw new ClassNotFoundException("Could not instantiate PDF transcoder - is Apache FOP installed?", e); + } + catch(IllegalAccessException e) { + throw new ClassNotFoundException("Could not instantiate PDF transcoder - is Apache FOP installed?", e); + } } /** @@ -417,9 +396,19 @@ public class SVGPlot { * @param file Output filename * @throws IOException On write errors * @throws TranscoderException On input/parsing errors. + * @throws ClassNotFoundException PS transcoder not installed */ - public void saveAsPS(File file) throws IOException, TranscoderException { - transcode(file, new PSTranscoder()); + public void saveAsPS(File file) throws IOException, TranscoderException, ClassNotFoundException { + try { + Object t = Class.forName("org.apache.fop.render.ps.PSTranscoder").newInstance(); + transcode(file, (Transcoder) t); + } + catch(InstantiationException e) { + throw new ClassNotFoundException("Could not instantiate PS transcoder - is Apache FOP installed?", e); + } + catch(IllegalAccessException e) { + throw new ClassNotFoundException("Could not instantiate PS transcoder - is Apache FOP installed?", e); + } } /** @@ -428,9 +417,36 @@ public class SVGPlot { * @param file Output filename * @throws IOException On write errors * @throws TranscoderException On input/parsing errors. + * @throws ClassNotFoundException EPS transcoder not installed */ - public void saveAsEPS(File file) throws IOException, TranscoderException { - transcode(file, new EPSTranscoder()); + public void saveAsEPS(File file) throws IOException, TranscoderException, ClassNotFoundException { + try { + Object t = Class.forName("org.apache.fop.render.ps.EPSTranscoder").newInstance(); + transcode(file, (Transcoder) t); + } + catch(InstantiationException e) { + throw new ClassNotFoundException("Could not instantiate EPS transcoder - is Apache FOP installed?", e); + } + catch(IllegalAccessException e) { + throw new ClassNotFoundException("Could not instantiate EPS transcoder - is Apache FOP installed?", e); + } + } + + /** + * Test whether FOP were installed (for PDF, PS and EPS output support). + * + * @return true when FOP is available. + */ + public static boolean hasFOPInstalled() { + try { + Class<?> c1 = Class.forName("org.apache.fop.svg.PDFTranscoder"); + Class<?> c2 = Class.forName("org.apache.fop.render.ps.PSTranscoder"); + Class<?> c3 = Class.forName("org.apache.fop.render.ps.EPSTranscoder"); + return (c1 != null) && (c2 != null) && (c3 != null); + } + catch(ClassNotFoundException e) { + return false; + } } /** @@ -491,8 +507,9 @@ public class SVGPlot { * @throws TranscoderException on transcoding errors * @throws TransformerFactoryConfigurationError on transcoding errors * @throws TransformerException on transcoding errors + * @throws ClassNotFoundException when the transcoder was not installed */ - public void saveAsANY(File file, int width, int height, double quality) throws IOException, TranscoderException, TransformerFactoryConfigurationError, TransformerException { + public void saveAsANY(File file, int width, int height, double quality) throws IOException, TranscoderException, TransformerFactoryConfigurationError, TransformerException, ClassNotFoundException { String extension = FileUtil.getFilenameExtension(file); if(extension.equals("svg")) { saveAsSVG(file); @@ -518,6 +535,23 @@ public class SVGPlot { } /** + * Convert the SVG to a thumbnail image. + * + * @param width Width of thumbnail + * @param height Height of thumbnail + * @return Buffered image + */ + public BufferedImage makeAWTImage(int width, int height) throws TranscoderException { + ThumbnailTranscoder t = new ThumbnailTranscoder(); + t.addTranscodingHint(PNGTranscoder.KEY_WIDTH, new Float(width)); + t.addTranscodingHint(PNGTranscoder.KEY_HEIGHT, new Float(height)); + // Don't clone. Assume this is used safely. + TranscoderInput input = new TranscoderInput(document); + t.transcode(input, null); + return t.getLastImage(); + } + + /** * Dump the SVG plot to a debug file. */ public void dumpDebugFile() { @@ -563,21 +597,12 @@ public class SVGPlot { } /** - * Get the plots update runner. - * - * @return update runner - */ - private UpdateRunner getUpdateRunner() { - return runner; - } - - /** * Schedule an update. * * @param runnable Runnable to schedule */ public void scheduleUpdate(Runnable runnable) { - getUpdateRunner().invokeLater(runnable); + runner.invokeLater(runnable); } /** @@ -586,7 +611,7 @@ public class SVGPlot { * @param sync Update synchronizer */ public void synchronizeWith(UpdateSynchronizer sync) { - getUpdateRunner().synchronizeWith(sync); + runner.synchronizeWith(sync); } /** @@ -595,7 +620,7 @@ public class SVGPlot { * @param sync Update synchronizer to detach from. */ public void unsynchronizeWith(UpdateSynchronizer sync) { - getUpdateRunner().unsynchronizeWith(sync); + runner.unsynchronizeWith(sync); } /** diff --git a/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGScoreBar.java b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGScoreBar.java new file mode 100644 index 00000000..185a6061 --- /dev/null +++ b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGScoreBar.java @@ -0,0 +1,118 @@ +package de.lmu.ifi.dbs.elki.visualization.svg; + +import java.text.NumberFormat; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.utilities.FormatUtil; + +/** + * Draw a score bar. Essentially like a progress bar, left-to-right, displaying + * a relative score. + * + * @author Sascha Goldhofer + */ +// TODO: refactor to get a progress bar? +public class SVGScoreBar { + /** + * Fill value + */ + protected double fill = 0.0; + + /** + * Total size + */ + protected double size = 1.0; + + /** + * Label (on the right) + */ + protected String label = null; + + /** + * Number format, set to print the actual score + */ + private NumberFormat format = null; + + /** + * Constructor. + */ + public SVGScoreBar() { + // Nothing to do here. + } + + /** + * Set the fill of the score bar. + * + * @param fill Fill value + * @param size Total size + */ + public void setFill(double fill, double size) { + this.fill = fill; + this.size = size; + } + + /** + * Set label (right of the bar) + * + * @param text Label text + */ + public void addLabel(String text) { + this.label = text; + } + + /** + * To show score values, set a number format + * + * @param format Number format + */ + public void showValues(NumberFormat format) { + this.format = format; + } + + /** + * Build the actual element + * + * @param svgp Plot to draw to + * @param x X coordinate + * @param y Y coordinate + * @param width Width + * @param height Height + * @return new element + */ + public Element build(SVGPlot svgp, double x, double y, double width, double height) { + Element barchart = svgp.svgElement(SVGConstants.SVG_G_TAG); + + // TODO: use style library for colors! + Element bar = svgp.svgRect(x, y, width, height); + bar.setAttribute(SVGConstants.SVG_FILL_ATTRIBUTE, "#a0a0a0"); + bar.setAttribute(SVGConstants.SVG_STROKE_ATTRIBUTE, "#a0a0a0"); + bar.setAttribute(SVGConstants.SVG_STROKE_WIDTH_ATTRIBUTE, "" + height * 0.01); + + double fpos = (fill / size) * (width - (0.04 * height)); + Element chart = svgp.svgRect(x + 0.02 * height, y + 0.02 * height, fpos, height - 0.04 * height); + chart.setAttribute(SVGConstants.SVG_FILL_ATTRIBUTE, "#d4e4f1"); + chart.setAttribute(SVGConstants.SVG_STROKE_ATTRIBUTE, "#a0a0a0"); + chart.setAttribute(SVGConstants.SVG_STROKE_WIDTH_ATTRIBUTE, "" + height * 0.01); + + barchart = svgp.svgElement(SVGConstants.SVG_G_TAG); + barchart.appendChild(bar); + barchart.appendChild(chart); + + // Draw the values: + if(format != null) { + Element lbl = svgp.svgText(x + 0.05 * width, y + 0.75 * height, FormatUtil.format(fill, format)); + lbl.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, "font-size: " + 0.75 * height + "; font-weight: bold"); + barchart.appendChild(lbl); + } + + // Draw the label + if(label != null) { + Element lbl = svgp.svgText(x + 1.05 * width, y + 0.75 * height, label); + lbl.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, "font-size: " + 0.75 * height + "; font-weight: normal"); + barchart.appendChild(lbl); + } + return barchart; + } +}
\ No newline at end of file diff --git a/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGSimpleLinearAxis.java b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGSimpleLinearAxis.java index c32fef04..9a676c38 100644 --- a/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGSimpleLinearAxis.java +++ b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGSimpleLinearAxis.java @@ -4,7 +4,7 @@ package de.lmu.ifi.dbs.elki.visualization.svg; This file is part of ELKI: Environment for Developing KDD-Applications Supported by Index-Structures - Copyright (C) 2011 + Copyright (C) 2012 Ludwig-Maximilians-Universität München Lehr- und Forschungseinheit für Datenbanksysteme ELKI Development Team @@ -26,10 +26,10 @@ package de.lmu.ifi.dbs.elki.visualization.svg; import org.apache.batik.util.SVGConstants; import org.w3c.dom.Element; +import de.lmu.ifi.dbs.elki.math.scales.LinearScale; import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager; import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict; -import de.lmu.ifi.dbs.elki.visualization.scales.LinearScale; import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; /** @@ -41,6 +41,7 @@ import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; * @apiviz.uses CSSClassManager * @apiviz.uses LinearScale * @apiviz.uses StyleLibrary + * @apiviz.uses STYLE * @apiviz.uses Element oneway - - «create» */ public class SVGSimpleLinearAxis { @@ -55,6 +56,15 @@ public class SVGSimpleLinearAxis { } /** + * Labeling style: left-handed, right-handed, no ticks, labels at ends + * + * @author Erich Schubert + */ + public enum LabelStyle { + LEFTHAND, RIGHTHAND, NOLABELS, NOTHING, ENDLABEL + } + + /** * CSS class name for the axes */ private final static String CSS_AXIS = "axis"; @@ -93,8 +103,6 @@ public class SVGSimpleLinearAxis { CSSClass label = new CSSClass(owner, CSS_AXIS_LABEL); label.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getTextColor(StyleLibrary.AXIS_LABEL)); label.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, style.getFontFamily(StyleLibrary.AXIS_LABEL)); - // label.setStatement(SVGConstants.SVG_TEXT_RENDERING_ATTRIBUTE, - // SVGConstants.SVG_OPTIMIZE_LEGIBILITY_VALUE); label.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, style.getTextSize(StyleLibrary.AXIS_LABEL)); manager.addClass(label); } @@ -110,13 +118,11 @@ public class SVGSimpleLinearAxis { * @param y1 starting coordinate * @param x2 ending coordinate * @param y2 ending coordinate - * @param labels control whether labels are printed. - * @param righthanded control whether to print labels on the right hand side - * or left hand side + * @param labelstyle Style for placing the labels * @param style Style library * @throws CSSNamingConflict when a conflict occurs in CSS */ - public static void drawAxis(SVGPlot plot, Element parent, LinearScale scale, double x1, double y1, double x2, double y2, boolean labels, boolean righthanded, StyleLibrary style) throws CSSNamingConflict { + public static void drawAxis(SVGPlot plot, Element parent, LinearScale scale, double x1, double y1, double x2, double y2, LabelStyle labelstyle, StyleLibrary style) throws CSSNamingConflict { assert (parent != null); Element line = plot.svgLine(x1, y1, x2, y2); SVGUtil.setCSSClass(line, CSS_AXIS); @@ -129,75 +135,109 @@ public class SVGSimpleLinearAxis { double th = -tx * 0.01; // choose where to print labels. + final boolean labels, ticks; + switch(labelstyle){ + case LEFTHAND: + case RIGHTHAND: + labels = true; + ticks = true; + break; + case NOLABELS: + labels = false; + ticks = true; + break; + case ENDLABEL: // end labels are handle specially + case NOTHING: + default: + labels = false; + ticks = false; + } ALIGNMENT pos = ALIGNMENT.LL; if(labels) { double angle = Math.atan2(ty, tx); // System.err.println(tx + " " + (-ty) + " " + angle); if(angle > 2.6) { // pi .. 2.6 = 180 .. 150 - pos = righthanded ? ALIGNMENT.RC : ALIGNMENT.LC; + pos = labelstyle == LabelStyle.RIGHTHAND ? ALIGNMENT.RC : ALIGNMENT.LC; } else if(angle > 0.5) { // 2.3 .. 0.7 = 130 .. 40 - pos = righthanded ? ALIGNMENT.RR : ALIGNMENT.LL; + pos = labelstyle == LabelStyle.RIGHTHAND ? ALIGNMENT.RR : ALIGNMENT.LL; } else if(angle > -0.5) { // 0.5 .. -0.5 = 30 .. -30 - pos = righthanded ? ALIGNMENT.RC : ALIGNMENT.LC; + pos = labelstyle == LabelStyle.RIGHTHAND ? ALIGNMENT.RC : ALIGNMENT.LC; } else if(angle > -2.6) { // -0.5 .. -2.6 = -30 .. -150 - pos = righthanded ? ALIGNMENT.RL : ALIGNMENT.LR; + pos = labelstyle == LabelStyle.RIGHTHAND ? ALIGNMENT.RL : ALIGNMENT.LR; } else { // -2.6 .. -pi = -150 .. -180 - pos = righthanded ? ALIGNMENT.RC : ALIGNMENT.LC; + pos = labelstyle == LabelStyle.RIGHTHAND ? ALIGNMENT.RC : ALIGNMENT.LC; } } // vertical text offset; align approximately with middle instead of // baseline. - double textvoff = 0.015; + double textvoff = style.getTextSize(StyleLibrary.AXIS_LABEL) * .35; // draw ticks on x axis - for(double tick = scale.getMin(); tick <= scale.getMax() + scale.getRes() / 10; tick += scale.getRes()) { - double x = x1 + tx * scale.getScaled(tick); - double y = y1 + ty * scale.getScaled(tick); - // This is correct. Vectors: (vec - tvec) to (vec + tvec) - Element tickline = plot.svgLine(x - tw, y - th, x + tw, y + th); - SVGUtil.setAtt(tickline, SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_AXIS_TICK); - parent.appendChild(tickline); - // draw labels - if(labels) { - double tex = x; - double tey = y; - switch(pos){ - case LL: - case LC: - case LR: - tex = x + tw * 2.5; - tey = y + th * 2.5 + textvoff; - break; - case RL: - case RC: - case RR: - tex = x - tw * 2.5; - tey = y - th * 2.5 + textvoff; + if(ticks || labels) { + for(double tick = scale.getMin(); tick <= scale.getMax() + scale.getRes() / 10; tick += scale.getRes()) { + double x = x1 + tx * scale.getScaled(tick); + double y = y1 + ty * scale.getScaled(tick); + if(ticks) { + // This is correct. Vectors: (vec - tvec) to (vec + tvec) + Element tickline = plot.svgLine(x - tw, y - th, x + tw, y + th); + SVGUtil.setAtt(tickline, SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_AXIS_TICK); + parent.appendChild(tickline); } - Element text = plot.svgText(tex, tey, scale.formatValue(tick)); - SVGUtil.setAtt(text, SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_AXIS_LABEL); - switch(pos){ - case LL: - case RL: - SVGUtil.setAtt(text, SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_START_VALUE); - break; - case LC: - case RC: - SVGUtil.setAtt(text, SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_MIDDLE_VALUE); - break; - case LR: - case RR: - SVGUtil.setAtt(text, SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_END_VALUE); - break; + // draw labels + if(labels) { + double tex = x; + double tey = y; + switch(pos){ + case LL: + case LC: + case LR: + tex = x + tw * 2.5; + tey = y + th * 2.5 + textvoff; + break; + case RL: + case RC: + case RR: + tex = x - tw * 2.5; + tey = y - th * 2.5 + textvoff; + } + Element text = plot.svgText(tex, tey, scale.formatValue(tick)); + text.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_AXIS_LABEL); + switch(pos){ + case LL: + case RL: + text.setAttribute(SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_START_VALUE); + break; + case LC: + case RC: + text.setAttribute(SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_MIDDLE_VALUE); + break; + case LR: + case RR: + text.setAttribute(SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_END_VALUE); + break; + } + parent.appendChild(text); } + } + } + if(labelstyle == LabelStyle.ENDLABEL) { + { + Element text = plot.svgText(x1 - tx * 0.02, y1 - ty * 0.02 + textvoff, scale.formatValue(scale.getMin())); + text.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_AXIS_LABEL); + text.setAttribute(SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_MIDDLE_VALUE); + parent.appendChild(text); + } + { + Element text = plot.svgText(x2 + tx * 0.02, y2 + ty * 0.02 + textvoff, scale.formatValue(scale.getMax())); + text.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_AXIS_LABEL); + text.setAttribute(SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_MIDDLE_VALUE); parent.appendChild(text); } } setupCSSClasses(plot, plot.getCSSClassManager(), style); } - -} +}
\ No newline at end of file diff --git a/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGUtil.java b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGUtil.java index b4e59fda..4e40d1b0 100644 --- a/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGUtil.java +++ b/src/de/lmu/ifi/dbs/elki/visualization/svg/SVGUtil.java @@ -4,7 +4,7 @@ package de.lmu.ifi.dbs.elki.visualization.svg; This file is part of ELKI: Environment for Developing KDD-Applications Supported by Index-Structures - Copyright (C) 2011 + Copyright (C) 2012 Ludwig-Maximilians-Universität München Lehr- und Forschungseinheit für Datenbanksysteme ELKI Development Team @@ -23,9 +23,10 @@ package de.lmu.ifi.dbs.elki.visualization.svg; along with this program. If not, see <http://www.gnu.org/licenses/>. */ +import gnu.trove.map.hash.TObjectIntHashMap; + import java.awt.Color; import java.text.NumberFormat; -import java.util.HashMap; import java.util.Locale; import javax.swing.text.html.StyleSheet; @@ -77,9 +78,16 @@ public final class SVGUtil { /** * SVG color names conversion. */ - final private static HashMap<String, Integer> SVG_COLOR_NAMES = new HashMap<String, Integer>(); + final private static TObjectIntHashMap<String> SVG_COLOR_NAMES; + + /** + * Key not found value. Not a reasonable color, fully transparent! + */ + final private static int NO_VALUE = 0x00123456; static { + // Build a reasonably sized hashmap. Use 0 + SVG_COLOR_NAMES = new TObjectIntHashMap<String>(90, .8f, NO_VALUE); // List taken from SVG specification: // http://www.w3.org/TR/SVG/types.html#ColorKeywords SVG_COLOR_NAMES.put("aliceblue", 0xFFF0F8FF); @@ -229,6 +237,8 @@ public final class SVGUtil { SVG_COLOR_NAMES.put("whitesmoke", 0xFFF5F5F5); SVG_COLOR_NAMES.put("yellow", 0xFFFFFF00); SVG_COLOR_NAMES.put("yellowgreen", 0xFF9ACD32); + // Nonstandard: + SVG_COLOR_NAMES.put("transparent", 0xFFFFFFFF); } /** @@ -468,8 +478,8 @@ public final class SVGUtil { * @return Color value */ public static Color stringToColor(String str) { - Integer icol = SVG_COLOR_NAMES.get(str.toLowerCase()); - if(icol != null) { + int icol = SVG_COLOR_NAMES.get(str.toLowerCase()); + if(icol != NO_VALUE) { return new Color(icol, false); } return colorLookupStylesheet.stringToColor(str); @@ -478,6 +488,8 @@ public final class SVGUtil { /** * Convert a color name from an AWT color object to CSS syntax * + * Note: currently only RGB (from ARGB order) are supported. + * * @param col Color value * @return Color string */ @@ -486,6 +498,18 @@ public final class SVGUtil { } /** + * Convert a color name from an AWT color object to CSS syntax + * + * Note: currently only RGB (from ARGB order) are supported. + * + * @param col Color value + * @return Color string + */ + public static String colorToString(int col) { + return String.format("#%02x%02x%02x", (col >>> 16) & 0xFF, (col >>> 8) & 0xFF, col & 0xFF); + } + + /** * Make a transform string to add margins * * @param owidth Width of outer (embedding) canvas @@ -572,4 +596,62 @@ public final class SVGUtil { tag.removeChild(last); } } + + /** + * Remove an element from its parent, if defined. + * + * @param elem Element to remove + */ + public static void removeFromParent(Element elem) { + if(elem != null) { + if(elem.getParentNode() != null) { + elem.getParentNode().removeChild(elem); + } + } + } + + /** + * Create a circle segment. + * + * @param svgp Plot to draw to + * @param centerx Center X position + * @param centery Center Y position + * @param angleStart Starting angle + * @param angleDelta Angle delta + * @param innerRadius inner radius + * @param outerRadius outer radius + * @return SVG element representing this circle segment + */ + public static Element svgCircleSegment(SVGPlot svgp, double centerx, double centery, double angleStart, double angleDelta, double innerRadius, double outerRadius) { + double sin1st = Math.sin(angleStart); + double cos1st = Math.cos(angleStart); + + double sin2nd = Math.sin(angleStart + angleDelta); + double cos2nd = Math.cos(angleStart + angleDelta); + + double inner1stx = centerx + (innerRadius * sin1st); + double inner1sty = centery - (innerRadius * cos1st); + double outer1stx = centerx + (outerRadius * sin1st); + double outer1sty = centery - (outerRadius * cos1st); + + double inner2ndx = centerx + (innerRadius * sin2nd); + double inner2ndy = centery - (innerRadius * cos2nd); + double outer2ndx = centerx + (outerRadius * sin2nd); + double outer2ndy = centery - (outerRadius * cos2nd); + + double largeArc = 0; + if(angleDelta >= Math.PI) { + largeArc = 1; + } + + SVGPath path = new SVGPath(inner1stx, inner1sty); + path.lineTo(outer1stx, outer1sty); + path.ellipticalArc(outerRadius, outerRadius, 0, largeArc, 1, outer2ndx, outer2ndy); + path.lineTo(inner2ndx, inner2ndy); + if (innerRadius > 0) { + path.ellipticalArc(innerRadius, innerRadius, 0, largeArc, 0, inner1stx, inner1sty); + } + + return path.makeElement(svgp); + } }
\ No newline at end of file diff --git a/src/de/lmu/ifi/dbs/elki/visualization/svg/UpdateRunner.java b/src/de/lmu/ifi/dbs/elki/visualization/svg/UpdateRunner.java index 03bc9d74..463c7c63 100644 --- a/src/de/lmu/ifi/dbs/elki/visualization/svg/UpdateRunner.java +++ b/src/de/lmu/ifi/dbs/elki/visualization/svg/UpdateRunner.java @@ -4,7 +4,7 @@ package de.lmu.ifi.dbs.elki.visualization.svg; This file is part of ELKI: Environment for Developing KDD-Applications Supported by Index-Structures - Copyright (C) 2011 + Copyright (C) 2012 Ludwig-Maximilians-Universität München Lehr- und Forschungseinheit für Datenbanksysteme ELKI Development Team @@ -23,7 +23,6 @@ package de.lmu.ifi.dbs.elki.visualization.svg; along with this program. If not, see <http://www.gnu.org/licenses/>. */ -import java.util.Collection; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; @@ -52,7 +51,7 @@ public class UpdateRunner { /** * Synchronizer that can block events from being executed right away. */ - private Collection<UpdateSynchronizer> synchronizer = new java.util.Vector<UpdateSynchronizer>(); + private UpdateSynchronizer synchronizer = null; /** * Construct a new update handler @@ -70,13 +69,11 @@ public class UpdateRunner { */ public void invokeLater(Runnable r) { queue.add(r); - if(synchronizer.size() > 0) { - for(UpdateSynchronizer s : synchronizer) { - s.activate(); - } + if(synchronizer == null) { + runQueue(); } else { - runQueue(); + synchronizer.activate(); } } @@ -128,14 +125,18 @@ public class UpdateRunner { * @param newsync Update synchronizer */ public synchronized void synchronizeWith(UpdateSynchronizer newsync) { - //LoggingUtil.warning("Synchronizing: " + sync + " " + newsync); - if(synchronizer.contains(newsync)) { + // LoggingUtil.warning("Synchronizing: " + sync + " " + newsync); + if(synchronizer == newsync) { LoggingUtil.warning("Double-synced to the same plot!"); + return; } - else { - synchronizer.add(newsync); - newsync.addUpdateRunner(this); + if(synchronizer != null) { + LoggingUtil.warning("Attempting to synchronize to more than one synchronizer."); + return; } + synchronizer = newsync; + newsync.addUpdateRunner(this); + } /** @@ -144,10 +145,17 @@ public class UpdateRunner { * @param oldsync Update synchronizer to remove */ public synchronized void unsynchronizeWith(UpdateSynchronizer oldsync) { - //LoggingUtil.warning("Unsynchronizing: " + sync + " " + oldsync); - synchronizer.remove(oldsync); - if(synchronizer.size() == 0) { - runQueue(); + if(synchronizer == null) { + LoggingUtil.warning("Warning: was not synchronized."); + } + else { + if(synchronizer != oldsync) { + LoggingUtil.warning("Warning: was synchronized differently!"); + return; + } } + // LoggingUtil.warning("Unsynchronizing: " + sync + " " + oldsync); + synchronizer = null; + runQueue(); } }
\ No newline at end of file diff --git a/src/de/lmu/ifi/dbs/elki/visualization/svg/UpdateSynchronizer.java b/src/de/lmu/ifi/dbs/elki/visualization/svg/UpdateSynchronizer.java index c65ae719..3f249871 100644 --- a/src/de/lmu/ifi/dbs/elki/visualization/svg/UpdateSynchronizer.java +++ b/src/de/lmu/ifi/dbs/elki/visualization/svg/UpdateSynchronizer.java @@ -4,7 +4,7 @@ package de.lmu.ifi.dbs.elki.visualization.svg; This file is part of ELKI: Environment for Developing KDD-Applications Supported by Index-Structures - Copyright (C) 2011 + Copyright (C) 2012 Ludwig-Maximilians-Universität München Lehr- und Forschungseinheit für Datenbanksysteme ELKI Development Team diff --git a/src/de/lmu/ifi/dbs/elki/visualization/svg/VoronoiDraw.java b/src/de/lmu/ifi/dbs/elki/visualization/svg/VoronoiDraw.java new file mode 100644 index 00000000..317a44ae --- /dev/null +++ b/src/de/lmu/ifi/dbs/elki/visualization/svg/VoronoiDraw.java @@ -0,0 +1,152 @@ +package de.lmu.ifi.dbs.elki.visualization.svg;
+/* + This file is part of ELKI: + Environment for Developing KDD-Applications Supported by Index-Structures + + Copyright (C) 2012 + Ludwig-Maximilians-Universität München + Lehr- und Forschungseinheit für Datenbanksysteme + ELKI Development Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +
+import java.util.List;
+
+import de.lmu.ifi.dbs.elki.math.geometry.SweepHullDelaunay2D;
+import de.lmu.ifi.dbs.elki.math.geometry.SweepHullDelaunay2D.Triangle;
+import de.lmu.ifi.dbs.elki.math.linearalgebra.VMath;
+import de.lmu.ifi.dbs.elki.visualization.projections.CanvasSize;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D;
+
+/**
+ * Draw the Voronoi cells
+ *
+ * @author Robert Rödler
+ * @author Erich Schubert
+ */
+public class VoronoiDraw {
+ /**
+ * Draw the Delaunay triangulation.
+ *
+ * @param proj Projection
+ * @param delaunay Triangulation
+ * @param means Means
+ * @return Path
+ */
+ public static SVGPath drawDelaunay(Projection2D proj, List<SweepHullDelaunay2D.Triangle> delaunay, List<double[]> means) {
+ final SVGPath path = new SVGPath();
+ for(SweepHullDelaunay2D.Triangle del : delaunay) {
+ path.moveTo(proj.fastProjectDataToRenderSpace(means.get(del.a)));
+ path.drawTo(proj.fastProjectDataToRenderSpace(means.get(del.b)));
+ path.drawTo(proj.fastProjectDataToRenderSpace(means.get(del.c)));
+ path.close();
+ }
+ return path;
+ }
+
+ /**
+ * Draw a Voronoi diagram
+ *
+ * @param proj Projection
+ * @param delaunay Delaunay triangulation
+ * @param means Cluster means
+ * @return SVG path
+ */
+ public static SVGPath drawVoronoi(Projection2D proj, List<SweepHullDelaunay2D.Triangle> delaunay, List<double[]> means) {
+ final SVGPath path = new SVGPath();
+ CanvasSize viewport = proj.estimateViewport();
+ for(int i = 0; i < delaunay.size(); i++) {
+ SweepHullDelaunay2D.Triangle del = delaunay.get(i);
+ final double[] projcx = proj.fastProjectDataToRenderSpace(del.m.getArrayRef());
+ if(del.ab > i) {
+ Triangle oth = delaunay.get(del.ab);
+ path.moveTo(projcx);
+ path.drawTo(proj.fastProjectDataToRenderSpace(oth.m.getArrayRef()));
+ }
+ else if(del.ab < 0) {
+ double[] dirv = VMath.minus(means.get(del.a), means.get(del.b));
+ VMath.rotate90Equals(dirv);
+ double[] dir = proj.fastProjectRelativeDataToRenderSpace(dirv);
+ final double factor = viewport.continueToMargin(projcx, dir);
+ if(factor > 0) {
+ path.moveTo(projcx);
+ path.relativeLineTo(factor * dir[0], factor * dir[1]);
+ }
+ }
+
+ if(del.bc > i) {
+ Triangle oth = delaunay.get(del.bc);
+ path.moveTo(projcx);
+ path.drawTo(proj.fastProjectDataToRenderSpace(oth.m.getArrayRef()));
+ }
+ else if(del.bc < 0) {
+ double[] dirv = VMath.minus(means.get(del.b), means.get(del.c));
+ VMath.rotate90Equals(dirv);
+ double[] dir = proj.fastProjectRelativeDataToRenderSpace(dirv);
+ final double factor = viewport.continueToMargin(projcx, dir);
+ if(factor > 0) {
+ path.moveTo(projcx);
+ path.relativeLineTo(factor * dir[0], factor * dir[1]);
+ }
+ }
+
+ if(del.ca > i) {
+ Triangle oth = delaunay.get(del.ca);
+ path.moveTo(projcx);
+ path.drawTo(proj.fastProjectDataToRenderSpace(oth.m.getArrayRef()));
+ }
+ else if(del.ca < 0) {
+ double[] dirv = VMath.minus(means.get(del.c), means.get(del.a));
+ VMath.rotate90Equals(dirv);
+ double[] dir = proj.fastProjectRelativeDataToRenderSpace(dirv);
+ final double factor = viewport.continueToMargin(projcx, dir);
+ if(factor > 0) {
+ path.moveTo(projcx);
+ path.relativeLineTo(factor * dir[0], factor * dir[1]);
+ }
+ }
+ }
+ return path;
+ }
+
+ /**
+ * Fake Voronoi diagram. For two means only
+ *
+ * @param proj Projection
+ * @param means Mean vectors
+ * @return SVG path
+ */
+ public static SVGPath drawFakeVoronoi(Projection2D proj, List<double[]> means) {
+ CanvasSize viewport = proj.estimateViewport();
+ final SVGPath path = new SVGPath();
+ // Difference
+ final double[] dirv = VMath.minus(means.get(1), means.get(0));
+ VMath.rotate90Equals(dirv);
+ double[] dir = proj.fastProjectRelativeDataToRenderSpace(dirv);
+ // Mean
+ final double[] mean = VMath.plus(means.get(0), means.get(1));
+ VMath.timesEquals(mean, 0.5);
+ double[] projmean = proj.fastProjectDataToRenderSpace(mean);
+
+ double factor = viewport.continueToMargin(projmean, dir);
+ path.moveTo(projmean[0] + factor * dir[0], projmean[1] + factor * dir[1]);
+ // Inverse direction:
+ dir[0] *= -1;
+ dir[1] *= -1;
+ factor = viewport.continueToMargin(projmean, dir);
+ path.drawTo(projmean[0] + factor * dir[0], projmean[1] + factor * dir[1]);
+ return path;
+ }
+}
\ No newline at end of file diff --git a/src/de/lmu/ifi/dbs/elki/visualization/svg/package-info.java b/src/de/lmu/ifi/dbs/elki/visualization/svg/package-info.java index 7c9ba1f9..7811b493 100644 --- a/src/de/lmu/ifi/dbs/elki/visualization/svg/package-info.java +++ b/src/de/lmu/ifi/dbs/elki/visualization/svg/package-info.java @@ -6,7 +6,7 @@ This file is part of ELKI: Environment for Developing KDD-Applications Supported by Index-Structures -Copyright (C) 2011 +Copyright (C) 2012 Ludwig-Maximilians-Universität München Lehr- und Forschungseinheit für Datenbanksysteme ELKI Development Team |