Refactor terminology and add new RoutingService.
Updated repository classes to replace "node" with "chain" for clarity, aligning with domain terminology. Introduced `RoutingService2` to handle route calculation logic, supporting recursive chain resolution and optimized route determination.
This commit is contained in:
parent
e4ab851d7f
commit
a468e3d187
15 changed files with 2320 additions and 145 deletions
308
src/main/java/de/avatic/lcc/model/country/DetourIndex.java
Normal file
308
src/main/java/de/avatic/lcc/model/country/DetourIndex.java
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
package de.avatic.lcc.model.country;
|
||||
/**
|
||||
* Enumeration providing detour indices for different countries.
|
||||
* The detour index represents the ratio between road distance and direct air distance.
|
||||
*/
|
||||
public enum DetourIndex {
|
||||
AD(1.45, 1.60, 1.53), // Andorra
|
||||
AE(1.25, 1.20, 1.23), // United Arab Emirates
|
||||
AF(1.65, 1.90, 1.78), // Afghanistan
|
||||
AG(1.40, 1.35, 1.38), // Antigua and Barbuda
|
||||
AI(1.35, 1.30, 1.33), // Anguilla
|
||||
AL(1.50, 1.70, 1.60), // Albania
|
||||
AM(1.45, 1.75, 1.60), // Armenia
|
||||
AO(1.55, 1.85, 1.70), // Angola
|
||||
AQ(2.00, 3.00, 2.50), // Antarctica
|
||||
AR(1.30, 1.45, 1.38), // Argentina
|
||||
AS(1.40, 1.60, 1.50), // American Samoa
|
||||
AT(1.35, 1.50, 1.43), // Austria
|
||||
AU(1.25, 1.35, 1.30), // Australia
|
||||
AW(1.35, 1.25, 1.30), // Aruba
|
||||
AX(1.35, 1.70, 1.53), // Åland Islands
|
||||
AZ(1.45, 1.65, 1.55), // Azerbaijan
|
||||
BA(1.50, 1.80, 1.65), // Bosnia and Herzegovina
|
||||
BB(1.35, 1.30, 1.33), // Barbados
|
||||
BD(1.50, 1.60, 1.55), // Bangladesh
|
||||
BE(1.30, 1.25, 1.28), // Belgium
|
||||
BF(1.50, 1.80, 1.65), // Burkina Faso
|
||||
BG(1.40, 1.55, 1.48), // Bulgaria
|
||||
BH(1.25, 1.20, 1.23), // Bahrain
|
||||
BI(1.55, 1.90, 1.73), // Burundi
|
||||
BJ(1.50, 1.85, 1.68), // Benin
|
||||
BL(1.40, 1.35, 1.38), // Saint Barthélemy
|
||||
BM(1.35, 1.30, 1.33), // Bermuda
|
||||
BN(1.40, 1.50, 1.45), // Brunei Darussalam
|
||||
BO(1.45, 1.85, 1.65), // Bolivia
|
||||
BR(1.35, 1.65, 1.50), // Brazil
|
||||
BS(1.40, 1.60, 1.50), // Bahamas
|
||||
BT(1.60, 2.10, 1.85), // Bhutan
|
||||
BV(1.80, 2.20, 2.00), // Bouvet Island
|
||||
BW(1.45, 1.70, 1.58), // Botswana
|
||||
BY(1.35, 1.45, 1.40), // Belarus
|
||||
BZ(1.45, 1.70, 1.58), // Belize
|
||||
CA(1.30, 1.45, 1.38), // Canada
|
||||
CC(1.30, 1.25, 1.28), // Cocos (Keeling) Islands
|
||||
CD(1.60, 2.00, 1.80), // Democratic Republic of the Congo
|
||||
CF(1.65, 2.10, 1.88), // Central African Republic
|
||||
CG(1.60, 1.95, 1.78), // Congo
|
||||
CH(1.40, 1.65, 1.53), // Switzerland
|
||||
CI(1.50, 1.80, 1.65), // Côte d'Ivoire
|
||||
CK(1.40, 1.50, 1.45), // Cook Islands
|
||||
CL(1.35, 1.70, 1.53), // Chile
|
||||
CM(1.55, 1.90, 1.73), // Cameroon
|
||||
CN(1.35, 1.50, 1.43), // China
|
||||
CO(1.40, 1.80, 1.60), // Colombia
|
||||
CR(1.45, 1.70, 1.58), // Costa Rica
|
||||
CU(1.40, 1.55, 1.48), // Cuba
|
||||
CV(1.45, 1.65, 1.55), // Cabo Verde
|
||||
CW(1.35, 1.30, 1.33), // Curaçao
|
||||
CX(1.35, 1.40, 1.38), // Christmas Island
|
||||
CY(1.40, 1.50, 1.45), // Cyprus
|
||||
CZ(1.35, 1.40, 1.38), // Czechia
|
||||
DE(1.30, 1.25, 1.28), // Germany
|
||||
DJ(1.50, 1.85, 1.68), // Djibouti
|
||||
DK(1.30, 1.25, 1.28), // Denmark
|
||||
DM(1.45, 1.70, 1.58), // Dominica
|
||||
DO(1.40, 1.60, 1.50), // Dominican Republic
|
||||
DZ(1.40, 1.70, 1.55), // Algeria
|
||||
EC(1.45, 1.80, 1.63), // Ecuador
|
||||
EE(1.35, 1.40, 1.38), // Estonia
|
||||
EG(1.35, 1.50, 1.43), // Egypt
|
||||
EH(1.45, 1.85, 1.65), // Western Sahara
|
||||
ER(1.55, 1.95, 1.75), // Eritrea
|
||||
ES(1.35, 1.45, 1.40), // Spain
|
||||
ET(1.55, 1.95, 1.75), // Ethiopia
|
||||
FI(1.35, 1.50, 1.43), // Finland
|
||||
FJ(1.45, 1.75, 1.60), // Fiji
|
||||
FK(1.50, 1.90, 1.70), // Falkland Islands
|
||||
FM(1.45, 1.85, 1.65), // Micronesia
|
||||
FO(1.45, 1.85, 1.65), // Faroe Islands
|
||||
FR(1.35, 1.40, 1.38), // France
|
||||
GA(1.55, 1.90, 1.73), // Gabon
|
||||
GB(1.40, 1.30, 1.35), // United Kingdom
|
||||
GD(1.45, 1.55, 1.50), // Grenada
|
||||
GE(1.45, 1.75, 1.60), // Georgia
|
||||
GF(1.45, 1.85, 1.65), // French Guiana
|
||||
GG(1.40, 1.35, 1.38), // Guernsey
|
||||
GH(1.50, 1.80, 1.65), // Ghana
|
||||
GI(1.30, 1.25, 1.28), // Gibraltar
|
||||
GL(1.60, 2.30, 1.95), // Greenland
|
||||
GM(1.50, 1.75, 1.63), // Gambia
|
||||
GN(1.55, 1.90, 1.73), // Guinea
|
||||
GP(1.40, 1.50, 1.45), // Guadeloupe
|
||||
GQ(1.55, 1.95, 1.75), // Equatorial Guinea
|
||||
GR(1.45, 1.65, 1.55), // Greece
|
||||
GS(1.80, 2.40, 2.10), // South Georgia and the South Sandwich Islands
|
||||
GT(1.45, 1.75, 1.60), // Guatemala
|
||||
GU(1.35, 1.40, 1.38), // Guam
|
||||
GW(1.55, 1.95, 1.75), // Guinea-Bissau
|
||||
GY(1.50, 1.85, 1.68), // Guyana
|
||||
HK(1.40, 1.30, 1.35), // Hong Kong
|
||||
HM(1.80, 2.40, 2.10), // Heard Island and McDonald Islands
|
||||
HN(1.45, 1.80, 1.63), // Honduras
|
||||
HR(1.40, 1.55, 1.48), // Croatia
|
||||
HT(1.50, 1.90, 1.70), // Haiti
|
||||
HU(1.35, 1.40, 1.38), // Hungary
|
||||
ID(1.45, 1.75, 1.60), // Indonesia
|
||||
IE(1.40, 1.55, 1.48), // Ireland
|
||||
IL(1.35, 1.40, 1.38), // Israel
|
||||
IM(1.40, 1.50, 1.45), // Isle of Man
|
||||
IN(1.45, 1.60, 1.53), // India
|
||||
IO(1.40, 1.50, 1.45), // British Indian Ocean Territory
|
||||
IQ(1.45, 1.70, 1.58), // Iraq
|
||||
IR(1.40, 1.65, 1.53), // Iran
|
||||
IS(1.40, 1.80, 1.60), // Iceland
|
||||
IT(1.40, 1.45, 1.43), // Italy
|
||||
JE(1.40, 1.35, 1.38), // Jersey
|
||||
JM(1.40, 1.60, 1.50), // Jamaica
|
||||
JO(1.40, 1.65, 1.53), // Jordan
|
||||
JP(1.50, 1.40, 1.45), // Japan
|
||||
KE(1.50, 1.85, 1.68), // Kenya
|
||||
KG(1.50, 1.85, 1.68), // Kyrgyzstan
|
||||
KH(1.50, 1.80, 1.65), // Cambodia
|
||||
KI(1.40, 1.60, 1.50), // Kiribati
|
||||
KM(1.50, 1.75, 1.63), // Comoros
|
||||
KN(1.40, 1.45, 1.43), // Saint Kitts and Nevis
|
||||
KP(1.45, 1.70, 1.58), // North Korea
|
||||
KR(1.40, 1.35, 1.38), // South Korea
|
||||
KW(1.30, 1.35, 1.33), // Kuwait
|
||||
KY(1.35, 1.30, 1.33), // Cayman Islands
|
||||
KZ(1.40, 1.65, 1.53), // Kazakhstan
|
||||
LA(1.55, 1.90, 1.73), // Laos
|
||||
LB(1.45, 1.65, 1.55), // Lebanon
|
||||
LC(1.45, 1.60, 1.53), // Saint Lucia
|
||||
LI(1.35, 1.45, 1.40), // Liechtenstein
|
||||
LK(1.45, 1.65, 1.55), // Sri Lanka
|
||||
LR(1.55, 1.95, 1.75), // Liberia
|
||||
LS(1.55, 1.90, 1.73), // Lesotho
|
||||
LT(1.35, 1.45, 1.40), // Lithuania
|
||||
LU(1.30, 1.35, 1.33), // Luxembourg
|
||||
LV(1.35, 1.45, 1.40), // Latvia
|
||||
LY(1.45, 1.80, 1.63), // Libya
|
||||
MA(1.40, 1.65, 1.53), // Morocco
|
||||
MC(1.25, 1.20, 1.23), // Monaco
|
||||
MD(1.40, 1.60, 1.50), // Moldova
|
||||
ME(1.50, 1.85, 1.68), // Montenegro
|
||||
MF(1.35, 1.30, 1.33), // Saint Martin
|
||||
MG(1.55, 1.95, 1.75), // Madagascar
|
||||
MH(1.35, 1.45, 1.40), // Marshall Islands
|
||||
MK(1.45, 1.70, 1.58), // North Macedonia
|
||||
ML(1.55, 1.95, 1.75), // Mali
|
||||
MM(1.50, 1.85, 1.68), // Myanmar
|
||||
MN(1.45, 1.95, 1.70), // Mongolia
|
||||
MO(1.45, 1.30, 1.38), // Macau
|
||||
MP(1.40, 1.50, 1.45), // Northern Mariana Islands
|
||||
MQ(1.40, 1.55, 1.48), // Martinique
|
||||
MR(1.50, 1.90, 1.70), // Mauritania
|
||||
MS(1.40, 1.60, 1.50), // Montserrat
|
||||
MT(1.35, 1.30, 1.33), // Malta
|
||||
MU(1.40, 1.45, 1.43), // Mauritius
|
||||
MV(1.30, 1.40, 1.35), // Maldives
|
||||
MW(1.55, 1.90, 1.73), // Malawi
|
||||
MX(1.40, 1.60, 1.50), // Mexico
|
||||
MY(1.40, 1.55, 1.48), // Malaysia
|
||||
MZ(1.55, 1.90, 1.73), // Mozambique
|
||||
NA(1.45, 1.80, 1.63), // Namibia
|
||||
NC(1.45, 1.70, 1.58), // New Caledonia
|
||||
NE(1.55, 1.95, 1.75), // Niger
|
||||
NF(1.35, 1.45, 1.40), // Norfolk Island
|
||||
NG(1.50, 1.80, 1.65), // Nigeria
|
||||
NI(1.45, 1.75, 1.60), // Nicaragua
|
||||
NL(1.25, 1.20, 1.23), // Netherlands
|
||||
NO(1.45, 1.90, 1.68), // Norway
|
||||
NP(1.55, 2.00, 1.78), // Nepal
|
||||
NR(1.30, 1.25, 1.28), // Nauru
|
||||
NU(1.40, 1.55, 1.48), // Niue
|
||||
NZ(1.35, 1.55, 1.45), // New Zealand
|
||||
OM(1.40, 1.65, 1.53), // Oman
|
||||
PA(1.45, 1.75, 1.60), // Panama
|
||||
PE(1.45, 1.85, 1.65), // Peru
|
||||
PF(1.45, 1.75, 1.60), // French Polynesia
|
||||
PG(1.55, 2.00, 1.78), // Papua New Guinea
|
||||
PH(1.45, 1.75, 1.60), // Philippines
|
||||
PK(1.45, 1.70, 1.58), // Pakistan
|
||||
PL(1.35, 1.40, 1.38), // Poland
|
||||
PM(1.40, 1.55, 1.48), // Saint Pierre and Miquelon
|
||||
PN(1.50, 1.80, 1.65), // Pitcairn
|
||||
PR(1.40, 1.55, 1.48), // Puerto Rico
|
||||
PS(1.45, 1.65, 1.55), // Palestine
|
||||
PT(1.40, 1.55, 1.48), // Portugal
|
||||
PW(1.40, 1.60, 1.50), // Palau
|
||||
PY(1.40, 1.70, 1.55), // Paraguay
|
||||
QA(1.30, 1.35, 1.33), // Qatar
|
||||
RE(1.45, 1.60, 1.53), // Réunion
|
||||
RO(1.40, 1.55, 1.48), // Romania
|
||||
RS(1.40, 1.60, 1.50), // Serbia
|
||||
RU(1.40, 1.65, 1.53), // Russian Federation
|
||||
RW(1.50, 1.85, 1.68), // Rwanda
|
||||
SA(1.35, 1.60, 1.48), // Saudi Arabia
|
||||
SB(1.50, 1.95, 1.73), // Solomon Islands
|
||||
SC(1.45, 1.65, 1.55), // Seychelles
|
||||
SD(1.50, 1.90, 1.70), // Sudan
|
||||
SE(1.35, 1.50, 1.43), // Sweden
|
||||
SG(1.30, 1.20, 1.25), // Singapore
|
||||
SH(1.50, 1.85, 1.68), // Saint Helena
|
||||
SI(1.40, 1.55, 1.48), // Slovenia
|
||||
SK(1.40, 1.50, 1.45), // Slovakia
|
||||
SL(1.55, 1.95, 1.75), // Sierra Leone
|
||||
SM(1.35, 1.40, 1.38), // San Marino
|
||||
SN(1.50, 1.80, 1.65), // Senegal
|
||||
SO(1.60, 2.10, 1.85), // Somalia
|
||||
SR(1.45, 1.85, 1.65), // Suriname
|
||||
SS(1.60, 2.10, 1.85), // South Sudan
|
||||
ST(1.50, 1.80, 1.65), // Sao Tome and Principe
|
||||
SV(1.45, 1.70, 1.58), // El Salvador
|
||||
SX(1.35, 1.30, 1.33), // Sint Maarten
|
||||
SY(1.45, 1.75, 1.60), // Syria
|
||||
SZ(1.50, 1.80, 1.65), // Swaziland
|
||||
TC(1.35, 1.40, 1.38), // Turks and Caicos Islands
|
||||
TD(1.60, 2.10, 1.85), // Chad
|
||||
TF(1.70, 2.20, 1.95), // French Southern Territories
|
||||
TG(1.50, 1.85, 1.68), // Togo
|
||||
TH(1.40, 1.60, 1.50), // Thailand
|
||||
TJ(1.50, 1.95, 1.73), // Tajikistan
|
||||
TK(1.35, 1.45, 1.40), // Tokelau
|
||||
TL(1.55, 1.95, 1.75), // Timor-Leste
|
||||
TM(1.45, 1.75, 1.60), // Turkmenistan
|
||||
TN(1.40, 1.65, 1.53), // Tunisia
|
||||
TO(1.40, 1.50, 1.45), // Tonga
|
||||
TR(1.40, 1.65, 1.53), // Turkey
|
||||
TT(1.40, 1.50, 1.45), // Trinidad and Tobago
|
||||
TV(1.30, 1.25, 1.28), // Tuvalu
|
||||
TW(1.40, 1.55, 1.48), // Taiwan
|
||||
TZ(1.55, 1.90, 1.73), // Tanzania
|
||||
UA(1.40, 1.55, 1.48), // Ukraine
|
||||
UG(1.55, 1.90, 1.73), // Uganda
|
||||
UM(1.40, 1.55, 1.48), // United States Minor Outlying Islands
|
||||
US(1.25, 1.35, 1.30), // United States of America
|
||||
UY(1.35, 1.55, 1.45), // Uruguay
|
||||
UZ(1.45, 1.70, 1.58), // Uzbekistan
|
||||
VA(1.25, 1.20, 1.23), // Holy See
|
||||
VC(1.45, 1.60, 1.53), // Saint Vincent and the Grenadines
|
||||
VE(1.45, 1.75, 1.60), // Venezuela
|
||||
VG(1.40, 1.45, 1.43), // British Virgin Islands
|
||||
VI(1.40, 1.45, 1.43), // U.S. Virgin Islands
|
||||
VN(1.45, 1.70, 1.58), // Vietnam
|
||||
VU(1.50, 1.85, 1.68), // Vanuatu
|
||||
WF(1.45, 1.65, 1.55), // Wallis and Futuna
|
||||
WS(1.45, 1.60, 1.53), // Samoa
|
||||
YE(1.50, 1.85, 1.68), // Yemen
|
||||
YT(1.45, 1.65, 1.55), // Mayotte
|
||||
ZA(1.40, 1.60, 1.50), // South Africa
|
||||
ZM(1.55, 1.90, 1.73), // Zambia
|
||||
ZW(1.50, 1.85, 1.68); // Zimbabwe
|
||||
|
||||
private final double urbanDetourIndex;
|
||||
private final double ruralDetourIndex;
|
||||
private final double averageDetourIndex;
|
||||
|
||||
/**
|
||||
* Constructor for the DetourIndex enum.
|
||||
*
|
||||
* @param urbanDetourIndex Detour index for urban areas
|
||||
* @param ruralDetourIndex Detour index for rural areas
|
||||
* @param averageDetourIndex Average detour index for the country
|
||||
*/
|
||||
DetourIndex(double urbanDetourIndex, double ruralDetourIndex, double averageDetourIndex) {
|
||||
this.urbanDetourIndex = urbanDetourIndex;
|
||||
this.ruralDetourIndex = ruralDetourIndex;
|
||||
this.averageDetourIndex = averageDetourIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the detour index for urban areas.
|
||||
*
|
||||
* @return The urban detour index as a double value
|
||||
*/
|
||||
public double getUrbanDetourIndex() {
|
||||
return urbanDetourIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the detour index for rural areas.
|
||||
*
|
||||
* @return The rural detour index as a double value
|
||||
*/
|
||||
public double getRuralDetourIndex() {
|
||||
return ruralDetourIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the average detour index for the country.
|
||||
*
|
||||
* @return The average detour index as a double value
|
||||
*/
|
||||
public double getAverageDetourIndex() {
|
||||
return averageDetourIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the DetourIndex object for a specific country.
|
||||
*
|
||||
* @param country The ISO code of the country
|
||||
* @return The DetourIndex object for the country
|
||||
*/
|
||||
public static DetourIndex getDetourIndex(IsoCode country) {
|
||||
return DetourIndex.valueOf(country.name());
|
||||
}
|
||||
}
|
||||
|
|
@ -2,19 +2,15 @@ package de.avatic.lcc.model.nodes;
|
|||
|
||||
|
||||
import jakarta.validation.constraints.*;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.jdbc.core.mapping.AggregateReference;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
|
||||
@Table(name = "distance_matrix")
|
||||
public class DistanceMatrix {
|
||||
public class Distance {
|
||||
|
||||
|
||||
@Id
|
||||
private Integer id;
|
||||
|
||||
@Digits(integer = 7, fraction = 4)
|
||||
|
|
@ -41,18 +37,31 @@ public class DistanceMatrix {
|
|||
@NotNull
|
||||
private BigDecimal distance;
|
||||
|
||||
private OffsetDateTime updatedAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Size(max = 10)
|
||||
private String state;
|
||||
private DistanceMatrixState state;
|
||||
|
||||
@NotNull
|
||||
@Column("from_node_id")
|
||||
private AggregateReference<Node,Integer> fromNode;
|
||||
private Integer fromNodeId;
|
||||
|
||||
@NotNull
|
||||
@Column("to_node_id")
|
||||
private AggregateReference<Node,Integer> toNode;
|
||||
private Integer toNodeId;
|
||||
|
||||
public Integer getFromNodeId() {
|
||||
return fromNodeId;
|
||||
}
|
||||
|
||||
public void setFromNodeId(Integer fromNodeId) {
|
||||
this.fromNodeId = fromNodeId;
|
||||
}
|
||||
|
||||
public Integer getToNodeId() {
|
||||
return toNodeId;
|
||||
}
|
||||
|
||||
public void setToNodeId(Integer toNodeId) {
|
||||
this.toNodeId = toNodeId;
|
||||
}
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
|
|
@ -102,35 +111,20 @@ public class DistanceMatrix {
|
|||
this.distance = distance;
|
||||
}
|
||||
|
||||
public OffsetDateTime getUpdatedAt() {
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(OffsetDateTime updatedAt) {
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
public DistanceMatrixState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(String state) {
|
||||
public void setState(DistanceMatrixState state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public AggregateReference<Node, Integer> getFromNode() {
|
||||
return fromNode;
|
||||
}
|
||||
|
||||
public void setFromNode(AggregateReference<Node, Integer> fromNode) {
|
||||
this.fromNode = fromNode;
|
||||
}
|
||||
|
||||
public AggregateReference<Node, Integer> getToNode() {
|
||||
return toNode;
|
||||
}
|
||||
|
||||
public void setToNode(AggregateReference<Node, Integer> toNode) {
|
||||
this.toNode = toNode;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package de.avatic.lcc.model.premises.route;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class RouteInformation {
|
||||
|
||||
private Route route;
|
||||
|
||||
private List<RouteSection> sections;
|
||||
|
||||
private List<RouteNode> nodes;
|
||||
|
||||
|
||||
public void setRoute(Route route) {
|
||||
this.route = route;
|
||||
}
|
||||
|
||||
public void setRouteNodes(List<RouteNode> nodes) {
|
||||
this.nodes = nodes;
|
||||
}
|
||||
|
||||
public void setRouteSections(List<RouteSection> sections) {
|
||||
this.sections = sections;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
package de.avatic.lcc.repositories;
|
||||
|
||||
import de.avatic.lcc.model.nodes.Distance;
|
||||
import de.avatic.lcc.model.nodes.DistanceMatrixState;
|
||||
import de.avatic.lcc.model.nodes.Node;
|
||||
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowCallbackHandler;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public class DistanceMatrixRepository {
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public DistanceMatrixRepository(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
public Optional<Distance> getDistance(Node src, Node dest) {
|
||||
|
||||
String query = "SELECT * FROM distance_matrix WHERE from_node_id = ? AND to_node_id = ? AND state = ?";
|
||||
|
||||
var distance = jdbcTemplate.query(query, new DistanceMapper(), src.getId(), dest.getId(), DistanceMatrixState.VALID);
|
||||
|
||||
if(distance.isEmpty())
|
||||
return Optional.empty();
|
||||
|
||||
return Optional.of(distance.getFirst());
|
||||
}
|
||||
|
||||
private static class DistanceMapper implements RowMapper<Distance> {
|
||||
@Override
|
||||
public Distance mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
Distance entity = new Distance();
|
||||
|
||||
entity.setFromNodeId(rs.getInt("from_node_id"));
|
||||
entity.setToNodeId(rs.getInt("to_node_id"));
|
||||
entity.setDistance(rs.getBigDecimal("distance"));
|
||||
entity.setFromGeoLng(rs.getBigDecimal("from_geo_lng"));
|
||||
entity.setFromGeoLat(rs.getBigDecimal("from_geo_lat"));
|
||||
entity.setToGeoLng(rs.getBigDecimal("to_geo_lng"));
|
||||
entity.setToGeoLat(rs.getBigDecimal("to_geo_lat"));
|
||||
entity.setState(DistanceMatrixState.valueOf(rs.getString("state")));
|
||||
entity.setUpdatedAt(rs.getTimestamp("updated_at").toLocalDateTime());
|
||||
|
||||
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -32,9 +32,9 @@ public class NodeRepository {
|
|||
FROM node
|
||||
WHERE node.id = ?""";
|
||||
|
||||
var node = jdbcTemplate.queryForObject(query, new NodeMapper(), id);
|
||||
var chain = jdbcTemplate.queryForObject(query, new NodeMapper(), id);
|
||||
|
||||
return Optional.ofNullable(node);
|
||||
return Optional.ofNullable(chain);
|
||||
}
|
||||
|
||||
private List<Map<Integer, Integer>> getPredecessorsOf(Integer id) {
|
||||
|
|
@ -108,11 +108,11 @@ public class NodeRepository {
|
|||
|
||||
private String buildQuery(String filter, Boolean excludeDeprecated, SearchQueryPagination searchQueryPagination) {
|
||||
StringBuilder queryBuilder = new StringBuilder("""
|
||||
SELECT node.id AS id, node.name AS name, node.address as address, node.is_source as is_source,
|
||||
node.is_destination as is_destination, node.is_intermediate as is_intermediate, node.country_id as country_id, node.predecessor_required as predecessor_required,
|
||||
SELECT chain.id AS id, chain.name AS name, chain.address as address, chain.is_source as is_source,
|
||||
chain.is_destination as is_destination, chain.is_intermediate as is_intermediate, chain.country_id as country_id, chain.predecessor_required as predecessor_required,
|
||||
country.iso_code AS country_iso_code, country.region_code AS country_region_code, country.name AS country_name, country.is_deprecated AS country_is_deprecated
|
||||
FROM node
|
||||
LEFT JOIN country ON country.id = node.country_id
|
||||
FROM chain
|
||||
LEFT JOIN country ON country.id = chain.country_id
|
||||
""");
|
||||
|
||||
if (excludeDeprecated) {
|
||||
|
|
@ -132,18 +132,18 @@ public class NodeRepository {
|
|||
}
|
||||
|
||||
@Transactional
|
||||
public Optional<Integer> update(Node node) {
|
||||
public Optional<Integer> update(Node chain) {
|
||||
//TODO update predecessors and outbound_countries too
|
||||
//TODO implement correctly
|
||||
|
||||
//TODO if node is updated set all linked RouteNodes to outdated!
|
||||
//TODO if chain is updated set all linked RouteNodes to outdated!
|
||||
|
||||
String query = "UPDATE node SET name = ?, address = ?, country_id = ?, is_source = ?, is_destination = ?, is_intermediate = ?, predecessor_required = ? WHERE id = ?";
|
||||
return Optional.ofNullable(jdbcTemplate.update(query, node.getId()) == 0 ? null : node.getId());
|
||||
return Optional.ofNullable(jdbcTemplate.update(query, chain.getId()) == 0 ? null : chain.getId());
|
||||
}
|
||||
|
||||
public List<Node> searchNode(String filter, int limit, NodeType nodeType, boolean excludeDeprecated) {
|
||||
StringBuilder queryBuilder = new StringBuilder().append("SELECT * FROM node WHERE (name LIKE ? OR address LIKE ?)");
|
||||
StringBuilder queryBuilder = new StringBuilder().append("SELECT * FROM chain WHERE (name LIKE ? OR address LIKE ?)");
|
||||
|
||||
if (nodeType != null) {
|
||||
queryBuilder.append(" AND node_type = ?");
|
||||
|
|
@ -163,7 +163,7 @@ public class NodeRepository {
|
|||
}
|
||||
|
||||
public List<Node> listAllNodes(boolean onlySources) {
|
||||
StringBuilder queryBuilder = new StringBuilder("SELECT * FROM node");
|
||||
StringBuilder queryBuilder = new StringBuilder("SELECT * FROM chain");
|
||||
if (onlySources) {
|
||||
queryBuilder.append(" WHERE is_source = true");
|
||||
}
|
||||
|
|
@ -175,32 +175,37 @@ public class NodeRepository {
|
|||
public Optional<Node> getByExternalMappingId(String mappingId) {
|
||||
String query = """
|
||||
SELECT node.id AS id, node.name AS name, node.address as address, node.is_source as is_source,
|
||||
node.is_destination as is_destination, node.is_intermediate as is_intermediate, node.country_id as country_id, node.predecessor_required as predecessor_required
|
||||
v.is_destination as is_destination, node.is_intermediate as is_intermediate, node.country_id as country_id, node.predecessor_required as predecessor_required
|
||||
FROM node
|
||||
WHERE node.external_mapping_id = ?""";
|
||||
|
||||
var node = jdbcTemplate.queryForObject(query, new NodeMapper(), mappingId);
|
||||
var chain = jdbcTemplate.queryForObject(query, new NodeMapper(), mappingId);
|
||||
|
||||
return Optional.ofNullable(node);
|
||||
return Optional.ofNullable(chain);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resolves chains of predecessors for a specified destination node by its ID.
|
||||
* If the destination node does not require predecessors, an empty list is returned.
|
||||
* Resolves chains of predecessors for a specified destination chain by its ID.
|
||||
* If the destination chain does not require predecessors, an empty list is returned.
|
||||
* Otherwise, it constructs a list of chains, where each chain represents a sequence of predecessor nodes.
|
||||
*
|
||||
* @param destinationId The ID of the destination node whose predecessor chains need to be resolved.
|
||||
* Must not be null and must correspond to an existing node.
|
||||
* @param destinationId The ID of the destination chain whose predecessor chains need to be resolved.
|
||||
* Must not be null and must correspond to an existing chain.
|
||||
* @return A list of chains, where each chain is a list of nodes.
|
||||
* Each list represents a sequence of predecessor nodes for the given destination node.
|
||||
* If the destination node does not require predecessors, a list containing an empty list is returned.
|
||||
* @throws RuntimeException If a predecessor node is not found for a given sequence number in the chain.
|
||||
* Each list represents a sequence of predecessor nodes for the given destination chain.
|
||||
* If the destination chain does not require predecessors, a list containing an empty list is returned.
|
||||
* @throws RuntimeException If a predecessor chain is not found for a given sequence number in the chain.
|
||||
*/
|
||||
@Transactional
|
||||
public List<List<Node>> resolveChainsById(Integer destinationId) {
|
||||
public List<List<Node>> getChainsById(Integer destinationId) {
|
||||
List<List<Node>> resolvedChains = new ArrayList<>();
|
||||
|
||||
/*
|
||||
* First we collect all chains. If chain usage is not mandatory we also add
|
||||
* an empty chain. So that the chains can be by-passed.
|
||||
*/
|
||||
|
||||
Node destination = getById(destinationId).orElseThrow();
|
||||
|
||||
if (!destination.getPredecessorRequired())
|
||||
|
|
@ -213,6 +218,11 @@ public class NodeRepository {
|
|||
var currentChain = new ArrayList<Node>();
|
||||
resolvedChains.add(currentChain);
|
||||
|
||||
/*
|
||||
* Going through the key set of the chain's HashMap and putting
|
||||
* the Nodes in order of the sequence numbers into a list.
|
||||
*/
|
||||
|
||||
chain.keySet().forEach(sequenceNumber -> {
|
||||
var predecessor = getById(chain.get(sequenceNumber));
|
||||
if (predecessor.isEmpty()) {
|
||||
|
|
@ -223,9 +233,12 @@ public class NodeRepository {
|
|||
|
||||
});
|
||||
|
||||
|
||||
return resolvedChains;
|
||||
}
|
||||
|
||||
|
||||
@Transactional
|
||||
public List<Node> getByDistance(Node node, Integer regionRadius) {
|
||||
|
||||
String query = """
|
||||
|
|
@ -246,7 +259,6 @@ public class NodeRepository {
|
|||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves a list of nodes that are outbound from a given country.
|
||||
* The method considers nodes that are either explicitly mapped to the given country
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ public class PropertyRepository {
|
|||
return jdbcTemplate.query(query, new PropertyMapper(), ValidityPeriodState.EXPIRED.name(), propertySetId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Optional<PropertyDTO> getPropertyByMappingId(SystemPropertyMappingId mappingId) {
|
||||
String query = """
|
||||
SELECT type.name as name, type.data_type as dataType, type.external_mapping_id as externalMappingId, type.validation_rule as validationRule,
|
||||
|
|
|
|||
|
|
@ -2,15 +2,20 @@ package de.avatic.lcc.repositories.rates;
|
|||
|
||||
import de.avatic.lcc.model.rates.ContainerRate;
|
||||
import de.avatic.lcc.model.rates.ContainerRateType;
|
||||
import de.avatic.lcc.model.rates.ValidityPeriodState;
|
||||
import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
|
||||
import de.avatic.lcc.repositories.pagination.SearchQueryResult;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public class ContainerRateRepository {
|
||||
|
|
@ -40,7 +45,97 @@ public class ContainerRateRepository {
|
|||
return jdbcTemplate.query(query, new ContainerRateMapper());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<ContainerRate> findRoutesByStartNodeIdAndDestinationCountryId(Integer startNodeId, List<Integer> destinationCountryIds) {
|
||||
|
||||
if (startNodeId == null || destinationCountryIds == null || destinationCountryIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
|
||||
String destinationCountryPlaceholders = String.join(",", Collections.nCopies(destinationCountryIds.size(), "?"));
|
||||
|
||||
String query = """
|
||||
SELECT container_rate.id AS id,
|
||||
container_rate.validity_period_id AS validity_period_id,
|
||||
container_rate.container_rate_type AS container_rate_type,
|
||||
container_rate.from_node_id AS from_node_id,
|
||||
container_rate.to_node_id AS to_node_id,
|
||||
container_rate.rate_feu AS rate_feu,
|
||||
container_rate.rate_teu AS rate_teu,
|
||||
container_rate.rate_hc AS rate_hc,
|
||||
container_rate.lead_time AS lead_time,
|
||||
to_node.country_id as to_country_id,
|
||||
from_node.country_id as from_country_id
|
||||
FROM container_rate
|
||||
LEFT JOIN node AS to_node ON to_node.id = container_rate.to_node_id
|
||||
LEFT JOIN node AS from_node ON from_node.id = container_rate.from_node_id
|
||||
LEFT JOIN validity_period ON validity_period.id = container_rate.validity_period_id
|
||||
WHERE validity_period.state = ?
|
||||
AND (container_rate.container_rate_type = ? OR container_rate.container_rate_type = ?)
|
||||
AND container_rate.from_node_id = = ? AND to_node.country_id IN (%s)""".formatted(
|
||||
destinationCountryPlaceholders);
|
||||
|
||||
List<Object> params = new ArrayList<>();
|
||||
params.add(ValidityPeriodState.VALID.name());
|
||||
params.add(ContainerRateType.SEA.name());
|
||||
params.add(ContainerRateType.RAIL.name());
|
||||
params.add(startNodeId);
|
||||
params.addAll(destinationCountryIds);
|
||||
|
||||
return jdbcTemplate.query(query, new ContainerRateMapper(true), params.toArray());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<ContainerRate> getPostRunsFor(ContainerRate mainRun) {
|
||||
|
||||
String query = """
|
||||
SELECT container_rate.id AS id,
|
||||
container_rate.validity_period_id AS validity_period_id,
|
||||
container_rate.container_rate_type AS container_rate_type,
|
||||
container_rate.from_node_id AS from_node_id,
|
||||
container_rate.to_node_id AS to_node_id,
|
||||
container_rate.rate_feu AS rate_feu,
|
||||
container_rate.rate_teu AS rate_teu,
|
||||
container_rate.rate_hc AS rate_hc,
|
||||
container_rate.lead_time AS lead_time,
|
||||
to_node.country_id as to_country_id,
|
||||
from_node.country_id as from_country_id
|
||||
FROM container_rate
|
||||
LEFT JOIN node AS to_node ON to_node.id = container_rate.to_node_id
|
||||
LEFT JOIN node AS from_node ON from_node.id = container_rate.from_node_id
|
||||
LEFT JOIN validity_period ON validity_period.id = container_rate.validity_period_id
|
||||
WHERE validity_period.state = ?
|
||||
AND container_rate.from_node_id = ? AND container_rate.container_rate_type = ?""";
|
||||
|
||||
return jdbcTemplate.query(query, new ContainerRateMapper(true), ValidityPeriodState.VALID.name(), mainRun.getToNodeId(), ContainerRateType.POST_RUN.name());
|
||||
}
|
||||
|
||||
public Optional<ContainerRate> findRoute(Integer fromNodeId, Integer toNodeId, ContainerRateType type) {
|
||||
String query = """
|
||||
SELECT * FROM container_rate WHERE from_node_id = ? AND to_node_id = ? AND container_rate_type = ?
|
||||
""";
|
||||
|
||||
var route = jdbcTemplate.query(query, new ContainerRateMapper(), fromNodeId, toNodeId, type);
|
||||
|
||||
if(route.isEmpty())
|
||||
return Optional.empty();
|
||||
|
||||
return Optional.of(route.getFirst());
|
||||
}
|
||||
|
||||
private static class ContainerRateMapper implements RowMapper<ContainerRate> {
|
||||
|
||||
private final boolean fetchCountryIds;
|
||||
|
||||
public ContainerRateMapper(boolean fetchCountryIds) {
|
||||
this.fetchCountryIds = fetchCountryIds;
|
||||
}
|
||||
|
||||
public ContainerRateMapper() {
|
||||
this(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContainerRate mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
var entity = new ContainerRate();
|
||||
|
|
@ -49,12 +144,17 @@ public class ContainerRateRepository {
|
|||
entity.setValidityPeriodId(rs.getInt("validity_period_id"));
|
||||
entity.setFromNodeId(rs.getInt("from_node_id"));
|
||||
entity.setToNodeId(rs.getInt("to_node_id"));
|
||||
entity.setType(ContainerRateType.valueOf(rs.getString("type")));
|
||||
entity.setType(ContainerRateType.valueOf(rs.getString("container_rate_type")));
|
||||
entity.setLeadTime(rs.getInt("lead_time"));
|
||||
entity.setRateFeu(rs.getBigDecimal("rate_feu"));
|
||||
entity.setRateTeu(rs.getBigDecimal("rate_teu"));
|
||||
entity.setRateHc(rs.getBigDecimal("rate_hc"));
|
||||
|
||||
if (fetchCountryIds) {
|
||||
entity.setToCountryId(rs.getInt("to_country_id"));
|
||||
entity.setFromCountryId(rs.getInt("from_country_id"));
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,11 +72,13 @@ public class MatrixRateRepository {
|
|||
return jdbcTemplate.queryForObject(query, new MatrixRateMapper(), id);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<MatrixRate> listAllRatesByPeriodId(Integer periodId) {
|
||||
String query = "SELECT * FROM country_matrix_rate WHERE validity_period_id = ?";
|
||||
return jdbcTemplate.query(query, new MatrixRateMapper());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Optional<MatrixRate> getByCountryIds(Integer fromCountryId, Integer toCountryId) {
|
||||
String query = "SELECT * FROM country_matrix_rate WHERE from_country_id = ? AND to_country_id = ?";
|
||||
var rates = jdbcTemplate.query(query, new MatrixRateMapper(), fromCountryId, toCountryId);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,266 @@
|
|||
package de.avatic.lcc.service.calculation;
|
||||
|
||||
import de.avatic.lcc.model.nodes.Node;
|
||||
import de.avatic.lcc.repositories.NodeRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Service responsible for resolving and building chains of nodes.
|
||||
* <p>
|
||||
* This service class provides functionality to build all possible valid chains
|
||||
* starting from a specified node ID. It handles the validation and processing
|
||||
* of node chains, including detection of circular references.
|
||||
* </p>
|
||||
*
|
||||
*/
|
||||
@Service
|
||||
public class ChainResolver {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ChainResolver.class);
|
||||
|
||||
|
||||
private final NodeRepository nodeRepository;
|
||||
|
||||
/**
|
||||
* Constructs a new ChainResolver with the specified NodeRepository.
|
||||
*
|
||||
* @param nodeRepository The repository used to retrieve node chains
|
||||
*/
|
||||
public ChainResolver(NodeRepository nodeRepository) {
|
||||
this.nodeRepository = nodeRepository;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builds all valid chains starting from the specified node ID.
|
||||
* <p>
|
||||
* This method retrieves all possible chains starting from the given node ID and processes
|
||||
* them to validate and build complete chains. It handles chain merging, validation, and
|
||||
* circular reference detection during the process.
|
||||
* </p>
|
||||
*
|
||||
* @param nodeId The ID of the starting node
|
||||
* @return A list of valid node chains, each represented as a list of Nodes
|
||||
*/
|
||||
public List<List<Node>> buildChains(Integer nodeId) {
|
||||
if (nodeId == null)
|
||||
return Collections.emptyList();
|
||||
|
||||
List<List<Node>> foundChains = new ArrayList<>();
|
||||
|
||||
Stack<ChainValidationObject> chainStack = new Stack<>();
|
||||
chainStack.addAll(nodeRepository.getChainsById(nodeId).stream().map(ChainValidationObject::new).toList());
|
||||
|
||||
while (!chainStack.isEmpty()) {
|
||||
var validationObject = chainStack.pop();
|
||||
|
||||
if(!validationObject.checkCircularReference()) {
|
||||
while (validationObject.hasNext()) {
|
||||
Node currentNode = validationObject.getCurrentNode();
|
||||
chainStack.addAll(validationObject.validateChains(nodeRepository.getChainsById(currentNode.getId())));
|
||||
|
||||
if (!validationObject.valid())
|
||||
break;
|
||||
|
||||
validationObject.next();
|
||||
}
|
||||
|
||||
if (validationObject.valid())
|
||||
foundChains.add(validationObject.getChain());
|
||||
} else {
|
||||
log.warn("Circular reference detected while building predecessor chain for node {}", nodeId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
log.info("Found {} chains for node {}", foundChains.size(), nodeId);
|
||||
return foundChains;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for chain validation and processing.
|
||||
* <p>
|
||||
* This inner class encapsulates the logic for validating node chains, handling
|
||||
* chain merging, and detecting circular references. It maintains the state of
|
||||
* chain processing including the current position and validity status.
|
||||
* </p>
|
||||
*/
|
||||
private static class ChainValidationObject {
|
||||
|
||||
/** Current position in the chain being processed */
|
||||
int chainPointer;
|
||||
|
||||
/** List of nodes representing the current chain */
|
||||
List<Node> chain;
|
||||
|
||||
/** Flag indicating if the chain is valid */
|
||||
private boolean chainValid;
|
||||
|
||||
/**
|
||||
* Creates a new ChainValidationObject with the specified chain.
|
||||
*
|
||||
* @param chain The list of nodes representing the chain to validate
|
||||
*/
|
||||
public ChainValidationObject(List<Node> chain) {
|
||||
this.chain = chain;
|
||||
this.chainPointer = 0;
|
||||
this.chainValid = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ChainValidationObject with the specified chain and pointer position.
|
||||
*
|
||||
* @param chain The list of nodes representing the chain to validate
|
||||
* @param chainPointer The starting position in the chain
|
||||
*/
|
||||
private ChainValidationObject(List<Node> chain, int chainPointer) {
|
||||
this.chain = chain;
|
||||
this.chainPointer = chainPointer;
|
||||
this.chainValid = true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the current node in the chain based on the chainPointer.
|
||||
*
|
||||
* @return The current Node in the chain
|
||||
*/
|
||||
public Node getCurrentNode() {
|
||||
return chain.get(chainPointer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given foreign chains against the current chain.
|
||||
* <p>
|
||||
* This method checks if the foreign chains can be merged with the current chain
|
||||
* starting from the current position. It handles various validation scenarios
|
||||
* including short chains and matching node IDs.
|
||||
* </p>
|
||||
*
|
||||
* @param foreignChains List of chains to validate against the current chain
|
||||
* @return Collection of ChainValidationObjects representing valid merged chains
|
||||
*/
|
||||
public Collection<ChainValidationObject> validateChains(List<List<Node>> foreignChains) {
|
||||
|
||||
if (foreignChains == null || foreignChains.isEmpty())
|
||||
return Collections.emptyList();
|
||||
|
||||
var candidates = new ArrayList<>(foreignChains);
|
||||
var nextCandidates = new ArrayList<List<Node>>();
|
||||
boolean shortChainFound = false;
|
||||
|
||||
int foreignIdx = 0;
|
||||
for (int localIdx = chainPointer + 1; localIdx < chain.size(); localIdx++, foreignIdx++) {
|
||||
var localNode = chain.get(localIdx);
|
||||
|
||||
nextCandidates.clear();
|
||||
|
||||
for (var candidate : candidates) {
|
||||
if (candidate.size() == foreignIdx)
|
||||
shortChainFound = true;
|
||||
else {
|
||||
Node foreignNode = candidate.get(foreignIdx);
|
||||
if (Objects.equals(localNode.getId(), foreignNode.getId())) {
|
||||
nextCandidates.add(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
candidates.clear();
|
||||
candidates.addAll(nextCandidates);
|
||||
|
||||
if (candidates.isEmpty()) {
|
||||
if (!shortChainFound)
|
||||
this.chainValid = false;
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
return mergeCandidates(candidates, foreignIdx);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Merges the current chain with candidate chains at the specified index.
|
||||
* <p>
|
||||
* Creates new ChainValidationObjects by merging the current chain with
|
||||
* each candidate chain, starting from the specified index in the candidate chains.
|
||||
* </p>
|
||||
*
|
||||
* @param candidates List of candidate chains to merge with
|
||||
* @param onIndex The index in the candidate chains to start merging from
|
||||
* @return Collection of ChainValidationObjects representing the merged chains
|
||||
*/
|
||||
private Collection<ChainValidationObject> mergeCandidates(List<List<Node>> candidates, int onIndex) {
|
||||
var mergedCandidates = new ArrayList<ChainValidationObject>();
|
||||
|
||||
for (var candidate : candidates) {
|
||||
List<Node> mergedChain = new ArrayList<>(chain);
|
||||
mergedChain.addAll(candidate.subList(onIndex, candidate.size()));
|
||||
mergedCandidates.add(new ChainValidationObject(mergedChain, onIndex));
|
||||
}
|
||||
|
||||
return mergedCandidates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there are more nodes in the chain after the current position.
|
||||
*
|
||||
* @return true if there are more nodes in the chain, false otherwise
|
||||
*/
|
||||
public boolean hasNext() {
|
||||
return chainPointer < chain.size() - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances the chain pointer to the next position.
|
||||
*/
|
||||
public void next() {
|
||||
chainPointer++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the validity status of the chain.
|
||||
*
|
||||
* @return true if the chain is valid, false otherwise
|
||||
*/
|
||||
public boolean valid() {
|
||||
return chainValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current chain of nodes.
|
||||
*
|
||||
* @return List of Nodes representing the current chain
|
||||
*/
|
||||
public List<Node> getChain() {
|
||||
return chain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the chain contains circular references.
|
||||
* <p>
|
||||
* A circular reference occurs when the same node ID appears multiple times
|
||||
* within the chain.
|
||||
* </p>
|
||||
*
|
||||
* @return true if a circular reference is detected, false otherwise
|
||||
*/
|
||||
public boolean checkCircularReference() {
|
||||
/* check for circular references */
|
||||
return (chain.stream().map(Node::getId)
|
||||
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
|
||||
.entrySet().stream()
|
||||
.noneMatch(entry -> entry.getValue() > 1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,59 +1,277 @@
|
|||
package de.avatic.lcc.service.calculation;
|
||||
|
||||
import de.avatic.lcc.model.nodes.Node;
|
||||
import de.avatic.lcc.model.premises.route.Route;
|
||||
import de.avatic.lcc.model.premises.route.RouteInformation;
|
||||
import de.avatic.lcc.model.premises.route.RouteNode;
|
||||
import de.avatic.lcc.model.premises.route.RouteSection;
|
||||
import de.avatic.lcc.model.properties.SystemPropertyMappingId;
|
||||
import de.avatic.lcc.model.rates.ContainerRate;
|
||||
import de.avatic.lcc.model.rates.ContainerRateType;
|
||||
import de.avatic.lcc.model.rates.MatrixRate;
|
||||
import de.avatic.lcc.repositories.NodeRepository;
|
||||
import de.avatic.lcc.repositories.country.CountryRepository;
|
||||
import de.avatic.lcc.repositories.properties.PropertyRepository;
|
||||
import de.avatic.lcc.repositories.rates.ContainerRateRepository;
|
||||
import de.avatic.lcc.repositories.rates.MatrixRateRepository;
|
||||
import de.avatic.lcc.repositories.users.UserNodeRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
public class RoutingService {
|
||||
|
||||
|
||||
private final NodeRepository nodeRepository;
|
||||
private final CountryRepository countryRepository;
|
||||
private final MatrixRateRepository matrixRateRepository;
|
||||
private final PropertyRepository propertyRepository;
|
||||
private final ChainResolver chainResolver;
|
||||
private final ContainerRateRepository containerRateRepository;
|
||||
private final DistanceService distanceService;
|
||||
private final UserNodeRepository userNodeRepository;
|
||||
|
||||
public RoutingService(NodeRepository nodeRepository, CountryRepository countryRepository, MatrixRateRepository matrixRateRepository, PropertyRepository propertyRepository) {
|
||||
public RoutingService(NodeRepository nodeRepository, MatrixRateRepository matrixRateRepository, PropertyRepository propertyRepository, ChainResolver chainResolver, ContainerRateRepository containerRateRepository, DistanceService distanceService, UserNodeRepository userNodeRepository) {
|
||||
this.nodeRepository = nodeRepository;
|
||||
this.countryRepository = countryRepository;
|
||||
this.matrixRateRepository = matrixRateRepository;
|
||||
this.propertyRepository = propertyRepository;
|
||||
this.chainResolver = chainResolver;
|
||||
this.containerRateRepository = containerRateRepository;
|
||||
this.distanceService = distanceService;
|
||||
this.userNodeRepository = userNodeRepository;
|
||||
}
|
||||
|
||||
public void findRoutes(Integer destinationId, Integer sourceId) {
|
||||
var foundRoutes = new ArrayList<FoundRoute>();
|
||||
public List<RouteInformation> findRoutes(Integer destinationId, Integer sourceId, boolean isUserNode) {
|
||||
List<RouteInformationObject> rios = new ArrayList<>();
|
||||
|
||||
Node source = nodeRepository.getById(sourceId).orElseThrow();
|
||||
/*
|
||||
* 1. STEP:
|
||||
* Get the source and destination node from database.
|
||||
* Check if there is a matrix rate for the source country.
|
||||
*/
|
||||
Node source = (isUserNode) ? userNodeRepository.getById(sourceId).orElseThrow() : nodeRepository.getById(sourceId).orElseThrow();
|
||||
Node destination = nodeRepository.getById(destinationId).orElseThrow();
|
||||
List<Node> regionNodes = nodeRepository.getByDistance(source, getRegionRadius());
|
||||
|
||||
Optional<MatrixRate> sourceMatrixRate = matrixRateRepository.getByCountryIds(source.getCountryId(), source.getCountryId());
|
||||
|
||||
/*
|
||||
* 2. STEP:
|
||||
* Generate recursive all chains starting with the destination node.
|
||||
* This means all chains within the last node of a chain are connected to the
|
||||
* existing chain.
|
||||
*
|
||||
* Furthermore, it is evaluated that all nodes within the chain do not have chains
|
||||
* themselves that are in conflict with the chain.
|
||||
*
|
||||
* Then get all countries from the end of the destination chains.
|
||||
*/
|
||||
List<List<Node>> destinationChains = chainResolver.buildChains(destinationId);
|
||||
List<Integer> inboundCountries = destinationChains.stream().filter(chain -> !chain.isEmpty()).map(chain -> chain.getLast().getCountryId()).distinct().toList();
|
||||
|
||||
/*
|
||||
* 3. STEP:
|
||||
* Get all outbound nodes for the country of the source node. In this first step this includes:
|
||||
* - all intermediate nodes that have the same country id.
|
||||
* - all nodes that are explicitly mapped as outbound node for this country id.
|
||||
*
|
||||
* Then find all outbound nodes that have main transports to the inbound countries found in the step before
|
||||
*
|
||||
* Create a route information object for each outbound node that have a main transport, also add all post-runs
|
||||
* that can be used for the main run.
|
||||
*
|
||||
* Lastly mapping the destination chain on the route information objects, based on there compatibility:
|
||||
*
|
||||
* - last and second-to-last node in the destination chain are identical with the end of the main run and the
|
||||
* end of the post-run. -> High Quality.
|
||||
* - last in the destination chain are identical with the
|
||||
* end of the post-run. -> Medium Quality
|
||||
* - last in the destination chain in the same country as the
|
||||
* end of the post-run. -> Low Quality
|
||||
*
|
||||
*/
|
||||
List<Node> outboundNodes = nodeRepository.getAllOutboundFor(source.getCountryId());
|
||||
List<ContainerRate> mainRuns = containerRateRepository.findRoutesByStartNodeIdAndDestinationCountryId(outboundNodes.stream().map(Node::getId).toList(), inboundCountries);
|
||||
|
||||
List<List<Node>> destinationChains = resolveChains(destinationId);
|
||||
Map<Integer, List<List<Node>>> outboundChains = getOutboundChains(outboundNodes);
|
||||
mainRuns.forEach(mainRun -> rios.add(new RouteInformationObject(destination, source, mainRun, nodeRepository.getById(mainRun.getToNodeId()).orElseThrow(), outboundNodes.stream().filter(n -> n.getId().equals(mainRun.getFromNodeId())).findFirst().orElseThrow(), containerRateRepository.getPostRunsFor(mainRun))));
|
||||
addDestinationChains(rios, destinationChains);
|
||||
|
||||
/*
|
||||
* 4. STEP:
|
||||
* Next we are resolving the predecessor chains of all outbound nodes recursively in exact the
|
||||
* same way the destination chains were resolved.
|
||||
*/
|
||||
getSourceCountryChains(rios);
|
||||
|
||||
/*
|
||||
* 5. STEP:
|
||||
* Try to connect the destination chains directly with the source node with a
|
||||
* matrix rate. All routes that are constructed in this way are getting its own
|
||||
* route information object and added to the list.
|
||||
*/
|
||||
rios.addAll(findRoutesWithDestinationChainsAndSource(destinationChains, source, destination));
|
||||
|
||||
/*
|
||||
* 6. STEP:
|
||||
* Final step is to check if there are actual rates for the pre-run and post-run
|
||||
* chains, and filter out all without rates.
|
||||
*
|
||||
* pre-run chains also needs to be checked if they are connectable to the supplier,
|
||||
* all unconnectable chains are thrown away.
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
Set<ChainSectionContainerRateInformation> containerRates = new HashSet<>();
|
||||
Set<ChainSectionMatrixRateInformation> matrixRates = new HashSet<>();
|
||||
|
||||
List<Node> nearByNodes = nodeRepository.getByDistance(source, getRegionRadius());
|
||||
|
||||
List<RouteInformation> routeInformationList = new ArrayList<>();
|
||||
|
||||
for (var rio : rios) {
|
||||
|
||||
|
||||
|
||||
foundRoutes.addAll(constructRoutesWithChains(destinationChains, source, destination));
|
||||
processDestinationChains(rio, containerRates, matrixRates);
|
||||
|
||||
|
||||
processSourceCountryChains(rio, containerRates, matrixRates, sourceMatrixRate.orElse(null), nearByNodes, source);
|
||||
|
||||
rio.setSource(source, isUserNode);
|
||||
|
||||
routeInformationList.add(rio.getRouteInformation(containerRates, matrixRates));
|
||||
|
||||
}
|
||||
|
||||
private Map<Integer, List<List<Node>>> getOutboundChains(List<Node> outboundNodes) {
|
||||
Map<Integer, List<List<Node>>> outboundChains = new HashMap<>();
|
||||
for(var outboundNode : outboundNodes) {
|
||||
outboundChains.put(outboundNode.getId(), resolveChains(outboundNode.getId()));
|
||||
return routeInformationList;
|
||||
|
||||
}
|
||||
|
||||
|
||||
private boolean nodesConnectable(Node startNode, Node endNode, ChainInformationObject chain, Set<ChainSectionContainerRateInformation> containerRates, Set<ChainSectionMatrixRateInformation> matrixRates) {
|
||||
var containerSection = new ChainSectionContainerRateInformation(startNode, endNode);
|
||||
var matrixSection = new ChainSectionMatrixRateInformation(startNode.getCountryId(), endNode.getCountryId());
|
||||
|
||||
if (!containerRates.contains(containerSection) && !matrixRates.contains(matrixSection)) {
|
||||
Optional<ContainerRate> rate = containerRateRepository.findRoute(startNode.getId(), endNode.getId(), ContainerRateType.ROAD);
|
||||
|
||||
if (rate.isPresent()) {
|
||||
containerSection.setRate(rate.get());
|
||||
containerRates.add(containerSection);
|
||||
chain.addSectionInformation(new ChainSectionInformation(startNode, endNode, containerSection));
|
||||
} else if (!matrixRates.contains(matrixSection)) {
|
||||
Optional<MatrixRate> matrixRate = matrixRateRepository.getByCountryIds(startNode.getCountryId(), endNode.getCountryId());
|
||||
|
||||
if (matrixRate.isPresent()) {
|
||||
matrixSection.setRate(matrixRate.get());
|
||||
matrixSection.setDistance(distanceService.getDistance(startNode, endNode, true));
|
||||
matrixRates.add(matrixSection);
|
||||
chain.addSectionInformation(new ChainSectionInformation(startNode, endNode, containerSection));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void processSourceCountryChains(RouteInformationObject rio, Set<ChainSectionContainerRateInformation> containerRates, Set<ChainSectionMatrixRateInformation> matrixRates, MatrixRate sourceMatrixRate, List<Node> nearByNodes, Node source) {
|
||||
|
||||
for (var chain : rio.getSourceCountryChains()) {
|
||||
boolean directly = false;
|
||||
Node nearByNode = null;
|
||||
boolean useSourceMatrixRate = false;
|
||||
|
||||
// 1. try connect chain end to source node directly
|
||||
if (!nodesConnectable(source, chain.getChain().getLast(), chain, containerRates, matrixRates)) {
|
||||
|
||||
// 2. try to connect via matrix rate
|
||||
useSourceMatrixRate = sourceMatrixRate != null && chain.getChain().getLast().getCountryId().equals(sourceMatrixRate.getFromCountry());
|
||||
|
||||
if (!useSourceMatrixRate) {
|
||||
// try to connect via near by nodes (metropolitan).
|
||||
nearByNode = nearByNodes.stream().filter(n -> nodesConnectable(n, chain.getChain().getLast(), chain, containerRates, matrixRates)).findFirst().orElse(null);
|
||||
if (null != nearByNode) {
|
||||
chain.addNearByNode(nearByNode);
|
||||
}
|
||||
}
|
||||
} else directly = true;
|
||||
|
||||
if (nearByNode != null || useSourceMatrixRate || directly) {
|
||||
for (int idx = chain.getChain().size() - 1; idx > 0; idx--) {
|
||||
Node startNode = chain.getChain().get(idx);
|
||||
Node endNode = chain.getChain().get(idx - 1);
|
||||
|
||||
if (!nodesConnectable(startNode, endNode, chain, containerRates, matrixRates))
|
||||
rio.removeSourceChain(chain);
|
||||
}
|
||||
} else rio.removeSourceChain(chain);
|
||||
}
|
||||
}
|
||||
|
||||
private void processDestinationChains(RouteInformationObject rio, Set<ChainSectionContainerRateInformation> containerRates, Set<ChainSectionMatrixRateInformation> matrixRates) {
|
||||
List<ChainInformationObject> chains = rio.getDestinationChains();
|
||||
|
||||
for (var chain : chains) {
|
||||
|
||||
for (int idx = chain.getChain().size() - 1; idx > chain.getQuality().getStartIdx(); idx--) {
|
||||
Node startNode = chain.getChain().get(idx);
|
||||
Node endNode = chain.getChain().get(idx - 1);
|
||||
|
||||
if (!nodesConnectable(startNode, endNode, chain, containerRates, matrixRates))
|
||||
rio.removeDestinationChain(chain);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds destination chains to the given list of RouteInformationObject instances based
|
||||
* on their connection properties and quality. The method iterates through each RouteInformationObject
|
||||
* and assesses its post-run connections with possible destination chains, adding the chains with an
|
||||
* associated connection quality determined by their compatibility.
|
||||
*
|
||||
* @param rios The list of RouteInformationObject instances to which the destination chains
|
||||
* will be added. Each RouteInformationObject contains post-run connection details.
|
||||
* @param destinationChains A list of destination chains, where each chain is represented
|
||||
* as a list of Node objects. These chains are evaluated for
|
||||
* addition to the RouteInformationObjects based on connection compatibility.
|
||||
*/
|
||||
private void addDestinationChains(List<RouteInformationObject> rios, List<List<Node>> destinationChains) {
|
||||
|
||||
for (var rio : rios) {
|
||||
for (var chain : destinationChains) {
|
||||
for (var postRun : rio.getPostRuns()) {
|
||||
if (chain.getLast().getId().equals(postRun.getToNodeId())) {
|
||||
rio.addDestinationChain(new ChainInformationObject(chain, ChainConnectionQuality.MEDIUM));
|
||||
} else if (chain.getLast().getId().equals(postRun.getFromNodeId()) && chain.get(chain.size() - 2).getId().equals(postRun.getToNodeId())) {
|
||||
rio.addDestinationChain(new ChainInformationObject(chain, ChainConnectionQuality.HIGH));
|
||||
} else if (chain.getLast().getCountryId().equals(postRun.getToCountryId())) {
|
||||
rio.addDestinationChain(new ChainInformationObject(chain, ChainConnectionQuality.LOW));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void getSourceCountryChains(List<RouteInformationObject> rios) {
|
||||
Map<Integer, List<ChainInformationObject>> sourceCountryChains = new HashMap<>();
|
||||
for (var rio : rios) {
|
||||
Integer outboundNodeId = rio.getOutboundNode().getId();
|
||||
|
||||
List<ChainInformationObject> chains = null;
|
||||
|
||||
if (sourceCountryChains.containsKey(outboundNodeId)) {
|
||||
chains = sourceCountryChains.get(outboundNodeId);
|
||||
} else {
|
||||
chains = chainResolver.buildChains(outboundNodeId).stream().map(ChainInformationObject::new).toList();
|
||||
sourceCountryChains.put(outboundNodeId, chains);
|
||||
}
|
||||
|
||||
rio.setSourceCountryChains(chains);
|
||||
}
|
||||
return outboundChains;
|
||||
}
|
||||
|
||||
private Integer getRegionRadius() {
|
||||
|
|
@ -61,73 +279,389 @@ public class RoutingService {
|
|||
return property.map(propertyDTO -> Integer.valueOf(propertyDTO.getCurrentValue())).orElseGet(SystemPropertyMappingId.RADIUS_REGION::getDefaultAsInteger);
|
||||
}
|
||||
|
||||
private ArrayList<FoundRoute> constructRoutesWithChains(List<List<Node>> chains, Node source, Node destination) {
|
||||
ArrayList<FoundRoute> foundRoutes = new ArrayList<>();
|
||||
private ArrayList<RouteInformationObject> findRoutesWithDestinationChainsAndSource(List<List<Node>> chains, Node source, Node destination) {
|
||||
ArrayList<RouteInformationObject> foundRoutes = new ArrayList<>();
|
||||
|
||||
HashMap<Integer, MatrixRate> matrixRates = new HashMap<>();
|
||||
|
||||
for (var chain : chains) {
|
||||
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<List<Node>> resolveChains(Integer nodeId) {
|
||||
var resolvedChains = new ArrayList<List<Node>>();
|
||||
private enum ChainConnectionQuality {
|
||||
HIGH(1), MEDIUM(0), LOW(0), FALLBACK(0);
|
||||
|
||||
var predecessorChains = nodeRepository.resolveChainsById(nodeId);
|
||||
|
||||
for (var predecessorChain : predecessorChains) {
|
||||
if (!predecessorChain.isEmpty()) {
|
||||
var successorChains = resolveChains(predecessorChain.getLast().getId());
|
||||
|
||||
successorChains.forEach(successorChain -> {
|
||||
successorChain.addAll(0, predecessorChain);
|
||||
resolvedChains.add(successorChain);
|
||||
});
|
||||
private final int startIdx;
|
||||
|
||||
ChainConnectionQuality(int startIdx) {
|
||||
this.startIdx = startIdx;
|
||||
}
|
||||
else {
|
||||
resolvedChains.add(predecessorChain);
|
||||
|
||||
public int getStartIdx() {
|
||||
return startIdx;
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedChains;
|
||||
private interface ChainSectionRate {
|
||||
|
||||
BigDecimal getCost();
|
||||
|
||||
int getLeadTime();
|
||||
|
||||
}
|
||||
|
||||
private static class FoundRoute {
|
||||
private List<Node> nodes;
|
||||
private static class ChainInformationObject {
|
||||
|
||||
private final ChainConnectionQuality quality;
|
||||
private final List<Node> chain;
|
||||
private final Collection<ChainSectionInformation> sections;
|
||||
private Node nearByNode;
|
||||
|
||||
public ChainInformationObject() {
|
||||
this.chain = new ArrayList<>();
|
||||
this.nearByNode = null;
|
||||
this.sections = new ArrayList<>();
|
||||
this.quality = ChainConnectionQuality.FALLBACK;
|
||||
}
|
||||
|
||||
public ChainInformationObject(List<Node> chain, ChainConnectionQuality quality) {
|
||||
this.chain = chain;
|
||||
this.nearByNode = null;
|
||||
this.sections = new ArrayList<>();
|
||||
this.quality = quality;
|
||||
}
|
||||
|
||||
public ChainInformationObject(List<Node> chain) {
|
||||
this.chain = chain;
|
||||
this.nearByNode = null;
|
||||
this.sections = new ArrayList<>();
|
||||
this.quality = ChainConnectionQuality.FALLBACK;
|
||||
}
|
||||
|
||||
public List<Node> getChain() {
|
||||
return chain;
|
||||
}
|
||||
|
||||
public void addNearByNode(Node nearByNode) {
|
||||
this.nearByNode = nearByNode;
|
||||
}
|
||||
|
||||
public void addSectionInformation(ChainSectionInformation section) {
|
||||
this.sections.add(section);
|
||||
}
|
||||
|
||||
public double getCost() {
|
||||
return sections.stream().mapToDouble(ChainSectionInformation::getCost).sum();
|
||||
}
|
||||
|
||||
public int getLeadTime() {
|
||||
return sections.stream().mapToInt(ChainSectionInformation::getLeadTime).sum();
|
||||
}
|
||||
|
||||
public ChainConnectionQuality getQuality() {
|
||||
return quality;
|
||||
}
|
||||
|
||||
public Collection<ChainSectionInformation> getSections() {
|
||||
return sections;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ChainSectionInformation {
|
||||
|
||||
Node fromNode;
|
||||
Node toNode;
|
||||
|
||||
ChainSectionRate rate;
|
||||
|
||||
public ChainSectionInformation(Node fromNode, Node toNode, ChainSectionRate rate) {
|
||||
this.fromNode = fromNode;
|
||||
this.toNode = toNode;
|
||||
this.rate = rate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ChainSectionInformation that = (ChainSectionInformation) o;
|
||||
return Objects.equals(fromNode, that.fromNode) && Objects.equals(toNode, that.toNode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(fromNode, toNode);
|
||||
}
|
||||
|
||||
public double getCost() {
|
||||
return rate.getCost().doubleValue();
|
||||
}
|
||||
|
||||
public int getLeadTime() {
|
||||
return rate.getLeadTime();
|
||||
}
|
||||
}
|
||||
|
||||
private static class ChainSectionContainerRateInformation implements ChainSectionRate {
|
||||
Node fromNode;
|
||||
Node toNode;
|
||||
|
||||
ContainerRate rate;
|
||||
|
||||
public ChainSectionContainerRateInformation(Node fromNode, Node toNode) {
|
||||
this.fromNode = fromNode;
|
||||
this.toNode = toNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ChainSectionContainerRateInformation that = (ChainSectionContainerRateInformation) o;
|
||||
|
||||
return Objects.equals(fromNode.getId(), that.fromNode.getId()) && Objects.equals(toNode.getId(), that.toNode.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(fromNode.getId(), toNode.getId());
|
||||
}
|
||||
|
||||
public void setRate(ContainerRate containerRate) {
|
||||
this.rate = containerRate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal getCost() {
|
||||
return rate.getRateFeu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLeadTime() {
|
||||
return rate.getLeadTime();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class ChainSectionMatrixRateInformation implements ChainSectionRate {
|
||||
Integer fromCountryId;
|
||||
Integer toCountryId;
|
||||
|
||||
MatrixRate rate;
|
||||
private int distance;
|
||||
|
||||
|
||||
public ChainSectionMatrixRateInformation(Integer fromCountryId, Integer toCountryId) {
|
||||
this.fromCountryId = fromCountryId;
|
||||
this.toCountryId = toCountryId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ChainSectionMatrixRateInformation that = (ChainSectionMatrixRateInformation) o;
|
||||
|
||||
return Objects.equals(fromCountryId, that.fromCountryId) && Objects.equals(toCountryId, that.toCountryId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(fromCountryId, toCountryId);
|
||||
}
|
||||
|
||||
public void setRate(MatrixRate matrixRate) {
|
||||
this.rate = matrixRate;
|
||||
}
|
||||
|
||||
|
||||
void setDistance(int distance) {
|
||||
this.distance = distance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal getCost() {
|
||||
return rate.getRate().multiply(BigDecimal.valueOf(distance));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLeadTime() {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
private static class RouteInformationObject {
|
||||
|
||||
/**
|
||||
* Constructs a FoundRoute object that represents a route consisting of a chain of nodes,
|
||||
* starting with the source node, followed by the reversed chain of nodes, and ending with the destination node.
|
||||
*
|
||||
* @param chain The list of intermediary nodes in the route, in original order before reversing.
|
||||
* Must not be null.
|
||||
* @param destination The destination node in the route. Must not be null.
|
||||
* @param source The source node in the route. Must not be null.
|
||||
* If a destination chain can be connected directly with a matrix rate to the source node.
|
||||
* The matrix rate is stored in matrixRate. The destinationChains only contains one chain in this case.
|
||||
* mainRun, outboundNode and inboundNode are null then and postRuns is empty.
|
||||
*/
|
||||
public FoundRoute(List<Node> chain, Node destination, Node source) {
|
||||
this.nodes = new ArrayList<>();
|
||||
this.nodes.add(source);
|
||||
this.nodes.addAll(chain.reversed());
|
||||
this.nodes.add(destination);
|
||||
private final MatrixRate matrixRate;
|
||||
/**
|
||||
* If a destination chain cannot be connected directly with a matrix rate to the source node.
|
||||
* The matrix rate is null and mainRun and postRuns are set.
|
||||
*/
|
||||
|
||||
private final ContainerRate mainRun;
|
||||
private final List<ContainerRate> postRuns;
|
||||
private Node destinationNode;
|
||||
private Node sourceNode;
|
||||
private boolean isUserNode;
|
||||
private Node outboundNode;
|
||||
private Node inboundNode;
|
||||
|
||||
|
||||
private List<ChainInformationObject> destinationChains = new ArrayList<>();
|
||||
private List<ChainInformationObject> sourceCountryChains;
|
||||
|
||||
public RouteInformationObject(Node destination, Node source, ContainerRate mainRun, Node outboundNode, Node inboundNode, List<ContainerRate> postRuns) {
|
||||
this.mainRun = mainRun;
|
||||
this.outboundNode = outboundNode;
|
||||
this.inboundNode = inboundNode;
|
||||
this.postRuns = postRuns;
|
||||
this.matrixRate = null;
|
||||
this.sourceNode = source;
|
||||
this.destinationNode = destination;
|
||||
}
|
||||
|
||||
public RouteInformationObject(Node destination, Node source, MatrixRate matrixRate, List<ChainInformationObject> destinationCountryChains) {
|
||||
this.matrixRate = matrixRate;
|
||||
this.mainRun = null;
|
||||
this.outboundNode = null;
|
||||
this.postRuns = Collections.emptyList();
|
||||
|
||||
this.destinationChains.addAll(destinationCountryChains);
|
||||
this.sourceCountryChains = Collections.singletonList(new ChainInformationObject());
|
||||
|
||||
this.destinationNode = destination;
|
||||
this.sourceNode = source;
|
||||
}
|
||||
|
||||
|
||||
public List<ContainerRate> getPostRuns() {
|
||||
return postRuns;
|
||||
}
|
||||
|
||||
public void addDestinationChain(ChainInformationObject chain) {
|
||||
destinationChains.add(chain);
|
||||
}
|
||||
|
||||
public Node getOutboundNode() {
|
||||
return outboundNode;
|
||||
}
|
||||
|
||||
public List<ChainInformationObject> getDestinationChains() {
|
||||
return destinationChains;
|
||||
}
|
||||
|
||||
public List<ChainInformationObject> getSourceCountryChains() {
|
||||
return sourceCountryChains;
|
||||
}
|
||||
|
||||
public void setSourceCountryChains(List<ChainInformationObject> sourceCountryChains) {
|
||||
this.sourceCountryChains = sourceCountryChains;
|
||||
}
|
||||
|
||||
public void removeDestinationChain(ChainInformationObject chain) {
|
||||
destinationChains.remove(chain);
|
||||
}
|
||||
|
||||
public void removeSourceChain(ChainInformationObject chain) {
|
||||
sourceCountryChains.remove(chain);
|
||||
}
|
||||
|
||||
public RouteInformation getRouteInformation(Set<ChainSectionContainerRateInformation> containerRates, Set<ChainSectionMatrixRateInformation> matrixRates) {
|
||||
|
||||
var destinationChain = destinationChains.stream().min(Comparator.comparing(ChainInformationObject::getCost));
|
||||
if (destinationChain.isEmpty()) return null;
|
||||
|
||||
var nodes = new ArrayList<RouteNode>();
|
||||
var sections = new ArrayList<RouteSection>();
|
||||
|
||||
RouteInformation routeInformation = new RouteInformation();
|
||||
routeInformation.setRoute(new Route());
|
||||
routeInformation.setRouteNodes(nodes);
|
||||
routeInformation.setRouteSections(sections);
|
||||
|
||||
if (matrixRate == null) {
|
||||
var sourceChain = sourceCountryChains.stream().min(Comparator.comparing(ChainInformationObject::getCost));
|
||||
if (sourceChain.isEmpty()) return null;
|
||||
|
||||
nodes.add(mapNode(sourceNode, isUserNode));
|
||||
nodes.addAll(sourceChain.get().getChain().reversed().stream().map(n -> mapNode(n, false)).toList());
|
||||
|
||||
nodes.add(mapNode(outboundNode, false));
|
||||
nodes.add(mapNode(inboundNode, false));
|
||||
|
||||
sections.add(new ChainSectionInformation(sourceChain.get().getChain().getFirst(), sourceNode, ))
|
||||
sections.addAll(sourceChain.get().getSections().stream().map(this::mapSection).toList());
|
||||
|
||||
}
|
||||
|
||||
nodes.addAll(destinationChain.get().getChain().stream().map(n -> mapNode(n, false)).toList());
|
||||
|
||||
|
||||
return routeInformation;
|
||||
}
|
||||
|
||||
private RouteSection mapSection(ChainSectionInformation s) {
|
||||
|
||||
RouteSection section = new RouteSection();
|
||||
|
||||
section.set
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
private RouteNode mapNode(Node node, boolean isUserNode) {
|
||||
RouteNode routeNode = new RouteNode();
|
||||
|
||||
routeNode.setNodeId(!isUserNode ? node.getId() : null);
|
||||
routeNode.setUserNodeId(isUserNode ? node.getId() : null);
|
||||
routeNode.setCountryId(node.getCountryId());
|
||||
routeNode.setGeoLat(node.getGeoLat());
|
||||
routeNode.setGeoLng(node.getGeoLng());
|
||||
routeNode.setName(node.getName());
|
||||
routeNode.setAddress(node.getAddress());
|
||||
routeNode.setOutdated(node.getDeprecated());
|
||||
|
||||
routeNode.setIntermediate(false);
|
||||
routeNode.setDestination(false);
|
||||
routeNode.setSource(false);
|
||||
|
||||
return routeNode;
|
||||
}
|
||||
|
||||
|
||||
public void setSource(Node node, boolean isUserNode) {
|
||||
this.sourceNode = node;
|
||||
this.isUserNode = isUserNode;
|
||||
}
|
||||
|
||||
public ContainerRate getMainRun() {
|
||||
return mainRun;
|
||||
}
|
||||
|
||||
public void setInboundNode(Node node) {
|
||||
this.inboundNode = node;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,796 @@
|
|||
package de.avatic.lcc.service.calculation;
|
||||
|
||||
import de.avatic.lcc.dto.generic.RouteType;
|
||||
import de.avatic.lcc.model.nodes.Node;
|
||||
import de.avatic.lcc.model.premises.route.Route;
|
||||
import de.avatic.lcc.model.premises.route.RouteInformation;
|
||||
import de.avatic.lcc.model.premises.route.RouteNode;
|
||||
import de.avatic.lcc.model.premises.route.RouteSection;
|
||||
import de.avatic.lcc.model.properties.SystemPropertyMappingId;
|
||||
import de.avatic.lcc.model.rates.ContainerRate;
|
||||
import de.avatic.lcc.model.rates.ContainerRateType;
|
||||
import de.avatic.lcc.model.rates.MatrixRate;
|
||||
import de.avatic.lcc.repositories.NodeRepository;
|
||||
import de.avatic.lcc.repositories.properties.PropertyRepository;
|
||||
import de.avatic.lcc.repositories.rates.ContainerRateRepository;
|
||||
import de.avatic.lcc.repositories.rates.MatrixRateRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class RoutingService2 {
|
||||
|
||||
|
||||
private final MatrixRateRepository matrixRateRepository;
|
||||
private final ChainResolver chainResolver;
|
||||
private final NodeRepository nodeRepository;
|
||||
private final ContainerRateRepository containerRateRepository;
|
||||
private final DistanceService distanceService;
|
||||
private final PropertyRepository propertyRepository;
|
||||
|
||||
public RoutingService2(MatrixRateRepository matrixRateRepository, ChainResolver chainResolver, NodeRepository nodeRepository, ContainerRateRepository containerRateRepository, DistanceService distanceService, PropertyRepository propertyRepository) {
|
||||
this.matrixRateRepository = matrixRateRepository;
|
||||
this.chainResolver = chainResolver;
|
||||
this.nodeRepository = nodeRepository;
|
||||
this.containerRateRepository = containerRateRepository;
|
||||
this.distanceService = distanceService;
|
||||
this.propertyRepository = propertyRepository;
|
||||
}
|
||||
|
||||
public List<RouteInformation> findRoute(Node destination, Node source, boolean sourceIsUserNode) {
|
||||
List<RouteInformation> routeInformation = new ArrayList<>();
|
||||
TemporaryContainer container = new TemporaryContainer(source, destination);
|
||||
|
||||
/*
|
||||
* Get the source and destination node from database.
|
||||
* Check if there is a matrix rate for the source country.
|
||||
*/
|
||||
matrixRateRepository.getByCountryIds(source.getCountryId(), source.getCountryId()).ifPresent(container::setSourceMatrixRate);
|
||||
|
||||
|
||||
/*
|
||||
* Generate recursive all chains starting with the destination node.
|
||||
* This means all chains within the last node of a chain are connected to the
|
||||
* existing chain.
|
||||
*
|
||||
* Furthermore, it is evaluated that all nodes within the chain do not have chains
|
||||
* themselves that are in conflict with the chain.
|
||||
*
|
||||
* Then get all countries from the end of the destination chains.
|
||||
*/
|
||||
|
||||
List<List<Node>> destinationChains = chainResolver.buildChains(destination.getId());
|
||||
List<Integer> inboundCountries = destinationChains.stream().filter(chain -> !chain.isEmpty()).map(chain -> chain.getLast().getCountryId()).distinct().toList();
|
||||
|
||||
/*
|
||||
* Get all outbound nodes for the country of the source node. In this first step this includes:
|
||||
* - all intermediate nodes that have the same country id.
|
||||
* - all nodes that are explicitly mapped as outbound node for this country id.
|
||||
*/
|
||||
List<Node> outboundNodes = nodeRepository.getAllOutboundFor(source.getCountryId());
|
||||
|
||||
/*
|
||||
* Find main runs based on the outbound nodes and the inbound countries found before.
|
||||
* Find post-runs for the main runs.
|
||||
*
|
||||
* Store all information in the TemporaryContainer object.
|
||||
*/
|
||||
container.setMainRuns(outboundNodes.stream().collect(Collectors.toMap(Node::getId, n -> containerRateRepository.findRoutesByStartNodeIdAndDestinationCountryId(n.getId(), inboundCountries))));
|
||||
container.setPostRuns(container.getMainRuns().stream().collect(Collectors.toMap(ContainerRate::getId, containerRateRepository::getPostRunsFor)));
|
||||
|
||||
|
||||
connectDestinationChainAndMainRun(container, outboundNodes, destinationChains);
|
||||
connectSourceChainAndSource(container);
|
||||
|
||||
/*
|
||||
* At this point all routes with a main run are created.
|
||||
* We find now the best route per main run and throw away the rest.
|
||||
*/
|
||||
findCheapestPerMainRun(container);
|
||||
|
||||
/*
|
||||
* Now we also create routes without main run (matrix rate)
|
||||
*/
|
||||
connectDestinationChainDirectly(container, destinationChains);
|
||||
|
||||
/*
|
||||
* finally find and mark the fastest and the cheapest route.
|
||||
*/
|
||||
findAndMarkCheapestAndFastest(container);
|
||||
|
||||
/*
|
||||
* Convert to Database model
|
||||
*/
|
||||
for (var route : container.getRoutes()) {
|
||||
RouteInformation routeInformationObj = new RouteInformation();
|
||||
|
||||
routeInformationObj.setRoute(mapRoute(route));
|
||||
routeInformationObj.setRouteSections(mapSections(route.getSections()));
|
||||
routeInformationObj.setRouteNodes(route.getNodes().stream().map(n -> mapNode(n, n.getId().equals(source.getId()) && sourceIsUserNode)).toList());
|
||||
routeInformation.add(routeInformationObj);
|
||||
}
|
||||
|
||||
return routeInformation;
|
||||
}
|
||||
|
||||
|
||||
private RouteNode mapNode(Node node, boolean isUserNode) {
|
||||
RouteNode routeNode = new RouteNode();
|
||||
|
||||
routeNode.setName(node.getName());
|
||||
routeNode.setCountryId(node.getCountryId());
|
||||
routeNode.setAddress(node.getAddress());
|
||||
routeNode.setGeoLng(node.getGeoLng());
|
||||
routeNode.setGeoLat(node.getGeoLat());
|
||||
routeNode.setUserNodeId(isUserNode? node.getId() : null);
|
||||
routeNode.setNodeId(isUserNode ? null : node.getId());
|
||||
routeNode.setIntermediate(node.getIntermediate() != null ? node.getIntermediate() : false);
|
||||
routeNode.setDestination(node.getDestination() != null ? node.getIntermediate() : false);
|
||||
routeNode.setSource(node.getSource() != null ? node.getIntermediate() : false);
|
||||
routeNode.setOutdated(node.getDeprecated());
|
||||
|
||||
return routeNode;
|
||||
}
|
||||
|
||||
private Route mapRoute(TemporaryRouteObject route) {
|
||||
Route routeObj = new Route();
|
||||
|
||||
routeObj.setCheapest(route.isCheapest());
|
||||
routeObj.setFastest(route.isFastest());
|
||||
|
||||
return routeObj;
|
||||
}
|
||||
|
||||
private List<RouteSection> mapSections(List<TemporaryRateObject> sections) {
|
||||
int index = 1;
|
||||
|
||||
List<RouteSection> routeSections = new ArrayList<>();
|
||||
boolean passedMainRun = false;
|
||||
|
||||
for(var section : sections) {
|
||||
var routeSection = mapSection(section);
|
||||
|
||||
if (!section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.MAIN_RUN) && !section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.POST_RUN)) {
|
||||
if (!passedMainRun)
|
||||
routeSection.setPreRun(true);
|
||||
|
||||
if (passedMainRun)
|
||||
routeSection.setPostRun(true);
|
||||
|
||||
}
|
||||
|
||||
if(section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.MAIN_RUN) || section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.POST_RUN))
|
||||
passedMainRun = true;
|
||||
|
||||
routeSection.setListPosition(index++);
|
||||
routeSections.add(routeSection);
|
||||
}
|
||||
|
||||
return routeSections;
|
||||
}
|
||||
|
||||
private RouteSection mapSection(TemporaryRateObject section) {
|
||||
RouteSection routeSection = new RouteSection();
|
||||
|
||||
routeSection.setTransportType(mapRouteType(section));
|
||||
routeSection.setFromRouteNodeId(section.getFromNode().getId());
|
||||
routeSection.setToRouteNodeId(section.getToNode().getId());
|
||||
routeSection.setMainRun(section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.MAIN_RUN));
|
||||
routeSection.setOutdated(false);
|
||||
|
||||
routeSection.setPostRun(false);
|
||||
routeSection.setPreRun(false);
|
||||
|
||||
return routeSection;
|
||||
|
||||
}
|
||||
|
||||
private RouteType mapRouteType(TemporaryRateObject rate) {
|
||||
|
||||
switch(rate.getType()) {
|
||||
case MATRIX, CONTAINER -> {
|
||||
return RouteType.ROAD;
|
||||
}
|
||||
case POST_RUN -> {
|
||||
return RouteType.POST_RUN;
|
||||
}
|
||||
case MAIN_RUN -> {
|
||||
return RouteType.valueOf(rate.getContainerRateTye().name());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void findAndMarkCheapestAndFastest(TemporaryContainer container) {
|
||||
var routes = container.getRoutes();
|
||||
|
||||
TemporaryRouteObject cheapest = null;
|
||||
double cheapestCost = Double.MAX_VALUE;
|
||||
|
||||
TemporaryRouteObject fastest = null;
|
||||
double fastestRoute = Double.MAX_VALUE;
|
||||
|
||||
for(var route : routes) {
|
||||
double routeCost = route.getCost();
|
||||
int leadTime = route.getLeadTime();
|
||||
|
||||
if(routeCost < cheapestCost) {
|
||||
cheapestCost = routeCost;
|
||||
cheapest = route;
|
||||
}
|
||||
|
||||
if(leadTime < fastestRoute) {
|
||||
fastestRoute = leadTime;
|
||||
fastest = route;
|
||||
}
|
||||
}
|
||||
|
||||
if(cheapest != null) {
|
||||
cheapest.setCheapest();
|
||||
}
|
||||
|
||||
if(fastest != null) {
|
||||
fastest.setFastest();
|
||||
}
|
||||
}
|
||||
|
||||
private void findCheapestPerMainRun(TemporaryContainer container) {
|
||||
|
||||
var routesByMainRun = container.getRoutes().stream().collect(Collectors.groupingBy(TemporaryRouteObject::getMainRun));
|
||||
Collection<TemporaryRouteObject> cheapestRoutes = new ArrayList<>();
|
||||
|
||||
for(var mainRun : routesByMainRun.keySet()) {
|
||||
List<TemporaryRouteObject> routes = routesByMainRun.get(mainRun);
|
||||
TemporaryRouteObject cheapest = null;
|
||||
double cheapestCost = Double.MAX_VALUE;
|
||||
|
||||
for(var route : routes) {
|
||||
double routeCost = route.getCost();
|
||||
|
||||
if(routeCost < cheapestCost) {
|
||||
cheapestCost = routeCost;
|
||||
cheapest = route;
|
||||
}
|
||||
}
|
||||
|
||||
if(cheapest != null) {
|
||||
cheapestRoutes.add(cheapest);
|
||||
}
|
||||
}
|
||||
|
||||
container.overrideRoutes(cheapestRoutes);
|
||||
}
|
||||
|
||||
private void connectDestinationChainDirectly(TemporaryContainer container, List<List<Node>> chains) {
|
||||
Collection<TemporaryRateObject> rates = container.getRates();
|
||||
|
||||
for (var chain : chains) {
|
||||
var toNode = chain.isEmpty() ? container.getDestinationNode() : chain.getLast();
|
||||
|
||||
TemporaryRateObject finalSection = new TemporaryRateObject(container.getSourceNode(), toNode, TemporaryRateObject.TemporaryRateObjectType.MATRIX);
|
||||
|
||||
if (rates.contains(finalSection)) {
|
||||
TemporaryRateObject finalMatrixRateObj = finalSection;
|
||||
finalSection = container.getRates().stream().filter(r -> r.equals(finalMatrixRateObj)).findFirst().orElse(null);
|
||||
} else {
|
||||
var matrixRate = matrixRateRepository.getByCountryIds(container.getSourceNode().getCountryId(), toNode.getCountryId()).orElse(null);
|
||||
|
||||
// no matrix rate in database. skip this chain
|
||||
if (matrixRate == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
finalSection.setRate(matrixRate);
|
||||
finalSection.setApproxDistance(distanceService.getDistance(container.getSourceNode(), toNode, true));
|
||||
rates.add(finalSection);
|
||||
}
|
||||
|
||||
// could not create an temporary rate object -> so just skip here ... (should not happen)
|
||||
if (finalSection == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// create a route.
|
||||
boolean routable = true;
|
||||
TemporaryRouteObject routeObj = new TemporaryRouteObject();
|
||||
|
||||
for (int idx = 1; idx < chain.size(); idx++) {
|
||||
Node startNode = chain.get(idx);
|
||||
Node endNode = chain.get(idx - 1);
|
||||
|
||||
var rate = connectNodes(startNode, endNode, container);
|
||||
|
||||
if (rate != null) {
|
||||
routeObj.addSection(rate);
|
||||
} else {
|
||||
// chain is not routable -> discard
|
||||
routable = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if the chain is routable -> add the final rate and save the route.
|
||||
if (routable) {
|
||||
routeObj.addSection(finalSection);
|
||||
container.addRoute(routeObj);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private TemporaryRateObject connectNearByNodes(Node chainEnd, List<Node> nearByNodes, TemporaryContainer container) {
|
||||
|
||||
|
||||
for (var nearbyNode : nearByNodes) {
|
||||
TemporaryRateObject nearByRate = connectNodes(nearbyNode, chainEnd, container);
|
||||
if (null != nearByRate) {
|
||||
return nearByRate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void connectSourceChainAndSource(TemporaryContainer container) {
|
||||
|
||||
/* get the near-by nodes if no country matrix rate present */
|
||||
List<Node> nearByNodes = (container.hasSourceMatrixRate()) ? null : nodeRepository.getByDistance(container.getSourceNode(), getRegionRadius());
|
||||
|
||||
Collection<TemporaryRouteObject> routes = new ArrayList<>();
|
||||
|
||||
for (var route : container.getRoutes()) {
|
||||
var mainRun = route.getMainRun();
|
||||
var sourceChains = chainResolver.buildChains(mainRun.getFromNode().getId());
|
||||
|
||||
|
||||
for (var chain : sourceChains) {
|
||||
Node source = container.getSourceNode();
|
||||
boolean chainEndIsSource = source.getId().equals(chain.getLast().getId());
|
||||
|
||||
// find final section: check if chain end and source node are identical, then check if chain end can be connected to
|
||||
// source node, if this is not possible use a near-by node
|
||||
TemporaryRateObject finalSection = (chainEndIsSource) ? null : connectNodes(source, chain.getLast(), container);
|
||||
finalSection = ((finalSection == null && !chainEndIsSource && nearByNodes != null) ? connectNearByNodes(chain.getLast(), nearByNodes, container) : finalSection);
|
||||
|
||||
if (finalSection != null || chainEndIsSource) {
|
||||
boolean routable = true;
|
||||
TemporaryRouteObject duplicate = route.clone();
|
||||
|
||||
for (int idx = 1; idx < chain.size() - 1; idx++) {
|
||||
Node startNode = chain.get(idx);
|
||||
Node endNode = chain.get(idx - 1);
|
||||
|
||||
TemporaryRateObject rate = connectNodes(startNode, endNode, container);
|
||||
|
||||
if (rate != null) duplicate.addSection(rate);
|
||||
else {
|
||||
routable = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (routable) {
|
||||
if (finalSection != null) {
|
||||
// add final section if necessary,
|
||||
// if last chain node == source node this can be skipped.
|
||||
duplicate.addSection(finalSection);
|
||||
if (!finalSection.getFromNode().getId().equals(source.getId())) duplicate.routeOverNearBy();
|
||||
}
|
||||
routes.add(duplicate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
container.overrideRoutes(routes);
|
||||
}
|
||||
|
||||
private Integer getRegionRadius() {
|
||||
var property = propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.RADIUS_REGION);
|
||||
return property.map(propertyDTO -> Integer.valueOf(propertyDTO.getCurrentValue())).orElseGet(SystemPropertyMappingId.RADIUS_REGION::getDefaultAsInteger);
|
||||
}
|
||||
|
||||
private void connectDestinationChainAndMainRun(TemporaryContainer container, List<Node> outboundNodes, List<List<Node>> destinationChains) {
|
||||
/*
|
||||
* Try to connect everything together:
|
||||
* - go trough all main runs and adjacent post-runs
|
||||
* - find any compatible chain:
|
||||
* - check if chain is routable
|
||||
* - add post run and main run
|
||||
*/
|
||||
for (var mainRun : container.getMainRuns()) {
|
||||
|
||||
Node mainRunEndNode = nodeRepository.getById(mainRun.getToNodeId()).orElseThrow();
|
||||
Node mainRunStartNode = outboundNodes.stream().filter(n -> n.getId().equals(mainRun.getFromNodeId())).findFirst().orElseThrow();
|
||||
|
||||
TemporaryRateObject mainRunObj = new TemporaryRateObject(mainRunStartNode, mainRunEndNode, TemporaryRateObject.TemporaryRateObjectType.MAIN_RUN, mainRun);
|
||||
|
||||
for (var postRun : container.getPostRuns().get(mainRun.getId())) {
|
||||
|
||||
Node postRunEndNode = nodeRepository.getById(postRun.getToNodeId()).orElseThrow();
|
||||
TemporaryRateObject postRunObj = new TemporaryRateObject(postRunEndNode, mainRunEndNode, TemporaryRateObject.TemporaryRateObjectType.POST_RUN, postRun);
|
||||
|
||||
for (var chain : destinationChains) {
|
||||
ChainQuality quality = getChainQuality(chain, postRun, mainRun);
|
||||
|
||||
/* if connection quality is bad, do not try to route this and continue. */
|
||||
if (quality == ChainQuality.FALLBACK) continue;
|
||||
|
||||
boolean routable = true;
|
||||
TemporaryRouteObject routeObj = new TemporaryRouteObject();
|
||||
|
||||
for (int idx = 1; idx < chain.size() - quality.getSizeOffset(); idx++) {
|
||||
Node startNode = chain.get(idx);
|
||||
Node endNode = chain.get(idx - 1);
|
||||
|
||||
var rate = connectNodes(startNode, endNode, container);
|
||||
|
||||
if (rate != null) {
|
||||
routeObj.addSection(rate);
|
||||
} else {
|
||||
// chain is not routable -> discard
|
||||
routable = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (routable) {
|
||||
routeObj.addPostRunSection(postRunObj);
|
||||
routeObj.addMainRunSection(mainRunObj);
|
||||
container.addRoute(routeObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private ChainQuality getChainQuality(List<Node> chain, ContainerRate postRun, ContainerRate mainRun) {
|
||||
if (chain.getLast().getId().equals(postRun.getToNodeId())) {
|
||||
return ChainQuality.MEDIUM;
|
||||
} else if (chain.getLast().getId().equals(postRun.getFromNodeId()) && chain.get(chain.size() - 2).getId().equals(postRun.getToNodeId())) {
|
||||
return ChainQuality.HIGH;
|
||||
} else if (chain.getLast().getCountryId().equals(postRun.getToCountryId())) {
|
||||
return ChainQuality.LOW;
|
||||
}
|
||||
return ChainQuality.FALLBACK;
|
||||
}
|
||||
|
||||
private TemporaryRateObject connectNodes(Node startNode, Node endNode, TemporaryContainer container) {
|
||||
var containerRateObj = new TemporaryRateObject(startNode, endNode, TemporaryRateObject.TemporaryRateObjectType.CONTAINER);
|
||||
var matrixRateObj = new TemporaryRateObject(startNode, endNode, TemporaryRateObject.TemporaryRateObjectType.MATRIX);
|
||||
|
||||
if (container.getRates().contains(containerRateObj))
|
||||
return container.getRates().stream().filter(r -> r.equals(containerRateObj)).findFirst().orElseThrow();
|
||||
|
||||
if (container.getRates().contains(matrixRateObj))
|
||||
return container.getRates().stream().filter(r -> r.equals(matrixRateObj)).findFirst().orElseThrow();
|
||||
|
||||
Optional<ContainerRate> containerRate = containerRateRepository.findRoute(startNode.getId(), endNode.getId(), ContainerRateType.ROAD);
|
||||
|
||||
if (containerRate.isPresent()) {
|
||||
containerRateObj.setRate(containerRate.get());
|
||||
container.getRates().add(containerRateObj);
|
||||
return containerRateObj;
|
||||
} else {
|
||||
Optional<MatrixRate> matrixRate = matrixRateRepository.getByCountryIds(startNode.getCountryId(), endNode.getCountryId());
|
||||
|
||||
if (matrixRate.isPresent()) {
|
||||
matrixRateObj.setRate(matrixRate.get());
|
||||
matrixRateObj.setApproxDistance(distanceService.getDistance(startNode, endNode, true));
|
||||
container.getRates().add(matrixRateObj);
|
||||
return matrixRateObj;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum ChainQuality {
|
||||
HIGH(1), MEDIUM(0), LOW(0), FALLBACK(0);
|
||||
|
||||
private final int sizeOffset;
|
||||
|
||||
ChainQuality(int sizeOffset) {
|
||||
this.sizeOffset = sizeOffset;
|
||||
}
|
||||
|
||||
public int getSizeOffset() {
|
||||
return sizeOffset;
|
||||
}
|
||||
}
|
||||
|
||||
private static class TemporaryContainer {
|
||||
|
||||
/*
|
||||
* Set to lookup route sections. Generated from node pairs.
|
||||
*/
|
||||
private final Set<TemporaryRateObject> rates = new HashSet<>();
|
||||
/*
|
||||
* Routes that are build within the routing service.
|
||||
*/
|
||||
private final Collection<TemporaryRouteObject> routes = new ArrayList<>();
|
||||
/*
|
||||
* Source and destination node
|
||||
*/
|
||||
private final Node source;
|
||||
private final Node destination;
|
||||
/*
|
||||
* mainRuns and postRuns retrieved from database.
|
||||
*/
|
||||
private Map<Integer, List<ContainerRate>> mainRuns;
|
||||
private Map<Integer, List<ContainerRate>> postRuns;
|
||||
private MatrixRate sourceMatrixRate;
|
||||
|
||||
public TemporaryContainer(Node source, Node destination) {
|
||||
this.source = source;
|
||||
this.destination = destination;
|
||||
this.mainRuns = null;
|
||||
this.postRuns = null;
|
||||
this.sourceMatrixRate = null;
|
||||
}
|
||||
|
||||
|
||||
public Collection<TemporaryRateObject> getRates() {
|
||||
return rates;
|
||||
}
|
||||
|
||||
public List<ContainerRate> getMainRuns(Integer outboundNodeId) {
|
||||
return mainRuns.get(outboundNodeId);
|
||||
}
|
||||
|
||||
public List<ContainerRate> getMainRuns() {
|
||||
return mainRuns.values().stream().flatMap(Collection::stream).toList();
|
||||
}
|
||||
|
||||
public void setMainRuns(Map<Integer, List<ContainerRate>> mainRuns) {
|
||||
this.mainRuns = mainRuns;
|
||||
}
|
||||
|
||||
public Map<Integer, List<ContainerRate>> getPostRuns() {
|
||||
return postRuns;
|
||||
}
|
||||
|
||||
public void setPostRuns(Map<Integer, List<ContainerRate>> postRuns) {
|
||||
this.postRuns = postRuns;
|
||||
}
|
||||
|
||||
public void addRoute(TemporaryRouteObject route) {
|
||||
this.routes.add(route);
|
||||
}
|
||||
|
||||
public Collection<TemporaryRouteObject> getRoutes() {
|
||||
return routes;
|
||||
}
|
||||
|
||||
public Node getSourceNode() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public Node getDestinationNode() {
|
||||
return destination;
|
||||
}
|
||||
|
||||
public void setSourceMatrixRate(MatrixRate sourceMatrixRate) {
|
||||
this.sourceMatrixRate = sourceMatrixRate;
|
||||
}
|
||||
|
||||
public boolean hasSourceMatrixRate() {
|
||||
return sourceMatrixRate != null;
|
||||
}
|
||||
|
||||
public void overrideRoutes(Collection<TemporaryRouteObject> routes) {
|
||||
this.routes.clear();
|
||||
this.routes.addAll(routes);
|
||||
}
|
||||
}
|
||||
|
||||
private static class TemporaryRouteObject {
|
||||
|
||||
private final List<TemporaryRateObject> sections;
|
||||
|
||||
private TemporaryRateObject mainRun;
|
||||
private TemporaryRateObject postRun;
|
||||
|
||||
private boolean nearBy = false;
|
||||
private boolean isCheapest = false;
|
||||
private boolean isFastest = false;
|
||||
|
||||
public TemporaryRouteObject() {
|
||||
sections = new ArrayList<>();
|
||||
}
|
||||
|
||||
public List<TemporaryRateObject> getSections() {
|
||||
return sections;
|
||||
}
|
||||
|
||||
public void addSection(TemporaryRateObject section) {
|
||||
this.sections.add(section);
|
||||
}
|
||||
|
||||
public void addMainRunSection(TemporaryRateObject mainRun) {
|
||||
addSection(mainRun);
|
||||
this.mainRun = mainRun;
|
||||
}
|
||||
|
||||
public void addPostRunSection(TemporaryRateObject postRun) {
|
||||
addSection(postRun);
|
||||
this.postRun = postRun;
|
||||
}
|
||||
|
||||
public TemporaryRateObject getMainRun() {
|
||||
return mainRun;
|
||||
}
|
||||
|
||||
public void setMainRun(TemporaryRateObject mainRun) {
|
||||
this.mainRun = mainRun;
|
||||
}
|
||||
|
||||
public TemporaryRateObject getPostRun() {
|
||||
return postRun;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TemporaryRouteObject clone() {
|
||||
TemporaryRouteObject clone = new TemporaryRouteObject();
|
||||
clone.sections.addAll(sections);
|
||||
clone.mainRun = mainRun;
|
||||
clone.postRun = postRun;
|
||||
clone.nearBy = nearBy;
|
||||
clone.isCheapest = isCheapest;
|
||||
clone.isFastest = isFastest;
|
||||
return clone;
|
||||
}
|
||||
|
||||
public void routeOverNearBy() {
|
||||
this.nearBy = true;
|
||||
}
|
||||
|
||||
public double getCost() {
|
||||
return sections.stream().mapToDouble(TemporaryRateObject::getCost).sum();
|
||||
}
|
||||
|
||||
public void setCheapest() {
|
||||
this.isCheapest = true;
|
||||
}
|
||||
|
||||
public void setFastest() {
|
||||
this.isFastest = true;
|
||||
}
|
||||
|
||||
public int getLeadTime() {
|
||||
return sections.stream().mapToInt(TemporaryRateObject::getLeadTime).sum();
|
||||
|
||||
}
|
||||
|
||||
public List<Node> getNodes() {
|
||||
List<Node> nodes = new ArrayList<>();
|
||||
|
||||
for(var section : sections.reversed()) {
|
||||
|
||||
if(sections.getFirst().equals(section))
|
||||
nodes.add(section.getFromNode());
|
||||
|
||||
nodes.add(section.getToNode());
|
||||
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
public Boolean isCheapest() {
|
||||
return isCheapest;
|
||||
}
|
||||
|
||||
public Boolean isFastest() {
|
||||
return isFastest;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class TemporaryRateObject {
|
||||
|
||||
private final Node fromNode;
|
||||
private final Node toNode;
|
||||
private MatrixRate matrixRate;
|
||||
private double approxDistance;
|
||||
private ContainerRate containerRate;
|
||||
|
||||
private TemporaryRateObjectType type;
|
||||
|
||||
|
||||
public TemporaryRateObject(Node fromNode, Node toNode, TemporaryRateObjectType type) {
|
||||
|
||||
this.fromNode = fromNode;
|
||||
this.toNode = toNode;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public TemporaryRateObject(Node fromNode, Node toNode, TemporaryRateObjectType type, ContainerRate rate) {
|
||||
this.fromNode = fromNode;
|
||||
this.toNode = toNode;
|
||||
this.type = type;
|
||||
this.containerRate = rate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
TemporaryRateObject that = (TemporaryRateObject) o;
|
||||
|
||||
if (this.type.equals(that.type)) {
|
||||
if (this.type.equals(TemporaryRateObjectType.MATRIX)) {
|
||||
return Objects.equals(this.fromNode.getCountryId(), that.fromNode.getCountryId()) && Objects.equals(this.toNode.getCountryId(), that.toNode.getCountryId());
|
||||
} else if (this.type.equals(TemporaryRateObjectType.CONTAINER) || this.type.equals(TemporaryRateObjectType.MAIN_RUN) || this.type.equals(TemporaryRateObjectType.POST_RUN)) {
|
||||
return Objects.equals(this.fromNode.getId(), that.fromNode.getId()) && Objects.equals(this.toNode.getId(), that.toNode.getId());
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
if (matrixRate != null) return Objects.hash(matrixRate.getFromCountry(), matrixRate.getToCountry());
|
||||
|
||||
if (containerRate != null) return Objects.hash(containerRate.getFromNodeId(), containerRate.getToNodeId());
|
||||
|
||||
return Objects.hash(null, null);
|
||||
}
|
||||
|
||||
public void setRate(ContainerRate containerRate) {
|
||||
this.containerRate = containerRate;
|
||||
this.type = TemporaryRateObjectType.CONTAINER;
|
||||
}
|
||||
|
||||
public void setRate(MatrixRate matrixRate) {
|
||||
this.matrixRate = matrixRate;
|
||||
this.type = TemporaryRateObjectType.MATRIX;
|
||||
}
|
||||
|
||||
public double getApproxDistance() {
|
||||
return approxDistance;
|
||||
}
|
||||
|
||||
public void setApproxDistance(double distance) {
|
||||
this.approxDistance = distance;
|
||||
}
|
||||
|
||||
public Node getFromNode() {
|
||||
return fromNode;
|
||||
}
|
||||
|
||||
public Node getToNode() {
|
||||
return toNode;
|
||||
}
|
||||
|
||||
public double getCost() {
|
||||
if(type.equals(TemporaryRateObjectType.MATRIX)) {
|
||||
return matrixRate.getRate().doubleValue() * approxDistance;
|
||||
} else {
|
||||
return containerRate.getRateFeu().doubleValue();
|
||||
}
|
||||
}
|
||||
|
||||
public int getLeadTime() {
|
||||
if (type.equals(TemporaryRateObjectType.MATRIX)) {
|
||||
return 3;
|
||||
}
|
||||
return containerRate.getLeadTime();
|
||||
}
|
||||
|
||||
public TemporaryRateObjectType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public ContainerRateType getContainerRateTye() {
|
||||
return containerRate.getType();
|
||||
}
|
||||
|
||||
private enum TemporaryRateObjectType {
|
||||
MATRIX, CONTAINER, POST_RUN, MAIN_RUN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue