/* ** Author(s): Miguel Calejo ** Contact: info@interprolog.com, http://interprolog.com ** Copyright (C) InterProlog Consulting / Renting Point - Serviços de Informática Lda., Portugal 2016 ** Use and distribution, without any warranties, under the terms of the ** Apache 2.0 License, readable in http://www.apache.org/licenses/LICENSE-2.0.html */ /* Utilities for visualizing LPS executions on SWISH */ :- module(visualizer,[ gov/1, gov/2, gov/3, gojson/1, gojson/5, golv/3, golv/2, golv/1, golpsv/3, golpsv/2, golpsv/1, collect_display_specs_lazy/2, has_d_clauses/0, visualization_options/1]). :- if(\+ current_prolog_flag(dialect, swi)). :- writeln("LPS requires SWI-Prolog"), throw(swi_prolog_required). :- endif. % ... and generic SWI: :- use_module(library(http/json)). :- use_module(library(lists)). :- use_module('../engine/interpreter.P',[ go/3, my_term_to_atom/2, abstract_numbers/2, endTime/1, d_event/1, l_int/2, print_error/3, action_/1, (observe)/2, lps_test_result_item/3, lps_test_action_ancestor/3, beginningOfSimulatedRealTime/1, simulatedRealTimePerCycle/1, u_swiclause/3, d/2 ]). % Notice that this is only valid in the LPS program thread: has_d_clauses :- u_swiclause(d(_,_),_,_), !. :- prolog_load_context(directory, Dir), asserta(user:file_search_path(lpsUtils, Dir)). :- use_module(lpsUtils('psyntax.P'),[ syntax2p_file/4 ]). make_json_object(L,json(NL)) :- json_colons_to_equals(L,NL). json_colons_to_equals(LC,LE) :- json_colons_to(LC,=,LE). % json_colons_to(+ListsWithColons,+Functor,-ListsWithFunctor) json_colons_to(V,_,NV) :- var(V), !, NV=V. json_colons_to([],_,[]) :- !. json_colons_to([A:V|L],F,[NX|NL]) :- NX=..[F,A,NV], !, json_colons_to(V,F,NV), json_colons_to(L,F,NL). json_colons_to([X|L],F,[NX|NL]) :- !, json_colons_to(X,F,NX), json_colons_to(L,F,NL). json_colons_to(json(L),F,json(NL)) :- json_colons_to(L,F,NL), !. json_colons_to(X,_,X). % converts one of our terms to a JSON dictionary term2json(Term,JSONDict) :- json_colons_to_equals(Term,SWIterm), % TODO: one step too many! merge into json_colons_to_equals atom_json_term(JSONs,SWIterm,[as(string)]), atom_json_dict(JSONs, JSONDict, []). create_counter(Dict) :- dict_create(Dict,'$_visualizer_counter',[value=0]). get_increment_counter(Dict,CurrentValue) :- get_dict(value,Dict,CurrentValue), New is CurrentValue+1, nb_set_dict(value,Dict,New). displayTimeline(_File,_JSON). %%% From here on, generic Prolog code % gov(ProgramFile) "go visual": executes LPSW file generating results and displays its timeline gov(File) :- gov(File,[]). % gov(ProgramFile,Options) gov(File,Options) :- gov(File,Options,[]). gov(File,Options,Results) :- go(File,[make_test|Options],Results), visual(File,_JSON,_DFAgraph). % Top level predicates to visualize timelines of a psyntax file golv(LPSP_file) :- golv(LPSP_file,[]). golv(LPSP_file,Options) :- golv(LPSP_file,Options,[]). golv(LPSP_file,Options,Results) :- concat_atom([LPSP_file,'_.P'],Pfile), syntax2p_file(LPSP_file,Pfile,lpsp2p,true), gov(Pfile,Options,Results). % Top level predicates to visualize timelines of a LPS (new syntax) file golpsv(LPSP_file) :- golpsv(LPSP_file,[]). golpsv(LPSP_file,Options) :- golpsv(LPSP_file,Options,[]). golpsv(LPSP_file,Options,Results) :- concat_atom([LPSP_file,'_.P'],Pfile), syntax2p_file(LPSP_file,Pfile,lps2p,true), gov(Pfile,Options,Results). % for SWISH: gojson(JSON) :- gojson(_File,[],[],JSON,_DFAgraph). gojson(File,Options,Results,JSON,DFAgraph) :- visualization_options(SD), subtract(Options,SD,Options_NoVis), ( go(File,[make_test,swish|Options_NoVis],Results) -> true ; writeln('gojson PROGRAM FAILED')), visual(File,Options,JSON,DFAgraph). visual(File,lps_visualization(Timeline,Sequence),DFAgraph) :- visual(File,[],lps_visualization(Timeline,Sequence),DFAgraph). % visual(+TestResultsFile,+Options,lps_visualization(TimelineJSON,TwoD_JSON)) This MUST be called after go(File,[...make_test,...],...) % Uses test facts still in memory % This needs to be in sync with test/3 calls in interpreter.P % Omits lps_gigantic(...) terms (which by the way usually popup in other Stages, e.g. goals) % works on facts asserted by either Wei (Stage = 'Observations', ...) or rak interpreter (Stage = fluent,event) visual(File,Options,lps_visualization(Timeline,Sequence),DFAgraph) :- % The following two loads can be skipped IF we execute IMMEDIATELY after go(...) % my_load_dyn(File), % we need the meta info about events % load_test_file_for(File), % Collect all % For events and actions times T1 and T2 are the usual; for fluents, they are time of initiation and termination % OFF BY ONE BUG HERE??? findall(t(X,T1-T2,G), ( (lps_test_result_item('Database state:',T1,X) ; lps_test_result_item(fluents,T1,X)), T1 \== 0, % hack to discard irrelevant state information T2 is T1+1, functor(X,F,N), G=F/N % fluent ), FluentTuples), sort(FluentTuples,Sorted), join_fluent_ranges(Sorted,RangedFluents), findall(t(X,T1-T2,G), ( lps_test_result_item(events,TT2,XX), T1 is TT2-1, T1 \== 0, % hack to discard irrelevant initial "transition" ((action_(XX), \+ (observe(Obs,TT2), member(XX,Obs)))-> % XX can be both event and action; consider observe(...) as evidence that XX is an event % notice that external (Prolog) events and web events (thar are also declared as action) are still visualised as actions G=action ;G=event), (XX=lps_meta(happens(X,T1,T2)) -> true ; XX=X, TT2=T2) ), SimpleEvents), findall(t(X,T1-T2,composite), ( member(composites,Options), (lps_test_result_item(composites,T2,happens(X,T1,T2)); lps_test_action_ancestor(X,T1,T2)), nonvar(T2), % discard unfinished events T2>T1+1 % "simple composites" are reported as events; also, discard trivial action ancestors % T1 \== 0, % hack to discard irrelevant initial "transition" ), Composites), append([RangedFluents,SimpleEvents,Composites],Ranged), % all we care for state diagrams; longer composite may trigger postconditions... % but we will not show them, as it would be eird to relate an older than the previous state: append(FluentTuples,SimpleEvents,FluentsEtAl), (member(abstract_numbers,Options)->AN=true; AN=false), (member(non_reflexive,Options)->NR=true; NR=false), dfa_graph(FluentsEtAl,AN,NR,DFAgraph), % Find groups (setof(G, T^X^member(t(X,T,G),Ranged), GroupIds)->true;GroupIds=[]), % allow for empty timelines (member(stacked_fluents,Options)->Stacked=[];Stacked=[subgroupStack:false]), findall(FluentGroup,( % remove subgroupStack:.. to have fluent tuples scattered vertically: make_json_object([id:Gatom,content:Gsignature,order:3|Stacked],FluentGroup), member(F/N,GroupIds), my_term_to_atom(F/N,Gatom), functor(GG,F,N), numbervars(GG), my_term_to_atom(GG,Gsignature) ), FluentGroups), %make_json_object([id:observation,content:'Observations'],ObsGroup), ( member(t(_,_,event),Ranged) -> make_json_object([id:event,order:1,content:'Events'],EventsGroup), G1 = [EventsGroup|FluentGroups] ; G1 = FluentGroups), ( member(t(_,_,action),Ranged) -> make_json_object([id:action,content:'Actions',order:4],ActionsGroup), append(G1,[ActionsGroup],Groups_) ; Groups_=G1), ( member(composite,GroupIds) -> make_json_object([id:composite,content:'Composites',order:2],CompositesGroup), Groups=[CompositesGroup|Groups_] ; Groups=Groups_), % term2json(Groups,GroupsJSON), (has_d_clauses -> % Is there a 2D display specification? collect_display_specs(Ranged,DS), % (member(XX,DS), writeln(XX), fail;true), prepare_display_sequence(DS,DSarray) % , (member(XX,DSarray), writeq(XX), nl, fail;true) ; DSarray=[] ), create_counter(IDs), % Render fluents % Let's find the end of time; since real time was introduced, we can't rely on endTime(_), % so we need to find the last time of all events and fluents findall(T2,member(t(_,_-T2,_),Ranged),Times), sort(Times,SortedTimes), ( append(_,[LastTime],SortedTimes) -> LastRelevantTime is LastTime+1 ; LastRelevantTime = 1), findall(Fluent,( make_json_object([id:ID,content:AtomAbridged,start:T1,end:T2,group:Gatom,title:Tip|Subgroup],Fluent), member(t(X,T1-T2,F/N),Ranged), abridge_fluent(X,AtomAbridged), my_term_to_atom(F/N,Gatom), my_term_to_atom(X,Atom), (T2==LastRelevantTime -> FluentSuffix = [' (end of time)'] ; FluentSuffix = []), concat_atom(['Fluent ',Atom,' initiated at ',T1,'
and terminated at transition to ',T2|FluentSuffix],Tip), (atomic(X)-> Subgroup=[]; arg(1,X,SS),my_term_to_atom(SS,SS_),Subgroup=[subgroup:SS_]), get_increment_counter(IDs,ID) ), Fluents), %... and the rest: findall(Item,( member(t(X,T1-T2,G),Ranged), \+ functor(G,/,2), % not a fluent my_term_to_atom(G,Gatom), my_term_to_atom(X,Atom), T2_ is T2+1, (G==observation -> S='color:Peru', More=[type:point,start:T2] ; G==action -> S='color:green', More=[type:point,start:T2] ; G==composite -> S='', More=[className:compositeEvent,start:T1,end:T2_] ; S='color:#E19735', More=[type:point,start:T2]), make_json_object([id:ID,content:Atom,group:Gatom,style:S,title:Tip|More],Item), my_term_to_atom(happens(X,T1,T2),Tip), get_increment_counter(IDs,ID) ), Others), append(Fluents,Others,Items), (beginningOfSimulatedRealTime(RTB) -> simulatedRealTimePerCycle(RTC), make_json_object([begin:RTB,cycle:RTC],BC), SRT = [simulatedRealTime:BC] ; SRT=[]), % render to JSON: make_json_object([groups:Groups,items:Items|SRT],All),term2json(All,Timeline), displayTimeline(File,Timeline), ( DSarray == [] -> Cycles=[] ; make_json_object([cycles:DSarray],Cycles)), term2json(Cycles,Sequence). %%% Lazy access to displayable data at each cycle % collect_display_specs_lazy(+Terms,Ops) For each term t(Literal,Type), where Type is fluent | event | action, % return one dict with an object specification (or a list thereof, for composityes). Actions/events are assumed atomic and happening 'to' the current cycle % Unlike in the eager version, the Javascript side will find differences vs the previous cycle it knows about, and % should generate kill (and possibly update) operations locally in the client collect_display_specs_lazy([T|Terms],[Op|Ops]) :- !, op_for_one_lazy(T,Op), collect_display_specs_lazy(Terms,Ops). collect_display_specs_lazy([],[]). % op_for_one_lazy(+Term,-Operation) op_for_one_lazy(t(X,Type), TheProps) :- % This is accessing d/2 via the interpreter... should be the same as done by the eager version. TODO: cleanup once(interpreter:d(X,Props1)), ensure_all_colons(Props1,Props2), (X==timeless -> enforce_property(Props2,timeless:true,Props3) ; Props2=Props3), (member(id:ID,Props3)->true;(member(PP,Props3), member(id:ID,PP))->true;ID=X), % TODO: move these checks to the syntax checking, before program loading: % Allow only one level of composition: ((member(PP,Props3), is_list(PP), member(P,PP), is_list(P)) -> print_message(error,composites_can_have_one_level_only(PP)), fail ; true), % Forbid composites with non list elements in the top props: (member([_|_],Props3) -> ( (member(P,Props3), \+ is_list(P)) -> print_message(error,composite_objects_cannot_have_top_props(Props3)) ; true ) ; true), enforce_property(Props3,id:ID,Props4), % id may be repeated in composite object parts...but not a problem, % composite objects will be associated to a single id (the first one found) ((Type==event;Type==action) -> enforce_property(Props4,event:true,Props5); Props4=Props5), atomize_values(Props5,Props6), lists_to_dicts(Props6,TheProps). lists_to_dicts(Props,TheProps) :- json_colons_to(Props,-,Props1), pairs_to_dicts(Props1,TheProps). % Assumes well formed props, cf. checks in op_for_one_lazy/2 pairs_to_dicts([SubProps|Props],[SubDict|Dicts]) :- is_list(SubProps), !, dict_pairs(SubDict,_,SubProps), pairs_to_dicts(Props,Dicts). pairs_to_dicts([],[]) :- !. pairs_to_dicts(Props,Dict) :- dict_pairs(Dict,_,Props). % atomize_values(Props,NewProps) replace any remaining terms by their string representation atomize_values([P:L|Props],[P:NL|NewProps]) :- is_list(L), !, atomize_values(L,NL), atomize_values(Props,NewProps). atomize_values([L|Props],[NL|NewProps]) :- is_list(L), !, atomize_values(L,NL), atomize_values(Props,NewProps). atomize_values([P:V|Props],[P:V|NewProps]) :- atomic(V), !, atomize_values(Props,NewProps). atomize_values([P:X|Props],[P:NX|NewProps]) :- !, term_string(X,NX), atomize_values(Props,NewProps). atomize_values([X|Props],[X|NewProps]) :- atomic(X), !, atomize_values(Props,NewProps). atomize_values([X|Props],[NX|NewProps]) :- !, term_string(X,NX), atomize_values(Props,NewProps). atomize_values([],[]). %%% Eager access to displayable data (all cycles at the end); also, common utility predicates % prepare_display_sequence(+DS,-Sequence) % Returns a list of lists, one for cycle 0,... until the last; all cycles have lists, added (empty) if needed prepare_display_sequence([c(First,Op)|Cycles],Sequence) :- prepare_display_sequence([c(First,Op)|Cycles],First,[],Sequence). % prepare_display_sequence(+CycleOps,+CurrentCycle,+CurrentCycleSequence,-Sequence) CycleOps already ordered by cycle prepare_display_sequence([c(T,Op)|Cycles],T,CS,Sequence) :- !, append(CS,[Op],NCS), % preserve order, in case we care later; use a diff list if performance here becomes a concern... prepare_display_sequence(Cycles,T,NCS,Sequence). prepare_display_sequence([c(NextT,Op)|Cycles],T,CS,[CS|Sequence]) :- NextT>T+1, !, % we need some empty cycle(s) sequence(s) NewT is T+1, prepare_display_sequence([c(NextT,Op)|Cycles],NewT,[],Sequence). prepare_display_sequence([c(NextT,Op)|Cycles],T,CS,[CS|Sequence]) :- NextT is T+1, !, prepare_display_sequence([c(NextT,Op)|Cycles],NextT,[],Sequence). prepare_display_sequence([],_,CS,[CS]). % collect_display_specs(+Items,-OpItems) % Each item is a t(Literal,T1-T2,Group). Each OpItem is a c(Cycle,Op); % sorted by ID, start cycle; end cycle irrelevant because there are no sobrepositions) % This is roughly O(N*N), can be speeded up if necessary by using a dictionary of ID -> Ops collect_display_specs(Items,TheOpItems_) :- find_max_time(Items,0,MaxTime), prepare_item_ids([t(timeless,0-MaxTime,dummy)|Items],WithIds), sort(WithIds,WithIds2), % ordered list of ID-di(T1-T2,X,G) ( ( member(ID-di(T1-T2,X,G),WithIds2), (member(ID-di(T1-_,Y,G),WithIds2), Y\=X ; member(ID-di(_-T2,Y,G),WithIds2), Y\=X) ) -> print_error(error,'Repeated display ID'(ID),null), fail ; true), retractall(id_props_cache(_,_,_)), collect_display_specs2(WithIds2,OpItems), ( % check that all items have the mandatory properties (even if still not dereferenced) member(c(T,{create:Props}),OpItems), mandatory_prop_name(P), \+ (member(P:_,Props) ; member(Prop,Props), member(P:_,Prop)), writeln(missing-P-c(T,{create:Props})), fail ; true ), % at this point id_props_cache may contain props with value id(..) prepare_display_specs(OpItems,TheOpItems), (ground(TheOpItems)->true; print_error(error, 'Failed to prepare ground display Ops',null), fail), sort(TheOpItems,TheOpItems_). find_max_time([t(_,_-T2,_)|Items],Max,MaxTime) :- !, (T2>Max->NewMax=T2;NewMax=Max), find_max_time(Items,NewMax,MaxTime). find_max_time([],Max,Max). % temporary cache to support dereferencing ids :- thread_local(id_props_cache/3). % ID, Cycle, Props list % collect_display_specs2(+Items,-NewItems) % DB is a list of prop:[Min,Max], e.g. [x:[10,200],y:[+99999,-99999]] 99999 meaning "infinity" or "unknown" collect_display_specs2([],[]) :- !. collect_display_specs2([ID-DI|Items],OpItems) :- collect_display_specs_for([ID-DI|Items],ID, create, ItemsLeft, OpItems1), collect_display_specs2(ItemsLeft,OpItems2), append(OpItems1,OpItems2,OpItems). % all objects must have these: mandatory_prop_name(type). % collect_display_specs_for(+Items,+ID, +Mode(create/update(LastProps)), -ItemsLeft, -OpItemsForId) % Collect display specs for an ID % Each OpItem is c(Cycle,Op), where Op is {kill:ID} or {create:Props} or {update:ID, oldProps:OldProps, newProps:Props} collect_display_specs_for([ID-di(T1-T2,X,_G)|Items],ID, Mode, Items, [c(T1,Op), c(T2,{kill:ID})]) :- (Items=[] ; Items = [OtherID-_|_], ID \== OtherID), !, % last item for this ID op_for_one_enter(ID,T1,X,Mode,_,Op). collect_display_specs_for([ID-di(T1-T2,X,_G), ID-di(T2-T3,XX,GG)|Items],ID, Mode, ItemsLeft, [c(T1,Op)|Ops]) :- \+ d_event(X), !, % same ID, contiguous (EXCEPT for events, which are assumed simple and never spanning cycles) % lacking this condition would cause identical events in consecutive cycles to be considered permament op_for_one_enter(ID,T1,X,Mode,Props,Op), collect_display_specs_for([ID-di(T2-T3,XX,GG)|Items],ID,update(Props),ItemsLeft,Ops). collect_display_specs_for([ID-di(T1-T2,X,_G), ID-di(T3-T4,XX,GG)|Items],ID, Mode, ItemsLeft, [c(T1,Op1),c(T2,{kill:ID})|Ops]) :- !, % same ID, noncontiguous op_for_one_enter(ID,T1,X,Mode,_Props,Op1), collect_display_specs_for([ID-di(T3-T4,XX,GG)|Items],ID,create,ItemsLeft,Ops). collect_display_specs_for([ID_-DI_|Items],ID,_Mode,[ID_-DI_|ItemsLeft],Ops) :- !, % skip item for different ID ID_ \== ID, % just checking... collect_display_specs_for(Items,ID,create,ItemsLeft,Ops). % collect_display_specs_for([],_ID,_Mode,[],[]). % op_for_one_enter(+ID,+Cycle,+FluentOrEvent,+Mode,-Props,-Operation) op_for_one_enter(ID,T,X,Mode,Props,Op) :- once(d(X,Props_)), % TODO later: delay to evaluate id_props! %( (member(Prop1,Props_), member(id:ID1,Prop1), member(Prop1,Props_), member(id:ID2,Prop1), ID1 \= ID2) -> % print_error(error,'Different display ID in display object parts'(ID1),null), fail % ; true), ensure_all_colons(Props_,Props), assert(id_props_cache(ID,T,Props)), ( Mode = update(OldProps) -> Op = ({(update):ID, oldProps:OldProps, newProps:Props}) ; Mode=create, (d_event(X) /* event or action */ -> enforce_property(Props,event:true,TheProps_); TheProps_=Props), enforce_property(TheProps_,id:ID,TheProps), Op= {create:TheProps} ). % Considers composite objects, in which case P must be present in ALL object parts % Notice that property values may be different % enforce_property(+Props,+P:Value,-NewProps). enforce_property([],_P,[]) :- !. enforce_property(Props,P:Value,NewProps) :- \+ member([_|_],Props), !, % simple object (member(P:_,Props) -> Props=NewProps ; NewProps=[P:Value|Props]). enforce_property([Props1|Props],P,[NewProp1|NewProps]) :- is_list(Props1), enforce_property(Props1,P,NewProp1), enforce_property(Props,P,NewProps). % allow boolean properties to be abbreviated, e.g. 'sendToBack', by completing them ensure_all_colons([P|Props],[P|NewProps]) :- (var(P);P=(_:_)), !, ensure_all_colons(Props,NewProps). ensure_all_colons([P|Props],[NewP|NewProps]) :- is_list(P), !, ensure_all_colons(P,NewP), ensure_all_colons(Props,NewProps). ensure_all_colons([P|Props],[P:true|NewProps]) :- ensure_all_colons(Props,NewProps). ensure_all_colons([],[]). % prepare_item_ids(+Tuples,-Items) Each Item is ID-di(T1-T2,FluentOrEvent,Group), easy to order by id and time range; % ID is extracted from props, or generated - right now it's the whole term, % for more compactness it could be an int key into a term dictionary instead prepare_item_ids([t(X,T1-T2,G)|Items], [ID-di(T1-T2,X,G)|WithIds]) :- d(X,Props), !, % displayable item % TODO later: DELAY previous to support id_props?? (member(id:ID,Props)->true;((member(PP,Props), member(id:ID,PP))->true;ID=X)), prepare_item_ids(Items,WithIds). prepare_item_ids([_|Items], WithIds) :- !, prepare_item_ids(Items,WithIds). prepare_item_ids([],[]). % Refine update args, resolve id(ID) props prepare_display_specs([c(T,Op)|OpItems],[c(T,NewOp)|NewOpItems]) :- !, prepare_one_display_spec(Op,T,NewOp), prepare_display_specs(OpItems,NewOpItems). prepare_display_specs([],[]). prepare_one_display_spec({kill:ID},_T,OpJSON) :- !, flatten_prop(id:ID,id:NewID), make_json_object([kill:NewID],OpJSON). prepare_one_display_spec({create:Props},T,OpJSON) :- !, resolve_id_props(Props,T,NewProps), make_json_list(NewProps,NewPropsJSON), make_json_object([create:NewPropsJSON],OpJSON). % list to {...} prepare_one_display_spec({(update):ID, oldProps:_OldProps, newProps:NewProps},T,OpsJSON) :- !, resolve_id_props(NewProps,T,NewProps_), % Unfortunately PaperJS does not ease minimal updating wrt props used at creation time; considering % this AND the convenience to represent composite object as mere arrays of props objects, commenting this for now: % resolve_id_props(OldProps,T,OldProps_), % Keep only properties whose values vary: % intersection(OldProps_,NewProps_,Common), % subtract(OldProps_,Common,TheOldProps), subtract(NewProps_,Common,TheNewProps), % now merge into a more convenient format: % make_json_object(TheOldProps,OldJSON), make_json_object(TheNewProps,NewJSON), make_json_list(NewProps_,NewJSON), flatten_prop(id:ID,id:NewID), make_json_object([(update):NewID, oldProps:[], newProps:NewJSON ],OpsJSON). % Makes an array if some element is a list, otherwise makes an object make_json_list(Props,JSON) :- member([_|_],Props), !, make_json_list2(Props,JSON). make_json_list(Props,JSON) :- make_json_object(Props,JSON). make_json_list2([P|Props],[J|JSON]) :- !, make_json_object(P,J), make_json_list2(Props,JSON). make_json_list2([],[]). % resolve_id_props(+Props,+Time,-ResolvedProps) replaces "referential" properties, that lookup their value in other object % This is actually not very useful in its current form, because values tend to be compound, e.g. rectangles; % Perhaps a path based notation might be more useful, e.g. from:[FromPerson.from[0],20] ...??? resolve_id_props([P:id(ID)|Props],T,[P:Value|NewProps]) :- !, ( (get_id_props(ID,T,IDProps), member(P:Value,IDProps), Value \= id(_)) -> true ; writeln(T/P:id(ID)), print_error(error,'Could not find display property value for'(T/P:id(ID)),null), fail), resolve_id_props(Props,T,NewProps). resolve_id_props([P:id(ID,OtherP)|Props],T,[P:Value|NewProps]) :- !, ( (get_id_props(ID,T,IDProps), member(OtherP:Value,IDProps), Value \= id(_)) -> true ; print_error(error,'Could not find display property value for'(T/P:id(ID,OtherP)),null), fail), resolve_id_props(Props,T,NewProps). resolve_id_props([Prop|Props],T,[FProp|NewProps]) :- is_list(Prop), !, resolve_id_props(Prop,T,FProp), resolve_id_props(Props,T,NewProps). resolve_id_props([Prop|Props],T,[FProp|NewProps]) :- !, flatten_prop(Prop,FProp), resolve_id_props(Props,T,NewProps). resolve_id_props([],_T,[]). % Make sure only atoms get to the JS side; this is a partial check, as points and sizes and possibly other future structured % terms are not checked flatten_prop(id:ID,id:NewID) :- (atomic(ID),ID=NewID ; my_term_to_atom(ID,NewID)), !. flatten_prop(label:ID,label:NewID) :- (atomic(ID),ID=NewID ; my_term_to_atom(ID,NewID)), !. flatten_prop(tip:ID,tip:NewID) :- (atomic(ID),ID=NewID ; my_term_to_atom(ID,NewID)), !. flatten_prop(P,P). % get props for ID at T, or most recent past get_id_props(ID,T,Props) :- id_props_cache(ID,T,Props), !. get_id_props(ID,T,Props) :- T>0, Past is T-1, get_id_props(ID,Past,Props). abridge_fluent(X,X) :- atomic(X), !. abridge_fluent(X,AtomAbridged) :- X=..[_|Args], abridge_fluent_args(Args,Atoms), concat_atom(Atoms,AtomAbridged). abridge_fluent_args([A1,A2|An],[Atom1, ','| Atoms]) :- !, my_term_to_atom(A1,Atom1), abridge_fluent_args([A2|An],Atoms). abridge_fluent_args([A],[Atom]) :- my_term_to_atom(A,Atom). nicer_vars(T) :- nicer_vars(T,65,_). nicer_vars(T,V1,Vn) :- var(T), !, atom_codes(T,[V1]), Vn is V1+1. nicer_vars([T1|Tn],V1,Vn) :- !, nicer_vars(T1,V1,V2), nicer_vars(Tn,V2,Vn). nicer_vars(T,V,V) :- atomic(T), !. nicer_vars(T,V1,Vn) :- T=..L, nicer_vars(L,V1,Vn). replace_backslashes(P,NP) :- atom(P), str_replace(P,'\\','/',NP). str_replace(A, X, Y, B) :- atom_chars(A, Ac), atom_chars(X, Xc), atom_chars(Y, Yc), str_replace1(Ac, Xc, Yc, Bc), concat_atom(Bc, B). str_replace1([], _, _, []). str_replace1(Ac, Xc, Yc, [B|C]) :- (append(Xc, Zc, Ac) -> B = Yc, str_replace1(Zc, Xc, Yc, C) ; Ac = [A|Acs], B = A, str_replace1(Acs, Xc, Yc, C) ). % join_fluent_ranges(Sorted,Ranged) merges contiguous fluent states join_fluent_ranges([t(X,T,G)|S],[t(X,T,G)|R]) :- \+ functor(G,/,2), !, % not a fluent join_fluent_ranges(S,R). join_fluent_ranges([t(X,T1-T2,G),t(XX,T2-T3,G)|S],R) :- variant(X,XX), !, join_fluent_ranges([t(X,T1-T3,G)|S],R). join_fluent_ranges([T|S],[T|R]) :- !, join_fluent_ranges(S,R). join_fluent_ranges([],[]). % dfa_graph(+FluentsEtAl,+AbstractNumbers,+NonReflexive,GraphVizGraph) list of t(X,T1-T2,G) , G=action or event for events % AbstractNumbers==true abstracts numbers into 'n', leading to smaller diagrams % NonReflexive==true removes reflexive edges (event/actions which do not change state) % Generates a GraphViz specification for a state transitions diagram dfa_graph(L,AbstractNumbers,NonReflexive,dot(digraph(Statements))) :- must_be(boolean,AbstractNumbers), must_be(boolean,NonReflexive), setof(T1-State, setof(Fl_,T2^G^Fl^( member(t(Fl,T1-T2,G),L), G\==event, G\==action, (AbstractNumbers==true->abstract_numbers(Fl,Fl_);Fl=Fl_) ),State) , StateHistory), StateHistory=[First-_|_], append(_,[Last-_],StateHistory), findall(T-[],(between(First,Last,T),\+ member(T-_,StateHistory)),EmptyStates), append(StateHistory,EmptyStates,StateHistory_), (setof(State-Times, setof(T,member(T-State,StateHistory_),Times), AbstractStates) -> true ; AbstractStates=[]), % our nodes findall(node(Times,[label=State_,fillcolor='#D7DCF5',style=filled,color=Line]), ( member(State-Times,AbstractStates), findall(Fl_,(member(Fl,State),term_string(Fl,Fl_)),Tuples), atomic_list_concat(Tuples,"\n",State_), % format(string(State__),"~s",[State_]), (member(First,Times)->Line=black;Line='#D7DCF5') % indicate the initial state ), Nodes), ( setof(edge((Times1->Times2),[label=Ev_,color=Color]), G^T1^T2^State1^State2^Ev^( member(t(Ev,T1-T2,G),L), (G==event, Color='#E19735'; G==action, Color=forestgreen), member(State1-Times1, AbstractStates), once(member(T1,Times1)), member(State2-Times2, AbstractStates), once(member(T2,Times2)), (NonReflexive==true->Times1\=Times2;true), (AbstractNumbers==true->abstract_numbers(Ev,Ev_);Ev=Ev_) ),Edges) -> true ; Edges=[]), !, append(Nodes,Edges,Statements). % missing either nodes or edges, no graph to show: dfa_graph(_L,_,_,dot(digraph([node(root,[color=red,label='A state transitions graph needs fluent states and events!'])]))). visualization_options([abstract_numbers,composites,non_reflexive,stacked_fluents]).