From 3a9691dc6f9fedaa4aa485a46d43c1f8c6329bae Mon Sep 17 00:00:00 2001 From: Jan Date: Sat, 27 Sep 2025 18:48:21 +0200 Subject: [PATCH] matrix routing: - only allow <2 countries to be passed. - do not route over chains with first node is port or station - check that intermediate nodes have all "intermediate"-proeprty (exception near by routing nodes) --- .../rates/ContainerRateRepository.java | 18 +++++++++++ .../service/calculation/ChainResolver.java | 30 ++++++++++++++----- .../service/calculation/RoutingService.java | 23 ++++++++++++++ 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java b/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java index 5229eab..149227c 100644 --- a/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java @@ -128,6 +128,8 @@ public class ContainerRateRepository { LEFT JOIN node AS from_node ON from_node.id = container_rate.from_node_id LEFT JOIN validity_period ON validity_period.id = container_rate.validity_period_id WHERE validity_period.state = ? + AND to_node.is_deprecated = FALSE + AND from_node.is_deprecated = FALSE AND (container_rate.container_rate_type = ? OR container_rate.container_rate_type = ?) AND container_rate.from_node_id = ? AND to_node.country_id IN (%s)""".formatted( destinationCountryPlaceholders); @@ -162,6 +164,8 @@ public class ContainerRateRepository { LEFT JOIN node AS from_node ON from_node.id = container_rate.from_node_id LEFT JOIN validity_period ON validity_period.id = container_rate.validity_period_id WHERE validity_period.state = ? + AND to_node.is_deprecated = FALSE + AND from_node.is_deprecated = FALSE AND container_rate.from_node_id = ? AND container_rate.container_rate_type = ?"""; return jdbcTemplate.query(query, new ContainerRateMapper(true), ValidityPeriodState.VALID.name(), mainRun.getToNodeId(), TransportType.POST_RUN.name()); @@ -206,6 +210,20 @@ public class ContainerRateRepository { ); } + @Transactional + public boolean hasMainRun(Integer nodeId) { + String query = """ + SELECT EXISTS( + SELECT 1 FROM container_rate + WHERE (from_node_id = ? OR to_node_id = ?) + AND (container_rate_type = ? OR container_rate_type = ?) + ) + """; + + return Boolean.TRUE.equals(jdbcTemplate.queryForObject(query, Boolean.class, + nodeId, nodeId, TransportType.SEA.name(), TransportType.RAIL.name())); + } + private static class ContainerRateMapper implements RowMapper { diff --git a/src/main/java/de/avatic/lcc/service/calculation/ChainResolver.java b/src/main/java/de/avatic/lcc/service/calculation/ChainResolver.java index eb926cc..d234d66 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/ChainResolver.java +++ b/src/main/java/de/avatic/lcc/service/calculation/ChainResolver.java @@ -61,7 +61,7 @@ public class ChainResolver { while (!chainStack.isEmpty()) { var validationObject = chainStack.pop(); - if(!validationObject.checkCircularReference()) { + if (!validationObject.checkCircularReference()) { while (validationObject.hasNext()) { Node currentNode = validationObject.getCurrentNode(); chainStack.addAll(validationObject.validateChains(nodeRepository.getChainsById(currentNode.getId()))); @@ -72,7 +72,7 @@ public class ChainResolver { validationObject.next(); } - if (validationObject.valid()) + if (validationObject.valid() && !checkDeprecated(validationObject.getChain())) foundChains.add(new ArrayList<>(validationObject.getChain())); } else { log.warn("Circular reference detected while building predecessor chain for node {}", nodeId); @@ -80,11 +80,19 @@ public class ChainResolver { } + // remove chains if there is a node that cannot act as intermediate. + foundChains = foundChains.stream().filter(chain -> chain.stream().anyMatch(n -> !n.getIntermediate())).toList(); + + log.info("Found {} chains for node {}", foundChains.size(), nodeId); log.info("Found chains: {}", foundChains); return foundChains; } + private boolean checkDeprecated(List chain) { + return chain.stream().anyMatch(Node::getDeprecated); + } + /** * Helper class for chain validation and processing. *

@@ -96,13 +104,19 @@ public class ChainResolver { @Renderer(text = "getFullChainView()") private static class ChainValidationObject { - /** Current position in the chain being processed */ + /** + * Current position in the chain being processed + */ int chainPointer; - /** List of nodes representing the current chain */ + /** + * List of nodes representing the current chain + */ List chain; - /** Flag indicating if the chain is valid */ + /** + * Flag indicating if the chain is valid + */ private boolean chainValid; /** @@ -119,7 +133,7 @@ public class ChainResolver { /** * Creates a new ChainValidationObject with the specified chain and pointer position. * - * @param chain The list of nodes representing the chain to validate + * @param chain The list of nodes representing the chain to validate * @param chainPointer The starting position in the chain */ private ChainValidationObject(List chain, int chainPointer) { @@ -163,7 +177,7 @@ public class ChainResolver { * the chain object itself is a solution -> return empty. */ - if(chainPointer == chain.size() - 1 && foreignChains.stream().allMatch(List::isEmpty)) + if (chainPointer == chain.size() - 1 && foreignChains.stream().allMatch(List::isEmpty)) return Collections.emptyList(); int foreignIdx = 0; @@ -216,7 +230,7 @@ public class ChainResolver { *

* * @param candidates List of candidate chains to merge with - * @param onIndex The index in the candidate chains to start merging from + * @param onIndex The index in the candidate chains to start merging from * @return Collection of ChainValidationObjects representing the merged chains */ private Collection mergeCandidates(List> candidates, int onIndex) { diff --git a/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java b/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java index 61f8230..6805818 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java @@ -309,6 +309,10 @@ public class RoutingService { Collection rates = container.getRates(); for (var chain : chains) { + + if (skipCain(chain, container)) + continue; + var toNode = chain.isEmpty() ? container.getDestinationNode() : chain.getLast(); TemporaryRateObject finalSection = new TemporaryRateObject(container.getSourceNode(), toNode, TemporaryRateObject.TemporaryRateObjectType.MATRIX); @@ -363,9 +367,21 @@ public class RoutingService { } } + private boolean skipCain(List chain, TemporaryContainer container) { + if (containerRateRepository.hasMainRun(chain.getFirst().getId())) + return true; + + Set countryIds = new HashSet<>(); + countryIds.add(container.getSourceNode().getCountryId()); + chain.forEach(node -> countryIds.add(node.getCountryId())); + return countryIds.size() > 2; + } + private TemporaryRateObject connectNearByNodes(Node chainEnd, List nearByNodes, TemporaryContainer container) { + if(nearByNodes == null) + return null; for (var nearbyNode : nearByNodes) { TemporaryRateObject nearByRate = connectNodes(nearbyNode, chainEnd, container); @@ -541,6 +557,9 @@ public class RoutingService { Node mainRunEndNode = nodeRepository.getById(mainRun.getToNodeId()).orElseThrow(); Node mainRunStartNode = outboundNodes.stream().filter(n -> n.getId().equals(mainRun.getFromNodeId())).findFirst().orElseThrow(); + if(!mainRunStartNode.getIntermediate() || !mainRunEndNode.getIntermediate()) + continue; + TemporaryRateObject mainRunObj = new TemporaryRateObject(mainRunStartNode, mainRunEndNode, TemporaryRateObject.TemporaryRateObjectType.MAIN_RUN, mainRun); var postRuns = container.getPostRuns().get(mainRun.getId()); @@ -556,6 +575,10 @@ public class RoutingService { for (var postRun : container.getPostRuns().get(mainRun.getId())) { Node postRunEndNode = nodeRepository.getById(postRun.getToNodeId()).orElseThrow(); + + if (!postRunEndNode.getIntermediate()) + continue; + TemporaryRateObject postRunObj = new TemporaryRateObject(mainRunEndNode, postRunEndNode, TemporaryRateObject.TemporaryRateObjectType.POST_RUN, postRun); var sortedChains = sortedChainMap.get(quality).get(postRun.getId());