/[schmitzm]/branches/1.0-gt2-2.6/src/skrueger/geotools/XMapPane.java
ViewVC logotype

Contents of /branches/1.0-gt2-2.6/src/skrueger/geotools/XMapPane.java

Parent Directory Parent Directory | Revision Log Revision Log


Revision 543 - (show annotations)
Sat Nov 21 15:21:41 2009 UTC (15 years, 3 months ago) by alfonx
File size: 70488 byte(s)
* Even better selecting.. BBOX filter was the wrong choice.. INTERSECTS is correct.
1 package skrueger.geotools;
2
3 import java.awt.Color;
4 import java.awt.Cursor;
5 import java.awt.Font;
6 import java.awt.Graphics;
7 import java.awt.Graphics2D;
8 import java.awt.Image;
9 import java.awt.Point;
10 import java.awt.Rectangle;
11 import java.awt.RenderingHints;
12 import java.awt.event.ActionEvent;
13 import java.awt.event.ActionListener;
14 import java.awt.event.ComponentAdapter;
15 import java.awt.event.ComponentEvent;
16 import java.awt.event.InputEvent;
17 import java.awt.event.MouseEvent;
18 import java.awt.event.MouseListener;
19 import java.awt.geom.AffineTransform;
20 import java.awt.geom.NoninvertibleTransformException;
21 import java.awt.geom.Point2D;
22 import java.awt.image.BufferedImage;
23 import java.io.IOException;
24 import java.util.ArrayList;
25 import java.util.HashMap;
26 import java.util.Map;
27 import java.util.Vector;
28
29 import javax.swing.Timer;
30
31 import org.apache.log4j.Logger;
32 import org.geotools.feature.FeatureCollection;
33 import org.geotools.geometry.Envelope2D;
34 import org.geotools.geometry.jts.JTS;
35 import org.geotools.geometry.jts.ReferencedEnvelope;
36 import org.geotools.map.DefaultMapContext;
37 import org.geotools.map.MapContext;
38 import org.geotools.map.MapLayer;
39 import org.geotools.map.event.MapLayerEvent;
40 import org.geotools.map.event.MapLayerListEvent;
41 import org.geotools.map.event.MapLayerListListener;
42 import org.geotools.map.event.MapLayerListener;
43 import org.geotools.referencing.CRS;
44 import org.geotools.renderer.GTRenderer;
45 import org.geotools.renderer.label.LabelCacheImpl;
46 import org.geotools.renderer.lite.LabelCache;
47 import org.geotools.renderer.lite.RendererUtilities;
48 import org.geotools.renderer.lite.StreamingRenderer;
49 import org.geotools.swing.JMapPane;
50 import org.geotools.swing.event.MapMouseEvent;
51 import org.geotools.swing.event.MapPaneEvent;
52 import org.geotools.swing.event.MapPaneListener;
53 import org.opengis.feature.simple.SimpleFeature;
54 import org.opengis.feature.simple.SimpleFeatureType;
55 import org.opengis.referencing.FactoryException;
56 import org.opengis.referencing.crs.CoordinateReferenceSystem;
57 import org.opengis.referencing.operation.MathTransform;
58 import org.opengis.referencing.operation.TransformException;
59
60 import schmitzm.geotools.GTUtil;
61 import schmitzm.geotools.JTSUtil;
62 import schmitzm.geotools.gui.SelectableXMapPane;
63 import schmitzm.geotools.io.GeoImportUtil;
64 import schmitzm.geotools.map.event.JMapPaneListener;
65 import schmitzm.geotools.map.event.MapLayerAdapter;
66 import schmitzm.lang.LangUtil;
67 import schmitzm.swing.JPanel;
68 import schmitzm.swing.SwingUtil;
69
70 import com.vividsolutions.jts.geom.Coordinate;
71 import com.vividsolutions.jts.geom.Envelope;
72 import com.vividsolutions.jts.geom.Geometry;
73
74 /**
75 * The {@link XMapPane} class uses a Geotools {@link GTRenderer} to paint up to
76 * two {@link MapContext}s: a "local" {@link MapContext} and a "background"
77 * {@link MapContext}. The idea is, that rendering a background layer made up of
78 * e.g. OSM data, may take much longer than rendering local data.<br>
79 * Every {@link MapContext} is rendered on a {@link Thread} of it's own.
80 * Starting/ cancelling these threads is done by the {@link RenderingExecutor}.<br>
81 * <br>
82 * While the renderers are rending the map, a <br>
83 * The {@link XMapPane} is based on schmitzm {@link JPanel}, so
84 * {@link #print(Graphics)} will automatically set the background of components
85 * to pure white.
86 *
87 * The XMapPane has a {@link MouseListener} that manages zooming.<br>
88 * A logo/icon to float in the lower left corner may be set with
89 * {@link #setMapImage(BufferedImage)}<br>
90 *
91 * @see SelectableXMapPane - an extension of {@link XMapPane} that supports
92 * selecting features.
93 *
94 * @author stefan
95 *
96 */
97 public class XMapPane extends JPanel {
98
99 private static final int IMAGETYPE = BufferedImage.TYPE_INT_RGB;
100 private static final int IMAGETYPE_withAlpha = BufferedImage.TYPE_INT_ARGB;
101
102 private static Logger LOGGER = Logger.getLogger(XMapPane.class);
103
104 private boolean acceptsRepaintCalls = true;
105
106 /**
107 * Main {@link MapContext} that holds all layers that are rendered into the
108 * {@link #localImage} by the {@link #localRenderer}
109 */
110 MapContext localContext;
111
112 /**
113 * {@link MapContext} holding the background layers. Use it for layers that
114 * CAN take very long for rendering, like layer from the Internet: WMS, WFS,
115 * OSM...<br>
116 * <code>null</code> by default.
117 *
118 * @see #setBgContext(MapContext)
119 * @see #getBgContext()
120 */
121 MapContext bgContext;
122
123 /**
124 * While threads are working, calls {@link XMapPane#updateFinalImage()}
125 * regularly and {@link #repaint()}. This {@link Timer} is stopped when all
126 * renderers have finished.
127 *
128 * @see #INITIAL_REPAINT_DELAYAL
129 * @see #REPEATING_REPAINT_DELAY
130 */
131 final private Timer repaintTimer;
132
133 /**
134 * The initial delay in milliseconds until the {@link #finalImage} is
135 * updated the first time.
136 */
137 public static final int INITIAL_REPAINT_DELAY = 1000;
138
139 /**
140 * While the {@link #bgExecuter} and {@link #localExecuter} are rendering,
141 * the {@link #repaintTimer} is regularly updating the {@link #finalImage}
142 * with previews.
143 */
144 public static final long REPEATING_REPAINT_DELAY = 500;
145
146 /**
147 * Default delay (milliseconds) before the map will be redrawn when resizing
148 * the pane. This is to avoid flickering while drag-resizing.
149 */
150 public static final int DEFAULT_RESIZING_PAINT_DELAY = 600;
151
152 public static final int NONE = -123;
153
154 /**
155 * Flag fuer Modus "Kartenausschnitt bewegen". Nicht fuer Window-Auswahl
156 * moeglich!
157 *
158 * @see #setState(int)
159 */
160 public static final int PAN = 1;
161
162 /**
163 * Flag fuer Modus "SimpleFeature-Auswahl auf allen (sichtbaren) Layern".
164 *
165 * @see #setState(int)
166 * @see #setState(int)
167 */
168 public static final int SELECT_ALL = 103;
169 /**
170 * Flag fuer Modus
171 * "Auswahl nur eines Features, das erste sichtbare von Oben".
172 *
173 * @see #setState(int)
174 * @see #setState(int)
175 */
176 public static final int SELECT_ONE_FROM_TOP = 104;
177 /**
178 * Flag fuer Modus
179 * "SimpleFeature-Auswahl auf dem obersten (sichtbaren) Layer".
180 *
181 * @see #setState(int)
182 * @see #setState(int)
183 */
184 public static final int SELECT_TOP = 4;
185
186 public static final Cursor WAIT_CURSOR = Cursor
187 .getPredefinedCursor(Cursor.WAIT_CURSOR);
188
189 final static Font waitFont = new Font("Arial", Font.BOLD, 30);
190 final String waitMsg = SwingUtil.R("WaitMess");
191 final static Font errorFont = new Font("Arial", Font.BOLD, 13);
192
193 /**
194 * Flag fuer Modus "Heran zoomen".
195 *
196 * @see #setState(int)
197 * @see #setState(int)
198 */
199 public static final int ZOOM_IN = 2;
200
201 /**
202 * Flag fuer Modus "Heraus zoomen". Nicht fuer Window-Auswahl moeglich!
203 *
204 * @see #setState(int)
205 */
206 public static final int ZOOM_OUT = 3;
207
208 /**
209 * Konvertiert die Maus-Koordinaten (relativ zum <code>JMapPane</code>) in
210 * Karten-Koordinaten.
211 *
212 * @param e
213 * Maus-Ereignis
214 */
215 public static Point2D getMapCoordinatesFromEvent(final MouseEvent e) {
216 // aktuelle Geo-Position aus GeoMouseEvent ermitteln
217 if (e != null && e instanceof MapMouseEvent)
218 try {
219 return ((MapMouseEvent) e).getMapPosition().toPoint2D();
220 } catch (final Exception err) {
221 LOGGER
222 .error(
223 "return ((GeoMouseEvent) e).getMapCoordinate(null).toPoint2D();",
224 err);
225 }
226
227 // aktuelle Geo-Position ueber Transformation des JMapPane berechnen
228 if (e != null && e.getSource() instanceof XMapPane) {
229
230 final XMapPane xMapPane = (XMapPane) e.getSource();
231
232 if (!xMapPane.isWellDefined())
233 return null;
234
235 final AffineTransform at = xMapPane.getScreenToWorld();
236 if (at != null)
237 return at.transform(e.getPoint(), null);
238 return null;
239 }
240 throw new IllegalArgumentException(
241 "MouseEvent has to be of instance MapMouseEvent or come from an XMapPane");
242 }
243
244 /**
245 * Listens to changes of the "background" {@link MapContext} and triggers
246 * repaints where needed.
247 */
248 private final MapLayerListListener bgContextListener = new MapLayerListListener() {
249
250 @Override
251 public void layerAdded(final MapLayerListEvent event) {
252
253 MapLayer layer = event.getLayer();
254
255 layer.addMapLayerListener(bgMapLayerListener);
256
257 if (localContext.getLayers().length == 0
258 && bgContext.getLayers().length == 1) { // the first one and
259 // localContext is
260 // empty
261 if (!setMapArea(localContext.getAreaOfInterest()))
262 requestStartRendering();
263 return;
264 }
265
266 // We need to redraw, even in case that the mapArea didn't change
267 // mapImageInvalid = true;
268 // repaint();
269 requestStartRendering();
270
271 }
272
273 @Override
274 public void layerChanged(final MapLayerListEvent event) {
275 // mapImageInvalid = true;
276 // repaint();
277 requestStartRendering();
278 }
279
280 @Override
281 public void layerMoved(final MapLayerListEvent event) {
282 // mapImageInvalid = true;
283 // repaint();
284 requestStartRendering();
285 }
286
287 @Override
288 public void layerRemoved(final MapLayerListEvent event) {
289 if (event.getLayer() != null)
290 event.getLayer().removeMapLayerListener(bgMapLayerListener);
291 // mapImageInvalid = true;
292 // repaint();
293 requestStartRendering();
294 }
295 };
296
297 /**
298 * This {@link RenderingExecutor} manages the creation and cancellation of
299 * up to one {@link Thread} for rendering the {@link #localContext}.
300 */
301 private final RenderingExecutor localExecuter = new RenderingExecutor(this);
302
303 /**
304 * This {@link RenderingExecutor} manages the creation and cancellation of
305 * up to one {@link Thread} for rendering the {@link #bgContext}.
306 */
307 protected RenderingExecutor bgExecuter;
308
309 /**
310 * The {@link #localRenderer} for the {@link #localContext} uses this
311 * {@link Image}.
312 */
313 private BufferedImage localImage;
314
315 /**
316 * The {@link #bgRenderer} for the {@link #bgContext} uses this
317 * {@link Image}.
318 */
319 private BufferedImage bgImage;
320
321 /**
322 * This {@link Image} is a merge of the {@link #bgImage},
323 * {@link #localImage} and {@link #addGadgets(Graphics2D, boolean)}. It is
324 * updated with {@link #updateFinalImage()} and used for painting in
325 * {@link #paintComponent(Graphics)}
326 */
327 private BufferedImage finalImage;
328
329
330 /**
331 * Optionally a transparent image to paint over the map in the lower right
332 * corner.
333 *
334 * @see #addGadgets(Graphics2D)
335 * @see #setMapImage(BufferedImage)
336 **/
337 private BufferedImage mapImage = null;
338
339 /**
340 * Listens to each layer in the local {@link MapContext} for changes and
341 * triggers repaints.
342 */
343 protected MapLayerListener bgMapLayerListener = new MapLayerAdapter() {
344
345 @Override
346 public void layerChanged(final MapLayerEvent event) {
347 requestStartRendering();
348 }
349
350 @Override
351 public void layerHidden(final MapLayerEvent event) {
352 requestStartRendering();
353 }
354
355 @Override
356 public void layerShown(final MapLayerEvent event) {
357 requestStartRendering();
358 }
359 };
360
361 /**
362 * A flag indicating if dispose() was already called. If true, then further
363 * use of this {@link SelectableXMapPane} is undefined.
364 */
365 private boolean disposed = false;
366
367 /**
368 * While dragging, the {@link #updateFinalImage()} method is translating the
369 * cached images while setting it together.
370 **/
371 Point imageOrigin = new Point(0, 0);
372 /**
373 * For every rendering thread started,
374 * {@link GTUtil#createGTRenderer(MapContext)} is called to create a new
375 * renderer. These Java2D rendering hints are passed to the
376 * {@link GTRenderer}. The java2dHints are the same for all renderers (bg
377 * and local).
378 */
379 private RenderingHints java2dHints;
380
381 protected LabelCache labelCache = new LabelCacheImpl();
382
383 /**
384 * Listens to changes of the "local" {@link MapContext} and triggers
385 * repaints where needed.
386 */
387 private final MapLayerListListener localContextListener = new MapLayerListListener() {
388
389 @Override
390 public void layerAdded(final MapLayerListEvent event) {
391 event.getLayer().addMapLayerListener(localMapLayerListener);
392
393 localRenderer.setContext(getContext());
394
395 if (localContext.getLayers().length == 1) { // the first one
396
397 // if the Area of Interest is unset, the LayerBounds are used
398 if (!setMapArea(localContext.getAreaOfInterest()))
399 repaint();
400
401 return;
402 }
403
404 // We need to redraw, even in case that the mapArea didn't change
405 // mapImageInvalid = true;
406 // repaint();
407 requestStartRendering();
408
409 }
410
411 @Override
412 public void layerChanged(final MapLayerListEvent event) {
413 // mapImageInvalid = true;
414 // repaint();
415 localRenderer.setContext(getContext());
416 requestStartRendering();
417 }
418
419 @Override
420 public void layerMoved(final MapLayerListEvent event) {
421 // mapImageInvalid = true;
422 // repaint();
423 localRenderer.setContext(getContext());
424 requestStartRendering();
425 }
426
427 @Override
428 public void layerRemoved(final MapLayerListEvent event) {
429 if (event.getLayer() != null)
430 event.getLayer().removeMapLayerListener(localMapLayerListener);
431 // mapImageInvalid = true;
432 // repaint();
433 localRenderer.setContext(getContext());
434 requestStartRendering();
435 }
436 };
437
438 /**
439 * Listens to each layer in the local {@link MapContext} for changes and
440 * triggers repaints.
441 */
442 protected MapLayerListener localMapLayerListener = new MapLayerAdapter() {
443
444 @Override
445 public void layerChanged(final MapLayerEvent event) {
446 localRenderer.setContext(getContext()); // betters for SLD changes?!
447 // Change of SLD for example
448 // mapImageInvalid = true;
449 // repaint();
450 requestStartRendering();
451 }
452
453 @Override
454 public void layerHidden(final MapLayerEvent event) {
455 // mapImageInvalid = true;
456 // repaint();
457 requestStartRendering();
458 }
459
460 @Override
461 public void layerShown(final MapLayerEvent event) {
462 // mapImageInvalid = true;
463 // repaint();
464 requestStartRendering();
465 }
466 };
467
468 private final GTRenderer localRenderer = GTUtil.createGTRenderer();
469
470 private final GTRenderer bgRenderer = GTUtil.createGTRenderer();
471
472 /**
473 * the area of the map to draw
474 */
475 protected Envelope mapArea = null;
476
477 /**
478 * A flag set it {@link #setMapArea(Envelope)} to indicated the
479 * {@link #paintComponent(Graphics)} method, that the image on-screen is
480 * associated with {@link #oldMapArea} and may hence be scaled for a quick
481 * preview.<br>
482 * The flag is reset
483 **/
484 private boolean mapAreaChanged = false;
485
486 /**
487 * This color is used as the default background color when painting a map.
488 */
489 private Color mapBackgroundColor = Color.WHITE;
490
491 /**
492 * A flag indicating that the shown image is invalid and needs to be
493 * re-rendered.
494 */
495 protected boolean mapImageInvalid = true;
496
497 /**
498 * Holds a flag for each layer, whether it is regarded or ignored on
499 * {@link #SELECT_TOP}, {@link #SELECT_ALL} and {@link #SELECT_ONE_FROM_TOP}
500 * actions.
501 */
502 final protected HashMap<MapLayer, Boolean> mapLayerSelectable = new HashMap<MapLayer, Boolean>();
503
504 /**
505 * List of listeners of this {@link XMapPane}
506 */
507 protected Vector<JMapPaneListener> mapPaneListeners = new Vector<JMapPaneListener>();
508 /**
509 * If not <code>null</code>, the {@link XMapPane} will not allow to zoom/pan
510 * out of that area
511 **/
512 private Envelope maxExtend = null;
513 private Double maxZoomScale = Double.MIN_VALUE;
514
515 private Double minZoomScale = Double.MAX_VALUE;
516
517 /**
518 * We store the old mapArea for a moment to use it for the
519 * "quick scaled preview" in case of ZoomOut
520 **/
521 protected Envelope oldMapArea = null;
522
523 /**
524 * We store the old transform for a moment to use it for the
525 * "quick scaled preview" in case of ZoomIn
526 **/
527 protected AffineTransform oldScreenToWorld = null;
528
529 /**
530 * A flag indicating, that the image size has changed and the buffered
531 * images are not big enough any more
532 **/
533 protected boolean paneResized = false;
534
535 private BufferedImage preFinalImage;
536
537 // if null, no quick preview will be shown
538 private int quickPreviewHint = 0;
539
540 private Map<Object, Object> rendererHints = GTUtil
541 .getDefaultGTRendererHints(localRenderer);
542
543 /** If set to <code>true</code>, the {@link #startRenderThreadsTimer} will start rendering a new {@link #localImage} **/
544 private volatile Boolean requestStartRenderingLocal = false;
545
546 /** If set to <code>true</code>, the {@link #startRenderThreadsTimer} will start rendering a new {@link #bgImage} **/
547 private volatile Boolean requestStartRenderingBg = false;
548
549 private final Timer resizeTimer;
550
551 /**
552 * Transformation zwischen Fenster-Koordinaten und Karten-Koordinaten
553 * (lat/lon)
554 */
555 protected AffineTransform screenToWorld = null;
556
557 /**
558 * The flag {@link #requestStartRenderingLocal} can be set to true by events.
559 * This {@link Timer} checks the flag regularly and starts one renderer
560 * thread.
561 */
562 final private Timer startRenderThreadsTimer;
563
564 /**
565 * The default state is ZOOM_IN, hence by default the
566 * {@link #zoomMapPaneMouseListener} is also enabled.
567 **/
568 private int state = ZOOM_IN;
569
570 /**
571 * Manuell gesetzter statischer Cursor, unabhaengig von der aktuellen
572 * MapPane-Funktion
573 */
574 protected Cursor staticCursor = null;
575
576 private AffineTransform worldToScreen;
577
578 /**
579 * This {@link MouseListener} is managing all zoom related tasks
580 */
581 public final ZoomXMapPaneMouseListener zoomMapPaneMouseListener = new ZoomXMapPaneMouseListener(
582 this);
583
584 /** Is set if a renderer has an error **/
585 protected ArrayList<Exception> renderingErrors = new ArrayList<Exception>();
586
587 public XMapPane() {
588 this(null, null);
589 }
590
591 /**
592 * full constructor extending JPanel
593 *
594 * @param rendererHints
595 * may be <code>null</code>. Otherwise a {@link Map<Object,
596 * Object>} of {@link RenderingHints} to override the default
597 * from {@link GTUtil#getDefaultGTRendererHints(GTRenderer)}
598 *
599 * @param localContext
600 * The main {@link MapContext} to use. If <code>null</code>, an
601 * empty {@link DefaultMapContext} will be created.
602 */
603 public XMapPane(final MapContext localContext_,
604 final Map<Object, Object> rendererHints) {
605 super(true);
606
607 setRendererHints(rendererHints);
608
609 setOpaque(true);
610
611 if (localContext_ != null)
612 setLocalContext(localContext_);
613
614 /**
615 * Adding the #zoomMapPaneMouseListener
616 */
617 this.addMouseListener(zoomMapPaneMouseListener);
618 this.addMouseMotionListener(zoomMapPaneMouseListener);
619 this.addMouseWheelListener(zoomMapPaneMouseListener);
620
621 /*
622 * We use a Timer object to avoid rendering delays and flickering when
623 * the user is drag-resizing the parent container of this map pane.
624 *
625 * Using a ComponentListener doesn't work because, unlike a JFrame, the
626 * pane receives a stream of events during drag-resizing.
627 */
628 resizeTimer = new Timer(DEFAULT_RESIZING_PAINT_DELAY,
629 new ActionListener() {
630
631 public void actionPerformed(final ActionEvent e) {
632 paneResized = true;
633
634 if (!isWellDefined())
635 return;
636
637 final Rectangle bounds = getVisibleRect();
638
639 final Envelope geoMapArea = tranformWindowToGeo(
640 bounds.x, bounds.y, bounds.x + bounds.width,
641 bounds.y + bounds.height);
642
643 setMapArea(bestAllowedMapArea(geoMapArea));
644 }
645 });
646 resizeTimer.setRepeats(false);
647
648 // this.addComponentListener(new ComponentAdapter() {
649 //
650 // @Override
651 // public void componentResized(final ComponentEvent e) {
652 // if (bgExecuter != null)
653 // bgExecuter.cancelTask();
654 // if (localExecuter != null)
655 // localExecuter.cancelTask();
656 // resizeTimer.restart();
657 // }
658 //
659 // });
660
661 /*
662 * Setting up the repaintTimer. Not started automatically.
663 */
664 repaintTimer = new Timer((int) REPEATING_REPAINT_DELAY,
665 new ActionListener() {
666
667 @Override
668 public void actionPerformed(final ActionEvent e) {
669 updateFinalImage();
670 XMapPane.this.repaint();
671 }
672 });
673 repaintTimer.setInitialDelay(INITIAL_REPAINT_DELAY);
674 repaintTimer.setRepeats(true);
675
676 /*
677 * Setting up the startRenderThreadsTimer. This Timer starts
678 * automatically.
679 */
680 startRenderThreadsTimer = new Timer(100, new ActionListener() {
681
682 @Override
683 public void actionPerformed(final ActionEvent e) {
684 synchronized (requestStartRenderingLocal) {
685 if (requestStartRenderingLocal && isWellDefined()) {
686
687 if (localExecuter.isRunning()) {
688 localExecuter.cancelTask();
689 } else {
690 requestStartRenderingLocal = false;
691 startRendering();
692 }
693 }
694 }
695 }
696 });
697 startRenderThreadsTimer.start();
698
699 }
700
701 /**
702 * Fuegt der Map einen Listener hinzu.
703 *
704 * @param l
705 * neuer Listener
706 */
707 public void addMapPaneListener(final JMapPaneListener l) {
708 mapPaneListeners.add(l);
709 }
710
711 /**
712 * Korrigiert den {@link Envelope} aka {@code mapArea} auf die beste
713 * erlaubte Flaeche damit die Massstabsbeschaenkungen noch eingehalten
714 * werden, FALLS der uebergeben Envelope nicht schon gueltig sein sollte.<br>
715 * Since 21. April 09: Before thecalculation starts, the aspect ratio is
716 * corrected. This change implies, that setMapArea() will most of the time
717 * not allow setting to a wrong aspectRatio.
718 *
719 * @author <a href="mailto:[email protected]">Stefan Alfons
720 * Kr&uuml;ger</a>
721 */
722 public Envelope bestAllowedMapArea(Envelope env) {
723 // return env;
724 if (getWidth() == 0)
725 return env;
726 if (env == null)
727 return null;
728
729 Envelope newArea = null;
730
731 /**
732 * Correct the aspect Ratio before we check the rest. Otherwise we might
733 * easily fail. We allow to grow here, because we don't check against
734 * the maxExtend
735 */
736 final Rectangle curPaintArea = getVisibleRect();
737
738 env = JTSUtil.fixAspectRatio(curPaintArea, env, true);
739
740 final double scale = env.getWidth() / getWidth();
741 final double centerX = env.getMinX() + env.getWidth() / 2.;
742 final double centerY = env.getMinY() + env.getHeight() / 2.;
743 double newWidth2 = 0;
744 double newHeight2 = 0;
745 if (scale < getMaxZoomScale()) {
746 // ****************************************************************************
747 // Wir zoomen weiter rein als erlaubt => Anpassen des envelope
748 // ****************************************************************************
749 newWidth2 = getMaxZoomScale() * getWidth() / 2.;
750 newHeight2 = getMaxZoomScale() * getHeight() / 2.;
751 } else if (scale > getMinZoomScale()) {
752 // ****************************************************************************
753 // Wir zoomen weiter raus als erlaubt => Anpassen des envelope
754 // ****************************************************************************
755 newWidth2 = getMinZoomScale() * getWidth() / 2.;
756 newHeight2 = getMinZoomScale() * getHeight() / 2.;
757 } else {
758 // ****************************************************************************
759 // Die mapArea / der Envelope ist ist gueltig! Keine Aenderungen
760 // ****************************************************************************
761 newArea = env;
762 }
763
764 if (newArea == null) {
765
766 final Coordinate ll = new Coordinate(centerX - newWidth2, centerY
767 - newHeight2);
768 final Coordinate ur = new Coordinate(centerX + newWidth2, centerY
769 + newHeight2);
770
771 newArea = new Envelope(ll, ur);
772 }
773
774 final Envelope maxAllowedExtend = getMaxExtend();
775 while (maxAllowedExtend != null && !maxAllowedExtend.contains(newArea)
776 && newArea != null && !newArea.isNull()
777 && !Double.isNaN(newArea.getMinX())
778 && !Double.isNaN(newArea.getMaxX())
779 && !Double.isNaN(newArea.getMinY())
780 && !Double.isNaN(newArea.getMaxY())) {
781 /*
782 * If a maxExtend is set, we have to honour that...
783 */
784
785 // Exceeds top? Move down and maybe cut
786 if (newArea.getMaxY() > maxAllowedExtend.getMaxY()) {
787 final double divY = newArea.getMaxY()
788 - maxAllowedExtend.getMaxY();
789 // LOGGER.debug("Moving area down by " + divY);
790
791 newArea = new Envelope(new Coordinate(newArea.getMinX(),
792 newArea.getMinY() - divY), new Coordinate(newArea
793 .getMaxX(), newArea.getMaxY() - divY));
794
795 if (newArea.getMinY() < maxAllowedExtend.getMinY()) {
796 // LOGGER.debug("Now it exeeds the bottom border.. cut!");
797 // And cut the bottom if it moved out of the area
798 newArea = new Envelope(new Coordinate(newArea.getMinX(),
799 maxAllowedExtend.getMinY()), new Coordinate(newArea
800 .getMaxX(), newArea.getMaxY()));
801
802 // LOGGER.debug("and fix aspect ratio");
803
804 newArea = JTSUtil.fixAspectRatio(getVisibleRect(), newArea,
805 false);
806 }
807 }
808
809 // Exceeds bottom? Move up and maybe cut
810 if (newArea.getMinY() < maxAllowedExtend.getMinY()) {
811 final double divY = newArea.getMinY()
812 - maxAllowedExtend.getMinY();
813 // LOGGER.debug("Moving area up by " + divY);
814
815 newArea = new Envelope(new Coordinate(newArea.getMinX(),
816 newArea.getMinY() - divY), new Coordinate(newArea
817 .getMaxX(), newArea.getMaxY() - divY));
818
819 if (newArea.getMaxY() > maxAllowedExtend.getMaxY()) {
820 // LOGGER.debug("Now it exeeds the top border.. cut!");
821 // And cut the bottom if it moved out of the area
822 newArea = new Envelope(new Coordinate(newArea.getMinX(),
823 newArea.getMinY()), new Coordinate(newArea
824 .getMaxX(), maxAllowedExtend.getMaxY()));
825
826 // LOGGER.debug("and fix aspect ratio");
827
828 newArea = JTSUtil.fixAspectRatio(getVisibleRect(), newArea,
829 false);
830 }
831 }
832
833 // Exceeds to the right? move and maybe cut
834 if (newArea.getMaxX() > maxAllowedExtend.getMaxX()) {
835
836 // Move left..
837 final double divX = newArea.getMaxX()
838 - maxAllowedExtend.getMaxX();
839 // LOGGER.debug("Moving area left by " + divX);
840
841 newArea = new Envelope(new Coordinate(newArea.getMinX() - divX,
842 newArea.getMinY()), new Coordinate(newArea.getMaxX()
843 - divX, newArea.getMaxY()));
844
845 if (newArea.getMinX() < maxAllowedExtend.getMinX()) {
846 // LOGGER.debug("Now it exeeds the left border.. cut!");
847 // And cut the left if it moved out of the area
848 newArea = new Envelope(new Coordinate(maxAllowedExtend
849 .getMinX(), newArea.getMinY()), new Coordinate(
850 newArea.getMaxX(), newArea.getMaxY()));
851
852 // LOGGER.debug("and fix aspect ratio");
853
854 newArea = JTSUtil.fixAspectRatio(getVisibleRect(), newArea,
855 false);
856 }
857 }
858
859 // Exceeds to the left? move and maybe cut
860 if (newArea.getMinX() < maxAllowedExtend.getMinX()) {
861
862 // Move right..
863 final double divX = newArea.getMinX()
864 - maxAllowedExtend.getMinX();
865 // LOGGER.debug("Moving area right by " + divX);
866
867 newArea = new Envelope(new Coordinate(newArea.getMinX() - divX,
868 newArea.getMinY()), new Coordinate(newArea.getMaxX()
869 - divX, newArea.getMaxY()));
870
871 if (newArea.getMaxX() > maxAllowedExtend.getMaxX()) {
872 // LOGGER.debug("Now it exeeds the right border.. cut!");
873 // And cut the left if it moved out of the area
874 newArea = new Envelope(new Coordinate(newArea.getMinX(),
875 newArea.getMinY()), new Coordinate(maxAllowedExtend
876 .getMaxX(), newArea.getMaxY()));
877
878 // LOGGER.debug("and fix aspect ratio");
879
880 newArea = JTSUtil.fixAspectRatio(getVisibleRect(), newArea,
881 false);
882 }
883 }
884 }
885
886 return newArea;
887 }
888
889 /**
890 * Should be called when the {@link JMapPane} is not needed no more to help
891 * the GarbageCollector
892 *
893 * Removes all {@link JMapPaneListener}s that are registered
894 *
895 * @author <a href="mailto:[email protected]">Stefan Alfons
896 * Kr&uuml;ger</a>
897 */
898 public void dispose() {
899 if (isDisposed())
900 return;
901
902 setPainting(false);
903
904 resizeTimer.stop();
905 startRenderThreadsTimer.stop();
906
907 disposed = true;
908
909 if (bgExecuter != null) {
910 bgExecuter.cancelTask();
911 bgExecuter.dispose();
912 }
913
914 if (localExecuter != null) {
915 int i = 0;
916 localExecuter.cancelTask();
917 while (i++ < 10 && localExecuter.isRunning()) {
918 try {
919 Thread.sleep(200);
920 } catch (final InterruptedException e) {
921 LOGGER
922 .warn(
923 "while XMapPane we are waiting for the localExcutor to stop",
924 e);
925 }
926 }
927 if (localExecuter.isRunning()) {
928 LOGGER
929 .warn("localExecutor Thread still running after 2s! Continuing anyways...");
930 }
931 localExecuter.dispose();
932 }
933
934 disposeImages();
935
936 // Remove all mapPaneListeners that have registered with us
937 mapPaneListeners.clear();
938
939 removeMouseMotionListener(zoomMapPaneMouseListener);
940 removeMouseListener(zoomMapPaneMouseListener);
941
942 if (localContext != null)
943 getContext().clearLayerList();
944 if (bgContext != null)
945 getBgContext().clearLayerList();
946
947 removeAll();
948 }
949
950 /**
951 * Draws a rectangle in XOR mode from the origin at {@link #startPos} to the
952 * given point. All in screen coordinates.
953 */
954 protected void drawRectangle(final Graphics graphics, final Point startPos,
955 final Point e) {
956
957 if (!isWellDefined())
958 return;
959
960 // undraw last box/draw new box
961 final int left = Math.min(startPos.x, e.x);
962 final int right = Math.max(startPos.x, e.x);
963 final int top = Math.max(startPos.y, e.y);
964 final int bottom = Math.min(startPos.y, e.y);
965 final int width = right - left;
966 final int height = top - bottom;
967
968 if (width == 0 && height == 0)
969 return;
970
971 graphics.setXORMode(Color.WHITE);
972 graphics.drawRect(left, bottom, width, height);
973 }
974
975 /**
976 * Diretly paints scaled preview into the {@link SelectableXMapPane}. Used
977 * to give the user something to look at while we are rendering. Method
978 * should be called after {@link #setMapArea(Envelope)} has been set to the
979 * new mapArea and transform has been reset.<br>
980 *
981 * @param g
982 * Graphics2D to paint the preview into
983 *
984 * @param state
985 * Max be {@link #ZOOM_IN} or {@link #ZOOM_OUT}
986 */
987 protected boolean drawScaledPreviewImage_Zoom(final Graphics2D graphics) {
988
989 // if (1 == 1)return false;
990
991 if (quickPreviewHint == 0)
992 return false;
993
994 graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
995 RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
996 graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
997 RenderingHints.VALUE_ANTIALIAS_OFF);
998 graphics.setRenderingHint(RenderingHints.KEY_RENDERING,
999 RenderingHints.VALUE_RENDER_SPEED);
1000
1001 if (oldMapArea == null)
1002 return false;
1003
1004 final Rectangle visibleArea = getVisibleRect();
1005
1006 // Calculate the oldMapArea in the current WindowCoordinates:
1007 final Envelope oldMapWindow = tranformGeoToWindow(oldMapArea.getMinX(),
1008 oldMapArea.getMinY(), oldMapArea.getMaxX(), oldMapArea
1009 .getMaxY());
1010
1011 final int xx1 = (int) Math.round(oldMapWindow.getMinX());
1012 final int yy1 = (int) Math.round(oldMapWindow.getMinY());
1013 final int xx2 = (int) Math.round(oldMapWindow.getMaxX());
1014 final int yy2 = (int) Math.round(oldMapWindow.getMaxY());
1015
1016 graphics.drawImage(getPreFinalImage(), xx1, yy1, xx2, yy2,
1017 (int) visibleArea.getMinX(), (int) visibleArea.getMinY(),
1018 (int) visibleArea.getMaxX(), (int) visibleArea.getMaxY(),
1019 getMapBackgroundColor(), null);
1020
1021 final Rectangle painedArea = new Rectangle(xx1, yy1, xx2 - xx1, yy2
1022 - yy1);
1023
1024 SwingUtil.clearAround(graphics, painedArea, visibleArea);
1025
1026 addGadgets(graphics, true);
1027
1028 quickPreviewHint = 0;
1029
1030 repaintTimer.restart();
1031
1032 graphics.dispose();
1033
1034 // Something has been drawn
1035 return true;
1036 }
1037
1038 public MapContext getBgContext() {
1039 return bgContext;
1040 }
1041
1042 /**
1043 * Lazyly initializes a {@link BufferedImage} for the background renderer.
1044 */
1045 private Image getBgImage() {
1046 //
1047 // if (bgImage == null) {
1048 // bgImage = createImage(getBounds().width, getBounds().height);
1049 // }
1050
1051 return bgImage;
1052 }
1053
1054 public MapContext getContext() {
1055 if (localContext == null) {
1056 setLocalContext(new DefaultMapContext());
1057 }
1058 return localContext;
1059 }
1060
1061 private BufferedImage getFinalImage() {
1062 //
1063 if (finalImage == null) {
1064 // Rectangle curPaintArea = getVisibleRect();
1065 finalImage = new BufferedImage(getVisibleRect().width,
1066 getVisibleRect().height, IMAGETYPE);
1067
1068 requestStartRendering();
1069 }
1070 return finalImage;
1071 }
1072
1073 public RenderingHints getJava2dHints() {
1074 return java2dHints;
1075 }
1076
1077 /**
1078 * Lazyly initializes a {@link BufferedImage} for the background renderer.
1079 */
1080 private BufferedImage getLocalImage() {
1081
1082 if (localImage == null) {
1083 localImage = new BufferedImage(getVisibleRect().width,
1084 getVisibleRect().height, IMAGETYPE_withAlpha);
1085 }
1086
1087 return localImage;
1088 }
1089
1090 /**
1091 * Returns a copy of the mapArea
1092 *
1093 * @return
1094 */
1095 public ReferencedEnvelope getMapArea() {
1096 if (mapArea == null) {
1097 ReferencedEnvelope mapArea_ = null;
1098 try {
1099 mapArea_ = localContext.getLayerBounds();
1100 } catch (final IOException e) {
1101 LOGGER.warn("context.getLayerBounds()", e);
1102 }
1103
1104 if (mapArea_ != null) {
1105 mapImageInvalid = true; /* note we need to redraw */
1106 // setMapArea(mapArea_); // results in a loop
1107 mapArea = bestAllowedMapArea(mapArea_);
1108 }
1109 }
1110
1111 if (mapArea == null)
1112 return null;
1113
1114 // TODO is needed at all, this should go to setMapArea maybe
1115 if (localContext.getCoordinateReferenceSystem() == null)
1116 try {
1117 localContext.setCoordinateReferenceSystem(GeoImportUtil
1118 .getDefaultCRS());
1119 } catch (Exception e) {
1120 throw new RuntimeException("setting context CRS:", e);
1121 }
1122
1123 return new ReferencedEnvelope(mapArea, localContext
1124 .getCoordinateReferenceSystem());
1125 }
1126
1127 /**
1128 * Returns the background {@link Color} of the map pane. Default is white.
1129 **/
1130 public Color getMapBackgroundColor() {
1131 return mapBackgroundColor;
1132 }
1133
1134 /**
1135 * Get the BufferedImage to use as a flaoting icon in the lower right
1136 * corner.
1137 *
1138 * @return <code>null</code> if the feature is deactivated.
1139 */
1140 public BufferedImage getMapImage() {
1141 return mapImage;
1142 }
1143
1144 /**
1145 * Returns the evelope of the viewable area. The JMapPane will never show
1146 * anything outside of this extend. If this has been set to
1147 * <code>null</code> via {@link #setMaxExtend(Envelope)}, it tries to return
1148 * quickly the context's bounds. It it takes to long to determine the
1149 * context bounds, <code>null</code> is returned.
1150 *
1151 * @param maxExtend
1152 * <code>null</code> to not have this restriction.
1153 */
1154
1155 public Envelope getMaxExtend() {
1156 if (maxExtend == null) {
1157 final ReferencedEnvelope layerBounds = GTUtil
1158 .getVisibleLayoutBounds(localContext);
1159 if (layerBounds == null) {
1160 // TODO Last fallback could be the CRS valid area
1161 return null;
1162 }
1163
1164 // Kartenbereich um 10% vergroessern
1165 return JTSUtil.fixAspectRatio(getVisibleRect(), JTSUtil
1166 .expandEnvelope(layerBounds, 0.1), true);
1167 }
1168 return maxExtend;
1169 }
1170
1171 /**
1172 * Retuns the maximum allowed zoom scale. This is the smaller number value
1173 * of the two. Defaults to {@link Double}.MIN_VALUE
1174 *
1175 * @author <a href="mailto:[email protected]">Stefan Alfons
1176 * Kr&uuml;ger</a>
1177 */
1178 public Double getMaxZoomScale() {
1179 return maxZoomScale;
1180 }
1181
1182 /**
1183 * Retuns the minimum allowed zoom scale. This is the bigger number value of
1184 * the two. Defaults to {@link Double}.MAX_VALUE
1185 *
1186 * @author <a href="mailto:[email protected]">Stefan Alfons
1187 * Kr&uuml;ger</a>
1188 */
1189 public Double getMinZoomScale() {
1190 return minZoomScale;
1191 }
1192
1193 private Image getPreFinalImage() {
1194 // if (preFinalImage == null) {
1195 //
1196 // // Rectangle curPaintArea = getVisibleRect();
1197 // // preFinalImage = new BufferedImage(curPaintArea.width,
1198 // // curPaintArea.height, BufferedImage.TYPE_INT_RGB);
1199 //
1200 // preFinalImage = createImage(getBounds().width, getBounds().height);
1201 //
1202 // requestStartRendering();
1203 // }
1204 return preFinalImage;
1205 }
1206
1207 public Map<Object, Object> getRendererHints() {
1208 // Clear label cache
1209 labelCache.clear();
1210 rendererHints.put(StreamingRenderer.LABEL_CACHE_KEY, labelCache);
1211
1212 return rendererHints;
1213 }
1214
1215 /**
1216 * Liefert eine affine Transformation, um von den Fenster-Koordinaten in die
1217 * Karten-Koordinaten (Lat/Lon) umzurechnen.
1218 *
1219 * @return eine Kopie der aktuellen Transformation; <code>null</code> wenn
1220 * noch keine Karte angezeigt wird
1221 */
1222 public AffineTransform getScreenToWorld() {
1223 if (screenToWorld == null)
1224 resetTransforms();
1225 // nur Kopie der Transformation zurueckgeben!
1226 if (screenToWorld == null)
1227 return null;
1228 return new AffineTransform(screenToWorld);
1229 }
1230
1231 public int getState() {
1232 return state;
1233 }
1234
1235 /**
1236 * Liefert den statisch eingestellten Cursor, der unabhaengig von der
1237 * eingestellten MapPane-Aktion (Zoom, Auswahl, ...) verwendet wird.
1238 *
1239 * @return {@code null}, wenn kein statischer Cursor verwendet, sondern der
1240 * Cursor automatisch je nach MapPane-Aktion eingestellt wird.
1241 */
1242 public Cursor getStaticCursor() {
1243 return this.staticCursor;
1244 }
1245
1246 public AffineTransform getWorldToScreenTransform() {
1247 if (worldToScreen == null) {
1248 resetTransforms();
1249 }
1250 // nur Kopie der Transformation zurueckgeben!
1251 return new AffineTransform(worldToScreen);
1252 }
1253
1254 /**
1255 * A flag indicating if dispose() has already been called. If true, then
1256 * further use of this {@link SelectableXMapPane} is undefined.
1257 */
1258 private boolean isDisposed() {
1259 return disposed;
1260 }
1261
1262 /**
1263 * Returns whether a layer is regarded or ignored on {@link #SELECT_TOP},
1264 * {@link #SELECT_ALL} and {@link #SELECT_ONE_FROM_TOP} actions. Returns
1265 * <code>true</code> if the selectability has not been defined.
1266 *
1267 * @param layer
1268 * a layer
1269 */
1270 public boolean isMapLayerSelectable(final MapLayer layer) {
1271 final Boolean selectable = mapLayerSelectable.get(layer);
1272 return selectable == null ? true : selectable;
1273 }
1274
1275 /**
1276 * Return <code>true</code> if a CRS and a {@link #mapArea} are set and the
1277 * {@link XMapPane} is visible and has bounds set.
1278 */
1279 public boolean isWellDefined() {
1280
1281 try {
1282
1283 if (getContext() == null)
1284 return false;
1285 if (getContext().getLayerCount() <= 0)
1286 return false;
1287 if (getMapArea() == null)
1288 return false;
1289 if (getBounds().getWidth() == 0)
1290 return false;
1291 if (getBounds().getHeight() == 0)
1292 return false;
1293 } catch (final Exception e) {
1294 return false;
1295 }
1296 return true;
1297 }
1298
1299 public void mouseDragged(final Point startPos, final Point lastPos,
1300 final MouseEvent event) {
1301
1302 if ((getState() == XMapPane.PAN)
1303 || ((event.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0)) {
1304
1305 if (getCursor() != SwingUtil.PANNING_CURSOR) {
1306 setCursor(SwingUtil.PANNING_CURSOR);
1307
1308 // While panning, we deactivate the rendering. So the tasts are
1309 // ready to start when the panning os done.
1310 if (bgExecuter != null)
1311 bgExecuter.cancelTask();
1312 if (localExecuter != null)
1313 localExecuter.cancelTask();
1314 }
1315
1316 if (lastPos.x > 0 && lastPos.y > 0) {
1317 final int dx = event.getX() - lastPos.x;
1318 final int dy = event.getY() - lastPos.y;
1319
1320 // TODO Stop dragging when the drag would not be valid...
1321 // boolean dragValid = true;
1322 // // check if this panning results in a valid mapArea
1323 // {
1324 // Rectangle winBounds = xMapPane.getBounds();
1325 // winBounds.translate(xMapPane.imageOrigin.x,
1326 // -xMapPane.imageOrigin.y);
1327 // Envelope newMapAreaBefore = xMapPane.tranformWindowToGeo(
1328 // winBounds.x, winBounds.y, winBounds.x
1329 // + winBounds.width, winBounds.y
1330 // + winBounds.height);
1331 //
1332 //
1333 // winBounds = xMapPane.getBounds();
1334 // Point testIng = new Point(xMapPane.imageOrigin);
1335 // testIng.translate(dx, dy);
1336 // winBounds.translate(testIng.x, -testIng.y);
1337 // Envelope newMapAreaAfter = xMapPane.tranformWindowToGeo(
1338 // winBounds.x, winBounds.y, winBounds.x
1339 // + winBounds.width, winBounds.y
1340 // + winBounds.height);
1341 //
1342 // // If the last drag doesn't change the MapArea anymore cancel
1343 // it.
1344 // if (xMapPane.bestAllowedMapArea(newMapAreaAfter).equals(
1345 // xMapPane.bestAllowedMapArea(newMapAreaBefore))){
1346 // dragValid = false;
1347 // return;
1348 // }
1349 // }
1350
1351 imageOrigin.translate(dx, dy);
1352 updateFinalImage();
1353 repaint();
1354 }
1355
1356 } else if ((getState() == XMapPane.ZOOM_IN)
1357 || (getState() == XMapPane.ZOOM_OUT)
1358 || (getState() == XMapPane.SELECT_ALL)
1359 || (getState() == XMapPane.SELECT_TOP)
1360 // || (getState() == XMapPane.SELECT_ONE_FROM_TOP)
1361 ) {
1362 final Graphics graphics = getGraphics();
1363
1364 drawRectangle(graphics, startPos, event.getPoint());
1365
1366 if ((lastPos.x > 0) && (lastPos.y > 0)) {
1367 drawRectangle(graphics, startPos, lastPos);
1368 }
1369
1370 graphics.dispose();
1371
1372 }
1373
1374 }
1375
1376 /**
1377 * Called by the {@link RenderingExecutor} when rendering was cancelled.
1378 */
1379 public void onRenderingCancelled() {
1380 repaintTimer.stop();
1381 LOGGER.debug("Rendering cancelled");
1382 }
1383
1384 /**
1385 * Called by the {@link RenderingExecutor} when rendering has been
1386 * completed.
1387 */
1388 public void onRenderingCompleted() {
1389 repaintTimer.stop();
1390 updateFinalImage();
1391 repaint();
1392 if (renderingErrors.size() > 0)
1393 renderingErrors.remove(0);
1394 }
1395
1396 /**
1397 * Called by the {@linkplain XMapPane.RenderingTask} when rendering failed.
1398 * Publishes a {@linkplain MapPaneEvent} of type {@code
1399 * MapPaneEvent.Type.RENDERING_STOPPED} to listeners.
1400 *
1401 * @param renderingError
1402 * The error that occured during rendering
1403 *
1404 * @see MapPaneListener#onRenderingStopped(org.geotools.swing.event.MapPaneEvent)
1405 */
1406 public void onRenderingFailed(final Exception renderingError) {
1407 this.renderingErrors.add(renderingError);
1408 if (renderingErrors.size() > 3)
1409 renderingErrors.remove(0);
1410 repaintTimer.stop();
1411 LOGGER.warn("Rendering failed", renderingError);
1412 updateFinalImage();
1413 repaint();
1414
1415 }
1416
1417 public void onRenderingPending() {
1418 // LOGGER.debug("Pending rendering updates the preview...");
1419 updateFinalImage();
1420 repaint();
1421 }
1422
1423 @Override
1424 protected void paintComponent(final Graphics g) {
1425 if (!acceptsRepaintCalls)
1426 return;
1427
1428 // Maybe update the cursor and maybe stop the repainting timer
1429 updateCursor();
1430
1431 // super.paintComponent(g); // candidate for removal
1432
1433 boolean paintedSomething = false;
1434
1435 if (mapImageInvalid) { /* if the map changed then redraw */
1436
1437 mapImageInvalid = false; // Reset for next round
1438
1439 // If the new mapArea and the oldMapArea intersect, we can draw some
1440 // quick scaled preview to make the user feel that something is
1441 // happening.
1442 if (mapAreaChanged && oldMapArea != null
1443 && getMapArea().intersects(oldMapArea)
1444 & !getMapArea().equals(oldMapArea)) {
1445
1446 mapAreaChanged = false;
1447
1448 if (getMapArea().covers(oldMapArea)) {
1449 setQuickPreviewHint(ZOOM_OUT);
1450 paintedSomething = drawScaledPreviewImage_Zoom((Graphics2D) g);
1451 } else if (oldMapArea.covers(getMapArea())) {
1452 setQuickPreviewHint(ZOOM_IN);
1453 paintedSomething = drawScaledPreviewImage_Zoom((Graphics2D) g);
1454 }
1455
1456 }
1457
1458 if (paneResized) {
1459 paneResized = false;
1460 disposeImages();
1461 }
1462
1463 // Start the Threads and Timers to render the image
1464 requestStartRendering();
1465
1466 }
1467
1468 if (!paintedSomething) {
1469
1470 g.drawImage(getFinalImage(), 0, 0, null);
1471
1472 g.dispose(); // cand. for removal
1473
1474 paintedSomething = true; // cand. for removal
1475 }
1476
1477 }
1478
1479 /**
1480 * Heavily works on releasing all resources related to the four
1481 * {@link Image}s used to cache the results of the different renderers.<br>
1482 * In November 2009 i had some memory leaking problems with {@link XMapPane}
1483 * . The resources of the buffered images were never released. It seem to be
1484 * important to call the GC right after flushing the images.<br>
1485 * Hence this method may take a while, because it calls the GC up to four
1486 * times.
1487 */
1488 private void disposeImages() {
1489
1490 // System.out.println("vorher = "
1491 // + new MbDecimalFormatter().format(LangUtil.gcTotal()));
1492 // bi.flush();
1493 // return bi = null;
1494 // System.out.println("nacher = "
1495 // + new MbDecimalFormatter().format(LangUtil.gcTotal()));
1496 //
1497 // System.out.println("\n");
1498
1499 if (preFinalImage != null) {
1500 preFinalImage.flush();
1501 preFinalImage = null;
1502 LangUtil.gc();
1503 }
1504 if (finalImage != null) {
1505 finalImage.flush();
1506 finalImage = null;
1507 LangUtil.gc();
1508 }
1509 if (localImage != null) {
1510 localImage.flush();
1511 localImage = null;
1512 LangUtil.gc();
1513 }
1514 if (bgImage != null) {
1515 bgImage.flush();
1516 bgImage = null;
1517 LangUtil.gc();
1518 }
1519
1520 }
1521
1522 /**
1523 * Performs a {@value #PAN} action. During panning, the displacement is
1524 * stored in {@link #imageOrigin} object. Calling {@link #performPan()} will
1525 * reset the offset and call {@link #setMapArea(Envelope)}.
1526 */
1527 public void performPan() {
1528
1529 Rectangle winBounds = getVisibleRect();
1530
1531 winBounds.translate(-imageOrigin.x, -imageOrigin.y);
1532 final Envelope newMapArea = tranformWindowToGeo(winBounds.x,
1533 winBounds.y, winBounds.x + winBounds.width, winBounds.y
1534 + winBounds.height);
1535
1536 imageOrigin.x = 0;
1537 imageOrigin.y = 0;
1538
1539 if (!setMapArea(newMapArea)) {
1540 /**
1541 * If setMapArea returns true, the finalImage is updated anyways.
1542 * This if-case exists to ensure that we repaint a correct image
1543 * even if the new panning area has been denied.
1544 */
1545 updateFinalImage();
1546 repaint();
1547 }
1548
1549 if (getCursor() == SwingUtil.PANNING_CURSOR)
1550 setCursor(SwingUtil.PAN_CURSOR);
1551 }
1552
1553 //
1554 // /**
1555 // * Nuetzlich wenn die Componente gedruckt (z.B. wenn ein Screenshot
1556 // gemacht
1557 // * wird) wird. Dann werden wird der Hintergrund auf WEISS gesetzt.
1558 // *
1559 // * @author <a href="mailto:[email protected]">Stefan Alfons
1560 // * Kr&uuml;ger</a>
1561 // */
1562 // @Override
1563 // public void print(final Graphics g) {
1564 // final Color orig = getBackground();
1565 // setBackground(Color.WHITE);
1566 //
1567 // // wrap in try/finally so that we always restore the state
1568 // try {
1569 // super.print(g);
1570 // } finally {
1571 // setBackground(orig);
1572 // }
1573 // }
1574
1575 /**
1576 * Entfernt einen Listener von der Map.
1577 *
1578 * @param l
1579 * zu entfernender Listener
1580 */
1581 public void removeMapPaneListener(final JMapPaneListener l) {
1582 mapPaneListeners.remove(l);
1583 }
1584
1585 /**
1586 * Cancels all running renderers and sets the flag to start new ones. <br>
1587 *
1588 * @see #startRenderThreadsTimer
1589 */
1590 private void requestStartRendering() {
1591 if (bgExecuter != null)
1592 bgExecuter.cancelTask();
1593 if (localExecuter != null)
1594 localExecuter.cancelTask();
1595 requestStartRenderingLocal = true;
1596
1597 }
1598
1599 //
1600 // /**
1601 // * Berechnet die Transformation zwischen Fenster- und Karten-Koordinaten
1602 // * neu.
1603 // */
1604 // protected void resetTransforms() {
1605 // if (getMapArea() == null || getWidth() == 0 || getHeight() == 0)
1606 // return;
1607 //
1608 // // We store the last Transform
1609 // oldScreenToWorld = screenToWorld;
1610 //
1611 // this.screenToWorld = new AffineTransform(
1612 // // Genauso wie die Fenster-Koordinaten, werden die Longitude-Koordinaten
1613 // // nach rechts (Osten) hin groesser
1614 // // --> positive Verschiebung
1615 // getMapArea().getWidth() / getWidth(),
1616 // // keine Verzerrung
1617 // 0.0, 0.0,
1618 // // Waehrend die Fenster-Koordinaten nach unten hin groesser
1619 // // werden,
1620 // // werden Latitude-Koordinaten nach Sueden hin keiner
1621 // // --> negative Verschiebung
1622 // -getMapArea().getHeight() / getHeight(),
1623 // // Die Longitude-Koordinaten werden nach Osten hin groesser
1624 // // --> obere linke Ecke des Fensters hat also den Minimalwert
1625 // getMapArea().getMinX(),
1626 // // Die Latitude-Koordinaten werden nach Norden hin groesser
1627 // // --> obere linke Ecke des Fensters hat also den Maximalwert
1628 // getMapArea().getMaxY());
1629 //
1630 // try {
1631 // this.worldToScreen = screenToWorld.createInverse();
1632 // } catch (final NoninvertibleTransformException e) {
1633 // LOGGER.error(e);
1634 // }
1635 // }
1636
1637 /**
1638 * Calculate the affine transforms used to convert between world and pixel
1639 * coordinates. The calculations here are very basic and assume a cartesian
1640 * reference system.
1641 * <p>
1642 * Tne transform is calculated such that {@code envelope} will be centred in
1643 * the display
1644 *
1645 * @param envelope
1646 * the current map extent (world coordinates)
1647 * @param paintArea
1648 * the current map pane extent (screen units)
1649 */
1650 private void resetTransforms() {
1651 ReferencedEnvelope refMapEnv = new ReferencedEnvelope(mapArea,
1652 getContext().getCoordinateReferenceSystem());
1653
1654 Rectangle paintArea = getVisibleRect(); // NOT USE GET BOUNDS!
1655
1656 //
1657 // double xscale = paintArea.getWidth() / refEnv.getWidth();
1658 // double yscale = paintArea.getHeight() / refEnv.getHeight();
1659 //
1660 // double scale = Math.min(xscale, yscale);
1661 //
1662 // double xoff = refEnv.getMedian(0) * scale - paintArea.getCenterX();
1663 // double yoff = refEnv.getMedian(1) * scale + paintArea.getCenterY();
1664
1665 worldToScreen = RendererUtilities.worldToScreenTransform(refMapEnv,
1666 paintArea);
1667
1668 // worldToScreen = new AffineTransform(scale, 0, 0, -scale, -xoff,
1669 // yoff);
1670 try {
1671 screenToWorld = worldToScreen.createInverse();
1672
1673 } catch (NoninvertibleTransformException ex) {
1674 ex.printStackTrace();
1675 }
1676 }
1677
1678 public void setBgContext(final MapContext context) {
1679
1680 // Remove the default listener from the old context
1681 if (this.bgContext != null) {
1682 this.bgContext.removeMapLayerListListener(bgContextListener);
1683
1684 // adding listener to all layers
1685 for (final MapLayer mapLayer : bgContext.getLayers()) {
1686 mapLayer.removeMapLayerListener(bgMapLayerListener);
1687 }
1688 }
1689
1690 this.bgContext = context;
1691
1692 if (context != null) {
1693 setMapArea(bgContext.getAreaOfInterest());
1694
1695 this.bgContext.addMapLayerListListener(bgContextListener);
1696
1697 // adding listener to all layers
1698 for (final MapLayer mapLayer : bgContext.getLayers()) {
1699 mapLayer.addMapLayerListener(bgMapLayerListener);
1700 }
1701 }
1702 mapImageInvalid = true;
1703 repaint();
1704 }
1705
1706 public void setJava2dHints(final RenderingHints java2dHints) {
1707 this.java2dHints = java2dHints;
1708 }
1709
1710 /**
1711 *
1712 * @param context
1713 */
1714 public void setLocalContext(final MapContext context) {
1715 // Remove the default listener from the old context
1716 if (this.localContext != null) {
1717 this.localContext.removeMapLayerListListener(localContextListener);
1718
1719 // adding listener to all layers
1720 for (final MapLayer mapLayer : localContext.getLayers()) {
1721 mapLayer.removeMapLayerListener(localMapLayerListener);
1722 }
1723 }
1724
1725 this.localContext = context;
1726
1727 if (context != null) {
1728
1729 setMapArea(localContext.getAreaOfInterest());
1730
1731 localRenderer.setContext(localContext);
1732
1733 this.localContext.addMapLayerListListener(localContextListener);
1734
1735 // adding listener to all layers
1736 for (final MapLayer mapLayer : localContext.getLayers()) {
1737 mapLayer.addMapLayerListener(localMapLayerListener);
1738 }
1739 }
1740
1741 mapImageInvalid = true;
1742 repaint();
1743 }
1744
1745 /**
1746 * @param newMapArea
1747 * @return <code>true</code> if the mapArea has been changed and a repaint
1748 * has been triggered.
1749 */
1750 public boolean setMapArea(final Envelope newMapArea) {
1751
1752 if (newMapArea == null
1753 || bestAllowedMapArea(newMapArea).equals(mapArea)) {
1754 // No change.. no need to repaint
1755 return false;
1756 }
1757
1758 // Testing, whether NaN or Infinity are used in the newMapArea
1759 if (newMapArea.isNull() || Double.isInfinite(newMapArea.getMaxX())
1760 || Double.isInfinite(newMapArea.getMaxY())
1761 || Double.isInfinite(newMapArea.getMinX())
1762 || Double.isInfinite(newMapArea.getMinY())) {
1763 // No change.. ugly new values
1764 LOGGER.warn("setMapArea has been called with newArea = "
1765 + newMapArea);
1766 return false;
1767 }
1768
1769 final Envelope candNew = bestAllowedMapArea(newMapArea);
1770
1771 // Testing, whether the difference if just minimal
1772 if (mapArea != null) {
1773 final double tolX = mapArea.getWidth() / 1000.;
1774 final double tolY = mapArea.getHeight() / 1000.;
1775 if ((candNew.getMinX() - tolX < mapArea.getMinX())
1776 && (mapArea.getMinX() < candNew.getMinX() + tolX)
1777 && (candNew.getMaxX() - tolX < mapArea.getMaxX())
1778 && (mapArea.getMaxX() < candNew.getMaxX() + tolX)
1779
1780 && (candNew.getMinY() - tolY < mapArea.getMinY())
1781 && (mapArea.getMinY() < candNew.getMinY() + tolY)
1782 && (candNew.getMaxY() - tolY < mapArea.getMaxY())
1783 && (mapArea.getMaxY() < candNew.getMaxY() + tolY)
1784
1785 ) {
1786 // The two mapAreas only differ my 1/1000th.. ignore
1787
1788 return false;
1789 }
1790 }
1791
1792 // New map are is accepted:
1793 oldMapArea = mapArea;
1794 mapArea = candNew;
1795 resetTransforms();
1796
1797 if (localContext != null) {
1798 localContext.setAreaOfInterest(mapArea, localContext
1799 .getCoordinateReferenceSystem());
1800 }
1801 if (bgContext != null) {
1802 bgContext.setAreaOfInterest(mapArea, localContext
1803 .getCoordinateReferenceSystem());
1804 }
1805 mapImageInvalid = true;
1806 mapAreaChanged = true;
1807 repaint();
1808
1809 LOGGER.debug("New maparea = " + mapArea);
1810 return true;
1811 }
1812
1813 /**
1814 * Set the background color of the map.
1815 *
1816 * @param if <code>null</code>, white is used.
1817 */
1818 public void setMapBackgroundColor(Color bgColor) {
1819 if (bgColor == null)
1820 bgColor = Color.WHITE;
1821 this.mapBackgroundColor = bgColor;
1822 }
1823
1824 /**
1825 * Set the BufferedImage to use as a flaoting icon in the lower right corner
1826 *
1827 * @param mapImageIcon
1828 * <code>null</code> is allowed and deactivates this icon.
1829 */
1830 public void setMapImage(final BufferedImage mapImage) {
1831 this.mapImage = mapImage;
1832 }
1833
1834 /**
1835 * Sets whether a layer is regarded or ignored on {@link #SELECT_TOP},
1836 * {@link #SELECT_ALL} and {@link #SELECT_ONE_FROM_TOP} actions.
1837 *
1838 * @param layer
1839 * a layer
1840 * @param selectable
1841 * if {@code false} the layer is ignored during the upper
1842 * mentioned actions. If <code>null</code>, the default (true)
1843 * will be used.
1844 */
1845 public void setMapLayerSelectable(final MapLayer layer,
1846 final Boolean selectable) {
1847 if (selectable == null)
1848 mapLayerSelectable.remove(layer);
1849 else
1850 mapLayerSelectable.put(layer, selectable);
1851 }
1852
1853 /**
1854 * Defines an evelope of the viwable area. The JMapPane will never show
1855 * anything outside of this extend.
1856 *
1857 * @param maxExtend
1858 * <code>null</code> to not have this restriction.
1859 */
1860 public void setMaxExtend(final Envelope maxExtend) {
1861 this.maxExtend = maxExtend;
1862 }
1863
1864 /**
1865 * Set the maximum allowed zoom scale. This is the smaller number value of
1866 * the two. If <code>null</code> is passed, Double.MINVALUE are used which
1867 * mean there is no restriction.
1868 *
1869 * @author <a href="mailto:[email protected]">Stefan Alfons
1870 * Kr&uuml;ger</a>
1871 */
1872 public void setMaxZoomScale(final Double maxZoomScale) {
1873 this.maxZoomScale = maxZoomScale == null ? Double.MIN_VALUE
1874 : maxZoomScale;
1875 }
1876
1877 // /** Stored the time used for the last real rendering in ms. **/
1878 // private long lastRenderingDuration = Long.MAX_VALUE;
1879
1880 /**
1881 * Set the minimum (nearest) allowed zoom scale. This is the bigger number
1882 * value of the two. If <code>null</code> is passed, Double.MAXVALUE are
1883 * used which mean there is no restriction.
1884 *
1885 *
1886 * @author <a href="mailto:[email protected]">Stefan Alfons
1887 * Kr&uuml;ger</a>
1888 */
1889 public void setMinZoomScale(final Double minZoomScale) {
1890 this.minZoomScale = minZoomScale == null ? Double.MAX_VALUE
1891 : minZoomScale;
1892 }
1893
1894 /**
1895 *
1896 * @param b
1897 */
1898 public void setPainting(final boolean b) {
1899 acceptsRepaintCalls = b;
1900 }
1901
1902 // /**
1903 // * Returns in milli seconds the time the last rending of the
1904 // * {@link SelectableXMapPane} took. #Long.MAX_VALUE if the JMapPane has
1905 // not
1906 // * been rendered yet.
1907 // */
1908 // public long getLastRenderingDuration() {
1909 // return lastRenderingDuration;
1910 // }
1911
1912 public void setQuickPreviewHint(final int quickPreviewHint) {
1913 this.quickPreviewHint = quickPreviewHint;
1914
1915 }
1916
1917 private void setRendererHints(final Map<Object, Object> rendererHints) {
1918 if (rendererHints != null)
1919 this.rendererHints = rendererHints;
1920 }
1921
1922 /**
1923 * Enables/Disables the ZOOM Mouse Listener. Upates the Cursor and stops the
1924 * repaint Timer if
1925 *
1926 * @param state
1927 */
1928 public void setState(final int state) {
1929 this.state = state;
1930
1931 zoomMapPaneMouseListener.setEnabled((state == ZOOM_IN
1932 || state == ZOOM_OUT || state == PAN));
1933
1934 // Je nach Aktion den Cursor umsetzen
1935 updateCursor();
1936 }
1937
1938 /**
1939 * Standardmaessig wird der Cursor automatisch je nach MapPane-Aktion (Zoom,
1940 * Auswahl, ...) gesetzt. Mit dieser Methode kann ein statischer Cursor
1941 * gesetzt werden, der unabhaengig von der aktuellen MapPanes-Aktion
1942 * beibehalten wird. Um diesen statischen Cursor wieder zu entfernen, kann
1943 * {@code null} als Parameter uebergeben werden
1944 *
1945 * @param cursor
1946 * Cursor
1947 */
1948 public void setStaticCursor(final Cursor cursor) {
1949 this.staticCursor = cursor;
1950 if (cursor != null)
1951 super.setCursor(cursor);
1952 }
1953
1954 /**
1955 * Starts rendering on one or two threads
1956 */
1957 private void startRendering() {
1958
1959 if (!isWellDefined())
1960 return;
1961
1962 if (bgExecuter != null) {
1963 // Stop all renderers
1964 bgExecuter.cancelTask();
1965 }
1966
1967 if (localExecuter != null) {
1968 localExecuter.cancelTask();
1969 }
1970
1971 final Rectangle curPaintArea = getVisibleRect();
1972
1973 /**
1974 * We have to set new renderer
1975 */
1976
1977 if (getBgContext() != null) {
1978 bgRenderer.setJava2DHints(getJava2dHints());
1979 bgRenderer.setRendererHints(getRendererHints());
1980
1981 // bgExecuter = new RenderingExecutor();
1982 // LOGGER.debug("starting bg renderer:");
1983 // // /* System.out.println("rendering"); */
1984 // final GTRenderer createGTRenderer = GTUtil.createGTRenderer(
1985 // bgContext, getRendererHints());
1986 // createGTRenderer.setJava2DHints(getJava2dHints());
1987 // bgExecuter.submit(getBgContext().getAreaOfInterest(),
1988 // curPaintArea,
1989 // (Graphics2D) getBgImage().getGraphics(), createGTRenderer);
1990 }
1991
1992 if (getContext() != null) {
1993 // localExecuter = new RenderingExecutor(this, 150l);
1994 // LOGGER.debug("starting local renderer:");
1995
1996 localRenderer.setJava2DHints(getJava2dHints());
1997 localRenderer.setRendererHints(getRendererHints());
1998
1999 ReferencedEnvelope areaOfInterest = getMapArea();
2000 final boolean submitted = localExecuter.submit(areaOfInterest,
2001 curPaintArea, (Graphics2D) getLocalImage().getGraphics(),
2002 localRenderer, getWorldToScreenTransform());
2003 if (submitted)
2004 repaintTimer.restart();
2005 else
2006 requestStartRenderingLocal = true; // Try to start rendering again in
2007 // a moment
2008 }
2009
2010 updateCursor();
2011 }
2012
2013 /**
2014 * Transformiert einen Geo-Koordinaten-Bereich in Fenster-Koordinaten.
2015 *
2016 * @param ox
2017 * X-Koordinate der VON-Position
2018 * @param oy
2019 * Y-Koordinate der VON-Position
2020 * @param px
2021 * X-Koordinate der BIS-Position
2022 * @param py
2023 * Y-Koordinate der BIS-Position
2024 * @param winToGeotransform
2025 * Eine Window to Geo transform. If <code>null</code>,
2026 * {@link #getScreenToWorld()} is used.
2027 */
2028 public Envelope tranformGeoToWindow(final double ox, final double oy,
2029 final double px, final double py) {
2030 final AffineTransform at = getWorldToScreenTransform();
2031 Point2D geoO;
2032 // try {
2033 geoO = at.transform(new Point2D.Double(ox, oy), null);
2034 final Point2D geoP = at.transform(new Point2D.Double(px, py), null);
2035 return new Envelope(geoO.getX(), geoP.getX(), geoO.getY(), geoP.getY());
2036 // } catch (final NoninvertibleTransformException e) {
2037 // LOGGER.error(e);
2038 // return new Envelope(ox, oy, px, py);
2039 // }
2040 }
2041
2042 /**
2043 * Transformiert einen Fenster-Koordinaten-Bereich in Geo-Koordinaten.
2044 *
2045 * @param ox
2046 * X-Koordinate der VON-Position
2047 * @param oy
2048 * Y-Koordinate der VON-Position
2049 * @param px
2050 * X-Koordinate der BIS-Position
2051 * @param py
2052 * Y-Koordinate der BIS-Position
2053 */
2054 public Envelope tranformWindowToGeo(final int ox, final int oy,
2055 final int px, final int py) {
2056 final AffineTransform at = getScreenToWorld();
2057 final Point2D geoO = at.transform(new Point2D.Double(ox, oy), null);
2058 final Point2D geoP = at.transform(new Point2D.Double(px, py), null);
2059
2060 // Mmmmm... don't really understand why its x,x,y,y
2061 // return new Envelope(geoO.getX(), geoP.getX(), geoO.getY(),
2062 // geoP.getY());
2063 return new Envelope(new Coordinate(geoO.getX(), geoO.getY()),
2064 new Coordinate(geoP.getX(), geoP.getY()));
2065 }
2066
2067 /**
2068 * Will update the cursor. If all rendering is finished also stops the
2069 * {@link #repaintTimer}
2070 */
2071 public void updateCursor() {
2072
2073 // if the renderers have stopped, also stop the timer that is updating
2074 // the final image
2075 if (bgExecuter != null && bgExecuter.isRunning()
2076 || localExecuter != null && localExecuter.isRunning()) {
2077 setCursor(WAIT_CURSOR);
2078 return;
2079 } else {
2080 // Allow one last rendering
2081 if (repaintTimer.isRunning()) {
2082 System.out.println("one last rendering....");
2083 repaintTimer.stop();
2084 updateFinalImage();
2085 repaint();
2086 }
2087 }
2088
2089 // wenn manueller Cursor gesetzt ist, dann diesen verwenden (unabhaengig
2090 // von der aktuellen Aktion
2091 if (this.staticCursor != null) {
2092 setCursor(staticCursor);
2093 return;
2094 }
2095 if (getCursor() == SwingUtil.PANNING_CURSOR) {
2096 // This cursor will reset itself
2097 return;
2098 }
2099
2100 // Set the cursor depending on what tool is in use...
2101 switch (state) {
2102 case SELECT_TOP:
2103 case SELECT_ONE_FROM_TOP:
2104 case SELECT_ALL:
2105 setCursor(SwingUtil.CROSSHAIR_CURSOR);
2106 break;
2107 case ZOOM_IN:
2108 setCursor(SwingUtil.ZOOMIN_CURSOR);
2109 break;
2110 case ZOOM_OUT:
2111 setCursor(SwingUtil.ZOOMOUT_CURSOR);
2112 break;
2113 case PAN:
2114 setCursor(SwingUtil.PAN_CURSOR);
2115 break;
2116 default:
2117 setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
2118 break;
2119 }
2120 }
2121
2122 /**
2123 * The renderers are all rendering into their own {@link Image}s. This
2124 * method combines all images to one {@link #finalImage}. The
2125 * {@link #repaintTimer} is calling this method regularely to update the
2126 * {@link #finalImage} even if the renderers are still working.
2127 */
2128 synchronized protected Image updateFinalImage() {
2129
2130 // Render the two map images first, into the preFinalImage
2131 if (bgExecuter != null) {
2132 final Graphics2D preFinalG = (Graphics2D) getPreFinalImage()
2133 .getGraphics();
2134 preFinalG.setBackground(getMapBackgroundColor());
2135
2136 preFinalG.drawImage(getBgImage(), 0, 0, getMapBackgroundColor(),
2137 null);
2138
2139 // // Draw the local layers image
2140 preFinalG.drawImage(getLocalImage(), 0, 0, null);
2141 preFinalG.dispose();
2142
2143 } else {
2144 preFinalImage = getLocalImage();
2145 }
2146
2147 final Graphics2D finalG = getFinalImage().createGraphics();
2148 finalG.setBackground(getMapBackgroundColor());
2149 finalG.drawImage(getPreFinalImage(), imageOrigin.x, imageOrigin.y,
2150 getMapBackgroundColor(), null);
2151
2152 final int finalImageHeight = getFinalImage().getHeight(null);
2153 final int finalImageWidth = getFinalImage().getWidth(null);
2154
2155 final Rectangle painedArea = new Rectangle(imageOrigin.x,
2156 imageOrigin.y, finalImageWidth, finalImageHeight);
2157 SwingUtil.clearAround(finalG, painedArea, getVisibleRect());
2158
2159 addGadgets(finalG, false);
2160
2161 finalG.dispose();
2162
2163 return finalImage;
2164 }
2165
2166 /**
2167 * Paints some optional stuff into the given {@link Graphics2D}. Usually
2168 * called as the last layer when {@link #updateFinalImage()}
2169 *
2170 * @param forceWait
2171 * if <code>true</code>, a Wait-message will be painted even
2172 * though the rendering threads may not yet have started. If
2173 * <code>false</code>, it will only depend on
2174 * {@link #localExecuter.isRunning} and #bgExecuter.isRunning
2175 */
2176 private void addGadgets(final Graphics2D graphics, boolean forceWait) {
2177
2178 // Paint a logo to the bottom right if available
2179 if (mapImage != null) {
2180 Rectangle visibleRect = getVisibleRect();
2181 graphics.drawImage(mapImage, visibleRect.width
2182 - mapImage.getWidth() - 10, getVisibleRect().height
2183 - mapImage.getHeight() - 10, null);
2184 }
2185
2186 int y = 17;
2187
2188 // If the rendering process is still running, indicate this is the image
2189 if (forceWait || bgExecuter != null && bgExecuter.isRunning()
2190 || localExecuter != null && localExecuter.isRunning()) {
2191
2192 y += 8;
2193
2194 final Color c = graphics.getColor();
2195 graphics.setFont(waitFont);
2196
2197 graphics.setColor(getMapBackgroundColor());
2198 graphics.drawString(waitMsg, 5, y);
2199 graphics.setColor(getMapBackgroundColor());
2200 graphics.drawString(waitMsg, 7, y + 2);
2201 graphics.setColor(Color.BLACK);
2202 graphics.drawString(waitMsg, 6, y + 1);
2203
2204 graphics.setColor(c);
2205
2206 y += 24;
2207 }
2208
2209 if (renderingErrors != null) {
2210
2211 final Color c = graphics.getColor();
2212 graphics.setFont(errorFont);
2213
2214 for (Exception ex : renderingErrors) {
2215
2216 if (ex instanceof java.lang.IllegalArgumentException
2217 && ex.getMessage().equals(
2218 "Argument \"sourceCRS\" should not be null."))
2219 continue;
2220
2221 String errStr = ex.getLocalizedMessage();
2222
2223 graphics.setColor(Color.WHITE);
2224 graphics.drawString(errStr, 5, y);
2225 graphics.setColor(Color.RED);
2226 graphics.drawString(errStr, 6, y + 1);
2227
2228 y += 19;
2229 }
2230
2231 graphics.setColor(c);
2232 }
2233
2234 }
2235
2236 /**
2237 * Sets the {@link #mapArea} to best possibly present the given features. If
2238 * only one single point is given, the window is moved over the point.
2239 *
2240 * @param features
2241 * if <code>null</code> or size==0, the function doesn nothing.
2242 */
2243 public void zoomTo(
2244 final FeatureCollection<SimpleFeatureType, SimpleFeature> features) {
2245
2246 final CoordinateReferenceSystem mapCRS = getContext()
2247 .getCoordinateReferenceSystem();
2248 final CoordinateReferenceSystem fCRS = features.getSchema()
2249 .getGeometryDescriptor().getCoordinateReferenceSystem();
2250
2251 double width = mapArea.getWidth();
2252 double height = mapArea.getHeight();
2253 final double ratio = height / width;
2254
2255 if (features == null || features.size() == 0) {
2256 // feature count == 0 Zoom to the full extend
2257 return;
2258 } else if (features.size() == 1) {
2259
2260 // feature count == 1 Just move the window to the point and zoom 'a
2261 // bit'
2262 final SimpleFeature singleFeature = features.iterator().next();
2263
2264 if (((Geometry) singleFeature.getDefaultGeometry())
2265 .getCoordinates().length > 1) {
2266 // System.out.println("Zoomed to only pne poylgon");
2267 // Poly
2268 // TODO max width vs. height
2269 width = features.getBounds().getWidth() * 3;
2270 height = ratio * width;
2271 } else {
2272 // System.out.println("Zoomed in a bit becasue only one point");
2273 // width *= .9;
2274 // height *= .9;
2275 }
2276
2277 Coordinate centre = features.getBounds().centre();
2278 if (!mapCRS.equals(fCRS)) {
2279 // only to calculations if the CRS differ
2280 try {
2281 MathTransform fToMap;
2282 fToMap = CRS.findMathTransform(fCRS, mapCRS);
2283 // centre is transformed to the mapCRS
2284 centre = JTS.transform(centre, null, fToMap);
2285 } catch (final FactoryException e) {
2286 LOGGER.error("Looking for a Math transform", e);
2287 } catch (final TransformException e) {
2288 LOGGER.error("Looking for a Math transform", e);
2289 }
2290 }
2291
2292 final Coordinate newLeftBottom = new Coordinate(centre.x - width
2293 / 2., centre.y - height / 2.);
2294 final Coordinate newTopRight = new Coordinate(
2295 centre.x + width / 2., centre.y + height / 2.);
2296
2297 final Envelope newMapArea = new Envelope(newLeftBottom, newTopRight);
2298
2299 setMapArea(newMapArea);
2300
2301 } else {
2302 final ReferencedEnvelope fBounds = features.getBounds();
2303
2304 Envelope bounds;
2305 if (!mapCRS.equals(fCRS)) {
2306 bounds = JTSUtil.transformEnvelope(fBounds, fCRS, mapCRS);
2307 } else {
2308 bounds = fBounds;
2309 }
2310 // BB umrechnen von Layer-CRS in Map-CRS
2311
2312 // Expand a bit
2313 bounds.expandBy(bounds.getWidth() / 6., bounds.getHeight() / 6.);
2314
2315 setMapArea(bounds);
2316 }
2317 }
2318
2319 /**
2320 * Zooms towards a point.
2321 *
2322 * @param center
2323 * position in window coordinates
2324 * @param zoomFactor
2325 * > 1 for zoom in, < 1 for zoom out. Default is 1.33
2326 */
2327 public void zoomTo(final Point center) {
2328 zoomTo(center, null);
2329 }
2330
2331 /**
2332 * Zooms towards a point.
2333 *
2334 * @param center
2335 * position in window coordinates
2336 * @param zoomFaktor
2337 * > 1 for zoom in, < 1 for zoom out. Default is 1.33.
2338 */
2339 public void zoomTo(Point center, Double zoomFaktor) {
2340 if (zoomFaktor == null || zoomFaktor == 0.)
2341 zoomFaktor = 2.;
2342
2343 final Point2D gcenter = getScreenToWorld().transform(center, null);
2344 center = null;
2345
2346 if (Double.isNaN(gcenter.getX()) || Double.isNaN(gcenter.getY())
2347 || Double.isInfinite(gcenter.getX())
2348 || Double.isInfinite(gcenter.getY())
2349
2350 ) {
2351 // Not inside valid CRS area! cancel
2352 return;
2353 }
2354
2355 final Envelope mapArea = getMapArea();
2356
2357 final Envelope newMapArea = new Envelope(mapArea);
2358 newMapArea.expandBy((mapArea.getWidth() * zoomFaktor - mapArea
2359 .getWidth()) / 2., (mapArea.getHeight() * zoomFaktor - mapArea
2360 .getHeight()) / 2.);
2361
2362 // TODO we actually want that
2363 // // Move the newMapArea above the new center
2364 // newMapArea.translate(gcenter.getX() - mapArea.centre().x, gcenter
2365 // .getY()
2366 // - mapArea.centre().y);
2367
2368 setMapArea(newMapArea);
2369 }
2370
2371 }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26