From e0fa95425697685401ebe9c088feb667ebd7a1fe Mon Sep 17 00:00:00 2001 From: "kr.angelov" Date: Wed, 2 Jul 2014 11:30:06 +0000 Subject: [PATCH] Visualization for parse trees on Android --- .../android/res/layout/alternative_item.xml | 14 +- src/ui/android/res/layout/lexical_item.xml | 27 +++ .../ui/android/AlternativesActivity.java | 133 ++++++----- .../ui/android/ParseTreeView.java | 221 ++++++++++++++++++ .../ui/android/Translator.java | 5 + 5 files changed, 331 insertions(+), 69 deletions(-) create mode 100644 src/ui/android/res/layout/lexical_item.xml create mode 100644 src/ui/android/src/org/grammaticalframework/ui/android/ParseTreeView.java diff --git a/src/ui/android/res/layout/alternative_item.xml b/src/ui/android/res/layout/alternative_item.xml index 49f090a26..40106c989 100644 --- a/src/ui/android/res/layout/alternative_item.xml +++ b/src/ui/android/res/layout/alternative_item.xml @@ -12,16 +12,18 @@ android:paddingRight="5dp" /> - + \ No newline at end of file diff --git a/src/ui/android/res/layout/lexical_item.xml b/src/ui/android/res/layout/lexical_item.xml new file mode 100644 index 000000000..e7f551491 --- /dev/null +++ b/src/ui/android/res/layout/lexical_item.xml @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/AlternativesActivity.java b/src/ui/android/src/org/grammaticalframework/ui/android/AlternativesActivity.java index d394417ce..964daf92f 100644 --- a/src/ui/android/src/org/grammaticalframework/ui/android/AlternativesActivity.java +++ b/src/ui/android/src/org/grammaticalframework/ui/android/AlternativesActivity.java @@ -103,8 +103,8 @@ public class AlternativesActivity extends ListActivity { ImageView arrow = (ImageView) expandedView.findViewById(R.id.arrow); arrow.setImageResource(R.drawable.open_arrow); - WebView inflectionView = (WebView) expandedView.findViewById(R.id.lexical_inflection); - ((RelativeLayout) expandedView).removeView(inflectionView); + View view = (View) expandedView.findViewById(R.id.desc_details); + ((RelativeLayout) expandedView).removeView(view); expandedView = null; } @@ -117,10 +117,10 @@ public class AlternativesActivity extends ListActivity { ImageView arrow = (ImageView) view.findViewById(R.id.arrow); arrow.setImageResource(R.drawable.close_arrow); - WebView inflectionView = (WebView) view.findViewById(R.id.lexical_inflection); + WebView inflectionView = (WebView) view.findViewById(R.id.desc_details); if (inflectionView == null) { inflectionView = new WebView(this); - inflectionView.setId(R.id.lexical_inflection); + inflectionView.setId(R.id.desc_details); RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); params.addRule(RelativeLayout.BELOW, R.id.lexical_desc); @@ -136,18 +136,18 @@ public class AlternativesActivity extends ListActivity { ImageView arrow = (ImageView) view.findViewById(R.id.arrow); arrow.setImageResource(R.drawable.close_arrow); - WebView inflectionView = (WebView) view.findViewById(R.id.lexical_inflection); - if (inflectionView == null) { - inflectionView = new WebView(this); - inflectionView.setId(R.id.lexical_inflection); + ParseTreeView parseView = (ParseTreeView) view.findViewById(R.id.desc_details); + if (parseView == null) { + parseView = new ParseTreeView(this); + parseView.setId(R.id.desc_details); RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); - params.addRule(RelativeLayout.BELOW, R.id.lexical_desc); - ((RelativeLayout) view).addView(inflectionView, params); + params.addRule(RelativeLayout.BELOW, R.id.alternative_desc); + ((RelativeLayout) view).addView(parseView, params); } - String content = String.format("[%.4f] %s", ep.getProb(), ep.getExpr()); - inflectionView.loadData(content, "text/plain; charset=UTF-8", null); + Object[] brackets = mTranslator.bracketedLinearize(ep.getExpr()); + parseView.setBrackets(brackets); expandedView = view; } @@ -160,16 +160,16 @@ public class AlternativesActivity extends ListActivity { public View getView(int position, View convertView, ViewGroup parent) { Object item = getItem(position); - if (convertView == null) { - LayoutInflater inflater = (LayoutInflater) - getContext().getSystemService(Activity.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(R.layout.alternative_item, null); - } - - TextView descView = (TextView) - convertView.findViewById(R.id.lexical_desc); - if (item instanceof MorphoAnalysis) { + if (convertView == null) { + LayoutInflater inflater = (LayoutInflater) + getContext().getSystemService(Activity.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.lexical_item, null); + } + + TextView descView = (TextView) + convertView.findViewById(R.id.lexical_desc); + final String lemma = ((MorphoAnalysis) item).getLemma(); String phrase = mTranslator.generateLexiconEntry(lemma); @@ -188,55 +188,62 @@ public class AlternativesActivity extends ListActivity { } } }); - } else { - if (item instanceof ExprProb) { - final ExprProb ep = (ExprProb) item; - - String phrase = mTranslator.linearize(ep.getExpr()); + } else if (item instanceof ExprProb) { + final ExprProb ep = (ExprProb) item; + + if (convertView == null) { + LayoutInflater inflater = (LayoutInflater) + getContext().getSystemService(Activity.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.alternative_item, null); + } - // parse by words, marked by %, darkest red color - if (phrase.charAt(0) == '%') { - descView.setBackgroundDrawable(getResources().getDrawable(R.drawable.second_person_worst_utterance_bg)); - phrase = phrase.substring(2); - } + TextView descView = (TextView) + convertView.findViewById(R.id.alternative_desc); - // parse by chunks, marked by *, red color - else if (phrase.charAt(0) == '*') { - descView.setBackgroundDrawable(getResources().getDrawable(R.drawable.second_person_chunk_utterance_bg)); - phrase = phrase.substring(2); - } + String phrase = mTranslator.linearize(ep.getExpr()); - // parse error or unknown translations (in []) present, darkest red color - else if (phrase.contains("parse error:") || phrase.contains("[")) { - descView.setBackgroundDrawable(getResources().getDrawable(R.drawable.second_person_worst_utterance_bg)); - } + // parse by words, marked by %, darkest red color + if (phrase.charAt(0) == '%') { + descView.setBackgroundDrawable(getResources().getDrawable(R.drawable.second_person_worst_utterance_bg)); + phrase = phrase.substring(2); + } - // parse by domain grammar, marked by +, green color - else if (phrase.charAt(0) == '+') { - descView.setBackgroundDrawable(getResources().getDrawable(R.drawable.second_person_best_utterance_bg)); - phrase = phrase.substring(2); - } - - else { - descView.setBackgroundDrawable(getResources().getDrawable(R.drawable.second_person_utterance_bg)); - } + // parse by chunks, marked by *, red color + else if (phrase.charAt(0) == '*') { + descView.setBackgroundDrawable(getResources().getDrawable(R.drawable.second_person_chunk_utterance_bg)); + phrase = phrase.substring(2); + } - descView.setText(phrase); + // parse error or unknown translations (in []) present, darkest red color + else if (phrase.contains("parse error:") || phrase.contains("[")) { + descView.setBackgroundDrawable(getResources().getDrawable(R.drawable.second_person_worst_utterance_bg)); + } - convertView.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - if (expandedView == view) - collapse(); - else if (expandedView == null) - expandExpr(view, ep); - else { - collapse(); - expandExpr(view, ep); - } + // parse by domain grammar, marked by +, green color + else if (phrase.charAt(0) == '+') { + descView.setBackgroundDrawable(getResources().getDrawable(R.drawable.second_person_best_utterance_bg)); + phrase = phrase.substring(2); + } + + else { + descView.setBackgroundDrawable(getResources().getDrawable(R.drawable.second_person_utterance_bg)); + } + + descView.setText(phrase); + + convertView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + if (expandedView == view) + collapse(); + else if (expandedView == null) + expandExpr(view, ep); + else { + collapse(); + expandExpr(view, ep); } - }); - } + } + }); } return convertView; diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/ParseTreeView.java b/src/ui/android/src/org/grammaticalframework/ui/android/ParseTreeView.java new file mode 100644 index 000000000..f443ab2eb --- /dev/null +++ b/src/ui/android/src/org/grammaticalframework/ui/android/ParseTreeView.java @@ -0,0 +1,221 @@ +package org.grammaticalframework.ui.android; + +import java.util.HashMap; +import java.util.Map; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.Pair; +import android.util.SparseArray; +import android.view.View; +import org.grammaticalframework.pgf.Bracket; + + +public class ParseTreeView extends View { + private static final float SISTER_SKIP = 25; + private static final float PARENT_SKIP = 0.5f; + private static final float ABOVE_LINE_SKIP = 0.1f; + private static final float BELOW_LINE_SKIP = 0.1f; + + private Paint paint; + private Object[] brackets; + + public ParseTreeView(Context context) { + super(context); + initialize(); + } + + public ParseTreeView(Context context, AttributeSet attrs) { + super(context, attrs); + initialize(); + } + + private void initialize() { + paint = new Paint(); + paint.setTextSize(60); + brackets = null; + } + + public Object[] getBrackets() { + return brackets; + } + + public void setBrackets(Object[] brackets) { + this.brackets = brackets; + } + + static class MeasureResult { + float width = 0.0f; + float height = 0.0f; + float nodeTab = 0.0f; + float nodeCenter = 0.0f; + float childTab = 0.0f; + float localWidth = 0.0f; + float localHeight = 0.0f; + } + + private MeasureResult mr = new MeasureResult(); + private SparseArray coords = new SparseArray(); + + private void measureTree(Object o, PointF zeroPoint) { + if (o instanceof Bracket) { + Bracket bracket = (Bracket) o; + + Rect bounds = new Rect(); + paint.getTextBounds(bracket.cat,0,bracket.cat.length(),bounds); + float localWidth = bounds.width(); + float localHeight = bounds.height(); + float layerHeight = localHeight * (1.0f + BELOW_LINE_SKIP + ABOVE_LINE_SKIP + PARENT_SKIP); + + PointF local = coords.get(bracket.fid); + if (local == null) { + if (zeroPoint != null) + coords.put(bracket.fid, zeroPoint); + } else { + localWidth = 0; + } + + float subWidth = 0.0f; + float subHeight = 0.0f; + float nodeCenter = 0.0f; + for (int i = 0; i < bracket.children.length; i++) { + measureTree(bracket.children[i], zeroPoint); + + if (i == 0) { + nodeCenter += (subWidth + mr.nodeCenter) / 2.0; + } + if (i == bracket.children.length - 1) { + nodeCenter += (subWidth + mr.nodeCenter) / 2.0; + } + + subWidth += mr.width; + if (i < bracket.children.length - 1) { + subWidth += SISTER_SKIP; + } + + if (subHeight < mr.height) + subHeight = mr.height; + } + float localLeft = localWidth / 2.0f; + float subLeft = nodeCenter; + float totalLeft = Math.max(localLeft, subLeft); + float localRight = localWidth / 2.0f; + float subRight = subWidth - nodeCenter; + float totalRight = Math.max(localRight, subRight); + mr.width = totalLeft + totalRight; + mr.height = layerHeight + subHeight; + mr.childTab = totalLeft - subLeft; + mr.nodeTab = totalLeft - localLeft; + mr.nodeCenter = nodeCenter + mr.childTab; + mr.localWidth = localWidth; + mr.localHeight = localHeight; + } else { + String word = o.toString(); + + Rect bounds = new Rect(); + paint.getTextBounds(word,0,word.length(),bounds); + mr.width = bounds.width(); + mr.height = bounds.height(); + mr.nodeTab = 0.0f; + mr.nodeCenter = bounds.width() / 2.0f; + mr.childTab = 0.0f; + mr.localWidth = bounds.width(); + mr.localHeight = bounds.height(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (brackets == null) { + setMeasuredDimension(0, 0); + return; + } + + float width = 0.0f; + float height = 0.0f; + PointF zeroPoint = new PointF(); + for (int i = 0; i < brackets.length; i++) { + measureTree(brackets[i], zeroPoint); + + width += mr.width; + if (i < brackets.length - 1) { + width += SISTER_SKIP; + } + + if (height < mr.height) + height = mr.height; + } + + width += 10; + height += 10; + + int w = getPaddingLeft() + (int) width + getPaddingRight(); + int h = getPaddingTop() + (int) height + getPaddingBottom(); + + setMeasuredDimension(w, h); + } + + private void drawTree(Canvas canvas, + float x, float y, float bottom, PointF parentPoint, + Object o) { + if (o instanceof Bracket) { + Bracket bracket = (Bracket) o; + + PointF lineStart = coords.get(bracket.fid); + if (lineStart == null) { + lineStart = new PointF(x + mr.nodeCenter, y + mr.localHeight * (1.0f + BELOW_LINE_SKIP)); + coords.put(bracket.fid, lineStart); + + if (parentPoint != null) { + float lineEndX = x + mr.nodeCenter; + float lineEndY = y; + canvas.drawLine(parentPoint.x, parentPoint.y, lineEndX, lineEndY, paint); + } + + canvas.drawText(bracket.cat, x+mr.nodeTab, y+mr.localHeight, paint); + } + + float layerMultiplier = (1.0f + BELOW_LINE_SKIP + ABOVE_LINE_SKIP + PARENT_SKIP); + float layerHeight = mr.localHeight * layerMultiplier; + float childStartX = x + mr.childTab; + float childStartY = y + layerHeight; + for (int i = 0; i < bracket.children.length; i++) { + Object child = bracket.children[i]; + measureTree(child, null); + float w = mr.width; + drawTree(canvas, childStartX, childStartY, bottom, lineStart, child); + childStartX += w + SISTER_SKIP; + } + } else { + float lineEndX = x + mr.nodeCenter; + float lineEndY = bottom - mr.height; + canvas.drawLine(parentPoint.x, parentPoint.y, lineEndX, lineEndY, paint); + canvas.drawText(o.toString(), x, bottom, paint); + } + } + + @Override + protected void onDraw (Canvas canvas) { + super.onDraw(canvas); + + if (brackets == null) { + return; + } + + coords.clear(); + + float startX = getPaddingLeft(); + for (int i = 0; i < brackets.length; i++) { + Object child = brackets[i]; + + measureTree(child, null); + float w = mr.width; + drawTree(canvas, startX, getPaddingTop(), getPaddingTop()+mr.height, null, child); + startX += w + SISTER_SKIP; + } + } +} diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/Translator.java b/src/ui/android/src/org/grammaticalframework/ui/android/Translator.java index 4643eaf4e..e9a5ecee5 100644 --- a/src/ui/android/src/org/grammaticalframework/ui/android/Translator.java +++ b/src/ui/android/src/org/grammaticalframework/ui/android/Translator.java @@ -340,6 +340,11 @@ public class Translator { return targetLang.linearize(expr); } + public Object[] bracketedLinearize(Expr expr) { + Concr targetLang = getTargetConcr(); + return targetLang.bracketedLinearize(expr); + } + public String generateLexiconEntry(String lemma) { Concr sourceLang = getSourceConcr(); Concr targetLang = getTargetConcr();