Source: map/layer/CsvLayer/CsvLayer.js

import { CsvLoader } from "../../../data/CsvLoader.js";
import { CsvLayerSchema } from "./CsvLayer.schema.js";
import { Folder } from "../Folder/Folder.js";
import { MeasurementsLayer } from "../MeasurementsLayer/MeasurementsLayer.js";
import { FILE_DATA_UNAVAILABLE } from "../../utils/messageTemplates.js";

export { CsvLayer }

/**
 * Layer for displaying Data from the DWS in the viewer
 * 
 * @author rhess <robin.hess@awi.de>
 * @memberof vef.map.layer
 */
class CsvLayer extends MeasurementsLayer {

    static promises = {};
    static layers = [];

    /**
     * Set all properties for the layer based on the options object.
     * 
     * @param {object} config 
     * @param {object} cache 
     * @param {string} id 
     */
    constructor(config, cache, id) {
        // remove "file" if an actual File instance
        // is given to avoid trying to serializing it
        // when the viewer is saved. only URL references
        // as strings should be saved
        const file = config?.file || cache?.file || null;
        if (config?.file instanceof File) delete config.file;
        if (cache?.file instanceof File) delete cache.file;

        // calling parent constructor
        super(config, cache, id);

        // init default values and define getters and setters
        this.setSchema_(CsvLayerSchema.getSchema());
        this.defaults.defaultYAxis = this.columnData;
        this.defaults.title = this.columnData;

        // generate a file id that is similar across all layers based on the same file
        if (!this.fileIdentifier && file) this.fileIdentifier = CsvLayer.generateFileIdentifier(file);

        if (!this.dataFrame && file) {
            CsvLayer.parseData(file, this.fileIdentifier).then(dataFrame => {
                this.setDataFrame(dataFrame);
            })
        } else if (!this.dataFrame) {
            this.addMessage(FILE_DATA_UNAVAILABLE);
        }
        CsvLayer.layers.push(this);
    }

    /**
     * Reload the data from a file or a url
     * 
     * @param {File | string} file file or url
     * @param {string} identifier optionally override the identifier
     */
    reloadFile(file, identifier) {
        CsvLayer.parseData(file, identifier || this.fileIdentifier).then(dataFrame => {
            if (identifier) this.fileIdentifier = identifier;
            this.setDataFrame(dataFrame);
            this.removeMessage(FILE_DATA_UNAVAILABLE);
        })
    }

    /**
     * open a file upload dialog and reload the content for each layer
     * containing the same fileIdentifier
     * 
     * @param {string} fileId override the identifier to ignore changes to the file
     */
    static requestFileUpload(fileId) {
        const input = document.createElement("input");
        input.type = "file";
        input.oninput = () => {
            if (input.files.length > 0) {
                const file = input.files[0];

                // update the identifier with the new file metadata
                const newId = CsvLayer.generateFileIdentifier(file);
                const oldId = fileId || newId;

                for (let i = 0; i < CsvLayer.layers.length; ++i) {
                    const l = CsvLayer.layers[i];
                    if (l.fileIdentifier == oldId) l.reloadFile(input.files[0], newId);
                }
            }
        }
        input.click();
    }

    /**
     * @param {string | File} file File object or path to a file
     * @returns {string} identifier
     */
    static generateFileIdentifier(file) {
        if (file instanceof File) {
            return file.name + ":" + file.size + ":" + file.lastModified;
        } else if (typeof file == "string") {
            return file
        }
    }

    /**
     * @param {string | File} file File object or path to a file
     * @param {string} identifier
     * @returns {Promise} dataframe
     */
    static parseData(file, identifier) {
        if (identifier && (identifier in CsvLayer.promises)) {
            return CsvLayer.promises[identifier];
        } else {
            const promise = new Promise((resolve, reject) => {
                const load = data => {
                    new CsvLoader(data).data().then(dataFrame => resolve(dataFrame));
                };

                if (typeof file == "string") {
                    fetch(file).then(res => res.text()).then(text => load(text));
                } else if (file instanceof File) {
                    const reader = new FileReader();
                    reader.onload = e => load(e.target.result);
                    reader.readAsText(file);
                } else {
                    reject("unsupported type");
                }
            });

            if (identifier) CsvLayer.promises[identifier] = promise;
            return promise;
        }
    }

    /**
     * @param {string | File} file File object or path to a file
     * @returns {Promise} array of layers
     */
    static createLayers(file, options) {
        const filename = (file instanceof File) ? file.name : file;
        return new Promise((resolve, reject) => {
            const identifier = CsvLayer.generateFileIdentifier(file);
            CsvLayer.parseData(file, identifier).then(dataFrame => {
                const layers = {};

                // prepare options
                options = Object.assign({
                    groupBy: "Event",
                    defaultYAxis: "Depth water [m]",
                    invertYAxis: true,
                    hiddenColumns: [
                        "Event",
                        "Latitude",
                        "Longitude"
                    ]
                }, options);

                // find grouping columns, hidden columns
                if (!options.groupBy) {
                    // possible column names to group for, from more to less specific
                    const GROUPS = ['Sample code', 'Sample name', 'Sample label', 'Sample code/label', 'Sample', 'Event'];
                    let groupBy = -1;
                    for (let g = 0; g < GROUPS.length; g++) {
                        groupBy = dataFrame.columns.indexOf(GROUPS[g]);
                        if (groupBy > -1) {
                            console.log('Groupping by', GROUPS[g]);
                            options.groupBy = GROUPS[g];
                            options.hiddenColumns.push(GROUPS[g]);
                            break;
                        }
                    }
                }

                for (let col in dataFrame.columnMap) {
                    if (options.hiddenColumns.includes(col)) continue;
                    const layer = new CsvLayer({
                        file: file,
                        fileIdentifier: identifier,
                        columnData: col,
                        dataFrame: dataFrame,
                        groupByColumn: options.groupBy,
                        defaultYAxis: options.defaultYAxis,
                        invertYAxis: options.invertYAxis,
                        defaultXAxis: options.defaultXAxis,
                        invertXAxis: options.invertXAxis,
                        hiddenColumns: options.hiddenColumns
                    });
                    layers[layer.uniqueId] = layer;
                }

                const folder = new Folder({ title: filename });
                const structure = { "#": [folder.uniqueId] };
                structure[folder.uniqueId] = Object.keys(layers);
                layers[folder.uniqueId] = folder;

                resolve({
                    structure: structure,
                    layers: layers
                });
            }).catch(error => {
                console.warn(error);
                resolve(null);
            })
        });
    }

}