001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.plugins.streetside.cubemap;
003
004import java.awt.image.BufferedImage;
005import java.text.MessageFormat;
006import java.util.ArrayList;
007import java.util.EnumSet;
008import java.util.HashMap;
009import java.util.List;
010import java.util.Map;
011import java.util.concurrent.Callable;
012import java.util.concurrent.ExecutionException;
013import java.util.concurrent.ConcurrentHashMap;
014import java.util.concurrent.ExecutorService;
015import java.util.concurrent.Executors;
016import java.util.concurrent.Future;
017
018import org.apache.log4j.Logger;
019import org.openstreetmap.josm.gui.MainApplication;
020import java.util.concurrent.ExecutionException;
021import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
022import org.openstreetmap.josm.plugins.streetside.StreetsideCubemap;
023import org.openstreetmap.josm.plugins.streetside.StreetsideDataListener;
024import org.openstreetmap.josm.plugins.streetside.gui.StreetsideViewerDialog;
025import org.openstreetmap.josm.plugins.streetside.gui.imageinfo.StreetsideViewerPanel;
026import org.openstreetmap.josm.plugins.streetside.utils.GraphicsUtils;
027import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
028
029import javafx.scene.image.Image;
030import javafx.scene.image.ImageView;
031
032// JavaFX access in Java 8
033public class CubemapBuilder implements ITileDownloadingTaskListener, StreetsideDataListener {
034
035  final static Logger logger = Logger.getLogger(CubemapBuilder.class);
036
037        private static CubemapBuilder instance;
038        private StreetsideCubemap cubemap;
039        protected boolean isBuilding;
040
041  private long startTime;
042
043        private Map<String, BufferedImage> tileImages = new ConcurrentHashMap<>();
044  private ExecutorService pool;
045
046        private int currentTileCount = 0;
047
048  /**
049   * @return the tileImages
050   */
051  public Map<String, BufferedImage> getTileImages() {
052    return tileImages;
053  }
054
055  /**
056   * @param tileImages the tileImages to set
057   */
058  public void setTileImages(Map<String, BufferedImage> tileImages) {
059    this.tileImages = tileImages;
060  }
061
062  private CubemapBuilder() {
063                // private constructor to avoid instantiation
064        }
065
066        /**
067   * Fired when any image is added to the database.
068   */
069  @Override
070        public void imagesAdded() {
071                // Not implemented by the CubemapBuilder
072        }
073
074        /**
075   * Fired when the selected image is changed by something different from
076   * manually clicking on the icon.
077   *
078   * @param oldImage
079   *          Old selected {@link StreetsideAbstractImage}
080   * @param newImage
081   *          New selected {@link StreetsideAbstractImage}
082   *
083   * @see StreetsideDataListener
084   */
085  @Override
086  public void selectedImageChanged(StreetsideAbstractImage oldImage, StreetsideAbstractImage newImage) {
087
088
089    startTime = System.currentTimeMillis();
090
091                if (newImage != null) {
092
093                        cubemap = null;
094                        cubemap = new StreetsideCubemap(newImage.getId(), newImage.getLatLon(), newImage.getHe());
095                        currentTileCount = 0;
096                        resetTileImages();
097
098                        // download cubemap images in different threads and then subsequently
099                        // set the cubeface images in JavaFX
100                        downloadCubemapImages(cubemap.getId());
101
102                        long runTime = (System.currentTimeMillis()-startTime)/1000;
103                        if(StreetsideProperties.DEBUGING_ENABLED.get()) {
104                          logger.debug(MessageFormat.format("Completed downloading tiles for {0} in {1} seconds.", newImage.getId() , runTime));
105                        }
106                }
107        }
108
109        public void reload(String imageId) {
110                if (cubemap != null && imageId.equals(cubemap.getId())) {
111                        tileImages = new HashMap<>();
112                  downloadCubemapImages(imageId);
113                }
114        }
115
116        public void downloadCubemapImages(String imageId) {
117    if(StreetsideViewerPanel.getThreeSixtyDegreeViewerPanel().getScene() != StreetsideViewerPanel.getThreeSixtyDegreeViewerPanel().getLoadingScene()) {
118      StreetsideViewerPanel.getThreeSixtyDegreeViewerPanel().setScene(
119              StreetsideViewerPanel.getThreeSixtyDegreeViewerPanel().getLoadingScene()
120          );
121          }
122
123          final int maxThreadCount = StreetsideProperties.DOWNLOAD_CUBEFACE_TILES_TOGETHER.get()?6:6 * CubemapUtils.getMaxCols() * CubemapUtils.getMaxRows();
124
125                int fails = 0;
126
127    // TODO: message for progress bar
128    String[] message = new String[2];
129    message[0] = MessageFormat.format("Downloading Streetside imagery for {0}", imageId);
130    message[1] = "Wait for completion…….";
131
132    long startTime = System.currentTimeMillis();
133
134    if(CubemapBuilder.getInstance().getTileImages().keySet().size() > 0) {
135      pool.shutdownNow();
136      CubemapBuilder.getInstance().resetTileImages();
137    }
138
139    try {
140
141      pool = Executors.newFixedThreadPool(maxThreadCount);
142      List<Callable<List<String>>> tasks = new ArrayList<>(maxThreadCount);
143
144      if (StreetsideProperties.DOWNLOAD_CUBEFACE_TILES_TOGETHER.get()) {
145        EnumSet.allOf(CubemapUtils.CubemapFaces.class).forEach(face -> {
146          String tileId = String.valueOf(imageId + face.getValue());
147          tasks.add(new TileDownloadingTask(tileId));
148        });
149      } else {
150
151        // launch 4-tiled (low-res) downloading tasks . . .
152        if (!StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get()) {
153          // download all imagery for each cubeface at once
154
155          for (int i = 0; i < CubemapUtils.NUM_SIDES; i++) {
156            int tileNr = 0;
157            for (int j = 0; j < CubemapUtils.getMaxCols(); j++) {
158              for (int k = 0; k < CubemapUtils.getMaxRows(); k++) {
159
160                String tileId = String
161                  .valueOf(imageId + CubemapUtils.getFaceNumberForCount(i) + Integer.valueOf(tileNr++).toString());
162                tasks.add(new TileDownloadingTask(tileId));
163              }
164            }
165          }
166        // launch 16-tiled (high-res) downloading tasks
167        } else if (StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get()) {
168
169          for (int i = 0; i < CubemapUtils.NUM_SIDES; i++) {
170            for (int j = 0; j < CubemapUtils.getMaxCols(); j++) {
171              for (int k = 0; k < CubemapUtils.getMaxRows(); k++) {
172
173                String tileId = String
174                  .valueOf(imageId + CubemapUtils.getFaceNumberForCount(i) + String.valueOf(Integer.valueOf(j).toString() + Integer.valueOf(k).toString()));
175                tasks.add(new TileDownloadingTask(tileId));
176              }
177            }
178          }
179        }
180      } // finish preparing tasks for invocation
181
182      // execute tasks
183                        MainApplication.worker.submit(() -> {
184                          try {
185          List<Future<List<String>>> results = pool.invokeAll(tasks);
186          
187          if(StreetsideProperties.DEBUGING_ENABLED.get() && results != null) {
188            for (Future<List<String>> ff : results) {
189              try {
190                logger.debug(MessageFormat.format("Completed tile downloading task {0} in {1} seconds.",ff.get().toString(),
191                  (System.currentTimeMillis() - startTime)/1000));
192              } catch (ExecutionException e) {
193                logger.error(e);
194              }
195            }
196          }
197        } catch (InterruptedException e) {
198         logger.error(e);
199        }
200                        });
201    } catch (Exception ee) {
202      fails++;
203      logger.error("Error loading tile for image " + imageId);
204      ee.printStackTrace();
205    }
206
207    long stopTime = System.currentTimeMillis();
208    long runTime = stopTime - startTime;
209
210    if (StreetsideProperties.DEBUGING_ENABLED.get()) {
211      logger.debug(MessageFormat.format("Tile imagery downloading tasks completed in {0} seconds.", runTime / 1000));
212    }
213
214    if (fails > 0) {
215      logger.error(Integer.valueOf(fails) + " downloading tasks failed!");
216    }
217  }
218
219  /**
220   * Fired when a TileDownloadingTask has completed downloading an image tile. When all of the tiles for the Cubemap
221   * have been downloaded, the CubemapBuilder assembles the cubemap.
222   *
223   * @param tileId
224   *          the complete quadKey of the imagery tile, including cubeface and row/column in quaternary.
225   * @see TileDownloadingTask
226   */
227  @Override
228  public void tileAdded(String tileId) {
229    // determine whether four tiles have been set for each of the
230    // six cubemap faces. If so, build the images for the faces
231    // and set the views in the cubemap box.
232
233    if(!tileId.startsWith(cubemap.getId())) {
234      return;
235    }
236
237    currentTileCount++;
238
239    if (currentTileCount == (CubemapUtils.NUM_SIDES * CubemapUtils.getMaxCols() * CubemapUtils.getMaxRows())) {
240      if (StreetsideProperties.DEBUGING_ENABLED.get()) {
241        long endTime = System.currentTimeMillis();
242        long runTime = (endTime - startTime) / 1000;
243        logger.debug(
244          MessageFormat.format(
245            "{0} tile images ready for building cumbemap faces for cubemap {1} in {2} seconds.", currentTileCount,
246            CubemapBuilder.getInstance().getCubemap().getId(), Long.toString(runTime))
247          );
248      }
249
250      buildCubemapFaces();
251    }
252  }
253
254  /**
255   * Assembles the cubemap once all of the tiles have been downloaded.
256   * <p>
257   * The tiles for each cubemap face are cropped and stitched together
258   * then the ImageViews of the cubemap are set with the new imagery.
259   *
260   * @see         StreetsideCubemap
261   */
262   private void buildCubemapFaces() {
263                CubemapBox cmb = StreetsideViewerDialog.getInstance().getStreetsideViewerPanel().getCubemapBox();
264                ImageView[] views = cmb.getViews();
265
266                Image finalImages[] = new Image[CubemapUtils.NUM_SIDES];
267
268                // build 4-tiled cubemap faces and crop buffers
269                if (!StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get()) {
270                        for (int i = 0; i < CubemapUtils.NUM_SIDES; i++) {
271
272                                BufferedImage[] faceTileImages = new BufferedImage[CubemapUtils.getMaxCols() * CubemapUtils.getMaxRows()];
273
274                                for (int j = 0; j < (CubemapUtils.getMaxCols() * CubemapUtils.getMaxRows()); j++) {
275                                        String tileId = String.valueOf(getCubemap().getId() + CubemapUtils.getFaceNumberForCount(i)
276                                                        + Integer.valueOf(j).toString());
277                                        BufferedImage currentTile = tileImages.get(tileId);
278
279                                        faceTileImages[j] = currentTile;
280                                }
281
282                                BufferedImage finalImg = GraphicsUtils.buildMultiTiledCubemapFaceImage(faceTileImages);
283
284                                // rotate top cubeface 180 degrees - misalignment workaround
285                                if (i == 4) {
286                                  finalImg = GraphicsUtils.rotateImage(finalImg);
287                                }
288                                finalImages[i] = GraphicsUtils.convertBufferedImage2JavaFXImage(finalImg);
289                        }
290                        // build 16-tiled cubemap faces and crop buffers
291                } else if (StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get()) {
292                        for (int i = 0; i < CubemapUtils.NUM_SIDES; i++) {
293
294                                int tileCount = 0;
295
296                                BufferedImage[] faceTileImages = new BufferedImage[StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY
297                                                .get() ? 16 : 4];
298
299                                for (int j = 0; j < CubemapUtils.getMaxCols(); j++) {
300                                        for (int k = 0; k < CubemapUtils.getMaxRows(); k++) {
301                                                String tileId = String.valueOf(getCubemap().getId() + CubemapUtils.getFaceNumberForCount(i)
302                                                                + CubemapUtils.convertDoubleCountNrto16TileNr(
303                                                                                String.valueOf(Integer.valueOf(j).toString() + Integer.valueOf(k).toString())));
304                                                BufferedImage currentTile = tileImages.get(tileId);
305                                                faceTileImages[tileCount++] = currentTile;
306                                        }
307                                }
308                                BufferedImage finalImg = GraphicsUtils.buildMultiTiledCubemapFaceImage(faceTileImages);
309                                // rotate top cubeface 180 degrees - misalignment workaround
310                                if (i == 4) {
311                                        finalImg = GraphicsUtils.rotateImage(finalImg);
312                                }
313                                finalImages[i] = GraphicsUtils.convertBufferedImage2JavaFXImage(finalImg);
314                        }
315                }
316
317                for (int i = 0; i < CubemapUtils.NUM_SIDES; i++) {
318                        views[i].setImage(finalImages[i]);
319                }
320
321    StreetsideViewerDialog.getInstance().getStreetsideViewerPanel().revalidate();
322    StreetsideViewerDialog.getInstance().getStreetsideViewerPanel().repaint();
323
324    StreetsideViewerPanel.getThreeSixtyDegreeViewerPanel()
325                .setScene(StreetsideViewerPanel.getThreeSixtyDegreeViewerPanel().getCubemapScene());
326
327    StreetsideViewerPanel.getThreeSixtyDegreeViewerPanel().revalidate();
328    StreetsideViewerPanel.getThreeSixtyDegreeViewerPanel().repaint();
329
330    long endTime = System.currentTimeMillis();
331    long runTime = (endTime - startTime) / 1000;
332
333    String message = MessageFormat.format("Completed downloading, assembling and setting cubemap imagery for cubemap {0} in  {1} seconds.", cubemap.getId(),
334      runTime);
335
336    if (StreetsideProperties.DEBUGING_ENABLED.get()) {
337      logger.debug(message);
338    }
339
340    // reset count and image map after assembly
341    resetTileImages();
342    currentTileCount = 0;
343    isBuilding = false;
344        }
345
346        private void resetTileImages() {
347    tileImages = new HashMap<>();
348  }
349
350  /**
351         * @return the cubemap
352         */
353        public synchronized StreetsideCubemap getCubemap() {
354                return cubemap;
355        }
356
357        /**
358         * @param cubemap
359         *            the cubemap to set
360         */
361        public static void setCubemap(StreetsideCubemap cubemap) {
362                CubemapBuilder.getInstance().cubemap = cubemap;
363        }
364
365        public static CubemapBuilder getInstance() {
366                if (instance == null) {
367                        instance = new CubemapBuilder();
368                }
369                return instance;
370        }
371
372        /**
373         * @return true, iff the singleton instance is present
374         */
375        public static boolean hasInstance() {
376                return CubemapBuilder.instance != null;
377        }
378
379        /**
380   * @return the isBuilding
381   */
382  public boolean isBuilding() {
383    return isBuilding;
384  }
385
386
387        /**
388         * Destroys the unique instance of the class.
389         */
390        public static synchronized void destroyInstance() {
391                CubemapBuilder.instance = null;
392        }
393}