Refactor terminology and add new RoutingService.

Updated repository classes to replace "node" with "chain" for clarity, aligning with domain terminology. Introduced `RoutingService2` to handle route calculation logic, supporting recursive chain resolution and optimized route determination.
This commit is contained in:
Jan 2025-04-27 12:13:54 +02:00
parent e4ab851d7f
commit a468e3d187
15 changed files with 2320 additions and 145 deletions

View file

@ -0,0 +1,308 @@
package de.avatic.lcc.model.country;
/**
* Enumeration providing detour indices for different countries.
* The detour index represents the ratio between road distance and direct air distance.
*/
public enum DetourIndex {
AD(1.45, 1.60, 1.53), // Andorra
AE(1.25, 1.20, 1.23), // United Arab Emirates
AF(1.65, 1.90, 1.78), // Afghanistan
AG(1.40, 1.35, 1.38), // Antigua and Barbuda
AI(1.35, 1.30, 1.33), // Anguilla
AL(1.50, 1.70, 1.60), // Albania
AM(1.45, 1.75, 1.60), // Armenia
AO(1.55, 1.85, 1.70), // Angola
AQ(2.00, 3.00, 2.50), // Antarctica
AR(1.30, 1.45, 1.38), // Argentina
AS(1.40, 1.60, 1.50), // American Samoa
AT(1.35, 1.50, 1.43), // Austria
AU(1.25, 1.35, 1.30), // Australia
AW(1.35, 1.25, 1.30), // Aruba
AX(1.35, 1.70, 1.53), // Åland Islands
AZ(1.45, 1.65, 1.55), // Azerbaijan
BA(1.50, 1.80, 1.65), // Bosnia and Herzegovina
BB(1.35, 1.30, 1.33), // Barbados
BD(1.50, 1.60, 1.55), // Bangladesh
BE(1.30, 1.25, 1.28), // Belgium
BF(1.50, 1.80, 1.65), // Burkina Faso
BG(1.40, 1.55, 1.48), // Bulgaria
BH(1.25, 1.20, 1.23), // Bahrain
BI(1.55, 1.90, 1.73), // Burundi
BJ(1.50, 1.85, 1.68), // Benin
BL(1.40, 1.35, 1.38), // Saint Barthélemy
BM(1.35, 1.30, 1.33), // Bermuda
BN(1.40, 1.50, 1.45), // Brunei Darussalam
BO(1.45, 1.85, 1.65), // Bolivia
BR(1.35, 1.65, 1.50), // Brazil
BS(1.40, 1.60, 1.50), // Bahamas
BT(1.60, 2.10, 1.85), // Bhutan
BV(1.80, 2.20, 2.00), // Bouvet Island
BW(1.45, 1.70, 1.58), // Botswana
BY(1.35, 1.45, 1.40), // Belarus
BZ(1.45, 1.70, 1.58), // Belize
CA(1.30, 1.45, 1.38), // Canada
CC(1.30, 1.25, 1.28), // Cocos (Keeling) Islands
CD(1.60, 2.00, 1.80), // Democratic Republic of the Congo
CF(1.65, 2.10, 1.88), // Central African Republic
CG(1.60, 1.95, 1.78), // Congo
CH(1.40, 1.65, 1.53), // Switzerland
CI(1.50, 1.80, 1.65), // Côte d'Ivoire
CK(1.40, 1.50, 1.45), // Cook Islands
CL(1.35, 1.70, 1.53), // Chile
CM(1.55, 1.90, 1.73), // Cameroon
CN(1.35, 1.50, 1.43), // China
CO(1.40, 1.80, 1.60), // Colombia
CR(1.45, 1.70, 1.58), // Costa Rica
CU(1.40, 1.55, 1.48), // Cuba
CV(1.45, 1.65, 1.55), // Cabo Verde
CW(1.35, 1.30, 1.33), // Curaçao
CX(1.35, 1.40, 1.38), // Christmas Island
CY(1.40, 1.50, 1.45), // Cyprus
CZ(1.35, 1.40, 1.38), // Czechia
DE(1.30, 1.25, 1.28), // Germany
DJ(1.50, 1.85, 1.68), // Djibouti
DK(1.30, 1.25, 1.28), // Denmark
DM(1.45, 1.70, 1.58), // Dominica
DO(1.40, 1.60, 1.50), // Dominican Republic
DZ(1.40, 1.70, 1.55), // Algeria
EC(1.45, 1.80, 1.63), // Ecuador
EE(1.35, 1.40, 1.38), // Estonia
EG(1.35, 1.50, 1.43), // Egypt
EH(1.45, 1.85, 1.65), // Western Sahara
ER(1.55, 1.95, 1.75), // Eritrea
ES(1.35, 1.45, 1.40), // Spain
ET(1.55, 1.95, 1.75), // Ethiopia
FI(1.35, 1.50, 1.43), // Finland
FJ(1.45, 1.75, 1.60), // Fiji
FK(1.50, 1.90, 1.70), // Falkland Islands
FM(1.45, 1.85, 1.65), // Micronesia
FO(1.45, 1.85, 1.65), // Faroe Islands
FR(1.35, 1.40, 1.38), // France
GA(1.55, 1.90, 1.73), // Gabon
GB(1.40, 1.30, 1.35), // United Kingdom
GD(1.45, 1.55, 1.50), // Grenada
GE(1.45, 1.75, 1.60), // Georgia
GF(1.45, 1.85, 1.65), // French Guiana
GG(1.40, 1.35, 1.38), // Guernsey
GH(1.50, 1.80, 1.65), // Ghana
GI(1.30, 1.25, 1.28), // Gibraltar
GL(1.60, 2.30, 1.95), // Greenland
GM(1.50, 1.75, 1.63), // Gambia
GN(1.55, 1.90, 1.73), // Guinea
GP(1.40, 1.50, 1.45), // Guadeloupe
GQ(1.55, 1.95, 1.75), // Equatorial Guinea
GR(1.45, 1.65, 1.55), // Greece
GS(1.80, 2.40, 2.10), // South Georgia and the South Sandwich Islands
GT(1.45, 1.75, 1.60), // Guatemala
GU(1.35, 1.40, 1.38), // Guam
GW(1.55, 1.95, 1.75), // Guinea-Bissau
GY(1.50, 1.85, 1.68), // Guyana
HK(1.40, 1.30, 1.35), // Hong Kong
HM(1.80, 2.40, 2.10), // Heard Island and McDonald Islands
HN(1.45, 1.80, 1.63), // Honduras
HR(1.40, 1.55, 1.48), // Croatia
HT(1.50, 1.90, 1.70), // Haiti
HU(1.35, 1.40, 1.38), // Hungary
ID(1.45, 1.75, 1.60), // Indonesia
IE(1.40, 1.55, 1.48), // Ireland
IL(1.35, 1.40, 1.38), // Israel
IM(1.40, 1.50, 1.45), // Isle of Man
IN(1.45, 1.60, 1.53), // India
IO(1.40, 1.50, 1.45), // British Indian Ocean Territory
IQ(1.45, 1.70, 1.58), // Iraq
IR(1.40, 1.65, 1.53), // Iran
IS(1.40, 1.80, 1.60), // Iceland
IT(1.40, 1.45, 1.43), // Italy
JE(1.40, 1.35, 1.38), // Jersey
JM(1.40, 1.60, 1.50), // Jamaica
JO(1.40, 1.65, 1.53), // Jordan
JP(1.50, 1.40, 1.45), // Japan
KE(1.50, 1.85, 1.68), // Kenya
KG(1.50, 1.85, 1.68), // Kyrgyzstan
KH(1.50, 1.80, 1.65), // Cambodia
KI(1.40, 1.60, 1.50), // Kiribati
KM(1.50, 1.75, 1.63), // Comoros
KN(1.40, 1.45, 1.43), // Saint Kitts and Nevis
KP(1.45, 1.70, 1.58), // North Korea
KR(1.40, 1.35, 1.38), // South Korea
KW(1.30, 1.35, 1.33), // Kuwait
KY(1.35, 1.30, 1.33), // Cayman Islands
KZ(1.40, 1.65, 1.53), // Kazakhstan
LA(1.55, 1.90, 1.73), // Laos
LB(1.45, 1.65, 1.55), // Lebanon
LC(1.45, 1.60, 1.53), // Saint Lucia
LI(1.35, 1.45, 1.40), // Liechtenstein
LK(1.45, 1.65, 1.55), // Sri Lanka
LR(1.55, 1.95, 1.75), // Liberia
LS(1.55, 1.90, 1.73), // Lesotho
LT(1.35, 1.45, 1.40), // Lithuania
LU(1.30, 1.35, 1.33), // Luxembourg
LV(1.35, 1.45, 1.40), // Latvia
LY(1.45, 1.80, 1.63), // Libya
MA(1.40, 1.65, 1.53), // Morocco
MC(1.25, 1.20, 1.23), // Monaco
MD(1.40, 1.60, 1.50), // Moldova
ME(1.50, 1.85, 1.68), // Montenegro
MF(1.35, 1.30, 1.33), // Saint Martin
MG(1.55, 1.95, 1.75), // Madagascar
MH(1.35, 1.45, 1.40), // Marshall Islands
MK(1.45, 1.70, 1.58), // North Macedonia
ML(1.55, 1.95, 1.75), // Mali
MM(1.50, 1.85, 1.68), // Myanmar
MN(1.45, 1.95, 1.70), // Mongolia
MO(1.45, 1.30, 1.38), // Macau
MP(1.40, 1.50, 1.45), // Northern Mariana Islands
MQ(1.40, 1.55, 1.48), // Martinique
MR(1.50, 1.90, 1.70), // Mauritania
MS(1.40, 1.60, 1.50), // Montserrat
MT(1.35, 1.30, 1.33), // Malta
MU(1.40, 1.45, 1.43), // Mauritius
MV(1.30, 1.40, 1.35), // Maldives
MW(1.55, 1.90, 1.73), // Malawi
MX(1.40, 1.60, 1.50), // Mexico
MY(1.40, 1.55, 1.48), // Malaysia
MZ(1.55, 1.90, 1.73), // Mozambique
NA(1.45, 1.80, 1.63), // Namibia
NC(1.45, 1.70, 1.58), // New Caledonia
NE(1.55, 1.95, 1.75), // Niger
NF(1.35, 1.45, 1.40), // Norfolk Island
NG(1.50, 1.80, 1.65), // Nigeria
NI(1.45, 1.75, 1.60), // Nicaragua
NL(1.25, 1.20, 1.23), // Netherlands
NO(1.45, 1.90, 1.68), // Norway
NP(1.55, 2.00, 1.78), // Nepal
NR(1.30, 1.25, 1.28), // Nauru
NU(1.40, 1.55, 1.48), // Niue
NZ(1.35, 1.55, 1.45), // New Zealand
OM(1.40, 1.65, 1.53), // Oman
PA(1.45, 1.75, 1.60), // Panama
PE(1.45, 1.85, 1.65), // Peru
PF(1.45, 1.75, 1.60), // French Polynesia
PG(1.55, 2.00, 1.78), // Papua New Guinea
PH(1.45, 1.75, 1.60), // Philippines
PK(1.45, 1.70, 1.58), // Pakistan
PL(1.35, 1.40, 1.38), // Poland
PM(1.40, 1.55, 1.48), // Saint Pierre and Miquelon
PN(1.50, 1.80, 1.65), // Pitcairn
PR(1.40, 1.55, 1.48), // Puerto Rico
PS(1.45, 1.65, 1.55), // Palestine
PT(1.40, 1.55, 1.48), // Portugal
PW(1.40, 1.60, 1.50), // Palau
PY(1.40, 1.70, 1.55), // Paraguay
QA(1.30, 1.35, 1.33), // Qatar
RE(1.45, 1.60, 1.53), // Réunion
RO(1.40, 1.55, 1.48), // Romania
RS(1.40, 1.60, 1.50), // Serbia
RU(1.40, 1.65, 1.53), // Russian Federation
RW(1.50, 1.85, 1.68), // Rwanda
SA(1.35, 1.60, 1.48), // Saudi Arabia
SB(1.50, 1.95, 1.73), // Solomon Islands
SC(1.45, 1.65, 1.55), // Seychelles
SD(1.50, 1.90, 1.70), // Sudan
SE(1.35, 1.50, 1.43), // Sweden
SG(1.30, 1.20, 1.25), // Singapore
SH(1.50, 1.85, 1.68), // Saint Helena
SI(1.40, 1.55, 1.48), // Slovenia
SK(1.40, 1.50, 1.45), // Slovakia
SL(1.55, 1.95, 1.75), // Sierra Leone
SM(1.35, 1.40, 1.38), // San Marino
SN(1.50, 1.80, 1.65), // Senegal
SO(1.60, 2.10, 1.85), // Somalia
SR(1.45, 1.85, 1.65), // Suriname
SS(1.60, 2.10, 1.85), // South Sudan
ST(1.50, 1.80, 1.65), // Sao Tome and Principe
SV(1.45, 1.70, 1.58), // El Salvador
SX(1.35, 1.30, 1.33), // Sint Maarten
SY(1.45, 1.75, 1.60), // Syria
SZ(1.50, 1.80, 1.65), // Swaziland
TC(1.35, 1.40, 1.38), // Turks and Caicos Islands
TD(1.60, 2.10, 1.85), // Chad
TF(1.70, 2.20, 1.95), // French Southern Territories
TG(1.50, 1.85, 1.68), // Togo
TH(1.40, 1.60, 1.50), // Thailand
TJ(1.50, 1.95, 1.73), // Tajikistan
TK(1.35, 1.45, 1.40), // Tokelau
TL(1.55, 1.95, 1.75), // Timor-Leste
TM(1.45, 1.75, 1.60), // Turkmenistan
TN(1.40, 1.65, 1.53), // Tunisia
TO(1.40, 1.50, 1.45), // Tonga
TR(1.40, 1.65, 1.53), // Turkey
TT(1.40, 1.50, 1.45), // Trinidad and Tobago
TV(1.30, 1.25, 1.28), // Tuvalu
TW(1.40, 1.55, 1.48), // Taiwan
TZ(1.55, 1.90, 1.73), // Tanzania
UA(1.40, 1.55, 1.48), // Ukraine
UG(1.55, 1.90, 1.73), // Uganda
UM(1.40, 1.55, 1.48), // United States Minor Outlying Islands
US(1.25, 1.35, 1.30), // United States of America
UY(1.35, 1.55, 1.45), // Uruguay
UZ(1.45, 1.70, 1.58), // Uzbekistan
VA(1.25, 1.20, 1.23), // Holy See
VC(1.45, 1.60, 1.53), // Saint Vincent and the Grenadines
VE(1.45, 1.75, 1.60), // Venezuela
VG(1.40, 1.45, 1.43), // British Virgin Islands
VI(1.40, 1.45, 1.43), // U.S. Virgin Islands
VN(1.45, 1.70, 1.58), // Vietnam
VU(1.50, 1.85, 1.68), // Vanuatu
WF(1.45, 1.65, 1.55), // Wallis and Futuna
WS(1.45, 1.60, 1.53), // Samoa
YE(1.50, 1.85, 1.68), // Yemen
YT(1.45, 1.65, 1.55), // Mayotte
ZA(1.40, 1.60, 1.50), // South Africa
ZM(1.55, 1.90, 1.73), // Zambia
ZW(1.50, 1.85, 1.68); // Zimbabwe
private final double urbanDetourIndex;
private final double ruralDetourIndex;
private final double averageDetourIndex;
/**
* Constructor for the DetourIndex enum.
*
* @param urbanDetourIndex Detour index for urban areas
* @param ruralDetourIndex Detour index for rural areas
* @param averageDetourIndex Average detour index for the country
*/
DetourIndex(double urbanDetourIndex, double ruralDetourIndex, double averageDetourIndex) {
this.urbanDetourIndex = urbanDetourIndex;
this.ruralDetourIndex = ruralDetourIndex;
this.averageDetourIndex = averageDetourIndex;
}
/**
* Get the detour index for urban areas.
*
* @return The urban detour index as a double value
*/
public double getUrbanDetourIndex() {
return urbanDetourIndex;
}
/**
* Get the detour index for rural areas.
*
* @return The rural detour index as a double value
*/
public double getRuralDetourIndex() {
return ruralDetourIndex;
}
/**
* Get the average detour index for the country.
*
* @return The average detour index as a double value
*/
public double getAverageDetourIndex() {
return averageDetourIndex;
}
/**
* Get the DetourIndex object for a specific country.
*
* @param country The ISO code of the country
* @return The DetourIndex object for the country
*/
public static DetourIndex getDetourIndex(IsoCode country) {
return DetourIndex.valueOf(country.name());
}
}

View file

@ -2,19 +2,15 @@ package de.avatic.lcc.model.nodes;
import jakarta.validation.constraints.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
@Table(name = "distance_matrix")
public class DistanceMatrix {
public class Distance {
@Id
private Integer id;
@Digits(integer = 7, fraction = 4)
@ -41,18 +37,31 @@ public class DistanceMatrix {
@NotNull
private BigDecimal distance;
private OffsetDateTime updatedAt;
private LocalDateTime updatedAt;
@Size(max = 10)
private String state;
private DistanceMatrixState state;
@NotNull
@Column("from_node_id")
private AggregateReference<Node,Integer> fromNode;
private Integer fromNodeId;
@NotNull
@Column("to_node_id")
private AggregateReference<Node,Integer> toNode;
private Integer toNodeId;
public Integer getFromNodeId() {
return fromNodeId;
}
public void setFromNodeId(Integer fromNodeId) {
this.fromNodeId = fromNodeId;
}
public Integer getToNodeId() {
return toNodeId;
}
public void setToNodeId(Integer toNodeId) {
this.toNodeId = toNodeId;
}
public Integer getId() {
return id;
@ -102,35 +111,20 @@ public class DistanceMatrix {
this.distance = distance;
}
public OffsetDateTime getUpdatedAt() {
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(OffsetDateTime updatedAt) {
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public String getState() {
public DistanceMatrixState getState() {
return state;
}
public void setState(String state) {
public void setState(DistanceMatrixState state) {
this.state = state;
}
public AggregateReference<Node, Integer> getFromNode() {
return fromNode;
}
public void setFromNode(AggregateReference<Node, Integer> fromNode) {
this.fromNode = fromNode;
}
public AggregateReference<Node, Integer> getToNode() {
return toNode;
}
public void setToNode(AggregateReference<Node, Integer> toNode) {
this.toNode = toNode;
}
}

View file

@ -0,0 +1,26 @@
package de.avatic.lcc.model.premises.route;
import java.util.ArrayList;
import java.util.List;
public class RouteInformation {
private Route route;
private List<RouteSection> sections;
private List<RouteNode> nodes;
public void setRoute(Route route) {
this.route = route;
}
public void setRouteNodes(List<RouteNode> nodes) {
this.nodes = nodes;
}
public void setRouteSections(List<RouteSection> sections) {
this.sections = sections;
}
}

View file

@ -32,6 +32,31 @@ public class ContainerRate {
private Integer validityPeriodId;
private Integer fromCountryId;
private Integer toCountryId;
public Integer getFromCountryId() {
return fromCountryId;
}
public void setFromCountryId(int fromCountryId) {
if (fromCountryId != 0)
this.fromCountryId = fromCountryId;
else this.fromCountryId = null;
}
public Integer getToCountryId() {
return toCountryId;
}
public void setToCountryId(int toCountryId) {
if (toCountryId != 0)
this.toCountryId = toCountryId;
else this.toCountryId = null;
}
public Integer getId() {
return id;
}

View file

@ -0,0 +1,55 @@
package de.avatic.lcc.repositories;
import de.avatic.lcc.model.nodes.Distance;
import de.avatic.lcc.model.nodes.DistanceMatrixState;
import de.avatic.lcc.model.nodes.Node;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowCallbackHandler;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;
@Repository
public class DistanceMatrixRepository {
private final JdbcTemplate jdbcTemplate;
public DistanceMatrixRepository(JdbcTemplate jdbcTemplate) {
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 = ?";
var distance = jdbcTemplate.query(query, new DistanceMapper(), src.getId(), dest.getId(), DistanceMatrixState.VALID);
if(distance.isEmpty())
return Optional.empty();
return Optional.of(distance.getFirst());
}
private static class DistanceMapper implements RowMapper<Distance> {
@Override
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.setDistance(rs.getBigDecimal("distance"));
entity.setFromGeoLng(rs.getBigDecimal("from_geo_lng"));
entity.setFromGeoLat(rs.getBigDecimal("from_geo_lat"));
entity.setToGeoLng(rs.getBigDecimal("to_geo_lng"));
entity.setToGeoLat(rs.getBigDecimal("to_geo_lat"));
entity.setState(DistanceMatrixState.valueOf(rs.getString("state")));
entity.setUpdatedAt(rs.getTimestamp("updated_at").toLocalDateTime());
return entity;
}
}
}

View file

@ -32,9 +32,9 @@ public class NodeRepository {
FROM node
WHERE node.id = ?""";
var node = jdbcTemplate.queryForObject(query, new NodeMapper(), id);
var chain = jdbcTemplate.queryForObject(query, new NodeMapper(), id);
return Optional.ofNullable(node);
return Optional.ofNullable(chain);
}
private List<Map<Integer, Integer>> getPredecessorsOf(Integer id) {
@ -108,11 +108,11 @@ public class NodeRepository {
private String buildQuery(String filter, Boolean excludeDeprecated, SearchQueryPagination searchQueryPagination) {
StringBuilder queryBuilder = new StringBuilder("""
SELECT node.id AS id, node.name AS name, node.address as address, node.is_source as is_source,
node.is_destination as is_destination, node.is_intermediate as is_intermediate, node.country_id as country_id, node.predecessor_required as predecessor_required,
SELECT chain.id AS id, chain.name AS name, chain.address as address, chain.is_source as is_source,
chain.is_destination as is_destination, chain.is_intermediate as is_intermediate, chain.country_id as country_id, chain.predecessor_required as predecessor_required,
country.iso_code AS country_iso_code, country.region_code AS country_region_code, country.name AS country_name, country.is_deprecated AS country_is_deprecated
FROM node
LEFT JOIN country ON country.id = node.country_id
FROM chain
LEFT JOIN country ON country.id = chain.country_id
""");
if (excludeDeprecated) {
@ -132,18 +132,18 @@ public class NodeRepository {
}
@Transactional
public Optional<Integer> update(Node node) {
public Optional<Integer> update(Node chain) {
//TODO update predecessors and outbound_countries too
//TODO implement correctly
//TODO if node is updated set all linked RouteNodes to outdated!
//TODO if chain is updated set all linked RouteNodes to outdated!
String query = "UPDATE node SET name = ?, address = ?, country_id = ?, is_source = ?, is_destination = ?, is_intermediate = ?, predecessor_required = ? WHERE id = ?";
return Optional.ofNullable(jdbcTemplate.update(query, node.getId()) == 0 ? null : node.getId());
return Optional.ofNullable(jdbcTemplate.update(query, chain.getId()) == 0 ? null : chain.getId());
}
public List<Node> searchNode(String filter, int limit, NodeType nodeType, boolean excludeDeprecated) {
StringBuilder queryBuilder = new StringBuilder().append("SELECT * FROM node WHERE (name LIKE ? OR address LIKE ?)");
StringBuilder queryBuilder = new StringBuilder().append("SELECT * FROM chain WHERE (name LIKE ? OR address LIKE ?)");
if (nodeType != null) {
queryBuilder.append(" AND node_type = ?");
@ -163,7 +163,7 @@ public class NodeRepository {
}
public List<Node> listAllNodes(boolean onlySources) {
StringBuilder queryBuilder = new StringBuilder("SELECT * FROM node");
StringBuilder queryBuilder = new StringBuilder("SELECT * FROM chain");
if (onlySources) {
queryBuilder.append(" WHERE is_source = true");
}
@ -175,32 +175,37 @@ public class NodeRepository {
public Optional<Node> getByExternalMappingId(String mappingId) {
String query = """
SELECT node.id AS id, node.name AS name, node.address as address, node.is_source as is_source,
node.is_destination as is_destination, node.is_intermediate as is_intermediate, node.country_id as country_id, node.predecessor_required as predecessor_required
v.is_destination as is_destination, node.is_intermediate as is_intermediate, node.country_id as country_id, node.predecessor_required as predecessor_required
FROM node
WHERE node.external_mapping_id = ?""";
var node = jdbcTemplate.queryForObject(query, new NodeMapper(), mappingId);
var chain = jdbcTemplate.queryForObject(query, new NodeMapper(), mappingId);
return Optional.ofNullable(node);
return Optional.ofNullable(chain);
}
/**
* Resolves chains of predecessors for a specified destination node by its ID.
* If the destination node does not require predecessors, an empty list is returned.
* Resolves chains of predecessors for a specified destination chain by its ID.
* If the destination chain does not require predecessors, an empty list is returned.
* Otherwise, it constructs a list of chains, where each chain represents a sequence of predecessor nodes.
*
* @param destinationId The ID of the destination node whose predecessor chains need to be resolved.
* Must not be null and must correspond to an existing node.
* @param destinationId The ID of the destination chain whose predecessor chains need to be resolved.
* Must not be null and must correspond to an existing chain.
* @return A list of chains, where each chain is a list of nodes.
* Each list represents a sequence of predecessor nodes for the given destination node.
* If the destination node does not require predecessors, a list containing an empty list is returned.
* @throws RuntimeException If a predecessor node is not found for a given sequence number in the chain.
* Each list represents a sequence of predecessor nodes for the given destination chain.
* If the destination chain does not require predecessors, a list containing an empty list is returned.
* @throws RuntimeException If a predecessor chain is not found for a given sequence number in the chain.
*/
@Transactional
public List<List<Node>> resolveChainsById(Integer destinationId) {
public List<List<Node>> getChainsById(Integer destinationId) {
List<List<Node>> resolvedChains = new ArrayList<>();
/*
* First we collect all chains. If chain usage is not mandatory we also add
* an empty chain. So that the chains can be by-passed.
*/
Node destination = getById(destinationId).orElseThrow();
if (!destination.getPredecessorRequired())
@ -213,6 +218,11 @@ public class NodeRepository {
var currentChain = new ArrayList<Node>();
resolvedChains.add(currentChain);
/*
* Going through the key set of the chain's HashMap and putting
* the Nodes in order of the sequence numbers into a list.
*/
chain.keySet().forEach(sequenceNumber -> {
var predecessor = getById(chain.get(sequenceNumber));
if (predecessor.isEmpty()) {
@ -223,9 +233,12 @@ public class NodeRepository {
});
return resolvedChains;
}
@Transactional
public List<Node> getByDistance(Node node, Integer regionRadius) {
String query = """
@ -246,7 +259,6 @@ public class NodeRepository {
}
/**
* Retrieves a list of nodes that are outbound from a given country.
* The method considers nodes that are either explicitly mapped to the given country

View file

@ -251,7 +251,7 @@ public class PremiseRepository {
* @param userId The ID of the user who owns the premises.
* @param premiseId The ID of the premise being checked; this premise will be excluded from the results.
* @param materialId The material ID to verify for potential conflicts.
* @param supplierId The supplier node ID to verify for potential conflicts.
* @param supplierId The supplier chain ID to verify for potential conflicts.
* @return A list of premises in the DRAFT state with the same material and supplier combination (excluding the specified premise).
* @throws IllegalArgumentException If any of the provided parameters are null.
*/
@ -305,7 +305,7 @@ public class PremiseRepository {
private static final String BASE_JOIN_QUERY = """
FROM premise AS p
LEFT JOIN material as m ON p.material_id = m.id
LEFT JOIN node as n ON p.supplier_node_id = n.id
LEFT JOIN chain as n ON p.supplier_node_id = n.id
LEFT JOIN sys_user_node as user_n ON p.user_supplier_node_id = user_n.id
WHERE p.userId = ?""";
@ -382,7 +382,7 @@ public class PremiseRepository {
// Map material
entity.setMaterial(mapMaterial(rs));
// Map supplier (either regular node or user node)
// Map supplier (either regular chain or user chain)
mapSupplierProperties(entity, rs);
return entity;

View file

@ -101,6 +101,7 @@ public class PropertyRepository {
return jdbcTemplate.query(query, new PropertyMapper(), ValidityPeriodState.EXPIRED.name(), propertySetId);
}
@Transactional
public Optional<PropertyDTO> getPropertyByMappingId(SystemPropertyMappingId mappingId) {
String query = """
SELECT type.name as name, type.data_type as dataType, type.external_mapping_id as externalMappingId, type.validation_rule as validationRule,

View file

@ -2,15 +2,20 @@ package de.avatic.lcc.repositories.rates;
import de.avatic.lcc.model.rates.ContainerRate;
import de.avatic.lcc.model.rates.ContainerRateType;
import de.avatic.lcc.model.rates.ValidityPeriodState;
import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
import de.avatic.lcc.repositories.pagination.SearchQueryResult;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@Repository
public class ContainerRateRepository {
@ -40,7 +45,97 @@ public class ContainerRateRepository {
return jdbcTemplate.query(query, new ContainerRateMapper());
}
@Transactional
public List<ContainerRate> findRoutesByStartNodeIdAndDestinationCountryId(Integer startNodeId, List<Integer> destinationCountryIds) {
if (startNodeId == null || destinationCountryIds == null || destinationCountryIds.isEmpty()) {
return Collections.emptyList();
}
String destinationCountryPlaceholders = String.join(",", Collections.nCopies(destinationCountryIds.size(), "?"));
String query = """
SELECT container_rate.id AS id,
container_rate.validity_period_id AS validity_period_id,
container_rate.container_rate_type AS container_rate_type,
container_rate.from_node_id AS from_node_id,
container_rate.to_node_id AS to_node_id,
container_rate.rate_feu AS rate_feu,
container_rate.rate_teu AS rate_teu,
container_rate.rate_hc AS rate_hc,
container_rate.lead_time AS lead_time,
to_node.country_id as to_country_id,
from_node.country_id as from_country_id
FROM container_rate
LEFT JOIN node AS to_node ON to_node.id = container_rate.to_node_id
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 (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);
List<Object> params = new ArrayList<>();
params.add(ValidityPeriodState.VALID.name());
params.add(ContainerRateType.SEA.name());
params.add(ContainerRateType.RAIL.name());
params.add(startNodeId);
params.addAll(destinationCountryIds);
return jdbcTemplate.query(query, new ContainerRateMapper(true), params.toArray());
}
@Transactional
public List<ContainerRate> getPostRunsFor(ContainerRate mainRun) {
String query = """
SELECT container_rate.id AS id,
container_rate.validity_period_id AS validity_period_id,
container_rate.container_rate_type AS container_rate_type,
container_rate.from_node_id AS from_node_id,
container_rate.to_node_id AS to_node_id,
container_rate.rate_feu AS rate_feu,
container_rate.rate_teu AS rate_teu,
container_rate.rate_hc AS rate_hc,
container_rate.lead_time AS lead_time,
to_node.country_id as to_country_id,
from_node.country_id as from_country_id
FROM container_rate
LEFT JOIN node AS to_node ON to_node.id = container_rate.to_node_id
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 container_rate.from_node_id = ? AND container_rate.container_rate_type = ?""";
return jdbcTemplate.query(query, new ContainerRateMapper(true), ValidityPeriodState.VALID.name(), mainRun.getToNodeId(), ContainerRateType.POST_RUN.name());
}
public Optional<ContainerRate> findRoute(Integer fromNodeId, Integer toNodeId, ContainerRateType type) {
String query = """
SELECT * FROM container_rate WHERE from_node_id = ? AND to_node_id = ? AND container_rate_type = ?
""";
var route = jdbcTemplate.query(query, new ContainerRateMapper(), fromNodeId, toNodeId, type);
if(route.isEmpty())
return Optional.empty();
return Optional.of(route.getFirst());
}
private static class ContainerRateMapper implements RowMapper<ContainerRate> {
private final boolean fetchCountryIds;
public ContainerRateMapper(boolean fetchCountryIds) {
this.fetchCountryIds = fetchCountryIds;
}
public ContainerRateMapper() {
this(false);
}
@Override
public ContainerRate mapRow(ResultSet rs, int rowNum) throws SQLException {
var entity = new ContainerRate();
@ -49,12 +144,17 @@ public class ContainerRateRepository {
entity.setValidityPeriodId(rs.getInt("validity_period_id"));
entity.setFromNodeId(rs.getInt("from_node_id"));
entity.setToNodeId(rs.getInt("to_node_id"));
entity.setType(ContainerRateType.valueOf(rs.getString("type")));
entity.setType(ContainerRateType.valueOf(rs.getString("container_rate_type")));
entity.setLeadTime(rs.getInt("lead_time"));
entity.setRateFeu(rs.getBigDecimal("rate_feu"));
entity.setRateTeu(rs.getBigDecimal("rate_teu"));
entity.setRateHc(rs.getBigDecimal("rate_hc"));
if (fetchCountryIds) {
entity.setToCountryId(rs.getInt("to_country_id"));
entity.setFromCountryId(rs.getInt("from_country_id"));
}
return entity;
}
}

View file

@ -72,11 +72,13 @@ public class MatrixRateRepository {
return jdbcTemplate.queryForObject(query, new MatrixRateMapper(), id);
}
@Transactional
public List<MatrixRate> listAllRatesByPeriodId(Integer periodId) {
String query = "SELECT * FROM country_matrix_rate WHERE validity_period_id = ?";
return jdbcTemplate.query(query, new MatrixRateMapper());
}
@Transactional
public Optional<MatrixRate> getByCountryIds(Integer fromCountryId, Integer toCountryId) {
String query = "SELECT * FROM country_matrix_rate WHERE from_country_id = ? AND to_country_id = ?";
var rates = jdbcTemplate.query(query, new MatrixRateMapper(), fromCountryId, toCountryId);

View file

@ -0,0 +1,266 @@
package de.avatic.lcc.service.calculation;
import de.avatic.lcc.model.nodes.Node;
import de.avatic.lcc.repositories.NodeRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Service responsible for resolving and building chains of nodes.
* <p>
* This service class provides functionality to build all possible valid chains
* starting from a specified node ID. It handles the validation and processing
* of node chains, including detection of circular references.
* </p>
*
*/
@Service
public class ChainResolver {
private static final Logger log = LoggerFactory.getLogger(ChainResolver.class);
private final NodeRepository nodeRepository;
/**
* Constructs a new ChainResolver with the specified NodeRepository.
*
* @param nodeRepository The repository used to retrieve node chains
*/
public ChainResolver(NodeRepository nodeRepository) {
this.nodeRepository = nodeRepository;
}
/**
* Builds all valid chains starting from the specified node ID.
* <p>
* This method retrieves all possible chains starting from the given node ID and processes
* them to validate and build complete chains. It handles chain merging, validation, and
* circular reference detection during the process.
* </p>
*
* @param nodeId The ID of the starting node
* @return A list of valid node chains, each represented as a list of Nodes
*/
public List<List<Node>> buildChains(Integer nodeId) {
if (nodeId == null)
return Collections.emptyList();
List<List<Node>> foundChains = new ArrayList<>();
Stack<ChainValidationObject> chainStack = new Stack<>();
chainStack.addAll(nodeRepository.getChainsById(nodeId).stream().map(ChainValidationObject::new).toList());
while (!chainStack.isEmpty()) {
var validationObject = chainStack.pop();
if(!validationObject.checkCircularReference()) {
while (validationObject.hasNext()) {
Node currentNode = validationObject.getCurrentNode();
chainStack.addAll(validationObject.validateChains(nodeRepository.getChainsById(currentNode.getId())));
if (!validationObject.valid())
break;
validationObject.next();
}
if (validationObject.valid())
foundChains.add(validationObject.getChain());
} else {
log.warn("Circular reference detected while building predecessor chain for node {}", nodeId);
}
}
log.info("Found {} chains for node {}", foundChains.size(), nodeId);
return foundChains;
}
/**
* Helper class for chain validation and processing.
* <p>
* This inner class encapsulates the logic for validating node chains, handling
* chain merging, and detecting circular references. It maintains the state of
* chain processing including the current position and validity status.
* </p>
*/
private static class ChainValidationObject {
/** Current position in the chain being processed */
int chainPointer;
/** List of nodes representing the current chain */
List<Node> chain;
/** Flag indicating if the chain is valid */
private boolean chainValid;
/**
* Creates a new ChainValidationObject with the specified chain.
*
* @param chain The list of nodes representing the chain to validate
*/
public ChainValidationObject(List<Node> chain) {
this.chain = chain;
this.chainPointer = 0;
this.chainValid = true;
}
/**
* Creates a new ChainValidationObject with the specified chain and pointer position.
*
* @param chain The list of nodes representing the chain to validate
* @param chainPointer The starting position in the chain
*/
private ChainValidationObject(List<Node> chain, int chainPointer) {
this.chain = chain;
this.chainPointer = chainPointer;
this.chainValid = true;
}
/**
* Returns the current node in the chain based on the chainPointer.
*
* @return The current Node in the chain
*/
public Node getCurrentNode() {
return chain.get(chainPointer);
}
/**
* Validates the given foreign chains against the current chain.
* <p>
* This method checks if the foreign chains can be merged with the current chain
* starting from the current position. It handles various validation scenarios
* including short chains and matching node IDs.
* </p>
*
* @param foreignChains List of chains to validate against the current chain
* @return Collection of ChainValidationObjects representing valid merged chains
*/
public Collection<ChainValidationObject> validateChains(List<List<Node>> foreignChains) {
if (foreignChains == null || foreignChains.isEmpty())
return Collections.emptyList();
var candidates = new ArrayList<>(foreignChains);
var nextCandidates = new ArrayList<List<Node>>();
boolean shortChainFound = false;
int foreignIdx = 0;
for (int localIdx = chainPointer + 1; localIdx < chain.size(); localIdx++, foreignIdx++) {
var localNode = chain.get(localIdx);
nextCandidates.clear();
for (var candidate : candidates) {
if (candidate.size() == foreignIdx)
shortChainFound = true;
else {
Node foreignNode = candidate.get(foreignIdx);
if (Objects.equals(localNode.getId(), foreignNode.getId())) {
nextCandidates.add(candidate);
}
}
}
candidates.clear();
candidates.addAll(nextCandidates);
if (candidates.isEmpty()) {
if (!shortChainFound)
this.chainValid = false;
return Collections.emptyList();
}
}
return mergeCandidates(candidates, foreignIdx);
}
/**
* Merges the current chain with candidate chains at the specified index.
* <p>
* Creates new ChainValidationObjects by merging the current chain with
* each candidate chain, starting from the specified index in the candidate chains.
* </p>
*
* @param candidates List of candidate chains to merge with
* @param onIndex The index in the candidate chains to start merging from
* @return Collection of ChainValidationObjects representing the merged chains
*/
private Collection<ChainValidationObject> mergeCandidates(List<List<Node>> candidates, int onIndex) {
var mergedCandidates = new ArrayList<ChainValidationObject>();
for (var candidate : candidates) {
List<Node> mergedChain = new ArrayList<>(chain);
mergedChain.addAll(candidate.subList(onIndex, candidate.size()));
mergedCandidates.add(new ChainValidationObject(mergedChain, onIndex));
}
return mergedCandidates;
}
/**
* Checks if there are more nodes in the chain after the current position.
*
* @return true if there are more nodes in the chain, false otherwise
*/
public boolean hasNext() {
return chainPointer < chain.size() - 1;
}
/**
* Advances the chain pointer to the next position.
*/
public void next() {
chainPointer++;
}
/**
* Returns the validity status of the chain.
*
* @return true if the chain is valid, false otherwise
*/
public boolean valid() {
return chainValid;
}
/**
* Returns the current chain of nodes.
*
* @return List of Nodes representing the current chain
*/
public List<Node> getChain() {
return chain;
}
/**
* Checks if the chain contains circular references.
* <p>
* A circular reference occurs when the same node ID appears multiple times
* within the chain.
* </p>
*
* @return true if a circular reference is detected, false otherwise
*/
public boolean checkCircularReference() {
/* check for circular references */
return (chain.stream().map(Node::getId)
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet().stream()
.noneMatch(entry -> entry.getValue() > 1));
}
}
}

View file

@ -0,0 +1,56 @@
package de.avatic.lcc.service.calculation;
import de.avatic.lcc.model.country.DetourIndex;
import de.avatic.lcc.model.nodes.Node;
import de.avatic.lcc.repositories.DistanceMatrixRepository;
import de.avatic.lcc.repositories.country.CountryRepository;
import org.springframework.stereotype.Service;
@Service
public class DistanceService {
private static final double EARTH_RADIUS = 6371.0;
private final DistanceMatrixRepository distanceMatrixRepository;
private final CountryRepository countryRepository;
public DistanceService(DistanceMatrixRepository distanceMatrixRepository, CountryRepository countryRepository) {
this.distanceMatrixRepository = distanceMatrixRepository;
this.countryRepository = countryRepository;
}
public double getDistance(Node src, Node dest, boolean fast) {
if (fast) return getDistanceFast(src, dest);
var distance = distanceMatrixRepository.getDistance(src, dest);
// TODO do a api call if empty!.
return distance.map(value -> value.getDistance().intValue()).orElse(0);
}
private double getDistanceFast(Node src, Node dest) {
double srcLatitudeRadians = Math.toRadians(src.getGeoLat().doubleValue());
double srcLongitudeRad = Math.toRadians(src.getGeoLng().doubleValue());
double destLatitudeRadians = Math.toRadians(dest.getGeoLat().doubleValue());
double destLongitudeRadians = Math.toRadians(dest.getGeoLng().doubleValue());
double latitudeDifference = destLatitudeRadians - srcLatitudeRadians;
double longitudeDifference = destLongitudeRadians - srcLongitudeRad;
double a = Math.sin(latitudeDifference / 2) * Math.sin(latitudeDifference / 2) + Math.cos(srcLatitudeRadians) * Math.cos(destLatitudeRadians) * Math.sin(longitudeDifference / 2) * Math.sin(longitudeDifference / 2);
return (getDetourIndex(src, dest) * (EARTH_RADIUS * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))));
}
private double getDetourIndex(Node src, Node dest) {
DetourIndex srcDetour = DetourIndex.getDetourIndex(countryRepository.getById(src.getCountryId()).orElseThrow().getIsoCode());
if (src.getCountryId().equals(dest.getCountryId())) return srcDetour.getAverageDetourIndex();
DetourIndex destDetour = DetourIndex.getDetourIndex(countryRepository.getById(dest.getCountryId()).orElseThrow().getIsoCode());
return (srcDetour.getAverageDetourIndex() + destDetour.getAverageDetourIndex()) / 2;
}
}

View file

@ -1,59 +1,277 @@
package de.avatic.lcc.service.calculation;
import de.avatic.lcc.model.nodes.Node;
import de.avatic.lcc.model.premises.route.Route;
import de.avatic.lcc.model.premises.route.RouteInformation;
import de.avatic.lcc.model.premises.route.RouteNode;
import de.avatic.lcc.model.premises.route.RouteSection;
import de.avatic.lcc.model.properties.SystemPropertyMappingId;
import de.avatic.lcc.model.rates.ContainerRate;
import de.avatic.lcc.model.rates.ContainerRateType;
import de.avatic.lcc.model.rates.MatrixRate;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.repositories.country.CountryRepository;
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 org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.math.BigDecimal;
import java.util.*;
@Service
public class RoutingService {
private final NodeRepository nodeRepository;
private final CountryRepository countryRepository;
private final MatrixRateRepository matrixRateRepository;
private final PropertyRepository propertyRepository;
private final ChainResolver chainResolver;
private final ContainerRateRepository containerRateRepository;
private final DistanceService distanceService;
private final UserNodeRepository userNodeRepository;
public RoutingService(NodeRepository nodeRepository, CountryRepository countryRepository, MatrixRateRepository matrixRateRepository, PropertyRepository propertyRepository) {
public RoutingService(NodeRepository nodeRepository, MatrixRateRepository matrixRateRepository, PropertyRepository propertyRepository, ChainResolver chainResolver, ContainerRateRepository containerRateRepository, DistanceService distanceService, UserNodeRepository userNodeRepository) {
this.nodeRepository = nodeRepository;
this.countryRepository = countryRepository;
this.matrixRateRepository = matrixRateRepository;
this.propertyRepository = propertyRepository;
this.chainResolver = chainResolver;
this.containerRateRepository = containerRateRepository;
this.distanceService = distanceService;
this.userNodeRepository = userNodeRepository;
}
public void findRoutes(Integer destinationId, Integer sourceId) {
var foundRoutes = new ArrayList<FoundRoute>();
public List<RouteInformation> findRoutes(Integer destinationId, Integer sourceId, boolean isUserNode) {
List<RouteInformationObject> rios = new ArrayList<>();
Node source = nodeRepository.getById(sourceId).orElseThrow();
/*
* 1. STEP:
* Get the source and destination node from database.
* Check if there is a matrix rate for the source country.
*/
Node source = (isUserNode) ? userNodeRepository.getById(sourceId).orElseThrow() : nodeRepository.getById(sourceId).orElseThrow();
Node destination = nodeRepository.getById(destinationId).orElseThrow();
List<Node> regionNodes = nodeRepository.getByDistance(source, getRegionRadius());
Optional<MatrixRate> sourceMatrixRate = matrixRateRepository.getByCountryIds(source.getCountryId(), source.getCountryId());
/*
* 2. STEP:
* Generate recursive all chains starting with the destination node.
* This means all chains within the last node of a chain are connected to the
* existing chain.
*
* Furthermore, it is evaluated that all nodes within the chain do not have chains
* themselves that are in conflict with the chain.
*
* Then get all countries from the end of the destination chains.
*/
List<List<Node>> destinationChains = chainResolver.buildChains(destinationId);
List<Integer> inboundCountries = destinationChains.stream().filter(chain -> !chain.isEmpty()).map(chain -> chain.getLast().getCountryId()).distinct().toList();
/*
* 3. STEP:
* Get all outbound nodes for the country of the source node. In this first step this includes:
* - all intermediate nodes that have the same country id.
* - all nodes that are explicitly mapped as outbound node for this country id.
*
* Then find all outbound nodes that have main transports to the inbound countries found in the step before
*
* Create a route information object for each outbound node that have a main transport, also add all post-runs
* that can be used for the main run.
*
* Lastly mapping the destination chain on the route information objects, based on there compatibility:
*
* - last and second-to-last node in the destination chain are identical with the end of the main run and the
* end of the post-run. -> High Quality.
* - last in the destination chain are identical with the
* end of the post-run. -> Medium Quality
* - last in the destination chain in the same country as the
* end of the post-run. -> Low Quality
*
*/
List<Node> outboundNodes = nodeRepository.getAllOutboundFor(source.getCountryId());
List<ContainerRate> mainRuns = containerRateRepository.findRoutesByStartNodeIdAndDestinationCountryId(outboundNodes.stream().map(Node::getId).toList(), inboundCountries);
List<List<Node>> destinationChains = resolveChains(destinationId);
Map<Integer, List<List<Node>>> outboundChains = getOutboundChains(outboundNodes);
mainRuns.forEach(mainRun -> rios.add(new RouteInformationObject(destination, source, mainRun, nodeRepository.getById(mainRun.getToNodeId()).orElseThrow(), outboundNodes.stream().filter(n -> n.getId().equals(mainRun.getFromNodeId())).findFirst().orElseThrow(), containerRateRepository.getPostRunsFor(mainRun))));
addDestinationChains(rios, destinationChains);
/*
* 4. STEP:
* Next we are resolving the predecessor chains of all outbound nodes recursively in exact the
* same way the destination chains were resolved.
*/
getSourceCountryChains(rios);
/*
* 5. STEP:
* Try to connect the destination chains directly with the source node with a
* matrix rate. All routes that are constructed in this way are getting its own
* route information object and added to the list.
*/
rios.addAll(findRoutesWithDestinationChainsAndSource(destinationChains, source, destination));
/*
* 6. STEP:
* Final step is to check if there are actual rates for the pre-run and post-run
* chains, and filter out all without rates.
*
* pre-run chains also needs to be checked if they are connectable to the supplier,
* all unconnectable chains are thrown away.
*
*
*
*/
Set<ChainSectionContainerRateInformation> containerRates = new HashSet<>();
Set<ChainSectionMatrixRateInformation> matrixRates = new HashSet<>();
List<Node> nearByNodes = nodeRepository.getByDistance(source, getRegionRadius());
List<RouteInformation> routeInformationList = new ArrayList<>();
for (var rio : rios) {
foundRoutes.addAll(constructRoutesWithChains(destinationChains, source, destination));
processDestinationChains(rio, containerRates, matrixRates);
processSourceCountryChains(rio, containerRates, matrixRates, sourceMatrixRate.orElse(null), nearByNodes, source);
rio.setSource(source, isUserNode);
routeInformationList.add(rio.getRouteInformation(containerRates, matrixRates));
}
private Map<Integer, List<List<Node>>> getOutboundChains(List<Node> outboundNodes) {
Map<Integer, List<List<Node>>> outboundChains = new HashMap<>();
for(var outboundNode : outboundNodes) {
outboundChains.put(outboundNode.getId(), resolveChains(outboundNode.getId()));
return routeInformationList;
}
private boolean nodesConnectable(Node startNode, Node endNode, ChainInformationObject chain, Set<ChainSectionContainerRateInformation> containerRates, Set<ChainSectionMatrixRateInformation> matrixRates) {
var containerSection = new ChainSectionContainerRateInformation(startNode, endNode);
var matrixSection = new ChainSectionMatrixRateInformation(startNode.getCountryId(), endNode.getCountryId());
if (!containerRates.contains(containerSection) && !matrixRates.contains(matrixSection)) {
Optional<ContainerRate> rate = containerRateRepository.findRoute(startNode.getId(), endNode.getId(), ContainerRateType.ROAD);
if (rate.isPresent()) {
containerSection.setRate(rate.get());
containerRates.add(containerSection);
chain.addSectionInformation(new ChainSectionInformation(startNode, endNode, containerSection));
} else if (!matrixRates.contains(matrixSection)) {
Optional<MatrixRate> matrixRate = matrixRateRepository.getByCountryIds(startNode.getCountryId(), endNode.getCountryId());
if (matrixRate.isPresent()) {
matrixSection.setRate(matrixRate.get());
matrixSection.setDistance(distanceService.getDistance(startNode, endNode, true));
matrixRates.add(matrixSection);
chain.addSectionInformation(new ChainSectionInformation(startNode, endNode, containerSection));
} else {
return false;
}
}
}
return true;
}
private void processSourceCountryChains(RouteInformationObject rio, Set<ChainSectionContainerRateInformation> containerRates, Set<ChainSectionMatrixRateInformation> matrixRates, MatrixRate sourceMatrixRate, List<Node> nearByNodes, Node source) {
for (var chain : rio.getSourceCountryChains()) {
boolean directly = false;
Node nearByNode = null;
boolean useSourceMatrixRate = false;
// 1. try connect chain end to source node directly
if (!nodesConnectable(source, chain.getChain().getLast(), chain, containerRates, matrixRates)) {
// 2. try to connect via matrix rate
useSourceMatrixRate = sourceMatrixRate != null && chain.getChain().getLast().getCountryId().equals(sourceMatrixRate.getFromCountry());
if (!useSourceMatrixRate) {
// try to connect via near by nodes (metropolitan).
nearByNode = nearByNodes.stream().filter(n -> nodesConnectable(n, chain.getChain().getLast(), chain, containerRates, matrixRates)).findFirst().orElse(null);
if (null != nearByNode) {
chain.addNearByNode(nearByNode);
}
}
} else directly = true;
if (nearByNode != null || useSourceMatrixRate || directly) {
for (int idx = chain.getChain().size() - 1; idx > 0; idx--) {
Node startNode = chain.getChain().get(idx);
Node endNode = chain.getChain().get(idx - 1);
if (!nodesConnectable(startNode, endNode, chain, containerRates, matrixRates))
rio.removeSourceChain(chain);
}
} else rio.removeSourceChain(chain);
}
}
private void processDestinationChains(RouteInformationObject rio, Set<ChainSectionContainerRateInformation> containerRates, Set<ChainSectionMatrixRateInformation> matrixRates) {
List<ChainInformationObject> chains = rio.getDestinationChains();
for (var chain : chains) {
for (int idx = chain.getChain().size() - 1; idx > chain.getQuality().getStartIdx(); idx--) {
Node startNode = chain.getChain().get(idx);
Node endNode = chain.getChain().get(idx - 1);
if (!nodesConnectable(startNode, endNode, chain, containerRates, matrixRates))
rio.removeDestinationChain(chain);
}
}
}
/**
* Adds destination chains to the given list of RouteInformationObject instances based
* on their connection properties and quality. The method iterates through each RouteInformationObject
* and assesses its post-run connections with possible destination chains, adding the chains with an
* associated connection quality determined by their compatibility.
*
* @param rios The list of RouteInformationObject instances to which the destination chains
* will be added. Each RouteInformationObject contains post-run connection details.
* @param destinationChains A list of destination chains, where each chain is represented
* as a list of Node objects. These chains are evaluated for
* addition to the RouteInformationObjects based on connection compatibility.
*/
private void addDestinationChains(List<RouteInformationObject> rios, List<List<Node>> destinationChains) {
for (var rio : rios) {
for (var chain : destinationChains) {
for (var postRun : rio.getPostRuns()) {
if (chain.getLast().getId().equals(postRun.getToNodeId())) {
rio.addDestinationChain(new ChainInformationObject(chain, ChainConnectionQuality.MEDIUM));
} else if (chain.getLast().getId().equals(postRun.getFromNodeId()) && chain.get(chain.size() - 2).getId().equals(postRun.getToNodeId())) {
rio.addDestinationChain(new ChainInformationObject(chain, ChainConnectionQuality.HIGH));
} else if (chain.getLast().getCountryId().equals(postRun.getToCountryId())) {
rio.addDestinationChain(new ChainInformationObject(chain, ChainConnectionQuality.LOW));
}
}
}
}
}
private void getSourceCountryChains(List<RouteInformationObject> rios) {
Map<Integer, List<ChainInformationObject>> sourceCountryChains = new HashMap<>();
for (var rio : rios) {
Integer outboundNodeId = rio.getOutboundNode().getId();
List<ChainInformationObject> chains = null;
if (sourceCountryChains.containsKey(outboundNodeId)) {
chains = sourceCountryChains.get(outboundNodeId);
} else {
chains = chainResolver.buildChains(outboundNodeId).stream().map(ChainInformationObject::new).toList();
sourceCountryChains.put(outboundNodeId, chains);
}
rio.setSourceCountryChains(chains);
}
return outboundChains;
}
private Integer getRegionRadius() {
@ -61,73 +279,389 @@ public class RoutingService {
return property.map(propertyDTO -> Integer.valueOf(propertyDTO.getCurrentValue())).orElseGet(SystemPropertyMappingId.RADIUS_REGION::getDefaultAsInteger);
}
private ArrayList<FoundRoute> constructRoutesWithChains(List<List<Node>> chains, Node source, Node destination) {
ArrayList<FoundRoute> foundRoutes = new ArrayList<>();
private ArrayList<RouteInformationObject> findRoutesWithDestinationChainsAndSource(List<List<Node>> chains, Node source, Node destination) {
ArrayList<RouteInformationObject> foundRoutes = new ArrayList<>();
HashMap<Integer, MatrixRate> matrixRates = new HashMap<>();
for (var chain : chains) {
var destinationCountryId = destination.getCountryId();
MatrixRate matrixRate = null;
if (!chain.isEmpty()) {
var matrixRate = matrixRateRepository.getByCountryIds(chain.getLast().getCountryId(), source.getCountryId());
if(matrixRate.isPresent()) {
foundRoutes.add(new FoundRoute(chain, destination, source));
destinationCountryId = chain.getLast().getCountryId();
}
if (!matrixRates.containsKey(destinationCountryId)) {
matrixRate = matrixRateRepository.getByCountryIds(source.getCountryId(), destinationCountryId).orElse(null);
matrixRates.put(destinationCountryId, matrixRate);
}
}
for (var destinationCountryId : matrixRates.keySet()) {
var matrixRate = matrixRates.get(destinationCountryId);
var destinationCountryChains = chains.stream().filter(chain -> chain.getLast().getCountryId().equals(destinationCountryId)).map(ChainInformationObject::new).toList();
foundRoutes.add(new RouteInformationObject(destination, source, matrixRate, destinationCountryChains));
}
return foundRoutes;
}
/**
* Resolves all possible chains of nodes starting from the specified node ID.
* This method recursively retrieves predecessor chains and appends successor chains
* to form complete resolution paths.
*
* @param nodeId The ID of the node from which the resolution process starts.
* Must not be null and must correspond to an existing node.
* @return A list of lists, where each inner list represents a fully resolved chain
* of nodes including predecessors and successors for the specified node ID.
*/
private List<List<Node>> resolveChains(Integer nodeId) {
var resolvedChains = new ArrayList<List<Node>>();
private enum ChainConnectionQuality {
HIGH(1), MEDIUM(0), LOW(0), FALLBACK(0);
var predecessorChains = nodeRepository.resolveChainsById(nodeId);
for (var predecessorChain : predecessorChains) {
if (!predecessorChain.isEmpty()) {
var successorChains = resolveChains(predecessorChain.getLast().getId());
successorChains.forEach(successorChain -> {
successorChain.addAll(0, predecessorChain);
resolvedChains.add(successorChain);
});
private final int startIdx;
ChainConnectionQuality(int startIdx) {
this.startIdx = startIdx;
}
else {
resolvedChains.add(predecessorChain);
public int getStartIdx() {
return startIdx;
}
}
return resolvedChains;
private interface ChainSectionRate {
BigDecimal getCost();
int getLeadTime();
}
private static class FoundRoute {
private List<Node> nodes;
private static class ChainInformationObject {
private final ChainConnectionQuality quality;
private final List<Node> chain;
private final Collection<ChainSectionInformation> sections;
private Node nearByNode;
public ChainInformationObject() {
this.chain = new ArrayList<>();
this.nearByNode = null;
this.sections = new ArrayList<>();
this.quality = ChainConnectionQuality.FALLBACK;
}
public ChainInformationObject(List<Node> chain, ChainConnectionQuality quality) {
this.chain = chain;
this.nearByNode = null;
this.sections = new ArrayList<>();
this.quality = quality;
}
public ChainInformationObject(List<Node> chain) {
this.chain = chain;
this.nearByNode = null;
this.sections = new ArrayList<>();
this.quality = ChainConnectionQuality.FALLBACK;
}
public List<Node> getChain() {
return chain;
}
public void addNearByNode(Node nearByNode) {
this.nearByNode = nearByNode;
}
public void addSectionInformation(ChainSectionInformation section) {
this.sections.add(section);
}
public double getCost() {
return sections.stream().mapToDouble(ChainSectionInformation::getCost).sum();
}
public int getLeadTime() {
return sections.stream().mapToInt(ChainSectionInformation::getLeadTime).sum();
}
public ChainConnectionQuality getQuality() {
return quality;
}
public Collection<ChainSectionInformation> getSections() {
return sections;
}
}
private static class ChainSectionInformation {
Node fromNode;
Node toNode;
ChainSectionRate rate;
public ChainSectionInformation(Node fromNode, Node toNode, ChainSectionRate rate) {
this.fromNode = fromNode;
this.toNode = toNode;
this.rate = rate;
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
ChainSectionInformation that = (ChainSectionInformation) o;
return Objects.equals(fromNode, that.fromNode) && Objects.equals(toNode, that.toNode);
}
@Override
public int hashCode() {
return Objects.hash(fromNode, toNode);
}
public double getCost() {
return rate.getCost().doubleValue();
}
public int getLeadTime() {
return rate.getLeadTime();
}
}
private static class ChainSectionContainerRateInformation implements ChainSectionRate {
Node fromNode;
Node toNode;
ContainerRate rate;
public ChainSectionContainerRateInformation(Node fromNode, Node toNode) {
this.fromNode = fromNode;
this.toNode = toNode;
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
ChainSectionContainerRateInformation that = (ChainSectionContainerRateInformation) o;
return Objects.equals(fromNode.getId(), that.fromNode.getId()) && Objects.equals(toNode.getId(), that.toNode.getId());
}
@Override
public int hashCode() {
return Objects.hash(fromNode.getId(), toNode.getId());
}
public void setRate(ContainerRate containerRate) {
this.rate = containerRate;
}
@Override
public BigDecimal getCost() {
return rate.getRateFeu();
}
@Override
public int getLeadTime() {
return rate.getLeadTime();
}
}
private static class ChainSectionMatrixRateInformation implements ChainSectionRate {
Integer fromCountryId;
Integer toCountryId;
MatrixRate rate;
private int distance;
public ChainSectionMatrixRateInformation(Integer fromCountryId, Integer toCountryId) {
this.fromCountryId = fromCountryId;
this.toCountryId = toCountryId;
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
ChainSectionMatrixRateInformation that = (ChainSectionMatrixRateInformation) o;
return Objects.equals(fromCountryId, that.fromCountryId) && Objects.equals(toCountryId, that.toCountryId);
}
@Override
public int hashCode() {
return Objects.hash(fromCountryId, toCountryId);
}
public void setRate(MatrixRate matrixRate) {
this.rate = matrixRate;
}
void setDistance(int distance) {
this.distance = distance;
}
@Override
public BigDecimal getCost() {
return rate.getRate().multiply(BigDecimal.valueOf(distance));
}
@Override
public int getLeadTime() {
return 3;
}
}
private static class RouteInformationObject {
/**
* Constructs a FoundRoute object that represents a route consisting of a chain of nodes,
* starting with the source node, followed by the reversed chain of nodes, and ending with the destination node.
*
* @param chain The list of intermediary nodes in the route, in original order before reversing.
* Must not be null.
* @param destination The destination node in the route. Must not be null.
* @param source The source node in the route. Must not be null.
* If a destination chain can be connected directly with a matrix rate to the source node.
* The matrix rate is stored in matrixRate. The destinationChains only contains one chain in this case.
* mainRun, outboundNode and inboundNode are null then and postRuns is empty.
*/
public FoundRoute(List<Node> chain, Node destination, Node source) {
this.nodes = new ArrayList<>();
this.nodes.add(source);
this.nodes.addAll(chain.reversed());
this.nodes.add(destination);
private final MatrixRate matrixRate;
/**
* If a destination chain cannot be connected directly with a matrix rate to the source node.
* The matrix rate is null and mainRun and postRuns are set.
*/
private final ContainerRate mainRun;
private final List<ContainerRate> postRuns;
private Node destinationNode;
private Node sourceNode;
private boolean isUserNode;
private Node outboundNode;
private Node inboundNode;
private List<ChainInformationObject> destinationChains = new ArrayList<>();
private List<ChainInformationObject> sourceCountryChains;
public RouteInformationObject(Node destination, Node source, ContainerRate mainRun, Node outboundNode, Node inboundNode, List<ContainerRate> postRuns) {
this.mainRun = mainRun;
this.outboundNode = outboundNode;
this.inboundNode = inboundNode;
this.postRuns = postRuns;
this.matrixRate = null;
this.sourceNode = source;
this.destinationNode = destination;
}
public RouteInformationObject(Node destination, Node source, MatrixRate matrixRate, List<ChainInformationObject> destinationCountryChains) {
this.matrixRate = matrixRate;
this.mainRun = null;
this.outboundNode = null;
this.postRuns = Collections.emptyList();
this.destinationChains.addAll(destinationCountryChains);
this.sourceCountryChains = Collections.singletonList(new ChainInformationObject());
this.destinationNode = destination;
this.sourceNode = source;
}
public List<ContainerRate> getPostRuns() {
return postRuns;
}
public void addDestinationChain(ChainInformationObject chain) {
destinationChains.add(chain);
}
public Node getOutboundNode() {
return outboundNode;
}
public List<ChainInformationObject> getDestinationChains() {
return destinationChains;
}
public List<ChainInformationObject> getSourceCountryChains() {
return sourceCountryChains;
}
public void setSourceCountryChains(List<ChainInformationObject> sourceCountryChains) {
this.sourceCountryChains = sourceCountryChains;
}
public void removeDestinationChain(ChainInformationObject chain) {
destinationChains.remove(chain);
}
public void removeSourceChain(ChainInformationObject chain) {
sourceCountryChains.remove(chain);
}
public RouteInformation getRouteInformation(Set<ChainSectionContainerRateInformation> containerRates, Set<ChainSectionMatrixRateInformation> matrixRates) {
var destinationChain = destinationChains.stream().min(Comparator.comparing(ChainInformationObject::getCost));
if (destinationChain.isEmpty()) return null;
var nodes = new ArrayList<RouteNode>();
var sections = new ArrayList<RouteSection>();
RouteInformation routeInformation = new RouteInformation();
routeInformation.setRoute(new Route());
routeInformation.setRouteNodes(nodes);
routeInformation.setRouteSections(sections);
if (matrixRate == null) {
var sourceChain = sourceCountryChains.stream().min(Comparator.comparing(ChainInformationObject::getCost));
if (sourceChain.isEmpty()) return null;
nodes.add(mapNode(sourceNode, isUserNode));
nodes.addAll(sourceChain.get().getChain().reversed().stream().map(n -> mapNode(n, false)).toList());
nodes.add(mapNode(outboundNode, false));
nodes.add(mapNode(inboundNode, false));
sections.add(new ChainSectionInformation(sourceChain.get().getChain().getFirst(), sourceNode, ))
sections.addAll(sourceChain.get().getSections().stream().map(this::mapSection).toList());
}
nodes.addAll(destinationChain.get().getChain().stream().map(n -> mapNode(n, false)).toList());
return routeInformation;
}
private RouteSection mapSection(ChainSectionInformation s) {
RouteSection section = new RouteSection();
section.set
return section;
}
private RouteNode mapNode(Node node, boolean isUserNode) {
RouteNode routeNode = new RouteNode();
routeNode.setNodeId(!isUserNode ? node.getId() : null);
routeNode.setUserNodeId(isUserNode ? node.getId() : null);
routeNode.setCountryId(node.getCountryId());
routeNode.setGeoLat(node.getGeoLat());
routeNode.setGeoLng(node.getGeoLng());
routeNode.setName(node.getName());
routeNode.setAddress(node.getAddress());
routeNode.setOutdated(node.getDeprecated());
routeNode.setIntermediate(false);
routeNode.setDestination(false);
routeNode.setSource(false);
return routeNode;
}
public void setSource(Node node, boolean isUserNode) {
this.sourceNode = node;
this.isUserNode = isUserNode;
}
public ContainerRate getMainRun() {
return mainRun;
}
public void setInboundNode(Node node) {
this.inboundNode = node;
}
}

View file

@ -0,0 +1,796 @@
package de.avatic.lcc.service.calculation;
import de.avatic.lcc.dto.generic.RouteType;
import de.avatic.lcc.model.nodes.Node;
import de.avatic.lcc.model.premises.route.Route;
import de.avatic.lcc.model.premises.route.RouteInformation;
import de.avatic.lcc.model.premises.route.RouteNode;
import de.avatic.lcc.model.premises.route.RouteSection;
import de.avatic.lcc.model.properties.SystemPropertyMappingId;
import de.avatic.lcc.model.rates.ContainerRate;
import de.avatic.lcc.model.rates.ContainerRateType;
import de.avatic.lcc.model.rates.MatrixRate;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.repositories.properties.PropertyRepository;
import de.avatic.lcc.repositories.rates.ContainerRateRepository;
import de.avatic.lcc.repositories.rates.MatrixRateRepository;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class RoutingService2 {
private final MatrixRateRepository matrixRateRepository;
private final ChainResolver chainResolver;
private final NodeRepository nodeRepository;
private final ContainerRateRepository containerRateRepository;
private final DistanceService distanceService;
private final PropertyRepository propertyRepository;
public RoutingService2(MatrixRateRepository matrixRateRepository, ChainResolver chainResolver, NodeRepository nodeRepository, ContainerRateRepository containerRateRepository, DistanceService distanceService, PropertyRepository propertyRepository) {
this.matrixRateRepository = matrixRateRepository;
this.chainResolver = chainResolver;
this.nodeRepository = nodeRepository;
this.containerRateRepository = containerRateRepository;
this.distanceService = distanceService;
this.propertyRepository = propertyRepository;
}
public List<RouteInformation> findRoute(Node destination, Node source, boolean sourceIsUserNode) {
List<RouteInformation> routeInformation = new ArrayList<>();
TemporaryContainer container = new TemporaryContainer(source, destination);
/*
* Get the source and destination node from database.
* Check if there is a matrix rate for the source country.
*/
matrixRateRepository.getByCountryIds(source.getCountryId(), source.getCountryId()).ifPresent(container::setSourceMatrixRate);
/*
* Generate recursive all chains starting with the destination node.
* This means all chains within the last node of a chain are connected to the
* existing chain.
*
* Furthermore, it is evaluated that all nodes within the chain do not have chains
* themselves that are in conflict with the chain.
*
* Then get all countries from the end of the destination chains.
*/
List<List<Node>> destinationChains = chainResolver.buildChains(destination.getId());
List<Integer> inboundCountries = destinationChains.stream().filter(chain -> !chain.isEmpty()).map(chain -> chain.getLast().getCountryId()).distinct().toList();
/*
* Get all outbound nodes for the country of the source node. In this first step this includes:
* - all intermediate nodes that have the same country id.
* - all nodes that are explicitly mapped as outbound node for this country id.
*/
List<Node> outboundNodes = nodeRepository.getAllOutboundFor(source.getCountryId());
/*
* Find main runs based on the outbound nodes and the inbound countries found before.
* Find post-runs for the main runs.
*
* Store all information in the TemporaryContainer object.
*/
container.setMainRuns(outboundNodes.stream().collect(Collectors.toMap(Node::getId, n -> containerRateRepository.findRoutesByStartNodeIdAndDestinationCountryId(n.getId(), inboundCountries))));
container.setPostRuns(container.getMainRuns().stream().collect(Collectors.toMap(ContainerRate::getId, containerRateRepository::getPostRunsFor)));
connectDestinationChainAndMainRun(container, outboundNodes, destinationChains);
connectSourceChainAndSource(container);
/*
* At this point all routes with a main run are created.
* We find now the best route per main run and throw away the rest.
*/
findCheapestPerMainRun(container);
/*
* Now we also create routes without main run (matrix rate)
*/
connectDestinationChainDirectly(container, destinationChains);
/*
* finally find and mark the fastest and the cheapest route.
*/
findAndMarkCheapestAndFastest(container);
/*
* Convert to Database model
*/
for (var route : container.getRoutes()) {
RouteInformation routeInformationObj = new RouteInformation();
routeInformationObj.setRoute(mapRoute(route));
routeInformationObj.setRouteSections(mapSections(route.getSections()));
routeInformationObj.setRouteNodes(route.getNodes().stream().map(n -> mapNode(n, n.getId().equals(source.getId()) && sourceIsUserNode)).toList());
routeInformation.add(routeInformationObj);
}
return routeInformation;
}
private RouteNode mapNode(Node node, boolean isUserNode) {
RouteNode routeNode = new RouteNode();
routeNode.setName(node.getName());
routeNode.setCountryId(node.getCountryId());
routeNode.setAddress(node.getAddress());
routeNode.setGeoLng(node.getGeoLng());
routeNode.setGeoLat(node.getGeoLat());
routeNode.setUserNodeId(isUserNode? node.getId() : null);
routeNode.setNodeId(isUserNode ? null : node.getId());
routeNode.setIntermediate(node.getIntermediate() != null ? node.getIntermediate() : false);
routeNode.setDestination(node.getDestination() != null ? node.getIntermediate() : false);
routeNode.setSource(node.getSource() != null ? node.getIntermediate() : false);
routeNode.setOutdated(node.getDeprecated());
return routeNode;
}
private Route mapRoute(TemporaryRouteObject route) {
Route routeObj = new Route();
routeObj.setCheapest(route.isCheapest());
routeObj.setFastest(route.isFastest());
return routeObj;
}
private List<RouteSection> mapSections(List<TemporaryRateObject> sections) {
int index = 1;
List<RouteSection> routeSections = new ArrayList<>();
boolean passedMainRun = false;
for(var section : sections) {
var routeSection = mapSection(section);
if (!section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.MAIN_RUN) && !section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.POST_RUN)) {
if (!passedMainRun)
routeSection.setPreRun(true);
if (passedMainRun)
routeSection.setPostRun(true);
}
if(section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.MAIN_RUN) || section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.POST_RUN))
passedMainRun = true;
routeSection.setListPosition(index++);
routeSections.add(routeSection);
}
return routeSections;
}
private RouteSection mapSection(TemporaryRateObject section) {
RouteSection routeSection = new RouteSection();
routeSection.setTransportType(mapRouteType(section));
routeSection.setFromRouteNodeId(section.getFromNode().getId());
routeSection.setToRouteNodeId(section.getToNode().getId());
routeSection.setMainRun(section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.MAIN_RUN));
routeSection.setOutdated(false);
routeSection.setPostRun(false);
routeSection.setPreRun(false);
return routeSection;
}
private RouteType mapRouteType(TemporaryRateObject rate) {
switch(rate.getType()) {
case MATRIX, CONTAINER -> {
return RouteType.ROAD;
}
case POST_RUN -> {
return RouteType.POST_RUN;
}
case MAIN_RUN -> {
return RouteType.valueOf(rate.getContainerRateTye().name());
}
}
return null;
}
private void findAndMarkCheapestAndFastest(TemporaryContainer container) {
var routes = container.getRoutes();
TemporaryRouteObject cheapest = null;
double cheapestCost = Double.MAX_VALUE;
TemporaryRouteObject fastest = null;
double fastestRoute = Double.MAX_VALUE;
for(var route : routes) {
double routeCost = route.getCost();
int leadTime = route.getLeadTime();
if(routeCost < cheapestCost) {
cheapestCost = routeCost;
cheapest = route;
}
if(leadTime < fastestRoute) {
fastestRoute = leadTime;
fastest = route;
}
}
if(cheapest != null) {
cheapest.setCheapest();
}
if(fastest != null) {
fastest.setFastest();
}
}
private void findCheapestPerMainRun(TemporaryContainer container) {
var routesByMainRun = container.getRoutes().stream().collect(Collectors.groupingBy(TemporaryRouteObject::getMainRun));
Collection<TemporaryRouteObject> cheapestRoutes = new ArrayList<>();
for(var mainRun : routesByMainRun.keySet()) {
List<TemporaryRouteObject> routes = routesByMainRun.get(mainRun);
TemporaryRouteObject cheapest = null;
double cheapestCost = Double.MAX_VALUE;
for(var route : routes) {
double routeCost = route.getCost();
if(routeCost < cheapestCost) {
cheapestCost = routeCost;
cheapest = route;
}
}
if(cheapest != null) {
cheapestRoutes.add(cheapest);
}
}
container.overrideRoutes(cheapestRoutes);
}
private void connectDestinationChainDirectly(TemporaryContainer container, List<List<Node>> chains) {
Collection<TemporaryRateObject> rates = container.getRates();
for (var chain : chains) {
var toNode = chain.isEmpty() ? container.getDestinationNode() : chain.getLast();
TemporaryRateObject finalSection = new TemporaryRateObject(container.getSourceNode(), toNode, TemporaryRateObject.TemporaryRateObjectType.MATRIX);
if (rates.contains(finalSection)) {
TemporaryRateObject finalMatrixRateObj = finalSection;
finalSection = container.getRates().stream().filter(r -> r.equals(finalMatrixRateObj)).findFirst().orElse(null);
} else {
var matrixRate = matrixRateRepository.getByCountryIds(container.getSourceNode().getCountryId(), toNode.getCountryId()).orElse(null);
// no matrix rate in database. skip this chain
if (matrixRate == null) {
continue;
}
finalSection.setRate(matrixRate);
finalSection.setApproxDistance(distanceService.getDistance(container.getSourceNode(), toNode, true));
rates.add(finalSection);
}
// could not create an temporary rate object -> so just skip here ... (should not happen)
if (finalSection == null) {
continue;
}
// create a route.
boolean routable = true;
TemporaryRouteObject routeObj = new TemporaryRouteObject();
for (int idx = 1; idx < chain.size(); idx++) {
Node startNode = chain.get(idx);
Node endNode = chain.get(idx - 1);
var rate = connectNodes(startNode, endNode, container);
if (rate != null) {
routeObj.addSection(rate);
} else {
// chain is not routable -> discard
routable = false;
break;
}
}
// if the chain is routable -> add the final rate and save the route.
if (routable) {
routeObj.addSection(finalSection);
container.addRoute(routeObj);
}
}
}
private TemporaryRateObject connectNearByNodes(Node chainEnd, List<Node> nearByNodes, TemporaryContainer container) {
for (var nearbyNode : nearByNodes) {
TemporaryRateObject nearByRate = connectNodes(nearbyNode, chainEnd, container);
if (null != nearByRate) {
return nearByRate;
}
}
return null;
}
private void connectSourceChainAndSource(TemporaryContainer container) {
/* get the near-by nodes if no country matrix rate present */
List<Node> nearByNodes = (container.hasSourceMatrixRate()) ? null : nodeRepository.getByDistance(container.getSourceNode(), getRegionRadius());
Collection<TemporaryRouteObject> routes = new ArrayList<>();
for (var route : container.getRoutes()) {
var mainRun = route.getMainRun();
var sourceChains = chainResolver.buildChains(mainRun.getFromNode().getId());
for (var chain : sourceChains) {
Node source = container.getSourceNode();
boolean chainEndIsSource = source.getId().equals(chain.getLast().getId());
// find final section: check if chain end and source node are identical, then check if chain end can be connected to
// source node, if this is not possible use a near-by node
TemporaryRateObject finalSection = (chainEndIsSource) ? null : connectNodes(source, chain.getLast(), container);
finalSection = ((finalSection == null && !chainEndIsSource && nearByNodes != null) ? connectNearByNodes(chain.getLast(), nearByNodes, container) : finalSection);
if (finalSection != null || chainEndIsSource) {
boolean routable = true;
TemporaryRouteObject duplicate = route.clone();
for (int idx = 1; idx < chain.size() - 1; idx++) {
Node startNode = chain.get(idx);
Node endNode = chain.get(idx - 1);
TemporaryRateObject rate = connectNodes(startNode, endNode, container);
if (rate != null) duplicate.addSection(rate);
else {
routable = false;
break;
}
}
if (routable) {
if (finalSection != null) {
// add final section if necessary,
// if last chain node == source node this can be skipped.
duplicate.addSection(finalSection);
if (!finalSection.getFromNode().getId().equals(source.getId())) duplicate.routeOverNearBy();
}
routes.add(duplicate);
}
}
}
}
container.overrideRoutes(routes);
}
private Integer getRegionRadius() {
var property = propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.RADIUS_REGION);
return property.map(propertyDTO -> Integer.valueOf(propertyDTO.getCurrentValue())).orElseGet(SystemPropertyMappingId.RADIUS_REGION::getDefaultAsInteger);
}
private void connectDestinationChainAndMainRun(TemporaryContainer container, List<Node> outboundNodes, List<List<Node>> destinationChains) {
/*
* Try to connect everything together:
* - go trough all main runs and adjacent post-runs
* - find any compatible chain:
* - check if chain is routable
* - add post run and main run
*/
for (var mainRun : container.getMainRuns()) {
Node mainRunEndNode = nodeRepository.getById(mainRun.getToNodeId()).orElseThrow();
Node mainRunStartNode = outboundNodes.stream().filter(n -> n.getId().equals(mainRun.getFromNodeId())).findFirst().orElseThrow();
TemporaryRateObject mainRunObj = new TemporaryRateObject(mainRunStartNode, mainRunEndNode, TemporaryRateObject.TemporaryRateObjectType.MAIN_RUN, mainRun);
for (var postRun : container.getPostRuns().get(mainRun.getId())) {
Node postRunEndNode = nodeRepository.getById(postRun.getToNodeId()).orElseThrow();
TemporaryRateObject postRunObj = new TemporaryRateObject(postRunEndNode, mainRunEndNode, TemporaryRateObject.TemporaryRateObjectType.POST_RUN, postRun);
for (var chain : destinationChains) {
ChainQuality quality = getChainQuality(chain, postRun, mainRun);
/* if connection quality is bad, do not try to route this and continue. */
if (quality == ChainQuality.FALLBACK) continue;
boolean routable = true;
TemporaryRouteObject routeObj = new TemporaryRouteObject();
for (int idx = 1; idx < chain.size() - quality.getSizeOffset(); idx++) {
Node startNode = chain.get(idx);
Node endNode = chain.get(idx - 1);
var rate = connectNodes(startNode, endNode, container);
if (rate != null) {
routeObj.addSection(rate);
} else {
// chain is not routable -> discard
routable = false;
break;
}
}
if (routable) {
routeObj.addPostRunSection(postRunObj);
routeObj.addMainRunSection(mainRunObj);
container.addRoute(routeObj);
}
}
}
}
}
private ChainQuality getChainQuality(List<Node> chain, ContainerRate postRun, ContainerRate mainRun) {
if (chain.getLast().getId().equals(postRun.getToNodeId())) {
return ChainQuality.MEDIUM;
} else if (chain.getLast().getId().equals(postRun.getFromNodeId()) && chain.get(chain.size() - 2).getId().equals(postRun.getToNodeId())) {
return ChainQuality.HIGH;
} else if (chain.getLast().getCountryId().equals(postRun.getToCountryId())) {
return ChainQuality.LOW;
}
return ChainQuality.FALLBACK;
}
private TemporaryRateObject connectNodes(Node startNode, Node endNode, TemporaryContainer container) {
var containerRateObj = new TemporaryRateObject(startNode, endNode, TemporaryRateObject.TemporaryRateObjectType.CONTAINER);
var matrixRateObj = new TemporaryRateObject(startNode, endNode, TemporaryRateObject.TemporaryRateObjectType.MATRIX);
if (container.getRates().contains(containerRateObj))
return container.getRates().stream().filter(r -> r.equals(containerRateObj)).findFirst().orElseThrow();
if (container.getRates().contains(matrixRateObj))
return container.getRates().stream().filter(r -> r.equals(matrixRateObj)).findFirst().orElseThrow();
Optional<ContainerRate> containerRate = containerRateRepository.findRoute(startNode.getId(), endNode.getId(), ContainerRateType.ROAD);
if (containerRate.isPresent()) {
containerRateObj.setRate(containerRate.get());
container.getRates().add(containerRateObj);
return containerRateObj;
} else {
Optional<MatrixRate> matrixRate = matrixRateRepository.getByCountryIds(startNode.getCountryId(), endNode.getCountryId());
if (matrixRate.isPresent()) {
matrixRateObj.setRate(matrixRate.get());
matrixRateObj.setApproxDistance(distanceService.getDistance(startNode, endNode, true));
container.getRates().add(matrixRateObj);
return matrixRateObj;
} else {
return null;
}
}
}
private enum ChainQuality {
HIGH(1), MEDIUM(0), LOW(0), FALLBACK(0);
private final int sizeOffset;
ChainQuality(int sizeOffset) {
this.sizeOffset = sizeOffset;
}
public int getSizeOffset() {
return sizeOffset;
}
}
private static class TemporaryContainer {
/*
* Set to lookup route sections. Generated from node pairs.
*/
private final Set<TemporaryRateObject> rates = new HashSet<>();
/*
* Routes that are build within the routing service.
*/
private final Collection<TemporaryRouteObject> routes = new ArrayList<>();
/*
* Source and destination node
*/
private final Node source;
private final Node destination;
/*
* mainRuns and postRuns retrieved from database.
*/
private Map<Integer, List<ContainerRate>> mainRuns;
private Map<Integer, List<ContainerRate>> postRuns;
private MatrixRate sourceMatrixRate;
public TemporaryContainer(Node source, Node destination) {
this.source = source;
this.destination = destination;
this.mainRuns = null;
this.postRuns = null;
this.sourceMatrixRate = null;
}
public Collection<TemporaryRateObject> getRates() {
return rates;
}
public List<ContainerRate> getMainRuns(Integer outboundNodeId) {
return mainRuns.get(outboundNodeId);
}
public List<ContainerRate> getMainRuns() {
return mainRuns.values().stream().flatMap(Collection::stream).toList();
}
public void setMainRuns(Map<Integer, List<ContainerRate>> mainRuns) {
this.mainRuns = mainRuns;
}
public Map<Integer, List<ContainerRate>> getPostRuns() {
return postRuns;
}
public void setPostRuns(Map<Integer, List<ContainerRate>> postRuns) {
this.postRuns = postRuns;
}
public void addRoute(TemporaryRouteObject route) {
this.routes.add(route);
}
public Collection<TemporaryRouteObject> getRoutes() {
return routes;
}
public Node getSourceNode() {
return source;
}
public Node getDestinationNode() {
return destination;
}
public void setSourceMatrixRate(MatrixRate sourceMatrixRate) {
this.sourceMatrixRate = sourceMatrixRate;
}
public boolean hasSourceMatrixRate() {
return sourceMatrixRate != null;
}
public void overrideRoutes(Collection<TemporaryRouteObject> routes) {
this.routes.clear();
this.routes.addAll(routes);
}
}
private static class TemporaryRouteObject {
private final List<TemporaryRateObject> sections;
private TemporaryRateObject mainRun;
private TemporaryRateObject postRun;
private boolean nearBy = false;
private boolean isCheapest = false;
private boolean isFastest = false;
public TemporaryRouteObject() {
sections = new ArrayList<>();
}
public List<TemporaryRateObject> getSections() {
return sections;
}
public void addSection(TemporaryRateObject section) {
this.sections.add(section);
}
public void addMainRunSection(TemporaryRateObject mainRun) {
addSection(mainRun);
this.mainRun = mainRun;
}
public void addPostRunSection(TemporaryRateObject postRun) {
addSection(postRun);
this.postRun = postRun;
}
public TemporaryRateObject getMainRun() {
return mainRun;
}
public void setMainRun(TemporaryRateObject mainRun) {
this.mainRun = mainRun;
}
public TemporaryRateObject getPostRun() {
return postRun;
}
@Override
public TemporaryRouteObject clone() {
TemporaryRouteObject clone = new TemporaryRouteObject();
clone.sections.addAll(sections);
clone.mainRun = mainRun;
clone.postRun = postRun;
clone.nearBy = nearBy;
clone.isCheapest = isCheapest;
clone.isFastest = isFastest;
return clone;
}
public void routeOverNearBy() {
this.nearBy = true;
}
public double getCost() {
return sections.stream().mapToDouble(TemporaryRateObject::getCost).sum();
}
public void setCheapest() {
this.isCheapest = true;
}
public void setFastest() {
this.isFastest = true;
}
public int getLeadTime() {
return sections.stream().mapToInt(TemporaryRateObject::getLeadTime).sum();
}
public List<Node> getNodes() {
List<Node> nodes = new ArrayList<>();
for(var section : sections.reversed()) {
if(sections.getFirst().equals(section))
nodes.add(section.getFromNode());
nodes.add(section.getToNode());
}
return nodes;
}
public Boolean isCheapest() {
return isCheapest;
}
public Boolean isFastest() {
return isFastest;
}
}
private static class TemporaryRateObject {
private final Node fromNode;
private final Node toNode;
private MatrixRate matrixRate;
private double approxDistance;
private ContainerRate containerRate;
private TemporaryRateObjectType type;
public TemporaryRateObject(Node fromNode, Node toNode, TemporaryRateObjectType type) {
this.fromNode = fromNode;
this.toNode = toNode;
this.type = type;
}
public TemporaryRateObject(Node fromNode, Node toNode, TemporaryRateObjectType type, ContainerRate rate) {
this.fromNode = fromNode;
this.toNode = toNode;
this.type = type;
this.containerRate = rate;
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
TemporaryRateObject that = (TemporaryRateObject) o;
if (this.type.equals(that.type)) {
if (this.type.equals(TemporaryRateObjectType.MATRIX)) {
return Objects.equals(this.fromNode.getCountryId(), that.fromNode.getCountryId()) && Objects.equals(this.toNode.getCountryId(), that.toNode.getCountryId());
} else if (this.type.equals(TemporaryRateObjectType.CONTAINER) || this.type.equals(TemporaryRateObjectType.MAIN_RUN) || this.type.equals(TemporaryRateObjectType.POST_RUN)) {
return Objects.equals(this.fromNode.getId(), that.fromNode.getId()) && Objects.equals(this.toNode.getId(), that.toNode.getId());
}
}
return false;
}
@Override
public int hashCode() {
if (matrixRate != null) return Objects.hash(matrixRate.getFromCountry(), matrixRate.getToCountry());
if (containerRate != null) return Objects.hash(containerRate.getFromNodeId(), containerRate.getToNodeId());
return Objects.hash(null, null);
}
public void setRate(ContainerRate containerRate) {
this.containerRate = containerRate;
this.type = TemporaryRateObjectType.CONTAINER;
}
public void setRate(MatrixRate matrixRate) {
this.matrixRate = matrixRate;
this.type = TemporaryRateObjectType.MATRIX;
}
public double getApproxDistance() {
return approxDistance;
}
public void setApproxDistance(double distance) {
this.approxDistance = distance;
}
public Node getFromNode() {
return fromNode;
}
public Node getToNode() {
return toNode;
}
public double getCost() {
if(type.equals(TemporaryRateObjectType.MATRIX)) {
return matrixRate.getRate().doubleValue() * approxDistance;
} else {
return containerRate.getRateFeu().doubleValue();
}
}
public int getLeadTime() {
if (type.equals(TemporaryRateObjectType.MATRIX)) {
return 3;
}
return containerRate.getLeadTime();
}
public TemporaryRateObjectType getType() {
return type;
}
public ContainerRateType getContainerRateTye() {
return containerRate.getType();
}
private enum TemporaryRateObjectType {
MATRIX, CONTAINER, POST_RUN, MAIN_RUN;
}
}
}

View file

@ -130,7 +130,7 @@ CREATE TABLE IF NOT EXISTS `sys_user_node`
-- logistic nodes
CREATE TABLE IF NOT EXISTS node
CREATE TABLE IF NOT EXISTS chain
(
id INT PRIMARY KEY,
country_id INT NOT NULL,
@ -153,7 +153,7 @@ CREATE TABLE IF NOT EXISTS node_predecessor_chain
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
node_id INT NOT NULL,
FOREIGN KEY (node_id) REFERENCES node (id)
FOREIGN KEY (node_id) REFERENCES chain (id)
);
@ -163,7 +163,7 @@ CREATE TABLE IF NOT EXISTS node_predecessor_entry
node_id INT NOT NULL,
node_predecessor_chain_id INT NOT NULL,
sequence_number INT NOT NULL CHECK (sequence_number > 0),
FOREIGN KEY (node_id) REFERENCES node (id),
FOREIGN KEY (node_id) REFERENCES chain (id),
FOREIGN KEY (node_predecessor_chain_id) REFERENCES node_predecessor_chain (id),
UNIQUE KEY uk_node_predecessor (node_predecessor_chain_id, sequence_number),
INDEX idx_node_predecessor (node_predecessor_chain_id),
@ -175,7 +175,7 @@ CREATE TABLE IF NOT EXISTS outbound_country_mapping
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
node_id INT NOT NULL,
country_id INT NOT NULL,
FOREIGN KEY (node_id) REFERENCES node (id),
FOREIGN KEY (node_id) REFERENCES chain (id),
FOREIGN KEY (country_id) REFERENCES country (id),
UNIQUE KEY uk_node_id_country_id (node_id, country_id),
INDEX idx_node_id (node_id),
@ -194,8 +194,8 @@ CREATE TABLE IF NOT EXISTS distance_matrix
distance DECIMAL(15, 2) NOT NULL COMMENT 'travel distance between the two nodes in meters',
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
state CHAR(10),
FOREIGN KEY (from_node_id) REFERENCES node (id),
FOREIGN KEY (to_node_id) REFERENCES node (id),
FOREIGN KEY (from_node_id) REFERENCES chain (id),
FOREIGN KEY (to_node_id) REFERENCES chain (id),
CONSTRAINT `chk_state` CHECK (`state` IN
('VALID', 'STALE')),
INDEX idx_from_to_nodes (from_node_id, to_node_id)
@ -223,8 +223,8 @@ CREATE TABLE IF NOT EXISTS container_rate
rate_hc DECIMAL(15, 2) NOT NULL COMMENT 'rate for 40ft HQ container in EUR',
lead_time INT UNSIGNED NOT NULL COMMENT 'lead time in days',
validity_period_id INT NOT NULL,
FOREIGN KEY (from_node_id) REFERENCES node (id),
FOREIGN KEY (to_node_id) REFERENCES node (id),
FOREIGN KEY (from_node_id) REFERENCES chain (id),
FOREIGN KEY (to_node_id) REFERENCES chain (id),
FOREIGN KEY (validity_period_id) REFERENCES validity_period (id),
INDEX idx_from_to_nodes (from_node_id, to_node_id),
INDEX idx_validity_period_id (validity_period_id)
@ -285,7 +285,7 @@ CREATE TABLE IF NOT EXISTS packaging
`hu_dimension_id` INT NOT NULL,
`shu_dimension_id` INT NOT NULL,
`is_deprecated` BOOLEAN NOT NULL DEFAULT FALSE,
FOREIGN KEY (supplier_node_id) REFERENCES node (id),
FOREIGN KEY (supplier_node_id) REFERENCES chain (id),
FOREIGN KEY (material_id) REFERENCES material (id),
FOREIGN KEY (hu_dimension_id) REFERENCES packaging_dimension (id),
FOREIGN KEY (shu_dimension_id) REFERENCES packaging_dimension (id),
@ -349,7 +349,7 @@ CREATE TABLE IF NOT EXISTS premise
hu_stackable BOOLEAN DEFAULT TRUE,
hu_mixable BOOLEAN DEFAULT TRUE,
FOREIGN KEY (material_id) REFERENCES material (id),
FOREIGN KEY (supplier_node_id) REFERENCES node (id),
FOREIGN KEY (supplier_node_id) REFERENCES chain (id),
FOREIGN KEY (user_supplier_node_id) REFERENCES sys_user_node (id),
FOREIGN KEY (packaging_id) REFERENCES packaging (id),
FOREIGN KEY (user_id) REFERENCES sys_user (id),
@ -377,7 +377,7 @@ CREATE TABLE IF NOT EXISTS premise_destination
handling_cost DECIMAL(15, 2) DEFAULT NULL,
disposal_cost DECIMAL(15, 2) DEFAULT NULL,
FOREIGN KEY (premise_id) REFERENCES premise (id),
FOREIGN KEY (destination_node_id) REFERENCES node (id),
FOREIGN KEY (destination_node_id) REFERENCES chain (id),
INDEX idx_destination_node_id (destination_node_id),
INDEX idx_premise_id (premise_id)
);
@ -406,7 +406,7 @@ CREATE TABLE IF NOT EXISTS premise_route_node
geo_lat DECIMAL(7, 4) CHECK (geo_lat BETWEEN -90 AND 90),
geo_lng DECIMAL(7, 4) CHECK (geo_lng BETWEEN -180 AND 180),
is_outdated BOOLEAN DEFAULT FALSE,
FOREIGN KEY (node_id) REFERENCES node (id),
FOREIGN KEY (node_id) REFERENCES chain (id),
FOREIGN KEY (country_id) REFERENCES country (id),
FOREIGN KEY (user_node_id) REFERENCES sys_user_node (id),
INDEX idx_node_id (node_id),
@ -497,10 +497,10 @@ CREATE TABLE IF NOT EXISTS calculation_job_route_section
is_main_run BOOLEAN DEFAULT FALSE,
is_post_run BOOLEAN DEFAULT FALSE,
rate DECIMAL(15, 2) NOT NULL COMMENT 'copy of the container rate resp. price matrix in EUR (depends on used_rule)',
distance DECIMAL(15, 2) DEFAULT NULL COMMENT 'distance of this route section im meters',
distance DECIMAL(15, 2) DEFAULT NULL COMMENT 'distance of this routeInformationObject section im meters',
cbm_price DECIMAL(15, 2) NOT NULL COMMENT 'calculated price per cubic meter',
weight_price DECIMAL(15, 2) NOT NULL COMMENT 'calculated price per kilogram',
annual_cost DECIMAL(15, 2) NOT NULL COMMENT 'annual costs for this route section, result depends on calculation method (mixed or unmixed, stacked or unstacked, per volume/per weight resp. container rate/price matrix)',
annual_cost DECIMAL(15, 2) NOT NULL COMMENT 'annual costs for this routeInformationObject section, result depends on calculation method (mixed or unmixed, stacked or unstacked, per volume/per weight resp. container rate/price matrix)',
transit_time INT UNSIGNED NOT NULL,
FOREIGN KEY (premise_route_section_id) REFERENCES premise_route_section (id),
FOREIGN KEY (calculation_job_transportation_id) REFERENCES calculation_job_transportation (id),