diff --git a/src/main/java/de/avatic/lcc/model/country/DetourIndex.java b/src/main/java/de/avatic/lcc/model/country/DetourIndex.java new file mode 100644 index 0000000..bc039ad --- /dev/null +++ b/src/main/java/de/avatic/lcc/model/country/DetourIndex.java @@ -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()); + } +} \ No newline at end of file diff --git a/src/main/java/de/avatic/lcc/model/nodes/DistanceMatrix.java b/src/main/java/de/avatic/lcc/model/nodes/Distance.java similarity index 63% rename from src/main/java/de/avatic/lcc/model/nodes/DistanceMatrix.java rename to src/main/java/de/avatic/lcc/model/nodes/Distance.java index 7687764..db77d7e 100644 --- a/src/main/java/de/avatic/lcc/model/nodes/DistanceMatrix.java +++ b/src/main/java/de/avatic/lcc/model/nodes/Distance.java @@ -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 fromNode; + private Integer fromNodeId; @NotNull - @Column("to_node_id") - private AggregateReference 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 getFromNode() { - return fromNode; - } - - public void setFromNode(AggregateReference fromNode) { - this.fromNode = fromNode; - } - - public AggregateReference getToNode() { - return toNode; - } - - public void setToNode(AggregateReference toNode) { - this.toNode = toNode; - } } diff --git a/src/main/java/de/avatic/lcc/model/premises/route/RouteInformation.java b/src/main/java/de/avatic/lcc/model/premises/route/RouteInformation.java new file mode 100644 index 0000000..cd6685e --- /dev/null +++ b/src/main/java/de/avatic/lcc/model/premises/route/RouteInformation.java @@ -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 sections; + + private List nodes; + + + public void setRoute(Route route) { + this.route = route; + } + + public void setRouteNodes(List nodes) { + this.nodes = nodes; + } + + public void setRouteSections(List sections) { + this.sections = sections; + } +} diff --git a/src/main/java/de/avatic/lcc/model/rates/ContainerRate.java b/src/main/java/de/avatic/lcc/model/rates/ContainerRate.java index 4cf66c2..a407b53 100644 --- a/src/main/java/de/avatic/lcc/model/rates/ContainerRate.java +++ b/src/main/java/de/avatic/lcc/model/rates/ContainerRate.java @@ -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; } diff --git a/src/main/java/de/avatic/lcc/repositories/DistanceMatrixRepository.java b/src/main/java/de/avatic/lcc/repositories/DistanceMatrixRepository.java new file mode 100644 index 0000000..13d2686 --- /dev/null +++ b/src/main/java/de/avatic/lcc/repositories/DistanceMatrixRepository.java @@ -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 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 { + @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; + } + } +} diff --git a/src/main/java/de/avatic/lcc/repositories/NodeRepository.java b/src/main/java/de/avatic/lcc/repositories/NodeRepository.java index b63ce0a..6a6405e 100644 --- a/src/main/java/de/avatic/lcc/repositories/NodeRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/NodeRepository.java @@ -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> 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 update(Node node) { + public Optional 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 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 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 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> resolveChainsById(Integer destinationId) { + public List> getChainsById(Integer destinationId) { List> 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(); 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 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 @@ -255,11 +267,11 @@ public class NodeRepository { * @param countryId The ID of the country for which outbound nodes are to be retrieved. * Must not be null. * @return A list of nodes that are outbound for the specified country. - * Returns an empty list if no outbound nodes are found. + * Returns an empty list if no outbound nodes are found. */ public List getAllOutboundFor(Integer countryId) { String query = """ - SELECT node.id AS id, node.name AS name, node.address as address, node.is_source as is_source, + 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 FROM node LEFT JOIN outbound_country_mapping ON outbound_country_mapping.node_id = node.id diff --git a/src/main/java/de/avatic/lcc/repositories/premise/PremiseRepository.java b/src/main/java/de/avatic/lcc/repositories/premise/PremiseRepository.java index 24ecc09..d006daf 100644 --- a/src/main/java/de/avatic/lcc/repositories/premise/PremiseRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/premise/PremiseRepository.java @@ -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; diff --git a/src/main/java/de/avatic/lcc/repositories/properties/PropertyRepository.java b/src/main/java/de/avatic/lcc/repositories/properties/PropertyRepository.java index fb8dc7e..674a0ba 100644 --- a/src/main/java/de/avatic/lcc/repositories/properties/PropertyRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/properties/PropertyRepository.java @@ -101,6 +101,7 @@ public class PropertyRepository { return jdbcTemplate.query(query, new PropertyMapper(), ValidityPeriodState.EXPIRED.name(), propertySetId); } + @Transactional public Optional 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, diff --git a/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java b/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java index 9a569ec..10d251c 100644 --- a/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java @@ -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 findRoutesByStartNodeIdAndDestinationCountryId(Integer startNodeId, List 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 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 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 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 { + + 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; } } diff --git a/src/main/java/de/avatic/lcc/repositories/rates/MatrixRateRepository.java b/src/main/java/de/avatic/lcc/repositories/rates/MatrixRateRepository.java index a659ec6..6ca6f5d 100644 --- a/src/main/java/de/avatic/lcc/repositories/rates/MatrixRateRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/rates/MatrixRateRepository.java @@ -72,11 +72,13 @@ public class MatrixRateRepository { return jdbcTemplate.queryForObject(query, new MatrixRateMapper(), id); } + @Transactional public List listAllRatesByPeriodId(Integer periodId) { String query = "SELECT * FROM country_matrix_rate WHERE validity_period_id = ?"; return jdbcTemplate.query(query, new MatrixRateMapper()); } + @Transactional public Optional 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); diff --git a/src/main/java/de/avatic/lcc/service/calculation/ChainResolver.java b/src/main/java/de/avatic/lcc/service/calculation/ChainResolver.java new file mode 100644 index 0000000..dddafaf --- /dev/null +++ b/src/main/java/de/avatic/lcc/service/calculation/ChainResolver.java @@ -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. + *

+ * 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. + *

+ * + */ +@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. + *

+ * 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. + *

+ * + * @param nodeId The ID of the starting node + * @return A list of valid node chains, each represented as a list of Nodes + */ + public List> buildChains(Integer nodeId) { + if (nodeId == null) + return Collections.emptyList(); + + List> foundChains = new ArrayList<>(); + + Stack 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. + *

+ * 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. + *

+ */ + private static class ChainValidationObject { + + /** Current position in the chain being processed */ + int chainPointer; + + /** List of nodes representing the current chain */ + List 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 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 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. + *

+ * 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. + *

+ * + * @param foreignChains List of chains to validate against the current chain + * @return Collection of ChainValidationObjects representing valid merged chains + */ + public Collection validateChains(List> foreignChains) { + + if (foreignChains == null || foreignChains.isEmpty()) + return Collections.emptyList(); + + var candidates = new ArrayList<>(foreignChains); + var nextCandidates = new ArrayList>(); + 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. + *

+ * Creates new ChainValidationObjects by merging the current chain with + * each candidate chain, starting from the specified index in the candidate chains. + *

+ * + * @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 mergeCandidates(List> candidates, int onIndex) { + var mergedCandidates = new ArrayList(); + + for (var candidate : candidates) { + List 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 getChain() { + return chain; + } + + /** + * Checks if the chain contains circular references. + *

+ * A circular reference occurs when the same node ID appears multiple times + * within the chain. + *

+ * + * @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)); + } + } + + +} diff --git a/src/main/java/de/avatic/lcc/service/calculation/DistanceService.java b/src/main/java/de/avatic/lcc/service/calculation/DistanceService.java new file mode 100644 index 0000000..f39cfb5 --- /dev/null +++ b/src/main/java/de/avatic/lcc/service/calculation/DistanceService.java @@ -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; + } + +} diff --git a/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java b/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java index 939600f..760fe32 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java @@ -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(); + public List findRoutes(Integer destinationId, Integer sourceId, boolean isUserNode) { + List 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 regionNodes = nodeRepository.getByDistance(source, getRegionRadius()); + + Optional 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> destinationChains = chainResolver.buildChains(destinationId); + List 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 outboundNodes = nodeRepository.getAllOutboundFor(source.getCountryId()); + List mainRuns = containerRateRepository.findRoutesByStartNodeIdAndDestinationCountryId(outboundNodes.stream().map(Node::getId).toList(), inboundCountries); - List> destinationChains = resolveChains(destinationId); - Map>> 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 containerRates = new HashSet<>(); + Set matrixRates = new HashSet<>(); + + List nearByNodes = nodeRepository.getByDistance(source, getRegionRadius()); + + List 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)); + + } + + return routeInformationList; } - private Map>> getOutboundChains(List outboundNodes) { - Map>> outboundChains = new HashMap<>(); - for(var outboundNode : outboundNodes) { - outboundChains.put(outboundNode.getId(), resolveChains(outboundNode.getId())); + + private boolean nodesConnectable(Node startNode, Node endNode, ChainInformationObject chain, Set containerRates, Set matrixRates) { + var containerSection = new ChainSectionContainerRateInformation(startNode, endNode); + var matrixSection = new ChainSectionMatrixRateInformation(startNode.getCountryId(), endNode.getCountryId()); + + if (!containerRates.contains(containerSection) && !matrixRates.contains(matrixSection)) { + Optional 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 = 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 containerRates, Set matrixRates, MatrixRate sourceMatrixRate, List 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 containerRates, Set matrixRates) { + List 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 rios, List> 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 rios) { + Map> sourceCountryChains = new HashMap<>(); + for (var rio : rios) { + Integer outboundNodeId = rio.getOutboundNode().getId(); + + List 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 constructRoutesWithChains(List> chains, Node source, Node destination) { - ArrayList foundRoutes = new ArrayList<>(); + private ArrayList findRoutesWithDestinationChainsAndSource(List> chains, Node source, Node destination) { + ArrayList foundRoutes = new ArrayList<>(); + + HashMap matrixRates = new HashMap<>(); for (var chain : chains) { - if(!chain.isEmpty()) { - var matrixRate = matrixRateRepository.getByCountryIds(chain.getLast().getCountryId(), source.getCountryId()); + var destinationCountryId = destination.getCountryId(); + MatrixRate matrixRate = null; - if(matrixRate.isPresent()) { - foundRoutes.add(new FoundRoute(chain, destination, source)); - } + if (!chain.isEmpty()) { + 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> resolveChains(Integer nodeId) { - var resolvedChains = new ArrayList>(); + private enum ChainConnectionQuality { + HIGH(1), MEDIUM(0), LOW(0), FALLBACK(0); - var predecessorChains = nodeRepository.resolveChainsById(nodeId); + private final int startIdx; - for (var predecessorChain : predecessorChains) { - if (!predecessorChain.isEmpty()) { - var successorChains = resolveChains(predecessorChain.getLast().getId()); - - successorChains.forEach(successorChain -> { - successorChain.addAll(0, predecessorChain); - resolvedChains.add(successorChain); - }); - - } - else { - resolvedChains.add(predecessorChain); - } + ChainConnectionQuality(int startIdx) { + this.startIdx = startIdx; } - return resolvedChains; + public int getStartIdx() { + return startIdx; + } } - private static class FoundRoute { - private List nodes; + private interface ChainSectionRate { + + BigDecimal getCost(); + + int getLeadTime(); + + } + + private static class ChainInformationObject { + + private final ChainConnectionQuality quality; + private final List chain; + private final Collection sections; + private Node nearByNode; + + public ChainInformationObject() { + this.chain = new ArrayList<>(); + this.nearByNode = null; + this.sections = new ArrayList<>(); + this.quality = ChainConnectionQuality.FALLBACK; + } + + public ChainInformationObject(List chain, ChainConnectionQuality quality) { + this.chain = chain; + this.nearByNode = null; + this.sections = new ArrayList<>(); + this.quality = quality; + } + + public ChainInformationObject(List chain) { + this.chain = chain; + this.nearByNode = null; + this.sections = new ArrayList<>(); + this.quality = ChainConnectionQuality.FALLBACK; + } + + public List 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 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 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 postRuns; + private Node destinationNode; + private Node sourceNode; + private boolean isUserNode; + private Node outboundNode; + private Node inboundNode; + + + private List destinationChains = new ArrayList<>(); + private List sourceCountryChains; + + public RouteInformationObject(Node destination, Node source, ContainerRate mainRun, Node outboundNode, Node inboundNode, List 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 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 getPostRuns() { + return postRuns; + } + + public void addDestinationChain(ChainInformationObject chain) { + destinationChains.add(chain); + } + + public Node getOutboundNode() { + return outboundNode; + } + + public List getDestinationChains() { + return destinationChains; + } + + public List getSourceCountryChains() { + return sourceCountryChains; + } + + public void setSourceCountryChains(List sourceCountryChains) { + this.sourceCountryChains = sourceCountryChains; + } + + public void removeDestinationChain(ChainInformationObject chain) { + destinationChains.remove(chain); + } + + public void removeSourceChain(ChainInformationObject chain) { + sourceCountryChains.remove(chain); + } + + public RouteInformation getRouteInformation(Set containerRates, Set matrixRates) { + + var destinationChain = destinationChains.stream().min(Comparator.comparing(ChainInformationObject::getCost)); + if (destinationChain.isEmpty()) return null; + + var nodes = new ArrayList(); + var sections = new ArrayList(); + + 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; } } diff --git a/src/main/java/de/avatic/lcc/service/calculation/RoutingService2.java b/src/main/java/de/avatic/lcc/service/calculation/RoutingService2.java new file mode 100644 index 0000000..0d7df6e --- /dev/null +++ b/src/main/java/de/avatic/lcc/service/calculation/RoutingService2.java @@ -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 findRoute(Node destination, Node source, boolean sourceIsUserNode) { + List 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> destinationChains = chainResolver.buildChains(destination.getId()); + List 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 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 mapSections(List sections) { + int index = 1; + + List 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 cheapestRoutes = new ArrayList<>(); + + for(var mainRun : routesByMainRun.keySet()) { + List 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> chains) { + Collection 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 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 nearByNodes = (container.hasSourceMatrixRate()) ? null : nodeRepository.getByDistance(container.getSourceNode(), getRegionRadius()); + + Collection 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 outboundNodes, List> 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 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 = containerRateRepository.findRoute(startNode.getId(), endNode.getId(), ContainerRateType.ROAD); + + if (containerRate.isPresent()) { + containerRateObj.setRate(containerRate.get()); + container.getRates().add(containerRateObj); + return containerRateObj; + } else { + Optional 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 rates = new HashSet<>(); + /* + * Routes that are build within the routing service. + */ + private final Collection routes = new ArrayList<>(); + /* + * Source and destination node + */ + private final Node source; + private final Node destination; + /* + * mainRuns and postRuns retrieved from database. + */ + private Map> mainRuns; + private Map> 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 getRates() { + return rates; + } + + public List getMainRuns(Integer outboundNodeId) { + return mainRuns.get(outboundNodeId); + } + + public List getMainRuns() { + return mainRuns.values().stream().flatMap(Collection::stream).toList(); + } + + public void setMainRuns(Map> mainRuns) { + this.mainRuns = mainRuns; + } + + public Map> getPostRuns() { + return postRuns; + } + + public void setPostRuns(Map> postRuns) { + this.postRuns = postRuns; + } + + public void addRoute(TemporaryRouteObject route) { + this.routes.add(route); + } + + public Collection 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 routes) { + this.routes.clear(); + this.routes.addAll(routes); + } + } + + private static class TemporaryRouteObject { + + private final List 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 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 getNodes() { + List 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; + } + } +} diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 24441b7..fbe3601 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -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),