package games.strategy.triplea.ui;
import static com.google.common.base.Preconditions.checkNotNull;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.SwingUtilities;
import games.strategy.engine.data.Change;
import games.strategy.engine.data.ChangeAttachmentChange;
import games.strategy.engine.data.CompositeChange;
import games.strategy.engine.data.GameData;
import games.strategy.engine.data.PlayerID;
import games.strategy.engine.data.Route;
import games.strategy.engine.data.Territory;
import games.strategy.engine.data.Unit;
import games.strategy.engine.data.events.GameDataChangeListener;
import games.strategy.engine.data.events.TerritoryListener;
import games.strategy.triplea.Constants;
import games.strategy.triplea.TripleAUnit;
import games.strategy.triplea.delegate.Matches;
import games.strategy.triplea.ui.screen.SmallMapImageManager;
import games.strategy.triplea.ui.screen.Tile;
import games.strategy.triplea.ui.screen.TileManager;
import games.strategy.triplea.ui.screen.UnitsDrawer;
import games.strategy.triplea.ui.screen.drawable.IDrawable.OptionalExtraBorderLevel;
import games.strategy.triplea.util.Stopwatch;
import games.strategy.triplea.util.UnitCategory;
import games.strategy.triplea.util.UnitSeperator;
import games.strategy.ui.ImageScrollModel;
import games.strategy.ui.ImageScrollerLargeView;
import games.strategy.ui.Util;
import games.strategy.util.ListenerList;
import games.strategy.util.Match;
import games.strategy.util.ThreadUtil;
import games.strategy.util.Tuple;
/**
* Responsible for drawing the large map and keeping it updated.
*/
public class MapPanel extends ImageScrollerLargeView {
private static final long serialVersionUID = -3571551538356292556L;
private static Logger logger = Logger.getLogger(MapPanel.class.getName());
private final ListenerList<MapSelectionListener> mapSelectionListeners = new ListenerList<>();
private final ListenerList<UnitSelectionListener> unitSelectionListeners = new ListenerList<>();
private final ListenerList<MouseOverUnitListener> mouseOverUnitsListeners = new ListenerList<>();
private GameData m_data;
// the territory that the mouse is
private Territory currentTerritory;
// currently over
// could be null
private final MapPanelSmallView smallView;
// units the mouse is currently over
private Tuple<Territory, List<Unit>> currentUnits;
private final SmallMapImageManager smallMapImageManager;
// keep a reference to the images from the last paint to
// prevent them from being gcd
private final List<Tile> images = new ArrayList<>();
private RouteDescription routeDescription;
private final TileManager tileManager;
private final BackgroundDrawer backgroundDrawer;
private BufferedImage mouseShadowImage = null;
private String movementLeftForCurrentUnits = "";
private final IUIContext uiContext;
private final LinkedBlockingQueue<Tile> undrawnTiles = new LinkedBlockingQueue<>();
private Map<Territory, List<Unit>> highlightedUnits;
private Cursor hiddenCursor = null;
private final MapRouteDrawer routeDrawer;
/** Creates new MapPanel. */
public MapPanel(final GameData data, final MapPanelSmallView smallView, final IUIContext uiContext,
final ImageScrollModel model, final Supplier<Integer> computeScrollSpeed) {
super(uiContext.getMapData().getMapDimensions(), model);
this.uiContext = uiContext;
routeDrawer = new MapRouteDrawer(this, uiContext.getMapData());
setCursor(this.uiContext.getCursor());
this.m_scale = this.uiContext.getScale();
this.backgroundDrawer = new BackgroundDrawer(this);
this.tileManager = new TileManager(this.uiContext);
final Thread t = new Thread(this.backgroundDrawer, "Map panel background drawer");
t.setDaemon(true);
t.start();
setDoubleBuffered(false);
this.smallView = smallView;
this.smallMapImageManager =
new SmallMapImageManager(smallView, this.uiContext.getMapImage().getSmallMapImage(), this.tileManager);
setGameData(data);
this.addMouseListener(new MouseAdapter() {
private boolean is4Pressed = false;
private boolean is5Pressed = false;
private int lastActive = -1;
/**
* Invoked when the mouse exits a component.
*/
@Override
public void mouseExited(final MouseEvent e) {
if (unitsChanged(null)) {
final MouseDetails md = convert(e);
currentUnits = null;
notifyMouseEnterUnit(Collections.emptyList(), getTerritory(e.getX(), e.getY()), md);
}
}
// this can't be mouseClicked, since a lot of people complain that clicking doesn't work well
@Override
public void mouseReleased(final MouseEvent e) {
final MouseDetails md = convert(e);
final double scaledMouseX = e.getX() / m_scale;
final double scaledMouseY = e.getY() / m_scale;
final double x = normalizeX(scaledMouseX + getXOffset());
final double y = normalizeY(scaledMouseY + getYOffset());
final Territory terr = getTerritory(x, y);
if (terr != null) {
notifyTerritorySelected(terr, md);
}
if (e.getButton() == 4 || e.getButton() == 5) {
//the numbers 4 and 5 stand for the corresponding mouse button
lastActive = is4Pressed && is5Pressed ? (e.getButton() == 4 ? 5 : 4) : -1;
//we only want to change the variables if the corresponding button was released
is4Pressed = e.getButton() == 4 ? false : is4Pressed;
is5Pressed = e.getButton() == 5 ? false : is5Pressed;
//we want to return here, because otherwise a menu might be opened
return;
}
if (!unitSelectionListeners.isEmpty()) {
Tuple<Territory, List<Unit>> tuple = tileManager.getUnitsAtPoint(x, y, m_data);
if (tuple == null) {
tuple = Tuple.of(getTerritory(x, y), new ArrayList<Unit>(0));
}
notifyUnitSelected(tuple.getSecond(), tuple.getFirst(), md);
}
}
@Override
public void mousePressed(final MouseEvent e) {
is4Pressed = e.getButton() == 4 ? true : is4Pressed;
is5Pressed = e.getButton() == 5 ? true : is5Pressed;
if (lastActive == -1) {
new Thread(() -> {
//Mouse Events are different than key events
//Thats why we're "simulating" multiple
//clicks while the mouse button is held down
//so the map keeps scrolling
while (lastActive != -1) {
final int diffPixel = computeScrollSpeed.get();
if (lastActive == 5) {
setTopLeft(getXOffset() + diffPixel, getYOffset());
} else if (lastActive == 4) {
setTopLeft(getXOffset() - diffPixel, getYOffset());
}
//50ms seems to be a good interval between "clicks"
//changing this number changes the scroll speed
ThreadUtil.sleep(50);
}
}).start();
}
lastActive = e.getButton();
}
});
this.addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseMoved(final MouseEvent e) {
final MouseDetails md = convert(e);
final double scaledMouseX = e.getX() / m_scale;
final double scaledMouseY = e.getY() / m_scale;
final double x = normalizeX(scaledMouseX + getXOffset());
final double y = normalizeY(scaledMouseY + getYOffset());
final Territory terr = getTerritory(x, y);
// we can use == here since they will be the same object.
// dont use .equals since we have nulls
if (terr != currentTerritory) {
currentTerritory = terr;
notifyMouseEntered(terr);
}
notifyMouseMoved(terr, md);
final Tuple<Territory, List<Unit>> tuple = tileManager.getUnitsAtPoint(x, y, m_data);
if (unitsChanged(tuple)) {
currentUnits = tuple;
if (tuple == null) {
notifyMouseEnterUnit(Collections.emptyList(), getTerritory(x, y), md);
} else {
notifyMouseEnterUnit(tuple.getSecond(), tuple.getFirst(), md);
}
}
}
});
this.addScrollListener((x2, y2) -> SwingUtilities.invokeLater(() -> repaint()));
recreateTiles(data, this.uiContext);
this.uiContext.addActive(() -> {
// super.deactivate
MapPanel.this.deactivate();
clearUndrawn();
backgroundDrawer.stop();
});
}
LinkedBlockingQueue<Tile> getUndrawnTiles() {
return undrawnTiles;
}
private void recreateTiles(final GameData data, final IUIContext uiContext) {
this.tileManager.createTiles(new Rectangle(this.uiContext.getMapData().getMapDimensions()), data,
this.uiContext.getMapData());
this.tileManager.resetTiles(data, uiContext.getMapData());
}
GameData getData() {
return m_data;
}
// Beagle Code used to chnage map skin
public void changeImage(final Dimension newDimensions) {
m_model.setMaxBounds((int) newDimensions.getWidth(), (int) newDimensions.getHeight());
tileManager.createTiles(new Rectangle(newDimensions), m_data, uiContext.getMapData());
tileManager.resetTiles(m_data, uiContext.getMapData());
}
@Override
public Dimension getPreferredSize() {
return getImageDimensions();
}
@Override
public Dimension getMinimumSize() {
return new Dimension(200, 200);
}
public boolean isShowing(final Territory territory) {
final Point territoryCenter = uiContext.getMapData().getCenter(territory);
final Rectangle2D screenBounds =
new Rectangle2D.Double(super.getXOffset(), super.getYOffset(), super.getScaledWidth(), super.getScaledHeight());
return screenBounds.contains(territoryCenter);
}
/**
* the units must all be in the same stack on the map, and exist in the given territory.
* call with an null args
*/
public void setUnitHighlight(final Map<Territory, List<Unit>> units) {
highlightedUnits = units;
SwingUtilities.invokeLater(() -> repaint());
}
protected Map<Territory, List<Unit>> getHighlightedUnits() {
return highlightedUnits;
}
public void centerOn(final Territory territory) {
if (territory == null || uiContext.getLockMap()) {
return;
}
final Point p = uiContext.getMapData().getCenter(territory);
// when centering dont want the map to wrap around,
// eg if centering on hawaii
super.setTopLeft((int) (p.x - (getScaledWidth() / 2)), (int) (p.y - (getScaledHeight() / 2)));
}
public void setRoute(final Route route) {
setRoute(route, null, null, null);
}
/**
* Set the route, could be null.
*/
public void setRoute(final Route route, final Point start, final Point end, final Image cursorImage) {
if (route == null) {
routeDescription = null;
SwingUtilities.invokeLater(() -> repaint());
return;
}
final RouteDescription newRouteDescription = new RouteDescription(route, start, end, cursorImage);
if (routeDescription != null && routeDescription.equals(newRouteDescription)) {
return;
}
routeDescription = newRouteDescription;
SwingUtilities.invokeLater(() -> repaint());
}
public void addMapSelectionListener(final MapSelectionListener listener) {
mapSelectionListeners.add(listener);
}
public void removeMapSelectionListener(final MapSelectionListener listener) {
mapSelectionListeners.remove(listener);
}
public void addMouseOverUnitListener(final MouseOverUnitListener listener) {
mouseOverUnitsListeners.add(listener);
}
public void removeMouseOverUnitListener(final MouseOverUnitListener listener) {
mouseOverUnitsListeners.remove(listener);
}
private void notifyTerritorySelected(final Territory t, final MouseDetails me) {
for (final MapSelectionListener msl : mapSelectionListeners) {
msl.territorySelected(t, me);
}
}
private void notifyMouseMoved(final Territory t, final MouseDetails me) {
for (final MapSelectionListener msl : mapSelectionListeners) {
msl.mouseMoved(t, me);
}
}
private void notifyMouseEntered(final Territory t) {
for (final MapSelectionListener msl : mapSelectionListeners) {
msl.mouseEntered(t);
}
}
public void addUnitSelectionListener(final UnitSelectionListener listener) {
unitSelectionListeners.add(listener);
}
public void removeUnitSelectionListener(final UnitSelectionListener listener) {
unitSelectionListeners.remove(listener);
}
private void notifyUnitSelected(final List<Unit> units, final Territory t, final MouseDetails me) {
for (final UnitSelectionListener listener : unitSelectionListeners) {
listener.unitsSelected(units, t, me);
}
}
private void notifyMouseEnterUnit(final List<Unit> units, final Territory t, final MouseDetails me) {
for (final MouseOverUnitListener listener : mouseOverUnitsListeners) {
listener.mouseEnter(units, t, me);
}
}
private Territory getTerritory(final double x, final double y) {
final String name = uiContext.getMapData().getTerritoryAt(normalizeX(x), normalizeY(y));
if (name == null) {
return null;
}
return m_data.getMap().getTerritory(name);
}
private double normalizeX(double x) {
if (!uiContext.getMapData().scrollWrapX()) {
return x;
}
final int imageWidth = (int) getImageDimensions().getWidth();
if (x < 0) {
x += imageWidth;
} else if (x > imageWidth) {
x -= imageWidth;
}
return x;
}
private double normalizeY(double y) {
if (!uiContext.getMapData().scrollWrapY()) {
return y;
}
final int imageHeight = (int) getImageDimensions().getHeight();
if (y < 0) {
y += imageHeight;
} else if (y > imageHeight) {
y -= imageHeight;
}
return y;
}
public void resetMap() {
tileManager.resetTiles(m_data, uiContext.getMapData());
SwingUtilities.invokeLater(() -> repaint());
initSmallMap();
// m_smallMapImageManager.update(m_data, m_uiContext.getMapData());
}
private MouseDetails convert(final MouseEvent me) {
final double scaledMouseX = me.getX() / m_scale;
final double scaledMouseY = me.getY() / m_scale;
final double x = normalizeX(scaledMouseX + getXOffset());
final double y = normalizeY(scaledMouseY + getYOffset());
return new MouseDetails(me, x, y);
}
private boolean unitsChanged(final Tuple<Territory, List<Unit>> newUnits) {
// both are null
if (newUnits == currentUnits) {
return false;
}
// one is null
if (newUnits == null || currentUnits == null) {
return true;
}
if (!newUnits.getFirst().equals(currentUnits.getFirst())) {
return true;
}
return !games.strategy.util.Util.equals(newUnits.getSecond(), currentUnits.getSecond());
}
public void updateCountries(final Collection<Territory> countries) {
tileManager.updateTerritories(countries, m_data, uiContext.getMapData());
smallMapImageManager.update(m_data, uiContext.getMapData());
SwingUtilities.invokeLater(() -> {
smallView.repaint();
repaint();
});
}
public void setGameData(final GameData data) {
// clean up any old listeners
if (m_data != null) {
m_data.removeTerritoryListener(TERRITORY_LISTENER);
m_data.removeDataChangeListener(TECH_UPDATE_LISTENER);
}
m_data = data;
m_data.addTerritoryListener(TERRITORY_LISTENER);
m_data.addDataChangeListener(TECH_UPDATE_LISTENER);
clearUndrawn();
tileManager.resetTiles(m_data, uiContext.getMapData());
}
private final TerritoryListener TERRITORY_LISTENER = new TerritoryListener() {
@Override
public void unitsChanged(final Territory territory) {
updateCountries(Collections.singleton(territory));
SwingUtilities.invokeLater(() -> repaint());
}
@Override
public void ownerChanged(final Territory territory) {
smallMapImageManager.updateTerritoryOwner(territory, m_data, uiContext.getMapData());
updateCountries(Collections.singleton(territory));
SwingUtilities.invokeLater(() -> repaint());
}
@Override
public void attachmentChanged(final Territory territory) {
updateCountries(Collections.singleton(territory));
SwingUtilities.invokeLater(() -> repaint());
}
};
private final GameDataChangeListener TECH_UPDATE_LISTENER = new GameDataChangeListener() {
@Override
public void gameDataChanged(final Change aChange) {
// find the players with tech changes
final Set<PlayerID> playersWithTechChange = new HashSet<>();
getPlayersWithTechChanges(aChange, playersWithTechChange);
if (playersWithTechChange.isEmpty()) {
return;
}
tileManager.resetTiles(m_data, uiContext.getMapData());
SwingUtilities.invokeLater(() -> repaint());
}
private void getPlayersWithTechChanges(final Change aChange, final Set<PlayerID> players) {
if (aChange instanceof CompositeChange) {
final CompositeChange composite = (CompositeChange) aChange;
for (final Change item : composite.getChanges()) {
getPlayersWithTechChanges(item, players);
}
} else {
if (aChange instanceof ChangeAttachmentChange) {
final ChangeAttachmentChange changeAttachment = (ChangeAttachmentChange) aChange;
if (changeAttachment.getAttachmentName().equals(Constants.TECH_ATTACHMENT_NAME)) {
players.add((PlayerID) changeAttachment.getAttachedTo());
}
}
}
}
};
@Override
public void setTopLeft(final int x, final int y) {
super.setTopLeft(x, y);
}
/**
* Draws an image of the complete map to the specified graphics context.
*
* <p>
* This method is useful for capturing screenshots. This method can be called from a thread other than the EDT.
* </p>
*
* @param g The graphics context on which to draw the map; must not be {@code null}.
*/
public void drawMapImage(final Graphics g) {
final Graphics2D g2d = (Graphics2D) checkNotNull(g);
// make sure we use the same data for the entire print
final GameData gameData = m_data;
gameData.acquireReadLock();
try {
final Rectangle2D.Double bounds = new Rectangle2D.Double(0, 0, getImageWidth(), getImageHeight());
final Collection<Tile> tileList = tileManager.getTiles(bounds);
for (final Tile tile : tileList) {
tile.acquireLock();
try {
final Image img = tile.getImage(gameData, uiContext.getMapData());
if (img != null) {
final AffineTransform t = new AffineTransform();
t.translate((tile.getBounds().x - bounds.getX()) * m_scale, (tile.getBounds().y - bounds.getY()) * m_scale);
g2d.drawImage(img, t, this);
}
} finally {
tile.releaseLock();
}
}
} finally {
gameData.releaseReadLock();
}
}
@Override
public void paint(final Graphics g) {
final Graphics2D g2d = (Graphics2D) g;
super.paint(g2d);
g2d.clip(new Rectangle2D.Double(0, 0, (getImageWidth() * m_scale), (getImageHeight() * m_scale)));
int x = m_model.getX();
int y = m_model.getY();
final List<Tile> images = new ArrayList<>();
final List<Tile> undrawnTiles = new ArrayList<>();
final Stopwatch stopWatch = new Stopwatch(logger, Level.FINER, "Paint");
// make sure we use the same data for the entire paint
final GameData data = m_data;
// if the map fits on screen, dont draw any overlap
final boolean fitAxisX = !mapWidthFitsOnScreen() && uiContext.getMapData().scrollWrapX();
final boolean fitAxisY = !mapHeightFitsOnScreen() && uiContext.getMapData().scrollWrapY();
if (fitAxisX || fitAxisY) {
if (fitAxisX && x + (int) getScaledWidth() > m_model.getMaxWidth()) {
x -= m_model.getMaxWidth();
}
if (fitAxisY && y + (int) getScaledHeight() > m_model.getMaxHeight()) {
y -= m_model.getMaxHeight();
}
// handle wrapping off the screen
if (fitAxisX && x < 0) {
if (fitAxisY && y < 0) {
final Rectangle2D.Double leftUpperBounds =
new Rectangle2D.Double(m_model.getMaxWidth() + x, m_model.getMaxHeight() + y, -x, -y);
drawTiles(g2d, images, data, leftUpperBounds, 0, 0, undrawnTiles);
}
final Rectangle2D.Double leftBounds =
new Rectangle2D.Double(m_model.getMaxWidth() + x, y, -x, getScaledHeight());
drawTiles(g2d, images, data, leftBounds, 0, 0, undrawnTiles);
}
if (fitAxisY && y < 0) {
final Rectangle2D.Double upperBounds =
new Rectangle2D.Double(x, m_model.getMaxHeight() + y, getScaledWidth(), -y);
drawTiles(g2d, images, data, upperBounds, 0, 0, undrawnTiles);
}
}
// handle non overlap
final Rectangle2D.Double mainBounds = new Rectangle2D.Double(x, y, getScaledWidth(), getScaledHeight());
drawTiles(g2d, images, data, mainBounds, 0, 0, undrawnTiles);
if (routeDescription != null && mouseShadowImage != null && routeDescription.getEnd() != null) {
final AffineTransform t = new AffineTransform();
t.translate(m_scale * normalizeX(routeDescription.getEnd().getX() - getXOffset()),
m_scale * normalizeY(routeDescription.getEnd().getY() - getYOffset()));
t.translate(mouseShadowImage.getWidth() / -2, mouseShadowImage.getHeight() / -2);
t.scale(m_scale, m_scale);
g2d.drawImage(mouseShadowImage, t, this);
}
if (routeDescription != null) {
routeDrawer.drawRoute(g2d, routeDescription, movementLeftForCurrentUnits);
}
// used to keep strong references to what is on the screen so it wont be garbage collected
// other references to the images are weak references
this.images.clear();
this.images.addAll(images);
if (highlightedUnits != null) {
for (final Entry<Territory, List<Unit>> entry : highlightedUnits.entrySet()) {
final Set<UnitCategory> categories = UnitSeperator.categorize(entry.getValue());
for (final UnitCategory category : categories) {
final List<Unit> territoryUnitsOfSameCategory = category.getUnits();
if (territoryUnitsOfSameCategory.isEmpty()) {
continue;
}
final Rectangle r = tileManager.getUnitRect(territoryUnitsOfSameCategory, m_data);
if (r == null) {
continue;
}
final Optional<Image> image = uiContext.getUnitImageFactory().getHighlightImage(category.getType(),
category.getOwner(), m_data, category.hasDamageOrBombingUnitDamage(), category.getDisabled());
if (image.isPresent()) {
final AffineTransform t = new AffineTransform();
t.translate(normalizeX(r.getX() - getXOffset()) * m_scale, normalizeY(r.getY() - getYOffset()) * m_scale);
t.scale(m_scale, m_scale);
g2d.drawImage(image.get(), t, this);
}
}
}
}
// draw the tiles nearest us first
// then draw farther away
updateUndrawnTiles(undrawnTiles, 30, true);
updateUndrawnTiles(undrawnTiles, 257, true);
// when we are this far away, dont force the tiles to stay in memroy
updateUndrawnTiles(undrawnTiles, 513, false);
updateUndrawnTiles(undrawnTiles, 767, false);
clearUndrawn();
this.undrawnTiles.addAll(undrawnTiles);
stopWatch.done();
}
private void clearUndrawn() {
for (int i = 0; i < 3; i++) {
try {
// several bug reports indicate that
// clear can throw an exception
// http://sourceforge.net/tracker/index.php?func=detail&aid=1832130&group_id=44492&atid=439737
// ignore
undrawnTiles.clear();
return;
} catch (final Exception e) {
e.printStackTrace(System.out);
}
}
}
boolean mapWidthFitsOnScreen() {
return m_model.getMaxWidth() < getScaledWidth();
}
boolean mapHeightFitsOnScreen() {
return m_model.getMaxHeight() < getScaledHeight();
}
/**
* If we have nothing left undrawn, draw the tiles within preDrawMargin of us, optionally
* forcing the tiles to remain in memory.
*/
private void updateUndrawnTiles(final List<Tile> undrawnTiles, final int preDrawMargin, final boolean forceInMemory) {
// draw tiles near us if we have nothing left to draw
// that way when we scroll slowly we wont notice a glitch
if (undrawnTiles.isEmpty()) {
final Rectangle2D extendedBounds = new Rectangle2D.Double(Math.max(m_model.getX() - preDrawMargin, 0),
Math.max(m_model.getY() - preDrawMargin, 0), getScaledWidth() + (2 * preDrawMargin),
getScaledHeight() + (2 * preDrawMargin));
final List<Tile> tileList = tileManager.getTiles(extendedBounds);
for (final Tile tile : tileList) {
if (tile.isDirty()) {
undrawnTiles.add(tile);
} else if (forceInMemory) {
images.add(tile);
}
}
}
}
private void drawTiles(final Graphics2D g, final List<Tile> images, final GameData data, Rectangle2D.Double bounds,
final double overlapX, final double overlapY, final List<Tile> undrawn) {
final List<Tile> tileList = tileManager.getTiles(bounds);
bounds = new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getHeight(), bounds.getWidth());
if (overlapX != 0) {
bounds.x += overlapX - getScaledWidth();
}
if (overlapY != 0) {
bounds.y += overlapY - getScaledHeight();
}
for (final Tile tile : tileList) {
Image img = null;
tile.acquireLock();
try {
if (tile.isDirty()) {
// take what we can get to avoid screen flicker
undrawn.add(tile);
img = tile.getRawImage();
} else {
img = tile.getImage(data, uiContext.getMapData());
images.add(tile);
}
if (img != null) {
final AffineTransform t = new AffineTransform();
t.translate(m_scale * (tile.getBounds().x - bounds.getX()), m_scale * (tile.getBounds().y - bounds.getY()));
g.drawImage(img, t, this);
}
} finally {
tile.releaseLock();
}
}
}
public Image getTerritoryImage(final Territory territory) {
getData().acquireReadLock();
try {
return tileManager.createTerritoryImage(territory, m_data, uiContext.getMapData());
} finally {
getData().releaseReadLock();
}
}
public Image getTerritoryImage(final Territory territory, final Territory focusOn) {
getData().acquireReadLock();
try {
return tileManager.createTerritoryImage(territory, focusOn, m_data, uiContext.getMapData());
} finally {
getData().releaseReadLock();
}
}
public double getScale() {
return m_scale;
}
@Override
public void setScale(final double newScale) {
super.setScale(newScale);
// setScale will check bounds, and normalize the scale correctly
final double normalizedScale = m_scale;
final OptionalExtraBorderLevel drawBorderOption = uiContext.getDrawTerritoryBordersAgain();
// so what is happening here is that when we zoom out, the territory borders get blurred or even removed
// so we have a special setter to have them be drawn a second time, on top of the relief tiles
if (normalizedScale >= 1) {
if (drawBorderOption != OptionalExtraBorderLevel.LOW) {
uiContext.resetDrawTerritoryBordersAgain();
}
} else {
if (drawBorderOption == OptionalExtraBorderLevel.LOW) {
uiContext.setDrawTerritoryBordersAgainToMedium();
}
}
uiContext.setScale(normalizedScale);
recreateTiles(getData(), uiContext);
repaint();
}
public void initSmallMap() {
final Iterator<Territory> territories = m_data.getMap().getTerritories().iterator();
while (territories.hasNext()) {
final Territory territory = territories.next();
smallMapImageManager.updateTerritoryOwner(territory, m_data, uiContext.getMapData());
}
smallMapImageManager.update(m_data, uiContext.getMapData());
}
public void changeSmallMapOffscreenMap() {
smallMapImageManager.updateOffscreenImage(uiContext.getMapImage().getSmallMapImage());
}
public void setMouseShadowUnits(final Collection<Unit> units) {
if (units == null || units.isEmpty()) {
movementLeftForCurrentUnits = "";
mouseShadowImage = null;
SwingUtilities.invokeLater(() -> repaint());
return;
}
final Tuple<Integer, Integer> movementLeft =
TripleAUnit.getMinAndMaxMovementLeft(Match.getMatches(units, Matches.unitIsBeingTransported().invert()));
movementLeftForCurrentUnits =
movementLeft.getFirst() + (movementLeft.getSecond() > movementLeft.getFirst() ? "+" : "");
final Set<UnitCategory> categories = UnitSeperator.categorize(units);
final int icon_width = uiContext.getUnitImageFactory().getUnitImageWidth();
final int xSpace = 5;
final BufferedImage img = Util.createImage(categories.size() * (xSpace + icon_width),
uiContext.getUnitImageFactory().getUnitImageHeight(), true);
final Graphics2D g = img.createGraphics();
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.6f));
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
final Rectangle bounds = new Rectangle(0, 0, 0, 0);
getData().acquireReadLock();
try {
int i = 0;
for (final UnitCategory category : categories) {
final Point place = new Point(i * (icon_width + xSpace), 0);
final UnitsDrawer drawer = new UnitsDrawer(category.getUnits().size(), category.getType().getName(),
category.getOwner().getName(), place, category.getDamaged(), category.getBombingDamage(),
category.getDisabled(), false, "", uiContext);
drawer.draw(bounds, m_data, g, uiContext.getMapData(), null, null);
i++;
}
} finally {
getData().releaseReadLock();
}
mouseShadowImage = img;
SwingUtilities.invokeLater(() -> repaint());
g.dispose();
}
public void setTerritoryOverlay(final Territory territory, final Color color, final int alpha) {
tileManager.setTerritoryOverlay(territory, color, alpha, m_data, uiContext.getMapData());
}
public void setTerritoryOverlayForBorder(final Territory territory, final Color color) {
tileManager.setTerritoryOverlayForBorder(territory, color, m_data, uiContext.getMapData());
}
public void clearTerritoryOverlay(final Territory territory) {
tileManager.clearTerritoryOverlay(territory, m_data, uiContext.getMapData());
}
public IUIContext getUIContext() {
return uiContext;
}
public void hideMouseCursor() {
if (hiddenCursor == null) {
hiddenCursor = getToolkit().createCustomCursor(new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR),
new Point(0, 0), "Hidden");
}
setCursor(hiddenCursor);
}
public void showMouseCursor() {
setCursor(uiContext.getCursor());
}
public Optional<Image> getErrorImage() {
return uiContext.getMapData().getErrorImage();
}
public Optional<Image> getWarningImage() {
return uiContext.getMapData().getWarningImage();
}
public Optional<Image> getInfoImage() {
return uiContext.getMapData().getInfoImage();
}
public Optional<Image> getHelpImage() {
return uiContext.getMapData().getHelpImage();
}
}
class BackgroundDrawer implements Runnable {
// use a weak reference, if we see the panel is gc'd, then we can stop this thread
private final WeakReference<MapPanel> m_mapPanelRef;
BackgroundDrawer(final MapPanel panel) {
m_mapPanelRef = new WeakReference<>(panel);
}
public void stop() {
// the thread will eventually wake up and notice we are done
m_mapPanelRef.clear();
}
@Override
public void run() {
while (m_mapPanelRef.get() != null) {
BlockingQueue<Tile> undrawnTiles;
MapPanel panel = m_mapPanelRef.get();
if (panel == null) {
continue;
}
undrawnTiles = panel.getUndrawnTiles();
panel = null;
Tile tile;
try {
tile = undrawnTiles.poll(2000, TimeUnit.MILLISECONDS);
} catch (final InterruptedException e) {
continue;
}
if (tile == null) {
continue;
}
final MapPanel mapPanel = m_mapPanelRef.get();
if (mapPanel == null) {
continue;
}
final GameData data = mapPanel.getData();
data.acquireReadLock();
try {
tile.getImage(data, mapPanel.getUIContext().getMapData());
} finally {
data.releaseReadLock();
}
SwingUtilities.invokeLater(() -> mapPanel.repaint());
}
}
}