Source: bemap-services-v2/ev/bemap-ev-vehicles.js

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