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
73
74
75
76
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
87
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
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
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
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
285 }
286
287 @Override
288 protected void onSucceeded(List<DictEntry> result) {
289
290 Collections.sort(result, new KanjipadComparator());
291
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 }