package org.basex.gui.view.map;
import static org.basex.core.Text.DOTS;
import org.basex.data.Data;
import org.basex.data.Nodes;
import static org.basex.gui.GUIConstants.*;
import org.basex.gui.GUIProp;
import static org.basex.gui.layout.BaseXKeys.*;
import org.basex.gui.layout.BaseXLayout;
import org.basex.gui.layout.BaseXPopup;
import org.basex.gui.view.View;
import org.basex.gui.view.ViewData;
import org.basex.gui.view.ViewNotifier;
import org.basex.util.Performance;
import org.basex.util.Token;
import org.basex.util.ft.FTLexer;
import org.basex.util.list.IntList;
import org.basex.util.list.TokenList;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
/**
* This view is a TreeMap implementation.
*
* @author BaseX Team 2005-12, BSD License
* @author Christian Gruen
* @author Joerg Hauser
* @author Bastian Lemke
*/
public final class MapView extends View implements Runnable {
/** Dynamic zooming steps. */
private static final int[] ZS = { 0, 0, 0, 0, 20, 80, 180, 320, 540, 840,
1240, 1740, 2380, 3120, 4000, 4980, 5980, 6860, 7600, 8240, 8740, 9140,
9440, 9660, 9800, 9900, 9960, 9980, 9980, 9980, 10000 };
/** Number of zooming steps. */
private static final int ZOOMSIZE = ZS.length - 1;
/** Maximum zooming step. */
private static final int MAXZS = ZS[ZOOMSIZE];
/** Array of current rectangles. */
private MapRects mainRects;
/** Data specific map layout. */
private transient MapPainter painter;
/** Text lengths. */
private int[] textLen;
/** Rectangle history. */
private final MapRect[] rectHist = new MapRect[ViewNotifier.MAXHIST];
/** Current zooming Step (set to 0 when no zooming takes place). */
private int zoomStep;
/** Main rectangle. */
private MapRect mainRect;
/** Dragged rectangle. */
private MapRect selBox;
/** Flag for zooming in/out. */
private boolean zoomIn;
/** Zooming speed. */
private int zoomSpeed;
/** Horizontal mouse position. */
private int mouseX = -1;
/** Vertical mouse position. */
private int mouseY = -1;
/** Drag tolerance. */
private int dragTol;
/** Currently focused rectangle. */
private MapRect focused;
/** TreeMap. */
private BufferedImage mainMap;
/** Zoomed TreeMap. */
private BufferedImage zoomMap;
/** Keeps the whole map layout. */
MapLayout layout;
/**
* Default constructor.
* @param man view manager
*/
public MapView(final ViewNotifier man) {
super(MAPVIEW, man);
new BaseXPopup(this, POPUP);
}
/**
* Creates a buffered image.
* @return buffered image
*/
private BufferedImage createImage() {
return new BufferedImage(Math.max(1, getWidth()), Math.max(1, getHeight()),
BufferedImage.TYPE_INT_BGR);
}
@Override
public void refreshInit() {
painter = null;
mainRects = null;
focused = null;
textLen = null;
zoomStep = 0;
final Data data = gui.context.data();
final GUIProp gprop = gui.gprop;
if(data != null && visible()) {
painter = new MapDefault(this, gprop);
mainMap = createImage();
zoomMap = createImage();
refreshLayout();
}
}
@Override
public void refreshFocus() {
final int f = gui.context.focused;
if(f == -1) focused = null;
if(mainRects == null) return;
final int ms = mainRects.size;
for(int mi = 0; mi < ms; ++mi) {
final MapRect rect = mainRects.get(mi);
if(f == rect.pre || mi + 1 == ms || f < mainRects.get(mi + 1).pre) {
focused = rect;
repaint();
break;
}
}
}
@Override
public void refreshMark() {
drawMap(mainMap, mainRects, 1f);
repaint();
}
@Override
public void refreshContext(final boolean more, final boolean quick) {
// use simple zooming animation for result node filtering
final Nodes context = gui.context.current();
final int hist = gui.notify.hist;
final boolean page = !more && rectHist[hist + 1] != null &&
rectHist[hist + 1].pre == 0 || more && (context.size() != 1 ||
focused == null || context.list[0] != focused.pre);
if(page) focused = new MapRect(0, 0, getWidth(), 1);
zoom(more, quick);
}
@Override
public void refreshLayout() {
if(painter == null) return;
// calculate map
calc(new MapRect(0, 0, getWidth(), getHeight(), 0, 0),
gui.context.current(), mainMap);
repaint();
}
@Override
public void refreshUpdate() {
textLen = null;
refreshContext(false, true);
}
@Override
public boolean visible() {
return gui.gprop.is(GUIProp.SHOWMAP);
}
@Override
public void visible(final boolean v) {
gui.gprop.set(GUIProp.SHOWMAP, v);
}
@Override
protected boolean db() {
return true;
}
/**
* Zooms the focused rectangle.
* @param more show more
* @param quick context switch (no animation)
*/
private void zoom(final boolean more, final boolean quick) {
gui.updating = !quick;
zoomIn = more;
// choose zooming rectangle
final int hist = gui.notify.hist;
if(more) {
rectHist[hist] = focused;
mainRect = rectHist[hist];
} else {
mainRect = rectHist[hist + 1];
}
if(mainRect == null) mainRect = new MapRect(0, 0, getWidth(), getHeight());
// reset data & start zooming
final BufferedImage tmpMap = zoomMap;
zoomMap = mainMap;
mainMap = tmpMap;
focused = null;
// create new context nodes
refreshLayout();
// calculate zooming speed (slower for large zooming scales)
if(mainRect.w > 0 && mainRect.h > 0) {
zoomSpeed = (int) (Math.log(64d * getWidth() / mainRect.w) +
Math.log(64d * getHeight() / mainRect.h));
}
if(quick) {
gui.updating = false;
focus();
repaint();
} else {
zoomStep = ZOOMSIZE;
new Thread(this).start();
}
}
@Override
public void run() {
focused = null;
// run zooming
while(zoomStep > 1) {
Performance.sleep(zoomSpeed);
--zoomStep;
repaint();
}
// wait until current painting is finished
while(gui.painting) Performance.sleep(zoomSpeed);
// remove old rectangle and repaint map
zoomStep = 0;
gui.updating = false;
focus();
repaint();
}
/**
* Finds the rectangle at the cursor position.
* @return focused rectangle
*/
private boolean focus() {
if(gui.updating || mainRects == null) return false;
/*
* Loop through all rectangles. As the rectangles are sorted by pre order
* and small rectangles are descendants of bigger ones, the focused
* rectangle can be found by simply parsing the array backwards.
*/
int r = mainRects.size;
while(--r >= 0) {
final MapRect rect = mainRects.get(r);
if(rect.contains(mouseX, mouseY)) break;
}
// don't focus top rectangles
final MapRect fr = r >= 0 ? mainRects.get(r) : null;
// find focused rectangle
final boolean nf = focused != fr || fr != null && fr.thumb;
focused = fr;
if(nf) gui.notify.focus(focused != null ? focused.pre : -1, this);
return nf;
}
/**
* Initializes the calculation of the main map.
* @param rect initial space to layout rectangles in
* @param nodes nodes to draw in the map
* @param map image to draw rectangles on
*/
private void calc(final MapRect rect, final Nodes nodes,
final BufferedImage map) {
// calculate new main rectangles
gui.cursor(CURSORWAIT);
initLen();
layout = new MapLayout(nodes.data, textLen, gui.gprop);
layout.makeMap(rect, new MapList(nodes.list.clone()),
0, (int) nodes.size() - 1);
// rectangles are copied to avoid synchronization issues
mainRects = layout.rectangles.copy();
drawMap(map, mainRects, 1f);
focus();
/*
* Screenshots: try { File file = new File("screenshot.png");
* ImageIO.write(mainMap, "png", file); } catch(IOException ex) {
* Util.stack(ex); }
*/
gui.cursor(CURSORARROW, true);
}
@Override
public void paintComponent(final Graphics g) {
final Data data = gui.context.data();
if(data == null) return;
if(mainRects == null || mainRects.size == 0 || mainRects.get(0).w == 0) {
super.paintComponent(g);
if(mainRects == null || mainRects.size != 0) refreshInit();
return;
}
// calculate map
gui.painting = true;
// paint map
final boolean in = zoomStep > 0 && zoomIn;
final Image img1 = in ? zoomMap : mainMap;
final Image img2 = in ? mainMap : zoomMap;
if(zoomStep > 0) {
drawImage(g, img1, -zoomStep);
drawImage(g, img2, zoomStep);
} else {
drawImage(g, mainMap, zoomStep);
}
// check if focused rectangle is valid
if(focused != null && focused.pre >= data.meta.size) focused = null;
// skip node path view
final MapRect f = focused;
if(f == null || mainRects.size == 1 && f == mainRects.get(0)) {
gui.painting = false;
if(f == null || !f.thumb) return;
}
final GUIProp gprop = gui.gprop;
if(gprop.num(GUIProp.MAPOFFSETS) == 0) {
g.setColor(color(32));
int pre = mainRects.size;
int par = ViewData.parent(data, f.pre);
while(--pre >= 0) {
final MapRect rect = mainRects.get(pre);
if(rect.pre == par) {
final int x = rect.x;
final int y = rect.y;
final int w = rect.w;
final int h = rect.h;
g.drawRect(x, y, w, h);
g.drawRect(x - 1, y - 1, w + 2, h + 2);
par = ViewData.parent(data, par);
}
}
}
if(selBox != null) {
g.setColor(colormark3);
g.drawRect(selBox.x, selBox.y, selBox.w, selBox.h);
g.drawRect(selBox.x - 1, selBox.y - 1, selBox.w + 2, selBox.h + 2);
} else {
// paint focused rectangle
final int x = f.x;
final int y = f.y;
final int w = f.w;
final int h = f.h;
g.setColor(color4);
g.drawRect(x, y, w, h);
g.drawRect(x + 1, y + 1, w - 2, h - 2);
// draw tag label
g.setFont(font);
smooth(g);
if(data.kind(f.pre) == Data.ELEM) {
String tt = Token.string(ViewData.tag(gprop, data, f.pre));
if(tt.length() > 32) tt = tt.substring(0, 30) + DOTS;
BaseXLayout.drawTooltip(g, tt, x, y, getWidth(), f.level + 5);
}
if(f.thumb) {
// draw tooltip for thumbnail
f.x += 3;
f.w -= 3;
// read content from disk
final byte[] text = MapPainter.content(data, f);
// calculate tooltip
final int[][] info = new FTLexer().init(text).info();
final TokenList tl = MapRenderer.calculateToolTip(f,
info, mouseX, mouseY, getWidth(), g);
final MapRect mr = new MapRect(getX(), getY(), getWidth(), getHeight());
// draw calculated tooltip
MapRenderer.drawToolTip(g, mouseX, mouseY, mr, tl,
gprop.num(GUIProp.FONTSIZE));
f.x -= 3;
f.w += 3;
}
}
gui.painting = false;
}
/**
* Draws image with correct scaling.
* @param g graphics reference
* @param img image to be drawn
* @param zi zooming factor
*/
private void drawImage(final Graphics g, final Image img, final int zi) {
if(img == null) return;
final MapRect r = new MapRect(0, 0, getWidth(), getHeight());
zoom(r, zi);
g.drawImage(img, r.x, r.y, r.x + r.w, r.y + r.h, 0, 0, getWidth(),
getHeight(), this);
}
/**
* Zooms the coordinates of the specified rectangle.
* @param r rectangle to be zoomed
* @param zs zooming step
*/
private void zoom(final MapRect r, final int zs) {
int xs = r.x;
int ys = r.y;
int xe = xs + r.w;
int ye = ys + r.h;
// calculate zooming rectangle
// get window size
if(zs != 0) {
final MapRect zr = mainRect;
final int tw = getWidth();
final int th = getHeight();
if(zs > 0) {
final long s = zoomIn ? ZS[zs] : ZS[ZOOMSIZE - zs];
xs = (int) ((zr.x + xs * zr.w / tw - xs) * s / MAXZS);
ys = (int) ((zr.y + ys * zr.h / th - ys) * s / MAXZS);
xe += (int) ((zr.x + xe * zr.w / tw - xe) * s / MAXZS);
ye += (int) ((zr.y + ye * zr.h / th - ye) * s / MAXZS);
} else {
final long s = 10000 - (zoomIn ? ZS[-zs] : ZS[ZOOMSIZE + zs]);
if(zr.w == 0) zr.w = 1;
if(zr.h == 0) zr.h = 1;
xs = (int) (-xe * zr.x / zr.w * s / MAXZS);
xe = (int) (xs + xe + xe * (xe - zr.w) / zr.w * s / MAXZS);
ys = (int) (-ye * zr.y / zr.h * s / MAXZS);
ye = (int) (ys + ye + ye * (ye - zr.h) / zr.h * s / MAXZS);
}
}
r.x = xs;
r.y = ys;
r.w = xe - xs;
r.h = ye - ys;
}
/**
* Creates a buffered image for the treemap.
* @param map Image to draw the map on
* @param rects calculated rectangles
* @param sc scale the rectangles
*/
private void drawMap(final BufferedImage map, final MapRects rects,
final float sc) {
final Graphics g = map.getGraphics();
smooth(g);
painter.drawRectangles(g, rects, sc);
}
@Override
public void mouseMoved(final MouseEvent e) {
if(gui.updating) return;
mouseX = e.getX();
mouseY = e.getY();
if(focus()) repaint();
}
@Override
public void mousePressed(final MouseEvent e) {
if(gui.updating) return;
super.mousePressed(e);
mouseX = e.getX();
mouseY = e.getY();
dragTol = 0;
if(!focus() && gui.context.focused == -1) return;
// add or remove marked node
final Nodes marked = gui.context.marked;
if(e.getClickCount() == 2) {
if(mainRects.size != 1) gui.notify.context(marked, false, null);
} else if(e.isShiftDown()) {
gui.notify.mark(1, null);
} else if(sc(e) && SwingUtilities.isLeftMouseButton(e)) {
gui.notify.mark(2, null);
} else {
if(!marked.contains(gui.context.focused)) gui.notify.mark(0, null);
}
}
@Override
public void mouseDragged(final MouseEvent e) {
if(gui.updating || ++dragTol < 8 || mainRects.sorted != mainRects.list)
return;
// refresh mouse focus
int mx = mouseX;
int my = mouseY;
int mw = e.getX() - mx;
int mh = e.getY() - my;
if(mw < 0) mx -= mw = -mw;
if(mh < 0) my -= mh = -mh;
selBox = new MapRect(mx, my, mw, mh);
final Data data = gui.context.data();
final IntList il = new IntList();
int np = 0;
final int rl = mainRects.size;
for(int r = 0; r < rl; ++r) {
final MapRect rect = mainRects.get(r);
if(mainRects.get(r).pre < np) continue;
if(selBox.contains(rect)) {
il.add(rect.pre);
np = rect.pre + ViewData.size(data, rect.pre);
}
}
gui.notify.mark(new Nodes(il.toArray(), data), null);
}
@Override
public void mouseReleased(final MouseEvent e) {
if(gui.updating) return;
if(selBox != null) {
selBox = null;
repaint();
}
}
@Override
public void mouseWheelMoved(final MouseWheelEvent e) {
if(gui.updating || gui.context.focused == -1) return;
if(e.getWheelRotation() <= 0) {
final Nodes m = new Nodes(gui.context.focused, gui.context.data());
gui.context.marked = m;
gui.notify.context(m, false, null);
} else {
gui.notify.hist(false);
}
}
@Override
public void keyPressed(final KeyEvent e) {
super.keyPressed(e);
if(gui.updating || mainRects == null || control(e)) return;
final boolean cursor = PREVLINE.is(e) || NEXTLINE.is(e) ||
PREV.is(e) || NEXT.is(e);
if(!cursor) return;
if(focused == null) focused = mainRects.get(0);
final int fs = gui.gprop.num(GUIProp.FONTSIZE);
int o = fs + 4;
final boolean shift = e.isShiftDown();
if(PREVLINE.is(e)) {
mouseY = focused.y + (shift ? focused.h - fs : 0) - 1;
if(shift) mouseX = focused.x + (focused.w >> 1);
} else if(NEXTLINE.is(e)) {
mouseY = focused.y + (shift ? o : focused.h + 1);
if(shift) mouseX = focused.x + (focused.w >> 1);
} else if(PREV.is(e)) {
mouseX = focused.x + (shift ? focused.w - fs : 0) - 1;
if(shift) mouseY = focused.y + (focused.h >> 1);
} else if(NEXT.is(e)) {
mouseX = focused.x + (shift ? o : focused.w + 1);
if(shift) mouseY = focused.y + (focused.h >> 1);
}
o = mainRects.get(0).w == getWidth() ? (o >> 1) + 1 : 0;
mouseX = Math.max(o, Math.min(getWidth() - o - 1, mouseX));
mouseY = Math.max(o << 1, Math.min(getHeight() - o - 1, mouseY));
if(focus()) repaint();
}
@Override
public void componentResized(final ComponentEvent e) {
if(gui.updating) return;
focused = null;
mainMap = createImage();
zoomMap = createImage();
refreshLayout();
}
/**
* Initializes the text lengths and stores them into an array.
*/
private void initLen() {
final Data data = gui.context.data();
if(textLen != null || gui.gprop.num(GUIProp.MAPWEIGHT) == 0) return;
final int size = data.meta.size;
textLen = new int[size];
final IntList pars = new IntList();
int l = 0;
for(int pre = 0; pre < size; ++pre) {
final int kind = data.kind(pre);
final int par = data.parent(pre, kind);
final int ll = l;
while(l > 0 && pars.get(l - 1) > par) {
textLen[pars.get(l - 1)] += textLen[pars.get(l)];
--l;
}
if(l > 0 && ll != l) textLen[pars.get(l - 1)] += textLen[pars.get(l)];
pars.set(l, pre);
if(kind == Data.DOC || kind == Data.ELEM) {
pars.set(++l, 0);
} else {
textLen[pre] = data.textLen(pre, kind != Data.ATTR);
}
}
while(--l >= 0) textLen[pars.get(l)] += textLen[pars.get(l + 1)];
}
}