View Javadoc
1   package fr.ifremer.dali.ui.swing.util.map.layer.tile;
2   
3   /*-
4    * #%L
5    * Dali :: UI
6    * %%
7    * Copyright (C) 2014 - 2017 Ifremer
8    * %%
9    * This program is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU Affero General Public License as published by
11   * the Free Software Foundation, either version 3 of the License, or
12   * (at your option) any later version.
13   * 
14   * This program is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17   * GNU General Public License for more details.
18   * 
19   * You should have received a copy of the GNU Affero General Public License
20   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21   * #L%
22   */
23  
24  import fr.ifremer.dali.ui.swing.util.map.DataMapPane;
25  import fr.ifremer.dali.ui.swing.util.map.layer.MapLayer;
26  import org.apache.commons.logging.Log;
27  import org.apache.commons.logging.LogFactory;
28  import org.geotools.geometry.jts.ReferencedEnvelope;
29  import org.geotools.map.DirectLayer;
30  import org.geotools.map.MapContent;
31  import org.geotools.map.MapViewport;
32  import org.geotools.renderer.lite.RendererUtilities;
33  import org.geotools.tile.Tile;
34  import org.geotools.tile.TileService;
35  import org.opengis.referencing.FactoryException;
36  import org.opengis.referencing.operation.TransformException;
37  
38  import java.awt.Graphics2D;
39  import java.awt.Rectangle;
40  import java.awt.RenderingHints;
41  import java.awt.geom.AffineTransform;
42  import java.awt.image.BufferedImage;
43  import java.util.Collection;
44  import java.util.concurrent.*;
45  
46  /**
47   * Map Layer for Tile service which can partially draw to the target graphic progressively
48   *
49   * @author peck7 on 02/10/2017.
50   */
51  public class MapTileLayer extends DirectLayer implements MapLayer {
52  
53      private static final Log LOG = LogFactory.getLog(MapTileLayer.class);
54      private static boolean trace = LOG.isTraceEnabled();
55  
56      private TileService service;
57      private DataMapPane mapPane;
58  
59      public MapTileLayer(TileService service, String title) {
60          super();
61          this.service = service;
62          setTitle(title);
63      }
64  
65      public void setMapPane(DataMapPane mapPane) {
66          this.mapPane = mapPane;
67      }
68  
69      @Override
70      public ReferencedEnvelope getBounds() {
71          return service.getBounds();
72      }
73  
74      @Override
75      public void draw(Graphics2D graphics, MapContent map, MapViewport theViewport) {
76  
77          final MapViewport viewport = new MapViewport(theViewport);
78  
79          final ReferencedEnvelope viewportExtent = viewport.getBounds();
80          int scale = calculateScale(viewportExtent, viewport.getScreenArea());
81  
82          Collection<Tile> tiles = service.findTilesInExtent(viewportExtent, scale, false, 256);
83  
84          if (LOG.isDebugEnabled()) {
85              LOG.debug(String.format("rendering %s tiles for envelope %s to screen %s (scale = %,d)",
86                      tiles.size(), viewportExtent, viewport.getScreenArea(), scale));
87          }
88  
89          try {
90              renderTiles(tiles, viewport.getScreenArea(), viewportExtent, viewport.getWorldToScreen(), graphics);
91          } catch (InterruptedException ignored) {
92          }
93  
94      }
95  
96      private void renderTiles(Collection<Tile> tiles, Rectangle rectangle,
97                               ReferencedEnvelope viewportExtent, AffineTransform worldToImageTransform, Graphics2D targetGraphics) throws InterruptedException {
98  
99          BufferedImage mosaicImage = new BufferedImage(rectangle.width, rectangle.height, BufferedImage.TYPE_INT_ARGB);
100         Graphics2D g2d = mosaicImage.createGraphics();
101 
102         g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
103 
104         ExecutorService pool = Executors.newFixedThreadPool(8);
105 
106         for (Tile tile : tiles) {
107             ReferencedEnvelope nativeTileEnvelope = tile.getExtent();
108 
109             ReferencedEnvelope tileEnvViewport;
110             try {
111                 tileEnvViewport = nativeTileEnvelope.transform(viewportExtent.getCoordinateReferenceSystem(), true);
112             } catch (TransformException | FactoryException e) {
113                 throw new RuntimeException(e);
114             }
115             double[] points = new double[4];
116             points[0] = tileEnvViewport.getMinX();
117             points[3] = tileEnvViewport.getMinY();
118             points[2] = tileEnvViewport.getMaxX();
119             points[1] = tileEnvViewport.getMaxY();
120 
121             worldToImageTransform.transform(points, 0, points, 0, 2);
122             Rectangle tileRect = new Rectangle((int) points[0], (int) points[1], (int) Math.ceil(points[2] - points[0]), (int) Math.ceil(points[3] - points[1]));
123 
124             // log the tile info
125             if (trace) {
126                 LOG.trace(String.format("ask for tile '%s' , bounds:%s", tile.getId(), tileRect));
127             }
128 
129             Runnable callback = () -> {
130                 if (mapPane != null && mapPane.getMapBuilder().getCurrentMapMode().isProgressiveRender()) {
131 
132                     // partial draw
133                     targetGraphics.drawImage(mosaicImage, 0, 0, null);
134                     // call mapPane to repaint on each iteration
135                     mapPane.repaint();
136 
137                 }
138             };
139 
140             pool.execute(new ImageLoader(tile, tileRect, g2d, callback));
141 
142         }
143 
144         pool.shutdown();
145         pool.awaitTermination(10, TimeUnit.SECONDS);
146 
147         // final draw
148         targetGraphics.drawImage(mosaicImage, 0, 0, null);
149 
150     }
151 
152     private class ImageLoader implements Runnable {
153 
154         private final FutureTask<BufferedImage> task;
155 
156         ImageLoader(Tile tile, Rectangle rectangle, Graphics2D g2d, Runnable callback) {
157 
158             Callable<BufferedImage> callable = () -> {
159                 if (trace) LOG.trace("load tile " + tile.getId());
160                 return tile.getBufferedImage();
161             };
162 
163             task = new FutureTask<BufferedImage>(callable) {
164                 @Override
165                 protected void done() {
166 
167                     if (isCancelled()) {
168                         if (trace) LOG.trace("cancel tile " + tile.getId());
169                         return;
170                     }
171                     if (trace) LOG.trace("draw tile " + tile.getId());
172                     try {
173 
174                         g2d.drawImage(get(), rectangle.x, rectangle.y, rectangle.width, rectangle.height, null);
175 
176                         // debug rectangle
177 //                        g2d.setColor(Color.RED);
178 //                        g2d.drawRect(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
179 
180                         callback.run();
181 
182                     } catch (InterruptedException | ExecutionException e) {
183                         LOG.error("can get the image", e);
184                     }
185 
186                 }
187             };
188         }
189 
190         @Override
191         public void run() {
192             task.run();
193         }
194     }
195 
196     private int calculateScale(ReferencedEnvelope extent, Rectangle screenArea) {
197 
198         int scale;
199         try {
200             scale = (int) Math.round(RendererUtilities.calculateScale(extent, screenArea.width, screenArea.height, 90));
201         } catch (FactoryException | TransformException ex) {
202             throw new RuntimeException("Failed to calculate scale", ex);
203         }
204         return scale;
205     }
206 }