Enhance distance handling in routing logic: add new distance attributes, improve fallback logic, and refine Azure Maps API integration.

This commit is contained in:
Jan 2025-12-12 15:08:07 +01:00
parent 735d8a707b
commit 3aa86b4eea
22 changed files with 357 additions and 141 deletions

View file

@ -116,7 +116,7 @@ export default {
case 'warning':
return 'secondary'
case 'info':
return 'primary'
return 'secondary'
case 'error':
return 'exception'
default:

View file

@ -13,9 +13,9 @@
</div>
<div class="dashboard-box-info">
<div v-if="total !== null" class="dashboard-box-number">{{ total }}</div>
<div v-if="completed !== null" class="dashboard-box-number">{{ completed }}</div>
<div v-else class="dashboard-box-number"><spinner size="s"/></div>
<div class="dashboard-box-number-text">Total</div>
<div class="dashboard-box-number-text">Completed</div>
</div>
</div>
</box>
@ -94,8 +94,8 @@ export default {
},
computed: {
...mapStores(useDashboardStore),
total() {
return this.dashboardStore.total
completed() {
return this.dashboardStore.completed
},
drafts() {
return this.dashboardStore.drafts

View file

@ -43,7 +43,7 @@ export const useBulkOperationStore = defineStore('bulkOperation', {
useNotificationStore().addNotification({
title: 'Bulk operation',
message: 'All your bulk operations have been completed.',
type: 'success',
variant: 'info',
icon: 'stack',
})

View file

@ -1,6 +1,7 @@
import {defineStore} from 'pinia'
import performRequest from "@/backend.js";
import {config} from '@/config'
import {useNotificationStore} from "@/store/notification.js";
export const useDashboardStore = defineStore('dashboard', {
state: () => ({
@ -9,9 +10,9 @@ export const useDashboardStore = defineStore('dashboard', {
pullTimer: null,
}),
getters: {
total(state) {
completed(state) {
if (state.stats)
return state.stats.total;
return state.stats.completed;
return null;
},
@ -38,13 +39,23 @@ export const useDashboardStore = defineStore('dashboard', {
async load() {
const url = `${config.backendUrl}/dashboard`;
const resp = await performRequest(this, 'GET', url, null);
if(this.stats?.running && this.stats.running > 0 && resp.data.running === 0) {
useNotificationStore().addNotification({
title: 'Calculation',
message: 'All your calculations have been completed.',
variant: 'info',
icon: 'calculator',
})
}
this.stats = resp.data;
},
startPulling() {
if (this.pullTimer) return
this.pullTimer = setTimeout(() => {
this.pull()
this.pullTimer = setTimeout(async () => {
await this.pull()
}, this.pullInterval)
},
stopPulling() {

View file

@ -33,6 +33,9 @@ export const useNotificationStore = defineStore('notification', {
return this.notifications.pop();
},
addNotification(notification) {
console.log("add notification", notification, this.notifications.length)
this.notifications.push({
icon: notification.icon ?? null,
message: notification.message ?? 'Unknown notification',

View file

@ -42,7 +42,7 @@ import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
@ -111,7 +111,7 @@ public class SecurityConfig {
.exceptionHandling(ex -> ex
.defaultAuthenticationEntryPointFor(
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
new AntPathRequestMatcher("/api/**")
PathPatternRequestMatcher.withDefaults().matcher("/api/**")
)
)
.csrf(csrf -> csrf

View file

@ -2,7 +2,7 @@ package de.avatic.lcc.dto.calculation.execution;
public class CalculationProcessingOverviewDTO {
private Integer total;
private Integer completed;
private Integer running;
@ -11,12 +11,12 @@ public class CalculationProcessingOverviewDTO {
private Integer failed;
public Integer getTotal() {
return total;
public Integer getCompleted() {
return completed;
}
public void setTotal(Integer total) {
this.total = total;
public void setCompleted(Integer completed) {
this.completed = completed;
}
public Integer getRunning() {

View file

@ -40,6 +40,7 @@ public class Distance {
private DistanceMatrixState state;
private Integer retries;
private Integer fromNodeId;
@ -144,4 +145,12 @@ public class Distance {
public void setToUserNodeId(Integer toUserNodeId) {
this.toUserNodeId = toUserNodeId;
}
public Integer getRetries() {
return retries;
}
public void setRetries(Integer retries) {
this.retries = retries;
}
}

View file

@ -31,6 +31,8 @@ public class Destination {
private Integer countryId;
private BigDecimal distanceD2d;
public Integer getLeadTimeD2d() {
return leadTimeD2d;
}
@ -134,4 +136,12 @@ public class Destination {
public void setDisposalCost(BigDecimal disposalCost) {
this.disposalCost = disposalCost;
}
public BigDecimal getDistanceD2d() {
return distanceD2d;
}
public void setDistanceD2d(BigDecimal distanceD2d) {
this.distanceD2d = distanceD2d;
}
}

View file

@ -27,6 +27,8 @@ public class RouteSection {
private Integer toRouteNodeId;
private Double distance;
public RateType getRateType() {
return rateType;
}
@ -114,4 +116,12 @@ public class RouteSection {
public void setToRouteNodeId(Integer toRouteNodeId) {
this.toRouteNodeId = toRouteNodeId;
}
public Double getDistance() {
return distance;
}
public void setDistance(Double distance) {
this.distance = distance;
}
}

View file

@ -29,10 +29,10 @@ public class DistanceMatrixRepository {
String fromCol = isUsrFrom ? "from_user_node_id" : "from_node_id";
String toCol = isUsrTo ? "to_user_node_id" : "to_node_id";
String query = "SELECT * FROM distance_matrix WHERE " + fromCol + " = ? AND " + toCol + " = ? AND state = ?";
String query = "SELECT * FROM distance_matrix WHERE " + fromCol + " = ? AND " + toCol + " = ?";
var distance = jdbcTemplate.query(query, new DistanceMapper(),
src.getId(), dest.getId(), DistanceMatrixState.VALID.name());
src.getId(), dest.getId());
if (distance.isEmpty())
return Optional.empty();
@ -40,6 +40,12 @@ public class DistanceMatrixRepository {
return Optional.of(distance.getFirst());
}
@Transactional
public void updateRetries(Integer id) {
String query = "UPDATE distance_matrix SET retries = retries + 1 WHERE id = ?";
jdbcTemplate.update(query, id);
}
@Transactional
public void saveDistance(Distance distance) {
try {
@ -119,10 +125,20 @@ public class DistanceMatrixRepository {
public Distance mapRow(ResultSet rs, int rowNum) throws SQLException {
Distance entity = new Distance();
entity.setFromNodeId(rs.getInt("from_node_id"));
entity.setToNodeId(rs.getInt("to_node_id"));
entity.setFromNodeId(rs.getInt("from_user_node_id"));
entity.setToNodeId(rs.getInt("to_user_node_id"));
entity.setId(rs.getInt("id"));
var fromNodeId = rs.getInt("from_node_id");
entity.setFromNodeId(rs.wasNull() ? null : fromNodeId);
var toNodeId = rs.getInt("to_node_id");
entity.setToNodeId(rs.wasNull() ? null : toNodeId);
var fromUserNodeId = rs.getInt("from_user_node_id");
entity.setFromUserNodeId(rs.wasNull() ? null : fromUserNodeId);
var toUserNodeId = rs.getInt("to_user_node_id");
entity.setToUserNodeId(rs.wasNull() ? null : toUserNodeId);
entity.setDistance(rs.getBigDecimal("distance"));
entity.setFromGeoLng(rs.getBigDecimal("from_geo_lng"));
entity.setFromGeoLat(rs.getBigDecimal("from_geo_lat"));
@ -131,6 +147,9 @@ public class DistanceMatrixRepository {
entity.setState(DistanceMatrixState.valueOf(rs.getString("state")));
entity.setUpdatedAt(rs.getTimestamp("updated_at").toLocalDateTime());
var retries = rs.getInt("retries");
entity.setRetries(rs.wasNull() ? 0 : retries);
return entity;
}
}

View file

@ -67,7 +67,7 @@ public class DestinationRepository {
}
@Transactional
public void update(Integer id, Integer annualAmount, BigDecimal repackingCost, BigDecimal disposalCost, BigDecimal handlingCost, Boolean isD2d, BigDecimal d2dRate, BigDecimal d2dLeadTime) {
public void update(Integer id, Integer annualAmount, BigDecimal repackingCost, BigDecimal disposalCost, BigDecimal handlingCost, Boolean isD2d, BigDecimal d2dRate, BigDecimal d2dLeadTime, BigDecimal distanceD2d) {
if (id == null) {
throw new InvalidArgumentException("ID cannot be null");
}
@ -99,6 +99,9 @@ public class DestinationRepository {
setClauses.add("lead_time_d2d = :d2dLeadTime");
parameters.put("d2dLeadTime", setD2d ? d2dLeadTime : null);
setClauses.add("distance_d2d = :distanceD2d");
parameters.put("distanceD2d", distanceD2d);
if (annualAmount != null) {
setClauses.add("annual_amount = :annualAmount");
@ -268,7 +271,7 @@ public class DestinationRepository {
public Integer insert(Destination destination) {
KeyHolder keyHolder = new GeneratedKeyHolder();
String query = "INSERT INTO premise_destination (annual_amount, premise_id, destination_node_id, country_id, rate_d2d, lead_time_d2d, is_d2d, repacking_cost, handling_cost, disposal_cost, geo_lat, geo_lng) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
String query = "INSERT INTO premise_destination (annual_amount, premise_id, destination_node_id, country_id, rate_d2d, lead_time_d2d, is_d2d, repacking_cost, handling_cost, disposal_cost, geo_lat, geo_lng, distance_d2d) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
jdbcTemplate.update(connection -> {
var ps = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS);
@ -297,6 +300,8 @@ public class DestinationRepository {
ps.setBigDecimal(11, destination.getGeoLat());
ps.setBigDecimal(12, destination.getGeoLng());
ps.setBigDecimal(13, destination.getDistanceD2d());
return ps;
}, keyHolder);
@ -363,6 +368,8 @@ public class DestinationRepository {
entity.setGeoLng(rs.getBigDecimal("geo_lng"));
entity.setCountryId(rs.getInt("country_id"));
entity.setDistanceD2d(rs.getBigDecimal("distance_d2d"));
return entity;
}
}

View file

@ -9,6 +9,7 @@ import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@ -45,8 +46,8 @@ public class RouteSectionRepository {
}
public Integer insert(RouteSection premiseRouteSection) {
String sql = "INSERT INTO premise_route_section (premise_route_id, from_route_node_id, to_route_node_id, list_position, transport_type, rate_type, is_pre_run, is_main_run, is_post_run, is_outdated) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
String sql = "INSERT INTO premise_route_section (premise_route_id, from_route_node_id, to_route_node_id, list_position, transport_type, rate_type, is_pre_run, is_main_run, is_post_run, is_outdated, distance) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
@ -61,6 +62,7 @@ public class RouteSectionRepository {
ps.setBoolean(8, premiseRouteSection.getMainRun());
ps.setBoolean(9, premiseRouteSection.getPostRun());
ps.setBoolean(10, premiseRouteSection.getOutdated());
ps.setBigDecimal(11, premiseRouteSection.getDistance() == null ? null : BigDecimal.valueOf(premiseRouteSection.getDistance()));
return ps;
}, keyHolder);
@ -92,6 +94,9 @@ public class RouteSectionRepository {
entity.setPostRun(rs.getBoolean("is_post_run"));
entity.setOutdated(rs.getBoolean("is_outdated"));
var distance = rs.getBigDecimal("distance");
entity.setDistance(rs.wasNull() ? null : distance.doubleValue());
return entity;
}
}

View file

@ -10,6 +10,7 @@ import de.avatic.lcc.model.db.premises.route.*;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.repositories.premise.*;
import de.avatic.lcc.repositories.users.UserNodeRepository;
import de.avatic.lcc.service.calculation.DistanceService;
import de.avatic.lcc.service.calculation.RoutingService;
import de.avatic.lcc.service.transformer.premise.DestinationTransformer;
import de.avatic.lcc.service.users.AuthorizationService;
@ -35,8 +36,9 @@ public class DestinationService {
private final PremiseRepository premiseRepository;
private final UserNodeRepository userNodeRepository;
private final AuthorizationService authorizationService;
private final DistanceService distanceService;
public DestinationService(DestinationRepository destinationRepository, DestinationTransformer destinationTransformer, RouteRepository routeRepository, RouteSectionRepository routeSectionRepository, RouteNodeRepository routeNodeRepository, RoutingService routingService, NodeRepository nodeRepository, PremiseRepository premiseRepository, UserNodeRepository userNodeRepository, AuthorizationService authorizationService) {
public DestinationService(DestinationRepository destinationRepository, DestinationTransformer destinationTransformer, RouteRepository routeRepository, RouteSectionRepository routeSectionRepository, RouteNodeRepository routeNodeRepository, RoutingService routingService, NodeRepository nodeRepository, PremiseRepository premiseRepository, UserNodeRepository userNodeRepository, AuthorizationService authorizationService, DistanceService distanceService) {
this.destinationRepository = destinationRepository;
this.destinationTransformer = destinationTransformer;
this.routeRepository = routeRepository;
@ -47,6 +49,7 @@ public class DestinationService {
this.premiseRepository = premiseRepository;
this.userNodeRepository = userNodeRepository;
this.authorizationService = authorizationService;
this.distanceService = distanceService;
}
@ -199,11 +202,30 @@ public class DestinationService {
destinationUpdateDTO.getDisposalCost() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getDisposalCost().doubleValue()),
destinationUpdateDTO.getHandlingCost() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getHandlingCost().doubleValue()),
destinationUpdateDTO.getD2d(), destinationUpdateDTO.getRateD2d() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getRateD2d().doubleValue()),
destinationUpdateDTO.getLeadtimeD2d() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getLeadtimeD2d())
destinationUpdateDTO.getLeadtimeD2d() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getLeadtimeD2d()),
destinationUpdateDTO.getD2d() ? getD2dDistance(id) : null
);
}
private BigDecimal getD2dDistance(Integer destinationId) {
var dest = destinationRepository.getById(destinationId);
if(dest.isPresent()) {
var premise = premiseRepository.getPremiseById(dest.get().getPremiseId());
if(premise.isPresent()) {
boolean isUserNode = premise.get().getSupplierNodeId() == null;
var from = isUserNode ? userNodeRepository.getById(premise.get().getUserSupplierNodeId()) : nodeRepository.getById(premise.get().getSupplierNodeId());
var to = nodeRepository.getById(dest.get().getDestinationNodeId());
return BigDecimal.valueOf(distanceService.getDistanceForNode(from.orElseThrow(), to.orElseThrow()));
}
}
return null;
}
private Map<RouteIds, List<RouteInformation>> findRoutes(List<Premise> premisses, Map<Integer, List<Integer>> routingRequest) {
Map<RouteIds, List<RouteInformation>> routes = new HashMap<>();
@ -267,6 +289,7 @@ public class DestinationService {
premiseRouteSection.setFromRouteNodeId(fromNodeId);
premiseRouteSection.setToRouteNodeId(toNodeId);
routeSectionRepository.insert(premiseRouteSection);
fromNodeId = toNodeId;

View file

@ -1,5 +1,7 @@
package de.avatic.lcc.service.api;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.avatic.lcc.model.azuremaps.route.RouteDirectionsResponse;
import de.avatic.lcc.model.db.nodes.Distance;
import de.avatic.lcc.model.db.nodes.DistanceMatrixState;
@ -10,6 +12,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
@ -25,14 +28,18 @@ public class DistanceApiService {
private final DistanceMatrixRepository distanceMatrixRepository;
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
private final GeoApiService geoApiService;
@Value("${azure.maps.subscription.key}")
private String subscriptionKey;
public DistanceApiService(DistanceMatrixRepository distanceMatrixRepository,
RestTemplate restTemplate) {
RestTemplate restTemplate, ObjectMapper objectMapper, GeoApiService geoApiService) {
this.distanceMatrixRepository = distanceMatrixRepository;
this.restTemplate = restTemplate;
this.objectMapper = objectMapper;
this.geoApiService = geoApiService;
}
public Optional<Integer> getDistance(Location from, Location to) {
@ -70,22 +77,55 @@ public class DistanceApiService {
Optional<Distance> cachedDistance = distanceMatrixRepository.getDistance(from, isUsrFrom, to, isUsrTo);
if (cachedDistance.isPresent()) {
if (cachedDistance.isPresent() && cachedDistance.get().getState() == DistanceMatrixState.VALID) {
logger.info("Found cached distance from node {} to node {}", from.getExternalMappingId(), to.getExternalMappingId());
return cachedDistance;
}
logger.info("Fetching distance from Azure Maps for nodes {} to {}", from.getExternalMappingId(), to.getExternalMappingId());
Optional<Distance> fetchedDistance = fetchDistanceFromAzureMaps(from, isUsrFrom, to, isUsrTo);
if (cachedDistance.isPresent() && cachedDistance.get().getState() == DistanceMatrixState.EXCEPTION) {
if (cachedDistance.get().getRetries() >= 3)
return Optional.empty();
if (fetchedDistance.isPresent()) {
distanceMatrixRepository.saveDistance(fetchedDistance.get());
return fetchedDistance;
distanceMatrixRepository.updateRetries(cachedDistance.get().getId());
}
logger.info("Fetching distance from Azure Maps for nodes {} to {}", from.getExternalMappingId(), to.getExternalMappingId());
AzureMapResponse distanceResponse = fetchDistanceFromAzureMaps(from, isUsrFrom, to, isUsrTo, true);
if (distanceResponse.distance != null) {
distanceMatrixRepository.saveDistance(distanceResponse.distance);
return Optional.of(distanceResponse.distance);
}
if(distanceResponse.errorType != AzureMapsErrorType.NO_ERROR) {
distanceMatrixRepository.saveDistance(getErrorDistance(cachedDistance, from, isUsrFrom, to, isUsrTo));
}
return Optional.empty();
}
private Distance getErrorDistance(Optional<Distance> cachedDistance, Node from, boolean isUsrFrom, Node to, boolean isUsrTo) {
var distance = cachedDistance.orElse(new Distance());
distance.setState(DistanceMatrixState.EXCEPTION);
distance.setUpdatedAt(LocalDateTime.now());
distance.setRetries(distance.getRetries() == null ? 0 : distance.getRetries() + 1);
if(cachedDistance.isEmpty()) {
distance.setFromUserNodeId(isUsrFrom ? from.getId() : null);
distance.setFromNodeId(isUsrFrom ? null : from.getId());
distance.setToUserNodeId(isUsrTo ? to.getId() : null);
distance.setToNodeId(isUsrTo ? null : to.getId());
distance.setFromGeoLat(from.getGeoLat());
distance.setFromGeoLng(from.getGeoLng());
distance.setToGeoLat(to.getGeoLat());
distance.setToGeoLng(to.getGeoLng());
}
return distance;
}
private RouteDirectionsResponse fetchDistanceFromAzureMaps(BigDecimal fromLat, BigDecimal fromLng, BigDecimal toLat, BigDecimal toLng) {
String url = UriComponentsBuilder.fromUriString(AZURE_MAPS_ROUTE_API)
.queryParam("api-version", "1.0")
@ -99,48 +139,21 @@ public class DistanceApiService {
return restTemplate.getForObject(url, RouteDirectionsResponse.class);
}
//TODO;
// private Optional<Distance> handleAzureMapsError(HttpClientErrorException e,
// Node from, boolean isUsrFrom,
// Node to, boolean isUsrTo) {
// try {
// String responseBody = e.getResponseBodyAsString();
// JsonNode errorNode = objectMapper.readTree(responseBody);
//
// String errorCode = errorNode.path("error").path("code").asText();
// String errorMessage = errorNode.path("error").path("message").asText();
//
// logger.warn("Azure Maps API Error for nodes {} to {}: {} - {}",
// from.getExternalMappingId(), to.getExternalMappingId(),
// errorCode, errorMessage);
//
// // Spezifische Fehlerbehandlung
// if (errorMessage.contains("MAP_MATCHING_FAILURE") ||
// errorMessage.contains("NO_ROUTE_FOUND")) {
//
// logger.info("Route calculation failed, falling back to straight line distance");
//
// // Fallback auf Luftlinie
// double straightLineKm = calculateHaversineDistance(
// from.getGeoLat(), from.getGeoLng(),
// to.getGeoLat(), to.getGeoLng()
// );
//
// return createStraightLineDistance(from, isUsrFrom, to, isUsrTo, straightLineKm);
// }
//
// } catch (Exception parseException) {
// logger.error("Failed to parse Azure Maps error response", parseException);
// }
//
// return Optional.empty();
// }
private Optional<Distance> fetchDistanceFromAzureMaps(Node from, boolean isUsrFrom, Node to, boolean isUsrTo) {
private AzureMapResponse fetchDistanceFromAzureMaps(Node from, boolean isUsrFrom, Node to, boolean isUsrTo, boolean allowFixing) {
try {
RouteDirectionsResponse response = fetchDistanceFromAzureMaps(from.getGeoLat(), from.getGeoLng(), to.getGeoLat(), to.getGeoLng());
return convertToDistance(response, from, isUsrFrom, to, isUsrTo);
} catch (Exception e) {
if (HttpClientErrorException.class.isAssignableFrom(e.getClass()))
return handleAzureMapsError((HttpClientErrorException) e, from, isUsrFrom, to, isUsrTo, allowFixing);
logger.error("Error fetching distance from Azure Maps", e);
return new AzureMapResponse(null, AzureMapsErrorType.OTHER_ERROR, null);
}
}
private AzureMapResponse convertToDistance(RouteDirectionsResponse response, Node from, boolean isUsrFrom, Node to, boolean isUsrTo) {
if (response != null && response.getRoutes() != null && !response.getRoutes().isEmpty()) {
Integer distanceInMeters = response.getRoutes().getFirst().getSummary().getLengthInMeters();
@ -170,18 +183,100 @@ public class DistanceApiService {
distance.setState(DistanceMatrixState.VALID);
distance.setUpdatedAt(LocalDateTime.now());
// reset to 0 if on success
distance.setRetries(0);
logger.info("Successfully fetched distance: {} meters", distanceInMeters);
return Optional.of(distance);
return new AzureMapResponse(distance, AzureMapsErrorType.NO_ERROR, null);
} else {
logger.warn("No routes found in Azure Maps response");
logger.error("No routes found in Azure Maps response");
return new AzureMapResponse(null, AzureMapsErrorType.ROUTE_ERROR, null);
}
} catch (Exception e) {
//TODO parse 400 Bad Request on GET request for "https://atlas.microsoft.com/route/directions/json": "{<EOL> "error": {<EOL> "code": "400 BadRequest",<EOL> "message": "Engine error while executing route request: MAP_MATCHING_FAILURE: Destination (31.364, 121.598)"<EOL> }<EOL>}
// "{<EOL> "error": {<EOL> "code": "400 BadRequest",<EOL> "message": "Engine error while executing route request: NO_ROUTE_FOUND: Origin and destination have different ProductId's."<EOL> }<EOL>}"
logger.error("Error fetching distance from Azure Maps", e);
}
return Optional.empty();
private AzureMapResponse handleAzureMapsError(HttpClientErrorException e,
Node from, boolean isUsrFrom,
Node to, boolean isUsrTo, boolean allowFixing) {
try {
String responseBody = e.getResponseBodyAsString();
JsonNode errorNode = objectMapper.readTree(responseBody);
String errorCode = errorNode.path("error").path("code").asText();
String errorMessage = errorNode.path("error").path("message").asText();
logger.warn("Azure Maps API Error for nodes {} ({}) to {} ({}): {} - {}",
from.getExternalMappingId(), from.getId(), to.getExternalMappingId(), to.getId(),
errorCode, errorMessage);
if (errorMessage.contains("NO_ROUTE_FOUND"))
return new AzureMapResponse(null, AzureMapsErrorType.ROUTE_ERROR, null);
if (errorMessage.contains("MAP_MATCHING_FAILURE")) {
if (errorMessage.contains("Destination")) {
if (allowFixing) {
var fixedNode = fixNode(to);
if (fixedNode != null)
return fetchDistanceFromAzureMaps(from, isUsrFrom, fixedNode, isUsrTo, false);
}
return new AzureMapResponse(null, AzureMapsErrorType.NODE_ERROR, AzureMapsDefectiveNode.FROM);
}
if (errorMessage.contains("Origin")) {
if (allowFixing) {
var fixedNode = fixNode(from);
if (fixedNode != null)
return fetchDistanceFromAzureMaps(fixedNode, isUsrFrom, to, isUsrTo, false);
}
return new AzureMapResponse(null, AzureMapsErrorType.NODE_ERROR, AzureMapsDefectiveNode.TO);
}
return new AzureMapResponse(null, AzureMapsErrorType.NODE_ERROR, AzureMapsDefectiveNode.UNKNOWN);
}
} catch (Exception parseException) {
logger.error("Failed to parse Azure Maps error response", parseException);
}
return new AzureMapResponse(null, AzureMapsErrorType.OTHER_ERROR, null);
}
private Node fixNode(Node node) {
logger.info("Try to fix node {} ({}) ", node.getExternalMappingId(), node.getId());
Location location = geoApiService.locate(node.getAddress() + ", " + node.getCountryId());
if (location != null && location.getLatitude() != null && location.getLongitude() != null) {
if (0 != BigDecimal.valueOf(location.getLatitude()).compareTo(node.getGeoLat()) || 0 != BigDecimal.valueOf(location.getLongitude()).compareTo(node.getGeoLng())) {
logger.info("Fixed node {} ({}) coordinates {}, {} -> {}, {}", node.getExternalMappingId(), node.getId(), node.getGeoLat(), node.getGeoLng(), location.getLatitude(), location.getLongitude());
node.setGeoLng(BigDecimal.valueOf(location.getLongitude()));
node.setGeoLat(BigDecimal.valueOf(location.getLatitude()));
return node;
}
}
return null;
}
private enum AzureMapsErrorType {
NO_ERROR, NODE_ERROR, ROUTE_ERROR, OTHER_ERROR
}
private enum AzureMapsDefectiveNode {
FROM, TO, UNKNOWN
}
private record AzureMapResponse(Distance distance, AzureMapsErrorType errorType,
AzureMapsDefectiveNode defectiveNode) {
}

View file

@ -43,6 +43,7 @@ public class GeoApiService {
}
public GeocodingResult geocode(String address) {
if (address == null || address.trim().isEmpty()) {
logger.warn("Address is null or empty");
return null;

View file

@ -4,7 +4,6 @@ import de.avatic.lcc.model.db.country.DetourIndex;
import de.avatic.lcc.model.db.country.IsoCode;
import de.avatic.lcc.model.db.nodes.Location;
import de.avatic.lcc.model.db.nodes.Node;
import de.avatic.lcc.repositories.DistanceMatrixRepository;
import de.avatic.lcc.repositories.country.CountryRepository;
import de.avatic.lcc.service.api.DistanceApiService;
import org.springframework.stereotype.Service;
@ -21,29 +20,36 @@ public class DistanceService {
this.distanceApiService = distanceApiService;
}
public double getDistance(Integer srcCountryId, Location src, Integer destCountryId, Location dest, boolean fast) {
if (fast) return getDistanceFast(srcCountryId, src, destCountryId, dest);
public double getDistanceForLocation(Integer srcCountryId, Location src, Integer destCountryId, Location dest, boolean fast) {
var fastDistance = getDistanceFast(srcCountryId, src, destCountryId, dest);
// use fast distance if more than 3000km
if (fast || fastDistance > 3000) return fastDistance;
var distance = distanceApiService.getDistance(src, dest);
if (distance.isEmpty()) return getDistanceFast(srcCountryId, src, destCountryId, dest);
if (distance.isEmpty()) return fastDistance;
return distance.map(value -> value/1000).orElse(0);
}
public double getDistance(Node src, Node dest, boolean fast) {
public double getDistanceForNode(Node src, Node dest) {
return getDistanceForNode(src, dest, false);
}
public double getDistanceForNode(Node src, Node dest, boolean fast) {
var fastDistance = getDistanceFast(src, dest);
// use fast distance if more than 3000km
if (fast || fastDistance > 3000) return fastDistance;
var distance = distanceApiService.getDistance(src, src.isUserNode(), dest, dest.isUserNode());
if (distance.isEmpty()) return getDistanceFast(src, dest);
if (distance.isEmpty()) return fastDistance;
return distance.map(value -> value.getDistance().intValue()/1000).orElse(0);
}
public double getDistanceFast(Integer srcCountryId, Location src, Integer destCountryId, Location dest ) {
private double getDistanceFast(Integer srcCountryId, Location src, Integer destCountryId, Location dest ) {
double srcLatitudeRadians = Math.toRadians(src.getLatitude());
double srcLongitudeRad = Math.toRadians(src.getLongitude());
double destLatitudeRadians = Math.toRadians(dest.getLatitude());

View file

@ -219,6 +219,8 @@ public class RoutingService {
routeSection.setPostRun(section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.POST_RUN));
routeSection.setPreRun(false);
routeSection.setDistance(section.getApproxDistance());
return routeSection;
}
@ -334,7 +336,7 @@ public class RoutingService {
}
finalSection.setRate(matrixRate);
finalSection.setApproxDistance(distanceService.getDistance(container.getSourceNode(), toNode, true));
finalSection.setApproxDistance(distanceService.getDistanceForNode(container.getSourceNode(), toNode));
rates.add(finalSection);
}
@ -699,7 +701,7 @@ public class RoutingService {
if (matrixRate.isPresent()) {
matrixRateObj.setRate(matrixRate.get());
matrixRateObj.setApproxDistance(distanceService.getDistance(startNode, endNode, true));
matrixRateObj.setApproxDistance(distanceService.getDistanceForNode(startNode, endNode));
container.getRates().add(matrixRateObj);
return matrixRateObj;
} else {
@ -927,16 +929,16 @@ public class RoutingService {
sb.append(sections.getLast().getFromNode().getDebugText());
for (var section : sections.reversed()) {
sb.append(" -");
sb.append(" --[");
sb.append(section.getType().getDebugText());
sb.append("-> ");
sb.append("]--> ");
sb.append(section.getToNode().getDebugText());
}
} else sb.append("Empty");
return String.format("Route: [%s, %s]", quality.name().charAt(0) + (isFastest ? "\uD83D\uDE80" : "") + (isCheapest ? "\uD83D\uDCB2" : ""), sb);
return String.format("Route: [%s, %s]", quality.name() + (isFastest ? " FAST" : "") + (isCheapest ? " CHEAP" : ""), sb);
}
public void setQuality(ChainQuality quality) {
@ -1074,8 +1076,8 @@ public class RoutingService {
}
private enum TemporaryRateObjectType {
NEAR_BY("\uD83D\uDCCD"), MATRIX("\uD83D\uDCCA"), CONTAINER("\uD83D\uDCE6"), POST_RUN("\uD83D\uDE9B"), MAIN_RUN("\uD83D\uDEA2");
// NEAR_BY("\uD83D\uDCCD"), MATRIX("\uD83D\uDCCA"), CONTAINER("\uD83D\uDCE6"), POST_RUN("\uD83D\uDE9B"), MAIN_RUN("\uD83D\uDEA2");
NEAR_BY("NEAR"), MATRIX("KM"), CONTAINER("CON"), POST_RUN("POST"), MAIN_RUN("MAIN");
private final String debugText;
TemporaryRateObjectType(String debugText) {

View file

@ -2,7 +2,6 @@ package de.avatic.lcc.service.calculation.execution;
import de.avatic.lcc.dto.calculation.execution.*;
import de.avatic.lcc.model.db.calculations.CalculationJob;
import de.avatic.lcc.model.db.calculations.CalculationJobPriority;
import de.avatic.lcc.model.db.calculations.CalculationJobState;
import de.avatic.lcc.model.db.premises.PremiseState;
import de.avatic.lcc.model.db.properties.PropertySet;
@ -126,7 +125,7 @@ public class CalculationJobProcessorManagementService {
dto.setFailed(failed);
dto.setDrafts(draft);
dto.setRunning(running);
dto.setTotal(completed + draft);
dto.setCompleted(completed);
return dto;
}

View file

@ -1,9 +1,9 @@
package de.avatic.lcc.service.calculation.execution.steps;
import de.avatic.lcc.model.calculation.ContainerCalculationResult;
import de.avatic.lcc.dto.generic.ContainerType;
import de.avatic.lcc.dto.generic.RateType;
import de.avatic.lcc.dto.generic.TransportType;
import de.avatic.lcc.model.calculation.ContainerCalculationResult;
import de.avatic.lcc.model.db.calculations.CalculationJobRouteSection;
import de.avatic.lcc.model.db.nodes.Location;
import de.avatic.lcc.model.db.nodes.Node;
@ -70,7 +70,7 @@ public class RouteSectionCostCalculationService {
Node fromNode = premise.getSupplierNodeId() != null ? nodeRepository.getById(premise.getSupplierNodeId()).orElseThrow() : userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow();
Node toNode = nodeRepository.getById(destination.getDestinationNodeId()).orElseThrow();
double distance = distanceService.getDistance(fromNode, toNode, false);
double distance = destination.getDistanceD2d() == null ? distanceService.getDistanceForNode(fromNode, toNode) : destination.getDistanceD2d().doubleValue();
result.setDistance(BigDecimal.valueOf(distance));
// Get rate and transit time
@ -130,8 +130,14 @@ public class RouteSectionCostCalculationService {
// Get nodes and distance
RouteNode fromNode = routeNodeRepository.getById(section.getFromRouteNodeId()).orElseThrow();
RouteNode toNode = routeNodeRepository.getById(section.getToRouteNodeId()).orElseThrow();
if (section.getDistance() == null) {
double distance = getDistance(fromNode, toNode);
result.setDistance(BigDecimal.valueOf(distance));
} else {
result.setDistance(BigDecimal.valueOf(section.getDistance()));
}
// Get rate and transit time
BigDecimal rate;
@ -143,7 +149,7 @@ public class RouteSectionCostCalculationService {
transitTime = containerRate.getLeadTime();
} else if (RateType.MATRIX == section.getRateType()) {
MatrixRate matrixRate = findMatrixRate(fromNode, toNode, periodId);
rate = matrixRate.getRate().multiply(BigDecimal.valueOf(distance));
rate = matrixRate.getRate().multiply(result.getDistance());
transitTime = 3; // Default transit time for matrix rate
} else if (RateType.NEAR_BY == section.getRateType()) {
rate = BigDecimal.ZERO;
@ -279,7 +285,7 @@ public class RouteSectionCostCalculationService {
private double getDistance(RouteNode fromNode, RouteNode toNode) {
if (fromNode.getOutdated() || toNode.getOutdated()) {
return distanceService.getDistance(
return distanceService.getDistanceForLocation(
fromNode.getCountryId(), new Location(fromNode.getGeoLng().doubleValue(), fromNode.getGeoLat().doubleValue()),
toNode.getCountryId(), new Location(toNode.getGeoLng().doubleValue(), toNode.getGeoLat().doubleValue()), false);
}
@ -294,7 +300,7 @@ public class RouteSectionCostCalculationService {
throw new NoSuchElementException("Destination node not found for route section" + toNode.getName());
}
return distanceService.getDistance(optSrcNode.get(), optDestNode.get(), false);
return distanceService.getDistanceForNode(optSrcNode.get(), optDestNode.get(), false);
}

View file

@ -7,5 +7,13 @@ ALTER TABLE calculation_job
ADD INDEX idx_priority (priority);
ALTER TABLE distance_matrix
ADD COLUMN retries INT NOT NULL DEFAULT 0,
DROP CONSTRAINT chk_distance_matrix_state,
ADD CONSTRAINT chk_distance_matrix_state CHECK (`state` IN ('VALID', 'STALE', 'EXCEPTION'));
ALTER TABLE premise_destination
ADD COLUMN distance_d2d DECIMAL(15, 2) DEFAULT NULL COMMENT 'travel distance between the two nodes in meters';
ALTER TABLE premise_route_section
ADD COLUMN distance DECIMAL(15, 2) DEFAULT NULL COMMENT 'travel distance between the two nodes in meters';

View file

@ -433,6 +433,7 @@ CREATE TABLE IF NOT EXISTS premise_destination
is_d2d BOOLEAN DEFAULT FALSE,
rate_d2d DECIMAL(15, 2) DEFAULT NULL CHECK (rate_d2d >= 0),
lead_time_d2d INT UNSIGNED DEFAULT NULL CHECK (lead_time_d2d >= 0),
distance_d2d DECIMAL(15, 2) DEFAULT NULL,
repacking_cost DECIMAL(15, 2) DEFAULT NULL CHECK (repacking_cost >= 0),
handling_cost DECIMAL(15, 2) DEFAULT NULL CHECK (handling_cost >= 0),
disposal_cost DECIMAL(15, 2) DEFAULT NULL CHECK (disposal_cost >= 0),
@ -496,6 +497,7 @@ CREATE TABLE IF NOT EXISTS premise_route_section
is_main_run BOOLEAN DEFAULT FALSE,
is_post_run BOOLEAN DEFAULT FALSE,
is_outdated BOOLEAN DEFAULT FALSE,
distance DECIMAL(15, 2) DEFAULT NULL COMMENT 'travel distance between the two nodes in meters',
CONSTRAINT fk_premise_route_section_premise_route_id FOREIGN KEY (premise_route_id) REFERENCES premise_route (id),
FOREIGN KEY (from_route_node_id) REFERENCES premise_route_node (id),
FOREIGN KEY (to_route_node_id) REFERENCES premise_route_node (id),