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

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==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}`;
		});
	}
})();