Enhanced distance calculation by integrating support for user-specific nodes:

- **Backend**: Updated `DistanceMatrixRepository`, `DistanceApiService`, and related services to handle user-node-specific relationships and unique constraints.
- **Database**: Modified `distance_matrix` table schema to include `from_user_node_id` and `to_user_node_id` with exclusive constraints and foreign key references to `sys_user_node`.
- **Frontend**: Refined error modal messages and adjusted layout for better usability. Increased pagination size in error logs for improved data display.
This commit is contained in:
Jan 2025-11-12 14:04:05 +01:00
parent dc6ed83853
commit b99e7b3b4f
11 changed files with 176 additions and 61 deletions

View file

@ -49,7 +49,7 @@ export default {
props: {isSelected: false, error: this.error},
},
{
title: 'Pinia store',
title: 'Frontend storage',
component: markRaw(ErrorModalPiniaStore),
props: {isSelected: false, error: this.error},
},

View file

@ -5,7 +5,7 @@
</div>
</div>
<div v-else class="no-data">
<p>No pinia data</p>
<span class="space-around">No frontend data available</span>
</div>
</template>
@ -82,9 +82,13 @@ export default {
}
.no-data {
padding: 1rem;
text-align: center;
color: #6b7280;
font-style: italic;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.6rem;
}
.space-around {
margin: 3rem;
}
</style>

View file

@ -65,7 +65,7 @@ export default {
const query = {
searchTerm: '',
page: 1,
pageSize: 10,
pageSize: 20,
}
await this.fetchData(query);
}

View file

@ -40,12 +40,15 @@ public class Distance {
private DistanceMatrixState state;
@NotNull
private Integer fromNodeId;
@NotNull
private Integer toNodeId;
private Integer fromUserNodeId;
private Integer toUserNodeId;
public Integer getFromNodeId() {
return fromNodeId;
}
@ -126,4 +129,19 @@ public class Distance {
this.state = state;
}
public Integer getFromUserNodeId() {
return fromUserNodeId;
}
public void setFromUserNodeId(Integer fromUserNodeId) {
this.fromUserNodeId = fromUserNodeId;
}
public Integer getToUserNodeId() {
return toUserNodeId;
}
public void setToUserNodeId(Integer toUserNodeId) {
this.toUserNodeId = toUserNodeId;
}
}

View file

@ -56,6 +56,8 @@ public class Node {
private Collection<Integer> outboundCountries;
private boolean isUserNode = false;
public Integer getId() {
return id;
}
@ -176,6 +178,14 @@ public class Node {
this.outboundCountries = outboundCountries;
}
public boolean isUserNode() {
return isUserNode;
}
public void setUserNode(boolean userNode) {
isUserNode = userNode;
}
public String getDebugText() {
return externalMappingId == null ? "\uD83D\uDC64" + name : externalMappingId;
}

View file

@ -3,7 +3,6 @@ package de.avatic.lcc.repositories;
import de.avatic.lcc.model.db.nodes.Distance;
import de.avatic.lcc.model.db.nodes.DistanceMatrixState;
import de.avatic.lcc.model.db.nodes.Node;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
@ -24,10 +23,10 @@ public class DistanceMatrixRepository {
this.jdbcTemplate = jdbcTemplate;
}
public Optional<Distance> getDistance(Node src, Node dest) {
String query = "SELECT * FROM distance_matrix WHERE from_node_id = ? AND to_node_id = ? AND state = ?";
public Optional<Distance> getDistance(Node src, boolean isUsrFrom, Node dest, boolean isUsrTo) {
String query = "SELECT * FROM distance_matrix WHERE ? = ? AND ? = ? AND state = ?";
var distance = jdbcTemplate.query(query, new DistanceMapper(), src.getId(), dest.getId(), DistanceMatrixState.VALID.name());
var distance = jdbcTemplate.query(query, new DistanceMapper(), isUsrFrom ? "from_user_node_id" : "from_node_id", src.getId(), isUsrTo ? "to_user_node_id" : "to_node_id", dest.getId(), DistanceMatrixState.VALID.name());
if (distance.isEmpty())
return Optional.empty();
@ -37,12 +36,15 @@ public class DistanceMatrixRepository {
public void saveDistance(Distance distance) {
try {
// First, check if an entry already exists
String checkQuery = "SELECT id FROM distance_matrix WHERE from_node_id = ? AND to_node_id = ?";
String checkQuery = "SELECT id FROM distance_matrix WHERE ? = ? AND ? = ?";
var existingIds = jdbcTemplate.query(checkQuery,
(rs, rowNum) -> rs.getInt("id"),
distance.getFromNodeId(),
distance.getToNodeId());
distance.getFromUserNodeId() != null ? "from_user_node_id" : "from_node_id",
distance.getFromUserNodeId() != null ? distance.getFromUserNodeId() : distance.getFromNodeId(),
distance.getToUserNodeId() != null ? "to_user_node_id" : "to_node_id",
distance.getToUserNodeId() != null ? distance.getToUserNodeId() : distance.getToNodeId());
if (!existingIds.isEmpty()) {
// Update existing entry
@ -55,7 +57,7 @@ public class DistanceMatrixRepository {
distance = ?,
state = ?,
updated_at = ?
WHERE from_node_id = ? AND to_node_id = ?
WHERE ? = ? AND ? = ?
""";
jdbcTemplate.update(updateQuery,
@ -66,8 +68,10 @@ public class DistanceMatrixRepository {
distance.getDistance(),
distance.getState().name(),
distance.getUpdatedAt(),
distance.getFromNodeId(),
distance.getToNodeId());
distance.getFromUserNodeId() != null ? "from_user_node_id" : "from_node_id",
distance.getFromUserNodeId() != null ? distance.getFromUserNodeId() : distance.getFromNodeId(),
distance.getToUserNodeId() != null ? "to_user_node_id" : "to_node_id",
distance.getToUserNodeId() != null ? distance.getToUserNodeId() : distance.getToNodeId());
logger.info("Updated existing distance entry for nodes {} -> {}",
distance.getFromNodeId(), distance.getToNodeId());
@ -75,13 +79,15 @@ public class DistanceMatrixRepository {
// Insert new entry
String insertQuery = """
INSERT INTO distance_matrix
(from_node_id, to_node_id, from_geo_lat, from_geo_lng, to_geo_lat, to_geo_lng, distance, state, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
(from_node_id, to_node_id, from_user_node_id, to_user_node_id, from_geo_lat, from_geo_lng, to_geo_lat, to_geo_lng, distance, state, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""";
jdbcTemplate.update(insertQuery,
distance.getFromNodeId(),
distance.getToNodeId(),
distance.getFromUserNodeId(),
distance.getToUserNodeId(),
distance.getFromGeoLat(),
distance.getFromGeoLng(),
distance.getToGeoLat(),
@ -107,6 +113,8 @@ public class DistanceMatrixRepository {
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.setDistance(rs.getBigDecimal("distance"));
entity.setFromGeoLng(rs.getBigDecimal("from_geo_lng"));
entity.setFromGeoLat(rs.getBigDecimal("from_geo_lat"));

View file

@ -169,6 +169,8 @@ public class UserNodeRepository {
node.setIntermediate(false);
node.setSource(true);
node.setUserNode(true);
return node;
}

View file

@ -3,6 +3,7 @@ package de.avatic.lcc.service.api;
import de.avatic.lcc.model.azuremaps.route.RouteDirectionsResponse;
import de.avatic.lcc.model.db.nodes.Distance;
import de.avatic.lcc.model.db.nodes.DistanceMatrixState;
import de.avatic.lcc.model.db.nodes.Location;
import de.avatic.lcc.model.db.nodes.Node;
import de.avatic.lcc.repositories.DistanceMatrixRepository;
import org.slf4j.Logger;
@ -13,7 +14,6 @@ import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.math.BigDecimal;
import java.net.URI;
import java.time.LocalDateTime;
import java.util.Optional;
@ -35,7 +35,27 @@ public class DistanceApiService {
this.restTemplate = restTemplate;
}
public Optional<Distance> getDistance(Node from, Node to) {
public Optional<Integer> getDistance(Location from, Location to) {
if (from == null || to == null) {
logger.warn("Source or destination location is null");
return Optional.empty();
}
if (from.getLatitude() == null || from.getLongitude() == null ||
to.getLatitude() == null || to.getLongitude() == null) {
return Optional.empty();
}
RouteDirectionsResponse response = fetchDistanceFromAzureMaps(BigDecimal.valueOf(from.getLatitude()), BigDecimal.valueOf(from.getLongitude()), BigDecimal.valueOf(to.getLatitude()), BigDecimal.valueOf(to.getLongitude()));
if (response != null && response.getRoutes() != null && !response.getRoutes().isEmpty()) {
return Optional.of(response.getRoutes().getFirst().getSummary().getLengthInMeters());
}
return Optional.empty();
}
public Optional<Distance> getDistance(Node from, boolean isUsrFrom, Node to, boolean isUsrTo) {
if (from == null || to == null) {
logger.warn("Source or destination node is null");
@ -48,7 +68,7 @@ public class DistanceApiService {
return Optional.empty();
}
Optional<Distance> cachedDistance = distanceMatrixRepository.getDistance(from, to);
Optional<Distance> cachedDistance = distanceMatrixRepository.getDistance(from, isUsrFrom, to, isUsrTo);
if (cachedDistance.isPresent()) {
logger.debug("Found cached distance from node {} to node {}", from.getId(), to.getId());
@ -56,7 +76,7 @@ public class DistanceApiService {
}
logger.debug("Fetching distance from Azure Maps for nodes {} to {}", from.getId(), to.getId());
Optional<Distance> fetchedDistance = fetchDistanceFromAzureMaps(from, to);
Optional<Distance> fetchedDistance = fetchDistanceFromAzureMaps(from, isUsrFrom, to, isUsrTo);
if (fetchedDistance.isPresent()) {
distanceMatrixRepository.saveDistance(fetchedDistance.get());
@ -66,25 +86,45 @@ public class DistanceApiService {
return Optional.empty();
}
private Optional<Distance> fetchDistanceFromAzureMaps(Node from, Node to) {
try {
private RouteDirectionsResponse fetchDistanceFromAzureMaps(BigDecimal fromLat, BigDecimal fromLng, BigDecimal toLat, BigDecimal toLng) {
String url = UriComponentsBuilder.fromUriString(AZURE_MAPS_ROUTE_API)
.queryParam("api-version", "1.0")
.queryParam("subscription-key", subscriptionKey)
.queryParam("query", String.format("%s,%s:%s,%s",
from.getGeoLat(), from.getGeoLng(),
to.getGeoLat(), to.getGeoLng()))
fromLat, fromLng,
toLat, toLng))
.encode()
.toUriString();
RouteDirectionsResponse response = restTemplate.getForObject(url, RouteDirectionsResponse.class);
return restTemplate.getForObject(url, RouteDirectionsResponse.class);
}
private Optional<Distance> fetchDistanceFromAzureMaps(Node from, boolean isUsrFrom, Node to, boolean isUsrTo) {
try {
RouteDirectionsResponse response = fetchDistanceFromAzureMaps(from.getGeoLat(), from.getGeoLng(), to.getGeoLat(), to.getGeoLng());
if (response != null && response.getRoutes() != null && !response.getRoutes().isEmpty()) {
Integer distanceInMeters = response.getRoutes().getFirst().getSummary().getLengthInMeters();
Distance distance = new Distance();
if (isUsrFrom) {
distance.setFromUserNodeId(from.getId());
distance.setFromNodeId(null);
} else {
distance.setFromUserNodeId(null);
distance.setFromNodeId(from.getId());
distance.setToNodeId(to.getId());
}
if (isUsrTo) {
distance.setToUserNodeId(to.getId());
distance.setToNodeId(null);
} else {
distance.setToUserNodeId(null);
distance.setToNodeId(from.getId());
}
distance.setFromGeoLat(from.getGeoLat());
distance.setFromGeoLng(from.getGeoLng());
distance.setToGeoLat(to.getGeoLat());

View file

@ -13,25 +13,33 @@ import org.springframework.stereotype.Service;
public class DistanceService {
private static final double EARTH_RADIUS = 6371.0;
private final DistanceMatrixRepository distanceMatrixRepository;
private final CountryRepository countryRepository;
private final DistanceApiService distanceApiService;
public DistanceService(DistanceMatrixRepository distanceMatrixRepository, CountryRepository countryRepository, DistanceApiService distanceApiService) {
this.distanceMatrixRepository = distanceMatrixRepository;
public DistanceService(CountryRepository countryRepository, DistanceApiService distanceApiService) {
this.countryRepository = countryRepository;
this.distanceApiService = distanceApiService;
}
public double getDistance(Integer srcCountryId, Location src, Integer destCountryId, Location dest, boolean fast) {
if (fast) return getDistanceFast(srcCountryId, src, destCountryId, dest);
var distance = distanceApiService.getDistance(src, dest);
if (distance.isEmpty()) return getDistanceFast(srcCountryId, src, destCountryId, dest);
return distance.map(value -> value/1000).orElse(0);
}
public double getDistance(Node src, Node dest, boolean fast) {
if (fast) return getDistanceFast(src, dest);
var distance = distanceApiService.getDistance(src, dest);
var distance = distanceApiService.getDistance(src, src.isUserNode(), dest, dest.isUserNode());
if (distance.isEmpty()) return getDistanceFast(src, dest);
return distance.map(value -> value.getDistance().intValue()/1000).orElse(0);
}
public double getDistanceFast(Integer srcCountryId, Location src, Integer destCountryId, Location dest ) {
double srcLatitudeRadians = Math.toRadians(src.getLatitude());
double srcLongitudeRad = Math.toRadians(src.getLongitude());

View file

@ -43,7 +43,7 @@ public class RoutingService {
public List<RouteInformation> findRoutes(Node destination, Node source, boolean sourceIsUserNode) {
List<RouteInformation> routeInformation = new ArrayList<>();
TemporaryContainer container = new TemporaryContainer(source, destination);
TemporaryContainer container = new TemporaryContainer(source, destination, sourceIsUserNode);
/*
* Get the source and destination node from database.
@ -740,6 +740,7 @@ public class RoutingService {
* Source and destination node
*/
private final Node source;
private final boolean sourceIsUserNode;
private final Node destination;
private final Map<Integer, List<List<Node>>> chains = new HashMap<>();
/*
@ -752,8 +753,9 @@ public class RoutingService {
private Map<Integer, List<ContainerRate>> postRuns;
private MatrixRate sourceMatrixRate;
public TemporaryContainer(Node source, Node destination) {
public TemporaryContainer(Node source, Node destination, boolean sourceIsUserNode) {
this.source = source;
this.sourceIsUserNode = sourceIsUserNode;
this.destination = destination;
this.mainRuns = null;
this.postRuns = null;
@ -821,6 +823,10 @@ public class RoutingService {
public void setChain(Integer id, List<List<Node>> chain) {
this.chains.put(id, chain);
}
public boolean isSourceIsUserNode() {
return sourceIsUserNode;
}
}
@Renderer(text = "getFullDebugText()")

View file

@ -21,6 +21,7 @@ import de.avatic.lcc.repositories.premise.RouteNodeRepository;
import de.avatic.lcc.repositories.properties.PropertyRepository;
import de.avatic.lcc.repositories.rates.ContainerRateRepository;
import de.avatic.lcc.repositories.rates.MatrixRateRepository;
import de.avatic.lcc.repositories.users.UserNodeRepository;
import de.avatic.lcc.service.calculation.DistanceService;
import org.springframework.stereotype.Service;
@ -38,8 +39,9 @@ public class RouteSectionCostCalculationService {
private final PropertyRepository propertyRepository;
private final ChangeRiskFactorCalculationService changeRiskFactorCalculationService;
private final NodeRepository nodeRepository;
private final UserNodeRepository userNodeRepository;
public RouteSectionCostCalculationService(ContainerRateRepository containerRateRepository, MatrixRateRepository matrixRateRepository, RouteNodeRepository routeNodeRepository, DistanceService distanceService, PropertyRepository propertyRepository, ChangeRiskFactorCalculationService changeRiskFactorCalculationService, NodeRepository nodeRepository) {
public RouteSectionCostCalculationService(ContainerRateRepository containerRateRepository, MatrixRateRepository matrixRateRepository, RouteNodeRepository routeNodeRepository, DistanceService distanceService, PropertyRepository propertyRepository, ChangeRiskFactorCalculationService changeRiskFactorCalculationService, NodeRepository nodeRepository, UserNodeRepository userNodeRepository) {
this.containerRateRepository = containerRateRepository;
this.matrixRateRepository = matrixRateRepository;
this.routeNodeRepository = routeNodeRepository;
@ -47,6 +49,7 @@ public class RouteSectionCostCalculationService {
this.propertyRepository = propertyRepository;
this.changeRiskFactorCalculationService = changeRiskFactorCalculationService;
this.nodeRepository = nodeRepository;
this.userNodeRepository = userNodeRepository;
}
public CalculationJobRouteSection doD2dCalculation(Integer setId, Integer periodId, Premise premise, Destination destination, ContainerCalculationResult containerCalculation) {
@ -274,9 +277,25 @@ public class RouteSectionCostCalculationService {
}
private double getDistance(RouteNode fromNode, RouteNode toNode) {
return distanceService.getDistanceFast(
if(fromNode.getOutdated() || toNode.getOutdated()) {
return distanceService.getDistance(
fromNode.getCountryId(), new Location(fromNode.getGeoLng().doubleValue(), fromNode.getGeoLat().doubleValue()),
toNode.getCountryId(), new Location(toNode.getGeoLng().doubleValue(), toNode.getGeoLat().doubleValue()));
toNode.getCountryId(), new Location(toNode.getGeoLng().doubleValue(), toNode.getGeoLat().doubleValue()), false);
}
var optSrcNode = fromNode.getNodeId() == null ? userNodeRepository.getById(fromNode.getUserNodeId()) : nodeRepository.getById(fromNode.getNodeId());
var optDestNode = toNode.getNodeId() == null ? userNodeRepository.getById(toNode.getUserNodeId()) : nodeRepository.getById(toNode.getNodeId());
if(optSrcNode.isEmpty() ) {
throw new NoSuchElementException("Source node not found for route section " + fromNode.getName());
}
if(optDestNode.isEmpty() ) {
throw new NoSuchElementException("Destination node not found for route section" + toNode.getName());
}
return distanceService.getDistance(optSrcNode.get(), optDestNode.get(), false);
}