User:Magnus Manske/sdc tool.js

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
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.
/*
(c) 2019 by [[User:Magnus Manske]]. GPLv3 or later.

This tool lets you quickly add statements for Structured Data on Commons (SDC) to (selected) files on galleries, category pages, and serach results.

Demo video: https://www.youtube.com/watch?v=RIjXRJNcbL0

Demo video 2 (item creation/Wikidata Infobox): https://www.youtube.com/watch?v=vvSimZSD_IU

To use this script, add it to your [https://commons.wikimedia.org/wiki/Special:MyPage/common.js common.js page], like so:
importScript('User:Magnus Manske/sdc_tool.js') ;

Activate by clicking on the "SDC" link in the box in the lower-right corner, or by pressing S with (Alt/Ctrl/whatever your browser uses).

*/
$.when(mw.loader.using(['oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows', 'mediawiki.ForeignApi']), $.ready).done ( function () {

	// Dialog
	function SDCdialog( config ) {
		SDCdialog.super.call( this, config );
	}
	OO.inheritClass( SDCdialog, OO.ui.Dialog ); 

	// Specify a name for .addWindows()
	SDCdialog.static.name = 'SDCdialog';
	// Specify a title statically (or, alternatively, with data passed to the opening() method).
	SDCdialog.static.title = 'Find a target item for SDC';

	SDCdialog.prototype.update_results = function () {
		let html = "" ;
		$.each ( this.results , function ( result_id , v ) {
			html += "<div style='margin:2px;padding:2px;border:1px solid #DDD;border-radius:3;display:flex;'>" ;
			html += "<div style='margin-right:0.2rem;'><input style='color:green;font-size:1.5em;' class='sdc_dialog_cegision_button' result='" ;
			html += result_id+"' type='button' value='✓' /></div>" ;
			html += "<div>" ;
			html += "<div><b class='result_label' result='"+result_id+"'></b> <small>[<a href='https://www.wikidata.org/wiki/"+v.q+"' target='_blank'>"+v.q + "</a>]</small></div>" ;
			html += "<div class='result_description' result='"+result_id+"'></div>" ;
			html += "</div>" ;
			html += "</div>" ;
		} ) ;
		if ( html === '' ) html = '<div style="margin:2px;"><i>No results found on Wikidata</i></div>' ;
		$('#sdc_dialog_results').html(html);
		$.each ( this.results , function ( result_id , v ) {
			$('#sdc_dialog_results b.result_label[result="'+result_id+'"]').text(v.label);
			$('#sdc_dialog_results div.result_description[result="'+result_id+'"]').text(v.description);
		} ) ;
		let me = this ;
		$('.sdc_dialog_cegision_button').click(function(){
			let result_id = $(this).attr('result')*1 ;
			console.log(me.results);
			let item = me.results[result_id] ;
			me.on_decision(item);

		});
	}

	SDCdialog.prototype.get_time_claim_for_year = function ( property , year ) {
		let value = {
            "time": "+"+year+"-01-01T00:00:00Z",
            "timezone": 0,
            "before": 0,
            "after": 0,
            "precision": 9,
            "calendarmodel": "http://www.wikidata.org/entity/Q1985727"
        } ;
		let claim = {mainsnak:{snaktype:"value",property:property,datavalue:{value:value,type:'time'}},type:"statement",rank:"normal"} ;
		return claim ;
	}

	SDCdialog.prototype.create_new_item_for_category = function () {
		let me = this ;
		let label = mw.config.values.wgTitle ;
		let data = {
			claims:[{mainsnak:{snaktype:"value",property:'P373',datavalue:{value:label,type:'string'}},type:"statement",rank:"normal"}],
			labels:{en:{language:"en",value:label}}
		} ;
		let summary = 'SDC: created item based on Commons category ' + label ;
		
		// If the page exists, add sitelink
		if ( mw.config.values.wgArticleId !== 0 ) {
			data.sitelinks = {"commonswiki":{site:"commonswiki",title:mw.config.values.wgPageName}} ;
		}

		let is_human = false ;
		let gender = '' ;
		$.each ( mw.config.values.wgCategories , function ( dummy , category ) {
			let m = category.match ( /^(\d{3,4}) births$/ ) ;
			if ( m !== null ) {
				data.claims.push ( me.get_time_claim_for_year ( 'P569' , m[1] ) ) ;
				is_human = true ;
			}
			m = category.match ( /^(\d{3,4}) deaths$/ ) ;
			if ( m !== null ) {
				data.claims.push ( me.get_time_claim_for_year ( 'P570' , m[1] ) ) ;
				is_human = true ;
			}
			if ( category.match ( /^Female / ) !== null ) gender = 'Q6581072' ;
			if ( category.match ( /^Male / ) !== null ) gender = 'Q6581097' ;
		} ) ;
		if ( gender !== '' ) {
			let value = {'entity-type':'item',id:gender} ;
			let claim = {mainsnak:{snaktype:"value",property:'P21',datavalue:{value:value,type:'wikibase-entityid'}},type:"statement",rank:"normal"} ;
			data.claims.push ( claim ) ;
			is_human = true ;
		}
		if ( is_human ) {
			let value = {'entity-type':'item',id:'Q5'} ;
			let human = {mainsnak:{snaktype:"value",property:'P31',datavalue:{value:value,type:'wikibase-entityid'}},type:"statement",rank:"normal"} ;
			data.claims.push ( human ) ;
		}

		sdc.get_wikidata_token ( function ( token ) {
			let params = {
				action:'wbeditentity',
				new:'item',
				data:JSON.stringify(data),
				token:token,
				summary:summary,
				format:'json'
			} ;
			let api = new mw.ForeignApi( 'https://www.wikidata.org/w/api.php' );
			api.post(params).done(function(d) {
				if ( d.success != 1 ) {
					console.log ( d ) ;
					alert ( "Something went wrong with item creation, see JS console" ) ;
					return ;
				}
				let q = d.entity.id ;
				let item = {q:q,label:label,description:''} ;
				me.on_decision(item);
			} ) ;
		} );
	}

	/*
	// Obsolete, but keeping code around in case of mw issues...
	SDCdialog.prototype.get_qs_creation = function () {
		let me = this ;
		let commands = [ "CREATE" ] ;
		commands.push ( "LAST\tLen\t\""+mw.config.values.wgTitle+"\"" ) ;
		commands.push ( "LAST\tP373\t\""+mw.config.values.wgTitle+"\"" ) ;

		let is_human = false ;
		$.each ( mw.config.values.wgCategories , function ( dummy , category ) {
			let m = category.match ( /^(\d{3,4}) births$/ ) ;
			if ( m !== null ) {
				commands.push ( "LAST\tP569\t+"+m[1]+"-01-01T00:00:00Z/9") ;
				is_human = true ;
			}
			m = category.match ( /^(\d{3,4}) deaths$/ ) ;
			if ( m !== null ) {
				commands.push ( "LAST\tP570\t+"+m[1]+"-01-01T00:00:00Z/9") ;
				is_human = true ;
			}
		} ) ;
		if ( is_human ) commands.push ( "LAST\tP31\tQ5") ;
		return commands.join("\n") ;
	}
	*/

	// Customize the initialize() function: This is where to add content to the dialog body and set up event handlers.
	SDCdialog.prototype.initialize = function () {
		// Call the parent method.
		SDCdialog.super.prototype.initialize.call( this );
		// Create and append a layout and some content.
		this.content = new OO.ui.PanelLayout( { 
			padded: true,
			expanded: false 
		} );

		let html = "<p>";
		html += "<div>Enter a search query for Wikidata items</div>" ;
		html += "<div><input type='text' id='sdc_dialog_query' style='width:100%' placeholder='search query' /></div>" ;
		html += "<div id='sdc_dialog_results' style='height:10rem;overflow:auto;'></div>" ;
		if ( mw.config.values.wgNamespaceNumber == 14 ) {
			html += "<div><i>or</i> <span id='create_new_item_container'></span></div>" ;
		}
		html += "</p>" ;

		this.content.$element.append( html );
		this.$body.append( this.content.$element );

		// Create a new item based on the category
		if ( mw.config.values.wgNamespaceNumber == 14 ) {
			//let qs = this.get_qs_creation() ;
			//console.log(qs);
			//let url = "https://tools.wmflabs.org/quickstatements/api.php?action=import&format=v1&temporary=1&openpage=1&data="+encodeURIComponent(qs) ;
			//let button_create_new_item = new OO.ui.ButtonWidget( { label: 'create new item based on this category in QuickStatements',href:url,target:'_blank' } ) ;
			let button_create_new_item = new OO.ui.ButtonWidget( { label: 'create new item based on this category',href:'#' } ) ;
			$('#create_new_item_container ').html(button_create_new_item.$element);
			$('#create_new_item_container a.oo-ui-buttonElement-button').click(function(){me.create_new_item_for_category();});
		}

		this.results = [] ;

		let me = this ;
		me.query_counter = 0 ;
		me.last_query = '' ;
		$('#sdc_dialog_query').keyup(function(){
			me.run_query() ;
		});
	};

	SDCdialog.prototype.run_query = function () {
		let me = this ;
		let query = $('#sdc_dialog_query').val();
		me.results = [] ;
		if ( query=='' || query==me.last_query ) return ;
		me.last_query = query ;
		me.query_counter++ ;
		let qc = me.query_counter ;
		var wgUserLanguage = mw.config.get('wgUserLanguage')
		$.getJSON('https://www.wikidata.org/w/api.php?callback=?',{
			action:'wbsearchentities',
			search:query,
			language:wgUserLanguage,
			limit:50,
			type:'item',
			format:'json'
		},function(d){
			if ( me.query_counter != qc ) return ; // New query was started while this one was running
			$(d.search).each(function(k,v){
				me.results.push({q:v.id,label:v.label,description:v.description||''});
			});
			me.update_results();
		});
	}

	// Override the getBodyHeight() method to specify a custom height (or don't to use the automatically generated height).
	SDCdialog.prototype.getBodyHeight = function () {
		return this.content.$element.outerHeight( true );
	};

	var sdc = {

		// Properties allowed to set; must be of 'Wikidata item' type
		properties : {
			P180 : 'depicts' ,
			P921 : 'main subject',
			P195 : 'collection' ,
			P793 : 'significant event' ,
			P1071 : 'location of creation' ,
			P7108 : 'location of the point of view' ,
			P5961 : 'depicted part' ,
			P6243 : 'digital representation of',
			P8058 : 'symbol of' ,
		} ,

		exclude_files:['Gtk-dialog-info-14px.png','Reasonator_logo_proposal_no_background.png'],
		is_stopped : true,
		active : false,
		dialog: new SDCdialog( { size: 'medium' } ),
		windowManager: new OO.ui.WindowManager(),
		mid_cache:[],
		mid2file:[],
		media_items:[],
		wikidata_items_to_load:[],
		wikidata_item_labels:[],
		target_item : {
			q:'',
			label:'',
			description:''
		},

		init : function () {
			if ( mw.config.values.wgNamespaceNumber == 6 ) return ; // Not for single images
			if ( $('a.image, a.mw-file-description').length == 0 ) return ; // No possible thumbnails
			let me = this ;
			me.dialog.on_decision = me.set_target_item;
			$( document.body ).append( me.windowManager.$element );
			me.windowManager.addWindows( [ me.dialog ] );

			me.show_main_element();
			me.try_guess_main_item();
			//me.toggle_main(); // Auto-open
		} ,

		try_guess_main_item : function () {
			let me = this ;
			if ( typeof wgWikibaseItemId == 'undefined' || wgWikibaseItemId == '' ) return ;

			me.try_set_main_item(wgWikibaseItemId);
		} ,

		try_set_main_item : function (q) {
			let me = this ;
			me.wikidata_items_to_load=[q] ;
			me.load_wikidata_items(function(){
				$.getJSON("https://www.wikidata.org/w/api.php?callback=?&action=wbgetentities&format=json&ids="+q,function(d){
					let item = (d.entities||{})[q] ;
					if ( typeof item == 'undefined' ) return ;
					if ( me.does_item_have_property_target(item,'P31','Q4167836') ) {
						let statement = ((item.claims||item.statements||{}).P301||[])[0] ;
						if ( typeof statement == 'undefined' ) return ; // No "category's main topic"
						let target_id = (((statement.mainsnak||{}).datavalue||{}).value||{}).id ;
						if ( typeof target_id != 'undefined' && target_id != q ) me.try_set_main_item(target_id);
						return ;
					}
					let label = $(me.wikidata_item_labels[q]).text();
					me.set_target_item({q:q,label:label,description:''});
				});
			});
		} ,

		update_main_position : function () {
			let bottom = $('#cat_a_lot_toggle').length>0 ? 30 : 0 ;
			$('#sdc_main').css({bottom:bottom+'px'})
		} ,

		show_action_button : function () {
			let me = this ;
			var action_button = new OO.ui.ButtonWidget( {
				label: 'Set '+me.get_property()+' to '+me.target_item.q,
				href: '#',
				flags: 'progressive'
			} );    
			$('#sdc_action_button_container').html(action_button.$element);
			$('#sdc_action_button_container a.oo-ui-buttonElement-button').click(function(){
				if ( $('input.sdc_checkbox:checked').length == 0 ) {
					alert("No files selected");
					return false ;
				}
				me.is_stopped = false ;
				me.show_action_stop_button();
				me.edit_next();
				return false;
			});
		} ,

		show_wikidata_infobox_button : function () {
			let me = this ;
			if ( mw.config.values.wgNamespaceNumber != 14 ) return ; // Categories only
			if ( $('#wdinfobox').length > 0 ) return ; // Already has infobox
			var wikidata_infobox_button = new OO.ui.ButtonWidget( {
				label: 'Add Wikidata Infobox for '+me.target_item.q,
				href: '#',
				flags: 'progressive'
			} );
			$('#sdc_add_wikidata_infobox_button').html(wikidata_infobox_button.$element);
			$('#sdc_add_wikidata_infobox_button a.oo-ui-buttonElement-button').click(function(){
				//let wikitext = '{{Wikidata Infobox|qid='+me.target_item.q+'}}\n' ; // old syntax
				let wikitext = '{{Wikidata Infobox}}\n' ;
				let summary = 'SDC: added '+wikitext ;
				$('#sdc_add_wikidata_infobox_button').html('<i>Editing...</i>');
				me.get_token ( function ( token ) {
					let params = {
						action:'edit',
						title:mw.config.values.wgPageName,
						prependtext:wikitext,
						token:token,
						summary:summary,
						format:'json'
					} ;
					$.post('/w/api.php',params,function(d){
						if ( (d.edit||{}).result != 'Success' ) {
							console.log(d);
							alert ( "Something went wrong with the edit, see JS console" ) ;
							return ;
						}
						let html = "<div id='wdinfobox' style='background-color:#d5fdf4'><tt>"+wikitext+"</tt> was added, reload page to show</div>" ;
						$('#bodyContent').prepend(html);
						$('#sdc_add_wikidata_infobox_button').html('');
					},'json');
				} );
				return false;
			});
		} ,

		show_action_stop_button : function () {
			let me = this ;
			var action_button = new OO.ui.ButtonWidget( {
				label: 'Stop editing',
				href: '#',
				flags: 'destructive'
			} );    
			$('#sdc_action_button_container').html(action_button.$element);
			$('#sdc_action_button_container a.oo-ui-buttonElement-button').click(function(){
				me.is_stopped=true;
				$('#sdc_action_button_container').html("<i>Stopping edits...</i>");
				return false;
			});
		} ,

		set_prominent_checkbox : function ( state ) {
			$('#sdc_prominent').prop( "checked", state );
		} ,

		edit_next : function () {
			let me = this ;
			if ( me.is_stopped ) return me.show_action_button(); // User stopped this
			let cbs = $('input.sdc_checkbox:checked') ;
			if ( cbs.length == 0 ) { // All done
				me.set_prominent_checkbox ( false ) ;
				return me.show_action_button();
			}

			let cb = $(cbs.get(0)) ;
			let file = decodeURIComponent(cb.attr('file')) ;
			if ( typeof file == 'undefined' ) { // All done
				me.set_prominent_checkbox ( false ) ;
				return me.show_action_button();
			}

			me.get_mediainfo_id_for_file ( file , function ( mediainfo_id ) {
				me.add_item_statement_to_item(mediainfo_id,me.get_property(),me.target_item.q,me.is_prominent(),function(ok){
					// TODO check ok
					cb.attr('checked', false);
					cb.prop('checked', false);
					me.edit_next();
					//delete me.media_items[mediainfo_id] ;
					me.load_media_items([mediainfo_id]);
				});
			} ) ;

		} ,

		is_prominent : function() {
			return $('#sdc_prominent').is(":checked");
		} ,

		// `file` WITHOUT File: prefix!
		get_mediainfo_id_for_file : function ( file , callback ) {
			let me = this ;
			if ( typeof me.mid_cache[file] != 'undefined') {
				return callback ( me.mid_cache[file] ) ;
			}
			$.get('/w/api.php',{
				action:'query',
				prop:'info',
				titles:"File:"+file,
				format:'json'
			},function(d){
				let mid = -1 ;
				$.each ( d.query.pages , function ( page_id , page_info ) { mid = page_id } ) ;
				if ( mid == -1 ) mid = '' ;
				else mid = 'M'+mid ;
				me.mid_cache[file] = mid ;
				callback(mid);
			},'json');
		} ,

		get_wikidata_token : function ( callback ) {
			let params = { action : 'query' , meta : 'tokens' , format : 'json' } ;
			let api = new mw.ForeignApi( 'https://www.wikidata.org/w/api.php' );
			api.post(params).done(function(d) {
				callback(d.query.tokens.csrftoken);
			} ) ;
		} ,

		get_token : function ( callback ) {
			$.post ( '/w/api.php' , {
				action : 'query' ,
				meta : 'tokens' ,
				format : 'json' ,
			} , function ( d ) {
				callback(d.query.tokens.csrftoken);
			} ) ;
		} ,

		does_item_have_property_target : function ( item , property , target_item_id ) {
			let statement_present = false ;
			$.each ( ((item.statements||item.claims||{})[property]||[]) , function ( dummy , statement ) {
				if ( ((((statement||{}).mainsnak||{}).datavalue||{}).value||{}).id == target_item_id ) {
					statement_present = true ;
				}
			} ) ;
			return statement_present ;
		} ,

		add_item_statement_to_item : function ( item_id , property , target_item_id , prominent , callback ) {
			let me = this ;
			let value = {'entity-type':'item',id:target_item_id} ;
			let data = {claims:[{mainsnak:{snaktype:"value",property:property,datavalue:{value:value,type:'wikibase-entityid'}},type:"statement",rank:"normal"}]} ;
			let summary = 'SDC: added [[d:Special:EntityPage/'+property+']] => [[d:Special:EntityPage/'+target_item_id+']]' ;
			if ( prominent ) {
				data.claims[0].rank = 'preferred' ;
				summary += ', prominent' ;
			}

			// Check if statement already present
			if ( typeof me.media_items[item_id] != 'undefined' ) {
				if ( me.does_item_have_property_target(me.media_items[item_id],property,target_item_id) ) return callback(true);
			}

			me.get_token ( function ( token ) {
				let params = {
					action:'wbeditentity',
					id:item_id,
					data:JSON.stringify(data),
					token:token,
					summary:summary,
					format:'json'
				} ;
				$.post('/w/api.php',params,function(d){
					callback(true);
				},'json');
			} );
		} ,

		set_target_item : function ( item ) {
			let me = sdc ;
			me.target_item = item ;

			let html = "" ;
			html += "<small>Target item: [<a href='https://www.wikidata.org/wiki/"+item.q+"' target='_blank'>"+item.q+"</a>]</small><br/><div id='sdc_target_item_label'></div>";
			html = "<div style='margin:0.2rem;'>"+html+"</div>" ;
			$('#sdc_target_item_display').html(html).show();
			$('#sdc_target_item_label').text(item.label);

			me.show_action_button();
			me.show_wikidata_infobox_button();

			me.update_main_position();
			me.windowManager.closeWindow( me.dialog );
		} ,

		get_property : function () {
			return $('#sdc_property').val();
		} ,

		open_dialog : function () {
			this.windowManager.openWindow( this.dialog );
			if ( $('#sdc_dialog_query').val() == '' ) {
				$('#sdc_dialog_query').val(mw.config.values.wgTitle) ;
				$('#sdc_dialog_query').keyup();
			}
			setTimeout(function(){$('#sdc_dialog_query').focus()},500);
		} ,

		show_main_element : function () {
			let me = this ;
			let html = "<div id='sdc_main' style='position:fixed;right:0px;bottom:0px;background-color:#FEF6E7;box-shadow:0 2px 4px rgba(0,0,0,0.5);font-size:.75em;padding:5px;'>" ;
			html += "<a href='#' id='sdc_main_button' accesskey='s' title='Structured Data on Commons tagger [shortcut:S]'>SDC</a>" ;
			html += "<div id='sdc_options' style='display:none'>" ;

			html += "<div><span id='sdc_cb_all'></span> <span id='sdc_cb_toggle'></span> <span id='sdc_cb_none'></span></div>" ;
			/*
			html += "<div>Check " ;
			html += "<a href='#' id='sdc_cb_all'>all</a> | " ;
			html += "<a href='#' id='sdc_cb_toggle'>toggle</a> | " ;
			html += "<a href='#' id='sdc_cb_none'>none</a>" ;
			html += "</div>" ;
			*/

			html += "<div style='margin:0.2em;'>Property "
			html += "<select id='sdc_property'>" ;
			$.each ( me.properties , function ( property , label ) {
				html += "<option value='"+property+"'" ;
				if ( property == 'P180' ) html += ' selected' ;
				html += ">"+label+" ["+property+"]</option>" ;
			} ) ;
			html += "</select>" ;
			html += "</div>" ;

			html += "<div id='sdc_target_item' style='margin:2px;padding:2px;border:1px solid #DDD;border-radius:3px'>";
			html += "<div id='sdc_target_item_display' style='display:none;'></div>" ;
			html += "<div id='sdc_target_item_button' style='text-align:center;'></div>" ;
			html += "<div id='sdc_add_wikidata_infobox_button' style='text-align:center;'></div>" ;
			html += "</div>" ;

			html += "<div id='sdc_action' style='text-align:center'>" ;
			html += "<div><label><input type='checkbox' id='sdc_prominent' /> Prominent</label></div>" ;
			html += "<div id='sdc_action_button_container'><i>Set a target item to perform an action</i></div>" ;
			html += "</div>" ;

			html += "</div>" ;
			html += "</div>" ;
			$('body').append(html);
			$('#sdc_main_button').click(function(){ me.toggle_main(); return false; });

			let button_all = new OO.ui.ButtonWidget( { label: 'All',href: '#' } ) ;
			let button_toggle = new OO.ui.ButtonWidget( { label: 'Toggle',href: '#' } ) ;
			let button_none = new OO.ui.ButtonWidget( { label: 'None',href: '#' } ) ;
			$('#sdc_cb_all ').html(button_all.$element);
			$('#sdc_cb_toggle').html(button_toggle.$element);
			$('#sdc_cb_none').html(button_none.$element);
			$('#sdc_cb_all a.oo-ui-buttonElement-button').click(function(){ $('input.sdc_checkbox').attr('checked', true); return false; });
			$('#sdc_cb_toggle a.oo-ui-buttonElement-button').click(function(){ $('input.sdc_checkbox').click(); return false; });
			$('#sdc_cb_none a.oo-ui-buttonElement-button').click(function(){ $('input.sdc_checkbox').attr('checked', false); return false; });

			let button_sti = new OO.ui.ButtonWidget( { label: 'Set target item',href: '#' } ) ;
			$('#sdc_target_item_button ').html(button_sti.$element);
			$('#sdc_target_item_button a.oo-ui-buttonElement-button').click(function(){me.open_dialog(me);});

			$('#sdc_property').change(function(){me.show_action_button()});

			me.update_main_position();
			setTimeout(me.update_main_position,200);
		} ,

		toggle_main : function () {
			this.active = !this.active ;
			if ( this.active ) {
				this.show_checkboxes();
				$('#sdc_options').show();
			} else {
				$('div.sdc_checkbox_container').remove();
				$('#sdc_options').hide();
			}
			this.update_main_position();
		} ,

		cache_file_media_ids : function ( files ) {
			let me = this ;
			let chunks = [ [] ] ;
			let MAX_CHUNK_SIZE = 50 ;
			$.each ( files , function ( dummy , file ) {
				if ( chunks[chunks.length-1].length < MAX_CHUNK_SIZE ) {
					chunks[chunks.length-1].push ( file ) ;
				} else {
					chunks.push ( [ file ] ) ;
				}
			} ) ;
			me.wikidata_items_to_load = [] ;
			$.each ( chunks , function ( dummy , chunk ) {
				me.cache_file_media_ids_chunk(chunk) ;
			} ) ;
		} ,

		cache_file_media_ids_chunk : function ( files ) {
			let me = this ;
			let params = {
				action:'query',
				prop:'info',
				titles:"File:"+files.join("|File:"),
				format:'json'
			} ;
			$.post('/w/api.php',params,function(d){
				let to_load = [] ;
				$.each ( d.query.pages , function ( page_id , page_info ) {
					if ( page_id == -1 ) return ; // Paranoia
					let mid = 'M'+page_id ;
					let file = page_info.title.replace(/^File:/,'').replace(/ /g,'_');
					me.mid_cache[file] = mid ;
					me.mid2file[mid] = file ;
					to_load.push ( mid ) ;
				} ) ;
				me.load_media_items(to_load);
			},'json');
		} ,

		load_media_items : function ( mids ) {
			if ( mids.length == 0 ) return ;
			let me = this ;
			let params = {
				action:'wbgetentities',
				ids:mids.join('|'),
				format:'json'
			} ;
			$.post('/w/api.php',params,function(d){
				$.each ( d.entities , function ( mid , mi ) {
					me.media_items[mid] = mi ;
					me.update_file_sdc(mid);
				} ) ;
				me.load_wikidata_items();
			},'json');
		} ,

		load_wikidata_items : function ( callback ) {
			let me = this ;
			let to_load = [] ; // Should never be more than 50
			$.each ( me.wikidata_items_to_load , function ( dummy , q ) {
				if ( typeof me.wikidata_item_labels[q] != 'undefined' ) {
					me.update_q_labels(q);
					return ;
				}
				if ( $.inArray(q,to_load) !== -1 ) return ;
				to_load.push(q) ;
			} ) ;
			me.wikidata_items_to_load = [] ;
			if ( to_load.length == 0 ) return ;
			$.getJSON('https://www.wikidata.org/w/api.php?callback=?',{
				action:'wbformatentities',
				ids:to_load.join('|'),
				format:'json'
			},function(d){
				$.each ( d.wbformatentities , function ( q , html ) {
					me.wikidata_item_labels[q] = html.replace('<a ','<a target="_blank" ') ;
					me.update_q_labels(q);
				} ) ;
				if ( typeof callback != 'undefined' ) callback() ;
			});
		} ,

		update_q_labels : function ( q ) {
			let me = this ;
			$('td.q_to_load[q="'+q.replace(/'/g, "&#39;")+'"]').removeClass('q_to_load').html(me.wikidata_item_labels[q]);
		} ,

		get_wikidata_item_label_td : function ( q , prominent ) {
			let me = this ;
			let style = 'vertical-align:top;' ;
			if ( prominent ) style += 'font-weight:bold;' ;
			if ( typeof me.wikidata_item_labels[q] == 'undefined' ) {
				return "<td class='q_to_load' q='"+q+"' style='"+style+"'>"+q+"</td>" ;
			} else {
				return "<td q='"+q+"' style='"+style+"'>"+me.wikidata_item_labels[q]+"</td>" ;
			}
		} ,

		update_file_sdc : function ( mid ) {
			let me = this ;
			let file = me.mid2file[mid] ;
			if ( typeof file == 'undefined' ) return ;
			let mi = me.media_items[mid] ;
			if ( typeof mi == 'undefined' ) return ;
			let out = [] ;
			$.each ( (mi.statements||{}) , function ( property , statements ) {
				$.each ( statements , function ( dummy , statement ) {
					if ( ((statement.mainsnak||{}).datavalue||{}).type != 'wikibase-entityid' ) return ;
					me.wikidata_items_to_load.push(property);
					let wd_item_id = statement.mainsnak.datavalue.value.id ;
					me.wikidata_items_to_load.push(wd_item_id);
					out.push({property:property,target:wd_item_id,prominent:statement.rank=='preferred'});
				} ) ;
			} ) ;

			let h = '' ;
			if ( out.length == 0 ) {
				let html = "<div class='sdc_statements' mid='"+mid+"' style='font-size:9pt;width:100%;text-align:left;'>" ;
				html += "<div style='color:#b32424;font-weight:bold;'>No SDC</div>" ;
				html += "</div>" ;
				h = $(html);
			} else {
				let html = "<div class='sdc_statements' mid='"+mid+"' style='font-size:9pt;width:100%;text-align:left;'>" ;
				html += "<table style='border-spacing:0;border-collapse:collapse;'><tbody>" ;
				$.each ( out , function ( dummy , row ) {
					html += "<tr>" ;
					html += me.get_wikidata_item_label_td(row.property,false) ;
					html += me.get_wikidata_item_label_td(row.target,row.prominent) ;
					html += "</tr>" ;
				} ) ;
				html += "</tbody></table></div>" ;
				h = $(html);
				h.find('td').css({padding:'1px','text-align':'left'});
			}

			let element = $($('input.sdc_checkbox[file="'+me.sanitize_file_attribute(file)+'"]').parent()) ;
			$('div.sdc_statements[mid="'+mid+'"').remove();
			element.after(h);
		} ,

		sanitize_file_attribute : function ( file ) {
		 	return encodeURIComponent(file).replace(/'/g, "&#39;") ;
		 } ,

		show_checkboxes : function () {
			let me = this ;
			let files = [] ;
			$('a.image, a.mw-file-description').each(function(num,a){
				let file = decodeURIComponent($(a).attr('href')).replace(/^.+\/File:/,'') ;
				if ( $.inArray(file,me.exclude_files) > -1 ) return ; // Bad file!
				if ( $(a).parents('#wdinfobox').length > 0 ) return ; // In infobox
				files.push(file);
				let html = "<div class='sdc_checkbox_container' style='display:flex'>" ;
				html += "<div><input type='checkbox' class='sdc_checkbox' file='"+me.sanitize_file_attribute(file)+"' /></div>" ;
				html += "</div>" ;
				$(a).after(html);
			});
			me.cache_file_media_ids(files);
		}

	} ;

	sdc.init();
});