/*
* GraphicsHelper.java
*
* Copyright � 1998-2011 Research In Motion Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Note: For the sake of simplicity, this sample application may not leverage
* resource bundles and resource strings. However, it is STRONGLY recommended
* that application developers make use of the localization features available
* within the BlackBerry development platform to ensure a seamless application
* experience across a variety of languages and geographies. For more information
* on localizing your application, please refer to the BlackBerry Java Development
* Environment Development Guide associated with this release.
*/
package com.rim.samples.device.unifiedsearchdemo;
import net.rim.device.api.ui.DrawStyle;
import net.rim.device.api.ui.Font;
import net.rim.device.api.ui.Graphics;
import net.rim.device.api.ui.XYRect;
import net.rim.device.api.util.CharacterUtilities;
import net.rim.device.api.util.IntVector;
import net.rim.device.api.util.SimpleSortingIntVector;
import net.rim.device.api.util.StringUtilities;
/**
* Helper class to highlight keyword text
*/
public class GraphicsHelper {
private int[] _highlightStartIndex = new int[0];
private int[] _highlightLengths = new int[0];
private int[] _highlightOffsets = new int[0];
private String[] _highlightWords = new String[0];
private String[] _queryWords = new String[0];
private final SimpleSortingIntVector _highlightStartIndexIntVector =
new SimpleSortingIntVector();
private final IntVector _highlightLengthsIntVector = new IntVector(0);
private String _currText;
private String _currQuery;
/**
* Draws text with bolded highlight regions
*
* @param g
* Graphics context
* @param text
* Text for which to highlight keywords
* @param query
* Keyword to be highlighted
* @param rect
* The extent in which to draw
*/
public void drawTextWithHighlight(final Graphics g, final String text,
final String query, final XYRect rect) {
final boolean isNewQuery =
_currQuery == null ? true : !_currQuery.equals(query);
final boolean isNewText =
_currText == null ? true : !_currText.equals(text);
String[] queryWords = _queryWords;
if (isNewQuery || isNewText) {
_currQuery = query;
queryWords =
query == null ? null : StringUtilities.stringToWords(query
.toLowerCase().trim());
}
drawTextWithHighlight(g, text, queryWords, rect);
}
/**
* Draws text with bolded highlight regions
*
* @param g
* Graphics context
* @param text
* Text for which to highlight keywords
* @param queryWords
* Keywords to be highlighted
* @param rect
* The extent in which to draw
*/
public void drawTextWithHighlight(final Graphics g, final String text,
final String[] queryWords, final XYRect rect) {
boolean isNewQuery =
_queryWords == null ? true
: _queryWords.length != queryWords.length;
final boolean isNewText =
_currText == null ? true : !_currText.equals(text);
if (!isNewQuery) {
// Before accepting that it is not a new query, verify that each
// query word is identical.
if (queryWords.length == _queryWords.length) {
for (int i = 0; i < queryWords.length; i++) {
isNewQuery |= !queryWords[i].equals(_queryWords[i]);
}
}
}
if (isNewQuery || isNewText) {
_queryWords = queryWords;
_currText = text;
// Since this is a new highlight request, create the highlight
// regions, which are the highlight offsets and lengths.
createHighlightRegions(_queryWords, text);
}
if (_highlightStartIndex.length > 0) {
drawTextWithHighlight(g, text, rect, _highlightStartIndex,
_highlightLengths);
} else {
g.drawText(text, rect.x, rect.y);
}
}
/**
* Draws text with bolded highlight regions
*
* @param g
* Graphics context
* @param text
* Text for which to highlight keywords
* @param rect
* The extent in which to draw
* @param highlightStartIndex
* Index at which to start highlighting
* @param highlightLength
* Length of region to highlight
*/
public void drawTextWithHighlight(final Graphics g, final String text,
final XYRect rect, final int[] highlightStartIndex,
final int[] highlightLength) {
final int x = rect.x;
final int y = rect.y;
final int textWidth = rect.width;
final Font oldFont = g.getFont();
final Font font = g.getFont();
final Font highlightFont =
font.derive(font.isBold() ? Font.EXTRA_BOLD : Font.BOLD);
try {
// Just draw the text normally if there's no highlight region
final int numHighlightStartIndexes =
highlightStartIndex == null ? 0
: highlightStartIndex.length;
if (numHighlightStartIndexes == 0) {
g.setFont(oldFont);
g.drawText(text, 0, text.length(), x, y, DrawStyle.ELLIPSIS,
textWidth);
return;
}
int textStartIndex = 0;
int offsetX = 0;
boolean hasEndText = true;
/**
* <pre>
* Text Highlight Algorithm
*
* Given the set of highlightStartIndex values and the
* highlightLength values, we already know exactly which
* portions of the text need to be highlighted and how many
* characters to highlight.
*
* The algorithm below uses the aforementioned values to
* determine the highlighted and non-highlighted regions of
* text. Then in a single pass through the text, it draws the
* text regions as required.
*
* The text highlighting algorithm works as follows by iterating
* through the set of highlight indexes and performing the required
* text highlighting operations. These operations are as follows:
*
* 1. Calculate the start index for the current portion of the text
* to draw.
*
* 2. Determine if the start index for the text to draw is
* equal to the current highlight index. If it's not equal,
* the characters from the start index to the current highlight
* index are not highlighted. If it is equal, then the amount of
* characters to highlight is dictated by the highlight length.
*
* 3. Draw the text before the current highlight index in a
* non-highlighted font.
*
* 4. Draw the text from the highlight index to the
* highlight index + highlight length in a highlighted font.
* Repeat steps 1 to 4 until the last highlight region is drawn.
*
* 5. There may be text after the last highlight index that hasn't
* been drawn. Therefore, there is a check to see if there is
* any such text and if so, it is drawn in the non-highlighted font.
* </pre>
*/
int i = 0;
for (i = 0; i < numHighlightStartIndexes; i++) {
if (i != 0) {
// Calculate the start index for current portion of the
// text to draw.
textStartIndex =
highlightStartIndex[i - 1] + highlightLength[i - 1];
}
// Draw the text before the highlight region
if (highlightStartIndex[i] != textStartIndex) {
g.setFont(oldFont);
g.drawText(text, textStartIndex, highlightStartIndex[i]
- textStartIndex, x + offsetX, y, 0, textWidth
- offsetX);
}
// Draw the highlighted text
offsetX +=
oldFont.getAdvance(text, textStartIndex,
highlightStartIndex[i] - textStartIndex);
g.setFont(highlightFont);
g.drawText(text, highlightStartIndex[i], highlightLength[i], x
+ offsetX, y, 0, textWidth - offsetX);
final int nextOffsetX =
highlightFont.getAdvance(text, highlightStartIndex[i],
highlightLength[i]);
offsetX += nextOffsetX;
}
i--;
// Draw the text after the highlight region
hasEndText =
highlightStartIndex[i] + highlightLength[i] != text
.length();
if (hasEndText) {
g.setFont(oldFont);
g.drawText(text, highlightStartIndex[i] + highlightLength[i],
text.length() - highlightStartIndex[i]
- highlightLength[i], x + offsetX, y, 0,
textWidth - offsetX);
}
} finally {
g.setFont(oldFont);
}
}
/**
* Creates highlight regions for the given text based on the queryWords
* parameter
*
* @param queryWords
* Keywords for which to create highlight regions
* @param text
* The text for which to create highlight regions
*/
private void createHighlightRegions(final String[] queryWords,
final String text) {
_highlightStartIndex = new int[0];
_highlightLengths = new int[0];
_highlightWords = new String[0];
_highlightStartIndexIntVector.removeAllElements();
_highlightLengthsIntVector.removeAllElements();
if (queryWords != null) {
final int len = queryWords.length;
// Create the initial set of highlight offsets based on the
// the index of each word in the text value. These are equal
// to the index of each word in the text.
StringUtilities.stringToWords(text, _highlightWords, 0);
_highlightOffsets = new int[_highlightWords.length];
StringUtilities.stringToWords(text, _highlightOffsets, 0);
boolean noFurtherMatches;
// Iterate through each of the query words and determine the
// corresponding text highlight start position and highlight
// length.
for (int i = 0; i < len; i++) {
noFurtherMatches = false;
for (int j = 0; j < _highlightOffsets.length
&& !noFurtherMatches; j++) {
final int highlightIndex =
getHighlightStartIndex(text, i,
_highlightOffsets[j]);
if (highlightIndex == -1) {
// If the given query was not found at the current
// offset position
// then there are no further matches for this query word
// and the
// algorithm can exit early.
noFurtherMatches = true;
} else if (j == _highlightOffsets.length - 1
|| highlightIndex < _highlightOffsets[j + 1]) {
int position =
_highlightStartIndexIntVector
.binarySearch(
highlightIndex,
SimpleSortingIntVector.SORT_TYPE_NUMERIC);
if (position < 0) {
position = -position;
if (position > _highlightStartIndexIntVector.size()
|| highlightIndex < _highlightStartIndexIntVector
.elementAt(position - 1)) {
--position;
}
// Since the position is less than 0, the highlight
// index is not yet present in the highlight start
// index array. Therefore, we add the the current
// highlight index value to the collection of
// highlight start indexes.
_highlightStartIndexIntVector.insertElementAt(
highlightIndex, position);
// Next we add the current query index as a
// place holder for the for the query length.
// This will be converted to the actual length
// of the highlight region later.
_highlightLengthsIntVector.insertElementAt(i,
position);
} else {
final int prevHighlightLength =
_queryWords[_highlightLengthsIntVector
.elementAt(position)].length();
final int currHighlightLength =
_queryWords[i].length();
// If the query is found at an existing highlight
// position in the text, and the current query is
// larger than the query previously associated with
// the found highlight position, then the algorithm
// replaces the previous query index with the
// current
// query index.
if (currHighlightLength > prevHighlightLength) {
_highlightLengthsIntVector.setElementAt(i,
position);
}
}
}
}
}
if (_highlightStartIndexIntVector.size() > 0) {
final int size = _highlightStartIndexIntVector.size();
_highlightStartIndex = new int[size];
_highlightStartIndexIntVector.copyInto(_highlightStartIndex);
_highlightLengths = new int[size];
_highlightLengthsIntVector.copyInto(_highlightLengths);
for (int i = 0; i < _highlightStartIndex.length; i++) {
// For each highlight length, the algorithm now replaces the
// query index with the actual highlight length. This
// highlight
// length is equal to the query length.
final int highlightLength =
_queryWords[_highlightLengths[i]].length();
_highlightLengths[i] = highlightLength;
}
}
}
}
/**
* Retrieves the highlight start index of a given string
*
* @param text
* The text containing a region to highlight
* @param queryIndex
* Index of the query word in the _queryWords array to highlight
* @param fromIndex
* Index at which to start looking for for a highlight region in
* the given text
* @return The highlight start index for the given text
*/
private int getHighlightStartIndex(final String text, final int queryIndex,
final int fromIndex) {
final String lCaseElement = text.toLowerCase();
int startIndex =
lCaseElement.indexOf(_queryWords[queryIndex], fromIndex);
if (startIndex > 0) {
int realStartIndex = startIndex;
char prevChar = lCaseElement.charAt(realStartIndex - 1);
// Ensure that we don't incorrectly find a mid-phrase match
while (realStartIndex != -1 && isAlphaNumericChar(prevChar)) {
realStartIndex =
lCaseElement.indexOf(_queryWords[queryIndex],
realStartIndex + 1);
if (realStartIndex != -1) {
prevChar = lCaseElement.charAt(realStartIndex - 1);
}
}
startIndex = realStartIndex;
}
return startIndex;
}
/**
* Checks whether a given char is alphanumeric
*
* @param ch
* The char to evaluate
* @return True if the char is alphanumeric, otherwise false
*/
private static boolean isAlphaNumericChar(final char ch) {
return Character.isDigit(ch) || CharacterUtilities.isLetter(ch);
}
}