yaz/commit
faceted video navigation
author | Michiel Hildebrand |
---|---|
Wed Jul 6 17:03:55 2011 +0200 | |
committer | Michiel Hildebrand |
Wed Jul 6 17:03:55 2011 +0200 | |
commit | ad3a9f3c2b70fbb7c21b608dc2273bfeed6f8f9f |
tree | 8bade1d7626f350dbf3e46411ded142b6b2d8012 |
parent | 8bf3e31de8c61c74922fbdd63d452ba9c06453d5 |
Diff style: patch stat
diff --git a/api/reconcile.pl b/api/reconcile.pl index e2049ae..00fd73d 100644 --- a/api/reconcile.pl +++ b/api/reconcile.pl @@ -43,7 +43,7 @@ flush_reconcile_cache :- % Returns a json object. http_reconcile(Request) :- - http_parameters(Request, + http_parameters(Request, [ query(Query, [atom, optional(true), @@ -53,7 +53,7 @@ http_reconcile(Request) :- [json, optional(true), description('a json object of the form {q1:{query:STRING}, ...}')]), - limit(Limit, + limit(Limit, [number, default(3), description('Number of results to return per query')]), type(Type, @@ -72,7 +72,7 @@ http_reconcile(Request) :- ( nonvar(Queries), Queries = json(QueryList) -> reconcile_list(QueryList, Limit, Type, Properties, Results), reply(Callback, json(Results)) - ; nonvar(Query) + ; nonvar(Query) -> reconcile(Query, Limit, Type, Properties, Hits), hits_to_json_results(Hits, Results), reply(Callback, json([result=Results])) @@ -98,7 +98,7 @@ reconcile_list([], _, _, _, []). reconcile_list([Key=json([query=Query])|Ts], Max, Type, Properties, [Key=json([result=Results])|Rs]) :- reconcile(Query, Max, Type, Properties, Hits), hits_to_json_results(Hits, Results), - reconcile_list(Ts, Max, Type, Properties, Rs). + reconcile_list(Ts, Max, Type, Properties, Rs). %% reconcile(+Query, +MaxResults, +Type, +Properties, %% -Concept:hit(score,uri,property,label)) @@ -113,7 +113,7 @@ reconcile(Query, Max, Type, Properties, Hits) :- !, reconcile_filter(Hits0, Max, Type, Properties, Hits). reconcile(Query, Max, Type, Properties, Hits) :- - label_list(LabelList), + label_list(LabelList), find_resource_by_name(Query, Hits0, [attributes(LabelList),match(case),distance(true)]), assert(reconcile_cache(Query, Hits0)), reconcile_filter(Hits0, Max, Type, Properties, Hits). @@ -214,7 +214,7 @@ http_save_reconcile(Request) :- http_parameters(Request, [ entry(TagEntry, [description('URI of tagentry event')]), - uri(URI, + uri(URI, [description('URI of resource tag is reconciled with')]) ]), valid_reconcile(TagEntry, URI, User, Tag, Error), @@ -233,10 +233,10 @@ valid_reconcile(TagEntry, _URI, _User, Tag, Error) :- ). assert_recon(ReconcileEvent, TagEntry, URI, User) :- - rdf_assert(ReconcileEvent, rdf:type, pprime:'ReconcileEvent', recon), + rdf_assert(ReconcileEvent, rdf:type, pprime:'ReconcileEvent', recon), rdf_assert(ReconcileEvent, pprime:reconciles, TagEntry, recon), rdf_assert(ReconcileEvent, pprime:reconcilesWith, URI, recon), - rdf_assert(ReconcileEvent, sem:hasActor, User, recon). + rdf_assert(ReconcileEvent, sem:hasActor, User, recon). reconcile_event_uri(TagEntry, ReconcileEventURI) :- % base URI on number of existing reconciliation for TagEntry @@ -264,7 +264,7 @@ json_reply_error(Error) :- freebase_reconcile(Tags, ReconciledTags) :- freebase_url(URL), - freebase_query(Tags, Query), + freebase_query(Tags, Query), freebase_option_string(Options), www_form_encode(Query, EncQuery), concat_atom([URL, '?queries=', EncQuery, Options], Request), diff --git a/applications/yaz_fplayer.pl b/applications/yaz_fplayer.pl new file mode 100644 index 0000000..8bed8f7 --- /dev/null +++ b/applications/yaz_fplayer.pl @@ -0,0 +1,241 @@ +:- module(yaz_fplayer, + []). + +:- 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(components(yaz_video_item)). + +:- use_module(api(reconcile)). + + +:- http_handler(yaz(fplayer), http_yaz_fplayer, []). + + +subject(R) :- + rdf(R, skos:inScheme, gtaa:'Onderwerpen'). +subject(R) :- + rdfs_individual_of(R, cornetto:'Synset'). +location(R) :- + rdf(R, skos:inScheme, gtaa:'GeografischeNamen'). +person(R) :- + rdf(R, skos:inScheme, gtaa:'Persoonsnamen'). +person(R) :- + rdf(R, skos:inScheme, gtaa:'Namen'). + + +%% http_yaz_fplayer(+Request) +% +% Emit an HTML page to link tags to concepts. + +http_yaz_fplayer(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 from this user are shown')]), + interval(Interval, + [default(10), number, + description('When set one entry per tag is returned in interval (in milliseconds)')]), + confirmed(Confirmed, + [boolean, default(false), + description('When true only tags that are entered by >1 user are shown')]) + ]), + Options0 = [video(Video), + process(Process), + user(User), + confirmed(Confirmed), + interval(Interval) + ], + delete_nonground(Options0, Options), + video_annotations(Video, Annotations, Options), + maplist(annotation_pair, Annotations, TagEntries0), + merge_entries(TagEntries0, TagEntries), + concept_entries(TagEntries, subject, Subjects, Rest0), + concept_entries(Rest0, location, Locations, Rest), + concept_entries(Rest, person, Persons, _), + html_page(Video, Subjects, Locations, Persons, Options). + +annotation_pair(annotation(Value,_,_,[E0|_],_), Value-E1) :- + E0 = i(URI, Time), + E1 = entry(URI,Value,Time). + +concept_entries(TagEntries, Goal, ConceptEntries, Rest) :- + concept_entries_(TagEntries, Goal, ConceptEntries0, Rest), + merge_entries(ConceptEntries0, ConceptEntries1), + maplist(concept_pair, ConceptEntries1, ConceptEntries). + +concept_entries_([], _, [], []). +concept_entries_([Tag-Entries|Ts], Goal, [Concept-Entries|Cs], Rest) :- + tag_concept(Tag, Goal, Concept), + !, + concept_entries_(Ts, Goal, Cs, Rest). +concept_entries_([Tag-Entries|Ts], Goal, Cs, [Tag-Entries|Rest]) :- + concept_entries_(Ts, Goal, Cs, Rest). + +tag_concept(Tag, Goal, Concept) :- + tag_value(Tag, Value), + reconcile(Value, 3, Hits), + member(hit(_,Concept,_,_), Hits), + call(Goal, Concept). + +concept_pair(Concept-Es, annotation(uri(Concept,Label), 0, 0, Es)) :- + rdf_display_label(Concept, Label). + +%% html_page(+Video, +Concepts, +Options) +% +% Emit an HTML page for concept gardening + +html_page(Video, Subjects, Locations, Persons, Options) :- + reply_html_page(yaz, + [ title(['YAZ - ', Video]) + ], + [ \html_requires(css('fplayer.css')), + \html_requires(css('tag.css')), + \yaz_video_header(Video), + div(id(tags), + [ \tag_facet(person, 'Person/Organization'), + \tag_facet(location, 'Location'), + \tag_facet(subject, 'Subject') + ]), + div(id(video), + [ div(id(videoplayer), []), + div([id(videoframes)], []) + ]), + script(type('text/javascript'), + \html_video_page_yui(Video, Subjects, Locations, Persons, Options)) + ]). + +tag_facet(Id, Label) --> + html(div(class(tagfacet), + [ div(class(hd), Label), + div(class(bd), + div(id(Id), [])) + ])). + +html_video_page_yui(Video, Subjects, Locations, Persons, Options) --> + { video_source(Video, Src, Duration), + 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, []), + annotation_to_json(Persons, JSONPersons), + annotation_to_json(Locations, JSONLocations), + annotation_to_json(Subjects, JSONSubjects) + }, + html_requires(js('videoplayer/swfobject.js')), + js_yui3([{modules:{'video-player':{fullpath:Videoplayer}, + 'video-frames':{fullpath:VideoFrames}, + 'tag-player':{fullpath:Tagplayer}, + 'timeline':{fullpath:Timeline} + }} + ], + [node,event,widget,anim, + 'json','querystring-stringify-simple',io, + 'video-player','video-frames','tag-player', + timeline + ], + [ \js_new(person, + 'Y.mazzle.TagPlayer'({tags:JSONPersons, + height:150, + width:200, + topIndent:symbol(false) + })), + \js_new(location, + 'Y.mazzle.TagPlayer'({tags:JSONLocations, + height:150, + width:200, + topIndent:symbol(false) + })), + \js_new(subject, + 'Y.mazzle.TagPlayer'({tags:JSONSubjects, + height:150, + width:200, + topIndent:symbol(false) + })), + \js_new(videoPlayer, + 'Y.mazzle.VideoPlayer'({filepath:FilePath, + src:Src, + width:560, + height:400, + autoplay:symbol(false), + controls:symbol(true), + duration:Duration + })), + \js_new(videoFrames, + 'Y.mazzle.VideoFrames'({frameServer:FrameServer, + video:Src, + duration:Duration, + playerPath:FilePath, + width:560, + confirm:symbol(true), + showRelated:symbol(false), + showTime:symbol(true) + })), + \js_yui3_decl(params, json(Options)), + \js_call('videoPlayer.render'('#videoplayer')), + \js_call('videoFrames.render'('#videoframes')), + \js_call('person.render'('#person')), + \js_call('location.render'('#location')), + \js_call('subject.render'('#subject')), + \js_yui3_on(person, itemSelect, \js_tag_select), + \js_yui3_on(location, itemSelect, \js_tag_select), + \js_yui3_on(subject, itemSelect, \js_tag_select), + \js_yui3_on(videoFrames, frameSelect, \js_frame_select) + ]). + + +js_tag_select --> + js_function([e], + \[ +' var tag = e.tag; + var entry = tag.annotations[0];console.log(entry);\n', +' var time = (entry.startTime/1000)-3; + videoPlayer.setTime(time, true);\n', +' videoFrames.set("frames", tag.annotations);\n' + ]). + +js_frame_select --> + js_function([e], + \[ +' var frame = e.frame; + var time = (frame.startTime/1000)-3; + videoPlayer.setTime(time, true);\n' + ]). + + + +merge_entries(Pairs, Merged) :- + keysort(Pairs, Sorted), + group_pairs_by_key(Sorted, Grouped), + maplist(value_flatten, Grouped, Merged). + +value_flatten(Key-Lists, Key-List) :- + flatten(Lists, List). + + +tag_value(literal(Tag), Tag). +tag_value(uri(_URI,Tag), Tag). diff --git a/applications/yaz_tag.pl b/applications/yaz_tag.pl index 824ca59..0cad6b5 100644 --- a/applications/yaz_tag.pl +++ b/applications/yaz_tag.pl @@ -46,13 +46,13 @@ http_yaz_tag_edit(Request) :- [default(page), oneof([page,form]), description('Return a complete html page or only the form')]) - ]), + ]), update_tag(Action, Entry, Value, Concept, Role), ( rdf(Entry, rdf:type, pprime:'TagEntry') -> annotation_value(Entry, _TagId, Label) ; Label = Entry ), - %annotation_provenance(Entry, Provenance), + %annotation_provenance(Entry, Provenance), tag_concepts(Label, Concepts), ( Format = page -> html_page(Entry, Label, Provenance, Concepts, Concept, Role) @@ -62,6 +62,8 @@ http_yaz_tag_edit(Request) :- print_html(HTML) ). + + annotation_value(Entry, TagId, Label) :- rdf(Entry, rdf:value, TagId), rdf_label(TagId, Lit), @@ -157,7 +159,7 @@ html_roles(Selected) --> html_radiobox(Group, Value, Selected, Label) --> { radio_checked(Value, Selected, Checked) }, - html([input([type(radio), name(Group), value(Value), Checked]), + html([input([type(radio), name(Group), value(Value), Checked]), span(Label) ]). @@ -170,21 +172,21 @@ html_provenance([action(_,Time,User,_,Action)|T]) --> html_provenance(T). html_provenance_action(added(_, _, Value, _PlayHead), Time, User) --> - html(div(class(paction), + html(div(class(paction), [ 'added ', \html_tag_value(Value), \html_time_user(Time, User)])). html_provenance_action(removed(_, _), Time, User) --> - html(div(class(paction), + html(div(class(paction), [ 'removed ', \html_time_user(Time, User)])). html_provenance_action(valueChange(_, Value), Time, User) --> - html(div(class(paction), + 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), + html(div(class(paction), [ 'changed time to ', PlayHead, \html_time_user(Time, User)])). diff --git a/lib/yaz_util.pl b/lib/yaz_util.pl index 57ab08f..3d1d482 100644 --- a/lib/yaz_util.pl +++ b/lib/yaz_util.pl @@ -16,7 +16,7 @@ tag_entry_time/2, iri_to_url/2, delete_nonground/2, - video_source/2, + video_source/2, video_source/3, annotation_to_json/2, video_desc/2, @@ -104,14 +104,14 @@ list_limit_([H|T], N, [H|T1], Rest) :- % elements in the value list. pairs_sort_by_value_count(Grouped, Sorted) :- - pairs_value_count(Grouped, Counted), + pairs_value_count(Grouped, Counted), keysort(Counted, Sorted0), reverse(Sorted0, Sorted). pairs_value_count([], []). pairs_value_count([Key-Values|T], [Count-Key|Rest]) :- length(Values, Count), - pairs_value_count(T, Rest). + pairs_value_count(T, Rest). %% sort_by_arg(+ListOfTerms, +Arg, -SortedList) @@ -296,7 +296,7 @@ delete_nonground([H|T], [H|Rest]) :- !, delete_nonground(T, Rest). delete_nonground([_H|T], Rest) :- - delete_nonground(T, Rest). + delete_nonground(T, Rest). %% video_source(+URL, -Video) @@ -337,6 +337,7 @@ http:convert_parameter(jsonresource, Atom, Term) :- literal(type:atom, value:_) + [type=literal], literal(value:_) + [type=literal], i(uri:atom, time:number), + entry(entry:atom, tag:_, startTime:number), annotation(tag:_, count:number), annotation(tag:_, tags:list, count:number), annotation(tag:_, startTime:number, endTime:number, annotations:list), diff --git a/web/css/fplayer.css b/web/css/fplayer.css new file mode 100644 index 0000000..53ee5f7 --- /dev/null +++ b/web/css/fplayer.css @@ -0,0 +1,118 @@ +#body { + margin: 0 auto; +} + +.video-results h2 { + margin-bottom: 0; +} +.video-results .desc { + margin-bottom: 15px; + max-height: 2em; + color: #888; + font-size: 95%; + clear: both; +} + +/* element style */ + +#video { + float: left; + margin-left: 10px; +} + +/* tag player */ +#tags { + float: left; +} +#tags input { + 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 .hd { + padding: 5px; + font-weight: bold; + background +} + +.yui3-tag-player { + background: transparent; + overflow: auto; + border: 1px solid #CCCCCC; + margin-bottom: 5px; +} +/* tag carousel */ +.yui3-tag-player ul { + margin: 0; + padding: 0; +} +.yui3-tag-player li { + overflow: hidden; + list-style: none; + margin: 1px 0; + padding: 4px 8px; + /*border-top: 1px solid #f2f2f2;*/ +} +.yui3-tag-player li:nth-child(even) { + background-color: #EEE; +} +.yui3-tag-player li.focus .label { + font-size: 150%; +} +.yui3-tag-player li .hidden { + display: none; +} +.yui3-tag-player li .label { + cursor: pointer; + float: left; +} +.yui3-tag-player li .count { + float: right; + color: #AAA; +} + +/* video frames */ +.yui3-video-frames { + margin-top: 10px; +} +.yui3-video-frames-content { + overflow: hidden; + width: 100%; +} +.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 .image { + position: relative; + z-index: 2; + height: 98px; + overflow: hidden; +} +.yui3-video-frames .tag, +.yui3-video-frames .frame-confirm { + text-align: center; + padding: 3px 0; + background-color: #DDD; + cursor: pointer; +} diff --git a/web/css/player.css b/web/css/player.css index 9f9667e..32d6d29 100644 --- a/web/css/player.css +++ b/web/css/player.css @@ -34,7 +34,6 @@ /* tag player */ #tags { float: left; - border: 1px solid #CCCCCC } #tags input { width: 100%; @@ -54,13 +53,20 @@ margin: 2px 0; padding: 1px 4px; } +#tags .hd { + padding: 5px; + font-weight: bold; + background +} #tagplayer.hidden { display: none; } -#tagplayer .yui3-tag-player { +.yui3-tag-player { background: transparent; overflow: auto; + border: 1px solid #CCCCCC; + margin-bottom: 5px; } /* tag carousel */ .yui3-tag-player ul { diff --git a/web/js/videoframes/videoframes.js b/web/js/videoframes/videoframes.js index 88e8db8..833202e 100644 --- a/web/js/videoframes/videoframes.js +++ b/web/js/videoframes/videoframes.js @@ -51,6 +51,12 @@ YUI.add('video-frames', function(Y) { }, interval : { value: 10 + }, + showRelated : { + value: true + }, + showTime : { + value: false } }; @@ -94,17 +100,19 @@ YUI.add('video-frames', function(Y) { for(var i=0; i < maxFrames; i++) { list.append('<li class="frame hidden"></li>'); } - - // 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; + + if(this.get("showRelated")) { + // 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.relatedNode = related; + } + }, _renderFrames : function() { @@ -113,7 +121,9 @@ YUI.add('video-frames', function(Y) { }, _renderRelated : function() { var frames = this.get("related"); - this._updateFrames(this.relatedNode, frames); + if(this.relatedNode) { + this._updateFrames(this.relatedNode, frames); + } }, _updateFrames : function(node, frames) { @@ -137,10 +147,10 @@ YUI.add('video-frames', function(Y) { formatFrame : function(frame) { var frameServer = this.get("frameServer"), video = this.get("video"), - time = (frame.startTime/1000), + 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>'; @@ -148,13 +158,22 @@ YUI.add('video-frames', function(Y) { //html += '<div class="frame-confirm">click to confirm</div>'; html += '<div class="frame-confirm '+role+'">' +this._roleLabel(role) - +label+'</div>'; + +label+' '+this.formatTime(time)+ + '</div>'; } else { - html += '<div class="tag">'+label+'</div>'; + html += '<div class="tag">'+label+' '+this.formatTime(time)+'</div>'; } return html; }, + formatTime : function(totalSeconds) { + if(this.get("showTime")) { + var minutes = Math.floor(totalSeconds / 60); + var seconds = Math.floor(totalSeconds % 60); + var spacer = (seconds<10) ? 0 : ''; + return '<span class="time">'+minutes+':'+spacer+seconds+'</span>'; + } + }, _roleLabel : function(role) { if(role=="depicted") {