View Javadoc

1   /***
2    *     Aedict - an EDICT browser for Android
3    Copyright (C) 2009 Martin Vysny
4    
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9    
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14  
15   You should have received a copy of the GNU General Public License
16   along with this program.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package sk.baka.aedict;
20  
21  import java.util.ArrayList;
22  import java.util.Collections;
23  import java.util.Comparator;
24  import java.util.Iterator;
25  import java.util.List;
26  import java.util.Set;
27  
28  import sk.baka.aedict.dict.DictEntry;
29  import sk.baka.aedict.dict.DictTypeEnum;
30  import sk.baka.aedict.dict.Dictionary;
31  import sk.baka.aedict.dict.KanjidicEntry;
32  import sk.baka.aedict.dict.LuceneSearch;
33  import sk.baka.aedict.dict.SearchQuery;
34  import sk.baka.aedict.kanji.Radicals;
35  import sk.baka.autils.AbstractTask;
36  import sk.baka.autils.AndroidUtils;
37  import sk.baka.autils.DialogUtils;
38  import sk.baka.autils.MiscUtils;
39  import sk.baka.autils.Progress;
40  import android.os.Bundle;
41  import android.util.DisplayMetrics;
42  import android.view.Gravity;
43  import android.view.View;
44  import android.widget.EditText;
45  import android.widget.ImageView;
46  import android.widget.ImageView.ScaleType;
47  import android.widget.TableLayout;
48  import android.widget.TableRow;
49  import android.widget.TextView;
50  
51  /***
52   * Allows search for Kanji characters using a Radical lookup.
53   * 
54   * @author Martin Vysny
55   * 
56   */
57  public class KanjiSearchRadicalActivity extends AbstractActivity {
58  	/***
59  	 * The component padding.
60  	 */
61  	private static final int PADDING_PIXELS = 3;
62  	/***
63  	 * The font size, in DIP. See {@link DisplayMetrics} for details.
64  	 */
65  	private static final int FONT_SIZE_DIP = 30;
66  
67  	@Override
68  	protected void onCreate(Bundle savedInstanceState) {
69  		super.onCreate(savedInstanceState);
70  		setContentView(R.layout.kanjisearch_radical);
71  		final TableLayout v = (TableLayout) findViewById(R.id.kanjisearchRadicals);
72  		// there is no stupid flow layout in the great Android. Oh well. Let's
73  		// emulate that with a table.
74  		// we cannot just use a simple conversion of 1 DIP = 1 pixel - it doesn't work on Archos5
75  		// do the conversion properly
76  		// fixes http://code.google.com/p/aedict/issues/detail?id=38
77  		final TextView tv = new TextView(this);
78  		tv.setTextSize(FONT_SIZE_DIP);
79  		radicalViewSizePixels = (int) tv.getPaint().getFontSpacing();
80  		final DisplayMetrics dm=new DisplayMetrics();
81  		getWindowManager().getDefaultDisplay().getMetrics(dm);
82  		radicalsPerRow = dm.widthPixels / (radicalViewSizePixels + 2 * PADDING_PIXELS);
83  		currentColumn = -1;
84  		row = null;
85  		int strokeCount = -1;
86  		// general idea: add two-state pushbutton-like textviews for each
87  		// radical. We cannot use ToggleButton as it is too large.
88  		for (final char radical : Radicals.RADICAL_ORDERING.toCharArray()) {
89  			final int strokes = Radicals.getRadical(radical).strokes;
90  			if (strokeCount != strokes) {
91  				strokeCount = strokes;
92  				addRadicalToggle(v, null, strokes);
93  			}
94  			addRadicalToggle(v, radical, strokes);
95  		}
96  		findViewById(R.id.btnRadicalsSearch).setOnClickListener(AndroidUtils.safe(this, new View.OnClickListener() {
97  
98  			public void onClick(View v) {
99  				performSearch();
100 			}
101 		}));
102 		// check that KANJIDIC exists
103 		AedictApp.getDownloader().checkDictionary(this, new Dictionary(DictTypeEnum.Kanjidic, null), null, false);
104 	}
105 
106 	/***
107 	 * The real size of the font character. Computed from {@link DisplayMetrics#scaledDensity}.
108 	 */
109 	private int radicalViewSizePixels;
110 	private int radicalsPerRow;
111 	private TableRow row = null;
112 	private int currentColumn = -1;
113 
114 	/***
115 	 * Appends a new "button" (actually an image view) to the activity, which
116 	 * toggles given radical. The button is correctly added to the next row if
117 	 * this one has no space free in the screen.
118 	 * 
119 	 * @param v
120 	 *            the layout instance.
121 	 * @param radical
122 	 *            the radical
123 	 * @param strokes
124 	 *            number of strokes in this radical.
125 	 */
126 	private void addRadicalToggle(final TableLayout v, final Character radical, final int strokes) {
127 		if (++currentColumn >= radicalsPerRow) {
128 			row = null;
129 			currentColumn = 0;
130 		}
131 		if (row == null) {
132 			row = new TableRow(this);
133 			v.addView(row);
134 		}
135 		int drawable = radical != null ? Radicals.getRadical(radical).resource : -1;
136 		final View vv;
137 		if (drawable != -1) {
138 			final ImageView iv = new ImageView(this);
139 			vv = iv;
140 			iv.setImageResource(drawable);
141 			iv.setMinimumHeight(radicalViewSizePixels + 2 * PADDING_PIXELS);
142 			iv.setMinimumWidth(radicalViewSizePixels + 2 * PADDING_PIXELS);
143 			iv.setScaleType(ScaleType.FIT_CENTER);
144 		} else {
145 			final TextView tv = new TextView(this);
146 			vv = tv;
147 			tv.setText(radical == null ? String.valueOf(strokes) : radical.toString());
148 			tv.setGravity(Gravity.CENTER);
149 			tv.setTextSize(FONT_SIZE_DIP);
150 			tv.setHeight(radicalViewSizePixels + 2 * PADDING_PIXELS);
151 			tv.setWidth(radicalViewSizePixels + 2 * PADDING_PIXELS);
152 			if (radical == null) {
153 				tv.setBackgroundColor(0xFF993333);
154 			}
155 		}
156 		vv.setPadding(PADDING_PIXELS, PADDING_PIXELS, PADDING_PIXELS, PADDING_PIXELS);
157 		if (radical != null) {
158 			final PushButtonListener pbl = new PushButtonListener(radical);
159 			vv.setOnClickListener(pbl);
160 			vv.setTag(pbl);
161 		}
162 		row.addView(vv);
163 	}
164 
165 	/***
166 	 * Adds a ToggleButton-like functionality to a view.
167 	 */
168 	private final class PushButtonListener implements View.OnClickListener {
169 
170 		public PushButtonListener(char radical) {
171 			super();
172 			this.radical = radical;
173 		}
174 
175 		public final char radical;
176 		private boolean pushed = false;
177 
178 		public boolean isPushed() {
179 			return pushed;
180 		}
181 
182 		public void onClick(View v) {
183 			pushed = !pushed;
184 			v.setBackgroundColor(pushed ? 0xFF449977 : 0x00000000);
185 			recomputeRadical();
186 		}
187 	}
188 
189 	/***
190 	 * Updates the activity caption to reflect selected radicals.
191 	 */
192 	private void recomputeRadical() {
193 		final String selectedRadicals = getRadicals();
194 		this.setTitle(getString(R.string.kanjiRadicalLookup) + ": " + selectedRadicals);
195 	}
196 
197 	/***
198 	 * Computes currently selected radicals.
199 	 */
200 	private String getRadicals() {
201 		final StringBuilder sb = new StringBuilder();
202 		final TableLayout v = (TableLayout) findViewById(R.id.kanjisearchRadicals);
203 		for (int i = 0; i < v.getChildCount(); i++) {
204 			final TableRow tr = (TableRow) v.getChildAt(i);
205 			for (int j = 0; j < tr.getChildCount(); j++) {
206 				final PushButtonListener pbl = (PushButtonListener) tr.getChildAt(j).getTag();
207 				if (pbl != null && pbl.isPushed()) {
208 					sb.append(pbl.radical);
209 				}
210 			}
211 		}
212 		return sb.toString();
213 	}
214 
215 	private Integer getInt(final int editResId) {
216 		final String text = ((EditText) findViewById(editResId)).getText().toString();
217 		try {
218 			return Integer.parseInt(text);
219 		} catch (NumberFormatException ex) {
220 			return null;
221 		}
222 	}
223 
224 	/***
225 	 * Performs kanji search.
226 	 */
227 	private void performSearch() {
228 		final String radicals = getRadicals();
229 		if (radicals.length() == 0) {
230 			new DialogUtils(this).showErrorDialog(R.string.no_radicals_selected);
231 			return;
232 		}
233 		final Integer strokes = getInt(R.id.editKanjiStrokes);
234 		Integer plusMinus = getInt(R.id.editKanjiStrokesPlusMinus);
235 		if (plusMinus != null) {
236 			if (plusMinus < 0 || plusMinus > 2) {
237 				new DialogUtils(this).showErrorDialog(R.string.plusMinusBetween0And2);
238 				return;
239 			}
240 		}
241 		new KanjiMatchTask().execute(this, radicals, strokes, plusMinus);
242 	}
243 
244 	private class KanjiMatchTask extends AbstractTask<Object, List<DictEntry>> {
245 		private final int REPORT_EACH_XTH_CHAR = 5;
246 
247 		@Override
248 		public List<DictEntry> impl(Object... params) throws Exception {
249 			publish(new Progress(AedictApp.getStr(R.string.searching), 0, 100));
250 			int charsReportCountdown = 0;
251 			int totalCharsProcessed = 0;
252 			final Set<Character> matches = Radicals.getKanjisWithRadicals(((String) params[0]).toCharArray());
253 			final List<DictEntry> entries = new ArrayList<DictEntry>();
254 			// filter the matches based on stroke count
255 			final LuceneSearch ls = new LuceneSearch(DictTypeEnum.Kanjidic, null, AedictApp.getConfig().isSorted());
256 			try {
257 				for (final Iterator<Character> kanjis = matches.iterator(); kanjis.hasNext();) {
258 					final char kanji = kanjis.next();
259 					final SearchQuery sq = SearchQuery.kanjiSearch(kanji, (Integer) params[1], (Integer) params[2]);
260 					final List<DictEntry> result = ls.search(sq, 1);
261 					DictEntry.removeInvalid(result);
262 					if (!result.isEmpty()) {
263 						// the kanji matched
264 						final DictEntry entry = result.get(0);
265 						entries.add(entry);
266 					}
267 					totalCharsProcessed++;
268 					if (++charsReportCountdown >= REPORT_EACH_XTH_CHAR) {
269 						charsReportCountdown = 0;
270 						publish(new Progress(null, totalCharsProcessed, matches.size()));
271 					}
272 					if (isCancelled()) {
273 						return null;
274 					}
275 				}
276 			} finally {
277 				MiscUtils.closeQuietly(ls);
278 			}
279 			return entries;
280 		}
281 
282 		@Override
283 		protected void cleanupAfterError(Exception ex) {
284 			// do nothing
285 		}
286 
287 		@Override
288 		protected void onSucceeded(List<DictEntry> result) {
289 			// we have the kanji list. first, sort the result list
290 			Collections.sort(result, new KanjipadComparator());
291 			// launch the analyze activity
292 			KanjiAnalyzeActivity.launch(KanjiSearchRadicalActivity.this, result,false);
293 		}
294 	}
295 
296 	/***
297 	 * Imposes an order upon kanjipad entries, such that: first, kanjis with
298 	 * lowest stroke counts are returned; next, the native EdictEntry comparator
299 	 * is used.
300 	 * 
301 	 * @author Martin Vysny
302 	 */
303 	public static class KanjipadComparator implements Comparator<DictEntry> {
304 
305 		public int compare(DictEntry object1, DictEntry object2) {
306 			final int result = getStrokes(object1).compareTo(getStrokes(object2));
307 			if (result != 0) {
308 				return result;
309 			}
310 			return object1.compareTo(object2);
311 		}
312 
313 		private Integer getStrokes(final DictEntry e) {
314 			return e instanceof KanjidicEntry ? ((KanjidicEntry) e).strokes : Integer.MAX_VALUE;
315 		}
316 	}
317 }