/*
* Copyright 2013 MovingBlocks
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.terasology.monitoring.gui;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.logic.players.LocalPlayer;
import org.terasology.math.ChunkMath;
import org.terasology.math.geom.Vector3i;
import org.terasology.monitoring.ThreadActivity;
import org.terasology.monitoring.ThreadMonitor;
import org.terasology.monitoring.chunk.ChunkMonitor;
import org.terasology.monitoring.chunk.ChunkMonitorEntry;
import org.terasology.monitoring.chunk.ChunkMonitorEvent;
import org.terasology.registry.CoreRegistry;
import org.terasology.world.chunks.Chunk;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.BufferedImage;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@SuppressWarnings("serial")
public class ChunkMonitorDisplay extends JPanel {
public static final Color COLOR_COMPLETE = new Color(0, 38, 28);
public static final Color COLOR_INTERNAL_LIGHT_GENERATION_PENDING = new Color(4, 76, 41);
public static final Color COLOR_ADJACENCY_GENERATION_PENDING = new Color(150, 237, 137);
public static final Color COLOR_HIGHLIGHT_TESSELLATION = Color.blue.brighter().brighter();
public static final Color COLOR_SELECTED_CHUNK = new Color(255, 102, 0);
public static final Color COLOR_DEAD = Color.lightGray;
public static final Color COLOR_INVALID = Color.red;
private static final Logger logger = LoggerFactory.getLogger(ChunkMonitorDisplay.class);
private final EventBus eventbus = new EventBus("ChunkMonitorDisplay");
private final List<ChunkMonitorEntry> chunks = Lists.newArrayList();
private final Map<Vector3i, ChunkMonitorEntry> map = Maps.newHashMap();
private final ImageBuffer image = new ImageBuffer();
private int refreshInterval;
private int centerOffsetX;
private int centerOffsetY;
private int offsetX;
private int offsetY;
private int chunkSize;
private int renderY;
private int minRenderY;
private int maxRenderY;
private boolean followPlayer = true;
private Vector3i selectedChunk;
private final BlockingQueue<Request> queue = new LinkedBlockingQueue<>();
private final transient ExecutorService executor;
private final transient Runnable renderTask;
public ChunkMonitorDisplay(int refreshInterval, int chunkSize) {
Preconditions.checkArgument(refreshInterval >= 500, "Parameter 'refreshInterval' has to be greater or equal 500 (" + refreshInterval + ")");
Preconditions.checkArgument(chunkSize >= 6, "Parameter 'chunkSize' has to be greater or equal 6 (" + chunkSize + ")");
addComponentListener(new ResizeListener());
final MouseInputListener ml = new MouseInputListener();
addMouseListener(ml);
addMouseMotionListener(ml);
addMouseWheelListener(ml);
this.refreshInterval = refreshInterval;
this.chunkSize = chunkSize;
this.executor = Executors.newSingleThreadExecutor();
this.renderTask = new RenderTask();
ChunkMonitor.registerForEvents(this);
queue.offer(new InitialRequest());
executor.execute(renderTask);
}
private void fireChunkSelectedEvent(Vector3i pos) {
eventbus.post(new ChunkMonitorDisplayEvent.Selected(this, pos, pos == null ? null : map.get(pos)));
}
private Vector3i mouseToChunkPos(Point p) {
Preconditions.checkNotNull(p, "The parameter 'p' must not be null");
int x = (p.x - centerOffsetX - offsetX) / chunkSize;
int z = (p.y - centerOffsetY - offsetY) / chunkSize;
return new Vector3i(x - 1, renderY, z);
}
private void updateDisplay() {
queue.offer(new RenderRequest());
}
private void updateDisplay(boolean fastResume) {
queue.offer(new RenderRequest(fastResume));
}
private void recomputeRenderY() {
int min = 0;
int max = 0;
int y = renderY;
for (ChunkMonitorEntry chunk : chunks) {
final Vector3i pos = chunk.getPosition();
if (pos.y < min) {
min = pos.y;
}
if (pos.y > max) {
max = pos.y;
}
}
if (y < min) {
y = min;
}
if (y > max) {
y = max;
}
minRenderY = min;
maxRenderY = max;
renderY = y;
}
private Vector3i calcPlayerChunkPos() {
final LocalPlayer p = CoreRegistry.get(LocalPlayer.class);
if (p != null) {
return ChunkMath.calcChunkPos(new Vector3i(p.getPosition()));
}
return null;
}
public int getChunkSize() {
return chunkSize;
}
public ChunkMonitorDisplay setChunkSize(int value) {
if (value != chunkSize) {
Preconditions.checkArgument(value >= 6, "Parameter 'value' has to be greater or equal 6 (" + value + ")");
chunkSize = value;
updateDisplay(true);
}
return this;
}
public Vector3i getSelectedChunk() {
if (selectedChunk == null) {
return null;
}
return new Vector3i(selectedChunk);
}
public ChunkMonitorDisplay setSelectedChunk(Vector3i chunk) {
if (selectedChunk == null) {
if (chunk != null) {
selectedChunk = chunk;
updateDisplay(true);
fireChunkSelectedEvent(chunk);
}
} else {
if (chunk == null || !selectedChunk.equals(chunk)) {
selectedChunk = chunk;
updateDisplay(true);
fireChunkSelectedEvent(chunk);
}
}
return this;
}
public int getRenderY() {
return renderY;
}
public int getMinRenderY() {
return minRenderY;
}
public int getMaxRenderY() {
return maxRenderY;
}
public ChunkMonitorDisplay setRenderY(int value) {
int clampedValue = value;
if (value < minRenderY) {
clampedValue = minRenderY;
}
if (value > maxRenderY) {
clampedValue = maxRenderY;
}
if (renderY != clampedValue) {
renderY = clampedValue;
updateDisplay(true);
}
return this;
}
public ChunkMonitorDisplay setRenderYDelta(int delta) {
return setRenderY(renderY + delta);
}
public boolean getFollowPlayer() {
return followPlayer;
}
public ChunkMonitorDisplay setFollowPlayer(boolean value) {
if (followPlayer != value) {
followPlayer = value;
updateDisplay();
}
return this;
}
public int getOffsetX() {
return offsetX;
}
public int getOffsetY() {
return offsetY;
}
public ChunkMonitorDisplay setOffset(int x, int y) {
if (offsetX != x || offsetY != y) {
this.offsetX = x;
this.offsetY = y;
updateDisplay(true);
}
return this;
}
public int getRefreshInterval() {
return refreshInterval;
}
public ChunkMonitorDisplay setRefreshInterval(int value) {
Preconditions.checkArgument(value >= 500, "Parameter 'value' has to be greater or equal 500 (" + value + ")");
this.refreshInterval = value;
return this;
}
public void registerForEvents(Object object) {
Preconditions.checkNotNull(object, "The parameter 'object' must not be null");
eventbus.register(object);
}
@Subscribe
public void receiveChunkEvent(ChunkMonitorEvent event) {
if (event != null) {
queue.offer(new ChunkRequest(event));
}
}
@Override
public void paint(Graphics g) {
if (!image.render(g, 0, 0)) {
super.paint(g);
}
}
private static class ImageBuffer {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private int width;
private int height;
private BufferedImage imageA;
private BufferedImage imageB;
ImageBuffer(int width, int height) {
resize(width, height);
}
ImageBuffer() {
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public Graphics2D getGraphics() {
lock.readLock().lock();
try {
if (imageB != null) {
return (Graphics2D) imageB.getGraphics();
}
} finally {
lock.readLock().unlock();
}
return null;
}
public void resize(int newWidth, int hewHeight) {
lock.writeLock().lock();
try {
this.width = newWidth;
this.height = hewHeight;
if (newWidth < 1 || hewHeight < 1) {
imageB = null;
} else if (imageB == null || newWidth != imageB.getWidth() || hewHeight != imageB.getHeight()) {
imageB = new BufferedImage(newWidth, hewHeight, BufferedImage.TYPE_INT_ARGB);
}
} catch (Exception e) {
imageB = null;
logger.error("Error allocating background buffer for chunk monitor display", e);
} finally {
lock.writeLock().unlock();
}
}
public void swap() {
lock.writeLock().lock();
try {
final BufferedImage tmp = imageA;
imageA = imageB;
imageB = tmp;
resize(width, height);
} finally {
lock.writeLock().unlock();
}
}
public boolean render(Graphics g, int x, int y) {
lock.readLock().lock();
try {
if (imageA != null) {
g.drawImage(imageA, x, y, null);
return true;
}
} finally {
lock.readLock().unlock();
}
return false;
}
}
private interface Request {
String getName();
boolean isChunkEvent();
boolean needsRendering();
boolean fastResume();
void execute();
}
private abstract class UpdateRequest implements Request {
@Override
public boolean isChunkEvent() {
return false;
}
}
private class RenderRequest extends UpdateRequest {
private final boolean fastResume;
RenderRequest(boolean fastResume) {
this.fastResume = fastResume;
}
RenderRequest() {
this.fastResume = false;
}
@Override
public void execute() {
}
@Override
public String getName() {
return "Render Request";
}
@Override
public boolean needsRendering() {
return true;
}
@Override
public boolean fastResume() {
return fastResume;
}
}
private class InitialRequest extends UpdateRequest {
@Override
public void execute() {
ChunkMonitor.getChunks(chunks);
recomputeRenderY();
}
@Override
public String getName() {
return "Initial Request";
}
@Override
public boolean needsRendering() {
return true;
}
@Override
public boolean fastResume() {
return false;
}
}
private class ResizeRequest extends UpdateRequest {
public final int width;
public final int height;
ResizeRequest(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public void execute() {
image.resize(width, height);
centerOffsetX = width / 2 - chunkSize / 2;
centerOffsetY = height / 2 - chunkSize / 2;
}
@Override
public String getName() {
return "Resize Request";
}
@Override
public boolean needsRendering() {
return true;
}
@Override
public boolean fastResume() {
return true;
}
}
private class ChunkRequest implements Request {
public final ChunkMonitorEvent event;
ChunkRequest(ChunkMonitorEvent event) {
Preconditions.checkNotNull(event, "The parameter 'event' must not be null");
this.event = event;
}
@Override
public String getName() {
return "Chunk Request";
}
@Override
public boolean isChunkEvent() {
return true;
}
@Override
public void execute() {
if (event instanceof ChunkMonitorEvent.ChunkProviderInitialized) {
chunks.clear();
map.clear();
ChunkMonitor.getChunks(chunks);
for (ChunkMonitorEntry e : chunks) {
map.put(e.getPosition(), e);
}
recomputeRenderY();
} else if (event instanceof ChunkMonitorEvent.ChunkProviderDisposed) {
chunks.clear();
map.clear();
recomputeRenderY();
} else if (event instanceof ChunkMonitorEvent.BasicChunkEvent) {
final ChunkMonitorEvent.BasicChunkEvent bEvent = (ChunkMonitorEvent.BasicChunkEvent) event;
final Vector3i pos = bEvent.getPosition();
final ChunkMonitorEntry entry;
if (event instanceof ChunkMonitorEvent.Created) {
final ChunkMonitorEvent.Created cEvent = (ChunkMonitorEvent.Created) event;
entry = cEvent.getEntry();
if (pos.y < minRenderY) {
minRenderY = pos.y;
}
if (pos.y > maxRenderY) {
maxRenderY = pos.y;
}
chunks.add(entry);
map.put(pos, entry);
} else {
entry = map.get(pos);
}
if (entry != null) {
entry.addEvent(bEvent);
} else {
logger.error("No chunk monitor entry found for position {}", pos);
}
}
}
@Override
public boolean needsRendering() {
return true;
}
@Override
public boolean fastResume() {
return false;
}
}
private class ResizeListener implements ComponentListener {
@Override
public void componentResized(ComponentEvent e) {
queue.offer(new ResizeRequest(getWidth(), getHeight()));
}
@Override
public void componentMoved(ComponentEvent e) {
}
@Override
public void componentShown(ComponentEvent e) {
}
@Override
public void componentHidden(ComponentEvent e) {
}
}
private class MouseInputListener implements MouseWheelListener, MouseMotionListener, MouseListener {
private Point leftPressed;
@Override
public void mouseDragged(MouseEvent e) {
if (leftPressed != null) {
final int dx = e.getPoint().x - leftPressed.x;
final int dy = e.getPoint().y - leftPressed.y;
setOffset(offsetX + dx, offsetY + dy);
}
}
@Override
public void mouseMoved(MouseEvent e) {
}
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
setRenderYDelta(e.getWheelRotation());
}
@Override
public void mouseClicked(MouseEvent e) {
if (e.getButton() == 2) {
final Vector3i pos = calcPlayerChunkPos();
if (pos != null) {
setRenderY(pos.y);
}
}
}
@Override
public void mousePressed(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) {
leftPressed = e.getPoint();
offsetX = getOffsetX();
offsetY = getOffsetY();
}
if (e.getButton() == MouseEvent.BUTTON2) {
setSelectedChunk(mouseToChunkPos(e.getPoint()));
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) {
leftPressed = null;
}
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
}
private final class RenderTask implements Runnable {
private RenderTask() {
}
private Rectangle calcBox(List<ChunkMonitorEntry> chunkEntries) {
if (chunkEntries.isEmpty()) {
return new Rectangle(0, 0, 0, 0);
}
int xmin = Integer.MAX_VALUE;
int xmax = Integer.MIN_VALUE;
int ymin = Integer.MAX_VALUE;
int ymax = Integer.MIN_VALUE;
for (ChunkMonitorEntry entry : chunkEntries) {
final Vector3i pos = entry.getPosition();
if (pos.y != renderY) {
continue;
}
if (pos.x < xmin) {
xmin = pos.x;
}
if (pos.x > xmax) {
xmax = pos.x;
}
if (pos.z < ymin) {
ymin = pos.z;
}
if (pos.z > ymax) {
ymax = pos.z;
}
}
return new Rectangle(xmin, ymin, xmax - xmin + 1, ymax - ymin + 1);
}
private Color calcChunkColor(ChunkMonitorEntry entry) {
final Chunk chunk = entry.getLatestChunk();
if (chunk == null) {
return COLOR_DEAD;
}
if (chunk.getMesh() != null) {
return COLOR_HIGHLIGHT_TESSELLATION;
}
if (chunk.isReady()) {
return COLOR_COMPLETE;
} else {
return COLOR_INTERNAL_LIGHT_GENERATION_PENDING;
}
}
private void renderSelectedChunk(Graphics2D g, int offsetx, int offsety, Vector3i pos) {
if (pos != null) {
g.setColor(COLOR_SELECTED_CHUNK);
g.drawRect(pos.x * chunkSize + offsetx, pos.z * chunkSize + offsety, chunkSize - 1, chunkSize - 1);
g.drawRect(pos.x * chunkSize + offsetx - 1, pos.z * chunkSize + offsety - 1, chunkSize + 1, chunkSize + 1);
}
}
private void renderBox(Graphics2D g, int offsetx, int offsety, Rectangle box) {
g.setColor(Color.white);
g.drawRect(box.x * chunkSize + offsetx, box.y * chunkSize + offsety, box.width * chunkSize - 1, box.height * chunkSize - 1);
}
private void renderBackground(Graphics2D g, int width, int height) {
g.setColor(Color.black);
g.fillRect(0, 0, width, height);
}
private void renderChunks(Graphics2D g, int offsetx, int offsety, List<ChunkMonitorEntry> chunkEntries) {
chunkEntries.stream().filter(entry -> entry.getPosition().y == renderY).forEach(entry ->
renderChunk(g, offsetx, offsety, entry.getPosition(), entry));
}
private void renderChunk(Graphics2D g, int offsetx, int offsety, Vector3i pos, ChunkMonitorEntry entry) {
g.setColor(calcChunkColor(entry));
g.fillRect(pos.x * chunkSize + offsetx + 1, pos.z * chunkSize + offsety + 1, chunkSize - 2, chunkSize - 2);
}
private void render(Graphics2D g, int offsetx, int offsety, int width, int height, List<ChunkMonitorEntry> chunkEntries) {
final Rectangle box = calcBox(chunkEntries);
renderBackground(g, width, height);
renderChunks(g, offsetx, offsety, chunkEntries);
renderBox(g, offsetx, offsety, box);
renderSelectedChunk(g, offsetx, offsety, selectedChunk);
}
private void render() {
final Graphics2D g = image.getGraphics();
if (g != null) {
final int iw = image.getWidth();
final int ih = image.getHeight();
render(g, centerOffsetX + offsetX, centerOffsetY + offsetY, iw, ih, chunks);
image.swap();
repaint();
}
}
private void repaint() {
SwingUtilities.invokeLater(ChunkMonitorDisplay.this::repaint);
}
private long poll(List<Request> output) throws InterruptedException {
long time = System.currentTimeMillis();
final Request r = queue.poll(500, TimeUnit.MILLISECONDS);
if (r != null) {
output.add(r);
queue.drainTo(output);
}
return (System.currentTimeMillis() - time);
}
private void doFollowPlayer() {
final Vector3i pos = calcPlayerChunkPos();
if (pos != null) {
setRenderY(pos.y);
}
}
@Override
public void run() {
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
final List<Request> requests = new LinkedList<>();
try {
while (true) {
final long slept = poll(requests);
boolean needsRendering = false;
boolean fastResume = false;
for (Request r : requests) {
try (ThreadActivity ignored = ThreadMonitor.startThreadActivity(r.getName())) {
r.execute();
} catch (Exception e) {
ThreadMonitor.addError(e);
logger.error("Thread error", e);
} finally {
needsRendering |= r.needsRendering();
fastResume |= r.fastResume();
}
}
requests.clear();
if (followPlayer) {
doFollowPlayer();
}
if (needsRendering) {
render();
}
if (!fastResume && (slept <= 400)) {
Thread.sleep(500 - slept);
}
}
} catch (Exception e) {
ThreadMonitor.addError(e);
logger.error("Thread error", e);
}
}
}
}