/* * Copyright (c) Henrik Niehaus * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * 3. Neither the name of the project (Lazy Bones) nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package lazybones.conflicts; import java.util.ArrayList; import java.util.Calendar; import java.util.List; import java.util.Set; import lazybones.ChannelManager; import lazybones.LazyBones; import lazybones.LazyBonesTimer; import lazybones.TimerManager; import lazybones.gui.TimelinePanel; import lazybones.utils.Utilities; import org.hampelratte.svdrp.responses.highlevel.Channel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import util.exc.TvBrowserException; import devplugin.Date; import devplugin.PluginManager; import devplugin.Program; import devplugin.ProgramFieldType; import devplugin.ProgramSearcher; /** * The ConflictResolver tries to resolve a conflict by searching for program repetitions. * Found repetitions are combined with a backtracking algorithm to find a combination, where no pair of two programs overlap. */ public class ConflictResolver { private static transient Logger logger = LoggerFactory.getLogger(ConflictResolver.class); private static final int DAYS_TO_LOOK_FOR_REPS = 6; private Conflict conflict; /** * For each program of a conflict set this list contains an array with all the repetitions of the program. */ private List<Program[]> repetitions; private List<LazyBonesTimer> allTimers; public ConflictResolver(Conflict conflict, List<LazyBonesTimer> allTimers) { this.conflict = conflict; this.allTimers = allTimers; } public void handleConflicts() { // show timeline LazyBones.getInstance().getMainDialog().setVisible(true); LazyBones.getInstance().getMainDialog().showTimeline(); // set timeline date to the date of the conflict TimelinePanel tp = LazyBones.getInstance().getMainDialog().getTimelinePanel(); Calendar startTime = (Calendar) conflict.getPeriod().getStartTime().clone(); int timelineStartHour = Integer.parseInt(LazyBones.getProperties().getProperty("timelineStartHour")); if (startTime.get(Calendar.HOUR_OF_DAY) < timelineStartHour) { startTime.add(Calendar.DAY_OF_MONTH, -1); } tp.setCalendar(startTime); LazyBones.getInstance().getMainDialog().getTimelinePanel().repaint(); } public List<Program> solveConflict() { List<Program> solution = new ArrayList<>(); repetitions = findRepetitions(conflict); boolean solutionFound = findConflictlessSolution(solution, 0); if (solutionFound) { // TODO make sure the found solution does not collide with other existing timers StringBuilder sb = new StringBuilder("Found combination without conflicts:"); for (Program program : solution) { sb.append("\n\t").append(program.toString()); } logger.info(sb.toString()); } else { throw new ConflictUnresolvableException("Conflict could not be solved"); } return solution; } private boolean findConflictlessSolution(List<Program> solution, int programToAdd) { logger.trace("Current program {}", programToAdd); Program[] programRepetitions = repetitions.get(programToAdd); for (int i = 0; i < programRepetitions.length; i++) { logger.trace("Current rep {}", i); Program candidate = programRepetitions[i]; if (!conflicts(candidate, solution) && !inThePast(candidate) && !conflictsWithOtherTimers(candidate)) { solution.add(candidate); logger.trace("Adding {} to solution", candidate); programToAdd++; if(programToAdd < repetitions.size()) { // we found a solution for all programs up to the current one, let's try // to add another one boolean solutionFound = findConflictlessSolution(solution, programToAdd); if(solutionFound) { return true; } else { logger.trace("Backtracking"); // backtracking // none of the current repetitions is conflict free with the current solution // now we have to go one step back and try again with another combination // remove the current repetition of this program from the solution and try the next one programToAdd--; solution.remove(solution.size()-1); continue; } } else { // we found a solution for the last program to add, so we can // now stop the recursion and return true return true; } } else { if (inThePast(candidate)) { logger.trace("Program is in the past {}", candidate); } else { logger.trace("Conflict with current solution {}", candidate); } } } return false; } private boolean conflictsWithOtherTimers(Program candidate) { ConflictFinder finder = new ConflictFinder(); List<LazyBonesTimer> timers = new ArrayList<LazyBonesTimer>(); timers.add(createTimerFromProgram(candidate)); List<LazyBonesTimer> otherTimers = new ArrayList<LazyBonesTimer>(allTimers); otherTimers.removeAll(conflict.getInvolvedTimers()); timers.addAll(otherTimers); Set<Conflict> conflicts = finder.findConflictingTimers(timers); return !conflicts.isEmpty(); } private LazyBonesTimer createTimerFromProgram(Program candidate) { Channel channel = ChannelManager.getChannelMapping().get(candidate.getChannel().getId()); LazyBonesTimer timer = new LazyBonesTimer(); timer.setChannelNumber(channel.getChannelNumber()); timer.setTitle(candidate.getTitle()); // start and end time with buffers timer.setStartTime(Utilities.getStartTime(candidate)); timer.setEndTime(Utilities.getEndTime(candidate)); TimerManager.setTimerBuffers(timer); return timer; } private boolean inThePast(Program candidate) { Calendar now = Calendar.getInstance(); Calendar programStart = Utilities.getStartTime(candidate); return programStart.before(now); } private boolean conflicts(Program repetition, List<Program> solution) { int bufferBefore = Integer.parseInt(LazyBones.getProperties().getProperty("timer.before")); int bufferAfter = Integer.parseInt(LazyBones.getProperties().getProperty("timer.after")); for (Program program : solution) { Calendar candidateStart = Utilities.getStartTime(repetition); candidateStart.add(Calendar.MINUTE, -bufferBefore); Calendar candidateEnd = Utilities.getEndTime(repetition); candidateEnd.add(Calendar.MINUTE, bufferAfter); Calendar programStart = Utilities.getStartTime(program); programStart.add(Calendar.MINUTE, -bufferBefore); Calendar programEnd = Utilities.getEndTime(program); programEnd.add(Calendar.MINUTE, bufferAfter); //@formatter:off if ( candidateStart.after(programStart) && candidateStart.before(programEnd) || candidateEnd.after(programStart) && candidateEnd.before(programEnd) || programStart.after(candidateStart) && programStart.before(candidateEnd) || programEnd.after(candidateStart) && programEnd.before(candidateEnd) || candidateStart.equals(programStart) || candidateEnd.equals(programEnd)) { return true; } //@formatter:on } return false; } private List<Program[]> findRepetitions(Conflict conflict) { List<Program[]> repetitions = new ArrayList<>(); try { for (LazyBonesTimer timer : conflict.getInvolvedTimers()) { String title = getProgramTitle(timer); ProgramSearcher searcher = LazyBones.getPluginManager().createProgramSearcher(PluginManager.SEARCHER_TYPE_EXACTLY, title, false); devplugin.Channel[] allChannels = LazyBones.getPluginManager().getSubscribedChannels(); ProgramFieldType[] searchFields = new ProgramFieldType[] { ProgramFieldType.TITLE_TYPE }; Program[] results = searcher.search(searchFields, new Date(), DAYS_TO_LOOK_FOR_REPS, allChannels, true); for (Program program : results) { logger.trace("Found repetition {}", program); } repetitions.add(results); } } catch (TvBrowserException e) { logger.error("Search for repetitions failed. Conflict resolution stopped!", e); } return repetitions; } private String getProgramTitle(LazyBonesTimer timer) { String title = timer.getTitle(); if (!timer.getTvBrowserProgIDs().isEmpty()) { String programId = timer.getTvBrowserProgIDs().get(0); Program program = LazyBones.getPluginManager().getProgram(programId); if (program != null) { title = program.getTitle(); } } return title; } }