Source: service/acquisitionPlans/AcquisitionPlansParser.js

import toGeoJSON from './toGeoJson/togeojson';

export class AcquisitionPlansParser {

    /**
     * @alias AcquisitionPlansParser
     * @constructor
     * @param {Workers} workers 
     */
    constructor(workers) {
        if (!workers) {
            throw (new Error('AcquisitionPlansParser - constructor - missing workers instance'));
        }

        this.workers = workers;

        this.InteriorCtor = WorldWind ? WorldWind.SurfacePolygon : null;
        this.OutlineCtor = WorldWind ? WorldWind.Path : null;
        this.ShapeAttributesCtor = WorldWind ? WorldWind.ShapeAttributes : null;
        this.PositionCtor = WorldWind ? WorldWind.Position : null;
        this.ColorCtor = WorldWind ? WorldWind.Color : null;

        /**
         * @type {DOMParser|null}
         */
        this.domParser = null;

        /**
         * @type {Object}
         */
        this.shapeAttributesMap = Object.create(null);
    }

    /**
     * Parses an acquisition plan kml file.
     * Parsing will be attempted in a web worker and if that fails it will be attemped on the main thread.
     * 
     * @param {{ satName: String, url: String, type: String, interior: Boolean, outline: Boolean, outlineAlpha: Number, interiorAlpha: Number, highlightAlpha: Number, filterDate: String }} fileInfo
     * 
     * @param {String} fileInfo.satName The satellite name for the acquisition plan. The convention is to use the short name: "s1a", "s2b", "s5p", etc.
     * @param {String} fileInfo.url The url for the acquisition plan file
     * @param {String} fileInfo.type A type for the web workers. Default is "downloadAndParseKmls"
     * @param {Boolean} fileInfo.interior A flag that indicates if interior renderables should be created. Default is true
     * @param {Boolean} fileInfo.outline A flag that indicates if outline renderables should be created. Default is true
     * @param {Number} fileInfo.outlineAlpha Alpha value fot the outline. Default is 1
     * @param {Number} fileInfo.interiorAlpha Alpha value for the interior. Default is 0.2
     * @param {Number} fileInfo.highlightAlpha Alpha value for the interior highlight. Default is 0.5
     * @param {String} fileInfo.filterDate An ISODate string used for filtering out shapes that lees than this value. Default is the current date
     * 
     * @param {Function} cb A callback function, will be called with error and entry params
     */
    parse(fileInfo, cb) {
        fileInfo.type = fileInfo.type || 'downloadAndParseKmls';
        fileInfo.filterDate = fileInfo.filterDate || new Date().toISOString();

        this.workers.process(fileInfo, (err, result) => {
            if (err) {
                return cb(err);
            }

            const { shapes, satName, url } = result;
            let parsedShapes;

            if (typeof shapes === 'string') {
                console.info('Worker unable to parse kml file', url);
                try {
                    parsedShapes = this.parseSync(shapes, satName, fileInfo.filterDate);
                }
                catch (error) {
                    return cb(error);
                }
            }
            else {
                parsedShapes = shapes;
            }

            let outlines;
            let interiors;
            let interval;

            try {
                ({ outlines, interiors, interval } = this.makeRenderables(parsedShapes, fileInfo));
            }
            catch (error) {
                return cb(error);
            }

            if ((outlines || interiors) && interval) {
                return cb(null, this.formatOutput({ satName, url }, interval, outlines, interiors));
            }
        });
    }

    parseSync(kmlString, satName, filterDate) {
        if (!this.domParser) {
            this.domParser = new DOMParser();
        }

        const kmlDoc = this.domParser.parseFromString(kmlString, 'text/xml');
        const geoJson = toGeoJSON.kml(kmlDoc);
        let shapes = geoJson.features;

        const isSentinel2 = satName.includes('s2');
        const nowDate = new Date(filterDate);
        shapes = shapes.filter(shape => {
            const isInTheFuture = new Date(shape.properties.timespan.end) >= nowDate;

            if (isSentinel2) {
                return (
                    isInTheFuture &&
                    shape.properties.Mode === 'NOBS' &&
                    shape.properties.Timeliness === 'NOMINAL'
                );
            }

            return isInTheFuture;
        });

        return shapes;
    }

    makeRenderables(shapes, fileInfo) {
        const outlines = [];
        const interiors = [];

        const interval = {
            startDate: Number.MAX_SAFE_INTEGER,
            endDate: Number.MIN_SAFE_INTEGER
        };

        for (let i = 0; i < shapes.length; i++) {
            let shape = shapes[i];
            let coords = shape.coordinates;
            let color = shape.color;
            let extendedData = shape.extendedData;

            if (shape.geometry) {
                coords = shape.geometry.coordinates[0];
            }
            if (shape.properties) {
                color = shape.properties.lineColor;
                extendedData = shape.properties;
            }

            let positions = this.makePositions(coords);

            if (fileInfo.outline !== false) {
                let pathAttributes = this.makePathAttributes(color, fileInfo.outlineAlpha);
                let path = new this.OutlineCtor(positions, pathAttributes);
                path.altitudeMode = 'clampToGround';
                path.followTerrain = true;
                path.expiryTime = Number.MAX_SAFE_INTEGER;
                this.setTimeInterval(path, extendedData, interval);
                path.kmlProps.satName = fileInfo.satName;
                outlines.push(path);
            }

            if (fileInfo.interior !== false) {
                let surfacePolygonAttributes = this.makeSurfacePolygonAttributes(color, fileInfo.interiorAlpha);
                let surfacePolygon = new this.InteriorCtor(positions, surfacePolygonAttributes);
                surfacePolygon.highlightAttributes = this.makeSurfacePolygonHighlightAttributes(color, fileInfo.highlightAlpha);
                this.setTimeInterval(surfacePolygon, extendedData, interval);
                surfacePolygon.kmlProps.satName = fileInfo.satName;
                interiors.push(surfacePolygon);
            }
        }

        interval.startDate = new Date(interval.startDate);
        interval.endDate = new Date(interval.endDate);

        return {
            outlines,
            interiors,
            interval,
        };
    }

    makePositions(coords) {
        const positions = [];

        for (let i = 0, len = coords.length; i < len; i++) {
            let coord = coords[i];
            let pos = new this.PositionCtor(
                coord[1] || coord.latitude,
                coord[0] || coord.longitude,
                coord[2] || 0
            );
            if (pos.longitude < -180) {
                pos.longitude = pos.longitude + 360;
            }
            if (pos.longitude > 180) {
                pos.longitude = pos.longitude - 360;
            }
            positions.push(pos);
        }

        return positions;
    }

    makePathAttributes(hexColor, alpha = 1) {
        const key = hexColor + 'path';
        
        if (this.shapeAttributesMap[key]) {
            return this.shapeAttributesMap[key];
        }

        const attributes =  new this.ShapeAttributesCtor(null);
        attributes.drawOutline = true;
        attributes.drawInterior = false;
        attributes.outlineColor = this.makeColor(hexColor, alpha);

        this.shapeAttributesMap[key] = attributes;

        return attributes;
    }

    makeSurfacePolygonAttributes(props, alpha = 0.2) {
        const hexColor = props.lineColor || props;
        const key = hexColor + 'poly';

        if (this.shapeAttributesMap[key]) {
            return this.shapeAttributesMap[key];
        }

        const attributes = new this.ShapeAttributesCtor(null);
        attributes.drawOutline = false;
        attributes.drawInterior = true;
        attributes.interiorColor = this.makeColor(hexColor, alpha);

        this.shapeAttributesMap[key] = attributes;

        return attributes;
    }

    makeSurfacePolygonHighlightAttributes(props, alpha = 0.5) {
        const hexColor = props.lineColor || props;

        const attributes = new this.ShapeAttributesCtor(null);
        attributes.drawOutline = false;
        attributes.drawInterior = true;
        attributes.interiorColor = this.makeColor(hexColor, alpha);

        return attributes;
    }

    makeColor(hexColor, alpha) {
        const blue = hexColor.substring(2, 4);
        const green = hexColor.substring(4, 6);
        const red = hexColor.substring(6, 8);

        const r = parseInt(red, 16) / 255;
        const g = parseInt(green, 16) / 255;
        const b = parseInt(blue, 16) / 255;

        return new this.ColorCtor(r, g, b, alpha);
    }

    setTimeInterval(renderable, props, interval) {
        const startTime = this.dateStringToUTC(props.ObservationTimeStart || props.timespan.begin);
        const endTime = this.dateStringToUTC(props.ObservationTimeStop || props.timespan.end);
        const startTimeMs = (new Date(startTime)).getTime();
        const endTimeMs = (new Date(endTime)).getTime();

        if (interval.startDate > startTimeMs) {
            interval.startDate = startTimeMs;
        }
        if (interval.endDate < endTimeMs) {
            interval.endDate = endTimeMs;
        }

        renderable.kmlProps = props;
        renderable.kmlProps.startDate = new Date(startTimeMs);
        renderable.kmlProps.endDate = new Date(endTimeMs);
        renderable.type = 'acqPlan';
    }

    dateStringToUTC(dateString) {
        const lastChar = dateString[dateString.length - 1];
        if (lastChar.toLowerCase() !== 'z') {
            return dateString + 'Z';
        }
        return dateString;
    }

    formatOutput(fileInfo, interval, outlines = [], interiors = []) {
        return {
            url: fileInfo.url,
            satName: fileInfo.satName,
            outlines: outlines,
            interiors: interiors,
            startDate: interval.startDate,
            endDate: interval.endDate,
        };
    }

}