// Near Infinity - An Infinity Engine Browser and Editor
// Copyright (C) 2001 - 2005 Jon Olav Hauglid
// See LICENSE.txt for license information
package org.infinity.resource.wmp;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.font.LineMetrics;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.Locale;
import javax.swing.BorderFactory;
import javax.swing.DefaultListCellRenderer;
import javax.swing.ImageIcon;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.infinity.NearInfinity;
import org.infinity.datatype.DecNumber;
import org.infinity.datatype.Flag;
import org.infinity.datatype.ResourceRef;
import org.infinity.datatype.SectionCount;
import org.infinity.datatype.SectionOffset;
import org.infinity.datatype.StringRef;
import org.infinity.gui.BrowserMenuBar;
import org.infinity.gui.RenderCanvas;
import org.infinity.gui.ViewerUtil;
import org.infinity.gui.WindowBlocker;
import org.infinity.gui.ViewerUtil.StructListPanel;
import org.infinity.resource.AbstractStruct;
import org.infinity.resource.ResourceFactory;
import org.infinity.resource.graphics.BamDecoder;
import org.infinity.resource.graphics.ColorConvert;
import org.infinity.resource.graphics.MosDecoder;
import org.infinity.resource.graphics.BamDecoder.BamControl;
import org.infinity.resource.key.ResourceEntry;
public class ViewerMap extends JPanel
{
// Needed to determine map edges to travel from/to
private enum Direction { NORTH, WEST, SOUTH, EAST }
private final JPopupMenu pmOptions = new JPopupMenu("Options");
private final JCheckBoxMenuItem miShowIcons = new JCheckBoxMenuItem("Show all map icons", true);
private final JCheckBoxMenuItem miShowDistances = new JCheckBoxMenuItem("Show travel distances", false);
private final BufferedImage iconDot;
private final Listeners listeners = new Listeners();
private final MapEntry mapEntry;
private RenderCanvas rcMap;
private BufferedImage mapOrig;
private BamDecoder mapIcons;
private BamControl mapIconsCtrl;
private StructListPanel listPanel;
private BufferedImage dotBackup;
private int dotX, dotY;
private JLabel lInfoSize, lInfoPos;
ViewerMap(MapEntry wmpMap)
{
super();
WindowBlocker.blockWindow(true);
try {
mapEntry = wmpMap;
// creating marker for selected map icon
iconDot = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = iconDot.createGraphics();
g.setColor(Color.RED);
g.fillRect(0, 0, iconDot.getWidth(), iconDot.getHeight());
g.setColor(Color.YELLOW);
g.drawRect(0, 0, iconDot.getWidth() - 1, iconDot.getHeight() - 1);
g.dispose();
g = null;
dotBackup = new BufferedImage(iconDot.getWidth(), iconDot.getHeight(), iconDot.getType());
dotX = dotY = -1;
miShowIcons.setMnemonic('i');
miShowIcons.addActionListener(listeners);
miShowDistances.setMnemonic('d');
miShowDistances.addActionListener(listeners);
pmOptions.add(miShowIcons);
pmOptions.add(miShowDistances);
try {
mapIcons = null;
ResourceRef iconRef = (ResourceRef)wmpMap.getAttribute(MapEntry.WMP_MAP_ICONS);
if (iconRef != null) {
ResourceEntry iconEntry = ResourceFactory.getResourceEntry(iconRef.getResourceName());
if (iconEntry != null) {
mapIcons = BamDecoder.loadBam(iconEntry);
mapIconsCtrl = mapIcons.createControl();
}
}
if (ResourceFactory.resourceExists(((ResourceRef)wmpMap.getAttribute(MapEntry.WMP_MAP_RESREF)).getResourceName())) {
mapOrig = loadMap();
rcMap = new RenderCanvas(ColorConvert.cloneImage(mapOrig));
rcMap.addMouseListener(listeners);
rcMap.addMouseMotionListener(listeners);
listPanel = (StructListPanel)ViewerUtil.makeListPanel("Areas", wmpMap, AreaEntry.class, AreaEntry.WMP_AREA_CURRENT,
new WmpAreaListRenderer(mapIcons), listeners);
JScrollPane mapScroll = new JScrollPane(rcMap);
mapScroll.getVerticalScrollBar().setUnitIncrement(16);
mapScroll.getHorizontalScrollBar().setUnitIncrement(16);
mapScroll.setBorder(BorderFactory.createEmptyBorder());
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, mapScroll, listPanel);
split.setDividerLocation(NearInfinity.getInstance().getWidth() - 475);
setLayout(new BorderLayout());
add(split, BorderLayout.CENTER);
JPanel pInfo = new JPanel(new FlowLayout(FlowLayout.LEADING, 8, 0));
lInfoSize = new JLabel(String.format("Worldmap size: %1$d x %2$d pixels", mapOrig.getWidth(), mapOrig.getHeight()));
lInfoSize.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
lInfoPos = new JLabel();
lInfoPos.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
pInfo.add(lInfoSize);
pInfo.add(lInfoPos);
add(pInfo, BorderLayout.SOUTH);
} else {
rcMap = null;
mapOrig = null;
}
} catch (Throwable t) {
t.printStackTrace();
}
// applying preselected overlays
showOverlays(miShowIcons.isSelected(), miShowDistances.isSelected());
} finally {
WindowBlocker.blockWindow(false);
}
}
// Returns current map entry structure
private MapEntry getEntry()
{
return mapEntry;
}
// Load and return map graphics
private BufferedImage loadMap()
{
String mapName = ((ResourceRef)getEntry().getAttribute(MapEntry.WMP_MAP_RESREF)).getResourceName();
if (ResourceFactory.resourceExists(mapName)) {
MosDecoder mos = MosDecoder.loadMos(ResourceFactory.getResourceEntry(mapName));
if (mos != null) {
return (BufferedImage)mos.getImage();
}
}
return null;
}
// show popup menu
private void showPopup(Component invoker, int x, int y)
{
pmOptions.show(invoker, x, y);
}
// display either or both map icons and travel distances
private void showOverlays(boolean showIcons, boolean showDistances)
{
resetMap();
if (showIcons) {
showMapIcons();
if (showDistances) {
showMapDistances(listPanel.getList().getSelectedIndex());
}
}
showDot((AreaEntry)listPanel.getList().getSelectedValue(), false);
rcMap.repaint();
}
// Draws map icons onto the map
private void showMapIcons()
{
if (mapIcons != null) {
Graphics2D g = ((BufferedImage)rcMap.getImage()).createGraphics();
try {
g.setFont(g.getFont().deriveFont(g.getFont().getSize2D()*0.9f));
for (int i = 0, count = listPanel.getList().getModel().getSize(); i < count; i++) {
AreaEntry area = getAreaEntry(i);
if (area != null) {
int iconIndex = ((DecNumber)area.getAttribute(AreaEntry.WMP_AREA_ICON_INDEX)).getValue();
int frameIndex = mapIconsCtrl.cycleGetFrameIndexAbsolute(iconIndex, 0);
if (frameIndex >= 0) {
BufferedImage mapIcon = (BufferedImage)mapIcons.frameGet(mapIconsCtrl, frameIndex);
String mapCode = ((ResourceRef)area.getAttribute(AreaEntry.WMP_AREA_CURRENT)).getResourceName();
if (ResourceFactory.resourceExists(mapCode)) {
mapCode = mapCode.replace(".ARE", "");
} else {
mapCode = "";
}
int x = ((DecNumber)area.getAttribute(AreaEntry.WMP_AREA_COORDINATE_X)).getValue();
int y = ((DecNumber)area.getAttribute(AreaEntry.WMP_AREA_COORDINATE_Y)).getValue();
int width = mapIcons.getFrameInfo(frameIndex).getWidth();
int height = mapIcons.getFrameInfo(frameIndex).getHeight();
int cx = mapIcons.getFrameInfo(frameIndex).getCenterX();
int cy = mapIcons.getFrameInfo(frameIndex).getCenterY();
x -= cx;
y -= cy;
g.drawImage(mapIcon, x, y, x+width, y+height, 0, 0, width, height, null);
// printing label
if (!mapCode.isEmpty()) {
LineMetrics lm = g.getFont().getLineMetrics(mapCode, g.getFontRenderContext());
Rectangle2D rectText = g.getFont().getStringBounds(mapCode, g.getFontRenderContext());
int textX = x + (width - rectText.getBounds().width) / 2;
int textY = y + height;
int textWidth = rectText.getBounds().width;
int textHeight = rectText.getBounds().height;
g.setColor(Color.WHITE);
g.fillRect(textX - 2, textY, textWidth + 4, textHeight);
g.setColor(Color.BLACK);
g.drawString(mapCode, (float)textX, (float)textY + lm.getAscent() + lm.getLeading());
}
}
}
}
} finally {
g.dispose();
g = null;
}
}
}
// Displays all map distances from the specified area (by index)
private void showMapDistances(int areaIndex)
{
AreaEntry area = getAreaEntry(areaIndex);
if (area != null) {
final Direction[] srcDir = { Direction.NORTH, Direction.WEST, Direction.SOUTH, Direction.EAST };
final Color[] dirColor = { Color.GREEN, Color.RED, Color.CYAN, Color.YELLOW };
final int[] links = new int[8];
final int linkSize = 216; // size of a single area link structure
int ofsLinkBase = ((SectionOffset)getEntry().getAttribute(MapEntry.WMP_MAP_OFFSET_AREA_LINKS)).getValue();
links[0] = ((DecNumber)area.getAttribute(AreaEntry.WMP_AREA_FIRST_LINK_NORTH)).getValue();
links[1] = ((SectionCount)area.getAttribute(AreaEntry.WMP_AREA_NUM_LINKS_NORTH)).getValue();
links[2] = ((DecNumber)area.getAttribute(AreaEntry.WMP_AREA_FIRST_LINK_WEST)).getValue();
links[3] = ((SectionCount)area.getAttribute(AreaEntry.WMP_AREA_NUM_LINKS_WEST)).getValue();
links[4] = ((DecNumber)area.getAttribute(AreaEntry.WMP_AREA_FIRST_LINK_SOUTH)).getValue();
links[5] = ((SectionCount)area.getAttribute(AreaEntry.WMP_AREA_NUM_LINKS_SOUTH)).getValue();
links[6] = ((DecNumber)area.getAttribute(AreaEntry.WMP_AREA_FIRST_LINK_EAST)).getValue();
links[7] = ((SectionCount)area.getAttribute(AreaEntry.WMP_AREA_NUM_LINKS_EAST)).getValue();
for (int dir = 0; dir < srcDir.length; dir++) {
Direction curDir = srcDir[dir];
Point ptOrigin = getMapIconCoordinate(areaIndex, curDir);
for (int dirIndex = 0, dirCount = links[dir * 2 + 1]; dirIndex < dirCount; dirIndex++) {
int ofsLink = ofsLinkBase + (links[dir * 2] + dirIndex)*linkSize;
AreaLink destLink = (AreaLink)area.getAttribute(ofsLink, false);
if (destLink != null) {
int dstAreaIndex = ((DecNumber)destLink.getAttribute(AreaLink.WMP_LINK_TARGET_AREA)).getValue();
Flag flag = (Flag)destLink.getAttribute(AreaLink.WMP_LINK_DEFAULT_ENTRANCE);
Direction dstDir = Direction.NORTH;
if (flag.isFlagSet(1)) {
dstDir = Direction.EAST;
} else if (flag.isFlagSet(2)) {
dstDir = Direction.SOUTH;
} else if (flag.isFlagSet(3)) {
dstDir = Direction.WEST;
}
Point ptTarget = getMapIconCoordinate(dstAreaIndex, dstDir);
// checking for random encounters during travels
boolean hasRandomEncounters = false;
if (((DecNumber)destLink.getAttribute(AreaLink.WMP_LINK_RANDOM_ENCOUNTER_PROBABILITY)).getValue() > 0) {
for (int rnd = 1; rnd < 6; rnd++) {
String rndArea = ((ResourceRef)destLink
.getAttribute(String.format(AreaLink.WMP_LINK_RANDOM_ENCOUNTER_AREA_FMT, rnd)))
.getResourceName();
if (ResourceFactory.resourceExists(rndArea)) {
hasRandomEncounters = true;
break;
}
}
}
Graphics2D g = ((BufferedImage)rcMap.getImage()).createGraphics();
g.setFont(g.getFont().deriveFont(g.getFont().getSize2D()*0.8f));
try {
// drawing line
g.setColor(dirColor[dir]);
if (hasRandomEncounters) {
g.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 1.0f,
new float[]{6.0f, 4.0f}, 0.0f));
} else {
g.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
}
g.drawLine(ptOrigin.x, ptOrigin.y, ptTarget.x, ptTarget.y);
// printing travel time (in hours)
String duration = String.format("%1$d h", ((DecNumber)destLink.getAttribute(AreaLink.WMP_LINK_DISTANCE_SCALE)).getValue() * 4);
LineMetrics lm = g.getFont().getLineMetrics(duration, g.getFontRenderContext());
Rectangle2D rectText = g.getFont().getStringBounds(duration, g.getFontRenderContext());
int textX = ptOrigin.x + ((ptTarget.x - ptOrigin.x) - rectText.getBounds().width) / 2;
int textY = ptOrigin.y + ((ptTarget.y - ptOrigin.y) - rectText.getBounds().height) / 2;
int textWidth = rectText.getBounds().width;
int textHeight = rectText.getBounds().height;
g.setColor(Color.LIGHT_GRAY);
g.fillRect(textX - 2, textY, textWidth + 4, textHeight);
g.setColor(Color.BLUE);
g.drawString(duration, (float)textX, (float)textY + lm.getAscent() + lm.getLeading());
} finally {
g.dispose();
g = null;
}
}
}
}
}
}
// Returns a pixel coordinate for one of the edges of the specified area icon
private Point getMapIconCoordinate(int areaIndex, Direction dir)
{
AreaEntry area = getAreaEntry(areaIndex);
if (area != null) {
int x = ((DecNumber)area.getAttribute(AreaEntry.WMP_AREA_COORDINATE_X)).getValue();
int y = ((DecNumber)area.getAttribute(AreaEntry.WMP_AREA_COORDINATE_Y)).getValue();
int iconIndex = ((DecNumber)area.getAttribute(AreaEntry.WMP_AREA_ICON_INDEX)).getValue();
int frameIndex = mapIconsCtrl.cycleGetFrameIndexAbsolute(iconIndex, 0);
int width, height;
if (frameIndex >= 0) {
width = mapIcons.getFrameInfo(frameIndex).getWidth();
height = mapIcons.getFrameInfo(frameIndex).getHeight();
x -= mapIcons.getFrameInfo(frameIndex).getCenterX();
y -= mapIcons.getFrameInfo(frameIndex).getCenterY();
} else {
width = height = 0;
}
Point retVal = new Point();
switch (dir) {
case NORTH:
retVal.x = x + (width / 2);
retVal.y = y;
break;
case WEST:
retVal.x = x;
retVal.y = y + (height / 2);
break;
case SOUTH:
retVal.x = x + (width / 2);
retVal.y = y + height - 1;
break;
case EAST:
retVal.x = x + width - 1;
retVal.y = y + (height / 2);
break;
}
return retVal;
}
return null;
}
// Returns area structure of specified item index
private AreaEntry getAreaEntry(int index)
{
if (index >= 0 && index < listPanel.getList().getModel().getSize()) {
return (AreaEntry)listPanel.getList().getModel().getElementAt(index);
} else {
return null;
}
}
// Show "dot" on specified map icon, optionally restore background graphics
private void showDot(AreaEntry entry, boolean restore)
{
if (restore) {
restoreDot();
}
if (entry != null) {
storeDot(entry);
int x = ((DecNumber)entry.getAttribute(AreaEntry.WMP_AREA_COORDINATE_X)).getValue();
int y = ((DecNumber)entry.getAttribute(AreaEntry.WMP_AREA_COORDINATE_Y)).getValue();
int width = iconDot.getWidth();
int height = iconDot.getHeight();
int xofs = width / 2;
int yofs = height / 2;
Graphics2D g = ((BufferedImage)rcMap.getImage()).createGraphics();
try {
g.drawImage(iconDot, x-xofs, y-yofs, x-xofs+width, y-yofs+height, 0, 0, width, height, null);
} finally {
g.dispose();
g = null;
}
}
}
// Stores background graphics of "dot"
private void storeDot(AreaEntry entry)
{
if (entry != null) {
int x = ((DecNumber)entry.getAttribute(AreaEntry.WMP_AREA_COORDINATE_X)).getValue();
int y = ((DecNumber)entry.getAttribute(AreaEntry.WMP_AREA_COORDINATE_Y)).getValue();
int width = dotBackup.getWidth();
int height = dotBackup.getHeight();
int xofs = width / 2;
int yofs = height / 2;
Graphics2D g = dotBackup.createGraphics();
try {
g.drawImage(rcMap.getImage(), 0, 0, width, height, x-xofs, y-yofs, x-xofs+width, y-yofs+height, null);
dotX = x-xofs;
dotY = y-yofs;
} finally {
g.dispose();
g = null;
}
}
}
// Restores background graphics of "dot"
private void restoreDot()
{
if (dotX != -1 && dotY != -1) {
int x = dotX;
int y = dotY;
int width = dotBackup.getWidth();
int height = dotBackup.getHeight();
Graphics2D g = ((BufferedImage)rcMap.getImage()).createGraphics();
try {
g.drawImage(dotBackup, x, y, x+width, y+height, 0, 0, width, height, null);
dotX = -1;
dotY = -1;
} finally {
g.dispose();
g = null;
}
}
}
// Attempts to restore the whole map graphics
private void resetMap()
{
Graphics2D g = ((BufferedImage)rcMap.getImage()).createGraphics();
try {
g.drawImage(mapOrig, 0, 0, null);
} finally {
g.dispose();
g = null;
}
}
// Shows specified coordinates as text info. Hides display for negative coordinates.
private void updateCursorInfo(int x, int y)
{
if (lInfoPos != null) {
if (x >= 0 && y >= 0) {
lInfoPos.setText(String.format("Cursor at (%1$d, %2$d)", x, y));
} else {
lInfoPos.setText("");
}
}
}
//-------------------------- INNER CLASSES --------------------------
private class Listeners implements ActionListener, MouseListener, MouseMotionListener, ListSelectionListener
{
//--------------------- Begin Interface ActionListener ---------------------
@Override
public void actionPerformed(ActionEvent e)
{
if (e.getSource() == miShowIcons) {
try {
WindowBlocker.blockWindow(true);
if (!miShowIcons.isSelected() && miShowDistances.isSelected()) {
miShowDistances.setSelected(false);
}
showOverlays(miShowIcons.isSelected(), miShowDistances.isSelected());
} finally {
WindowBlocker.blockWindow(false);
}
} else if (e.getSource() == miShowDistances) {
try {
WindowBlocker.blockWindow(true);
if (miShowDistances.isSelected() && !miShowIcons.isSelected()) {
miShowIcons.setSelected(true);
}
showOverlays(miShowIcons.isSelected(), miShowDistances.isSelected());
} finally {
WindowBlocker.blockWindow(false);
}
}
}
//--------------------- End Interface ActionListener ---------------------
//--------------------- Begin Interface MouseListener ---------------------
@Override
public void mouseClicked(MouseEvent e) {}
@Override
public void mousePressed(MouseEvent e)
{
if (e.isPopupTrigger() && e.getComponent() == rcMap) {
showPopup(e.getComponent(), e.getX(), e.getY());
}
}
@Override
public void mouseReleased(MouseEvent e)
{
if (e.isPopupTrigger() && e.getComponent() == rcMap) {
showPopup(e.getComponent(), e.getX(), e.getY());
}
}
@Override
public void mouseEntered(MouseEvent e) {}
@Override
public void mouseExited(MouseEvent e) {}
//--------------------- End Interface MouseListener ---------------------
//--------------------- Begin Interface MouseMotionListener ---------------------
@Override
public void mouseDragged(MouseEvent e)
{
}
@Override
public void mouseMoved(MouseEvent e)
{
if (e.getSource() == rcMap) {
int ctrlWidth = rcMap.getWidth();
int ctrlHeight = rcMap.getHeight();
Image image = rcMap.getImage();
int imgWidth = image.getWidth(null);
int imgHeight = image.getHeight(null);
int startX = (ctrlWidth - imgWidth) / 2;
int startY = (ctrlHeight - imgHeight) / 2;
int x = e.getX();
int y = e.getY();
if (x >= startX && x < startX + imgWidth && y >= startY && y < startY + imgHeight) {
updateCursorInfo(x - startX, y - startY);
} else {
updateCursorInfo(-1, -1);
}
}
}
//--------------------- End Interface MouseMotionListener ---------------------
// --------------------- Begin Interface ListSelectionListener ---------------------
@Override
public void valueChanged(ListSelectionEvent event)
{
if (!event.getValueIsAdjusting()) {
JList<?> list = (JList<?>)event.getSource();
if (miShowDistances.isSelected()) {
showOverlays(miShowIcons.isSelected(), miShowDistances.isSelected());
} else {
showDot((AreaEntry)list.getSelectedValue(), true);
}
repaint();
}
}
// --------------------- End Interface ListSelectionListener ---------------------
}
private static final class WmpAreaListRenderer extends DefaultListCellRenderer
{
private final BamDecoder bam;
private final BamControl ctrl;
private WmpAreaListRenderer(BamDecoder decoder)
{
bam = decoder;
ctrl = (bam != null) ? bam.createControl() : null;
}
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected,
boolean cellHasFocus)
{
JLabel label = (JLabel)super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
AbstractStruct struct = (AbstractStruct)value;
StringRef areaName = (StringRef)struct.getAttribute(AreaEntry.WMP_AREA_NAME);
ResourceRef areaRef = (ResourceRef)struct.getAttribute(AreaEntry.WMP_AREA_CURRENT);
String text1 = null, text2 = null;
if (areaName.getValue() >= 0) {
text1 = areaName.toString(BrowserMenuBar.getInstance().showStrrefs());
} else {
text1 = "";
}
text2 = areaRef.getResourceName();
if (!text2.equalsIgnoreCase("NONE")) {
text2 = text2.toUpperCase(Locale.ENGLISH).replace(".ARE", "");
}
label.setText(String.format("[%1$s] %2$s", text2, text1));
DecNumber animNr = (DecNumber)struct.getAttribute(AreaEntry.WMP_AREA_ICON_INDEX);
setIcon(null);
if (ctrl != null) {
setIcon(new ImageIcon(bam.frameGet(ctrl, ctrl.cycleGetFrameIndexAbsolute(animNr.getValue(), 0))));
}
return label;
}
}
}