001: /*
002: * The Unified Mapping Platform (JUMP) is an extensible, interactive GUI
003: * for visualizing and manipulating spatial features with geometry and attributes.
004: *
005: * Copyright (C) 2003 Vivid Solutions
006: *
007: * This program is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU General Public License
009: * as published by the Free Software Foundation; either version 2
010: * of the License, or (at your option) any later version.
011: *
012: * This program is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
015: * GNU General Public License for more details.
016: *
017: * You should have received a copy of the GNU General Public License
018: * along with this program; if not, write to the Free Software
019: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
020: *
021: * For more information, contact:
022: *
023: * Vivid Solutions
024: * Suite #1A
025: * 2328 Government Street
026: * Victoria BC V8T 5G5
027: * Canada
028: *
029: * (250)385-6040
030: * www.vividsolutions.com
031: */
032: package com.vividsolutions.jump.workbench.ui;
033:
034: import java.awt.*;
035: import java.awt.Dimension;
036: import java.awt.event.ComponentEvent;
037: import java.awt.event.MouseAdapter;
038: import java.awt.event.MouseEvent;
039: import java.awt.event.MouseMotionAdapter;
040: import java.awt.event.MouseWheelListener;
041: import java.awt.event.MouseWheelEvent;
042: import java.awt.geom.NoninvertibleTransformException;
043: import java.awt.geom.Point2D;
044: import java.awt.geom.Rectangle2D;
045: import java.awt.image.BufferedImage;
046: import java.text.DecimalFormat;
047: import java.util.*;
048: import java.util.List;
049:
050: import javax.swing.JPanel;
051: import javax.swing.JPopupMenu;
052: import javax.swing.SwingUtilities;
053:
054: import com.vividsolutions.jts.geom.*;
055: import com.vividsolutions.jts.util.Assert;
056: import com.vividsolutions.jump.feature.Feature;
057: import com.vividsolutions.jump.util.Blackboard;
058: import com.vividsolutions.jump.workbench.model.*;
059: import com.vividsolutions.jump.workbench.ui.cursortool.CursorTool;
060: import com.vividsolutions.jump.workbench.ui.cursortool.DummyTool;
061: import com.vividsolutions.jump.workbench.ui.renderer.RenderingManager;
062: import com.vividsolutions.jump.workbench.ui.renderer.java2D.Java2DConverter;
063: import com.vividsolutions.jump.workbench.ui.renderer.style.PinEqualCoordinatesStyle;
064: import com.vividsolutions.jump.workbench.ui.zoom.ZoomTool;
065: import com.vividsolutions.jump.workbench.ui.cursortool.QuasimodeTool;
066:
067: //<<TODO:FIX>> One user (GK) gets an infinite repaint loop (the map moves around
068: //chaotically) when the LayerViewPanel is put side by side with the LayerTreePanel
069: //in a GridBagLayout. Something to do with determining the size, I think --
070: //the problem doesn't occur when the size is well defined (as when the two
071: //panels are in a GridLayout or SplitPane). [Jon Aquino]
072:
073: /**
074: * Be sure to call #dispose() when the LayerViewPanel is no longer needed.
075: */
076: public class LayerViewPanel extends JPanel implements LayerListener,
077: LayerManagerProxy, SelectionManagerProxy {
078: private static JPopupMenu popupMenu = new TrackedPopupMenu();
079: private ToolTipWriter toolTipWriter = new ToolTipWriter(this );
080: BorderLayout borderLayout1 = new BorderLayout();
081: private LayerManager layerManager;
082: private CursorTool currentCursorTool = new DummyTool();
083: private Viewport viewport = new Viewport(this );
084: private boolean viewportInitialized = false;
085: private java.awt.Point lastClickedPoint;
086: private ArrayList listeners = new ArrayList();
087: private LayerViewPanelContext context;
088: private RenderingManager renderingManager = new RenderingManager(
089: this );
090: private FenceLayerFinder fenceLayerFinder;
091: private SelectionManager selectionManager;
092: private Blackboard blackboard = new Blackboard();
093: private boolean deferLayerEvents = false;
094:
095: class MouseWheelZoomListener implements MouseWheelListener {
096: public void mouseWheelMoved(MouseWheelEvent e) {
097: if (((QuasimodeTool) currentCursorTool).getDelegate() instanceof ZoomTool) {
098: ((ZoomTool) ((QuasimodeTool) currentCursorTool)
099: .getDelegate()).mouseWheelMoved(e);
100: }
101: }
102: }
103:
104: public LayerViewPanel(LayerManager layerManager,
105: LayerViewPanelContext context) {
106: //Errors occur if the LayerViewPanel is sized to 0. [Jon Aquino]
107: setMinimumSize(new Dimension(100, 100));
108:
109: //Set toolTipText to null to disable, "" to use default (i.e. show all
110: // attributes),
111: //or a custom template. [Jon Aquino]
112: setToolTipText("");
113: GUIUtil.fixClicks(this );
114:
115: try {
116: this .context = context;
117: this .layerManager = layerManager;
118: selectionManager = new SelectionManager(this , this );
119: fenceLayerFinder = new FenceLayerFinder(this );
120:
121: //Immediately register with the LayerManager because
122: // #getLayerManager will
123: //be called right away (when #setBackground is called in #jbInit)
124: // [Jon Aquino]
125: layerManager.addLayerListener(this );
126:
127: try {
128: jbInit();
129: } catch (Exception ex) {
130: ex.printStackTrace();
131: }
132: addMouseListener(new MouseAdapter() {
133: public void mouseEntered(MouseEvent e) {
134: //Re-activate WorkbenchFrame. Otherwise, user may try
135: // entering
136: //a quasi-mode by pressing a modifier key -- nothing will
137: // happen because the
138: //WorkbenchFrame does not have focus. [Jon Aquino]
139: //JavaDoc for #toFront says some platforms will not
140: // activate the window.
141: //So use #requestFocus instead. [Jon Aquino 12/9/2003]
142: WorkbenchFrame workbenchFrame = (WorkbenchFrame) SwingUtilities
143: .getAncestorOfClass(WorkbenchFrame.class,
144: LayerViewPanel.this );
145: if (workbenchFrame != null
146: && !workbenchFrame.isActive()) {
147: workbenchFrame.requestFocus();
148: }
149: }
150: });
151:
152: addMouseMotionListener(new MouseMotionAdapter() {
153: public void mouseDragged(MouseEvent e) {
154: mouseLocationChanged(e);
155: }
156:
157: public void mouseMoved(MouseEvent e) {
158: mouseLocationChanged(e);
159: }
160:
161: private void mouseLocationChanged(MouseEvent e) {
162: try {
163: Point2D p = getViewport().toModelPoint(
164: e.getPoint());
165: fireCursorPositionChanged(format(p.getX()),
166: format(p.getY()));
167: } catch (Throwable t) {
168: LayerViewPanel.this .context.handleThrowable(t);
169: }
170: }
171: });
172:
173: addMouseWheelListener(new MouseWheelZoomListener());
174:
175: } catch (Throwable t) {
176: context.handleThrowable(t);
177: }
178: }
179:
180: public ToolTipWriter getToolTipWriter() {
181: return toolTipWriter;
182: }
183:
184: //In Java 1.3, if you try and do a #mouseClicked or a #mouseDragged on an
185: //inactive internal frame, it won't work. [Jon Aquino]
186: //In Java 1.4, the #mouseDragged will work, but not the #mouseClicked.
187: //See the Sun Java Bug Database, ID 4398733. The evaluation for Bug ID
188: // 4256525
189: //states that the fix is scheduled for the Java release codenamed Tiger.
190: //[Jon Aquino]
191: public String getToolTipText(MouseEvent event) {
192: return toolTipWriter.write(getToolTipText(), event.getPoint());
193: }
194:
195: public static List components(Geometry g) {
196: if (!(g instanceof GeometryCollection)) {
197: return Arrays.asList(new Object[] { g });
198: }
199:
200: GeometryCollection c = (GeometryCollection) g;
201: ArrayList components = new ArrayList();
202:
203: for (int i = 0; i < c.getNumGeometries(); i++) {
204: components.addAll(components(c.getGeometryN(i)));
205: }
206:
207: return components;
208: }
209:
210: /**
211: * Workaround for the fact that GeometryCollection#intersects is not
212: * currently implemented.
213: */
214: public static boolean intersects(Geometry a, Geometry b) {
215: GeometryFactory factory = new GeometryFactory(a
216: .getPrecisionModel(), a.getSRID());
217: List aComponents = components(a);
218: List bComponents = components(b);
219:
220: for (Iterator i = aComponents.iterator(); i.hasNext();) {
221: Geometry aComponent = (Geometry) i.next();
222: Assert.isTrue(!(aComponent instanceof GeometryCollection));
223:
224: //Collapse to point as workaround for JTS defect: #contains doesn't
225: // work for
226: //polygons and zero-length vectors. [Jon Aquino]
227: aComponent = collapseToPointIfPossible(aComponent, factory);
228:
229: for (Iterator j = bComponents.iterator(); j.hasNext();) {
230: Geometry bComponent = (Geometry) j.next();
231: Assert
232: .isTrue(!(bComponent instanceof GeometryCollection));
233: bComponent = collapseToPointIfPossible(bComponent,
234: factory);
235:
236: if (aComponent.intersects(bComponent)) {
237: return true;
238: }
239: }
240: }
241:
242: return false;
243: }
244:
245: private static Geometry collapseToPointIfPossible(Geometry g,
246: GeometryFactory factory) {
247: if (!g.isEmpty()
248: && PinEqualCoordinatesStyle.coordinatesEqual(g)) {
249: g = factory.createPoint(g.getCoordinate());
250: }
251:
252: return g;
253: }
254:
255: /**
256: * The Fence layer will be excluded.
257: */
258: public Map visibleLayerToFeaturesInFenceMap() {
259: Map visibleLayerToFeaturesInFenceMap = visibleLayerToFeaturesInFenceMap(getFence());
260: visibleLayerToFeaturesInFenceMap.remove(new FenceLayerFinder(
261: this ).getLayer());
262:
263: return visibleLayerToFeaturesInFenceMap;
264: }
265:
266: /**
267: * The Fence layer will be included.
268: */
269: public Map visibleLayerToFeaturesInFenceMap(Geometry fence) {
270: Map map = new HashMap();
271:
272: for (Iterator i = getLayerManager().iterator(); i.hasNext();) {
273: Layer layer = (Layer) i.next();
274:
275: if (!layer.isVisible()) {
276: continue;
277: }
278:
279: HashSet features = new HashSet();
280:
281: for (Iterator j = layer.getFeatureCollectionWrapper()
282: .query(fence.getEnvelopeInternal()).iterator(); j
283: .hasNext();) {
284: Feature candidate = (Feature) j.next();
285:
286: if (intersects(candidate.getGeometry(), fence)) {
287: features.add(candidate);
288: }
289: }
290:
291: if (!features.isEmpty()) {
292: map.put(layer, features);
293: }
294: }
295:
296: return map;
297: }
298:
299: public static JPopupMenu popupMenu() {
300: return popupMenu;
301: }
302:
303: public void setCurrentCursorTool(CursorTool currentCursorTool) {
304: this .currentCursorTool.deactivate();
305: removeMouseListener(this .currentCursorTool);
306: removeMouseMotionListener(this .currentCursorTool);
307: this .currentCursorTool = currentCursorTool;
308: currentCursorTool.activate(this );
309: setCursor(currentCursorTool.getCursor());
310: addMouseListener(currentCursorTool);
311: addMouseMotionListener(currentCursorTool);
312: }
313:
314: /**
315: * When a layer is added, if this flag is false, the viewport will be zoomed
316: * to the extent of the layer.
317: */
318: public void setViewportInitialized(boolean viewportInitialized) {
319: this .viewportInitialized = viewportInitialized;
320: }
321:
322: public CursorTool getCurrentCursorTool() {
323: return currentCursorTool;
324: }
325:
326: /**
327: * Note: the popup menu is shown only if the user right-clicks the panel.
328: * Thus, popup-menu event handlers don't need to check whether the return
329: * value is null.
330: */
331: public java.awt.Point getLastClickedPoint() {
332: return lastClickedPoint;
333: }
334:
335: public Viewport getViewport() {
336: return viewport;
337: }
338:
339: public Java2DConverter getJava2DConverter() {
340: return viewport.getJava2DConverter();
341: }
342:
343: /**
344: * @return the fence in model-coordinates, or null if there is no fence
345: */
346: public Geometry getFence() {
347: return fenceLayerFinder.getFence();
348: }
349:
350: public LayerManager getLayerManager() {
351: return layerManager;
352: }
353:
354: public void featuresChanged(FeatureEvent e) {
355: }
356:
357: public void categoryChanged(CategoryEvent e) {
358: }
359:
360: public void layerChanged(LayerEvent e) {
361: try {
362: if (e.getType() == LayerEventType.METADATA_CHANGED) {
363: return;
364: }
365:
366: SwingUtilities.invokeLater(new Runnable() {
367: public void run() {
368: try {
369: //Invoke later because other layers may be created in a
370: // few
371: //moments. [Jon Aquino]
372: initializeViewportIfNecessary();
373: } catch (Throwable t) {
374: context.handleThrowable(t);
375: }
376: }
377: });
378:
379: if (!deferLayerEvents) {
380: if ((e.getType() == LayerEventType.ADDED)
381: || (e.getType() == LayerEventType.REMOVED)
382: || (e.getType() == LayerEventType.APPEARANCE_CHANGED)) {
383: renderingManager.render(e.getLayerable());
384: } else if (e.getType() == LayerEventType.VISIBILITY_CHANGED) {
385: renderingManager.render(e.getLayerable(), false);
386: } else {
387: Assert.shouldNeverReachHere();
388: }
389: }
390: } catch (Throwable t) {
391: context.handleThrowable(t);
392: }
393: }
394:
395: /**
396: * Returns an image with the dimensions of this panel. Note that the image
397: * has an alpha component, and thus is not suitable for creating JPEGs --
398: * they will look pinkish.
399: */
400: public Image createBlankPanelImage() {
401: //The pixels will be transparent because we're creating a BufferedImage
402: //from scratch instead of calling #createImage. [Jon Aquino]
403: return new BufferedImage(getWidth(), getHeight(),
404: BufferedImage.TYPE_INT_ARGB);
405: }
406:
407: public void repaint() {
408: if (renderingManager == null) {
409: //It's null during initialization [Jon Aquino]
410: super Repaint();
411:
412: return;
413: }
414:
415: renderingManager.renderAll();
416: }
417:
418: public void super Repaint() {
419: super .repaint();
420: }
421:
422: public void paintComponent(Graphics g) {
423: try {
424: ((Graphics2D) g).setRenderingHint(
425: RenderingHints.KEY_ANTIALIASING,
426: RenderingHints.VALUE_ANTIALIAS_ON);
427: super .paintComponent(g);
428: erase((Graphics2D) g);
429: renderingManager.copyTo((Graphics2D) g);
430:
431: //g may not be the same as the result of #getGraphics; it may be an
432: //off-screen buffer. [Jon Aquino]
433: firePainted(g);
434: } catch (Throwable t) {
435: context.handleThrowable(t);
436: }
437: }
438:
439: public void erase(Graphics2D g) {
440: fill(g, getBackground());
441: }
442:
443: public void fill(Graphics2D g, Color color) {
444: g.setColor(color);
445:
446: Rectangle2D.Double r = new Rectangle2D.Double(0, 0, getWidth(),
447: getHeight());
448: g.fill(r);
449: }
450:
451: void jbInit() throws Exception {
452: this .setBackground(Color.white);
453: this .addMouseListener(new java.awt.event.MouseAdapter() {
454: public void mouseReleased(MouseEvent e) {
455: this _mouseReleased(e);
456: }
457: });
458: this
459: .addComponentListener(new java.awt.event.ComponentAdapter() {
460: public void componentResized(ComponentEvent e) {
461: this _componentResized(e);
462: }
463: });
464: this .setLayout(borderLayout1);
465: }
466:
467: void this _componentResized(ComponentEvent e) {
468: try {
469: viewport.update();
470: } catch (Throwable t) {
471: context.handleThrowable(t);
472: }
473: }
474:
475: public LayerViewPanelContext getContext() {
476: return context;
477: }
478:
479: void this _mouseReleased(MouseEvent e) {
480: lastClickedPoint = e.getPoint();
481:
482: if (currentCursorTool.isRightMouseButtonUsed()) {
483: return;
484: }
485:
486: if (SwingUtilities.isRightMouseButton(e)) {
487: //Custom workbenches might not add any items to the LayerViewPanel
488: // popup menu.
489: //[Jon Aquino]
490: if (popupMenu.getSubElements().length == 0) {
491: return;
492: }
493:
494: popupMenu.show(e.getComponent(), e.getX(), e.getY());
495: }
496: }
497:
498: /**
499: * When the first layer is added, zoom to its extent.
500: */
501: private void initializeViewportIfNecessary()
502: throws NoninvertibleTransformException {
503: //Check envelope of *visible* layers because #zoomToFullExtent
504: //now considers only visible layers [Jon Aquino 2004-06-18]
505: if (!viewportInitialized
506: && (layerManager.size() > 0)
507: && (layerManager.getEnvelopeOfAllLayers(true)
508: .getWidth() > 0)) {
509: setViewportInitialized(true);
510: viewport.zoomToFullExtent();
511: //Return here because #zoomToFullExtent will eventually cause a
512: // call to #paintComponent [Jon Aquino]
513: return;
514: }
515: }
516:
517: public void addListener(LayerViewPanelListener listener) {
518: listeners.add(listener);
519: }
520:
521: public void removeListener(LayerViewPanelListener listener) {
522: listeners.remove(listener);
523: }
524:
525: /**
526: * @return d rounded off to the distance represented by one pixel
527: */
528: public String format(double d) {
529: double pixelWidthInModelUnits = viewport
530: .getEnvelopeInModelCoordinates().getWidth()
531: / getWidth();
532:
533: return format(d, pixelWidthInModelUnits);
534: }
535:
536: protected String format(double d, double pixelWidthInModelUnits) {
537: int precisionInDecimalPlaces = (int) Math.max(0, //because
538: // if
539: // pixelWidthInModelUnits
540: // > 1,
541: // the
542: // negative
543: // log
544: // will
545: // be
546: // negative
547: Math.round( //not floor, which brings 0.999 down to
548: // 0
549: (-Math.log(pixelWidthInModelUnits))
550: / Math.log(10)));
551: precisionInDecimalPlaces++;
552:
553: //An extra decimal place, for good measure [Jon Aquino]
554: String formatString = "#.";
555:
556: for (int i = 0; i < precisionInDecimalPlaces; i++) {
557: formatString += "#";
558: }
559:
560: return new DecimalFormat(formatString).format(d);
561: }
562:
563: private void firePainted(Graphics graphics) {
564: for (Iterator i = listeners.iterator(); i.hasNext();) {
565: LayerViewPanelListener l = (LayerViewPanelListener) i
566: .next();
567: l.painted(graphics);
568: }
569: }
570:
571: public void fireSelectionChanged() {
572: for (Iterator i = listeners.iterator(); i.hasNext();) {
573: LayerViewPanelListener l = (LayerViewPanelListener) i
574: .next();
575: l.selectionChanged();
576: }
577: }
578:
579: private void fireCursorPositionChanged(String x, String y) {
580: for (Iterator i = listeners.iterator(); i.hasNext();) {
581: LayerViewPanelListener l = (LayerViewPanelListener) i
582: .next();
583: l.cursorPositionChanged(x, y);
584: }
585: }
586:
587: public RenderingManager getRenderingManager() {
588: return renderingManager;
589: }
590:
591: //Not sure where this method should reside. [Jon Aquino]
592: public Collection featuresWithVertex(Point2D viewPoint,
593: double viewTolerance, Collection features)
594: throws NoninvertibleTransformException {
595: Point2D modelPoint = viewport.toModelPoint(viewPoint);
596: double modelTolerance = viewTolerance / viewport.getScale();
597: Envelope searchEnvelope = new Envelope(modelPoint.getX()
598: - modelTolerance, modelPoint.getX() + modelTolerance,
599: modelPoint.getY() - modelTolerance, modelPoint.getY()
600: + modelTolerance);
601: Collection featuresWithVertex = new ArrayList();
602:
603: for (Iterator j = features.iterator(); j.hasNext();) {
604: Feature feature = (Feature) j.next();
605:
606: if (geometryHasVertex(feature.getGeometry(), searchEnvelope)) {
607: featuresWithVertex.add(feature);
608: }
609: }
610:
611: return featuresWithVertex;
612: }
613:
614: private boolean geometryHasVertex(Geometry geometry,
615: Envelope searchEnvelope) {
616: Coordinate[] coordinates = geometry.getCoordinates();
617:
618: for (int i = 0; i < coordinates.length; i++) {
619: if (searchEnvelope.contains(coordinates[i])) {
620: return true;
621: }
622: }
623:
624: return false;
625: }
626:
627: public void dispose() {
628: renderingManager.dispose();
629: selectionManager.dispose();
630: layerManager.removeLayerListener(this );
631: }
632:
633: /**
634: * @param millisecondDelay
635: * the GUI will be unresponsive for this length of time, so keep
636: * it short!
637: */
638: public void flash(final Shape shape, Color color, Stroke stroke,
639: final int millisecondDelay) {
640: final Graphics2D graphics = (Graphics2D) getGraphics();
641: graphics.setColor(color);
642: graphics.setXORMode(Color.white);
643: graphics.setStroke(stroke);
644:
645: try {
646: GUIUtil.invokeOnEventThread(new Runnable() {
647: public void run() {
648: try {
649: graphics.draw(shape);
650:
651: //Use sleep rather than Timer (which could allow a
652: // third party to paint
653: //the panel between my XOR draws, messing up the XOR).
654: // Hopefully the user
655: //won't Alt-Tab away and back! [Jon Aquino]
656: Thread.sleep(millisecondDelay);
657: graphics.draw(shape);
658: } catch (Throwable t) {
659: getContext().handleThrowable(t);
660: }
661: }
662: });
663: } catch (Throwable t) {
664: getContext().handleThrowable(t);
665: }
666: }
667:
668: public SelectionManager getSelectionManager() {
669: return selectionManager;
670: }
671:
672: public Blackboard getBlackboard() {
673: return blackboard;
674: }
675:
676: public void flash(final GeometryCollection geometryCollection)
677: throws NoninvertibleTransformException {
678: flash(getViewport().getJava2DConverter().toShape(
679: geometryCollection), Color.red, new BasicStroke(5,
680: BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND), 100);
681: }
682:
683: public void setDeferLayerEvents(boolean defer) {
684: deferLayerEvents = defer;
685: }
686: }
|