/*
* Copyright (c) 2014 tabletoptool.com team.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*
* Contributors:
* rptools.com team - initial implementation
* tabletoptool.com team - further development
*/
package com.t3.client.ui.zone;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.PathIterator;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import javax.swing.JFrame;
import javax.swing.JPanel;
import org.apache.log4j.Logger;
import com.t3.CodeTimer;
import com.t3.client.AppUtil;
import com.t3.client.TabletopTool;
import com.t3.client.ui.zone.vbl.AreaOcean;
import com.t3.client.ui.zone.vbl.AreaTree;
import com.t3.client.ui.zone.vbl.VisibleAreaSegment;
import com.t3.guid.GUID;
import com.t3.model.CellPoint;
import com.t3.model.ExposedAreaMetaData;
import com.t3.model.Path;
import com.t3.model.Player.Role;
import com.t3.model.Token;
import com.t3.model.Zone;
import com.t3.model.ZonePoint;
import com.t3.model.grid.Grid;
import com.t3.model.grid.GridCapabilities;
public class FogUtil {
private static final Logger log = Logger.getLogger(FogUtil.class);
public static Area calculateVisibility(int x, int y, Area vision, AreaTree topology) {
CodeTimer timer = new CodeTimer("calculateVisibility");
vision = new Area(vision);
vision.transform(AffineTransform.getTranslateInstance(x, y));
// sanity check
// if (topology.contains(x, y)) {
// return null;
// }
Point origin = new Point(x, y);
AreaOcean ocean = topology.getOceanAt(origin);
if (ocean == null) {
return null;
}
int skippedAreas = 0;
List<VisibleAreaSegment> segmentList = new ArrayList<VisibleAreaSegment>(ocean.getVisibleAreaSegments(origin));
Collections.sort(segmentList);
List<Area> clearedAreaList = new LinkedList<Area>();
nextSegment:
for (VisibleAreaSegment segment : segmentList) {
Rectangle r = segment.getPath().getBounds();
for (Area clearedArea : clearedAreaList) {
if (clearedArea.contains(r)) {
skippedAreas++;
continue nextSegment;
}
}
Area area = segment.getArea();
timer.start("combine");
Area intersectedArea = null;
for (ListIterator<Area> iter = clearedAreaList.listIterator(); iter.hasNext();) {
Area clearedArea = iter.next();
if (clearedArea.intersects(r)) {
clearedArea.add(area);
iter.remove(); // we'll put it on the back of the list to prevent crazy growth at the front
intersectedArea = clearedArea;
break;
}
}
timer.stop("combine");
clearedAreaList.add(intersectedArea != null ? intersectedArea : area);
}
// blockCount = segmentList.size();
// int metaBlockCount = clearedAreaList.size();
while (clearedAreaList.size() > 1) {
Area a1 = clearedAreaList.remove(0);
Area a2 = clearedAreaList.remove(0);
a1.add(a2);
clearedAreaList.add(a1);
}
if (clearedAreaList.size() > 0) {
vision.subtract(clearedAreaList.get(0));
}
// System.out.println("Blocks: " + blockCount + " Skipped: " + skippedAreas + " metaBlocks: " + metaBlockCount );
// System.out.println(timer);
// For simplicity, this catches some of the edge cases
return vision;
}
// @formatter:off
/*
private static class RelativeLine {
private final Line2D line;
private final double distance;
public RelativeLine(Line2D line, double distance) {
this.line = line;
this.distance = distance;
}
}
private static Area createBlockArea(Point2D origin, Line2D line) {
Point2D p1 = line.getP1();
Point2D p2 = line.getP2();
Point2D p1out = GraphicsUtil.getProjectedPoint(origin, p1, Integer.MAX_VALUE / 2);
Point2D p2out = GraphicsUtil.getProjectedPoint(origin, p2, Integer.MAX_VALUE / 2);
// TODO: Remove the (float) when we move to jdk6
GeneralPath path = new GeneralPath();
path.moveTo((float) p1.getX(), (float) p1.getY());
path.lineTo((float) p2.getX(), (float) p2.getY());
path.lineTo((float) p2out.getX(), (float) p2out.getY());
path.lineTo((float) p1out.getX(), (float) p1out.getY());
path.closePath();
return new Area(path);
}
*/
// @formatter:on
public static void exposeVisibleArea(ZoneRenderer renderer, Set<GUID> tokenSet) {
Zone zone = renderer.getZone();
for (GUID tokenGUID : tokenSet) {
Token token = zone.getToken(tokenGUID);
if (token == null) {
continue;
}
if (!token.getHasSight()) {
continue;
}
if (token.isVisibleOnlyToOwner() && !AppUtil.playerOwns(token)) {
continue;
}
renderer.flush(token);
Area tokenVision = renderer.getVisibleArea(token);
if (tokenVision != null) {
Set<GUID> filteredToks = new HashSet<GUID>();
filteredToks.add(token.getId());
zone.exposeArea(tokenVision, filteredToks);
TabletopTool.serverCommand().exposeFoW(zone.getId(), tokenVision, filteredToks);
}
}
}
public static void exposeVisibleArea(ZoneRenderer renderer, List<Token> tokenSet) {
Zone zone = renderer.getZone();
for (Token token : tokenSet) {
if (!token.getHasSight()) {
continue;
}
if (token.isVisibleOnlyToOwner() && !AppUtil.playerOwns(token)) {
continue;
}
renderer.flush(token);
Area tokenVision = renderer.getVisibleArea(token);
if (tokenVision != null) {
Set<GUID> filteredToks = new HashSet<GUID>();
filteredToks.add(token.getId());
zone.exposeArea(tokenVision, filteredToks);
TabletopTool.serverCommand().exposeFoW(zone.getId(), tokenVision, filteredToks);
}
}
}
public static void exposePCArea(ZoneRenderer renderer) {
Set<GUID> tokenSet = new HashSet<GUID>();
List<Token> tokList = renderer.getZone().getPlayerTokens();
String playerName = TabletopTool.getPlayer().getName();
boolean isGM = TabletopTool.getPlayer().getRole() == Role.GM;
for (Token token : tokList) {
if (!token.getHasSight()) {
continue;
}
boolean owner = token.isOwner(playerName) || isGM;
if ((!TabletopTool.isPersonalServer() || TabletopTool.getServerPolicy().isUseIndividualViews()) && !owner) {
continue;
}
tokenSet.add(token.getId());
}
renderer.getZone().clearExposedArea();
exposeVisibleArea(renderer, tokenSet);
}
public static void exposeLastPath(ZoneRenderer renderer, Set<GUID> tokenSet) {
Zone zone = renderer.getZone();
Grid grid = zone.getGrid();
GridCapabilities caps = grid.getCapabilities();
ZoneView zoneView = renderer.getZoneView();
if (!caps.isPathingSupported() || !caps.isSnapToGridSupported()) {
return;
}
Set<GUID> filteredToks = new HashSet<GUID>(2);
for (GUID tokenGUID : tokenSet) {
Token token = zone.getToken(tokenGUID);
if (token == null) {
continue;
}
if (!token.getHasSight()) {
continue;
}
if (!token.isSnapToGrid()) {
// We don't support this currently
log.warn("Exposing a token's path is not supported for non-SnapToGrid tokens and maps");
continue;
}
@SuppressWarnings("unchecked")
Path<CellPoint> lastPath = (Path<CellPoint>) token.getLastPath();
if (lastPath == null) {
continue;
}
Area visionArea = new Area();
Token tokenClone = new Token(token);
Map<GUID, ExposedAreaMetaData> fullMeta = zone.getExposedAreaMetaData();
GUID exposedGUID = token.getExposedAreaGUID();
ExposedAreaMetaData meta = fullMeta.get(exposedGUID);
if (meta == null) {
meta = new ExposedAreaMetaData();
fullMeta.put(exposedGUID, meta);
}
List<CellPoint> lp = lastPath.getCellPath();
/* Lee: this assumes that all tokens that pass through the checks above stored CellPoints.
Well, they don't, not in the context of a snapped to grid follower following an unsnapped key
token. Commenting out and replacing...*/
// for (CellPoint cell : lastPath.getCellPath()) {
for (Object cell : lastPath.getCellPath()) {
/*Lee: quick fix for a bug that happens when a snapped PC token with vision is following an
unsnapped key token.*/
if (cell instanceof CellPoint) {
ZonePoint zp = grid.convert((CellPoint)cell);
tokenClone.setX(zp.x);
tokenClone.setY(zp.y);
}
Area currVisionArea = zoneView.getVisibleArea(tokenClone);
if (currVisionArea != null) {
visionArea.add(currVisionArea);
meta.addToExposedAreaHistory(currVisionArea);
}
zoneView.flush(tokenClone);
}
renderer.flush(token); // calls ZoneView.flush() -- too bad, I'd like to eliminate it...
filteredToks.clear();
filteredToks.add(token.getId());
// zone.exposeArea(visionArea, filteredToks);
zone.exposeArea(visionArea, token);
zone.putToken(token);
TabletopTool.serverCommand().exposeFoW(zone.getId(), visionArea, filteredToks);
TabletopTool.serverCommand().updateExposedAreaMeta(zone.getId(), exposedGUID, meta);
}
}
/**
* Find the center point of a vision TODO: This is a horrible horrible method. the API is just plain disgusting. But
* it'll work to consolidate all the places this has to be done until we can encapsulate it into the vision itself
*/
public static Point calculateVisionCenter(Token token, Zone zone) {
Grid grid = zone.getGrid();
int x = 0, y = 0;
Rectangle bounds = null;
if (token.isSnapToGrid()) {
bounds = token.getFootprint(grid).getBounds(grid, grid.convert(new ZonePoint(token.getX(), token.getY())));
} else {
bounds = token.getBounds(zone);
}
x = bounds.x + bounds.width / 2;
y = bounds.y + bounds.height / 2;
return new Point(x, y);
}
public static void main(String[] args) {
System.out.println("Creating topology");
final int topSize = 10000;
final Area topology = new Area();
Random r = new Random(12345);
for (int i = 0; i < 500; i++) {
int x = r.nextInt(topSize);
int y = r.nextInt(topSize);
int w = r.nextInt(500) + 50;
int h = r.nextInt(500) + 50;
topology.add(new Area(new Rectangle(x, y, w, h)));
}
// Make sure the the center point is not contained inside the blocked area
topology.subtract(new Area(new Rectangle(topSize / 2 - 200, topSize / 2 - 200, 400, 400)));
final Area vision = new Area(new Rectangle(-Integer.MAX_VALUE / 2, -Integer.MAX_VALUE / 2, Integer.MAX_VALUE, Integer.MAX_VALUE));
int pointCount = 0;
for (PathIterator iter = topology.getPathIterator(null); !iter.isDone(); iter.next()) {
pointCount++;
}
System.out.println("Starting test " + pointCount + " points");
final AreaData data = new AreaData(topology);
data.digest();
final AreaTree tree = new AreaTree(topology);
// Make sure all classes are loaded
calculateVisibility(topSize / 2, topSize / 2, vision, tree);
Area area1 = new Area();
for (int i = 0; i < 1; i++) {
// Return value isn't used except for debugging
area1 = calculateVisibility(topSize / 2, topSize / 2, vision, tree);
}
area1.equals(null); // Eliminates the warning about "area1 never read locally" from Eclipse
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setBounds(0, 0, 400, 200);
f.setLayout(new GridLayout());
f.add(new JPanel() {
BufferedImage topImage = null;
Area theArea = null;
{
addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
final long start = System.currentTimeMillis();
Dimension size = getSize();
int x = (int) ((e.getX() - (size.width / 2)) / (size.width / 2.0 / topSize));
int y = (int) (e.getY() / (size.height / 2.0 / topSize) / 2);
theArea = FogUtil.calculateVisibility(x, y, vision, tree);
System.out.println("Calc: " + (System.currentTimeMillis() - start));
repaint();
}
});
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
final long start = System.currentTimeMillis();
Dimension size = getSize();
int x = (int) ((e.getX() - (size.width / 2)) / (size.width / 2.0 / topSize));
int y = (int) (e.getY() / (size.height / 2.0 / topSize) / 2);
theArea = FogUtil.calculateVisibility(x, y, vision, tree);
System.out.println("Calc: " + (System.currentTimeMillis() - start));
repaint();
}
});
}
@Override
protected void paintComponent(Graphics g) {
Dimension size = getSize();
g.setColor(Color.white);
g.fillRect(0, 0, size.width, size.height);
Graphics2D g2d = (Graphics2D) g;
AffineTransform at = AffineTransform.getScaleInstance((size.width / 2) / (double) topSize, (size.height) / (double) topSize);
if (topImage == null) {
Area top = topology.createTransformedArea(at);
topImage = new BufferedImage(size.width / 2, size.height, BufferedImage.OPAQUE);
Graphics2D g2 = topImage.createGraphics();
g2.setColor(Color.white);
g2.fillRect(0, 0, size.width / 2, size.height);
g2.setColor(Color.green);
g2.fill(top);
g2.dispose();
}
g.setColor(Color.black);
g.drawLine(size.width / 2, 0, size.width / 2, size.height);
// g.setClip(new Rectangle(0, 0, size.width/2, size.height));
// g.setColor(Color.green);
// g2d.fill(top);
//
// g.setColor(Color.lightGray);
// g2d.fill(a1.createTransformedArea(at));
g.setClip(new Rectangle(size.width / 2, 0, size.width / 2, size.height));
g2d.translate(200, 0);
g.setColor(Color.green);
g2d.drawImage(topImage, 0, 0, this);
g.setColor(Color.gray);
if (theArea != null) {
g2d.fill(theArea.createTransformedArea(at));
}
for (AreaMeta areaMeta : data.getAreaList(new Point(0, 0))) {
g.setColor(Color.red);
g2d.draw(areaMeta.area.createTransformedArea(at));
}
// g.setColor(Color.red);
// System.out.println("Size: " + data.metaList.size() + " - " + skippedAreaList.size());
// for (Area area : skippedAreaList) {
// g2d.fill(area.createTransformedArea(at));
// }
g2d.translate(-200, 0);
}
});
f.setVisible(true);
}
}