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

org.pepsoft.worldpainter.WorldPainter 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;

import com.google.common.collect.Sets;
import org.pepsoft.minecraft.MapGenerator;
import org.pepsoft.util.MemoryUtils;
import org.pepsoft.worldpainter.Configuration.OverlayType;
import org.pepsoft.worldpainter.Dimension.Anchor;
import org.pepsoft.worldpainter.TileRenderer.LightOrigin;
import org.pepsoft.worldpainter.biomeschemes.CustomBiomeManager;
import org.pepsoft.worldpainter.brushes.BrushShape;
import org.pepsoft.worldpainter.layers.Biome;
import org.pepsoft.worldpainter.layers.Layer;
import org.pepsoft.worldpainter.ramps.ColourRamp;
import org.pepsoft.worldpainter.tools.BiomesTileProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

import static java.awt.BasicStroke.CAP_SQUARE;
import static java.awt.BasicStroke.JOIN_MITER;
import static java.util.Collections.singleton;
import static java.util.Collections.unmodifiableSet;
import static org.pepsoft.util.AwtUtils.doLaterOnEventThread;
import static org.pepsoft.util.AwtUtils.doOnEventThread;
import static org.pepsoft.worldpainter.Configuration.OverlayType.SCALE_ON_LOAD;
import static org.pepsoft.worldpainter.Constants.*;
import static org.pepsoft.worldpainter.DefaultPlugin.JAVA_ANVIL;
import static org.pepsoft.worldpainter.DefaultPlugin.JAVA_MCREGION;
import static org.pepsoft.worldpainter.Dimension.Anchor.NORMAL_DETAIL;
import static org.pepsoft.worldpainter.Generator.DEFAULT;
import static org.pepsoft.worldpainter.Generator.LARGE_BIOMES;
import static org.pepsoft.worldpainter.TileRenderer.FLUIDS_AS_LAYER;
import static org.pepsoft.worldpainter.TileRenderer.TERRAIN_AS_LAYER;
import static org.pepsoft.worldpainter.WPTileProvider.Effect.FADE_TO_FIFTY_PERCENT;

/**
 *
 * @author pepijn
 */
public class WorldPainter extends WorldPainterView implements MouseMotionListener, PropertyChangeListener, Dimension.Listener {
    public WorldPainter(ColourScheme colourScheme, CustomBiomeManager customBiomeManager) {
        super(false, false);
        this.colourScheme = colourScheme;
        this.customBiomeManager = customBiomeManager;
        setOpaque(true);
        addMouseMotionListener(this);
        enableInputMethods(false);
    }

    public WorldPainter(Dimension dimension, ColourScheme colourScheme, CustomBiomeManager customBiomeManager) {
        this(colourScheme, customBiomeManager);
        setDimension(dimension);
    }

    @Override
    public final Dimension getDimension() {
        return dimension;
    }

    @Override
    public final void setDimension(Dimension dimension) {
        setDimension(dimension, true);
    }

    final void setDimension(Dimension dimension, boolean refreshTiles) {
        Dimension oldDimension = this.dimension;
        if (oldDimension != null) {
            oldDimension.removePropertyChangeListener(this);
            if (oldDimension.getAnchor().dim == DIM_NORMAL) {
                oldDimension.getWorld().removePropertyChangeListener("spawnPoint", this);
            }
            oldDimension.removeDimensionListener(this);
            for (Overlay overlay: oldDimension.getOverlays()) {
                overlay.removePropertyChangeListener(this);
            }
        }
        this.dimension = dimension;
        if (dimension != null) {
            drawContours = dimension.isContoursEnabled();
            contourSeparation = dimension.getContourSeparation();
            dimension.addPropertyChangeListener(this);
            if (dimension.getAnchor().dim == DIM_NORMAL) {
                dimension.getWorld().addPropertyChangeListener("spawnPoint", this);
            }
            dimension.addDimensionListener(this);
            for (Overlay overlay: dimension.getOverlays()) {
                overlay.addPropertyChangeListener(this);
            }

            setGridSize(dimension.getGridSize());
            setPaintGrid(dimension.isGridEnabled());
            setLabelScale((int) dimension.getScale());
            
            overlayType = Configuration.getInstance().getOverlayType();
            drawOverlays = dimension.isOverlaysEnabled();
            setMarkerCoords((dimension.getAnchor().dim == DIM_NORMAL) ? dimension.getWorld().getSpawnPoint() : null);
        } else {
            drawOverlays = false;
            setMarkerCoords(null);
        }
        firePropertyChange("dimension", oldDimension, dimension);
        if (refreshTiles) {
            refreshTiles();
        }
    }

    public ColourScheme getColourScheme() {
        return colourScheme;
    }

    public void setColourScheme(ColourScheme colourScheme) {
        this.colourScheme = colourScheme;
        refreshTiles();
    }

    public boolean isDrawBrush() {
        return drawBrush;
    }

    public void setDrawBrush(boolean drawBrush) {
        if (drawBrush != this.drawBrush) {
            this.drawBrush = drawBrush;
            firePropertyChange("drawBrush", !drawBrush, drawBrush);
            repaintWorld(getBrushBounds());
        }
    }

    public boolean isDrawViewDistance() {
        return drawViewDistance;
    }

    public void setDrawViewDistance(boolean drawViewDistance) {
        if (drawViewDistance != this.drawViewDistance) {
            this.drawViewDistance = drawViewDistance;
            firePropertyChange("drawViewDistance", !drawViewDistance, drawViewDistance);
            final int scaledRadius = (dimension != null) ? (int) Math.ceil(viewDistance / dimension.getScale()) : viewDistance;
            repaintWorld(mouseX - scaledRadius, mouseY - scaledRadius, (2 * scaledRadius) + 1, (2 * scaledRadius) + 1);
        }
    }

    public int getViewDistance() {
        return viewDistance;
    }

    public void setViewDistance(int viewDistance) {
        if (viewDistance != this.viewDistance) {
            final int oldViewDistance = this.viewDistance;
            this.viewDistance = viewDistance;
            firePropertyChange("viewDistance", oldViewDistance, viewDistance);
            if (drawViewDistance) {
                final int largestDistance = Math.max(oldViewDistance, viewDistance);
                final int scaledRadius = (dimension != null) ? (int) Math.ceil(largestDistance / dimension.getScale()) : largestDistance;
                repaintWorld(mouseX - scaledRadius, mouseY - scaledRadius, (2 * scaledRadius) + 1, (2 * scaledRadius) + 1);
            }
        }
    }

    public boolean isDrawWalkingDistance() {
        return drawWalkingDistance;
    }

    public void setDrawWalkingDistance(boolean drawWalkingDistance) {
        if (drawWalkingDistance != this.drawWalkingDistance) {
            this.drawWalkingDistance = drawWalkingDistance;
            firePropertyChange("drawWalkingDistance", !drawWalkingDistance, drawWalkingDistance);
            final int scaledRadius = (dimension != null) ? (int) Math.ceil(DAY_NIGHT_WALK_DISTANCE_RADIUS / dimension.getScale()) : DAY_NIGHT_WALK_DISTANCE_RADIUS;
            repaintWorld(mouseX - scaledRadius, mouseY - scaledRadius, (2 * scaledRadius) + 1, (2 * scaledRadius) + 1);
        }
    }

    public int getRadius() {
        return radius;
    }

    public void setRadius(int radius) {
        int oldRadius = this.radius;
        int oldEffectiveRadius = this.effectiveRadius;
        this.radius = radius;
        if ((brushShape == BrushShape.CIRCLE) || ((brushRotation % 90) == 0)) {
            effectiveRadius = radius;
        } else {
            double a = brushRotation / 180.0 * Math.PI;
            effectiveRadius = (int) Math.ceil(Math.abs(Math.sin(a)) * radius + Math.abs(Math.cos(a)) * radius);
        }
        firePropertyChange("radius", oldRadius, radius);
        if (drawBrush && (brushShape != BrushShape.CUSTOM)) {
            int largestRadius = Math.max(oldEffectiveRadius, effectiveRadius);
            int diameter = largestRadius * 2 + 1;
            repaintWorld(mouseX - largestRadius, mouseY - largestRadius, diameter, diameter);
        }
    }

    public BrushShape getBrushShape() {
        return brushShape;
    }

    public void setBrushShape(BrushShape brushShape) {
        if (brushShape != this.brushShape) {
            BrushShape oldBrushShape = this.brushShape;
            Rectangle oldBounds = getBrushBounds();
            this.brushShape = brushShape;
            if ((brushShape == BrushShape.CIRCLE) || (brushShape == BrushShape.CUSTOM) || ((brushRotation % 90) == 0)) {
                effectiveRadius = radius;
            } else {
                double a = brushRotation / 180.0 * Math.PI;
                effectiveRadius = (int) Math.ceil(Math.abs(Math.sin(a)) * radius + Math.abs(Math.cos(a)) * radius);
            }
            firePropertyChange("brushShape", oldBrushShape, brushShape);
            if (drawBrush) {
                repaintWorld(getBrushBounds().union(oldBounds));
            }
        }
    }

    public Shape getCustomBrushShape() {
        return customBrushShape;
    }

    public void setCustomBrushShape(Shape customBrushShape) {
        final Shape oldCustomBrushShape = this.customBrushShape;
        final Rectangle oldBrushBounds = getBrushBounds();
        this.customBrushShape = customBrushShape;
        if ((drawBrush) && (brushShape == BrushShape.CUSTOM)) {
            repaintWorld(getBrushBounds().union(oldBrushBounds));
        }
        firePropertyChange("customBrushShape", oldCustomBrushShape, customBrushShape);
    }

    public int getContourSeparation() {
        return contourSeparation;
    }

    public void setContourSeparation(int contourSeparation) {
        if (contourSeparation != this.contourSeparation) {
            int oldContourSeparation = this.contourSeparation;
            this.contourSeparation = contourSeparation;
            refreshTiles();
            firePropertyChange("contourSeparation", oldContourSeparation, contourSeparation);
        }
    }

    public boolean isDrawContours() {
        return drawContours;
    }

    public void setDrawContours(boolean drawContours) {
        if (drawContours != this.drawContours) {
            this.drawContours = drawContours;
            refreshTiles();
            firePropertyChange("drawContours", ! drawContours, drawContours);
        }
    }

    public ColourRamp getColourRamp() {
        return colourRamp;
    }

    /**
     * Set or remove the colour ramp.
     *
     * @param colourRamp The colour ramp to set. May be {@code null} to remove the colour ramp.
     * @return A boolean which indicates whether the tiles have been refreshed as a result of changing the colour ramp.
     */
    public boolean setColourRamp(ColourRamp colourRamp) {
        if (! Objects.equals(colourRamp, this.colourRamp)) {
            final ColourRamp oldColourRamp = this.colourRamp;
            this.colourRamp = colourRamp;
            if ((hiddenLayers != null) && hiddenLayers.contains(TERRAIN_AS_LAYER)) {
                refreshTiles();
            }
            firePropertyChange("colourRamp", oldColourRamp, colourRamp);
        }
        return (hiddenLayers != null) && hiddenLayers.contains(TERRAIN_AS_LAYER);
    }

    public void refreshBrush() {
        Point mousePos = getMousePosition();
        if (mousePos != null) {
            mouseMoved(new MouseEvent(this, MouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 0, mousePos.x, mousePos.y, 0, false));
        }
    }

    public void setHiddenLayers(Set hiddenLayers) {
        final Set oldHiddenLayers = new HashSet<>(this.hiddenLayers);
        this.hiddenLayers.clear();
        if (hiddenLayers != null) {
            this.hiddenLayers.addAll(hiddenLayers);
        }
        final Set difference = Sets.symmetricDifference(oldHiddenLayers, this.hiddenLayers);
        if (! difference.isEmpty()) {
            if (dimension != null) {
                tileProvider.setHiddenLayers(this.hiddenLayers);
                if ((difference.contains(TERRAIN_AS_LAYER)) || (difference.contains(FLUIDS_AS_LAYER))) {
                    refreshTiles();
                } else if (!difference.isEmpty()) {
                    refreshTilesForLayers(difference, true);
                }
            }
            firePropertyChange("hiddenLayers", oldHiddenLayers, hiddenLayers);
        }
    }

    public Set getHiddenLayers() {
        return unmodifiableSet(hiddenLayers);
    }

    public void refreshTiles() {
        if (dimension != null) {
            int biomeAlgorithm = -1;
            long minecraftSeed = -1;
            final Anchor anchor = dimension.getAnchor();
            if (drawBiomes
                    && (anchor.equals(NORMAL_DETAIL))
                    && ((dimension.getBorder() == null) || (! dimension.getBorder().isEndless()))) {
                final World2 world = dimension.getWorld();
                if (world != null) {
                    final Platform platform = world.getPlatform();
                    if (platform == JAVA_MCREGION) {
                        biomeAlgorithm = BIOME_ALGORITHM_1_1;
                        minecraftSeed = dimension.getMinecraftSeed();
                    } else if (platform == JAVA_ANVIL) { // TODO add support for newer platforms
                        minecraftSeed = dimension.getMinecraftSeed();
                        final MapGenerator generator = dimension.getGenerator();
                        if (generator.getType() == DEFAULT) {
                            biomeAlgorithm = BIOME_ALGORITHM_1_7_DEFAULT;
                        } else if (generator.getType() == LARGE_BIOMES) {
                            biomeAlgorithm = BIOME_ALGORITHM_1_7_LARGE;
                        }
                    }
                }
            }
            if (biomeAlgorithm != -1) {
                final BiomesTileProvider biomesTileProvider = new BiomesTileProvider(biomeAlgorithm, minecraftSeed, colourScheme, 0, true);
                setTileProvider(LAYER_BIOMES, biomesTileProvider);
            } else {
                removeTileProvider(LAYER_BIOMES);
            }

            tileProvider = new WPTileProvider(dimension, colourScheme, customBiomeManager, hiddenLayers, drawContours, contourSeparation, lightOrigin, true, null, backgroundDimension == null, colourRamp);
            setTileProvider(LAYER_DETAILS, tileProvider);

            if (backgroundDimension != null) {
                backgroundTileProvider = new WPTileProvider(backgroundDimension, colourScheme, customBiomeManager, hiddenLayers, false, contourSeparation, lightOrigin, false, FADE_TO_FIFTY_PERCENT, true, colourRamp);
                setTileProvider(LAYER_BACKGROUND, backgroundTileProvider);
                setTileProviderZoom(backgroundTileProvider, backgroundDimensionZoom);
            } else {
                removeTileProvider(LAYER_BACKGROUND);
            }

            if (drawBorders && (dimension.getBorder() != null)) {
                setTileProvider(LAYER_BORDER, new WPBorderTileProvider(dimension, colourScheme));
            } else {
                removeTileProvider(LAYER_BORDER);
            }
        } else {
            if (getTileProviderCount() > 0) {
                removeAllTileProviders();
            }
            tileProvider = null;
        }
    }

    public void refreshTilesForLayer(Layer layer, boolean evenIfHidden) {
        refreshTilesForLayers(singleton(layer), evenIfHidden);
    }

    public void refreshTilesForLayers(Set layers, boolean evenIfHidden) {
        if ((hiddenLayers.containsAll(layers) && (! evenIfHidden)) || (dimension == null)) {
            return;
        }
        final long start = System.currentTimeMillis();
        Set coords = new HashSet<>();
        if (getZoom() < 0) {
            final int shift = -getZoom();
            for (Tile tile: dimension.getTiles()) {
                for (Layer layer: layers) {
                    if (tile.hasLayer(layer)) {
                        coords.add(new Point(tile.getX() >> shift, tile.getY() >> shift));
                        break;
                    }
                }
            }
        } else {
            for (Tile tile: dimension.getTiles()) {
                for (Layer layer: layers) {
                    if (tile.hasLayer(layer)) {
                        coords.add(new Point(tile.getX(), tile.getY()));
                        break;
                    }
                }
            }
        }
        if (! coords.isEmpty()) {
            refresh(tileProvider, coords);
        }
        if (logger.isDebugEnabled()) {
            logger.debug("Refreshing {} tiles for layers {} took {} ms", coords.size(), layers, System.currentTimeMillis() - start);
        }
    }
    
    @Override
    public void updateStatusBar(int x, int y) {
        App.getInstance().updateStatusBar(x, y);
    }

    public BufferedImage getImage() {
        if (dimension == null) {
            return null;
        }
        TileRenderer tileRenderer = new TileRenderer(dimension, colourScheme, customBiomeManager, 0, true, colourRamp);
        tileRenderer.setContourLines(drawContours);
        tileRenderer.setContourSeparation(contourSeparation);
        tileRenderer.setHiddenLayers(hiddenLayers);
        tileRenderer.setLightOrigin(lightOrigin);
        int xOffset = dimension.getLowestX(), yOffset = dimension.getLowestY();
        BufferedImage image = new BufferedImage(dimension.getWidth() << TILE_SIZE_BITS, dimension.getHeight() << TILE_SIZE_BITS, BufferedImage.TYPE_INT_ARGB);
        for (Tile tile: dimension.getTiles()) {
            tileRenderer.renderTile(tile, image, (tile.getX() - xOffset) << TILE_SIZE_BITS, (tile.getY() - yOffset) << TILE_SIZE_BITS);
        }
        return image;
    }

    public void rotateLightLeft() {
        lightOrigin = lightOrigin.left();
        refreshTiles();
    }

    public void rotateLightRight() {
        lightOrigin = lightOrigin.right();
        refreshTiles();
    }
    
    public LightOrigin getLightOrigin() {
        return lightOrigin;
    }
    
    public void setLightOrigin(LightOrigin lightOrigin) {
        if (lightOrigin == null) {
            throw new NullPointerException();
        }
        if (lightOrigin != this.lightOrigin) {
            this.lightOrigin = lightOrigin;
            refreshTiles();
        }
    }
    
    public void moveToSpawn() {
        if ((dimension != null) && (dimension.getAnchor().dim == DIM_NORMAL)) {
            moveToMarker();
        }
    }

    public Point getViewCentreInWorldCoords() {
        return new Point(getViewX(), getViewY());
    }

    public int getBrushRotation() {
        return brushRotation;
    }
    
    public void setBrushRotation(int brushRotation) {
        int oldBrushRotation = this.brushRotation;
        int oldEffectiveRadius = effectiveRadius;
        this.brushRotation = brushRotation;
        if ((brushShape == BrushShape.CIRCLE) || ((brushRotation % 90) == 0)) {
            effectiveRadius = radius;
        } else {
            double a = brushRotation / 180.0 * Math.PI;
            effectiveRadius = (int) Math.ceil(Math.abs(Math.sin(a)) * radius + Math.abs(Math.cos(a)) * radius);
        }
        firePropertyChange("brushRotation", oldBrushRotation, brushRotation);
        if (drawBrush && (brushShape != BrushShape.CIRCLE)) {
            int largestRadius = Math.max(oldEffectiveRadius, effectiveRadius);
            int diameter = largestRadius * 2 + 1;
            repaintWorld(mouseX - largestRadius, mouseY - largestRadius, diameter, diameter);
        }
    }
    
    public void minecraftSeedChanged(Dimension dimension, long newSeed) {
        if ((! isInhibitUpdates()) && (! hiddenLayers.contains(Biome.INSTANCE))) {
            refreshTiles();
        }
    }

    public boolean isDrawMinecraftBorder() {
        return drawMinecraftBorder;
    }

    public void setDrawMinecraftBorder(boolean drawMinecraftBorder) {
        if (drawMinecraftBorder != this.drawMinecraftBorder) {
            this.drawMinecraftBorder = drawMinecraftBorder;
            firePropertyChange("drawMinecraftBorder", ! drawMinecraftBorder, drawMinecraftBorder);
            repaint();
        }
    }

    public boolean isDrawBorders() {
        return drawBorders;
    }

    public void setDrawBorders(boolean drawBorders) {
        if (drawBorders != this.drawBorders) {
            this.drawBorders = drawBorders;
            firePropertyChange("drawBorders", ! drawBorders, drawBorders);
            refreshTiles();
        }
    }

    public boolean isDrawBiomes() {
        return drawBiomes;
    }

    public void setDrawBiomes(boolean drawBiomes) {
        if (drawBiomes != this.drawBiomes) {
            this.drawBiomes = drawBiomes;
            if ((dimension != null) && (dimension.getAnchor().dim == DIM_NORMAL)) {
                refreshTiles();
            }
            firePropertyChange("drawBiomes", ! drawBiomes, drawBiomes);
        }
    }

    @Override
    public Point getMousePosition() throws HeadlessException {
        Point translation = new Point(0, 0);
        Component component = this;
        while (component != null) {
            Point mousePosition = (component == this) ? super.getMousePosition() : component.getMousePosition();
            if (mousePosition != null) {
                mousePosition.translate(-translation.x, -translation.y);
                return mousePosition;
            } else {
                translation.translate(component.getX(), component.getY());
                component = component.getParent();
            }
        }
        return null;
    }

    public Dimension getBackgroundDimension() {
        return backgroundDimension;
    }

    public void setBackgroundDimension(Dimension backgroundDimension, int zoomLevel, WPTileProvider.Effect effect) {
        this.backgroundDimension = backgroundDimension;
        backgroundDimensionZoom = zoomLevel;
        refreshTiles();
    }

    // MouseMotionListener

    @Override
    public void mouseDragged(MouseEvent e) {
        int oldMouseX = mouseX;
        int oldMouseY = mouseY;
        Point mouseInWorld = viewToWorld(e.getPoint());
        mouseX = mouseInWorld.x;
        mouseY = mouseInWorld.y;
        if ((mouseX == oldMouseX) && (mouseY == oldMouseY)) {
            return;
        }
        Rectangle repaintArea = null; // The repaint area in world coordinates relative to the mouse position
        if (drawBrush) {
            if (brushShape != BrushShape.CUSTOM) {
                repaintArea = new Rectangle(-effectiveRadius, -effectiveRadius, effectiveRadius * 2 + 1, effectiveRadius * 2 + 1);
            } else {
                repaintArea = customBrushShape.getBounds();
            }
        }
        if (dimension != null) {
            if (drawViewDistance) {
                final int scaledRadius = (int) Math.ceil(viewDistance / dimension.getScale());
                Rectangle viewDistanceArea = new Rectangle(-scaledRadius, -scaledRadius, scaledRadius * 2, scaledRadius * 2);
                if (repaintArea != null) {
                    repaintArea = repaintArea.union(viewDistanceArea);
                } else {
                    repaintArea = viewDistanceArea;
                }
            }
            if (drawWalkingDistance) {
                final int scaledRadius = (int) Math.ceil(viewDistance / dimension.getScale());
                Rectangle walkingDistanceArea = new Rectangle(-scaledRadius, -scaledRadius, scaledRadius * 2, scaledRadius * 2);
                if (repaintArea != null) {
                    repaintArea = repaintArea.union(walkingDistanceArea);
                } else {
                    repaintArea = walkingDistanceArea;
                }
            }
        }
        if (repaintArea != null) {
            Rectangle oldRectangle = new Rectangle(oldMouseX + repaintArea.x, oldMouseY + repaintArea.y, repaintArea.width, repaintArea.height);
            Rectangle newRectangle = new Rectangle(mouseX + repaintArea.x, mouseY + repaintArea.y, repaintArea.width, repaintArea.height);
            if (oldRectangle.intersects(newRectangle)) {
                repaintWorld(oldRectangle.union(newRectangle));
            } else {
                // Two separate repaints to avoid having to repaint a huge area
                // just because the cursor jumps a large distance for some
                // reason
                repaintWorld(oldRectangle);
                SwingUtilities.invokeLater(() -> repaintWorld(newRectangle));
            }
        }
    }

    @Override
    public void mouseMoved(MouseEvent e) {
        mouseDragged(e);
    }

    // PropertyChangeListener

    @Override
    public void propertyChange(PropertyChangeEvent evt) {
        if ((evt.getSource() == dimension.getWorld()) && evt.getPropertyName().equals("spawnPoint")) {
            setMarkerCoords((Point) evt.getNewValue());
        } else if (evt.getSource() == dimension) {
            if ("overlaysEnabled".equals(evt.getPropertyName())) {
                drawOverlays = ((Boolean) evt.getNewValue());
                repaint();
            }
        } else if (evt.getSource() instanceof Overlay) {
            if (evt.getPropertyName().equals("image")) {
                // Do nothing
            } else if ((evt.getPropertyName().equals("scale")) && (Configuration.getInstance().getOverlayType() == SCALE_ON_LOAD)) {
                // The overlay type is set to scale on load, so since the scale has changed the image has to be reloaded
                ((Overlay) evt.getSource()).setImage(null);
                if (drawOverlays) {
                    // Since reloading an image is slow, wait with triggering it in case the value is still adjusting
                    scheduleRepaint(250);
                }
            } else if (drawOverlays) {
                repaint();
            }
        }
    }

    // Dimension.Listener

    @Override public void tilesAdded(Dimension dimension, Set tiles) {}
    @Override public void tilesRemoved(Dimension dimension, Set tiles) {}

    @Override
    public void overlayAdded(Dimension dimension, int index, Overlay overlay) {
        overlay.addPropertyChangeListener(this);
        if (drawOverlays) {
            repaint();
        }
    }

    @Override
    public void overlayRemoved(Dimension dimension, int index, Overlay overlay) {
        overlay.removePropertyChangeListener(this);
        if (drawOverlays) {
            repaint();
        }
    }

    long getOverlayImageSize() {
        long total = 0L;
        if (dimension != null) {
            for (Overlay overlay: dimension.getOverlays()) {
                total += (overlay.getImage() != null) ? MemoryUtils.getSize(overlay.getImage(), Collections.emptySet()) : 0L;
            }
        }
        return total;
    }
    
    @Override
    protected void paintComponent(Graphics g) {
        // Paint the tiles, grid and markers:
        super.paintComponent(g);

        if (dimension != null) {
            // Paint anything else:
            final Graphics2D g2 = (Graphics2D) g;
            final Color savedColour = g2.getColor();
            final Object savedAAValue = g2.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
//            final Object savedInterpolationValue = g2.getRenderingHint(RenderingHints.KEY_INTERPOLATION);
            final Stroke savedStroke = g2.getStroke();
            final AffineTransform savedTransform = g2.getTransform();
            final Font savedFont = g2.getFont();
            try {
                if (drawMinecraftBorder && (dimension.getWorld() != null)) {
                    drawMinecraftBorderIfNecessary(g2, dimension.getWorld().getBorderSettings());
                }

                // Switch to world coordinate system
                final float scale = transformGraphics(g2);
                if (drawOverlays) {
                    drawOverlays(g2);
                }
                if (drawBrush || drawViewDistance || drawWalkingDistance) {
                    g2.setColor(Color.BLACK);
                    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                    drawBrushEtc(g2, scale, false);
                    g2.setColor(Color.WHITE);
                    drawBrushEtc(g2, scale, true);
                }
            } finally {
                g2.setColor(savedColour);
                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, savedAAValue);
//                if (savedInterpolationValue != null) {
//                    g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, savedInterpolationValue);
//                }
                g2.setStroke(savedStroke);
                g2.setTransform(savedTransform);
                g2.setFont(savedFont);
            }
        }
    }

    private void drawBrushEtc(Graphics2D g2, float scale, boolean dashed) {
        final float onePixel = 1 / scale;
        if (! dashed) {
            g2.setStroke(new BasicStroke(onePixel));
        }
        if (drawBrush) {
            if (dashed) {
                g2.setStroke(new BasicStroke(onePixel, CAP_SQUARE, JOIN_MITER, 10.0f, new float[] { 4 * onePixel, 6 * onePixel }, 0));
            }
            final int diameter = radius * 2 + 1;
            switch (brushShape) {
                case CIRCLE:
                    g2.drawOval(mouseX - radius, mouseY - radius, diameter, diameter);
                    break;
                case SQUARE:
                    if (brushRotation % 90 == 0) {
                        g2.drawRect(mouseX - radius, mouseY - radius, diameter, diameter);
                    } else {
                        AffineTransform existingTransform = g2.getTransform();
                        try {
                            if (scale > 1.0f) {
                                g2.rotate(brushRotation / 180.0 * Math.PI, mouseX + 0.5, mouseY + 0.5);
                            } else {
                                g2.rotate(brushRotation / 180.0 * Math.PI, mouseX, mouseY);
                            }
                            g2.drawRect(mouseX - radius, mouseY - radius, diameter, diameter);
                        } finally {
                            g2.setTransform(existingTransform);
                        }
                    }
                    break;
                case BITMAP:
                    final int arrowSize = radius / 2;
                    if (brushRotation == 0) {
                        g2.drawRect(mouseX - radius, mouseY - radius, diameter, diameter);
                        if (arrowSize > 0) {
                            g2.drawLine(mouseX, mouseY - radius, mouseX - arrowSize, mouseY - radius + arrowSize);
                            g2.drawLine(mouseX - arrowSize, mouseY - radius + arrowSize, mouseX + arrowSize + 1, mouseY - radius + arrowSize);
                            g2.drawLine(mouseX + arrowSize + 1, mouseY - radius + arrowSize, mouseX + 1, mouseY - radius);
                        }
                    } else {
                        AffineTransform existingTransform = g2.getTransform();
                        try {
                            if (scale > 1.0f) {
                                g2.rotate(brushRotation / 180.0 * Math.PI, mouseX + 0.5, mouseY + 0.5);
                            } else {
                                g2.rotate(brushRotation / 180.0 * Math.PI, mouseX, mouseY);
                            }
                            g2.drawRect(mouseX - radius, mouseY - radius, diameter, diameter);
                            if (arrowSize > 0) {
                                g2.drawLine(mouseX, mouseY - radius, mouseX - arrowSize, mouseY - radius + arrowSize);
                                g2.drawLine(mouseX - arrowSize, mouseY - radius + arrowSize, mouseX + arrowSize + 1, mouseY - radius + arrowSize);
                                g2.drawLine(mouseX + arrowSize + 1, mouseY - radius + arrowSize, mouseX + 1, mouseY - radius);
                            }
                        } finally {
                            g2.setTransform(existingTransform);
                        }
                    }
                    break;
                case CUSTOM:
                    AffineTransform existingTransform = g2.getTransform();
                    try {
                        g2.translate(mouseX, mouseY);
                        g2.draw(customBrushShape);
                    } finally {
                        g2.setTransform(existingTransform);
                    }
            }
        }
        if (drawViewDistance) {
            if (dashed) {
                g2.setStroke(new BasicStroke(onePixel, CAP_SQUARE, JOIN_MITER, 10.0f, new float[] { 9 * onePixel, 11 * onePixel }, 0));
            }
            final int scaledRadius = (int) Math.ceil(viewDistance / dimension.getScale());
            g2.drawOval(mouseX - scaledRadius, mouseY - scaledRadius, scaledRadius * 2, scaledRadius * 2);
        }
        if (drawWalkingDistance) {
            if (dashed) {
                g2.setStroke(new BasicStroke(onePixel, CAP_SQUARE, JOIN_MITER, 10.0f, new float[] { 19 * onePixel, 21 * onePixel }, 0));
            }
            int scaledRadius = (int) Math.ceil(DAY_NIGHT_WALK_DISTANCE_RADIUS / dimension.getScale());
            g2.drawOval(mouseX - scaledRadius, mouseY - scaledRadius, scaledRadius * 2, scaledRadius * 2);
            setFont(NORMAL_FONT.deriveFont(10 * onePixel));
            g2.drawString("day + night", mouseX - scaledRadius + onePixel * 3, mouseY);
            scaledRadius = (int) Math.ceil(DAY_WALK_DISTANCE_RADIUS / dimension.getScale());
            g2.drawOval(mouseX - scaledRadius, mouseY - scaledRadius, scaledRadius * 2, scaledRadius * 2);
            g2.drawString("1 day", mouseX - scaledRadius + onePixel * 3, mouseY);
            scaledRadius = (int) Math.ceil(FIVE_MINUTE_WALK_DISTANCE_RADIUS / dimension.getScale());
            g2.drawOval(mouseX - scaledRadius, mouseY - scaledRadius, scaledRadius * 2, scaledRadius * 2);
            g2.drawString("5 min.", mouseX - scaledRadius + onePixel * 3, mouseY);
        }
    }

    /**
     * Get the rectangular bounds of the current brush shape.
     *
     * @return The bounds of the brush.
     */
    private Rectangle getBrushBounds() {
        if (brushShape == BrushShape.CUSTOM) {
            Rectangle bounds = customBrushShape.getBounds();
            bounds.translate(mouseX, mouseY);
            return bounds;
        } else {
            return new Rectangle(mouseX - effectiveRadius, mouseY - effectiveRadius, effectiveRadius * 2 + 1, effectiveRadius * 2 + 1);
        }
    }

    private void loadOverlay(Overlay overlay) {
        File file = overlay.getFile();
        if ((file != null) && file.isFile()) {
            if (file.canRead()) {
                logger.info("Loading image");
                BufferedImage overlayImage;
                try {
                    overlayImage = ImageIO.read(file);
                } catch (IOException e) {
                    logger.error("I/O error while loading image " + file ,e);
                    doLaterOnEventThread(() -> JOptionPane.showMessageDialog(this, "An error occurred while loading the overlay image.\nIt may not be a valid or supported image file, or the file may be corrupted.", "Error Loading Image", JOptionPane.ERROR_MESSAGE));
                    overlay.setEnabled(false);
                    return;
                } catch (RuntimeException | Error e) {
                    logger.error(e.getClass().getSimpleName() + " while loading image " + file ,e);
                    doLaterOnEventThread(() -> JOptionPane.showMessageDialog(this, "An error occurred while loading the overlay image.\nThere may not be enough available memory, or the image may be too large.", "Error Loading Image", JOptionPane.ERROR_MESSAGE));
                    overlay.setEnabled(false);
                    return;
                }
                if (overlayImage != null) {
                    switch (overlayType) {
                        case OPTIMISE_ON_LOAD:
                            // "Scale" to 100%, which optimises the image for the screen environment
                            overlayImage = scaleImage(overlayImage, getGraphicsConfiguration(), 1.0f);
                            break;
                        case SCALE_ON_LOAD:
                            overlayImage = scaleImage(overlayImage, getGraphicsConfiguration(), overlay.getScale());
                            break;
                    }
                } else {
                    logger.error("Image overlay file " + file + " did not contain a recognisable image");
                    doLaterOnEventThread(() -> JOptionPane.showMessageDialog(this, "Image overlay file did not contain a recognisable image. It may have been corrupted.\n" + file, "Error Loading Image", JOptionPane.ERROR_MESSAGE));
                }
                if (overlayImage != null) {
                    overlay.setImage(overlayImage);
                } else {
                    // The loading, scaling or optimisation failed
                    overlay.setEnabled(false);
                }
            } else {
                doLaterOnEventThread(() -> JOptionPane.showMessageDialog(this, "Access denied to overlay image\n" + file, "Error Enabling Overlay", JOptionPane.ERROR_MESSAGE));
                overlay.setEnabled(false);
            }
        } else {
            doLaterOnEventThread(() -> JOptionPane.showMessageDialog(this, "Overlay image file not found\n" + file, "Error Enabling Overlay", JOptionPane.ERROR_MESSAGE));
            overlay.setEnabled(false);
        }
    }

    private BufferedImage scaleImage(BufferedImage image, GraphicsConfiguration graphicsConfiguration, float scale) {
        try {
            final boolean alpha = image.getColorModel().hasAlpha();
            if (scale == 1.0f) {
                logger.info("Optimising image");
                final BufferedImage optimumImage = graphicsConfiguration.createCompatibleImage(image.getWidth(), image.getHeight(), alpha ? Transparency.TRANSLUCENT : Transparency.OPAQUE);
                final Graphics2D g2 = optimumImage.createGraphics();
                try {
                    g2.drawImage(image, 0, 0, null);
                } finally {
                    g2.dispose();
                }
                return optimumImage;
            } else {
                logger.info("Scaling image");
                final int width = Math.round(image.getWidth() * scale);
                final int height = Math.round(image.getHeight() * scale);
                final BufferedImage optimumImage = graphicsConfiguration.createCompatibleImage(width, height, alpha ? Transparency.TRANSLUCENT : Transparency.OPAQUE);
                final Graphics2D g2 = optimumImage.createGraphics();
                try {
                    g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
                    g2.drawImage(image, 0, 0, width, height, null);
                } finally {
                    g2.dispose();
                }
                return optimumImage;
            }
        } catch (RuntimeException | Error e) {
            logger.error(e.getClass().getSimpleName() + " while scaling image of size " + image.getWidth() + "x" + image.getHeight() + " and type " + image.getType() + " to " + scale + "%", e);
            doLaterOnEventThread(() -> JOptionPane.showMessageDialog(null, "An error occurred while " + ((scale == 100) ? "optimising" : "scaling") + " the overlay image.\nThere may not be enough available memory, or the image may be too large.", "Error " + ((scale == 100) ? "Optimising" : "Scaling") + " Image", JOptionPane.ERROR_MESSAGE));
            return null;
        }
    }

    private void drawMinecraftBorderIfNecessary(Graphics2D g2, World2.BorderSettings borderSettings) {
        final int size = borderSettings.getSize(), radius = size / 2;
        final Rectangle border = worldToView(borderSettings.getCentreX() - radius, borderSettings.getCentreY() - radius, size, size);
        Rectangle clip = g2.getClipBounds();
        if ((border.x >= clip.x) || (border.y >= clip.y) || ((border.x + border.width) < (clip.x + clip.width)) || ((border.y + border.height) < (clip.y + clip.height))) {
            g2.setColor(Color.RED);
            g2.setStroke(new BasicStroke(1, CAP_SQUARE, JOIN_MITER, 10.0f, new float[] { 3, 3 }, 0));
            if ((border.width < 5000) && (border.height < 5000)) {
                // If it's small enough performance of drawing it at once is fine
                g2.drawRect(border.x, border.y, border.width, border.height);
            } else if (clip.intersects(border)) {
                // For very large rectangles performance of drawing it as a rect
                // tanks, so draw each line individually, constraining the
                // lengths to the clip bounds
                g2.drawLine(border.x, Math.max(border.y, clip.y), border.x, Math.min(border.y + border.height, clip.y + clip.height));
                g2.drawLine(Math.max(border.x, clip.x), border.y + border.height, Math.min(border.x + border.width, clip.x + clip.width), border.y + border.height);
                g2.drawLine(border.x + border.width, Math.min(border.y + border.height, clip.y + clip.height), border.x + border.width, Math.max(border.y, clip.y));
                g2.drawLine(Math.min(border.x + border.width, clip.x + clip.width), border.y, Math.max(border.x, clip.x), border.y);
            }
        }
    }

    /**
     * Repaint an area in world coordinates, plus a few pixels extra to
     * compensate for sloppiness in painting the brush.
     * 
     * @param x The x coordinate of the area to repaint, in world coordinates.
     * @param y The y coordinate of the area to repaint, in world coordinates.
     * @param width The width of the area to repaint, in world coordinates.
     * @param height The height of the area to repaint, in world coordinates.
     */
    private void repaintWorld(int x, int y, int width, int height) {
        Rectangle area = worldToView(x, y, width, height);
        repaint(area.x - 2, area.y - 2, area.width + 4, area.height + 4);
    }

    /**
     * Repaint an area in world coordinates, plus a few pixels extra to
     * compensate for sloppiness in painting the brush.
     *
     * @param area The the area to repaint, in world coordinates.
     */
    private void repaintWorld(Rectangle area) {
        area = worldToView(area);
        repaint(area.x - 2, area.y - 2, area.width + 4, area.height + 4);
    }

    private void drawOverlays(Graphics2D g2) {
        if (dimension.getOverlays().isEmpty()) {
            return;
        }
        Composite savedComposite = g2.getComposite();
        try {
            for (Overlay overlay: dimension.getOverlays()) {
                final float overlayTransparency = overlay.getTransparency();
                if ((! overlay.isEnabled()) || (overlay.getTransparency() == 1.0f)) {
                    // Not enabled, or fully transparent
                    continue;
                } else {
                    if (overlay.getImage() == null) {
                        loadOverlay(overlay);
                    }
                    final BufferedImage overlayImage = overlay.getImage();
                    if (overlayImage == null) {
                        // If it is _still_ null then loading has failed
                        continue;
                    }

                    // Translucent or fully opaque
                    if (overlayTransparency > 0.0f) {
                        // Translucent
                        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f - overlayTransparency));
                    }
                    final float overlayScale = overlay.getScale();
                    final int overlayOffsetX = overlay.getOffsetX(), overlayOffsetY = overlay.getOffsetY();
                    if ((overlayType == SCALE_ON_LOAD) || (overlayScale == 1.0f)) {
                        // 1:1 scale, or the image has already been scaled on loading
                        g2.drawImage(overlayImage, overlayOffsetX, overlayOffsetY, null);
                    } else {
                        final int width = Math.round(overlayImage.getWidth() * overlayScale);
                        final int height = Math.round(overlayImage.getHeight() * overlayScale);
                        final Object savedInterpolation = g2.getRenderingHint(RenderingHints.KEY_INTERPOLATION);
                        try {
                            g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
                            g2.drawImage(overlayImage, overlayOffsetX, overlayOffsetY, width, height, null);
                        } finally {
                            if (savedInterpolation != null) {
                                g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, savedInterpolation);
                            }
                        }
                    }
                }
            }
        } finally {
            g2.setComposite(savedComposite);
        }
    }

    /**
     * Schedules a {@link #repaint()} for {@code delay} ms after now. If a repaint was already scheduled, that is
     * postponed.
     *
     * @param delay The number of ms to delay the repaint.
     */
    private void scheduleRepaint(int delay) {
        doOnEventThread(() -> {
            if (repaintTimer != null) {
                repaintTimer.stop();
            }
            repaintTimer = new Timer(delay, event -> {
                repaint();
                repaintTimer = null;
            });
            repaintTimer.setRepeats(false);
            repaintTimer.start();
        });
    }

    private HashSet hiddenLayers = new HashSet<>();
    private final CustomBiomeManager customBiomeManager;
    private Dimension dimension, backgroundDimension; // TODO make this more generic
    private int mouseX, mouseY, radius, effectiveRadius, contourSeparation, brushRotation, backgroundDimensionZoom,
            viewDistance;
    private boolean drawBrush, drawOverlays, drawContours, drawViewDistance, drawWalkingDistance,
            drawMinecraftBorder = true, drawBorders = true, drawBiomes = true;
    private BrushShape brushShape;
    private ColourScheme colourScheme;
    private LightOrigin lightOrigin = LightOrigin.NORTHWEST;
    private WPTileProvider tileProvider, backgroundTileProvider;
    private Shape customBrushShape;
    private OverlayType overlayType;
    private ColourRamp colourRamp;
    private Timer repaintTimer;

    private static final int FIVE_MINUTE_WALK_DISTANCE_RADIUS = 1280;
    private static final int DAY_WALK_DISTANCE_RADIUS = 3328;
    private static final int DAY_NIGHT_WALK_DISTANCE_RADIUS = 5120;
    private static final Font NORMAL_FONT = new Font("SansSerif", Font.PLAIN, 10);
    private static final Logger logger = LoggerFactory.getLogger(WorldPainter.class);
    private static final long serialVersionUID = 1L;

    private static final int LAYER_BIOMES     = -3;
    private static final int LAYER_BORDER     = -2;
    private static final int LAYER_BACKGROUND = -1;
    private static final int LAYER_DETAILS    =  0;
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy