package net.osmand.plus.render;
import gnu.trove.map.hash.TIntObjectHashMap;
import gnu.trove.procedure.TIntObjectProcedure;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import net.osmand.binary.BinaryMapDataObject;
import net.osmand.binary.BinaryMapIndexReader.TagValuePair;
import net.osmand.data.QuadRect;
import net.osmand.data.QuadTree;
import net.osmand.plus.render.OsmandRenderer.RenderingContext;
import net.osmand.render.RenderingRuleSearchRequest;
import net.osmand.render.RenderingRulesStorage;
import net.osmand.util.Algorithms;
import net.sf.junidecode.Junidecode;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
public class TextRenderer {
private Paint paintText;
private final Context context;
private Paint paintIcon;
private Typeface defaultTypeface;
private Typeface boldItalicTypeface;
private Typeface italicTypeface;
private Typeface boldTypeface;
static class TextDrawInfo {
public TextDrawInfo(String text) {
this.text = text;
}
String text = null;
Path drawOnPath = null;
QuadRect bounds = null;
float vOffset = 0;
float centerX = 0;
float pathRotate = 0;
float centerY = 0;
float textSize = 0;
float minDistance = 0;
int textColor = Color.BLACK;
int textShadow = 0;
int textWrap = 0;
boolean bold = false;
boolean italic = false;
String shieldRes = null;
String shieldResIcon = null;
int textOrder = 100;
int textShadowColor = Color.WHITE;
public void fillProperties(RenderingContext rc, RenderingRuleSearchRequest render, float centerX, float centerY) {
this.centerX = centerX;
// used only for draw on path where centerY doesn't play role
this.vOffset = (int) rc.getComplexValue(render, render.ALL.R_TEXT_DY);
this.centerY = centerY + this.vOffset;
textColor = render.getIntPropertyValue(render.ALL.R_TEXT_COLOR);
if (textColor == 0) {
textColor = Color.BLACK;
}
textSize = rc.getComplexValue(render, render.ALL.R_TEXT_SIZE) ;
textShadow = (int) rc.getComplexValue(render, render.ALL.R_TEXT_HALO_RADIUS);
textShadowColor = render.getIntPropertyValue(render.ALL.R_TEXT_HALO_COLOR);
if(textShadowColor == 0) {
textShadowColor = Color.WHITE;
}
textWrap = (int) rc.getComplexValue(render, render.ALL.R_TEXT_WRAP_WIDTH);
bold = render.getIntPropertyValue(render.ALL.R_TEXT_BOLD, 0) > 0;
italic = render.getIntPropertyValue(render.ALL.R_TEXT_ITALIC, 0) > 0;
minDistance = rc.getComplexValue(render, render.ALL.R_TEXT_MIN_DISTANCE);
if (render.isSpecified(render.ALL.R_TEXT_SHIELD)) {
shieldRes = render.getStringPropertyValue(render.ALL.R_TEXT_SHIELD);
}
if (render.isSpecified(render.ALL.R_ICON)) {
shieldResIcon = render.getStringPropertyValue(render.ALL.R_ICON);
}
textOrder = render.getIntPropertyValue(render.ALL.R_TEXT_ORDER, 100);
}
}
public TextRenderer(Context context) {
this.context = context;
paintText = new Paint();
paintText.setStyle(Style.FILL);
paintText.setStrokeWidth(1);
paintText.setColor(Color.BLACK);
paintText.setTextAlign(Align.CENTER);
defaultTypeface = Typeface.create("Droid Serif", Typeface.NORMAL);
boldItalicTypeface = Typeface.create("Droid Serif", Typeface.BOLD_ITALIC);
italicTypeface = Typeface.create("Droid Serif", Typeface.ITALIC);
boldTypeface = Typeface.create("Droid Serif", Typeface.BOLD);
paintText.setTypeface(defaultTypeface); //$NON-NLS-1$
paintText.setAntiAlias(true);
paintIcon = new Paint();
paintIcon.setStyle(Style.STROKE);
}
public Paint getPaintText() {
return paintText;
}
private double sqr(double a) {
return a * a;
}
private float fsqr(float a) {
return a * a;
}
boolean intersects(QuadRect tRect, float tRot, QuadRect sRect, float sRot) {
if (Math.abs(tRot) < Math.PI / 15 && Math.abs(sRot) < Math.PI / 15) {
return QuadRect.intersects(tRect, sRect);
}
double dist = Math.sqrt(sqr(tRect.centerX() - sRect.centerX()) + sqr(tRect.centerY() - sRect.centerY()));
if (dist < 3) {
return true;
}
// difference close to 90/270 degrees
if (Math.abs(Math.cos(tRot - sRot)) < 0.3) {
// rotate one rectangle to 90 degrees
tRot += Math.PI / 2;
double l = tRect.centerX() - tRect.height() / 2;
double t = tRect.centerY() - tRect.width() / 2;
tRect = new QuadRect(l, t, l + tRect.height(), t + tRect.width());
}
// determine difference close to 180/0 degrees
if (Math.abs(Math.sin(tRot - sRot)) < 0.3) {
// rotate t box
// (calculate offset for t center suppose we rotate around s center)
float diff = (float) (-Math.atan2(tRect.centerX() - sRect.centerX(), tRect.centerY() - sRect.centerY()) + Math.PI / 2);
diff -= sRot;
double left = sRect.centerX() + dist * Math.cos(diff) - tRect.width() / 2;
double top = sRect.centerY() - dist * Math.sin(diff) - tRect.height() / 2;
QuadRect nRect = new QuadRect(left, top, left + tRect.width(), top + tRect.height());
return QuadRect.intersects(nRect, sRect);
}
// TODO other cases not covered
return QuadRect.intersects(tRect, sRect);
}
void drawTestBox(Canvas cv, RectF r, float rot, String text) {
cv.save();
cv.translate(r.centerX(), r.centerY());
cv.rotate((float) (rot * 180 / Math.PI));
RectF rs = new RectF(-r.width() / 2, -r.height() / 2, r.width() / 2, r.height() / 2);
cv.drawRect(rs, paintIcon);
if (text != null) {
paintText.setTextSize(paintText.getTextSize() - 4);
cv.drawText(text, rs.centerX(), rs.centerY(), paintText);
paintText.setTextSize(paintText.getTextSize() + 4);
}
cv.restore();
}
List<TextDrawInfo> tempSearch = new ArrayList<TextDrawInfo>();
private boolean findTextIntersection(Canvas cv, RenderingContext rc, QuadTree<TextDrawInfo> boundIntersections, TextDrawInfo text) {
// for test purposes
// drawTestBox(cv, text.bounds, text.pathRotate, text.text);
boundIntersections.queryInBox(text.bounds, tempSearch);
for (int i = 0; i < tempSearch.size(); i++) {
TextDrawInfo t = tempSearch.get(i);
if (intersects(text.bounds, text.pathRotate, t.bounds, t.pathRotate)) {
return true;
}
}
if (text.minDistance > 0) {
QuadRect boundsSearch = new QuadRect(text.bounds);
boundsSearch.inset(-Math.max(rc.getDensityValue(5.0f), text.minDistance), -rc.getDensityValue(15));
boundIntersections.queryInBox(boundsSearch, tempSearch);
// drawTestBox(cv, &boundsSearch, text.pathRotate, paintIcon, text.text, NULL/*paintText*/);
for (int i = 0; i < tempSearch.size(); i++) {
TextDrawInfo t = tempSearch.get(i);
if (t.minDistance > 0 && t.text.equals(text.text) &&
intersects(boundsSearch, text.pathRotate, t.bounds, t.pathRotate)) {
return true;
}
}
}
boundIntersections.insert(text, text.bounds);
return false;
}
private void drawTextOnCanvas(Canvas cv, String text, float centerX, float centerY, Paint paint, int shadowColor,
float textShadow) {
if (textShadow > 0) {
int c = paintText.getColor();
paintText.setStyle(Style.STROKE);
paintText.setColor(shadowColor);
paintText.setStrokeWidth(2 + textShadow);
cv.drawText(text, centerX, centerY, paint);
// reset
paintText.setStrokeWidth(2);
paintText.setStyle(Style.FILL);
paintText.setColor(c);
}
cv.drawText(text, centerX, centerY, paint);
}
public void drawTextOverCanvas(RenderingContext rc, Canvas cv, String preferredLocale) {
int size = rc.textToDraw.size();
// 1. Sort text using text order
Collections.sort(rc.textToDraw, new Comparator<TextDrawInfo>() {
@Override
public int compare(TextDrawInfo object1, TextDrawInfo object2) {
return object1.textOrder - object2.textOrder;
}
});
QuadRect r = new QuadRect(0, 0, rc.width, rc.height);
r.inset(-100, -100);
QuadTree<TextDrawInfo> nonIntersectedBounds = new QuadTree<TextDrawInfo>(r, 4, 0.6f);
for (int i = 0; i < size; i++) {
TextDrawInfo text = rc.textToDraw.get(i);
if (text.text != null && text.text.length() > 0) {
if (preferredLocale.length() > 0) {
text.text = Junidecode.unidecode(text.text);
}
// sest text size before finding intersection (it is used there)
float textSize = text.textSize * rc.textScale ;
paintText.setTextSize(textSize);
if(text.bold && text.italic) {
paintText.setTypeface(boldItalicTypeface);
} else if(text.bold) {
paintText.setTypeface(boldTypeface);
} else if(text.italic) {
paintText.setTypeface(italicTypeface);
} else {
paintText.setTypeface(defaultTypeface);
}
paintText.setFakeBoldText(text.bold);
paintText.setColor(text.textColor);
// align center y
text.centerY += (-paintText.ascent());
// calculate if there is intersection
boolean intersects = findTextIntersection(cv, rc, nonIntersectedBounds, text);
if (!intersects) {
if (text.drawOnPath != null) {
if (text.textShadow > 0) {
paintText.setColor(text.textShadowColor);
paintText.setStyle(Style.STROKE);
paintText.setStrokeWidth(2 + text.textShadow);
cv.drawTextOnPath(text.text, text.drawOnPath, 0,
text.vOffset - ( paintText.ascent()/2 + paintText.descent()), paintText);
// reset
paintText.setStyle(Style.FILL);
paintText.setStrokeWidth(2);
paintText.setColor(text.textColor);
}
cv.drawTextOnPath(text.text, text.drawOnPath, 0,
text.vOffset - ( paintText.ascent()/2 + paintText.descent()), paintText);
} else {
drawShieldIcon(rc, cv, text, text.shieldRes);
drawShieldIcon(rc, cv, text, text.shieldResIcon);
drawWrappedText(cv, text, textSize);
}
}
}
}
}
private void drawShieldIcon(RenderingContext rc, Canvas cv, TextDrawInfo text, String sr) {
if (sr != null) {
float coef = rc.getDensityValue(rc.screenDensityRatio * rc.textScale);
Bitmap ico = RenderingIcons.getIcon(context, sr, true);
if (ico != null) {
float left = text.centerX - ico.getWidth() / 2 * coef - 0.5f;
float top = text.centerY - ico.getHeight() / 2 * coef - paintText.descent() - 0.5f;
if(rc.screenDensityRatio != 1f){
RectF rf = new RectF(left, top, left + ico.getWidth() * coef,
top + ico.getHeight() * coef);
Rect src = new Rect(0, 0, ico.getWidth(), ico
.getHeight());
cv.drawBitmap(ico, src, rf, paintIcon);
} else {
cv.drawBitmap(ico, left, top, paintIcon);
}
}
}
}
private void drawWrappedText(Canvas cv, TextDrawInfo text, float textSize) {
if (text.textWrap == 0) {
// set maximum for all text
text.textWrap = 40;
}
if (text.text.length() > text.textWrap) {
int start = 0;
int end = text.text.length();
int lastSpace = -1;
int line = 0;
int pos = 0;
int limit = 0;
while (pos < end) {
lastSpace = -1;
limit += text.textWrap;
while (pos < limit && pos < end) {
if (!Character.isLetterOrDigit(text.text.charAt(pos))) {
lastSpace = pos;
}
pos++;
}
if (lastSpace == -1 || pos == end) {
drawTextOnCanvas(cv, text.text.substring(start, pos), text.centerX, text.centerY + line * (textSize + 2),
paintText, text.textShadowColor, text.textShadow);
start = pos;
} else {
drawTextOnCanvas(cv, text.text.substring(start, lastSpace), text.centerX, text.centerY + line * (textSize + 2),
paintText, text.textShadowColor, text.textShadow);
start = lastSpace + 1;
limit += (start - pos) - 1;
}
line++;
}
} else {
drawTextOnCanvas(cv, text.text, text.centerX, text.centerY, paintText, text.textShadowColor, text.textShadow);
}
}
private void createTextDrawInfo(final BinaryMapDataObject o, RenderingRuleSearchRequest render, RenderingContext rc, TagValuePair pair, final float xMid, float yMid,
Path path, final PointF[] points, String name, String tagName) {
render.setInitialTagValueZoom(pair.tag, pair.value, rc.zoom, o);
render.setIntFilter(render.ALL.R_TEXT_LENGTH, name.length());
render.setStringFilter(render.ALL.R_NAME_TAG, tagName);
if(render.search(RenderingRulesStorage.TEXT_RULES)){
if(render.getFloatPropertyValue(render.ALL.R_TEXT_SIZE) > 0){
final TextDrawInfo text = new TextDrawInfo(name);
text.fillProperties(rc, render, xMid, yMid);
final String tagName2 = render.getStringPropertyValue(render.ALL.R_NAME_TAG2);
if (!Algorithms.isEmpty(tagName2)) {
o.getObjectNames().forEachEntry(new TIntObjectProcedure<String>() {
@Override
public boolean execute(int tagid, String nname) {
String tagNameN2 = o.getMapIndex().decodeType(tagid).tag;
if (tagName2.equals(tagNameN2)) {
if (nname != null && nname.trim().length() > 0) {
text.text += " (" + nname +")";
}
return false;
}
return true;
}
});
}
paintText.setTextSize(text.textSize);
Rect bs = new Rect();
paintText.getTextBounds(name, 0, name.length(), bs);
text.bounds = new QuadRect(bs.left, bs.top, bs.right, bs.bottom);
text.bounds.inset(-rc.getDensityValue(3), -rc.getDensityValue(10));
boolean display = true;
if(path != null) {
text.drawOnPath = path;
display = calculatePathToRotate(rc, text, points,
render.getIntPropertyValue(render.ALL.R_TEXT_ON_PATH, 0) != 0);
}
if(text.drawOnPath == null) {
text.bounds.offset(text.centerX, text.centerY);
// shift to match alignment
text.bounds.offset(-text.bounds.width()/2, 0);
} else {
text.bounds.offset(text.centerX - text.bounds.width()/2, text.centerY - text.bounds.height()/2);
}
if(display) {
rc.textToDraw.add(text);
}
}
}
}
public void renderText(final BinaryMapDataObject obj, final RenderingRuleSearchRequest render, final RenderingContext rc,
final TagValuePair pair, final float xMid, final float yMid, final Path path, final PointF[] points) {
final TIntObjectHashMap<String> map = obj.getObjectNames();
if (map != null) {
map.forEachEntry(new TIntObjectProcedure<String>() {
@Override
public boolean execute(int tag, String name) {
if (name != null && name.trim().length() > 0) {
boolean isName = tag == obj.getMapIndex().nameEncodingType;
String nameTag = isName ? "" : obj.getMapIndex().decodeType(tag).tag;
boolean skip = false;
// not completely correct we should check "name"+rc.preferredLocale
if (isName && !rc.preferredLocale.equals("") &&
map.containsKey(obj.getMapIndex().nameEnEncodingType)) {
skip = true;
}
// if (tag == obj.getMapIndex().nameEnEncodingType && !rc.useEnglishNames) {
// skip = true;
// }
if(!skip) {
createTextDrawInfo(obj, render, rc, pair, xMid, yMid, path, points, name, nameTag);
}
}
return true;
}
});
}
}
boolean calculatePathToRotate(RenderingContext rc, TextDrawInfo p, PointF[] points, boolean drawOnPath) {
int len = points.length;
if (!drawOnPath) {
p.drawOnPath = null;
// simply calculate rotation of path used for shields
float px = 0;
float py = 0;
for (int i = 1; i < len; i++) {
px += points[i].x - points[i - 1].x;
py += points[i].y - points[i - 1].y;
}
if (px != 0 || py != 0) {
p.pathRotate = (float) (-Math.atan2(px, py) + Math.PI / 2);
}
return true;
}
boolean inverse = false;
float roadLength = 0;
boolean prevInside = false;
float visibleRoadLength = 0;
float textw = (float) p.bounds.width();
int last = 0;
int startVisible = 0;
float[] distances = new float[points.length - 1];
float normalTextLen = 1.5f * textw;
for (int i = 0; i < len; i++, last++) {
boolean inside = points[i].x >= 0 && points[i].x <= rc.width &&
points[i].x >= 0 && points[i].y <= rc.height;
if (i > 0) {
float d = (float) Math.sqrt(fsqr(points[i].x - points[i - 1].x) +
fsqr(points[i].y - points[i - 1].y));
distances[i-1]= d;
roadLength += d;
if(inside) {
visibleRoadLength += d;
if(!prevInside) {
startVisible = i - 1;
}
} else if(prevInside) {
if(visibleRoadLength >= normalTextLen) {
break;
}
visibleRoadLength = 0;
}
}
prevInside = inside;
}
if (textw >= roadLength) {
return false;
}
int startInd = 0;
int endInd = len;
if(textw < visibleRoadLength && last - startVisible > 1) {
startInd = startVisible;
endInd = last;
// display long road name in center
if (visibleRoadLength > 3 * textw) {
boolean ch ;
do {
ch = false;
if(endInd - startInd > 2 && visibleRoadLength - distances[startInd] > normalTextLen){
visibleRoadLength -= distances[startInd];
startInd++;
ch = true;
}
if(endInd - startInd > 2 && visibleRoadLength - distances[endInd - 2] > normalTextLen){
visibleRoadLength -= distances[endInd - 2];
endInd--;
ch = true;
}
} while(ch);
}
}
// shrink path to display more text
if (startInd > 0 || endInd < len) {
// find subpath
Path path = new Path();
for (int i = startInd; i < endInd; i++) {
if (i == startInd) {
path.moveTo(points[i].x, points[i].y);
} else {
path.lineTo(points[i].x, points[i].y);
}
}
p.drawOnPath = path;
}
// calculate vector of the road (px, py) to proper rotate it
float px = 0;
float py = 0;
for (int i = startInd + 1; i < endInd; i++) {
px += points[i].x - points[i - 1].x;
py += points[i].y - points[i - 1].y;
}
float scale = 0.5f;
float plen = (float) Math.sqrt(px * px + py * py);
// vector ox,oy orthogonal to px,py to measure height
float ox = -py;
float oy = px;
if(plen > 0) {
float rot = (float) (-Math.atan2(px, py) + Math.PI / 2);
if (rot < 0) rot += Math.PI * 2;
if (rot > Math.PI / 2f && rot < 3 * Math.PI / 2f) {
rot += Math.PI;
inverse = true;
ox = -ox;
oy = -oy;
}
p.pathRotate = rot;
ox *= (p.bounds.height() / plen) / 2;
oy *= (p.bounds.height() / plen) / 2;
}
p.centerX = points[startInd].x + scale * px + ox;
p.centerY = points[startInd].y + scale * py + oy;
// p.hOffset = 0;
if (inverse) {
Path path = new Path();
for (int i = endInd - 1; i >= startInd; i--) {
if (i == endInd - 1) {
path.moveTo(points[i].x, points[i].y);
} else {
path.lineTo(points[i].x, points[i].y);
}
}
p.drawOnPath = path;
}
return true;
}
}