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:
parent
dc6ed83853
commit
b99e7b3b4f
11 changed files with 176 additions and 61 deletions
|
|
@ -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},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -65,7 +65,7 @@ export default {
|
|||
const query = {
|
||||
searchTerm: '',
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
}
|
||||
await this.fetchData(query);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -169,6 +169,8 @@ public class UserNodeRepository {
|
|||
node.setIntermediate(false);
|
||||
node.setSource(true);
|
||||
|
||||
node.setUserNode(true);
|
||||
|
||||
return node;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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()")
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue