Mist Legacy Interactive Map Search

This adds a search field to more easily find things on the interactive map website for the game Mist Legacy

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==UserScript==
// @name		Mist Legacy Interactive Map Search
// @namespace	mist-legacy-interactive-map-search
// @description This adds a search field to more easily find things on the interactive map website for the game Mist Legacy
// @version		1.2.0
// @license     MIT
// @match		http://199.180.155.43/
// @match		http://199.180.155.43/map
// @run-at		document-start
// @grant		none
// ==/UserScript==

(function() {
  'use strict';
	let hooked = false;

	// Hooking L.Map.djangoMap
	function hookLeaflet() {
		if (!window.L || !L.Map || !L.Map.djangoMap || hooked) return;

		console.log("Hooked Leaflet")
		hooked = true;
		const original = L.Map.djangoMap;

		L.Map.djangoMap = function(id, options) {
			const map = original.apply(this, arguments);
			setTimeout(() => addSearchControl(map), 0);
			return map;
		};
	}

	// Poll until Leaflet + djangoMap exist
	const poll = setInterval(() => {
		if (window.L && L.Map && L.Map.djangoMap) {
			clearInterval(poll);
			hookLeaflet();
		}
	}, 10);

	function addSearchControl(map) {
		if (!map) return;

		console.log("Creating Search Control")
		const L = window.L;
		const SearchControl = L.Control.extend({
			options: {
				position: 'topright'
			},
			onAdd() {
				const div = L.DomUtil.create('div', 'leaflet-control leaflet-bar');
				div.style.cssText = `
					background:#fff;
					padding:6px;
					width:225px;
					max-height:300px;
					display:flex;
					flex-direction:column;
				`;
				div.innerHTML = `
					<div class="search-header">
						<input class="search-input" placeholder="Search..." style="width:100%; margin-bottom:4px; box-sizing:border-box;">
						<label style="font-size:12px;display:block;margin-bottom:4px;">
							<input type="checkbox" class="search-highlight" checked>
							Highlight matches
						</label>
						<div class="search-count" style="font-size: 12px; margin-bottom: 4px;"></div>
						<hr style="margin:4px 0;">
					</div>
					<ul class="search-results" style="list-style: none; padding:0; margin:0; font-size:12px; overflow: auto; flex: 1;"></ul>
				`;
				L.DomEvent.disableClickPropagation(div);
				L.DomEvent.disableScrollPropagation(div);
				return div;
			}
		});

		map.addControl(new SearchControl());
		console.log("Search box added")

		// Add CSS
		document.head.insertAdjacentHTML('beforeend', `
			<style>
				.leaflet-search-link {
					display:inline !important;
					width:auto !important;
					height:auto !important;
					line-height:normal !important;
					white-space:normal;
					color:#0645ad !important;
					text-decoration:underline !important;
					cursor:pointer;
				}
				.leaflet-search-results li { margin-bottom:2px; }

				/* Coordinate display */
				.leaflet-coord-box {
					background: rgba(255,255,255,0.75);
					padding:4px 8px;
					font-size:12px;
					border-radius:4px;
					pointer-events:none;
					transform: translateX(-50%);
				}
			</style>
		`);

		const container = map.getContainer().parentElement;
		const input = container.querySelector('.search-input');
		const checkbox = container.querySelector('.search-highlight');
		const count = container.querySelector('.search-count');
		const list = container.querySelector('.search-results');

		const searchableLayers = () =>
			Object.values(map._layers).filter(l => l.feature && l.feature.properties);

		function getMapPoi(layer) {
			return String(layer.feature?.properties?.map_poi || '').toLowerCase();
		}

		function getDisplayLabel(layer) {
			const raw = layer.feature?.properties?.map_poi ?? layer.options?.title ?? 'Unnamed';
			return String(raw)
				.split(/<\/?br\s*\/?>|\n/i)[0]
				.trim()
				.replace(/<[^>]*>/g, '');
		}

		function focusLayer(layer) {
			if (layer.getBounds) {	// Polygon / MultiPolygon / FeatureGroup
				map.fitBounds(layer.getBounds(), { padding: [20, 20] });
			} else if (layer.getLatLng) {	// Point
				map.setView(layer.getLatLng(), Math.max(map.getZoom(), 7), { animate: true });
			}
			setTimeout(() => {
				if (layer.getPopup) layer.openPopup();
				else layer.fire?.('click');
			}, 300);
		}

		function highlightLayer(layer, on) {
			const iconUrl = layer.feature?.properties?.icon_url;
			if (!iconUrl || !layer._icon) return;

			layer._icon.style.background = on ? 'yellow' : '';
			layer._icon.style.border = on ? '4px outset red' : '';
		}

		function runSearch() {
			const q = input.value.toLowerCase();
			const layers = searchableLayers();
			list.innerHTML = '';

			// Reset highlights
			layers.forEach(l => highlightLayer(l, false));

			if (q.length < 3) {
				count.textContent = 'Type at least 3 characters';
				return;
			}

			const matches = layers.filter(l => getMapPoi(l).includes(q));
			count.textContent = `${matches.length} match${matches.length !== 1 ? 'es' : ''}`;

			matches.forEach(layer => {
				if (checkbox.checked) highlightLayer(layer, true);

				const li = document.createElement('li');
				const a = document.createElement('a');

				a.href = '#';
				a.className = 'leaflet-search-link';
				a.textContent = getDisplayLabel(layer);

				a.onclick = e => {
					e.preventDefault();
					focusLayer(layer);
				};

				li.appendChild(a);
				list.appendChild(li);
			});
		}

		input.addEventListener('input', runSearch);
		checkbox.addEventListener('change', runSearch);

		/*************************************************
		 * Coordinate Display Control (Bottom Center)
		 *************************************************/
		const CoordControl = L.Control.extend({
			options: { position: 'bottomleft' },
			onAdd() {
				const div = L.DomUtil.create('div', 'leaflet-coord-box');
				div.textContent = 'X: –, Y: –';
				return div;
			}
		});

		const coordControl = new CoordControl();
		map.addControl(coordControl);

		const coordBox = map.getContainer().querySelector('.leaflet-coord-box');
		if (coordBox) {
			coordBox.style.position = 'fixed';
			coordBox.style.left = '50%';
			coordBox.style.bottom = '6px';
			coordBox.style.transform = 'translateX(-50%)';
		}

		map.on('mousemove', e => {
			const lat = e.latlng.lat.toFixed(5);
			const lng = e.latlng.lng.toFixed(5);
			if (coordBox) coordBox.textContent = `X: ${lng}, Y: ${lat}`;
		});
	}
})();