All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jfxtras.internal.scene.control.skin.agenda.base24hour.AppointmentAbstractTrackedPane Maven / Gradle / Ivy

The newest version!
/**
 * AppointmentAbstractTrackedPane.java
 *
 * Copyright (c) 2011-2016, JFXtras
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * 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.
 *     * Neither the name of the organization 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  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 jfxtras.internal.scene.control.skin.agenda.base24hour;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import jfxtras.scene.control.agenda.Agenda;

abstract class AppointmentAbstractTrackedPane extends AppointmentAbstractPane {
	/**
	 * @param calendar
	 * @param appointment
	 */
	AppointmentAbstractTrackedPane(LocalDate localDate, Agenda.Appointment appointment, LayoutHelp layoutHelp)
	{
		super(appointment, layoutHelp);
		
		// we know start and end optionally are set
		startDateTime = appointment.getStartLocalDateTime().toLocalDate().isEqual(localDate) ? appointment.getStartLocalDateTime() : localDate.atStartOfDay();
		if (appointment.getEndLocalDateTime() == null) {
			endDateTime = null; // a task does not have an end time 
			durationInMS = 0;
		}
		else {
			endDateTime = appointment.getEndLocalDateTime().toLocalDate().isEqual(localDate) ? appointment.getEndLocalDateTime() : localDate.plusDays(1).atStartOfDay();
			durationInMS = startDateTime.until(endDateTime, ChronoUnit.MILLIS);
		}
		firstPaneOfAppointment = startDateTime.equals(appointment.getStartLocalDateTime());
		lastPaneOfAppointment = (endDateTime != null && endDateTime.equals(appointment.getEndLocalDateTime()));
		intermediatePaneOfAppointment = !firstPaneOfAppointment && !lastPaneOfAppointment;
	}
	protected final LocalDateTime startDateTime;
	protected final LocalDateTime endDateTime;
	protected final long durationInMS;
	protected final boolean firstPaneOfAppointment;
	protected final boolean intermediatePaneOfAppointment;
	protected final boolean lastPaneOfAppointment;

	@Override
	protected  boolean showStartTimeText() {
		return firstPaneOfAppointment;
	}
	@Override
	protected  boolean showEndTimeText() {
		return lastPaneOfAppointment;
	}

	// for the role of cluster owner
	List clusterMembers = new ArrayList<>(); 
	List> clusterTracks = new ArrayList<>();
	
	// for the role of cluster member
	AppointmentAbstractTrackedPane clusterOwner = this;
	int clusterTrackIdx = -1;

	/**
	 * This method prepares a day for being drawn.
	 * The appointments within one day might overlap, this method will create a data structure so it is clear how these overlapping appointments should be drawn.
	 * All appointments in one day are process based on their start time; earliest first, and if there are more with the same start time, longest duration first.
	 * The appointments are then place onto (parallel) tracks; an appointment initially is placed in track 0. 
	 * But if there is already an (partially overlapping) appointment there, then the appointment is moved to track 1. 
	 * Unless there also is an appointment already in that track 1, then the next track is tried, and so forth, until a free track is found.
	 * For example (the letters are not the sequence in which the appointments are processed, they're just for identifying them):
	 * 
	 *  tracks
	 *  0 1 2 3
	 *  -------
	 *  . . . .
	 *  . . . .
	 *  A . . .
	 *  A B C .
	 *  A B C D
	 *  A B . D
	 *  A . . D
	 *  A E . D
	 *  A . . D
	 *  . . . D
	 *  . . . D
	 *  F . . D
	 *  F H . D 
	 *  . . . .
	 *  G . . . 
	 *  . . . .
	 * 
	 * Appointment A was rendered first and put into track 0 and its start time.
	 * Then appointment B was added, initially it was put in track 0, but appointment A already uses the that slot, so B was moved into track 1.
	 * C moved from track 0, conflicting with A, to track 1, conflicting with B, and ended up in track 2. And so forth.
	 * F and H show that even though D overlaps them, they could perfectly be placed in lower tracks.
	 * 
	 * A cluster of appointments always starts with a free standing appointment in track 0, for example A or G, such appointment is called the cluster owner.
	 * When the next appointment is added to the tracks, and finds that it cannot be put in track 0, it will be added as a member to the cluster represented by the appointment in track 0.
	 * Special attention must be paid to an appointment that is placed in track 0, but is linked to a cluster by a earlier appointment in a higher track; such an appointment is not the cluster owner.
	 * In the example above, F is linked through D to the cluster owned by A. So F is not a cluster owner, but a member of the cluster owned by A.
	 * And appointment H through F is also part of the cluster owned by A.  
	 * G finally starts a new cluster.
	 * The cluster owner knows all members and how many tracks there are, each member knows in what track it is and has a direct link to the cluster owner. 
	 *  
	 * When rendering the appointments above, parallel appointments are rendered narrower & indented, so appointments partially overlap and the left side of an appointment is always visible to the user.
	 * In the example above the single appointment G is rendered full width, while for example A, B, C and D are overlapping.
	 * F and H are drawn in the same dimensions as A and B in order to allow D to overlap then.
	 * The size and amount of indentation depends on the number of appointments that are rendered next to each other.
	 * In order to compute its location and size, each appointment needs to know:
	 * - its start and ending time,
	 * - its track number,
	 * - its total number of tracks,
	 * - and naturally the total width and height available to draw the day.
	 * 
	 */
	static List determineTracks(List appointmentAbstractTrackedPanes) {
		
		// sort on start time and then decreasing duration
		Collections.sort(appointmentAbstractTrackedPanes, new Comparator() {
			@Override
			public int compare(AppointmentAbstractTrackedPane o1, AppointmentAbstractTrackedPane o2) {
				// if not same start, then compare on starttime
				if (!o1.startDateTime.isEqual(o2.startDateTime)) {
					return o1.startDateTime.compareTo(o2.startDateTime);
				}
				
				// task after appointment
				if (o1 instanceof AppointmentRegularBodyPane && o2 instanceof AppointmentTaskBodyPane) {
					return -1;
				}
				if (o1 instanceof AppointmentTaskBodyPane && o2 instanceof AppointmentRegularBodyPane) {
					return 1;
				}
				
				// longest last 
				return o1.durationInMS == o2.durationInMS ? 0 : (o1.durationInMS > o2.durationInMS ? -1 : 1);
			}
		});
		
		// start placing appointments in the tracks
		AppointmentAbstractTrackedPane lClusterOwner = null;
		for (AppointmentAbstractTrackedPane lAppointmentPane : appointmentAbstractTrackedPanes) 
		{
			// if there is no cluster owner
			if (lClusterOwner == null) {
				
				// than the current becomes an owner
				// only create a minimal cluster, because it will be setup fully in the code below
				lClusterOwner = lAppointmentPane;
				lClusterOwner.clusterTracks = new ArrayList>();
			}
			
			// in which track should it be added
			int lTrackNr = determineTrackWhereAppointmentCanBeAdded(lClusterOwner.clusterTracks, lAppointmentPane);
			// if it can be added to track 0, then we have a "situation". Track 0 could mean
			// - we must start a new cluster
			// - the appointment is still linked to the running cluster by means of a linking appointment in the higher tracks
			if (lTrackNr == 0) {
				
				// So let's see if there is a linking appointment higher up
				boolean lOverlaps = false;
				for (int i = 1; i < lClusterOwner.clusterTracks.size() && lOverlaps == false; i++) {
					lOverlaps = checkIfTheAppointmentOverlapsAnAppointmentAlreadyInThisTrack(lClusterOwner.clusterTracks, i, lAppointmentPane);
				}
				
				// if it does not overlap, we start a new cluster
				if (lOverlaps == false) {
					lClusterOwner = lAppointmentPane;
					lClusterOwner.clusterMembers = new ArrayList(); 
					lClusterOwner.clusterTracks = new ArrayList>();
					lClusterOwner.clusterTracks.add(new ArrayList());
				}
			}
			
			// add it to the track (and setup all other cluster data)
			lClusterOwner.clusterMembers.add(lAppointmentPane);
			lClusterOwner.clusterTracks.get(lTrackNr).add(lAppointmentPane);
			lAppointmentPane.clusterOwner = lClusterOwner;
			lAppointmentPane.clusterTrackIdx = lTrackNr;				
			// for debug  System.out.println("----"); for (int i = 0; i < lClusterOwner.clusterTracks.size(); i++) { System.out.println(i + ": " + lClusterOwner.clusterTracks.get(i) ); } System.out.println("----");
		}
		
		// done
		return appointmentAbstractTrackedPanes;
	}
	
	/**
	 * 
	 */
	static private int determineTrackWhereAppointmentCanBeAdded(List> tracks, AppointmentAbstractTrackedPane appointmentPane)
	{
		int lTrackNr = 0;
		while (true)
		{
			// make sure there is a arraylist for this track
			if (lTrackNr == tracks.size()) {
				tracks.add(new ArrayList());
			}
			
			// scan all existing appointments in this track and see if there is an overlap
			if (checkIfTheAppointmentOverlapsAnAppointmentAlreadyInThisTrack(tracks, lTrackNr, appointmentPane) == false)
			{
				// no overlap, it can be added here
				return lTrackNr;
			}

			// overlap, try next track
			lTrackNr++;
		}
	}
	
	/**
	 * 
	 */
	static private boolean checkIfTheAppointmentOverlapsAnAppointmentAlreadyInThisTrack(List> tracks, int tracknr, AppointmentAbstractTrackedPane newAppointmentPane)
	{
		// get the track
		List lTrack = tracks.get(tracknr);
		
		// scan all existing appointments in this track
		for (AppointmentAbstractTrackedPane lAppointmentPane : lTrack)
		{
			// There is an overlap:
			// if the start time of the already placed appointment is before or equals the new appointment's end time 
			// and the end time of the already placed appointment is after the new appointment's start time (equals will put two consequative appointments into separate tracks) 
			// ...PPPPPPPPP...    
			// .NNNN.......... -> Ps <= Ne & Pe >= Ns -> overlap
			// .....NNNNN..... -> Ps <= Ne & Pe >= Ns -> overlap
			// ..........NNN.. -> Ps <= Ne & Pe >= Ns -> overlap
			// .NNNNNNNNNNNNN. -> Ps <= Ne & Pe >= Ns -> overlap
			// .N............. -> false    & Pe >= Ns -> no overlap
			// .............N. -> Ps <= Ne & false	  -> no overlap
			LocalDateTime lPlacedStart = lAppointmentPane.startDateTime;
			LocalDateTime lPlacedEnd = (lAppointmentPane.endDateTime != null ? lAppointmentPane.endDateTime : lAppointmentPane.startDateTime.plusMinutes(10));
			LocalDateTime lNewStart = newAppointmentPane.startDateTime;
			LocalDateTime lNewEnd = (newAppointmentPane.endDateTime != null ? newAppointmentPane.endDateTime : newAppointmentPane.startDateTime.plusMinutes(10));
			if ( (lPlacedStart.isEqual(lNewStart) || lNewEnd == null || lPlacedStart.isBefore(lNewEnd)) 
			  && lPlacedEnd != null && lPlacedEnd.isAfter(lNewStart)
			   )
			{
				// overlap
				return true;
			}
		}
		
		// no overlap
		return false;
	}
	
	/**
	 * 
	 */
//	static private boolean checkIfTheAppointmentOverlapsAnAppointmentAlreadyInThisTrack(List> tracks, int tracknr, AppointmentAbstractTrackedPane newAppointmentPane)
//	{
//		// get the track
//		List lTrack = tracks.get(tracknr);
//		if (lTrack.size() == 0) {
//			return false; // no overlap
//		}
//		
//		// scan all existing appointments in this track
//		for (AppointmentAbstractTrackedPane lPlacedAppointmentPane : lTrack)
//		{
//			// There is an overlap:
//			// if the start time of the already placed appointment is before or equals the new appointment's end time 
//			// and the end time of the already placed appointment is after or equals the new appointment's start time
//			//
//			// ...PPPPPPPPP... already placed appointment with end date
//			//
//			// .NNNN.......... -> overlap
//			// .....NNNNN..... -> overlap
//			// ..........NNN.. -> overlap
//			// .NNNNNNNNNNNNN. -> overlap
//			// .N............. -> no overlap -> N-end <= P-start [1]
//			// .............N. -> no overlap -> N-start >= p-end [2]
//			// .NNNNNNNNNNNNNN -> overlap (new without end date)
//			// ......NNNNNNNNN -> overlap (new without end date)
//			// .............NN -> no overlap (new without end date) -> N-start >= p-end [3]
//			//
//			// ...PPPPPPPPPPPP already placed appointment without end date
//			// .NNNN.......... -> overlap
//			// .....NNNNN..... -> overlap
//			// ..........NNN.. -> overlap
//			// .NNNNNNNNNNNNN. -> overlap
//			// .N............. -> no overlap -> N-end <= P-start [4]
//			// .............N. -> overlap
//			// .NNNNNNNNNNNNNN -> overlap (new without end date)
//			// ......NNNNNNNNN -> overlap (new without end date)
//			// .............NN -> overlap (new without end date)
//			if ( (lPlacedAppointmentPane.endDateTime != null && newAppointmentPane.endDateTime != null && isBeforeOrEqual(newAppointmentPane.endDateTime, lPlacedAppointmentPane.startDateTime)) // [1]
//			  || (lPlacedAppointmentPane.endDateTime != null && newAppointmentPane.endDateTime != null && isAfterOrEqual(newAppointmentPane.startDateTime, lPlacedAppointmentPane.endDateTime)) // [2]
//			  || (lPlacedAppointmentPane.endDateTime != null && newAppointmentPane.endDateTime == null && isAfterOrEqual(newAppointmentPane.startDateTime, lPlacedAppointmentPane.endDateTime)) // [3]
//			  || (lPlacedAppointmentPane.endDateTime == null && newAppointmentPane.endDateTime != null && isBeforeOrEqual(newAppointmentPane.endDateTime, lPlacedAppointmentPane.startDateTime)) // [4]
//			   )
//			{
//				// no overlap
//				return false;
//			}
//		}
//		// overlap
//		return true;
//	}
//
//	static private boolean isBeforeOrEqual(LocalDateTime ldt1, LocalDateTime ldt2) {
//		return ldt1.isBefore(ldt2) || ldt1.isEqual(ldt2);
//	}
//	
//	static private boolean isAfterOrEqual(LocalDateTime ldt1, LocalDateTime ldt2) {
//		return ldt1.isAfter(ldt2) || ldt1.isEqual(ldt2);
//	}

	/**
	 * 
	 */
	public String toString()
	{
		return "pane=" + startDateTime + "-" + endDateTime
		     + ";" 
		     + super.toString()
			 ;
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy