/* * Copyright 2010 Red Hat, Inc. and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.optaplanner.examples.nurserostering.swingui; import java.awt.BorderLayout; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.swing.AbstractAction; import javax.swing.BoxLayout; import javax.swing.GroupLayout; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTextField; import org.optaplanner.examples.common.swingui.SolutionPanel; import org.optaplanner.examples.nurserostering.domain.Employee; import org.optaplanner.examples.nurserostering.domain.NurseRoster; import org.optaplanner.examples.nurserostering.domain.NurseRosterParametrization; import org.optaplanner.examples.nurserostering.domain.Shift; import org.optaplanner.examples.nurserostering.domain.ShiftAssignment; import org.optaplanner.examples.nurserostering.domain.ShiftDate; public class NurseRosteringPanel extends SolutionPanel<NurseRoster> { public static final String LOGO_PATH = "/org/optaplanner/examples/nurserostering/swingui/nurseRosteringLogo.png"; private final ImageIcon employeeIcon; private final ImageIcon deleteEmployeeIcon; private JPanel employeeListPanel; private JTextField planningWindowStartField; private AbstractAction advancePlanningWindowStartAction; private EmployeePanel unassignedPanel; private Map<Employee, EmployeePanel> employeeToPanelMap; public NurseRosteringPanel() { employeeIcon = new ImageIcon(getClass().getResource("employee.png")); deleteEmployeeIcon = new ImageIcon(getClass().getResource("deleteEmployee.png")); GroupLayout layout = new GroupLayout(this); setLayout(layout); createEmployeeListPanel(); JPanel headerPanel = createHeaderPanel(); layout.setHorizontalGroup(layout.createParallelGroup() .addComponent(headerPanel).addComponent(employeeListPanel)); layout.setVerticalGroup(layout.createSequentialGroup() .addComponent(headerPanel, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE) .addComponent(employeeListPanel, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)); } public ImageIcon getEmployeeIcon() { return employeeIcon; } public ImageIcon getDeleteEmployeeIcon() { return deleteEmployeeIcon; } private JPanel createHeaderPanel() { JPanel headerPanel = new JPanel(new BorderLayout(20, 0)); JPanel planningWindowPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); planningWindowPanel.add(new JLabel("Planning window start:")); planningWindowStartField = new JTextField(10); planningWindowStartField.setEditable(false); planningWindowPanel.add(planningWindowStartField); advancePlanningWindowStartAction = new AbstractAction("Advance 1 day into the future") { @Override public void actionPerformed(ActionEvent e) { advancePlanningWindowStart(); } }; advancePlanningWindowStartAction.setEnabled(false); planningWindowPanel.add(new JButton(advancePlanningWindowStartAction)); headerPanel.add(planningWindowPanel, BorderLayout.WEST); JLabel shiftTypeExplanation = new JLabel("E = Early shift, L = Late shift, ..."); headerPanel.add(shiftTypeExplanation, BorderLayout.CENTER); return headerPanel; } private void createEmployeeListPanel() { employeeListPanel = new JPanel(); employeeListPanel.setLayout(new BoxLayout(employeeListPanel, BoxLayout.Y_AXIS)); unassignedPanel = new EmployeePanel(this, Collections.<ShiftDate>emptyList(), Collections.<Shift>emptyList(), null); employeeListPanel.add(unassignedPanel); employeeToPanelMap = new LinkedHashMap<>(); employeeToPanelMap.put(null, unassignedPanel); } @Override public void resetPanel(NurseRoster nurseRoster) { for (EmployeePanel employeePanel : employeeToPanelMap.values()) { if (employeePanel.getEmployee() != null) { employeeListPanel.remove(employeePanel); } } employeeToPanelMap.clear(); employeeToPanelMap.put(null, unassignedPanel); unassignedPanel.clearShiftAssignments(); preparePlanningEntityColors(nurseRoster.getShiftAssignmentList()); List<ShiftDate> shiftDateList = nurseRoster.getShiftDateList(); List<Shift> shiftList = nurseRoster.getShiftList(); unassignedPanel.setShiftDateListAndShiftList(shiftDateList, shiftList); updatePanel(nurseRoster); advancePlanningWindowStartAction.setEnabled(true); planningWindowStartField.setText(nurseRoster.getNurseRosterParametrization().getPlanningWindowStart().getLabel()); } @Override public void updatePanel(NurseRoster nurseRoster) { preparePlanningEntityColors(nurseRoster.getShiftAssignmentList()); List<ShiftDate> shiftDateList = nurseRoster.getShiftDateList(); List<Shift> shiftList = nurseRoster.getShiftList(); Set<Employee> deadEmployeeSet = new LinkedHashSet<>(employeeToPanelMap.keySet()); deadEmployeeSet.remove(null); for (Employee employee : nurseRoster.getEmployeeList()) { deadEmployeeSet.remove(employee); EmployeePanel employeePanel = employeeToPanelMap.get(employee); if (employeePanel == null) { employeePanel = new EmployeePanel(this, shiftDateList, shiftList, employee); employeeListPanel.add(employeePanel); employeeToPanelMap.put(employee, employeePanel); } employeePanel.clearShiftAssignments(); } unassignedPanel.clearShiftAssignments(); for (ShiftAssignment shiftAssignment : nurseRoster.getShiftAssignmentList()) { Employee employee = shiftAssignment.getEmployee(); EmployeePanel employeePanel = employeeToPanelMap.get(employee); employeePanel.addShiftAssignment(shiftAssignment); } for (Employee deadEmployee : deadEmployeeSet) { EmployeePanel deadEmployeePanel = employeeToPanelMap.remove(deadEmployee); employeeListPanel.remove(deadEmployeePanel); } for (EmployeePanel employeePanel : employeeToPanelMap.values()) { employeePanel.update(); } } @Override public boolean isIndictmentHeatMapEnabled() { return true; } private void advancePlanningWindowStart() { logger.info("Advancing planningWindowStart."); if (solutionBusiness.isSolving()) { JOptionPane.showMessageDialog(this.getTopLevelAncestor(), "The GUI does not support this action yet during solving.\nOptaPlanner itself does support it.\n" + "\nTerminate solving first and try again.", "Unsupported in GUI", JOptionPane.INFORMATION_MESSAGE); return; } doProblemFactChange(scoreDirector -> { NurseRoster nurseRoster = scoreDirector.getWorkingSolution(); NurseRosterParametrization nurseRosterParametrization = nurseRoster.getNurseRosterParametrization(); List<ShiftDate> shiftDateList = nurseRoster.getShiftDateList(); ShiftDate planningWindowStart = nurseRosterParametrization.getPlanningWindowStart(); int windowStartIndex = shiftDateList.indexOf(planningWindowStart); if (windowStartIndex < 0) { throw new IllegalStateException("The planningWindowStart (" + planningWindowStart + ") must be in the shiftDateList (" + shiftDateList + ")."); } ShiftDate oldLastShiftDate = shiftDateList.get(shiftDateList.size() - 1); ShiftDate newShiftDate = new ShiftDate(); newShiftDate.setId(oldLastShiftDate.getId() + 1L); newShiftDate.setDayIndex(oldLastShiftDate.getDayIndex() + 1); newShiftDate.setDate(oldLastShiftDate.getDate().plusDays(1)); List<Shift> refShiftList = planningWindowStart.getShiftList(); List<Shift> newShiftList = new ArrayList<>(refShiftList.size()); newShiftDate.setShiftList(newShiftList); nurseRoster.getShiftDateList().add(newShiftDate); scoreDirector.afterProblemFactAdded(newShiftDate); Shift oldLastShift = nurseRoster.getShiftList().get(nurseRoster.getShiftList().size() - 1); long shiftId = oldLastShift.getId() + 1L; int shiftIndex = oldLastShift.getIndex() + 1; long shiftAssignmentId = nurseRoster.getShiftAssignmentList().get( nurseRoster.getShiftAssignmentList().size() - 1).getId() + 1L; for (Shift refShift : refShiftList) { Shift newShift = new Shift(); newShift.setId(shiftId); shiftId++; newShift.setShiftDate(newShiftDate); newShift.setShiftType(refShift.getShiftType()); newShift.setIndex(shiftIndex); shiftIndex++; newShift.setRequiredEmployeeSize(refShift.getRequiredEmployeeSize()); newShiftList.add(newShift); nurseRoster.getShiftList().add(newShift); scoreDirector.afterProblemFactAdded(newShift); for (int indexInShift = 0; indexInShift < newShift.getRequiredEmployeeSize(); indexInShift++) { ShiftAssignment newShiftAssignment = new ShiftAssignment(); newShiftAssignment.setId(shiftAssignmentId); shiftAssignmentId++; newShiftAssignment.setShift(newShift); newShiftAssignment.setIndexInShift(indexInShift); nurseRoster.getShiftAssignmentList().add(newShiftAssignment); scoreDirector.afterEntityAdded(newShiftAssignment); } } windowStartIndex++; ShiftDate newPlanningWindowStart = shiftDateList.get(windowStartIndex); scoreDirector.beforeProblemPropertyChanged(nurseRosterParametrization); nurseRosterParametrization.setPlanningWindowStart(newPlanningWindowStart); nurseRosterParametrization.setLastShiftDate(newShiftDate); scoreDirector.afterProblemPropertyChanged(nurseRosterParametrization); }, true); } public void deleteEmployee(final Employee employee) { logger.info("Scheduling delete of employee ({}).", employee); doProblemFactChange(scoreDirector -> { NurseRoster nurseRoster = scoreDirector.getWorkingSolution(); Employee workingEmployee = scoreDirector.lookUpWorkingObject(employee); if (workingEmployee == null) { // The employee has already been deleted (the UI asked to changed the same employee twice), so do nothing return; } // First remove the problem fact from all planning entities that use it for (ShiftAssignment shiftAssignment : nurseRoster.getShiftAssignmentList()) { if (shiftAssignment.getEmployee() == workingEmployee) { scoreDirector.beforeVariableChanged(shiftAssignment, "employee"); shiftAssignment.setEmployee(null); scoreDirector.afterVariableChanged(shiftAssignment, "employee"); } } // A SolutionCloner does not clone problem fact lists (such as employeeList) // Shallow clone the employeeList so only workingSolution is affected, not bestSolution or guiSolution ArrayList<Employee> employeeList = new ArrayList<>(nurseRoster.getEmployeeList()); nurseRoster.setEmployeeList(employeeList); // Remove it the problem fact itself scoreDirector.beforeProblemFactRemoved(workingEmployee); employeeList.remove(workingEmployee); scoreDirector.afterProblemFactRemoved(workingEmployee); scoreDirector.triggerVariableListeners(); }); } public void moveShiftAssignmentToEmployee(ShiftAssignment shiftAssignment, Employee toEmployee) { solutionBusiness.doChangeMove(shiftAssignment, "employee", toEmployee); solverAndPersistenceFrame.resetScreen(); } }