diff --git a/diylc/diylc-core/src/org/diylc/presenter/DrawingCache.java b/diylc/diylc-core/src/org/diylc/presenter/DrawingCache.java index 8d52b743..462f9882 100644 --- a/diylc/diylc-core/src/org/diylc/presenter/DrawingCache.java +++ b/diylc/diylc-core/src/org/diylc/presenter/DrawingCache.java @@ -31,6 +31,7 @@ import org.diylc.common.ComponentType; import org.diylc.core.ComponentState; import org.diylc.core.IDIYComponent; import org.diylc.core.Project; +import org.diylc.core.annotations.ComponentDescriptor; import org.diylc.utils.Pair; /** @@ -50,7 +51,40 @@ public class DrawingCache { private static final Logger LOG = Logger.getLogger(DrawingCache.class); public static DrawingCache Instance = new DrawingCache(); + + /** + * Prepares cached renderings of those components from the specified map that have {@link ComponentDescriptor#enableCache()} set to true. + * + * @param components + * @param g2d + * @param outlineMode + * @param project + * @param zoom + */ + @SuppressWarnings("unchecked") + public void bulkPrepare(Map, ComponentState> components, G2DWrapper g2d, boolean outlineMode, Project project, double zoom) { + // run all renders in parallel to utilize all CPU cores + components.entrySet().stream().parallel().forEach(x -> { + ComponentType type = ComponentProcessor.getInstance() + .extractComponentTypeFrom((Class>) x.getKey().getClass()); + if (type.getEnableCache()) { + renderAndCache(x.getKey(), g2d, x.getValue(), outlineMode, project, zoom); + } + }); + } + + /** + * Draws a component onto the specified {@link G2DWrapper}. If the component has {@link ComponentDescriptor#enableCache()} set to true, + * the drawing will come from the cache. Otherwise, it will be drawn as usual. + * + * @param component + * @param g2d + * @param componentState + * @param outlineMode + * @param project + * @param zoom + */ @SuppressWarnings("unchecked") public void draw(IDIYComponent component, G2DWrapper g2d, ComponentState componentState, boolean outlineMode, Project project, double zoom) { @@ -60,87 +94,8 @@ public class DrawingCache { if (type.getEnableCache()) { // if we need to apply caching Point firstPoint = component.getControlPoint(0); - - // cache hit! - CacheValue value = null; - if (imageCache.containsKey(component)) - value = imageCache.get(component); - - // only honor the cache if the component hasn't changed in the meantime - if (value == null || !value.getComponent().equalsTo(component) - || value.getState() != componentState || value.getZoom() != zoom) { - // figure out size and placement of buffer image - Rectangle2D rect = component.getCachingBounds(); - int width = (int)Math.round(rect.getWidth() * zoom); - int height = (int)Math.round(rect.getHeight() * zoom); - // calculate the position of the first point relative to the caching bounds - int dx = (int) (firstPoint.x - rect.getX()); - int dy = (int) (firstPoint.y - rect.getY()); - - // create image - BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - // create graphics - Graphics2D cg2d = image.createGraphics(); - - // copy over rendering settings so our cached image has the same features - cg2d.setRenderingHints(g2d.getRenderingHints()); - - // set clip bounds to the whole image - cg2d.setClip(0, 0, width, height); - - // apply zoom - if (Math.abs(1.0 - zoom) > 1e-4) { - cg2d.scale(zoom, zoom); - } - // translate to make the top-left corner of the component match (0, 0) - cg2d.translate(-firstPoint.x + dx, -firstPoint.y + dy); - - // wrap the graphics so we can track - G2DWrapper wrapper = new G2DWrapper(cg2d, zoom); - - // initialize wrapper - wrapper.startedDrawingComponent(); - - long componentStart = System.nanoTime(); - - // now draw the component to the buffer image - component.draw(wrapper, componentState, outlineMode, project, wrapper); - - long componentEnd = System.nanoTime(); - - Pair stats; - String key = component.getClass().getCanonicalName().replace("org.diylc.components.", ""); - if (renderStatsByType.containsKey(key)) { - stats = renderStatsByType.get(key); - } else { - stats = new Pair(new Counter(), new Counter()); - renderStatsByType.put(key, stats); - } - stats.getFirst().add(componentEnd - componentStart); - - // finalize wrapper - wrapper.finishedDrawingComponent(); - - cg2d.dispose(); - - // output cached drawings to separate files -// File outputfile = new File("d:\\tmp\\image_" + component.getName() + ".png"); -// try { -// ImageIO.write(image, "png", outputfile); -// } catch (IOException e) { -// // TODO Auto-generated catch block -// e.printStackTrace(); -// } - - if (wrapper.getCurrentArea() == null || wrapper.getCurrentArea().isEmpty()) { - LOG.warn("Empty outline area detected for " + component.getName()); - } - - // add to the cache - value = new CacheValue(component, image, wrapper.getCurrentArea(), wrapper.getContinuityPositiveAreas(), - wrapper.getContinuityNegativeAreas(), componentState, zoom, dx, dy); - imageCache.put(component, value); - } + + CacheValue value = renderAndCache(component, g2d, componentState, outlineMode, project, zoom); long componentStart = System.nanoTime(); @@ -179,7 +134,7 @@ public class DrawingCache { g2d.stopTracking(); g2d.stopTrackingContinuityArea(); } - + public void logStats() { int totalSizeMB = imageCache.values().stream().map(x -> x.image.getData().getDataBuffer().getSize()).reduce(0, Integer::sum) * 4 / 1024 / 1024; // 4 bytes per pixel LOG.debug(String.format("Render cache contains %d elements, approx size is %d MB", imageCache.size(), totalSizeMB)); @@ -195,6 +150,95 @@ public class DrawingCache { public void clear() { imageCache.clear(); } + + private CacheValue renderAndCache(IDIYComponent component, G2DWrapper g2d, ComponentState componentState, + boolean outlineMode, Project project, double zoom) { + // if we need to apply caching + Point firstPoint = component.getControlPoint(0); + + // cache hit! + CacheValue value = null; + if (imageCache.containsKey(component)) + value = imageCache.get(component); + + // only honor the cache if the component hasn't changed in the meantime + if (value == null || !value.getComponent().equalsTo(component) + || value.getState() != componentState || value.getZoom() != zoom) { + // figure out size and placement of buffer image + Rectangle2D rect = component.getCachingBounds(); + int width = (int)Math.round(rect.getWidth() * zoom); + int height = (int)Math.round(rect.getHeight() * zoom); + // calculate the position of the first point relative to the caching bounds + int dx = (int) (firstPoint.x - rect.getX()); + int dy = (int) (firstPoint.y - rect.getY()); + + // create image + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + // create graphics + Graphics2D cg2d = image.createGraphics(); + + // copy over rendering settings so our cached image has the same features + cg2d.setRenderingHints(g2d.getRenderingHints()); + + // set clip bounds to the whole image + cg2d.setClip(0, 0, width, height); + + // apply zoom + if (Math.abs(1.0 - zoom) > 1e-4) { + cg2d.scale(zoom, zoom); + } + // translate to make the top-left corner of the component match (0, 0) + cg2d.translate(-firstPoint.x + dx, -firstPoint.y + dy); + + // wrap the graphics so we can track + G2DWrapper wrapper = new G2DWrapper(cg2d, zoom); + + // initialize wrapper + wrapper.startedDrawingComponent(); + + long componentStart = System.nanoTime(); + + // now draw the component to the buffer image + component.draw(wrapper, componentState, outlineMode, project, wrapper); + + long componentEnd = System.nanoTime(); + + Pair stats; + String key = component.getClass().getCanonicalName().replace("org.diylc.components.", ""); + if (renderStatsByType.containsKey(key)) { + stats = renderStatsByType.get(key); + } else { + stats = new Pair(new Counter(), new Counter()); + renderStatsByType.put(key, stats); + } + stats.getFirst().add(componentEnd - componentStart); + + // finalize wrapper + wrapper.finishedDrawingComponent(); + + cg2d.dispose(); + + // output cached drawings to separate files +// File outputfile = new File("d:\\tmp\\image_" + component.getName() + ".png"); +// try { +// ImageIO.write(image, "png", outputfile); +// } catch (IOException e) { +// // TODO Auto-generated catch block +// e.printStackTrace(); +// } + + if (wrapper.getCurrentArea() == null || wrapper.getCurrentArea().isEmpty()) { + LOG.warn("Empty outline area detected for " + component.getName()); + } + + // add to the cache + value = new CacheValue(component, image, wrapper.getCurrentArea(), wrapper.getContinuityPositiveAreas(), + wrapper.getContinuityNegativeAreas(), componentState, zoom, dx, dy); + imageCache.put(component, value); + } + + return value; + } private class CacheValue { IDIYComponent component; diff --git a/diylc/diylc-core/src/org/diylc/presenter/DrawingManager.java b/diylc/diylc-core/src/org/diylc/presenter/DrawingManager.java index 49653247..6b0810d2 100644 --- a/diylc/diylc-core/src/org/diylc/presenter/DrawingManager.java +++ b/diylc/diylc-core/src/org/diylc/presenter/DrawingManager.java @@ -254,13 +254,10 @@ public class DrawingManager { // AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f); // g2dWrapper.resetTx(); - - // componentAreaMap.clear(); + + // put components and their states in a map + Map, ComponentState> componentStateMap = new HashMap, ComponentState>(); for (IDIYComponent component : project.getComponents()) { - // Do not draw the component if it's filtered out. - if (filter != null && !filter.testComponent(component)) { - continue; - } ComponentState state = ComponentState.NORMAL; if (drawOptions.contains(DrawOption.SELECTION) && selectedComponents.contains(component)) { if (dragInProgress) { @@ -269,6 +266,23 @@ public class DrawingManager { state = ComponentState.SELECTED; } } + componentStateMap.put(component, state); + } + + boolean outlineMode = drawOptions.contains(DrawOption.OUTLINE_MODE); + + // give the cache a chance to prepare all the components in multiple threads + if (drawOptions.contains(DrawOption.ENABLE_CACHING)) { + DrawingCache.Instance.bulkPrepare(componentStateMap, g2dWrapper, outlineMode, project, zoom); + } + + // componentAreaMap.clear(); + for (IDIYComponent component : project.getComponents()) { + // Do not draw the component if it's filtered out. + if (filter != null && !filter.testComponent(component)) { + continue; + } + ComponentState state = componentStateMap.get(component); // Do not track the area if component is not invalidated and was // drawn in the same state. boolean trackArea = lastDrawnStateMap.get(component) != state; @@ -289,7 +303,7 @@ public class DrawingManager { if (drawOptions.contains(DrawOption.ENABLE_CACHING)) // go through the DrawingCache DrawingCache.Instance.draw(component, g2dWrapper, state, drawOptions.contains(DrawOption.OUTLINE_MODE), project, zoom); else // go stragiht to the wrapper - component.draw(g2dWrapper, state, drawOptions.contains(DrawOption.OUTLINE_MODE), project, g2dWrapper); + component.draw(g2dWrapper, state, outlineMode, project, g2dWrapper); if (g2dWrapper.isTrackingContinuityArea()) { LOG.info("Component " + component.getName() + " of type " + component.getClass().getName() + " did not stop tracking continuity area."); @@ -652,32 +666,53 @@ public class DrawingManager { List newAreas = new ArrayList(); boolean[] consumed = new boolean[areas.size()]; for (int i = 0; i < areas.size(); i++) { + Area a1 = areas.get(i); + Rectangle2D bounds1 = a1.getBounds2D(); + + Map candidates = new HashMap(); + for (int j = i + 1; j < areas.size(); j++) { - if (consumed[j]) - continue; - Area a1 = areas.get(i); +// if (consumed[j]) +// continue; Area a2 = areas.get(j); - Area intersection = null; - if (a1.getBounds2D().intersects(a2.getBounds())) { - intersection = new Area(a1); - intersection.intersect(a2); - } - // if the two areas intersect, make a union and consume the second area - if (intersection != null && !intersection.isEmpty()) { - a1.add(a2); - consumed[j] = true; - } else { // maybe there's a connection between them - for (Connection p : connections) { // use getBounds to optimize the computation, don't get into complex math if not needed - if ((a1.getBounds().contains(p.getP1()) && a2.getBounds().contains(p.getP2()) && a1.contains(p.getP1()) && a2.contains(p.getP2())) || - (a1.getBounds().contains(p.getP2()) && a2.getBounds().contains(p.getP1())) && a1.contains(p.getP2()) && a2.contains(p.getP1())) { - a1.add(a2); - consumed[j] = true; - break; - } + Rectangle2D bounds2 = a2.getBounds2D(); + + // maybe there's a direct connection between them, try that first because it's faster than area overlap + for (Connection p : connections) { // use getBounds to optimize the computation, don't get into complex math if not needed + if ((bounds1.contains(p.getP1()) && bounds2.contains(p.getP2()) && a1.contains(p.getP1()) && a2.contains(p.getP2())) || + (bounds1.contains(p.getP2()) && bounds2.contains(p.getP1())) && a1.contains(p.getP2()) && a2.contains(p.getP1())) { + a1.add(a2); + consumed[j] = true; + break; } } + + if (bounds1.intersects(bounds2)) { + candidates.put(a2, j); + } } + + Set mergeQueue = new HashSet(); + + // run a parallel for-each to determine which areas overlap with a1 and add them to a queue to be added to a1 later + if (candidates.size() > 0) { + candidates.entrySet().parallelStream().forEach(x -> { + Area intersection = new Area(a1); + intersection.intersect(x.getKey()); + if (intersection != null && !intersection.isEmpty()) { + synchronized (mergeQueue) { + mergeQueue.add(x.getKey()); + } + consumed[x.getValue()] = true; + } + }); + } + + // if make a union of all intersecting areas + for(Area a2 : mergeQueue) + a1.add(a2); } + for (int i = 0; i < areas.size(); i++) if (!consumed[i]) newAreas.add(areas.get(i));