/*******************************************************************************
* Copyright 2012 Geoscience Australia
*
* 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 au.gov.ga.earthsci.bookmark.ui;
import static au.gov.ga.earthsci.bookmark.ui.Messages.*;
import gov.nasa.worldwind.View;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.eclipse.e4.core.di.annotations.Creatable;
import org.eclipse.jface.dialogs.InputDialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.dialogs.MessageDialogWithToggle;
import org.eclipse.swt.widgets.Display;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import au.gov.ga.earthsci.bookmark.BookmarkFactory;
import au.gov.ga.earthsci.bookmark.BookmarkPropertyApplicatorRegistry;
import au.gov.ga.earthsci.bookmark.IBookmarkPropertyAnimator;
import au.gov.ga.earthsci.bookmark.IBookmarkPropertyApplicator;
import au.gov.ga.earthsci.bookmark.model.Bookmark;
import au.gov.ga.earthsci.bookmark.model.BookmarkList;
import au.gov.ga.earthsci.bookmark.model.IBookmark;
import au.gov.ga.earthsci.bookmark.model.IBookmarkList;
import au.gov.ga.earthsci.bookmark.model.IBookmarkProperty;
import au.gov.ga.earthsci.bookmark.model.IBookmarks;
import au.gov.ga.earthsci.bookmark.ui.editor.BookmarkEditorDialog;
import au.gov.ga.earthsci.bookmark.ui.preferences.IBookmarksPreferences;
import au.gov.ga.earthsci.common.ui.dialogs.EmptyStringInputValidator;
import au.gov.ga.earthsci.common.util.AbstractPropertyChangeBean;
import au.gov.ga.earthsci.worldwind.common.WorldWindowRegistry;
import au.gov.ga.earthsci.worldwind.common.util.Util;
/**
* The default implementation of the {@link IBookmarksController} interface
*
* @author James Navin (james.navin@ga.gov.au)
*/
@Creatable
@Singleton
public class BookmarksController extends AbstractPropertyChangeBean implements IBookmarksController
{
private static final Logger logger = LoggerFactory.getLogger(BookmarksController.class);
@Inject
private IBookmarksPreferences preferences;
@Inject
private IBookmarks bookmarks;
private IBookmarkList currentList;
private BookmarksPart part;
/**
* A property change listener used to stop the bookmark applicator thread on
* {@link View#VIEW_STOPPED} events.
*/
private final PropertyChangeListener viewStopListener = new PropertyChangeListener()
{
@Override
public void propertyChange(PropertyChangeEvent evt)
{
if (evt.getPropertyName().equals(View.VIEW_STOPPED))
{
stopCurrentTransition();
}
}
};
/**
* A mouse listener used to stop the bookmark applicator thread on mouse
* pressed events
*/
private final MouseListener mouseStopListener = new MouseAdapter()
{
@Override
public void mousePressed(MouseEvent e)
{
stop();
}
};
/**
* An executor service used to execute the application of a single bookmark
* state to the world
*/
private final ExecutorService applicatorService = Executors.newSingleThreadExecutor(new ThreadFactory()
{
@Override
public Thread newThread(final Runnable runnable)
{
return new Thread(runnable, "Bookmark Applicator Thread"); //$NON-NLS-1$
}
});
/**
* The currently executing bookmark applicator. <code>null</code> implies no
* running application.
*/
private transient Future<?> currentApplicatorTask;
/**
* An executor service used to play through a bookmark list
*/
private final ExecutorService playlistService = Executors.newSingleThreadExecutor(new ThreadFactory()
{
@Override
public Thread newThread(Runnable r)
{
return new Thread(r, "Bookmark Playlist Thread"); //$NON-NLS-1$
}
});
/**
* The currently executing playlist applicator. <code>null</code> implies no
* running playlist.
*/
private transient Future<?> currentPlaylistTask;
public BookmarksController()
{
addPropertyChangeListener("currentList", new PropertyChangeListener() //$NON-NLS-1$
{
@Override
public void propertyChange(PropertyChangeEvent evt)
{
part.refreshDropdown();
}
});
}
@Override
public IBookmark createNew()
{
return createNew(getCurrentList());
}
@Override
public IBookmark createNew(IBookmarkList list)
{
stop();
IBookmark b = BookmarkFactory.createBookmark(preferences.getDefaultPropertyTypes());
list.getBookmarks().add(b);
return b;
}
@Override
public void apply(final IBookmark bookmark)
{
stop();
doApply(bookmark);
}
/**
* Perform the application of the bookmark to the world, without stopping
* any running animations
*/
private void doApply(final IBookmark bookmark)
{
View view = WorldWindowRegistry.INSTANCE.getActiveView();
if (view != null)
{
part.highlight(bookmark);
currentApplicatorTask =
applicatorService.submit(new BookmarkApplicatorRunnable(view, bookmark, getDuration(bookmark)));
}
}
@Override
public void edit(final IBookmark bookmark)
{
stop();
Display.getDefault().asyncExec(new Runnable()
{
@Override
public void run()
{
BookmarkEditorDialog dialog = new BookmarkEditorDialog(bookmark, Display.getDefault().getActiveShell());
dialog.open();
}
});
}
@Override
public IBookmarkList createNewBookmarkList()
{
stop();
InputDialog dialog =
new InputDialog(Display.getDefault().getActiveShell(), BookmarksController_NewListDialogTitle,
BookmarksController_NewListDialogMessage, BookmarksController_DefaultNewListName,
new EmptyStringInputValidator(BookmarksController_NewListValidationMessage));
dialog.open();
if (dialog.getReturnCode() != InputDialog.OK)
{
return null;
}
BookmarkList result = new BookmarkList(dialog.getValue());
bookmarks.addList(result);
return result;
}
@Override
public void renameBookmarkList(IBookmarkList list)
{
stop();
InputDialog dialog =
new InputDialog(Display.getDefault().getActiveShell(), BookmarksController_RenameListDialogTitle,
BookmarksController_RenameListDialogMessage, list.getName(), new EmptyStringInputValidator(
BookmarksController_RenameListValidationMessation));
dialog.open();
if (dialog.getReturnCode() != InputDialog.OK)
{
return;
}
list.setName(dialog.getValue());
}
@Override
public boolean deleteBookmarkList(IBookmarkList list)
{
if (list == null || list == bookmarks.getDefaultList())
{
return false;
}
if (preferences.askForListDeleteConfirmation())
{
MessageDialogWithToggle dialog =
MessageDialogWithToggle.openOkCancelConfirm(Display.getDefault().getActiveShell(),
Messages.BookmarksController_BookmarkListDeleteDialogTitle,
Messages.BookmarksController_BookmarkListDeleteDialogMessage,
Messages.BookmarksController_BookmarkListDeleteDialogToggleMessage,
preferences.askForListDeleteConfirmation(), null, null);
if (dialog.getReturnCode() != MessageDialog.OK)
{
return false;
}
preferences.setAskForListDeleteConfirmation(dialog.getToggleState());
}
return bookmarks.removeList(list);
}
@Override
public void delete(IBookmark bookmark)
{
if (bookmark == null)
{
return;
}
stop();
getCurrentList().getBookmarks().remove(bookmark);
}
@Override
public void delete(IBookmark... bookmarks)
{
if (bookmarks == null || bookmarks.length == 0)
{
return;
}
stop();
for (IBookmark b : bookmarks)
{
getCurrentList().getBookmarks().remove(b);
}
}
@Override
public IBookmarkList getCurrentList()
{
if (currentList == null)
{
return bookmarks.getDefaultList();
}
return currentList;
}
@Override
public void setCurrentList(IBookmarkList list)
{
stop();
if (list == currentList)
{
return;
}
firePropertyChange("currentList", currentList, currentList = list); //$NON-NLS-1$
}
@Override
public void play(IBookmark bookmark)
{
play(getCurrentList(), bookmark);
}
@Override
public void play(IBookmarkList list, IBookmark bookmark)
{
stop();
if (list == null)
{
logger.debug("No bookmark list provided. Aborting play."); //$NON-NLS-1$
return;
}
View wwview = WorldWindowRegistry.INSTANCE.getActiveView();
if (wwview == null)
{
logger.debug("No view found. Aborting play."); //$NON-NLS-1$
return;
}
currentPlaylistTask = playlistService.submit(new BookmarkPlaylistRunnable(wwview, list, bookmark));
}
@Override
public boolean isPlaying()
{
return currentPlaylistTask != null;
}
@Override
public void stop()
{
if (currentPlaylistTask != null)
{
currentPlaylistTask.cancel(true);
currentPlaylistTask = null;
}
stopCurrentTransition();
}
@Override
public void moveBookmarks(IBookmark[] bookmarks, int targetIndex)
{
moveBookmarks(getCurrentList(), bookmarks, getCurrentList(), targetIndex);
}
@Override
public void moveBookmarks(IBookmarkList sourceList, IBookmark[] bookmarks, IBookmarkList targetList, int targetIndex)
{
if (bookmarks == null || bookmarks.length == 0)
{
return;
}
stop();
targetIndex = Util.clamp(targetIndex, 0, targetList.getBookmarks().size());
ArrayList<IBookmark> sourceBookmarksList = new ArrayList<IBookmark>(sourceList.getBookmarks());
ArrayList<IBookmark> targetBookmarksList;
if (sourceList == targetList)
{
targetBookmarksList = sourceBookmarksList;
}
else
{
targetBookmarksList = new ArrayList<IBookmark>(targetList.getBookmarks());
}
int[] currentIndices = new int[bookmarks.length];
for (int i = 0; i < bookmarks.length; i++)
{
currentIndices[i] = sourceBookmarksList.indexOf(bookmarks[i]);
}
for (int i = bookmarks.length - 1; i >= 0; i -= 1)
{
sourceBookmarksList.remove(bookmarks[i]);
if (currentIndices[i] < targetIndex && sourceList == targetList)
{
targetIndex--;
}
targetBookmarksList.add(targetIndex, bookmarks[i]);
}
sourceList.setBookmarks(sourceBookmarksList);
targetList.setBookmarks(targetBookmarksList);
}
@Override
public void copyBookmarks(IBookmark[] bookmarks, int targetIndex)
{
copyBookmarks(getCurrentList(), bookmarks, getCurrentList(), targetIndex);
}
@Override
public void copyBookmarks(IBookmarkList sourceList, IBookmark[] bookmarks, IBookmarkList targetList, int targetIndex)
{
if (bookmarks == null || bookmarks.length == 0)
{
return;
}
targetIndex = Util.clamp(targetIndex, 0, targetList.getBookmarks().size());
ArrayList<IBookmark> targetBookmarksList = new ArrayList<IBookmark>(targetList.getBookmarks());
IBookmark[] copies = copy(bookmarks);
for (int i = copies.length - 1; i >= 0; i -= 1)
{
targetBookmarksList.add(targetIndex, copies[i]);
}
targetList.setBookmarks(targetBookmarksList);
}
private IBookmark[] copy(IBookmark... bookmarks)
{
BookmarkTransferData btd = BookmarkTransferData.fromBookmarks(bookmarks);
try
{
ByteArrayOutputStream os = new ByteArrayOutputStream();
BookmarkTransferData.save(btd, os);
btd = BookmarkTransferData.load(new ByteArrayInputStream(os.toByteArray()));
IBookmark[] copies = new IBookmark[bookmarks.length];
for (int i = 0; i < copies.length; i++)
{
copies[i] = new Bookmark(btd.getBookmarks()[i]);
}
return copies;
}
catch (Exception e)
{
logger.error("Exception copying bookmarks", e); //$NON-NLS-1$
}
return btd.getBookmarks();
}
/**
* Stop any current bookmark transitions that are running
*/
public void stopCurrentTransition()
{
if (currentApplicatorTask != null)
{
currentApplicatorTask.cancel(true);
currentApplicatorTask = null;
}
}
/**
* Return the duration (in milliseconds) to be used for transitioning to the
* given bookmark
*/
private long getDuration(final IBookmark bookmark)
{
return bookmark.getTransitionDuration() == null ? preferences.getDefaultTransitionDuration() : bookmark
.getTransitionDuration();
}
/**
* A {@link Runnable} that triggers the animation between the current world
* state and the selected bookmark.
*/
private class BookmarkApplicatorRunnable implements Runnable
{
private final long duration;
private final View view;
private final List<IBookmarkPropertyAnimator> animators;
private long endTime;
BookmarkApplicatorRunnable(final View view, final IBookmark bookmark, final long duration)
{
this.duration = duration;
this.view = view;
this.animators = new ArrayList<IBookmarkPropertyAnimator>();
final IBookmark currentState = BookmarkFactory.createBookmark();
for (IBookmarkProperty property : bookmark.getProperties())
{
final IBookmarkProperty currentProperty = currentState.getProperty(property.getType());
final IBookmarkPropertyApplicator applicator =
BookmarkPropertyApplicatorRegistry.getApplicator(property);
if (applicator != null)
{
IBookmarkPropertyAnimator animator = applicator.createAnimator(currentProperty, property, duration);
if (animator != null)
{
animators.add(animator);
}
}
}
}
@Override
public void run()
{
view.stopMovement();
view.stopAnimations();
view.addPropertyChangeListener(View.VIEW_STOPPED, viewStopListener);
view.getViewInputHandler().getWorldWindow().getInputHandler().addMouseListener(mouseStopListener);
endTime = System.currentTimeMillis() + duration;
while (System.currentTimeMillis() <= endTime)
{
if (Thread.interrupted())
{
break;
}
applyAnimators();
}
applyAnimators();
view.getViewInputHandler().getWorldWindow().getInputHandler().removeMouseListener(mouseStopListener);
view.removePropertyChangeListener(View.VIEW_STOPPED, viewStopListener);
}
private void applyAnimators()
{
for (IBookmarkPropertyAnimator animator : animators)
{
try
{
if (!animator.isInitialised())
{
animator.init();
}
animator.applyFrame();
}
catch (Exception e)
{
logger.error("Error applying animator frame", e); //$NON-NLS-1$
}
}
view.getViewInputHandler().getWorldWindow().redraw();
}
}
/**
* A runnable that loops through the provided bookmark list and applies each
* bookmark in turn
*/
private class BookmarkPlaylistRunnable implements Runnable
{
private final View view;
private final List<IBookmark> list;
private IBookmark currentBookmark;
BookmarkPlaylistRunnable(View view, IBookmarkList list, IBookmark bookmark)
{
this.view = view;
this.list = new ArrayList<IBookmark>(list.getBookmarks());
this.currentBookmark = this.list.contains(bookmark) ? bookmark : this.list.get(0);
}
@Override
public void run()
{
view.stopAnimations();
view.stopMovement();
while (true)
{
try
{
// Apply current bookmark and wait for completion
doApply(currentBookmark);
currentApplicatorTask.get();
// Wait for user specified time
view.getViewInputHandler().getWorldWindow().getInputHandler().addMouseListener(mouseStopListener);
Thread.sleep(preferences.getPlayBookmarksWaitDuration());
view.getViewInputHandler().getWorldWindow().getInputHandler()
.removeMouseListener(mouseStopListener);
// Proceed to next bookmark
int nextIndex = (list.indexOf(currentBookmark) + 1) % list.size();
currentBookmark = list.get(nextIndex);
if (Thread.interrupted())
{
break;
}
}
catch (InterruptedException e)
{
// Expected if user cancels playlist during wait phase
break;
}
catch (Exception e)
{
logger.error("Exception occurred while running playlist.", e); //$NON-NLS-1$
break;
}
}
currentPlaylistTask = null;
}
}
/**
* Set the user preferences on this controller
*/
public void setPreferences(final IBookmarksPreferences preferences)
{
this.preferences = preferences;
}
@Override
public void setView(BookmarksPart part)
{
this.part = part;
}
/**
* Set the current bookmarks model on this controller
*/
public void setBookmarks(IBookmarks bookmarks)
{
this.bookmarks = bookmarks;
}
}