Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions addon/components/admin/valhalla-settings.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<ContentPanel @title="Configure Valhalla" @open={{true}} @wrapperClass="bordered-classic">
<InputGroup @name="Valhalla API Host" @helpText="Base URL for the default Valhalla routing service used across organizations.">
<Input @value={{this.apiHost}} placeholder="https://valhalla1.openstreetmap.de" class="w-full form-input" />
</InputGroup>
<InputGroup @name="Valhalla API Key" @helpText="Optional API key for the system-wide Valhalla service.">
<Input @value={{this.apiKey}} placeholder="Enter system Valhalla API key" class="w-full form-input" />
</InputGroup>

<div class="mt-4">
<Button @text="Save Valhalla Settings" @type="primary" @size="sm" @icon="save" @onClick={{perform this.saveValhallaSettings}} @isLoading={{this.saveValhallaSettings.isRunning}} />
</div>
</ContentPanel>
44 changes: 44 additions & 0 deletions addon/components/admin/valhalla-settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { debug } from '@ember/debug';
import { task } from 'ember-concurrency';

export default class AdminValhallaSettingsComponent extends Component {
@service fetch;
@service notifications;
@tracked apiKey;
@tracked apiHost;

constructor() {
super(...arguments);
this.loadValhallaSettings.perform();
}

@task *loadValhallaSettings() {
try {
const { api_key, api_host } = yield this.fetch.get('admin-settings', {}, { namespace: 'valhalla/int/v1' });
this.apiKey = api_key;
this.apiHost = api_host;
} catch (err) {
debug(`Valhalla : Error fetching admin valhalla settings: ${err.message}`);
}
}

@task *saveValhallaSettings() {
try {
yield this.fetch.post(
'admin-settings',
{
api_key: this.apiKey,
api_host: this.apiHost,
},
{ namespace: 'valhalla/int/v1' }
);
this.notifications.success('Valhalla settings saved.');
} catch (err) {
debug(`Valhalla : Error saving admin valhalla settings: ${err.message}`);
this.notifications.serverError(err);
}
}
}
16 changes: 16 additions & 0 deletions addon/components/organization/valhalla-settings.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{{#if this.isActive}}
<ContentPanel @title="Configure Valhalla" @open={{true}} @wrapperClass="bordered-classic">
<InputGroup @name="Organization Override">
<Checkbox @value={{this.useOwnServer}} @label="Use my own Valhalla server" @onToggle={{fn (mut this.useOwnServer)}} @alignItems="center" @labelClass="mb-0i" />
</InputGroup>

{{#if this.useOwnServer}}
<InputGroup @name="Valhalla API Host" @helpText="Organization-specific Valhalla host override.">
<Input @value={{this.apiHost}} placeholder="https://your-valhalla-host.example.com" class="w-full form-input" />
</InputGroup>
<InputGroup @name="Valhalla API Key" @helpText="Optional organization-specific API key override.">
<Input @value={{this.apiKey}} placeholder="Enter organization Valhalla API key" class="w-full form-input" />
</InputGroup>
{{/if}}
</ContentPanel>
{{/if}}
67 changes: 67 additions & 0 deletions addon/components/organization/valhalla-settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { debug } from '@ember/debug';
import { task } from 'ember-concurrency';

export default class OrganizationValhallaSettingsComponent extends Component {
@service fetch;
@service notifications;
@tracked useOwnServer = false;
@tracked apiKey;
@tracked apiHost;

get saveTaskKey() {
return 'organization:valhalla-settings';
}

constructor(owner, args) {
super(owner, args);
args?.controller?.registerSaveTask(this.saveTaskKey, this.saveValhallaSettings);
this.loadValhallaSettings.perform();
}

willDestroy() {
super.willDestroy(...arguments);
this.controller?.unregisterSaveTask(this.saveTaskKey);
}

get controller() {
return this.args.controller;
}

get isActive() {
return this.controller?.displayEngine === 'valhalla' || this.controller?.optimizationEngine === 'valhalla';
}

@task *loadValhallaSettings() {
try {
const { api_key, api_host } = yield this.fetch.get('settings', {}, { namespace: 'valhalla/int/v1' });
this.apiKey = api_key;
this.apiHost = api_host;
this.useOwnServer = Boolean((typeof api_key === 'string' && api_key.trim() !== '') || (typeof api_host === 'string' && api_host.trim() !== ''));
} catch (err) {
debug(`Valhalla : Error fetching organization valhalla settings: ${err.message}`);
}
}

@task *saveValhallaSettings() {
if (!this.isActive) {
return true;
}

try {
yield this.fetch.post(
'settings',
{
api_key: this.useOwnServer ? this.apiKey : null,
api_host: this.useOwnServer ? this.apiHost : null,
},
{ namespace: 'valhalla/int/v1' }
);
} catch (err) {
debug(`Valhalla : Error saving organization valhalla settings: ${err.message}`);
this.notifications.serverError(err);
}
}
}
10 changes: 10 additions & 0 deletions addon/extension.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { ExtensionComponent } from '@fleetbase/ember-core/contracts';

export default {
setupExtension(app, universe) {
// Register Valhalla Route Optimization
universe.whenEngineLoaded('@fleetbase/fleetops-engine', this.registerValhalla);

// Register settings components
universe.registerRenderableComponent('fleet-ops:template:settings:routing', new ExtensionComponent('@fleetbase/valhalla-engine', 'organization/valhalla-settings'));
universe.registerRenderableComponent('fleet-ops:component:admin:routing-settings', new ExtensionComponent('@fleetbase/valhalla-engine', 'admin/valhalla-settings'));
},

async registerValhalla(fleetopsEngine, universe) {
const valhallaEngine = await universe.extensionManager.ensureEngineLoaded('@fleetbase/valhalla-engine');
const routeOptimization = fleetopsEngine.lookup('service:route-optimization');
const routeEngine = fleetopsEngine.lookup('service:route-engine');
const valhalla = valhallaEngine.lookup('service:valhalla');
if (routeOptimization && valhalla) {
routeOptimization.register('valhalla', valhalla);
}
if (routeEngine && valhalla) {
routeEngine.register('valhalla', valhalla, { display: true });
}
},
};
75 changes: 74 additions & 1 deletion addon/services/valhalla.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
import RouteOptimizationInterfaceService from '@fleetbase/fleetops-engine/services/route-optimization-interface';
import { isArray } from '@ember/array';
import { debug } from '@ember/debug';
import polyline from '@fleetbase/ember-core/utils/polyline';

export default class ValhallaService extends RouteOptimizationInterfaceService {
name = 'Valhalla';

async computeRoute(waypoints = [], options = {}) {
const locations = (isArray(waypoints) ? waypoints : [])
.map(([lat, lng], index, arr) => ({
lat: Number(lat),
lon: Number(lng),
type: index === 0 || index === arr.length - 1 ? 'break' : 'through',
}))
.filter((location) => Number.isFinite(location.lat) && Number.isFinite(location.lon));

if (locations.length < 2) {
throw new Error('At least 2 waypoints are required to compute a route.');
}

const result = await this.#request('route', { locations, costing: options.costing ?? 'auto' }, options);
return this.#normalizeRouteResult(result, waypoints);
}

async optimize({ order, waypoints, coordinates }, options = {}) {
const driverAssigned = order.driver_assigned;
const driverPosition = driverAssigned?.location?.coordinates; // [lon,lat] | undefined
Expand All @@ -28,8 +47,22 @@ export default class ValhallaService extends RouteOptimizationInterfaceService {

// Extract the Ember models (null-safe)
const sortedWaypoints = payloadPairs.map((p) => p.model).filter(Boolean);
const normalizedRoute = this.#normalizeRouteResult(
result,
sortedWaypoints.map((wp) => [wp.place.latitude, wp.place.longitude])
);

return { sortedWaypoints, result, engine: 'valhalla' };
return {
sortedWaypoints,
route: normalizedRoute.coordinates,
trip: {
distance: normalizedRoute.summary.totalDistance,
duration: normalizedRoute.summary.totalTime,
locations: result?.trip?.locations ?? [],
},
result,
engine: 'valhalla',
};
} catch (err) {
debug(`[Valhalla] Error optimizing route : ${err.message}`);
throw err;
Expand All @@ -39,4 +72,44 @@ export default class ValhallaService extends RouteOptimizationInterfaceService {
#request(path, data = {}, options = {}) {
return this.fetch.post(path, data, { namespace: 'valhalla/int/v1', ...options });
}

#normalizeRouteResult(result, waypoints = []) {
const legs = isArray(result?.trip?.legs) ? result.trip.legs : [];
const coordinates = [];

legs.forEach((leg) => {
const decoded = leg?.shape ? polyline.decode(leg.shape, 6) : [];
decoded.forEach((coord) => coordinates.push(coord));
});

return {
engine: 'valhalla',
waypoints,
coordinates,
bounds: this.#boundsFromCoordinates(coordinates),
summary: {
totalDistance: result?.trip?.summary?.length ?? 0,
totalTime: result?.trip?.summary?.time ?? 0,
},
legs,
raw: result,
};
}

#boundsFromCoordinates(coordinates = []) {
if (!coordinates.length) {
return [
[0, 0],
[0, 0],
];
}

const lats = coordinates.map(([lat]) => lat);
const lngs = coordinates.map(([, lng]) => lng);

return [
[Math.min(...lats), Math.min(...lngs)],
[Math.max(...lats), Math.max(...lngs)],
];
}
}
1 change: 1 addition & 0 deletions app/components/admin/valhalla-settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@fleetbase/valhalla-engine/components/admin/valhalla-settings';
1 change: 1 addition & 0 deletions app/components/organization/valhalla-settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@fleetbase/valhalla-engine/components/organization/valhalla-settings';
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fleetbase/valhalla-api",
"version": "0.0.3",
"version": "0.0.4",
"description": "Valhalla routing engine extension for Fleetbase",
"keywords": [
"fleetbase",
Expand Down
2 changes: 1 addition & 1 deletion extension.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "valhalla",
"version": "0.0.3",
"version": "0.0.4",
"description": "Valhalla routing engine integration extension",
"repository": "https://github.com/fleetbase/valhalla",
"license": "AGPL-3.0-or-later",
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fleetbase/valhalla-engine",
"version": "0.0.3",
"version": "0.0.4",
"description": "Valhalla routing engine extension for Fleetbase",
"keywords": [
"fleetbase-extension",
Expand Down Expand Up @@ -42,8 +42,8 @@
},
"dependencies": {
"@babel/core": "^7.23.2",
"@fleetbase/ember-core": "^0.3.8",
"@fleetbase/ember-ui": "^0.3.14",
"@fleetbase/ember-core": "^0.3.18",
"@fleetbase/ember-ui": "^0.3.29",
"@fleetbase/leaflet-routing-machine": "^3.2.17",
"@fortawesome/ember-fontawesome": "^2.0.0",
"@fortawesome/fontawesome-svg-core": "6.4.0",
Expand Down
Loading
Loading