Revision: 5328 http://sourceforge.net/p/jump-pilot/code/5328 Author: michaudm Date: 2017-01-20 22:29:48 +0000 (Fri, 20 Jan 2017) Log Message: ----------- Add a BeanTool to search for a plugin in the menus
Modified Paths: -------------- core/trunk/ChangeLog Added Paths: ----------- core/trunk/src/com/vividsolutions/jump/util/SuggestTree.java core/trunk/src/com/vividsolutions/jump/workbench/ui/SuggestTreeComboBox.java Modified: core/trunk/ChangeLog =================================================================== --- core/trunk/ChangeLog 2017-01-17 18:11:26 UTC (rev 5327) +++ core/trunk/ChangeLog 2017-01-20 22:29:48 UTC (rev 5328) @@ -3,6 +3,9 @@ # 2. make sure that lines break at 80 chars for constricted display situations #<-------------------------------- 80 chars ----------------------------------># +2017-01-20 mmichaud <m.michael.mich...@orange.fr> + * Add a BeanTool to search for a plugin in the menus + 2017-01-17 mmichaud <m.michael.mich...@orange.fr> * Upgrade GraphToolBox to 0.4.0 for PLUS version Added: core/trunk/src/com/vividsolutions/jump/util/SuggestTree.java =================================================================== --- core/trunk/src/com/vividsolutions/jump/util/SuggestTree.java (rev 0) +++ core/trunk/src/com/vividsolutions/jump/util/SuggestTree.java 2017-01-20 22:29:48 UTC (rev 5328) @@ -0,0 +1,683 @@ +/* + * Copyright (c) 2008-2014 Nicolai Diethelm + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * (MIT License) + */ + +package com.vividsolutions.jump.util; + +import java.util.Arrays; +import java.util.NoSuchElementException; +import java.util.Random; + +/** + * An autocomplete data structure that enables you to quickly look up the top k + * terms with a given prefix in a set of weighted terms. The structure is based + * on a compressed trie of the terms (implemented as a randomized ternary search + * tree) in which each node holds a weight-ordered list of the k + * highest-weighted terms in its subtree. + * + * <p>Note that this implementation is not synchronized. If multiple threads + * access a Suggest Tree concurrently, and at least one of the threads modifies + * the tree, it must be synchronized externally. + */ +public class SuggestTree { + + private final Random random = new Random(); + private final int k; + private Node root; + private int size; + + /** + * Creates a Suggest Tree with the specified k-value. + * + * @param k the maximum number of autocomplete suggestions to return for a + * given prefix + * @throws IllegalArgumentException if the specified k-value is less than 1 + */ + public SuggestTree(int k) { + if(k < 1) + throw new IllegalArgumentException(); + this.k = k; + root = null; + size = 0; + } + + /** + * Returns the k highest-weighted terms in the tree that start with the + * specified prefix, or null if there is no such term. + * + * @param prefix the prefix for which to return autocomplete suggestions + * @return the k highest-weighted terms in the tree that start with the + * specified prefix, or null if there is no such term + * @throws IllegalArgumentException if the specified prefix is an empty + * string + * @throws NullPointerException if the specified prefix is null + */ + public Node getAutocompleteSuggestions(String prefix) { + return search(prefix); + } + + /** + * Returns the tree entry for the specified term, or null if there is no + * such entry. + * + * @param term the term for which to return the corresponding tree entry + * @return the tree entry for the specified term, or null if there is no + * such entry + * @throws IllegalArgumentException if the specified term is an empty string + * @throws NullPointerException if the specified term is null + */ + public Entry getEntry(String term) { + Node n = search(term); + if(n == null || n.charEnd > term.length()) + return null; + else + return n.entry; + } + + /** + * Returns an iterator over the terms in the tree. + * + * @return an iterator over the terms in the tree + */ + public Iterator iterator() { + return new Iterator(); + } + + /** + * Returns the number of terms in the tree. + * + * @return the number of terms in the tree + */ + public int size() { + return size; + } + + /** + * Inserts the specified term with the specified weight into the tree, or + * reweights the term if it is already present. + * + * @param term the term to be inserted or reweighted + * @param weight the weight to be assigned to the term + * @throws IllegalArgumentException if the specified term is an empty string + * or much too long (longer than 32767 characters) + * @throws NullPointerException if the specified term is null + */ + public void put(String term, int weight) { + if(term.isEmpty() || term.length() > Short.MAX_VALUE) + throw new IllegalArgumentException(); + if(root == null) { + root = new Node(term, weight, 0, null); + finishInsertion(root); + return; + } + int i = 0; + Node n = root; + while(true) { + if(term.charAt(i) < n.firstChar) { + if(n.left == null) { + n.left = new Node(term, weight, i, n); + finishInsertion(n.left); + return; + }else + n = n.left; + }else if(term.charAt(i) > n.firstChar) { + if(n.right == null) { + n.right = new Node(term, weight, i, n); + finishInsertion(n.right); + return; + }else + n = n.right; + }else{ + while(++i < n.charEnd) { + if(i == term.length() || term.charAt(i) != n.charAt(i)) { + n = split(n, i); + break; + } + } + if(i < term.length()) { + if(n.mid == null) { + n.mid = new Node(term, weight, i, n); + finishInsertion(n.mid); + return; + }else + n = n.mid; + }else{ + if(n.entry == null) { + n.entry = new Entry(term, weight); + finishInsertion(n); + }else if(n.entry.weight < weight) + increaseWeight(n, weight); + else if(n.entry.weight > weight) + reduceWeight(n, weight); + return; + } + } + } + } + + /** + * Removes the specified term from the tree. + * + * @param term the term to be removed + * @throws IllegalArgumentException if the specified term is an empty string + * @throws NullPointerException if the specified term is null + */ + public void remove(String term) { + Node n = search(term); + if(n == null || n.entry == null || n.charEnd > term.length()) + return; + randomizeDeletion(n); + Entry e = n.entry; + n.entry = null; + if(n.mid == null) { + Node p = parent(n); + delete(n); + n = p; + } + if(n != null && n.entry == null && n.mid.left == null && n.mid.right == null) { + Node p = parent(n); + merge(n, n.mid); + n = p; + } + removeFromLists(e, n); + size--; + } + + private Node search(String s) { + if(s.isEmpty()) + throw new IllegalArgumentException(); + int i = 0; + Node n = root; + while(n != null) { + if(s.charAt(i) < n.firstChar) + n = n.left; + else if(s.charAt(i) > n.firstChar) + n = n.right; + else{ + while(++i < n.charEnd) { + if(i == s.length()) + return n; + else if(s.charAt(i) != n.charAt(i)) + return null; + } + if(i == s.length()) + return n; + else + n = n.mid; + } + } + return null; + } + + private Node split(Node n, int position) { + Node s = new Node(n, position); + if(n.list.length == k) + s.list = Arrays.copyOf(n.list, k); + else // the list is copied in insertIntoLists() + s.list = n.list; + if(n.left != null) + n.left.up = s; + if(n.right != null) + n.right.up = s; + if(n == root) + root = s; + else if(n == n.up.left) + n.up.left = s; + else if(n == n.up.right) + n.up.right = s; + else + n.up.mid = s; + n.firstChar = n.charAt(position); + n.left = n.right = null; + n.up = s; + return s; + } + + private void merge(Node n, Node m) { + m.firstChar = n.firstChar; + m.left = n.left; + m.right = n.right; + m.up = n.up; + if(n.left != null) + n.left.up = m; + if(n.right != null) + n.right.up = m; + if(n == root) + root = m; + else if(n == n.up.left) + n.up.left = m; + else if(n == n.up.right) + n.up.right = m; + else + n.up.mid = m; + } + + private void delete(Node n) { + if(n == root) + root = null; + else if(n == n.up.left) + n.up.left = null; + else if(n == n.up.right) + n.up.right = null; + else + n.up.mid = null; + } + + private void finishInsertion(Node n) { + randomizeInsertion(n); + insertIntoLists(n); + size++; + } + + private void randomizeInsertion(Node n) { + n.entry.priority = random.nextInt(); + n.priority = higherPriority(n.entry, n.mid); + while(n != root && n.up.priority < n.priority) { + if(n == n.up.left) + rotateRight(n.up); + else if(n == n.up.right) + rotateLeft(n.up); + else{ + n.up.priority = n.priority; + n = n.up; + } + } + } + + private void randomizeDeletion(Node n) { + int p = n.entry.priority; + n.entry.priority = Integer.MIN_VALUE; + while(n != null && n.priority == p) { + n.priority = higherPriority(n.entry, n.mid); + Node h = higherPriorityNode(n.left, n.right); + while(h != null && h.priority >= n.priority) { + if(h == n.left) + rotateRight(n); + else + rotateLeft(n); + h = higherPriorityNode(n.left, n.right); + } + n = parent(n); + } + } + + private int higherPriority(Entry e, Node n) { + if(e == null) + return n.priority; + else if(n == null) + return e.priority; + else if(e.priority < n.priority) + return n.priority; + else + return e.priority; + } + + private Node higherPriorityNode(Node n, Node m) { + if(n == null) + return m; + else if(m == null) + return n; + else if(n.priority < m.priority) + return m; + else + return n; + } + + private void rotateLeft(Node n) { + Node r = n.right; + n.right = r.left; + if(r.left != null) + r.left.up = n; + r.up = n.up; + if(n == root) + root = r; + else if(n == n.up.left) + n.up.left = r; + else if(n == n.up.right) + n.up.right = r; + else + n.up.mid = r; + r.left = n; + n.up = r; + } + + private void rotateRight(Node n) { + Node l = n.left; + n.left = l.right; + if(l.right != null) + l.right.up = n; + l.up = n.up; + if(n == root) + root = l; + else if(n == n.up.left) + n.up.left = l; + else if(n == n.up.right) + n.up.right = l; + else + n.up.mid = l; + l.right = n; + n.up = l; + } + + private void insertIntoLists(Node n) { + Entry e = n.entry; + for( ; n != null; n = parent(n)) { + if(n.mid == null) + n.list = new Entry[1]; + else if(n.list.length < k) + n.list = Arrays.copyOf(n.list, n.list.length + 1); + else if(e.weight <= n.list[k - 1].weight) + return; + int i = n.list.length - 1; + while(i > 0 && e.weight > n.list[i - 1].weight) { + n.list[i] = n.list[i - 1]; + i--; + } + n.list[i] = e; + } + } + + private void increaseWeight(Node n, int newWeight) { + Entry e = n.entry; + e.weight = newWeight; + for( ; n != null; n = parent(n)) { + int i = n.listIndexOf(e); + if(i == -1) { + if(e.weight <= n.list[k - 1].weight) + return; + else + i = k - 1; + } + while(i > 0 && e.weight > n.list[i - 1].weight) { + n.list[i] = n.list[i - 1]; + i--; + } + n.list[i] = e; + } + } + + private void reduceWeight(Node n, int newWeight) { + Entry e = n.entry; + e.weight = newWeight; + for( ; n != null; n = parent(n)) { + int i = n.listIndexOf(e); + if(i == -1) + return; + while(i < n.list.length - 1 && e.weight < n.list[i + 1].weight) { + n.list[i] = n.list[i + 1]; + i++; + } + n.list[i] = e; + if(i == k - 1) { + Entry t = topUnlistedTerm(n); + if(t != null && t.weight > e.weight) + n.list[i] = t; + } + } + } + + private void removeFromLists(Entry e, Node n) { + for( ; n != null; n = parent(n)) { + int i = n.listIndexOf(e); + if(i == -1) + return; + while(i < n.list.length - 1) { + n.list[i] = n.list[i + 1]; + i++; + } + n.list[i] = e; + if(n.list.length < k) + n.list = Arrays.copyOf(n.list, n.list.length - 1); + else{ + Entry t = topUnlistedTerm(n); + if(t == null) + n.list = Arrays.copyOf(n.list, k - 1); + else + n.list[i] = t; + } + } + } + + private Entry topUnlistedTerm(Node n) { + Entry t = null; + if(n.entry != null && n.listIndexOf(n.entry) == -1) + t = n.entry; + for(Node c = leftmostChild(n); c != null; c = rightSibling(c)) { + for(Entry e : c.list) { + if(n.listIndexOf(e) == -1) { + if(t == null || t.weight < e.weight) + t = e; + break; + } + } + } + return t; + } + + private Node leftmostChild(Node n) { + n = n.mid; + if(n != null) { + while(n.left != null) + n = n.left; + } + return n; + } + + private Node rightSibling(Node n) { + if(n.right != null) { + n = n.right; + while(n.left != null) + n = n.left; + return n; + }else{ + while(n == n.up.right) + n = n.up; + if(n == n.up.left) + return n.up; + else + return null; + } + } + + private Node parent(Node n) { + while(n != root && n != n.up.mid) + n = n.up; + return n.up; + } + + /** + * An iterator over the terms in the tree. The iterator returns the terms in + * alphabetical order and allows the caller to remove returned terms from + * the tree during the iteration. + */ + public class Iterator { + + private Node next; + + private Iterator() { + if(root == null) + next = null; + else + next = firstEntry(root); + } + + /** + * Returns true if the iteration has more terms. + * + * @return true if the iteration has more terms + */ + public boolean hasNext() { + return (next != null); + } + + /** + * Returns the next term in the iteration. + * + * @return the next term in the iteration + * @throws NoSuchElementException if the iteration has no more terms + */ + public Entry next() { + if(next == null) + throw new NoSuchElementException(); + Entry e = next.entry; + next = nextEntry(next); + return e; + } + + private Node firstEntry(Node n) { + while(true) { + while(n.left != null) + n = n.left; + if(n.entry == null) + n = n.mid; + else + return n; + } + } + + private Node nextEntry(Node n) { + if(n.mid != null) + return firstEntry(n.mid); + else if(n.right != null) + return firstEntry(n.right); + else{ + while(n.up != null) { + if(n == n.up.left) { + if(n.up.entry != null) + return n.up; + else + return firstEntry(n.up.mid); + }else if(n == n.up.mid && n.up.right != null) + return firstEntry(n.up.right); + else + n = n.up; + } + return null; + } + } + } + + /** + * A list of autocomplete suggestions, ordered from highest weight to lowest + * weight. + */ + public static class Node { + + private Entry[] list; + private Entry entry; + private char firstChar; + private short charEnd; + private int priority; + private Node left, mid, right; + private Node up; // parent in the ternary search tree + + private Node(String term, int weight, int charStart, Node up) { + entry = new Entry(term, weight); + firstChar = term.charAt(charStart); + charEnd = (short) term.length(); + left = mid = right = null; + this.up = up; + } + + private Node(Node n, int charEnd) { + entry = null; + firstChar = n.firstChar; + this.charEnd = (short) charEnd; + priority = n.priority; + left = n.left; + mid = n; + right = n.right; + up = n.up; + } + + /** + * Returns the suggestion at the specified index in the list. The first + * suggestion is at index 0, the second at index 1, and so on. + * + * @param index the index of the suggestion to return + * @return the suggestion at the specified index in the list + * @throws ArrayIndexOutOfBoundsException if the specified index is + * negative or not less than the list length + */ + public Entry getSuggestion(int index) { + return list[index]; + } + + /** + * Returns the number of suggestions in the list. + * + * @return the number of suggestions in the list + */ + public int listLength() { + return list.length; + } + + private char charAt(int index) { + if(entry != null) + return entry.term.charAt(index); + else + return list[0].term.charAt(index); + } + + private int listIndexOf(Entry e) { + for(int i = 0; i < list.length; i++) { + if(list[i] == e) + return i; + } + return -1; + } + } + + /** + * A weighted term. + */ + public static class Entry { + + private final String term; + private int weight; + private int priority; + + private Entry(String term, int weight) { + this.term = term; + this.weight = weight; + } + + /** + * Returns the term. + * + * @return the term + */ + public String getTerm() { + return term; + } + + /** + * Returns the weight of the term. + * + * @return the weight of the term + */ + public int getWeight() { + return weight; + } + } +} \ No newline at end of file Added: core/trunk/src/com/vividsolutions/jump/workbench/ui/SuggestTreeComboBox.java =================================================================== --- core/trunk/src/com/vividsolutions/jump/workbench/ui/SuggestTreeComboBox.java (rev 0) +++ core/trunk/src/com/vividsolutions/jump/workbench/ui/SuggestTreeComboBox.java 2017-01-20 22:29:48 UTC (rev 5328) @@ -0,0 +1,100 @@ +package com.vividsolutions.jump.workbench.ui; + +import com.vividsolutions.jump.util.SuggestTree; + +import javax.swing.*; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.util.*; + +/** + * SuggestTreeComboBox presents a selection of items starting with the + * same letters as the one in the editor using a SuggestTree (Trie). + * To be more flexible, suggestions are not limited to words starting + * with the token typed in the editor but also includes items containing + * such tokens. + */ +public class SuggestTreeComboBox extends JComboBox<String> { + + private SuggestTree trie; + private Map<String,String> map; + + public SuggestTreeComboBox(final String[] array, int size) { + super(); + map = new HashMap<>(array.length); + this.trie = initTrie(array, size); + this.setEditable(true); + this.getEditor().getEditorComponent().addKeyListener(new MyKeyAdapter(this)); + } + + public void changeModel(String[] array) { + map = new HashMap<>(array.length); + this.trie = initTrie(array, trie.size()); + } + + private SuggestTree initTrie(String[] array, int size) { + SuggestTree trie = new SuggestTree(size); + for (String s : array) { + String sm = s.toLowerCase(); + map.put(sm, s); + trie.put(sm, 2); + for (int split : split(sm)) { + String permuted = sm.substring(split) + sm.substring(0, split); + map.put(permuted, s); + trie.put(permuted, 1); + } + } + return trie; + } + + private Integer[] split(String s) { + List<Integer> list = new ArrayList<>(); + for (int i = 1 ; i < s.length() ; i++) { + if (!Character.isLetter(s.charAt(i-1)) && Character.isLetter(s.charAt(i))) list.add(i); + if (!Character.isDigit(s.charAt(i-1)) && Character.isDigit(s.charAt(i))) list.add(i); + } + return list.toArray(new Integer[list.size()]); + } + + class MyKeyAdapter extends KeyAdapter { + + final SuggestTreeComboBox cb; + + MyKeyAdapter(SuggestTreeComboBox cb) { + this.cb = cb; + } + @Override + public void keyReleased(KeyEvent e) { + super.keyReleased(e); + if (e.getKeyCode() == KeyEvent.VK_DOWN || e.getKeyCode() == KeyEvent.VK_UP) { + cb.getEditor().setItem( + cb.getSelectedItem() + ); + return; + } + String newValue = ((String)cb.getEditor().getItem()); + if (newValue.length() == 0) return; + SuggestTree.Node node = cb.trie.getAutocompleteSuggestions(newValue.toLowerCase()); + if (node == null || node.listLength() == 0) { + return; + } else if (node.listLength() == 1) { + cb.setSelectedItem(map.get(node.getSuggestion(0).getTerm())); + cb.hidePopup(); + } else { + Set<String> set = new HashSet<>(); + for (int i = 0; i < node.listLength(); i++) { + set.add(map.get(node.getSuggestion(i).getTerm())); + } + cb.setModel(new DefaultComboBoxModel<>(set.toArray(new String[set.size()]))); + cb.showPopup(); + cb.getEditor().setItem(newValue); + // Very strange : if I start OpenJUMP from my IDE with ant, I don't need to do that, + // but in production, if I don't unselect editor's content, next key will replace + // previous content, and I can't simply add a letter to the prvious one ! + ((JTextField)cb.editor.getEditorComponent()).select(newValue.length(),newValue.length()); + } + } + } + +} + ------------------------------------------------------------------------------ Check out the vibrant tech community on one of the world's most engaging tech sites, SlashDot.org! http://sdm.link/slashdot _______________________________________________ Jump-pilot-devel mailing list Jump-pilot-devel@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/jump-pilot-devel