User:JesseW/conflicting superclass warnings.js

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)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
( function ( mw ) {
	// TODO: Ideal functionality:
	// When a new value is selected for either the instance-of or subclass-of
	// properties, BEFORE it is saved, the system should look up
	// the existing superclasses (via instance-of if that is what is being
	// changed, or subclass-of otherwise, in both cases handling multiple existing values)
	// and the superclasses of the newly selected value, and check for conflicts.
	// If conflicts are found, display a notification of the appropriate form
	// (always ending with: "Is this right?" or "Please check this", or something; and having the Q numbers shown and linked):
	// - This is already a "natural object" (Q123). Adding "instance of" 
	//   "boat" (Q123) makes it also an "artificial entity" (Q123).
	// - This is already a subclass of "natural object" (Q123). Adding "subclass of"
	//   "boat" (Q123) makes it a subclass of "artificial entity" (Q123), too.
	// - This is currently a "natural object". Changing "instance of" "tree" to 
	//   "rocket" makes it both a "natural object" and an "artificial entity".
	// - This is currently a "natural object". Changing "instance of" "tree" to 
	//   "rocket" makes it instead an "artificial entity".
	// - This is currently a subclass of "natural object". Changing "subclass of"
	//   "tree" to "rocket" makes it a subclass of both "natural object" and "artificial entity".
	// - This is currently a subclass of "natural object". Changing "subclass of"
	//   "tree" to "rocket" makes it instead a subclass of "artificial entity".
	
	// Currently implemented features include checking subclass-of and instance-of
	// when a replacement value is selected. Additions are not checked, and it always uses
	// the original value from when the page was loaded (so multiple edits 
	// without reloading the page will give wrong results). It shows all the
	// conflicts, but doesn't distinguish all the cases listed above.
	
	var CONFLICTS = [
		[ 26907166, 58416391], // temporal entity, spatial entity
		[112276019, 7048977],  // physical entity, non-physical entity 
		[ 14897293, 96196524], // fictional entity, current entity
		[  3249551, 4406616],  // process, concrete object
		[ 54989186, 58416391], // mental state, spatial entity
		[ 35145263, 35145743], // natural geographic object, human-made geographic object
		// The following are listed as "disjoint union of" "entity" 
		// https://www.wikidata.org/wiki/Q35120#P2738
		[ 23958946, 23960977], // individual entity, (meta)class
		[ 15893266, 96196524], // former entity, current entity
		[ 29651224, 16686448], // natural object, artificial entity
		[   488383, 937228, 3249551], // object, property, process
		[ 30241068, 3799040],  // observable entity, unobservable entity
	];

	var api;
	var maxdepth = 20;

	var labelCache = {};
	function getLabels(itemIds) {
		var idsToLoad = $.grep(itemIds, function (i) {return !labelCache[i]; })
			.map(function (x) { return 'Q' + x; }).join('|');
		var args = {action: 'wbgetentities', props: ['labels'], languages: 'en',
			ids: idsToLoad};
		return (idsToLoad ? api.get(args) : $.Deferred().resolve())
			.then(function (data) {
				var rtn = {};
				itemIds.forEach(function (x) {
					fresh = ((data || {}).entities || {})['Q' + x];
					if (fresh) { 
						labelCache[x] = fresh.labels.en.value;
						console.log('added to label cache ' + x + " = " + labelCache[x]);
					}
					rtn[x] = labelCache[x];
				});
				console.log('labels returned: ' + JSON.stringify(rtn));
				return rtn;
			});
	}

	var claimCache={};
	function getClaims(itemId, property) {
		var key = itemId + ":" + property;
		if (claimCache[key]) {
			console.log('got claims from cache for: ' + key);
			return $.Deferred().resolve().then(function () {
				return claimCache[key];
			});
		}
		//console.log('making getClaims api call: ' + key);
		return api.get( {
				action: 'wbgetclaims',
				entity: 'Q' + itemId,
				property: property,
				props: [ ]
		} ).then( function ( data ) {
			claimCache[key] = (data.claims[property] || [])
			   .filter(function (x) { return x.mainsnak.datavalue; })
			   .map(function (x) { return x.mainsnak.datavalue.value['numeric-id'];});
			console.log('added to claim cache ' + key + " = " + claimCache[key]);
			return claimCache[key];
		} );
	}

	var superclassesCache = {};
	function getSuperclasses(rootItemId) {
		if (superclassesCache[rootItemId]) {
			console.log('got superclasses from cache for ' + rootItemId);
			return $.Deferred().resolve().then(function () {
				return superclassesCache[rootItemId];
			});
		}
		console.log('getting superclasses for ' + rootItemId);
		var data = {};
		var children={};
		var visiting={};
		function visit(itemId, depth) {
			if (visiting[itemId]) {
				return $.Deferred().resolve(depth);
			}
			//console.log("visiting " + itemId + " at depth " + depth);
			visiting[itemId] = true;
			if (children[itemId]) {
				visiting[itemId] = false;
				return $.Deferred().resolve(depth + 1);
			} else if (depth <= maxdepth) {
				return getClaims(itemId, 'P279').then(function (childIds) {
					childIds.forEach(function (childId) {
						data[childId] = true;
					});
					return $.when.apply(null, childIds.map(function (childId) {
						return visit(childId, depth +1);
					})).then(function () {
						children[itemId] = childIds;
						visiting[itemId] = false;
						return depth + 1;
					});
				});
			}
		}
		return visit(rootItemId, 1).then(function () {
			superclassesCache[rootItemId] = data;
			return data;
		});
	}

	function checkForConflicts(n, o) {
		var problems = {"old": {}, "new": {}, "has": false};
		CONFLICTS.forEach(function (conflict) {
			conflict.forEach(function (q) {
				//console.log('checking ' + q + ": " + o[q] + "; " + n[q]);
				if (o[q]) {
					conflict.forEach(function (q2) {
						//console.log('checking for conflict ' + q2 + ": " + o[q2] + "; " + n[q2]);
						if (q != q2 && n[q2]) {
							problems.has = true;
							problems.old[q] = true;
							problems.new[q2] = true;
						}
					});
				}
			});
		});
		return problems;
	}

	function humanizeList(list) {
		// Adapted from https://github.com/johno/humanize-list/blob/master/index.js
		var listLength = list.length;
		if (listLength === 1) {
			return list[0];
		}
		var humanizedList = '';
		for (var i = 0; i < listLength; i++) {
		    if (i === listLength - 1) {
		    	humanizedList += ' and ';
		    } else if (i !== 0) {
		      humanizedList += ', ';
		    }
		    humanizedList += list[i];
		}
		return humanizedList;
	}

	function getOriginalId(element) {
		return $("a", element)[0].href.split("/").slice(-1)[0].substr(1);
	}
	function keys(obj) {
		return $.map(obj, function (v, k) {return k;});
	}

	function makeDisplayError(originalId, newId, propertyLabel) {
		function displayError(problems) {
			var itemsToLabel = [originalId, newId].concat(keys(problems.old), keys(problems.new));
			getLabels(itemsToLabel).then(function (labels) {
				function labelAndLink(unused, id) {
					return "\"<a href=\"https://www.wikidata.org/wiki/Q" + id + "\">" + labels[id] + "</a>\"";
				}
				var message = $("<span>This is currently " + propertyLabel + " of " + 
					humanizeList($.map(problems.old, labelAndLink)) + "." +
					" Changing " + labelAndLink(null, originalId) + " to " + labelAndLink(null, newId) +
					" makes it " + propertyLabel + " of " +
					humanizeList($.map(problems.new, labelAndLink)) +
					". Is that right?</span>");
				mw.notify(message, {autoHide: false});
			});
		}
		return displayError;
	}
	
	function makeCheckFunction(displayError) {
		return function (n, o) {
			problems = checkForConflicts(n, o);
			if (problems.has) {
				displayError(problems);
			} else {
				mw.notify("No problems found.");
			}
		};
	}
	mw.loader.using('mediawiki.api', function() {
		//console.log('modules loaded');
		api = new mw.Api();
		//doTest();
		var currentPageId=$(".wikibase-title-id").text().split(")")[0].substr(2);

		// Possibility for checking added values:
		// $("#P31").on("entityselectorselected", ".wikibase-snakview-value",

		$("#P279 .wikibase-snakview-value").each(function (index, element) {
			var originalId = getOriginalId(element);
			$(element).on("entityselectorselected", function (event, qid) {
				var newId=qid.substr(1);
				$.when(getSuperclasses(newId), getSuperclasses(currentPageId))
				.then(makeCheckFunction(makeDisplayError(originalId, newId, "a subclass")));
			});
		});
		$("#P31 .wikibase-snakview-value").each(function (index, element) {
			var originalId = getOriginalId(element);
			$(element).on("entityselectorselected", function (event, qid) {
				var newId=qid.substr(1);
				var currentClasses = getClaims(currentPageId, "P31").then(function (claims) {
					return $.when.apply(null, claims.map(getSuperclasses)).then(function () {
						all_superclasses = {};
						$.each(arguments, function (idx, ss) {
							$.each(ss, function (s) {
								// getLabels([s]).then(function (labels) {
								// 	console.log('adding current instance of ' + labels[s] + "(" + s + ")");
								// });
								all_superclasses[s] = true;
							});
						});
						return all_superclasses;
					});
				});
				$.when(getSuperclasses(newId), currentClasses)
				.then(makeCheckFunction(makeDisplayError(originalId, newId, "an instance")));
			});
		});
	});
	
	function doTest() {
		// probably broken at this point
		getSuperclasses(35872).then(function (data) {
			//console.log(data); 
			getLabels($.map(data, function (v,k) {return k;})).then(console.log);
			getSuperclasses(10884).then(function (data2) {
				getLabels($.map(data2, function (v,k) {return k;})).then(console.log);
				console.log(checkForConflicts(data, data2));
			});
		});
	}
} 
( mw ) 
);

// // Random commented-out leftovers:

		//mw.hook('wikibase.statement.saved').add(do_check);
		//mw.hook( 'wikibase.entityPage.entityView.rendered' ).add( init );

	// function getSuperclassesOfStatementValue(s) {
	// 	return getSuperclasses(s._claim._mainSnak._value._serialization.substr(1));
	// }

	// function do_check(entityId, statementId, oldS, newS) {
	// 	thing={oldS: oldS, newS:newS};
	// 	// A change in subclassOf
	// 	if (newS._claim._mainSnak._propertyId == "P279") {
	// 		if (oldS != null) {
	// 			$.when(
	// 				getSuperclassesOfStatementValue(newS),
	// 				getSuperclassesOfStatementValue(oldS)
	// 			).then(function (n, o) {
	// 				thing.newSC = n;
	// 				thing.oldSC = o;
	// 				thing.problems = checkForConflicts(n, o);
	// 				if (thing.problems.length) {
	// 					getLabels(thing.problems[0]).then(function (labels) {
	// 						mw.notify("Problems! " + labels, {autoHide: false});
	// 					});
	// 				}
	// 			});
	// 		}
	// 	}
	// }

		// getClaims(mw.config.values.wbEntityId.substr(1), 'P31', function (childIds) {
		// 	thing.instanceOf = childIds.map(function (childId) { return getSuperclasses(childId); });
		// });
		// thing.subclassOf = getSuperclasses(mw.config.values.wbEntityId.substr(1));