source âź© hypertext âź© public âź© indigo âź© craft âź© route âź© journeyplanner.js

const lines = {
	1: {
		name: {
			en: "Indigo line",
			nl: "Indigolijn",
			nlArticle: "de"
		},
		route: "bkl 2m30 nsc 40 sat 1m10 omp 1m30 flh 50 cax 50 mla 30 mca 15 mra 20 mgx 30 mjo 35 ghx 20 hah 1m15 hjz 2m5 tri 40 bri 55 gwx 1m40 ztm 1m smh 1m40 ccb 1m40 pdr 30 gfj 1m10 nem 55 nep 4m15 arc 1m10 plu"
	},
	2: {
		name: { en: "Central line", nl: "Centrale lijn", nlArticle: "de" },
		route: "ely 2m5 ldg 30 omp 1m10 cha 2m pom 45 bat 1m eli 1m20 mcx 3m55 pvp 30 wes 30 wen"
	},
	3: {
		name: { en: "Gouda line", nl: "Goudalijn", nlArticle: "de" },
		route: "bbm 2m anc 2m45 stl 2m10 twn 35 ssx 1m10 mqu 45 mgx 20 msa 30 cvh 35 chc 2m50 tsc 1m10 mos 55 tsk 30 dav 35 kat"
	},
	4: {
		name: { en: "Andesite line", nl: "Andesietlijn", nlArticle: "de" },
		route: "blr 3m55 stl 2m pel 1m15 cax 30 cal 1m thi"
	},
	5: {
		name: { en: "Blaze line", nl: "Vlammenlijn", nlArticle: "de" },
		route: "ggu 1m anc 2m50 oce 3m15 omp 55 khh 35 bdl 15 vog 3m hai 2m50 brw 1m35 hav 1m40 grm 45 lbb 1m15 osa"
	},
	6: {
		name: { en: "Tulip line", nl: "Tulpenlijn", nlArticle: "de" },
		route: "gam 1m15 tin 1m30 oce 45 ely 35 pel 30 aig 1m10 mqu 35 mbo 55 mso 25 mgs"
	},
	7: {
		name: { en: "Silk line", nl: "Zijdelijn", nlArticle: "de" },
		route: "gwx 1m10 vix 1m15 dav"
	},
	8: {
		name: { en: "Lavender line", nl: "Lavendellijn", nlArticle: "de" },
		route: "atl 55 sbo 45 for 1m dio 40 mbo 30 mgx 1m10 ldw 40 dry 25 thi 1m50 map 1m hai 3m15 ooi"
	},
	9: {
		name: { en: "Ocean line", nl: "Oceaanlijn", nlArticle: "de" },
		route: "oce 40 alp 20 bet 2m20 sat 1m5 cha 15 bik"
	},
	10: {
		name: { en: "Podzol line", nl: "Podzollijn", nlArticle: "de" },
		route: "alh 1m40 ent 25 osa 40 mfo 35 tba 1m15 umq"
	},
	11: {
		name: { en: "Caduceus line", nl: "Caduceuslijn", nlArticle: "de" },
		route: "bbm 1m45 oly 1m45 gam 3m10 lbk 1m25 bkl 50 hqs 1m25 mcx"
	},
	12: {
		name: { en: "Copper line", nl: "Koperlijn", nlArticle: "de" },
		route: "sip 40 pvp"
	},
	X: {
		name: { en: "Xtal Xpress", nl: "Xtal-xpres", nlArticle: "de" },
		route: "omp 4m15 mgx 9m50 osa"
	},
	O1: {
		name: {
			en: "Spawnland Underground",
			nl: "Omfalosondergrond",
			nlArticle: "de"
		},
		route: "cvt 55 aga 25 omp"
	},
	O2: {
		name: {
			en: "Oldcastle Spur",
			nl: "Oldcastle-zijdlijn",
			nlArticle: "de"
		},
		route: "flh 40 old 20 khh"
	},
	A1: {
		name: {
			en: "MacGyver Orbital",
			nl: "MacGyverringweg",
			nlArticle: "de"
		},
		route: "mra 40 msa 30 mmu 20 mjo 45 mbo 45 mse 50 mqu 25 mra"
	},
	A2: {
		name: { en: "Spine line", nl: "Ruggengraatlijn", nlArticle: "de" },
		route: "mgx 1m sau 50 bbc 35 pcl 50 otc"
	},
	W1: {
		name: {
			en: "Theta Valley Thundertrack",
			nl: "Thetadaldonderweg",
			nlArticle: "de"
		},
		route: "wes 15 sxf 20 day 20 qcn 10 mdl 5 gzn 10 ppk 10 sip"
	}
};

const stations = {
	aga: {
		name: {
			en: "Agartha"
		},
		lines: [],
		neighbours: []
	},
	aig: {
		name: {
			en: "Aig Asca"
		},
		lines: [],
		neighbours: []
	},
	alh: {
		name: {
			en: "Alhambra"
		},
		lines: [],
		neighbours: []
	},
	alp: {
		name: {
			en: "Alphalpha",
			nl: "Alfalfa",
			pt: "Alfalfa",
			sw: "Alfalfa"
		},
		lines: [],
		neighbours: []
	},
	anc: {
		name: {
			en: "Ancapistan",
			pt: "AncapistĂŁo"
		},
		lines: [],
		neighbours: []
	},
	arc: {
		name: {
			en: "Arcol"
		},
		lines: [],
		neighbours: []
	},
	atl: {
		name: {
			en: "Atlantropa",
			pt: "Atlántropa"
		},
		lines: [],
		neighbours: []
	},
	bat: {
		name: {
			en: "Batulita"
		},
		lines: [],
		neighbours: []
	},
	bbc: {
		name: {
			en: "Boa Braça"
		},
		lines: [],
		neighbours: []
	},
	bbm: {
		name: {
			en: "Birchbridge Market",
			nl: "Berkenbrugse Markt",
			pt: "Mercado da Birchbridge",
			sw: "Daraja wa Mbetula"
		},
		lines: [],
		neighbours: []
	},
	bdl: {
		name: {
			en: "Budd Lake",
			pt: "Lago Budd",
			sw: "Ziwa wa Budd"
		},
		lines: [],
		neighbours: []
	},
	bet: {
		name: {
			en: "Betaborough",
			pt: "Betápolis"
		},
		lines: [],
		neighbours: []
	},
	bik: {
		name: {
			en: "Bikini Bottom",
			nl: "Bikinibroek",
			pt: "Fenda do BiquĂ­ni",
			sw: "Bonde wa BiquĂ­ni"
		},
		lines: [],
		neighbours: []
	},
	bkl: {
		name: {
			en: "Breukelen"
		},
		lines: [],
		neighbours: []
	},
	blr: {
		name: {
			en: "Blackridge"
		},
		lines: [],
		neighbours: []
	},
	bri: {
		name: {
			en: "Brimstone Island",
			nl: "Zwaveleiland",
			pt: "Ilha de Enxofe",
			sw: "Kisiwa cha Kiberiti"
		},
		lines: [],
		neighbours: []
	},
	brw: {
		name: {
			en: "Berwyn"
		},
		lines: [],
		neighbours: []
	},
	cal: {
		name: {
			en: "Callahan",
			pt: "Cállahan"
		},
		lines: [],
		neighbours: []
	},
	cax: {
		name: {
			en: "Callahan Junction",
			nl: "Callahan Kruispunt",
			pt: "Junção de Cállahan",
			sw: "Makutano ya Callahan"
		},
		lines: [],
		neighbours: []
	},
	ccb: {
		name: {
			en: "Cinco Beach",
			nl: "Cincostrand",
			pt: "Praia de Cinco",
			sw: "Pwani pa Cinco"
		},
		lines: [],
		neighbours: []
	},
	cha: {
		name: {
			en: "Challenger City",
			nl: "Challengerstad",
			pt: "Cidade do Challenger",
			sw: "Mji wa Challenger"
		},
		lines: [],
		neighbours: []
	},
	chc: {
		name: {
			en: "Calvary Hill Cemetery",
			nl: "Calvarieberg Kerkhof"
		},
		lines: [],
		neighbours: []
	},
	cvh: {
		name: {
			en: "Calvary Hill",
			nl: "Calvarieberg"
		},
		lines: [],
		neighbours: []
	},
	cvt: {
		name: {
			en: "Cavetown",
			nl: "Grotstad"
		},
		lines: [],
		neighbours: []
	},
	dav: {
		name: {
			en: "Davnakand"
		},
		lines: [],
		neighbours: []
	},
	day: {
		name: {
			en: "Daytona"
		},
		lines: [],
		neighbours: []
	},
	dio: {
		name: {
			en: "Diogenes"
		},
		lines: [],
		neighbours: []
	},
	dry: {
		name: {
			en: "Drysle"
		},
		lines: [],
		neighbours: []
	},
	eli: {
		name: {
			en: "Elia"
		},
		lines: [],
		neighbours: []
	},
	ely: {
		name: {
			en: "ElyĹżium",
			nl: "ElyĹżion"
		},
		lines: [],
		neighbours: []
	},
	ent: {
		name: {
			en: "Enong Town",
			nl: "Enongstad"
		},
		lines: [],
		neighbours: []
	},
	flh: {
		name: {
			en: "Flag Hall",
			nl: "Vlaggenzaal"
		},
		lines: [],
		neighbours: []
	},
	for: {
		name: {
			en: "Forsythe"
		},
		lines: [],
		neighbours: []
	},
	gam: {
		name: {
			en: "Gambiarra"
		},
		lines: [],
		neighbours: []
	},
	gfj: {
		name: {
			en: "Grijze Fjord"
		},
		lines: [],
		neighbours: []
	},
	ggu: {
		name: {
			en: "Galt’s Gulch"
		},
		lines: [],
		neighbours: []
	},
	ghx: {
		name: {
			en: "Goodhope–Huriǂoaxa",
			nl: "Goede Hoop–Huriǂoaxa"
		},
		lines: [],
		neighbours: []
	},
	grm: {
		name: {
			en: "Gerem"
		},
		lines: [],
		neighbours: []
	},
	gwx: {
		name: {
			en: "Godwebben Cross"
		},
		lines: [],
		neighbours: []
	},
	gzn: {
		name: {
			en: "Gazania"
		},
		lines: [],
		neighbours: []
	},
	hah: {
		name: {
			en: "Halicarnassus House",
			nl: "Halikarnassoshuis"
		},
		lines: [],
		neighbours: []
	},
	hai: {
		name: {
			en: "Haishenwai"
		},
		lines: [],
		neighbours: []
	},
	hav: {
		name: {
			en: "Hannah Valley",
			nl: "Hannahdal"
		},
		lines: [],
		neighbours: []
	},
	hjz: {
		name: {
			en: "Hejaz",
			nl: "Hidjaz"
		},
		lines: [],
		neighbours: []
	},
	hqs: {
		name: {
			en: "Harold Q. Spleef Memorial Arena and Environs",
			nl: "Herdenkingsarena Harold Q. Spleef en Omgeving"
		},
		lines: [],
		neighbours: []
	},
	kat: {
		name: {
			en: "Kattakurgan"
		},
		lines: [],
		neighbours: []
	},
	khh: {
		name: {
			en: "Khama House",
			nl: "Khamahuis"
		},
		lines: [],
		neighbours: []
	},
	lbb: {
		name: {
			en: "LeBond Bay",
			nl: "LeBondbaai"
		},
		lines: [],
		neighbours: []
	},
	lbk: {
		name: {
			en: "Little Breukelen",
			nl: "Klein-Breukelen"
		},
		lines: [],
		neighbours: []
	},
	ldg: {
		name: {
			en: "Lundegaard"
		},
		lines: [],
		neighbours: []
	},
	ldw: {
		name: {
			en: "Little Dunwich",
			nl: "Klein-Dunwich"
		},
		lines: [],
		neighbours: []
	},
	map: {
		name: {
			en: "World Map",
			nl: "Wereldkaart"
		},
		lines: [],
		neighbours: []
	},
	mbo: {
		name: {
			en: "MacGyver Bohit"
		},
		lines: [],
		neighbours: []
	},
	mca: {
		name: {
			en: "MacGyver Campus"
		},
		lines: [],
		neighbours: []
	},
	mcx: {
		name: {
			en: "Marĉeto Interchange",
			nl: "Knooppunt Marĉeto"
		},
		lines: [],
		neighbours: []
	},
	mdl: {
		name: {
			en: "Midlothian"
		},
		lines: [],
		neighbours: []
	},
	mfo: {
		name: {
			en: "Mfose"
		},
		lines: [],
		neighbours: []
	},
	mgs: {
		name: {
			en: "MacGyver Greathope Square",
			nl: "MacGyver Greathopeplaats"
		},
		lines: [],
		neighbours: []
	},
	mgx: {
		name: {
			en: "MacGyver Central",
			nl: "MacGyver Centraal"
		},
		lines: [],
		neighbours: []
	},
	mjo: {
		name: {
			en: "MacGyver Joboken"
		},
		lines: [],
		neighbours: []
	},
	mla: {
		name: {
			en: "MacGyver Landfill",
			nl: "MacGyver Vuilstort"
		},
		lines: [],
		neighbours: []
	},
	mmu: {
		name: {
			en: "MacGyver Museum"
		},
		lines: [],
		neighbours: []
	},
	mos: {
		name: {
			en: "Mosmont"
		},
		lines: [],
		neighbours: []
	},
	mqu: {
		name: {
			en: "MacGyver Quaymother"
		},
		lines: [],
		neighbours: []
	},
	mra: {
		name: {
			en: "MacGyver Ravinia",
			nl: "MacGyver Ravinië"
		},
		lines: [],
		neighbours: []
	},
	msa: {
		name: {
			en: "MacGyver Santora"
		},
		lines: [],
		neighbours: []
	},
	mse: {
		name: {
			en: "MacGyver Sevensea",
			nl: "MacGyver Zevenzee"
		},
		lines: [],
		neighbours: []
	},
	mso: {
		name: {
			en: "MacGyver SoJoBo",
			nl: "MacGyver Zuid-Joboken"
		},
		lines: [],
		neighbours: []
	},
	nem: {
		name: {
			en: "Nereid Monument",
			nl: "NereĂŻdenmonument"
		},
		lines: [],
		neighbours: []
	},
	nep: {
		name: {
			en: "Neptune",
			nl: "Neptunus"
		},
		lines: [],
		neighbours: []
	},
	nsc: {
		name: {
			en: "Nieuw-Schokland"
		},
		lines: [],
		neighbours: []
	},
	oce: {
		name: {
			en: "O Cebreiro"
		},
		lines: [],
		neighbours: []
	},
	old: {
		name: {
			en: "Oldcastle"
		},
		lines: [],
		neighbours: []
	},
	oly: {
		name: {
			en: "Olympia"
		},
		lines: [],
		neighbours: []
	},
	omp: {
		name: {
			en: "Omphalos"
		},
		lines: [],
		neighbours: []
	},
	ooi: {
		name: {
			en: "Ooievaarden"
		},
		lines: [],
		neighbours: []
	},
	osa: {
		name: {
			en: "Osanga"
		},
		lines: [],
		neighbours: []
	},
	otc: {
		name: {
			en: "Ottertail City",
			nl: "Otterstaartstad"
		},
		lines: [],
		neighbours: []
	},
	pcl: {
		name: {
			en: "Port Clair"
		},
		lines: [],
		neighbours: []
	},
	pdr: {
		name: {
			en: "Pandora"
		},
		lines: [],
		neighbours: []
	},
	pel: {
		name: {
			en: "Pelts"
		},
		lines: [],
		neighbours: []
	},
	plu: {
		name: {
			en: "Pluto"
		},
		lines: [],
		neighbours: []
	},
	pom: {
		name: {
			en: "Pompeiii",
			nl: "PompeĂŻĂŻĂŻ"
		},
		lines: [],
		neighbours: []
	},
	ppk: {
		name: {
			en: "Penta Park"
		},
		lines: [],
		neighbours: []
	},
	pvp: {
		name: {
			en: "Pavelpur"
		},
		lines: [],
		neighbours: []
	},
	qcn: {
		name: {
			en: "Qacha’s Nek"
		},
		lines: [],
		neighbours: []
	},
	sat: {
		name: {
			en: "Saturnalia Temple",
			nl: "Saturnaliëntempel"
		},
		lines: [],
		neighbours: []
	},
	sau: {
		name: {
			en: "Saudades"
		},
		lines: [],
		neighbours: []
	},
	sbo: {
		name: {
			en: "South Boarhunt",
			nl: "Zuid-Berenjacht"
		},
		lines: [],
		neighbours: []
	},
	sip: {
		name: {
			en: "Sotha Iyer Plaza",
			nl: "Sotha Iyerplaats"
		},
		lines: [],
		neighbours: []
	},
	smh: {
		name: {
			en: "Samhain"
		},
		lines: [],
		neighbours: []
	},
	ssx: {
		name: {
			en: "Sussex"
		},
		lines: [],
		neighbours: []
	},
	stl: {
		name: {
			en: "Saint Louis"
		},
		lines: [],
		neighbours: []
	},
	sxf: {
		name: {
			en: "Six Flags"
		},
		lines: [],
		neighbours: []
	},
	tba: {
		name: {
			en: "The Barleye"
		},
		lines: [],
		neighbours: []
	},
	thi: {
		name: {
			en: "Thousand Island",
			nl: "Duizendeiland"
		},
		lines: [],
		neighbours: []
	},
	tin: {
		name: {
			en: "Tinfoil",
			nl: "Tinfolie"
		},
		lines: [],
		neighbours: []
	},
	tri: {
		name: {
			en: "Trichilia",
			nl: "Trichilië"
		},
		lines: [],
		neighbours: []
	},
	tsc: {
		name: {
			en: "’t Schulp"
		},
		lines: [],
		neighbours: []
	},
	tsk: {
		name: {
			en: "Tashkichik",
			nl: "Tasjkitsjik"
		},
		lines: [],
		neighbours: []
	},
	twn: {
		name: {
			en: "Townsville"
		},
		lines: [],
		neighbours: []
	},
	umq: {
		name: {
			en: "Umm Qasr"
		},
		lines: [],
		neighbours: []
	},
	vix: {
		name: {
			en: "Viaduct Junction",
			nl: "Viaductkruispunt"
		},
		lines: [],
		neighbours: []
	},
	vog: {
		name: {
			en: "Valley of the Gods",
			nl: "Dal der Goden"
		},
		lines: [],
		neighbours: []
	},
	wen: {
		name: {
			en: "Wenceslas North",
			nl: "Wenceslas Noord"
		},
		lines: [],
		neighbours: []
	},
	wes: {
		name: {
			en: "Wenceslas South",
			nl: "Wenceslas Zuid"
		},
		lines: [],
		neighbours: []
	},
	ztm: {
		name: {
			en: "Zhitong Monument",
			nl: "Zhitongmonument"
		},
		lines: [],
		neighbours: []
	}
};

for (const lineName in lines) {
	const lineRoute = lines[lineName].route.split(" ").map((x, idx) => {
		if (idx % 2 === 0) return x;
		if (!x.includes("m")) return +x;

		const minsSecs = x.split("m");
		if (minsSecs.length === 1) return 60 * +minsSecs[0];
		return 60 * +minsSecs[0] + +minsSecs[1];
	});

	lineRoute.forEach((stop, idx) => {
		if (idx % 2 == 0) {
			// Station
			if (!stations[stop].lines.includes(lineName)) {
				stations[stop].lines.push(lineName);
			}
		} else {
			// Edge
			stations[lineRoute[idx - 1]].neighbours.push({
				line: lineName,
				time: stop,
				stop: lineRoute[idx + 1]
			});
			stations[lineRoute[idx + 1]].neighbours.push({
				line: lineName,
				time: stop,
				stop: lineRoute[idx - 1]
			});
		}
	});
}

// Dijkstra’s algorithm
const findPath = (start, end) => {
	// Set all edges to unvisited
	Object.values(stations)
		.map(x => x.neighbours)
		.flat()
		.forEach(edge => {
			edge.visited = false;
		});

	const unvisited = Object.fromEntries(
		Object.entries(stations).map(x => [
			x[0],
			{
				time: Infinity,
				paths: []
			}
		])
	);
	unvisited[start] = {
		time: 0,
		paths: [[]]
	};

	let currentStation = start;

	while (currentStation != end) {
		for (const nextStop of stations[currentStation].neighbours) {
			if (!(nextStop.stop in unvisited)) continue;

			const timeToNextStop =
				unvisited[currentStation].time + nextStop.time;
			const nextStopObj = unvisited[nextStop.stop];

			if (timeToNextStop <= nextStopObj.time) {
				const currentStationPaths = unvisited[currentStation].paths.map(
					x => [...x, { stop: currentStation, line: nextStop.line }]
				);

				if (timeToNextStop == nextStopObj.time) {
					nextStopObj.paths = JSON.parse(
						JSON.stringify([
							...nextStopObj.paths,
							...currentStationPaths
						])
					);
				} else {
					nextStopObj.time = timeToNextStop;
					nextStopObj.paths = currentStationPaths;
				}
			}
		}

		delete unvisited[currentStation];

		// Find the unvisited station with the least travel time needed
		const leastTravelTime = Math.min(
			...Object.values(unvisited).map(x => x.time)
		);

		currentStation = Object.entries(unvisited).find(
			x => x[1].time == leastTravelTime
		)[0];
	}

	let finalPath = unvisited[currentStation].paths[0];

	for (idx = 1; idx < unvisited[currentStation].paths.length; idx++) {
		const testPath = unvisited[currentStation].paths[idx];

		// Check if there are fewer interchanges or stations in the tested path than the current consensus
		const numInterchanges = {
			test: new Set(testPath.map(x => x.line)).size,
			final: new Set(finalPath.map(x => x.line)).size
		};

		if (
			numInterchanges.test < numInterchanges.final ||
			(numInterchanges.test == numInterchanges.final &&
				testPath.length < finalPath.length)
		) {
			finalPath = testPath;
		}
	}

	return {
		time: unvisited[currentStation].time,
		path: [
			...finalPath.flatMap(x => [stations[x.stop].name.en, x.line]),
			stations[end].name.en
		]
	};
};

const locale = {
	en: {
		count: new Intl.PluralRules("en"),
		startPoint: "Starting point",
		endPoint: "Destination",

		time: t => {
			const seconds = t % 60;
			const minutes = (t - seconds) / 60;

			if (minutes == 0) return `${seconds} seconds`;
			if (seconds == 0)
				return `${minutes} ${
					locale.en.count.select(minutes) == "other"
						? "minutes"
						: "minute"
				}`;
			return `${minutes} ${
				locale.en.count.select(minutes) == "other"
					? "minutes"
					: "minute"
			}, ${seconds} seconds`;
		},

		rideAlong: (stops, line) =>
			`Ride ${stops} ${
				locale.en.count.select(stops) == "other" ? "stops" : "stop"
			} along the <strong>${lines[line].name.en}</strong>.`
	},
	nl: {
		count: new Intl.PluralRules("nl"),
		startPoint: "Vertrekpunt",
		endPoint: "Bestemming",

		time: t => {
			const seconds = t % 60;
			const minutes = (t - seconds) / 60;

			if (minutes == 0) return `${seconds} seconden`;
			if (seconds == 0)
				return `${minutes} ${
					locale.nl.count.select(minutes) == "other"
						? "minuten"
						: "minuut"
				}`;
			return `${minutes} ${
				locale.nl.count.select(minutes) == "other"
					? "minuten"
					: "minuut"
			}, ${seconds} seconden`;
		},

		rideAlong: (stops, line) =>
			`Reis ${stops} ${
				locale.nl.count.select(stops) == "other" ? "haltes" : "halte"
			} langs ${lines[line].name.nlArticle ?? "de"} <strong>${
				lines[line].name.nl ?? lines[line].name.en
			}</strong>.`
	}
};

/* 
Ponto de partida
Kituo cha kuondoka

Destino
Kituo cha kufika

{m} (minuto/minutos), {s} (segundo/segundos)
Dakika {m}, sekunde {s} 

Apanhe ${article} ${line} para ${stops} (paragem/paragens).
Abiri (kituo/vituo) ${stops} kwa ${line}.
*/

findPath("omp", "chc");