/**
* BeNomad BeMap JavaScript API - EvVehicles service (v1.5)
*
* Promise-based wrapper around the v1.1 vehicle catalogue endpoints:
*
* - `GET service/vehicle/1.1/getbrands` → `{brands: [{id, label}, ...]}`
* - `GET service/vehicle/1.1/findvehicles` → `{vehicles: [VehicleInfo, ...]}`
*
* **Why v1.1 (not the lightweight `evsmartrouting/vehicles*` endpoints).**
* v1.1 returns structured plaintext fields — brandName, name, year,
* variant, batteryName (kWh), chargerPowerDC (kW), chargerPowerAcThreePhases
* (kW), connectorTypes[], wltp, consumptionInWhPerKm, dimensions — same
* shape the Flutter SDK consumes via
* `bemap-flutter-api/lib/service/vehicle/model/vehicle.dart`. No regex
* parsing of pre-formatted titles, no AES decryption of `encDatasheet`.
*
* Auth roles: `ROLE_VEHICLE` (+ `ROLE_VEHICLE_DATASHEET` for
* `enableDatasheet: true`).
*
* `bemap.EvSmartRouting.getBrands()` / `.getVehicles()` keep their existing
* lightweight `evsmartrouting/vehicles*` endpoints for backward compat —
* the two services serve different use cases.
*
* @since 1.5.0
* @public
* @constructor
* @extends {bemap.ServiceV2}
* @param {bemap.Context} context
* @param {Object} [options]
*/
bemap.EvVehicles = function(context, options) {
bemap.ServiceV2.call(this, context, options);
};
bemap.inherits(bemap.EvVehicles, bemap.ServiceV2);
/**
* List EV brands the current user has access to.
* @public
* @param {Object} [options]
* @param {AbortSignal} [options.signal]
* @return {Promise<Array<bemap.EvBrand>>} Sorted by label for deterministic UI.
*/
bemap.EvVehicles.prototype.brands = function(options) {
var opts = options || {};
return this._get('service/vehicle/1.1/getbrands', { signal: opts.signal })
.then(function(json) {
var raw = (json && Array.isArray(json.brands)) ? json.brands : [];
var brands = raw.map(bemap.EvBrand.fromJson);
brands.sort(function(a, b) {
var la = (a.getLabel() || '').toLowerCase();
var lb = (b.getLabel() || '').toLowerCase();
if (la < lb) return -1;
if (la > lb) return 1;
return 0;
});
return brands;
});
};
/**
* List vehicles, optionally filtered by brand or by an exact vehicle key.
*
* @public
* @param {Object} [filters]
* @param {String} [filters.brandId] Restrict to one brand.
* @param {String} [filters.vehicle] Restrict to one vehicle key (returns 0 or 1 entries).
* @param {String} [filters.variant] Restrict to one variant (v1.1).
* @param {Boolean} [filters.enableDatasheet=false] Include the encrypted datasheet blob.
* @param {AbortSignal} [filters.signal]
* @return {Promise<Array<bemap.EvVehicle>>}
*/
bemap.EvVehicles.prototype.list = function(filters) {
var f = filters || {};
var qs = [];
if (f.brandId) qs.push('brandId=' + encodeURIComponent(f.brandId));
if (f.vehicle) qs.push('vehicle=' + encodeURIComponent(f.vehicle));
if (f.variant) qs.push('variant=' + encodeURIComponent(f.variant));
if (f.enableDatasheet) qs.push('enableDatasheet=true');
var path = 'service/vehicle/1.1/findvehicles' + (qs.length ? '?' + qs.join('&') : '');
return this._get(path, { signal: f.signal })
.then(function(json) {
var raw = (json && Array.isArray(json.vehicles)) ? json.vehicles : [];
return raw.map(bemap.EvVehicle.fromJson);
});
};
/**
* Fetch one vehicle by its UUID key.
*
* @public
* @param {String} vehicleKey
* @param {Object} [options]
* @param {Boolean} [options.enableDatasheet=false]
* @param {AbortSignal} [options.signal]
* @return {Promise<bemap.EvVehicle>} Rejects with `bemap.Error.VEHICLE_NOT_FOUND` when no match.
*/
bemap.EvVehicles.prototype.get = function(vehicleKey, options) {
if (!vehicleKey) {
return Promise.reject(new bemap.Error({
code: bemap.Error.INVALID_ARGUMENT,
message: 'EvVehicles.get requires a vehicleKey'
}));
}
var opts = options || {};
return this.list({
vehicle: vehicleKey,
enableDatasheet: opts.enableDatasheet,
signal: opts.signal
}).then(function(vehicles) {
if (!vehicles.length) {
throw new bemap.Error({
code: bemap.Error.VEHICLE_NOT_FOUND,
message: "Vehicle '" + vehicleKey + "' was not found in the catalogue"
});
}
return vehicles[0];
});
};
/**
* Walk the catalogue progressively, one attribute at a time — used by
* cascading "brand → name → variant → battery → DC power → AC power"
* UI flows (as in the `evmove5` car-select screen). Wraps
* `GET service/vehicle/1.1/getlevelvehicleinfo`.
*
* The `level` argument names which attribute the server returns. Each
* deeper level filters by the levels already chosen.
*
* Levels: `'NAME'` · `'VARIANT'` · `'YEAR'` · `'BATTERY_NAME'` ·
* `'CHARGE_POWER_DC'` · `'CHARGE_POWER_AC'` · `'MOTOR_TYPE'`.
*
* @public
* @since 1.5.0
* @param {Object} filters
* @param {String} filters.level **R** One of the level names above.
* @param {String} filters.brandId **R**
* @param {String} [filters.name]
* @param {String} [filters.variant]
* @param {String} [filters.year]
* @param {String} [filters.batteryName]
* @param {String} [filters.motorType]
* @param {Number} [filters.chargerPowerDC]
* @param {Number} [filters.chargerPowerAC]
* @param {AbortSignal} [filters.signal]
* @return {Promise<Array<String>>}
*/
bemap.EvVehicles.prototype.levels = function(filters) {
var f = filters || {};
if (!f.level) {
return Promise.reject(new bemap.Error({
code: bemap.Error.INVALID_ARGUMENT,
message: 'EvVehicles.levels: filters.level is required'
}));
}
if (!f.brandId) {
return Promise.reject(new bemap.Error({
code: bemap.Error.INVALID_ARGUMENT,
message: 'EvVehicles.levels: filters.brandId is required'
}));
}
var qs = ['level=' + encodeURIComponent(f.level), 'brandId=' + encodeURIComponent(f.brandId)];
if (f.name) qs.push('name=' + encodeURIComponent(f.name));
if (f.variant) qs.push('variant=' + encodeURIComponent(f.variant));
if (f.year) qs.push('year=' + encodeURIComponent(f.year));
if (f.batteryName) qs.push('batteryName=' + encodeURIComponent(f.batteryName));
if (f.motorType) qs.push('motorType=' + encodeURIComponent(f.motorType));
if (typeof f.chargerPowerDC === 'number') qs.push('chargerPowerDC=' + f.chargerPowerDC);
if (typeof f.chargerPowerAC === 'number') qs.push('chargerPowerAC=' + f.chargerPowerAC);
var path = 'service/vehicle/1.1/getlevelvehicleinfo?' + qs.join('&');
return this._get(path, { signal: f.signal })
.then(function(json) {
return Array.isArray(json) ? json : [];
});
};
/**
* Fetch the JSON-wrapped logo description for an EV brand (includes the
* base64-encoded image bytes and the picture format). Wraps
* `GET service/vehicle/1.1/getbrandlogo`.
*
* For embedding the logo directly in `<img src="...">` markup, prefer
* `getBrandLogoUrl()` — it returns the URL of the binary endpoint.
*
* @public
* @since 1.5.0
* @param {String} brandId
* @param {Object} [options]
* @param {AbortSignal} [options.signal]
* @return {Promise<{brandId: String, pictureFileFormat: Object, logo: String}>}
* `logo` is base64-encoded image bytes.
*/
bemap.EvVehicles.prototype.brandLogo = function(brandId, options) {
if (!brandId) {
return Promise.reject(new bemap.Error({
code: bemap.Error.INVALID_ARGUMENT,
message: 'EvVehicles.brandLogo: brandId is required'
}));
}
var opts = options || {};
return this._get('service/vehicle/1.1/getbrandlogo?brandId=' + encodeURIComponent(brandId),
{ signal: opts.signal });
};
/**
* Build the URL of the binary brand-logo endpoint
* (`GET service/vehicle/1.1/getbrandlogolite`) — returns raw image bytes
* with the correct `Content-Type`. Customers fetch this with their own
* auth (Basic via `Authorization` header) and turn the response into a
* `Blob` URL for `<img src>`.
*
* **In proxy mode** (`ctx.proxy`, EVMOVE-307) do NOT add an `Authorization`
* header — the URL already points at the proxy, which injects the credentials
* server-side. Fetch it with no auth header at all.
*
* @public
* @since 1.5.0
* @param {String} brandId
* @return {String} Full URL including the Context's base URL.
*/
bemap.EvVehicles.prototype.getBrandLogoUrl = function(brandId) {
if (!brandId) return null;
return this.ctx.getBaseUrl() + 'service/vehicle/1.1/getbrandlogolite?brandId=' + encodeURIComponent(brandId);
};
/**
* Build the URL of the vehicle-photo endpoint
* (`GET service/vehicle/picture/1.0/getone?vehicleUuidKey=<key>`). Used
* by customer UIs (e.g. `evmove5`'s car-select screen) to render vehicle
* thumbnails. Customers fetch with their own auth.
*
* @public
* @since 1.5.0
* @param {String} vehicleKey
* @return {String}
*/
bemap.EvVehicles.prototype.getPictureUrl = function(vehicleKey) {
if (!vehicleKey) return null;
return this.ctx.getBaseUrl() + 'service/vehicle/picture/1.0/getone?vehicleUuidKey=' + encodeURIComponent(vehicleKey);
};
/**
* Convenience: fetch a vehicle and return a `RoutingVehicleProfile` ready
* to plug into `bemap.RoutingV2.calculate()` or
* `bemap.EvSmartRouting.calculate()`.
*
* @public
* @param {String} vehicleKey
* @param {Object} [options]
* @param {AbortSignal} [options.signal]
* @return {Promise<bemap.RoutingVehicleProfile>}
*/
bemap.EvVehicles.prototype.toRoutingProfile = function(vehicleKey, options) {
return this.get(vehicleKey, options).then(function(vehicle) {
var profile = {};
if (vehicle.getTransportType()) profile.transportMode = vehicle.getTransportType();
var dims = vehicle.getDimensions();
if (dims && dims.height !== null) profile.height = dims.height;
if (dims && dims.width !== null) profile.width = dims.width;
if (dims && dims.length !== null) profile.length = dims.length;
if (vehicle.getMaxWeight() !== null) profile.weight = vehicle.getMaxWeight();
if (vehicle.getMaxWeightPerAxle() !== null) profile.axleWeight = vehicle.getMaxWeightPerAxle();
return new bemap.RoutingVehicleProfile(profile);
});
};