User:Ricordisamoa/FlagSense.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.
/* <nowiki>
 *
 * FlagSense
 * @author [[User:Ricordisamoa]]
 *
 * the current action to be made in the edit page is stored in the sessionStorage
*/
/* global mediaWiki, jQuery */
( function ( mw, $ ) {
	'use strict';
	var FlagSense = {

		storage: sessionStorage,

		messages: {
			en: {
				'add-template-VVA': 'Add {{Vector version available}} to the file description page',
				'add-template-toSVG': 'Add {{Convert to SVG|flag}} to the file description page',
				'add-template-toPNG': 'Add {{Convert to PNG}} to the file description page',
				'add-template-ssPNG': 'Add {{SupersededPNG}} to the file description page',
				'alreadySVG': 'This is already a vector image.\nIt doesn\'t need to be converted in SVG again.',
				'vvaConfirmToSVG': 'This page already contains the template {{Vector version available}}\n' +
					'To replace {{Vector version available}} with {{Convert to SVG}}, press OK;\n' +
					'To skip editing this page, press UNDO',
				'vvaConfirmToPNG': 'This page already contains the template {{Vector version available}}\n' +
					'To replace {{Vector version available}} with {{Convert to PNG}}, press OK;\n' +
					'To skip editing this page, press UNDO',
				'vvaConfirmNew': 'This page already contains the template {{Vector version available}}\n' +
					'To replace the existing istance with this new one, press OK;\n' +
					'To skip editing this page, press UNDO',
				'svgAlternative': 'The name of the SVG alternative (without File: prefix):',
				'pngAlternative': 'The name of the PNG alternative (without File: prefix):',
				'alreadySameCategory': 'FlagSense detected that, by a size of $1×$2, the proper category for this image was: «$3».\n' +
					'However, the image is already in this category.',
				'alreadyOtherCategory': 'FlagSense detected that, by a size of $1×$2, the proper category for this image would be: «$3».\n' +
					'However, the image is already categorized by aspect ratio.\nDo you anyway want to replace the old category with this one?',
				'noMatchFound': 'FlagSense tried to search the proper category for this image, but no matches were found.\n\n' +
					'Details:\nimage size: $1×$2\naspect ratio: $3/$4'
			},
			it: {
				'add-template-VVA': 'Aggiungi {{Vector version available}} alla pagina di descrizione del file',
				'add-template-toSVG': 'Aggiungi {{Convert to SVG|flag}} alla pagina di descrizione del file',
				'add-template-toPNG': 'Aggiungi {{Convert to PNG}} alla pagina di descrizione del file',
				'add-template-ssPNG': 'Aggiungi {{SupersededPNG}} alla pagina di descrizione del file',
				'alreadySVG': 'Questa è già una immagine vettoriale.\nNon necessita di essere convertita in SVG di nuovo.',
				'vvaConfirmToSVG': 'Questa pagina contiene già il template {{Vector version available}}\n' +
					'Per sostituire {{Vector version available}} con {{Convert to SVG}}, premi OK;\n' +
					'Per evitare di modificare questa pagina, premi ANNULLA',
				'vvaConfirmToPNG': 'Questa pagina contiene già il template {{Vector version available}}\n' +
					'Per sostituire {{Vector version available}} con {{Convert to PNG}}, premi OK;\n' +
					'Per evitare di modificare questa pagina, premi ANNULLA',
				'vvaConfirmNew': 'Questa pagina contiene già il template {{Vector version available}}\n' +
					'Per sostituire la vecchia istanza con quella nuova, premi OK;\n'+
					'Per evitare di modificare questa pagina, premi ANNULLA',
				'svgAlternative': 'Il nome dell\'alternativa SVG (senza prefisso File:)',
				'pngAlternative': 'Il nome dell\'alternativa PNG (senza prefisso File:)',
				'alreadySameCategory': 'FlagSense ha rilevato che, con una dimensione di $1×$2, la categoria appropriata per questa immagine sarebbe stata: «$3».\n' +
					'Tuttavia, l\'immagine è già in questa categoria.',
				'alreadyOtherCategory': 'FlagSense ha rilevato che, con una dimensione di $1×$2, la categoria appropriata per questa immagine sarebbe: «$3».\n' +
					'Tuttavia, l\'immagine è già categorizzata per proporzioni.\nVuoi comunque sostituire la vecchia categoria con questa?',
				'noMatchFound': 'FlagSense ha provato a cercate la categoria appropriata per questa immagine, ma non ha trovato corrispondenze.\n\n' +
					'Dettagli:\ndimensioni immagine: $1×$2\nproporzioni: $3/$4'
			}
		},

		easyInsert: function ( options ) {
			if ( options.regex.test( this.$text.val() ) === false ) {
				// add the code at the top
				this.$text.val( options.code + '\n' + this.$text.val() );
			} else if ( confirm( this.getMessage( options.confirm ) ) === true ) {
				// replace the old code with the new one
				this.$text.val( this.$text.val().replace( options.regex, options.code ) );
			} else {
				// go back to the file description page
				window.history.back();
			}
			// precompiled edit summary
			this.$summary.val( options.summary );
			this.closeEdit();
		},

		start: function () {
			var self = this;
			// parse the file extension from the page name
			self.fileExtension = mw.config.get( 'wgPageName' ).split( '.' ).slice( -1 )[0].toLowerCase();

			if( $( '#editform' ).length > 0 && self.storage['FlagSense-currentAction'] ) {
				// the user is editing the page and the current page was previously marked for editing

				self.$text = $( '#wpTextbox1' );   // the edit textarea
				self.$summary = $( '#wpSummary' ); // the summary of changes
				self.$minor = $( '#wpMinoredit' ); // the checkbox for marking a minor edit

				switch ( self.storage['FlagSense-currentAction'] ) {

					// the user is going to mark this image as to be converted to SVG
					case 'toSVG':
						self.easyInsert( {
							code: '{{Convert to SVG|flag}}',
							regex: self.regex.VVA,
							confirm: 'vvaConfirmToSVG',
							summary: 'Added template {{[[Template:Convert to SVG|Convert to SVG]]|flag}} ' +
								'using [[User:Ricordisamoa/FlagSense|FlagSense]]'
						} );
						break;

					// the user is going to mark this image as to be converted to PNG
					case 'toPNG':
						self.easyInsert( {
							code: '{{Convert to PNG}}',
							regex: self.regex.VVA,
							confirm: 'vvaConfirmToPNG',
							summary: 'Added template {{[[Template:Convert to PNG|Convert to PNG]]}} ' +
								'using [[User:Ricordisamoa/FlagSense|FlagSense]]'
						} );
						break;

					// the user is going to point out a vector version of this image
					case 'VVA':
						self.easyInsert( {
							code: '{{Vector version available|' +
								prompt( self.getMessage( 'svgAlternative' ) ) + '}}',
							regex: self.regex.VVA,
							confirm: 'vvaConfirmNew',
							summary: 'Added template {{[[Template:Vector version available|Vector version available]]}} ' +
								'using [[User:Ricordisamoa/FlagSense|FlagSense]]'
						} );
						break;

					// the user is going to point out a PNG version of this image
					case 'ssPNG':
						self.easyInsert( {
							code: '{{SupersededPNG|' +
								prompt( self.getMessage( 'pngAlternative' ) ) + '}}',
							regex: self.regex.VVA,
							confirm: 'vvaConfirmToPNG',
							summary: 'Added template {{[[Template:SupersededPNG|SupersededPNG]]}} ' +
								'using [[User:Ricordisamoa/FlagSense|FlagSense]]'
						} );
						break;

					case 'cat':
						var cat = self.storage['FlagSense-category'],
							accuracy = parseFloat( self.storage['FlagSense-accuracy'] );
						if ( self.regex.aspectRatioCat.test( self.$text.val() ) ) {
							self.regex.aspectRatioCat.lastIndex = 0;
							// precompiled summary
							self.$summary.val(
								'removed [[:Category:' +
								self.regex.aspectRatioCat.exec( self.$text.val() )[0].toString()
								.replace( /\[\[Category:/i, '') + '; '
							);
							// replace the old category with the new one
							self.$text.val( self.$text.val().replace( self.regex.aspectRatioCat, '[[Category:' + cat + ']]' ) );
						} else {
							// add the category at the bottom
							self.$text.val(
								self.$text.val() + ( /\n$/.test( self.$text.val() ) ? '' : '\n' ) +
								'[[Category:' + cat + ']]'
							);
						}
						// precompiled summary
						self.$summary.val(
							self.$summary.val() + 'added [[:Category:' + cat + ']] using ' +
							'[[User:Ricordisamoa/FlagSense|FlagSense]]' +
							( accuracy > 0 ? ' with an error of ±' + accuracy : '' )
						);
						self.$minor.prop( 'checked', true );
						self.closeEdit();
						break;

				}

			} else {

				var fileSize = $( '.filehistory > tbody > tr:nth-child(2) > td:nth-child(4) ' )[0]
					.firstChild.nodeValue.split( ' ' ).join( '' ).split( '\u00A0' ).join( '' ).split( '×' ),
				width = parseInt( fileSize[0] ),
				height = parseInt( fileSize[1] ),
				gcd = self.gcd( width, height ), // greatest common divisor of width and height
				ratio = width / gcd + ':' + height / gcd,
				cat = self.getCategoryByAspectRatio( width, height ),
				$box = $( '<div>' )
					.css( {
						float: 'right',
						background: 'whitesmoke',
						padding: '8px'
					} )
					.append( '<h3>FlagSense</h3>' )
					.append( 'Format: ' + self.fileExtension.toUpperCase() );

				if ( self.fileExtension !== 'svg' ) {
					$( '<p>' )
					.attr( 'id', 'FlagSense-nonSVG' )
					.append(
						$( '<button>' )
						.text( 'VVA' )
						.attr( 'id', 'FlagSense-button-VVA' )
						.attr( 'title', self.getMessage( 'add-template-VVA' ) )
						.click( function () {
							self.openEdit( 'VVA' );
						} )
					)
					.appendTo( $box );
				}
				if ( self.fileExtension !== 'png' ) {
					$( '<p>' )
					.attr( 'id', 'FlagSense-nonPNG' )
					.append(
						$( '<button>' )
						.text( 'SupersededPNG' )
						.attr( 'id', 'FlagSense-button-ssPNG' )
						.attr( 'title', self.getMessage( 'add-template-ssPNG' ) )
						.click( function () {
							self.openEdit( 'ssPNG' );
						} )
					)
					.appendTo( $box );
				}
				var imgAttrs = {};
				if ( cat !== null && mw.config.get( 'wgCategories' ).indexOf( cat.cat ) !== -1 ) {
					imgAttrs.src = '//upload.wikimedia.org/wikipedia/commons/thumb/b/be/P_yes.svg/20px-P_yes.svg.png';
					imgAttrs.title = 'already in this category';
				} else if ( cat === null ) {
					imgAttrs.src = '//upload.wikimedia.org/wikipedia/commons/thumb/4/42/P_no.svg/20px-P_no.svg.png';
					imgAttrs.title = 'no proper category available for this image';
				} else {
					imgAttrs.src = '//upload.wikimedia.org/wikipedia/commons/thumb/5/5e/Exclamation.svg/20px-Exclamation.svg.png';
					imgAttrs.title = 'a category has been found' +
						( self.isCategorizedByAspectRatio() ? ' but the image is already categorized by aspect ratio' : '' );
				}
				$( '<img>' )
				.attr( imgAttrs )
				.css( 'background', 'none' )// remove the default checkered background
				.appendTo( $box );
				$( '<p>' )
				.text( 'Aspect ratio: ' )
				.append(
					cat !== null ? [
						$( '<a>' ).text( cat.ratio ).attr( 'href', mw.util.getUrl( 'Category:' + cat.cat ) ),
						( ' with' + ( cat.accuracy > 0 ? ' an error of ±' + Math.round( cat.accuracy * 100000 ) / 100000 : 'out error' ) )
					] : ratio
				)
				.appendTo( $box );
				if ( cat !== null && mw.config.get( 'wgCategories' ).indexOf( cat.cat ) === -1 ) {
					$( '<button>' )
					.text( self.isCategorizedByAspectRatio() ? 'Change category' : 'Add category' )
					.click( function () {
						self.openEdit( 'cat', cat.cat, Math.round( cat.accuracy * 100000 ) / 100000 );
					} )
					.appendTo( $box );
				}
				$( '<button>' )
				.attr( 'disabled', true )
				.append( 'Categorize by elements and colors<br/>(coming soon)' )
				.click( function () {
					var $img = $( '#file a > img' ),
					canvas = $( '<canvas>' )
						.attr( {
							width: $img.attr( 'width' ),
							height: $img.attr( 'height' )
						} )
						.css( 'background', $img.css( 'background' ) ),
					ctx = canvas.get( 0 ).getContext( '2d' ),
					imageObj = new Image();
					imageObj.crossOrigin = 'Anonymous';// allow CORS images
					imageObj.onload = function () {
						ctx.drawImage( imageObj, 0, 0 );
						canvas.click( function () {
							alert( 'This feature is not available yet.\nPlease check soon.' );
						} );
					};
					imageObj.src = $img.attr( 'src' );// get the URL of the in-page thumbnail
					$img.parent().replaceWith( canvas );// replace the in-page thumb (and the link)
				} )
				.appendTo( $box );

				$( '#file' ).prepend( $box );

				if ( $( 'p#FlagSense-nonSVG' ).length > 0 ) {
					self.isTemplated( self.regex.toSVG, function ( result ) {
						if ( result === false ) {
							$( '<button>' )
							.text( 'Should be SVG' )
							.attr( 'id', 'FlagSense-button-toSVG' )
							.attr( 'title', self.getMessage( 'add-template-toSVG' ) )
							.click( function () {
								self.openEdit( 'toSVG' );
							} )
							.prependTo( 'p#FlagSense-nonSVG' );
						}
					} );

					self.isTemplated( self.regex.VVA, function ( result ) {
						if ( result === true ) {
							$( 'button#FlagSense-button-VVA' )
							.attr( 'title', $( 'button#FlagSense-button-VVA' ).attr( 'title' ) + ' (change)' )
							.append( ' (change)' );
						}
					} );
				}

				if ( $( 'p#FlagSense-nonPNG' ).length > 0 ) {
					self.isTemplated( self.regex.toPNG, function ( result ) {
						if ( result === false ) {
							$( '<button>' )
							.text( 'Should be PNG' )
							.attr( 'id', 'FlagSense-button-toPNG' )
							.attr( 'title', self.getMessage( 'add-template-toPNG' ) )
							.click( function () {
								self.openEdit( 'toPNG' );
							} )
							.prependTo( 'p#FlagSense-nonPNG' );
						}
					} );

					self.isTemplated( self.regex.ssPNG, function ( result ) {
						if ( result === true ) {
							$( 'button#FlagSense-button-ssPNG' )
							.attr( 'title', $( 'button#FlagSense-button-ssPNG' ).attr( 'title' ) + ' (change)' )
							.append( ' (change)' );
						}
					} );
				}

			}

		},

		getCategoryByAspectRatio: function ( width, height ) {
			// see https://commons.wikimedia.org/wiki/Category:Flags_by_aspect_ratio
			var ratios = [
				'1:1', '2:1', '3:2', '4:3', '5:3', '18:11',
				'18:13', '20:11', '25:14', '8:5'
			],
			paragorns = [];
			$.each( ratios, function ( index, ratio ) {
				var rs = ratio.split( ':' ),
					// absolute difference between the two ratios
					diff = Math.abs( parseInt( rs[0] ) / parseInt( rs[1] ) - width / height );
				console.log( 'FlagSense: examining ratio: ' + rs.join( '/' ) + '; difference is: ' + diff );
				if ( diff < 0.04 ) {
					paragorns.push( [ratio, diff] );
				}
			} );
			if ( paragorns.length === 0 ) {
				return null;
			}
			console.log( 'FlagSense: unordered ratios: ' + paragorns );
			// sort all ratios whose difference is lower than 0.04
			var paragorn = paragorns.sort( this.sortRatios )[0];
			// the first is the most appropriate
			console.log( 'FlagSense: most appropriate ratio: ' + paragorn );
			var ratio = paragorn[0],
				accuracy = paragorn[1], // the difference between the flag's aspect ratio and the closest existing category
				cat = 'Flags with an aspect ratio of ' + ratio; // proper category by aspect ratio
			if ( ratio === '1:1' ) {
				cat = 'Square flags';
			}
			if ( this.fileExtension === 'svg' ) {
				if ( ratio === '1:1' ) {
					cat = 'SVG square flags';
				} else {
					cat = cat.replace( /^Flags/, 'SVG flags' );
				}
			}
			if ( ratio === '3:2' ) {
				if ( this.fileExtension === 'png' ) {
					cat = 'PNG flags - aspect ratio of 3:2';
				} else if ( this.fileExtension === 'gif' ) {
					cat = 'GIF flags with aspect ratio of 3:2';
				}
			}
			return {
				ratio: ratio,
				cat: cat,
				accuracy: accuracy
			};
		},

		sortRatios: function ( a , b ) {
			return a[1] - b[1]; // sort two aspect ratios basing on the accuracy level
		},

		isTemplated: function ( regex, callback ) {
			$.get( mw.util.wikiScript(), { title: mw.config.get( 'wgPageName' ), action: 'raw' } )
			.done( function ( data ) {
				callback( regex.test( data ) );
			} );
		},

		gcd: function ( a, b ) { // recursive function for Greatest Common Divisor
			if ( b ) {
				return this.gcd( b, a % b );
			} else {
				return Math.abs( a );
			}
		},

		getMessage: function ( key ) {
			var params = Array.prototype.slice.call( arguments, 1 );
			return new mw.Message( this.messages, key, params ).text();
		},

		isCategorizedByAspectRatio: function () {
			$.each( mw.config.get( 'wgCategories' ), function ( index, cat ) {// loop in all categories of the image
				if ( cat.indexOf( 'flag' ) !== -1 && cat.indexOf( 'aspect ratio' ) !== -1 ) {
					return true;// there is a category by aspect ratio
				}
			} );
			return false;// there aren't
		},

		openEdit: function ( action, category, accuracy ) {
			// mark this page for adding the template (just later)
			this.storage['FlagSense-currentAction'] = action;
			if ( category !== null && accuracy !== null ) {
				this.storage['FlagSense-category'] = category;
				this.storage['FlagSense-accuracy'] = accuracy;
			}
			document.location = $( '#ca-edit a' ).attr( 'href' ); // go to edit page
		},

		closeEdit: function () {
			// reset the storage
			this.storage.removeItem( 'FlagSense-currentAction' );
			this.storage.removeItem( 'FlagSense-category' );
			this.storage.removeItem( 'FlagSense-accuracy' );
			if ( this.autoSubmit === true ) {
				document.editform.submit(); // automatically submit the edit form
			}
		},

		regex: {
			VVA: /\{\{(Vector Version Available|Converted to SVG|SVG available|NowSVG|VVA)\|[^\n\{\}]+\}\}/i,
			toSVG: /\{\{([Cc]onvert( to |[Tt]o)|[Ss]hould( be |[Bb]e)|[Tt]o)?SVG(\|[a-zA-Z]+)?\}\}/,
			toPNG: /\{\{([Cc]onvert to |[Ss]hould( be |[Bb]e))PNG\}\}/,
			ssPNG: /\{\{[Ss]upersededPNG\|[^\n\{\}]+\}\}/,
			aspectRatioCat: /\[\[Category:.*?Flag.+?aspect ratio.+?\]\]/gi
		},

		autoSubmit: false

	};
	if ( mw.config.get( 'wgCanonicalNamespace' ) === 'File' && FlagSense.storage !== undefined ) {
		// Make sure the page is a file description
		FlagSense.messages = new mw.Map( $.extend(
			{}, FlagSense.messages.en,
			( FlagSense.messages[mw.config.get( 'wgUserLanguage' )] || {} )
		) );
		$( document ).ready( function () {
			FlagSense.start();
		} );
	}
}( mediaWiki, jQuery ) );