mirror of
https://github.com/graphhopper/jsprit.git
synced 2020-01-24 07:45:05 +01:00
Merge branch 'master' into multiple-cap-constraints
Conflicts: jsprit-core/src/main/java/jsprit/core/problem/vehicle/VehicleType.java jsprit-core/src/main/java/jsprit/core/problem/vehicle/VehicleTypeImpl.java jsprit-core/src/test/java/jsprit/core/problem/vehicle/VehicleImplTest.java jsprit-core/src/test/java/jsprit/core/problem/vehicle/VehicleTypeImplTest.java
This commit is contained in:
commit
2993202d49
35 changed files with 2623 additions and 64 deletions
|
|
@ -17,6 +17,8 @@
|
|||
package jsprit.core.problem;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
|
@ -36,6 +38,7 @@ import jsprit.core.problem.job.Shipment;
|
|||
import jsprit.core.problem.solution.route.activity.TourActivity;
|
||||
import jsprit.core.problem.vehicle.Vehicle;
|
||||
import jsprit.core.problem.vehicle.VehicleImpl;
|
||||
import jsprit.core.problem.vehicle.VehicleType;
|
||||
import jsprit.core.problem.vehicle.VehicleTypeImpl;
|
||||
|
||||
import org.junit.Test;
|
||||
|
|
@ -278,5 +281,194 @@ public class VehicleRoutingProblemTest {
|
|||
VehicleRoutingProblem problem = builder.build();
|
||||
assertEquals(4.0,problem.getTransportCosts().getTransportCost("", "", 0.0, null, null),0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenAddingAVehicle_getAddedVehicleTypesShouldReturnItsType(){
|
||||
VehicleRoutingProblem.Builder builder = VehicleRoutingProblem.Builder.newInstance();
|
||||
VehicleType type = VehicleTypeImpl.Builder.newInstance("type", 0).build();
|
||||
Vehicle vehicle = VehicleImpl.Builder.newInstance("v").setLocationId("loc").setType(type).build();
|
||||
builder.addVehicle(vehicle);
|
||||
|
||||
assertEquals(1,builder.getAddedVehicleTypes().size());
|
||||
assertEquals(type,builder.getAddedVehicleTypes().iterator().next());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenAddingTwoVehicleWithSameType_getAddedVehicleTypesShouldReturnOnlyOneType(){
|
||||
VehicleRoutingProblem.Builder builder = VehicleRoutingProblem.Builder.newInstance();
|
||||
VehicleType type = VehicleTypeImpl.Builder.newInstance("type", 0).build();
|
||||
Vehicle vehicle = VehicleImpl.Builder.newInstance("v").setLocationId("loc").setType(type).build();
|
||||
Vehicle vehicle2 = VehicleImpl.Builder.newInstance("v").setLocationId("loc").setType(type).build();
|
||||
|
||||
builder.addVehicle(vehicle);
|
||||
builder.addVehicle(vehicle2);
|
||||
|
||||
assertEquals(1,builder.getAddedVehicleTypes().size());
|
||||
assertEquals(type,builder.getAddedVehicleTypes().iterator().next());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenAddingTwoVehicleWithDiffType_getAddedVehicleTypesShouldReturnTheseType(){
|
||||
VehicleRoutingProblem.Builder builder = VehicleRoutingProblem.Builder.newInstance();
|
||||
VehicleType type = VehicleTypeImpl.Builder.newInstance("type", 0).build();
|
||||
VehicleType type2 = VehicleTypeImpl.Builder.newInstance("type2", 0).build();
|
||||
|
||||
Vehicle vehicle = VehicleImpl.Builder.newInstance("v").setLocationId("loc").setType(type).build();
|
||||
Vehicle vehicle2 = VehicleImpl.Builder.newInstance("v").setLocationId("loc").setType(type2).build();
|
||||
|
||||
builder.addVehicle(vehicle);
|
||||
builder.addVehicle(vehicle2);
|
||||
|
||||
assertEquals(2,builder.getAddedVehicleTypes().size());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenSettingAddPenaltyVehicleOptions_itShouldAddPenaltyVehicle(){
|
||||
VehicleRoutingProblem.Builder builder = VehicleRoutingProblem.Builder.newInstance();
|
||||
VehicleType type = VehicleTypeImpl.Builder.newInstance("type", 0).build();
|
||||
Vehicle vehicle = VehicleImpl.Builder.newInstance("v").setLocationId("loc").setType(type).build();
|
||||
|
||||
builder.addVehicle(vehicle);
|
||||
builder.setFleetSize(FleetSize.FINITE);
|
||||
builder.addPenaltyVehicles(3.0);
|
||||
|
||||
VehicleRoutingProblem vrp = builder.build();
|
||||
|
||||
assertEquals(2,vrp.getVehicles().size());
|
||||
|
||||
boolean penaltyVehicleInCollection = false;
|
||||
for(Vehicle v : vrp.getVehicles()){
|
||||
if(v.getId().equals("penaltyVehicle_loc_type")) penaltyVehicleInCollection = true;
|
||||
}
|
||||
assertTrue(penaltyVehicleInCollection);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenSettingAddPenaltyVehicleOptionsAndFleetSizeIsInfinite_noPenaltyVehicleIsAdded(){
|
||||
VehicleRoutingProblem.Builder builder = VehicleRoutingProblem.Builder.newInstance();
|
||||
VehicleType type = VehicleTypeImpl.Builder.newInstance("type", 0).build();
|
||||
Vehicle vehicle = VehicleImpl.Builder.newInstance("v").setLocationId("loc").setType(type).build();
|
||||
|
||||
builder.addVehicle(vehicle);
|
||||
builder.addPenaltyVehicles(3.0);
|
||||
|
||||
VehicleRoutingProblem vrp = builder.build();
|
||||
|
||||
assertEquals(1,vrp.getVehicles().size());
|
||||
|
||||
boolean penaltyVehicleInCollection = false;
|
||||
for(Vehicle v : vrp.getVehicles()){
|
||||
if(v.getId().equals("penaltyVehicle_loc_type")) penaltyVehicleInCollection = true;
|
||||
}
|
||||
assertFalse(penaltyVehicleInCollection);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenSettingAddPenaltyVehicleOptionsAndTwoVehiclesWithSameLocationAndType_onlyOnePenaltyVehicleIsAdded(){
|
||||
VehicleRoutingProblem.Builder builder = VehicleRoutingProblem.Builder.newInstance();
|
||||
VehicleType type = VehicleTypeImpl.Builder.newInstance("type", 0).build();
|
||||
Vehicle vehicle = VehicleImpl.Builder.newInstance("v").setLocationId("loc").setType(type).build();
|
||||
Vehicle vehicle2 = VehicleImpl.Builder.newInstance("v2").setLocationId("loc").setType(type).build();
|
||||
|
||||
builder.addVehicle(vehicle);
|
||||
builder.addVehicle(vehicle2);
|
||||
builder.setFleetSize(FleetSize.FINITE);
|
||||
builder.addPenaltyVehicles(3.0);
|
||||
|
||||
VehicleRoutingProblem vrp = builder.build();
|
||||
|
||||
assertEquals(3,vrp.getVehicles().size());
|
||||
|
||||
boolean penaltyVehicleInCollection = false;
|
||||
for(Vehicle v : vrp.getVehicles()){
|
||||
if(v.getId().equals("penaltyVehicle_loc_type")) penaltyVehicleInCollection = true;
|
||||
}
|
||||
assertTrue(penaltyVehicleInCollection);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenSettingAddPenaltyVehicleOptionsWithAbsoluteFixedCostsAndTwoVehiclesWithSameLocationAndType_onePenaltyVehicleIsAddedWithTheCorrectPenaltyFixedCosts(){
|
||||
VehicleRoutingProblem.Builder builder = VehicleRoutingProblem.Builder.newInstance();
|
||||
VehicleType type = VehicleTypeImpl.Builder.newInstance("type", 0).build();
|
||||
Vehicle vehicle = VehicleImpl.Builder.newInstance("v").setLocationId("loc").setType(type).build();
|
||||
Vehicle vehicle2 = VehicleImpl.Builder.newInstance("v2").setLocationId("loc").setType(type).build();
|
||||
|
||||
builder.addVehicle(vehicle);
|
||||
builder.addVehicle(vehicle2);
|
||||
builder.setFleetSize(FleetSize.FINITE);
|
||||
builder.addPenaltyVehicles(3.0,10000);
|
||||
|
||||
VehicleRoutingProblem vrp = builder.build();
|
||||
|
||||
assertEquals(3,vrp.getVehicles().size());
|
||||
|
||||
double fix = 0.0;
|
||||
for(Vehicle v : vrp.getVehicles()){
|
||||
if(v.getId().equals("penaltyVehicle_loc_type")) {
|
||||
fix = v.getType().getVehicleCostParams().fix;
|
||||
}
|
||||
}
|
||||
assertEquals(10000,fix,0.01);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenSettingAddPenaltyVehicleOptionsAndTwoVehiclesWithDiffLocationAndType_twoPenaltyVehicleIsAdded(){
|
||||
VehicleRoutingProblem.Builder builder = VehicleRoutingProblem.Builder.newInstance();
|
||||
VehicleType type = VehicleTypeImpl.Builder.newInstance("type", 0).build();
|
||||
Vehicle vehicle = VehicleImpl.Builder.newInstance("v").setLocationId("loc").setType(type).build();
|
||||
Vehicle vehicle2 = VehicleImpl.Builder.newInstance("v2").setLocationId("loc2").setType(type).build();
|
||||
|
||||
builder.addVehicle(vehicle);
|
||||
builder.addVehicle(vehicle2);
|
||||
builder.setFleetSize(FleetSize.FINITE);
|
||||
builder.addPenaltyVehicles(3.0);
|
||||
|
||||
VehicleRoutingProblem vrp = builder.build();
|
||||
|
||||
assertEquals(4,vrp.getVehicles().size());
|
||||
|
||||
boolean penaltyVehicleInCollection = false;
|
||||
boolean anotherPenVehInCollection = false;
|
||||
for(Vehicle v : vrp.getVehicles()){
|
||||
if(v.getId().equals("penaltyVehicle_loc_type")) penaltyVehicleInCollection = true;
|
||||
if(v.getId().equals("penaltyVehicle_loc2_type")) anotherPenVehInCollection = true;
|
||||
}
|
||||
assertTrue(penaltyVehicleInCollection);
|
||||
assertTrue(anotherPenVehInCollection);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenSettingAddPenaltyVehicleOptionsAndTwoVehiclesWithSameLocationButDiffType_twoPenaltyVehicleIsAdded(){
|
||||
VehicleRoutingProblem.Builder builder = VehicleRoutingProblem.Builder.newInstance();
|
||||
VehicleType type = VehicleTypeImpl.Builder.newInstance("type", 0).build();
|
||||
VehicleType type2 = VehicleTypeImpl.Builder.newInstance("type2", 0).build();
|
||||
Vehicle vehicle = VehicleImpl.Builder.newInstance("v").setLocationId("loc").setType(type).build();
|
||||
Vehicle vehicle2 = VehicleImpl.Builder.newInstance("v2").setLocationId("loc").setType(type2).build();
|
||||
|
||||
builder.addVehicle(vehicle);
|
||||
builder.addVehicle(vehicle2);
|
||||
builder.setFleetSize(FleetSize.FINITE);
|
||||
builder.addPenaltyVehicles(3.0);
|
||||
|
||||
VehicleRoutingProblem vrp = builder.build();
|
||||
|
||||
assertEquals(4,vrp.getVehicles().size());
|
||||
|
||||
boolean penaltyVehicleInCollection = false;
|
||||
boolean anotherPenVehInCollection = false;
|
||||
for(Vehicle v : vrp.getVehicles()){
|
||||
if(v.getId().equals("penaltyVehicle_loc_type")) penaltyVehicleInCollection = true;
|
||||
if(v.getId().equals("penaltyVehicle_loc_type2")) anotherPenVehInCollection = true;
|
||||
}
|
||||
assertTrue(penaltyVehicleInCollection);
|
||||
assertTrue(anotherPenVehInCollection);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package jsprit.core.problem.vehicle;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class FiniteVehicleFleetManagerFactoryTest {
|
||||
|
||||
@Test
|
||||
public void whenFiniteVehicleManagerIsCreated_itShouldReturnCorrectManager(){
|
||||
// VehicleFleetManager vfm = new FiniteFleetManagerFactory(Arrays.asList(mock(Vehicle.class))).createFleetManager();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ import jsprit.core.problem.vehicle.VehicleImpl;
|
|||
import jsprit.core.problem.vehicle.VehicleTypeImpl;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
public class TestVehicleFleetManager extends TestCase{
|
||||
public class TestVehicleFleetManagerImpl extends TestCase{
|
||||
|
||||
VehicleFleetManager fleetManager;
|
||||
|
||||
|
|
@ -1,7 +1,102 @@
|
|||
package jsprit.core.problem.vehicle;
|
||||
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import jsprit.core.problem.vehicle.VehicleImpl.NoVehicle;
|
||||
import jsprit.core.util.Coordinate;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
|
||||
public class VehicleImplTest {
|
||||
|
||||
@Test
|
||||
public void whenSettingTypeWithBuilder_typeShouldBeSet(){
|
||||
VehicleType type = mock(VehicleType.class);
|
||||
Vehicle v = VehicleImpl.Builder.newInstance("v").setLocationId("loc").setType(type).build();
|
||||
assertEquals(type,v.getType());
|
||||
}
|
||||
|
||||
@Test(expected=IllegalStateException.class)
|
||||
public void whenTypeIsNull_itThrowsIllegalStateException(){
|
||||
@SuppressWarnings("unused")
|
||||
Vehicle v = VehicleImpl.Builder.newInstance("v").setLocationId("loc").setType(null).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenTypeIsNotSet_defaultTypeIsSet(){
|
||||
Vehicle v = VehicleImpl.Builder.newInstance("v").setLocationId("loc").build();
|
||||
assertEquals("default",v.getType().getTypeId());
|
||||
assertEquals(0,v.getType().getCapacity());
|
||||
}
|
||||
|
||||
@Test(expected=IllegalStateException.class)
|
||||
public void whenVehicleIsBuiltWithoutSettingNeitherLocationNorCoord_itThrowsAnIllegalStateException(){
|
||||
@SuppressWarnings("unused")
|
||||
Vehicle v = VehicleImpl.Builder.newInstance("v").build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenVehicleIsBuiltAndReturnToDepotFlagIsNotSet_itShouldReturnToDepot(){
|
||||
Vehicle v = VehicleImpl.Builder.newInstance("v").setLocationId("loc").build();
|
||||
assertTrue(v.isReturnToDepot());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenVehicleIsBuiltToReturnToDepot_itShouldReturnToDepot(){
|
||||
Vehicle v = VehicleImpl.Builder.newInstance("v").setReturnToDepot(true).setLocationId("loc").build();
|
||||
assertTrue(v.isReturnToDepot());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenVehicleIsBuiltToNotReturnToDepot_itShouldNotReturnToDepot(){
|
||||
Vehicle v = VehicleImpl.Builder.newInstance("v").setReturnToDepot(false).setLocationId("loc").build();
|
||||
assertFalse(v.isReturnToDepot());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenVehicleIsBuiltWithLocation_itShouldHvTheCorrectLocation(){
|
||||
Vehicle v = VehicleImpl.Builder.newInstance("v").setLocationId("loc").build();
|
||||
assertEquals("loc",v.getLocationId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenVehicleIsBuiltWithCoord_itShouldHvTheCorrectCoord(){
|
||||
Vehicle v = VehicleImpl.Builder.newInstance("v").setLocationCoord(Coordinate.newInstance(1, 2)).build();
|
||||
assertEquals(1.0,v.getCoord().getX(),0.01);
|
||||
assertEquals(2.0,v.getCoord().getY(),0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenVehicleIsBuiltAndEarliestStartIsNotSet_itShouldSetTheDefaultOfZero(){
|
||||
Vehicle v = VehicleImpl.Builder.newInstance("v").setLocationCoord(Coordinate.newInstance(1, 2)).build();
|
||||
assertEquals(0.0,v.getEarliestDeparture(),0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenVehicleIsBuiltAndEarliestStartSet_itShouldBeSetCorrectly(){
|
||||
Vehicle v = VehicleImpl.Builder.newInstance("v").setEarliestStart(10.0).setLocationCoord(Coordinate.newInstance(1, 2)).build();
|
||||
assertEquals(10.0,v.getEarliestDeparture(),0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenVehicleIsBuiltAndLatestArrivalIsNotSet_itShouldSetDefaultOfDoubleMaxValue(){
|
||||
Vehicle v = VehicleImpl.Builder.newInstance("v").setLocationCoord(Coordinate.newInstance(1, 2)).build();
|
||||
assertEquals(Double.MAX_VALUE,v.getLatestArrival(),0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenVehicleIsBuiltAndLatestArrivalIsSet_itShouldBeSetCorrectly(){
|
||||
Vehicle v = VehicleImpl.Builder.newInstance("v").setLatestArrival(30.0).setLocationCoord(Coordinate.newInstance(1, 2)).build();
|
||||
assertEquals(30.0,v.getLatestArrival(),0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenNoVehicleIsCreate_itShouldHvTheCorrectId(){
|
||||
Vehicle v = VehicleImpl.createNoVehicle();
|
||||
assertTrue(v instanceof NoVehicle);
|
||||
assertEquals("noVehicle",v.getId());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import org.junit.Test;
|
|||
|
||||
public class VehicleTypeImplTest {
|
||||
|
||||
|
||||
@Test(expected=IllegalStateException.class)
|
||||
public void whenTypeHasNegativeCapacityVal_throwIllegalStateExpception(){
|
||||
@SuppressWarnings("unused")
|
||||
|
|
@ -50,4 +51,80 @@ public class VehicleTypeImplTest {
|
|||
assertEquals(20,type.getCapacityDimensions().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenCallingStaticNewBuilderInstance_itShouldReturnNewBuilderInstance(){
|
||||
VehicleTypeImpl.Builder builder = VehicleTypeImpl.Builder.newInstance("foo", 0);
|
||||
assertNotNull(builder);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenBuildingTypeJustByCallingNewInstance_typeIdMustBeCorrect(){
|
||||
VehicleTypeImpl type = VehicleTypeImpl.Builder.newInstance("foo", 0).build();
|
||||
assertEquals("foo",type.getTypeId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenBuildingTypeJustByCallingNewInstance_capMustBeCorrect(){
|
||||
VehicleTypeImpl type = VehicleTypeImpl.Builder.newInstance("foo", 0).build();
|
||||
assertEquals(0,type.getCapacity());
|
||||
}
|
||||
|
||||
@Test(expected=IllegalStateException.class)
|
||||
public void whenBuildingTypeWithCapSmallerThanZero_throwIllegalStateException(){
|
||||
@SuppressWarnings("unused")
|
||||
VehicleTypeImpl type = VehicleTypeImpl.Builder.newInstance("foo", -10).build();
|
||||
}
|
||||
|
||||
@Test(expected=IllegalStateException.class)
|
||||
public void whenBuildingTypeWithNullId_throwIllegalStateException(){
|
||||
@SuppressWarnings("unused")
|
||||
VehicleTypeImpl type = VehicleTypeImpl.Builder.newInstance(null, 10).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenSettingMaxVelocity_itShouldBeSetCorrectly(){
|
||||
VehicleTypeImpl type = VehicleTypeImpl.Builder.newInstance("type", 10).setMaxVelocity(10).build();
|
||||
assertEquals(10,type.getMaxVelocity(),0.0);
|
||||
}
|
||||
|
||||
@Test(expected=IllegalStateException.class)
|
||||
public void whenMaxVelocitySmallerThanZero_itShouldThrowException(){
|
||||
@SuppressWarnings("unused")
|
||||
VehicleTypeImpl type = VehicleTypeImpl.Builder.newInstance("type", 10).setMaxVelocity(-10).build();
|
||||
}
|
||||
|
||||
@Test(expected=IllegalStateException.class)
|
||||
public void whenFixedCostsSmallerThanZero_itShouldThrowException(){
|
||||
@SuppressWarnings("unused")
|
||||
VehicleTypeImpl type = VehicleTypeImpl.Builder.newInstance("type", 10).setFixedCost(-10).build();
|
||||
}
|
||||
|
||||
public void whenSettingFixedCosts_itShouldBeSetCorrectly(){
|
||||
VehicleTypeImpl type = VehicleTypeImpl.Builder.newInstance("type", 10).setFixedCost(10).build();
|
||||
assertEquals(10.0, type.getVehicleCostParams().fix,0.0);
|
||||
}
|
||||
|
||||
@Test(expected=IllegalStateException.class)
|
||||
public void whenPerDistanceCostsSmallerThanZero_itShouldThrowException(){
|
||||
@SuppressWarnings("unused")
|
||||
VehicleTypeImpl type = VehicleTypeImpl.Builder.newInstance("type", 10).setCostPerDistance(-10).build();
|
||||
}
|
||||
|
||||
public void whenSettingPerDistanceCosts_itShouldBeSetCorrectly(){
|
||||
VehicleTypeImpl type = VehicleTypeImpl.Builder.newInstance("type", 10).setCostPerDistance(10).build();
|
||||
assertEquals(10.0, type.getVehicleCostParams().perDistanceUnit,0.0);
|
||||
}
|
||||
|
||||
@Test(expected=IllegalStateException.class)
|
||||
public void whenPerTimeCostsSmallerThanZero_itShouldThrowException(){
|
||||
@SuppressWarnings("unused")
|
||||
VehicleTypeImpl type = VehicleTypeImpl.Builder.newInstance("type", 10).setCostPerTime(-10).build();
|
||||
}
|
||||
|
||||
public void whenSettingPerTimeCosts_itShouldBeSetCorrectly(){
|
||||
VehicleTypeImpl type = VehicleTypeImpl.Builder.newInstance("type", 10).setCostPerTime(10).build();
|
||||
assertEquals(10.0, type.getVehicleCostParams().perDistanceUnit,0.0);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue