Added image-to-map rendering.

* NEW: Implemented the RendererExecutor for splitted and scaled images.
* NEW: Implemented image saving.
* NEW: Worker callbacks now take as a parameter the return value of the Runnable.
* NEW: Worker runnables can now request data from the main thread using the Future API.
* BUG: null is now a valid value for the "name" field.
* BUG: Fix map loading for Posters and Single maps.
* OPT: The PosterImage class now only makes the primary calculations when instanciated.
  The splitting is now a separate method.
* OPT: When a map data entry is invalid, only the error message is shown in console 
  rather than the full backtrace.
This commit is contained in:
Prokopyl 2015-03-26 16:03:40 +01:00
parent 5c369c65cd
commit 5c9feddd6e
15 changed files with 495 additions and 42 deletions

View File

@ -18,11 +18,13 @@
package fr.moribus.imageonmap.commands.maptool;
import fr.moribus.imageonmap.PluginLogger;
import fr.moribus.imageonmap.commands.Command;
import fr.moribus.imageonmap.commands.CommandException;
import fr.moribus.imageonmap.commands.CommandInfo;
import fr.moribus.imageonmap.commands.Commands;
import fr.moribus.imageonmap.image.ImageRendererExecutor;
import fr.moribus.imageonmap.map.ImageMap;
import fr.moribus.imageonmap.worker.WorkerCallback;
import java.net.MalformedURLException;
import java.net.URL;
@ -39,6 +41,7 @@ public class NewCommand extends Command
protected void run() throws CommandException
{
final Player player = playerSender();
boolean scaling = false;
URL url;
if(args.length < 1) throwInvalidArgument("You must give an URL to take the image from.");
@ -50,26 +53,29 @@ public class NewCommand extends Command
catch(MalformedURLException ex)
{
throwInvalidArgument("Invalid URL.");
return;
}
if(args.length < 2)
if(args.length >= 2)
{
if(args[1].equals("resize")) scaling = true;
}
info("Working ...");
ImageRendererExecutor.Test(new WorkerCallback()
info("Rendering ...");
ImageRendererExecutor.Render(url, scaling, player.getUniqueId(), new WorkerCallback<ImageMap>()
{
@Override
public void finished(Object... args)
public void finished(ImageMap result)
{
player.sendMessage("Long task finished !");
player.sendMessage("§7Rendering finished !");
result.give(player.getInventory());
}
@Override
public void errored(Throwable exception)
{
player.sendMessage("Whoops, an error occured !");
player.sendMessage("§cMap rendering failed : " + exception.getMessage());
PluginLogger.LogWarning("Rendering from '" + player.getName() + "' failed", exception);
}
});
}

View File

@ -48,14 +48,28 @@ public class ImageIOExecutor extends Worker
static public void loadImage(final File file, final Renderer mapRenderer)
{
instance.submitQuery(new WorkerRunnable()
instance.submitQuery(new WorkerRunnable<Void>()
{
@Override
public void run() throws Exception
public Void run() throws Exception
{
BufferedImage image = ImageIO.read(file);
mapRenderer.setImage(image);
return null;
}
});
}
static public void saveImage(final File file, final BufferedImage image)
{
instance.submitQuery(new WorkerRunnable<Void>()
{
@Override
public Void run() throws Throwable
{
ImageIO.write(image, "png", file);
return null;
}
});
}
}

View File

@ -18,9 +18,21 @@
package fr.moribus.imageonmap.image;
import fr.moribus.imageonmap.ImageOnMap;
import fr.moribus.imageonmap.PluginLogger;
import fr.moribus.imageonmap.map.ImageMap;
import fr.moribus.imageonmap.map.MapManager;
import fr.moribus.imageonmap.worker.Worker;
import fr.moribus.imageonmap.worker.WorkerCallback;
import fr.moribus.imageonmap.worker.WorkerRunnable;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import javax.imageio.ImageIO;
public class ImageRendererExecutor extends Worker
{
@ -41,19 +53,132 @@ public class ImageRendererExecutor extends Worker
private ImageRendererExecutor()
{
super("Image IO");
super("Image IO", true);
}
static public void Test(WorkerCallback callback)
{
instance.submitQuery(new WorkerRunnable()
instance.submitQuery(new WorkerRunnable<Void>()
{
@Override
public void run() throws Throwable
public Void run() throws Throwable
{
Thread.sleep(5000);
return null;
}
}, callback);
}
static public void Render(final URL url, final boolean scaling, final UUID playerUUID, WorkerCallback<ImageMap> callback)
{
instance.submitQuery(new WorkerRunnable<ImageMap>()
{
@Override
public ImageMap run() throws Throwable
{
final BufferedImage image = ImageIO.read(url);
if(image == null) throw new IOException("The given URL is not a valid image");
if(scaling) return RenderSingle(image, playerUUID);
else return RenderPoster(image, playerUUID);
}
}, callback);
}
static private ImageMap RenderSingle(final BufferedImage image, final UUID playerUUID) throws Throwable
{
final short mapID = instance.submitToMainThread(new Callable<Short>()
{
@Override
public Short call() throws Exception
{
return MapManager.getNewMapsIds(1)[0];
}
}).get();
final BufferedImage finalImage = ResizeImage(image, ImageMap.WIDTH, ImageMap.HEIGHT);
ImageIOExecutor.saveImage(ImageOnMap.getPlugin().getImageFile(mapID), finalImage);
final ImageMap newMap = instance.submitToMainThread(new Callable<ImageMap>()
{
@Override
public ImageMap call() throws Exception
{
Renderer.installRenderer(finalImage, mapID);
return MapManager.createMap(playerUUID, mapID);
}
}).get();
return newMap;
}
static private ImageMap RenderPoster(final BufferedImage image, final UUID playerUUID) throws Throwable
{
final PosterImage poster = new PosterImage(image);
final int mapCount = poster.getImagesCount();
final Future<short[]> futureMapsIds = instance.submitToMainThread(new Callable<short[]>()
{
@Override
public short[] call() throws Exception
{
return MapManager.getNewMapsIds(mapCount);
}
});
poster.splitImages();
final short[] mapsIDs = futureMapsIds.get();
for(short mapID : mapsIDs)
{
ImageIOExecutor.saveImage(ImageOnMap.getPlugin().getImageFile(mapID), image);
}
final ImageMap newMap = instance.submitToMainThread(new Callable<ImageMap>()
{
@Override
public ImageMap call() throws Exception
{
Renderer.installRenderer(poster, mapsIDs);
return MapManager.createMap(poster, playerUUID, mapsIDs);
}
}).get();
return newMap;
}
static private BufferedImage ResizeImage(BufferedImage source, int destinationW, int destinationH)
{
float ratioW = (float)destinationW / (float)source.getWidth();
float ratioH = (float)destinationH / (float)source.getHeight();
int finalW, finalH;
if(ratioW < ratioH)
{
finalW = destinationW;
finalH = (int)(source.getHeight() * ratioW);
}
else
{
finalW = (int)(source.getWidth() * ratioH);
finalH = destinationH;
}
int x, y;
x = (destinationW - finalW) / 2;
y = (destinationH - finalH) / 2;
PluginLogger.LogInfo(finalW + " " + finalH + " : " + x + " " + y);
BufferedImage newImage = new BufferedImage(destinationW, destinationH, BufferedImage.TYPE_INT_ARGB);
Graphics graphics = newImage.getGraphics();
graphics.drawImage(source, x, y, finalW, finalH, null);
graphics.dispose();
return newImage;
}
}

View File

@ -29,6 +29,7 @@ public class PosterImage
static private final int WIDTH = 128;
static private final int HEIGHT = 128;
private BufferedImage originalImage;
private BufferedImage[] cutImages;
private int lines;
private int columns;
@ -36,15 +37,16 @@ public class PosterImage
private int remainderX, remainderY;
/**
* Creates and splits a new Poster from an entire image
* Creates a new Poster from an entire image
* @param originalImage the original image
*/
public PosterImage(BufferedImage originalImage)
{
splitImages(originalImage);
this.originalImage = originalImage;
calculateDimensions();
}
private void splitImages(BufferedImage originalImage)
private void calculateDimensions()
{
int originalWidth = originalImage.getWidth();
int originalHeight = originalImage.getHeight();
@ -59,6 +61,10 @@ public class PosterImage
if(remainderY > 0) lines++;
cutImagesCount = columns * lines;
}
public void splitImages()
{
cutImages = new BufferedImage[cutImagesCount];
int imageX;
@ -73,6 +79,8 @@ public class PosterImage
}
imageY += HEIGHT;
}
originalImage = null;
}
/**

View File

@ -19,6 +19,7 @@
package fr.moribus.imageonmap.image;
import java.awt.image.BufferedImage;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.map.MapCanvas;
import org.bukkit.map.MapRenderer;
@ -35,6 +36,19 @@ public class Renderer extends MapRenderer
return false;
}
static public void installRenderer(PosterImage image, short[] mapsIds)
{
for(int i = 0; i < mapsIds.length; i++)
{
installRenderer(image.getImageAt(i), mapsIds[i]);
}
}
static public void installRenderer(BufferedImage image, short mapID)
{
installRenderer(Bukkit.getMap(mapID)).setImage(image);
}
static public Renderer installRenderer(MapView map)
{
Renderer renderer = new Renderer();

View File

@ -21,8 +21,11 @@ package fr.moribus.imageonmap.map;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.bukkit.Material;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
public abstract class ImageMap implements ConfigurationSerializable
{
@ -48,6 +51,16 @@ public abstract class ImageMap implements ConfigurationSerializable
public abstract short[] getMapsIDs();
public abstract boolean managesMap(short mapID);
public void give(Inventory inventory)
{
short[] mapsIDs = getMapsIDs();
for(short mapID : mapsIDs)
{
ItemStack itemMap = new ItemStack(Material.MAP, 1, mapID);
inventory.addItem(itemMap);
}
}
/* ====== Serialization methods ====== */
static public ImageMap fromConfig(Map<String, Object> map, UUID userUUID) throws InvalidConfigurationException
@ -73,7 +86,7 @@ public abstract class ImageMap implements ConfigurationSerializable
protected ImageMap(Map<String, Object> map, UUID userUUID, Type mapType) throws InvalidConfigurationException
{
this(userUUID, mapType);
this.imageName = getFieldValue(map, "name");
this.imageName = getNullableFieldValue(map, "name");
}
protected abstract void postSerialize(Map<String, Object> map);
@ -84,10 +97,18 @@ public abstract class ImageMap implements ConfigurationSerializable
Map<String, Object> map = new HashMap<String, Object>();
map.put("type", mapType.toString());
map.put("name", imageName);
this.postSerialize(map);
return map;
}
static protected <T> T getFieldValue(Map<String, Object> map, String fieldName) throws InvalidConfigurationException
{
T value = getNullableFieldValue(map, fieldName);
if(value == null) throw new InvalidConfigurationException("Field value not found for \"" + fieldName + "\"");
return value;
}
static protected <T> T getNullableFieldValue(Map<String, Object> map, String fieldName) throws InvalidConfigurationException
{
try
{

View File

@ -19,6 +19,7 @@
package fr.moribus.imageonmap.map;
import fr.moribus.imageonmap.ImageOnMap;
import fr.moribus.imageonmap.image.PosterImage;
import java.util.ArrayList;
import java.util.UUID;
import org.bukkit.Bukkit;
@ -37,8 +38,8 @@ abstract public class MapManager
static public void exit()
{
playerMaps.clear();
save();
playerMaps.clear();
if(autosaveTask != null) autosaveTask.cancel();
}
@ -54,6 +55,43 @@ abstract public class MapManager
return false;
}
static public ImageMap createMap(UUID playerUUID, short mapID)
{
ImageMap newMap = new SingleMap(playerUUID, mapID);
addMap(newMap, playerUUID);
return newMap;
}
static public ImageMap createMap(PosterImage image, UUID playerUUID, short[] mapsIDs)
{
ImageMap newMap;
if(image.getImagesCount() == 1)
{
newMap = new SingleMap(playerUUID, mapsIDs[0]);
}
else
{
newMap = new PosterMap(playerUUID, mapsIDs, image.getColumns(), image.getLines());
}
addMap(newMap, playerUUID);
return newMap;
}
static public short[] getNewMapsIds(int amount)
{
short[] mapsIds = new short[amount];
for(int i = 0; i < amount; i++)
{
mapsIds[i] = Bukkit.createMap(Bukkit.getWorlds().get(0)).getId();
}
return mapsIds;
}
static public void addMap(ImageMap map, UUID playerUUID)
{
getPlayerMapStore(playerUUID).addMap(map);
}
static public void notifyModification(UUID playerUUID)
{
getPlayerMapStore(playerUUID).notifyModification();

View File

@ -54,6 +54,12 @@ public class PlayerMapStore implements ConfigurationSerializable
return false;
}
public void addMap(ImageMap map)
{
mapList.add(map);
notifyModification();
}
/* ===== Getters & Setters ===== */
public UUID getUUID()
@ -104,7 +110,7 @@ public class PlayerMapStore implements ConfigurationSerializable
}
catch(InvalidConfigurationException ex)
{
PluginLogger.LogWarning("Could not load map data", ex);
PluginLogger.LogWarning("Could not load map data : " + ex.getMessage());
}
}
}
@ -145,6 +151,7 @@ public class PlayerMapStore implements ConfigurationSerializable
{
PluginLogger.LogError("Could not save maps file for player " + playerUUID.toString(), ex);
}
PluginLogger.LogInfo("Saving maps file for " + playerUUID.toString());
modified = false;
}
}

View File

@ -61,7 +61,7 @@ public class PosterMap extends ImageMap
columnCount = getFieldValue(map, "columns");
rowCount = getFieldValue(map, "rows");
mapsIDs = getFieldValue(map, "mapIDs");
mapsIDs = getFieldValue(map, "mapsIDs");
}
@Override

View File

@ -49,7 +49,8 @@ public class SingleMap extends ImageMap
public SingleMap(Map<String, Object> map, UUID userUUID) throws InvalidConfigurationException
{
super(map, userUUID, Type.SINGLE);
mapID = getFieldValue(map, "mapID");
int _mapID = getFieldValue(map, "mapID");
mapID = (short) _mapID;//Meh
}
@Override

View File

@ -20,6 +20,8 @@ package fr.moribus.imageonmap.worker;
import fr.moribus.imageonmap.PluginLogger;
import java.util.ArrayDeque;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
public abstract class Worker
{
@ -27,12 +29,19 @@ public abstract class Worker
private final ArrayDeque<WorkerRunnable> runQueue = new ArrayDeque<>();
private final WorkerCallbackManager callbackManager;
private final WorkerMainThreadExecutor mainThreadExecutor;
private Thread thread;
protected Worker(String name)
{
this(name, false);
}
protected Worker(String name, boolean runMainThreadExecutor)
{
this.name = name;
this.callbackManager = new WorkerCallbackManager(name);
this.mainThreadExecutor = runMainThreadExecutor ? new WorkerMainThreadExecutor(name) : null;
}
public void init()
@ -43,6 +52,7 @@ public abstract class Worker
exit();
}
callbackManager.init();
if(mainThreadExecutor != null) mainThreadExecutor.init();
thread = createThread();
thread.start();
}
@ -51,6 +61,7 @@ public abstract class Worker
{
thread.interrupt();
callbackManager.exit();
if(mainThreadExecutor != null) mainThreadExecutor.exit();
thread = null;
}
@ -75,12 +86,11 @@ public abstract class Worker
try
{
currentRunnable.run();
callbackManager.callback(currentRunnable);
callbackManager.callback(currentRunnable, currentRunnable.run());
}
catch(Throwable ex)
{
callbackManager.callback(currentRunnable, ex);
callbackManager.callback(currentRunnable, null, ex);
}
}
}
@ -94,12 +104,17 @@ public abstract class Worker
}
}
protected void submitQuery(WorkerRunnable runnable, WorkerCallback callback, Object... args)
protected void submitQuery(WorkerRunnable runnable, WorkerCallback callback)
{
callbackManager.setupCallback(runnable, callback, args);
callbackManager.setupCallback(runnable, callback);
submitQuery(runnable);
}
protected <T> Future<T> submitToMainThread(Callable<T> callable)
{
if(mainThreadExecutor != null) return mainThreadExecutor.submit(callable);
return null;
}
private Thread createThread()
{

View File

@ -17,8 +17,8 @@
*/
package fr.moribus.imageonmap.worker;
public interface WorkerCallback
public interface WorkerCallback<T>
{
public void finished(Object... args);
public void finished(T result);
public void errored(Throwable exception);
}

View File

@ -26,7 +26,7 @@ import org.bukkit.scheduler.BukkitTask;
class WorkerCallbackManager implements Runnable
{
static private final int WATCH_LOOP_DELAY = 40;
static private final int WATCH_LOOP_DELAY = 5;
private final HashMap<WorkerRunnable, WorkerRunnableInfo> callbacks;
private final ArrayDeque<WorkerRunnableInfo> callbackQueue;
@ -47,28 +47,29 @@ class WorkerCallbackManager implements Runnable
selfTask = Bukkit.getScheduler().runTaskTimer(ImageOnMap.getPlugin(), this, 0, WATCH_LOOP_DELAY);
}
public void setupCallback(WorkerRunnable runnable, WorkerCallback callback, Object[] args)
public void setupCallback(WorkerRunnable runnable, WorkerCallback callback)
{
synchronized(callbacks)
{
callbacks.put(runnable, new WorkerRunnableInfo(callback, args));
callbacks.put(runnable, new WorkerRunnableInfo(callback));
}
}
public void callback(WorkerRunnable runnable)
public <T> void callback(WorkerRunnable<T> runnable, T result)
{
callback(runnable, null);
callback(runnable, result, null);
}
public void callback(WorkerRunnable runnable, Throwable exception)
public <T> void callback(WorkerRunnable<T> runnable, T result, Throwable exception)
{
WorkerRunnableInfo runnableInfo;
WorkerRunnableInfo<T> runnableInfo;
synchronized(callbacks)
{
runnableInfo = callbacks.get(runnable);
}
if(runnableInfo == null) return;
runnableInfo.setRunnableException(exception);
runnableInfo.setResult(result);
enqueueCallback(runnableInfo);
}
@ -99,16 +100,15 @@ class WorkerCallbackManager implements Runnable
currentRunnableInfo.runCallback();
}
private class WorkerRunnableInfo
private class WorkerRunnableInfo<T>
{
private final WorkerCallback callback;
private final Object[] args;
private final WorkerCallback<T> callback;
private T result;
private Throwable runnableException;
public WorkerRunnableInfo(WorkerCallback callback, Object[] args)
public WorkerRunnableInfo(WorkerCallback callback)
{
this.callback = callback;
this.args = args;
this.runnableException = null;
}
@ -125,9 +125,14 @@ class WorkerCallbackManager implements Runnable
}
else
{
callback.finished(args);
callback.finished(result);
}
}
public void setResult(T result)
{
this.result = result;
}
public Throwable getRunnableException()
{

View File

@ -0,0 +1,199 @@
/*
* Copyright (C) 2013 Moribus
* Copyright (C) 2015 ProkopyL <prokopylmc@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package fr.moribus.imageonmap.worker;
import fr.moribus.imageonmap.ImageOnMap;
import java.util.ArrayDeque;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitTask;
class WorkerMainThreadExecutor implements Runnable
{
static private final int WATCH_LOOP_DELAY = 1;
private final String name;
private final ArrayDeque<WorkerFuture> mainThreadQueue = new ArrayDeque<>();
private BukkitTask mainThreadTask;
public WorkerMainThreadExecutor(String name)
{
this.name = name;
}
public void init()
{
mainThreadTask = Bukkit.getScheduler().runTaskTimer(ImageOnMap.getPlugin(), this, 0, WATCH_LOOP_DELAY);
}
public void exit()
{
mainThreadTask.cancel();
mainThreadTask = null;
}
public <T> Future<T> submit(Callable<T> callable)
{
WorkerFuture<T> future = new WorkerFuture<T>(callable);
synchronized(mainThreadQueue)
{
mainThreadQueue.add(future);
}
return future;
}
@Override
public void run()
{
WorkerFuture currentFuture;
synchronized(mainThreadQueue)
{
if(mainThreadQueue.isEmpty()) return;
currentFuture = mainThreadQueue.pop();
}
currentFuture.runCallable();
}
private class WorkerFuture<T> implements Future<T>
{
private final Callable<T> callable;
private boolean isCancelled;
private boolean isDone;
private Exception executionException;
private T value;
public WorkerFuture(Callable<T> callable)
{
this.callable = callable;
}
public void runCallable()
{
try
{
value = callable.call();
}
catch(Exception ex)
{
executionException = ex;
}
finally
{
isDone = true;
synchronized(this){this.notifyAll();}
}
}
@Override
public boolean cancel(boolean mayInterruptIfRunning)
{
if(this.isCancelled || this.isDone) return false;
this.isCancelled = true;
this.isDone = true;
return true;
}
@Override
public boolean isCancelled()
{
return this.isCancelled;
}
@Override
public boolean isDone()
{
return this.isDone;
}
@Override
public T get() throws InterruptedException, ExecutionException
{
waitForCompletion();
if(executionException != null) throw new ExecutionException(executionException);
return value;
}
@Override
public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
{
waitForCompletion(timeout, unit);
if(executionException != null) throw new ExecutionException(executionException);
return value;
}
private void waitForCompletion(long timeout) throws InterruptedException, TimeoutException
{
synchronized(this)
{
long remainingTime;
long timeoutTime = System.currentTimeMillis() + timeout;
while(!isDone)
{
remainingTime = timeoutTime - System.currentTimeMillis();
if(remainingTime <= 0) throw new TimeoutException();
this.wait(remainingTime);
}
}
}
private void waitForCompletion() throws InterruptedException
{
synchronized(this)
{
while(!isDone) this.wait();
}
}
private void waitForCompletion(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
{
long millis = 0;
switch(unit)
{
case NANOSECONDS:
millis = timeout / 10^6;
break;
case MICROSECONDS:
millis = timeout / 10^3;
break;
case MILLISECONDS:
millis = timeout;
break;
case SECONDS:
millis = timeout * 10^3;
break;
case MINUTES:
millis = timeout * 10^3 * 60;
break;
case HOURS:
millis = timeout * 10^3 * 3600;
break;
case DAYS:
millis = timeout * 10^3 * 3600 * 24;
}
waitForCompletion(millis);
}
}
}

View File

@ -17,7 +17,7 @@
*/
package fr.moribus.imageonmap.worker;
public interface WorkerRunnable
public interface WorkerRunnable<T>
{
public void run() throws Throwable;
public T run() throws Throwable;
}