
org.pepsoft.worldpainter.threedeeview.ThreeDeeView Maven / Gradle / Ivy
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package org.pepsoft.worldpainter.threedeeview;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.swing.JComponent;
import javax.swing.JViewport;
import javax.swing.Scrollable;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import org.pepsoft.util.ProgressReceiver;
import org.pepsoft.worldpainter.BiomeScheme;
import org.pepsoft.worldpainter.ColourScheme;
import static org.pepsoft.worldpainter.Constants.*;
import org.pepsoft.worldpainter.Dimension;
import org.pepsoft.worldpainter.HeightMapTileFactory;
import org.pepsoft.worldpainter.Tile;
import org.pepsoft.worldpainter.biomeschemes.CustomBiomeManager;
import org.pepsoft.worldpainter.layers.Layer;
/**
*
* @author pepijn
*/
public class ThreeDeeView extends JComponent implements Dimension.Listener, Tile.Listener, HierarchyListener, ActionListener, Scrollable {
public ThreeDeeView(Dimension dimension, ColourScheme colourScheme, BiomeScheme biomeScheme, CustomBiomeManager customBiomeManager, int rotation, int zoom) {
this.dimension = dimension;
this.colourScheme = colourScheme;
this.biomeScheme = biomeScheme;
this.customBiomeManager = customBiomeManager;
this.rotation = rotation;
this.zoom = zoom;
scale = (int) Math.pow(2.0, Math.abs(zoom - 1));
// System.out.println("Zoom " + zoom + " -> scale " + scale);
maxHeight = dimension.getMaxHeight();
if (dimension.getTileFactory() instanceof HeightMapTileFactory) {
waterLevel = ((HeightMapTileFactory) dimension.getTileFactory()).getWaterHeight();
} else {
waterLevel = maxHeight / 2;
}
upsideDown = dimension.getDim() < 0; // Ceiling dimension
switch (rotation) {
case 0:
zSortedTiles = new TreeSet<>((t1, t2) -> {
if (t1.getY() != t2.getY()) {
return t1.getY() - t2.getY();
} else {
return t1.getX() - t2.getX();
}
});
break;
case 1:
zSortedTiles = new TreeSet<>((t1, t2) -> {
if (t1.getX() != t2.getX()) {
return t1.getX() - t2.getX();
} else {
return t2.getY() - t1.getY();
}
});
break;
case 2:
zSortedTiles = new TreeSet<>((t1, t2) -> {
if (t1.getY() != t2.getY()) {
return t2.getY() - t1.getY();
} else {
return t2.getX() - t1.getX();
}
});
break;
case 3:
zSortedTiles = new TreeSet<>((t1, t2) -> {
if (t1.getX() != t2.getX()) {
return t2.getX() - t1.getX();
} else {
return t1.getY() - t2.getY();
}
});
break;
default:
throw new IllegalArgumentException();
}
zSortedTiles.addAll(dimension.getTiles());
threeDeeRenderManager = new ThreeDeeRenderManager(dimension, colourScheme, biomeScheme, customBiomeManager, rotation);
dimension.addDimensionListener(this);
for (Tile tile: dimension.getTiles()) {
tile.addListener(this);
}
int width = dimension.getWidth() * TILE_SIZE + dimension.getHeight() * TILE_SIZE;
int height = width / 2 + maxHeight - 1;
// maxX = dimension.getHighestX();
// maxY = dimension.getHighestY();
maxX = maxY = 0;
// xOffset = 512;
// yOffset = 256;
// xOffset = yOffset = 0;
switch (rotation) {
case 0:
xOffset = -getTileBounds(dimension.getLowestX(), dimension.getHighestY()).x;
yOffset = -getTileBounds(dimension.getLowestX(), dimension.getLowestY()).y;
break;
case 1:
xOffset = -getTileBounds(dimension.getHighestX(), dimension.getHighestY()).x;
yOffset = -getTileBounds(dimension.getLowestX(), dimension.getHighestY()).y;
break;
case 2:
xOffset = -getTileBounds(dimension.getHighestX(), dimension.getLowestY()).x;
yOffset = -getTileBounds(dimension.getHighestX(), dimension.getHighestY()).y;
break;
case 3:
xOffset = -getTileBounds(dimension.getLowestX(), dimension.getLowestY()).x;
yOffset = -getTileBounds(dimension.getHighestX(), dimension.getLowestY()).y;
break;
default:
throw new IllegalArgumentException();
}
// System.out.println("xOffset: " + xOffset + ", yOffset: " + yOffset);
java.awt.Dimension preferredSize = zoom(new java.awt.Dimension(width, height));
setPreferredSize(preferredSize);
setMinimumSize(preferredSize);
setMaximumSize(preferredSize);
setSize(preferredSize);
addHierarchyListener(this);
}
public RefreshMode getRefreshMode() {
return refreshMode;
}
public void setRefreshMode(RefreshMode refreshMode) {
this.refreshMode = refreshMode;
}
public BufferedImage getImage(ProgressReceiver progressReceiver) throws ProgressReceiver.OperationCancelled {
Tile3DRenderer renderer = new Tile3DRenderer(dimension, colourScheme, biomeScheme, customBiomeManager, rotation);
// Paint the complete image
java.awt.Dimension preferredSize = unzoom(getPreferredSize());
BufferedImage image = new BufferedImage(preferredSize.width, preferredSize.height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = image.createGraphics();
try {
int tileCount = zSortedTiles.size(), tileNo = 0;
for (Tile tile : zSortedTiles) {
Rectangle tileBounds = getTileBounds(tile.getX(), tile.getY());
g2.drawImage(renderer.render(tile), tileBounds.x, tileBounds.y, null);
if (progressReceiver != null) {
tileNo++;
progressReceiver.setProgress((float) tileNo / tileCount);
}
}
} finally {
g2.dispose();
}
return image;
}
public Point worldToView(int x, int y) {
// highlightTile = new Point(x >> 7, y >> 7);
switch (rotation) {
case 0:
return zoom(new Point(xOffset + TILE_SIZE + x - y, yOffset + maxHeight - 1 - TILE_SIZE / 2 + (y + x) / 2));
case 1:
return zoom(new Point(xOffset + TILE_SIZE * 2 - x - y, yOffset + maxHeight - 1 - (y - x) / 2));
case 2:
return zoom(new Point(xOffset + TILE_SIZE - x + y, yOffset + maxHeight - 1 + TILE_SIZE / 2 - (y + x) / 2));
case 3:
return zoom(new Point(xOffset + x + y, yOffset + maxHeight - 1 + (y - x) / 2));
default:
throw new IllegalArgumentException();
}
}
public Point getCentreMostTile() {
return centreTile;
}
public Point getHighlightTile() {
return highlightTile;
}
public void setHighlightTile(Point highlightTile) {
this.highlightTile = highlightTile;
repaint();
}
public int getZoom() {
return zoom;
}
public void setZoom(int zoom) {
this.zoom = zoom;
scale = (int) Math.pow(2.0, Math.abs(zoom - 1));
// System.out.println("Zoom " + zoom + " -> scale " + scale);
int width = dimension.getWidth() * TILE_SIZE + dimension.getHeight() * TILE_SIZE;
int height = width / 2 + maxHeight - 1;
java.awt.Dimension preferredSize = zoom(new java.awt.Dimension(width, height));
setPreferredSize(preferredSize);
setMinimumSize(preferredSize);
setMaximumSize(preferredSize);
setSize(preferredSize);
repaint();
}
/**
* Centre the view on a particular tile. Specifically, centre the view on a
* point waterLevel above the centre of the floor of the tile.
*
* @param tileX The X coordinate in tiles of the tile to centre the view on.
* @param tileY The Y coordinate in tiles of the tile to centre the view on.
*/
public void moveToTile(int tileX, int tileY) {
Rectangle tileBounds = zoom(getTileBounds(tileX, tileY));
moveTo(new Point(tileBounds.x + tileBounds.width / 2, tileBounds.y + tileBounds.height - TILE_SIZE / 2));
// highlightTile = new Point(tileX, tileY);
}
/**
* Centre the view on a particular point in the world. Specifically, centre
* the view on a point waterLevel above the floor of the world at
* the specified coordinates.
*
* @param x The X coordinate in blocks on which to centre the view.
* @param y The Y coordinate in blocks on which to centre the view.
*/
public void moveTo(int x, int y) {
Point coords = worldToView(x, y);
// highlightPoint = coords;
moveTo(coords);
}
public void refresh() {
threeDeeRenderManager.stop();
renderedTiles.clear();
repaint();
}
// Dimension.Listener
@Override
public void tilesAdded(Dimension dimension, Set tiles) {
// threeDeeRenderManager.renderTile(tile);
zSortedTiles.addAll(tiles);
for (Tile tile: tiles) {
tile.addListener(this);
}
}
@Override
public void tilesRemoved(Dimension dimension, Set tiles) {
for (Tile tile: tiles) {
tile.removeListener(this);
}
zSortedTiles.removeAll(tiles);
// renderedTiles.remove(new Point(tile.getX(), tile.getY()));
// TODO: the tile will be re-added if it was on the render queue, but
// since this can currently never happen anyway we will deal with that
// when it becomes necessary
}
// Tile.Listener
@Override
public void heightMapChanged(Tile tile) {
scheduleTileForRendering(tile);
}
@Override
public void terrainChanged(Tile tile) {
scheduleTileForRendering(tile);
}
@Override
public void waterLevelChanged(Tile tile) {
scheduleTileForRendering(tile);
}
@Override
public void seedsChanged(Tile tile) {
scheduleTileForRendering(tile);
}
@Override
public void layerDataChanged(Tile tile, Set changedLayers) {
for (Layer layer: changedLayers) {
if (! Tile3DRenderer.DEFAULT_HIDDEN_LAYERS.contains(layer)) {
scheduleTileForRendering(tile);
return;
}
}
}
@Override
public void allBitLayerDataChanged(Tile tile) {
scheduleTileForRendering(tile);
}
@Override
public void allNonBitlayerDataChanged(Tile tile) {
scheduleTileForRendering(tile);
}
// HierarchyListener
@Override
public void hierarchyChanged(HierarchyEvent event) {
if ((event.getChangeFlags() & HierarchyEvent.DISPLAYABILITY_CHANGED) != 0) {
if (isDisplayable()) {
// for (Tile tile: dimension.getTiles()) {
// threeDeeRenderManager.renderTile(tile);
// }
timer = new Timer(250, this);
timer.start();
} else {
timer.stop();
timer = null;
threeDeeRenderManager.stop();
for (Tile tile : dimension.getTiles()) {
tile.removeListener(this);
}
dimension.removeDimensionListener(this);
}
}
}
// ActionListener
@Override
public void actionPerformed(ActionEvent e) {
// Send tiles to be rendered
if ((!tilesWaitingToBeRendered.isEmpty()) && ((System.currentTimeMillis() - lastTileChange) > 250)) {
tilesWaitingToBeRendered.forEach(threeDeeRenderManager::renderTile);
tilesWaitingToBeRendered.clear();
}
// Collect rendered tiles
Set renderResults = threeDeeRenderManager.getRenderedTiles();
Rectangle repaintArea = null;
for (RenderResult renderResult : renderResults) {
Tile tile = renderResult.getTile();
int x = tile.getX(), y = tile.getY();
renderedTiles.put(tile, renderResult.getImage());
Rectangle tileBounds = zoom(getTileBounds(x, y));
if (repaintArea == null) {
repaintArea = tileBounds;
} else {
repaintArea = repaintArea.union(tileBounds);
}
}
if (repaintArea != null) {
// System.out.println("Repainting " + repaintArea);
repaint(repaintArea);
}
}
// Scrollable
@Override
public java.awt.Dimension getPreferredScrollableViewportSize() {
return getPreferredSize();
}
@Override
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
return 10;
}
@Override
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
return 100;
}
@Override
public boolean getScrollableTracksViewportWidth() {
return false;
}
@Override
public boolean getScrollableTracksViewportHeight() {
return false;
}
@Override
protected void paintComponent(Graphics g) {
// System.out.println("Drawing");
Graphics2D g2 = (Graphics2D) g;
if (zoom != 1) {
double scaleFactor = Math.pow(2.0, zoom - 1);
// System.out.println("Scaling with factor " + scaleFactor);
g2.scale(scaleFactor, scaleFactor);
if (zoom > 1) {
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
} else {
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
}
}
if (upsideDown) {
g2.scale(1.0, -1.0);
g2.translate(0, -getHeight());
}
Rectangle visibleRect = unzoom(getVisibleRect());
// System.out.println("Unzoomed visible rectangle: " + visibleRect);
int centerX = visibleRect.x + visibleRect.width / 2;
int centerY = visibleRect.y + visibleRect.height / 2 + waterLevel;
Tile mostCentredTile = null;
int smallestDistance = Integer.MAX_VALUE;
Rectangle clipBounds = g.getClipBounds();
for (Tile tile : zSortedTiles) {
Rectangle tileBounds = getTileBounds(tile.getX(), tile.getY());
// System.out.print("Tile bounds: " + tileBounds);
if (tileBounds.intersects(clipBounds)) {
// System.out.println(" intersects");
int dx = tileBounds.x + tileBounds.width / 2 - centerX;
int dy = tileBounds.y + tileBounds.height - TILE_SIZE / 2 - centerY;
int dist = (int) Math.sqrt((dx * dx) + (dy * dy));
if (dist < smallestDistance) {
smallestDistance = dist;
mostCentredTile = tile;
}
BufferedImage tileImg = renderedTiles.get(tile);
if (tileImg != null) {
g.drawImage(tileImg, tileBounds.x, tileBounds.y, null);
} else {
tilesWaitingToBeRendered.add(0, tile);
}
// } else {
// System.out.println(" does NOT intersect");
}
}
if (mostCentredTile != null) {
centreTile = new Point(mostCentredTile.getX(), mostCentredTile.getY());
}
if (highlightTile != null) {
g.setColor(Color.RED);
Rectangle rect = getTileBounds(highlightTile.x, highlightTile.y);
g.drawRect(rect.x, rect.y, rect.width, rect.height);
}
if (highlightPoint != null) {
g.setColor(Color.RED);
g.drawLine(highlightPoint.x - 2, highlightPoint.y, highlightPoint.x + 2, highlightPoint.y);
g.drawLine(highlightPoint.x, highlightPoint.y - 2, highlightPoint.x, highlightPoint.y + 2);
}
// for (Map.Entry entry: renderedTiles.entrySet()) {
// Point tileCoords = entry.getKey();
// BufferedImage tileImg = entry.getValue();
// Rectangle tileBounds = getTileBounds(tileCoords.x, tileCoords.y);
// if (tileBounds.intersects(clipBounds)) {
// g.drawImage(tileImg, tileBounds.x, tileBounds.y, null);
//// g.setColor(Color.RED);
//// g.drawRect(tileBounds.x, tileBounds.y, tileBounds.width, tileBounds.height);
// }
// }
}
private void scheduleTileForRendering(final Tile tile) {
// System.out.println("Scheduling tile for rendering: " + tile.getX() + ", " + tile.getY());
if (SwingUtilities.isEventDispatchThread()) {
Rectangle visibleArea = ((JViewport) getParent()).getViewRect();
Rectangle tileBounds = zoom(getTileBounds(tile.getX(), tile.getY()));
if (tileBounds.intersects(visibleArea)) {
// The tile is (partially) visible, so it should be repainted
// immediately
switch (refreshMode) {
case IMMEDIATE:
threeDeeRenderManager.renderTile(tile);
break;
case DELAYED:
tilesWaitingToBeRendered.add(tile);
lastTileChange = System.currentTimeMillis();
break;
case MANUAL:
// Do nothing
break;
default:
throw new InternalError();
}
} else {
// The tile is not visible, so repaint it when it becomes visible
tilesWaitingToBeRendered.remove(tile);
renderedTiles.remove(tile);
}
} else {
SwingUtilities.invokeLater(() -> {
Rectangle visibleArea = ((JViewport) getParent()).getViewRect();
Rectangle tileBounds = zoom(getTileBounds(tile.getX(), tile.getY()));
if (tileBounds.intersects(visibleArea)) {
// The tile is (partially) visible, so it should be repainted
// immediately
switch (refreshMode) {
case IMMEDIATE:
threeDeeRenderManager.renderTile(tile);
break;
case DELAYED:
tilesWaitingToBeRendered.add(tile);
lastTileChange = System.currentTimeMillis();
break;
case MANUAL:
// Do nothing
break;
default:
throw new InternalError();
}
} else {
// The tile is not visible, so repaint it when it becomes visible
tilesWaitingToBeRendered.remove(tile);
renderedTiles.remove(tile);
}
});
}
}
private Rectangle getTileBounds(int x, int y) {
switch (rotation) {
case 0:
return new Rectangle(xOffset + (x - y) * TILE_SIZE,
yOffset + (x + y) * TILE_SIZE / 2,
2 * TILE_SIZE,
TILE_SIZE + maxHeight - 1);
case 1:
return new Rectangle(xOffset + ((maxY - y) - x) * TILE_SIZE,
yOffset + ((maxY - y) + x) * TILE_SIZE / 2,
2 * TILE_SIZE,
TILE_SIZE + maxHeight - 1);
case 2:
return new Rectangle(xOffset + ((maxX - x) - (maxY - y)) * TILE_SIZE,
yOffset + ((maxX - x) + (maxY - y)) * TILE_SIZE / 2,
2 * TILE_SIZE,
TILE_SIZE + maxHeight - 1);
case 3:
return new Rectangle(xOffset + (y - (maxX - x)) * TILE_SIZE,
yOffset + (y + (maxX - x)) * TILE_SIZE / 2,
2 * TILE_SIZE,
TILE_SIZE + maxHeight - 1);
default:
throw new IllegalArgumentException();
}
}
/**
* Centre the view on a particular point in view coordinates.
*
* @param coords The point to centre on in view coordinates.
*/
private void moveTo(Point coords) {
Rectangle visibleRect = getVisibleRect();
scrollRectToVisible(new Rectangle(coords.x - visibleRect.width / 2, coords.y - visibleRect.height / 2, visibleRect.width, visibleRect.height));
}
private java.awt.Dimension zoom(java.awt.Dimension dimension) {
if (zoom < 1) {
dimension.width /= scale;
dimension.height /= scale;
} else if (zoom > 1) {
dimension.width *= scale;
dimension.height *= scale;
}
return dimension;
}
private Point zoom(Point point) {
if (zoom < 1) {
point.x /= scale;
point.y /= scale;
} else if (zoom > 1) {
point.x *= scale;
point.y *= scale;
}
return point;
}
private Rectangle zoom(Rectangle rectangle) {
if (zoom < 1) {
rectangle.x /= scale;
rectangle.y /= scale;
rectangle.width /= scale;
rectangle.height /= scale;
} else if (zoom > 1) {
rectangle.x *= scale;
rectangle.y *= scale;
rectangle.width *= scale;
rectangle.height *= scale;
}
return rectangle;
}
private Rectangle unzoom(Rectangle rectangle) {
if (zoom < 1) {
rectangle.x *= scale;
rectangle.y *= scale;
rectangle.width *= scale;
rectangle.height *= scale;
} else if (zoom > 1) {
rectangle.x /= scale;
rectangle.y /= scale;
rectangle.width /= scale;
rectangle.height /= scale;
}
return rectangle;
}
private java.awt.Dimension unzoom(java.awt.Dimension dimension) {
if (zoom < 1) {
dimension.width *= scale;
dimension.height *= scale;
} else if (zoom > 1) {
dimension.width /= scale;
dimension.height /= scale;
}
return dimension;
}
private final Dimension dimension;
private final Map renderedTiles = new HashMap<>();
private final ThreeDeeRenderManager threeDeeRenderManager;
private final ColourScheme colourScheme;
private final BiomeScheme biomeScheme;
private final List tilesWaitingToBeRendered = new LinkedList<>();
private final int maxHeight;
private final int xOffset, yOffset, maxX, maxY;
private final int rotation;
private final SortedSet zSortedTiles;
private final CustomBiomeManager customBiomeManager;
private final boolean upsideDown;
private Timer timer;
private long lastTileChange;
private RefreshMode refreshMode = RefreshMode.DELAYED;
private Point centreTile;
private int waterLevel, zoom = 1, scale = 1;
private Point highlightTile, highlightPoint;
// private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(ThreeDeeView.class);
private static final long serialVersionUID = 2011101701L;
public enum RefreshMode {IMMEDIATE, DELAYED, MANUAL}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy