/*
* 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.Point;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.t3.client.AppState;
import com.t3.client.AppUtil;
import com.t3.client.TabletopTool;
import com.t3.client.ui.zone.vbl.AreaTree;
import com.t3.guid.GUID;
import com.t3.model.AttachedLightSource;
import com.t3.model.Direction;
import com.t3.model.Light;
import com.t3.model.LightSource;
import com.t3.model.ModelChangeEvent;
import com.t3.model.ModelChangeListener;
import com.t3.model.SightType;
import com.t3.model.Token;
import com.t3.model.Zone;
import com.t3.model.Zone.TokenFilter;
public class ZoneView implements ModelChangeListener {
private final Zone zone;
// VISION
private final Map<GUID, Area> tokenVisibleAreaCache = new HashMap<GUID, Area>();
private final Map<GUID, Area> tokenVisionCache = new HashMap<GUID, Area>();
private final Map<GUID, Map<String, Area>> lightSourceCache = new HashMap<GUID, Map<String, Area>>();
private final Map<LightSource.Type, Set<GUID>> lightSourceMap = new HashMap<LightSource.Type, Set<GUID>>();
private final Map<GUID, Map<String, Set<DrawableLight>>> drawableLightCache = new HashMap<GUID, Map<String, Set<DrawableLight>>>();
private final Map<GUID, Map<String, Set<Area>>> brightLightCache = new Hashtable<GUID, Map<String, Set<Area>>>();
private final Map<PlayerView, VisibleAreaMeta> visibleAreaMap = new HashMap<PlayerView, VisibleAreaMeta>();
private AreaData topologyAreaData;
private AreaTree topology;
public ZoneView(Zone zone) {
this.zone = zone;
findLightSources();
zone.addModelChangeListener(this);
}
public Area getVisibleArea(PlayerView view) {
calculateVisibleArea(view);
ZoneView.VisibleAreaMeta visible = visibleAreaMap.get(view);
// if (visible == null)
// System.out.println("ZoneView: visible == null. Please report this on our forum @ forum.rptools.net. Thank you!");
return visible != null ? visible.visibleArea : new Area();
}
public boolean isUsingVision() {
return zone.getVisionType() != Zone.VisionType.OFF;
}
public AreaTree getTopology() {
if (topology == null) {
topology = new AreaTree(zone.getTopology());
}
return topology;
}
public AreaData getTopologyAreaData() {
if (topologyAreaData == null) {
topologyAreaData = new AreaData(zone.getTopology());
topologyAreaData.digest();
}
return topologyAreaData;
}
public Area getLightSourceArea(Token token, Token lightSourceToken) {
// Cached ?
Map<String, Area> areaBySightMap = lightSourceCache.get(lightSourceToken.getId());
if (areaBySightMap != null) {
Area lightSourceArea = areaBySightMap.get(token.getSightType());
if (lightSourceArea != null) {
return lightSourceArea;
}
} else {
areaBySightMap = new HashMap<String, Area>();
lightSourceCache.put(lightSourceToken.getId(), areaBySightMap);
}
// Calculate
Area area = new Area();
for (AttachedLightSource attachedLightSource : lightSourceToken.getLightSources()) {
LightSource lightSource = attachedLightSource.getLightSource();
if (lightSource == null) {
continue;
}
SightType sight = TabletopTool.getCampaign().getSightType(token.getSightType());
Area visibleArea = calculateLightSourceArea(lightSource, lightSourceToken, sight, attachedLightSource.getDirection());
// I don't like the NORMAL check here, it doesn't feel right, the API needs to change to support
// getting arbitrary light source types, but that's not a simple change
if (visibleArea != null && lightSource.getType() == LightSource.Type.NORMAL) {
area.add(visibleArea);
}
}
// Cache
areaBySightMap.put(token.getSightType(), area);
return area;
}
private Area calculatePersonalLightSourceArea(LightSource lightSource, Token lightSourceToken, SightType sight, Direction direction) {
return calculateLightSourceArea(lightSource, lightSourceToken, sight, direction, true);
}
private Area calculateLightSourceArea(LightSource lightSource, Token lightSourceToken, SightType sight, Direction direction) {
return calculateLightSourceArea(lightSource, lightSourceToken, sight, direction, false);
}
private Area calculateLightSourceArea(LightSource lightSource, Token lightSourceToken, SightType sight, Direction direction, boolean isPersonalLight) {
if (sight == null) {
return null;
}
Point p = FogUtil.calculateVisionCenter(lightSourceToken, zone);
Area lightSourceArea = lightSource.getArea(lightSourceToken, zone, direction);
// Calculate exposed area
// TODO: This won't work with directed light, need to add an anchor or something
if (sight.getMultiplier() != 1) {
lightSourceArea.transform(AffineTransform.getScaleInstance(sight.getMultiplier(), sight.getMultiplier()));
}
Area visibleArea = FogUtil.calculateVisibility(p.x, p.y, lightSourceArea, getTopology());
if (visibleArea == null) {
return null;
}
if (lightSource.getType() != LightSource.Type.NORMAL) {
return visibleArea;
}
// Keep track of colored light
Set<DrawableLight> lightSet = new HashSet<DrawableLight>();
Set<Area> brightLightSet = new HashSet<Area>();
for (Light light : lightSource.getLightList()) {
Area lightArea = lightSource.getArea(lightSourceToken, zone, direction, light);
if (sight.getMultiplier() != 1) {
lightArea.transform(AffineTransform.getScaleInstance(sight.getMultiplier(), sight.getMultiplier()));
}
lightArea.transform(AffineTransform.getTranslateInstance(p.x, p.y));
lightArea.intersect(visibleArea);
if (light.getPaint() != null || isPersonalLight) {
lightSet.add(new DrawableLight(lightSource.getType(), light.getPaint(), lightArea));
} else {
brightLightSet.add(lightArea);
}
}
// FIXME There was a bug report of a ConcurrentModificationException regarding drawableLightCache.
// I don't see how, but perhaps this code -- and the ones in flush() and flush(Token) -- should be
// wrapped in a synchronization block? This method is probably called only on the same thread as
// getDrawableLights() but the two flush() methods may be called from different threads. How to
// verify this with Eclipse? Maybe the flush() methods should defer modifications to the EventDispatchingThread?
Map<String, Set<DrawableLight>> lightMap = drawableLightCache.get(lightSourceToken.getId());
if (lightMap == null) {
lightMap = new HashMap<String, Set<DrawableLight>>();
drawableLightCache.put(lightSourceToken.getId(), lightMap);
}
if (lightMap.get(sight.getName()) != null) {
lightMap.get(sight.getName()).addAll(lightSet);
} else {
lightMap.put(sight.getName(), lightSet);
}
Map<String, Set<Area>> brightLightMap = brightLightCache.get(lightSourceToken.getId());
if (brightLightMap == null) {
brightLightMap = new HashMap<String, Set<Area>>();
brightLightCache.put(lightSourceToken.getId(), brightLightMap);
}
if (brightLightMap.get(sight.getName()) != null) {
brightLightMap.get(sight.getName()).addAll(brightLightSet);
} else {
brightLightMap.put(sight.getName(), brightLightSet);
}
return visibleArea;
}
public Area getVisibleArea(Token token) {
// Sanity
if (token == null || !token.getHasSight()) {
return null;
}
// Cache ?
Area tokenVisibleArea = tokenVisionCache.get(token.getId());
if (tokenVisibleArea != null) {
return tokenVisibleArea;
}
SightType sight = TabletopTool.getCampaign().getSightType(token.getSightType());
// More sanity checks; maybe sight type removed from campaign after token set?
if (sight == null) {
// TODO Should we turn off the token's HasSight flag? Would speed things up for later...
return null;
}
// Combine the player visible area with the available light sources
tokenVisibleArea = tokenVisibleAreaCache.get(token.getId());
if (tokenVisibleArea == null) {
Point p = FogUtil.calculateVisionCenter(token, zone);
Area visibleArea = sight.getVisionShape(token, zone);
tokenVisibleArea = FogUtil.calculateVisibility(p.x, p.y, visibleArea, getTopology());
tokenVisibleAreaCache.put(token.getId(), tokenVisibleArea);
}
// Combine in the visible light areas
if (tokenVisibleArea != null && zone.getVisionType() == Zone.VisionType.NIGHT) {
Rectangle2D origBounds = tokenVisibleArea.getBounds();
// Combine all light sources that might intersect our vision
List<Area> intersects = new LinkedList<Area>();
List<Token> lightSourceTokens = new ArrayList<Token>();
if (lightSourceMap.get(LightSource.Type.NORMAL) != null) {
for (GUID lightSourceTokenId : lightSourceMap.get(LightSource.Type.NORMAL)) {
Token lightSourceToken = zone.getToken(lightSourceTokenId);
if (lightSourceToken != null) {
lightSourceTokens.add(lightSourceToken);
}
}
}
if (token.hasLightSources() && !lightSourceTokens.contains(token)) {
// This accounts for temporary tokens (such as during an Expose Last Path)
lightSourceTokens.add(token);
}
for (Token lightSourceToken : lightSourceTokens) {
Area lightArea = getLightSourceArea(token, lightSourceToken);
if (origBounds.intersects(lightArea.getBounds2D())) {
Area intersection = new Area(tokenVisibleArea);
intersection.intersect(lightArea);
intersects.add(intersection);
}
}
// Check for personal vision
if (sight.hasPersonalLightSource()) {
Area lightArea = calculatePersonalLightSourceArea(sight.getPersonalLightSource(), token, sight, Direction.CENTER);
if (lightArea != null) {
Area intersection = new Area(tokenVisibleArea);
intersection.intersect(lightArea);
intersects.add(intersection);
}
}
while (intersects.size() > 1) {
Area a1 = intersects.remove(0);
Area a2 = intersects.remove(0);
a1.add(a2);
intersects.add(a1);
}
tokenVisibleArea = !intersects.isEmpty() ? intersects.get(0) : new Area();
}
tokenVisionCache.put(token.getId(), tokenVisibleArea);
return tokenVisibleArea;
}
public List<DrawableLight> getLights(LightSource.Type type) {
List<DrawableLight> lightList = new LinkedList<DrawableLight>();
if (lightSourceMap.get(type) != null) {
for (GUID lightSourceToken : lightSourceMap.get(type)) {
Token token = zone.getToken(lightSourceToken);
if (token == null) {
continue;
}
Point p = FogUtil.calculateVisionCenter(token, zone);
for (AttachedLightSource als : token.getLightSources()) {
LightSource lightSource = als.getLightSource();
if (lightSource == null) {
continue;
}
if (lightSource.getType() == type) {
// This needs to be cached somehow
Area lightSourceArea = lightSource.getArea(token, zone, Direction.CENTER);
Area visibleArea = FogUtil.calculateVisibility(p.x, p.y, lightSourceArea, getTopology());
if (visibleArea == null) {
continue;
}
for (Light light : lightSource.getLightList()) {
boolean isOwner = token.getOwners().contains(TabletopTool.getPlayer().getName());
if ((light.isGM() && !TabletopTool.getPlayer().isGM())) {
continue;
}
if ((light.isGM() || !token.isVisible()) && TabletopTool.getPlayer().isGM() && AppState.isShowAsPlayer()) {
continue;
}
if (token.isVisibleOnlyToOwner() && !AppUtil.playerOwns(token)) {
continue;
}
if (light.isOwnerOnly() && lightSource.getType() == LightSource.Type.AURA) {
if (!isOwner && !TabletopTool.getPlayer().isGM()) {
continue;
}
}
lightList.add(new DrawableLight(type, light.getPaint(), visibleArea));
}
}
}
}
}
return lightList;
}
private void findLightSources() {
lightSourceMap.clear();
for (Token token : zone.getAllTokens()) {
if (token.hasLightSources() && token.isVisible())
if (!token.isVisibleOnlyToOwner() || (token.isVisibleOnlyToOwner() && AppUtil.playerOwns(token))) {
for (AttachedLightSource als : token.getLightSources()) {
LightSource lightSource = als.getLightSource();
if (lightSource == null) {
continue;
}
Set<GUID> lightSet = lightSourceMap.get(lightSource.getType());
if (lightSet == null) {
lightSet = new HashSet<GUID>();
lightSourceMap.put(lightSource.getType(), lightSet);
}
lightSet.add(token.getId());
}
}
}
}
public Set<DrawableLight> getDrawableLights() {
Set<DrawableLight> lightSet = new HashSet<DrawableLight>();
for (Map<String, Set<DrawableLight>> map : drawableLightCache.values()) {
for (Set<DrawableLight> set : map.values()) {
lightSet.addAll(set);
}
}
return lightSet;
}
public Set<Area> getBrightLights() {
Set<Area> lightSet = new HashSet<Area>();
// MJ: There seems to be contention for this cache, but that looks inconspicuous enough to
// try this easy way out. Better: solve the synchronization issues.
Collection<Map<String, Set<Area>>> copy = new ArrayList<Map<String, Set<Area>>>(brightLightCache.values());
for (Map<String, Set<Area>> map : copy) {
for (Set<Area> set : map.values()) {
lightSet.addAll(set);
}
}
return lightSet;
}
public void flush() {
tokenVisibleAreaCache.clear();
tokenVisionCache.clear();
lightSourceCache.clear();
visibleAreaMap.clear();
drawableLightCache.clear();
brightLightCache.clear();
}
public void flush(Token token) {
boolean hadLightSource = lightSourceCache.get(token.getId()) != null;
tokenVisionCache.remove(token.getId());
tokenVisibleAreaCache.remove(token.getId());
lightSourceCache.remove(token.getId());
drawableLightCache.remove(token.getId());
brightLightCache.remove(token.getId());
visibleAreaMap.clear();
if (hadLightSource || token.hasLightSources()) {
// Have to recalculate all token vision
tokenVisionCache.clear();
}
if (token.getHasSight()) {
visibleAreaMap.clear();
}
// TODO: This fixes a bug with changing vision type, I don't like it though, it needs to be optimized back out
// lightSourceCache.clear();
}
private void calculateVisibleArea(PlayerView view) {
if (visibleAreaMap.get(view) != null && visibleAreaMap.get(view).visibleArea.getBounds().getCenterX() != 0.0d) {
return;
}
// Cache it
VisibleAreaMeta meta = new VisibleAreaMeta();
meta.visibleArea = new Area();
visibleAreaMap.put(view, meta);
// Calculate it
final boolean isGMview = view.isGMView();
final boolean checkOwnership = TabletopTool.getServerPolicy().isUseIndividualViews() || TabletopTool.isPersonalServer();
List<Token> tokenList = view.isUsingTokenView() ? view.getTokens() : zone.getTokensFiltered(new TokenFilter() {
@Override
public boolean filter(Token t) {
return t.isToken() && t.getHasSight() && (isGMview || t.isVisible());
}
});
for (Token token : tokenList) {
boolean weOwnIt = AppUtil.playerOwns(token);
// Permission
if (checkOwnership) {
if (!weOwnIt) {
continue;
}
} else {
// If we're viewing the map as a player and the token is not a PC or we're not the GM, then skip it.
// This used to be the code:
// if ((token.getType() != Token.Type.PC && !view.isGMView() || (!view.isGMView() && TabletopTool.getPlayer().getRole() == Role.GM))) {
if (!isGMview && (token.getType() != Token.Type.PC || TabletopTool.getPlayer().isGM())) {
continue;
}
}
// player ownership permission
if (token.isVisibleOnlyToOwner() && !weOwnIt) {
continue;
}
Area tokenVision = getVisibleArea(token);
if (tokenVision != null) {
meta.visibleArea.add(tokenVision);
}
}
}
////
// MODEL CHANGE LISTENER
@Override
public void modelChanged(ModelChangeEvent event) {
Object evt = event.getEvent();
if (event.getModel() instanceof Zone) {
if (evt == Zone.Event.TOPOLOGY_CHANGED) {
tokenVisionCache.clear();
lightSourceCache.clear();
visibleAreaMap.clear();
topologyAreaData = null;
topology = null;
tokenVisibleAreaCache.clear();
}
if (evt == Zone.Event.TOKEN_CHANGED || evt == Zone.Event.TOKEN_REMOVED) {
if (event.getArg() instanceof List<?>) {
@SuppressWarnings("unchecked")
List<Token> list = (List<Token>) (event.getArg());
for (Token token : list) {
flush(token);
}
} else {
flush((Token) event.getArg());
}
}
if (evt == Zone.Event.TOKEN_ADDED || evt == Zone.Event.TOKEN_CHANGED) {
Object o = event.getArg();
List<Token> tokens = null;
if (o instanceof Token) {
tokens = new ArrayList<Token>(1);
tokens.add((Token) o);
} else {
tokens = (List<Token>) o;
}
processTokenAddChangeEvent(tokens);
}
if (evt == Zone.Event.TOKEN_REMOVED) {
Token token = (Token) event.getArg();
for (AttachedLightSource als : token.getLightSources()) {
LightSource lightSource = als.getLightSource();
if (lightSource == null) {
continue;
}
Set<GUID> lightSet = lightSourceMap.get(lightSource.getType());
if (lightSet != null) {
lightSet.remove(token.getId());
}
}
}
}
}
/**
*
*/
private void processTokenAddChangeEvent(List<Token> tokens) {
boolean hasSight = false;
for (Token token : tokens) {
boolean hasLightSource = token.hasLightSources() && (token.isVisible() || (TabletopTool.getPlayer().isGM() && !AppState.isShowAsPlayer()));
for (AttachedLightSource als : token.getLightSources()) {
LightSource lightSource = als.getLightSource();
if (lightSource != null) {
Set<GUID> lightSet = lightSourceMap.get(lightSource.getType());
if (hasLightSource) {
if (lightSet == null) {
lightSet = new HashSet<GUID>();
lightSourceMap.put(lightSource.getType(), lightSet);
}
lightSet.add(token.getId());
} else if (lightSet != null)
lightSet.remove(token.getId());
}
}
hasSight |= token.getHasSight();
}
if (hasSight)
visibleAreaMap.clear();
}
private static class VisibleAreaMeta {
Area visibleArea;
}
}