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

/**
 * BeNomad BeMap JavaScript API - EvSmartRoutingRequest (v1.5)
 *
 * Strongly-typed request builder. `toJson()` emits the REST **v1.0** body
 * (`POST service/evsmartrouting/1.0`); `toJsonV2()` emits the REST **v2.0.0**
 * body (`POST service/2.0/evsmartrouting`).
 *
 * Wire shapes cross-checked against the BeMap server contracts in
 * `bemap_idea/.../service/v1_0_0/model/evSmartRouting/request/EvSmartRoutingRequest.java`
 * and `.../service/v2_0_0/model/evSmartRouting/request/EvSmartRoutingRequest.java`.
 *
 * The server requires:
 *   - `startLon`, `startLat`, `stopLon`, `stopLat` (decimal degrees, WGS84)
 *   - `vehicle` — a vehicle key from the user's allowed EV brand catalogue
 *   - At least one entry in `chargingStationProviders` (`csps` on the wire)
 *
 * Example:
 *
 *     var req = new bemap.EvSmartRoutingRequest({
 *         start: new bemap.Coordinate(2.35, 48.85),
 *         stop:  new bemap.Coordinate(4.83, 45.75),
 *         vehicle: 'renault-megane-e-tech-ev60',
 *         chargingStationProviders: ['ecoMovement'],
 *         initBatteryLevel: 80,
 *         minBatteryLevel: 10,
 *         minArrivalBatteryLevel: 20,
 *         temperature: 18
 *     });
 *
 * @since 1.5.0
 * @public
 * @constructor
 * @param {Object} [options]
 * @param {bemap.Coordinate|{lon:Number, lat:Number}} options.start
 *        Departure coordinate (decimal degrees, WGS84).
 * @param {bemap.Coordinate|{lon:Number, lat:Number}} options.stop
 *        Arrival coordinate (decimal degrees, WGS84).
 * @param {Array.<bemap.Coordinate|{lon:Number, lat:Number}>} [options.vias]
 *        Intermediate waypoints, in order.
 * @param {String} options.vehicle Vehicle key (UUID-like, from the EV catalogue).
 * @param {Array.<String>} options.chargingStationProviders
 *        Allowed CSP keys (e.g. `'ecoMovement'`). Maps to the `csps` field.
 * @param {Array.<String>} [options.chargingStationFilters]
 *        Filter expressions — see BeMap docs. Maps to `csfs`.
 * @param {Boolean} [options.chargingStationDeprecatedConnector=false]
 *        Allow stops on stations with deprecated connector types.
 * @param {Number} [options.initBatteryLevel=100]
 *        Initial battery level (0-100).
 * @param {Number} [options.minBatteryLevel=0]
 *        Minimum battery level allowed during the journey (0-100).
 * @param {Number} [options.minArrivalBatteryLevel=0]
 *        Minimum battery level required at destination (0-100).
 * @param {Number} [options.maxAfterChargeBatteryLevel]
 *        Maximum battery level after a stop (50-100). Vehicle default ≈ 90.
 * @param {Boolean} [options.weather=false]
 *        Include real-time weather impact in the energy calculation.
 * @param {String} [options.weatherProvider]
 *        Optional explicit weather provider key.
 * @param {Number} [options.temperature=20]    Ambient temperature, °C.
 * @param {String|Date|Number} [options.departureTime]
 *        Epoch ms, ISO string, or Date — when the trip starts.
 * @param {Number} [options.stepPointPluggingTime=0]
 *        Extra time (in seconds) for each charging stop (plugging delay).
 * @param {Number} [options.payload=75]        Extra weight in kg.
 * @param {Array.<String>} [options.criterias]
 *        EV-specific criteria — e.g. `AVOID_MOTORWAYS`, `AVOID_TOLLS`.
 * @param {String} [options.optimMode]         Optimisation mode, e.g. `FASTEST`.
 * @param {Array.<Number>} [options.connectorTypes] Filter on these connector IDs.
 * @param {Boolean} [options.restrictedEvse=false]
 *        Allow private / employees-only stations.
 * @param {Boolean} [options.polyline=true]    Include the route polyline.
 * @param {Boolean} [options.encodedPolyline=false]
 *        Include the Google-encoded polyline.
 * @param {Boolean} [options.events=false]     Include route timeline events.
 * @param {Number} [options.eventFrequency=60] Event sampling, seconds (`evtFreq`).
 * @param {Number} [options.alternative=0]     Alternative-route index (0..2) (`alr`).
 * @param {String} [options.currency]          ISO 4217 currency for cost calc.
 * @param {String} [options.drivingStyle]      Driving style enum.
 * @param {Boolean} [options.allowNaStatus=false]
 *        Allow stops on charging stations with unknown availability (`allowNaStatus`).
 * @param {Boolean} [options.allowMaxSpeedReco=false]
 *        Recommend max speed between step points (`allowMaxSpdReco`).
 * @param {Boolean} [options.co2Emissions=false]
 *        Include saved CO2 estimation in the journey summary.
 * @param {Boolean} [options.debugStat=false]  Server-debug flag, leave false in prod.
 * @param {String|Date|Number} [options.arrivalTime]  Arrive-by time (epoch ms / Date / ISO) — alternative to departureTime.
 * @param {Number} [options.extraPayload]      Extra payload weight (kg) on top of `payload`.
 * @param {Array.<Object>} [options.stepPointTimeSlots]  Per-step charge time-slot windows (forced charges take priority).
 * @param {Boolean} [options.aroundEvse=false] Include charging stations around step points in the response.
 * @param {Boolean} [options.ignoreStatus=false]  Ignore charging-station availability status.
 * @param {String} [options.evtExtKey]         Extended route-timeline key (requires events:true).
 * @param {String} [options.geoserver]         Override the Context geoserver.
 * @param {Number} [options.chargingStationFiltersVersion] (v2) Charging-station filter logic version (1|2).
 * @param {Array.<String>} [options.tariffChargePassHashIds] (v2) Eligible charge-pass hash IDs.
 * @param {Boolean} [options.tollCost=false]   (v2) Enable toll-cost calculation + toll events.
 * @param {Boolean} [options.routesheet=false] (v2) Enable the route-sheet.
 * @param {String} [options.routesheetLanguage] (v2) Route-sheet language.
 * @param {String} [options.routesheetMode]     (v2) Route-sheet mode: `TEXT`|`DETAILS`|`TEXT_DETAILS`.
 * @param {String} [options.routesheetVerboseLevel] (v2) Route-sheet verbosity: `LOW`|`MEDIUM`|`HIGH`.
 *
 * NOTE ON VERSIONS: the same public options serialise to either wire shape —
 * `toJson()` builds the REST **v1.0** body (flat), `toJsonV2()` builds the REST
 * **v2.0.0** body (nested `vehicle`/`start`/`stop`/`vias`/`condition`).
 * `bemap.EvSmartRouting.calculate()` uses `toJsonV2()`; the `(v2)` options above
 * are honoured only by `toJsonV2()`.
 */
bemap.EvSmartRoutingRequest = function(options) {
    var opts = options || {};

    this.start = opts.start || null;
    this.stop = opts.stop || null;
    this.vias = Array.isArray(opts.vias) ? opts.vias.slice() : null;

    this.vehicle = opts.vehicle || null;
    this.chargingStationProviders = Array.isArray(opts.chargingStationProviders) ? opts.chargingStationProviders.slice() : null;
    this.chargingStationFilters = Array.isArray(opts.chargingStationFilters) ? opts.chargingStationFilters.slice() : null;
    this.chargingStationDeprecatedConnector = !!opts.chargingStationDeprecatedConnector;

    this.initBatteryLevel = (typeof opts.initBatteryLevel === 'number') ? opts.initBatteryLevel : 100;
    this.minBatteryLevel = (typeof opts.minBatteryLevel === 'number') ? opts.minBatteryLevel : 0;
    this.minArrivalBatteryLevel = (typeof opts.minArrivalBatteryLevel === 'number') ? opts.minArrivalBatteryLevel : 0;
    this.maxAfterChargeBatteryLevel = (typeof opts.maxAfterChargeBatteryLevel === 'number') ? opts.maxAfterChargeBatteryLevel : null;

    this.weather = !!opts.weather;
    this.weatherProvider = opts.weatherProvider || null;
    this.temperature = (typeof opts.temperature === 'number') ? opts.temperature : 20;

    this.departureTime = opts.departureTime || null;
    this.stepPointPluggingTime = (typeof opts.stepPointPluggingTime === 'number') ? opts.stepPointPluggingTime : 0;
    this.payload = (typeof opts.payload === 'number') ? opts.payload : 75;

    this.criterias = Array.isArray(opts.criterias) ? opts.criterias.slice() : null;
    this.optimMode = opts.optimMode || null;

    this.connectorTypes = Array.isArray(opts.connectorTypes) ? opts.connectorTypes.slice() : null;
    this.restrictedEvse = !!opts.restrictedEvse;

    this.polyline = (opts.polyline !== false);
    this.encodedPolyline = !!opts.encodedPolyline;
    this.events = !!opts.events;
    this.eventFrequency = (typeof opts.eventFrequency === 'number') ? opts.eventFrequency : 60;
    this.alternative = (typeof opts.alternative === 'number') ? opts.alternative : 0;
    this.currency = opts.currency || null;
    this.drivingStyle = opts.drivingStyle || null;

    this.allowNaStatus = !!opts.allowNaStatus;
    this.allowMaxSpeedReco = !!opts.allowMaxSpeedReco;
    this.co2Emissions = !!opts.co2Emissions;
    this.debugStat = !!opts.debugStat;

    this.arrivalTime = opts.arrivalTime || null;
    this.extraPayload = (typeof opts.extraPayload === 'number') ? opts.extraPayload : null;
    this.stepPointTimeSlots = Array.isArray(opts.stepPointTimeSlots) ? opts.stepPointTimeSlots.slice() : null;
    this.aroundEvse = !!opts.aroundEvse;
    this.ignoreStatus = !!opts.ignoreStatus;
    this.evtExtKey = opts.evtExtKey || null;

    // --- v2-only optional fields (REST v2.0.0 — service/2.0/evsmartrouting) ---
    // Accepted but optional; the v1 `toJson()` path ignores them. See `toJsonV2()`.
    this.chargingStationFiltersVersion = (typeof opts.chargingStationFiltersVersion === 'number') ? opts.chargingStationFiltersVersion : null;
    this.tariffChargePassHashIds = Array.isArray(opts.tariffChargePassHashIds) ? opts.tariffChargePassHashIds.slice() : null;
    this.tollCost = !!opts.tollCost;
    this.routesheet = !!opts.routesheet;
    this.routesheetLanguage = opts.routesheetLanguage || null;
    this.routesheetMode = opts.routesheetMode || null;
    this.routesheetVerboseLevel = opts.routesheetVerboseLevel || null;

    this.geoserver = opts.geoserver || null;
};

/**
 * Coordinate -> {lon, lat} or extract lon/lat to flat numeric fields, used
 * by `toJson()` for endpoints with start/stop expressed as four scalars.
 * @private
 */
bemap.EvSmartRoutingRequest._lonOf = function(c) {
    if (!c) return null;
    if (typeof c.getLon === 'function') return c.getLon();
    if (c.lon !== undefined) return c.lon;
    if (c.longitude !== undefined) return c.longitude;
    return null;
};
bemap.EvSmartRoutingRequest._latOf = function(c) {
    if (!c) return null;
    if (typeof c.getLat === 'function') return c.getLat();
    if (c.lat !== undefined) return c.lat;
    if (c.latitude !== undefined) return c.latitude;
    return null;
};

/**
 * Build the wire-format JSON body. Drops null / default fields so the
 * payload stays minimal.
 *
 * @public
 * @return {Object}
 */
bemap.EvSmartRoutingRequest.prototype.toJson = function() {
    var body = {};
    var lonOf = bemap.EvSmartRoutingRequest._lonOf;
    var latOf = bemap.EvSmartRoutingRequest._latOf;

    if (this.start) {
        body.startLon = lonOf(this.start);
        body.startLat = latOf(this.start);
    }
    if (this.stop) {
        body.stopLon = lonOf(this.stop);
        body.stopLat = latOf(this.stop);
    }
    if (this.vias && this.vias.length) {
        body.vias = this.vias.map(function(v) {
            return { lon: lonOf(v), lat: latOf(v) };
        });
    }

    if (this.vehicle) body.vehicle = this.vehicle;
    if (this.chargingStationProviders && this.chargingStationProviders.length) {
        body.csps = this.chargingStationProviders.slice();
    }
    if (this.chargingStationFilters && this.chargingStationFilters.length) {
        body.csfs = this.chargingStationFilters.slice();
    }
    if (this.chargingStationDeprecatedConnector) body.csdepcnt = true;

    body.initBatLvl = this.initBatteryLevel;
    if (this.minBatteryLevel) body.minBatLvl = this.minBatteryLevel;
    if (this.minArrivalBatteryLevel) body.minArrivalBatLvl = this.minArrivalBatteryLevel;
    if (this.maxAfterChargeBatteryLevel !== null) body.maxAfterChargeBatLvl = this.maxAfterChargeBatteryLevel;

    if (this.weather) body.weather = true;
    if (this.weatherProvider) body.wp = this.weatherProvider;
    if (this.temperature !== 20) body.temperature = this.temperature;

    if (this.departureTime !== null && this.departureTime !== undefined) {
        // The Java DTO declares departureTime as String (epoch-ms as a string,
        // or an ISO local-date-time). Coerce a Date to its epoch-ms string
        // form so the JSON value is unambiguously a string regardless of
        // Jackson coercion rules on the server.
        if (this.departureTime instanceof Date) body.departureTime = String(this.departureTime.getTime());
        else if (typeof this.departureTime === 'number') body.departureTime = String(this.departureTime);
        else body.departureTime = this.departureTime;
    }
    if (this.stepPointPluggingTime) body.stepPointPluggingTime = this.stepPointPluggingTime;
    if (this.payload !== 75) body.payload = this.payload;

    if (this.criterias && this.criterias.length) body.criterias = this.criterias.slice();
    if (this.optimMode) body.optimMode = this.optimMode;

    if (this.connectorTypes && this.connectorTypes.length) body.connectorTypes = this.connectorTypes.slice();
    if (this.restrictedEvse) body.restrictedEvse = true;

    body.pl = this.polyline;
    if (this.encodedPolyline) body.epl = true;
    if (this.events) {
        body.evt = true;
        if (this.eventFrequency !== 60) body.evtFreq = this.eventFrequency;
    }
    if (this.alternative) body.alr = this.alternative;
    if (this.currency) body.cur = this.currency;
    if (this.drivingStyle) body.drivingStyle = this.drivingStyle;
    if (this.allowNaStatus) body.allowNaStatus = true;
    if (this.allowMaxSpeedReco) body.allowMaxSpdReco = true;
    if (this.co2Emissions) body.co2emissions = true;
    if (this.debugStat) body.debugStat = true;

    if (this.arrivalTime !== null && this.arrivalTime !== undefined) {
        // Server declares arrivalTime as String (epoch-ms) — mirror departureTime handling.
        if (this.arrivalTime instanceof Date) body.arrivalTime = String(this.arrivalTime.getTime());
        else if (typeof this.arrivalTime === 'number') body.arrivalTime = String(this.arrivalTime);
        else body.arrivalTime = this.arrivalTime;
    }
    if (this.extraPayload !== null) body.extraPayload = this.extraPayload;
    if (this.stepPointTimeSlots && this.stepPointTimeSlots.length) body.stepPointTimeSlots = this.stepPointTimeSlots.slice();
    if (this.aroundEvse) body.aroundEvse = true;
    if (this.ignoreStatus) body.ignoreStatus = true;
    if (this.evtExtKey) body.evtExtKey = this.evtExtKey;

    if (this.geoserver) body.geoserver = this.geoserver;

    return body;
};

/**
 * Build the REST **v2.0.0** wire-format body for `POST service/2.0/evsmartrouting`.
 *
 * Same public options as `toJson()` (v1) — only the wire SHAPE differs: v2 nests
 * `vehicle{key,initBatLvl,payload}`, `start`/`stop`/`vias` as `{lon,lat}` places,
 * and most trip options inside `condition{}` (with the v2 key names). Drops
 * null / default fields. The v1 `toJson()` above is left untouched.
 *
 * @public
 * @since 2.0.0
 * @return {Object}
 */
bemap.EvSmartRoutingRequest.prototype.toJsonV2 = function() {
    var body = {};
    var lonOf = bemap.EvSmartRoutingRequest._lonOf;
    var latOf = bemap.EvSmartRoutingRequest._latOf;

    // vehicle{} (required) — key + battery/payload move inside the object.
    var vehicle = {};
    if (this.vehicle) vehicle.key = this.vehicle;
    vehicle.initBatLvl = this.initBatteryLevel;      // always sent (default 100)
    vehicle.payload = this.payload;                  // always sent (default 75)
    body.vehicle = vehicle;

    // start / stop / vias — structured PlaceFront (lon/lat).
    if (this.start) body.start = { lon: lonOf(this.start), lat: latOf(this.start) };
    if (this.stop) body.stop = { lon: lonOf(this.stop), lat: latOf(this.stop) };
    if (this.vias && this.vias.length) {
        body.vias = this.vias.map(function(v) { return { lon: lonOf(v), lat: latOf(v) }; });
    }

    // Charging providers / filters stay top-level in v2.
    if (this.chargingStationProviders && this.chargingStationProviders.length) body.csps = this.chargingStationProviders.slice();
    if (this.chargingStationFilters && this.chargingStationFilters.length) body.csfs = this.chargingStationFilters.slice();
    if (this.chargingStationFiltersVersion !== null) body.csfsVersion = this.chargingStationFiltersVersion;
    if (this.tariffChargePassHashIds && this.tariffChargePassHashIds.length) body.tariffChargePassHashIds = this.tariffChargePassHashIds.slice();

    // condition{} — most former top-level options live here in v2, with new keys.
    var cond = {};
    if (this.optimMode) cond.optimMode = this.optimMode;
    if (this.criterias && this.criterias.length) cond.criterias = this.criterias.slice();
    if (this.alternative) cond.alternative = this.alternative;
    if (this.departureTime !== null && this.departureTime !== undefined) {
        // Server field is String (epoch-ms or ISO local date-time) — mirror v1 coercion.
        if (this.departureTime instanceof Date) cond.departureTime = String(this.departureTime.getTime());
        else if (typeof this.departureTime === 'number') cond.departureTime = String(this.departureTime);
        else cond.departureTime = this.departureTime;
    }
    if (this.temperature !== 20) cond.temperature = this.temperature;
    if (this.minBatteryLevel) cond.minBatLvl = this.minBatteryLevel;
    if (this.minArrivalBatteryLevel) cond.minArrivalBatLvl = this.minArrivalBatteryLevel;
    if (this.maxAfterChargeBatteryLevel !== null) cond.maxAfterChargeBatLvl = this.maxAfterChargeBatteryLevel;
    if (this.connectorTypes && this.connectorTypes.length) cond.connectorTypes = this.connectorTypes.slice();
    if (this.chargingStationDeprecatedConnector) cond.chargingStationDeprecatedConnector = true;
    // v2 renamed `stepPointPluggingTime` → `chargePluggingTime` (server default 300s).
    // Send the SDK value (default 0) explicitly so behaviour matches v1 — no
    // silent 300s-per-stop when the consumer did not ask for it.
    cond.chargePluggingTime = this.stepPointPluggingTime;
    if (this.stepPointTimeSlots && this.stepPointTimeSlots.length) cond.chargeTimeSlots = this.stepPointTimeSlots.slice();
    if (this.restrictedEvse) cond.restrictedEvse = true;
    if (this.allowNaStatus) cond.allowNaStatus = true;
    if (this.ignoreStatus) cond.ignoreAvailableStatus = true;   // renamed from `ignoreStatus`
    if (this.drivingStyle) {
        // v1 was a string enum; v2 is an object `{mode, …}`. Wrap a string; copy an
        // object so a later caller mutation cannot corrupt the already-built body.
        cond.drivingStyle = (typeof this.drivingStyle === 'string')
            ? { mode: this.drivingStyle }
            : Object.assign({}, this.drivingStyle);
    }
    if (this.allowMaxSpeedReco) cond.allowMaxSpeedRecommendation = true;   // renamed from `allowMaxSpdReco`
    // Polyline defaults ON in the SDK; v2 `geometry` defaults false server-side, so send it.
    cond.geometry = this.polyline;
    if (this.encodedPolyline) cond.encodedGeometry = true;      // renamed from `epl`
    if (this.co2Emissions) cond.co2emissions = true;
    if (this.weather) cond.weather = true;
    if (this.weatherProvider) cond.weatherProvider = this.weatherProvider;
    if (this.currency) cond.currency = this.currency;
    if (this.events) {                                          // `evt` → `routeDetails`
        cond.routeDetails = true;
        if (this.eventFrequency !== 60) cond.routeDetailsFreq = this.eventFrequency;
    }
    if (this.evtExtKey) cond.routeDetailsExtKey = this.evtExtKey;
    // v2-only optional extras.
    if (this.tollCost) cond.tollCost = true;
    if (this.routesheet) cond.routesheet = true;
    if (this.routesheetLanguage) cond.routesheetLanguage = this.routesheetLanguage;
    if (this.routesheetMode) cond.routesheetMode = this.routesheetMode;
    if (this.routesheetVerboseLevel) cond.routesheetVerboseLevel = this.routesheetVerboseLevel;
    body.condition = cond;

    // v1-only options with no v2 equivalent (arrivalTime, aroundEvse, debugStat,
    // extraPayload) are intentionally omitted here; they remain available via the
    // v1 `toJson()` path.

    if (this.geoserver) body.geoserver = this.geoserver;

    return body;
};