yaz/commit

integrate various types of gardening functionality into the yaz player

authorMichiel Hildebrand
Sun Mar 13 01:27:56 2011 +0100
committerMichiel Hildebrand
Sun Mar 13 01:27:56 2011 +0100
commit56b83702d20921ee979e22ec0f50479a79a6d67a
tree3751bea84b2ab30184ec8bfc6e70945b647305a7
parent6e5f40acce48e14e991d7ef3d88aa7f7651ca390
Diff style: patch stat
diff --git a/api/video_frames.pl b/api/video_frames.pl
index 346ec19..aa05800 100644
--- a/api/video_frames.pl
+++ b/api/video_frames.pl
@@ -39,16 +39,24 @@ user:file_search_path(video, videos).
 serve_video(Request) :-
 	ensure_logged_on(_),
 	http_parameters(Request,
-			[ t(Time,
+			[ url(URL,
+			      [ optional(true), description('URL of video that is stored locally')
+			      ]),
+			  t(Time,
 			      [ optional(true), description('URL of the video')])
  			]),
-	memberchk(path_info(Video), Request),
+	(   nonvar(URL)
+	->  www_form_encode(URL, Video)
+	;   memberchk(path_info(Video), Request)
+	),
 	(   nonvar(Time),
 	    parse_time_param(Time, Start, End)
 	->  video_fragment(Video, Start, End, Fragment)
 	;   Fragment = video(Video)
 	),
-	http_reply_file(Fragment, [mimetype('video/flv'), unsafe(true)], Request).
+	%Mimetype = 'video/x-ms-wmv',
+	Mimetype = 'video/flv',
+	http_reply_file(Fragment, [mimetype(Mimetype), unsafe(true)], Request).
 
 %%	video_fragment(+VideoFile, +Start, +End -Fragment)
 %
diff --git a/applications/yaz_player.pl b/applications/yaz_player.pl
index e60c091..b22a6c8 100644
--- a/applications/yaz_player.pl
+++ b/applications/yaz_player.pl
@@ -20,12 +20,20 @@
 :- use_module(library(yui3)).
 :- use_module(library(video_annotation)).
 
+:- use_module(applications(yaz_tag)).
 :- use_module(components(label)).
 :- use_module(components(yaz_page)).
 :- use_module(components(yaz_video_item)).
 
+:- use_module(library(rdf_history)).
+:- use_module(library(user_process)).
+
+
 :- http_handler(yaz(player), http_yaz_player, []).
 :- http_handler(yaz(data/tags), http_data_tags, []).
+:- http_handler(yaz(data/relatedtags), http_data_related_tags, []).
+:- http_handler(yaz(data/updateentries), http_data_update_entries, []).
+
 
 %%	http_yaz_player(+Request)
 %
@@ -51,6 +59,12 @@ http_yaz_player(Request) :-
 			  query(Query,
 				[default(''),
 				 description('search string to filter the tags by')]),
+			  type(Type,
+			       [default(false),
+				description('filter tags by type')]),
+			  role(Role,
+			       [default(false),
+				description('filter tags by role')]),
 			  limit(Limit,
 				[default(1000), number,
 				 description('limit number of tags shown')]),
@@ -66,19 +80,21 @@ http_yaz_player(Request) :-
 		    confirmed(Confirmed),
 		    interval(Interval),
 		    query(Query),
+		    type(Type),
+		    role(Role),
 		    limit(Limit),
 		    offset(Offset)
  		  ],
 	delete_nonground(Options0, Options),
-	findall(P, video_process(Video, P, User), Processes0),
-	findall(U, video_process(Video, Process, U), Users0),
-	sort(Processes0, Processes),
-	sort(Users0, Users),
+	%findall(P, video_process(Video, P, User), Processes0),
+	%findall(U, video_process(Video, Process, U), Users0),
+	%sort(Processes0, Processes),
+	%sort(Users0, Users),
  	% annotations
  	video_annotations(Video, Annotations0, Options),
 	sort_by_arg(Annotations0, 2, Annotations1),
 	list_limit(Annotations1, Limit, Annotations, _),
- 	html_page(Video, Processes, Users, Annotations, StartTime, Options).
+ 	html_page(Video, Annotations, StartTime, Options).
 
 %%	video_process(+Video,	-Process, -User)
 %
@@ -89,38 +105,66 @@ video_process(Video, Process, User) :-
 	rdf(Process, rdf:type, pprime:'Game'),
 	rdf_has(Process, opmv:wasControlledBy, User).
 
-%%	html_page(+Video, +Processes, +Users, +Annotations, +StartTime,
+type_option(false, 'filter by type').
+type_option(person, person).
+type_option(location, location).
+type_option(subject, subject).
+
+role_option(false, 'filter by role').
+role_option(depicted, depicted).
+role_option(associated, associated).
+
+%%	html_page(+Video, +Annotations, +StartTime,
 %%	+Options)
 %
 %	Emit an HTML page with a video player and a tag carousel.
 
-html_page(Video, Processes, Users, Annotations, StartTime, Options) :-
+html_page(Video, Annotations, StartTime, Options) :-
 	option(query(Query), Options, ''),
+	option(type(Type), Options, 'filter by type'),
+	findall(option(V, type, N), type_option(V, N), TypeOptions),
+	option(role(Role), Options, 'filter by role'),
+	findall(option(V, role, N), role_option(V, N), RoleOptions),
 	reply_html_page(yaz,
 			[ title(['YAZ - ', Video])
 			],
 			[ \html_requires(css('player.css')),
+			  \html_requires(css('tag.css')),
 			  \yaz_video_header(Video),
-			  div([a([href('javascript:{}'), id(showOptions)],
-				 'show options')
+			  div(class(controls),
+			      [a([href('javascript:{}'), id(toggleOptions)],
+				 'show options'),
+			       a([href('javascript:{}'), id(toggleFrames)],
+				 'show frames')
 			      ]),
-			  div(id(configuration),
+			  div([id(configuration), class(hidden)],
 			      [ \html_tag_options(Options),
-				\html_tag_sliders(Options),
-				\html_facets(Video, Processes, Users, Options)
+				\html_tag_sliders(Options)
+				%\html_facets(Video, Processes, Users, Options)
 			      ]),
 			  div(id(tags),
-			      [ input([id(tagsearch), value(Query)]),
+			      [ div(select([id(tagtype), name(type), value(Type)],
+				       \html_select_options(TypeOptions))),
+				div(select([id(tagrole), name(role), value(Role)],
+				       \html_select_options(RoleOptions))),
+				div(input([id(tagsearch), autocomplete(false), value(Query)])),
 				div(id(tagplayer), [])
 			      ]),
 			  div(id(video),
 			      [ div(id(timeline), []),
-				div(id(videoplayer), [])
+				div(id(videoplayer), []),
+				div([id(videoframes), class(hidden)], [])
 			      ]),
+			  div(id(tagEdit), []),
 			  script(type('text/javascript'),
 				\html_video_page_yui(Video, Annotations, StartTime, Options))
 			]).
 
+html_select_options([]) --> !.
+html_select_options([option(Value, Name, Label)|Ts]) -->
+	html(option([value(Value), name(Name)], Label)),
+	html_select_options(Ts).
+
 html_tag_options(Options) -->
 	{ option(confirmed(Confirmed), Options, false),
 	  option(subtitles(Subtitles), Options, true)
@@ -208,50 +252,65 @@ html_user_list([User|T], Selected, VideoPlayer) -->
 html_video_page_yui(Video, Annotations, StartTime, Options) -->
 	{ video_source(Video, Src, Duration),
 	  option(interval(Interval), Options, 0),
-	  http_location_by_id(http_data_tags, TagServer),
- 	  http_absolute_location(js('videoplayer/'), FilePath, []),
-	  http_absolute_location(js('videoplayer/videoplayer.js'), VideoPlayer, []),
-	  http_absolute_location(js('tagcarousel/tagcarousel.js'), TagCarousel, []),
+	  http_location_by_id(serve_video_frame, FrameServer),
+  	  http_absolute_location(js('videoplayer/'), FilePath, []),
+	  http_absolute_location(js('videoplayer/videoplayer.js'), Videoplayer, []),
+	  http_absolute_location(js('videoframes/videoframes.js'), VideoFrames, []),
+	  http_absolute_location(js('tagplayer/tagplayer.js'), Tagplayer, []),
 	  http_absolute_location(js('timeline/timeline.js'), Timeline, []),
-	  http_absolute_location(js('valueSlider/valueSlider.js'), ValueSlider, []),
-	  annotation_to_json(Annotations, JSONTags)
+	  http_absolute_location(js('valueslider/valueslider.js'), Valueslider, []),
+	  annotation_to_json(Annotations, JSONTags),
+	  list_limit(JSONTags, 20, JSONFrames, _)
    	},
 	html_requires(js('videoplayer/swfobject.js')),
- 	js_yui3([{modules:{'video-player':{fullpath:VideoPlayer},
-			   'tag-carousel':{fullpath:TagCarousel},
+ 	js_yui3([{modules:{'video-player':{fullpath:Videoplayer},
+			   'video-frames':{fullpath:VideoFrames},
+			   'tag-player':{fullpath:Tagplayer},
 			   'timeline':{fullpath:Timeline},
-			   'valueSlider':{fullpath:ValueSlider}
+			   'value-slider':{fullpath:Valueslider}
 			  }}
 		],
 		[node,event,widget,anim,slider,
 		 'json','querystring-stringify-simple',io,
-  		 'video-player','tag-carousel',timeline,valueSlider
+  		 'video-player','video-frames','tag-player',
+		 timeline,'value-slider'
  		],
 		[ \js_new(videoPlayer,
 			  'Y.mazzle.VideoPlayer'({filepath:FilePath,
  						  src:Src,
-						  width:540,
+						  width:560,
 						  height:400,
 						  autoplay:symbol(false),
 						  controls:symbol(true),
  						  start:StartTime,
 						  duration:Duration
 						 })),
+		  \js_new(videoFrames,
+			 'Y.mazzle.VideoFrames'({frameServer:FrameServer,
+ 						 frames:JSONFrames,
+						 video:Src,
+  						 duration:Duration,
+						 playerPath:FilePath,
+						 width:560,
+						 confirm:true
+ 						})),
+
 		  \js_new(tagPlayer,
-			  'Y.mazzle.TagCarousel'({tags:JSONTags,
-						  height:400,
-						  width:200
-						 })),
+			  'Y.mazzle.TagPlayer'({tags:JSONTags,
+						height:380,
+						width:200,
+						edit:symbol(true)
+					       })),
 		  \js_new(timeline,
 			  'Y.mazzle.Timeline'({height:20,
-					       width:540,
+					       width:560,
 					       duration:Duration,
 					       items:JSONTags
 					      })),
-		  \js_new(intervalSlider,
+		 \js_new(intervalSlider,
 			  'Y.mazzle.ValueSlider'({node:symbol('Y.one("#interval")'),
 						  name:interval,
-						  label:interval,
+						  label:'group same tags in interval:',
 						  min:0,
 						  max:60,
 						  value:Interval
@@ -259,34 +318,143 @@ html_video_page_yui(Video, Annotations, StartTime, Options) -->
   		  'var oldTime;\n',
 		  \js_yui3_decl(params, json(Options)),
 		  \js_yui3_decl(delayID, -1),
-		  \js_fetch_tags(TagServer),
+		  \js_fetch_tags,
+		  \js_fetch_tag_frames,
+		  \js_fetch_tag_info,
+		  \js_update_entries(Video),
  		  \js_call('videoPlayer.render'('#videoplayer')),
+		  \js_call('videoFrames.render'('#videoframes')),
 		  \js_call('tagPlayer.render'('#tagplayer')),
 		  \js_call('timeline.render'('#timeline')),
    		  \js_yui3_on(videoPlayer, timeChange, \js_video_time_change),
 		  \js_yui3_on(tagPlayer, itemSelect, \js_tag_select),
 		  \js_yui3_on(intervalSlider, valueUpdate, \js_slider_select),
 		  \js_yui3_delegate('.option', input, click, \js_option_select, []),
-		  \js_yui3_delegate(symbol('tagPlayer.listNode'), li, mouseover, \js_tag_hover, []),
-		  \js_yui3_on('Y.one("#showOptions")', click,
-			      'function() {Y.one("#configuration").toggleClass("hidden")}'),
-		  \js_yui3_on('Y.one("#tagsearch")', keyup,  \js_search)
-  		]).
+		  \js_yui3_on(tagPlayer, itemHover, \js_tag_hover),
+		  \js_yui3_on(videoFrames, frameHover, \js_tag_hover),
+		  \js_yui3_on(videoFrames, roleSelect, \js_role_select(Video)),
+		  \js_yui3_on('Y.one("#toggleOptions")', click, \js_toggle_options),
+		  \js_yui3_on('Y.one("#toggleFrames")', click, \js_toggle_frames),
+ 		  \js_yui3_on('Y.one("#tagsearch")', keyup,  \js_search),
+		  \js_yui3_on('Y.one("#tagtype")', change, \js_filter),
+		  \js_yui3_on('Y.one("#tagrole")', change, \js_filter)
+   		]).
+
+js_toggle_options -->
+	js_function([e],
+		    \[
+'     Y.one("#configuration").toggleClass("hidden");\n'
+		     ]).
+js_toggle_frames -->
+	js_function([e],
+		    \[
+'     var frames = Y.one("#videoframes")
+	  video = Y.one("#videoplayer");\n',
+'     if(frames.hasClass("hidden")) {
+	    e.target.setContent("show video");
+	    videoPlayer.pause();
+	    video.addClass("hidden");
+	    frames.removeClass("hidden");
+       }\n',
+'     else {
+	    e.target.setContent("show frames");
+	    frames.addClass("hidden");
+	    video.removeClass("hidden");
+       }\n'
+		     ]).
+
+js_fetch_tags -->
+	{ http_location_by_id(http_data_tags, Server)
+	},
+	js_function_decl(syncUI, [e,o],
+			 \[
+'    var tags = Y.JSON.parse(o.responseText).tags;
+     tagPlayer.set("tags", tags);
+     videoFrames.set("frames", tags);
+     videoFrames.set("related", []);
+     timeline.set("items", tags);\n'
+			  ]),
+
+	js_function_decl(fetchTags, [conf],
+			  \[
+'    var data = Y.params;
+     if(conf) {
+	   for(var key in conf) { data[key] = conf[key] }
+     }\n',
+'    Y.io("',Server,'",
+	  { data: data,
+	    on: { success: syncUI },
+	  });\n'
+			   ]),
+
+	js_function_decl(updateTags, [],
+			  \[
+'    Y.io("',Server,'",
+	  { data: Y.params,
+	    on: { success: function(e,o) {
+	       var tags = Y.JSON.parse(o.responseText).tags;
+	       tagPlayer.set("tags", tags);}},
+	  });\n'
+			   ]).
+
+js_fetch_tag_frames -->
+	{ http_location_by_id(http_data_related_tags, Server)
+	},
+	js_function_decl(setFrames, [e,o],
+			 \[
+'    var r = Y.JSON.parse(o.responseText);
+     videoFrames.set("related", r.related);
+     videoFrames.set("frames", r.frames);\n'
+			  ]),
+
+	js_function_decl(fetchTagFrames, [entry],
+			 \[
+'   var data = Y.params;
+    data.entry = entry;
+    Y.io("',Server,'",
+	 { data: data,
+	   on: { success: setFrames }})\n'
+ 			  ]).
+
+js_fetch_tag_info -->
+	{ http_location_by_id(http_yaz_tag_edit, Server)
+	},
+	js_function_decl(setTagInfo, [e,o],
+			  \[
+'    Y.one("#tagEdit").setContent(o.responseText);
+     Y.one("#apply").on("click", function() {updateEntries()});
+     Y.one("#applyall").on("click", function() {updateEntries(true)});\n'
+			   ]),
+
+	js_function_decl(fetchTagInfo, [entry],
+			  \[
+'    Y.io("',Server,'",
+	  { data: {entry:entry,
+		   format:"form"},
+	    on: { success: setTagInfo }});\n'
+			   ]).
 
 js_tag_select -->
 	js_function([e],
 		    \[
-'    if(e.tag.startTime)
-     { var time = (e.tag.startTime/1000)-2;
+'    Y.tag = e.tag;
+     var data = Y.params;
+     var entry = e.tag.annotations[0].uri;\n',	% hack, there can be multiple entries grouped in one tag
+
+'    if(e.tag.startTime&&!Y.one("#videoplayer").hasClass("hidden")) {
+       var time = (e.tag.startTime/1000)-2;
        videoPlayer.setTime(time, true);
-     }\n'
-		    ]).
+     }\n',
+'    if(!Y.one("#videoframes").hasClass("hidden")) {
+       fetchTagFrames(entry);
+     }\n',
+'    fetchTagInfo(entry);\n'
+		     ]).
 
 js_tag_hover -->
 	js_function([e],
 		    \[
-'   var index = e.container.all("li").indexOf(e.currentTarget);
-    timeline.highlightIndex(index);\n'
+'   timeline.highlightIndex(e.index);\n'
 		     ]).
 
 js_video_time_change -->
@@ -319,26 +487,6 @@ js_slider_select -->
 		     ]).
 
 
-js_fetch_tags(DataServer) -->
-	js_function_decl(syncUI, [e,o],
-			 \[
-'    var tags = Y.JSON.parse(o.responseText).tags;
-     tagPlayer.set("tags", tags);
-     timeline.set("items", tags);\n'
-			  ]),
-
-	js_function_decl(fetchTags, [conf],
-			  \[
-'    var data = Y.params;
-     if(conf) {
-	   for(var key in conf) { data[key] = conf[key] }
-     }\n',
-'    Y.io("',DataServer,'",
-	  { data: data,
-	    on: { success: syncUI },
-	  });\n'
-			   ]).
-
 js_search -->
 	js_function([e],
 		    \[
@@ -350,6 +498,58 @@ js_search -->
      Y.delayID = setTimeout(fetchTags, delay);\n'
  		     ]).
 
+js_filter -->
+	js_function([e],
+		    \[
+'    	console.log(e);var filter = e.target.get("name");
+        e.target.get("options").each( function() {
+	      if(this.get("selected")&&this.get("value")) {
+		    var conf = {};
+		    if(this.get("value")) {conf[filter] = this.get("value")}
+		    fetchTags(conf)
+	}});\n'
+ 		     ]).
+
+js_update_entries(Video) -->
+	{ http_location_by_id(http_data_update_entries, Server)
+	},
+	js_function_decl(updateEntries, [all],
+		    \[
+'    var entry = Y.tag.annotations[0].uri
+         entries = [],
+	 as = Y.tag.annotations,
+	 rs = videoFrames.get("related");
+     for(var i=0; i<as.length; i++) { entries.push(as[i].uri) }
+     if(all) { for(i=0; i<rs.length; i++) { entries.push(rs[i].entry) }}\n',
+'    var data = {video:"',Video,'",
+		 entries:entries,
+		 value:Y.one("#value").get("value")
+		};
+     if( Y.one("[name=concept]:checked")) {data.concept = Y.one("[name=concept]:checked").get("value");}
+     if( Y.one("[name=role]:checked")) {data.role = Y.one("[name=role]:checked").get("value");}\n',
+'    Y.io("',Server,'",
+	  { data: data,
+	    on: { success: function(e,o) {fetchTagFrames(entry);
+					  updateTags()
+					 }},
+	  });\n'
+		     ]).
+
+js_role_select(Video) -->
+	{ http_location_by_id(http_data_update_entries, Server)
+	},
+	js_function([o],
+			  \[
+'    console.log(o);
+     var data = {video:"',Video,'",
+		 role:o.type,
+		 entries:[o.frame.entry]
+		};\n',
+'    Y.io("',Server,'",
+	  { data: data,
+	    on: { success: function() {Y.log("role updated")}}});\n'
+			   ]).
+
 
 
 %%	http_tags(+Request)
@@ -375,6 +575,12 @@ http_data_tags(Request) :-
 			  query(Query,
 				[default(''),
 				 description('search string to filter the tags by')]),
+			  type(Type,
+			       [default(false),
+				description('filter tags by type')]),
+			  role(Role,
+			       [default(false),
+				description('filter tags by role')]),
 			  limit(Limit,
 				[default(10000), number,
 				 description('limit number of tags shown')]),
@@ -386,7 +592,9 @@ http_data_tags(Request) :-
 		   user(User),
 		   interval(Interval),
 		   confirmed(Confirmed),
-		   query(Query)
+		   query(Query),
+		   type(Type),
+		   role(Role)
 		  ],
   	% annotations
  	video_annotations(Video, Annotations0, Options),
@@ -396,3 +604,118 @@ http_data_tags(Request) :-
 	annotation_to_json(Annotations, JSONTags),
   	reply_json(json([tags=JSONTags])).
 
+
+%%	http_data_related_tags(+Request)
+%
+%       Emit a JSON object with all frames for a given tag and video.
+
+http_data_related_tags(Request) :-
+	http_parameters(Request,
+			[ video(Video,
+				[description('Current video')]),
+			  process(Process,
+			       [optional(true),
+				desription('When set only annotations within this process are shown')]),
+			  user(User,
+				[optional(true),
+				 description('When set only annotations created by this user are shown')]),
+			  entry(Entry,
+			      [optional(true),
+			       jsonresource,
+			       description('tag entry to find related tags for')]),
+			  limit(Limit,
+				[default(20), number,
+				 description('limit number of tags shown')]),
+			  offset(Offset,
+				 [default(0), number,
+				  description('first result that is returned')])
+			]),
+	Options = [process(Process),
+		   user(User)
+ 		  ],
+	rdf(Entry, rdf:value, Tag),
+  	Obj = Time-json([entry=Id, tag=json([uri=Tag, label=Label]), startTime=Time, role=R]),
+	findall(Obj, (video_annotation(Video, Id, uri(Tag,Label), Time, _, Options),
+ 		      tag_role(Id, R)
+		     ), Entries0),
+	keysort(Entries0, Entries1),
+	list_offset(Entries1, Offset, Entries2),
+	list_limit(Entries2, Limit, Entries, _),
+	pairs_values(Entries, Related0),
+	select(json([entry=Entry|F]), Related0, Related),
+	reply_json(json([frames=[json([entry=Entry|F])], related=Related])).
+
+tag_role(Entry, Role) :-
+	rdf(Entry, pprime:role, literal(Role)),
+	!.
+tag_role(_Entry, false).
+
+
+
+%%	http_data_update_entries(+Request)
+%
+%	Emit an HTML page with gardening options for a tag.
+
+http_data_update_entries(Request) :-
+	http_parameters(Request,
+			[ video(Video,
+			       [description('video we are updating entries of')]),
+			  entries(Entries,
+				[zero_or_more, description('entries to update')]),
+ 			  value(Value,
+				[optional(true), description('text value')]),
+			  concept(Concept,
+				  [optional(true), description('Link to a concept')]),
+			  role(Role,
+			       [default(false), description('role of the tag')])
+   			]),
+	logged_on(User0, anonymous),
+	user_property(User0, url(User)),
+	(   current_user_process(Process),
+	    rdf(Process, rdf:type, pprime:'TagGarden'),
+	    rdf(Process, opmv:used, Video)
+	->  true
+	;   create_user_process(User, [rdf:type=pprime:'TagGarden',
+					  opmv:used=Video
+					 ], _GardenProcess)
+	),
+	rdfh_transaction(update_entries(Entries, Value, Concept, Role, Updated)),
+	reply_json(Updated).
+
+update_entries([], _, _, _, []).
+update_entries([Entry|Es], Value, Concept, Role, [json([entry=Entry, tag=NewTag, role=Role])|Rest]) :-
+	update_value(Entry, Concept, Value, NewTag),
+ 	update_role(Entry, Role),
+	update_entries(Es, Value, Concept, Role, Rest).
+
+update_value(E, Concept, _Value, Concept) :-
+	nonvar(Concept),
+	!,
+	rdf(E, rdf:value, Tag),
+	rdfh_update(E, rdf:value, Tag->Concept).
+update_value(E, _Concept, Value, Tag) :-
+	nonvar(Value),
+	!,
+ 	rdf(E, rdf:value, Tag0),
+	rdf(Tag0, rdf:type, pprime:'Tag'),
+	(   rdf(Tag0, rdfs:label, literal(Value))
+	->  Tag = Tag0
+	;   rdf(Tag, rdfs:label, literal(Value))
+	->  rdfh_update(E, rdf:value, Tag0->Tag)
+	;   new_tag(Value),
+	    rdfh_update(E, rdf:value, Tag0->Tag)
+	).
+update_value(_, _, _, unknown).
+
+update_role(E, Role) :-
+	nonvar(Role),
+	!,
+	%rdf_global_id(pprime:role, URL),
+	rdfh_retractall(E, pprime:role, _),
+	rdfh_assert(E, pprime:role, literal(Role)).
+update_role(_, _).
+
+new_tag(Value) :-
+	rdf_bnode(Tag),
+	rdfh_assert(Tag, rdf:type, pprime:'Tag'),
+	rdfh_assert(Tag, rdfs:label, literal(Value)).
diff --git a/applications/yaz_tag.pl b/applications/yaz_tag.pl
new file mode 100644
index 0000000..7948e23
--- /dev/null
+++ b/applications/yaz_tag.pl
@@ -0,0 +1,219 @@
+:- module(yaz_tag,
+	  [ http_yaz_tag_edit/1
+	  ]).
+
+:- use_module(library(http/http_dispatch)).
+:- use_module(library(http/http_parameters)).
+:- use_module(library(http/http_path)).
+:- use_module(library(http/html_write)).
+:- use_module(library(http/html_head)).
+:- use_module(library(http/http_json)).
+:- use_module(library(http/js_write)).
+:- use_module(library(http/json)).
+:- use_module(library(http/http_session)).
+:- use_module(user(user_db)).
+:- use_module(library(semweb/rdf_db)).
+:- use_module(library(semweb/rdfs)).
+:- use_module(library(semweb/rdf_label)).
+
+:- use_module(library(yaz_util)).
+:- use_module(library(yui3)).
+:- use_module(library(video_annotation)).
+
+:- use_module(components(label)).
+:- use_module(components(yaz_page)).
+:- use_module(api(reconcile)).
+
+:- http_handler(yaz(tagedit), http_yaz_tag_edit, []).
+
+%%	http_yaz_tag_edit(+Request)
+%
+%	Emit an HTML page with gardening options for a tag.
+
+http_yaz_tag_edit(Request) :-
+	http_parameters(Request,
+			[ entry(Entry,
+				[description('URL of tag entry')]),
+			  action(Action,
+				 [default(request)]),
+			  value(Value,
+				[optional(true), description('text value')]),
+			  concept(Concept,
+				  [optional(true), description('Link to a concept')]),
+			  role(Role,
+			       [default(false), description('role of the tag')]),
+			  format(Format,
+				 [default(page),
+				  oneof([page,form]),
+				  description('Return a complete html page or only the form')])
+  			]),
+	update_tag(Action, Entry, Value, Concept, Role),
+	annotation_value(Entry, _TagId, Label),
+ 	annotation_provenance(Entry, Provenance),
+	tag_concepts(Label, Concepts),
+	(   Format = page
+	->  html_page(Entry, Label, Provenance, Concepts, Concept, Role)
+	;   html_current_option(content_type(Type)),
+	    phrase(html_form(Entry, Label, Provenance, Concepts, Concept, Role), HTML),
+	    format('Content-type: ~w~n~n', [Type]),
+	    print_html(HTML)
+	).
+
+annotation_value(Entry, TagId, Label) :-
+	rdf(Entry, rdf:value, TagId),
+	rdf_label(TagId, Lit),
+	literal_text(Lit, Label).
+
+tag_concepts(Label, Concepts) :-
+	rdf_equal(skos:'Concept', Type),
+	reconcile(Label, 3, Type, [], Hits),
+	maplist(hit_concept, Hits, Concepts).
+
+hit_concept(hit(_,URI,_,Label), concept(URI,Label,Alt,Desc)) :-
+	(   rdf_has(URI, rdfs:label, Lit),
+	    literal_text(Lit, Alt),
+	    \+ Alt == Label
+	->  true
+	;   Alt = ''
+	),
+	(   rdf_has(URI,skos:scopeNote,Txt)
+	->  literal_text(Txt,Desc)
+	;   rdf_has(URI, skos:definition,Txt)
+	->  literal_text(Txt,Desc)
+	;   Desc = ''
+	).
+
+		 /*******************************
+		 *		update		*
+		 *******************************/
+
+update_tag(request, _, _, _, _).
+update_tag(confirm, Entry, Value, Concept, Role) :-
+	true.
+
+
+		 /*******************************
+		 *               HTML		*
+		 *******************************/
+
+html_page(Entry, TagLabel, Provenance, Concepts, Concept, Role) :-
+	reply_html_page(yaz,
+			[ title(['YAZ - ', Entry])
+			],
+			[ \html_requires(css('tag.css')),
+			  \html_form(Entry, TagLabel, Provenance, Concepts, Concept, Role)
+			]).
+
+html_form(Entry, TagLabel, _Provenance, Concepts, _Concept, Role) -->
+	html(%form(action(location_by_id(http_yaz_tag_edit)),
+		  [ input([type(hidden), name(entry), value(Entry)]),
+		    input([type(hidden), name(action), value(confirm)]),
+		    div([class(block), id(edit)],
+			[ h4('edit tag'),
+			  \html_tag_edit(TagLabel)
+			]),
+		    div([class(block), id(concept)],
+			[ h4('identify tag'),
+			  ul(class(concepts),
+			     \html_concepts(Concepts))
+			]),
+		    div([class(block), id(role)],
+			[ h4('select role'),
+			  \html_roles(Role)
+			]),
+		    /*div([class(block), id(provenance)],
+			[ h4('history'),
+			  \html_provenance(Provenance)
+				     ]),*/
+		    div(class(submit),
+			[ input([id(applyall), type(submit), value('apply to all')]),
+			  input([id(apply),  type(submit), value('apply')])
+			]) /*,
+		    script(type('text/javascript'),
+			   \html_video_page_yui(Video, Annotations, StartTime, Options))*/
+		  ]).
+
+html_tag_edit(Value) -->
+	html(input([name(value), id(value), type(text), value(Value)])).
+
+html_concepts([]) --> !.
+html_concepts([concept(URI,Label,_Alt,Desc)|Cs]) -->
+	html(li(\html_radiobox(concept, URI, false,
+			       [ a(href(URI), Label),
+				 div(class(desc), Desc)
+			       ]))),
+	html_concepts(Cs).
+
+
+html_roles(Selected) -->
+	 html(ul(class(options),
+		 [ li(\html_radiobox(role, depicted, Selected, 'depicted')),
+		   li(\html_radiobox(role, associated, Selected, 'associacted'))
+		 ])).
+
+html_radiobox(Group, Value, Selected, Label) -->
+	{ radio_checked(Value, Selected, Checked)
+	},
+ 	html([input([type(radio), name(Group), value(Value), Checked]),
+	      span(Label)
+	     ]).
+
+radio_checked(Selected, Selected, checked) :- !.
+radio_checked(_, _, '').
+
+html_provenance([]) --> !.
+html_provenance([action(_,Time,User,_,Action)|T]) -->
+	html_provenance_action(Action,Time,User),
+	html_provenance(T).
+
+html_provenance_action(added(_, _, Value, _PlayHead), Time, User) -->
+ 	html(div(class(paction),
+		 [ 'added ',
+		   \html_tag_value(Value),
+		   \html_time_user(Time, User)])).
+html_provenance_action(removed(_, _), Time, User) -->
+ 	html(div(class(paction),
+		 [ 'removed ',
+		   \html_time_user(Time, User)])).
+html_provenance_action(valueChange(_, Value), Time, User) -->
+ 	html(div(class(paction),
+		 [ 'changed to ',
+		   \html_tag_value(Value),
+		   \html_time_user(Time, User)])).
+html_provenance_action(timeChange(_, PlayHead), Time, User) -->
+ 	html(div(class(paction),
+		 [ 'changed time to ', PlayHead,
+		   \html_time_user(Time, User)])).
+
+html_time_user(Time, _User) -->
+	html([' at ', \html_time(Time)]).
+%	      ' by ', \html_user(User) ]).
+
+html_time(Time) -->
+	{ time_format(Time, Formatted) },
+	html(Formatted).
+html_user(UserURL) -->
+	{ display_label(UserURL, Name)
+	},
+	html(Name).
+html_tag_value(literal(Lit)) -->
+	{ literal_text(Lit, L)
+	},
+	html(['"',L,'"']).
+html_tag_value(URI) -->
+	{ rdf_label(URI, Lit),
+	  literal_text(Lit, L)
+	},
+	html(['"',L,'"']).
+
+time_format(TimeStamp, Formatted) :-
+	catch(format_time(atom(Formatted), '%Y-%m-%d %T', TimeStamp), _, fail),
+	!.
+time_format(Time, Time).
+
+
+
+
+
+
+
diff --git a/applications/yaz_user.pl b/applications/yaz_user.pl
index 7a2b297..5a4bc9e 100644
--- a/applications/yaz_user.pl
+++ b/applications/yaz_user.pl
@@ -53,6 +53,19 @@ http_yaz_user(Request) :-
 %	 * User
 %	 When defined Videos are limited to annotated by this User.
 
+% hack for the review demo to get 5 nice videos
+
+user_videos(User, Videos) :-
+	var(User),
+	!,
+	Videos = [ 'http://semanticweb.cs.vu.nl/prestoprime/video655',
+		   'http://semanticweb.cs.vu.nl/prestoprime/video2726',
+		   'http://semanticweb.cs.vu.nl/prestoprime/video585',
+		   'http://semanticweb.cs.vu.nl/prestoprime/video420',
+		   'http://semanticweb.cs.vu.nl/prestoprime/video714'
+
+		 ].
+
 user_videos(User, SortedVideos) :-
 	%fail,
 	findall(Time-Video,
diff --git a/lib/video_annotation.pl b/lib/video_annotation.pl
index 06c432a..931a762 100644
--- a/lib/video_annotation.pl
+++ b/lib/video_annotation.pl
@@ -30,6 +30,7 @@
 :- use_module(library(http/json_convert)).
 :- use_module(library(http/http_session)).
 :- use_module(library(semweb/rdf_db)).
+:- use_module(library(semweb/rdfs)).
 :- use_module(library(semweb/rdf_label)).
 :- use_module(user(user_db)).
 
@@ -122,7 +123,17 @@ value_annotation(Value, Process, User, Time) :-
 video_annotations(Video, Annotations, Options) :-
 	A = a(Value,Time,Id,Score),
 	findall(A, video_annotation(Video, Id, Value, Time, Score, Options), As0),
-	sort(As0, As),
+	(   option(type(Type), Options),
+	    Type \== false
+	->  filter_by_type(As0, Type, As1)
+	;   As1 = As0
+	),
+	(   option(role(Role), Options),
+	    Role \== false
+	->  filter_by_role(As1, Role, As2)
+	;   As2 = As1
+	),
+	sort(As2, As),
 	(   option(interval(Interval0), Options),
 	    Interval0 > 0
 	->  Interval is Interval0*1000,
@@ -133,6 +144,36 @@ video_annotations(Video, Annotations, Options) :-
 annotation_term(a(Value,Time,Id,Score),
 		annotation(Value,Time,Time,[i(Id,Time)],Score)).
 
+filter_by_type([], _, []).
+filter_by_type([A|As], Type, Filtered) :-
+	A = a(Value,_,_,_),
+	(   Value = uri(R, _),
+	    tag_of_type(Type, R)
+	->  Filtered = [A|Rest]
+	;   Filtered = Rest
+	),
+	filter_by_type(As, Type, Rest).
+
+filter_by_role([], _, []).
+filter_by_role([A|As], Role, Filtered) :-
+	A = a(_,_,Id,_),
+	(   rdf(Id, pprime:role, literal(Role))
+	->  Filtered = [A|Rest]
+	;   Filtered = Rest
+	),
+	filter_by_role(As, Role, Rest).
+
+tag_of_type(person, Tag) :-
+	rdf(Tag, skos:inScheme, gtaa:'Persoonsnamen').
+tag_of_type(person, Tag) :-
+	rdf(Tag, skos:inScheme, gtaa:'Namen').
+tag_of_type(location, Tag) :-
+	rdf(Tag, skos:inScheme, gtaa:'GeografischeNamen').
+tag_of_type(subject, Tag) :-
+	rdf(Tag, skos:inScheme, gtaa:'Subjects').
+tag_of_type(subject, Tag) :-
+	rdfs_individual_of(Tag, skos:'Concept').
+
 %%	annotations_per_interval(+Terms, +Interval, -GroupedAnnotations)
 %
 %	A group contains all annotations with a similar value and within
@@ -176,7 +217,7 @@ video_annotation(Video, AnnotationId, uri(Tag,Label), Time, Score, Options) :-
 	option(confirmed(Confirmed), Options, false),
 	find_literal(Query, prefix, Label),
 	rdf(Tag, rdfs:label, literal(Label)),
-	rdf(Tag, rdf:type, pprime:'Tag'),
+	%rdf(Tag, rdf:type, pprime:'Tag'),
  	rdf(AnnotationId, rdf:value, Tag),
 	annotation_in_process(Process, Video, AnnotationId),
 	rdf(AnnotationId, pprime:creator, User),
@@ -294,6 +335,14 @@ annotation_provenance(AnnotationId, Provenance) :-
 %	True if Transaction is an addition or a modification of an
 %	annotation.
 
+annotation_transaction(AnnotationId, Transaction) :-
+	rdf(AnnotationId, pprime:creator, User, Graph),
+	rdf(AnnotationId, opmv:wasPerformedAt, literal(Time)),
+	rdf(AnnotationId, pprime:videoPlayhead, literal(Playhead)),
+	rdf(AnnotationId, rdf:value, Value),
+	rdf(Video, pprime:hasAnnotation, AnnotationId),
+	Transaction = action(AnnotationId,Time,User,Graph,Action),
+ 	Action = added(Video, AnnotationId, Value, Playhead).
 annotation_transaction(AnnotationId, Transaction) :-
 	findall(P, rdf(AnnotationId,_,_,P), Processes0),
 	sort(Processes0, Processes),
@@ -440,6 +489,10 @@ update_annotation_time(AnnotationId, NewTime0) :-
 %	Translate a list of transaction to a provenance description.
 
 transactions_to_provenance([], []).
+transactions_to_provenance([Action|Ts], [Action|Ps]) :-
+	Action = action(_,_,_,_,_), % already in right format
+	!,
+	transactions_to_provenance(Ts, Ps).
 transactions_to_provenance([T|Ts], [P|Ps]) :-
 	T = rdf_transaction(Id, _Nesting, Time, rdfh(Message), Actions, _Graphs),
 	canonical_action(Actions, CanonicalAction),
@@ -515,3 +568,4 @@ valid_time(Time, Valid) :-
 
 rdf_history:rdfh_hook(graph(Process)) :-
 	current_user_process(Process).
+rdf_history:rdfh_hook(user(anonymous)).
diff --git a/web/css/player.css b/web/css/player.css
index bd3cf92..220381a 100644
--- a/web/css/player.css
+++ b/web/css/player.css
@@ -15,70 +15,84 @@
 
 /* element style */
 
-/* controls */
-.topcontrols {
-	text-align: right;
-	height: 20px;
-}
-.topcontrols a {
-	padding-top: 5px;
-	color: #CCC;
-}
 #video {
 	float: left;
 	margin-left: 10px;
 }
+#tagEdit {
+	float: right;
+	min-height: 200px;
+	padding: 4px;
+    width: 175px;
+	border: 1px solid #CCC;
+}
+
 /* tag list */
 
 /* tag player */
 #tags {
 	float: left;
+	border: 1px solid #CCCCCC
 }
 #tags input {
-	width: 98%;
+	width: 100%;
+	border-width: 0 0 1px 0;
+	border-style: solid;
+	border-color: #CCC;
+	padding: 4px 0;
+	background: url("../icons/search_bg.png") no-repeat scroll 98% 60% #FFFFFF;
+}
+#tags select {
+	width: 100%;
+	border-width: 0 0 1px 0;
+	border-style: solid;
+	border-color: #CCC;
+}
+#tags option {
+	margin: 2px 0;
+	padding: 1px 4px;
 }
 
 #tagplayer.hidden {
 	display: none;
 }
-#tagplayer .yui3-tag-carousel {
+#tagplayer .yui3-tag-player {
 	background: transparent;
 	overflow: auto;
-	border: 1px solid #CCC;
 }	
 /* tag carousel */
-.yui3-tag-carousel ul {
+.yui3-tag-player ul {
 	margin: 0;
 	padding: 0;
 }
-.yui3-tag-carousel li {
+.yui3-tag-player li {
 	overflow: hidden;
 	list-style: none;
 	margin: 1px 0;
 	padding: 4px 8px;
 	/*border-top: 1px solid #f2f2f2;*/
 }
-.yui3-tag-carousel li:nth-child(even) {
+.yui3-tag-player li:nth-child(even) {
 	background-color: #EEE;
 }
-.yui3-tag-carousel li.focus .label {
-    font-size: 125%;
+.yui3-tag-player li.focus .label {
+    font-size: 150%;
 }
-.yui3-tag-carousel li .hidden {
+.yui3-tag-player li .hidden {
 	display: none;
 }
-.yui3-tag-carousel li .label {
+.yui3-tag-player li .label {
 	cursor: pointer;
 	float: left;
 }
-.yui3-tag-carousel li .count {
+.yui3-tag-player li .count {
 	float: right;
 	color: #AAA;
 }
 
-.yui3-tag-carousel li .edit,
-.yui3-tag-carousel li .remove,
-.yui3-tag-carousel li .score {
+.yui3-tag-player li .edit,
+.yui3-tag-player li .remove,
+.yui3-tag-player li .score {
 	float: right;
 	color: #222;
 	padding: 1px 3px 1px 2px;
@@ -88,49 +102,49 @@
 	font-weight: bold;
 	font-size: 90%;
 }
-.yui3-tag-carousel li .edit a,
-.yui3-tag-carousel li .remove a {
+.yui3-tag-player li .edit a,
+.yui3-tag-player li .remove a {
 	color: #222;
 }
-.yui3-tag-carousel li .edit a:hover,
-.yui3-tag-carousel li .remove a:hover {
+.yui3-tag-player li .edit a:hover,
+.yui3-tag-player li .remove a:hover {
 	color: red;
 	text-decoration: none;
 }
-.yui3-tag-carousel li .match {
+.yui3-tag-player li .match {
 	line-height: 16px;
 	clear: both;
 	overflow: auto;
 	color: #999;
 }
-.yui3-tag-carousel li .match .label {
+.yui3-tag-player li .match .label {
 	padding: 0 0 2px 8px;
 }
-.yui3-tag-carousel li .match.accept {
+.yui3-tag-player li .match.accept {
     color: green;
 }
-.yui3-tag-carousel li .match.reject {
+.yui3-tag-player li .match.reject {
 	color: red;
 }
-.yui3-tag-carousel li .match.reject .score {
+.yui3-tag-player li .match.reject .score {
 	border-color: red;
 }
-.yui3-tag-carousel li .match.accept .score {
+.yui3-tag-player li .match.accept .score {
 	border-color: green;
 }
-.yui3-tag-carousel li .confirm {
+.yui3-tag-player li .confirm {
 	float: right;
 }
-.yui3-tag-carousel li .confirm .accept {
+.yui3-tag-player li .confirm .accept {
 	padding-left: 16px;
 	background-image: url('../icons/accept.png');
 }
-.yui3-tag-carousel li .confirm .reject {
+.yui3-tag-player li .confirm .reject {
 	padding-left: 16px;
 	background-image: url('../icons/cancel.png');
 }
 
-.yui3-tag-carousel li .label input {
+.yui3-tag-player li .label input {
 	width: 120px;
 }
 
@@ -316,3 +330,78 @@ ul.game-players {
 .yui3-timeline li.highlight {
 	background-color:red;
 }
+
+/* video frames */
+#toggleFrames {
+	float: right;
+}
+
+#videoframes.hidden {
+	display: none;
+}
+#videoplayer.hidden {
+	height: 0;
+	margin-left: -1000px;
+}
+
+.yui3-video-frames-content {
+	overflow: hidden;
+}
+.yui3-video-frames .header {
+	padding-bottom: 2px;
+	margin: 12px 0 4px;
+	font-weight: bold;
+	border-bottom: 1px solid #CCC;
+	clear: both;
+}
+.yui3-video-frames ul.frames-list {
+	margin: 0;
+	padding: 0;
+}
+.yui3-video-frames li {
+	width: 175px;
+	float: left;
+	overflow: hidden;
+	list-style: none;
+	margin: 0 10px 10px 0;
+}
+.yui3-video-frames img {
+	width: 100%;
+}
+.yui3-video-frames .tag,
+.yui3-video-frames .frame-confirm {
+	text-align: center;
+	padding: 3px 0;
+	background-color: #DDD;
+}
+.yui3-video-frames li.hidden {
+	display: none;
+}
+.yui3-video-frames .frame-confirm.depicted {
+	background-color: green;
+}
+.yui3-video-frames .frame-confirm.associated {
+	background-color: blue;
+}
+.yui3-video-frames .frame-confirm.rejected  {
+	background-color: red;
+}
+.yui3-video-frames .users {
+	z-index:4;
+	width: 20px;
+	height: 20px;
+	position:relative;
+	margin-bottom: -20px;
+	background-color:white;
+	-moz-border-radius: 0 0 20px 0;
+	border-radius: 0 0 20px 0;
+}
+.yui3-video-frames .image {
+	position: relative;
+	z-index: 2;
+	height: 98px;
+	overflow: hidden;
+}
+.yui3-video-frames .users.hidden {
+	display: none;
+}
\ No newline at end of file
diff --git a/web/css/tag.css b/web/css/tag.css
new file mode 100644
index 0000000..cab0f63
--- /dev/null
+++ b/web/css/tag.css
@@ -0,0 +1,22 @@
+.block h4 {
+	border-bottom: 1px solid #CCC;
+}
+
+.block ul {
+	margin: 0;
+	padding: 0;
+}
+.block li {
+	list-style: none;
+	padding: 2px 4px;
+}
+
+li .desc {
+	font-size: 95%;
+	color: #666;
+	padding-left: 2em;
+}
+.submit {
+	margin-top: 12px;
+	float: right;
+}
\ No newline at end of file
diff --git a/web/css/yaz.css b/web/css/yaz.css
index 29d9172..ecce930 100644
--- a/web/css/yaz.css
+++ b/web/css/yaz.css
@@ -24,7 +24,7 @@ body {
 }
 #header h1 a:hover,
 #header h1 a:visited {
-	color: #222;
+	color: #0033CC;
 	text-decoration: none;
 }
 
diff --git a/web/icons/search_bg.png b/web/icons/search_bg.png
new file mode 100644
index 0000000..717691e
Binary files /dev/null and b/web/icons/search_bg.png differ
diff --git a/web/js/tagplayer/tagplayer.js b/web/js/tagplayer/tagplayer.js
new file mode 100644
index 0000000..4d1b06b
--- /dev/null
+++ b/web/js/tagplayer/tagplayer.js
@@ -0,0 +1,217 @@
+YUI.add('tag-player', function(Y) {
+
+	var Lang = Y.Lang,
+		Widget = Y.Widget,
+		Node = Y.Node;
+
+	var NS = Y.namespace('mazzle');	
+	NS.TagPlayer = TagPlayer;
+	
+	/* TagPlayer class constructor */
+	function TagPlayer(config) {
+		TagPlayer.superclass.constructor.apply(this, arguments);
+	}
+
+	/* 
+	 * Required NAME static field, to identify the Widget class and 
+	 * used as an event prefix, to generate class names etc. (set to the 
+	 * class name in camel case). 
+	 */
+	TagPlayer.NAME = "tag-player";
+
+	/*
+	 * The attribute configuration for the TagPlayer widget. Attributes can be
+	 * defined with default values, get/set functions and validator functions
+	 * as with any other class extending Base.
+	 */
+	TagPlayer.ATTRS = {
+		tags: {
+			value: []
+		},
+		active: {
+			value: true
+		},
+		topIndent: {
+			value: true
+		},
+		edit: {
+			value: false
+		}
+	};
+
+	/* Static constants used to define the markup templates used to create TagPlayer DOM elements */
+	TagPlayer.LIST_CLASS = 'tag-list';
+	TagPlayer.LIST_TEMPLATE = '<ul class="'+TagPlayer.LIST_CLASS+'"></ul>';
+
+	/* TagPlayer extends the base Widget class */
+	Y.extend(TagPlayer, Widget, {
+
+		initializer: function() {
+		},
+
+		destructor : function() {
+		},
+
+		renderUI : function() {
+			var content = this.get("contentBox"),
+				height = this.get("height");
+						
+			// tag list
+			content.setStyle("position", "relative");
+			if(this.get("topIndent")) {
+				content.setStyle("top", height/2+"px");
+			}
+			this.listNode = content.appendChild(Node.create(TagPlayer.LIST_TEMPLATE));
+		},
+
+		bindUI : function() {
+			this.after("tagsChange", this.syncUI);
+			Y.delegate("click", this._itemSelect, this.listNode, "li .label", this);
+			Y.delegate("mouseover", this._itemHover, this.listNode, "li", this);
+			this._scrollAnim = new Y.Anim({
+			    node: this.get("boundingBox"),
+			    duration: 1,
+			    easing: Y.Easing.easeOut
+			});
+		},
+
+		syncUI : function() {
+			this._renderItems();
+		},
+
+		_renderItems : function() {
+			var tags = this.get("tags"),
+				timeIndex = {};
+			
+			this.listNode.setContent("");
+			
+			// format the items and store in the time index	
+			for(var i=0; i < tags.length; i++) {
+				this.listNode.append('<li>'+this.formatItem(tags[i])+'</li>');	
+				var time = Math.round(tags[i].startTime/1000); //TBD make this hookable
+				timeIndex[time] = i;
+			}
+			this._timeIndex = timeIndex;
+		},
+	
+		formatItem : function(item) { 
+			var tag = item.tag,
+				label = tag.label ? tag.label : tag.value,
+				score = item.score;
+
+			html = '<div class="label">'+label+'</div>';				
+			if(score) {
+				html += '<div class="score">'+score+'</div>';
+			} else {
+				html += '<div class="score hidden"></div>';
+			}			
+			if(item.uri) {
+				return '<a href="javascript:{}">'+html+'</a>';
+			} else {
+				return html;
+			}
+			
+		},
+		
+		_itemSelect : function(e) {
+			// item click
+			var node = e.currentTarget.get("parentNode"),
+				index = e.container.all("li").indexOf(node),
+				item = this.get("tags")[index],
+				arg = {li:node, index:index, tag:item};
+			
+			if(!node.one('input')) {	
+				Y.log('clicked tag '+item.tag.value+' at index '+index);	
+				this._highlight(index);
+	        	this.fire("itemSelect", arg);
+			}
+		},
+		
+		_itemHover : function(e) {
+			var node = e.currentTarget,
+				index = e.container.all("li").indexOf(node),
+				item = this.get("tags")[index],
+				arg = {li:node, index:index, tag:item};
+	        this.fire("itemHover", arg);
+		},
+		
+		focusTag : function(tag) {			
+			this.focusIndex(this.tagIndex(tag));
+		},
+		
+		focusNode : function(node) {
+			var index = this.listNode.all("li").indexOf(node);
+			this.focusIndex(index);
+		},
+		
+		focusTime : function(time) {
+			var timeIndex = this._timeIndex,
+				index = timeIndex[time];
+
+			if(index>=0) {
+				Y.log('tagged '+this.get("tags")[index].tag);
+				this.focusIndex(index);
+			}
+		},
+		
+		focusIndex : function(index) {
+			if(this.get("active")) {
+				this._scrollTo(index);
+				this._highlight(index);
+			}
+		},
+		
+		scoreIndex : function(index, score, id, action) {
+			var item = this.listNode.all("li").item(index),
+				el;
+			if(id) {
+				this.scores[id] = item;
+			}	
+			if(type=='edit'&&score>0) {
+				el = '.edit';
+			} else if(type=='confirm') {
+				el = '.confirm';
+			}
+			if(el) {
+				item.addClass('scored '+action);
+				item.one(el).addClass("hidden");
+				if(score>0) {
+					item.one('.score')
+						.setContent(score)
+						.removeClass("hidden");
+				}
+			}
+		},
+		
+		tagIndex : function(tag) {
+			var tags = this.get("tags");
+			var i = 0;
+			for (i; i < tags.length; i++) {
+				if(tags[i].tag === tag) {
+					return i;
+				}
+			}
+		},
+		
+		_scrollTo : function(index) {
+			var node = this.get("boundingBox"),
+				items = this.listNode.all("li"),
+				anim = this._scrollAnim,
+				scrollTo = Math.abs(this.listNode.getY() - items.item(index).getY());
+			
+			Y.log('scroll from '+node.get('scrollTop')+' to '+scrollTo);
+			anim.set('to', { scroll: [node.get('scrollTop'), scrollTo] });
+        	anim.run();
+		},
+		
+		_highlight : function(index) {
+			var items = this.listNode.all("li");
+			// removeFocus from other items
+			items.removeClass('focus');
+			// add focus class to current item
+			items.item(index).addClass('focus');
+		}
+
+	});
+	  
+}, 'gallery-2010.03.02-18' ,{requires:['node','anim','widget']});
\ No newline at end of file
diff --git a/web/js/valueslider/valueslider.js b/web/js/valueslider/valueslider.js
new file mode 100644
index 0000000..7c4f11b
--- /dev/null
+++ b/web/js/valueslider/valueslider.js
@@ -0,0 +1,75 @@
+YUI.add('value-slider', function(Y) {
+
+	var Lang = Y.Lang,
+		Widget = Y.Widget,
+		Node = Y.Node;
+
+	var NS = Y.namespace('mazzle');	
+	NS.ValueSlider = ValueSlider;
+	
+	function ValueSlider(config) {
+		ValueSlider.superclass.constructor.apply(this, arguments);
+	}
+	
+	ValueSlider.ATTRS = {
+		label: {
+			value: '',
+        },
+		name: {
+			value: ''
+		},
+		node: {
+			value: null
+		},
+		min: {
+			value: 0
+		},
+		max: {
+			value: 100
+		},
+		value: {
+			value: 0
+		} 
+	};
+	
+	Y.extend(ValueSlider, Y.Base, {
+		initializer : function() {
+			var node = this.get("node"),
+				name = this.get("name"),
+				label = this.get("label"),
+				min = this.get("min"),
+				max = this.get("max"),
+				value = this.get("value");
+			node.append('<label for='+name+'_value>'+label+'</label>');
+			var sliderNode = node.appendChild(Node.create('<span></span>'));
+			var input = node.appendChild(Node.create('<input id="'+name+'_value">'));
+			input.set("value", value);
+			var slider = new Y.Slider({min:min,max:max,value:value});
+			input.setData( { slider: slider } );
+			slider.after("valueChange", function(e) { 
+				input.set("value", e.newVal);
+			});
+			input.on( "keyup", function(e) { 
+				var data   = input.getData(),
+        		slider = data.slider,
+        		value  = parseInt( input.get( "value" ), 10 );
+				if ( data.wait ) {
+        			data.wait.cancel();
+    			}
+		    	// Update the Slider on a delay to allow time for typing
+				data.wait = Y.later( 200, this, function () {
+					data.wait = null;
+					if(Y.Lang.isNumber(value)) {
+						slider.set( "value", value );
+						this.fire("valueUpdate", {name:name,value:value});
+					}
+		    	});
+			}, this);
+			slider.render(sliderNode);
+			slider.on("slideEnd", function(e) {
+				this.fire("valueUpdate", {name:name,value:slider.get("value")});
+			}, this);
+		}	
+	});
+	  
+}, 'gallery-2010.03.02-18' ,{requires:['node','base','slider']});
\ No newline at end of file
diff --git a/web/js/videoframes/videoframes.js b/web/js/videoframes/videoframes.js
index e7de428..3a75fb1 100644
--- a/web/js/videoframes/videoframes.js
+++ b/web/js/videoframes/videoframes.js
@@ -31,17 +31,14 @@ YUI.add('video-frames', function(Y) {
 		dataServer: {
 			value: null
 		},
-		playerPath: {
-			value: '/js/videoplayer/'
-		},
 		maxFrames: {
-			value: 250
+			value: 20
 		},
 		frames: {
 			value: []
 		},
-		video: {
-			value: null
+		related: {
+			value: []
 		},
 		duration: {
 			value: null
@@ -49,14 +46,8 @@ YUI.add('video-frames', function(Y) {
 		confirm: {
 			value: false
 		},
-		concept: {
+		video: {
 			value: null
-		},
-		interval: {
-			value: 10
-		},
-		users: {
-			value: 2
 		}
 	};
 
@@ -69,9 +60,6 @@ YUI.add('video-frames', function(Y) {
 
 		initializer: function() {
 			this.listNode = null;
-			this.player = null;
-			this.videoBufferReady = false;
-			this.timeline = null;
 		},
 
 		destructor : function() {
@@ -79,120 +67,23 @@ YUI.add('video-frames', function(Y) {
 
 		renderUI : function() {
 			var content = this.get("contentBox");
-			this._renderPlayer(content);
-			this._renderTimeline(content);
-			this._renderControls(content);
 			this._renderFrameList(content);
-			this.player.on("bufferReady", function() {
-				var duration = (this.get("duration") || this.player.getDuration())*1000
-				this.videoBufferReady = true;
-				Y.one('.yui3-videoplayer').setStyle("left", "-10000px");
-				this.timeline.set("duration", duration);
-			}, this);
 		},
 
 		bindUI : function() {
-			this.after("framesChange", this.syncUI);
+			this.after("framesChange", this._renderFrames);
+			this.after("relatedChange", this._renderRelated);
 			
 			// frame click
 			Y.delegate("click", this._onFrameSelect, this.listNode, "li .image", this);
-			Y.delegate("mouseover", this._onFrameHover, this.listNode, "li", this);
-			Y.delegate("click", this._onConfirmSelect, this.listNode, "li div.frame-confirm", this);
+			Y.delegate("mouseover", this._onFrameHover, this.get("contentBox"), "li", this);
+			Y.delegate("click", this._onRoleSelect, this.get("contentBox"), "li div.frame-confirm", this);
 		},
 
 		syncUI : function() {
-			var frames = this.get("frames"),
-				interval = this.get("interval")*1000,
-				userLimit = this.get("users");
-			
-			// hide the video
-			if(this.videoBufferReady) {
-				this.player.pause();
-				Y.one('.yui3-videoplayer').setStyle("left", "-10000px");
-			}
-			// First we render all frames afterwards we hide frames according 
-			// to the settings of the interval and user limit.
-			this._renderFrames(frames);
-			this.timeline.set("items", frames);
-			if(frames.length>0&&interval>0) {
-				this._filterFrames();
-			}		
-		},
-		
-		_renderPlayer : function(node) {
-			this.player = new Y.mazzle.VideoPlayer({
-				filepath:this.get("playerPath"),
-				src:this.get("video"),
-				width:175,
-				height:98,
-				autoplay:false,
-				controls:false
-				//visible:false
-			})
-			.render(node);
-			Y.one('.yui3-videoplayer').plug(Y.Plugin.Align);
-		},
-		
-		_renderTimeline : function(node) {
-			this.timeline = new Y.mazzle.Timeline({
-				height:20
-			})
-			.render(node);
-		},
-		
-		_renderControls : function(node) {
-			var controls = node.appendChild(Node.create('<div class="controls"></div>'));
-			// add controls for scene selection
-			var sceneNode = controls.appendChild(Node.create('<div class="sceneSelect"></div>'));
-			this._renderSlider(sceneNode, 'group frames by interval', 'interval', {
-				min:0,
-				max:60,
-				value:this.get("interval")
-			});
-			
-			var userNode = controls.appendChild(Node.create('<div class="userSelect"></div>'));
-			this._renderSlider(userNode, 'minimal users', 'users', {
-				min:1,
-				max:10,
-				value:this.get("users")
-			});
+			this._renderFrames();
 		},
-		
-		_renderSlider : function(node, label, name, conf) {
-			var content = node.appendChild(Node.create('<div class="control"></div>'));
-			content.append('<label for='+name+'_value>'+label+'</label>');
-			var sliderNode = content.appendChild(Node.create('<span></span>'));
-			var input = content.appendChild(Node.create('<input id="'+name+'_value">'));
-			input.set("value", conf.value);
-			var slider = new Y.Slider(conf);
-			input.setData( { slider: slider } );
-			slider.after("valueChange", function(e) { 
-				input.set("value", e.newVal);
-			});
-			input.on( "keyup", function(e) { 
-				var data   = input.getData(),
-		        slider = data.slider,
-		        value  = parseInt( input.get( "value" ), 10 );
-				if ( data.wait ) {
-		        	data.wait.cancel();
-		    	}
-		    	// Update the Slider on a delay to allow time for typing
-				data.wait = Y.later( 200, this, function () {
-					data.wait = null;
-					if(Y.Lang.isNumber(value)) {
-						slider.set( "value", value );
-						this.set(name, value);
-						this._filterFrames();
-					}
-		    	});
-			}, this);
-			slider.render(sliderNode);
-			slider.on("slideEnd", function(e) {
-				this.set(name, slider.get("value"));
-				this._filterFrames();
-			}, this);
-		},
-		
+						
 		_renderFrameList : function(node) {
 			var list = node.appendChild(Node.create(VideoFrames.LIST_TEMPLATE));
 			// create list elements
@@ -200,20 +91,32 @@ YUI.add('video-frames', function(Y) {
 			for(var i=0; i < maxFrames; i++) {
 				list.append('<li class="frame hidden"></li>');
 			}
-			this.listNode = list;
 			
-			var tag = "";
-			// render suggestion list
-			node.appendChild('<div class="header"><span class="tagTitle">'+tag+'</span> might also describe:</div>');
-			this.suggestList = node.appendChild(Node.create(VideoFrames.LIST_TEMPLATE));
+			// create list for related frames
+			node.appendChild('<div class="header">similar annotations:</div>');
+			related = node.appendChild(Node.create(VideoFrames.LIST_TEMPLATE));
+			related.addClass("related");
+			for(var i=0; i < maxFrames; i++) {
+				related.append('<li class="frame hidden"></li>');
+			}
+			
+			this.listNode = list;
+			this.relatedNode = related;
+		},
+		
+		_renderFrames : function() {
+			var frames = this.get("frames");
+			this._updateFrames(this.listNode, frames);
+		},
+		_renderRelated : function() {
+			var frames = this.get("related");
+			this._updateFrames(this.relatedNode, frames);
 		},
 		
-		_renderFrames : function(frames) {
-			// update all list elements
-			this.listNode.all("li").each(function(node, i) {
+		_updateFrames : function(node, frames) {
+			node.all("li").each(function(node, i) {
 				if(frames[i]) {
 					node.setContent(this.formatFrame(frames[i]));
-					node.prepend('<div class="users hidden"></div>');
 					node.removeClass("hidden");
 				} else {
 					node.setContent("");
@@ -221,174 +124,96 @@ YUI.add('video-frames', function(Y) {
 				}
 			}, this);
 		},
-	
+		
 		formatFrame : function(frame) {
 			var frameServer = this.get("frameServer"),
-				video = frame.video||this.get("video"),
-				time = (frame.time/1000),
-				tag = frame.tag;
+				video = this.get("video"),
+				time = (frame.startTime/1000),
+				label = frame.tag.label,
+				role = frame.role ? frame.role : '';
 
 			var html = '<div class="image">'
 				+'<img src="'+frameServer+'?url='+video+'&time='+time+'">'
 				+'</div>';
 			if(this.get("confirm")) {
 				//html += '<div class="frame-confirm">click to confirm</div>';
-				html += '<div class="frame-confirm">'+tag+'?</div>';
+				html += '<div class="frame-confirm '+role+'">'
+					+this._roleLabel(role)
+					+label+'</div>';
 			} else {
-				html += '<div class="tag">'+tag+'</div>';
+				html += '<div class="tag">'+label+'</div>';
 			}
 
 			return html;
 		},
 		
+		_roleLabel : function(role) {
+			if(role=="depicted") {
+				return "depicts ";
+			} else if(role=="associated") {
+				return "associated with ";
+			} else if(role=="rejected") {
+				return "not ";
+			} else {
+				return "";
+			}
+		},
+		
 		_onFrameSelect : function(e) {
-			var parent = e.currentTarget.get("parentNode"),
-				index = e.container.all("li").indexOf(parent),
-	            frame = this.get("frames")[index],
-				arg = {li:e.currentTarget, index:index, frame:frame};
+			var node = e.currentTarget.get("parentNode"),
+				list = node.get("parentNode");
+				index = list.all("li").indexOf(node),
+	            frame = this._getFrame(list, index),
+				arg = {li:node, index:index, frame:frame};
 			
-			Y.log('clicked frame '+frame+' at index '+index);	
-			Y.one('.yui3-videoplayer').align.to(e.currentTarget, "tl","tl");
-			this.player.setTime(frame.time/1000, true);	
-				
+			Y.log('clicked frame '+frame+' at index '+index);					
        		this.fire("frameSelect", arg);
 		},
 		
 		_onFrameHover : function(e) {
-			var index = e.container.all("li").indexOf(e.currentTarget);
-			this.timeline.highlightIndex(index);
-		},
-		
-		_onConfirmSelect : function(e) {
-			var target = e.currentTarget,
-				parent = e.currentTarget.get("parentNode"),
-				index = this.listNode.all("li").indexOf(parent),
-				frame = this.get("frames")[index];
-
-			Y.log('frame confirm selected at index '+index);
+			var node = e.currentTarget,
+				list = node.get("parentNode"),
+				index = list.all("li").indexOf(node),
+				frame = this._getFrame(list, index);
+				arg = {li:node, index:index, frame:frame};
+			this.fire('frameHover', arg);
+		},
+		
+		_onRoleSelect : function(e) {
+ 			var target = e.currentTarget,
+				node = e.currentTarget.get("parentNode"),
+				list = node.get("parentNode");
+				index = list.all("li").indexOf(node),
+				frame = this._getFrame(list, index),
+				tag = frame.tag.label;
+			
+			Y.log('frame role selected at index '+index);
 			
-			var type = "depicted",
-				label = "depicts";
+			var type = "depicted";
 			if(frame.confirm) {
 				if(frame.confirm=="depicted") {
-					type="associated"
-					label="associated with"
+					type="associated";
 				} else if(frame.confirm=="associated") {
 					type = "rejected";
-					label = "not";
 				}
 				target.replaceClass(frame.confirm, type);
 			} else {
 				target.addClass(type);
 			};
 			frame.confirm = type;
-			var concept = this.get("concept");
-			var tag = (concept&&concept.name) ? concept.name : frame.tag;
 			
-			e.target.setContent(label+" "+tag);
-			this.fire("confirmSelect", {type:type, index:index, frame:frame, concept:concept});
+			e.target.setContent(this._roleLabel(type)+tag);
+			this.fire("roleSelect", {type:type, index:index, frame:frame});
 		},
 		
-		fetchData: function(conf) {
-			// default request
-			var data = {
-				video:this.get("video"),
-				interval:this.get("interval"),
-				users:this.get("users")
-			}
-			// update with conf parameters
-			if(conf) {
-				for(var key in conf) {
-					if(key) {
-						data[key] = conf[key];
-					}
-				}
-			}
-			Y.io(this.get("dataServer"), {
-				data: data,
-				on: { success: this.dataResponse },
-				context:this
-			});
-		},
-		
-		dataResponse: function(id,o) {
- 			this.set("frames", Y.JSON.parse(o.responseText).fragments);
-		},
-		
-		_filterFrames:  function() {
-			var frames = this.get("frames"),
-				interval = this.get("interval")*1000,
-				userLimit = this.get("users"),
-				groups = {};
-			
-			if(interval>0) {	
-				groups = this._groupFrames(frames, interval);
-				this._updateFrames(groups, userLimit);
+		_getFrame : function(list, index) {
+			if(list.hasClass("related")) {
+				return this.get("related")[index];
 			} else {
-				this.listNode.all("li").each(function(node, i) {
-					if(frames[i]) {
-						node.removeClass("hidden");
-						node.one(".users").addClass("hidden");	
-					}
-				})
-			}
-			this._updateSuggestFrames(groups, userLimit);
-			this.timeline.updateToInterval(groups, interval);
-		},
-		
-		/* _groupFrames
-		 *
-		 * group frames that are within the same interval
-		 * the groups are stored as an array of arrays with indices
-		*/
-		_groupFrames: function(frames, interval, userLimit) {
-			// groups of frames are 
-			
-			var groups = {}, group = 0, end = -1;
-			for (var i=0; i < frames.length; i++) {
-				var frame = frames[i];
-				if(frame.time < end) {
-					groups[group].push(frame);
-				} else {
-					group = i;
-					end = frame.time + interval;
-					groups[group] = [frame];
-				}
-			}
-			return groups;
-		},
-		
-		_updateFrames: function(groups, userLimit) {
-			// The grouped frames are visualized by showing only the first frame
-			// of the group and hiding the next ones.
-			// In addition we show the number of unique users in the first frame.
-			// TBD use unique users instead of the number of tag entries, 
-			// which may contain the same user multiple times)
-			this.listNode.all("li").each(function(node, i) {
-				if(groups[i]&&groups[i].length>=userLimit) {
-					node.removeClass("hidden");
-					node.one(".users")
-						.setContent("<span>"+groups[i].length+"</span>")
-						.removeClass("hidden");
-				} else {
-					node.addClass("hidden");
-				}
-			})
-		},
-		
-		_updateSuggestFrames: function(groups, userLimit) {
-			var list = this.suggestList;
-			this.suggestList.setContent("");
-			for(key in groups) {
-				var frames = groups[key];
-				if(frames&&frames.length<userLimit) {
-					var node = list.appendChild(Node.create('<li class="frame"></li>'));
-					node.setContent(this.formatFrame(frames[0]));
-					//node.prepend('<div class="users hidden"></div>');
-				}
+				return this.get("frames")[index]
 			}
 		}
-		
+				
 	});
 	  
 }, 'gallery-2010.03.02-18' ,{requires:['node','widget','io-base','json-parse']});
\ No newline at end of file