View Javadoc
1   package fr.ifremer.dali.ui.swing.util.map;
2   
3   /*-
4    * #%L
5    * Dali :: UI
6    * $Id:$
7    * $HeadURL:$
8    * %%
9    * Copyright (C) 2014 - 2017 Ifremer
10   * %%
11   * This program is free software: you can redistribute it and/or modify
12   * it under the terms of the GNU Affero General Public License as published by
13   * the Free Software Foundation, either version 3 of the License, or
14   * (at your option) any later version.
15   *
16   * This program is distributed in the hope that it will be useful,
17   * but WITHOUT ANY WARRANTY; without even the implied warranty of
18   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19   * GNU General Public License for more details.
20   *
21   * You should have received a copy of the GNU Affero General Public License
22   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
23   * #L%
24   */
25  
26  import com.google.common.collect.Lists;
27  import fr.ifremer.dali.map.Maps;
28  import fr.ifremer.dali.ui.swing.util.map.layer.*;
29  import fr.ifremer.dali.ui.swing.util.map.layer.tile.MapTileLayer;
30  import org.apache.commons.collections4.CollectionUtils;
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  import org.geotools.geometry.jts.ReferencedEnvelope;
34  import org.geotools.map.Layer;
35  import org.geotools.map.MapContent;
36  import org.geotools.map.MapViewport;
37  import org.geotools.referencing.CRS;
38  import org.geotools.referencing.crs.DefaultGeographicCRS;
39  import org.geotools.renderer.GTRenderer;
40  import org.geotools.renderer.label.LabelCacheImpl;
41  import org.geotools.renderer.lite.LabelCache;
42  import org.geotools.renderer.lite.StreamingRenderer;
43  import org.geotools.swing.AbstractMapPane;
44  import org.geotools.swing.RenderingExecutor;
45  import org.geotools.swing.event.MapPaneAdapter;
46  import org.geotools.swing.event.MapPaneEvent;
47  import org.opengis.geometry.Envelope;
48  
49  import java.awt.*;
50  import java.awt.event.ComponentListener;
51  import java.awt.image.BufferedImage;
52  import java.awt.image.ColorModel;
53  import java.awt.image.RenderedImage;
54  import java.awt.image.WritableRaster;
55  import java.util.List;
56  import java.util.*;
57  
58  /**
59   * A featured MapPane with better buffered image than JMapPane
60   *
61   * @author peck7 on 21/06/2017.
62   */
63  public class DataMapPane extends AbstractMapPane {
64  
65      private static final Log LOG = LogFactory.getLog(DataMapPane.class);
66  
67      /**
68       * The default step ratio
69       */
70      public static final double ZOOM_STEP_RATIO = 0.3;
71  
72      private GTRenderer renderer;
73      private BufferedImage baseImage;
74      private Graphics2D baseImageGraphics;
75  
76      // interstitial image painted before next render
77      private BufferedImage interstitialImage;
78      private Rectangle interstitialRectangle = new Rectangle();
79      private boolean drawInterstitialImageOnce;
80      private boolean busy;
81      private boolean newContentPending;
82      private boolean graticuleVisible = true;
83      private boolean legendVisible = false;
84  
85      // the map builder
86      private MapBuilder mapBuilder;
87  
88      // the data layer collection build by the map builder
89      private final List<DataLayerCollection> dataLayerCollections;
90      private final Set<DataSelectionListener> selectionListeners = new HashSet<>();
91  
92      /**
93       * Creates a new map pane.
94       */
95      public DataMapPane() {
96          this(null);
97      }
98  
99      /**
100      * Creates a new map pane.
101      *
102      * @param content the map content containing the layers to display
103      *                (may be {@code null})
104      */
105     public DataMapPane(MapContent content) {
106         this(content, null, null);
107     }
108 
109     /**
110      * Creates a new map pane. Any or all arguments may be {@code null}
111      *
112      * @param content  the map content containing the layers to display
113      * @param executor the rendering executor to manage drawing
114      * @param renderer the renderer to use for drawing layers
115      */
116     public DataMapPane(MapContent content, RenderingExecutor executor, GTRenderer renderer) {
117         super(content, executor);
118         doSetRenderer(renderer);
119         setDoubleBuffered(true);
120 
121         dataLayerCollections = Lists.newArrayList();
122 
123         // default tool
124         setCursorTool(new MultipleCursorTool(this));
125 
126         // add mapPane listener
127         addMapPaneListener(new MapPaneAdapter() {
128             @Override
129             public void onDisplayAreaChanged(MapPaneEvent ev) {
130                 ReferencedEnvelope newDisplayArea = (ReferencedEnvelope) ev.getData();
131                 pendingDisplayArea = newDisplayArea;
132 
133                 if (!newContentPending) {
134                     setLayersVisible(newDisplayArea);
135                 }
136             }
137 
138             @Override
139             public void onRenderingStarted(MapPaneEvent ev) {
140                 busy = true;
141             }
142 
143             @Override
144             public void onRenderingStopped(MapPaneEvent ev) {
145                 busy = false;
146             }
147         });
148     }
149 
150     public MapBuilder getMapBuilder() {
151         return mapBuilder;
152     }
153 
154     public void setMapBuilder(MapBuilder mapBuilder) {
155         this.mapBuilder = mapBuilder;
156 
157         // Affect default background
158         setBackground(mapBuilder.getBackgroundColor());
159     }
160 
161     public List<DataLayerCollection> getDataLayerCollections() {
162         return dataLayerCollections;
163     }
164 
165     public void clearDataLayerCollections() {
166         dataLayerCollections.clear();
167     }
168 
169     public DataLayerCollection newDataLayerCollection(Object dataObject) throws Exception {
170 
171         DataLayerCollection dataLayerCollection = mapBuilder.buildDataLayerCollection(dataObject);
172         dataLayerCollections.add(dataLayerCollection);
173         return dataLayerCollection;
174 
175     }
176 
177     /**
178      * Get the overall envelope (=union of all envelope in data layer collection)
179      *
180      * @return overall envelope
181      */
182     public ReferencedEnvelope getOverallEnvelope() {
183         ReferencedEnvelope overallEnvelope = new ReferencedEnvelope(
184             !CRS.equalsIgnoreMetadata(DefaultGeographicCRS.WGS84, getMapBuilder().getTargetCRS())
185                 ? DefaultGeographicCRS.WGS84
186                 : null);
187         for (DataLayerCollection dataLayerCollection : getDataLayerCollections()) {
188             overallEnvelope.expandToInclude(dataLayerCollection.getEnvelope());
189         }
190         return overallEnvelope;
191     }
192 
193     /**
194      * The overall envelope with a buffer
195      */
196     public ReferencedEnvelope getOverallDisplayEnvelope() {
197         ReferencedEnvelope envelope = getOverallEnvelope();
198         if (!envelope.isEmpty()) {
199             // expand envelope by 10% of the biggest dimension or by a fixed size if envelope is a point
200             if (envelope.getHeight() == 0 && envelope.getWidth() == 0)
201                 envelope.expandBy(0.05);
202             else
203                 envelope.expandBy(Math.max(envelope.getHeight(), envelope.getWidth()) * 0.1);
204         }
205         return envelope;
206     }
207 
208     public void addDataSelectionListener(DataSelectionListener listener) {
209         if (listener == null) {
210             throw new IllegalArgumentException("listener must not be null");
211         }
212         selectionListeners.add(listener);
213     }
214 
215     public void removeDataSelectionListener(DataSelectionListener listener) {
216         if (listener != null) selectionListeners.remove(listener);
217     }
218 
219     void publishSelectionEvent(DataSelectionEvent event) {
220         for (DataSelectionListener listener : selectionListeners) {
221             switch (event.getType()) {
222                 case DATA_LAYER_SELECTED:
223                     listener.onDataLayerSelected(event);
224                     break;
225                 case EMPTY_SELECTION:
226                     listener.onEmptySelection(event);
227                     break;
228             }
229         }
230     }
231 
232     /*
233      MAP CONTENT HANDLING
234      */
235 
236     public void clearMapContent() {
237         if (getMapContent() != null) {
238             getMapContent().dispose();
239             setMapContent(null);
240         }
241     }
242 
243     /**
244      * Override default setMapContent and doSetMapContent (private) to set the pending display area at first render
245      *
246      * @param content new content
247      */
248     @Override
249     public void setMapContent(MapContent content) {
250         //super.setMapContent(content); // Don't call super method
251 
252         paramsLock.writeLock().lock();
253         try {
254             doSetMapContent(content);
255 
256         } finally {
257             paramsLock.writeLock().unlock();
258         }
259 
260         if (content != null && getRenderer() != null) {
261             // If the new map content had layers to draw, and this pane is visible,
262             // then the map content will already have been set with the renderer
263             //
264             if (getRenderer().getMapContent() != content) { // just check reference equality
265                 getRenderer().setMapContent(mapContent);
266             }
267         }
268     }
269 
270     /**
271      * Override the private method from org.geotools.swing.AbstractMapPane
272      *
273      * @param newMapContent new map content
274      */
275     private void doSetMapContent(MapContent newMapContent) {
276         if (mapContent != newMapContent) {
277 
278             newContentPending = true;
279 
280             if (mapContent != null) {
281                 mapContent.removeMapLayerListListener(this);
282                 for (Layer layer : mapContent.layers()) {
283                     if (layer instanceof ComponentListener) {
284                         removeComponentListener((ComponentListener) layer);
285                     }
286                 }
287             }
288 
289             mapContent = newMapContent;
290 
291             if (mapContent != null) {
292                 MapViewport viewport = mapContent.getViewport();
293                 viewport.setMatchingAspectRatio(true);
294                 Rectangle rect = getVisibleRect();
295                 if (!rect.isEmpty()) {
296                     viewport.setScreenArea(rect);
297                 }
298 
299                 mapContent.addMapLayerListListener(this);
300                 mapContent.addMapBoundsListener(this);
301 
302                 if (!mapContent.layers().isEmpty()) {
303                     // set all layers as selected by default for the info tool
304                     for (Layer layer : mapContent.layers()) {
305                         layer.setSelected(true);
306 
307                         if (layer instanceof ComponentListener) {
308                             addComponentListener((ComponentListener) layer);
309                         }
310 
311                         // affect this to osm layers
312                         if (layer instanceof MapTileLayer) {
313                             ((MapTileLayer) layer).setMapPane(this);
314                         }
315                     }
316 
317                     setFullExtent();
318 
319                     // If pending display area is set, so affect it directly at first render
320                     if (pendingDisplayArea == null) {
321                         doSetDisplayArea(mapContent.getViewport().getBounds());
322                     } else {
323 
324                         // transform if needed
325                         doSetDisplayArea(pendingDisplayArea);
326                     }
327                 }
328             }
329 
330             MapPaneEvent event = new MapPaneEvent(this, MapPaneEvent.Type.NEW_MAPCONTENT, mapContent);
331             publishEvent(event);
332 
333             // Clear label cache to prevent old labels still rendered after new content
334             clearLabelCache();
335 
336             drawLayers(true); // Force create a new image when content changes (Mantis #52670)
337             newContentPending = false;
338         }
339     }
340 
341     @Override
342     public void setDisplayArea(Envelope envelope) {
343 
344         InfoPanelLayer infoPanelLayer = getInfoPanelLayer();
345         if (infoPanelLayer != null)
346             infoPanelLayer.clear();
347 
348         imageOrigin.setLocation(0, 0);
349         if (!drawInterstitialImageOnce)
350             interstitialRectangle = new Rectangle();
351 
352         super.setDisplayArea(envelope);
353     }
354 
355     @Override
356     protected void doSetDisplayArea(Envelope envelope) {
357 
358         if (envelope instanceof ReferencedEnvelope) {
359             ReferencedEnvelope referencedEnvelope = (ReferencedEnvelope) envelope;
360 
361             // transform envelope to map CRS
362             referencedEnvelope = Maps.transformReferencedEnvelope(referencedEnvelope,
363                 getMapContent() != null ? getMapContent().getCoordinateReferenceSystem() : getMapBuilder().getTargetCRS());
364 
365             // limit envelope to restricted area
366             if (fullExtent != null) {
367 
368                 if (referencedEnvelope.getMinY() < fullExtent.getMinY() && referencedEnvelope.getMaxY() > fullExtent.getMaxY()
369                     && referencedEnvelope.getMinX() < fullExtent.getMinX() && referencedEnvelope.getMaxX() > fullExtent.getMaxX()) {
370 
371                     // Reset to fullExtend if both axis over limits
372                     referencedEnvelope = fullExtent;
373                     if (LOG.isDebugEnabled()) LOG.debug("envelope reset to full extent");
374 
375                 } else {
376 
377                     // limit envelope in y axis
378                     if (referencedEnvelope.getMinY() < fullExtent.getMinY() && referencedEnvelope.getMaxY() < fullExtent.getMaxY()) {
379                         referencedEnvelope.translate(0, Math.min(fullExtent.getMaxY() - referencedEnvelope.getMaxY(), fullExtent.getMinY() - referencedEnvelope.getMinY()));
380                         if (LOG.isDebugEnabled()) LOG.debug("Y ↑");
381                     }
382                     if (referencedEnvelope.getMinY() > fullExtent.getMinY() && referencedEnvelope.getMaxY() > fullExtent.getMaxY()) {
383                         referencedEnvelope.translate(0, Math.max(fullExtent.getMaxY() - referencedEnvelope.getMaxY(), fullExtent.getMinY() - referencedEnvelope.getMinY()));
384                         if (LOG.isDebugEnabled()) LOG.debug("Y ↓");
385                     }
386 
387                     // limit envelope in x axis
388                     if (referencedEnvelope.getMinX() < fullExtent.getMinX() && referencedEnvelope.getMaxX() < fullExtent.getMaxX()) {
389                         referencedEnvelope.translate(Math.min(fullExtent.getMaxX() - referencedEnvelope.getMaxX(), fullExtent.getMinX() - referencedEnvelope.getMinX()), 0);
390                         if (LOG.isDebugEnabled()) LOG.debug("← X");
391                     }
392                     if (referencedEnvelope.getMinX() > fullExtent.getMinX() && referencedEnvelope.getMaxX() > fullExtent.getMaxX()) {
393                         referencedEnvelope.translate(Math.max(fullExtent.getMaxX() - referencedEnvelope.getMaxX(), fullExtent.getMinX() - referencedEnvelope.getMinX()), 0);
394                         if (LOG.isDebugEnabled()) LOG.debug("X →");
395                     }
396                 }
397             }
398 
399             super.doSetDisplayArea(referencedEnvelope);
400 
401         } else {
402 
403             super.doSetDisplayArea(envelope);
404         }
405 
406     }
407 
408     private void clearLabelCache() {
409         if (labelCache != null) {
410             labelCache.stop();
411             labelCache.clear();
412         }
413     }
414 
415     /**
416      * Gets the renderer, creating a default one if required.
417      *
418      * @return the renderer
419      */
420     public GTRenderer getRenderer() {
421         if (renderer == null) {
422             doSetRenderer(new StreamingRenderer());
423         }
424         return renderer;
425     }
426 
427     /**
428      * Sets the renderer to be used by this map pane.
429      *
430      * @param renderer the renderer to use
431      */
432     public void setRenderer(GTRenderer renderer) {
433         doSetRenderer(renderer);
434     }
435 
436     private void doSetRenderer(GTRenderer newRenderer) {
437         if (newRenderer != null) {
438             Map<Object, Object> hints = newRenderer.getRendererHints();
439             if (hints == null) {
440                 hints = new HashMap<>();
441             }
442 
443             if (newRenderer instanceof StreamingRenderer) {
444                 if (hints.containsKey(StreamingRenderer.LABEL_CACHE_KEY)) {
445                     labelCache = (LabelCache) hints.get(StreamingRenderer.LABEL_CACHE_KEY);
446                 } else {
447                     labelCache = new LabelCacheImpl();
448                     hints.put(StreamingRenderer.LABEL_CACHE_KEY, labelCache);
449                 }
450 
451                 // set render modes
452                 hints.put(StreamingRenderer.TEXT_RENDERING_KEY, StreamingRenderer.TEXT_RENDERING_OUTLINE);
453             }
454 
455             Map<RenderingHints.Key, Object> j2dHintsMap = new HashMap<>();
456             j2dHintsMap.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
457             j2dHintsMap.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_GASP);
458             RenderingHints j2dHints = new RenderingHints(j2dHintsMap);
459 
460             newRenderer.setJava2DHints(j2dHints);
461             newRenderer.setRendererHints(hints);
462 
463             if (mapContent != null) {
464                 newRenderer.setMapContent(mapContent);
465             }
466         }
467 
468         renderer = newRenderer;
469     }
470 
471     /**
472      * Retrieve the map pane's current base image.
473      * <p>
474      * The map pane caches the most recent rendering of map layers
475      * as an image to avoid time-consuming rendering requests whenever
476      * possible. The base image will be re-drawn whenever there is a
477      * change to map layer data, style or visibility; and it will be
478      * replaced by a new image when the pane is resized.
479      * <p>
480      * This method returns a <b>live</b> reference to the current
481      * base image. Use with caution.
482      *
483      * @return a live reference to the current base image
484      */
485     public RenderedImage getBaseImage() {
486         return this.baseImage;
487     }
488 
489     public boolean isBusy() {
490         return busy;
491     }
492 
493     @Override
494     public void reset() {
495 
496         super.reset();
497 
498         // Reset the pending display area
499         pendingDisplayArea = null;
500     }
501 
502     @Override
503     protected void onShownOrResized() {
504 
505         // Don't schedule the 'setForNewSize' if nothing to compute.
506         if (mapContent == null) return;
507 
508         super.onShownOrResized();
509 
510     }
511 
512     @Override
513     public void moveImage(int dx, int dy) {
514         drawingLock.lock();
515         try {
516             if (isShowing() && !getVisibleRect().isEmpty()) {
517 
518                 if (interstitialRectangle.isEmpty()) {
519                     interstitialRectangle = getMapContent().getViewport().getScreenArea().getBounds();
520                 }
521 
522                 interstitialRectangle.translate(dx, dy);
523                 drawInterstitialImageOnce = false;
524                 repaint();
525             }
526 
527         } finally {
528             drawingLock.unlock();
529         }
530     }
531 
532     public void zoomIn() {
533         applyZoom(1 + ZOOM_STEP_RATIO, null);
534     }
535 
536     public void zoomOut() {
537         applyZoom(1 - ZOOM_STEP_RATIO, null);
538     }
539 
540     public void applyZoom(double zoomRatio, Point zoomCenter) {
541 //        if (busy) return; // Don't wait for render finished (Mantis #52670)
542 
543         ReferencedEnvelope displayArea = getDisplayArea();
544         Rectangle bounds = getMapContent().getViewport().getScreenArea();
545 
546         // calculate the deltas for envelope
547         double deltaWidth = displayArea.getWidth() * (zoomRatio - 1);
548         double deltaHeight = displayArea.getHeight() * (zoomRatio - 1);
549         // calculate the deltas for view (zoomRation is 10% larger here to compensate the envelope enlargement due to StreamingRender)
550         int deltaViewWidth = (int) ((bounds.width * (zoomRatio * 1.1 - 1)));
551         int deltaViewHeight = (int) ((bounds.height * (zoomRatio * 1.1 - 1)));
552 
553         // calculate shifting from center point if defined, or 0.5 on each axe to preserve center
554         double ratioLeft = zoomCenter != null ? zoomCenter.getX() * 1d / getWidth() : 0.5;
555         double ratioTop = zoomCenter != null ? 1 - (zoomCenter.getY() * 1d / getHeight()) : 0.5; // Y axis inverted between swing and crs
556 
557         // the delta for X
558         double deltaLeft = deltaWidth * ratioLeft;
559         double deltaRight = deltaLeft - deltaWidth;
560         int deltaViewLeft = (int) (deltaViewWidth * ratioLeft);
561 
562         // the delta for Y
563         double deltaTop = deltaHeight * ratioTop;
564         double deltaBottom = deltaTop - deltaHeight;
565         int deltaViewTop = (int) (deltaViewHeight * (1 - ratioTop));
566 
567         if (LOG.isDebugEnabled()) {
568             LOG.debug(String.format("Map mouse zoom (zoom ratio : %s, deltaLeft : %s, deltaRight : %s, deltaTop : %s, deltaBottom : %s)",
569                 zoomRatio, deltaLeft, deltaRight, deltaRight, deltaBottom));
570         }
571 
572         // compute new envelope
573         ReferencedEnvelope newDisplayArea = new ReferencedEnvelope(
574             displayArea.getMinX() + deltaLeft,
575             displayArea.getMaxX() + deltaRight,
576             displayArea.getMinY() + deltaTop,
577             displayArea.getMaxY() + deltaBottom,
578             displayArea.getCoordinateReferenceSystem()
579         );
580 
581         // compute new bounds
582         Rectangle newViewBounds = new Rectangle(
583             bounds.x - deltaViewLeft,
584             bounds.y - deltaViewTop,
585             bounds.width + deltaViewWidth,
586             bounds.height + deltaViewHeight
587         );
588         // Paint zoomed image
589         resizeImage(newViewBounds);
590 
591         // Set display area (submit a render)
592         setDisplayArea(newDisplayArea);
593 
594     }
595 
596     /**
597      * Draw the interstitial image in the provided rectangle
598      *
599      * @param rectangle target rectangle
600      */
601     public void resizeImage(Rectangle rectangle) {
602         if (isShowing() && !getVisibleRect().isEmpty()
603             // Don't paint buffered image when shape map is used (aka offline), the render is too fast to have a good behavior
604             && mapBuilder.getCurrentMapMode().isOnline()) {
605 
606             // Set the interstitial image bounds
607             interstitialRectangle.setBounds(rectangle);
608             drawInterstitialImageOnce = true;
609             repaint();
610         }
611     }
612 
613     @Override
614     protected void paintComponent(Graphics g) {
615         if (drawingLock.tryLock()) {
616             try {
617                 Graphics2D g2 = (Graphics2D) g;
618 
619                 // If interstitial image is available with a target rectangle
620                 if (interstitialImage != null && interstitialImage.getWidth() > 0 && !interstitialRectangle.isEmpty()) {
621 
622                     // Antialiasing ON
623                     g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
624                     g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
625 
626                     // Draw interstitial image if valid
627                     g2.drawImage(interstitialImage, interstitialRectangle.x, interstitialRectangle.y, interstitialRectangle.width, interstitialRectangle.height, null);
628 
629                     // Reset the zoom rectangle to avoid another interstitial repaint
630                     if (drawInterstitialImageOnce)
631                         interstitialRectangle = new Rectangle();
632 
633                 } else
634 
635                     // If base image has been rendered
636                     if (baseImage != null) {
637 
638                         // Draw rendered image
639                         g2.drawImage(baseImage, imageOrigin.x, imageOrigin.y, null);
640 
641                         // copy baseImage to interstitial image
642                         interstitialImage = deepCopy(baseImage);
643 
644                     }
645             } finally {
646                 drawingLock.unlock();
647             }
648         }
649     }
650 
651     public void redrawLayers() {
652         redrawLayers(false);
653     }
654 
655     public void redrawLayers(boolean createNewImage) {
656         if (mapContent != null) {
657             clearLabelCache.set(true);
658             if (createNewImage) clearLabelCache();
659             drawLayers(createNewImage);
660         }
661     }
662 
663     /**
664      * Default drawLayers method
665      *
666      * @param createNewImage true will force redraw on new image
667      */
668     @Override
669     protected void drawLayers(boolean createNewImage) {
670         drawingLock.lock();
671         try {
672             if (mapContent != null
673                 && !mapContent.getViewport().isEmpty()
674                 && acceptRepaintRequests.get()) {
675 
676                 Rectangle r = getVisibleRect();
677                 if (baseImage == null || createNewImage) {
678                     baseImage = GraphicsEnvironment.getLocalGraphicsEnvironment().
679                         getDefaultScreenDevice().getDefaultConfiguration().
680                         createCompatibleImage(r.width, r.height, Transparency.TRANSLUCENT);
681 
682                     if (baseImageGraphics != null) {
683                         baseImageGraphics.dispose();
684                     }
685 
686                     baseImageGraphics = baseImage.createGraphics();
687                     clearLabelCache.set(true);
688 
689                 }
690                 // Always reset background
691                 if (baseImageGraphics != null) {
692                     baseImageGraphics.setBackground(getBackground());
693 
694                     // reset full background due to transparency
695                     if (mapBuilder.getCurrentMapMode().isTransparent()
696                         // or if current enveloppe is over full extend
697                         || isCurrentDisplayAreaOverFullExtend()) {
698 
699                         baseImageGraphics.clearRect(0, 0, r.width, r.height);
700                     }
701                 }
702 
703                 if (mapContent != null && !mapContent.layers().isEmpty()) {
704 
705                     // Submit a render. the result will be drawn in baseImage
706                     getRenderingExecutor().submit(mapContent, getRenderer(), baseImageGraphics, this);
707                 }
708             }
709         } finally {
710             drawingLock.unlock();
711         }
712     }
713 
714     private boolean isCurrentDisplayAreaOverFullExtend() {
715         if (fullExtent != null) {
716             return !fullExtent.covers(getDisplayArea());
717         }
718         return false;
719     }
720 
721     private static BufferedImage deepCopy(BufferedImage bi) {
722         ColorModel cm = bi.getColorModel();
723         boolean isAlphaPremultiplied = cm.isAlphaPremultiplied();
724         WritableRaster raster = bi.copyData(null);
725         return new BufferedImage(cm, raster, isAlphaPremultiplied, null);
726     }
727 
728     /**
729      * Compute the visibility of each layer in the current map depending on their definition (see MapLayerDefinition)
730      *
731      * @param envelope the envelope
732      */
733     public void setLayersVisible(ReferencedEnvelope envelope) {
734         if (mapContent != null) {
735             setLayersVisible(mapContent.layers(), envelope);
736         }
737     }
738 
739     /**
740      * Compute the visibility of each layer depending on their definition (see MapLayerDefinition)
741      *
742      * @param layers   the layer collection
743      * @param envelope the envelope
744      */
745     public void setLayersVisible(List<Layer> layers, ReferencedEnvelope envelope) {
746 
747         if (CollectionUtils.isNotEmpty(layers)) {
748             List<String> displayableLayerNames = mapBuilder.getDisplayableLayerNames(envelope);
749             for (Layer layer : layers) {
750                 if (layer instanceof MapLayer) {
751                     // A map layer is visible if mapBuilder allows it
752                     layer.setVisible(displayableLayerNames.contains(layer.getTitle()));
753                 } else if (layer instanceof GraticuleLayer) {
754                     // A graticule layer is visible depending graticuleVisible property
755                     layer.setVisible(isGraticuleVisible());
756                 } else if (layer instanceof LegendLayer) {
757                     // A legend layer is visible depending legendVisible property
758                     layer.setVisible(isLegendVisible());
759                 } else {
760                     // A data layer is always visible
761                     layer.setVisible(true);
762                 }
763             }
764         }
765     }
766 
767     /**
768      * Toggle graticule visibility
769      */
770     public void toggleGraticuleVisibility() {
771         graticuleVisible = !graticuleVisible;
772 
773         if (getMapContent() != null) {
774             for (Layer layer : getMapContent().layers()) {
775                 if (layer instanceof GraticuleLayer) {
776                     layer.setVisible(graticuleVisible);
777                 }
778             }
779         }
780 
781     }
782 
783     /**
784      * Determine if graticule is visible
785      */
786     public boolean isGraticuleVisible() {
787         return graticuleVisible;
788     }
789 
790     /**
791      * Toggle legend visibility
792      */
793     public void toggleLegendVisibility() {
794         legendVisible = !legendVisible;
795 
796         if (getMapContent() != null) {
797             for (Layer layer : getMapContent().layers()) {
798                 if (layer instanceof LegendLayer) {
799                     layer.setVisible(legendVisible);
800                 }
801             }
802         }
803 
804     }
805 
806     /**
807      * Determine if legend is visible
808      */
809     public boolean isLegendVisible() {
810         return legendVisible;
811     }
812 
813     public InfoPanelLayer getInfoPanelLayer() {
814         if (mapContent != null) {
815             for (Layer layer : mapContent.layers()) {
816                 if (layer instanceof InfoPanelLayer) return (InfoPanelLayer) layer;
817             }
818         }
819         return null;
820     }
821 
822     public void fixFullExtend() {
823 
824         getRenderer().stopRendering();
825 
826         try {
827             Thread.sleep(1000);
828         } catch (InterruptedException ignored) {
829         }
830 
831         zoomIn();
832 
833     }
834 }