diff --git a/src/ui/android/AndroidManifest.xml b/src/ui/android/AndroidManifest.xml index 63602ddb1..37f1efeca 100644 --- a/src/ui/android/AndroidManifest.xml +++ b/src/ui/android/AndroidManifest.xml @@ -27,6 +27,16 @@ + + + + + + + @@ -34,6 +44,10 @@ + + + - diff --git a/src/ui/android/res/drawable-hdpi/ic_search_black_24dp.png b/src/ui/android/res/drawable-hdpi/ic_search_black_24dp.png new file mode 100644 index 000000000..c593e7ad8 Binary files /dev/null and b/src/ui/android/res/drawable-hdpi/ic_search_black_24dp.png differ diff --git a/src/ui/android/res/drawable-mdpi/ic_search_black_24dp.png b/src/ui/android/res/drawable-mdpi/ic_search_black_24dp.png new file mode 100644 index 000000000..6b1634323 Binary files /dev/null and b/src/ui/android/res/drawable-mdpi/ic_search_black_24dp.png differ diff --git a/src/ui/android/res/drawable-xhdpi/ic_search_black_24dp.png b/src/ui/android/res/drawable-xhdpi/ic_search_black_24dp.png new file mode 100644 index 000000000..638190268 Binary files /dev/null and b/src/ui/android/res/drawable-xhdpi/ic_search_black_24dp.png differ diff --git a/src/ui/android/res/drawable-xxhdpi/ic_search_black_24dp.png b/src/ui/android/res/drawable-xxhdpi/ic_search_black_24dp.png new file mode 100644 index 000000000..3ae490ef9 Binary files /dev/null and b/src/ui/android/res/drawable-xxhdpi/ic_search_black_24dp.png differ diff --git a/src/ui/android/res/drawable-xxxhdpi/ic_search_black_24dp.png b/src/ui/android/res/drawable-xxxhdpi/ic_search_black_24dp.png new file mode 100644 index 000000000..21be57299 Binary files /dev/null and b/src/ui/android/res/drawable-xxxhdpi/ic_search_black_24dp.png differ diff --git a/src/ui/android/res/layout/activity_help.xml b/src/ui/android/res/layout/activity_help.xml index 645f061af..3cb88a569 100644 --- a/src/ui/android/res/layout/activity_help.xml +++ b/src/ui/android/res/layout/activity_help.xml @@ -8,4 +8,4 @@ android:layout_width="fill_parent" android:layout_height="fill_parent" /> - \ No newline at end of file + diff --git a/src/ui/android/res/layout/activity_semantic_graph.xml b/src/ui/android/res/layout/activity_semantic_graph.xml new file mode 100644 index 000000000..004e22a7c --- /dev/null +++ b/src/ui/android/res/layout/activity_semantic_graph.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/ui/android/res/values/strings.xml b/src/ui/android/res/values/strings.xml index ca103623e..6ce31d213 100644 --- a/src/ui/android/res/values/strings.xml +++ b/src/ui/android/res/values/strings.xml @@ -7,6 +7,7 @@ Opening Speech Input Keyboard Input + Semantic Graph Help org.grammaticalframework.ui.android.GLOBAL_PREFERENCES @@ -24,4 +25,7 @@ normalKeyboardMode internalKeyboardMode + + Search word: + Search for words in the lexicon diff --git a/src/ui/android/res/xml/searchable.xml b/src/ui/android/res/xml/searchable.xml new file mode 100644 index 000000000..7e7b6a846 --- /dev/null +++ b/src/ui/android/res/xml/searchable.xml @@ -0,0 +1,8 @@ + + + diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/LexiconSuggestionProvider.java b/src/ui/android/src/org/grammaticalframework/ui/android/LexiconSuggestionProvider.java new file mode 100644 index 000000000..7b9813b7d --- /dev/null +++ b/src/ui/android/src/org/grammaticalframework/ui/android/LexiconSuggestionProvider.java @@ -0,0 +1,52 @@ +package org.grammaticalframework.ui.android; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.provider.BaseColumns; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.app.SearchManager; +import android.net.Uri; +import android.util.Log; +import android.view.inputmethod.CompletionInfo; + +public class LexiconSuggestionProvider extends ContentProvider { + private Translator mTranslator; + + public boolean onCreate() { + return true; + } + + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + mTranslator = ((GFTranslator) getContext().getApplicationContext()).getTranslator(); + String[] columns = new String[] { + BaseColumns._ID, + SearchManager.SUGGEST_COLUMN_TEXT_1, + SearchManager.SUGGEST_COLUMN_QUERY + }; + + String query = uri.getLastPathSegment(); + MatrixCursor cursor = new MatrixCursor(columns, 100); + for (CompletionInfo info : mTranslator.lookupWordPrefix(query)) { + cursor.addRow(new String[] {Long.toString(info.getId()),info.getText().toString(),info.getText().toString()}); + } + + return cursor; + } + + public Uri insert (Uri uri, ContentValues values) { + throw new UnsupportedOperationException(); + } + + public int update (Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + public int delete (Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + public String getType (Uri uri) { + return null; + } +} diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/RotationGestureDetector.java b/src/ui/android/src/org/grammaticalframework/ui/android/RotationGestureDetector.java new file mode 100644 index 000000000..d077db1bc --- /dev/null +++ b/src/ui/android/src/org/grammaticalframework/ui/android/RotationGestureDetector.java @@ -0,0 +1,109 @@ +package org.grammaticalframework.ui.android; + +import android.view.MotionEvent; + +public class RotationGestureDetector { + + private static final int INVALID_POINTER_ID = -1; + private float fX, fY, sX, sY, focalX, focalY; + private int ptrID1, ptrID2; + private float mAngle; + private boolean firstTouch; + + private OnRotationGestureListener mListener; + + public RotationGestureDetector(OnRotationGestureListener listener) { + mListener = listener; + ptrID1 = INVALID_POINTER_ID; + ptrID2 = INVALID_POINTER_ID; + } + + public float getAngle() { + return mAngle; + } + + public boolean onTouchEvent(MotionEvent event){ + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + sX = event.getX(); + sY = event.getY(); + ptrID1 = event.getPointerId(0); + mAngle = 0; + firstTouch = true; + break; + case MotionEvent.ACTION_POINTER_DOWN: + fX = event.getX(); + fY = event.getY(); + focalX = getMidpoint(fX, sX); + focalY = getMidpoint(fY, sY); + ptrID2 = event.getPointerId(event.getActionIndex()); + mAngle = 0; + firstTouch = true; + break; + case MotionEvent.ACTION_MOVE: + if(ptrID1 != INVALID_POINTER_ID && ptrID2 != INVALID_POINTER_ID) { + float nfX, nfY, nsX, nsY; + nsX = event.getX(event.findPointerIndex(ptrID1)); + nsY = event.getY(event.findPointerIndex(ptrID1)); + nfX = event.getX(event.findPointerIndex(ptrID2)); + nfY = event.getY(event.findPointerIndex(ptrID2)); + if (firstTouch) { + mAngle = 0; + firstTouch = false; + } else { + mAngle = angleBetweenLines(fX, fY, sX, sY, nfX, nfY, nsX, nsY); + } + + if (mListener != null) { + mListener.OnRotation(this); + } + fX = nfX; + fY = nfY; + sX = nsX; + sY = nsY; + } + break; + case MotionEvent.ACTION_UP: + ptrID1 = INVALID_POINTER_ID; + break; + case MotionEvent.ACTION_POINTER_UP: + ptrID2 = INVALID_POINTER_ID; + break; + } + return true; + } + + private float getMidpoint(float a, float b) { + return (a + b) / 2; + } + + private float findAngleDelta(float angle1, float angle2) + { + angle2 = angle2 % 360.0f; + angle1 = angle1 % 360.0f; + + float dist = angle1 - angle2; + if (dist < -180.0f) + { + dist += 360.0f; + } + else if (dist > 180.0f) + { + dist -= 360.0f; + } + + return dist; + } + + private float angleBetweenLines(float fx1, float fy1, float fx2, float fy2, float sx1, float sy1, float sx2, float sy2) + { + float angle1 = (float) Math.atan2((fy1 - fy2), (fx1 - fx2)); + float angle2 = (float) Math.atan2((sy1 - sy2), (sx1 - sx2)); + + return findAngleDelta((float)Math.toDegrees(angle1),(float)Math.toDegrees(angle2)); + } + + public static interface OnRotationGestureListener { + public boolean OnRotation(RotationGestureDetector detector); + } +} diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/SemanticGraph.java b/src/ui/android/src/org/grammaticalframework/ui/android/SemanticGraph.java new file mode 100644 index 000000000..50f83bda0 --- /dev/null +++ b/src/ui/android/src/org/grammaticalframework/ui/android/SemanticGraph.java @@ -0,0 +1,235 @@ +package org.grammaticalframework.ui.android; + +import java.util.*; + +public class SemanticGraph { + private Map nodes; + private List edges; + + private float layoutMinX; + private float layoutMaxX; + private float layoutMinY; + private float layoutMaxY; + + public SemanticGraph() { + nodes = new HashMap(); + edges = new ArrayList(); + + layoutMinX = 0; + layoutMaxX = 0; + layoutMinY = 0; + layoutMaxY = 0; + } + + public Node addNode(String lemma) { + Node n = nodes.get(lemma); + if (n == null) { + n = new Node(lemma, new String[(int) (10*Math.random())]); + } + nodes.put(lemma,n); + return n; + } + + public Node getNode(String lemma) { + return nodes.get(lemma); + } + + public Collection getNodes() { + return Collections.unmodifiableCollection(nodes.values()); + } + + public Edge addEdge(Node node1, Node node2) { + Edge edge = new Edge(node1, node2); + edges.add(edge); + return edge; + } + + private static final int LAYOUT_ITERATIONS = 500; + private static final float LAYOUT_K = 2; + private static final float LAYOUT_C = 0.01f; + private static final float LAYOUT_MAX_VERTEX_MOVEMENT = 0.5f; + private static final float LAYOUT_MAX_REPULSIVE_FORCE_DISTANCE = 6; + + public void layout() { + layoutPrepare(); + for (int i = 0; i < LAYOUT_ITERATIONS; i++) { + layoutIteration(); + } + layoutCalcBounds(); + } + + public float getLayoutMinX() { + return layoutMinX; + } + + public float getLayoutMaxX() { + return layoutMaxX; + } + + public float getLayoutMinY() { + return layoutMinY; + } + + public float getLayoutMaxY() { + return layoutMaxY; + } + + private void layoutPrepare() { + for (Node node : nodes.values()) { + node.layoutForceX = 0; + node.layoutForceY = 0; + } + } + + private void layoutIteration() { + List prev = new ArrayList(); + for(Node node1 : this.nodes.values()) { + for (Node node2 : prev) { + layoutRepulsive(node1, node2); + } + prev.add(node1); + } + + // Forces on nodes due to edge attractions + for (Edge edge : edges) { + layoutAttractive(edge); + } + + // Move by the given force + for (Node node : nodes.values()) { + float xmove = LAYOUT_C * node.layoutForceX; + float ymove = LAYOUT_C * node.layoutForceY; + + float max = LAYOUT_MAX_VERTEX_MOVEMENT; + if (xmove > max) xmove = max; + if (xmove < -max) xmove = -max; + if (ymove > max) ymove = max; + if (ymove < -max) ymove = -max; + + node.layoutPosX += xmove; + node.layoutPosY += ymove; + node.layoutForceX = 0; + node.layoutForceY = 0; + } + } + + private void layoutRepulsive(Node node1, Node node2) { + float dx = node2.layoutPosX - node1.layoutPosX; + float dy = node2.layoutPosY - node1.layoutPosY; + float d2 = dx * dx + dy * dy; + if (d2 < 0.01) { + dx = (float) (0.1 * Math.random() + 0.1); + dy = (float) (0.1 * Math.random() + 0.1); + d2 = dx * dx + dy * dy; + } + float d = (float) Math.sqrt(d2); + if (d < LAYOUT_MAX_REPULSIVE_FORCE_DISTANCE) { + float repulsiveForce = LAYOUT_K * LAYOUT_K / d; + node2.layoutForceX += repulsiveForce * dx / d; + node2.layoutForceY += repulsiveForce * dy / d; + node1.layoutForceX -= repulsiveForce * dx / d; + node1.layoutForceY -= repulsiveForce * dy / d; + } + } + + private void layoutAttractive(Edge edge) { + Node node1 = edge.source; + Node node2 = edge.target; + + float dx = node2.layoutPosX - node1.layoutPosX; + float dy = node2.layoutPosY - node1.layoutPosY; + float d2 = dx * dx + dy * dy; + if (d2 < 0.01) { + dx = (float) (0.1 * Math.random() + 0.1); + dy = (float) (0.1 * Math.random() + 0.1); + d2 = dx * dx + dy * dy; + } + float d = (float) Math.sqrt(d2); + if (d > LAYOUT_MAX_REPULSIVE_FORCE_DISTANCE) { + d = LAYOUT_MAX_REPULSIVE_FORCE_DISTANCE; + d2 = d * d; + } + float attractiveForce = (d2 - LAYOUT_K * LAYOUT_K) / LAYOUT_K; + attractiveForce *= Math.log(edge.attraction) * 0.5 + 1; + + node2.layoutForceX -= attractiveForce * dx / d; + node2.layoutForceY -= attractiveForce * dy / d; + node1.layoutForceX += attractiveForce * dx / d; + node1.layoutForceY += attractiveForce * dy / d; + } + + private void layoutCalcBounds() { + float minx = Float.POSITIVE_INFINITY, + maxx = Float.NEGATIVE_INFINITY, + miny = Float.POSITIVE_INFINITY, + maxy = Float.NEGATIVE_INFINITY; + + for (Node node : nodes.values()) { + float x = node.layoutPosX; + float y = node.layoutPosY; + + if (x > maxx) maxx = x; + if (x < minx) minx = x; + if (y > maxy) maxy = y; + if (y < miny) miny = y; + } + + layoutMinX = minx; + layoutMaxX = maxx; + layoutMinY = miny; + layoutMaxY = maxy; + } + + public static class Node { + private String lemma; + private String[] senses; + + private float layoutPosX; + private float layoutPosY; + private float layoutForceX; + private float layoutForceY; + + + private Node(String lemma, String[] senses) { + this.lemma = lemma; + this.senses = senses; + + layoutPosX = 0; + layoutPosY = 0; + layoutForceX = 0; + layoutForceY = 0; + } + + public String getLemma() { + return lemma; + } + + public int getSenseCount() { + return senses.length; + } + + public String getSenseId(int i) { + return senses[i]; + } + + public float getLayoutX() { + return layoutPosX; + } + + public float getLayoutY() { + return layoutPosY; + } + } + + public static class Edge { + private Node source; + private Node target; + private float attraction; + + private Edge(Node source, Node target) { + this.source = source; + this.target = target; + this.attraction = 1; + } + } +} diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/SemanticGraphActivity.java b/src/ui/android/src/org/grammaticalframework/ui/android/SemanticGraphActivity.java new file mode 100644 index 000000000..893f023b2 --- /dev/null +++ b/src/ui/android/src/org/grammaticalframework/ui/android/SemanticGraphActivity.java @@ -0,0 +1,102 @@ +package org.grammaticalframework.ui.android; + +import java.util.*; + +import android.app.Activity; +import android.app.SearchManager; +import android.os.Bundle; +import android.os.AsyncTask; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ImageView; +import android.widget.Toast; +import android.content.Intent; + +import org.grammaticalframework.pgf.MorphoAnalysis; +import org.grammaticalframework.ui.android.LanguageSelector.OnLanguageSelectedListener; + +public class SemanticGraphActivity extends Activity { + private Translator mTranslator; + + private LanguageSelector mLanguageView; + private View mProgressBarView = null; + private ImageView mAddWordButton; + private SemanticGraphView mGraphView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_semantic_graph); + + mTranslator = ((GFTranslator) getApplicationContext()).getTranslator(); + + mLanguageView = (LanguageSelector) findViewById(R.id.show_language); + mLanguageView.setLanguages(mTranslator.getAvailableLanguages()); + mLanguageView.setOnLanguageSelectedListener(new OnLanguageSelectedListener() { + @Override + public void onLanguageSelected(final Language language) { + new AsyncTask() { + @Override + protected void onPreExecute() { + showProgressBar(); + } + + @Override + protected Void doInBackground(Void... params) { + mTranslator.setSourceLanguage(language); + mTranslator.isTargetLanguageLoaded(); + return null; + } + + @Override + protected void onPostExecute(Void result) { + hideProgressBar(); + } + }.execute(); + } + }); + + mAddWordButton = (ImageView) findViewById(R.id.add_word); + + mGraphView = (SemanticGraphView) findViewById(R.id.semantic_graph); + + mAddWordButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + onSearchRequested(); + } + }); + + mProgressBarView = findViewById(R.id.progressBarView); + } + + @Override + protected void onResume() { + super.onResume(); + + mLanguageView.setSelectedLanguage(mTranslator.getSourceLanguage()); + } + + private void showProgressBar() { + mProgressBarView.setVisibility(View.VISIBLE); + } + + private void hideProgressBar() { + mProgressBarView.setVisibility(View.GONE); + } + + @Override + protected void onNewIntent (Intent intent) { + if (Intent.ACTION_SEARCH.equals(intent.getAction())) { + String query = intent.getStringExtra(SearchManager.QUERY); + List list = mTranslator.lookupMorpho(query); + if (list == null || list.size() == 0) { + Toast toast = Toast.makeText(this, "\""+query+"\" doesn't match", Toast.LENGTH_SHORT); + toast.show(); + } else { + mGraphView.getGraph().addNode(query); + mGraphView.refresh(); + } + } + } +} diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/SemanticGraphView.java b/src/ui/android/src/org/grammaticalframework/ui/android/SemanticGraphView.java new file mode 100644 index 000000000..81785d5ce --- /dev/null +++ b/src/ui/android/src/org/grammaticalframework/ui/android/SemanticGraphView.java @@ -0,0 +1,144 @@ +package org.grammaticalframework.ui.android; + +import android.view.View; +import android.view.GestureDetector; +import android.view.ScaleGestureDetector; +import android.view.MotionEvent; +import android.graphics.Paint; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Rect; +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; + +public class SemanticGraphView extends View implements GestureDetector.OnGestureListener, ScaleGestureDetector.OnScaleGestureListener, RotationGestureDetector.OnRotationGestureListener { + + private SemanticGraph mGraph = new SemanticGraph(); + + private float mStartX = 0; + private float mStartY = 0; + private float mFocusX = 0; + private float mFocusY = 0; + private float mScale = 1; + private float mAngle = 0; + + private Paint mPaint; + + private GestureDetector mGD; + private ScaleGestureDetector mSGD; + private RotationGestureDetector mRGD; + + private static final float TEXT_PAD = 10; + private static final float SENSE_POINT_RADIUS = 5; + + public SemanticGraphView(Context context, AttributeSet attrs) { + super(context, attrs); + + mPaint = new Paint(); + mPaint.setTextSize(60); + + mGD = new GestureDetector(this); + mSGD = new ScaleGestureDetector(context,this); + mRGD = new RotationGestureDetector(this); + } + + public SemanticGraph getGraph() { + return mGraph; + } + + public void refresh() { + mGraph.layout(); + invalidate(); + } + + @Override + protected void onDraw (Canvas canvas) { + super.onDraw(canvas); + + canvas.scale(mScale,mScale,mFocusX,mFocusY); + canvas.translate(mStartX, mStartY); + canvas.rotate(mAngle); + + Rect bounds = new Rect(); + + float dx = mGraph.getLayoutMinX(); + float sx = getWidth()/(mGraph.getLayoutMaxX()-mGraph.getLayoutMinX()); + float dy = mGraph.getLayoutMinY(); + float sy = getHeight()/(mGraph.getLayoutMaxY()-mGraph.getLayoutMinY()); + for (SemanticGraph.Node node : mGraph.getNodes()) { + mPaint.getTextBounds(node.getLemma().toCharArray(), 0, node.getLemma().length(), bounds); + + float left = (node.getLayoutX()-dx)*sx - TEXT_PAD; + float base = (node.getLayoutY()-dy)*sy; + float top = base - bounds.height() - TEXT_PAD; + float right = left + bounds.right + TEXT_PAD; + float bottom = base + bounds.bottom + TEXT_PAD; + float sqrt2 = (float) Math.sqrt(2); + + canvas.drawText(node.getLemma(), left + TEXT_PAD, base, mPaint); + + float pi = (float) Math.PI; + for (int i = 0; i < node.getSenseCount(); i++) { + float phi = i * 2*pi / node.getSenseCount(); + float cx = ((left+right) + (right-left)*sqrt2*((float) Math.sin(phi)))/2; + float cy = ((top+bottom) + (bottom-top)*sqrt2*((float) Math.cos(phi)))/2; + + canvas.drawCircle(cx,cy,SENSE_POINT_RADIUS,mPaint); + } + } + } + + public boolean onTouchEvent(MotionEvent ev) { + mGD.onTouchEvent(ev); + mSGD.onTouchEvent(ev); + mRGD.onTouchEvent(ev); + return true; + } + + public boolean onDown(MotionEvent e) { + return true; + } + + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + return true; + } + + public void onLongPress(MotionEvent e) { + } + + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + mStartX -= distanceX; + mStartY -= distanceY; + invalidate(); + return true; + } + + public void onShowPress(MotionEvent e) { + } + + public boolean onSingleTapUp(MotionEvent e) { + return true; + } + + public boolean onScaleBegin(ScaleGestureDetector detector) { + return true; + } + + public boolean onScale(ScaleGestureDetector detector) { + mScale *= detector.getScaleFactor(); + mFocusX = detector.getFocusX(); + mFocusY = detector.getFocusY(); + invalidate(); + return true; + } + + public void onScaleEnd(ScaleGestureDetector detector) { + } + + public boolean OnRotation(RotationGestureDetector detector) { + mAngle -= detector.getAngle(); + invalidate(); + return true; + } +}