/**
* @description WhatElse - For Wikidata items (somehow) related to this image, what other items with images are there?
* @author (c) 2018 Magnus Manske
* @license released under GPL v2+
* <nowiki>
*/
/*global jQuery, mediaWiki*/
/*jshint multistr:true*/
( function( $, mw ) {
'use strict';
var whatElse = {
language:'en', // TODO
investigate_same_property_value:['P84','P631','P127','P170','P50','P180','P195','P135','P276','P921','P136',
'P186','P571','P735','P569','P570','P19','P20','P509','P119','P106','P39','P166','P69','P410','P607','P734'],
conf:{},
sparql_url:'https://query.wikidata.org/sparql' ,
related_items:[] ,
items:{} ,
sections:[] ,
init: function () {
this.conf = mw.config.get(['wgAction', 'wgNamespaceNumber', 'wgPageName', 'wgTitle']) ;
if (this.conf.wgNamespaceNumber !== 6 ) return ; // not a file
this.getRelatedItems() ;
} ,
getRelatedItems : function () {
let self = this ;
self.related_items = [] ;
self.sections = [] ;
Promise.all ( [
new Promise(function(resolve, reject) { // Usage on Wikidata
self.checkGlobalUsage ( (items) => { self.related_items = self.related_items.concat(items); resolve()} ) ;
} ) ,
new Promise(function(resolve, reject) { // Stock items
self.loadItems ( self.investigate_same_property_value , resolve )
} ) ,
] ) . then ( function () {
if ( self.related_items.length == 0 ) return ; // No items to process
self.related_items = self.related_items.filter((v, i, a) => a.indexOf(v) === i); // Unique
self.loadItems ( self.related_items , function () {
$.each ( self.related_items , (index,q) => { self.investigateItem(q)} ) ;
} ) ;
} ) ;
} ,
investigateItem : function ( q ) {
let self = this ;
if ( typeof self.items[q] == 'undefined' ) return ;
let i = self.items[q] ;
if ( typeof i.claims == 'undefined' ) return ;
self.investigateNearby ( q , i ) ;
self.investigateLocation ( q , i ) ;
self.investigateTaxon ( q , i ) ;
self.investigateInstanceOf ( q , i ) ;
$.each ( self.investigate_same_property_value , function (k,v) { self.investigateSamePropertyValue(q,i,v) } ) ;
} ,
showDialog: function ( url ) { // TODO use internal viewer
// Example:
// this.showDialog('https://query.wikidata.org/embed.html#%23defaultView%3AImageGrid%0ASELECT%20%3Fq%20%3Fimg%20%7B%20%3Fq%20wdt%3AP131*%20wd%3AQ148349%20%3B%20wdt%3AP18%20%3Fimg%20%7D%20LIMIT%202000') ;
let content = '<iframe style="width: 100%; height: 50vh; border: none;" src="'+url+'" referrerpolicy="origin" sandbox="allow-scripts allow-same-origin allow-popups" ></iframe>' ;
function MyDialog( config ) { MyDialog.super.call( this, config ) }
OO.inheritClass( MyDialog, OO.ui.Dialog );
MyDialog.static.name = 'myDialog';
MyDialog.static.title = 'Simple dialog';
MyDialog.prototype.initialize = function () {
// Call the parent method
MyDialog.super.prototype.initialize.call( this );
// Create and append a layout and some content.
this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
this.content.$element.append( '<p>'+content+'</p>' );
this.$body.append( this.content.$element );
};
var myDialog = new MyDialog( { size: 'full' } );
var windowManager = new OO.ui.WindowManager();
$( 'body' ).append( windowManager.$element );
windowManager.addWindows( [ myDialog ] );
windowManager.openWindow( myDialog );
} ,
getLabel : function ( id ) {
let self = this ;
if ( typeof self.items[id] == 'undefined' ) return id ; // Fallback
if ( typeof self.items[id].labels == 'undefined' ) return id ; // Fallback
if ( typeof self.items[id].labels[self.language] != 'undefined' ) return self.items[id].labels[self.language].value ;
return id ; // TODO more fallback languages
} ,
investigateSamePropertyValue : function ( q , i , prop ) {
let self = this ;
if ( typeof i.claims[prop] == 'undefined' ) return ;
let datatype = self.items[prop].datatype ;
if ( datatype == 'wikibase-item' ) return self.investigateSamePropertyValueWikiBaseItem ( q , i , prop ) ;
if ( datatype == 'time' ) return self.investigateSamePropertyValueTime ( q , i , prop ) ;
} ,
investigateSamePropertyValueWikiBaseItem : function ( q , i , prop ) {
let self = this ;
let to_load = [] ;
$.each ( i.claims[prop] , function ( k , v ) { to_load.push ( v.mainsnak.datavalue.value.id ) } ) ;
self.loadItems ( to_load , function () {
let section = { header:self.getLabel(prop) , rows:[] } ;
$.each ( to_load , function ( dummy , q2 ) {
let query = "SELECT ?q ?img { ?q wdt:"+prop+" wd:"+q2+" ; wdt:P18 ?img }" ;
let url = self.getImageGridURL ( query ) ;
let h = "<a class='external' target='_blank' href='" + url + "'>" + self.getLabel(q2) + "</a>" ;
section.rows.push ( {html:h} ) ;
} ) ;
self.addNewSection ( section ) ;
} ) ;
} ,
investigateSamePropertyValueTime : function ( q , i , prop ) {
let self = this ;
let section = { header:self.getLabel(prop) , rows:[] } ;
$.each ( i.claims[prop] , function ( dummy , claim ) {
let t = claim.mainsnak.datavalue.value.time ;
let year = t.replace(/^([-+]{0,1}\d+).*$/,'$1') ;
let query = "SELECT ?q ?img { ?q wdt:"+prop+" ?time ; wdt:P18 ?img . FILTER ( year(?time)="+year+" ) }" ;
let url = self.getImageGridURL ( query ) ;
let h = "<a class='external' target='_blank' href='" + url + "'>" + year.replace(/^\+/,'') + "</a>" ;
section.rows.push ( {html:h} ) ;
} ) ;
self.addNewSection ( section ) ;
} ,
investigateNearby : function ( q , i ) {
if ( typeof i.claims.P625 == 'undefined' ) return ;
let self = this ;
let value = i.claims.P625[0].mainsnak.datavalue.value ;
let lat = value.latitude * 1 ;
let lon = value.longitude * 1 ;
let section = { header:'Nearby' , rows:[] } ;
$.each ( [0.5,1,2,5,10] , function ( dummy , radius ) {
let query = 'SELECT ?q ?img WHERE { SERVICE wikibase:around { ?q wdt:P625 ?location . bd:serviceParam wikibase:center "Point('+lon+' '+lat+')"^^geo:wktLiteral . bd:serviceParam wikibase:radius "'+radius+'" . bd:serviceParam wikibase:distance ?distance } ?q wdt:P18 ?img } ORDER BY ?distance' ;
let url = self.getImageGridURL ( query ) ;
let h = "<a class='external' target='_blank' href='" + url + "'>Within "+radius+" km</a>" ;
section.rows.push ( { html:h } ) ;
} ) ;
self.addNewSection ( section ) ;
} ,
investigateInstanceOf : function ( q , i ) {
if ( typeof i.claims.P31 == 'undefined' ) return ;
let self = this ;
let p31s = [] ;
$.each ( i.claims.P31 , function ( k , v ) { p31s.push ( v.mainsnak.datavalue.value.id ) } ) ;
self.loadItems ( p31s , function () {
$.each ( p31s , function ( dummy , q2 ) {
if ( typeof self.items[q2] == 'undefined' ) return ;
let i2 = self.items[q2] ;
let label = self.getLabel(q2) ;
self.investigateGenericPath ( q2 , i2 , 'P279' , label , true ) ;
} ) ;
} ) ;
} ,
investigateLocation : function ( q , i ) {
this.investigateGenericPath ( q , i , 'P131' , 'Location' , false ) ;
} ,
investigateGenericPath : function ( q , i , prop , header , include_self = false ) {
if ( typeof i.claims[prop] == 'undefined' ) return ;
let self = this ;
let sparql = 'SELECT DISTINCT ?q ?parent_q ?qLabel { wd:'+q+' wdt:'+prop+'* ?q . OPTIONAL { ?q wdt:'+prop+' ?parent_q } . SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". } }' ;
self.loadSPARQL ( sparql , function ( d ) {
if ( typeof d.results == 'undefined' || d.results.bindings == 'undefined' || d.results.bindings.length == 0 ) return ;
d = self.indexSPARQLbyQ ( d ) ;
let lp = self.findLongestPath ( { start:q , props:['parent_q'] , data:d } ) ;
let section = { header:header , rows:[] } ;
$.each ( lp , function ( dummy , item_q ) {
if ( q == item_q && !include_self ) return ; // Don't include self
let name = d[item_q].qLabel ;
let taxon_name = d[item_q].tn ;
let condition = "wdt:"+prop+"*" ;
if ( prop == 'P279' ) condition = "wdt:P31|" + condition ; // HARDCODED UGLY EXCEPTION
let url = self.getImageGridURL ( "SELECT ?q ?img { ?q "+condition+" wd:"+item_q+" ; wdt:P18 ?img }" ) ;
let h = "<a class='external' target='_blank' href='" + url + "'>" + name + "</a>" ;
section.rows.push ( { html:h } ) ;
} ) ;
self.addNewSection ( section ) ;
} ) ;
} ,
investigateTaxon : function ( q , i ) {
if ( typeof i.claims.P171 == 'undefined' ) return ;
let self = this ;
let sparql = 'SELECT DISTINCT ?q ?tn ?pt ?qLabel { wd:'+q+' wdt:P171* ?q . ?q wdt:P225 ?tn . OPTIONAL { ?q wdt:P171 ?pt } . SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". } }' ;
self.loadSPARQL ( sparql , function ( d ) {
if ( typeof d.results == 'undefined' || d.results.bindings == 'undefined' || d.results.bindings.length == 0 ) return ;
d = self.indexSPARQLbyQ ( d ) ;
let lp = self.findLongestPath ( { start:q , props:['pt'] , data:d } ) ;
let section = { header:'Taxonomy' , rows:[] } ;
$.each ( lp , function ( dummy , taxon_q ) {
if ( q == taxon_q ) return ; // Don't include self
let name = d[taxon_q].qLabel ;
let taxon_name = d[taxon_q].tn ;
let url = self.getImageGridURL ( "SELECT ?q ?img { ?q wdt:P171* wd:"+taxon_q+" ; wdt:P18 ?img }" ) ;
let h = "<a class='external' target='_blank' href='" + url + "'>" + name + "</a> [<i>" + taxon_name + "</i>]" ;
section.rows.push ( { html:h } ) ;
} ) ;
self.addNewSection ( section ) ;
} ) ;
} ,
indexSPARQLbyQ : function ( d , q_var_name = 'q' ) {
let ret = {} ;
$.each ( d.results.bindings , function ( dummy , b ) {
let o = {} ;
$.each ( b , function ( var_name , v ) {
if ( v.type == 'uri' && /^http:\/\/www\.wikidata\.org\/entity\//.test(v.value) ) o[var_name] = v.value.replace(/^.*\//,'') ;
else o[var_name] = v.value ;
} ) ;
ret[o[q_var_name]] = o ;
} ) ;
return ret ;
} ,
getImageGridURL : function ( query ) {
let encoded_query = "%23defaultView%3AImageGrid%0A" + encodeURIComponent(query+' LIMIT 2000') ;
//let url = "https://query.wikidata.org/#" + encoded_query" ; // Editor
let url = "https://query.wikidata.org/embed.html#" + encoded_query ; // Results
return url ;
} ,
/**
* In a graph of items linked by properties, finds the longest path from a give item to a root item.
* @param {hash} o - Hash with keys "start" (start item ID) and "props" (array of property IDs).
* @returns {array} List of item IDs, starting with the "o.start" item, ending with the root item.
* [this function copied from Reasonator, and modified]
*/
findLongestPath : function ( o ) {
var self = this ;
var tree = {} ;
function preset ( qs ) {
$.each ( qs , function ( dummy , q ) {
var new_q = [] ;
if ( undefined !== tree[q] ) return ;
tree[q] = [] ;
$.each ( o.props , function ( dummy , p ) {
if ( 'undefined' == typeof o.data[q] ) return ;
if ( 'undefined' == typeof o.data[q][p] ) return ;
let qx = o.data[q][p] ;
if ( -1 != $.inArray ( qx , new_q ) ) return ;
new_q.push ( qx ) ;
tree[q].push ( qx ) ;
} ) ;
preset ( new_q ) ;
} ) ;
}
preset ( [ o.start ] ) ;
function iterate ( qs ) {
var nqs = [] ;
$.each ( qs , function ( dummy , i ) {
var sub_q = [] ;
$.each ( tree[i.q] , function ( dummy2 , v ) {
if ( -1 != $.inArray ( v , i.hist ) ) return ;
sub_q.push ( v ) ;
} ) ;
$.each ( sub_q , function ( k , v ) {
var nh = i.hist.slice() ;
nh.push ( v ) ;
nqs.push ( { q:v , hist: nh } ) ;
} ) ;
} ) ;
if ( nqs.length > 0 ) {
qs = [] ;
return iterate ( nqs ) ;
} else {
var longest = [] ;
$.each ( qs , function ( dummy , i ) {
if ( i.hist.length > longest.length ) longest = i.hist ;
} ) ;
return longest ;
}
}
var ret = iterate ( [ { q:o.start , hist:[o.start] } ] ) ;
return ret ;
} ,
addNewSection : function ( section ) {
let self = this ;
if ( self.sections.length == 0 ) {
let h = "<div id='whatelse_container' style='float:right;max-width:33%;'>" ;
h += "<h4>See also:</h4>" ;
h += "<div id='whatelse_contents' style='display:flex;align-items: flex-start; flex-wrap: wrap;font-size:9pt;'></div>" ;
h += "</div>" ;
$('#file').before(h) ;
}
self.sections.push ( section ) ;
let h = '' ;
h += "<div style='display:flex;flex-direction:column;margin-right:0.2rem;'>" ;
h += "<h5 style='background-color:#EFE'>" + section.header + "</h5>" ;
$.each ( section.rows , function ( num , row ) {
h += "<div>" + row.html + "</div>" ;
} ) ;
h += "</div>" ;
$('#whatelse_contents').append ( h ) ;
} ,
loadSPARQL : function ( query , callback , callback_fail ) {
var url = this.sparql_url+"?format=json&query=" + encodeURIComponent(query) ;
$.get ( url , function ( d ) {
callback ( d ) ;
} , 'json' ) . fail ( callback_fail ) ;
} ,
loadItems : function ( items , callback ) {
let self = this ;
items = items.filter((v, i, a) => a.indexOf(v) === i); // Unique
items = items.filter((v, i, a) => typeof self.items[v] == 'undefined' ); // remove items already loaded
// Chunk
let promises = [] ;
for (let i=0,j=items.length; i<j; i+=50) {
let temparray = items.slice(i,i+50);
if ( temparray.length == 0 ) continue ;
promises.push ( new Promise(function(resolve, reject) {
$.getJSON ( 'https://www.wikidata.org/w/api.php?callback=?' , {
action:'wbgetentities',
ids:temparray.join('|'),
format:'json'
} , function ( d ) {
$.each ( d.entities , function ( q , item ) {
if ( typeof item.missing != 'undefined' ) return ;
self.items[q] = item ;
} ) ;
resolve();
} ) ;
} ) ) ;
}
Promise.all ( promises ) . then ( callback ) ;
} ,
checkGlobalUsage: function (callback,continuation,items) {
let self = this;
let params = {
action: 'query',
prop: 'globalusage',
gulimit: 500,
gufilterlocal: 1,
guprop: 'namespace',
format: 'json',
titles: self.conf.wgPageName
};
if ( !items ) items = [] ;
if (continuation) $.extend(params, continuation);
$.post('/w/api.php', params, function (d) {
$.each(((d.query || {}).pages || {}), function (file_page_id, v) {
$.each((v.globalusage || []), function (k2, v2) {
if ( v2.ns*1 == 0 && v2.wiki=='www.wikidata.org' ) items.push ( v2.title ) ;
});
});
if (d['continue']) self.checkGlobalUsage(callback,d['continue'],items);
else callback(items) ;
}, 'json');
},
fin: ''
};
mw.loader.using(['oojs','oojs-ui-core','oojs-ui-windows','mediawiki.widgets']).then ( function () {
$(function () {
whatElse.init();
});
} ) ;
}(jQuery, mediaWiki));
// </nowiki>