| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.sis.internal.processing.isoline; |
| |
| import java.util.Map; |
| import java.util.EnumMap; |
| import java.awt.Shape; |
| import java.awt.Color; |
| import java.awt.Graphics; |
| import java.awt.Graphics2D; |
| import java.awt.BasicStroke; |
| import java.awt.EventQueue; |
| import java.awt.BorderLayout; |
| import java.awt.Container; |
| import java.awt.Rectangle; |
| import java.awt.event.ActionEvent; |
| import java.awt.event.ActionListener; |
| import java.awt.geom.Path2D; |
| import java.awt.geom.PathIterator; |
| import java.awt.image.RenderedImage; |
| import javax.swing.Timer; |
| import javax.swing.JFrame; |
| import javax.swing.JPanel; |
| import javax.swing.JLabel; |
| import javax.swing.JButton; |
| import javax.swing.JComponent; |
| import javax.swing.ButtonModel; |
| import javax.swing.event.ChangeEvent; |
| import javax.swing.event.ChangeListener; |
| import java.util.function.BiConsumer; |
| import java.util.concurrent.CountDownLatch; |
| import org.opengis.referencing.operation.TransformException; |
| import org.apache.sis.internal.referencing.j2d.AffineTransform2D; |
| |
| import static org.junit.Assert.*; |
| |
| |
| /** |
| * A viewer for showing isoline generation step-by-step. |
| * For enabling the use of this class, temporarily remove {@code private} and {@code final} keywords in |
| * {@link Isolines#LISTENER}, then uncomment the {@link #setListener(StepsViewer)} constructor body. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| * @version 1.3 |
| * @since 1.3 |
| * @module |
| */ |
| @SuppressWarnings("serial") |
| public final class StepsViewer extends JComponent implements BiConsumer<String,Isolines>, ChangeListener, ActionListener { |
| /** |
| * Sets the component to be notified after each row of isolines generated from the rendered image. |
| * The body of this method is commented-out because {@link Isolines#LISTENER} is private and final. |
| * The body should be uncommented only temporarily during debugging phases. |
| */ |
| private static void setListener(final StepsViewer listener) { |
| // Isolines.LISTENER = listener; |
| } |
| |
| /** |
| * Entry point for debugging. Edit this method body as needed for loading an image to use as test data. |
| * |
| * @param args ignored. |
| * @throws Exception if an error occurred during I/O or isoline generation. |
| */ |
| public static void main(final String[] args) throws Exception { |
| // showStepByStep(local.test.DebugIsoline.data(), 0); |
| } |
| |
| /** |
| * Size of the window and spacing between borders and isolines. All values are in pixels. |
| */ |
| private static final int CANVAS_WIDTH = 1600, CANVAS_HEIGHT = 1000, PADDING = 3; |
| |
| /** |
| * Whether to flip X and/or Y axis. |
| */ |
| private static final boolean FLIP_X = false, FLIP_Y = true; |
| |
| /** |
| * Description of current step. This title is updated at each isoline generation step, |
| * when {@link #accept(String, Shape)} is invoked. |
| */ |
| private final JLabel stepTitle; |
| |
| /** |
| * The button for moving to the next step. When this button is enabled, the isoline process is blocked |
| * by {@link #blocker} until this button is pressed. When this button is pressed, the isoline process |
| * continue until {@link #accept(String, Shape)} is invoked again. |
| * |
| * @see #actionPerformed(ActionEvent) |
| */ |
| private final JButton next; |
| |
| /** |
| * Simulate a "next" action after some delay. This is used when users keep the "Next" button pressed. |
| */ |
| private final Timer delayedNext; |
| |
| /** |
| * Blocks the isoline computation thread until the user is ready to see the next step. |
| */ |
| private CountDownLatch blocker; |
| |
| /** |
| * The isolines to show. |
| */ |
| private final Map<PolylineStage,Path2D> isolines; |
| |
| /** |
| * The colors to associate to the isoline for each stage. |
| * Array indices are {@link PolylineStage#ordinal()} values. |
| */ |
| private final Color[] stageColors; |
| |
| /** |
| * Bounds of {@link #isolines}, slightly expanded for making easier to see. |
| */ |
| private Rectangle bounds; |
| |
| /** |
| * Conversion from pixel indices in the source image to pixel indices in the displayed window. |
| */ |
| private final AffineTransform2D sourceToCanvas; |
| |
| /** |
| * Creates a new viewer. |
| * |
| * @param data the source of data for isolines. |
| * @param pane the container where to add components. |
| */ |
| @SuppressWarnings("ThisEscapedInObjectConstruction") |
| private StepsViewer(final RenderedImage data, final Container pane) { |
| isolines = new EnumMap<>(PolylineStage.class); |
| stageColors = new Color[] {Color.YELLOW, Color.CYAN, Color.GRAY}; |
| setBackground(Color.BLACK); |
| setOpaque(true); |
| final double scaleX = (CANVAS_WIDTH - 2*PADDING) / (double) data.getWidth(); |
| final double scaleY = (CANVAS_HEIGHT - 2*PADDING) / (double) data.getHeight(); |
| sourceToCanvas = new AffineTransform2D( |
| FLIP_X ? -scaleX : scaleX, 0, 0, FLIP_Y ? -scaleY : scaleY, |
| scaleX * (PADDING + data.getMinX() + (FLIP_X ? data.getWidth() : 0)), |
| scaleY * (PADDING + data.getMinY() + (FLIP_Y ? data.getHeight() : 0))); |
| |
| stepTitle = new JLabel(); |
| next = new JButton("Next"); |
| next.setEnabled(false); |
| next.addActionListener(this); |
| next.getModel().addChangeListener(this); |
| delayedNext = new Timer(1000, this::fastForward); // 1 second delay before fast forward. |
| delayedNext.setRepeats(false); |
| |
| final JPanel bar = new JPanel(new BorderLayout()); |
| bar .add(stepTitle, BorderLayout.CENTER); |
| bar .add(next, BorderLayout.EAST); |
| pane.add(bar, BorderLayout.NORTH); |
| pane.add(this, BorderLayout.CENTER); |
| } |
| |
| /** |
| * Generates isolines for the given image and show the result step by step. |
| * The given image shall have only one band. |
| * |
| * @param data the source of data for isolines. |
| * @param levels levels of isolones to generate. |
| */ |
| public static void showStepByStep(final RenderedImage data, final double... levels) { |
| assertEquals("Unsupported number of bands.", 1, data.getSampleModel().getNumBands()); |
| final JFrame frame = new JFrame("Step-by-step isoline viewer"); |
| frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); |
| frame.setLayout(new BorderLayout()); |
| final StepsViewer viewer = new StepsViewer(data, frame.getContentPane()); |
| final Isolines iso; |
| try { |
| setListener(viewer); |
| frame.setVisible(true); |
| frame.setSize(CANVAS_WIDTH, CANVAS_HEIGHT); |
| iso = Isolines.generate(data, new double[][] {levels}, null)[0]; |
| } catch (TransformException e) { |
| throw new AssertionError(e); // Should not happen because we specified an identity transform. |
| } finally { |
| setListener(null); |
| } |
| final Path2D path = new Path2D.Float(); |
| for (final Shape shape : iso.polylines().values()) { |
| path.append(shape, false); |
| } |
| } |
| |
| /** |
| * Invoked when the isolines need to be drawn. |
| */ |
| @Override |
| protected void paintComponent(final Graphics g) { |
| super.paintComponent(g); |
| final Graphics2D gh = (Graphics2D) g; |
| gh.setColor(getBackground()); |
| gh.fillRect(0, 0, getWidth(), getHeight()); |
| if (bounds != null) { |
| gh.setStroke(new BasicStroke(2)); |
| gh.setColor(Color.RED); |
| gh.draw(bounds); |
| } |
| for (final Map.Entry<PolylineStage,Path2D> entry : isolines.entrySet()) { |
| final int stage = entry.getKey().ordinal(); |
| gh.setStroke(new BasicStroke(stageColors.length - stage)); |
| gh.setColor(stageColors[stage]); |
| gh.draw(entry.getValue()); |
| } |
| } |
| |
| /** |
| * Returns {@code true} if the shapes described by given iterators are equal. |
| * This is used for deciding if it is worth to bother the user with a request |
| * for pressing the "Next" button. |
| */ |
| private static boolean equal(final PathIterator it1, final PathIterator it2) { |
| final float[] a1 = new float[6]; |
| final float[] a2 = new float[6]; |
| while (!it1.isDone()) { |
| if (it2.isDone()) return false; |
| final int code = it1.currentSegment(a1); |
| if (code != it2.currentSegment(a2)) { |
| return false; |
| } |
| int n; |
| switch (code) { |
| case PathIterator.SEG_MOVETO: |
| case PathIterator.SEG_LINETO: n = 2; break; |
| case PathIterator.SEG_QUADTO: n = 4; break; |
| case PathIterator.SEG_CUBICTO: n = 6; break; |
| case PathIterator.SEG_CLOSE: n = 0; break; |
| default: throw new AssertionError(code); |
| } |
| while (--n >= 0) { |
| if (Float.floatToIntBits(a1[n]) != Float.floatToIntBits(a2[n])) { |
| return false; |
| } |
| } |
| it1.next(); |
| it2.next(); |
| } |
| return it2.isDone(); |
| } |
| |
| /** |
| * Invoked after a row has been processed during the isoline generation. |
| * This is invoked from the main thread (<strong>not</strong> the Swing thread). |
| * |
| * @param title description of current state. |
| * @param generator new generator of isolines. |
| */ |
| @Override |
| public void accept(final String title, final Isolines generator) { |
| final Map<PolylineStage, Path2D> paths = generator.toRawPath(); |
| for (final Map.Entry<PolylineStage,Path2D> entry : paths.entrySet()) { |
| entry.getValue().transform(sourceToCanvas); |
| } |
| try { |
| final CountDownLatch c = new CountDownLatch(1); |
| EventQueue.invokeLater(() -> { |
| Rectangle b = null; |
| boolean unchanged = true; |
| for (final PolylineStage stage : PolylineStage.values()) { |
| final Path2D current = isolines.get(stage); |
| final Path2D update = paths.get(stage); |
| if (unchanged && current != update && !(current != null && update != null && |
| equal(current.getPathIterator(null), update.getPathIterator(null)))) |
| { |
| unchanged = false; |
| } |
| if (update == null) { |
| isolines.remove(stage); |
| } else { |
| isolines.put(stage, update); |
| if (stage == PolylineStage.BUFFER) { |
| b = update.getBounds(); |
| b.x -= PADDING; |
| b.y -= PADDING; |
| b.width += PADDING * 2; |
| b.height += PADDING * 2; |
| bounds = b; |
| } |
| } |
| } |
| bounds = b; |
| if (unchanged) { |
| stepTitle.setText(title + " (no change)"); |
| c.countDown(); |
| } else { |
| stepTitle.setText(title); |
| repaint(); |
| assertNull(blocker); |
| if (next.getModel().isPressed()) { |
| c.countDown(); |
| } else { |
| blocker = c; |
| next.setEnabled(true); |
| } |
| } |
| }); |
| c.await(); |
| } catch (InterruptedException e) { |
| throw new AssertionError(e); // Stop the test. |
| } |
| } |
| |
| /** |
| * Invoked by Swing when user presses the "Next" button. |
| * This method resumes isoline computation. |
| * |
| * @param event ignored. |
| */ |
| @Override |
| public void actionPerformed(final ActionEvent event) { |
| next.setEnabled(false); |
| if (blocker != null) { |
| blocker.countDown(); |
| blocker = null; |
| } |
| } |
| |
| /** |
| * Invoked when the "Next" button is kept pressed. |
| * The effect is to start the "fast forward" mode. |
| * This method shall be invoked in Swing thread. |
| * |
| * @param event ignored. |
| */ |
| private void fastForward(final ActionEvent event) { |
| if (next.getModel().isPressed()) { |
| if (blocker != null) { |
| blocker.countDown(); |
| blocker = null; |
| } |
| } |
| } |
| |
| /** |
| * Invoked by Swing when the state of the "Next" button (pressed or not) changed. |
| * If the button is pressed one second without being released, then we enter a |
| * "fast forward" mode until the button is released. |
| * |
| * @param event ignored. |
| */ |
| @Override |
| public void stateChanged(final ChangeEvent event) { |
| final ButtonModel m = (ButtonModel) event.getSource(); |
| if (m.isPressed()) { |
| delayedNext.restart(); |
| } else { |
| delayedNext.stop(); |
| } |
| } |
| } |