/*
* The Unified Mapping Platform (JUMP) is an extensible, interactive GUI
* for visualizing and manipulating spatial features with geometry and attributes.
*
* Copyright (C) 2003 Vivid Solutions
*
* This program 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.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
* For more information, contact:
*
* Vivid Solutions
* Suite #1A
* 2328 Government Street
* Victoria BC V8T 5G5
* Canada
*
* (250)385-6040
* www.vividsolutions.com
*/
package com.vividsolutions.jump.workbench.ui;
import java.awt.*;
import java.awt.Dimension;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseWheelListener;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.text.DecimalFormat;
import java.util.*;
import java.util.List;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import com.vividsolutions.jts.geom.*;
import com.vividsolutions.jts.util.Assert;
import com.vividsolutions.jump.feature.Feature;
import com.vividsolutions.jump.util.Blackboard;
import com.vividsolutions.jump.workbench.model.*;
import com.vividsolutions.jump.workbench.ui.cursortool.CursorTool;
import com.vividsolutions.jump.workbench.ui.cursortool.DummyTool;
import com.vividsolutions.jump.workbench.ui.cursortool.LeftClickFilter;
import com.vividsolutions.jump.workbench.ui.renderer.RenderingManager;
import com.vividsolutions.jump.workbench.ui.renderer.java2D.Java2DConverter;
import com.vividsolutions.jump.workbench.ui.renderer.style.PinEqualCoordinatesStyle;
import com.vividsolutions.jump.workbench.ui.zoom.PanTool;
import com.vividsolutions.jump.workbench.ui.zoom.ZoomTool;
import com.vividsolutions.jump.workbench.ui.cursortool.QuasimodeTool;
//<<TODO:FIX>> One user (GK) gets an infinite repaint loop (the map moves around
//chaotically) when the LayerViewPanel is put side by side with the LayerTreePanel
//in a GridBagLayout. Something to do with determining the size, I think --
//the problem doesn't occur when the size is well defined (as when the two
//panels are in a GridLayout or SplitPane). [Jon Aquino]
/**
* Be sure to call #dispose() when the LayerViewPanel is no longer needed.
*/
public class LayerViewPanel extends JPanel
implements
LayerListener,
LayerManagerProxy,
SelectionManagerProxy {
private static JPopupMenu popupMenu = new TrackedPopupMenu();
private ToolTipWriter toolTipWriter = new ToolTipWriter(this);
BorderLayout borderLayout1 = new BorderLayout();
private LayerManager layerManager;
private CursorTool currentCursorTool = new DummyTool();
private Viewport viewport = new Viewport(this);
private boolean viewportInitialized = false;
private java.awt.Point lastClickedPoint;
private ArrayList listeners = new ArrayList();
private LayerViewPanelContext context;
private RenderingManager renderingManager = new RenderingManager(this);
private FenceLayerFinder fenceLayerFinder;
private SelectionManager selectionManager;
private Blackboard blackboard = new Blackboard();
private boolean deferLayerEvents = false;
class MouseWheelZoomListener implements MouseWheelListener {
public void mouseWheelMoved(MouseWheelEvent e) {
if (currentCursorTool instanceof QuasimodeTool) {
Object tool = ((QuasimodeTool) currentCursorTool).getDelegate();
if (tool instanceof ZoomTool) {
((ZoomTool) tool).mouseWheelMoved(e);
} else if (tool instanceof LeftClickFilter) {
CursorTool wrappee = ((LeftClickFilter) tool).getWrappee();
if (wrappee instanceof PanTool)
((PanTool) wrappee).mouseWheelMoved(e);
}
}
}
}
public LayerViewPanel(LayerManager layerManager,
LayerViewPanelContext context) {
//Errors occur if the LayerViewPanel is sized to 0. [Jon Aquino]
setMinimumSize(new Dimension(100, 100));
//Set toolTipText to null to disable, "" to use default (i.e. show all
// attributes),
//or a custom template. [Jon Aquino]
setToolTipText("");
GUIUtil.fixClicks(this);
try {
this.context = context;
this.layerManager = layerManager;
selectionManager = new SelectionManager(this, this);
fenceLayerFinder = new FenceLayerFinder(this);
//Immediately register with the LayerManager because
// #getLayerManager will
//be called right away (when #setBackground is called in #jbInit)
// [Jon Aquino]
layerManager.addLayerListener(this);
try {
jbInit();
} catch (Exception ex) {
ex.printStackTrace();
}
addMouseListener(new MouseAdapter() {
public void mouseEntered(MouseEvent e) {
//Re-activate WorkbenchFrame. Otherwise, user may try
// entering
//a quasi-mode by pressing a modifier key -- nothing will
// happen because the
//WorkbenchFrame does not have focus. [Jon Aquino]
//JavaDoc for #toFront says some platforms will not
// activate the window.
//So use #requestFocus instead. [Jon Aquino 12/9/2003]
WorkbenchFrame workbenchFrame = (WorkbenchFrame) SwingUtilities
.getAncestorOfClass(WorkbenchFrame.class,
LayerViewPanel.this);
if (workbenchFrame != null && !workbenchFrame.isActive()) {
workbenchFrame.requestFocus();
}
}
});
addMouseMotionListener(new MouseMotionAdapter() {
public void mouseDragged(MouseEvent e) {
mouseLocationChanged(e);
}
public void mouseMoved(MouseEvent e) {
mouseLocationChanged(e);
}
private void mouseLocationChanged(MouseEvent e) {
try {
Point2D p = getViewport().toModelPoint(e.getPoint());
fireCursorPositionChanged(format(p.getX()), format(p
.getY()));
} catch (Throwable t) {
LayerViewPanel.this.context.handleThrowable(t);
}
}
});
addMouseWheelListener(new MouseWheelZoomListener() );
} catch (Throwable t) {
context.handleThrowable(t);
}
}
public ToolTipWriter getToolTipWriter() {
return toolTipWriter;
}
//In Java 1.3, if you try and do a #mouseClicked or a #mouseDragged on an
//inactive internal frame, it won't work. [Jon Aquino]
//In Java 1.4, the #mouseDragged will work, but not the #mouseClicked.
//See the Sun Java Bug Database, ID 4398733. The evaluation for Bug ID
// 4256525
//states that the fix is scheduled for the Java release codenamed Tiger.
//[Jon Aquino]
public String getToolTipText(MouseEvent event) {
return toolTipWriter.write(getToolTipText(), event.getPoint());
}
public static List components(Geometry g) {
if (!(g instanceof GeometryCollection)) {
return Arrays.asList(new Object[]{g});
}
GeometryCollection c = (GeometryCollection) g;
ArrayList components = new ArrayList();
for (int i = 0; i < c.getNumGeometries(); i++) {
components.addAll(components(c.getGeometryN(i)));
}
return components;
}
/**
* Workaround for the fact that GeometryCollection#intersects is not
* currently implemented.
*/
public static boolean intersects(Geometry a, Geometry b) {
GeometryFactory factory = new GeometryFactory(a.getPrecisionModel(), a
.getSRID());
List aComponents = components(a);
List bComponents = components(b);
for (Iterator i = aComponents.iterator(); i.hasNext();) {
Geometry aComponent = (Geometry) i.next();
Assert.isTrue(!(aComponent instanceof GeometryCollection));
//Collapse to point as workaround for JTS defect: #contains doesn't
// work for
//polygons and zero-length vectors. [Jon Aquino]
aComponent = collapseToPointIfPossible(aComponent, factory);
for (Iterator j = bComponents.iterator(); j.hasNext();) {
Geometry bComponent = (Geometry) j.next();
Assert.isTrue(!(bComponent instanceof GeometryCollection));
bComponent = collapseToPointIfPossible(bComponent, factory);
if (aComponent.intersects(bComponent)) {
return true;
}
}
}
return false;
}
private static Geometry collapseToPointIfPossible(Geometry g,
GeometryFactory factory) {
if (!g.isEmpty() && PinEqualCoordinatesStyle.coordinatesEqual(g)) {
g = factory.createPoint(g.getCoordinate());
}
return g;
}
/**
* The Fence layer will be excluded.
*/
public Map visibleLayerToFeaturesInFenceMap() {
Map visibleLayerToFeaturesInFenceMap = visibleLayerToFeaturesInFenceMap(getFence());
visibleLayerToFeaturesInFenceMap.remove(new FenceLayerFinder(this)
.getLayer());
return visibleLayerToFeaturesInFenceMap;
}
/**
* The Fence layer will be included.
*/
public Map visibleLayerToFeaturesInFenceMap(Geometry fence) {
Map map = new HashMap();
for (Iterator i = getLayerManager().iterator(); i.hasNext();) {
Layer layer = (Layer) i.next();
if (!layer.isVisible()) {
continue;
}
HashSet features = new HashSet();
for (Iterator j = layer.getFeatureCollectionWrapper().query(
fence.getEnvelopeInternal()).iterator(); j.hasNext();) {
Feature candidate = (Feature) j.next();
if (intersects(candidate.getGeometry(), fence)) {
features.add(candidate);
}
}
if (!features.isEmpty()) {
map.put(layer, features);
}
}
return map;
}
public static JPopupMenu popupMenu() {
return popupMenu;
}
public void setCurrentCursorTool(CursorTool currentCursorTool) {
this.currentCursorTool.deactivate();
removeMouseListener(this.currentCursorTool);
removeMouseMotionListener(this.currentCursorTool);
this.currentCursorTool = currentCursorTool;
currentCursorTool.activate(this);
setCursor(currentCursorTool.getCursor());
addMouseListener(currentCursorTool);
addMouseMotionListener(currentCursorTool);
}
/**
* When a layer is added, if this flag is false, the viewport will be zoomed
* to the extent of the layer.
*/
public void setViewportInitialized(boolean viewportInitialized) {
this.viewportInitialized = viewportInitialized;
}
public CursorTool getCurrentCursorTool() {
return currentCursorTool;
}
/**
* Note: the popup menu is shown only if the user right-clicks the panel.
* Thus, popup-menu event handlers don't need to check whether the return
* value is null.
*/
public java.awt.Point getLastClickedPoint() {
return lastClickedPoint;
}
public Viewport getViewport() {
return viewport;
}
public Java2DConverter getJava2DConverter() {
return viewport.getJava2DConverter();
}
/**
* @return the fence in model-coordinates, or null if there is no fence
*/
public Geometry getFence() {
return fenceLayerFinder.getFence();
}
public LayerManager getLayerManager() {
return layerManager;
}
public void featuresChanged(FeatureEvent e) {
}
public void categoryChanged(CategoryEvent e) {
}
public void layerChanged(LayerEvent e) {
try {
if (e.getType() == LayerEventType.METADATA_CHANGED) {
return;
}
SwingUtilities.invokeLater(new Runnable() {
public void run() {
try {
//Invoke later because other layers may be created in a
// few
//moments. [Jon Aquino]
initializeViewportIfNecessary();
} catch (Throwable t) {
context.handleThrowable(t);
}
}
});
if (! deferLayerEvents)
{
if ((e.getType() == LayerEventType.ADDED)
|| (e.getType() == LayerEventType.REMOVED)
|| (e.getType() == LayerEventType.APPEARANCE_CHANGED)) {
renderingManager.render(e.getLayerable());
} else if (e.getType() == LayerEventType.VISIBILITY_CHANGED) {
renderingManager.render(e.getLayerable(), false);
} else {
Assert.shouldNeverReachHere();
}
}
} catch (Throwable t) {
context.handleThrowable(t);
}
}
/**
* Returns an image with the dimensions of this panel. Note that the image
* has an alpha component, and thus is not suitable for creating JPEGs --
* they will look pinkish.
*/
public Image createBlankPanelImage() {
//The pixels will be transparent because we're creating a BufferedImage
//from scratch instead of calling #createImage. [Jon Aquino]
return new BufferedImage(getWidth(), getHeight(),
BufferedImage.TYPE_INT_ARGB);
}
public void repaint() {
if (renderingManager == null) {
//It's null during initialization [Jon Aquino]
superRepaint();
return;
}
renderingManager.renderAll();
}
public void superRepaint() {
super.repaint();
}
public void paintComponent(Graphics g) {
try {
((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
super.paintComponent(g);
erase((Graphics2D) g);
renderingManager.copyTo((Graphics2D) g);
//g may not be the same as the result of #getGraphics; it may be an
//off-screen buffer. [Jon Aquino]
firePainted(g);
} catch (Throwable t) {
context.handleThrowable(t);
}
}
public void erase(Graphics2D g) {
fill(g, getBackground());
}
public void fill(Graphics2D g, Color color) {
g.setColor(color);
Rectangle2D.Double r = new Rectangle2D.Double(0, 0, getWidth(),
getHeight());
g.fill(r);
}
void jbInit() throws Exception {
this.setBackground(Color.white);
this.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseReleased(MouseEvent e) {
this_mouseReleased(e);
}
});
this.addComponentListener(new java.awt.event.ComponentAdapter() {
public void componentResized(ComponentEvent e) {
this_componentResized(e);
}
});
this.setLayout(borderLayout1);
}
void this_componentResized(ComponentEvent e) {
try {
viewport.update();
} catch (Throwable t) {
context.handleThrowable(t);
}
}
public LayerViewPanelContext getContext() {
return context;
}
void this_mouseReleased(MouseEvent e) {
lastClickedPoint = e.getPoint();
if (currentCursorTool.isRightMouseButtonUsed()) {
return;
}
if (SwingUtilities.isRightMouseButton(e)) {
//Custom workbenches might not add any items to the LayerViewPanel
// popup menu.
//[Jon Aquino]
if (popupMenu.getSubElements().length == 0) {
return;
}
popupMenu.show(e.getComponent(), e.getX(), e.getY());
}
}
/**
* When the first layer is added, zoom to its extent.
*/
private void initializeViewportIfNecessary()
throws NoninvertibleTransformException {
//Check envelope of *visible* layers because #zoomToFullExtent
//now considers only visible layers [Jon Aquino 2004-06-18]
if (!viewportInitialized && (layerManager.size() > 0)
&& (layerManager.getEnvelopeOfAllLayers(true).getWidth() > 0)) {
setViewportInitialized(true);
viewport.zoomToFullExtent();
//Return here because #zoomToFullExtent will eventually cause a
// call to #paintComponent [Jon Aquino]
return;
}
}
public void addListener(LayerViewPanelListener listener) {
listeners.add(listener);
}
public void removeListener(LayerViewPanelListener listener) {
listeners.remove(listener);
}
/**
* @return d rounded off to the distance represented by one pixel
*/
public String format(double d) {
double pixelWidthInModelUnits = viewport
.getEnvelopeInModelCoordinates().getWidth()
/ getWidth();
return format(d, pixelWidthInModelUnits);
}
protected String format(double d, double pixelWidthInModelUnits) {
int precisionInDecimalPlaces = (int) Math.max(0, //because
// if
// pixelWidthInModelUnits
// > 1,
// the
// negative
// log
// will
// be
// negative
Math.round( //not floor, which brings 0.999 down to
// 0
(-Math.log(pixelWidthInModelUnits)) / Math.log(10)));
precisionInDecimalPlaces++;
//An extra decimal place, for good measure [Jon Aquino]
String formatString = "#.";
for (int i = 0; i < precisionInDecimalPlaces; i++) {
formatString += "#";
}
return new DecimalFormat(formatString).format(d);
}
private void firePainted(Graphics graphics) {
for (Iterator i = listeners.iterator(); i.hasNext();) {
LayerViewPanelListener l = (LayerViewPanelListener) i.next();
l.painted(graphics);
}
}
public void fireSelectionChanged() {
for (Iterator i = listeners.iterator(); i.hasNext();) {
LayerViewPanelListener l = (LayerViewPanelListener) i.next();
l.selectionChanged();
}
}
private void fireCursorPositionChanged(String x, String y) {
for (Iterator i = listeners.iterator(); i.hasNext();) {
LayerViewPanelListener l = (LayerViewPanelListener) i.next();
l.cursorPositionChanged(x, y);
}
}
public RenderingManager getRenderingManager() {
return renderingManager;
}
//Not sure where this method should reside. [Jon Aquino]
public Collection featuresWithVertex(Point2D viewPoint,
double viewTolerance, Collection features)
throws NoninvertibleTransformException {
Point2D modelPoint = viewport.toModelPoint(viewPoint);
double modelTolerance = viewTolerance / viewport.getScale();
Envelope searchEnvelope = new Envelope(modelPoint.getX()
- modelTolerance, modelPoint.getX() + modelTolerance,
modelPoint.getY() - modelTolerance, modelPoint.getY()
+ modelTolerance);
Collection featuresWithVertex = new ArrayList();
for (Iterator j = features.iterator(); j.hasNext();) {
Feature feature = (Feature) j.next();
if (geometryHasVertex(feature.getGeometry(), searchEnvelope)) {
featuresWithVertex.add(feature);
}
}
return featuresWithVertex;
}
private boolean geometryHasVertex(Geometry geometry, Envelope searchEnvelope) {
Coordinate[] coordinates = geometry.getCoordinates();
for (int i = 0; i < coordinates.length; i++) {
if (searchEnvelope.contains(coordinates[i])) {
return true;
}
}
return false;
}
public void dispose() {
renderingManager.dispose();
selectionManager.dispose();
layerManager.removeLayerListener(this);
}
/**
* @param millisecondDelay
* the GUI will be unresponsive for this length of time, so keep
* it short!
*/
public void flash(final Shape shape, Color color, Stroke stroke,
final int millisecondDelay) {
final Graphics2D graphics = (Graphics2D) getGraphics();
graphics.setColor(color);
graphics.setXORMode(Color.white);
graphics.setStroke(stroke);
try {
GUIUtil.invokeOnEventThread(new Runnable() {
public void run() {
try {
graphics.draw(shape);
//Use sleep rather than Timer (which could allow a
// third party to paint
//the panel between my XOR draws, messing up the XOR).
// Hopefully the user
//won't Alt-Tab away and back! [Jon Aquino]
Thread.sleep(millisecondDelay);
graphics.draw(shape);
} catch (Throwable t) {
getContext().handleThrowable(t);
}
}
});
} catch (Throwable t) {
getContext().handleThrowable(t);
}
}
public SelectionManager getSelectionManager() {
return selectionManager;
}
public Blackboard getBlackboard() {
return blackboard;
}
public void flash(final GeometryCollection geometryCollection)
throws NoninvertibleTransformException {
flash(getViewport().getJava2DConverter().toShape(geometryCollection),
Color.red, new BasicStroke(5, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND), 100);
}
public void setDeferLayerEvents(boolean defer)
{
deferLayerEvents = defer;
}
}