package org.basex.gui.view.map;
import static org.basex.gui.GUIConstants.*;
import static org.basex.util.Token.*;
import java.awt.Color;
import java.awt.Graphics;
import java.util.Arrays;
import org.basex.core.Text;
import org.basex.data.FTPos;
import org.basex.gui.layout.BaseXLayout;
import org.basex.util.ft.FTLexer;
import org.basex.util.ft.FTSpan;
import org.basex.util.list.BoolList;
import org.basex.util.list.IntList;
import org.basex.util.list.TokenList;
/**
* This class assembles utility methods for painting rectangle contents.
*
* @author BaseX Team 2005-12, BSD License
* @author Christian Gruen
* @author Sebastian Gath
*/
final class MapRenderer {
/** Color for each tooltip token. */
private static BoolList ttcol;
/** Index of tooltip token to underline. */
private static int ul;
/** Private constructor. */
private MapRenderer() { }
/**
* Calculates the height of the specified text.
* @param g graphics reference
* @param r rectangle
* @param s text to be drawn
* @param fs font size
* @return last height that was occupied
*/
static int calcHeight(final Graphics g, final MapRect r, final byte[] s,
final int fs) {
return drawText(g, r, s, false, fs);
}
/**
* Draws a text.
*
* @param g graphics reference
* @param r rectangle
* @param s text to be drawn
* @param fs font size
*/
static void drawText(final Graphics g, final MapRect r, final byte[] s,
final int fs) {
drawText(g, r, s, true, fs);
}
/**
* Draws a text.
* @param g graphics reference
* @param r rectangle
* @param s text to be drawn
* @param draw draw text (otherwise: just calculate space)
* @param fs font size
* @return height of the text
*/
private static int drawText(final Graphics g, final MapRect r,
final byte[] s, final boolean draw, final int fs) {
// limit string to given space
final int[] cw = fontWidths(g.getFont());
final int fh = (int) (1.2 * fs);
final Color textc = g.getColor();
int xx = r.x;
int yy = r.y + fh;
final int ww = r.w;
// get index on first pre value
int ll = 0;
final FTLexer lex = new FTLexer().sc().init(s);
while(lex.hasNext()) {
final FTSpan span = lex.next();
byte[] tok = span.text;
int wl = 0;
for(int n = 0; n < tok.length; n += cl(tok, n))
wl += BaseXLayout.width(g, cw, cp(tok, n));
if(ll + wl >= ww) {
xx = r.x;
if(ll != 0) yy += fh;
if(yy + fh > r.y + r.h) {
// text to high, skip drawing
if(draw) g.drawString(Text.DOTS, xx + ll, yy);
return r.h;
}
ll = 0;
if(draw && wl >= ww) {
// single word is too long for the rectangle
int twl = 2 * BaseXLayout.width(g, cw, '.');
if(twl >= ww) return Integer.MAX_VALUE;
int n = 0;
for(; n < tok.length; n += cl(tok, n)) {
final int l = BaseXLayout.width(g, cw, cp(tok, n));
if(twl + l >= ww) break;
twl += l;
}
tok = Arrays.copyOf(tok, n + 2);
tok[n] = '.';
tok[n + 1] = '.';
}
}
if(draw) {
// color each full-text hit
g.setColor(r.pos != null && r.pos.contains(span.pos) &&
!span.special ? GREEN : textc);
g.drawString(string(tok), xx + ll, yy);
}
ll += wl;
if(lex.paragraph()) {
// new paragraph
ll = 0;
yy += fh;
if(yy + fh > r.y + r.h) {
// text to high, skip drawing
if(draw) g.drawString(Text.DOTS, xx + ll, yy);
return r.h;
}
}
}
return yy - r.y;
}
/**
* Draws a text using thumbnail visualization.
* Calculates the needed space and chooses an abstraction level.
* Token/Sentence/Paragraphs
* @param g graphics reference
* @param r rectangle
* @param s text to be drawn
* @param fs font size
*/
static void drawThumbnails(final Graphics g, final MapRect r, final byte[] s,
final int fs) {
// thumbnail width
final double ffmax = 0.25;
final double ffmin = 0.14;
// thumbnail height
final double ffhmax = 0.5;
final double ffhmin = 0.28;
// empty line height
final double flhmax = 0.3;
final double flhmin = 0.168;
double ff = ffmax, ffh = ffhmax, flh = flhmax;
double fftmin = ffmin, fftmax = ffmax, ffhtmin = ffhmin,
ffhtmax = ffhmax, flhtmin = flhmin, flhtmax = flhmax;
double bff = ffmax, bffh = ffhmax, bflh = flhmax;
byte lhmi = (byte) Math.max(3, ffh * fs);
byte fhmi = (byte) Math.max(6, (flh + ffh) * fs);
int h = r.h;
r.thumbf = ff * fs;
r.thumbal = 0;
final int[][] data = new FTLexer().init(s).info();
boolean l = false;
while(r.thumbal < 2) {
// find parameter setting for the available space
ff = round(fftmax, fftmin);
r.thumbf = ff * fs;
ffh = round(ffhtmax, ffhtmin);
r.thumbfh = (byte) Math.max(1, ffh * fs);
flh = round(flhtmax, flhtmin);
r.thumblh = (byte) Math.max(1, (flh + ffh) * fs);
r.thumbsw = r.thumbf;
switch(r.thumbal) {
case 0:
h = drawToken(g, r, data, false);
break;
case 1: case 2:
h = drawSentence(g, r, data, false, r.h);
break;
}
if(h >= r.h || le(ffmax, ff) || le(ffhmax, ffh) || le(flhmax, flh)) {
if(l) {
// use last setup to draw
r.thumbf = bff * fs;
r.thumbfh = (byte) Math.max(1, bffh * fs);
r.thumblh = (byte) Math.max(1, (bflh + bffh) * fs);
r.thumbsw = r.thumbf;
switch(r.thumbal) {
case 0:
drawToken(g, r, data, true);
return;
case 1: case 2:
drawSentence(g, r, data, true, r.h);
return;
}
}
if(le(ff, ffmin) || le(ffh, ffhmin) || le(flh, flhmin)) {
// change abstraction level
r.thumbal++;
fhmi = r.thumbfh;
lhmi = r.thumblh;
fftmin = ffmin;
fftmax = ffmax;
ffhtmin = ffhmin;
ffhtmax = ffhmax;
flhtmin = flhmin;
flhtmax = flhmax;
} else {
// shrink size
fftmax = ff;
ffhtmax = ffh;
flhtmax = flh;
}
} else {
l = true;
// backup and try to enlarge
bff = ff;
bffh = ffh;
bflh = flh;
fftmin = ff;
ffhtmin = ffh;
flhtmin = flh;
}
}
// calculate parameter setting
// total number of bytes
final double sum = data[3].length + data[4].length;
// number of lines printable
final double nl = (r.h - 3.0) / lhmi;
// factor for the width of a thumbnail
final double fnew = (nl * (r.w - 3) - data[4].length) / sum;
r.thumbf = fnew;
r.thumbfh = fhmi;
r.thumblh = lhmi;
r.thumbsw = Math.max(1, fnew);
drawSentence(g, r, data, true, r.h);
}
/**
* Checks if the first is smaller than the second value, ignoring a
* small difference.
* @param a double 1
* @param b double 2
* @return true if a < b
*/
private static boolean le(final double a, final double b) {
return a < b || a / b < 1.05;
}
/**
* Returns the rounded average of the specified values.
* @param a first double
* @param b second double
* @return rounded double
*/
private static double round(final double a, final double b) {
final double v = (a + b) / 2;
final double d = v * 100000;
final int i = (int) d;
final double r = d - i >= 0.5 ? i + 1 : i;
return r / 100000;
}
/**
* Draws a text using thumbnail visualization, that represents a sentence
* through a thumbnail. Sentences are separated through black thumbnails.
* @param g graphics reference
* @param r rectangle
* @param data full-text to be drawn
* @param draw boolean for drawing (used for calculating the height)
* @param mh maximum height
* @return height
*/
private static int drawSentence(final Graphics g,
final MapRect r, final int[][] data, final boolean draw, final int mh) {
final boolean sen = r.thumbal == 1;
final FTPos ftp = r.pos;
final int xx = r.x;
final int ww = r.w;
int yy = r.y + 3;
int wl, ll = 0; // word and line length
final Color textc = color(r.level + 4);
g.setColor(textc);
int lastl, count = -1;
int ct, pp = 0, sl = 0, pl = 0;
int psl = 0, ppl = 0;
double error = 0;
int i = 0;
while(i < data[0].length) {
wl = 0;
ct = 0;
g.setColor(textc);
while(i < data[0].length && ppl < data[2].length && data[2][ppl] > pl &&
(psl < data[1].length && data[1][psl] > sl || psl >= data[1].length)) {
sl += data[0][i];
pl += data[0][i];
lastl = (int) (data[0][i] * r.thumbf);
error += data[0][i] * r.thumbf - lastl;
if(error >= 1) {
wl += (int) error;
error -= (int) error;
}
wl += lastl;
if(ftp != null && ftp.contains(count)) {
++ct;
++pp;
}
++count;
if(i < data[0].length) ++i;
else break;
}
if(ct == 0) {
while(ll + wl >= ww) {
if(draw) g.fillRect(xx + ll, yy, ww - ll, r.thumbfh);
wl -= ww - ll;
ll = 0;
yy += r.thumblh;
if(yy + r.thumblh >= r.y + mh) {
// height to big
return r.h;
}
}
if(draw) g.fillRect(xx + ll, yy, wl, r.thumbfh);
ll += wl;
} else {
int cttmp = 0;
int wltmp = wl / ct;
while(cttmp < ct) {
if(pp - ct + cttmp < ftp.size()) g.setColor(GREEN);
while(ll + wltmp >= ww) {
if(draw) g.fillRect(xx + ll, yy, ww - ll, r.thumbfh);
wltmp -= ww - ll;
ll = 0;
yy += r.thumblh;
// skip rest if no space is left
if(yy + r.thumblh >= r.y + mh) return r.h;
}
if(draw) g.fillRect(xx + ll, yy, wltmp, r.thumbfh);
ll += wltmp;
wltmp = wl / ct + (cttmp == ct - 2 ? wl - wl / ct * ct : 0);
++cttmp;
}
}
// new sentence
if(psl < data[1].length && data[1][psl] == sl) {
if(ll + r.thumbsw >= ww) {
yy += r.thumblh;
ll = 0;
// skip rest if no space is left
if(yy + r.thumblh >= r.y + mh) return r.h;
}
if(draw) {
g.setColor(Color.black);
g.fillRect(xx + ll, yy, (int) r.thumbsw, r.thumbfh);
g.setColor(textc);
}
ll += r.thumbsw;
sl = 0;
++psl;
}
// new paragraph
if(ppl < data[2].length && data[2][ppl] == pl) {
pl = 0;
++ppl;
if(sen) {
yy += r.thumblh;
ll = 0;
// skip rest if no space is left
if(yy + r.thumblh >= r.y + mh) return r.h;
}
}
}
return yy - r.y + r.thumbfh;
}
/**
* Draws a text using thumbnail visualization, that represents a token
* through a thumbnail.
* @param g graphics reference
* @param r rectangle
* @param data full-text to be drawn
* @param draw boolean for drawing (used for calculating the height)
* @return heights
*/
private static int drawToken(final Graphics g, final MapRect r,
final int[][] data, final boolean draw) {
final double xx = r.x;
final double ww = r.w;
final FTPos ftp = r.pos;
int yy = r.y + 3;
int wl; // word length
double ll = 0; // line length
double e = 0;
final Color textc = color(r.level + 4);
int count = 0;
int sl = 0, pl = 0;
int psl = 0, ppl = 0;
for(int i = 0; i < data[0].length; ++i) {
wl = (int) (data[0][i] * r.thumbf);
e += data[0][i] * r.thumbf - wl;
if(e >= 1) {
wl += (int) e;
e -= (int) e;
}
sl += data[0][i];
pl += data[0][i];
// check if rectangle fits in line - don't split token and dot
if(ll + wl + r.thumbsw * (psl < data[1].length
&& sl == data[1][psl] ? 1 : 0) >= ww) {
yy += r.thumblh;
ll = 0;
if(wl >= ww) return r.h + 3;
}
if(draw) {
// draw word
g.setColor(ftp != null && ftp.contains(count) ? GREEN : textc);
g.fillRect((int) (xx + ll), yy, wl, r.thumbfh);
}
ll += wl;
++count;
if(psl < data[1].length && sl == data[1][psl]) {
// new sentence, draw dot
if(draw) {
g.setColor(Color.black);
g.fillRect((int) (xx + ll), yy, (int) r.thumbsw, r.thumbfh);
g.setColor(textc);
}
ll += r.thumbsw;
++psl;
sl = 0;
}
ll += r.thumbf;
if(ppl < data[2].length && pl == data[2][ppl]) {
// new paragraph
yy += r.thumblh;
ll = 0;
++ppl;
pl = 0;
}
}
return yy - r.y + 3;
}
/**
* Checks if cursor is inside the rect.
* @param rx int x-value of the rect
* @param ry int y-value of the rect
* @param rw double width of the rect
* @param rh int height of the rect
* @param xx int x-value of the cursor
* @param yy int y-value of the cursor
* @return boolean
*/
private static boolean inRect(final double rx, final int ry,
final double rw, final int rh, final int xx, final int yy) {
return xx >= rx && xx <= rx + rw && yy >= ry && yy <= ry + rh;
}
/**
* Calculates a the tooltip text for the thumbnail visualization.
* @param r rectangle
* @param data full-text to be drawn
* @param x mouseX
* @param y mouseY
* @param w width of map view
* @param g Graphics
* @return token list
*/
static TokenList calculateToolTip(final MapRect r, final int[][] data,
final int x, final int y, final int w, final Graphics g) {
// rectangle is empty - don't need a tooltip
if(r.thumbf == 0) return null;
final boolean sen = r.thumbal < 2;
final boolean ds = r.thumbal < 1;
final FTPos ftp = r.pos;
final int ww = r.w;
int yy = r.y + 3;
double ll = 0; // line length
double error = 0;
ul = -1;
int psl = 0, ppl = 0, pl = 0, sl = 0, cc = 0;
final TokenList tl = new TokenList();
ttcol = new BoolList();
for(int i = 0; i < data[0].length; ++i) {
boolean ir = false;
double wl = data[0][i] * r.thumbf;
// sum up error, caused by int cast
error += data[0][i] * r.thumbf - wl;
if(error >= 1) {
// adjust word length
wl += error;
error -= (int) error;
}
pl += data[0][i];
sl += data[0][i];
cc += data[0][i];
// find hovered thumbnail and corresponding text
if(ll + wl + (ds && psl < data[1].length &&
data[1][psl] == sl ? r.thumbsw : 0) >= ww) {
if(ds) {
// do not split token
yy += r.thumblh;
ir = inRect(r.x, yy, wl, r.thumbfh, x, y);
ll = wl + (psl < data[1].length && data[1][psl] == sl ?
r.thumbsw : r.thumbf);
} else {
// split token to safe space
yy += r.thumblh;
wl -= ww - ll;
ir = inRect(r.x, yy, wl, r.thumbfh, x, y);
ll = wl +
(psl < data[1].length && data[1][psl] == sl ? r.thumbsw : r.thumbf);
}
} else {
ir |= inRect(r.x + ll, yy, wl, r.thumbfh, x, y);
ll += wl + (ds ? r.thumbf : 0);
}
if(ir) {
// go back and forward through the text, centralize the hovered token
final int si = i;
final int[] cw = fontWidths(g.getFont());
final int sp = BaseXLayout.width(g, cw, ' ');
final int sd = BaseXLayout.width(g, cw, '.');
// go some tokens backwards form current token
final int bpsl = data[1][psl] == sl ? psl + 1 : psl;
final int bsl = data[1][psl] == sl ? 0 : sl;
ll = sd * 2 + sp;
int l;
byte[] tok;
int p = cc >= data[0][i] ? cc - data[0][i] : 0;
boolean apm;
while(p > -1 && i > -1) {
// append punctuation mark
apm = psl < data[1].length && data[1][psl] == sl;
tok = new byte[data[0][i] + (apm ? 1 : 0)];
for(int k = 0; k < tok.length - (apm ? 1 : 0); ++k) {
tok[k] = (byte) data[3][p + k];
}
if(apm) {
tok[tok.length - 1] = (byte) data[4][psl];
++sl;
}
sl -= tok.length;
if(sl == 0) {
--psl;
if(psl == -1) psl = data[1].length;
else sl = data[1][psl];
}
l = 0;
for(int n = 0; n < tok.length; n += cl(tok, n))
l += BaseXLayout.width(g, cw, cp(tok, n));
if(si > i && ll + l + sp >= w / 2d) break;
ll += l + sp;
tl.add(tok);
// find token color
ttcol.add(ftp != null && ftp.contains(i));
if(i == 0) break;
p -= data[0][i - 1];
--i;
}
if(i > 0) {
tl.add(new byte[] { '.', '.' });
ttcol.add(false);
}
i = si + 1;
p = cc;
// invert tokens
ul = tl.size() - 1;
final byte[][] toks = tl.toArray();
final boolean[] tc = ttcol.toArray();
tl.reset();
ttcol.reset();
for(int j = toks.length - 1; j >= 0; j--) {
tl.add(toks[j]);
ttcol.add(tc[j]);
}
ll = 0;
sl = bsl;
psl = bpsl;
// process tokens after current token
while(p < data[3].length && i < data[0].length) {
apm = false;
if(psl < data[1].length && data[1][psl] == sl + data[0][i]) {
apm = true;
sl = 0;
++psl;
}
tok = new byte[data[0][i] + (apm ? 1 : 0)];
l = 0;
for(int k = 0; k < tok.length - (apm ? 1 : 0); ++k) {
tok[k] = (byte) data[3][p + k];
}
if(apm) tok[tok.length - 1] = (byte) data[4][psl - 1];
sl += apm ? sl : tok.length;
for(int n = 0; n < tok.length; n += cl(tok, n))
l += BaseXLayout.width(g, cw, cp(tok, n));
if(ll + l + sp + 2 * sd >= w / 2d) break;
ll += l + sp;
tl.add(tok);
ttcol.add(ftp != null && ftp.contains(i));
p += tok.length - (apm ? 1 : 0);
++i;
}
if(i < data[0].length) {
tl.add(new byte[] { '.', '.' });
ttcol.add(false);
}
return tl;
}
// new sentence
if(ds && psl < data[1].length && data[1][psl] == sl) {
if(ll + r.thumbsw >= ww) {
yy += r.thumblh;
ll -= ww;
}
ll += r.thumbsw;
sl = 0;
++psl;
}
// new paragraph
if(ppl < data[2].length && data[2][ppl] == pl) {
pl = 0;
++ppl;
if(sen) {
yy += r.thumblh;
ll = 0;
}
}
}
return tl;
}
/**
* Draws pre-calculated tooltip.
* @param g graphics reference
* @param x x-value
* @param y y-value
* @param mr view rectangle
* @param tl token list
* @param fs font size
*/
static void drawToolTip(final Graphics g, final int x, final int y,
final MapRect mr, final TokenList tl, final int fs) {
if(tl == null || tl.size() == 0) return;
final int[] cw = fontWidths(g.getFont());
final int sw = BaseXLayout.width(g, cw, ' ');
int wl = 0;
int nl = 1;
int wi = mr.w / 2;
final IntList len = new IntList();
for(int i = 0; i < tl.size(); ++i) {
int l = 0;
final byte[] tok = tl.get(i);
final int ns = tok.length;
for(int n = 0; n < ns; n += cl(tok, n)) {
l += BaseXLayout.width(g, cw, cp(tok, n));
}
if(wl + l + sw < wi) {
wl += l + sw;
} else {
++nl;
if(l > wi) wi = l;
wl = l + sw;
}
len.add(l);
}
final int ww = nl == 1 && wl < wi ? wl : wi;
// find optimal position for the tooltip
final int xx = x + 10 + ww >= mr.x + mr.w ? mr.x + mr.w - ww - 2 : x + 10;
int yy = y + 28 + fs * nl + 4 < mr.y + mr.h ? y + 28 :
y - mr.y - 4 > fs * nl ? y - fs * nl : mr.y + mr.h - 4 - fs * nl;
g.setColor(color(10));
g.drawRect(xx - 3, yy - fs - 1, ww + 3, fs * nl + 7);
g.setColor(color(0));
g.fillRect(xx - 2, yy - fs, ww + 2, fs * nl + 6);
g.setColor(color(20));
wl = 0;
final int is = tl.size();
for(int i = 0; i < is; ++i) {
final int l = len.get(i);
if(wl + l + sw >= wi) {
yy += fs + 1;
wl = 0;
}
final boolean pm = !ftChar(tl.get(i)[tl.get(i).length - 1]);
if(ttcol.get(i)) g.setColor(GREEN);
g.drawString(string(tl.get(i)), xx + wl, yy);
if(i == ul) {
g.drawLine(xx + wl, yy + 1, xx + wl + (pm ? l - sw : l), yy + 1);
}
g.setColor(color(24));
wl += l + sw;
}
}
}