/*
* This file is part of Caliph & Emir.
*
* Caliph & Emir is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* Caliph & Emir 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Caliph & Emir; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* Copyright statement:
* --------------------
* (c) 2002-2005 by Mathias Lux (mathias@juggle.at)
* http://www.juggle.at, http://caliph-emir.sourceforge.net
*/
package at.lux.fotoretrieval.panels;
import at.lux.fotoretrieval.EmirConfiguration;
import at.lux.retrieval.calculations.DistanceMatrix;
import at.lux.retrieval.fdp.FDP;
import at.lux.retrieval.fdp.FDPParameters;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.Arrays;
import java.lang.reflect.Array;
/**
* Date: 14.01.2005
* Time: 00:00:38
*
* @author Mathias Lux, mathias@juggle.at
*/
public class Visualization2DPanelWithFdp extends JPanel implements MouseMotionListener, MouseListener, MouseWheelListener {
protected EmirConfiguration emirConfiguration = EmirConfiguration.getInstance();
protected boolean demoMode = emirConfiguration.getBoolean("emir.demomode");
private float points[][], initialPoints[][];
private DistanceMatrix matrixFastmap;
private float maxX = 0f, maxY = 0f, minX = 0f, minY = 0f;
private java.util.List<String> fileList;
private static final int OFFSET = 20;
private java.util.List<BufferedImage> imageList;
private final float IMG_MAXIMUM_SIDE = emirConfiguration.getFloat("MdsVisPanel.ImageLoader.MaxImageSideLength");
private double thumbSizeModifier = 0.5;
private int imagesLoaded = 0;
private AffineTransform transform;
private double moveX = 0d, moveY = 0d;
private FdpThread fdpThread;
private double zoom = 1d;
private boolean showPleaseWait = false;
private boolean showPleaseWaitAllowed = emirConfiguration.getBoolean("MdsVisPanel.ImageLoader.ShowPleaseWait");
private boolean antiAliased = true;
private ImageLoaderThread imageLoaderThread;
private enum MouseState {
NONE, BUTTON1_PRESSED
}
;
private MouseState state = MouseState.NONE;
private Point lastPoint = null;
private FDP fdp = null;
/**
* Creates a new <code>JPanel</code> with a double buffer
* and a flow layout.
*/
public Visualization2DPanelWithFdp(float[][] points, DistanceMatrix matrixFastmap, java.util.List<String> files, boolean autoStartFDP) {
this.points = points;
initialPoints = new float[points.length][points[0].length];
for (int i = 0; i < points.length; i++) {
float[] point = points[i];
for (int j = 0; j < point.length; j++) {
initialPoints[i][j] = point[j];
}
}
this.matrixFastmap = matrixFastmap;
fileList = files;
addMouseMotionListener(this);
addMouseListener(this);
addMouseWheelListener(this);
init();
imageLoaderThread = new ImageLoaderThread(this);
new Thread(imageLoaderThread).start();
FDPParameters fdpParameters = EmirConfiguration.getInstance().getFDPParameters();
fdpParameters.setGravitation(3d / Math.sqrt(points.length));
fdp = new FDP(matrixFastmap, fdpParameters, points);
if (autoStartFDP) {
fdpThread = new FdpThread(fdp, this);
fdpThread.start();
antiAliased = false;
showPleaseWait = true;
}
}
protected void reinit() {
for (int i = 0; i < points.length; i++) {
float[] point = points[i];
for (int j = 0; j < point.length; j++) {
point[j] = initialPoints[i][j];
}
}
init();
imageLoaderThread.stopThread();
imagesLoaded = 0;
imageLoaderThread = new ImageLoaderThread(this);
new Thread(imageLoaderThread).start();
FDPParameters fdpParameters = EmirConfiguration.getInstance().getFDPParameters();
fdpParameters.setGravitation(3d / Math.sqrt(points.length));
fdp = new FDP(matrixFastmap, fdpParameters, points);
fdpThread = new FdpThread(fdp, this);
fdpThread.start();
antiAliased = false;
showPleaseWait = true;
}
private void init() {
imageList = new LinkedList<BufferedImage>();
for (int i = 0; i < points.length; i++) {
float[] point = points[i];
if (point[0] > maxX) maxX = point[0];
if (point[1] > maxY) maxY = point[1];
if (point[0] < minX) minX = point[0];
if (point[1] < minY) minX = point[1];
}
}
/**
* Allows the iterative loading of image files ...
*
* @return true if another image file can be loaded, false otherwise.
*/
public boolean initNextImage() {
if (imagesLoaded < points.length) {
String pathname = fileList.get(imagesLoaded);
pathname = pathname.replace(".mp7.xml", ".jpg");
File f = new File(pathname);
BufferedImage bi = null;
try {
// System.out.println("Reading file " + imagesLoaded + ": " + pathname);
bi = ImageIO.read(f);
if (bi != null) bi = resize(bi);
} catch (IOException e) {
System.err.println("Error reading image " + pathname);
}
imageList.add(bi);
}
imagesLoaded++;
repaint();
if (imagesLoaded > points.length)
return false;
else
return true;
}
private BufferedImage resize(BufferedImage img) {
int height = img.getHeight();
int width = img.getWidth();
float scaleFactor = ((float) width) / IMG_MAXIMUM_SIDE;
if (height > width) {
scaleFactor = ((float) height) / IMG_MAXIMUM_SIDE;
}
int widthNew = (int) (((float) width) / scaleFactor);
int heightNew = (int) (((float) height) / scaleFactor);
BufferedImage bi = new BufferedImage(widthNew, heightNew, BufferedImage.TYPE_INT_RGB);
bi.getGraphics().drawImage(img, 0, 0, bi.getWidth(), bi.getHeight(), null);
return bi;
}
/**
* Calls the UI delegate's paint method, if the UI delegate
* is non-<code>null</code>. We pass the delegate a copy of the
* <code>Graphics</code> object to protect the rest of the
* paint code from irrevocable changes
* (for example, <code>Graphics.translate</code>).
* <p/>
* If you override this in a subclass you should not make permanent
* changes to the passed in <code>Graphics</code>. For example, you
* should not alter the clip <code>Rectangle</code> or modify the
* transform. If you need to do these operations you may find it
* easier to create a new <code>Graphics</code> from the passed in
* <code>Graphics</code> and manipulate it. Further, if you do not
* invoker super's implementation you must honor the opaque property,
* that is
* if this component is opaque, you must completely fill in the background
* in a non-opaque color. If you do not honor the opaque property you
* will likely see visual artifacts.
* <p/>
* The passed in <code>Graphics</code> object might
* have a transform other than the identify transform
* installed on it. In this case, you might get
* unexpected results if you cumulatively apply
* another transform.
*
* @param g the <code>Graphics</code> object to protect
* @see #paint
* @see javax.swing.plaf.ComponentUI
*/
protected void paintComponent(Graphics g) {
maxX = getMax(0);
maxY = getMax(1);
minX = getMin(0);
minY = getMin(1);
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
// if (true)
if (antiAliased)
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
else
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
// save old transformations ...
AffineTransform old = g2.getTransform();
// erase background
g2.setColor(Color.black);
g2.fillRect(0, 0, getWidth(), getHeight());
g2.setColor(Color.green);
// Apply move
transform = AffineTransform.getTranslateInstance(moveX, moveY);
AffineTransform scale = AffineTransform.getScaleInstance(zoom, zoom);
transform.concatenate(scale);
g2.setTransform(transform);
float width = this.getWidth() - 2 * OFFSET;
float height = this.getHeight() - 2 * OFFSET;
for (int i = 0; i < points.length; i++) {
float[] point = points[i];
float x = (point[0] - minX) / (maxX - minX);
float y = (point[1] - minY) / (maxY - minY);
int projectionX = (int) (x * width) + OFFSET;
int projectionY = (int) (y * height) + OFFSET;
BufferedImage bi = null;
if (i < imageList.size()) bi = imageList.get(i);
if (bi != null) {
int bimgWidth = (int) Math.round((double) bi.getWidth() * thumbSizeModifier);
int bimgHeight = (int) Math.round((double) bi.getHeight() * thumbSizeModifier);
g2.drawImage(bi, projectionX - bimgWidth / 2, projectionY - bimgHeight / 2, bimgWidth, bimgHeight, null);
} else {
g2.fillOval(projectionX - 2, projectionY - 2, 4, 4);
}
}
// restore old transformations:
g2.setTransform(old);
g2.setFont(g2.getFont().deriveFont(Font.ITALIC, 8.5f));
g2.setColor(Color.gray.brighter());
g2.drawString("Click <alt> + <right mouse button> to start / stop FDP, zoom with mouse wheel.", 5, this.getHeight() - 5);
if (showPleaseWait && showPleaseWaitAllowed) {
Font bigFont = g2.getFont().deriveFont(Font.BOLD, 48f);
FontMetrics fontMetrics = g2.getFontMetrics(bigFont);
g2.setFont(bigFont);
g2.setColor(Color.white);
String pleaseWait = "Please Wait!";
int pwWidth = (int) fontMetrics.getStringBounds(pleaseWait, g2).getWidth();
int pwHeight = (int) fontMetrics.getStringBounds(pleaseWait, g2).getHeight();
g2.drawString(pleaseWait, (getWidth() - pwWidth) / 2, (getHeight() - pwHeight) / 2);
// showPleaseWait = false;
}
// g2.setColor(Color.white);
// g2.drawString("Click <alt> + <left mouse button> to start / stop FDP.", 9, this.getHeight()-11);
}
/**
* Invoked when a mouse button is pressed on a component and then
* dragged. <code>MOUSE_DRAGGED</code> events will continue to be
* delivered to the component where the drag originated until the
* mouse button is released (regardless of whether the mouse position
* is within the bounds of the component).
* <p/>
* Due to platform-dependent Drag&Drop implementations,
* <code>MOUSE_DRAGGED</code> events may not be delivered during a native
* Drag&Drop operation.
*/
public void mouseDragged(MouseEvent e) {
// System.out.println("Dragged: " + e.getPoint());
if (lastPoint == null) lastPoint = e.getPoint();
if (state == MouseState.BUTTON1_PRESSED) {
moveX += e.getPoint().x - lastPoint.x;
moveY += e.getPoint().y - lastPoint.y;
repaint();
}
lastPoint = e.getPoint();
}
/**
* Invoked when the mouse cursor has been moved onto a component
* but no buttons have been pushed.
*/
public void mouseMoved(MouseEvent e) {
// System.out.println("Moved: " + e.getPoint());
// if (lastPoint==null) lastPoint = e.getPoint();
// if (state == MouseState.BUTTON1_PRESSED) {
// moveX += e.getPoint().x-lastPoint.x;
// moveY += e.getPoint().y-lastPoint.y;
// }
// lastPoint = e.getPoint();
}
/**
* Invoked when the mouse button has been clicked (pressed
* and released) on a component.
*/
public void mouseClicked(MouseEvent e) {
}
/**
* Invoked when a mouse button has been pressed on a component.
*/
public void mousePressed(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) {
state = MouseState.BUTTON1_PRESSED;
} else if (e.getButton() == MouseEvent.BUTTON3 && e.isAltDown()) {
if (fdpThread != null && fdpThread.isRunning()) {
fdpThread.setRunning(false);
} else {
fdpThread = new FdpThread(fdp, this);
showPleaseWait = true;
antiAliased = false;
repaint();
fdpThread.start();
}
}
}
/**
* Invoked when a mouse button has been released on a component.
*/
public void mouseReleased(MouseEvent e) {
state = MouseState.NONE;
lastPoint = null;
}
/**
* Invoked when the mouse enters a component.
*/
public void mouseEntered(MouseEvent e) {
}
/**
* Invoked when the mouse exits a component.
*/
public void mouseExited(MouseEvent e) {
}
/**
* Invoked when the mouse wheel is rotated.
*
* @see java.awt.event.MouseWheelEvent
*/
public void mouseWheelMoved(MouseWheelEvent e) {
if (e.getModifiersEx() == MouseEvent.ALT_DOWN_MASK)
thumbSizeModifier += (double) e.getWheelRotation() / 10.0;
else
increaseZoom(((double) e.getWheelRotation()) / 10d);
repaint();
}
/**
* Sets the new zoom and recalculates the center of the zoom.
*
* @param amount
*/
private void increaseZoom(double amount) {
double zoomDifference = amount;
// added max zoomout ...
zoom = Math.max(zoom + amount, 0.01d);
Point2D.Double centerPoint = new Point2D.Double(((double) (getWidth() >> 1)), ((double) (getHeight() >> 1)));
AffineTransform transform = AffineTransform.getTranslateInstance(moveX, moveY);
AffineTransform scale = AffineTransform.getScaleInstance(zoom, zoom);
transform.concatenate(scale);
try {
centerPoint = (Point2D.Double) transform.inverseTransform(centerPoint, null);
moveX = (moveX - (zoomDifference * centerPoint.getX()));
moveY = (moveY - (zoomDifference * centerPoint.getY()));
} catch (NoninvertibleTransformException e) {
System.err.println("Error: " + e.toString());
}
}
/*
private void setZoomFactor() {
int centerX = this.getWidth() / 2;
int centerY = this.getHeight() / 2;
Point2D p2 = new Point2D.Double(centerX, centerY);
AffineTransform scaleTransformation = AffineTransform.getScaleInstance(zoom, zoom);
AffineTransform translateTransformation = AffineTransform.getTranslateInstance(moveX, moveY);
translateTransformation.concatenate(scaleTransformation);
Point2D dp = null;
double zoomDiff = zoomFactor;
zoomFactor = singleZoomFactor*singleZoomFactor;
zoomDiff = zoomFactor - zoomDiff;
try {
dp = translateTransformation.inverseTransform(p2, null);
translateX = translateX - (zoomDiff * dp.getX());
translateY = translateY - (zoomDiff * dp.getY());
} catch (NoninvertibleTransformException e) {
log.error(e.toString());
e.printStackTrace();
}
// translate.setLocation(translate.x - (int) , translate.y - (int) (v*centerY));
// System.out.println("Zoomfactor: " + zoomFactor);
repaint();
}
*/
private float getMax(int index) {
float f = points[0][index];
for (int i = 1; i < points.length; i++) {
if (f < points[i][index]) f = points[i][index];
}
return f;
}
private float getMin(int index) {
float f = points[0][index];
for (int i = 1; i < points.length; i++) {
if (f > points[i][index]) f = points[i][index];
}
return f;
}
public boolean isShowPleaseWait() {
return showPleaseWait;
}
public void setShowPleaseWait(boolean showPleaseWait) {
this.showPleaseWait = showPleaseWait;
}
public boolean isAntiAliased() {
return antiAliased;
}
public void setAntiAliased(boolean antiAliased) {
this.antiAliased = antiAliased;
}
}
class ImageLoaderThread implements Runnable {
private Visualization2DPanelWithFdp panel;
private EmirConfiguration emirConfiguration = EmirConfiguration.getInstance();
int imageLoaderStartWait = emirConfiguration.getInt("MdsVisPanel.ImageLoader.StartWait");
int imageLoaderStepWait = emirConfiguration.getInt("MdsVisPanel.ImageLoader.StepWait");
private boolean isRunning = true;
public ImageLoaderThread(Visualization2DPanelWithFdp panel) {
this.panel = panel;
}
public void run() {
try {
Thread.sleep(imageLoaderStartWait);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (panel.initNextImage() && isRunning) {
try {
Thread.sleep(imageLoaderStepWait);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
isRunning = false;
}
public void stopThread() {
isRunning = false;
}
}
class FdpThread extends Thread {
FDP fdp;
JPanel panel;
private EmirConfiguration emirConfiguration = EmirConfiguration.getInstance();
private float STOP_CONDITION = emirConfiguration.getFloat("MdsVisPanel.FDP.StopCondition");
private int fdpStepWait = emirConfiguration.getInt("MdsVisPanel.FDP.StepWait");
private int fdpStartWait = emirConfiguration.getInt("MdsVisPanel.FDP.StartWait");
private boolean running = false;
private Visualization2DPanelWithFdp vPanel = null;
public FdpThread(FDP fdp, JPanel panel) {
this.fdp = fdp;
this.panel = panel;
init();
}
public FdpThread(FDP fdp, int fdpStepWait, JPanel panel, float stopAtMovement) {
this.fdp = fdp;
this.fdpStepWait = fdpStepWait;
this.panel = panel;
this.STOP_CONDITION = stopAtMovement;
init();
}
public FdpThread(FDP fdp, int fdpStepWait, JPanel panel) {
this.fdp = fdp;
this.fdpStepWait = fdpStepWait;
this.panel = panel;
init();
}
private void init() {
if (panel instanceof Visualization2DPanelWithFdp)
vPanel = (Visualization2DPanelWithFdp) panel;
}
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p/>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see Thread#run()
*/
public void run() {
running = true;
float currentMovement = -100;
try {
// fdpStartWait = 1000;
Thread.sleep(fdpStartWait);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 1000 && running; i++) {
try {
Thread.sleep(fdpStepWait);
} catch (InterruptedException e) {
e.printStackTrace();
}
fdp.step();
if (vPanel != null) {
vPanel.setShowPleaseWait(false);
}
if (currentMovement > 0 && (Math.abs(currentMovement - fdp.getCurrentMovement()) < STOP_CONDITION)) {
System.out.println("Needed " + i + " steps for stable layout.");
break;
}
currentMovement = fdp.getCurrentMovement();
// System.out.println("Current movement: " + Math.abs(currentMovement-fdp.getCurrentMovement()));
panel.repaint();
}
// System.out.println("No stable layout reached within time.");
if (vPanel != null) {
vPanel.setAntiAliased(true);
}
panel.repaint();
running = false;
if (vPanel.demoMode) {
try {
Thread.sleep(7000);
} catch (InterruptedException e) {
e.printStackTrace();
}
vPanel.reinit();
}
}
public boolean isRunning() {
return running;
}
public void setRunning(boolean running) {
this.running = running;
}
}