Benutzer:Nw520/AddressTools.js

Aus Wikivoyage

Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.

  • Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
  • Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
  • Internet Explorer/Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
  • Opera: Strg+F5
//<nowiki>
$.when( mw.loader.using( [ 'mediawiki.util' ] ), $.ready ).then( function () {
	const addressTools = {
		/**
		 * Whether to automatically lookup zipcodes if addresses are missing them.
		 */
		AUTOLOOKUP_ZIPCODES: false,
		/**
		 * @param {string} address
		 * @param {Set<string>} countries
		 * @return {boolean} Whether the address includese a zip code for at least one of the listed countries, `null` if no country is supported.
		 */
		containsZipCode: function ( address, countries ) {
			let found = false;
			let supported = false;
			
			if ( countries.has( 'at' ) ) {
				found = found || address.match( /, \d{4} .+/ ) !== null;
				supported = true;
			}
			if ( countries.has( 'de' ) ) {
				found = found || address.match( /, \d{5} .+/ ) !== null;
				supported = true;
			}
			if ( countries.has( 'lu' ) ) {
				found = found || address.match( /, (L-)?\d{4} .+/ ) !== null;
				supported = true;
			}
			
			if ( !supported ) {
				return null;
			} else {
				return found;
			}
		},
		/**
		 * Looks up the address via Nominatim.
		 *
		 * @param {string} address
		 * @param {string} city
		 * @return {Object}
		 */
		lookup: async function ( address, city ) {
			const baseUrl = 'https://nominatim.openstreetmap.org/search';
			const headers = new Headers();
			headers.append( 'X-Referer', 'https://de.wikivoyage.org/wiki/User:Nw520/AddressTools.js' );
			headers.append( 'X-User-Agent', 'AddressTools, https://de.wikivoyage.org/wiki/User_Discussion:Nw520/AddressTools.js' );

			const params = new URLSearchParams( {
				addressdetails: 1,
				format: 'json',
				q: `${address}, ${city}`
			} );

			const response = await fetch( new Request( `${baseUrl}?${params.toString()}`, {
  			cache: 'default',
				headers: headers,
				method: 'GET',
				mode: 'cors'
			} ) );
			const data = await response.json();

			if ( ( data ?? null ) === null || data.length === 0 ) {
				return null;
			}

			return data;
		},
		/**
		 * Looks up the address via Nominatim and returns its zip code.
		 *
		 * @param {string} address
		 * @param {string} city
		 * @return {?string} Zip code and city of the address.
		 */
		zipCode: async function ( address, city ) {
			const data = await addressTools.lookup( address, city );
			const filteredData = data?.filter( ( i ) => {
				return ( i?.address?.postcode ?? null ) !== null && ( i?.address?.city ?? i?.address?.town ?? i?.address?.village ?? null ) !== null;
			} ) ?? null;

			// No results
			if ( filteredData === null || filteredData.length === 0 ) {
				return null;
			}
			
			console.log(`${data[ 0 ].address.postcode} ${data[ 0 ].address.city ?? data[ 0 ].address.town ?? data[ 0 ].address.village}`)
			return `${data[ 0 ].address.postcode} ${data[ 0 ].address.city ?? data[ 0 ].address.town ?? data[ 0 ].address.village}`;
		}
	};

	const strings = {
		'ext-addresstools-cityname-prompt': {
			de: 'Bitte gib den Namen der Stadt an.',
			en: 'Please enter the city\'s name.'
		},
		'ext-addresstools-clipboard-suggestion-fail': {
			de: 'Postleitzahl konnte nicht in die Zwischenablage kopiert werden.',
			en: 'Failed to copy zipcode to clipboard.'
		},
		'ext-addresstools-clipboard-suggestion-success': {
			de: 'Postleitzahl in die Zwischenablage kopiert: $1',
			en: 'Zipcode copied to clipboard: $1'
		},
		'ext-addresstools-indicator-tooltip-suggestion': {
			de: 'In der Adresse fehlt die Postleitzahl, bei Klick wird ein Vorschlag in die Zwischenablage kopiert',
			en: 'The address is missing a zipcode, on click a suggestion is copied to your clipboard'
		},
		'ext-addresstools-indicator-tooltip-nozip': {
			de: 'In der Adresse fehlt die Postleitzahl',
			en: 'The address is missing a zipcode'
		},
		'ext-addresstools-portlet-lookup': {
			de: 'Postleitzahlen nachschlagen',
			en: 'Lookup zipcodes'
		},
	};

	const memory = {};
	let city = null;
	let portlet = null;
	let toLookup = [];

	async function main() {
		setupStrings();
		setupCss();
		
		const countries = await getCountries();
		if ( countries.size <= 0 ) {
			return;
		}

		mw.hook( 'wikipage.content' ).add( async ( $content ) => {
			// Reset vCards to lookup
			toLookup = [];

			// Find relevant vCards and add indicator
			for ( const vcard of $content[ 0 ].querySelectorAll( '.vcard' ) ) {
				const addressEl = vcard.querySelector( '.listing-address' );
				if ( addressEl === null ) {
					continue;
				}
				const address = addressEl.textContent;

				if ( addressTools.containsZipCode( address, countries ) === false ) {
					const indicator = document.createElement( 'span' );
					indicator.classList.add( 'ext-addresstools-indicator' );
					indicator.classList.add( 'ext-addresstools-indicator-nozip' );
					indicator.classList.add( 'ext-addresstools-indicator-unchecked' );
					indicator.textContent = '〒';
					indicator.title = mw.msg( 'ext-addresstools-indicator-tooltip-nozip' );
					indicator.addEventListener( 'click', ( e ) => {
						e.preventDefault();
						if ( indicator.classList.contains( 'ext-addresstools-indicator-unchecked' ) ) {
							lookupVcard( address, indicator, vcard, true );	
						}
					} );
					vcard.insertAdjacentElement( 'beforeend', indicator );

					toLookup.push( {
						address,
						indicator,
						vcard
					} );
				}
			}

			// Lookup addresses that are missing zipcode
			toLookup = await lookupVcards( toLookup, addressTools.AUTOLOOKUP_ZIPCODES );

			updatePortlet();
		} );
	}
	
	/**
	 * @param {string} title
	 * @param {Array<string>} terminalCategories
	 * @param {mw.Api} api
	 * @return {Set<string>}
	 */
	async function getCategories( title, terminalCategories, api ) {
		const formattedTerminalCategories = terminalCategories.map( ( category ) => `${mw.config.get( 'wgFormattedNamespaces' )[ 14 ]}:${category}` );
		let toRequest = [ title ];
		let categories = new Set();
		let fullfilled = new Set();

		while ( toRequest.length > 0 ) {
			for ( const item of toRequest ) {
				fullfilled.add( item );
			}

			const res = await api.get( {
				"action": "query",
				"format": "json",
				"prop": "categories",
				"titles": toRequest.join( '|' ),
				"formatversion": "2",
				"clshow": "!hidden",
				"cllimit": "max"
			} );
			toRequest = [];

			// Collect categories
			const pageCategories = res.query?.pages.flatMap( ( pageRes ) => {
				return pageRes.categories?.map( ( categoryRes ) => {
					return categoryRes.title;
				} );
			} ).filter( ( category ) => ( category ?? null ) !== null );

			// Recurse down
			for ( const pageCategory of pageCategories ) {
				categories.add( pageCategory );
				if ( !formattedTerminalCategories.includes( pageCategory ) ) {
					toRequest.push( pageCategory );
				}
			}

			toRequest = toRequest.filter( ( category ) => !fullfilled.has( category ) );
		}
		return categories;
	}
	
	/**
	 * @return {Set<string>}
	 */
	async function getCountries() {
		const api = new mw.Api();
		const countries = new Set();
		const categories = await getCategories( mw.config.get( 'wgTitle' ), [ 'Deutschland', 'Luxemburg', 'Österreich' ], api );
		
		if ( categories.has( `${mw.config.get( 'wgFormattedNamespaces' )[ 14 ]}:Österreich` ) ) {
			countries.add( 'at' );
		}
		if ( categories.has( `${mw.config.get( 'wgFormattedNamespaces' )[ 14 ]}:Schweiz` ) ) {
			countries.add( 'ch' );
		}
		if ( categories.has( `${mw.config.get( 'wgFormattedNamespaces' )[ 14 ]}:Deutschland` ) ) {
			countries.add( 'de' );
		}
		if ( categories.has( `${mw.config.get( 'wgFormattedNamespaces' )[ 14 ]}:Frankreich` ) ) {
			countries.add( 'fr' );
		}
		if ( categories.has( `${mw.config.get( 'wgFormattedNamespaces' )[ 14 ]}:Luxemburg` ) ) {
			countries.add( 'lu' );
		}
		return countries;
	}
	
	/**
	 * @param {string} address
	 * @param {HTMLElement} indicator
	 * @param {HTMLElement} vcard
	 * @param {boolean} allowNominatim
	 * @return {{found: boolean, requeue: boolean}}
	 */
	async function lookupVcard( address, indicator, vcard, allowNominatim ) {
		let lookupZip = null;

		// Unable to fulfill
		if ( memory[ address ] === undefined && !allowNominatim ) {
			indicator.classList.remove( 'ext-addresstools-indicator-working' );
			return {
				found: null,
				requeue: true
			};
		}

		if ( memory[ address ] !== undefined ) {
			lookupZip = memory[ address ];
		} else {
			if ( city === null || city === '' ) {
				city = await OO.ui.prompt( mw.msg( 'ext-addresstools-cityname-prompt' ), {
					textInput: {
						value: mw.config.get( 'wgTitle' )
					}
				} );
				if ( city === null || city === '' ) {
					return;
				}
			}

			lookupZip = await addressTools.zipCode( address, city );
		} 

		// Update indicator
		indicator.classList.add( 'ext-addresstools-indicator-checked' );
		indicator.classList.remove( 'ext-addresstools-indicator-unchecked' );
		indicator.classList.remove( 'ext-addresstools-indicator-working' );

		// No result
		if ( lookupZip === null ) {
			return {
				found: false,
				requeue: false
			};
		}

		// Some result
		memory[ address ] = lookupZip;
		indicator.dataset.result = lookupZip;
		indicator.classList.add( 'ext-addresstools-indicator-suggestion' );
		indicator.title = mw.msg( 'ext-addresstools-indicator-tooltip-suggestion' );
		indicator.addEventListener( 'click', async ( e ) => {
			e.preventDefault();

			try {
				await navigator.clipboard.writeText( `, ${lookupZip}` );
				mw.notify( mw.msg( 'ext-addresstools-clipboard-suggestion-success', lookupZip ), {
					tag: 'ext-addresstools',
					title: 'AddressTools',
					type: 'success'
				} );
			} catch ( ex ) {
				console.error( ex );
				mw.notify( mw.msg( 'ext-addresstools-clipboard-suggestion-fail', lookupZip ), {
					tag: 'ext-addresstools',
					title: 'AddressTools',
					type: 'error'
				} );
			}
		} );
		
		return {
			found: true,
			requeue: false
		};
	}

	/**
	 * @param {Array<Object>} vcards
	 * @param {boolean} allowNominatim
	 * @return {Array<Object>} List of skipped vCards
	 */
	async function lookupVcards( vcards, allowNominatim ) {
		for ( const { address, indicator, vcard } of vcards ) {
			indicator.classList.add( 'ext-addresstools-indicator-working' );
		}

		vcards = vcards.reverse(); // reverse, so that list behaves like a queue when popping
		const requeue = [];
		while ( vcards.length > 0 ) {
			const { address, indicator, vcard } = vcards.pop();
			const result = await lookupVcard( address, indicator, vcard, allowNominatim );
			
			if ( result.requeue === true ) {
				requeue.push( { address, indicator, vcard } );
			}
		}
		return requeue;
	}

	function setupCss() {
		mw.util.addCSS( `.ext-addresstools-indicator {
			background-color: white;
			border: solid thin #6c757d;
			border-radius: 5px;
			color: #6c757d;
			font-weight: bold;
			margin-left: .1rem;
			padding: 0 0.35em;
		}
		.ext-addresstools-indicator-nozip {
			border-color: #990000;
			color: #990000;
		}
		.ext-addresstools-indicator-checked.ext-addresstools-indicator-nozip:not(.ext-addresstools-indicator-suggestion) {
			border-color: rgba(153, 0, 0, .5);
			color: rgba(153, 0, 0, .5);
		}
		.ext-addresstools-indicator-working {
			animation-duration: 1s;
			animation-iteration-count: infinite;
			animation-name: ext-addresstools-working;
		}
		.ext-addresstools-indicator-suggestion {
			border-color: orangered;
			color: orangered;
			cursor: pointer;
		}
		@keyframes ext-addresstools-working {
			0% {
				background-color: white;
			}
			50% {
				background-color: #eee;
			}
			100% {
				background-color: white;
			}
		}` );
	}

	function setupStrings() {
		const lang = mw.config.get( 'wgUserLanguage' );
		mw.messages.set( Object.fromEntries( Object.keys( strings ).map( ( stringKey ) => {
			return [ stringKey, strings[ stringKey ][ lang ] ?? strings[ stringKey ].en ];
		} ) ) );
	}

	function updatePortlet() {
		if ( toLookup.length > 0 && !addressTools.AUTOLOOKUP_ZIPCODES && portlet === null ) {
			portlet = mw.util.addPortletLink( 'p-tb', '#', mw.msg( 'ext-addresstools-portlet-lookup' ) );
			portlet.addEventListener( 'click', async ( e ) => {
				e.preventDefault();

				portlet.remove();
				portlet = null;

				await lookupVcards( toLookup, true );
			} );
		} else if ( toLookup.length === 0 && portlet !== null ) {
			portlet.remove();
			portlet = null;
		}
	}

	if ( window.nw520 === undefined ) {
		window.nw520 = {};
	}
	window.nw520.addressTools = addressTools;

	mw.hook( 'ext.addresstools.loaded' ).fire( addressTools );

	main();
} );
//</nowiki>