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.
0 comments:
Post a Comment