Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sync de mudanças do repositório original #4

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -1240,7 +1240,24 @@ export class CsvUploadDialogComponent implements OnDestroy, OnInit {
shipments = shipmentsResults.map((result) => result.shipment);
}
if (this.vehicleFile) {
vehicles = this.service.csvToVehicles(res[1].data, this.mappingFormVehicles.value);
const vehiclesResults = this.service.csvToVehicles(
res[1].data,
this.mappingFormVehicles.value
);

if (vehiclesResults.some((result) => result.errors.length)) {
vehiclesResults.forEach((result, index) => {
this.validationErrors.push(
...result.errors.map(
(error) =>
`Vehicle ${result.vehicle.label || ''} at index ${index}: ${error.message}`
)
);
});
throw Error('Vehicle validation error');
}

vehicles = vehiclesResults.map((result) => result.vehicle);
}

// geocode all shipments, then all vehicles
Expand Down
153 changes: 153 additions & 0 deletions application/frontend/src/app/core/services/csv.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,157 @@ describe('CsvService', () => {
expect(parsedCsv).not.toContain(key);
});
});

describe('Validate shipments', () => {
it('should not throw error on a valid shipment', () => {
const shipments = [{ label: 'test shipment' }];
const testMapping = { Label: 'label' };
const result = service.csvToShipments(shipments, testMapping);
expect(result.length).toBe(1);
expect(result[0].errors.length).toBe(0);
});

it('should not throw error on shipment with a valid load demand', () => {
const shipments = [
{
LoadDemand1Type: 'weight',
LoadDemand1Value: 10,
},
];
const testMapping = {
LoadDemand1Type: 'LoadDemand1Type',
LoadDemand1Value: 'LoadDemand1Value',
};
const result = service.csvToShipments(shipments, testMapping);
expect(result.length).toBe(1);
expect(result[0].errors.length).toBe(0);
});

it('should throw an error on shipment with an invalid load demand', () => {
const shipments = [
{
LoadDemand1Type: 'weight',
LoadDemand1Value: 'invalid',
},
{
LoadDemand1Type: 'weight',
LoadDemand1Value: 10.5,
},
];
const testMapping = {
LoadDemand1Type: 'LoadDemand1Type',
LoadDemand1Value: 'LoadDemand1Value',
};
const result = service.csvToShipments(shipments, testMapping);
expect(result.length).toBe(2);
expect(result[0].errors.length).toBe(1);
expect(result[1].errors.length).toBe(1);
});
});

describe('Validate vehicles', () => {
it('should throw no errors on a valid vehicle', () => {
const vehicles = [{ label: 'test vehicle' }];
const testVehicleMapping = { Label: 'label' };
const result = service.csvToVehicles(vehicles, testVehicleMapping);
expect(result.length).toBe(1);
expect(result[0].errors.length).toBe(0);
});

it('should throw no errors on a vehicle with a valid load limit', () => {
const vehicles = [
{
label: 'test vehicle',
LoadLimit1Type: 'weight',
LoadLimit1Value: 10,
},
];
const testVehicleMapping = {
Label: 'label',
LoadLimit1Type: 'LoadLimit1Type',
LoadLimit1Value: 'LoadLimit1Value',
};
const result = service.csvToVehicles(vehicles, testVehicleMapping);
expect(result.length).toBe(1);
expect(result[0].errors.length).toBe(0);
});

it('should throw an error on a vehicle with a negative load limit', () => {
const vehicles = [
{
label: 'test vehicle',
LoadLimit1Type: 'weight',
LoadLimit1Value: -10,
},
];
const testVehicleMapping = {
Label: 'label',
LoadLimit1Type: 'LoadLimit1Type',
LoadLimit1Value: 'LoadLimit1Value',
};
const result = service.csvToVehicles(vehicles, testVehicleMapping);
expect(result.length).toBe(1);
expect(result[0].errors.length).toBe(1);
});

it('should throw an error on a vehicle with a zero load limit', () => {
const vehicles = [
{
label: 'test vehicle',
LoadLimit1Type: 'weight',
LoadLimit1Value: 0,
},
];
const testVehicleMapping = {
Label: 'label',
LoadLimit1Type: 'LoadLimit1Type',
LoadLimit1Value: 'LoadLimit1Value',
};
const result = service.csvToVehicles(vehicles, testVehicleMapping);
expect(result.length).toBe(1);
expect(result[0].errors.length).toBe(1);
});

it('should throw no errors on a vehicle with a valid travel mode', () => {
const vehicles = [
{
label: 'test vehicle',
TravelMode: 'DRIVING',
},
{
label: 'test vehicle',
TravelMode: 'walking',
},
];
const testVehicleMapping = {
Label: 'label',
TravelMode: 'TravelMode',
};
const result = service.csvToVehicles(vehicles, testVehicleMapping);
expect(result.length).toBe(2);
expect(result[0].errors.length).toBe(0);
expect(result[1].errors.length).toBe(0);
});

it('should throw an error on vehicles with an invalid travel mode', () => {
const vehicles = [
{
label: 'test vehicle',
TravelMode: 'DRIVING',
},
{
label: 'test vehicle',
TravelMode: 'invalid',
},
];
const testVehicleMapping = {
Label: 'label',
TravelMode: 'TravelMode',
};
const result = service.csvToVehicles(vehicles, testVehicleMapping);
expect(result.length).toBe(2);
expect(result[0].errors.length).toBe(0);
expect(result[1].errors.length).toBe(1);
});
});
});
51 changes: 48 additions & 3 deletions application/frontend/src/app/core/services/csv.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
ILatLng,
IShipment,
IVehicle,
TravelMode,
ValidationErrorResponse,
} from '../models';
import { FileService } from './file.service';
Expand Down Expand Up @@ -256,12 +257,21 @@ export class CsvService {
return errors;
}

csvToVehicles(csvVehicles: any[], mapping: { [key: string]: string }): IVehicle[] {
csvToVehicles(
csvVehicles: any[],
mapping: { [key: string]: string }
): { vehicle: IVehicle; errors: ValidationErrorResponse[] }[] {
return csvVehicles.map((vehicle) => {
// Conditionally add each field to the vehicle object, converting from csv strings as needed
const parsedVehicle = {
...this.mapKeyToModelValue('label', 'Label', vehicle, mapping),
...this.mapKeyToModelValue('travelMode', 'TravelMode', vehicle, mapping),
...this.mapKeyToModelValue(
'travelMode',
'TravelMode',
vehicle,
mapping,
this.parseTravelMode
),
...this.mapKeyToModelValue('unloadingPolicy', 'UnloadingPolicy', vehicle, mapping),
...this.mapKeyToModelValue('startWaypoint', 'StartWaypoint', vehicle, mapping),
...this.mapKeyToModelValue('endWaypoint', 'EndWaypoint', vehicle, mapping),
Expand Down Expand Up @@ -298,10 +308,41 @@ export class CsvService {
...this.mapToLoadLimits(vehicle, mapping),
...this.mapToVehicleTimeWindows(vehicle, mapping),
};
return parsedVehicle;
return {
vehicle: parsedVehicle,
errors: this.validateVehicle(parsedVehicle),
};
});
}

private validateVehicle(vehicle: IVehicle): ValidationErrorResponse[] {
const errors = [];

const loadLimitsError = Object.keys(vehicle.loadLimits).some((limitKey) => {
const limit = vehicle.loadLimits[limitKey];
const value = Number.parseFloat(limit.maxLoad as string);
return !Number.isInteger(value) || value < 1;
});

if (loadLimitsError) {
errors.push({
error: true,
message: 'Vehicle contains invalid load limits',
vehicle,
});
}

if ('travelMode' in vehicle && !vehicle.travelMode) {
errors.push({
error: true,
message: 'Vehicle has an invalid travel mode',
vehicle,
});
}

return errors;
}

private mapToPickup(shipment: any, mapping: { [key: string]: string }, timeWindow: any): any {
const pickup = {
...this.mapKeyToModelValue('arrivalWaypoint', 'PickupArrivalWaypoint', shipment, mapping),
Expand Down Expand Up @@ -536,6 +577,10 @@ export class CsvService {
}
}

private parseTravelMode(value: string): number {
return TravelMode[value.toUpperCase()];
}

// Check map has the provided mapKey and if the model object has a value for the converted key
// Optionally run the final value through a conversiion function to transform its type
private mapKeyToModelValue(
Expand Down
14 changes: 7 additions & 7 deletions python/gmpro/json/cfr_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,29 +100,29 @@ def optimize_tours(
host: str | None = None,
path: str | None = None,
) -> cfr_json.OptimizeToursResponse:
"""Solves request using the Google CFR API.
"""Solves request using the GMPRO API.

Args:
request: The request to be solved.
google_cloud_project: The name of the Google Cloud project used in the API.
google_cloud_token: The Google Cloud access token used to invoke the API.
timeout: The solve deadline for the request.
host: The host of the CFR API endpoint. When None, the default CFR endpoint
is used.
host: The host of the GMPRO API endpoint. When None, the default GMPRO
endpoint is used.
path: The path of the optimizeTours API method. When it contains "{project}"
as a substring, it will be replaced by the name of the project when making
the HTTP API call. When None, the default CFR API path for optimizeTours
the HTTP API call. When None, the default GMPRO API path for optimizeTours
is used.

Returns:
Upon success, returns the response from the server.

Raises:
ApiCallError: When the CFR API invocation fails. The exception contains the
status, explanation, and the body of the response.
ApiCallError: When the GMPRO API invocation fails. The exception contains
the status, explanation, and the body of the response.
"""
if host is None:
host = "cloudoptimization.googleapis.com"
host = "routeoptimization.googleapis.com"
if path is None:
path = "/v1/projects/{project}:optimizeTours"
path = path.format(project=google_cloud_project)
Expand Down
2 changes: 2 additions & 0 deletions python/gmpro/json/cfr_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ class VisitRequest(TypedDict, total=False):
timeWindows: list[TimeWindow]
duration: DurationString
cost: float
avoidUTurns: bool

tags: list[str]

Expand Down Expand Up @@ -235,6 +236,7 @@ class Visit(TypedDict, total=False):
detour: str
isPickup: bool
visitRequestIndex: int
injectedSolutionLocationToken: int


class EncodedPolyline(TypedDict, total=False):
Expand Down
2 changes: 1 addition & 1 deletion python/gmpro/json/evaluate_solution_howto.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ python3 -m cfr.json.evaluate_solution \
--project "${YOUR_GCLOUD_PROJECT}" \
--token "${YOUR_GCLOUD_TOKEN}" \
--input_request /tmp/scenario.json \
--input_response /tmp/response-alt.json \
--input_response /tmp/solution-alt.json \
--output_response /tmp/response-alt-full.json
```
where `${YOUR_GCLOUD_PROJECT}` is the name of the Google cloud project, and
Expand Down
Loading