yaz/commit
integrate various types of gardening functionality into the yaz player
author | Michiel Hildebrand |
---|---|
Sun Mar 13 01:27:56 2011 +0100 | |
committer | Michiel Hildebrand |
Sun Mar 13 01:27:56 2011 +0100 | |
commit | 56b83702d20921ee979e22ec0f50479a79a6d67a |
tree | 3751bea84b2ab30184ec8bfc6e70945b647305a7 |
parent | 6e5f40acce48e14e991d7ef3d88aa7f7651ca390 |
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