/* Copyright (c) 2010-2016 Jesper Öqvist <jesper@llbit.se>
*
* This file is part of Chunky.
*
* Chunky 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.
*
* Chunky 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 Chunky. If not, see <http://www.gnu.org/licenses/>.
*/
package se.llbit.chunky.map;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritableImage;
import javafx.scene.image.WritablePixelFormat;
import javafx.scene.paint.Color;
import se.llbit.chunky.ui.MapViewMode;
import se.llbit.chunky.world.Block;
import se.llbit.chunky.world.ChunkPosition;
import se.llbit.chunky.world.ChunkView;
import se.llbit.png.PngFileWriter;
import se.llbit.util.RingBuffer;
import se.llbit.util.TaskTracker;
import java.io.File;
import java.io.IOException;
import java.nio.IntBuffer;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
/**
* Keeps a buffered image of rendered map tiles. We only re-render chunks when
* they are not buffered. The buffer contains all visible chunks, plus some
* outside of the view. Chunks outside the view are rendered so that the
* rendering and chunk loading delay when panning is minimized.
*
* @author Jesper Öqvist (jesper@llbit.se)
*/
public class MapBuffer {
private static final WritablePixelFormat<IntBuffer> PIXEL_FORMAT =
PixelFormat.getIntArgbInstance();
private int[] pixels;
private int width;
private int height;
private WritableImage image = null;
private boolean cached = false;
private boolean highlightEnabled = false;
private Block highlightBlock = Block.get(Block.DIAMONDORE_ID);
private Color highlightColor = Color.CRIMSON;
private ChunkView view = ChunkView.EMPTY;
private RingBuffer<MapTile> tileCache = new RingBuffer<>(140);
private Map<ChunkPosition, MapTile> activeTiles = new HashMap<>();
public MapBuffer() {
updateView(ChunkView.EMPTY, true);
}
/**
* Called when this render buffer should buffer another view.
*/
public synchronized void updateView(ChunkView newView, WorldMapLoader loader) {
boolean rebuild = newView.scale != view.scale || newView.renderer != view.renderer
|| (newView.renderer == MapViewMode.LAYER && newView.layer != view.layer);
if (newView.renderer == MapViewMode.LAYER) {
if (loader.highlightEnabled() != highlightEnabled
|| loader.highlightBlock() != highlightBlock
|| !loader.highlightColor().equals(highlightColor)) {
rebuild = true;
highlightEnabled = loader.highlightEnabled();
highlightBlock = loader.highlightBlock();
highlightColor = loader.highlightColor();
}
}
updateView(newView, rebuild);
}
private synchronized void updateView(ChunkView newView, boolean rebuild) {
int newWidth = newView.chunkScale * (newView.px1 - newView.px0 + 1);
int newHeight = newView.chunkScale * (newView.pz1 - newView.pz0 + 1);
if (newWidth != width || newHeight != height || pixels == null) {
width = newWidth;
height = newHeight;
pixels = new int[width * height];
}
updateActiveTiles(newView, rebuild);
view = newView;
}
private synchronized void updateActiveTiles(ChunkView newView, boolean rebuild) {
Collection<MapTile> discarded = new LinkedList<>();
for (MapTile tile : activeTiles.values()) {
if (!newView.shouldPreload(tile.pos)) {
discarded.add(tile);
} else if (rebuild) {
tile.rebuild(tile.pos, newView);
}
}
for (MapTile tile : discarded) {
tileCache.append(tile);
activeTiles.remove(tile.pos);
}
int x0, x1, z0, z1;
if (newView.chunkScale >= 16) {
x0 = newView.px0;
x1 = newView.px1;
z0 = newView.pz0;
z1 = newView.pz1;
} else {
x0 = newView.prx0;
x1 = newView.prx1;
z0 = newView.prz0;
z1 = newView.prz1;
}
for (int x = x0; x <= x1; ++x) {
for (int z = z0; z <= z1; ++z) {
ChunkPosition pos = ChunkPosition.get(x, z);
if (!activeTiles.containsKey(pos)) {
activeTiles.put(pos, newTile(pos, newView));
}
}
}
}
/**
* @return The buffered view
*/
public ChunkView getView() {
return view;
}
/**
* Redraws the given tile.
*/
public synchronized void drawTile(WorldMapLoader mapLoader, ChunkPosition chunk) {
MapTile tile = activeTiles.get(chunk);
if (tile != null) {
tile.draw(this, mapLoader, view);
cached = false;
}
}
/**
* Attempts to draw the tile using cached image.
*/
public synchronized void drawTileCached(WorldMapLoader mapLoader, ChunkPosition chunk) {
MapTile tile = activeTiles.get(chunk);
if (tile != null) {
tile.drawCached(this, mapLoader, view);
cached = false;
}
}
/**
* Redraw all tiles in the current view.
* This draws to the map buffer, it does not render to the map canvas.
*/
public synchronized void redrawView(WorldMapLoader mapLoader) {
int x0, x1, z0, z1;
if (view.chunkScale >= 16) {
x0 = view.px0;
x1 = view.px1;
z0 = view.pz0;
z1 = view.pz1;
} else {
x0 = view.prx0;
x1 = view.prx1;
z0 = view.prz0;
z1 = view.prz1;
}
for (int x = x0; x <= x1; ++x) {
for (int z = z0; z <= z1; ++z) {
drawTileCached(mapLoader, ChunkPosition.get(x, z));
}
}
}
/**
* Create a new map tile to use in the map buffer.
* This reuses existing map tiles when possible.
*/
private MapTile newTile(ChunkPosition pos, ChunkView view) {
if (tileCache.isEmpty()) {
return new MapTile(pos, view);
} else {
MapTile tile = tileCache.remove();
tile.rebuild(pos, view);
return tile;
}
}
/**
* Copies a contiguous block of pixels into the buffer.
*/
public void copyPixels(int[] data, int srcPos, int x, int z, int size) {
System.arraycopy(data, srcPos, pixels, z * width + x, size);
}
/**
* Draws the current buffered map to a map canvas (via a GraphicsContext).
* We use a manual scaling implementation to avoid the blurry JavaFX upscaling.
*
* <p>Drawing the image would be much simpler if we could rely on JavaFX
* for scaling the image, unfortunately if JavaFX is used to scale the image
* it will use a blurry upscaling algorithm which looks bad for the 2D map.
* It is not possible to disable the JavaFX scaling interpolation,
* so we do our own scaling here instead.
*/
public synchronized void drawBuffered(GraphicsContext gc) {
if (!cached) {
if (image == null || image.getWidth() != view.width || image.getHeight() != view.height) {
image = new WritableImage(view.width, view.height);
}
// Here we make sure to scale only the part of the image that will be drawn.
float scale = view.scale / (float) view.chunkScale;
double x0 = view.chunkScale * (view.x0 - view.px0);
double z0 = view.chunkScale * (view.z0 - view.pz0);
float diffY = 0;
int[] scaled = new int[view.width * view.height];
int index = 0;
int sourceX = (int) (0.5 + x0);
int sourceY = (int) (0.5 + z0);
int destY = 0;
for (int y = 0; y < (view.height / scale); ++y) {
while (diffY < scale && destY < view.height) {
float diffX = 0;
int destX = 0;
int pixelOffset = (sourceY + y) * width + sourceX;
for (int x = 0; x < (view.width / scale); ++x) {
int pixel = pixels[pixelOffset++];
while (diffX < scale && destX < view.width) {
scaled[index++] = pixel;
diffX += 1;
destX += 1;
}
diffX -= scale;
}
diffY += 1;
destY += 1;
}
diffY -= scale;
}
image.getPixelWriter()
.setPixels(0, 0, view.width, view.height, PIXEL_FORMAT, scaled, 0, view.width);
cached = true;
}
gc.clearRect(0, 0, view.width, view.height);
gc.drawImage(image, 0, 0);
}
public boolean highlightEnabled() {
return highlightEnabled;
}
public Block highlightBlock() {
return highlightBlock;
}
public Color highlightColor() {
return highlightColor;
}
/**
* Forces all tiles to be redrawn on the next draw operation.
*/
public synchronized void clearBuffer() {
updateActiveTiles(view, true);
}
public synchronized void renderPng(File targetFile) throws IOException {
int width = view.width;
int height = view.height;
int[] pixels = new int[width * height];
image.getPixelReader().getPixels(0, 0, width, height, PIXEL_FORMAT, pixels, 0, width);
try (PngFileWriter pngWriter = new PngFileWriter(targetFile)) {
pngWriter.write(pixels, width, height, TaskTracker.Task.NONE);
}
}
}