Mongo DB grouping datatype changes - mongodb

I came across an odd occurrence while using mongodb + their java driver.
When I do a grouping query the datatype for the key changes from an int to a double.
(ie. I am grouping on a key for 'hours', which is stored as an int within all the objects, but the key changes into a double type in the results I get back).
It isn't a huge issue...but it is weird that it would just arbitrarily change the datatype of a key-value pair like that. Has anyone else had this come up? is this normal behaviour?
Thanks,
p.s. Doing a regular .find() query returns correct datatype, fyi.
Edit:
Some example code:
import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.QueryOperators;
public class MongoTestQueries {
private static final String TESTDBNAME = "badgerbadgerbadger";
private static final String TESTCOLNAME = "mushroom";
private static final Long TESTMAX = 50L;
private static final String KEY1 = "a";
private static final String KEY2 = "snake";
private static final String KEY3 = "plane";
/**
* This starts running it.
*
* #param args
* the arguments.
*/
public static void main(final String[] args) {
//You'll need to write your own code here for connecting to db as you see fit.
MongoConnection mc = new MongoConnection("someserver.com", TESTDBNAME);
mc.setCurCol(TESTCOLNAME);
mc.getCurCol().drop();
mc.setCurCol(TESTCOLNAME);
DBCollection col = mc.getCurCol();
populateCollection(col);
System.out.println(col.count() + " inserted into db.");
regGroupSearch(col);
}
private static void populateCollection(DBCollection col) {
for (Long l = 0L; l < TESTMAX; l++) {
col.insert(new BasicDBObject(KEY1, new Integer(l.intValue())).append(KEY2,
Math.random()).append(KEY3, (TESTMAX - l) + "a string"));
}
}
private static void regGroupSearch(final DBCollection col) {
System.out.println("Group Search:");
DBObject key = new BasicDBObject(KEY1, true).append(KEY3, true);
DBObject cond = new BasicDBObject().append(KEY1, new BasicDBObject(QueryOperators.GT, 4.0));
DBObject initial = new BasicDBObject("count", 0).append("sum", 0);
String reduce = "function(obj,prev){prev.sum+=obj." + KEY2 + ",prev.count+=1}";
String finalize = "function(obj){obj.ave = obj.sum/obj.count}";
DBObject groupResult = col.group(key, cond, initial, reduce, finalize);
printDBObject(groupResult);
System.out.println("Done.");
}
private static void printDBObject(final DBObject toPrint) {
for (String k : toPrint.keySet()) {
System.out.println(k + ": " + toPrint.get(k));
}
}
}

Related

Replacement for "GROUP BY" in ContentResolver query in Android Q ( Android 10, API 29 changes)

I'm upgrading some legacy to target Android Q, and of course this code stop working:
String[] PROJECTION_BUCKET = {MediaStore.Images.ImageColumns.BUCKET_ID,
MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
MediaStore.Images.ImageColumns.DATE_TAKEN,
MediaStore.Images.ImageColumns.DATA,
"COUNT(" + MediaStore.Images.ImageColumns._ID + ") AS COUNT",
MediaStore.Files.FileColumns.MEDIA_TYPE,
MediaStore.MediaColumns._ID};
String BUCKET_GROUP_BY = " 1) and " + BUCKET_WHERE.toString() + " GROUP BY 1,(2";
cur = context.getContentResolver().query(images, PROJECTION_BUCKET,
BUCKET_GROUP_BY, null, BUCKET_ORDER_BY);
android.database.sqlite.SQLiteException: near "GROUP": syntax error (code 1 SQLITE_ERROR[1])
Here it supposed to obtain list of images with album name, date, count of pictures - one image for each album, so we can create album picker screen without querying all pictures and loop through it to create albums.
Is it possible to group query results with contentResolver since SQL queries stoped work?
(I know that ImageColumns.DATA and "COUNT() AS COUNT" are deprecated too, but this is a question about GROUP BY)
(There is a way to query albums and separately query photo, to obtain photo uri for album cover, but i want to avoid overheads)
Unfortunately Group By is no longer supported in Android 10 and above, neither any aggregated functions such as COUNT. This is by design and there is no workaround.
The solution is what you are actually trying to avoid, which is to query, iterate, and get metrics.
To get you started you can use the next snipped, which will resolve the buckets (albums), and the amount of records in each one.
I haven't added code to resolve the thumbnails, but is easy. You must perform a query for each bucket Id from all the Album instances, and use the image from the first record.
public final class AlbumQuery
{
#NonNull
public static HashMap<String, AlbumQuery.Album> get(#NonNull final Context context)
{
final HashMap<String, AlbumQuery.Album> output = new HashMap<>();
final Uri contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
final String[] projection = {MediaStore.Images.Media.BUCKET_DISPLAY_NAME, MediaStore.Images.Media.BUCKET_ID};
try (final Cursor cursor = context.getContentResolver().query(contentUri, projection, null, null, null))
{
if ((cursor != null) && (cursor.moveToFirst() == true))
{
final int columnBucketName = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.BUCKET_DISPLAY_NAME);
final int columnBucketId = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.BUCKET_ID);
do
{
final String bucketId = cursor.getString(columnBucketId);
final String bucketName = cursor.getString(columnBucketName);
if (output.containsKey(bucketId) == false)
{
final int count = AlbumQuery.getCount(context, contentUri, bucketId);
final AlbumQuery.Album album = new AlbumQuery.Album(bucketId, bucketName, count);
output.put(bucketId, album);
}
} while (cursor.moveToNext());
}
}
return output;
}
private static int getCount(#NonNull final Context context, #NonNull final Uri contentUri, #NonNull final String bucketId)
{
try (final Cursor cursor = context.getContentResolver().query(contentUri,
null, MediaStore.Images.Media.BUCKET_ID + "=?", new String[]{bucketId}, null))
{
return ((cursor == null) || (cursor.moveToFirst() == false)) ? 0 : cursor.getCount();
}
}
public static final class Album
{
#NonNull
public final String buckedId;
#NonNull
public final String bucketName;
public final int count;
Album(#NonNull final String bucketId, #NonNull final String bucketName, final int count)
{
this.buckedId = bucketId;
this.bucketName = bucketName;
this.count = count;
}
}
}
This is a more efficient(not perfect) way to do that.
I am doing it for videos, but doing so is the same for images to. just change MediaStore.Video.Media.X to MediaStore.Images.Media.X
public class QUtils {
/*created by Nasib June 6, 2020*/
#RequiresApi(api = Build.VERSION_CODES.Q)
public static ArrayList<FolderHolder> loadListOfFolders(Context context) {
ArrayList<FolderHolder> allFolders = new ArrayList<>();//list that we need
HashMap<Long, String> folders = new HashMap<>(); //hashmap to track(no duplicates) folders by using their ids
String[] projection = {MediaStore.Video.Media._ID,
MediaStore.Video.Media.BUCKET_ID,
MediaStore.Video.Media.BUCKET_DISPLAY_NAME,
MediaStore.Video.Media.DATE_ADDED};
ContentResolver CR = context.getContentResolver();
Uri root = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
Cursor c = CR.query(root, projection, null, null, MediaStore.Video.Media.DATE_ADDED + " desc");
if (c != null && c.moveToFirst()) {
int folderIdIndex = c.getColumnIndexOrThrow(MediaStore.Video.Media.BUCKET_ID);
int folderNameIndex = c.getColumnIndexOrThrow(MediaStore.Video.Media.BUCKET_DISPLAY_NAME);
int thumbIdIndex = c.getColumnIndexOrThrow(MediaStore.Video.Media._ID);
int dateAddedIndex = c.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_ADDED);
do {
Long folderId = c.getLong(folderIdIndex);
if (folders.containsKey(folderId) == false) { //proceed only if the folder data has not been inserted already :)
long thumbId = c.getLong(thumbIdIndex);
String folderName = c.getString(folderNameIndex);
String dateAdded = c.getString(dateAddedIndex);
Uri thumbPath = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, thumbId);
folders.put(folderId, folderName);
allFolders.add(new FolderHolder(String.valueOf(thumbPath), folderName, dateAdded));
}
} while (c.moveToNext());
c.close(); //close cursor
folders.clear(); //clear the hashmap becuase it's no more useful
}
return allFolders;
}
}
FolderHolder model class
public class FolderHolder {
private String folderName;
public long dateAdded;
private String thumbnailPath;
public long folderId;
public void setPath(String thumbnailPath) {
this.thumbnailPath = thumbnailPath;
}
public String getthumbnailPath() {
return thumbnailPath;
}
public FolderHolder(long folderId, String thumbnailPath, String folderName, long dateAdded) {
this.folderId = folderId;
this.folderName = folderName;
this.thumbnailPath = thumbnailPath;
this.dateAdded = dateAdded;
}
public String getFolderName() {
return folderName;
}
}
GROUP_BY supporting in case of using Bundle:
val bundle = Bundle().apply {
putString(
ContentResolver.QUERY_ARG_SQL_SORT_ORDER,
"${MediaStore.MediaColumns.DATE_MODIFIED} DESC"
)
putString(
ContentResolver.QUERY_ARG_SQL_GROUP_BY,
MediaStore.Images.ImageColumns.BUCKET_ID
)
}
contentResolver.query(
uri,
arrayOf(
MediaStore.Images.ImageColumns.BUCKET_ID,
MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
MediaStore.Images.ImageColumns.DATE_TAKEN,
MediaStore.Images.ImageColumns.DATA
),
bundle,
null
)

Could someone give me an example of how to extract coordinates for a 'word' using PDFBox

Could someone give me an example of how to extract coordinates for a 'word' with PDFBox
I am using this link to extract positions of individual characters:
https://www.tutorialkart.com/pdfbox/how-to-extract-coordinates-or-position-of-characters-in-pdf/
I am using this link to extract words:
https://www.tutorialkart.com/pdfbox/extract-words-from-pdf-document/
I am stuck getting coordinates for whole words.
You can extract the coordinates of words by collecting all the TextPosition objects building a word and combining their bounding boxes.
Implementing this along the lines of the two tutorials you referenced, you can extend PDFTextStripper like this:
public class GetWordLocationAndSize extends PDFTextStripper {
public GetWordLocationAndSize() throws IOException {
}
#Override
protected void writeString(String string, List<TextPosition> textPositions) throws IOException {
String wordSeparator = getWordSeparator();
List<TextPosition> word = new ArrayList<>();
for (TextPosition text : textPositions) {
String thisChar = text.getUnicode();
if (thisChar != null) {
if (thisChar.length() >= 1) {
if (!thisChar.equals(wordSeparator)) {
word.add(text);
} else if (!word.isEmpty()) {
printWord(word);
word.clear();
}
}
}
}
if (!word.isEmpty()) {
printWord(word);
word.clear();
}
}
void printWord(List<TextPosition> word) {
Rectangle2D boundingBox = null;
StringBuilder builder = new StringBuilder();
for (TextPosition text : word) {
Rectangle2D box = new Rectangle2D.Float(text.getXDirAdj(), text.getYDirAdj(), text.getWidthDirAdj(), text.getHeightDir());
if (boundingBox == null)
boundingBox = box;
else
boundingBox.add(box);
builder.append(text.getUnicode());
}
System.out.println(builder.toString() + " [(X=" + boundingBox.getX() + ",Y=" + boundingBox.getY()
+ ") height=" + boundingBox.getHeight() + " width=" + boundingBox.getWidth() + "]");
}
}
(ExtractWordCoordinates inner class)
and run it like this:
PDDocument document = PDDocument.load(resource);
PDFTextStripper stripper = new GetWordLocationAndSize();
stripper.setSortByPosition( true );
stripper.setStartPage( 0 );
stripper.setEndPage( document.getNumberOfPages() );
Writer dummy = new OutputStreamWriter(new ByteArrayOutputStream());
stripper.writeText(document, dummy);
(ExtractWordCoordinates test testExtractWordsForGoodJuJu)
Applied to the apache.pdf example the tutorials use you get:
2017-8-6 [(X=26.004425048828125,Y=22.00372314453125) height=5.833024024963379 width=36.31868362426758]
Welcome [(X=226.44479370117188,Y=22.00372314453125) height=5.833024024963379 width=36.5999755859375]
to [(X=265.5881652832031,Y=22.00372314453125) height=5.833024024963379 width=8.032623291015625]
The [(X=276.1641845703125,Y=22.00372314453125) height=5.833024024963379 width=14.881439208984375]
Apache [(X=293.5890197753906,Y=22.00372314453125) height=5.833024024963379 width=29.848846435546875]
Software [(X=325.98126220703125,Y=22.00372314453125) height=5.833024024963379 width=35.271636962890625]
Foundation! [(X=363.7962951660156,Y=22.00372314453125) height=5.833024024963379 width=47.871429443359375]
Custom [(X=334.0334777832031,Y=157.6195068359375) height=4.546705722808838 width=25.03936767578125]
Search [(X=360.8929138183594,Y=157.6195068359375) height=4.546705722808838 width=22.702728271484375]
You can create CustomPDFTextStripper which extends PDFTextStripper and override protected void writeString(String text, List<TextPosition> textPositions). In this overriden method you need to split textPositions by the word separator to get List<TextPosition> for each word. After that you can join each character and compute bounding box.
Full example below which contains also drawing of the resulting bounding boxes.
package com.example;
import lombok.Value;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition;
import org.junit.Ignore;
import org.junit.Test;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class PdfBoxTest {
private static final String BASE_DIR_PATH = "C:\\Users\\Milan\\50330484";
private static final String INPUT_FILE_PATH = "input.pdf";
private static final String OUTPUT_IMAGE_PATH = "output.jpg";
private static final String OUTPUT_BBOX_IMAGE_PATH = "output-bbox.jpg";
private static final float FROM_72_TO_300_DPI = 300.0f / 72.0f;
#Test
public void run() throws Exception {
pdfToImage();
drawBoundingBoxes();
}
#Ignore
#Test
public void pdfToImage() throws IOException {
PDDocument document = PDDocument.load(new File(BASE_DIR_PATH, INPUT_FILE_PATH));
PDFRenderer renderer = new PDFRenderer(document);
BufferedImage image = renderer.renderImageWithDPI(0, 300);
ImageIO.write(image, "JPEG", new File(BASE_DIR_PATH, OUTPUT_IMAGE_PATH));
}
#Ignore
#Test
public void drawBoundingBoxes() throws IOException {
PDDocument document = PDDocument.load(new File(BASE_DIR_PATH, INPUT_FILE_PATH));
List<WordWithBBox> words = getWords(document);
draw(words);
}
private List<WordWithBBox> getWords(PDDocument document) throws IOException {
CustomPDFTextStripper customPDFTextStripper = new CustomPDFTextStripper();
customPDFTextStripper.setSortByPosition(true);
customPDFTextStripper.setStartPage(0);
customPDFTextStripper.setEndPage(1);
Writer writer = new OutputStreamWriter(new ByteArrayOutputStream());
customPDFTextStripper.writeText(document, writer);
List<WordWithBBox> words = customPDFTextStripper.getWords();
return words;
}
private void draw(List<WordWithBBox> words) throws IOException {
BufferedImage bufferedImage = ImageIO.read(new File(BASE_DIR_PATH, OUTPUT_IMAGE_PATH));
Graphics2D graphics = bufferedImage.createGraphics();
graphics.setColor(Color.GREEN);
List<Rectangle> rectangles = words.stream()
.map(word -> new Rectangle(word.getX(), word.getY(), word.getWidth(), word.getHeight()))
.collect(Collectors.toList());
rectangles.forEach(graphics::draw);
graphics.dispose();
ImageIO.write(bufferedImage, "JPEG", new File(BASE_DIR_PATH, OUTPUT_BBOX_IMAGE_PATH));
}
private class CustomPDFTextStripper extends PDFTextStripper {
private final List<WordWithBBox> words;
public CustomPDFTextStripper() throws IOException {
this.words = new ArrayList<>();
}
public List<WordWithBBox> getWords() {
return new ArrayList<>(words);
}
#Override
protected void writeString(String text, List<TextPosition> textPositions) throws IOException {
String wordSeparator = getWordSeparator();
List<TextPosition> wordTextPositions = new ArrayList<>();
for (TextPosition textPosition : textPositions) {
String str = textPosition.getUnicode();
if (wordSeparator.equals(str)) {
if (!wordTextPositions.isEmpty()) {
this.words.add(createWord(wordTextPositions));
wordTextPositions.clear();
}
} else {
wordTextPositions.add(textPosition);
}
}
super.writeString(text, textPositions);
}
private WordWithBBox createWord(List<TextPosition> wordTextPositions) {
String word = wordTextPositions.stream()
.map(TextPosition::getUnicode)
.collect(Collectors.joining());
int minX = Integer.MAX_VALUE;
int minY = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int maxY = Integer.MIN_VALUE;
for (TextPosition wordTextPosition : wordTextPositions) {
minX = Math.min(minX, from72To300Dpi(wordTextPosition.getXDirAdj()));
minY = Math.min(minY, from72To300Dpi(wordTextPosition.getYDirAdj() - wordTextPosition.getHeightDir()));
maxX = Math.max(maxX, from72To300Dpi(wordTextPosition.getXDirAdj() + wordTextPosition.getWidthDirAdj()));
maxY = Math.max(maxY, from72To300Dpi(wordTextPosition.getYDirAdj()));
}
return new WordWithBBox(word, minX, minY, maxX - minX, maxY - minY);
}
}
private int from72To300Dpi(float f) {
return Math.round(f * FROM_72_TO_300_DPI);
}
#Value
private class WordWithBBox {
private final String word;
private final int x;
private final int y;
private final int width;
private final int height;
}
}
Note:
If you are interested in other options, you can check also Poppler
PDF to image
pdftoppm -r 300 -jpeg input.pdf output
Generate an XHTML file containing bounding box information for each word in the file.
pdftotext -r 300 -bbox input.pdf

How can I avoid converting an empty HashMap to null in morphia?

We are using org.mongodb.morphia to convert objects BasicDBObjects before persistence. One issue encountered is that in some cases the object to convert contains an empty HashMap whose size is 0, after conversion, the HashMap is converted to null. So NullPointerException throw in later accessing. I want to ask experts for help, Is there any way to avoid this? I mean, after conversion, it's still an HashMap with size 0.
Part of the class to be converted:
public class ProjectServiceAdapterConfig {
#NotNull
private String serviceAdapterId;
#NotNull
private String projectId;
#Embedded
#Flatten
private Map<String, Map<String, String>> connections = new HashMap<>();
//...... setter and getter skipped here
}
code for conversion:
// create a mapper with default MapperOptions
private Mapper createMapper() {
return new Mapper();
}
ReplaceableItem objectToItem(final ProjectServiceAdapterConfig obj) {
final Mapper mapper = createMapper();
final MappedClass mc = mapper.getMappedClass(obj.getClass());
final Map<String, Object> map = mapper.toDBObject(obj).toMap();
}
the obj is created in other place. After some debug, I found that, the obj contains an empty Map(following data copied from IntelliJ IDEA debugger):
connections = {java.util.LinkedHashMap#8890} size = 1
[0] = {java.util.LinkedHashMap$Entry#8894}"accounts" -> size = 0
key: java.lang.String = {java.lang.String#8895}"accounts"
value: java.util.LinkedHashMap = {java.util.LinkedHashMap#8896} size = 0
and the one after converted:
[2] = {java.util.LinkedHashMap$Entry#8910}"connections" -> size = 1
key: java.lang.String = {java.lang.String#8911}"connections"
value: com.mongodb.BasicDBObject = {com.mongodb.BasicDBObject#8912} size = 1
[0] = {java.util.LinkedHashMap$Entry#8923}"accounts" -> null
key: java.lang.String = {java.lang.String#8895}"accounts"
value: = null
As you can see , it's converted to null which we try to avoid.
Thanks
Before you call morphia.mapPackage(), do this:
morphia.getMapper().getOptions().storeEmpties = true;
That should map back probably to an empty map for you.
I assume I cannot avoid it without customizing the MapOfValuesConverter. See from the source code that the empty map will be always converted to null:
#Override
public Object encode(Object value, MappedField mf) {
if (value == null)
return null
Map<Object, Object> map = (Map<Object, Object>) value;
if ((map != null) && (map.size() > 0)) {
Map mapForDb = new HashMap();
for (Map.Entry<Object, Object> entry : map.entrySet()) {
String strKey = converters.encode(entry.getKey()).toString();
mapForDb.put(strKey, converters.encode(entry.getValue()));
}
return mapForDb;
}
return null;
}
In case morphia.getMapper().getOptions().setStoreEmpties(true); doesn't work for you another solution would be to use the #PostLoad annotation to check whether you have a null collection and create an empty one if necessary.
import java.util.*;
import org.mongodb.morphia.annotations.*;
import org.bson.types.ObjectId;
#Entity
public class Model {
#Id
private ObjectId id;
private Map<String, String> map;
protected Model() {}
public Model(HashMap<String, String> map) {
super();
setMap(map);
}
public void setMap(HashMap<String, String> map) {
this.map = map;
checkForNullMap();
}
#PostLoad
private void checkForNullMap() {
if (map == null) {
map = new HashMap<String, String>();
}
}
}

Android SQLite cannot bind argument

I'm trying to do a simple query of my database, where a unique Identification number is stored for a PendingIntent. To allow me to cancel a notification set by AlarmManager if needed.
The insertion of a value works fine, but I am unable to overcome the error:
java.lang.IllegalArgumentException: Cannot bind argument at index 1 because the index is out of range. The statement has 0 parameters.
Database structure:
public class DBAdapter {
private static final String TAG = "DBAdapter";
public static final String KEY_ROWID = "_id";
public static final String TASK = "task";
public static final String NOTIFICATION = "notification";
public static final int COL_ROWID = 0;
public static final int COL_TASK = 1;
public static final int COL_NOTIFICATION = 2;
public static final String[] ALL_KEYS = new String[] {KEY_ROWID, TASK, NOTIFICATION};
// DB info: it's name, and the table.
public static final String DATABASE_NAME = "TaskDB";
public static final String DATABASE_TABLE = "CurrentTasks";
public static final int DATABASE_VERSION = 3;
private static final String DATABASE_CREATE_SQL =
"create table " + DATABASE_TABLE
+ " (" + KEY_ROWID + " integer primary key, "
+ TASK + " text not null, "
+ NOTIFICATION + " integer"
+ ");";
Now I have created a method to extract the notification ID from the database as needed, using the following code:
public int getNotifID(long notifID){
String[] x = {ALL_KEYS[2]};
String[]args = new String[]{NOTIFICATION};
String where = NOTIFICATION + "=" + notifID;
int y = 0;
//String select = "SELECT "+NOTIFICATION+" FROM "+DATABASE_TABLE+" WHERE "+notifID+"="+NOTIFICATION;
//Cursor c = db.rawQuery(select,new String[]{});
Cursor c = db.query(true,DATABASE_TABLE,x,Long.toString(notifID),args,null,null,null,null,null);
if (c!= null && c.moveToFirst()){
y = c.getInt(COL_NOTIFICATION);
}
return y;
}
As you can see I have attempted to do this both with a rawQuery and a regular query, but with no success.
Rewrite your raw query to:
String select = "SELECT "+NOTIFICATION+" FROM "+DATABASE_TABLE+" WHERE "+NOTIFICATION+"=?";
Cursor c = db.rawQuery(select,new String[]{""+notifId});
if(c!=null && c.getCount()>0) {
c.moveToFirst();
}
c.close();

Change y-axis format of GWT Visualization from milliseconds to hr:min:sec

I am currently generating LineChart graphs with the GWT Visualization library that show the run time of a set of jobs. The time values are in milliseconds and I would like the graph to display the y-axis labels in hr:min:sec format instead of milliseconds. I have used the setFormattedValue method to make this conversion, but unfortunately, only the tooltip values display the formatted value while the y-axis continues to display in milliseconds.
Here's my code:
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import com.electriccloud.commander.gwt.client.util.CommanderUrlBuilder;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.visualization.client.AbstractDataTable;
import com.google.gwt.visualization.client.AbstractDataTable.ColumnType;
import com.google.gwt.visualization.client.DataTable;
import com.google.gwt.visualization.client.VisualizationUtils;
import com.google.gwt.visualization.client.visualizations.Table;
import com.google.gwt.visualization.client.visualizations.corechart.HorizontalAxisOptions;
import com.google.gwt.visualization.client.visualizations.corechart.LineChart;
public class ScheduledJobMonitorFancyChartPanel extends HorizontalPanel{
protected static final int MS = 0;
protected static final int HR_MIN_SEC = 1;
private String scheduleName;
private HashMap<String, AverageElapsedTime> elapsedTimeData;
private Table dataTable;
private LineChart lineChart;
private boolean graphIsVisible = true;
private int displayStyle;
public ScheduledJobMonitorFancyChartPanel(){
super();
this.setStyleName("hidden");
}
public ScheduledJobMonitorFancyChartPanel(String schedName, HashMap<String, AverageElapsedTime> data, int displayType){
this();
this.scheduleName = schedName;
this.elapsedTimeData = data;
this.displayStyle = displayType;
createTableAndChart();
this.setVisible(graphIsVisible);
}
private void createTableAndChart(){
// this block defines the table and chart
Runnable onLoadCallback = new Runnable() {
public void run() {
VerticalPanel outterPanel = new VerticalPanel();
Label chartTitle = new Label("ElapsedTime Data for " + scheduleName);
chartTitle.setStylePrimaryName("chartTitle");
outterPanel.add(chartTitle);
HorizontalPanel allChartGroups = new HorizontalPanel();
allChartGroups.setStylePrimaryName("allChartGroupsStyle");
// Since a single Job may have multiple steps being monitored, this creates the charts
// for each step, but groups them all (horizontally) under the same job
Collection<String> c = elapsedTimeData.keySet();
Iterator<String> itr = c.iterator();
while(itr.hasNext()){
String stepName = itr.next();
AverageElapsedTime aet = elapsedTimeData.get(stepName);
AbstractDataTable linkableTable = createTableWithLinks(aet);
AbstractDataTable table = createTable(aet);
dataTable = new Table(linkableTable, createDataTableOptions());
dataTable.setStylePrimaryName("dataTableStyle");
lineChart = new LineChart(table, createLineChartOptions(stepName));
lineChart.setStylePrimaryName("lineChartStyle");
HorizontalPanel tableAndChartGroup = new HorizontalPanel();
tableAndChartGroup.setStylePrimaryName("tableAndChartGroup");
tableAndChartGroup.add(dataTable);
tableAndChartGroup.add(lineChart);
allChartGroups.add(tableAndChartGroup);
}
outterPanel.add(allChartGroups);
addToPanel(outterPanel);
}
};
// this line gets the table/chart defined above displayed on the screen
VisualizationUtils.loadVisualizationApi(onLoadCallback, LineChart.PACKAGE, Table.PACKAGE);
}
// Because the table/chart is created inside an annonymous Runnable object, this method
// exposes it to being added to "this"
private void addToPanel(Widget widget){
this.add(widget);
}
// set up the table used by the LineChart
private AbstractDataTable createTable(AverageElapsedTime aet){
DataTable data = DataTable.create();
data.addColumn(ColumnType.STRING, "JobId");
data.addColumn(ColumnType.NUMBER, "ElapsedTime");
data.addRows(aet.getSize());
HashMap<Long, Long> jobIdElapsedTimeHash = aet.getListOfTimes();
Collection<Long> c = jobIdElapsedTimeHash.keySet();
Iterator<Long> itr = c.iterator();
int row = 0;
while(itr.hasNext()){
Long jobId = itr.next();
data.setValue(row, 0, jobId.toString());
if(this.displayStyle == ScheduledJobMonitorFancyChartPanel.MS)
data.setValue(row, 1, jobIdElapsedTimeHash.get(jobId));
else if(this.displayStyle == ScheduledJobMonitorFancyChartPanel.HR_MIN_SEC){
data.setValue(row, 1, jobIdElapsedTimeHash.get(jobId));
String formattedValue = AverageElapsedTime.getDisplayTime(jobIdElapsedTimeHash.get(jobId));
data.setFormattedValue(row, 1, formattedValue);
}
row++;
}
return data;
}
// set up the table used by the DataTable - It embeds links to the jobId listed
private AbstractDataTable createTableWithLinks(AverageElapsedTime aet){
DataTable data = DataTable.create();
data.addColumn(ColumnType.STRING, "JobId");
data.addColumn(ColumnType.NUMBER, "ElapsedTime");
data.addRows(aet.getSize());
HashMap<Long, Long> jobIdElapsedTimeHash = aet.getListOfTimes();
Collection<Long> c = jobIdElapsedTimeHash.keySet();
Iterator<Long> itr = c.iterator();
String urlBase = CommanderUrlBuilder.getBase();
int row = 0;
while(itr.hasNext()){
Long jobId = itr.next();
data.setValue(row, 0, "<a href='" + urlBase + "link/jobDetails/jobs/" + jobId + "' target='_blank'>" + jobId + "</a>");
// data.setValue(row, 1, jobIdElapsedTimeHash.get(jobId));
if(this.displayStyle == ScheduledJobMonitorFancyChartPanel.MS)
data.setValue(row, 1, jobIdElapsedTimeHash.get(jobId));
else if(this.displayStyle == ScheduledJobMonitorFancyChartPanel.HR_MIN_SEC){
data.setValue(row, 1, jobIdElapsedTimeHash.get(jobId));
String formattedValue = AverageElapsedTime.getDisplayTime(jobIdElapsedTimeHash.get(jobId));
data.setFormattedValue(row, 1, formattedValue);
}
row++;
}
return data;
}
// set the options for the DataTable
private Table.Options createDataTableOptions(){
Table.Options options = Table.Options.create();
options.setHeight("300");
options.setWidth("190");
options.setAllowHtml(true);
return options;
}
// set the options for the LineChart
private com.google.gwt.visualization.client.visualizations.corechart.Options createLineChartOptions(String stepName){
com.google.gwt.visualization.client.visualizations.corechart.Options options = com.google.gwt.visualization.client.visualizations.corechart.Options.create();
options.setWidth(500);
options.setHeight(300);
options.setCurveType("function");
options.setColors("#336E95");
options.setTitle(stepName);
HorizontalAxisOptions hao = HorizontalAxisOptions.create();
hao.setSlantedText(true);
hao.setSlantedTextAngle(45);
options.setHAxisOptions(hao);
return options;
}
public void setTimeDisplay(int displayType) {
switch(displayType){
case 0:
break;
case 1:
this.displayStyle = ScheduledJobMonitorFancyChartPanel.HR_MIN_SEC;
}
}
}
I don't have the code at hand.
Only, I remember that the documentation for Google Graphs API for GWT is far from completed. You only need to know that it's a wrapper over Google Graphs, the javascript library.
This means that some options you can set directly with setters, for some others you'll have to look into the JS library parameters and inject them somehow with the generic option setter (there's a method to set options on axis on a "string -> value" basis).
Here you can find a description of the JS library parameters:
https://developers.google.com/chart/interactive/docs/gallery/areachart#Data_Format
If you look at "hAxis.format", you'll probably find what you are looking for.
EDIT:
To complete my answer, you'll have to use the HorizontalAxisOptions class and its set method.
Beware that the format you send is tricky and not firing an error if it's wrong, but I would bet on set("hAxis.format", "{format:'HH:mm:ss'}");