/** * This software is released as part of the Pumpernickel project. * * All com.pump resources in the Pumpernickel project are distributed under the * MIT License: * https://raw.githubusercontent.com/mickleness/pumpernickel/master/License.txt * * More information about the Pumpernickel project is available here: * https://mickleness.github.io/pumpernickel/ */ package com.pump.geom; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.geom.Area; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.imageio.ImageIO; import javax.swing.ImageIcon; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.SwingConstants; import com.pump.inspector.Inspector; public class AddRulesTest extends BasicTestElement { static class Case { List<Shape> shapes = new ArrayList<Shape>(); String name; BufferedImage image; public Case(Shape[] shapes, String name) { for (int a = 0; a < shapes.length; a++) { this.shapes.add(shapes[a]); } this.name = name; image = render(); } public Case(String name) { URL url = AddRulesTest.class.getResource(name); InputStream in = null; try { in = url.openStream(); BufferedReader br = new BufferedReader( new InputStreamReader(in)); String s = br.readLine(); while (s != null) { add(s); s = br.readLine(); } } catch (IOException e) { RuntimeException e2 = new RuntimeException(); e2.initCause(e); throw e2; } finally { try { in.close(); } catch (Exception e) { } ; } image = scale(render(), new Dimension(240, 240)); StringBuffer sb = new StringBuffer(name.length()); for (int a = 0; a < name.length(); a++) { char c = name.charAt(a); if (a != 0 && Character.isUpperCase(c)) { sb.append(' '); } sb.append(c); } this.name = sb.toString(); } /** * This is a VERY expensive call that will break apart clip art * represented in massive strings with multiple subpaths. */ @SuppressWarnings("unused") private Case split() { List<Shape> newShapes = new ArrayList<Shape>(); for (int a = 0; a < shapes.size(); a++) { Shape shape = shapes.get(a); split(shape, newShapes); } Case returnValue = new Case(newShapes.toArray(new Shape[newShapes .size()]), name + " Split"); try { returnValue.write(); } catch (IOException e) { e.printStackTrace(); } return returnValue; } /** * Serializes this case as a ".shapes" file. * * @throws IOException * if an IO problem occurs. */ public void write() throws IOException { File file = new File(name + ".shapes"); file.createNewFile(); OutputStream out = null; try { out = new FileOutputStream(file); PrintStream ps = new PrintStream(out); for (int a = 0; a < shapes.size(); a++) { Shape shape = shapes.get(a); ps.println(ShapeStringUtils.toString(shape)); } } finally { try { if (out != null) out.close(); } catch (IOException e) { } } } private static void split(Shape shape, List<Shape> newShapes) { String s = ShapeStringUtils.toString(shape).trim(); List<Object> subpaths = new ArrayList<Object>(); while (s.indexOf('m') != -1) { int i = s.indexOf('m'); int i2 = s.indexOf('m', i + 1); if (i2 != -1) { subpaths.add(s.substring(i, i2).trim()); s = s.substring(i2).trim(); } else { subpaths.add(s); s = ""; } } for (int b = 0; b < subpaths.size(); b++) { String source = (String) subpaths.get(b); AreaX area = new AreaX( ShapeStringUtils.createGeneralPath(source)); subpaths.set(b, new Object[] { source, area }); } for (int b = 0; b < subpaths.size(); b++) { Object[] array1 = (Object[]) subpaths.get(b); for (int c = b + 1; c < subpaths.size(); c++) { Object[] array2 = (Object[]) subpaths.get(c); int rel = ((AreaX) array1[1]).getRelationship(null, ((AreaX) array2[1]), null); if (rel != AreaX.RELATIONSHIP_NONE) { String newPath = ((String) array1[0]) + " " + ((String) array2[0]); array1[0] = newPath; AreaX t = new AreaX( ShapeStringUtils.createGeneralPath(newPath)); array1[1] = t; subpaths.remove(c); c = b; } } String source = (String) array1[0]; newShapes.add(ShapeStringUtils.createGeneralPath(source)); } } private void add(String shape) { add(ShapeStringUtils.createGeneralPath(shape)); } private void add(Shape shape) { shapes.add(shape); } public String getName() { return name; } @Override public String toString() { return getName(); } public Rectangle2D getBounds() { Rectangle2D sum = null; for (int a = 0; a < shapes.size(); a++) { Shape shape = shapes.get(a); try { Rectangle2D r = ShapeBounds.getBounds(shape); if (sum == null) { sum = r; } else { sum.add(r); } } catch (EmptyPathException e) { } } return sum; } public BufferedImage render() { Rectangle2D bounds = getBounds(); Rectangle r = bounds.getBounds(); BufferedImage bi = new BufferedImage(r.width, r.height, BufferedImage.TYPE_INT_ARGB); Graphics2D g = bi.createGraphics(); g.translate(-r.x, -r.y); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); for (int a = 0; a < shapes.size(); a++) { float hue = (a) / 11f; float bri = .75f + .25f * a / ((shapes.size())); Shape shape = shapes.get(a); g.setColor(new Color(Color.HSBtoRGB(hue, .75f, bri))); g.fill(shape); } g.dispose(); return bi; } } JLabel iconLabel = new JLabel(); List<Case> cases = new ArrayList<Case>(); JLabel caseLabel = new JLabel("Case:"); JComboBox comboBox; PrintStream printStream; public AddRulesTest(PrintStream stream) { printStream = stream; // define the cases: cases.add(new Case("Ambulance.shapes")); cases.add(new Case("Book.shapes")); cases.add(new Case("Campfire.shapes")); // cases.add(new Case("Chalet.shapes")); cases.add(new Case("Cornucopia.shapes")); cases.add(new Case("Dinosaur.shapes")); // cases.add(new Case("Fireworks.shapes")); cases.add(new Case("KingTut.shapes")); cases.add(new Case("Kitten.shapes")); cases.add(new Case("Library.shapes")); cases.add(new Case("Lincoln.shapes")); cases.add(new Case("Lion.shapes")); cases.add(new Case("Pancakes.shapes")); cases.add(new Case("RomanRelief.shapes")); cases.add(new Case("SpaceShuttle.shapes")); cases.add(new Case("Tiger.shapes")); cases.add(new Case("Truck.shapes")); cases.add(new Case("Washington.shapes")); cases.add(new Case("Windmill.shapes")); cases.add(new Case("Wren.shapes")); cases.add(new Case("BookSplit.shapes")); cases.add(new Case("KingTutSplit.shapes")); cases.add(new Case("TruckSplit.shapes")); cases.add(new Case("WashingtonSplit.shapes")); cases.add(new Case("WindmillSplit.shapes")); comboBox = new JComboBox(); comboBox.addItem("All"); for (int a = 0; a < cases.size(); a++) { comboBox.addItem(cases.get(a)); } comboBox.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Object value = comboBox.getSelectedItem(); if (value instanceof Case) { iconLabel.setIcon(new ImageIcon(((Case) value).image)); } else { iconLabel.setIcon(null); } } }); } public static BufferedImage scale(BufferedImage render, Dimension maxSize) { Dimension imageSize = new Dimension(render.getWidth(), render.getHeight()); Dimension newSize = scaleDimensionsProportionally(imageSize, maxSize); BufferedImage newImage = new BufferedImage(newSize.width, newSize.height, BufferedImage.TYPE_INT_ARGB); Graphics2D g = newImage.createGraphics(); double sx = ((double) newSize.width) / ((double) imageSize.width); double sy = ((double) newSize.height) / ((double) imageSize.height); g.scale(sx, sy); g.drawImage(render, 0, 0, null); g.dispose(); return newImage; } /** * Copied from Scaling.java to simplify class dependencies. This is a * convenience method to calculate how to scale down an image * proportionally. * * @param originalSize * the original image dimensions. * @param maxSize * the maximum new dimensions. * @return dimensions that are <code>maxSize</code> or smaller. */ public static Dimension scaleDimensionsProportionally( Dimension originalSize, Dimension maxSize) { float widthRatio = ((float) maxSize.width) / ((float) originalSize.width); float heightRatio = ((float) maxSize.height) / ((float) originalSize.height); int w, h; if (widthRatio < heightRatio) { w = maxSize.width; h = (int) (widthRatio * originalSize.height); } else { h = maxSize.height; w = (int) (heightRatio * originalSize.width); } return new Dimension(w, h); } @Override public JComponent getComponent() { if (panel == null) { panel = new JPanel(new GridBagLayout()); panel.setOpaque(false); JPanel controls = new JPanel(); controls.setOpaque(false); GridBagConstraints c = new GridBagConstraints(); c.gridx = 0; c.gridy = 0; c.weightx = 1; c.weighty = 0; c.fill = GridBagConstraints.HORIZONTAL; c.gridwidth = GridBagConstraints.REMAINDER; c.insets = new Insets(8, 8, 8, 8); panel.add(description, c); c.gridwidth = 1; c.weightx = 1; c.weighty = 1; c.gridy++; c.anchor = GridBagConstraints.NORTH; c.fill = GridBagConstraints.HORIZONTAL; panel.add(controls, c); c.gridx++; panel.add(iconLabel, c); Inspector layout = new Inspector(controls); addControls(layout); layout.addRow(null, progress, false); layout.addRow(start, false); layout.addRow(cancel, false); } return panel; } @Override public void addControls(Inspector layout) { layout.addRow(caseLabel, comboBox, false); } @Override public void doTest() { Object value = comboBox.getSelectedItem(); Case[] cases; if (value instanceof Case) { cases = new Case[] { (Case) value }; } else { // all cases = this.cases.toArray(new Case[this.cases.size()]); } AreaXRules[] rules = new AreaXRules[] { new AreaXRules(), new BoundsRules(true, false), new BoundsRules(false, true), new BoundsRules(true, true) }; String[] names = new String[] { "No Rules", "Inner", "Merging", "Both" }; long[][] times = new long[cases.length][rules.length + 1]; long[] tempList = new long[5]; // how many times we repeat each method Area goldenStandard = null; for (int a = 0; a < cases.length; a++) { if (cases.length > 1) comboBox.setSelectedIndex(a + 1); float base = ((float) a) / ((float) cases.length); for (int b = 0; b < tempList.length; b++) { System.gc(); System.runFinalization(); if (cancelled) return; float m = base + ((float) b) / ((float) tempList.length) / (cases.length) / (rules.length + 1f); progress.setValue((int) (m * (progress.getMaximum() - progress .getMinimum()))); tempList[b] = System.currentTimeMillis(); Area sum = new Area(); for (int c = 0; c < cases[a].shapes.size(); c++) { sum.add(new Area(cases[a].shapes.get(c))); } goldenStandard = sum; sum.getPathIterator(null); tempList[b] = System.currentTimeMillis() - tempList[b]; } Arrays.sort(tempList); times[a][0] = tempList[tempList.length / 2]; for (int i = 0; i < rules.length; i++) { AreaX copy = null; for (int b = 0; b < tempList.length; b++) { System.gc(); System.runFinalization(); if (cancelled) return; float m = base + (i + 1f) / (rules.length + 1f) / (cases.length) + ((float) b) / ((float) tempList.length) / (cases.length) / 3f; progress.setValue((int) (m * (progress.getMaximum() - progress .getMinimum()))); tempList[b] = System.currentTimeMillis(); AreaX sum = new AreaX(); sum.setRules(rules[i]); for (int c = 0; c < cases[a].shapes.size(); c++) { sum.add((cases[a].shapes.get(c))); } sum.getPathIterator(null); copy = sum; tempList[b] = System.currentTimeMillis() - tempList[b]; } if (equals(goldenStandard, copy, printStream) == false) { reportFailure(cases[a].getName() + " - " + names[i], goldenStandard, copy); } Arrays.sort(tempList); times[a][i + 1] = tempList[tempList.length / 2]; } } if (cases.length > 1) comboBox.setSelectedIndex(0); String header = new String("\tArea"); for (int a = 0; a < names.length; a++) { header = header + "\t" + names[a]; } for (int a = 0; a < names.length; a++) { header = header + "\t" + names[a] + " (%)"; } printStream.println(header); for (int a = 0; a < times.length; a++) { String line = cases[a].getName() + "\t" + times[a][0]; for (int i = 1; i < times[a].length; i++) { line = line + "\t" + times[a][i]; } for (int i = 1; i < times[a].length; i++) { double percent = ((double) times[a][i]) / ((double) times[a][0]) * 100; line = line + "\t" + percent; } printStream.println(line); } } static int ctr = 0; private void reportFailure(String id, Shape good, Shape bad) { Rectangle2D r = good.getBounds2D(); r.add(bad.getBounds2D()); BufferedImage image = new BufferedImage(500, 500, BufferedImage.TYPE_INT_ARGB); Graphics2D g = image.createGraphics(); g.transform(RectangularTransform.create(r, new Rectangle(0, 0, image.getWidth(), image.getHeight()))); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setColor(Color.blue); g.fill(good); g.setColor(new Color(255, 0, 0, 128)); g.fill(bad); g.dispose(); printStream.println("the resulting shape for \"" + id + "\" wasn't correct."); try { ImageIO.write(image, "png", new File("comparison " + (ctr++) + ".png")); } catch (IOException e) { e.printStackTrace(); } } @Override public String getDescription() { return "This examines the time taken to calculate the total outline of a variety of clip art using 3 different approaches: the Area class, the AreaX class with no rules applied, and the AreaX class with merging rules applied.\n\n" + "(Only the shape data of each piece of clip art was preserved. As you thumb through the list of clip art a thumbnail will appear to the right using vivid semi-random colors just to communicate the basic composition of the clip art.)"; } @Override public String getName() { return "Add Rules Test"; } }