Introducción:

Antes de empezar sería una buena idea haber leído este artículo para poder entender algo de lo que sigue más abajo.

Intentemos realizar algunas mejoras al sistema web original, lo cual no debería ser muy difícil. La idea es solo utilizar javascript puro con leafletjs y un plugin (necesario para agregar rotaciones a marcadores).

Funcionalidades a mejorar y/o agregar:

  1. Todas las líneas de colectivos en el mismo menú.
  2. Permitir elegir si se deberían actualizar las posiciones de los colectivos.
  3. Mostrar todas las rutas de colectivos al mismo tiempo.
    • Permitir hacer click en cualquier ruta e identificarla con información relevante.
    • Cada ruta debería tener un color diferente para facilitar su identificación.
    • Optimizar todo el proceso.
  4. Mostrar las posiciones de todos los colectivos al mismo tiempo.
    • Permitir hacer click en cualquier colectivo e identificarlo.
  5. Combiar 4 y 5 sin afectar la performance.

Implementación:

El código está disponible y comentado en github. Primero diseñamos una simple interfaz con un selectElement centrado en el tope de la página donde vamos a ubicar todas las líneas de colectivo ordenadas por número. En el pie de la página agregamos las opciones que nos interesan, tanto con el selectElement como con los checkBox jugamos con la opacidad para que molesten lo menos posible:

<select name="listadelineas" id="sel">
					<option value="0" selected disabled>Líneas de Colectivos</option>
				</select>
				
				<div id="chkboxes">
					<label for="chkrealtime" style="word-wrap:break-word">
						<input type="checkbox" id="chkrealtime" name="chkrut" />Colectivos en tiempo real
					</label><br />
					<label for="chklimp" style="word-wrap:break-word">
						<input type="checkbox" id="chklimp" name="chklimp" checked />Mostrar solo la línea seleccionada
					</label><br />
					<label for="chkrut" style="word-wrap:break-word">
						<input type="checkbox" id="chkrut" name="chkrut" />Mostrar todas las rutas
					</label><br />
					<label for="chkallbuses" style="word-wrap:break-word">
						<input type="checkbox" id="chkallbuses" name="chklimp" />Mostrar todos los colectivos
					</label><br />
				</div>

Como Leaflet se encarga de todo lo relacionado al mapa, nosotros solo le damos la ubicación inicial sabiendo que el resto de los marcadores van a estar relativamente cerca:

const map = L.map('map').setView([-26.8083, -65.2176], 13);

const tiles = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
	maxZoom: 19,
	attribution: '© OpenStreetMap'
}).addTo(map);

Entendiendo que tenemos un JSON con la lista de todas las líneas disponibles pero divido en 3 segmentos extraemos todas las líneas pero sin segmentarlas y las enviamos a nuestro selectElement con una función asincrónica:

async function fetchgrupoLineas() {
	const response = await fetch('UBICACIONDELARCHIVO.json', { 
		method: 'GET',
		header: {
			'Accept': 'application/json',
		},
		mode: 'cors'
	});
    obj = await response.json();
		dropdownLineas();
}

Al final de nuestra función anterior llamamos a otra función que es la que realmente hace el trabajo:

async function dropdownLineas() {
	const urbanos = obj.grupos.subGrupos[0].subGrupos;
	const interurbanos = obj.grupos.subGrupos[1].subGrupos;
	const rurales = obj.grupos.subGrupos[2].lineas;

	for (var key in Object.values(urbanos)) {
		for (var yek in Object.values(urbanos[key])[2]) {
			sel.appendChild(new Option(Object.values(urbanos[key])[2][yek].descripcion, Object.values(urbanos[key])[2][yek].codLinea)).cloneNode(true);
		}
	}

	for (var key in Object.values(interurbanos)) {
		for (var yek in Object.values(interurbanos[key])[2]) {
			sel.appendChild(new Option(Object.values(interurbanos[key])[2][yek].descripcion, Object.values(interurbanos[key])[2][yek].codLinea)).cloneNode(true);
		}
	}

	for (var key in Object.values(rurales)) {
		sel.appendChild(new Option(Object.values(rurales[key])[1], Object.values(rurales[key])[0])).cloneNode(true);
	}

	sel.selectedIndex = 0;
}

Ahora después de elegir una línea del menú creamos su ruta con un color al azar y la agregamos a una capa, este mismo procedimiento lo podemos re-utilizar para mostrar todas las líneas al mismo tiempo o las que quisieramos (incluso podríamos agruparlas en colores):

if (Object.values(objParadas.nodos[key])[2] === true) {
			let latLng = L.latLng([Object.values(objParadas.nodos[key])[0], Object.values(objParadas.nodos[key])[1]]);
			L.marker(latLng, {icon: paradaIcon}).addTo(layerParadas).bindPopup("Parada: " + Object.values(objParadas.nodos[key])[4]);
		}
		
		// for the bus route it should use all the wavepoints
		let lattemp = parseFloat(Object.values(objParadas.nodos[key])[0]);
		let lngtemp = parseFloat(Object.values(objParadas.nodos[key])[1]);
		// send items to array
		items.push([lattemp, lngtemp]);

ranColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16);

polyline = L.polyline(items, {color: ranColor,
							weight: 7,
							opacity: 1,
							smoothFactor: 1}).bindPopup(selectElement.options[selectElement.selectedIndex].text).addTo(layerPath);

En otra parte del código utilicé una forma distinta de generar un color relativamente aleatoreo, la necesidad en ese caso era obtener colores similares pero que se pudieran diferenciar (distintos tonos de azul por ej.), para eso en vez de utilizar colores hexadecimales podemos usar colores en formato rgb y controlar el rojo, verde o azul independientemente, en mi caso utilizando los datos del bucle y variando un poco el valor del index sabiendo que 255 es el límite máximo de cada color en rgb es posible obtener un degradé de colores uniformes.

Finalmente obtenemos la posición de cada colectivo correspondiente a la línea seleccionada, esto también puede repetirse para obtener todas o solo algúnas al mismo tiempo, el problema más importante acá es CORS, como los datos que queremos obtener no están en el mismo dominio, incluso si el que hace el pedido es el usuario (que siempre es así) la solicitud nos devuelve un error 403 como corresponde, lo que podemos hacer para saltear este problema es enviar nuestra solicitud a través de un proxy CORS, o deshabilitar CORS en el navegador o utilizar alguna opción más dudosa, en este caso con un simple proxy no hay mayores problemas, obviamente la desventaja del proxy es el costo; en la parte final se vé como la función se llama a si misma si la opción de actualizar las posiciones de los colectivos está activada, con hacer cada pedido cada 15 segundos es más que suficiente para ver cambios sin recargar el servidor.

// retrieve current location
	fetch(corsProxy + 'https://tucuman.miredbus.com.ar/rest/posicionesBuses/' + clickedOption, {
		method: 'GET',
		header: {
			'Accept': 'application/json',
		},
		mode: 'cors'
	})
	.then(function(response) { return response.json(); })
	.then(function(json) {
		// already parsed, no need for JSON.parse(json)
		objBusLoc = json;

		// clean the layer before displaying it again but only if real updates is enabled
		if (checkboxRealtime.checked === true) {
			layerBuses.clearLayers();
		}
		
		// display the location of all the buses
		for (var key in Object.values(objBusLoc.posiciones)) {
			
			let latLng = L.latLng([Object.values(objBusLoc.posiciones[key])[1], Object.values(objBusLoc.posiciones[key])[2]]);
			L.marker(latLng, {icon: busIcon, zIndexOffset: 9999}).addTo(layerBuses).bindPopup("Colectivo: " + busName + "<br> Interno: " + Object.values(objBusLoc.posiciones[key])[0] + "<br> Próxima parada: " + Object.values(objBusLoc.posiciones[key])[4]);
			// add bus direction
			L.marker(latLng, {rotationAngle: Object.values(objBusLoc.posiciones[key])[3], rotationOrigin: "center", icon: directionIcon, zIndexOffset: 9998}).addTo(layerBuses);
		}
	});
	// update location only if the checkbox is selected
	if (checkboxRealtime.checked === true) {
		busloopId = setTimeout( () => {buslocation(cbus, bNam);}, 15000); // 15 secs is enough
		console.log("Real time update complete.");
	}

A continuación podemos ver el sistema con nuestras nuevas funciones habilitadas:

devtools1
Todas las rutas al mismo tiempo.
devtools2
Todos los colectivos al mismo tiempo
devtools3
Combinamos las 2 funciones.

Algo interesante a notar es que si prestamos atención al código en la parte que utilizamos para mostrar todas las rutas de todas las líneas de colectivo podemos ver algo interesante, como cada ruta ahora tiene un color diferente y es facilmente identificable podemos hacer zoom a una ubicación de nuestro interés y después de hacer click en alguna de las líneas ésta devuelve información relevante, como por ejemplo a que línea de colectivo pertenece, entonces ahora si no sabemos que colectivo llega a nuestro lugar de interés podemos usar esta función, por ejemplo haciendo un poco de zoom al parque Percy Hill:

devtools1
Parque Percy Hill.
devtools2
Parque Percy Hill.
devtools3
Parque Percy Hill.

Ahora si combinamos todas las rutas con todos los colectivos en tiempo real obtenemos algo similar a la siguiente imagen, y si hacemos click en el colectivo también obtenemos información relevante como el nro de interno.

devtools3
Un colectivo seleccionado al azar en el mapa.

Para ver una demo funcional del código en tiempo real se puede visitar este link.


Posdata: Hace unos meses la municipalidad de SMT publicó su versión del sistema que aunque sea difícil de creer es incluso peor que la de RedBus, como no hay mucho código para revisar ni comparar utilicemos una imagen (animada en este caso) que según dicen vale más que mil palabras, quiero creer que no se habrá gastado mucho en el “nuevo sistema”:

devtools3
¯_(ツ)_/¯