/******************************************************************************* * Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com) * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser Public License v3 * which accompanies this distribution, and is available at http://www.gnu.org/licenses/lgpl.txt ******************************************************************************/ package com.opendoorlogistics.components.jsprit.tabledefinitions; import static com.opendoorlogistics.api.components.PredefinedTags.ADDRESS; import static com.opendoorlogistics.api.components.PredefinedTags.ID; import static com.opendoorlogistics.api.components.PredefinedTags.JOB_ID; import static com.opendoorlogistics.api.components.PredefinedTags.NAME; import static com.opendoorlogistics.api.components.PredefinedTags.QUANTITY; import static com.opendoorlogistics.api.components.PredefinedTags.SERVICE_DURATION; import static com.opendoorlogistics.api.components.PredefinedTags.TYPE; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import com.opendoorlogistics.api.ODLApi; import com.opendoorlogistics.api.tables.ODLDatastoreAlterable; import com.opendoorlogistics.api.tables.ODLTableDefinitionAlterable; import com.opendoorlogistics.api.tables.ODLTableReadOnly; import com.opendoorlogistics.api.tables.ODLTime; import com.opendoorlogistics.api.tables.TableFlags; import com.opendoorlogistics.components.jsprit.VRPConfig; import com.opendoorlogistics.components.jsprit.VRPConstants; import com.opendoorlogistics.components.jsprit.VRPUtils; public class StopsTableDefn extends TableDfn { public static final String STOPS_TABLE_NAME = "Stops"; public final int[] quantityIndices; private final ODLApi api; public final int jobId; public final int type; public final int name; public final int id; public final int address; public final int requiredSkills; public final LatLongDfn latLong; public final int serviceDuration; public final TimeWindowDfn tw; public enum StopType { //NORMAL_STOP(1,"stop","S", null, ""), UNLINKED_DELIVERY(1,"delivery" , "D"), UNLINKED_PICKUP(1,"pickup", "P"), LINKED_PICKUP(2,"pickup (paired)", "LP"), LINKED_DELIVERY(2,"delivery (paired)", "LD"); private final String[] codes; private final int nbStopsInJob; private final String keyword; private StopType(int nbStopsInJob,String keyword,String... codes) { this.nbStopsInJob = nbStopsInJob; this.keyword =keyword; this.codes = codes; } public int getNbStopsInJob(){ return nbStopsInJob; } public String getKeyword(){ return keyword; } public static StopType identify(ODLApi api, String s) { for (StopType type : StopType.values()) { for (String code : type.codes) { if ((code == null && api.stringConventions().isEmptyString(s)) || api.stringConventions().equalStandardised(code, s)) { return type; } } } return null; } public String getPrimaryCode() { return codes[0]; } } public ODLTime[] getTW(ODLTableReadOnly table, int row) { if (tw != null) { return tw.get(table, row); } return null; } public String getId(ODLTableReadOnly table, int row) { String ret = (String) table.getValueAt(row, id); if (api.stringConventions().isEmptyString(ret)) { onRowException("Empty stop-id", row); } return ret; } public String getJobId(ODLTableReadOnly table, int row){ if(jobId==-1){ return null; } String ret = (String) table.getValueAt(row, jobId); StopType type = getStopType(table, row); if(api.stringConventions().isEmptyString(ret) && type.getNbStopsInJob()>1){ onRowException("Found empty " + JOB_ID + " for multi-stop job", row); } return ret; } public ODLTime getDuration(ODLTableReadOnly table, int row) { ODLTime ret = (ODLTime) table.getValueAt(row, serviceDuration); if (ret == null) { ret =new ODLTime(0); } return ret; } public StopsTableDefn(ODLApi api,ODLDatastoreAlterable<? extends ODLTableDefinitionAlterable> ds, VRPConfig config) { super(ds, STOPS_TABLE_NAME); this.api = api; id = addStrColumn(ID); jobId = addStrColumn(JOB_ID); type = addStrColumn(TYPE); table.setColumnDefaultValue(type, StopType.UNLINKED_DELIVERY.getPrimaryCode()); table.setColumnFlags(jobId, table.getColumnFlags(jobId)|TableFlags.FLAG_IS_OPTIONAL); table.setColumnFlags(type, table.getColumnFlags(type)|TableFlags.FLAG_IS_OPTIONAL); name = addStrColumn(NAME); api.tables().setColumnIsOptional(table, name, true); address = addStrColumn(ADDRESS); api.tables().setColumnIsOptional(table, address, true); latLong = new LatLongDfn(api,table, ""); serviceDuration = addTimeColumn(SERVICE_DURATION); tw = new TimeWindowDfn(table, ""); quantityIndices = addQuantities(QUANTITY, config); requiredSkills = addStrColumn("required-skills"); table.setColumnDefaultValue(requiredSkills, ""); table.setColumnFlags(requiredSkills, table.getColumnFlags(requiredSkills)|TableFlags.FLAG_IS_OPTIONAL); } public int getQuantity(ODLTableReadOnly table, int row, int quantityIndex) { Long val = (Long) table.getValueAt(row, quantityIndices[quantityIndex]); if (val == null) { val = 0L; } if (VRPUtils.isOkQuantity(val) == false) { onRowException("Invalid stop quantity", row); } return val.intValue(); } public int [] getQuantities(ODLTableReadOnly table,int row){ int[] ret = new int[quantityIndices.length]; for(int i =0 ; i<ret.length;i++){ ret[i]= getQuantity(table, row, i); } return ret; } public StopType getStopType(ODLTableReadOnly table, int row) { if(type == -1){ return StopType.UNLINKED_DELIVERY; } StopType ret = StopType.identify(api,(String) table.getValueAt(row, type)); if (ret == null) { onRowException("Unidentified stop type", row); } return ret; } public void onRowException(String messagePrefix, int row) { throw new RuntimeException(messagePrefix + " on stops table row " + (row + 1) + "."); } /** * Get a map containing the row and stop index by stop ids. Also check for duplicate stop ids and throw exception if found. * * @param table * @return */ public Map<String,Integer> getStopIdMap(ODLTableReadOnly table) { Map<String,Integer> ret = api.stringConventions().createStandardisedMap(); int n = table.getRowCount(); for (int row = 0; row < n; row++) { String id = getId(table, row); if (ret.get(id) != null) { onRowException("Duplicate stop id", row); } ret.put(id, row); } return ret; } public Map<String,List<Integer>> getGroupedByMultiStopJob(ODLTableReadOnly stops, boolean validate){ // check multi-stop jobs are correct Map<String,List<Integer>> rowsByJobMap = api.stringConventions().createStandardisedMap(); int n = stops.getRowCount(); for(int row =0 ; row<n;row++){ StopType type = getStopType(stops, row); if(type.getNbStopsInJob()>1){ String jobId = getJobId(stops, row); List<Integer> list = rowsByJobMap.get(jobId); if(list==null){ list = new ArrayList<>(); rowsByJobMap.put(jobId, list); } list.add(row); } } // validate pickup-deliveries and ensure pickup is first in the list for(Map.Entry<String,List<Integer>> entry:rowsByJobMap.entrySet()){ List<Integer> jobStops = entry.getValue(); if(validate){ if(jobStops.size()!=2){ throw new RuntimeException("Incorrect number of stops for job " + entry.getKey() + " in stops table."); } } // get pickup first, delivery second if(getStopType(stops, jobStops.get(0))== StopType.LINKED_DELIVERY){ Collections.reverse(jobStops); } if(validate){ if(getStopType(stops, jobStops.get(0))!= StopType.LINKED_PICKUP || getStopType(stops, jobStops.get(1))!=StopType.LINKED_DELIVERY){ throw new RuntimeException("Job " + entry.getKey() + " in stops is a pickup-delivery but does not have one pickup stop and one delivery stop."); } } } return rowsByJobMap; } }