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}