So far in this Freebase Contributing Artists App series, we've been developing a JavaFX application that enables the user to navigate connections among musicians. I'm calling this application BandmatesFX, and it uses the JSONHandler feature of the JFXtras open source library to easily query the Freebase.com database.
Note: While using this app, I noticed that drummer Aynsley Dunbar has played with lots of bands over the years, so he's a good starting point for trying to navigate to another given musician in six or less degrees. Try, for example, to navigate from Aynsley Dunbar to Roy Orbison. Here's one way in five degrees:
- Aynsley Dunbar played in The Animals, with Andy Summers
- Andy Summers played in The Police, with Sting
- Sting played in Band Aid, with Paul McCartney
- Paul McCartney played in The Quarrymen (and of course The Beatles), with George Harrison
- George Harrison played in The Traveling Wilburys, with Roy Orbison
Today I'm going to point out some enhancements to this application, and the updated code, which are reflected in the following screenshot:
These enhancements include:
- The user can type in an artist name, and with each keystroke the Freebase database web service is invoked, returning suggested names from which the user can choose.
- Band and artist pictures are presented in a cover-flow style, using the JFXtras Shelf component. In addition, the JFXtras TitledBorder component is employed to enclose and label the cover-flows.
- When the user clicks the central image in the bottom (e.g. Steve Smith in the screenshot above), that artist becomes the featured artist at the top of the page. The group cover-flow images are updated to reflect the groups in which the artist has played, and the member cover-flow images contain the artists that have played in the currently selected group.
One of the cool feature of the JFXtras Shelf component is that the mouse wheel moves the images horizontally on the Shelf, enabling the user to navigate even more quickly than clicking on one of the images. Before invoking this application from the Web Start link at the end of this article, take a look at the main script of the program (named BandmatesFX.fx) in the listing below:
/*
* BandmatesMain.fx
*
* Uses Freebase and JFXtras with JavaFX to explore connections
* between musical artists
*
* Developed by James L. Weaver to demonstrate using JavaFX and JFXtras
*/
package javafxpert;
import javafx.io.http.HttpRequest;
import javafx.animation.transition.*;
import javafx.geometry.HPos;
import javafx.scene.*;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.ListView;
import javafx.scene.control.TextBox;
import javafx.scene.image.*;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.*;
import javafx.scene.text.*;
import javafx.stage.Stage;
import org.jfxtras.data.pull.*;
import org.jfxtras.scene.border.TitledBorder;
import org.jfxtras.scene.control.Shelf;
/**
* Height of the images that we'll be working with
*/
def IMAGE_HEIGHT:Integer = 140;
/**
* Artist for which we're searching bandmates
*/
var artistToSearch:String = "/en/aynsley_dunbar";
/**
* Name for which we're finding matches and ids
*/
var nameToFind:String = "Aynsley Dunbar";
var textBoxRef:TextBox;
var listViewRef:ListView;
var selectedNameIndex = bind listViewRef.selectedIndex on replace {
def selectedId = freebaseSearchResult.result[selectedNameIndex].id;
if (selectedId != "") {
println("selectedId:{selectedId}");
nameToFind = freebaseSearchResult.result[selectedNameIndex].name;
obtainGroupsForArtist(selectedId);
}
};
/**
* Hover text that contains the name of the artist or group
*/
var nameHoverText:String;
/**
* A reference to the HTTP request, for the purpose of monitoring progress
*/
var req:HttpRequest;
/**
* The root class that will hold the object graph from the JSON results
*/
var freebaseResult:FreebaseResult;
/**
* The root class that will hold the object graph from the JSON results
*/
var freebaseSearchResult:FreebaseSearchResult;
/**
* The base URL for the freebase query
*/
def freebaseURL = "http://www.freebase.com/api/service/mqlread?";
/**
* The base URL for a freebase search request
*/
def freebaseSearchURL = "http://www.freebase.com/api/service/search?";
/**
* The base URL to get a freebase image
*/
def freebaseImageURL = "http://img.freebase.com/api/trans/image_thumb";
var artistsNode:Node;
/**
* Fade in transition for artists
*/
def shelfFadeIn = FadeTransition {
node: bind artistsNode
duration: 1500ms
fromValue: 0.2
toValue: 1.0
}
/**
* Indexe into groups list
*/
var groupOneIndex:Integer on replace {
shelfFadeIn.playFromStart();
// Choose the middle one
def numArtists:Integer = sizeof freebaseResult.result.
musicGroupMemberMembership[groupOneIndex].
group.musicMusicalGroupMember;
artistTwoIndex = if (numArtists > 1) numArtists / 2 else 0;
groupOneName = freebaseResult.result.
musicGroupMemberMembership[groupOneIndex].group.name;
};
/**
* Name of the currently selected band
*/
var groupOneName:String;
/**
* Index into artists list
*/
var artistTwoIndex:Integer;
/**
* Create a search query and invoke the JSON handler
*/
function obtainIdForArtistPartialName(artistPartialName:String) {
listViewRef.visible = true;
nameToFind = artistPartialName;
def partialName = artistPartialName.replace(" ", "+");
var searchUrl = "{freebaseSearchURL}prefix={partialName}&type=/music/artist&limit=10&mql_output=[\{\"id\":null,\"name\":null\}]";
println("searchUrl:{searchUrl}");
var albumHandler:JSONHandler = JSONHandler {
rootClass: "javafxpert.FreebaseSearchResult"
onDone: function(obj, isSequence): Void {
freebaseSearchResult = obj as FreebaseSearchResult;
println("# of search results:{sizeof freebaseSearchResult.result},freebaseSearchResult:{freebaseSearchResult.code}");
req.stop();
}
};
req = HttpRequest {
location: searchUrl
onInput: function(is: java.io.InputStream) {
albumHandler.parse(is);
}
};
req.start();
}
/**
* Create the Freebase query and invoke the JSON handler
*/
function obtainGroupsForArtist(artistFreebaseId:String) {
listViewRef.visible = false;
artistToSearch = artistFreebaseId;
var queryUrl = "{freebaseURL}query=\{\"query\":"
" \{ "
" \"/common/topic/image\": [\{ "
" \"id\": null "
" \}], "
" \"/music/group_member/membership\": [\{ "
" \"group\": \{ "
" \"name\": null, "
" \"id\": null, "
" \"/common/topic/image\": [\{ "
" \"id\": null "
" \}], "
" \"/music/musical_group/member\": [\{ "
" \"member\": \{ "
" \"name\": null, "
" \"id\": null, "
" \"/common/topic/image\": [\{ "
" \"id\": null "
" \}] "
" \} "
" \}] "
" \} "
" \}], "
" \"id\": \"{artistFreebaseId}\", "
" \"name\": null, "
" \"type\": \"/music/artist\" "
" \} \}";
println("queryUrl:{queryUrl}");
var albumHandler:JSONHandler = JSONHandler {
rootClass: "javafxpert.FreebaseResult"
onDone: function(obj, isSequence): Void {
freebaseResult = obj as FreebaseResult;
println("# of bands:{sizeof freebaseResult.result.musicGroupMemberMembership}");
req.stop();
// Choose the middle one
def numGroups:Integer = sizeof freebaseResult.result.musicGroupMemberMembership;
groupOneIndex = if (numGroups > 1) numGroups / 2 else 0;
}
};
req = HttpRequest {
location: queryUrl
onInput: function(is: java.io.InputStream) {
albumHandler.parse(is);
}
};
req.start();
}
var sceneRef:Scene;
Stage {
title: bind "BandmatesFX - {nameToFind}"
scene: sceneRef = Scene {
width: 1000
height: 700
content: [
ImageView {
image: Image {
url: "http://www.popsci.com/files/imagecache/article_image_large/files/articles/guitar.gif"
}
fitWidth: bind sceneRef.width
preserveRatio: true
opacity: 0.1
},
VBox {
nodeHPos: HPos.CENTER
layoutY: 10
spacing: 10
content: [
HBox {
spacing: 10
content: [
ImageView {
image: bind Image {
url: "{freebaseImageURL}{artistToSearch}?maxheight={IMAGE_HEIGHT}"
}
},
VBox {
content: [
textBoxRef = TextBox {
promptText: "Enter artist name"
text: bind nameToFind with inverse
columns: 20
font: Font.font(null, 14)
action:function():Void {
if (nameToFind != "" and (req.percentDone == 0.0 or req.percentDone == 100.0)) {
obtainIdForArtistPartialName(nameToFind);
}
}
onKeyTyped:function(ke:KeyEvent) {
println("req.percentDone:{req.percentDone}");
if (textBoxRef.rawText != "" and (req.percentDone == 0.0 or req.percentDone == 100.0)) {
obtainIdForArtistPartialName(textBoxRef.rawText);
}
}
},
listViewRef = ListView {
visible: false
height: 100
items: bind for (rslt in freebaseSearchResult.result) {
rslt.name
}
layoutInfo: LayoutInfo {
width: bind textBoxRef.width - 10
height: 80
}
}
]
},
ProgressIndicator {
progress: bind req.progress
},
Label {
text: bind nameHoverText
font: Font.font(null, FontWeight.BOLD, 18)
}
]
},
TitledBorder {
text: bind "{nameToFind} played in these groups:"
font: Font.font(null, FontWeight.BOLD, 14)
lineColor: Color.GREY
node: Shelf {
reflection: false
showScrollBar: false
showText: true
imageUrls: bind for (grp in freebaseResult.result.musicGroupMemberMembership) {
"{freebaseImageURL}{grp.group.id}?maxheight={IMAGE_HEIGHT}"
}
imageNames: bind for (grp in freebaseResult.result.musicGroupMemberMembership) {
grp.group.name;
}
index: bind groupOneIndex with inverse
thumbnailWidth: IMAGE_HEIGHT
thumbnailHeight: IMAGE_HEIGHT
layoutInfo: LayoutInfo {
width: bind sceneRef.width
height: IMAGE_HEIGHT + 120
}
}
},
artistsNode = Panel {
content: bind for (grp in freebaseResult.result.musicGroupMemberMembership) {
TitledBorder {
text: bind "{groupOneName} has had the following members:"
font: Font.font(null, FontWeight.BOLD, 14)
lineColor: Color.GREY
node: Shelf {
visible: bind indexof grp == groupOneIndex
reflection: false
showScrollBar: false
showText: true
imageUrls: bind for (mmbr in freebaseResult.result.
musicGroupMemberMembership[indexof grp].
group.musicMusicalGroupMember) {
"{freebaseImageURL}{mmbr.member.id}?maxheight={IMAGE_HEIGHT}"
}
imageNames: bind for (mmbr in freebaseResult.result.
musicGroupMemberMembership[indexof grp].
group.musicMusicalGroupMember) {
mmbr.member.name
}
onCenterImagePressed:function(me:MouseEvent):Void {
var mmbr = freebaseResult.result.
musicGroupMemberMembership[indexof grp].
group.musicMusicalGroupMember[artistTwoIndex];
nameToFind = mmbr.member.name;
artistToSearch = mmbr.member.id;
obtainGroupsForArtist(mmbr.member.id);
}
index: bind artistTwoIndex with inverse
thumbnailWidth: IMAGE_HEIGHT
thumbnailHeight: IMAGE_HEIGHT
layoutInfo: LayoutInfo {
width: bind sceneRef.width
height: IMAGE_HEIGHT + 120
}
}
}
}
layoutInfo: LayoutInfo {
width: bind sceneRef.width
height: IMAGE_HEIGHT + 120
}
}
]
}
]
}
}
obtainGroupsForArtist(artistToSearch);
To try out this application, click the Web Start link below. Since this application is a work in progress, I'll give you a couple of pointers:
- When typing the name of an artist in the text box, each key invokes a web service, so wait until the progress indicator is solid before typing the final character (I'll make this smoother in a future iteration).
- Not all artists are in groups, and I'm not doing the necessary exception handling yet to catch this in the response from Freebase. I know that many artists such as Eric Clapton, Bob Dylan, and Steve Winwood work, as well as any artist that you can click in the bottom cover flow (by definition they are members of a group).
For more background on this application, see the previous article in this series. As always, please leave a comment if you have any questions. Also, if you have any suggestions for future enhancements, please leave a comment as well.
Regards,
Jim Weaver
Thomas,
By the way, the direct link to the JFXtras SVN repo is:
http://code.google.com/p/jfxtras/source/checkout
Thanks,
Jim Weaver
Posted by: Jim Weaver | August 05, 2009 at 02:01 PM
Thomas,
You're right, I was using a version of the Shelf class that I built from the JFXtras open source repository, rather than the JFXtras 0.5 release. The JFXtras project is located at http://jfxtras.org
Regards,
Jim Weaver
Posted by: Jim Weaver | August 05, 2009 at 01:58 PM
Thomas, see
http://code.google.com/p/crudfx/
It includes all you need for enterprise applications (grid, tree, JDBC, XML, JSON, Google map).
Posted by: surikov | August 03, 2009 at 04:23 PM
Dear Jim,
I tried to get it running, but actual JFXtras-0.5.jar has Shelf class that is missing many of the members you are using. I guess you are using a newer version of JFXtras. I tried to find newer release at jfxtras.org, but couldn't. Would pls give me a hint, where to find it?
In general: I am a developer with 20 yrs of experience and I am rather disappointed about the information about JavaFX. It's really cool, but most samples are based on V1.0 or V1.1 and do not compile with JavaFX 1.2. SUN's language reference is full of TODOs. For a professional developer this is a mess. I really like your work and I own your book. But, if you meet some of the SUN guys, pls tell them that there is much more work to be done to get to enterprise level (and SUN stated it will offer enterprise level).
Again, I really like YOUR work!
Best regards,
Thomas
Posted by: Thomas | August 01, 2009 at 05:16 AM