All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.pepsoft.worldpainter.threedeeview.ThreeDeeView Maven / Gradle / Ivy

There is a newer version: 2.23.2
Show newest version
/*
 * 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