/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.pepsoft.worldpainter.importing;
import org.pepsoft.util.MathUtils;
import org.pepsoft.util.ProgressReceiver;
import org.pepsoft.worldpainter.Dimension;
import org.pepsoft.worldpainter.Terrain;
import org.pepsoft.worldpainter.Tile;
import org.pepsoft.worldpainter.biomeschemes.Minecraft1_7Biomes;
import org.pepsoft.worldpainter.history.HistoryEntry;
import org.pepsoft.worldpainter.layers.Annotations;
import org.pepsoft.worldpainter.layers.Biome;
import org.pepsoft.worldpainter.layers.Layer;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.awt.image.Raster;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.List;
import static org.pepsoft.worldpainter.Constants.TILE_SIZE;
import static org.pepsoft.worldpainter.Constants.TILE_SIZE_BITS;
import static org.pepsoft.worldpainter.importing.MaskImporter.InputType.SIXTEEN_BIT_GREY_SCALE;
/**
*
* @author pepijn
*/
public class MaskImporter {
public MaskImporter(Dimension dimension, File imageFile, List<Layer> allLayers) throws IOException {
this(dimension, imageFile, ImageIO.read(imageFile), allLayers);
}
public MaskImporter(Dimension dimension, File imageFile, BufferedImage image, List<Layer> allLayers) {
this.dimension = dimension;
this.imageFile = imageFile;
this.image = image;
this.allLayers = allLayers;
int sampleSize = image.getSampleModel().getSampleSize(0);
if (sampleSize == 1) {
inputType = InputType.ONE_BIT_GRAY_SCALE;
} else if (image.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_GRAY) {
if (sampleSize == 8) {
inputType = InputType.EIGHT_BIT_GREY_SCALE;
} else if (sampleSize == 16) {
inputType = InputType.SIXTEEN_BIT_GREY_SCALE;
} else {
inputType = InputType.UNSUPPORTED;
}
} else {
inputType = InputType.COLOUR;
}
switch (inputType) {
case ONE_BIT_GRAY_SCALE:
imageLowValue = 0;
imageHighValue = 1;
break;
case EIGHT_BIT_GREY_SCALE:
case SIXTEEN_BIT_GREY_SCALE:
final int width = image.getWidth(), height = image.getHeight();
final Raster raster = image.getRaster();
int imageLowValue = Integer.MAX_VALUE, imageHighValue = Integer.MIN_VALUE;
outer: for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
int value = raster.getSample(x, y, 0);
if (value < imageLowValue) {
imageLowValue = value;
}
if (value > imageHighValue) {
imageHighValue = value;
}
if ((imageLowValue == 0) && ((inputType == SIXTEEN_BIT_GREY_SCALE) ? (imageHighValue == 65535) : (imageHighValue == 255))) {
// Lowest and highest possible values found; no
// point in looking any further!
break outer;
}
}
}
this.imageLowValue = imageLowValue;
this.imageHighValue = imageHighValue;
break;
default:
this.imageLowValue = -1;
this.imageHighValue = -1;
break;
}
}
public void doImport(ProgressReceiver progressReceiver) throws ProgressReceiver.OperationCancelled {
if ((! applyToTerrain) && (applyToLayer == null)) {
throw new IllegalStateException("Target not set");
}
if (mapping == null) {
throw new IllegalStateException("Mapping not set");
}
if ((mapping == Mapping.THRESHOLD) && (threshold == -1)) {
throw new IllegalStateException("Threshold not set");
}
final int maxValue;
switch (inputType) {
case ONE_BIT_GRAY_SCALE:
maxValue = 1;
break;
case EIGHT_BIT_GREY_SCALE:
maxValue = 255;
break;
case SIXTEEN_BIT_GREY_SCALE:
maxValue = 65535;
break;
default:
maxValue = 0;
break;
}
final Random random = new Random(dimension.getSeed() + xOffset * 31 + yOffset);
// Scale the mask, if necessary
final BufferedImage scaledImage;
if (scale == 100) {
// No scaling necessary
scaledImage = image;
} else {
final int newWidth = image.getWidth() * scale / 100, newHeight = image.getHeight() * scale / 100;
if (image.getColorModel() instanceof IndexColorModel) {
scaledImage = new BufferedImage(newWidth, newHeight, image.getType(), (IndexColorModel) image.getColorModel());
} else {
scaledImage = new BufferedImage(newWidth, newHeight, image.getType());
}
Graphics2D g2 = scaledImage.createGraphics();
try {
if (mapping == Mapping.FULL_RANGE) {
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
}
g2.drawImage(image, 0, 0, newWidth, newHeight, null);
} finally {
g2.dispose();
}
}
image = null; // The original image is no longer necessary, so allow it to be garbage collected to make more space available for the import
// Create the appropriate mapping logic
abstract class Applicator {
void setTile(Tile tile) {
this.tile = tile;
}
abstract void apply(int x, int y, int value);
Tile tile;
}
final Applicator applicator;
final String aspect;
switch (inputType) {
case ONE_BIT_GRAY_SCALE:
if (removeExistingLayer) {
applicator = new Applicator() {
@Override
void apply(int x, int y, int value) {
tile.setBitLayerValue(applyToLayer, x, y, value != 0);
}
};
} else {
applicator = new Applicator() {
@Override
void apply(int x, int y, int value) {
if (value != 0) {
tile.setBitLayerValue(applyToLayer, x, y, true);
}
}
};
}
aspect = "layer " + applyToLayer.getName();
break;
case EIGHT_BIT_GREY_SCALE:
case SIXTEEN_BIT_GREY_SCALE:
switch (mapping) {
case ONE_TO_ONE:
if (applyToTerrain) {
applicator = new Applicator() {
@Override
void apply(int x, int y, int value) {
tile.setTerrain(x, y, Terrain.VALUES[value]);
}
};
aspect = "terrain";
} else {
final int defaultValue = applyToLayer.getDefaultValue();
if (removeExistingLayer) {
applicator = new Applicator() {
@Override
void apply(int x, int y, int value) {
if ((value != defaultValue)
|| (tile.getLayerValue(applyToLayer, x, y) != defaultValue)) {
tile.setLayerValue(applyToLayer, x, y, value);
}
}
};
} else {
applicator = new Applicator() {
@Override
void apply(int x, int y, int value) {
if ((value != defaultValue) && (value > tile.getLayerValue(applyToLayer, x, y))) {
tile.setLayerValue(applyToLayer, x, y, value);
}
}
};
}
aspect = "layer " + applyToLayer.getName();
}
break;
case DITHERING:
if (removeExistingLayer) {
applicator = new Applicator() {
@Override
void apply(int x, int y, int value) {
boolean layerValue = (value > 0) && (random.nextInt(limit) <= value);
if (layerValue || (tile.getBitLayerValue(applyToLayer, x, y))) {
tile.setBitLayerValue(applyToLayer, x, y, layerValue);
}
}
private final int limit = maxValue + 1;
};
} else {
applicator = new Applicator() {
@Override
void apply(int x, int y, int value) {
if ((value > 0) && (random.nextInt(limit) <= value)) {
tile.setBitLayerValue(applyToLayer, x, y, true);
}
}
private final int limit = maxValue + 1;
};
}
aspect = "layer " + applyToLayer.getName();
break;
case THRESHOLD:
if (removeExistingLayer) {
applicator = new Applicator() {
@Override
void apply(int x, int y, int value) {
boolean layerValue = value >= threshold;
if (layerValue || tile.getBitLayerValue(applyToLayer, x, y)) {
tile.setBitLayerValue(applyToLayer, x, y, layerValue);
}
}
};
} else {
applicator = new Applicator() {
@Override
void apply(int x, int y, int value) {
if (value >= threshold) {
tile.setBitLayerValue(applyToLayer, x, y, true);
}
}
};
}
aspect = "layer " + applyToLayer.getName();
break;
case FULL_RANGE:
final int layerLimit;
if (applyToLayer.getDataSize() == Layer.DataSize.NIBBLE) {
layerLimit = 16;
} else if (applyToLayer.getDataSize() == Layer.DataSize.BYTE) {
layerLimit = 256;
} else {
throw new IllegalArgumentException();
}
final int defaultValue = applyToLayer.getDefaultValue();
if (removeExistingLayer) {
applicator = new Applicator() {
@Override
void apply(int x, int y, int value) {
int layerValue = value * layerLimit / limit;
if ((layerValue != defaultValue) || (tile.getLayerValue(applyToLayer, x, y) != defaultValue)) {
tile.setLayerValue(applyToLayer, x, y, layerValue);
}
}
private final int limit = maxValue + 1;
};
} else {
applicator = new Applicator() {
@Override
void apply(int x, int y, int value) {
int layerValue = value * layerLimit / limit;
if ((layerValue != defaultValue) && (layerValue > tile.getLayerValue(applyToLayer, x, y))) {
tile.setLayerValue(applyToLayer, x, y, layerValue);
}
}
private final int limit = maxValue + 1;
};
}
aspect = "layer " + applyToLayer.getName();
break;
default:
throw new IllegalArgumentException("Don't know how to apply this combo");
}
break;
case COLOUR:
if (removeExistingLayer) {
applicator = new Applicator() {
@Override
void apply(int x, int y, int value) {
if (((value >> 24) & 0xff) > 0x7f) {
tile.setLayerValue(Annotations.INSTANCE, x, y, COLOUR_ANNOTATION_MAPPING[((value >> 12) & 0xf00) | ((value >> 8) & 0xf0) | ((value >> 4) & 0xf)]);
} else if (tile.getLayerValue(Annotations.INSTANCE, x, y) != 0) {
tile.setLayerValue(Annotations.INSTANCE, x, y, 0);
}
}
};
} else {
applicator = new Applicator() {
@Override
void apply(int x, int y, int value) {
if (((value >> 24) & 0xff) > 0x7f) {
tile.setLayerValue(Annotations.INSTANCE, x, y, COLOUR_ANNOTATION_MAPPING[((value >> 12) & 0xf00) | ((value >> 8) & 0xf0) | ((value >> 4) & 0xf)]);
}
}
};
}
aspect = "annotations";
break;
default:
throw new IllegalArgumentException("Don't know how to apply this combo");
}
if (dimension.getWorld() != null) {
dimension.getWorld().addHistoryEntry(HistoryEntry.WORLD_MASK_IMPORTED_TO_DIMENSION, dimension.getName(), imageFile, aspect);
}
// Apply the mask tile by tile
final int width = scaledImage.getWidth(), height = scaledImage.getHeight();
final int tileX1 = xOffset >> TILE_SIZE_BITS, tileX2 = (xOffset + width - 1) >> TILE_SIZE_BITS;
final int tileY1 = yOffset >> TILE_SIZE_BITS, tileY2 = (yOffset + height- 1) >> TILE_SIZE_BITS;
final int noOfTiles = (tileX2 - tileX1 + 1) * (tileY2 - tileY1 + 1);
int tileCount = 0;
for (int tileX = tileX1; tileX <= tileX2; tileX++) {
for (int tileY = tileY1; tileY <= tileY2; tileY++) {
final Tile tile = dimension.getTile(tileX, tileY);
if (tile == null) {
tileCount++;
if (progressReceiver != null) {
progressReceiver.setProgress((float) tileCount / noOfTiles);
}
continue;
}
tile.inhibitEvents();
try {
final int tileOffsetX = (tileX << TILE_SIZE_BITS) - xOffset, tileOffsetY = (tileY << TILE_SIZE_BITS) - yOffset;
final Raster raster = scaledImage.getRaster();
applicator.setTile(tile);
if (inputType == InputType.COLOUR) {
for (int xInTile = 0; xInTile < TILE_SIZE; xInTile++) {
for (int yInTile = 0; yInTile < TILE_SIZE; yInTile++) {
final int imageX = tileOffsetX + xInTile, imageY = tileOffsetY + yInTile;
if ((imageX >= 0) && (imageX < width) && (imageY >= 0) && (imageY < height)) {
applicator.apply(xInTile, yInTile, scaledImage.getRGB(imageX, imageY));
}
}
}
} else {
for (int xInTile = 0; xInTile < TILE_SIZE; xInTile++) {
for (int yInTile = 0; yInTile < TILE_SIZE; yInTile++) {
final int imageX = tileOffsetX + xInTile, imageY = tileOffsetY + yInTile;
if ((imageX >= 0) && (imageX < width) && (imageY >= 0) && (imageY < height)) {
applicator.apply(xInTile, yInTile, raster.getSample(imageX, imageY, 0));
}
}
}
}
} finally {
tile.releaseEvents();
}
tileCount++;
if (progressReceiver != null) {
progressReceiver.setProgress((float) tileCount / noOfTiles);
}
}
}
}
public InputType getInputType() {
return inputType;
}
public boolean isSupported() {
return inputType != InputType.UNSUPPORTED;
}
/**
* @return Whether this image type can be mapped to the terrain.
*/
public boolean isTerrainPossible() {
// Only gray scale images with at least 8 bits can be mapped to the
// terrain. Possible in the future we will support mapping colours to
// the terrain, but not yet.
return (inputType == InputType.EIGHT_BIT_GREY_SCALE || inputType == InputType.SIXTEEN_BIT_GREY_SCALE)
&& (imageHighValue < Terrain.VALUES.length);
}
// TODO: dithered en threshold ook ondersteunen voor terrain en continue layers - mappen op één waarde
// of beter nog: generieke mapping mode bouwen
/**
* @return The list of layers to which this image type can be mapped. May be
* empty.
*/
public List<Layer> getPossibleLayers() {
List<Layer> possibleLayers = new ArrayList<>(allLayers.size());
for (Layer layer: allLayers) {
if (layer.equals(Annotations.INSTANCE)) {
// Annotations are a special case; since they are coloured we
// support importing a colour image as annotations
if (((inputType == InputType.EIGHT_BIT_GREY_SCALE || inputType == InputType.SIXTEEN_BIT_GREY_SCALE) && (imageHighValue < 16)) || inputType == InputType.COLOUR) {
possibleLayers.add(layer);
}
} else if (layer.equals(Biome.INSTANCE)) {
// Biomes are a discrete layer which can only be mapped one on one
if ((inputType == InputType.EIGHT_BIT_GREY_SCALE || inputType == InputType.SIXTEEN_BIT_GREY_SCALE) && (imageHighValue <= Minecraft1_7Biomes.HIGHEST_BIOME_ID )) {
possibleLayers.add(layer);
}
} else if (layer.getDataSize() == Layer.DataSize.BIT || layer.getDataSize() == Layer.DataSize.BIT_PER_CHUNK) {
// 8 or 16 bit masks can be applied by either dithering or applying a threshold
if (inputType == InputType.ONE_BIT_GRAY_SCALE || inputType == InputType.EIGHT_BIT_GREY_SCALE || inputType == InputType.SIXTEEN_BIT_GREY_SCALE) {
possibleLayers.add(layer);
}
} else {
// Continuous layers need a gray scale mask
if (inputType == InputType.EIGHT_BIT_GREY_SCALE || inputType == InputType.SIXTEEN_BIT_GREY_SCALE) {
possibleLayers.add(layer);
}
}
}
return possibleLayers;
}
public Layer getApplyToLayer() {
return applyToLayer;
}
public void setApplyToLayer(Layer applyToLayer) {
if ((applyToLayer != null) && (! getPossibleLayers().contains(applyToLayer))) {
throw new IllegalArgumentException("This image type cannot be applied to the specified layer");
}
this.applyToLayer = applyToLayer;
if (applyToLayer != null) {
applyToTerrain = false;
}
}
public boolean isApplyToTerrain() {
return applyToTerrain;
}
public void setApplyToTerrain(boolean applyToTerrain) {
if (applyToTerrain && (! isTerrainPossible())) {
throw new IllegalArgumentException("This image type cannot be applied to the terrain");
}
this.applyToTerrain = applyToTerrain;
if (applyToTerrain) {
applyToLayer = null;
}
}
/**
* @return The types of mapping possible for the specified target (terrain
* or layer).
*/
public Set<Mapping> getPossibleMappings() {
if (applyToTerrain) {
return EnumSet.of(Mapping.ONE_TO_ONE);
} else if (applyToLayer.equals(Annotations.INSTANCE)) {
return EnumSet.of(Mapping.ONE_TO_ONE);
} else if (applyToLayer.equals(Biome.INSTANCE)) {
return EnumSet.of(Mapping.ONE_TO_ONE);
} else if (applyToLayer.getDataSize() == Layer.DataSize.BIT || applyToLayer.getDataSize() == Layer.DataSize.BIT_PER_CHUNK) {
switch (inputType) {
case ONE_BIT_GRAY_SCALE:
return EnumSet.of(Mapping.ONE_TO_ONE);
case EIGHT_BIT_GREY_SCALE:
case SIXTEEN_BIT_GREY_SCALE:
return EnumSet.of(Mapping.DITHERING, Mapping.THRESHOLD);
}
} else {
return EnumSet.of(Mapping.FULL_RANGE);
}
throw new IllegalStateException();
}
public Mapping getMapping() {
return mapping;
}
public void setMapping(Mapping mapping) {
if (! getPossibleMappings().contains(mapping)) {
throw new IllegalArgumentException();
}
this.mapping = mapping;
}
public int getScale() {
return scale;
}
public void setScale(int scale) {
this.scale = scale;
}
public int getxOffset() {
return xOffset;
}
public void setxOffset(int xOffset) {
this.xOffset = xOffset;
}
public int getyOffset() {
return yOffset;
}
public void setyOffset(int yOffset) {
this.yOffset = yOffset;
}
public int getThreshold() {
return threshold;
}
public void setThreshold(int threshold) {
if (threshold < 0) {
throw new IllegalArgumentException();
}
this.threshold = threshold;
}
public boolean isRemoveExistingLayer() {
return removeExistingLayer;
}
public void setRemoveExistingLayer(boolean removeExistingLayer) {
this.removeExistingLayer = removeExistingLayer;
}
public int getImageHighValue() {
return imageHighValue;
}
public int getImageLowValue() {
return imageLowValue;
}
private static int findNearestAnnotationValue(int colour) {
final int red = (colour & 0xff0000) >> 16;
final int green = (colour & 0xff00) >> 8;
final int blue = colour & 0xff;
float minDistance = Float.MAX_VALUE;
int minDistanceIndex = -1;
for (int i = 0; i < ANNOTATIONS_PALETTE.length; i++) {
final float distance = MathUtils.getDistance(red - ANNOTATIONS_PALETTE[i][0], green - ANNOTATIONS_PALETTE[i][1], blue - ANNOTATIONS_PALETTE[i][2]);
if (distance < minDistance) {
minDistance = distance;
minDistanceIndex = i;
}
}
return minDistanceIndex + 1;
}
private final Dimension dimension;
private final List<Layer> allLayers;
private final InputType inputType;
private final int imageLowValue, imageHighValue;
private final File imageFile;
private BufferedImage image;
private boolean applyToTerrain, removeExistingLayer;
private Layer applyToLayer;
private Mapping mapping;
private int scale, xOffset, yOffset, threshold = -1;
public enum InputType {UNSUPPORTED, ONE_BIT_GRAY_SCALE, EIGHT_BIT_GREY_SCALE, SIXTEEN_BIT_GREY_SCALE, COLOUR}
public enum Mapping {ONE_TO_ONE, DITHERING, THRESHOLD, FULL_RANGE}
private static int[][] ANNOTATIONS_PALETTE = {
{0xdd, 0xdd, 0xdd},
{0xdb, 0x7d, 0x3e},
{0xb3, 0x50, 0xbc},
{0x6a, 0x8a, 0xc9},
{0xb1, 0xa6, 0x27},
{0x41, 0xae, 0x38},
{0xd0, 0x84, 0x99},
{0x9a, 0xa1, 0xa1},
{0x2e, 0x6e, 0x89},
{0x7e, 0x3d, 0xb5},
{0x2e, 0x38, 0x8d},
{0x4f, 0x32, 0x1f},
{0x35, 0x46, 0x1b},
{0x96, 0x34, 0x30},
{0x19, 0x16, 0x16}
};
/**
* A table which maps colours to annotation layer values. The colours have
* their last four bits stripped to keep the table small.
*/
private static final int[] COLOUR_ANNOTATION_MAPPING = new int[4096];
static {
for (int i = 0; i < 4096; i++) {
COLOUR_ANNOTATION_MAPPING[i] = findNearestAnnotationValue(((i & 0xf00) << 12) | ((i & 0xf0) << 8) | ((i & 0xf) << 4));
}
}
}