User:TomT0m/classification.js
Jump to navigation
Jump to search
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/*jshint esversion: 8, esnext:false */
(function(mw, $, wb) {
mw.hook('wikibase.entityPage.entityView.rendered').add(
async function () {
"use strict";
let namespace = mw.config.get("wgNamespaceNumber");
if (namespace !== mw.config.get("wgNamespaceIds").item){
return;
}
var api = new mw.Api();
var lang = mw.config.get( 'wgUserLanguage' );
var gadget = "[[User:TomT0m/classification.js]]";
var item_label = $( ".wikibase-title-label" ).text();
// colors founds from https://davidmathlogic.com/colorblind/#%23332288-%23117733-%2344AA99-%2388CCEE-%23DDCC77-%23CC6677-%23AA4499-%23882255
const violations_colors = {
unique : ["E69F00", "0072B2", "56B4E9"],
several : "D55E00",
top : "009E73",
bottom : "CC79A7",
};
// Variable to store the tooltranslate object if it is loadable
let tool_translate;
// the object accessible anywhere to get the massages translations.
var messages ;
// factory for the message object
let load_messages = function () {
// default translations if tooltranslate could not load for some reason
var translations = {
en: {
'title': 'Classification gadget'
/*'title': 'Classification gadget'*/,
'new_window': "(show in a new window)"
/* "(show in a new window)" */,
'paths_to_parent' : "Paths to some parent class"
/* "Paths to some parent class" */,
'path_to_parent' : "Path from this class to $parent"
/* param : $parent (a class label)*/,
'close_view' : "close view"
/* close an iframe containing a wdqs query visualisation*/,
'embedded_superclass_tree' : "embedded superclass tree "
/*:"embedded superclass tree "*/,
'embedded_classes_of_instance_tree' : "Embedded tree of the classes of this instance"
/*:"Embedded tree of the classes of this instance"*/,
'weird_to_discuss' :"(This seems weird or incorrect, discuss it)"
/* :"(This seems weird or incorrect, discuss it)" */,
'and' : " and "
/*" and "*/,
'instance_explanation' :
"$item_label is an explicit instance of $explicit_classes "
+ " and it is/they are linked to $superclass this class by a subclass chain."/*
"$item_label is an explicit instance of $explicit_classes "
+ " and it is/they are linked to $superclass this class by a subclass chain."
parameters :
$explicit classes : a list of classes our item is an explicit instance of
$superclass : the superclass to show the path to
$item_label : the instance (main item) label
*/,
'show_samplesize' : "(showing $num_sample / $total_available results)"
/*"(showing $num_sample / $total_available results"
parameters :
$num_sample : number of class instances to show
$total_available : total number of classes the item is an instance of
*/,
'item_is_also_instanceof' : "$item_label is also a(n) …"
/*"$item_label is also a(n) …"
parameter:
$item_label : the label of the main item
*/,
'more': 'more',
'show_in_wdqs': "show class tree in wdqs (bigger)"
/* "show class tree in wdqs (bigger)" */,
'choose_parent' : 'Choose some parent class'
/* 'Choose some parent class' */,
'this_class' : 'Current class',
/* Current class */
'loops' : 'There are loop(s) in the class tree !',
/* There are loop(s) in the class tree ! */
'activate_gadget' : 'Activate classification gadget',
/* Activate classification gadget */
'deactivate_gadget' : "Don't show classification from next time",
/* Don't show classification from next time */
'disjointness_violation' : `Disjointness violation !`,
/* `Disjointness violation !` */
'instance_of_several_classes' : `This item is an instance of two or more classes supposed to be disjoint, there are several ways to fix it <br> <ul><li>One is to remove one of the class of its <instance of> statement</li><li>one other is to fix the class tree. One of the class in the class tree is a subclass of conflicting classes, you are hinted which one(s) below</li></ul> You can expand the class tree for help, classes that are subclasses of both conflicting classes are colored <span style="background-color:#992211;">this way</span>.`,
classes_are_disjoint :
"On class $class it is stated that the following classes are disjoint, and this is an instance of them !",
/* "On class $class it is stated that the following classes, and this is an instance of them !" */
superclasses_are_disjoint : "On class $class it is stated that the following classes, and this is a subclass of them !",
/* "On class $class it is stated that the following classes, and this is a subclass of them !" */
probable_culprit : "=> Probable culprit(s), highest classes in the tree to be subclasses of conflicting classes (if any) :",
/* " => Probable culprit(s), highest classes in the tree to be subclasses of conflicting classes (if any) :", */
see_conflict_tree : "See this disjointness conflict in its class tree … ",
/* "See this instance disjointness conflict instance in its class tree … " */
subclass_of_several_classes : "This class is a subclass of two or more classes that are supposed to be disjoint. Fixing this involves removing a \"subclass of\" statement either on this class or in one of its superclass. The informations on this box give a candidate (super)class for this. You can visualize the class tree by expanding the detail arrow below, a superclass tree colored according to which class is a subclass of which conflicting class.",
parent_class_loop_title : "Loop in the parent class tree !",
help_loop_removal : `here is a loop in the class tree. To solve this you need to either remove one 'subclass of
' statement on this path, or maybe merge two items if they are both subclass of each other because they are identical.`,
subclass_of_both : `subclass of both`,
subclass_of_c : `subclass of <i>$class</i>`,
disjoint_union_class : `Top class : <i>$class</i>`,
this_class_ : `This class : <i>$class</i>`,
this_instance : `This instance : <i>$instance</i>`
},
};
let sanitizeHTML = function ( s ) {
return s.replace(/<\s*script/i,'') ;
};
// the message object to return
let ret = {};
// load the english static defaults
mw.language.getFallbackLanguageChain().reverse().forEach(
function(lang) {
if (translations.hasOwnProperty(lang)) {
$.extend(ret, translations[lang]);
}
}
);
if (tool_translate) {
// if tool_translate is loaded
ret.$ = (key) => {
let val = tool_translate.t(key);
if (val)
return sanitizeHTML(val);
else
// if tooltranslate fails in the dynamic messages for some reason, fallback to our in script messages
return sanitizeHTML(messages[key]);
};
} else {
// if tool translate is not loaded, fallback to the static messages in script version
ret.$ = (key) => sanitizeHTML(messages[key]);
}
// messages is a global object
messages = ret;
};
/* CSS
*/
function instance_details_style(){
return `
details {
border: 1px solid #aaa;
border-radius: 4px;
padding: .5em .5em 0;
}
summary {
font-weight: bold;
margin: -.5em -.5em 0;
padding: .5em;
}
details[open] {
padding: .5em;
}
details[open] summary {
margin-bottom: .5em;
}
.classification-gadget-property-section {
background-color:#F1F1F1;
border-bottom:1px solid;
border-left:1px solid;
border-right:1px solid;
border-bottom-left-radius : 1em;
border-bottom-right-radius : 1em;
padding-bottom: 1em;
}
.classification-gadget-report-link {
font-size:smaller;
}
.classification-gadget-embedded-tree{
width: 100%;
height: 70vh;
border: none;
}
.classification-gadget-constraint-violation {
border: 1px double #a12290;
}
.align-right {
float:right;
}
ul.classification-gadget-horizontal-list li {
display:inline;
padding : 1rem .5rem;
}
.classification-gadget-violation-colors-unique-1 {
background-color : #${violations_colors.unique[0]};
}
.classification-gadget-violation-colors-unique-2 {
background-color : #${violations_colors.unique[1]};
}
.classification-gadget-violation-colors-unique-3 {
background-color : #${violations_colors.unique[2]};
}
.classification-gadget-violation-colors-several {
background-color : #${violations_colors.several};
}
.classification-gadget-violation-colors-top {
background-color : #${violations_colors.top};
}
.classification-gadget-violation-colors-bottom {
background-color : #${violations_colors.bottom};
}
}
`;
}
async function makeSPARQLQuery( endpointUrl, sparqlQuery, doneCallback ) {
function fetchRetry(url, options = {}) {
return fetch(url, options).then(res => {
// handle rate limiting
if (res.status === 429) {
const retryAfter = res.headers.get("retry-after");
return sleep(+retryAfter * 1000).then(fetchRetry);
}
return res;
});
}
var settings = {
headers: { Accept: 'application/sparql-results+json' },
//method:"POST",
//mode: "cors",
};
const params = { query: sparqlQuery };
let answer = await fetchRetry( endpointUrl + "?" + new URLSearchParams( params ), settings );
// .then( doneCallback );
let json = await answer.json();
if (doneCallback)
return doneCallback(json);
return json;
}
var endpointUrl = 'https://query.wikidata.org/sparql';
// adding the gadget CSS classes to the page
$(document.head).append($("<style>",{
text: instance_details_style()
}));
// Utility function, create a container to add the gadget UI below the statements
// of some given property in Wikidata UI
function create_property_container(propertyid, content){
var property_section = $("#" + propertyid);
var id = "classification-gadget-" + propertyid + "-container";
var new_property_section = $("<div>",{
id:id,
class:"wikibase-statementgroupview",
});
property_section.wrap(new_property_section).append(content);
$("#"+id).append(content);
return id;
}
/* SPARQL utilities and queries */
function query_label_service_language(){
return `bd:serviceParam wikibase:language "${mw.language.getFallbackLanguageChain().join(",")},[AUTO_LANGUAGE],en"`;
}
function query_label_service(){
return `SERVICE wikibase:label { ${query_label_service_language()} . }\n`;
}
// a query to draw the class path between two classes
function query_path(bottom, top) {
return "#defaultView:Graph\n"+
"#query_path\n"+
"select ?topSubclass ?parent ?parentLabel ?topSubclassLabel\n"+
"with {\n"+
" select ?bottomSuperclass {\n"+
" bind ( wd:" + bottom + " as ?bottom) .\n"+
" ?bottom wdt:P279* ?bottomSuperclass. \n"+
" }\n"+
"} as %superclasses\n"+
"\n"+
"with {\n"+
" select (?bottomSuperclass as ?topSubclass) ?top {\n"+
" include %superclasses\n"+
" bind ( wd:" + top + " as ?top) .\n"+
" ?bottomSuperclass wdt:P279* ?top.\n"+
" }\n"+
"} as %subclasses\n"+
"{\n"+
" include %subclasses .\n"+
" ?topSubclass wdt:P279 ?parent filter exists { { select (?topSubclass as ?class ){ include %subclasses} } filter (?class = ?parent) }\n"+
" " + query_label_service() +
"}\n";
}
// a query to draw a path from an instance to one of its member class
function query_inst_path(instance, top) {
return "#defaultView:Graph\n"+
"#query_inst_path\n"+
"select ?topSubclass ?parent ?parentLabel ?topSubclassLabel\n"+
"with {\n"+
" select ?bottomSuperclass {\n"+
" bind ( wd:" + instance + " as ?bottom) .\n"+
" ?bottom wdt:P31/wdt:P279* ?bottomSuperclass. \n"+
" }\n"+
"} as %superclasses\n"+
"\n"+
"with {\n"+
" select (?bottomSuperclass as ?topSubclass) ?top {\n"+
" include %superclasses\n"+
" bind ( <" + top + "> as ?top) .\n"+
" ?bottomSuperclass wdt:P279* ?top.\n"+
" }\n"+
"} as %subclasses\n"+
"{\n"+
" include %subclasses .\n"+
" ?topSubclass wdt:P279 ?parent filter exists { { select (?topSubclass as ?class ){ include %subclasses} } filter (?class = ?parent) }\n"+
" " + query_label_service() +
"}\n";
}
// find all superclasses of some class
function query_superclass_tree(bottom) {
return `#defaultView:Graph
#query_superclass_tree
select ?bottomSuperclass ?rgb ?parent ?bottomSuperclassLabel ?parentLabel{
bind ( wd:${bottom} as ?bottom) .
?bottom wdt:P279* ?bottomSuperclass.
?bottomSuperclass wdt:P279 ?parent .
OPTIONAL {
VALUES (?bottomSuperclass ?rgb) { (wd:${bottom} '${violations_colors.bottom}') }
}
${query_label_service()}
}`;
}
function query_instance_superclass_tree(bottomInstance) {
return "#defaultView:Graph\n"+
"SELECT ?superClass ?rgb ?parent ?superClassLabel ?parentLabel\n"+
"WHERE {\n"+
" {\n"+
" bind ('FF4444' as ?rgb).\n"+
" bind (wd:" + bottomInstance + " as ?superClass).\n"+
" ?superClass wdt:P31 ?parent.\n"+
" } UNION {\n"+
" bind ('FFFFFF' as ?rgb).\n"+
" wd:" + bottomInstance + " wdt:P31 ?class.\n"+
" ?class wdt:P279* ?superClass.\n"+
" ?superClass wdt:P279 ?parent.\n"+
" }\n"+
" " + query_label_service() +
"}\n";
}
// find all classes an instance is instance of
function query_other_classes(instance) {
return "#defaultView:Graph\n"+
"select distinct ?class ?classLabel {\n"+
" bind ( wd:" + instance + " as ?instance) .\n"+
" ?instance wdt:P31/wdt:P279* ?class. \n"+
" " + query_label_service() +
"}\n";
}
// find all classes an instance is instance of
function query_superclasses(class_) {
return `#query_superclasses
select distinct ?class ?classLabel {
wd:${class_} wdt:P279* ?class.
${query_label_service()}
}`;
}
// find all classes in the path between an instance an a class it is an instance of
function query_direct_classes_subclasses_of_superclass(instance, superclass) {
return "select ?class ?classLabel\n" +
"#query_direct_classes_subclasses_of_superclass\n" +
"with {\n" +
" select ?bottomSuperclass {\n" +
" bind ( wd:" + instance + " as ?bottom) .\n" +
" ?bottom wdt:P31/wdt:P279* ?bottomSuperclass. \n" +
" }\n" +
"} as %superclasses\n" +
"\n" +
"with {\n" +
" select (?bottomSuperclass as ?class) ?top {\n" +
" include %superclasses\n" +
" bind ( <" + superclass + "> as ?top) .\n" +
" ?bottomSuperclass wdt:P279* ?top.\n" +
" }\n" +
"} as %subclasses\n" +
"{\n" +
" include %subclasses .\n" +
" wd:" + instance + " wdt:P31 ?class.\n" +
" \n" +
" " + query_label_service() +
"}";
}
function query_disjoint_statements_base_query(select_baseclasses_sparql){
return `#query_disjoint_statements_base_query
select ?class ?st
(count(distinct ?classes) as ?count)
(group_concat(distinct ?classes;separator=",") as ?class_set)
{
?class p:P2738 ?st .
?st pq:P642|pq:P11260 ?classes ;
a wikibase:BestRank .
?classes wdt:P279* ?class .
${select_baseclasses_sparql}
?baseclass wdt:P279* ?classes .
} group by ?class ?st
having (?count > 1)
limit 3`;
}
function query_disjoint_statements_violated_by_instance(instance){
return query_disjoint_statements_base_query(`wd:${instance} wdt:P31 ?baseclass .\n `);
}
function query_disjoint_statements_violated_by_superclasses(baseclass){
return query_disjoint_statements_base_query(`wd:${baseclass} wdt:P279 ?baseclass .\n `);
}
function query_conflicting_class_tree_visualisation(base, link_prop, classes_from_base_query,top, violated_1, violated_2){
return `#query_conflicting_class_tree_visualisation
#defaultView:Graph
prefix violated_top: <${top.class.value}>
prefix violated_1: <${violated_1.class.value}>
prefix violated_2: <${violated_2.class.value}>
prefix base: <http://www.wikidata.org/entity/${base}>
select ?class ?rgb ?classLabel ?img
?parent
?edgeLabel
?size
with {
select distinct ?class
(if(count(distinct ?rgb_)>=2,"992211",sample(?rgb_)) as ?rgb) # color selection : if instance of both classes, a different color
(concat(?classLabel_, " : ",group_concat(distinct ?violatedLabel_;separator=", ")) as ?classLabel)
(group_concat(distinct ?violatedLabel_;separator=", ") as ?edgeLabel)
{
base: wdt:${link_prop} ?baseclass.
?baseclass wdt:P279* ?class .
?class wdt:P279* ?violated .
values (?violated ?rgb_) {
(violated_1: "${violations_colors.unique[0]}")
(violated_2: "${violations_colors.unique[1]}")
}
SERVICE wikibase:label {
?class rdfs:label ?classLabel_ .
?violated rdfs:label ?violatedLabel_ .
${query_label_service_language()} .
}
} group by ?class ?classLabel_
} as %edges
{
{
include %edges .
hint:Query hint:optimizer "None".
{select (?class as ?parent) { include %edges} }
?class wdt:P279 ?parent .
} union {
bind (base: as ?class)
?class wdt:${link_prop} ?parent .
bind ("${violations_colors.bottom}" as ?rgb) .
bind (12 as ?size) .
bind (wd:${link_prop} as ?prop) .
service wikibase:label {
${query_label_service_language()} .
?prop rdfs:label ?edgeLabel .
?class rdfs:label ?classLabel
}
} union {
values (?class ?rgb ?classLabel) {
(violated_1: "${violations_colors.unique[0]}" "${violated_1.classLabel.value}")
(violated_2: "${violations_colors.unique[1]}" "${violated_2.classLabel.value}")
}
# link the disjoint classes, helpful ?
values (?class ?parent ?edgeLabel) {
(violated_1: violated_top: "")
(violated_2: violated_top: "")
# (violated_1: violated_2: "disjoint with")
# (violated_2: violated_1: "disjoint with")
}
} union {
values (?class ?rgb ?size ?classLabel){
(violated_top: "${violations_colors.top}" "13"^^xsd:number "${top.classLabel.value}")
}
}
}`;
}
function query_highest_subclass_of_both_conflicting_superclass_of(bottomclass, conf1, conf2){
return query_upper_subclass_of_both_conflicting_class_base(
`wd:${bottomclass} wdt:P279* ?class .`,
conf1,
conf2);
}
function query_highest_subclass_of_both_conflicting_class_of(instance, conf1, conf2){
return query_upper_subclass_of_both_conflicting_class_base(
`<http://www.wikidata.org/entity/${instance}> wdt:P31/wdt:P279* ?class .`,
conf1,
conf2);
}
function query_upper_subclass_of_both_conflicting_class_base(base_subclasses_subquery, conf1, conf2){
// this query won’t work if there is a loop involved in the path, but …
// … whatever. Just fix the loop firt /o\
return `#query_upper_subclass_of_both_conflicting_class_base
prefix violated_1: <${conf1}>
prefix violated_2: <${conf2}>
select ?class
with {
select ?class
{
${base_subclasses_subquery}
?class wdt:P279+ violated_1: . hint:Prior hint:gearing "forward" .
?class wdt:P279+ violated_2: . hint:Prior hint:gearing "forward"
}
} as %classes
{
include %classes .
minus {
{ select (?class as ?parent) { include %classes } }
?class wdt:P279 ?parent
}
}`;
}
// Utility : create a frame that shows embedded results of
// a query by wdqs, a graph for example
function create_embedded_frame(query, additional={}){
var iframe = $("<iframe>",
$.extend(
{
class:"classification-gadget-embedded-tree",
src : "https://query.wikidata.org/embed.html#" + encodeURIComponent(query)
},
additional
)
);
return iframe;
}
function create_constraint_violation_box(title, message, id) {
return $("<div>")
.append($("<h4>").append(title))
.addClass("classification-gadget-constraint-violation")
.append(new OO.ui.PopupButtonWidget( {
$overlay: true,
label: '',
indicator: 'down',
icon : "non-mandatory-constraint-violation",
infusable:true,
popup: {
$content: $("<div>").append(message),
padded: false,
//align: align,
width: 400,
},
//class:["mw-portlet-lang"],
id : id
} ).$element.addClass("align-right"));
}
function make_list_item(elem){
return $("<li>",{append:elem});
}
function create_full_class_tree(tree_query, message, caption) {
const wdqs_fullclasstree_link = $("<a>",{
href:"https://query.wikidata.org/embed.html#" + encodeURIComponent(tree_query),
text: messages.$("show_in_wdqs") /* "show class tree in wdqs (bigger)" */
}
),
full_class_tree = $(
"<details>",
{
class:"classification-gadget-full-superclass-tree",
append:$("<summary>", {
append:$("<span>", {
append:[message," "]
}).append(wdqs_fullclasstree_link)
})
}
);
full_class_tree.on("toggle", function() {
if (!full_class_tree.find(".classification-gadget-embedded-tree").length){
let wrapped_caption;
if(caption !== undefined){
wrapped_caption = $("<figcaption>").append(caption);
}
full_class_tree.append(
$("<figure>")
//.addClass("classification-gadget-embedded-tree")
.append([create_embedded_frame(tree_query),wrapped_caption])
);
}
});
return full_class_tree;
}
//finds the Qid String from a entity url
function urlToQid(url){
return url.replace(/^.*entity\/(.*)$/,"$1");
}
//// Utility function for computing and rendering loops constraints.
// converts a lists of result mapping to a dictionary
// output keys : items uris ("bottomSuperclass" variable value)
// output values : wdqs binding object
function hashgraphFromResults(results){
var map = {};
for(var idx in results){
var binding = results[idx];
if (map[binding.bottomSuperclass.value]) {
map[binding.bottomSuperclass.value].push(binding);
} else {
map[binding.bottomSuperclass.value] = [binding];
}
}
return map;
}
// computes a path to an item value from
// in a graph as computed by the "hashgraphFromResults" function
function pathTo(graph, bottomclass, aim, explored){
for (var parentidx in graph[bottomclass]){
var parentbinding = graph[bottomclass][parentidx] ;
var parent = parentbinding.parent.value;
if ( parent === aim ) {
return [parentbinding];
} else if (! explored.includes(parent)) {
var explored_bis = [...explored];
explored_bis.push(bottomclass) ;// do we need to make a fresh explored object at this point ?
var search = pathTo(graph, parent, aim, explored_bis );
if (search) {
search.push(parentbinding);
return search;
}
}
}
}
function render_path(path){
var loop_render = $("<span>");
for (var pidx in path){
loop_render.append(item_span(path[pidx], "parent","parentLabel"));
loop_render.append(" → ");
}
return $("<li>",{
append: $("<div>", {
append: [
"[" + messages.$("this_class") +"] → ",
loop_render,
" ↶ [" + messages.$("this_class") +"]"
]
} )
}
);
}
//////// loops utilities
// loop section generation
async function loops_infos(bottom){
let data = await makeSPARQLQuery(endpointUrl, query_superclass_tree(bottom));
var classes = data.results.bindings;
var graph = hashgraphFromResults(classes);
const parents = classes.filter(result=>result.parent.value).map(result => result.parent.value);
let loops_endpoints = await makeSPARQLQuery(
endpointUrl,
`#loops_endpoints
SELECT distinct ?parent { ?parent wdt:P279 wd:${bottom} . values ?parent { $values } } `
.replace("$values", parents.map(parent=> "wd:" + urlToQid(parent)).join(" ")));
var urlbottom = "http://www.wikidata.org/entity/" + bottom ;
const endpoints = loops_endpoints.results.bindings;
if (endpoints.length === 0)
return ;
var show_class_loops = $("<div>", {
id:"classification-gadget-loops"
});
var rpaths = endpoints
.map( endpoint => endpoint.parent.value )
.map( parent => {
var path = pathTo(graph, urlbottom, parent, []).reverse();
return render_path(path);
});
if (rpaths.length > 0){
show_class_loops.append(
create_constraint_violation_box(
messages.$("parent_class_loop_title"), // "Loop in the parent class tree !"
messages.$("help_loop_removal") // "here is a loop in the class tree, please remove one."
).append(
$("<span>",{
append: [
$("<h4>").append(messages.$("loops")),
$("<ul>").append(rpaths)
]})
)
);
}
return show_class_loops;
}
function show_path_to_superclass_with_selector(selectid, bottom){
// a selector to select a parent class, the class we want to find show path to
var select = $("<input>",{id:selectid}).entityselector( {
url: '/w/api.php',
language: lang
} ) ;
// when a class is selected, we query and show the results of the query
// replacing the view of an old path if any
select.on("change", function() {
//var select = $("#classification-gadget-path-topclass-selector");
var entity = select.data("entityselector").selectedEntity();
var base_class = bottom;
if (! entity ) return ; // invalid entity
var now_entity = entity.id;
var link_to_wdqs = $("<a>",{
href: "https://query.wikidata.org/#" + encodeURIComponent(query_path(bottom, now_entity)),
text: messages.$("new_window")// "(show in a new window)"
}
);
var container = $("<div>", {
append: $("<h5>",{
text:messages.$("path_to_parent").replace("$parent", entity.label) /*"Path from this class to $parent"*/
}
).add(
link_to_wdqs
).add(
create_embedded_frame(query_path(bottom, now_entity))
)
});
var close_link = $("<a>", {
text: messages.$("close_view") /*"close view"*/,
on:{
click: function(){container.remove()}
}
});
container.append(close_link);
select.after(container);
});
return [
$("<h4>", {
text: messages.$("paths_to_parent") /* "Paths to some parent class"*/
}),
$("<label>",{
for:selectid,
text:messages.$("choose_parent") /*"Choose some parent class"*/ }),
select
];
}
// intialize the gadget below the « subclass of » statements
async function init_subclassof(bottom){
var selectid = "classification-gadget-path-topclass-selector";
var class_section = $("#P279");
var classification_infos = $("<div>", {
id:"classification-gadget-classes",
class:"wikibase-statementgroupview classification-gadget-property-section"
});
var cont_id = create_property_container("P279", classification_infos);
register_subsection(classification_infos,() => create_full_class_tree(
query_superclass_tree(bottom),
messages.$("embedded_superclass_tree") /*:"embedded superclass tree "*/
));
register_subsection(classification_infos, () => get_subclass_disjointness_violations_infos(bottom));
register_subsection(classification_infos, () => show_path_to_superclass_with_selector(selectid, bottom));
register_subsection(classification_infos, () => loops_infos(bottom));
}
// utility : function to generate a html label from a
// query result binding (as generated by a call to the query service)
// res : a result object
// name : a string that is the name of a sparql variable
function label_span(res, name){
return $("<span>",{
lang:res[name]["xml:lang"],
append:res[name].value
});
}
// like item_span except takes also an item to generate a link to the item
function item_span(res, url, name){
return $("<a>",{
lang:res[name]["xml:lang"],
append:res[name].value,
href:res[url].value
});
}
async function get_upperclasses_disjointness_violations_infos(bottom, classes){
}
// renders a class list into a html horizontal list
function display_as_list(class_list){
return $("<ul>").addClass("classification-gadget-horizontal-list").append(
class_list.map(
class_=> make_list_item(item_span(class_,"class","classLabel"))
)
);
}
function get_html(jqobj){
return jqobj[0].outerHTML;
}
function render_disjointness_violation_for_instance(instance, classes, violation, conflicting_classes, upper_conflicting_class){
let violation_parent_class = classes.find(class_ => class_.class.value == violation.class.value );
const class_label = get_html(label_span(violation_parent_class, "classLabel"));
let [class1, class2] = conflicting_classes.map(c => get_html(classes.label_span(c.class.value)));
return create_constraint_violation_box(
messages.$("disjointness_violation"), // `Disjointness violation !`
messages.$("instance_of_several_classes")
//"This instance is an instance of two or more classes, there are several ways to fix it [TODO]"
).append([
messages.$("classes_are_disjoint")
/* "On class $class it is stated that the following classes, and this is an instance of them !" */
.replace("$class", `<a href="${violation.st.value}">${class_label}</a> `), "<br/>",
display_as_list(conflicting_classes),
"<br/>",
messages.$("probable_culprit"),
/* " => Probable culprit(s), highest classes in the tree to be subclasses of conflicting classes (if any) :", */
display_as_list(upper_conflicting_class),
create_full_class_tree(
query_conflicting_class_tree_visualisation(instance, "P31", "base: wdt:P31 ?baseclass.", violation_parent_class, conflicting_classes[0], conflicting_classes[1]),
messages.$("see_conflict_tree"), /* "Visualize this disjointness conflict in its class tree … " */
create_classtree_disjointness_caption(
messages.$("disjoint_union_class").replace("$class", get_html(classes.label_span(violation.class.value))),
messages.$("this_instance").replace("$instance",item_label),
messages.$("subclass_of_c").replace("$class", class1),
messages.$("subclass_of_c").replace("$class", class2),
messages.$("subclass_of_both")
)
),
]
);
}
function array_join(array, sep){
return [
...array.slice(0, -1).flatMap(elem => [elem, sep]),
...array.slice(-1)
];
}
function create_classtree_disjointness_caption(top, bottom, unique1, unique2, several){
let items = [
[top, "classification-gadget-violation-colors-top"],
[bottom, "classification-gadget-violation-colors-bottom"],
[unique1, "classification-gadget-violation-colors-unique-1"],
[unique2, "classification-gadget-violation-colors-unique-2"],
[several, "classification-gadget-violation-colors-several"]
].map(([msg, color]) => $("<span>").addClass(color).append(msg));
return $("<div>").append(
array_join(items," ")
);
}
function render_disjointness_violation_for_subclass(subclass, classes, violation, conflicting_classes, upper_conflicting_class){
let violation_parent_class = classes.find(class_ => class_.class.value == violation.class.value );
const class_label = label_span(violation_parent_class, "classLabel");
let [class1, class2] = conflicting_classes.map(c => get_html(classes.label_span(c.class.value)));
return create_constraint_violation_box(
messages.$("disjointness_violation"), // `Disjointness violation !`
messages.$("subclass_of_several_classes")
//"This instance is an instance of two or more classes, there are several ways to fix it [TODO]"
).append([
messages.$("superclasses_are_disjoint")
/* "On class $class it is stated that the following classes, and this is a subclass of them !" */
.replace("$class", `<a href="${violation.st.value}">${class_label.html()}</a> `), "<br/>",
display_as_list(conflicting_classes),
"<br/>",
messages.$("probable_culprit"),
/* " => Probable culprit(s), highest classes in the tree to be subclasses of conflicting classes (if any) :", */
display_as_list(upper_conflicting_class),
create_full_class_tree(
query_conflicting_class_tree_visualisation(subclass,"P279", "base: wdt:P279 ?baseclass." ,violation_parent_class, conflicting_classes[0], conflicting_classes[1]),
messages.$("see_conflict_tree"), /* "Visualize this disjointness conflict in its class tree … " */
create_classtree_disjointness_caption(
messages.$("disjoint_union_class").replace("$class", get_html(classes.label_span(violation.class.value))),
messages.$("this_class_").replace("$class",item_label),
messages.$("subclass_of_c").replace("$class", class1),
messages.$("subclass_of_c").replace("$class", class2),
messages.$("subclass_of_both")
)
),
]
);
}
async function get_disjointness_violation_infos(
classes,
violation,
culprit_query_creator){
const copy_classes = [...classes];
const disjoint_classes = violation.class_set.value.split(",");
let violation_parent_class = copy_classes.find(class_ => class_.class.value == violation.class.value );
let conflicting_classes = copy_classes.filter(class_ => disjoint_classes.includes(class_.class.value));
let probable_conflicting_class_results = await makeSPARQLQuery(
endpointUrl,
culprit_query_creator(
conflicting_classes[0].class.value,
conflicting_classes[1].class.value
)
);
let probable_conflicting_class = probable_conflicting_class_results.results.bindings;
let upper_conflicting_class = probable_conflicting_class.map(
(cclass) => {
return copy_classes.find(
(pclass) => pclass.class.value === cclass.class.value
);
}
);
return [conflicting_classes, upper_conflicting_class];
}
async function get_disjointness_violations_infos(classes, violations_results, culprit_query_creator, render_violation){
let section = $("<div/>");
let violations = violations_results.results.bindings;
if (violations.length === 0){
return;
}
for(const violation of violations){
let [conflicting_classes, upper_conflicting_class] = await get_disjointness_violation_infos(classes, violation, culprit_query_creator);
section.append($("<span/>").append(
render_violation( classes, violation, conflicting_classes, upper_conflicting_class)
));
}
return section;
}
async function get_instance_disjointness_violations_infos(instance, classes){
let get_query = (conf1, conf2) =>
query_highest_subclass_of_both_conflicting_class_of(
instance,
conf1,
conf2,
);
let violations_results = await makeSPARQLQuery(endpointUrl, query_disjoint_statements_violated_by_instance(instance));
return (get_disjointness_violations_infos(classes, violations_results, get_query, ( ...args )=>render_disjointness_violation_for_instance(instance, ...args)));
}
async function get_subclass_disjointness_violations_infos(base_class){
let data = await makeSPARQLQuery(endpointUrl, query_superclasses(base_class));
var classes = data.results.bindings;
augment_classlist(classes);
let get_query = (conf1, conf2) =>
query_highest_subclass_of_both_conflicting_superclass_of(
base_class,
conf1,
conf2,
);
let violations_results = await makeSPARQLQuery(endpointUrl, query_disjoint_statements_violated_by_superclasses(base_class));
return (get_disjointness_violations_infos(classes, violations_results, get_query, ( ...args ) => render_disjointness_violation_for_subclass(base_class, ...args)));
}
function create_report_link(sample, instance){
return $("<a>",{
href : "/wiki/Wikidata Talk:WikiProject Ontology?" + $.param(
{
action:"edit",
section:"new",
preloadtitle: "{{Q|$1}} is an instance of {{Q|$2}} and that is weird"
.replace("$1",instance)
.replace("$2",urlToQid(sample.class.value)),
summary:"Reported Classtree Bug (gadget classification.js)",
preload:"Template:ReportWeirdSubclassChain",
preloadparams:[
instance,
urlToQid(sample.class.value),
encodeURIComponent(query_inst_path(instance, sample.class.value)),
gadget
]
}),
class : "classification-gadget-report-link",
text : messages.$("weird_to_discuss") /* :"(This seems weird or incorrect, discuss it)" */
}
);
}
function create_inferred_instance_detail(sample, instance){
return $("<details>",{
append: [
$("<summary>",{
append:
[
$("<a>",{
href:sample.class.value,
append:label_span(sample, "classLabel")
}
),
" ",
create_report_link(sample, instance)
]
}),
$("<div>")
],
one:{
toggle:
function(event){
var self = event.target;
makeSPARQLQuery(endpointUrl,
query_direct_classes_subclasses_of_superclass(instance, sample.class.value),
function(data) {
$("summary", self).after(
messages.$("instance_explanation") /*
"$item_label is an explicit instance of $explicit_classes "
+ " and it is/they are linked to $superclass this class by a subclass chain."*/
.replace("$superclass", label_span(sample, "classLabel").prop('outerHTML'))
.replace("$explicit_classes",
data.results.bindings.map(
function(res) { return label_span(res,"classLabel").html() ; }
).join(messages.$("and") /*" and "*/))
.replace("$item_label", item_label)
);
}
);
$(self).append(
create_embedded_frame(
query_inst_path(instance, sample.class.value)
)
);
}
}
});
}
function superclass_list(classes, instance, item_label){
let section = $("<h4>",{
text: messages.$("item_is_also_instanceof") /*"$item_label is also a(n) …"*/
.replace("$item_label", item_label)
});
var num_classes = classes.length;
var sample_classes = classes.filter(
function(){
return (num_classes < 4 || Math.random() < (4/num_classes) );
});
var sample_classes_links = sample_classes.map(
(res) => create_inferred_instance_detail(res, instance)
);
return [
section,
messages.$("show_samplesize") /*"(showing $num_sample / $total_available results"*/
.replace("$num_sample", sample_classes.length)
.replace("$total_available", num_classes),
$("<ul>",{
append:sample_classes_links.map(make_list_item),
style:"list-style:none;",
}),
$("<a>", {
text:messages.$("more"),
one:{
click:function(event){
var not_shown_yet = classes.filter(elem => !sample_classes.includes(elem));
$("ul", event.target.parentNode).append(
not_shown_yet
.map((res) => create_inferred_instance_detail(res, instance))
.map(make_list_item));
event.target.remove();
}
}
})
];
}
// initialize the section below the « instance of » statements
async function register_subsection(section, creation_function){
let div = $("<div>");
section.append(div);
let res = await creation_function();
if(res){
div.append(res);
}
}
function augment_classlist(classes){
classes.get_class = function(searchclass){
return (this.find(class_ => class_.class.value == searchclass ));
};
classes.label_span = function(searchclass){
return label_span(this.get_class(searchclass), "classLabel");
};
classes.item_span = function(searchclass){
return item_span(this.get_class(searchclass), searchclass, "classLabel");
};
}
async function init_instances(instance){
var explain_id = "classification-gadget-instance-explain";
var item_label = $( ".wikibase-title-label" ).text();
var instances_info=$("<div>",{
id:"classification-gadget-instances",
class:"wikibase-statementgroupview classification-gadget-property-section",});
var cont_id = create_property_container("P31", instances_info);
let full_class_tree = () => create_full_class_tree(
query_instance_superclass_tree(instance),
messages.$("embedded_classes_of_instance_tree") /*:"Embedded tree of the classes of this instance"*/
);
register_subsection(instances_info, full_class_tree);
// getting the necessary informations, the superclasses
let classes_q = await makeSPARQLQuery( endpointUrl, query_other_classes(instance));
var classes = classes_q.results.bindings;
augment_classlist(classes);
register_subsection(instances_info, () => superclass_list(classes, instance, item_label));
register_subsection(instances_info, () => get_instance_disjointness_violations_infos(instance, classes));
}
function show_classification_data(){
const entity = mw.config.get( 'wbEntityId' );
init_subclassof(entity);
init_instances(entity);
}
// functions to deal with the activation / deactivation of the gadget
function show_deactivate_link(confkey, timeout){
var link = mw.util.addPortletLink(
'p-tb',
"#",
messages.$("deactivate_gadget")
);
$( link ).on( 'click', function ( e ) {
mw.storage.set(confkey, false, timeout);
e.preventDefault();
e.target.parentNode.remove();
show_activate_link(confkey, timeout, true);
} );
}
function show_activate_link(confkey, timeout, loaded){
var link = mw.util.addPortletLink(
'p-tb',
"#",
messages.$("activate_gadget")
);
$( link ).on( 'click', function ( e ) {
mw.storage.set(confkey, true, timeout);
e.preventDefault();
e.target.parentNode.remove();
if(!loaded){
show_classification_data();
}
show_deactivate_link(confkey, timeout);
} );
}
function init(){
var entity = mw.config.get( 'wbEntityId' );
const config_save_timeout = 3*3600*24*30;
const activated_key = "classification-gadget.activated";
// gadget active by default
if (mw.storage.get(activated_key) === null){
mw.storage.set(activated_key, true, config_save_timeout);
}
const activated = mw.storage.get(activated_key);
if ( activated === "false"){
show_activate_link(activated_key, config_save_timeout, false);
} else {
// load translation object
show_classification_data();
show_deactivate_link(activated_key, config_save_timeout);
}
}
// load tootranslate before anything, we cannot really do this asynchronously because we need the localised message in the toolbox to activate / deactivate the gadget loading anyway, so we have to load the translation. If not we could not be able to show a localized link to activate. (maybe load in english first and then update ?)
try {
await mw.loader.getScript("//tools-static.wmflabs.org/tooltranslate/tt.js");
} catch(e) {
mw.notify("Classification gadget : cannot load tooltranslate, using english messages");
mw.log(e);
}
mw.loader.using(
["oojs-ui.styles.icons-editing-advanced"]
).then(
// ensure that the messages are loaded before calling "init" because there might be asynchronous issues with loading
() => {
if(typeof(ToolTranslation) !== "undefined"){
tool_translate = new ToolTranslation (
{
language: lang,
languages : mw.language.getFallbackLanguageChain().reverse() ,
debug:1 ,
tool: 'lassificationjs',
callback : () => { load_messages(); init() },
no_interface_update : true
}
);
} else {
load_messages(); init();
}
}
);
});
})(mw, $, wb);