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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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