Source: Sim_RS.js

/*
    Carlos Tortosa Micó
    Trabajo final de Grado
    Grado en Tecnologías Interactivas

    18/03/2021
*/


// ---------------------------------------------
// SETUP INICIAL DE LEAFLET
let baseLayers = {
    "Base": L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    })
}

let pointersSVA = L.layerGroup();
let poinersSVB = L.layerGroup();
let pointersBases = L.layerGroup();

let isocronasVisibles = false;

let DOCUMENTO_CARGADO = false;

const elMapa = L.map('mapa', {
    zoomControl: false,
    minZoom: 2,
    layers: [baseLayers["Base"]]
});

elMapa.on('click', (e) => {
    setSelectionMode(false);
})



L.control.zoom({
    position: 'topright'
}).addTo(elMapa);

elMapa.setView([39, -0.6], 10);
// ---------------------------
const enlaceABackend = new EnlaceABackend();


// ---------------------------
const entityTypes = {
    Vehiculo: [
        'SVA',
        'SVB',
    ],
    Map: [
        'Base'
    ]
}
// ---------------------------
let overlapCandidates = [];
let currentOverlap = null;

let lastBaseClicked = null;
let selectionMode = false;
// ---------------------------
let entidadesMapa = {
    SVA: [],
    SVB: [],
    Base: []
}

let capasShapeFile = [];
// --------------------------
let tiempoDeIsocronas = 15;
let tiempoLabel = document.getElementById('cantidadDeTiempoIsocrona');

let sliderTiempoIsocronas = document.getElementById('sliderTiempo');

sliderTiempoIsocronas.oninput = (ev) => {
    let tiempo = sliderTiempoIsocronas.value;
    tiempoLabel.innerHTML = tiempo + ' min';
}

sliderTiempoIsocronas.onchange = (ev) => {
    if (currentOverlap) {
        currentOverlap.hide();
        currentOverlap = null;
    }
    tiempoDeIsocronas = parseInt(sliderTiempoIsocronas.value);
    updateTiempoDeIsocronas(parseInt(sliderTiempoIsocronas.value));
}

let botonToggleIsocronas = document.getElementById('botonToggleIsocronas');
// --------------------------

let overlays = {
    "SVA": pointersSVA,
    "SVB": poinersSVB,
    "Bases": pointersBases
}

// --------------------------
// Controles del mapa
// --------------------------
desactivarControles();
L.control.layers(baseLayers, overlays).addTo(elMapa);


// INPUT CSVs
document.getElementById('ElInputDeCSV').onchange = onArchivoRecibidoEnInputCSV;

// --------------------------
// Cargamos las bases, que son datos permanentes
enlaceABackend.getBases_DB((err, res) => {
    if (err) {
        console.error(err);
        return;
    }

    res.forEach((baseData) => {
        let base = new Base(baseData.Lat, baseData.Lng, baseData.Descripcion, elMapa);
        base.marcador.addTo(pointersBases);
        entidadesMapa.Base.push(base);
    });

    DOCUMENTO_CARGADO = true;
})

/**
 * Realiza una petición al backend para sustraer los datos
 * referidos a un tipo de entidad del mapa
 * @param {string} tipo Tipo de entidad del mapa 
 * @param {function} callback Callback para resultados
 */
function getDatos(tipo = 'SVA', callback) {
    enlaceABackend.getVehiculos(tipo, (datos, error) => {
        callback(datos, error);
    })
}

/**
 * Activa los elementos del panel de control referidos
 * a las isocronas
 */
function activarControles() {
    sliderTiempoIsocronas.disabled = false;
    botonToggleIsocronas.classList.replace("btn-secondary", "btn-primary")
    botonToggleIsocronas.disabled = false;
}


/**
 * Desactiva los controles referidos a la isocronas del panel de contol
 */
function desactivarControles() {
    sliderTiempoIsocronas.disabled = true;
    botonToggleIsocronas.disabled = true;
}

/**
 * Función disparada del evento click cargar CSV.
 * 
 */
function cargarFicheroCSVdeVehiculos() {
    let input = document.createElement('input');
    input.type = 'file';
    input.onchange = onArchivoRecibidoEnInputCSV;
    input.click();
}

/**
 * Función inicial de carga de datos
 * @param {object} datos 
 */
function cargarDatos(datos) {

    let losSVA = datos['SVA'];
    let losSVB = datos['SVB'];

    losSVA.forEach((sva) => {
        let vehiculo = new Vehiculo(
            sva.Lat,
            sva.Lng,
            'SVA',
            sva.Disponibilidad,
            tiempoDeIsocronas,
            elMapa,
            sva.Descripcion
        );

        vehiculo.marcador.on('click', (e) => {
            onIsochroneMoved(e.latlng, vehiculo)

            if (overlapCandidates.includes(vehiculo)) {
                if (currentOverlap) {
                    let visible1 = overlapCandidates[0].esLaIsocronaVisible();
                    let visible2 = overlapCandidates[1].esLaIsocronaVisible();

                    if (!visible1 || !visible2) {
                        currentOverlap.hide();
                    }
                }
            }
        });

        vehiculo.marcador.on('dragstart', (e) => {
            vehiculo.setVisibilidadIsocrona(false);
            if (overlapCandidates.includes(vehiculo) && currentOverlap !== null) {
                currentOverlap.hide();
            }
        })

        vehiculo.marcador.on('dragend', (e) => {
            let nuevaPosicion = e.target._latlng;
            vehiculo.desplazarA(nuevaPosicion.lat, nuevaPosicion.lng);
            vehiculo.isocrona = null;
        })

        vehiculo.marcador.addTo(pointersSVA);
        entidadesMapa.SVA.push(vehiculo);
    });

    losSVB.forEach((svb) => {
        let vehiculo = new Vehiculo(
            svb.Lat,
            svb.Lng,
            'SVB',
            svb.Disponibilidad,
            tiempoDeIsocronas,
            elMapa,
            svb.Descripcion
        );

        vehiculo.marcador.on('click', (e) => {
            onIsochroneMoved(e.latlng, vehiculo)

            if (overlapCandidates.includes(vehiculo)) {
                if (currentOverlap) {
                    let visible1 = overlapCandidates[0].esLaIsocronaVisible();
                    let visible2 = overlapCandidates[1].esLaIsocronaVisible();

                    if (!visible1 || !visible2) {
                        currentOverlap.hide();
                    }
                }
            }
        });

        vehiculo.marcador.on('dragstart', (e) => {
            vehiculo.setVisibilidadIsocrona(false);
            if (overlapCandidates.includes(vehiculo) && currentOverlap !== null) {
                currentOverlap.hide();
            }
        })

        vehiculo.marcador.on('dragend', (e) => {
            let nuevaPosicion = e.target._latlng;
            vehiculo.desplazarA(nuevaPosicion.lat, nuevaPosicion.lng);
            vehiculo.isocrona = null;
        })

        vehiculo.marcador.addTo(poinersSVB);
        entidadesMapa.SVB.push(vehiculo);
    });

}

/**
 * Evento disparado al clickar el marcador
 * @param {IsochroneEntity} isochroneEntity
 */
function onIsochroneMoved(e, isochroneEntity) {
    if (!overlapCandidates.includes(isochroneEntity)) {
        overlapCandidates.push(isochroneEntity);
    }

    // De momento solo nos interesa realizar la
    // comparación entre dos isócronas
    overlapCandidates = overlapCandidates.slice(-2, undefined);

    isochroneEntity.onDragMarcador((e), (isocrona) => {
        if (currentOverlap !== null) currentOverlap.hide();

        // No queremos realizar el cálculo si lo que ha ocurrido
        // es que el usuario ha movido el mismo marcador dos veces
        if (overlapCandidates.length === 2) {
            let overlapGeometry = overlapCandidates[0].checkSolapeCon(overlapCandidates[1]);

            if (overlapGeometry) {
                currentOverlap = new Overlap(overlapGeometry, elMapa);
                currentOverlap.show();
            }
        }
    });

}

/**
 * Alterna la visibilidad de las isócronas
 * @param {event} e Evento click del botón html
 */
function toggleIsocronas(e) {
    if (currentOverlap) {
        currentOverlap.hide();
    }
    let flag = true;
    isocronasVisibles = !isocronasVisibles;

    Object.keys(entidadesMapa).forEach((tipo) => {
        if (tipo === 'SVA' || tipo === 'SVB') {
            for (let i = 0; i < entidadesMapa[tipo].length; i++) {
                if (entidadesMapa[tipo][i].esLaIsocronaVisible()) {
                    flag = false;
                }
            }

            for (let i = 0; i < entidadesMapa[tipo].length; i++) {
                entidadesMapa[tipo][i].setVisibilidadIsocrona(flag);
                if (currentOverlap) {
                    if (flag) {
                        currentOverlap.show();
                    } else {
                        currentOverlap.hide();
                    }
                }
            }
        }
    })

}

/**
 * Notifica a la base que extraiga un vehículo
 */
function extraerVehiculo() {
    if (lastBaseClicked) {
        lastBaseClicked.extraerVehiculo();
    }
}


/**
 * Notificar a la base que añada el vehiculo del parámetro
 * @param {Vehiculo} vehiculo 
 */
function anyadirVehiculo(vehiculo) {
    if (lastBaseClicked) {
        lastBaseClicked.anyadirVehiculo(vehiculo);

        if (overlapCandidates.includes(vehiculo) && currentOverlap) {
            currentOverlap.hide();
            currentOverlap = null;
        }
    }
}

/**
 * Seleccionar si nos encontramos en modo selección de vehículos
 * @param {boolean} selecting 
 */
function setSelectionMode(selecting) {
    selectionMode = selecting;
}

/**
 * Actualiza las entidades del mapa con el nuevo tiempo de isocrona
 * @param {number} tiempo Nuevo tiempo de las isocronas
 */
function updateTiempoDeIsocronas(tiempo) {
    Object.keys(entidadesMapa).forEach((subseccion) => {

        // TODO: Considerar cambiar este if statement por algo
        // del estilo if (subseccion !== "Base")
        if (subseccion === "SVA" || subseccion === "SVB") {
            entidadesMapa[subseccion].forEach((vehiculo) => {
                vehiculo.tiempoDeIsocrona = tiempo;

                if (vehiculo.tieneIsocrona()) {
                    vehiculo.actualizarIsocrona(tiempo, (res, err) => {
                        if (err) console.error(err);
                        vehiculo.setVisibilidadIsocrona(true);
                    });
                }
            });
        };
    });
}


/**
 * Función evento para cuando se pulsa el botón
 * subir shapefile
 */
function onSubirShapeFile() {
    let input = document.createElement('input');
    input.type = 'file'
    input.onchange = function (ev) {

        let file = ev.target.files[0];
        const nombreFile = file.name;

        let reader = new FileReader();
        reader.onload = function (e) {
            let capaGeoJson = null;
            shp(e.target.result).then(function (geojson) {
                try {

                    capaGeoJson = L.geoJSON(geojson, {});
                    setErrorMessageExtensionFichero(false, 'mensajeAlertaShapeFile');

                    anyadirCapaShapefile(nombreFile, capaGeoJson);

                } catch (error) {
                    console.error(error);
                    setErrorMessageExtensionFichero(true, 'mensajeAlertaShapeFile');
                    return;
                }
            }).catch(function (err) {
                setErrorMessageExtensionFichero(true, 'mensajeAlertaShapeFile');
                console.error(err);
                return;
            });
        };
        reader.readAsArrayBuffer(file);
    }

    input.click();
}

/**
 * Añade la capa shapefile al registro y el mapa
 * @param {string} nombreShapefile 
 * @param {object} capa 
 */
function anyadirCapaShapefile(nombreShapefile, capa) {
    let sanitizedName = sanitizeString(nombreShapefile);
    let tenemosLaCapa = elShapeFileYaEstaEnElMapa('elementoCapa' + sanitizedName);

    // Puesto que vamos a incorporar el nombre al DOM, realizamos una limpieza
    // de carácteres especiales para evitar las inyecciones

    if (!tenemosLaCapa) {
        capasShapeFile.push({
            FileName: nombreShapefile,
            SanitizedName: 'elementoCapa' + sanitizedName,
            GeoJson: capa
        });
        capa.addTo(elMapa);
        crearElementoHTMLCapa(sanitizedName);
    }
}

/**
 * Elimina los elementos no alphanumericos del string
 * @param {string} input String al cual hacer sanitizing
 */
function sanitizeString(input) {
    input = input.replace('zip', '');
    return input.replace(/\W/g, '');
}

/**
 * Añade a la lista de capas un elemento capa que se puede eliminar
 * interactuando con el botón
 * @param {string} nombre ID para el elemento HTML
 */
function crearElementoHTMLCapa(nombre) {
    let elemento = `<li class="list-group-item capaDeLaLista" id="elementoCapa${nombre}">
    <div class="card cardCapa">
        <div class="card-body cuerpoCardCapa">
            <p class="col-10">${nombre}</p>
            <button  class="btn btn-light col-2" id="elementoCapaBoton${nombre}">
                <i class="far fa-trash-alt"></i>
            </button>
        </div>
    </div>
</li>`
    try {
        const lista = document.getElementById('listaDeCapas');
        lista.insertAdjacentHTML('beforeend', elemento);
        let elElementoAnyadido = document.getElementById('elementoCapaBoton' + nombre);

        elElementoAnyadido.addEventListener('click', (e) => {


            for (let i = 0; i < capasShapeFile.length; i++) {
                if (capasShapeFile[i].SanitizedName === 'elementoCapa' + nombre) {

                    let elementoPadre = document.getElementById('elementoCapa' + nombre);
                    elementoPadre.parentNode.removeChild(elementoPadre);

                    // Quitamos la capa del mapa
                    elMapa.removeLayer(capasShapeFile[i].GeoJson);

                    // Eliminamos el registro de la capa
                    capasShapeFile.splice(capasShapeFile.indexOf(capasShapeFile[i]), 1);
                    return;
                }
            }
        });

    } catch (e) {
        console.error(e);
    }
}


/**
 * ¿Está el shapefile ya en el mapa? Esta función te lo dice
 * @param {string} nombreShapefile 
 * @returns {boolean} Boolean
 */
function elShapeFileYaEstaEnElMapa(nombreShapefile) {

    for (let i = 0; i < capasShapeFile.length; i++) {
        if (capasShapeFile[i].SanitizedName === nombreShapefile) {
            return true;
        }
    }
    return false;
}

/**
 * Elimina todos los vehículos e isócronas del mapa
 */
function resetPage() {
    console.log("Reseteo la página");
    // Elimina los vehiculos del mapa
    Object.keys(entidadesMapa).forEach((tipo) => {
        entidadesMapa[tipo].forEach((entidad) => {
            if (tipo === 'SVA' || tipo === 'SVB') {
                entidad.destruir();
            }
        })
    });

    overlapCandidates = [];

    // Restablece controles
    tiempoDeIsocronas = 15;
    sliderTiempoIsocronas.value = 15;
    tiempoLabel.innerHTML = 15 + ' min';

    if (currentOverlap) currentOverlap.hide();
    currentOverlap = null;

    isocronasVisibles = false;

    entidadesMapa = {
        SVA: [],
        SVB: [],
        Base: entidadesMapa.Base
    };
}

/**
 * Activa o desactiva el mensaje de error de extensión de fichero
 * @param {boolean} hayError 
 */
function setErrorMessageExtensionFichero(hayError, id) {
    let mensaje = document.getElementById(id);

    if (hayError) {
        mensaje.classList.remove('invisible');
    } else {
        mensaje.classList.add('invisible');
    }
}

/**
 * Función de procesado de archivo CSV para cargar vehiculos
 * @param {event} e Evento onChange del input archivos 
 */
function onArchivoRecibidoEnInputCSV(e) {
    let file = e.target.files[0];

    const extension = file.name.split('.').pop();

    if (extension !== 'csv') {
        setErrorMessageExtensionFichero(true, 'mensajeAlertaExtension');
        return;
    }

    Papa.parse(file, {
        header: true,
        complete: function (res) {
            if (res.errors.length > 0) {
                res.errors.forEach((error) => {
                    console.error(error.message);
                })

                setErrorMessageExtensionFichero(true, 'mensajeAlertaExtension');
                return;
            }

            let losDatos = {
                SVA: [],
                SVB: []
            }

            try {
                res.data.forEach((vehiculo) => {
                    if (vehiculo.Tipo == 'SVA') {
                        losDatos.SVA.push(vehiculo);
                    } else {
                        losDatos.SVB.push(vehiculo);
                    }
                });

                resetPage();
                setErrorMessageExtensionFichero(false, 'mensajeAlertaExtension');
                cargarDatos(losDatos);
                activarControles();

                document.getElementById('botonCargarFicheroCSV').innerHTML = "Cargar datos nuevos";

            } catch (error) {
                setErrorMessageExtensionFichero(true);
                console.error(error);
                return;
            }
        }
    });
}