package org.openstreetmap.josm.plugins.imagery_cachexport;
import static org.openstreetmap.josm.tools.I18n.tr;
import static org.openstreetmap.josm.tools.I18n.trn;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.Set;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import org.apache.commons.jcs.access.CacheAccess;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
import org.openstreetmap.josm.gui.Notification;
import org.openstreetmap.josm.gui.PleaseWaitRunnable;
import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
import org.openstreetmap.josm.gui.layer.AbstractCachedTileSourceLayer;
import org.openstreetmap.josm.gui.layer.Layer;
import org.openstreetmap.josm.gui.layer.Layer.LayerAction;
import org.openstreetmap.josm.gui.util.GuiHelper;
import org.openstreetmap.josm.tools.ImageProvider;
/**
* Imagery tile export action. This is a menu entry in a imagery layer
* context menu that shows a dialog with tile export information and then
* exports the tiles.
*/
public abstract class AbstractImageryCacheExportAction
extends AbstractAction implements LayerAction {
/** Define menu entry (text and image). */
public AbstractImageryCacheExportAction() {
super(tr("Export tiles"), ImageProvider.get("imageryexport"));
putValue(SHORT_DESCRIPTION, tr("Export cached tiles to file system."));
}
/**
* Get the layer this menu entry belongs to.
*
* @return Currently selected layer.
*/
private AbstractCachedTileSourceLayer getSelectedLayer() {
return (AbstractCachedTileSourceLayer)LayerListDialog.getInstance().getModel()
.getSelectedLayers().get(0);
}
/**
* Get the cache object of the imagery layer.
*
* @return Cache object of the imagery layer.
*/
protected abstract CacheAccess<String, BufferedImageCacheEntry> getCache();
/**
* Get file name for a cache key.
*
* @param key Tile cache key. That is the full cache key with the key
* prefix removed.
*
* @return File name for tile.
*/
protected abstract String getFilename(String key);
/**
* Get the cache key prefix of the imagery layer.
*
* @param layer Imagery layer.
*
* @return Cache key prefix.
*/
protected String getCacheKeyPrefix(final AbstractCachedTileSourceLayer layer) {
return layer.getName().replace(':', '_');
}
/**
* This is called after the menu entry was selected.
*
* @param evt Menu item selection event.
*/
@Override
public void actionPerformed(ActionEvent evt) {
final AbstractCachedTileSourceLayer layer = getSelectedLayer();
final String cacheName = layer.getName();
final CacheAccess<String, BufferedImageCacheEntry> cache = getCache();
final String cacheKeyPrefix = getCacheKeyPrefix(layer);
ImageryTileExportDialog dialog =
new ImageryTileExportDialog(cache, cacheName, cacheKeyPrefix);
if (dialog.getValue() == 1) {
// OK button was pushed.
final String exportPath = dialog.getExportPath();
dialog.storePrefs();
exportImagery(exportPath, layer, cache);
}
}
/**
* Class that does the tile export in a task.
*/
private class ExportImageryTask extends PleaseWaitRunnable {
private String exportPath;
private final CacheAccess<String, BufferedImageCacheEntry> cache;
private String cacheName;
private String cacheKeyPrefix;
private final Set<String> keySet;
private int numberOfObjects;
private boolean cancel = false;
public ExportImageryTask(String exportPath,
final CacheAccess<String, BufferedImageCacheEntry> cache,
String cacheName,
String cacheKeyPrefix,
final Set<String> keySet,
int numberOfObjects) {
super(tr("Exporting cached tiles"));
this.exportPath = exportPath;
this.cache = cache;
this.cacheName = cacheName;
this.cacheKeyPrefix = cacheKeyPrefix;
this.keySet = keySet;
this.numberOfObjects = numberOfObjects;
}
@Override
protected void cancel() {
cancel = true;
}
@Override
protected void finish() {
// Do nothing.
}
@Override
protected void realRun() {
progressMonitor.setTicksCount(numberOfObjects);
int objectNum = 0;
for (String key: keySet) {
String[] keyParts = key.split(":", 2);
if (keyParts.length == 2) {
if (cacheKeyPrefix.equals(keyParts[0])) {
final String filename = getFilename(keyParts[1]);
if (filename != null) {
File file = new File(exportPath, filename);
BufferedImageCacheEntry entry = cache.get(key);
try {
BufferedImage image = entry.getImage();
if (image != null) {
writeImage(image, file);
objectNum++;
}
} catch (IOException exn) {
final String ioMessage = exn.getLocalizedMessage();
final String message = (ioMessage != null ?
// {0} is the file name, {1} is the error message.
tr("Failed to write image file {0}: {1}", file.getAbsolutePath(), ioMessage) :
// {0} is the file name.
tr("Failed to write image file {0}.", file.getAbsolutePath()));
GuiHelper.runInEDT(new Runnable() {
@Override
public void run() {
JOptionPane.showMessageDialog(Main.parent, message,
tr("Error"), JOptionPane.ERROR_MESSAGE);
}
});
break;
}
}
}
}
progressMonitor.worked(1);
if (cancel) {
break;
}
}
if (objectNum > 0) {
// {0} is a number, {1} is the layer name.
final String message = trn("Exported {0} tile from layer {1}.",
"Exported {0} tiles from layer {1}.",
objectNum, objectNum, cacheName);
GuiHelper.runInEDT(new Runnable() {
@Override
public void run() {
new Notification(message).show();
}
});
}
}
}
/**
* Export tiles.
*
* @param exportPath Export directory name.
* @param layer Imagery layer whose tiles are to be exported.
* @param cache Cache object.
*/
private void exportImagery(final String exportPath,
final AbstractCachedTileSourceLayer layer,
final CacheAccess<String, BufferedImageCacheEntry> cache) {
try {
Files.createDirectories(Paths.get(exportPath));
} catch (FileAlreadyExistsException exn) {
JOptionPane.showMessageDialog(Main.parent,
tr("Export file system path already exists but is not a directory."),
tr("Error"),
JOptionPane.ERROR_MESSAGE);
return;
} catch (IOException exn) {
final String message = exn.getLocalizedMessage();
if (message != null) {
JOptionPane.showMessageDialog(Main.parent,
tr("Failed to create export directory: {0}", message),
tr("Error"),
JOptionPane.ERROR_MESSAGE);
} else {
JOptionPane.showMessageDialog(Main.parent,
tr("Failed to create export directory."),
tr("Error"),
JOptionPane.ERROR_MESSAGE);
}
return;
}
final String cacheName = layer.getName();
final String cacheKeyPrefix = getCacheKeyPrefix(layer);
final Set<String> keySet = cache.getCacheControl().getKeySet();
int objects = 0;
for (String key: keySet) {
String[] keyParts = key.split(":", 2);
if (keyParts.length == 2) {
if (cacheKeyPrefix.equals(keyParts[0])) {
objects++;
}
}
}
if (objects < 1) {
return;
}
final ExportImageryTask task =
new ExportImageryTask(exportPath, cache, cacheName,
cacheKeyPrefix, keySet, objects);
if (task != null) {
Main.worker.submit(task);
}
}
/**
* Write an image into a file.
*
* @param image Image to be written.
* @param file File the image is to be written to.
*/
public void writeImage(BufferedImage image, File file) throws IOException {
// File must exist for ImageIO.write();
file.createNewFile();
try {
ImageIO.write(image, "jpg", file);
} catch (IOException exn) {
if (image.getType() == BufferedImage.TYPE_4BYTE_ABGR) {
// https://stackoverflow.com/questions/3432388/imageio-not-able-to-write-a-jpeg-file
int width = image.getWidth();
int height = image.getHeight();
BufferedImage imgBGR = new BufferedImage(width, height,
BufferedImage.TYPE_3BYTE_BGR);
int[] pixels = new int[width * height];
image.getRGB(0, 0, width, height, pixels, 0, width);
imgBGR.setRGB(0, 0, width, height, pixels, 0, width);
ImageIO.write(imgBGR, "jpg", file);
} else {
throw exn;
}
}
}
/**
* Create actual menu entry.
*
* @return The menu component.
*/
@Override
public Component createMenuComponent() {
JMenuItem toggleItem = new JMenuItem(this);
return toggleItem;
}
/**
* Check if the current layer is supported.
*
* @param layers List of layers that is to be checked.
*
* @return {@code true} if this action supports the given list of layers,
* {@code false} otherwise.
*/
@Override
public boolean supportLayers(List<Layer> layers) {
return layers.size() == 1 && layers.get(0) instanceof AbstractCachedTileSourceLayer;
}
}