package org.salvipeter.ardict;
import java.awt.BorderLayout;
import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.html.HTMLDocument;
public class ArabicDictionary extends JFrame {
private static final long serialVersionUID = 1L;
private JTextField search;
private JEditorPane entries;
private Font arabic_font, english_font;
private List<String[]> dictionary;
private Map<Character, List<Integer>> index;
private SwingWorker<String, Void> worker;
public ArabicDictionary() throws FontFormatException, IOException, URISyntaxException {
InputStream is = getClass().getResourceAsStream("/ScheherazadeRegOT.ttf");
arabic_font = Font.createFont(Font.TRUETYPE_FONT, is).deriveFont(36.0f);
is.close();
english_font = new Font("SansSerif", Font.PLAIN, 16);
setTitle("Arabic Dictionary");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(400, 400);
setLocationRelativeTo(null);
JMenuBar menu = new JMenuBar();
JMenu options = new JMenu("Settings");
menu.add(options);
JMenuItem help = new JMenuItem("Help");
JMenuItem smaller = new JMenuItem("Smaller text");
JMenuItem larger = new JMenuItem("Larger text");
options.add(help); help.addActionListener(new MenuListener());
options.add(smaller); smaller.addActionListener(new MenuListener());
options.add(larger); larger.addActionListener(new MenuListener());
setJMenuBar(menu);
setLayout(new BorderLayout());
search = new JTextField();
search.setFont(arabic_font);
search.addActionListener(new SearchListener());
search.getDocument().addDocumentListener(new SearchDocListener());
entries = new JEditorPane("text/html", "");
entries.setDocument(new FontedDocument());
entries.setEditable(false);
add(search, BorderLayout.PAGE_START);
add(new JScrollPane(entries), BorderLayout.CENTER);
dictionary = new ArrayList<String[]>();
BufferedReader br = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/buckwalter-sorted.txt")));
String line;
while ((line = br.readLine()) != null) {
String words[] = line.split("\t");
dictionary.add(words);
}
br.close();
index = new HashMap<Character, List<Integer>>();
byte[] one_char = new byte[1];
byte[] one_int16 = new byte[2];
BufferedInputStream bis = new BufferedInputStream(getClass().getResourceAsStream("/buckwalter-sorted.idx"));
while (bis.read(one_char) > 0) {
char c = (char)((int)one_char[0] & 0xFF);
int i;
List<Integer> list = new ArrayList<Integer>();
while (true) {
bis.read(one_int16);
int low = (int)one_int16[0] & 0xFF;
int high = (int)one_int16[1] & 0xFF;
i = high * 256 + low;
if (i == 0)
break;
list.add(i-1);
}
index.put(buckwalterToArabic(c), list);
}
bis.close();
}
private static char buckwalterToArabic(char bw) {
final String arabic = "ابتثجحخدذرزسشصضطظعغفقكلمنهويءإأؤئآٱٰىًٌٍَُِّْةـ";
final String buckwalter = "AbtvjHxd*rzs$SDTZEgfqklmnhwy'IOW}|{`YauiFNK~op_";
int index = buckwalter.indexOf(bw);
if (index >= 0)
return arabic.charAt(index);
return bw;
}
private static boolean isDiacritic(char c) {
final String diacritics = "ًٌٍَُِّْٰ";
return diacritics.indexOf(c) >= 0;
}
private static boolean isAlif(char c) {
final String alifs = "اإأآٱ";
return alifs.indexOf(c) >= 0;
}
private static boolean isArabic(char c) {
final String arabic = "ابتثجحخدذرزسشصضطظعغفقكلمنهويءإأؤئآٱٰىًٌٍَُِّْةـ";
return arabic.indexOf(c) >= 0;
}
private static boolean isJustDiacritics(String str) {
for (char c : str.toCharArray()) {
if (!isDiacritic(c))
return false;
}
return true;
}
private static boolean isPrefixFrom(String prefix, String str, int pi, int si) {
if (prefix.length() == pi)
return true;
if (str.length() == si)
return isJustDiacritics(prefix);
if (prefix.charAt(pi) == str.charAt(si) ||
(prefix.charAt(pi) == 'ا' && isAlif(str.charAt(si))))
return isPrefixFrom(prefix, str, pi + 1, si + 1);
if (!isDiacritic(prefix.charAt(pi)) && isDiacritic(str.charAt(si)))
return isPrefixFrom(prefix, str, pi, si + 1);
return false;
}
private static boolean isPrefix(String prefix, String str) {
return isPrefixFrom(prefix, str, 0, 0);
}
private static boolean isMatching(String substr, String str) {
if (substr.endsWith(".")) {
String real_substr = substr.substring(0, substr.length() - 1);
return str.toLowerCase().contains(real_substr.toLowerCase());
}
if (substr.endsWith("-")) {
String real_substr = substr.substring(0, substr.length() - 1);
return str.matches("(?i).*\\b" + real_substr + ".*");
}
return str.matches("(?i).*\\b" + substr + "\\b.*");
}
private List<Integer> findArabicWords(String str) {
List<Integer> starters = index.get(str.charAt(0));
List<Integer> result = new ArrayList<Integer>();
if (str.length() == 1 || starters == null)
return result;
for (int i : starters) {
if (isPrefix(str, dictionary.get(i)[1]))
result.add(i);
}
return result;
}
private List<Integer> findEnglishWords(String str) {
List<Integer> result = new ArrayList<Integer>();
if (str.length() < 3)
return result;
for (int i = 0; i < dictionary.size(); ++i) {
if (isMatching(str, dictionary.get(i)[0]))
result.add(i);
}
return result;
}
private List<Integer> findWords(String str) {
if (str.length() == 0)
return new ArrayList<Integer>();
if (isArabic(str.charAt(0)))
return findArabicWords(str);
return findEnglishWords(str);
}
private String formatMatches(List<Integer> words) {
String header = ""
+ "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\n"
+ "<html><head>\n"
+ "<style type=\"text/css\"><!--\n"
+ "table { width: \"90%\" }\n"
+ ".english { font-family: \"EnglishFont\"; text-align: \"left\" }\n"
+ ".arabic { font-family: \"ArabicFont\"; text-align: \"right\" }\n"
+ "--></style></head>\n"
+ "<body><table>\n";
StringBuilder result = new StringBuilder(header);
for (int i : words) {
result.append("<tr><td class=\"english\">");
result.append(dictionary.get(i)[0]);
result.append("</td><td class=\"arabic\">");
result.append(dictionary.get(i)[1]);
result.append("</td></tr>\n");
}
result.append("</table></body></html>");
return result.toString();
}
private void changeFontSizeBy(float x)
{
arabic_font = arabic_font.deriveFont(arabic_font.getSize() * x);
english_font = english_font.deriveFont(english_font.getSize() * x);
search.setFont(arabic_font);
entries.updateUI();
}
private class FontedDocument extends HTMLDocument
{
private static final long serialVersionUID = 1L;
@Override
public Font getFont(AttributeSet attr) {
Object family = attr.getAttribute(StyleConstants.FontFamily);
if (family != null && family.equals("ArabicFont"))
return arabic_font;
return english_font;
}
}
private class MenuListener implements ActionListener
{
@Override
public void actionPerformed(ActionEvent event) {
JMenuItem item = (JMenuItem)event.getSource();
if (item.getText().equals("Smaller text")) {
changeFontSizeBy(1.0f/1.2f);
} else if (item.getText().equals("Larger text")) {
changeFontSizeBy(1.2f);
} else {
String text = ""
+ "Arabic-English / English-Arabic Dictionary\n"
+ " by Peter Salvi, 2013\n"
+ "\n"
+ "(based on the \"Buckwalter Arabic Wordlist\"\n"
+ " acquired from the Perseus Digital Library)\n"
+ "\n"
+ "Arabic => English:\n"
+ "- searches for words starting with the given text\n"
+ " (at least 2 characters)\n"
+ "- alif (ا) also stands for أ/إ/آ/ٱ (but you can be specific)\n"
+ "- tashkeel can be omitted (but counts if given)\n"
+ "- verbs are given in dictionary form (past tense)\n"
+ "\n"
+ "English => Arabic:\n"
+ "- searches for whole words (at least 3 characters)\n"
+ "- case insensitive\n"
+ "- with a final hyphen (-) it treats the word as a prefix\n"
+ "- with a final dot (.) it searches for every occurrence";
JOptionPane.showMessageDialog(null, text);
}
}
}
private class Scroller implements Runnable
{
@Override
public void run() {
entries.scrollRectToVisible(new Rectangle());
}
}
private class WordFinder extends SwingWorker<String, Void>
{
long wait;
String search_str;
public WordFinder(String search_str, long wait) {
this.search_str = search_str;
this.wait = wait;
}
@Override
protected String doInBackground() {
try {
Thread.sleep(wait);
} catch (InterruptedException e) {
}
if (isCancelled())
return "";
List<Integer> indices = findWords(search_str);
if (isCancelled())
return "";
String text = formatMatches(indices);
return text;
}
@Override
protected void done() {
if (isCancelled())
return;
try {
entries.setText(get());
SwingUtilities.invokeLater(new Thread(new Scroller()));
} catch (InterruptedException | ExecutionException e) {
}
}
}
private class SearchDocListener implements DocumentListener
{
public void changed() {
if (worker != null)
worker.cancel(true);
worker = new WordFinder(search.getText(), 300);
worker.execute();
}
@Override
public void changedUpdate(DocumentEvent event) {
changed();
}
@Override
public void insertUpdate(DocumentEvent event) {
changed();
}
@Override
public void removeUpdate(DocumentEvent event) {
changed();
}
}
private class SearchListener implements ActionListener
{
@Override
public void actionPerformed(ActionEvent event) {
search.setSelectionStart(0);
search.setSelectionEnd(search.getText().length());
}
}
public static void main(String[] args) {
JFrame frame;
try {
frame = new ArabicDictionary();
frame.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
}