feat: refs #7882 Osrm service
gitea/salix/pipeline/pr-dev There was a failure building this commit Details

This commit is contained in:
Guillermo Bonet 2024-12-11 09:03:14 +01:00
parent cba5d88c5e
commit c8ec94bed9
9 changed files with 211 additions and 59 deletions

View File

@ -9,9 +9,13 @@ module.exports = Self => {
arg: 'addressIds', arg: 'addressIds',
type: 'array', type: 'array',
required: true required: true
}, {
arg: 'firstAddressId',
type: 'number',
required: false
}], }],
returns: { returns: {
type: 'string', type: 'object',
root: true root: true
}, },
http: { http: {
@ -20,21 +24,22 @@ module.exports = Self => {
} }
}); });
Self.optimize = async addressIds => { Self.optimize = async(addressIds, firstAddressId) => {
const models = Self.app.models; const models = Self.app.models;
try { try {
const osrmConfig = await models.OsrmConfig.findOne(); const osrmConfig = await models.OsrmConfig.findOne();
if (!osrmConfig) throw new UserError(`OSRM service is not configured`); if (!osrmConfig) throw new UserError(`OSRM service is not configured`);
let coords = []; let coords = [];
if (firstAddressId) {
const address = await models.Address.findById(32308); // Aquí irá el address asociada a la zona const firstAddress = await models.Address.findById(firstAddressId);
if (address.latitude && address.longitude) { if (firstAddress.latitude && firstAddress.longitude) {
coords.push({ coords.push({
addressId: address.id, addressId: firstAddress.id,
latitude: address.latitude.toFixed(6), latitude: firstAddress.latitude.toFixed(6),
longitude: address.longitude.toFixed(6) longitude: firstAddress.longitude.toFixed(6)
}); });
}
} }
for (const addressId of addressIds) { for (const addressId of addressIds) {
@ -47,6 +52,9 @@ module.exports = Self => {
}); });
} }
} }
if (!coords.length) throw new UserError('No address has coordinates');
const concatCoords = coords const concatCoords = coords
.map(coord => `${coord.longitude},${coord.latitude}`) .map(coord => `${coord.longitude},${coord.latitude}`)
.join(';'); .join(';');

View File

@ -1,7 +1,20 @@
CREATE TABLE `vn`.`osrmConfig` ( CREATE TABLE `vn`.`osrmConfig` (
`id` int(10) unsigned NOT NULL, `id` int(10) unsigned NOT NULL,
`url` varchar(100) NOT NULL COMMENT 'Dirección base de la API', `url` varchar(100) NOT NULL COMMENT 'Dirección base de la API',
`tolerance` decimal(6,6) NOT NULL DEFAULT 0 COMMENT 'Tolerancia entre las coordenadas enviadas y las retornadas', `tolerance` decimal(6,6) NOT NULL DEFAULT 0 COMMENT 'Tolerancia entre las coordenadas enviadas y las retornadas',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
CONSTRAINT `osrmConfig_check` CHECK (`id` = 1) CONSTRAINT `osrmConfig_check` CHECK (`id` = 1)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
-- Para que no de error al añadir la FK de zone
UPDATE vn.zone
SET price = 0.1
WHERE price = 0;
ALTER TABLE vn.`zone`
ADD addressFk int(11) DEFAULT NULL COMMENT 'Punto de distribución de donde salen para repartir',
ADD CONSTRAINT zone_address_FK FOREIGN KEY (addressFk) REFERENCES vn.address(id) ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE vn.zoneConfig
ADD defaultAddressFk int(11) DEFAULT NULL NULL COMMENT 'Punto de distribución por defecto',
ADD CONSTRAINT zoneConfig_address_FK FOREIGN KEY (defaultAddressFk) REFERENCES vn.address(id) ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -393,5 +393,6 @@
"There are tickets to be invoiced": "Hay tickets para esta zona, borralos primero", "There are tickets to be invoiced": "Hay tickets para esta zona, borralos primero",
"No trips found because input coordinates are not connected": "No se encontraron rutas porque las coordenadas de entrada no están conectadas", "No trips found because input coordinates are not connected": "No se encontraron rutas porque las coordenadas de entrada no están conectadas",
"This request is not supported": "Esta solicitud no es compatible", "This request is not supported": "Esta solicitud no es compatible",
"Invalid options or too many coordinates": "Opciones invalidas o demasiadas coordenadas" "Invalid options or too many coordinates": "Opciones invalidas o demasiadas coordenadas",
"No address has coordinates": "Ninguna dirección tiene coordenadas"
} }

View File

@ -0,0 +1,119 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('optimizePriority', {
description: 'Updates the ticket priority of tickets without priority',
accepts: {
arg: 'id',
type: 'number',
required: true,
description: 'Route id',
http: {source: 'path'}
},
returns: {
type: 'object',
root: true
},
http: {
path: '/:id/optimizePriority',
verb: 'POST'
}
});
Self.optimizePriority = async(id, options) => {
const models = Self.app.models;
const myOptions = {};
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
const ticketsIds = await models.Ticket.find({
where: {routeFk: id}
}, myOptions);
let ticketAddress = [];
for (const ticketId of ticketsIds) {
ticketAddress.push({
ticketId: ticketId.id,
addressId: ticketId.addressFk,
zoneId: ticketId.zoneFk,
priority: ticketId.priority
});
}
// Igualamos los priority del mismo addressId
const addressPriorityMap = ticketAddress.reduce((acc, {addressId, priority}) => {
if (priority !== null) {
acc[addressId] = acc[addressId] === undefined
? priority
: Math.max(acc[addressId], priority);
}
return acc;
});
ticketAddress.forEach(item => {
const maxPriority = addressPriorityMap[item.addressId];
if (maxPriority) item.priority = maxPriority;
});
// Añadimos las direcciones a optimizar
let addressIds = [];
ticketAddress.forEach(h => {
if (!addressIds.includes(h.addressId) && !h.priority)
addressIds.push(h.addressId);
});
if (!addressIds.length) throw new UserError('All tickets have a route order');
// Obtenemos el zoneId más frecuente
const zoneFrequency = ticketAddress.reduce((acc, {zoneId}) => {
if (zoneId != null) acc[zoneId] = (acc[zoneId] || 0) + 1;
return acc;
}, {});
const [mostFrequentZoneId] = Object.entries(zoneFrequency)
.reduce((maxEntry, entry) => entry[1] > maxEntry[1] ? entry : maxEntry, [null, 0]);
const zone = await models.Zone.findById(mostFrequentZoneId, myOptions);
let firstAddress = zone.addressFk;
if (!firstAddress) firstAddress = (await models.ZoneConfig.findOne()).defaultAddressFk;
// Revisamos las coincidencias y actualizamos la prioridad en el array
const addressPositions = await models.OsrmConfig.optimize(addressIds, firstAddress, myOptions);
const maxPosition = Math.max(...ticketAddress.map(g => g.priority));
await Promise.all(ticketAddress.map(async i => {
const foundPosition = addressPositions.coords.find(item => item.addressId === i.addressId);
if (foundPosition) i.priority = foundPosition.position + (maxPosition + 1);
}));
// Suavizado de prioridad para que no hayan escalones
const allPriorities = ticketAddress
.map(item => item.priority)
.filter(p => p !== null);
const uniquePriorities = [...new Set(allPriorities)].sort((a, b) => a - b);
const priorityMap = {};
uniquePriorities.forEach((p, index) => {
priorityMap[p] = index + 1;
});
ticketAddress.forEach(item => {
if (item.priority !== null) item.priority = priorityMap[item.priority];
});
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
// Realizamos el update en la base de datos
try {
await Promise.all(ticketAddress.map(async y => {
if (y.priority) {
const ticket = await models.Ticket.findById(y.ticketId);
await ticket.updateAttribute('priority', y.priority, myOptions);
}
}));
if (tx) await tx.commit();
return;
} catch (err) {
if (tx) await tx.rollback();
throw err;
}
};
};

View File

@ -1,25 +0,0 @@
module.exports = Self => {
Self.remoteMethod('optimizeStops', {
description: 'Updates the ticket priority of tickets without priority',
accepts: [
{
arg: 'routeFk',
type: 'number',
required: true
}
],
returns: {
type: 'object',
root: true
},
http: {
path: '/optimizeStops',
verb: 'post'
}
});
Self.optimizeStops = async(routeFk, options) => {
return;
};
};

View File

@ -16,5 +16,5 @@ module.exports = Self => {
require('../methods/route/downloadZip')(Self); require('../methods/route/downloadZip')(Self);
require('../methods/route/getExpeditionSummary')(Self); require('../methods/route/getExpeditionSummary')(Self);
require('../methods/route/getByWorker')(Self); require('../methods/route/getByWorker')(Self);
require('../methods/route/optimizeStops')(Self); require('../methods/route/optimizePriority')(Self);
}; };

View File

@ -17,6 +17,9 @@
"ZoneClosure": { "ZoneClosure": {
"dataSource": "vn" "dataSource": "vn"
}, },
"ZoneConfig": {
"dataSource": "vn"
},
"ZoneEvent": { "ZoneEvent": {
"dataSource": "vn" "dataSource": "vn"
}, },

View File

@ -0,0 +1,28 @@
{
"name": "ZoneConfig",
"options": {
"mysql": {
"table": "zoneConfig"
}
},
"properties": {
"id": {
"type": "number",
"id": true,
"description": "Identifier"
},
"scope": {
"type": "number"
},
"forwardDays": {
"type": "number"
}
},
"relations": {
"address": {
"type": "belongsTo",
"model": "Address",
"foreignKey": "defaultAddressFk"
}
}
}

View File

@ -1,9 +1,9 @@
{ {
"name": "Zone", "name": "Zone",
"base": "VnModel", "base": "VnModel",
"mixins": { "mixins": {
"Loggable": true "Loggable": true
}, },
"options": { "options": {
"mysql": { "mysql": {
"table": "zone" "table": "zone"
@ -48,30 +48,35 @@
} }
}, },
"relations": { "relations": {
"agencyMode": { "agencyMode": {
"type": "belongsTo", "type": "belongsTo",
"model": "AgencyMode", "model": "AgencyMode",
"foreignKey": "agencyModeFk" "foreignKey": "agencyModeFk"
}, },
"events": { "events": {
"type": "hasMany", "type": "hasMany",
"model": "ZoneEvent", "model": "ZoneEvent",
"foreignKey": "zoneFk" "foreignKey": "zoneFk"
}, },
"exclusions": { "exclusions": {
"type": "hasMany", "type": "hasMany",
"model": "ZoneExclusion", "model": "ZoneExclusion",
"foreignKey": "zoneFk" "foreignKey": "zoneFk"
}, },
"warehouses": { "warehouses": {
"type": "hasMany", "type": "hasMany",
"model": "ZoneWarehouse", "model": "ZoneWarehouse",
"foreignKey": "zoneFk" "foreignKey": "zoneFk"
}, },
"closures": { "closures": {
"type": "hasMany", "type": "hasMany",
"model": "ZoneClosure", "model": "ZoneClosure",
"foreignKey": "zoneFk" "foreignKey": "zoneFk"
} },
} "address": {
"type": "belongsTo",
"model": "Address",
"foreignKey": "addressFk"
}
}
} }