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

View File

@ -5,3 +5,16 @@ CREATE TABLE `vn`.`osrmConfig` (
PRIMARY KEY (`id`),
CONSTRAINT `osrmConfig_check` CHECK (`id` = 1)
) 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",
"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",
"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/getExpeditionSummary')(Self);
require('../methods/route/getByWorker')(Self);
require('../methods/route/optimizeStops')(Self);
require('../methods/route/optimizePriority')(Self);
};

View File

@ -17,6 +17,9 @@
"ZoneClosure": {
"dataSource": "vn"
},
"ZoneConfig": {
"dataSource": "vn"
},
"ZoneEvent": {
"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

@ -72,6 +72,11 @@
"type": "hasMany",
"model": "ZoneClosure",
"foreignKey": "zoneFk"
},
"address": {
"type": "belongsTo",
"model": "Address",
"foreignKey": "addressFk"
}
}
}