User:Magnus Manske/whatelse.js

Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
/**
* @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>