mirror of https://github.com/FreeCol/freecol.git
1788 lines
76 KiB
Java
1788 lines
76 KiB
Java
/**
|
|
* Copyright (C) 2002-2022 The FreeCol Team
|
|
*
|
|
* This file is part of FreeCol.
|
|
*
|
|
* FreeCol 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 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* FreeCol 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 FreeCol. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package net.sf.freecol.client.gui.mapviewer;
|
|
|
|
import static net.sf.freecol.common.util.Utils.now;
|
|
|
|
import java.awt.AlphaComposite;
|
|
import java.awt.BasicStroke;
|
|
import java.awt.Color;
|
|
import java.awt.Composite;
|
|
import java.awt.Dimension;
|
|
import java.awt.Font;
|
|
import java.awt.FontMetrics;
|
|
import java.awt.Graphics2D;
|
|
import java.awt.Point;
|
|
import java.awt.Rectangle;
|
|
import java.awt.RenderingHints;
|
|
import java.awt.Shape;
|
|
import java.awt.Stroke;
|
|
import java.awt.event.ActionListener;
|
|
import java.awt.font.TextLayout;
|
|
import java.awt.geom.AffineTransform;
|
|
import java.awt.geom.Ellipse2D;
|
|
import java.awt.geom.GeneralPath;
|
|
import java.awt.geom.Point2D.Float;
|
|
import java.awt.geom.Rectangle2D;
|
|
import java.awt.geom.RoundRectangle2D;
|
|
import java.awt.image.BufferedImage;
|
|
import java.awt.image.RescaleOp;
|
|
import java.awt.image.VolatileImage;
|
|
import java.util.ArrayList;
|
|
import java.util.EnumMap;
|
|
import java.util.List;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
import java.util.stream.Collectors;
|
|
|
|
import javax.swing.SwingUtilities;
|
|
|
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
|
import net.sf.freecol.client.ClientOptions;
|
|
import net.sf.freecol.client.FreeColClient;
|
|
import net.sf.freecol.client.control.FreeColClientHolder;
|
|
import net.sf.freecol.client.gui.Canvas;
|
|
import net.sf.freecol.client.gui.GUI;
|
|
import net.sf.freecol.client.gui.GUI.ViewMode;
|
|
import net.sf.freecol.client.gui.ImageLibrary;
|
|
import net.sf.freecol.client.gui.SwingGUI;
|
|
import net.sf.freecol.common.debug.FreeColDebugger;
|
|
import net.sf.freecol.common.i18n.Messages;
|
|
import net.sf.freecol.common.model.Ability;
|
|
import net.sf.freecol.common.model.Area;
|
|
import net.sf.freecol.common.model.BuildableType;
|
|
import net.sf.freecol.common.model.Colony;
|
|
import net.sf.freecol.common.model.Direction;
|
|
import net.sf.freecol.common.model.IndianSettlement;
|
|
import net.sf.freecol.common.model.Map;
|
|
import net.sf.freecol.common.model.PathNode;
|
|
import net.sf.freecol.common.model.Player;
|
|
import net.sf.freecol.common.model.Region;
|
|
import net.sf.freecol.common.model.Settlement;
|
|
import net.sf.freecol.common.model.Tile;
|
|
import net.sf.freecol.common.model.Turn;
|
|
import net.sf.freecol.common.model.Unit;
|
|
import net.sf.freecol.common.option.GameOptions;
|
|
import net.sf.freecol.common.util.ImageUtils;
|
|
import net.sf.freecol.server.ai.AIObject;
|
|
import net.sf.freecol.server.ai.EuropeanAIPlayer;
|
|
import net.sf.freecol.server.ai.military.DefensiveMap;
|
|
import net.sf.freecol.server.ai.military.DefensiveZone;
|
|
|
|
|
|
/**
|
|
* MapViewer is a private helper class of Canvas and SwingGUI.
|
|
*
|
|
* This class is used by {@link CanvasMapViewer} for drawing the map on the {@link Canvas}.
|
|
*
|
|
* The method {@link #displayMap(Graphics2D, Dimension)} renders the entire map, or just parts
|
|
* of it depending on clip bounds and the dirty state controlled by
|
|
* {@link MapViewerRepaintManager}.
|
|
*
|
|
* Unit animations are still handled {@link UnitAnimator separately}, but will probably be moved
|
|
* into {@code displayMap} in the future.
|
|
*
|
|
* @see MapViewerBounds
|
|
*/
|
|
public final class MapViewer extends FreeColClientHolder {
|
|
|
|
private static final Logger logger = Logger.getLogger(MapViewer.class.getName());
|
|
|
|
private static enum BorderType { COUNTRY, REGION }
|
|
|
|
/**
|
|
* Calculates what part of the {@link Map} is visible on screen. This includes handling
|
|
* the size, scaling and focus of the map.
|
|
*
|
|
* Please note that when repainting the Map, it is only necessary to paint the
|
|
* requested area given by {@link Graphics2D#getClipBounds}. Translating the
|
|
* the clip bounds into tiles is performed by {@link TileClippingBounds}.
|
|
*/
|
|
private final MapViewerBounds mapViewerBounds = new MapViewerBounds();
|
|
|
|
/**
|
|
* Holds state that is not part of the Game/Map state, but is still used when drawing
|
|
* the map. For example the current active unit.
|
|
*/
|
|
private final MapViewerState mapViewerState;
|
|
|
|
/**
|
|
* The internal scaled tile viewer to use.
|
|
*/
|
|
private final TileViewer tv;
|
|
|
|
/**
|
|
* Holds buffers and determines the dirty state when drawing.
|
|
*/
|
|
private final MapViewerRepaintManager rpm;
|
|
|
|
/**
|
|
* Utility functions that considers the current map size and scale.
|
|
*/
|
|
private final MapViewerScaledUtils mapViewerScaledUtils;
|
|
|
|
/**
|
|
* Scaled image library to use only for map operations.
|
|
*/
|
|
private final ImageLibrary lib;
|
|
|
|
/**
|
|
* Bounds of the tiles to be rendered. These bounds are scaled according to the
|
|
* zoom level of the map.
|
|
*/
|
|
private TileBounds tileBounds = new TileBounds(new Dimension(0, 0), 1f);
|
|
|
|
private List<Rectangle> fullyRepaintedAreas = new ArrayList<>();
|
|
|
|
/**
|
|
* An asynchronous painter. If enabled (not null), then the {@code asyncPainter} will be
|
|
* used when painting this {@code MapViewer}.
|
|
*/
|
|
private MapAsyncPainter asyncPainter = null;
|
|
|
|
|
|
/**
|
|
* The constructor to use.
|
|
*
|
|
* @param freeColClient The {@code FreeColClient} for the game.
|
|
* @param lib An {@code ImageLibrary} to use for drawing to the map
|
|
* (and this is subject to the map scaling).
|
|
* @param al An {@code ActionListener} for the cursor.
|
|
*/
|
|
public MapViewer(FreeColClient freeColClient, ImageLibrary lib, ActionListener al) {
|
|
super(freeColClient);
|
|
|
|
this.lib = lib;
|
|
this.tv = new TileViewer(freeColClient, lib);
|
|
|
|
final ChatDisplay chatDisplay = new ChatDisplay(freeColClient);
|
|
final UnitAnimator unitAnimator = new UnitAnimator(freeColClient, this, lib);
|
|
this.mapViewerState = new MapViewerState(chatDisplay, unitAnimator, al);
|
|
this.mapViewerScaledUtils = new MapViewerScaledUtils();
|
|
this.rpm = new MapViewerRepaintManager();
|
|
|
|
updateScaledVariables();
|
|
}
|
|
|
|
|
|
// Public API
|
|
|
|
/**
|
|
* Change the scale of the map.
|
|
*
|
|
* @param newScale The new map scale.
|
|
*/
|
|
public void changeScale(float newScale) {
|
|
this.lib.changeScaleFactor(newScale);
|
|
this.tv.updateScaledVariables();
|
|
updateScaledVariables();
|
|
mapViewerBounds.positionMap();
|
|
rpm.markAsDirty();
|
|
}
|
|
|
|
/**
|
|
* Update the variables that depend on the image library scale.
|
|
*/
|
|
private void updateScaledVariables() {
|
|
// ATTENTION: we assume that all base tiles have the same size
|
|
this.tileBounds = new TileBounds(lib.getTileSize(), lib.getScaleFactor());
|
|
|
|
mapViewerBounds.updateSizeVariables(tileBounds);
|
|
mapViewerScaledUtils.updateScaledVariables(lib);
|
|
}
|
|
|
|
/**
|
|
* Change the displayed map size.
|
|
*
|
|
* @param size The new map size.
|
|
*/
|
|
public void changeSize(Dimension size) {
|
|
this.tileBounds = new TileBounds(this.lib.getTileSize(), this.lib.getScaleFactor());
|
|
mapViewerBounds.changeSize(size, this.tileBounds);
|
|
rpm.markAsDirty();
|
|
}
|
|
|
|
/**
|
|
* Converts the given screen coordinates to Map coordinates.
|
|
* It checks to see to which Tile the given pixel 'belongs'.
|
|
*
|
|
* @param x The x-coordinate in pixels.
|
|
* @param y The y-coordinate in pixels.
|
|
* @return The {@code Tile} that is located at the given position
|
|
* on the screen.
|
|
*/
|
|
public Tile convertToMapTile(int x, int y) {
|
|
return mapViewerBounds.convertToMapTile(getMap(), x, y);
|
|
}
|
|
|
|
/**
|
|
* This method should only be called using {@link GUI#useMapAsyncPainter()}.
|
|
*/
|
|
public MapAsyncPainter useMapAsyncPainter() {
|
|
assert SwingUtilities.isEventDispatchThread();
|
|
|
|
if (asyncPainter != null && !asyncPainter.isStopped()) {
|
|
return asyncPainter;
|
|
}
|
|
|
|
asyncPainter = new MapAsyncPainter(this);
|
|
|
|
return asyncPainter;
|
|
}
|
|
|
|
/**
|
|
* This method should only be called using {@link GUI#stopMapAsyncPainter()}.
|
|
*/
|
|
public void stopMapAsyncPainter() {
|
|
assert SwingUtilities.isEventDispatchThread();
|
|
|
|
final MapAsyncPainter theAsyncPainter = asyncPainter;
|
|
if (theAsyncPainter != null) {
|
|
theAsyncPainter.stop();
|
|
}
|
|
asyncPainter = null;
|
|
}
|
|
|
|
/**
|
|
* Displays the Map.
|
|
*
|
|
* @param g2d The {@code Graphics2D} object on which to draw the Map.
|
|
* @param size The size of the map.
|
|
* @return {@code true} if the entire map has been repainted.
|
|
*/
|
|
@SuppressFBWarnings(value="NP_LOAD_OF_KNOWN_NULL_VALUE",
|
|
justification="lazy load of extra tiles")
|
|
public boolean displayMap(Graphics2D g2d, Dimension size) {
|
|
final MapAsyncPainter thePainter = asyncPainter;
|
|
if (thePainter != null) {
|
|
final BufferedImage backBufferImage = thePainter.getBackBufferImage();
|
|
if (backBufferImage == null) {
|
|
rpm.markAsDirty();
|
|
return paintMap(g2d, size, mapViewerBounds, true);
|
|
}
|
|
|
|
g2d.setColor(Color.BLACK);
|
|
g2d.drawImage(backBufferImage, 0, 0, null);
|
|
|
|
mapViewerState.getChatDisplay().display(g2d, mapViewerBounds.getSize());
|
|
|
|
return false;
|
|
}
|
|
|
|
if (rpm.isRepaintsBlocked(size)) {
|
|
final VolatileImage backBufferImage = rpm.getBackBufferImage();
|
|
g2d.setColor(Color.black);
|
|
g2d.fillRect(0, 0, size.width, size.height);
|
|
g2d.drawImage(backBufferImage, 0, 0, null);
|
|
|
|
mapViewerState.getChatDisplay().display(g2d, mapViewerBounds.getSize());
|
|
|
|
return false;
|
|
}
|
|
|
|
return paintMap(g2d, size, mapViewerBounds, true);
|
|
}
|
|
|
|
/**
|
|
* Displays the Map.
|
|
*
|
|
* @param g2d The {@code Graphics2D} object on which to draw the Map.
|
|
* @param size The size of the map.
|
|
* @param mapViewerBounds The bounds to be used when drawing the map. This can be
|
|
* a different object than {@link #getMapViewerBounds()} when painting to
|
|
* buffers etc.
|
|
*
|
|
* @return {@code true} if the entire map has been repainted.
|
|
*/
|
|
@SuppressFBWarnings(value="NP_LOAD_OF_KNOWN_NULL_VALUE",
|
|
justification="lazy load of extra tiles")
|
|
public boolean paintMap(Graphics2D g2d, Dimension size, MapViewerBounds mapViewerBounds) {
|
|
return paintMap(g2d, size, mapViewerBounds, false);
|
|
}
|
|
|
|
private boolean paintMap(Graphics2D g2d, Dimension size, MapViewerBounds mapViewerBounds, boolean useBuffers) {
|
|
final long startMs = now();
|
|
|
|
final Rectangle clipBounds = (useBuffers) ? g2d.getClipBounds() : new Rectangle(0, 0, size.width, size.height);
|
|
if (mapViewerBounds.getFocus() == null) {
|
|
if (g2d != null) {
|
|
paintBlackBackground(g2d, clipBounds);
|
|
}
|
|
return false;
|
|
}
|
|
final Rectangle dirtyClipBounds;
|
|
boolean fullMapRenderedWithoutUsingBackBuffer;
|
|
if (useBuffers) {
|
|
fullMapRenderedWithoutUsingBackBuffer = rpm.prepareBuffers(mapViewerBounds, mapViewerBounds.getFocus());
|
|
dirtyClipBounds = rpm.getDirtyClipBounds();
|
|
if (rpm.isAllDirty()) {
|
|
fullMapRenderedWithoutUsingBackBuffer = true;
|
|
}
|
|
} else {
|
|
dirtyClipBounds = clipBounds;
|
|
fullMapRenderedWithoutUsingBackBuffer = true;
|
|
}
|
|
|
|
final VolatileImage backBufferImage;
|
|
final BufferedImage nonAnimationBufferImage;
|
|
final Graphics2D backBufferG2d;
|
|
final Graphics2D nonAnimationG2d;
|
|
if (useBuffers) {
|
|
backBufferImage = rpm.getBackBufferImage();
|
|
nonAnimationBufferImage = rpm.getNonAnimationBufferImage();
|
|
backBufferG2d = backBufferImage.createGraphics();
|
|
nonAnimationG2d = nonAnimationBufferImage.createGraphics();
|
|
} else {
|
|
backBufferImage = null;
|
|
nonAnimationBufferImage = null;
|
|
backBufferG2d = g2d;
|
|
nonAnimationG2d = g2d;
|
|
}
|
|
|
|
applyRenderingHints(g2d);
|
|
applyRenderingHints(backBufferG2d);
|
|
applyRenderingHints(nonAnimationG2d);
|
|
|
|
final AffineTransform backBufferOriginTransform = backBufferG2d.getTransform();
|
|
|
|
final Map map = getMap();
|
|
|
|
Rectangle allRenderingClipBounds;
|
|
if (dirtyClipBounds.isEmpty()) {
|
|
allRenderingClipBounds = clipBounds;
|
|
} else {
|
|
allRenderingClipBounds = clipBounds.union(dirtyClipBounds);
|
|
}
|
|
|
|
if (!getClientOptions().isTerrainAnimationsEnabled()) {
|
|
allRenderingClipBounds = dirtyClipBounds;
|
|
}
|
|
|
|
paintBlackBackground(backBufferG2d, allRenderingClipBounds);
|
|
|
|
// Display the animated base tiles:
|
|
final TileClippingBounds animatedBaseTileTcb = new TileClippingBounds(mapViewerBounds, map, allRenderingClipBounds);
|
|
final long initMs = now();
|
|
if (useBuffers) {
|
|
backBufferG2d.setClip(allRenderingClipBounds);
|
|
}
|
|
backBufferG2d.translate(animatedBaseTileTcb.clipLeftX, animatedBaseTileTcb.clipTopY);
|
|
paintEachTile(backBufferG2d, animatedBaseTileTcb, (tileG2d, tile) -> this.tv.displayAnimatedBaseTiles(tileG2d, tile, false));
|
|
if (!useBuffers) {
|
|
backBufferG2d.translate(-animatedBaseTileTcb.clipLeftX, -animatedBaseTileTcb.clipTopY);
|
|
}
|
|
|
|
// Display everything else:
|
|
final long animatedBaseMs = now();
|
|
if (!dirtyClipBounds.isEmpty()) {
|
|
displayToNonAnimationBufferImage(mapViewerBounds, dirtyClipBounds, nonAnimationG2d, map, useBuffers);
|
|
}
|
|
|
|
if (useBuffers) {
|
|
nonAnimationG2d.dispose();
|
|
}
|
|
|
|
final long nonAnimatedMs = now();
|
|
if (useBuffers) {
|
|
backBufferG2d.setTransform(backBufferOriginTransform);
|
|
backBufferG2d.setClip(allRenderingClipBounds);
|
|
backBufferG2d.drawImage(nonAnimationBufferImage, 0, 0, null);
|
|
backBufferG2d.dispose();
|
|
g2d.drawImage(backBufferImage, 0, 0, null);
|
|
}
|
|
|
|
final long useBuffersMs = now();
|
|
|
|
// Display cursor for selected tile or active unit
|
|
final Tile cursorTile = getVisibleCursorTile(mapViewerBounds);
|
|
if (cursorTile != null && mapViewerState.getCursor().isActive() && !mapViewerState.getUnitAnimator().isUnitsOutForAnimation()) {
|
|
/*
|
|
* The cursor is hidden when units are animated.
|
|
*/
|
|
final Point p = mapViewerBounds.calculateTilePosition(cursorTile, false);
|
|
final String key = mapViewerState.getViewMode() == ViewMode.MOVE_UNITS ? ImageLibrary.UNIT_SELECT : ImageLibrary.TILE_SELECT;
|
|
final BufferedImage image = this.lib.getScaledImage(key);
|
|
g2d.drawImage(image, p.x - (image.getWidth() - tileBounds.getWidth() ) / 2, p.y - (image.getHeight() - tileBounds.getHeight()) / 2, null);
|
|
}
|
|
final long cursorTileMs = now();
|
|
|
|
// Display goto path
|
|
if (mapViewerState.getGotoPath() != null) {
|
|
displayPath(g2d, mapViewerState.getGotoPath(), mapViewerBounds);
|
|
} else if (mapViewerState.getUnitPath() != null) {
|
|
displayPath(g2d, mapViewerState.getUnitPath(), mapViewerBounds);
|
|
}
|
|
final long gotoPathMs = now();
|
|
|
|
if (mapViewerState.isRangedAttackMode()
|
|
&& mapViewerState.getActiveUnit() != null
|
|
&& mapViewerState.getActiveUnit().getTile() != null) {
|
|
final BufferedImage rangedTarget = lib.getRangedTargetCrosshair();
|
|
final Iterable<Tile> possibleTargets = map.getCircleTiles(mapViewerState.getActiveUnit().getTile(),
|
|
true,
|
|
mapViewerState.getActiveUnit().getType().getAttackRange());
|
|
for (Tile t : possibleTargets) {
|
|
if (mapViewerState.getActiveUnit().canAttackRanged(t)) {
|
|
final Point point = mapViewerBounds.calculateTilePosition(t, false);
|
|
if (point == null) {
|
|
continue;
|
|
}
|
|
g2d.drawImage(rangedTarget,
|
|
point.x + (tileBounds.getWidth() - rangedTarget.getWidth()) / 2,
|
|
point.y + (tileBounds.getHeight() - rangedTarget.getHeight()) / 2,
|
|
null);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw the chat
|
|
mapViewerState.getChatDisplay().display(g2d, mapViewerBounds.getSize());
|
|
final long chatMs = now();
|
|
|
|
if (FreeColDebugger.debugRendering()) {
|
|
fullyRepaintedAreas.add(dirtyClipBounds);
|
|
g2d.setColor(new Color(255, 0, 0, 100));
|
|
for (Rectangle r : fullyRepaintedAreas) {
|
|
g2d.fill(r);
|
|
}
|
|
fullyRepaintedAreas.clear();
|
|
}
|
|
|
|
if (useBuffers) {
|
|
verifyAndMarkAsClean(size, clipBounds);
|
|
}
|
|
|
|
/*
|
|
* Remove the check for "fullMapRenderedWithoutUsingBackBuffer" to get every repaint
|
|
* logged: This includes several animations per second.
|
|
*/
|
|
if (fullMapRenderedWithoutUsingBackBuffer && logger.isLoggable(Level.FINEST)) {
|
|
final long endMs = now();
|
|
final long gap = endMs - startMs;
|
|
final StringBuilder sb = new StringBuilder(128);
|
|
sb.append("displayMap fullRendering=").append(fullMapRenderedWithoutUsingBackBuffer)
|
|
.append(" time= ").append(gap)
|
|
.append(" init=").append(initMs - startMs)
|
|
.append(" animated=").append(animatedBaseMs - initMs)
|
|
.append(" displayNonAnimationImages=").append(nonAnimatedMs - animatedBaseMs)
|
|
.append(" buffers=").append(useBuffersMs - nonAnimatedMs)
|
|
.append(" cursorTile=").append(cursorTileMs - useBuffersMs)
|
|
.append(" goto=").append(gotoPathMs - cursorTileMs)
|
|
.append(" chat=").append(chatMs - gotoPathMs)
|
|
.append(" finish=").append(endMs - chatMs)
|
|
;
|
|
logger.finest(sb.toString());
|
|
}
|
|
|
|
return fullMapRenderedWithoutUsingBackBuffer;
|
|
}
|
|
|
|
private void applyRenderingHints(Graphics2D g2d) {
|
|
if (getClientOptions().getRange(ClientOptions.GRAPHICS_QUALITY) == ClientOptions.GRAPHICS_QUALITY_LOWEST) {
|
|
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
|
|
g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED);
|
|
g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED);
|
|
g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
|
|
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_OFF);
|
|
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
|
|
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
|
|
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_DEFAULT);
|
|
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Paints the dirty tiles to the buffers. The screen will
|
|
* be updated the next time {@link #displayMap(Graphics2D, Dimension)}
|
|
* gets called.
|
|
*
|
|
* This is used for handling several small dirty areas that would otherwise
|
|
* force a full repaint since the bounding box covers the entire screen.
|
|
* An example of this is diagonal scrolling.
|
|
*/
|
|
public void paintImmediatelyToBuffersOnly() {
|
|
if (mapViewerBounds.getFocus() == null
|
|
|| rpm.isRepaintsBlocked(mapViewerBounds.getSize())) {
|
|
return;
|
|
}
|
|
|
|
rpm.prepareBuffers(mapViewerBounds, mapViewerBounds.getFocus());
|
|
final Rectangle dirtyClipBounds = rpm.getDirtyClipBounds();
|
|
|
|
if (dirtyClipBounds.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
final BufferedImage nonAnimationBufferImage = rpm.getNonAnimationBufferImage();
|
|
final Map map = getMap();
|
|
final Graphics2D nonAnimationG2d = nonAnimationBufferImage.createGraphics();
|
|
displayToNonAnimationBufferImage(mapViewerBounds, dirtyClipBounds, nonAnimationG2d, map, true);
|
|
nonAnimationG2d.dispose();
|
|
if (FreeColDebugger.debugRendering()) {
|
|
fullyRepaintedAreas.add(dirtyClipBounds);
|
|
}
|
|
rpm.markAsClean();
|
|
}
|
|
|
|
private void displayToNonAnimationBufferImage(MapViewerBounds mapViewerBounds, Rectangle dirtyClipBounds, Graphics2D nonAnimationG2d, Map map, boolean useBuffers) {
|
|
final TileClippingBounds tcb = new TileClippingBounds(mapViewerBounds, map, dirtyClipBounds);
|
|
displayNonAnimationImages(nonAnimationG2d, dirtyClipBounds, tcb, useBuffers);
|
|
}
|
|
|
|
private void displayNonAnimationImages(Graphics2D nonAnimationG2d,
|
|
Rectangle clipBounds,
|
|
TileClippingBounds tcb,
|
|
boolean useBuffers) {
|
|
|
|
long t0 = now();
|
|
final Player player = getMyPlayer(); // Check, can be null in map editor
|
|
final ClientOptions options = getClientOptions();
|
|
|
|
/* For settlement names and territorial borders 1 extra row needs
|
|
to be drawn in north to prevent missing parts on partial redraws,
|
|
as they can reach below their tiles, see BR#2580 */
|
|
|
|
if (useBuffers) {
|
|
nonAnimationG2d.setComposite(AlphaComposite.Clear);
|
|
nonAnimationG2d.fill(clipBounds);
|
|
nonAnimationG2d.setComposite(AlphaComposite.SrcOver);
|
|
nonAnimationG2d.setClip(clipBounds);
|
|
}
|
|
nonAnimationG2d.translate(tcb.clipLeftX, tcb.clipTopY);
|
|
|
|
long t1 = now();
|
|
|
|
paintEachTile(nonAnimationG2d, tcb, (tileG2d, tile) -> this.tv.displayTileWithBeach(tileG2d, tile));
|
|
|
|
nonAnimationG2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
|
|
RenderingHints.VALUE_ANTIALIAS_ON);
|
|
|
|
// Display the borders
|
|
long t2 = now();
|
|
paintEachTile(nonAnimationG2d, tcb, (tileG2d, tile) -> {
|
|
if (getClientOptions().isRiverAnimationEnabled()
|
|
&& (tile.hasRiver() || tv.hasRiverDelta(tile))) {
|
|
return;
|
|
}
|
|
this.tv.drawBaseTileTransitions(tileG2d, tile);
|
|
});
|
|
|
|
// Draw the grid, if needed
|
|
long t3 = now();
|
|
displayGrid(nonAnimationG2d, options, tcb);
|
|
|
|
// Paint full region borders
|
|
long t4 = now();
|
|
if (options.getInteger(ClientOptions.DISPLAY_TILE_TEXT) == ClientOptions.DISPLAY_TILE_TEXT_REGIONS) {
|
|
paintEachTileWithExtendedImageSize(nonAnimationG2d, tcb, (tileG2d, tile) -> displayTerritorialBorders(tileG2d, tile, BorderType.REGION, true));
|
|
}
|
|
|
|
// Paint full country borders
|
|
long t5 = now();
|
|
if (options.getBoolean(ClientOptions.DISPLAY_BORDERS)) {
|
|
paintEachTileWithExtendedImageSize(nonAnimationG2d, tcb, (tileG2d, tile) -> displayTerritorialBorders(tileG2d, tile, BorderType.COUNTRY, true));
|
|
}
|
|
|
|
// Apply fog of war to flat parts of all tiles
|
|
long t6 = now();
|
|
nonAnimationG2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
|
|
RenderingHints.VALUE_ANTIALIAS_OFF);
|
|
final RescaleOp fow;
|
|
if (shouldFogOfWarBeDisplayed(player, options)) {
|
|
// Knowing that we have FOW, prepare a rescaling for the
|
|
// overlay step below.
|
|
fow = new RescaleOp(new float[] { 0.8f, 0.8f, 0.8f, 1f },
|
|
new float[] { 0, 0, 0, 0 },
|
|
null);
|
|
|
|
final Composite oldComposite = nonAnimationG2d.getComposite();
|
|
nonAnimationG2d.setColor(Color.BLACK);
|
|
nonAnimationG2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
|
|
0.2f));
|
|
|
|
paintEachTile(nonAnimationG2d, tcb, (tileG2d, tile) -> {
|
|
if (!tile.isExplored() || player.canSee(tile)) {
|
|
return;
|
|
}
|
|
tileG2d.fill(mapViewerScaledUtils.getFog());
|
|
});
|
|
nonAnimationG2d.setComposite(oldComposite);
|
|
} else {
|
|
fow = null;
|
|
}
|
|
|
|
// Display unknown tile borders:
|
|
long t7 = now();
|
|
paintEachTile(nonAnimationG2d, tcb, (tileG2d, tile) -> this.tv.displayUnknownTileBorder(tileG2d, tile));
|
|
|
|
// Display the Tile overlays
|
|
long t8 = now();
|
|
final int colonyLabels = options.getInteger(ClientOptions.DISPLAY_COLONY_LABELS);
|
|
boolean withNumbers = (colonyLabels == ClientOptions.COLONY_LABELS_CLASSIC);
|
|
paintEachTileWithExtendedImageSize(nonAnimationG2d, tcb, (tileG2d, tile) -> {
|
|
if (!tile.isExplored()) {
|
|
return;
|
|
}
|
|
BufferedImage overlayImage = this.lib.getScaledOverlayImage(tile);
|
|
RescaleOp rop = (player == null || player.canSee(tile)) ? null : fow;
|
|
this.tv.displayTileItems(tileG2d, tile, rop, overlayImage);
|
|
this.tv.displaySettlementWithChipsOrPopulationNumber(tileG2d, tile,
|
|
withNumbers, rop);
|
|
this.tv.displayOptionalTileText(tileG2d, tile);
|
|
});
|
|
|
|
// Paint transparent region borders
|
|
long t9 = now();
|
|
nonAnimationG2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
|
|
RenderingHints.VALUE_ANTIALIAS_ON);
|
|
if (options.getInteger(ClientOptions.DISPLAY_TILE_TEXT) == ClientOptions.DISPLAY_TILE_TEXT_REGIONS) {
|
|
paintEachTileWithExtendedImageSize(nonAnimationG2d, tcb, (tileG2d, tile) -> displayTerritorialBorders(tileG2d, tile, BorderType.REGION, false));
|
|
}
|
|
|
|
// Paint transparent country borders
|
|
long t10 = now();
|
|
if (options.getBoolean(ClientOptions.DISPLAY_BORDERS)) {
|
|
paintEachTileWithExtendedImageSize(nonAnimationG2d, tcb, (tileG2d, tile) -> displayTerritorialBorders(tileG2d, tile, BorderType.COUNTRY, false));
|
|
}
|
|
|
|
// Display units
|
|
long t12 = now();
|
|
nonAnimationG2d.setColor(Color.BLACK);
|
|
final boolean revengeMode = getGame().isInRevengeMode();
|
|
if (!revengeMode) {
|
|
paintEachTile(nonAnimationG2d, tcb.getTopLeftDirtyTile(), tcb.getUnitTiles(), (tileG2d, tile) -> {
|
|
final Unit unit = mapViewerState.findUnitInFront(tile);
|
|
if (unit == null || mapViewerState.getUnitAnimator().isOutForAnimation(unit)) {
|
|
return;
|
|
}
|
|
displayUnit(tileG2d, unit);
|
|
});
|
|
} else {
|
|
final BufferedImage darkness = this.lib.getScaledImage(ImageLibrary.DARKNESS);
|
|
paintEachTileWithSuperExtendedImageSize(nonAnimationG2d, tcb, (tileG2d, tile) -> {
|
|
final Unit unit = mapViewerState.findUnitInFront(tile);
|
|
if (unit == null || mapViewerState.getUnitAnimator().isOutForAnimation(unit)) {
|
|
return;
|
|
}
|
|
|
|
if (unit.isUndead()) {
|
|
this.tv.displayCenteredImage(tileG2d, darkness);
|
|
}
|
|
displayUnit(tileG2d, unit);
|
|
|
|
});
|
|
}
|
|
|
|
long t13 = now();
|
|
paintEachTileWithExtendedImageSize(nonAnimationG2d, tcb, (tileG2d, tile) -> {
|
|
if (!tile.isExplored()) {
|
|
return;
|
|
}
|
|
BufferedImage overlayImage = this.lib.getScaledAboveTileImage(tile);
|
|
if (overlayImage != null) {
|
|
tileG2d.drawImage(overlayImage,
|
|
(tileBounds.getWidth() - overlayImage.getWidth()) / 2,
|
|
(tileBounds.getHeight() - overlayImage.getHeight()) / 2,
|
|
null);
|
|
}
|
|
});
|
|
|
|
displayDebugAiDefensiveMap(nonAnimationG2d, tcb);
|
|
|
|
if (getFreeColClient().isMapEditor()) {
|
|
displayAreasInMapEditor(nonAnimationG2d, tcb);
|
|
}
|
|
|
|
// Display the colony names, if needed
|
|
long t14 = now();
|
|
if (colonyLabels != ClientOptions.COLONY_LABELS_NONE) {
|
|
paintEachTileWithExtendedImageSize(nonAnimationG2d, tcb, (tileG2d, tile) -> {
|
|
final Settlement settlement = tile.getSettlement();
|
|
if (settlement == null) {
|
|
return;
|
|
}
|
|
final RescaleOp rop = (player == null || player.canSee(tile)) ? null : fow;
|
|
displaySettlementLabels(tileG2d, settlement, player, colonyLabels, rop);
|
|
});
|
|
}
|
|
|
|
long t15 = now();
|
|
|
|
if (!useBuffers) {
|
|
nonAnimationG2d.translate(-tcb.clipLeftX, -tcb.clipTopY);
|
|
}
|
|
|
|
if (logger.isLoggable(Level.FINEST)) {
|
|
final long gap = now() - t0;
|
|
final Map.Position bottomRight = tcb.getBottomRightDirtyTile();
|
|
final Map.Position topLeft = tcb.getTopLeftDirtyTile();
|
|
final double avg = ((double)gap)
|
|
/ ((bottomRight.getX() - topLeft.getX())
|
|
* (bottomRight.getY() - topLeft.getY()));
|
|
final StringBuilder sb = new StringBuilder(128);
|
|
sb.append("displayNonAnimationImages time = ").append(gap)
|
|
.append(" for ").append(tcb.getTopLeftDirtyTile())
|
|
.append(" to ").append(tcb.getBottomRightDirtyTile())
|
|
.append(" average ").append(avg)
|
|
.append(" t1=").append(t1 - t0)
|
|
.append(" t2=").append(t2 - t1)
|
|
.append(" t3=").append(t3 - t2)
|
|
.append(" t4=").append(t4 - t3)
|
|
.append(" t5=").append(t5 - t4)
|
|
.append(" t6=").append(t6 - t5)
|
|
.append(" t7=").append(t7 - t6)
|
|
.append(" t8=").append(t8 - t7)
|
|
.append(" t9=").append(t9 - t8)
|
|
.append(" t10=").append(t10 - t9)
|
|
.append(" t12=").append(t12 - t10)
|
|
.append(" t13=").append(t13 - t12)
|
|
.append(" t14=").append(t14 - t13)
|
|
.append(" t15=").append(t15 - t14)
|
|
;
|
|
logger.finest(sb.toString());
|
|
}
|
|
}
|
|
|
|
|
|
private void displayAreasInMapEditor(Graphics2D nonAnimationG2d, TileClippingBounds tcb) {
|
|
final Object oldAntialiasingHint = nonAnimationG2d.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
|
|
nonAnimationG2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
|
|
|
|
final Composite oldComposite = nonAnimationG2d.getComposite();
|
|
nonAnimationG2d.setColor(Color.BLACK);
|
|
nonAnimationG2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f));
|
|
|
|
final Color oldColor = nonAnimationG2d.getColor();
|
|
|
|
final GeneralPath baseTileOutline = mapViewerScaledUtils.getBaseTileOutline();
|
|
paintEachTile(nonAnimationG2d, tcb, (tileG2d, tile) -> {
|
|
// This can easily be optimized if slow on some systems.
|
|
final List<Area> areas = getGame().getAreas().stream()
|
|
.filter(a -> a.containsTile(tile))
|
|
.collect(Collectors.toList());
|
|
if (areas.isEmpty()) {
|
|
return;
|
|
}
|
|
if (areas.size() == 1) {
|
|
final Area area = areas.get(0);
|
|
tileG2d.setColor(area.getColor());
|
|
tileG2d.fill(baseTileOutline);
|
|
return;
|
|
}
|
|
|
|
final Shape oldClip = tileG2d.getClip();
|
|
final Stroke oldStroke = tileG2d.getStroke();
|
|
tileG2d.setClip(baseTileOutline);
|
|
|
|
final int stepSize = lib.scaleInt(4);
|
|
tileG2d.setStroke(new BasicStroke(stepSize));
|
|
|
|
int step = 0;
|
|
int index = 0;
|
|
while (step * stepSize < tileBounds.getHeight()) {
|
|
final Area area = areas.get(index);
|
|
tileG2d.setColor(area.getColor());
|
|
tileG2d.drawLine(0, step*stepSize, tileBounds.getWidth(), step*stepSize);
|
|
|
|
step++;
|
|
index++;
|
|
if (index == areas.size()) {
|
|
index = 0;
|
|
}
|
|
}
|
|
|
|
tileG2d.setClip(oldClip);
|
|
tileG2d.setStroke(oldStroke);
|
|
});
|
|
nonAnimationG2d.setColor(oldColor);
|
|
nonAnimationG2d.setComposite(oldComposite);
|
|
nonAnimationG2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldAntialiasingHint);
|
|
}
|
|
|
|
private void displayDebugAiDefensiveMap(Graphics2D nonAnimationG2d, TileClippingBounds tcb) {
|
|
if (FreeColDebugger.debugShowDefenceMapForPlayer() != null
|
|
&& getFreeColServer() != null
|
|
&& getFreeColServer().getAIMain() != null) {
|
|
final AIObject aiObject = getFreeColServer().getAIMain().getAIObject(FreeColDebugger.debugShowDefenceMapForPlayer().getId());
|
|
if (aiObject != null && aiObject instanceof EuropeanAIPlayer) {
|
|
final EuropeanAIPlayer eap = (EuropeanAIPlayer) aiObject;
|
|
final DefensiveMap defensiveMap = DefensiveMap.createDefensiveMap(eap);
|
|
paintEachTile(nonAnimationG2d, tcb.getTopLeftDirtyTile(), tcb.getBaseTiles(), (tileG2d, tile) -> {
|
|
final DefensiveZone defensiveZone = defensiveMap.getDefensiveZone(tile);
|
|
if (defensiveZone == null) {
|
|
return;
|
|
}
|
|
if (defensiveZone.getNumberOfMilitaryEnemies() > 0) {
|
|
tileG2d.setColor(new Color(255, 0, 0, 150));
|
|
} else if (defensiveZone.isEnemiesInNeighbour()) {
|
|
tileG2d.setColor(new Color(255, 100, 100, 150));
|
|
} else if (defensiveZone.isExposed()) {
|
|
tileG2d.setColor(new Color(255, 255, 0, 150));
|
|
} else {
|
|
tileG2d.setColor(new Color(0, 255, 0, 150));
|
|
}
|
|
tileG2d.fill(mapViewerScaledUtils.getFog());
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
private boolean shouldFogOfWarBeDisplayed(final Player player, final ClientOptions options) {
|
|
return player != null && getSpecification().getBoolean(GameOptions.FOG_OF_WAR) && options.getBoolean(ClientOptions.DISPLAY_FOG_OF_WAR);
|
|
}
|
|
|
|
private void displayGrid(Graphics2D g2d, final ClientOptions options, final TileClippingBounds tcb) {
|
|
if (options.getBoolean(ClientOptions.DISPLAY_GRID)) {
|
|
// Generate a zigzag GeneralPath
|
|
final AffineTransform baseTransform = g2d.getTransform();
|
|
GeneralPath gridPath = new GeneralPath();
|
|
gridPath.moveTo(0, 0);
|
|
int nextX = tileBounds.getHalfWidth();
|
|
int nextY = -tileBounds.getHalfHeight();
|
|
for (int i = 0; i <= ((tcb.getBottomRightDirtyTile().getX() - tcb.getTopLeftDirtyTile().getX()) * 2 + 1); i++) {
|
|
gridPath.lineTo(nextX, nextY);
|
|
nextX += tileBounds.getHalfWidth();
|
|
nextY = (nextY == 0) ? -tileBounds.getHalfHeight() : 0;
|
|
}
|
|
|
|
// Display the grid
|
|
g2d.setStroke(mapViewerScaledUtils.getGridStroke());
|
|
g2d.setColor(Color.BLACK);
|
|
for (int row = tcb.getTopLeftDirtyTile().getY(); row <= tcb.getBottomRightDirtyTile().getY(); row++) {
|
|
g2d.translate(0, tileBounds.getHalfHeight());
|
|
AffineTransform rowTransform = g2d.getTransform();
|
|
if ((row & 1) == 1) {
|
|
g2d.translate(tileBounds.getHalfWidth(), 0);
|
|
}
|
|
g2d.draw(gridPath);
|
|
g2d.setTransform(rowTransform);
|
|
}
|
|
g2d.setTransform(baseTransform);
|
|
}
|
|
}
|
|
|
|
private void paintBlackBackground(Graphics2D g2d, final Rectangle rectangle) {
|
|
g2d.setColor(Color.black);
|
|
g2d.fillRect(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
|
|
}
|
|
|
|
private void displaySettlementLabels(Graphics2D g2d, Settlement settlement,
|
|
Player player, int colonyLabels,
|
|
RescaleOp rop) {
|
|
if (settlement.isDisposed()) {
|
|
logger.warning("Settlement display race detected: "
|
|
+ settlement.getName());
|
|
return;
|
|
}
|
|
String name = Messages.message(settlement.getLocationLabelFor(player));
|
|
if (name == null) return;
|
|
|
|
Color backgroundColor = settlement.getOwner().getNationColor();
|
|
if (backgroundColor == null) backgroundColor = Color.WHITE;
|
|
// int yOffset = this.lib.getSettlementImage(settlement).getHeight() + 1;
|
|
int yOffset = tileBounds.getHeight();
|
|
switch (colonyLabels) {
|
|
case ClientOptions.COLONY_LABELS_CLASSIC:
|
|
BufferedImage img = this.lib.getStringImage(g2d, name, backgroundColor,
|
|
mapViewerScaledUtils.getFontNormal());
|
|
g2d.drawImage(img, rop, (tileBounds.getWidth() - img.getWidth())/2 + 1,
|
|
yOffset);
|
|
break;
|
|
|
|
case ClientOptions.COLONY_LABELS_MODERN:
|
|
default:
|
|
backgroundColor = new Color(backgroundColor.getRed(),
|
|
backgroundColor.getGreen(),
|
|
backgroundColor.getBlue(), 128);
|
|
TextSpecification[] specs = new TextSpecification[1];
|
|
if (settlement instanceof Colony
|
|
&& settlement.getOwner() == player) {
|
|
Colony colony = (Colony) settlement;
|
|
BuildableType buildable = colony.getCurrentlyBuilding();
|
|
if (buildable != null && mapViewerScaledUtils.getFontProduction() != null) {
|
|
specs = new TextSpecification[2];
|
|
String t = Messages.getName(buildable) + " " +
|
|
Turn.getTurnsText(colony.getTurnsToComplete(buildable));
|
|
specs[1] = new TextSpecification(t, mapViewerScaledUtils.getFontProduction());
|
|
}
|
|
}
|
|
specs[0] = new TextSpecification(name, mapViewerScaledUtils.getFontNormal());
|
|
|
|
BufferedImage nameImage = createLabel(g2d, specs, backgroundColor);
|
|
int spacing = this.lib.scaleInt(3);
|
|
BufferedImage leftImage = null;
|
|
BufferedImage rightImage = null;
|
|
if (settlement instanceof Colony) {
|
|
Colony colony = (Colony)settlement;
|
|
String string = Integer.toString(colony.getApparentUnitCount());
|
|
leftImage = createLabel(g2d, string,
|
|
((colony.getPreferredSizeChange() > 0)
|
|
? mapViewerScaledUtils.getFontItalic() : mapViewerScaledUtils.getFontNormal()),
|
|
backgroundColor);
|
|
if (player.owns(settlement)) {
|
|
int bonusProduction = colony.getProductionBonus();
|
|
if (bonusProduction != 0) {
|
|
String bonus = (bonusProduction > 0)
|
|
? "+" + bonusProduction
|
|
: Integer.toString(bonusProduction);
|
|
rightImage = createLabel(g2d, bonus, mapViewerScaledUtils.getFontNormal(),
|
|
backgroundColor);
|
|
}
|
|
}
|
|
} else if (settlement instanceof IndianSettlement) {
|
|
IndianSettlement is = (IndianSettlement) settlement;
|
|
if (is.getType().isCapital()) {
|
|
leftImage = createCapitalLabel(nameImage.getHeight(),
|
|
5, backgroundColor);
|
|
}
|
|
|
|
Unit missionary = is.getMissionary();
|
|
if (missionary != null) {
|
|
boolean expert = missionary.hasAbility(Ability.EXPERT_MISSIONARY);
|
|
backgroundColor = missionary.getOwner().getNationColor();
|
|
backgroundColor = new Color(backgroundColor.getRed(),
|
|
backgroundColor.getGreen(),
|
|
backgroundColor.getBlue(), 128);
|
|
rightImage = createReligiousMissionLabel(nameImage.getHeight(), 5,
|
|
backgroundColor, expert);
|
|
}
|
|
}
|
|
|
|
int xOffset = tileBounds.getWidth() / 2 - nameImage.getWidth() / 2
|
|
- ((leftImage == null) ? 0 : leftImage.getWidth() + spacing);
|
|
yOffset -= nameImage.getHeight() / 2;
|
|
if (leftImage != null) {
|
|
g2d.drawImage(leftImage, rop, xOffset, yOffset);
|
|
xOffset += leftImage.getWidth() + spacing;
|
|
}
|
|
g2d.drawImage(nameImage, rop, xOffset, yOffset);
|
|
if (rightImage != null) {
|
|
xOffset += nameImage.getWidth() + spacing;
|
|
g2d.drawImage(rightImage, rop, xOffset, yOffset);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draws the pentagram indicating a native capital.
|
|
*
|
|
* @param extent The nominal height of the image.
|
|
* @param padding Padding to add around the image.
|
|
* @param backgroundColor The image background color.
|
|
* @return A suitable {@code BufferedImage}.
|
|
*/
|
|
private static BufferedImage createCapitalLabel(int extent, int padding,
|
|
Color backgroundColor) {
|
|
// create path
|
|
double deg2rad = Math.PI/180.0;
|
|
double angle = -90.0 * deg2rad;
|
|
double offset = extent * 0.5;
|
|
double size1 = (extent - padding - padding) * 0.5;
|
|
|
|
GeneralPath path = new GeneralPath();
|
|
path.moveTo(Math.cos(angle) * size1 + offset, Math.sin(angle) * size1 + offset);
|
|
for (int i = 0; i < 4; i++) {
|
|
angle += 144 * deg2rad;
|
|
path.lineTo(Math.cos(angle) * size1 + offset, Math.sin(angle) * size1 + offset);
|
|
}
|
|
path.closePath();
|
|
|
|
// draw everything
|
|
BufferedImage bi = ImageUtils.createBufferedImage(extent, extent);
|
|
Graphics2D g2d = bi.createGraphics();
|
|
g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
|
|
RenderingHints.VALUE_RENDER_QUALITY);
|
|
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
|
|
RenderingHints.VALUE_ANTIALIAS_ON);
|
|
g2d.setColor(backgroundColor);
|
|
g2d.fill(new RoundRectangle2D.Float(0, 0, extent, extent, padding, padding));
|
|
g2d.setColor(Color.BLACK);
|
|
g2d.setStroke(new BasicStroke(2.4f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
|
g2d.draw(path);
|
|
g2d.setColor(Color.WHITE);
|
|
g2d.fill(path);
|
|
g2d.dispose();
|
|
return bi;
|
|
}
|
|
|
|
/**
|
|
* Creates an BufferedImage that shows the given text centred on a
|
|
* translucent rounded rectangle with the given color.
|
|
*
|
|
* @param g2d a {@code Graphics2D}
|
|
* @param text a {@code String}
|
|
* @param font a {@code Font}
|
|
* @param backgroundColor a {@code Color}
|
|
* @return an {@code BufferedImage}
|
|
*/
|
|
private static BufferedImage createLabel(Graphics2D g2d, String text,
|
|
Font font, Color backgroundColor) {
|
|
TextSpecification[] specs = new TextSpecification[1];
|
|
specs[0] = new TextSpecification(text, font);
|
|
return createLabel(g2d, specs, backgroundColor);
|
|
}
|
|
|
|
/**
|
|
* Creates an BufferedImage that shows the given text centred on a
|
|
* translucent rounded rectangle with the given color.
|
|
*
|
|
* @param g2d a {@code Graphics2D}
|
|
* @param textSpecs a {@code TextSpecification} array
|
|
* @param backgroundColor a {@code Color}
|
|
* @return a {@code BufferedImage}
|
|
*/
|
|
private static BufferedImage createLabel(Graphics2D g2d,
|
|
TextSpecification[] textSpecs,
|
|
Color backgroundColor) {
|
|
int hPadding = 15;
|
|
int vPadding = 10;
|
|
int linePadding = 5;
|
|
int width = 0;
|
|
int height = vPadding;
|
|
int i;
|
|
|
|
TextSpecification spec;
|
|
TextLayout[] labels = new TextLayout[textSpecs.length];
|
|
TextLayout label;
|
|
|
|
for (i = 0; i < textSpecs.length; i++) {
|
|
spec = textSpecs[i];
|
|
label = new TextLayout(spec.text, spec.font,
|
|
g2d.getFontRenderContext());
|
|
labels[i] = label;
|
|
Rectangle textRectangle = label.getPixelBounds(null, 0, 0);
|
|
width = Math.max(width, textRectangle.width + hPadding);
|
|
if (i > 0) height += linePadding;
|
|
height += (int) (label.getAscent() + label.getDescent());
|
|
}
|
|
|
|
int radius = Math.min(hPadding, vPadding);
|
|
|
|
BufferedImage bi = ImageUtils.createBufferedImage(width, height);
|
|
Graphics2D g2 = bi.createGraphics();
|
|
g2.setRenderingHint(RenderingHints.KEY_RENDERING,
|
|
RenderingHints.VALUE_RENDER_QUALITY);
|
|
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
|
|
RenderingHints.VALUE_ANTIALIAS_ON);
|
|
g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
|
|
RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
|
g2.setColor(backgroundColor);
|
|
g2.fill(new RoundRectangle2D.Float(0, 0, width, height, radius, radius));
|
|
g2.setColor(ImageLibrary.makeForegroundColor(backgroundColor));
|
|
float y = vPadding / 2.0f;
|
|
for (i = 0; i < labels.length; i++) {
|
|
Rectangle textRectangle = labels[i].getPixelBounds(null, 0, 0);
|
|
float x = (width - textRectangle.width) / 2.0f;
|
|
y += labels[i].getAscent();
|
|
labels[i].draw(g2, x, y);
|
|
y += labels[i].getDescent() + linePadding;
|
|
}
|
|
g2.dispose();
|
|
return bi;
|
|
}
|
|
|
|
/**
|
|
* Draws a cross indicating a religious mission is present in the
|
|
* native village.
|
|
*
|
|
* @param extent The nominal height of the image.
|
|
* @param padding Padding to add around the image.
|
|
* @param backgroundColor The image background color.
|
|
* @param expertMissionary True if the label should show expertise.
|
|
* @return A suitable {@code BufferedImage}.
|
|
*/
|
|
private static BufferedImage createReligiousMissionLabel(int extent,
|
|
int padding, Color backgroundColor, boolean expertMissionary) {
|
|
// create path
|
|
double offset = extent * 0.5;
|
|
double size1 = extent - padding - padding;
|
|
double bar = size1 / 3.0;
|
|
double inset = 0.0;
|
|
double kludge = 0.0;
|
|
|
|
GeneralPath circle = new GeneralPath();
|
|
GeneralPath cross = new GeneralPath();
|
|
if (expertMissionary) {
|
|
// this is meant to represent the eucharist (the -1, +1 thing is a nasty kludge)
|
|
circle.append(new Ellipse2D.Double(padding-1, padding-1, size1+1, size1+1), false);
|
|
inset = 4.0;
|
|
bar = (size1 - inset - inset) / 3.0;
|
|
// more nasty -1, +1 kludges
|
|
kludge = 1.0;
|
|
}
|
|
offset -= 1.0;
|
|
cross.moveTo(offset, padding + inset - kludge);
|
|
cross.lineTo(offset, extent - padding - inset);
|
|
cross.moveTo(offset - bar, padding + bar + inset);
|
|
cross.lineTo(offset + bar + 1, padding + bar + inset);
|
|
|
|
// draw everything
|
|
BufferedImage bi = ImageUtils.createBufferedImage(extent, extent);
|
|
Graphics2D g2d = bi.createGraphics();
|
|
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
|
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
|
g2d.setColor(backgroundColor);
|
|
g2d.fill(new RoundRectangle2D.Float(0, 0, extent, extent, padding, padding));
|
|
g2d.setColor(ImageLibrary.makeForegroundColor(backgroundColor));
|
|
if (expertMissionary) {
|
|
g2d.setStroke(new BasicStroke(2.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
|
g2d.draw(circle);
|
|
g2d.setStroke(new BasicStroke(1.6f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
|
} else {
|
|
g2d.setStroke(new BasicStroke(2.4f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
|
}
|
|
g2d.draw(cross);
|
|
g2d.dispose();
|
|
return bi;
|
|
}
|
|
|
|
/**
|
|
* Display a path.
|
|
*
|
|
* @param g2d The {@code Graphics2D} to display on.
|
|
* @param path The {@code PathNode} to display.
|
|
*/
|
|
private void displayPath(Graphics2D g2d, PathNode path, MapViewerBounds mapViewerBounds) {
|
|
if (path == null || path.next == null) {
|
|
return;
|
|
}
|
|
|
|
final Stroke defaultStroke = g2d.getStroke();
|
|
final Color oldColor = g2d.getColor();
|
|
final Font oldFont = g2d.getFont();
|
|
g2d.setStroke(new BasicStroke(lib.scaleInt(5), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
|
g2d.setFont(mapViewerScaledUtils.getFontNormal());
|
|
|
|
Point previousPoint = null;
|
|
for (PathNode p = path; p != null; p = p.next) {
|
|
final Tile newTile = p.getTile();
|
|
if (newTile == null) {
|
|
previousPoint = null;
|
|
continue;
|
|
}
|
|
|
|
final Point nextPoint = mapViewerBounds.calculateCenterTilePosition(newTile);
|
|
if (nextPoint == null) {
|
|
previousPoint = null;
|
|
continue;
|
|
}
|
|
|
|
final Color pathLineColor = (p.getTurns() > 0) ? new Color(255, 255, 0, 128) : new Color(0, 255, 0, 128);
|
|
g2d.setColor(pathLineColor);
|
|
|
|
if (previousPoint != null) {
|
|
g2d.drawLine(previousPoint.x, previousPoint.y, nextPoint.x, nextPoint.y);
|
|
}
|
|
previousPoint = nextPoint;
|
|
|
|
}
|
|
|
|
g2d.setStroke(new BasicStroke(lib.scaleInt(2)));
|
|
final FontMetrics fm = g2d.getFontMetrics();
|
|
final int fontCenterY = (fm.getAscent() - fm.getDescent() - fm.getLeading()) / 2;
|
|
for (PathNode p = path; p != null; p = p.next) {
|
|
final Tile newTile = p.getTile();
|
|
if (newTile == null) {
|
|
continue;
|
|
}
|
|
final Point newP = mapViewerBounds.calculateCenterTilePosition(newTile);
|
|
if (newP == null) {
|
|
continue;
|
|
}
|
|
|
|
final int r = lib.scaleInt(20);
|
|
final Color pathBgColor = (p.getTurns() > 0) ? new Color(255, 255, 0, 255) : new Color(0, 255, 0, 255);
|
|
g2d.setColor(pathBgColor);
|
|
g2d.fillOval(newP.x - r/2, newP.y - r/2, r, r);
|
|
g2d.setColor(new Color(0, 0, 0, 128));
|
|
g2d.drawOval(newP.x - r/2, newP.y - r/2, r, r);
|
|
|
|
if (mapViewerScaledUtils.getFontTiny() != null && p.getTurns() > 0) {
|
|
final String text = Integer.toString(p.getTurns());
|
|
final Rectangle2D bounds = fm.getStringBounds(text, g2d);
|
|
g2d.setColor(new Color(0, 0, 0));
|
|
g2d.drawString(text, newP.x - (int) bounds.getWidth() / 2, newP.y + fontCenterY);
|
|
}
|
|
}
|
|
g2d.setStroke(defaultStroke);
|
|
g2d.setColor(oldColor);
|
|
g2d.setFont(oldFont);
|
|
}
|
|
|
|
/**
|
|
* Displays the given Unit onto the given Graphics2D object at the
|
|
* location specified by the coordinates.
|
|
*
|
|
* @param g2d The Graphics2D object on which to draw the Unit.
|
|
* @param unit The Unit to draw.
|
|
*/
|
|
void displayUnit(Graphics2D g2d, Unit unit) {
|
|
final Player player = getMyPlayer();
|
|
|
|
// Draw the unit.
|
|
// If unit is sentry, draw in grayscale
|
|
boolean fade = (unit.getState() == Unit.UnitState.SENTRY)
|
|
|| (unit.hasTile()
|
|
&& player != null && !player.canSee(unit.getTile()));
|
|
BufferedImage image = this.lib.getScaledUnitImage(unit, fade);
|
|
Point p = calculateUnitImagePositionInTile(image);
|
|
g2d.drawImage(image, p.x, p.y, null);
|
|
|
|
// Draw an occupation and nation indicator.
|
|
String text = Messages.message(unit.getOccupationLabel(player, false));
|
|
g2d.drawImage(this.lib.getOccupationIndicatorChip(g2d, unit, text),
|
|
this.lib.scaleInt(TileViewer.STATE_OFFSET_X), 0, null);
|
|
|
|
// Draw one small line for each additional unit (like in civ3).
|
|
int unitsOnTile = 0;
|
|
if (unit.hasTile()) {
|
|
// When a unit is moving from tile to tile, it is
|
|
// removed from the source tile. So the unit stack
|
|
// indicator cannot be drawn during the movement see
|
|
// UnitMoveAnimation.animate() for details
|
|
unitsOnTile = unit.getTile().getTotalUnitCount();
|
|
}
|
|
if (unitsOnTile > 1) {
|
|
g2d.setColor(Color.WHITE);
|
|
int unitLinesY = TileBounds.OTHER_UNITS_OFFSET_Y;
|
|
int x1 = this.lib.scaleInt(TileViewer.STATE_OFFSET_X
|
|
+ TileBounds.OTHER_UNITS_OFFSET_X);
|
|
int x2 = this.lib.scaleInt(TileViewer.STATE_OFFSET_X
|
|
+ TileBounds.OTHER_UNITS_OFFSET_X + TileBounds.OTHER_UNITS_WIDTH);
|
|
for (int i = 0; i < unitsOnTile && i < TileBounds.MAX_OTHER_UNITS; i++) {
|
|
g2d.drawLine(x1, unitLinesY, x2, unitLinesY);
|
|
unitLinesY += 2;
|
|
}
|
|
}
|
|
|
|
if (unit.getHitPoints() >= 0 && unit.getHitPoints() < unit.getMaximumHitPoints()) {
|
|
final int offsetX = lib.scaleInt(8);
|
|
final int hitpointsBarWidth = lib.scaleInt(5);
|
|
final int hitpointsBarMargin = lib.scaleInt(5);
|
|
final int fullHeight = tileBounds.getHeight() - 2 * hitpointsBarMargin;
|
|
final int filledHeight = (int) (fullHeight * (((float) unit.getHitPoints()) / unit.getMaximumHitPoints()));
|
|
g2d.setColor(new Color(0, 255, 0, 255));
|
|
g2d.fillRect(hitpointsBarMargin + hitpointsBarWidth + offsetX,
|
|
hitpointsBarMargin - this.lib.scaleInt(TileBounds.UNIT_OFFSET)
|
|
+ fullHeight - filledHeight,
|
|
hitpointsBarWidth,
|
|
filledHeight);
|
|
final Stroke defaultStroke = g2d.getStroke();
|
|
g2d.setStroke(new BasicStroke(lib.scaleInt(1)));
|
|
g2d.setColor(Color.BLACK);
|
|
g2d.drawRect(hitpointsBarMargin + hitpointsBarWidth + offsetX,
|
|
hitpointsBarMargin - this.lib.scaleInt(TileBounds.UNIT_OFFSET),
|
|
hitpointsBarWidth,
|
|
fullHeight);
|
|
g2d.setStroke(defaultStroke);
|
|
}
|
|
|
|
// FOR DEBUGGING
|
|
net.sf.freecol.server.ai.AIUnit au;
|
|
if (FreeColDebugger.isInDebugMode(FreeColDebugger.DebugMode.MENUS)
|
|
&& player != null && !player.owns(unit)
|
|
&& unit.getOwner().isAI()
|
|
&& getFreeColServer() != null
|
|
&& getFreeColServer().getAIMain() != null
|
|
&& (au = getFreeColServer().getAIMain().getAIUnit(unit)) != null) {
|
|
if (FreeColDebugger.debugShowMission()) {
|
|
String missionString = (!au.hasMission())
|
|
? "No mission"
|
|
: au.getMission().getClass().getSimpleName().replaceAll("Mission$", "");
|
|
final Font origFont = g2d.getFont();
|
|
if (FreeColDebugger.debugShowMissionInfo() && au.hasMission()) {
|
|
missionString += "\n" + au.getMission().toStringForDebugExtraMissionInfo();
|
|
g2d.setFont(origFont.deriveFont(origFont.getSize2D() * 2 / 3));
|
|
}
|
|
drawCenteredMultilineDebugText(g2d, missionString, 0, 0);
|
|
g2d.setFont(origFont);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void drawCenteredMultilineDebugText(Graphics2D g2d, String text, int x, int y) {
|
|
final FontMetrics fm = g2d.getFontMetrics();
|
|
final String[] lines = text.split("\n");
|
|
final Rectangle2D[] lineTextBoundingBoxes = new Rectangle2D[lines.length];
|
|
|
|
Dimension allTextBoundingBox = new Dimension(0, 0);
|
|
int backgroundOffsetY = 0;
|
|
for (int i=0; i<lines.length; i++) {
|
|
Rectangle2D lineTextBoundingBox = fm.getStringBounds(lines[i], g2d);
|
|
lineTextBoundingBoxes[i] = lineTextBoundingBox;
|
|
allTextBoundingBox = new Dimension((int) Math.max(allTextBoundingBox.width, lineTextBoundingBox.getWidth()),
|
|
(int) (allTextBoundingBox.height + lineTextBoundingBox.getHeight()));
|
|
if ((int) lineTextBoundingBox.getY() < backgroundOffsetY) {
|
|
backgroundOffsetY = (int) lineTextBoundingBox.getY();
|
|
}
|
|
}
|
|
|
|
final int tileCenterX = (tileBounds.getWidth() - allTextBoundingBox.width) / 2;
|
|
|
|
g2d.setColor(new Color(0, 0, 0, 128));
|
|
g2d.fillRect(x + tileCenterX, y + backgroundOffsetY, allTextBoundingBox.width, allTextBoundingBox.height);
|
|
|
|
int offsetY = 0;
|
|
for (int i=0; i<lines.length; i++) {
|
|
g2d.setColor(Color.WHITE);
|
|
final int centerX = (int) (allTextBoundingBox.getWidth() - lineTextBoundingBoxes[i].getWidth()) / 2;
|
|
g2d.drawString(lines[i], x + centerX + tileCenterX, y + offsetY);
|
|
offsetY += (int) lineTextBoundingBoxes[i].getHeight();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the coordinates to draw a unit in a given tile.
|
|
*
|
|
* @param unitImage The unit's image
|
|
* @return The coordinates where the unit should be drawn on screen
|
|
*/
|
|
private Point calculateUnitImagePositionInTile(BufferedImage unitImage) {
|
|
int unitX = (tileBounds.getWidth() - unitImage.getWidth()) / 2;
|
|
int unitY = (tileBounds.getHeight() - unitImage.getHeight()) / 2
|
|
- this.lib.scaleInt(TileBounds.UNIT_OFFSET);
|
|
return new Point(unitX, unitY);
|
|
}
|
|
|
|
/**
|
|
* Draws the borders of a territory on the given Tile. The
|
|
* territory is either a country or a region.
|
|
*
|
|
* @param g2d a {@code Graphics2D}
|
|
* @param tile a {@code Tile}
|
|
* @param type a {@code BorderType}
|
|
* @param opaque a {@code boolean}
|
|
*/
|
|
private void displayTerritorialBorders(Graphics2D g2d, Tile tile,
|
|
BorderType type, boolean opaque) {
|
|
Player owner = tile.getOwner();
|
|
Region region = tile.getRegion();
|
|
if ((type == BorderType.COUNTRY && owner != null)
|
|
|| (type == BorderType.REGION && region != null)) {
|
|
Stroke oldStroke = g2d.getStroke();
|
|
g2d.setStroke(mapViewerScaledUtils.getBorderStroke());
|
|
Color oldColor = g2d.getColor();
|
|
Color c = null;
|
|
if (type == BorderType.COUNTRY)
|
|
c = owner.getNationColor();
|
|
if (c == null)
|
|
c = Color.WHITE;
|
|
Color newColor = new Color(c.getRed(), c.getGreen(), c.getBlue(),
|
|
(opaque) ? 255 : 100);
|
|
g2d.setColor(newColor);
|
|
GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
|
|
final EnumMap<Direction, Float> borderPoints = mapViewerScaledUtils.getBorderPoints();
|
|
EnumMap<Direction, Float> controlPoints = mapViewerScaledUtils.getControlPoints();
|
|
|
|
path.moveTo(borderPoints.get(Direction.longSides.get(0)).x,
|
|
borderPoints.get(Direction.longSides.get(0)).y);
|
|
for (Direction d : Direction.longSides) {
|
|
Tile otherTile = tile.getNeighbourOrNull(d);
|
|
Direction next = d.getNextDirection();
|
|
Direction next2 = next.getNextDirection();
|
|
if (otherTile == null
|
|
|| (type == BorderType.COUNTRY && !owner.owns(otherTile))
|
|
|| (type == BorderType.REGION && otherTile.getRegion() != region)) {
|
|
Tile tile1 = tile.getNeighbourOrNull(next);
|
|
Tile tile2 = tile.getNeighbourOrNull(next2);
|
|
if (tile2 == null
|
|
|| (type == BorderType.COUNTRY && !owner.owns(tile2))
|
|
|| (type == BorderType.REGION && tile2.getRegion() != region)) {
|
|
// small corner
|
|
path.lineTo(borderPoints.get(next).x,
|
|
borderPoints.get(next).y);
|
|
path.quadTo(controlPoints.get(next).x,
|
|
controlPoints.get(next).y,
|
|
borderPoints.get(next2).x,
|
|
borderPoints.get(next2).y);
|
|
} else {
|
|
int dx = 0, dy = 0;
|
|
switch(d) {
|
|
case NW: dx = tileBounds.getHalfWidth(); dy = -tileBounds.getHalfHeight(); break;
|
|
case NE: dx = tileBounds.getHalfWidth(); dy = tileBounds.getHalfHeight(); break;
|
|
case SE: dx = -tileBounds.getHalfWidth(); dy = tileBounds.getHalfHeight(); break;
|
|
case SW: dx = -tileBounds.getHalfWidth(); dy = -tileBounds.getHalfHeight(); break;
|
|
default: break;
|
|
}
|
|
if (tile1 != null
|
|
&& ((type == BorderType.COUNTRY && owner.owns(tile1))
|
|
|| (type == BorderType.REGION && tile1.getRegion() == region))) {
|
|
// short straight line
|
|
path.lineTo(borderPoints.get(next).x,
|
|
borderPoints.get(next).y);
|
|
// big corner
|
|
Direction previous = d.getPreviousDirection();
|
|
Direction previous2 = previous.getPreviousDirection();
|
|
int ddx = 0, ddy = 0;
|
|
switch(d) {
|
|
case NW: ddy = -tileBounds.getHeight(); break;
|
|
case NE: ddx = tileBounds.getWidth(); break;
|
|
case SE: ddy = tileBounds.getHeight(); break;
|
|
case SW: ddx = -tileBounds.getWidth(); break;
|
|
default: break;
|
|
}
|
|
path.quadTo(controlPoints.get(previous).x + dx,
|
|
controlPoints.get(previous).y + dy,
|
|
borderPoints.get(previous2).x + ddx,
|
|
borderPoints.get(previous2).y + ddy);
|
|
} else {
|
|
// straight line
|
|
path.lineTo(borderPoints.get(d).x + dx,
|
|
borderPoints.get(d).y + dy);
|
|
}
|
|
}
|
|
} else {
|
|
path.moveTo(borderPoints.get(next2).x,
|
|
borderPoints.get(next2).y);
|
|
}
|
|
}
|
|
g2d.draw(path);
|
|
g2d.setColor(oldColor);
|
|
g2d.setStroke(oldStroke);
|
|
}
|
|
}
|
|
|
|
private void verifyAndMarkAsClean(Dimension size, final Rectangle clipBounds) {
|
|
final Rectangle entireScreen = new Rectangle(0, 0, size.width, size.height);
|
|
final Rectangle relevantDirtyClipBounds = rpm.getDirtyClipBounds().intersection(entireScreen);
|
|
|
|
if (relevantDirtyClipBounds.isEmpty() || clipBounds.contains(relevantDirtyClipBounds)) {
|
|
rpm.markAsClean();
|
|
} else {
|
|
logger.info("Repaint has been called for a smaller area than what is dirty. "
|
|
+ "Have you forgotten to call repaint() after marking stuff as dirty? "
|
|
+ "The only known OK instance of this happening is when the GUI is "
|
|
+ "starting up. Bounds: "
|
|
+ clipBounds
|
|
+ " ==> "
|
|
+ relevantDirtyClipBounds);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get either the tile with the active unit or the selected tile,
|
|
* but only if it is visible.
|
|
*
|
|
* Used to determine where to display the cursor, for displayMap and
|
|
* and the cursor action listener.
|
|
*
|
|
* @return The {@code Tile} found or null.
|
|
*/
|
|
private Tile getVisibleCursorTile(MapViewerBounds mapViewerBounds) {
|
|
Tile ret = mapViewerState.getCursorTile();
|
|
return (mapViewerBounds.isTileVisible(ret)) ? ret : null;
|
|
}
|
|
|
|
/**
|
|
* Internal class for the {@link MapViewer} that handles what part of the
|
|
* {@link Map} is visible on screen
|
|
*
|
|
* Methods in this class should only be used by {@link SwingGUI},
|
|
* {@link Canvas} or {@link MapViewer}.
|
|
*
|
|
* @return The visible map bounds.
|
|
*/
|
|
public MapViewerBounds getMapViewerBounds() {
|
|
return mapViewerBounds;
|
|
}
|
|
|
|
/**
|
|
* Bounds of the tiles to be rendered. These bounds are scaled
|
|
* according to the zoom level of the map.
|
|
*
|
|
* @return The tile bounds.
|
|
*/
|
|
public TileBounds getTileBounds() {
|
|
return tileBounds;
|
|
}
|
|
|
|
/**
|
|
* Internal state for the {@link MapViewer}.
|
|
*
|
|
* Methods in this class should only be used by {@link SwingGUI},
|
|
* {@link Canvas} or {@link MapViewer}.
|
|
*
|
|
* @return The {@code MapViewerState}.
|
|
*/
|
|
public MapViewerState getMapViewerState() {
|
|
return mapViewerState;
|
|
}
|
|
|
|
/**
|
|
* Gets the internal class that handles buffers and dirty state of
|
|
* {@link MapViewer}.
|
|
*
|
|
* Methods in this class should only be used by {@link SwingGUI},
|
|
* {@link Canvas} or {@link MapViewer}
|
|
*
|
|
* @return The repaint manager.
|
|
*/
|
|
public MapViewerRepaintManager getMapViewerRepaintManager() {
|
|
return rpm;
|
|
}
|
|
|
|
/**
|
|
* Paints a single tile using the provided callback.
|
|
*
|
|
* @param g2d The {@code Graphics2D} that is used for rendering.
|
|
* @param tcb The bounds used for clipping the area to be rendered.
|
|
* @param tile The {@code Tile} to be rendered.
|
|
* @param c A callback that should render the tile. The coordinates for the
|
|
* {@code Graphics2D}, that's provided by the, callback will be
|
|
* translated so that position (0, 0) is the upper left corner of the
|
|
* tile image (that is, outside of the tile diamond itself).
|
|
*/
|
|
private void paintSingleTile(Graphics2D g2d, TileClippingBounds tcb,
|
|
Tile tile, TileRenderingCallback c) {
|
|
paintEachTile(g2d, tcb.getTopLeftDirtyTile(), List.of(tile), c);
|
|
}
|
|
|
|
/**
|
|
* Paints all "dirty" tiles.
|
|
*
|
|
* @param g2d The {@code Graphics2D} that is used for rendering.
|
|
* @param tcb The bounds used for clipping the area to be rendered.
|
|
* @param c A callback that should render the tile. The coordinates for the
|
|
* {@code Graphics2D}, that's provided by the, callback will be
|
|
* translated so that position (0, 0) is the upper left corner of the
|
|
* tile image (that is, outside of the tile diamond itself).
|
|
*/
|
|
private void paintEachTile(Graphics2D g2d, TileClippingBounds tcb, TileRenderingCallback c) {
|
|
paintEachTile(g2d, tcb.getTopLeftDirtyTile(), tcb.getBaseTiles(), c);
|
|
}
|
|
|
|
/**
|
|
* Paints all "dirty" tiles and includes
|
|
* {@link TileClippingBounds#getExtendedTiles() extra tiles} to be rendered.
|
|
*
|
|
* This method should only be used if the rendered graphics can go beyond
|
|
* the tile size.
|
|
*
|
|
* @param g2d The {@code Graphics2D} that is used for rendering.
|
|
* @param tcb The bounds used for clipping the area to be rendered.
|
|
* @param c A callback that should render the tile. The coordinates for the
|
|
* {@code Graphics2D}, that's provided by the, callback will be
|
|
* translated so that position (0, 0) is the upper left corner of the
|
|
* tile image (that is, outside of the tile diamond itself).
|
|
*/
|
|
private void paintEachTileWithExtendedImageSize(Graphics2D g2d, TileClippingBounds tcb, TileRenderingCallback c) {
|
|
paintEachTile(g2d, tcb.getTopLeftDirtyTile(), tcb.getExtendedTiles(), c);
|
|
}
|
|
|
|
/**
|
|
* Paints all "dirty" tiles and includes
|
|
* {@link TileClippingBounds#getSuperExtendedTiles() many extra tiles} to be rendered.
|
|
*
|
|
* This method should only be used if the rendered graphics can go way
|
|
* beyond the tile size.
|
|
*
|
|
* @param g2d The {@code Graphics2D} that is used for rendering.
|
|
* @param tcb The bounds used for clipping the area to be rendered.
|
|
* @param c A callback that should render the tile. The coordinates for the
|
|
* {@code Graphics2D}, that's provided by the, callback will be
|
|
* translated so that position (0, 0) is the upper left corner of the
|
|
* tile image (that is, outside of the tile diamond itself).
|
|
*/
|
|
private void paintEachTileWithSuperExtendedImageSize(Graphics2D g2d, TileClippingBounds tcb, TileRenderingCallback c) {
|
|
paintEachTile(g2d, tcb.getTopLeftDirtyTile(), tcb.getSuperExtendedTiles(), c);
|
|
}
|
|
|
|
private void paintEachTile(Graphics2D g2d, Map.Position firstTile, List<Tile> tiles, TileRenderingCallback c) {
|
|
if (tiles.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
final int x0 = firstTile.getX();
|
|
final int y0 = firstTile.getY();
|
|
|
|
int xt0 = 0, yt0 = 0;
|
|
for (Tile t : tiles) {
|
|
final int x = t.getX();
|
|
final int y = t.getY();
|
|
final int xt = (x-x0) * tileBounds.getWidth()
|
|
+ (y&1) * tileBounds.getHalfWidth();
|
|
final int yt = (y-y0) * tileBounds.getHalfHeight();
|
|
g2d.translate(xt - xt0, yt - yt0);
|
|
xt0 = xt; yt0 = yt;
|
|
|
|
c.render(g2d, t);
|
|
}
|
|
g2d.translate(-xt0, -yt0);
|
|
}
|
|
|
|
/**
|
|
* Calculates the tile clipping bounds from a Graphics' clipBounds.
|
|
*
|
|
* The tiles to be redrawn is the area defined by the {@code topLeftDirtyTile}
|
|
* (the upper left corner of the area to be repainted)
|
|
* and {@code bottomRightDirtyTile}
|
|
* (the lower right corner of the area to be repainted).
|
|
*
|
|
* The list of tiles to be repainted depends on the possible image sizes
|
|
* that can be drawn on a tile: {@link #getBaseTiles() baseTiles},
|
|
* {@link #getExtendedTiles() extendedTiles} and
|
|
* {@link #getSuperExtendedTiles() superExtendedTiles}.
|
|
*/
|
|
private static final class TileClippingBounds {
|
|
private final Map.Position topLeftDirtyTile;
|
|
private final Map.Position bottomRightDirtyTile;
|
|
private final int clipLeftX;
|
|
private final int clipTopY;
|
|
|
|
private final List<Tile> baseTiles;
|
|
private final List<Tile> unitTiles;
|
|
private final List<Tile> extendedTiles;
|
|
private final List<Tile> superExtendedTiles;
|
|
|
|
private TileClippingBounds(MapViewerBounds mapViewerBounds, Map map, Rectangle clipBounds) {
|
|
final TileBounds tileBounds = mapViewerBounds.getTileBounds();
|
|
final int firstRowTiles = (clipBounds.y - mapViewerBounds.getTopLeftVisibleTilePoint().y) / tileBounds.getHalfHeight() - 1;
|
|
this.clipTopY = mapViewerBounds.getTopLeftVisibleTilePoint().y + firstRowTiles * tileBounds.getHalfHeight();
|
|
final int firstRow = mapViewerBounds.getTopLeftVisibleTile().getY() + firstRowTiles;
|
|
|
|
final int firstColumnTiles = (clipBounds.x - mapViewerBounds.getTopLeftVisibleTilePoint().x) / tileBounds.getWidth() - 1;
|
|
this.clipLeftX = mapViewerBounds.getTopLeftVisibleTilePoint().x + firstColumnTiles * tileBounds.getWidth();
|
|
final int firstColumn = mapViewerBounds.getTopLeftVisibleTile().getX() + firstColumnTiles;
|
|
|
|
final int lastRowTiles = (clipBounds.y + clipBounds.height - mapViewerBounds.getTopLeftVisibleTilePoint().y) / tileBounds.getHalfHeight();
|
|
final int lastRow = mapViewerBounds.getTopLeftVisibleTile().getY() + lastRowTiles;
|
|
final int lastColumnTiles = (clipBounds.x + clipBounds.width - mapViewerBounds.getTopLeftVisibleTilePoint().x) / tileBounds.getWidth();
|
|
final int lastColumn = mapViewerBounds.getTopLeftVisibleTile().getX() + lastColumnTiles;
|
|
|
|
this.topLeftDirtyTile = new Map.Position(firstColumn, firstRow);
|
|
this.bottomRightDirtyTile = new Map.Position(lastColumn, lastRow);
|
|
|
|
/* For testing MapViewerBounds -- just ignore the logic above, and do:
|
|
this.topLeftDirtyTile = mapViewerBounds.getTopLeftVisibleTile();
|
|
//this.bottomRightDirtyTile = new Map.Position(mapViewerBounds.getTopLeftVisibleTile().x + 50, mapViewerBounds.getTopLeftVisibleTile().y + 50);
|
|
this.bottomRightDirtyTile = mapViewerBounds.getBottomRightVisibleTile();
|
|
this.clipLeftX = mapViewerBounds.getTopLeftVisibleTilePoint().x;
|
|
this.clipTopY = mapViewerBounds.getTopLeftVisibleTilePoint().y;
|
|
*/
|
|
|
|
final int subMapWidth = bottomRightDirtyTile.getX() - topLeftDirtyTile.getX() + 1;
|
|
final int subMapHeight = bottomRightDirtyTile.getY() - topLeftDirtyTile.getY() + 1;
|
|
|
|
baseTiles = map.subMap(
|
|
topLeftDirtyTile.getX(),
|
|
topLeftDirtyTile.getY(),
|
|
subMapWidth,
|
|
subMapHeight);
|
|
|
|
unitTiles = map.subMap(
|
|
topLeftDirtyTile.getX(),
|
|
topLeftDirtyTile.getY(),
|
|
subMapWidth,
|
|
subMapHeight + 1);
|
|
|
|
extendedTiles = map.subMap(
|
|
topLeftDirtyTile.getX(),
|
|
topLeftDirtyTile.getY() - 1,
|
|
subMapWidth,
|
|
subMapHeight + 2);
|
|
|
|
superExtendedTiles = map.subMap(
|
|
topLeftDirtyTile.getX() - 2,
|
|
topLeftDirtyTile.getY() - 4,
|
|
subMapWidth + 4,
|
|
subMapHeight + 8);
|
|
}
|
|
|
|
public Map.Position getTopLeftDirtyTile() {
|
|
return topLeftDirtyTile;
|
|
}
|
|
|
|
public Map.Position getBottomRightDirtyTile() {
|
|
return bottomRightDirtyTile;
|
|
}
|
|
|
|
/**
|
|
* The tiles to be repainted for graphics that does not extend
|
|
* beyond the tile size.
|
|
*
|
|
* @return The list of tiles to repaint.
|
|
*/
|
|
public List<Tile> getBaseTiles() {
|
|
return baseTiles;
|
|
}
|
|
|
|
/**
|
|
* The tiles to be repainted for unit graphics that might extend
|
|
* up to half a tile in height above the base tile.
|
|
*
|
|
* @return The list of tiles to repaint.
|
|
*/
|
|
public List<Tile> getUnitTiles() {
|
|
return unitTiles;
|
|
}
|
|
|
|
/**
|
|
* The tiles to be repainted for graphics that might have
|
|
* double height compared to the tile size.
|
|
*
|
|
* @return The list of potential double height tiles.
|
|
*/
|
|
public List<Tile> getExtendedTiles() {
|
|
return extendedTiles;
|
|
}
|
|
|
|
/**
|
|
* The tiles to be repainted for graphics that might extend
|
|
* far into other tiles in every direction (typically halos,
|
|
* like in revenge mode).
|
|
*
|
|
* @return The list of potentially haloed tiles.
|
|
*/
|
|
public List<Tile> getSuperExtendedTiles() {
|
|
return superExtendedTiles;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A callback for rendering a single tile.
|
|
*/
|
|
private interface TileRenderingCallback {
|
|
|
|
/**
|
|
* Should render a single tile.
|
|
*
|
|
* @param tileG2d The {@code Graphics2D} that should be used when drawing
|
|
* the tile. The coordinates for the {@code Graphics2D} will be
|
|
* translated so that position (0, 0) is the upper left corner of
|
|
* the tile image (that is, outside of the tile diamond itself).
|
|
* @param tile The {@code Tile} to be rendered.
|
|
*/
|
|
void render(Graphics2D tileG2d, Tile tile);
|
|
}
|
|
|
|
private static class TextSpecification {
|
|
|
|
public final String text;
|
|
public final Font font;
|
|
|
|
public TextSpecification(String newText, Font newFont) {
|
|
this.text = newText;
|
|
this.font = newFont;
|
|
}
|
|
}
|
|
}
|