Files
ATCS/src/com/gpl/rpg/atcontentstudio/ui/map/WorldMapView.java
Nathan Watson ff4d8a6b6c Fix disappearing worldmap labels bug
The "worldmap.labels" map is keyed by namedarea.id (corresponding to map.area), while the "mapLocations" map is keyed by map.id. This causes worldmap labels to be marked for removal when there's a namedarea.id that does not match any map.id, which happens to be true for all namedareas except crossglen and crossroads.

This logic is unnecessary anyway, because WorldMapView.pushToModel is only invoked by WorldmapSegment.pushToModel, which only triggers when maps are added or moved, but not deleted. Deletions are instead handled by WorldmapSegment.elementChanged. As such, it should never be the case that a label is removable in a pushToModel call.
2021-01-04 01:07:57 -08:00

527 lines
19 KiB
Java

package com.gpl.rpg.atcontentstudio.ui.map;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
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.Shape;
import java.awt.Stroke;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.swing.JComponent;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel;
import javax.swing.Scrollable;
import javax.swing.ToolTipManager;
import tiled.view.MapRenderer;
import tiled.view.OrthogonalRenderer;
import com.gpl.rpg.atcontentstudio.model.Project;
import com.gpl.rpg.atcontentstudio.model.maps.MapChange;
import com.gpl.rpg.atcontentstudio.model.maps.MapObject;
import com.gpl.rpg.atcontentstudio.model.maps.MapObjectGroup;
import com.gpl.rpg.atcontentstudio.model.maps.TMXMap;
import com.gpl.rpg.atcontentstudio.model.maps.WorldmapSegment;
public class WorldMapView extends JComponent implements Scrollable {
private static final long serialVersionUID = -4111374378777093799L;
public static final int TILE_SIZE = 32;
public static final int MIN_ZOOM = 5;
public static final int MAX_ZOOM = 250;
public static final int INC_ZOOM = 5;
public static final float ZOOM_RATIO = 0.01f;
WorldmapSegment worldmap;
Project proj;
public Map<String, Rectangle> mapLocations = new LinkedHashMap<String, Rectangle>();
public ListSelectionModel selectedSelectionModel = null;
public ListModel<TMXMap> selectedListModel = null;
public ListModel<TMXMap> highlightedListModel = null;
public float zoomLevel = 0.1f;
int sizeX = 0, sizeY = 0;
int offsetX = 0, offsetY = 0;
static final Color selectOutlineColor = new Color(255, 0, 0);
static final Stroke selectOutlineStroke = new BasicStroke(4f);
static final Color highlightOutlineColor = Color.CYAN;
static final Stroke highlightOutlineStroke = new BasicStroke(4f);
static final Color mapIdLabelOutlineColor = Color.BLACK;
static final Stroke thinLabelOutlineStroke = new BasicStroke(1.5f);
static final Stroke labelOutlineStroke = new BasicStroke(3f);
public WorldMapView(WorldmapSegment worldmap) {
this.worldmap = worldmap;
this.proj = worldmap.getProject();
updateFromModel();
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
String selectedMap = null;
// boolean update = false;
int x = (int) (e.getX() / zoomLevel);
int y = (int) (e.getY() / zoomLevel);
for (String s : mapLocations.keySet()) {
if (mapLocations.get(s).contains(x, y)) {
selectedMap = s;
break;
}
}
if (selectedMap != null) {
x = x - mapLocations.get(selectedMap).x;
y = y - mapLocations.get(selectedMap).y;
//Look for a mapchange there
TMXMap map = proj.getMap(selectedMap);
boolean mapchangeFound = false;
for (MapObjectGroup group : map.groups) {
for (MapObject obj : group.mapObjects) {
if (obj instanceof MapChange) {
if (x >= obj.x && x < obj.x + obj.w && y >= obj.y && y < obj.y + obj.h) {
String mapId = ((MapChange)obj).map != null ? ((MapChange)obj).map.id : ((MapChange)obj).map_id;
mapChangeClicked(e, proj.getMap(selectedMap), proj.getMap(mapId));
mapchangeFound = true;
}
}
}
}
if (!mapchangeFound) {
mapClicked(e, WorldMapView.this.worldmap.getProject().getMap(selectedMap));
}
} else {
backgroundClicked(e);
}
}
});
addMouseMotionListener(new MouseAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
String selectedMap = null;
int x = (int) (e.getX() / zoomLevel);
int y = (int) (e.getY() / zoomLevel);
for (String s : mapLocations.keySet()) {
if (mapLocations.get(s).contains(x, y)) {
selectedMap = s;
break;
}
}
if (selectedMap != null) {
//Reuse x,y to indicate to tile-within-the-map coordinates.
x = x - mapLocations.get(selectedMap).x;
y = y - mapLocations.get(selectedMap).y;
//Look for a mapchange there
TMXMap map = proj.getMap(selectedMap);
boolean mapchangeFound = false;
for (MapObjectGroup group : map.groups) {
for (MapObject obj : group.mapObjects) {
if (obj instanceof MapChange) {
if (x >= obj.x && x < obj.x + obj.w && y >= obj.y && y < obj.y + obj.h) {
String mapId = ((MapChange)obj).map != null ? ((MapChange)obj).map.id : ((MapChange)obj).map_id;
setToolTipText(selectedMap+"->"+mapId);
mapchangeFound = true;
}
}
}
}
if (!mapchangeFound) {
setToolTipText(selectedMap);
}
ToolTipManager.sharedInstance().registerComponent(WorldMapView.this);
ToolTipManager.sharedInstance().setEnabled(true);
} else {
ToolTipManager.sharedInstance().setEnabled(false);
ToolTipManager.sharedInstance().unregisterComponent(WorldMapView.this);
setToolTipText(null);
}
}
});
}
@Override
public Point getToolTipLocation(MouseEvent event) {
return event.getPoint();
}
private void paintOnGraphics(Graphics2D g2) {
g2.setPaint(new Color(100, 100, 100));
g2.fillRect(0, 0, sizeX, sizeY);
g2.setPaint(selectOutlineColor);
g2.setStroke(selectOutlineStroke);
Font areaNameFont = g2.getFont();
areaNameFont = areaNameFont.deriveFont(70f).deriveFont(Font.BOLD);
Font mapIdFont = g2.getFont();
mapIdFont = mapIdFont.deriveFont(50f).deriveFont(Font.BOLD);
g2.setFont(mapIdFont);
FontMetrics mifm = g2.getFontMetrics();
int mapIdLabelHeight = mifm.getHeight();
for (String s : new HashSet<String>(mapLocations.keySet())) {
int x = mapLocations.get(s).x;
int y = mapLocations.get(s).y;
g2.translate(x, y);
TMXMap map = proj.getMap(s);
if (map == null) continue;
MapRenderer renderer = new OrthogonalRenderer(map.tmxMap);
// Draw each tile map layer
for (tiled.core.MapLayer layer : ((TMXMap)map).tmxMap) {
if (layer instanceof tiled.core.TileLayer && layer.isVisible()) {
if (layer.getName().equalsIgnoreCase("walkable")) continue;
renderer.paintTileLayer(g2, (tiled.core.TileLayer) layer);
} else if (layer instanceof tiled.core.ObjectGroup) {
paintObjectGroup(g2, map, (tiled.core.ObjectGroup) layer);
}
}
if (map.colorFilter != null) {
Shape oldClip = g2.getClip();
g2.setClip(0, 0, map.tmxMap.getWidth() * TILE_SIZE, map.tmxMap.getHeight() * TILE_SIZE);
MapColorFilters.applyColorfilter(map.colorFilter, g2);
g2.setClip(oldClip);
}
g2.translate(-x, -y);
}
if (highlightedListModel != null) {
outlineFromListModel(g2, highlightedListModel, null, highlightOutlineColor, highlightOutlineStroke, mapIdFont, mapIdLabelHeight);
}
if (selectedListModel != null && selectedSelectionModel != null) {
outlineFromListModel(g2, selectedListModel, selectedSelectionModel, selectOutlineColor, selectOutlineStroke, mapIdFont, mapIdLabelHeight);
}
g2.setStroke(labelOutlineStroke);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setFont(areaNameFont);
FontMetrics fm = g2.getFontMetrics();
FontRenderContext frc = g2.getFontRenderContext();
for (String s : worldmap.labels.keySet()) {
String label = worldmap.labels.get(s).name;
if (label != null) {
Rectangle areaCovered = new Rectangle(0, 0, -1, -1);
for (String map : worldmap.labelledMaps.get(s)) {
areaCovered.add(mapLocations.get(map));
}
Rectangle2D stringBounds = fm.getStringBounds(label, g2);
GlyphVector gv = areaNameFont.createGlyphVector(frc, label);
g2.setColor(Color.WHITE);
g2.fill(gv.getOutline((int)(areaCovered.getCenterX() - stringBounds.getCenterX()), (int)(areaCovered.getCenterY() - stringBounds.getCenterY())));
g2.setColor(Color.BLACK);
g2.draw(gv.getOutline((int)(areaCovered.getCenterX() - stringBounds.getCenterX()), (int)(areaCovered.getCenterY() - stringBounds.getCenterY())));
}
}
}
private void paintObjectGroup(Graphics2D g2d, TMXMap map, tiled.core.ObjectGroup layer) {
for (MapObjectGroup group : map.groups) {
if (group.tmxGroup == layer) {
for (MapObject object : group.mapObjects) {
if (object instanceof MapChange) {
// Only show mapchange areas pointing to maps not shown in this worldmap
if (((MapChange)object).map != null && !mapLocations.containsKey(((MapChange)object).map.id)) {
drawObject(object, g2d, new Color(20, 20, 190));
}
}
}
break;
}
}
}
private void drawObject(MapObject object, Graphics2D g2d, Color color) {
g2d.setPaint(color);
g2d.drawRect(object.x+1, object.y+1, object.w-3, object.h-3);
g2d.drawRect(object.x+2, object.y+2, object.w-5, object.h-5);
g2d.setPaint(color.darker().darker());
g2d.drawLine(object.x, object.y + object.h - 1, object.x + object.w - 1, object.y + object.h - 1);
g2d.drawLine(object.x + object.w - 1, object.y, object.x + object.w - 1, object.y + object.h - 1);
g2d.drawLine(object.x + 3, object.y + 3, object.x + object.w - 4, object.y + 3);
g2d.drawLine(object.x + 3, object.y + 3, object.x + 3, object.y + object.h - 4);
g2d.setPaint(color.brighter().brighter().brighter());
g2d.drawLine(object.x, object.y, object.x + object.w - 1, object.y);
g2d.drawLine(object.x, object.y, object.x, object.y + object.h - 1);
g2d.drawLine(object.x + 3, object.y + object.h - 4, object.x + object.w - 4, object.y + object.h - 4);
g2d.drawLine(object.x + object.w - 4, object.y + 3, object.x + object.w - 4, object.y + object.h - 4);
Image img = object.getIcon();
g2d.setColor(new Color(255, 255, 255, 120));
g2d.fillRect(object.x + 2, object.y + 2, img.getWidth(null), img.getHeight(null));
g2d.drawImage(object.getIcon(), object.x + 2, object.y + 2, null);
}
private void outlineFromListModel(Graphics2D g2, ListModel<TMXMap> listModel, ListSelectionModel selectionModel, Color outlineColor, Stroke outlineStroke, Font mapIdFont, int mapIdLabelHeight) {
for (int i =0; i<listModel.getSize(); i++) {
//No selection model ? We want to highlight the whole list.
if (selectionModel == null || selectionModel.isSelectedIndex(i)) {
TMXMap map = listModel.getElementAt(i);
int x = mapLocations.get(map.id).x;
int y = mapLocations.get(map.id).y;
g2.translate(x, y);
GlyphVector gv = mapIdFont.createGlyphVector(g2.getFontRenderContext(), map.id);
g2.setStroke(outlineStroke);
g2.setColor(outlineColor);
g2.drawRect(0, 0, map.tmxMap.getWidth() * TILE_SIZE, map.tmxMap.getHeight() * TILE_SIZE);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setStroke(thinLabelOutlineStroke);
g2.fill(gv.getOutline(8, 8 + mapIdLabelHeight));
g2.setColor(mapIdLabelOutlineColor);
g2.draw(gv.getOutline(8, 8 + mapIdLabelHeight));
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
g2.translate(-x, -y);
}
}
}
public List<String> getSelectedMapsIDs() {
List<String> result = new ArrayList<String>();
for (int i =0; i<selectedListModel.getSize(); i++) {
if (selectedSelectionModel.isSelectedIndex(i)) {
TMXMap map = selectedListModel.getElementAt(i);
result.add(map.id);
}
}
return result;
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D)g.create();
try {
g2.scale(zoomLevel, zoomLevel);
// g2.drawImage(img, 0, 0, null);
paintOnGraphics(g2);
} finally {
g2.dispose();
}
}
public interface MapClickListener {
public void mapClicked(MouseEvent e, TMXMap m);
public void mapChangeClicked(MouseEvent e, TMXMap m, TMXMap changeTarget);
public void backgroundClicked(MouseEvent e);
}
private List<MapClickListener> listeners = new CopyOnWriteArrayList<MapClickListener>();
public void addMapClickListener(MapClickListener l) {
listeners.add(l);
}
public void removeMapClickListener(MapClickListener l) {
listeners.remove(l);
}
private void mapClicked(MouseEvent e, TMXMap m) {
for (MapClickListener l : listeners) l.mapClicked(e, m);
}
private void mapChangeClicked(MouseEvent e, TMXMap m, TMXMap changeTarget) {
for (MapClickListener l : listeners) l.mapChangeClicked(e, m, changeTarget);
}
private void backgroundClicked(MouseEvent e) {
for (MapClickListener l : listeners) l.backgroundClicked(e);
}
// private boolean paintObjectGroup(Graphics2D g2d, TMXMap map, tiled.core.ObjectGroup layer) {
// boolean paintSelected = false;
// for (MapObjectGroup group : map.groups) {
// if (group.tmxGroup == layer) {
// for (MapObject object : group.mapObjects) {
// drawObject(object, g2d, new Color(20, 20, 190));
// }
// break;
// }
// }
// return paintSelected;
// }
//
// private void drawObject(MapObject object, Graphics2D g2d, Color color) {
// g2d.setPaint(color);
// g2d.drawRect(object.x+1, object.y+1, object.w-3, object.h-3);
// g2d.drawRect(object.x+2, object.y+2, object.w-5, object.h-5);
// g2d.setPaint(color.darker().darker());
// g2d.drawLine(object.x, object.y + object.h - 1, object.x + object.w - 1, object.y + object.h - 1);
// g2d.drawLine(object.x + object.w - 1, object.y, object.x + object.w - 1, object.y + object.h - 1);
// g2d.drawLine(object.x + 3, object.y + 3, object.x + object.w - 4, object.y + 3);
// g2d.drawLine(object.x + 3, object.y + 3, object.x + 3, object.y + object.h - 4);
// g2d.setPaint(color.brighter().brighter().brighter());
// g2d.drawLine(object.x, object.y, object.x + object.w - 1, object.y);
// g2d.drawLine(object.x, object.y, object.x, object.y + object.h - 1);
// g2d.drawLine(object.x + 3, object.y + object.h - 4, object.x + object.w - 4, object.y + object.h - 4);
// g2d.drawLine(object.x + object.w - 4, object.y + 3, object.x + object.w - 4, object.y + object.h - 4);
// Image img = object.getIcon();
// g2d.setColor(new Color(255, 255, 255, 120));
// g2d.fillRect(object.x + 2, object.y + 2, img.getWidth(null), img.getHeight(null));
// g2d.drawImage(object.getIcon(), object.x + 2, object.y + 2, null);
// }
@Override
public Dimension getPreferredSize() {
return new Dimension((int)(zoomLevel * sizeX), (int)(zoomLevel * sizeY));
}
@Override
public Dimension getPreferredScrollableViewportSize() {
return getPreferredSize();
}
@Override
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
return TILE_SIZE;
}
@Override
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
return 4 * TILE_SIZE;
}
@Override
public boolean getScrollableTracksViewportWidth() {
return false;
}
@Override
public boolean getScrollableTracksViewportHeight() {
return false;
}
public boolean recomputeSize() {
sizeX = sizeY = 0;
boolean originMoved = false;
int minX = Integer.MAX_VALUE, minY= Integer.MAX_VALUE;
for (String s : mapLocations.keySet()) {
int x = mapLocations.get(s).x;
int w = proj.getMap(s).tmxMap.getWidth() * TILE_SIZE;
int y = mapLocations.get(s).y;
int h = proj.getMap(s).tmxMap.getHeight() * TILE_SIZE;
sizeX = Math.max(sizeX, x + w);
sizeY = Math.max(sizeY, y + h);
minX = Math.min(minX, x);
minY = Math.min(minY, y);
}
if (minX != 0) {
for (String s : mapLocations.keySet()) {
mapLocations.get(s).x -= minX;
}
sizeX -= minX;
offsetX += minX;
originMoved = true;
}
if (minY != 0) {
for (String s : mapLocations.keySet()) {
mapLocations.get(s).y -= minY;
}
sizeY -= minY;
offsetY += minY;
originMoved = true;
}
return originMoved;
}
public void updateFromModel() {
mapLocations.clear();
sizeX = sizeY = 0;
offsetX = worldmap.segmentX * TILE_SIZE;
offsetY = worldmap.segmentY * TILE_SIZE;
for (String s : worldmap.mapLocations.keySet()) {
if (proj.getMap(s) == null) {
System.err.println("Warning. Worldmap "+worldmap.id+" references map "+s+" but it doesn't exist in this project");
continue;
}
int x = worldmap.mapLocations.get(s).x * TILE_SIZE;
int w = proj.getMap(s).tmxMap.getWidth() * TILE_SIZE;
int y = worldmap.mapLocations.get(s).y * TILE_SIZE;
int h = proj.getMap(s).tmxMap.getHeight() * TILE_SIZE;
sizeX = Math.max(sizeX, x + w);
sizeY = Math.max(sizeY, y + h);
mapLocations.put(s, new Rectangle(x, y, w, h));
}
}
public void pushToModel() {
worldmap.segmentX = offsetX / TILE_SIZE;
worldmap.segmentY = offsetY / TILE_SIZE;
for (String id : worldmap.mapLocations.keySet()) {
if (worldmap.getProject().getMap(id) == null) {
System.err.println("Warning. Worldmap "+worldmap.id+" references map "+id+" but it doesn't exist in this project");
continue;
}
worldmap.getProject().getMap(id).removeBacklink(worldmap);
}
worldmap.mapLocations.clear();
for (String s : mapLocations.keySet()) {
int x = mapLocations.get(s).x / TILE_SIZE;
int y = mapLocations.get(s).y / TILE_SIZE;
worldmap.mapLocations.put(s, new Point(x, y));
}
for (String id : worldmap.mapLocations.keySet()) {
if (worldmap.getProject().getMap(id) == null) {
System.err.println("Warning. Worldmap "+worldmap.id+" references map "+id+" but it doesn't exist in this project");
continue;
}
worldmap.getProject().getMap(id).addBacklink(worldmap);
}
}
}