Source: bemap-services-v2/ev-routing/bemap-ev-smart-routing-response.js

/**
 * BeNomad BeMap JavaScript API - EvSmartRoutingResponse (v1.5)
 *
 * Strongly-typed response parser. `fromJson()` accepts both the REST **v1.0**
 * shape (single `journey` + `route`) and the REST **v2.0.0** shape (`journeys[]`
 * of `{summary, events[]}`), exposing both through the same getters.
 *
 * Wire shapes mirror `EvSmartRoutingResponse` server-side
 * (`bemap_idea/.../service/{v1_0_0,v2_0_0}/model/evSmartRouting/response/EvSmartRoutingResponse.java`):
 *
 *   {
 *     logTag,
 *     journey: { distance, duration, batteryLevel, consumed, chargingTime,
 *                chargingCost, departureTime, arrivalTime, vehicle,
 *                savedCo2Emissions },
 *     inputInfo,
 *     boundingBox,
 *     route: { stepPoints, polyline?, encodedPolyline?, events?, timeSlotWarns? }
 *   }
 *
 * @public
 * @constructor
 * @param {Object} options Parsed JSON response from the server.
 */
bemap.EvSmartRoutingResponse = function(options) {
    var opts = options || {};
    this.logTag = opts.logTag || null;
    this.journey = opts.journey || null;
    this.inputInfo = opts.inputInfo || null;
    this.boundingBox = opts.boundingBox || null;
    this.route = opts.route || null;
    this.debugStat = opts.debugStat || null;

    // REST v2.0.0 extras — populated by `_fromJsonV2`, `null` on a v1 response.
    this.journeys = opts.journeys || null;
    this.events = opts.events || null;
    this.summary = opts.summary || null;
    this.vehicleInfo = opts.vehicleInfo || null;
    this.chargingCost = opts.chargingCost || null;
    this.tollSumFees = opts.tollSumFees || null;
    this.timeSlotWarns = opts.timeSlotWarns || null;
    this.routeConsumptions = opts.routeConsumptions || null;
};

bemap.EvSmartRoutingResponse.prototype.getLogTag = function() { return this.logTag; };
bemap.EvSmartRoutingResponse.prototype.getJourney = function() { return this.journey; };
bemap.EvSmartRoutingResponse.prototype.getInputInfo = function() { return this.inputInfo; };
bemap.EvSmartRoutingResponse.prototype.getRoute = function() { return this.route; };
bemap.EvSmartRoutingResponse.prototype.getDebugStat = function() { return this.debugStat; };

/** @public @since 2.0.0 @return {Array.<Object>|null} All v2 journeys (alternatives); `null` on v1. */
bemap.EvSmartRoutingResponse.prototype.getJourneys = function() { return this.journeys; };
/** @public @since 2.0.0 @return {Array.<Object>|null} Ordered v2 route-timeline events; `null` on v1. */
bemap.EvSmartRoutingResponse.prototype.getEvents = function() { return this.events; };
/** @public @since 2.0.0 @return {Object|null} v2 primary-journey summary; `null` on v1. */
bemap.EvSmartRoutingResponse.prototype.getSummary = function() { return this.summary; };
/** @public @since 2.0.0 @return {Object|null} Vehicle info `{brand,name,variant,year}` (v2). */
bemap.EvSmartRoutingResponse.prototype.getVehicleInfo = function() { return this.vehicleInfo; };
/** @public @since 2.0.0 @return {Object|null} Charging-cost summary (v2). */
bemap.EvSmartRoutingResponse.prototype.getChargingCost = function() { return this.chargingCost; };
/** @public @since 2.0.0 @return {Object|null} Toll sum fees `{currency,feeMin,feeMax}` (v2). */
bemap.EvSmartRoutingResponse.prototype.getTollSumFees = function() { return this.tollSumFees; };
/** @public @since 2.0.0 @return {Array.<Object>|null} Time-slot warnings (v2). */
bemap.EvSmartRoutingResponse.prototype.getTimeSlotWarnings = function() { return this.timeSlotWarns; };
/** @public @since 2.0.0 @return {Array.<Object>|null} Per-point route consumptions (v2, routeDetails on). */
bemap.EvSmartRoutingResponse.prototype.getRouteConsumptions = function() { return this.routeConsumptions; };

/**
 * `bemap.BoundingBox` covering the whole journey, or `null` when absent.
 * `fromJson` already lifts `{topLeft, bottomRight}` to a `bemap.BoundingBox`,
 * so under normal conditions this returns a `bemap.BoundingBox`. If the
 * BoundingBox lift failed (e.g. malformed payload), the raw JSON object is
 * returned so callers can still inspect it.
 *
 * @public
 * @return {bemap.BoundingBox|null}
 */
bemap.EvSmartRoutingResponse.prototype.getBoundingBox = function() {
    return this.boundingBox || null;
};

/**
 * Charging stops in order along the journey, parsed into `EvStepPoint`.
 * @public
 * @return {Array.<bemap.EvStepPoint>}
 */
bemap.EvSmartRoutingResponse.prototype.getStepPoints = function() {
    if (!this.route || !Array.isArray(this.route.stepPoints)) return [];
    return this.route.stepPoints;
};

/**
 * Route polyline as an Array of `bemap.Coordinate`, or empty when the
 * server omitted the polyline (e.g. `pl=false` in the request).
 * @public
 * @return {Array.<bemap.Coordinate>}
 */
bemap.EvSmartRoutingResponse.prototype.getPolyline = function() {
    if (!this.route || !Array.isArray(this.route.polyline)) return [];
    return this.route.polyline.map(function(p) {
        var lon = (typeof p.getLon === 'function') ? p.getLon() : (p.lon !== undefined ? p.lon : p.longitude);
        var lat = (typeof p.getLat === 'function') ? p.getLat() : (p.lat !== undefined ? p.lat : p.latitude);
        return new bemap.Coordinate(lon, lat);
    });
};

/**
 * Google-encoded polyline. On a **v1** response this is a single `String`; on a
 * **v2** response it is an `Array.<String>` — one encoded string per ROUTE
 * segment, since v2 splits the geometry across ROUTE events. `null` when absent.
 * @public
 * @return {String|Array.<String>|null}
 */
bemap.EvSmartRoutingResponse.prototype.getEncodedPolyline = function() {
    return (this.route && this.route.encodedPolyline) ? this.route.encodedPolyline : null;
};

/**
 * Distance of the whole journey, in metres.
 * @public
 * @return {Number} `0` when the journey is missing.
 */
bemap.EvSmartRoutingResponse.prototype.getDistance = function() {
    return (this.journey && this.journey.distance) ? this.journey.distance : 0;
};

/**
 * Duration of the whole journey, in seconds (including charging stops).
 * @public
 * @return {Number} `0` when the journey is missing.
 */
bemap.EvSmartRoutingResponse.prototype.getDuration = function() {
    return (this.journey && this.journey.duration) ? this.journey.duration : 0;
};

/**
 * Total time spent charging across all stops, in seconds.
 * @public
 * @return {Number}
 */
bemap.EvSmartRoutingResponse.prototype.getChargingTime = function() {
    return (this.journey && this.journey.chargingTime) ? this.journey.chargingTime : 0;
};

/**
 * Remaining battery level (percent, 0-100) at the end of the trip.
 * @public
 * @return {Number}
 */
bemap.EvSmartRoutingResponse.prototype.getArrivalBatteryLevel = function() {
    return (this.journey && typeof this.journey.batteryLevel === 'number') ? this.journey.batteryLevel : 0;
};

/**
 * Total energy consumed across the whole journey, in kWh.
 * @public
 * @return {Number}
 */
bemap.EvSmartRoutingResponse.prototype.getConsumed = function() {
    return (this.journey && typeof this.journey.consumed === 'number') ? this.journey.consumed : 0;
};

/**
 * Saved CO2 emissions (kg), when `co2Emissions` was set in the request.
 * @public
 * @return {Number|null}
 */
bemap.EvSmartRoutingResponse.prototype.getSavedCo2Emissions = function() {
    if (!this.journey || typeof this.journey.savedCo2Emissions !== 'number') return null;
    return this.journey.savedCo2Emissions;
};

/**
 * Parse the raw JSON response into a `bemap.EvSmartRoutingResponse`.
 *
 * @public
 * @param {Object} json
 * @return {bemap.EvSmartRoutingResponse}
 */
bemap.EvSmartRoutingResponse.fromJson = function(json) {
    if (!json) return new bemap.EvSmartRoutingResponse();
    // REST v2.0.0 responses carry a `journeys[]` array; v1 carried a single `journey`.
    if (Array.isArray(json.journeys)) return bemap.EvSmartRoutingResponse._fromJsonV2(json);

    var route = null;
    if (json.route) {
        route = {
            stepPoints: Array.isArray(json.route.stepPoints)
                ? json.route.stepPoints.map(function(s) { return new bemap.EvStepPoint(s); })
                : [],
            polyline: Array.isArray(json.route.polyline) ? json.route.polyline.slice() : null,
            encodedPolyline: json.route.encodedPolyline || null,
            events: Array.isArray(json.route.events) ? json.route.events.slice() : null,
            timeSlotWarns: Array.isArray(json.route.timeSlotWarns) ? json.route.timeSlotWarns.slice() : null,
            chargingStationPools: Array.isArray(json.route.chargingStationPools) ? json.route.chargingStationPools.slice() : null
        };
    }

    var bbox = null;
    if (json.boundingBox) {
        var b = json.boundingBox;
        if (b.topLeft && b.bottomRight) {
            try {
                bbox = new bemap.BoundingBox(
                    new bemap.Coordinate(b.topLeft.lon, b.topLeft.lat),
                    new bemap.Coordinate(b.bottomRight.lon, b.bottomRight.lat)
                );
            } catch (e) {
                // Fall back to the raw payload so callers can still inspect
                // it, but surface the failure on the console rather than
                // swallow it silently.
                if (typeof console !== 'undefined' && console.warn) {
                    console.warn('bemap.EvSmartRoutingResponse: bounding-box parse failed', e);
                }
                bbox = b;
            }
        } else {
            bbox = b;
        }
    }

    return new bemap.EvSmartRoutingResponse({
        logTag: json.logTag || null,
        journey: json.journey || null,
        inputInfo: json.inputInfo || null,
        boundingBox: bbox,
        route: route,
        debugStat: json.debugStat || null
    });
};

/**
 * Parse a REST **v2.0.0** response (`{ logTag, journeys: [{ summary, events[] }] }`)
 * into the same public shape the v1 getters expect, plus the new v2 getters.
 *
 * The single-journey getters read the **primary** journey (`journeys[0]`); use
 * `getJourneys()` for alternatives. The v2 event timeline is unrolled into the
 * legacy `route.{stepPoints,polyline,encodedPolyline}` view: `CHARGE` events →
 * `EvStepPoint`s, `ROUTE` events' `geometry` → the concatenated polyline (and
 * each ROUTE leg's distance/duration/consumed is carried onto the next step
 * point so `EvStepPoint.getDistance()` etc. keep working).
 *
 * @private
 * @since 2.0.0
 * @param {Object} json
 * @return {bemap.EvSmartRoutingResponse}
 */
bemap.EvSmartRoutingResponse._fromJsonV2 = function(json) {
    var journeys = Array.isArray(json.journeys) ? json.journeys : [];
    var primary = journeys[0] || null;
    var summary = (primary && primary.summary) || null;
    var events = (primary && Array.isArray(primary.events)) ? primary.events : [];

    // Unroll the ordered event timeline.
    var stepPoints = [];
    var polyline = [];
    var encoded = [];
    var routeConsumptions = [];
    // Accumulate the ROUTE leg(s) travelled since the last step point — a via can
    // split one leg into several ROUTE events — and attach the total to the next
    // CHARGE, so EvStepPoint.getDistance()/getDuration()/getConsumed() keep working.
    var leg = null;
    function addLeg(e) {
        if (!leg) leg = { distance: 0, duration: 0, consumed: 0 };
        if (typeof e.distance === 'number') leg.distance += e.distance;
        if (typeof e.duration === 'number') leg.duration += e.duration;
        if (typeof e.consumed === 'number') leg.consumed += e.consumed;
    }
    for (var i = 0; i < events.length; i++) {
        var evt = events[i] || {};
        var type = evt.eventType;
        if (type === 'ROUTE') {
            if (Array.isArray(evt.geometry)) polyline = polyline.concat(evt.geometry);
            if (evt.encodedGeometry) encoded.push(evt.encodedGeometry);
            if (Array.isArray(evt.routeConsumptions)) routeConsumptions = routeConsumptions.concat(evt.routeConsumptions);
            addLeg(evt);
        } else if (type === 'CHARGE') {
            stepPoints.push(bemap.EvStepPoint.fromChargeEvent(evt, leg));
            leg = null;   // reset — start accumulating the next segment's leg
        }
        // START / STOP / VIA / TOLL / EXCEPTION remain available via getEvents();
        // v1 `stepPoints` were charging stops only, so only CHARGE maps here.
    }

    // v2 boundingBox is `{minLon,minLat,maxLon,maxLat}` (v1 was topLeft/bottomRight).
    var bbox = null;
    var bb = summary && summary.boundingBox;
    if (bb) {
        if (typeof bb.minLon === 'number') {
            try {
                bbox = new bemap.BoundingBox(
                    new bemap.Coordinate(bb.minLon, bb.maxLat),   // top-left
                    new bemap.Coordinate(bb.maxLon, bb.minLat)    // bottom-right
                );
            } catch (e) {
                if (typeof console !== 'undefined' && console.warn) {
                    console.warn('bemap.EvSmartRoutingResponse: v2 bounding-box parse failed', e);
                }
                bbox = bb;
            }
        } else {
            bbox = bb;
        }
    }

    // Legacy `journey` shim so getDistance()/getDuration()/… keep working.
    var journey = summary ? {
        distance: summary.distance,
        duration: summary.duration,
        batteryLevel: summary.batteryLevel,
        consumed: summary.consumed,
        chargingTime: summary.chargingTime,
        departureTime: summary.departureTime,
        arrivalTime: summary.arrivalTime,
        savedCo2Emissions: (typeof summary.savedCo2Emissions === 'number') ? summary.savedCo2Emissions : null,
        vehicle: summary.vehicleInfo || null,
        chargingCost: summary.chargingCost || null
    } : null;

    var route = {
        stepPoints: stepPoints,
        polyline: polyline.length ? polyline : null,
        encodedPolyline: encoded.length ? encoded : null,   // v2: array of per-ROUTE-segment strings
        events: events
    };

    return new bemap.EvSmartRoutingResponse({
        logTag: json.logTag || null,
        journey: journey,
        boundingBox: bbox,
        route: route,
        journeys: journeys,
        events: events,
        summary: summary,
        vehicleInfo: (summary && summary.vehicleInfo) || null,
        chargingCost: (summary && summary.chargingCost) || null,
        tollSumFees: (summary && summary.tollSumFees) || null,
        timeSlotWarns: (primary && Array.isArray(primary.timeSlotWarns)) ? primary.timeSlotWarns : null,
        routeConsumptions: routeConsumptions.length ? routeConsumptions : null
    });
};