/**
* 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;
};