"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.DelayComputationManager = void 0;
const CoreToken_1 = require("@golemio/core/dist/helpers/ioc/CoreToken");
const config_1 = require("@golemio/core/dist/integration-engine/config");
const helpers_1 = require("@golemio/core/dist/integration-engine/helpers");
const ioc_1 = require("@golemio/core/dist/integration-engine/ioc");
const golemio_errors_1 = require("@golemio/core/dist/shared/golemio-errors");
const turf = __importStar(require("@turf/turf"));
class DelayComputationManager {
    constructor(gtfsTripsRepository) {
        this.gtfsTripsRepository = gtfsTripsRepository;
        this.METRO_TRIP_PREFIX_REGEX = /^(991|992|993)/;
        const simpleConfig = ioc_1.IntegrationEngineContainer.resolve(CoreToken_1.CoreToken.SimpleConfig);
        this.stepBetweenPoints = config_1.config.vehiclePositions.stepBetweenPoints;
        this.stepBetweenPointsforMetro = Number(simpleConfig.getValue("module.pid.vehicle-positions.shapes.metro.stepBetweenPointsForMetro", "0.05"));
    }
    /**
     * Return shape anchor points along trip track shape with stop times parameters
     *
     * @param {string} tripId - Internal DB trip id
     * @returns {IComputationTrip} - Result with shape anchor points
     */
    async getComputationObject(tripId) {
        const gtfsTrip = await this.gtfsTripsRepository.findByIdForDelayComputation(tripId);
        if (!gtfsTrip) {
            helpers_1.log.info(`getComputationObject: GTFS trip ${tripId} was not found`);
            return null;
        }
        const { shape_id, shapes, stop_times } = gtfsTrip;
        if (!shapes?.length) {
            throw new golemio_errors_1.GeneralError(`getComputationObject: GTFS trip ${tripId} has no shapes`, this.constructor.name);
        }
        else if (!stop_times?.length) {
            throw new golemio_errors_1.GeneralError(`getComputationObject: GTFS trip ${tripId} has no stop_times`, this.constructor.name);
        }
        const isMetro = this.METRO_TRIP_PREFIX_REGEX.test(tripId);
        const shapesGeometryAnchorPoints = this.getShapesGeometryAnchorPoints(shape_id, shapes, stop_times, isMetro);
        // MAKING COPY
        const shapePoints = [];
        const lastPointIndex = shapesGeometryAnchorPoints.length - 1;
        // const to determine shape points of stops (in meters)
        const distanceBefore = 0.1;
        const distanceAfter = 0.05;
        for (let i = 0; i < shapesGeometryAnchorPoints.length; i++) {
            const isLastPoint = i === lastPointIndex;
            const bearing = this.setupBearingBetweenPoints(i, shapesGeometryAnchorPoints, shapePoints, isLastPoint);
            // init values
            let thisStopSequence = null;
            let lastStopSequence = shapesGeometryAnchorPoints[i].last_stop_sequence;
            let nextStopSequence = shapesGeometryAnchorPoints[i].next_stop_sequence;
            // these are origin stop_times shape point fron shapes table, clearly at stops points
            if (shapesGeometryAnchorPoints[i].this_stop_sequence) {
                thisStopSequence = shapesGeometryAnchorPoints[i].this_stop_sequence;
            }
            // if is shape anchor point in close distance AFTER LAST stop, then set it also AT STOP
            else if (i > 0 &&
                shapesGeometryAnchorPoints[i].shape_dist_traveled -
                    stop_times[shapesGeometryAnchorPoints[i].last_stop_sequence - 1].shape_dist_traveled <=
                    distanceAfter) {
                thisStopSequence = shapesGeometryAnchorPoints[i].last_stop_sequence;
            }
            // if is shape anchor point in close distance BEFORE NEXT stop, then set it also AT STOP
            else if (i < shapesGeometryAnchorPoints.length - 1 &&
                stop_times[shapesGeometryAnchorPoints[i].next_stop_sequence - 1].shape_dist_traveled -
                    shapesGeometryAnchorPoints[i].shape_dist_traveled <=
                    distanceBefore) {
                thisStopSequence = shapesGeometryAnchorPoints[i].next_stop_sequence;
                lastStopSequence = shapesGeometryAnchorPoints[i].next_stop_sequence;
                nextStopSequence = Math.min(shapesGeometryAnchorPoints[i].next_stop_sequence + 1, stop_times.length);
            }
            let shapeDistTraveled = shapesGeometryAnchorPoints[i].shape_dist_traveled;
            let distanceFromLastStop;
            // fallback to shape_dist_traveled from GTFS stop_times, if the vehicle is at stop
            // and set distance from last stop to 0 (the stop is literally at stop)
            if (thisStopSequence !== null) {
                shapeDistTraveled = stop_times[thisStopSequence - 1].shape_dist_traveled;
                distanceFromLastStop = 0;
            }
            else {
                distanceFromLastStop = this.roundToDecimals(shapeDistTraveled - stop_times[lastStopSequence - 1].shape_dist_traveled, 1000);
            }
            // if is trip at stop than set departure by this stop
            // between stops do linear interpolation from departure from last stop and arrival of next stop
            const timeScheduledSeconds = thisStopSequence !== null
                ? stop_times[lastStopSequence - 1].arrival_time_seconds
                : stop_times[lastStopSequence - 1].departure_time_seconds +
                    Math.round(((stop_times[nextStopSequence - 1].arrival_time_seconds -
                        stop_times[lastStopSequence - 1].departure_time_seconds) *
                        distanceFromLastStop) /
                        (stop_times[nextStopSequence - 1].shape_dist_traveled -
                            stop_times[lastStopSequence - 1].shape_dist_traveled));
            shapePoints.push({
                index: i,
                at_stop: thisStopSequence !== null,
                bearing,
                coordinates: shapesGeometryAnchorPoints[i].coordinates,
                distance_from_last_stop: distanceFromLastStop,
                last_stop_sequence: lastStopSequence,
                next_stop_sequence: nextStopSequence,
                shape_dist_traveled: shapeDistTraveled,
                this_stop_sequence: thisStopSequence,
                time_scheduled_seconds: timeScheduledSeconds,
            });
        }
        return {
            trip_id: tripId,
            stop_times: stop_times,
            shapes_anchor_points: shapePoints,
            shapes: this.mapGtfsShapesToTripShapes(shapes),
        };
    }
    roundToDecimals(n, fraction = 100000) {
        return Math.round(n * fraction) / fraction;
    }
    shouldReadCoordsFromPrevPoint(points, i) {
        if (i <= 1) {
            return false;
        }
        return (points[i].coordinates[0] === points[i - 1].coordinates[0] && points[i].coordinates[1] === points[i - 1].coordinates[1]);
    }
    setupBearingBetweenPoints(index, source, target, isLast) {
        // add bearing from shape computed for previous shapePoint
        if (index === 0) {
            return 0;
        }
        let bearing;
        if (index > 1 && source[index - 1].this_stop_sequence && !source[index].this_stop_sequence) {
            // Was the previous point at_stop? We'll ignore any bearing computation,
            // it would point possibly to the after stop segment, usually behind a cross
            // or after a curve. Thus we'll use the at_stop's incoming (prev point) bearing,
            // because vehicle stays at_stop for some time, then it starts moving
            // and we compute bearing in the 1st point after the stop in next iteration
            return target[index - 1].bearing;
        }
        if (this.shouldReadCoordsFromPrevPoint(source, index)) {
            // take bearing of the previous point (i - 1),
            // as long as current point and prev. point have same coords
            // ... to be saved into previous point (i - 1)
            bearing = target[index - 1].bearing;
        }
        else {
            // compute bearing from two shape points
            bearing = turf.bearing(turf.point(source[index - 1].coordinates), turf.point(source[index].coordinates));
        }
        // turf.bearing returns -180 to 180, when 0 is north
        // we need 0 to 359 => modulo 360
        bearing = Math.round((360 + bearing) % 360);
        // save bearing
        target[index - 1].bearing = bearing;
        // for the last shapePoint copy the bearing from last computed one
        if (isLast) {
            bearing = target[index - 1].bearing;
        }
        return bearing;
    }
    shapePointCompareDistance(point, currentStop, nextStop) {
        if (currentStop.shape_dist_traveled === point.shape_dist_traveled) {
            point.shape_at_stop = true;
        }
        if (nextStop) {
            if (point.shape_dist_traveled >= currentStop.shape_dist_traveled &&
                point.shape_dist_traveled <= nextStop.shape_dist_traveled) {
                // current segment
                return 0;
            }
            else if (point.shape_dist_traveled > nextStop.shape_dist_traveled) {
                // future segment
                return 1;
            }
        }
        // previous stop segment OR missing nextStop (currentStop is last stop)
        return -1;
    }
    /**
     * Return geometry of anchor points with distance from start along trip track shape
     *
     * @param {string} shapeId - GTFS shape id
     * @param {ITransformedTripShape[]} shapes - GTFS shape points
     * @param {IStopTime[]} stops
     * @returns {IShapeGeometryAnchorPoint[]} - Result array of geometry points along trip track shape
     */
    getShapesGeometryAnchorPoints(shapeId, shapes, stops, isMetro = false) {
        try {
            let segmentShapes = [];
            // Splits shapes array to multiple arrays "delimited" by stops
            let startShapeIndex = 0;
            for (let st = 0; st < stops.length; st++) {
                const currentSegment = [];
                for (let sh = startShapeIndex; sh < shapes.length; sh++) {
                    const shapePoint = { ...shapes[sh], shape_at_stop: false };
                    const shapePointSituation = this.shapePointCompareDistance(shapePoint, stops[st], stops[st + 1]);
                    if (shapePointSituation === 0) {
                        // next stop iteration (outer for-cycle) can be shorter
                        startShapeIndex = sh;
                        // each segment starts with "at_stop=true" point
                        currentSegment.push(shapePoint);
                    }
                    else if (shapePointSituation === 1) {
                        // no need to iterate over all future stops segments
                        break;
                    }
                }
                if (currentSegment.length) {
                    segmentShapes.push(currentSegment);
                }
            }
            let segmentArr = [];
            // concat arrays of stop divided segments with distributed shape points
            for (const segment of segmentShapes) {
                const line = this.divideSegmentLine(segment, isMetro ? this.stepBetweenPointsforMetro : this.stepBetweenPoints);
                segmentArr.push(...line);
            }
            // Make initial shapesGeometryAnchorPoints
            let stopCounter = 0;
            const resultArr = segmentArr.map((segment, index) => {
                let this_stop_sequence;
                if (segment.shape_at_stop) {
                    stopCounter++;
                    this_stop_sequence = stopCounter;
                }
                else {
                    this_stop_sequence = null;
                }
                return {
                    index,
                    ...this.getAnchorPointProps(segment.shape_pt_lon, segment.shape_pt_lat, stopCounter, stopCounter + 1, segment.shape_dist_traveled),
                    this_stop_sequence,
                };
            });
            stopCounter++;
            // and lastly shape point for last stop as there is no segment for last stop
            resultArr.push({
                index: resultArr.length,
                ...this.getAnchorPointProps(shapes[shapes.length - 1].shape_pt_lon, shapes[shapes.length - 1].shape_pt_lat, stopCounter, stopCounter, shapes[shapes.length - 1].shape_dist_traveled),
                this_stop_sequence: stopCounter,
            });
            return resultArr;
        }
        catch (err) {
            helpers_1.log.error(err);
            helpers_1.log.error(shapeId);
            return [];
        }
    }
    getAnchorPointProps(lon, lat, last, next, dist) {
        return {
            coordinates: [this.roundToDecimals(lon), this.roundToDecimals(lat)],
            last_stop_sequence: last,
            next_stop_sequence: next,
            shape_dist_traveled: this.roundToDecimals(dist, 1000),
        };
    }
    /**
     * Generate new shape points from segment of trip shape along original one
     *   - preserving start and end shape points belonging to stops
     *   - creates new set of shape points between stops to get same point distance along original shape
     *
     * @param {ISegmentShape[]} segmentsArr - Original shape points segment between two stops
     * @param {number} distance - Minimal distance for two consequent shape points
     * @returns {ISegmentShape[]} - Result array of new shape points
     */
    divideSegmentLine(segmentArr, distance = config_1.config.vehiclePositions.stepBetweenPoints) {
        const shapeId = segmentArr[0].shape_id;
        const shapeCount = segmentArr.length;
        const startShapeDistTraveled = segmentArr[0].shape_dist_traveled;
        const endShapeDistTraveled = segmentArr[shapeCount - 1].shape_dist_traveled;
        const shapeDistLength = endShapeDistTraveled - startShapeDistTraveled;
        const segmentLine = turf.lineString(segmentArr.map((s) => {
            return [s.shape_pt_lon, s.shape_pt_lat];
        }));
        // because we dont believe source shape distances, rather compute length with turf.js
        const segmentLength = turf.length(segmentLine);
        const middlePointsCount = Math.floor(segmentLength / distance);
        const distanceOffset = (segmentLength - distance * middlePointsCount) / 2;
        // put first shape point
        const resultSegmentPoints = [
            {
                ...segmentArr[0],
                shape_pt_sequence: 1,
            },
        ];
        // for each middle point create acording shape point with interpolated origin shape dist traveled
        for (let i = 0; i < middlePointsCount; i++) {
            const currentDistance = i * distance + distanceOffset;
            const currentDistDistance = (shapeDistLength / segmentLength) * currentDistance;
            // There was an idea to skip possibly really small remaining shape distance
            // to the first computed segment point... but it created possibly more issues
            // with trams/buses having stops at crossings and having bearing into next point
            // behind the crossing. Possibly investigate an option to compute points
            // dynamically on a distance +- 50 meters apart between stops, not just shapes
            // between stops (excluding next stop)
            // const minimalDistanceLimit = distance / 10;
            // if (currentDistDistance < minimalDistanceLimit) {
            //     // point would be too close, skip it
            //     // continue;
            // }
            const newPoint = turf.along(segmentLine, i * distance + distanceOffset);
            resultSegmentPoints.push({
                shape_id: shapeId,
                shape_at_stop: false,
                shape_pt_sequence: resultSegmentPoints.length + 1,
                shape_pt_lat: this.roundToDecimals(newPoint.geometry.coordinates[1]),
                shape_pt_lon: this.roundToDecimals(newPoint.geometry.coordinates[0]),
                shape_dist_traveled: this.roundToDecimals(startShapeDistTraveled + currentDistDistance, 1000),
            });
        }
        return resultSegmentPoints;
    }
    mapGtfsShapesToTripShapes(shapes) {
        return shapes.map((shape) => {
            return {
                dist: shape.shape_dist_traveled,
                coords: [shape.shape_pt_lon, shape.shape_pt_lat],
            };
        });
    }
}
exports.DelayComputationManager = DelayComputationManager;
//# sourceMappingURL=DelayComputationManager.js.map